Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions cookiecutter.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
{
"project_name": "my-cortex-plugin",
"license": "MIT",
"version": "0.1.0"
}
"version": "0.1.0",
"_copy_without_render": [
"src/**"
]
}
15 changes: 13 additions & 2 deletions {{cookiecutter.project_name}}/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,17 @@ The following commands come pre-configured in this repository. You can see all a
- `lintfix` - runs eslint in fix mode to fix any linting errors that can be fixed automatically
- `formatfix` - runs Prettier in fix mode to fix any formatting errors that can be fixed automatically

### Available React components
## Styling your plugin

See available UI components via our [Storybook](https://cortexapps.github.io/plugin-core/).
The Cortex UI allows for custom brand colors, and has a light/dark mode switcher. We expose CSS variables to your plugin
so you may match the current theme. These styles are injected into your plugin iframe once we receive an `init` message
via plugin-core, and are re-injected each time the theme is changed (such as dark mode switch).

The CSS variables are documented within the demo app in this project. If your plugin requires additional theme
information, please reach out to Cortex to see about having it added!

### Plugin init

Mentioned above, we inject styles once the `init` message is received. This is necessary due to constraints with
iframes. To reduce a "flash of unstyled content", it is important that your plugin sends the init message as soon as possible. Avoid calling `init` within library code, and instead keep it at the top level of your scripts so it runs once
the plugin loads in the browser.
14 changes: 10 additions & 4 deletions {{cookiecutter.project_name}}/jest.config.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
// jest.config.js
module.exports = {
moduleNameMapper: {
// map static asset imports to a stub file under the assumption they are not important to our tests
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$":
"<rootDir>/__mocks__/fileMock.js",
// map style asset imports to a stub file under the assumption they are not important to our tests
"\\.(css|less)$": "<rootDir>/__mocks__/styleMock.js",
"\\.(css|less|scss|sass)$": "<rootDir>/__mocks__/styleMock.js",
"^style-inject$": "<rootDir>/__mocks__/styleInjectMock.js",
"@cortexapps/plugin-core/components":
"<rootDir>/node_modules/@cortexapps/plugin-core/dist/components.cjs.js",
"@cortexapps/plugin-core":
"<rootDir>/node_modules/@cortexapps/plugin-core/dist/index.cjs.js",
"@cortexapps/react-plugin-ui":
"<rootDir>/node_modules/@cortexapps/react-plugin-ui/dist/index.js",
},
setupFilesAfterEnv: ["<rootDir>/setupTests.ts"],
testEnvironment: "jsdom",
transformIgnorePatterns: [
// transform files from @cortexapps/react-plugin-ui and uuid because they are ESM
"/node_modules/(?!(?:@cortexapps/react-plugin-ui|uuid)/)",
],
transform: {
"^.+\\.tsx?$": "babel-jest",
"^.+\\.[tj]sx?$": "babel-jest",
},
};
22 changes: 20 additions & 2 deletions {{cookiecutter.project_name}}/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,38 @@
"version": "{{cookiecutter.version}}",
"license": "{{cookiecutter.license}}",
"dependencies": {
"@cortexapps/plugin-core": "^2.0.0",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/view": "^6.36.5",
"@iframe-resizer/child": "5.3.1",
"@radix-ui/react-popover": "^1.1.7",
"@tanstack/react-query": "^5.69.0",
"@tanstack/react-query-devtools": "^5.69.0",
"@uiw/codemirror-themes-all": "^4.23.10",
"@uiw/react-codemirror": "^4.23.10",
"react": "^18.2.0",
"react-dom": "^18.2.0"
"react-dom": "^18.2.0",
"react-router-dom": "^7.4.0"
},
"devDependencies": {
"@babel/core": "^7.21.3",
"@babel/plugin-syntax-jsx": "^7.18.6",
"@babel/preset-env": "^7.20.2",
"@babel/preset-react": "^7.18.6",
"@babel/preset-typescript": "^7.21.0",
"@cortexapps/plugin-core": "^3.0.0",
"@cortexapps/react-plugin-ui": "^1.0.0",
"@popperjs/core": "^2.11.8",
"@svgr/webpack": "^8.1.0",
"@tailwindcss/postcss": "^4.1.3",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^14.6.1",
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.55.0",
"babel-jest": "^29.5.0",
"clsx": "^2.1.1",
"css-loader": "^6.7.3",
"eslint": "^8.0.1",
"eslint-config-prettier": "^8.7.0",
Expand All @@ -34,10 +48,14 @@
"jest": "^29.5.0",
"jest-environment-jsdom": "^29.5.0",
"npm-run-all": "^4.1.5",
"postcss": "^8.5.3",
"postcss-loader": "^8.1.1",
"postcss-preset-env": "^10.1.5",
"prettier": "^2.8.4",
"prop-types": "^15.8.1",
"react-dev-utils": "^12.0.1",
"style-loader": "^3.3.1",
"tailwindcss": "^4.1.3",
"terser-webpack-plugin": "^5.3.7",
"ts-loader": "^9.4.2",
"typescript": "^4.9.5",
Expand Down
5 changes: 5 additions & 0 deletions {{cookiecutter.project_name}}/postcss.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export default {
plugins: {
"@tailwindcss/postcss": {},
},
};
9 changes: 8 additions & 1 deletion {{cookiecutter.project_name}}/setupTests.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import "@testing-library/jest-dom/extend-expect";
import { TextEncoder, TextDecoder } from "util";

// @ts-ignore
global.TextEncoder = TextEncoder;
// @ts-ignore
global.TextDecoder = TextDecoder;

const mockContext = {
apiBaseUrl: "https://api.cortex.dev",
Expand Down Expand Up @@ -26,6 +32,7 @@ const mockContext = {
name: "Ganesh Datta",
role: "ADMIN",
},
tag: "example",
};

jest.mock("@cortexapps/plugin-core/components", () => {
Expand All @@ -49,7 +56,7 @@ jest.mock("@cortexapps/plugin-core", () => {
...originalModule,
CortexApi: {
...originalModule.CortexApi,
getContext: () => {
getContext: async () => {
return mockContext;
},
},
Expand Down
129 changes: 129 additions & 0 deletions {{cookiecutter.project_name}}/src/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# Getting Started with the Cortex Plugin Sample

Welcome! This sample plugin demonstrates a full “batteries included” Cortex plugin setup including routing, theming, UI components, context hooks, and request proxy. Use it as a starting point for your own plugin development.

## 🚀 What’s Inside

The plugin ships with five example tabs:

1. **Components**
Demonstrates the ready-to-use UI components included in `@cortexapps/react-plugin-ui`.

2. **Context**
Shows your current plugin context via the `usePluginContextProvider` hook (entity info, user info, theme, and more; available from the `PluginContextProvider` component).

4. **Entity**
Renders details for the current entity:
- Descriptor from the `useEntityDescriptor` hook
- Custom data from the `useEntityCustomData` hook
- Custom events from the `useEntityCustomEvents` hook

4. **Colors**
Generates swatches of the theme variables applied by `<PluginStyler>` (supports light mode and dark mode).

5. **Proxy**
A simple REST fetch tester that makes requests through your plugin proxy, for trying out backend endpoints.

## 📁 Code Layout

- **Build target**
Webpack is preconfigured to bundle everything into `dist/ui.html` — you’ll see module-size warnings during build; you can ignore them, as they don’t affect this environment.

- **Tests**
Jest is already set up for component testing. See `src/App.test.tsx` for a minimal example.

- **Plugin Initialization**
index.tsx calls the init hook as early as possible to avoid a flash:
```ts
// src/index.tsx
import App from "./components/App";
import { CortexApi } from "@cortexapps/plugin-core";

document.addEventListener("DOMContentLoaded", function () {
CortexApi.pluginInit();

const container = document.getElementById("cortex-plugin-root");
const root = createRoot(container!);
root.render(<App />);
});
```

- **Plugin Shell**
The main component, `<AppTabs>`, is wrapped in a `<RoutingPluginProvider>`:
```tsx
// src/components/App.tsx
import type React from "react";

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import PluginProvider from "./RoutingPluginProvider";
import ErrorBoundary from "./ErrorBoundary";
import AppTabs from "./AppTabs";

import "../baseStyles.css";

const App: React.FC = () => {
const queryClient = new QueryClient();

return (
<ErrorBoundary>
<QueryClientProvider client={queryClient}>
<PluginProvider enableRouting initialEntries={["/basic"]}>
<AppTabs />
</PluginProvider>
</QueryClientProvider>
</ErrorBoundary>
);
};

export default App;
```

The routing plugin provider includes:
- `<PluginRouter>`, which wraps React Router and persists URLs in the query string — supports deep links via `useLocation` and `useNavigate`. Routing functionality in the plugin provider is turned on when you provide the `enableRouting` and `initialEntries` props.
- `<PluginStyler>`, which injects theme CSS variables into the iframe’s document element.
- `<PluginContextProvider>`, which exposes `usePluginContext()` for entity, user, theme, and other plugin context.

## 💻 Recommended Development Workflow

1. **Build the sample**
```bash
yarn && yarn build
```
This produces `dist/ui.html`.

2. **Register and upload**
- Create a draft plugin in Cortex.
- Upload `dist/ui.html` as the plugin bundle.

3. **Run the dev server locally**
```bash
yarn dev
```
This starts a live-reload server for rapid iteration.

4. **Enable dev mode**
In the plugin settings, turn **Dev Mode** so Cortex loads from your local server instead of the published bundle. Keep this window open as you do your development so that you can see your changes as you make them.

5. **Add your component**
- Create a new file under `src/components/`.
- Add a tab in `src/components/AppTabs.tsx`:
```tsx
// src/components/AppTabs.tsx
import MyNewComponent from './components/MyNewComponent';

const tabs = [
{ path: '/new', label: 'New', element: <MyNewComponent /> },
...
];
```

6. **Develop and test**
- Build your component using the information in the **Components**, **Context**, **Entity**, and **Proxy** tabs.
- Write Jest tests alongside your components, named as `ComponentName.test.tsx`

7. **Cleanup**
Before release, remove any sample tabs you don’t need from the `AppTabs` component. If your plugin is single-page, you can mount your main component directly under the plugin provider and remove the `enableRouting` and `initialEntries` props.

---

Happy coding! 🎉
72 changes: 70 additions & 2 deletions {{cookiecutter.project_name}}/src/baseStyles.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,71 @@
body {
font: 14px sans-serif;
@import "tailwindcss";

.cm-editor .cm-cursor {
border-left: 2px solid var(--cortex-plugin-primary);
}

.swatch-container {
display: flex;
align-items: end;
gap: 8px;
font-family: monospace;
}

.swatch-container > div {
display: flex;
flex-direction: column;
align-items: center;
margin: 0;
}

.swatch {
border: 1px solid var(--cortex-plugin-border);
height: 25px;
width: 25px;
border-radius: 0.375rem;
}

.swatch-label {
margin: 0 0 4px 0;
font-size: 0.75rem;
font-weight: 300;
font-style: italic;
line-height: 1.25rem;
}

.swatch--cortex-plugin-background {
background-color: var(--cortex-plugin-background);
}
.swatch--cortex-plugin-foreground {
background-color: var(--cortex-plugin-foreground);
}
.swatch--cortex-plugin-primary {
background-color: var(--cortex-plugin-primary);
}
.swatch--cortex-plugin-secondary {
background-color: var(--cortex-plugin-secondary);
}
.swatch--cortex-plugin-muted {
background-color: var(--cortex-plugin-muted);
}
.swatch--cortex-plugin-accent {
background-color: var(--cortex-plugin-accent);
}
.swatch--cortex-plugin-accent {
background-color: var(--cortex-plugin-accent);
}
.swatch--cortex-plugin-destructive {
background-color: var(--cortex-plugin-destructive);
}
.swatch--cortex-plugin-destructive-foreground {
background-color: var(--cortex-plugin-destructive-foreground);
}
.swatch--cortex-plugin-border {
background-color: var(--cortex-plugin-border);
}
.swatch--cortex-plugin-input {
background-color: var(--cortex-plugin-input);
}
.swatch--cortex-plugin-ring {
background-color: var(--cortex-plugin-ring);
}
Loading
Loading