Add Operations - WO Shortages page with MTS/MTO shortage tracking
Automates the production manager's manual workflow of checking xTuple WO Schedule + Kit Material Shortage for FA department work orders. Two tabs: WO Shortages (detail per WO + shortage line with customer name, YYYY-MM-DD dates) and Critical Parts (aggregated parts blocking near-term shipments with QOH from MPE warehouse). Nav button added to all pages. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
12
.cursor/rules/no-docs-directory.mdc
Normal file
12
.cursor/rules/no-docs-directory.mdc
Normal file
@@ -0,0 +1,12 @@
|
||||
---
|
||||
description: Do not create or add to the /docs directory
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# No /docs Directory Changes
|
||||
|
||||
Do **not** create new files under `docs/` or add to or modify any content in the `docs/` directory.
|
||||
|
||||
- Do not create new files in `docs/`.
|
||||
- Do not edit or append to existing files in `docs/`.
|
||||
- When a task would normally involve documentation (e.g. README updates, changelog notes), skip any changes under `docs/` unless the user explicitly asks to change that directory.
|
||||
BIN
.cursor/skills/.DS_Store
vendored
Normal file
BIN
.cursor/skills/.DS_Store
vendored
Normal file
Binary file not shown.
107
.cursor/skills/add-query-to-table/SKILL.md
Normal file
107
.cursor/skills/add-query-to-table/SKILL.md
Normal file
@@ -0,0 +1,107 @@
|
||||
---
|
||||
name: add-query-to-table
|
||||
description: Adds a new DB query to a page and wires it to a Table widget; documents how parameter/search syntax works (widget bindings, optional date range). Use when the user asks to add a query, connect a query to a table, create a new query, or how parameters/filters work in queries.
|
||||
---
|
||||
|
||||
# Add Query to Table & Parameter Search Syntax
|
||||
|
||||
Use this skill when adding a new query to a page, wiring a query to a table, or when you need the correct syntax for parameterized filters (e.g. date range from widgets).
|
||||
|
||||
## 1. Query structure (single source of truth)
|
||||
|
||||
Each query lives under the page in a folder named after the query:
|
||||
|
||||
- **Path**: `pages/<PageName>/queries/<query_name>/`
|
||||
- **Files**:
|
||||
- **`<query_name>.txt`** -- Raw SQL. This is the **source of truth** for the query body. Keep it readable (real newlines, no escaping).
|
||||
- **`metadata.json`** -- Appsmith action config. The `body` inside `unpublishedAction.actionConfiguration` must match the SQL in the `.txt` file, stored as a single-line string with `\n` for newlines and `\"` for double quotes.
|
||||
|
||||
**Naming**: Use lowercase with underscores (e.g. `units_ordered_by_series`, `pending_pos_slx_pending`). The query name is the identifier used in bindings (e.g. `{{query_name.data}}`).
|
||||
|
||||
## 2. Metadata.json required fields
|
||||
|
||||
- **`gitSyncId`** (CRITICAL): Must be the **first field** in the root object. Format: `"<24-char-hex>_<uuid-v4>"`. Without this, Appsmith will auto-remove the query on the next pull/sync. Generate with:
|
||||
```bash
|
||||
python3 -c "import uuid; print(uuid.uuid4().hex[:24] + '_' + str(uuid.uuid4()))"
|
||||
```
|
||||
Use the same 24-char hex prefix as the page's `gitSyncId` but a unique UUID suffix.
|
||||
- **`id`**: `"<PageName>_<query_name>"` (e.g. `"Pending POs - SLx Pending_pending_pos_slx_pending"`).
|
||||
- **`name`**: `"<query_name>"` (must match the folder and the name used in bindings).
|
||||
- **`unpublishedAction.actionConfiguration.body`**: The exact SQL from `<query_name>.txt`, escaped for JSON (newlines -> `\n`, `"` -> `\"`).
|
||||
- **`unpublishedAction.datasource`**: Use the same as other queries on the page (e.g. `xTuple_Sandbox`).
|
||||
- **`unpublishedAction.pageId`**: `"<PageName>"`.
|
||||
- **`unpublishedAction.dynamicBindingPathList`**: `[{"key":"body"}]` when the body contains `{{...}}` widget references.
|
||||
- Keep **`pluginId`**: `"postgres-plugin"`, **`pluginType`**: `"DB"`, and existing flags like **`encodeParamsToggle`**, **`paginationType`**, **`pluginSpecifiedTemplates`**, **`timeoutInMillisecond`** consistent with other DB queries in the app.
|
||||
|
||||
When adding a new query, copy an existing query's `metadata.json` from the same page and change `gitSyncId`, `id`, `name`, and `body` (and ensure `body` stays in sync with the new `.txt` file).
|
||||
|
||||
## 3. Wiring the query to a Table widget
|
||||
|
||||
- In the Table widget JSON (e.g. `widgets/.../Table1.json`), set:
|
||||
- **`tableData`**: `"{{<query_name>.data}}"`
|
||||
- Table columns read from the query result by **key**. The key is the **SELECT alias** from the query (e.g. `"Product"`, `"Qty Ordered"`). In `primaryColumns`, the column **id** can be a safe identifier (e.g. `Qty_Ordered`) while **originalId** / **alias** / **label** match the display name; **computedValue** must use the same key as in the query result, e.g. `currentRow["Qty Ordered"]`.
|
||||
|
||||
So: **query SELECT aliases = keys in the table's row object**. Keep column keys and any `currentRow["..."]` references in the table in sync with those aliases.
|
||||
|
||||
## 4. Parameter search syntax (widgets in SQL)
|
||||
|
||||
### 4.1 Referencing a widget value
|
||||
|
||||
- **Syntax**: `{{WidgetName.property}}`
|
||||
- **Examples**:
|
||||
- DatePicker date: `{{SeriesDateFrom.selectedDate}}`, `{{PowerDateTo.selectedDate}}`
|
||||
- Other widgets: use the widget's value property (e.g. `selectedOptionValue`, `text`) as per Appsmith docs.
|
||||
|
||||
Values are injected as strings. For PostgreSQL dates you typically cast in SQL, e.g. `'{{DatePicker1.selectedDate}}'::date`.
|
||||
|
||||
### 4.2 Optional date range (no filter when either date is empty)
|
||||
|
||||
Use this pattern so that:
|
||||
- If **either** date widget is empty -> the date condition is **not** applied (all dates allowed).
|
||||
- If **both** are set -> filter by `date_column BETWEEN from ::date AND to ::date`.
|
||||
|
||||
**SQL pattern** (replace widget names and column as needed):
|
||||
|
||||
```sql
|
||||
AND (
|
||||
NULLIF('{{DateFromWidget.selectedDate}}','') IS NULL
|
||||
OR NULLIF('{{DateToWidget.selectedDate}}','') IS NULL
|
||||
OR date_column BETWEEN
|
||||
NULLIF('{{DateFromWidget.selectedDate}}','')::date
|
||||
AND NULLIF('{{DateToWidget.selectedDate}}','')::date
|
||||
)
|
||||
```
|
||||
|
||||
- **Logic**: `NULLIF('{{...}}','')` turns an empty string into SQL `NULL`. If either widget is empty, one of the first two conditions is true, so the whole `AND (...)` is true and the BETWEEN is not applied. When both are non-empty, the third branch applies the range.
|
||||
- **Widget names**: Use the actual widget names (e.g. `SeriesDateFrom` / `SeriesDateTo` for one tab, `PowerDateFrom` / `PowerDateTo` for another). Ensure those widgets exist on the same page and (if in a tab) the same tab so the query runs with the right context.
|
||||
|
||||
### 4.3 Required parameters (always filter)
|
||||
|
||||
If the filter must always be applied (no "show all" when empty):
|
||||
|
||||
- Use the binding directly and ensure the widget always has a value, or use a default in the widget.
|
||||
- Example: `AND cohead_orderdate BETWEEN '{{DateFrom.selectedDate}}'::date AND '{{DateTo.selectedDate}}'::date` -- then empty dates may produce invalid SQL or empty results, so prefer the optional pattern above unless the UI guarantees non-empty values.
|
||||
|
||||
## 5. Checklist when adding a new query
|
||||
|
||||
1. Create `pages/<PageName>/queries/<query_name>/`.
|
||||
2. Add `<query_name>.txt` with the full SQL (source of truth).
|
||||
3. Add `metadata.json` with correct `gitSyncId`, `id`, `name`, `body` (body = SQL from .txt, JSON-escaped), and same datasource/pageId as other page queries.
|
||||
4. If the SQL uses `{{...}}`, set `dynamicBindingPathList` to `[{"key":"body"}]`.
|
||||
5. In the Table widget that should show the data, set `tableData` to `{{<query_name>.data}}`.
|
||||
6. Ensure table column keys / `currentRow["..."]` match the query's SELECT aliases.
|
||||
|
||||
## 6. Renaming or replacing a query
|
||||
|
||||
- To **rename** (e.g. `units_shipped_by_series` -> `units_ordered_by_series`):
|
||||
- Create the new folder and files under the new name (with a new `gitSyncId`).
|
||||
- Update every reference to the old query (e.g. `tableData`: `{{old_name.data}}` -> `{{new_name.data}}`).
|
||||
- Remove the old query folder (both `.txt` and `metadata.json`).
|
||||
- When **replacing** the SQL but keeping the same name, update the `.txt` first, then update the `body` in `metadata.json` to match (same content, JSON-escaped).
|
||||
|
||||
## Reference
|
||||
|
||||
- **gitSyncId**: `<24-char-hex>_<uuid-v4>`. Without this, Appsmith auto-removes the query on sync.
|
||||
- **Optional date range**: NULLIF + IS NULL + OR + BETWEEN as above.
|
||||
- **Table data binding**: `{{<query_name>.data}}`.
|
||||
- **Column keys**: Must match query SELECT aliases (e.g. `"Product"`, `"Qty Ordered"`).
|
||||
216
.cursor/skills/add-tabs-to-page/SKILL.md
Normal file
216
.cursor/skills/add-tabs-to-page/SKILL.md
Normal file
@@ -0,0 +1,216 @@
|
||||
---
|
||||
name: add-tabs-to-page
|
||||
description: Adds a TABS_WIDGET to a page so multiple data views live under one page instead of separate pages. Use when the user asks to add tabs, consolidate pages into tabs, create tabbed views, or replace a single table with a tabbed layout.
|
||||
---
|
||||
|
||||
# Add Tabs to a Page
|
||||
|
||||
|
||||
Replaces a page's standalone content (e.g. a single `TABLE_WIDGET_V2`) with a `TABS_WIDGET` containing multiple tabs, each with its own widgets and query binding.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Tab definitions**: User must provide the tab labels and the query each tab should display.
|
||||
- **Page prefix**: Each page uses a short widget-ID prefix (e.g. `pa1`, `cp1`). Reuse the existing prefix from the page's widgets.
|
||||
- Follow the **add-query-to-table** skill for creating any new queries needed by the tabs.
|
||||
|
||||
## File Layout
|
||||
|
||||
The Tabs widget and all widgets **inside** tabs live under a `Tabs1/` directory:
|
||||
|
||||
```
|
||||
pages/<PageName>/widgets/Tabs1/
|
||||
├── Tabs1.json # The TABS_WIDGET itself
|
||||
├── <Table1>.json # Table inside tab 1
|
||||
├── <Table2>.json # Table inside tab 2
|
||||
├── <DatePicker>.json # Optional: date pickers inside a tab
|
||||
└── ...
|
||||
```
|
||||
|
||||
If the page previously had a standalone `Table1.json` at `widgets/Table1.json`, **delete it** after creating the tabbed replacement.
|
||||
|
||||
## Instructions
|
||||
|
||||
### 1. Design the tab structure
|
||||
|
||||
Decide on tab count, labels, and content. Example:
|
||||
|
||||
| Tab ID | Label | Canvas widgetId | Content |
|
||||
|--------|-------|-----------------|---------|
|
||||
| tab1 | All | `{prefix}cnvsall` | TableAll bound to `query_all` |
|
||||
| tab2 | SLx | `{prefix}cnvsslx` | TableSLx bound to `query_slx` |
|
||||
|
||||
### 2. Create `Tabs1.json`
|
||||
|
||||
The TABS_WIDGET has three critical sections:
|
||||
|
||||
**A) `tabsObj`** — declares each tab with id, label, index, and canvas widgetId:
|
||||
|
||||
```json
|
||||
"tabsObj": {
|
||||
"tab1": {
|
||||
"id": "tab1",
|
||||
"index": 0,
|
||||
"isVisible": true,
|
||||
"label": "All",
|
||||
"positioning": "vertical",
|
||||
"widgetId": "<canvas-widgetId-for-tab1>"
|
||||
},
|
||||
"tab2": { ... }
|
||||
}
|
||||
```
|
||||
|
||||
**B) `children`** — array of `CANVAS_WIDGET` entries, one per tab. Each canvas must reference:
|
||||
- `"parentId"`: the Tabs widget's `widgetId`
|
||||
- `"tabId"`: matching key from `tabsObj` (e.g. `"tab1"`)
|
||||
- `"tabName"`: the display label
|
||||
- `"widgetId"`: unique canvas ID (referenced in `tabsObj` and by child widgets)
|
||||
|
||||
**C) Top-level Tabs properties:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "TABS_WIDGET",
|
||||
"version": 3,
|
||||
"isCanvas": true,
|
||||
"shouldShowTabs": true,
|
||||
"defaultTab": "<label of default tab>",
|
||||
"parentId": "0",
|
||||
"leftColumn": 9,
|
||||
"rightColumn": 64,
|
||||
"topRow": 5,
|
||||
"bottomRow": 67,
|
||||
"dynamicHeight": "AUTO_HEIGHT",
|
||||
"dynamicBindingPathList": [
|
||||
{"key": "accentColor"},
|
||||
{"key": "boxShadow"}
|
||||
],
|
||||
"accentColor": "{{appsmith.theme.colors.primaryColor}}",
|
||||
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}",
|
||||
"backgroundColor": "#FFFFFF",
|
||||
"borderColor": "#E0DEDE",
|
||||
"borderRadius": "0.375rem",
|
||||
"borderWidth": 1
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Create widgets inside each tab
|
||||
|
||||
Each widget inside a tab sets `"parentId"` to that tab's **canvas widgetId** (not the Tabs widgetId). Widgets start at `topRow: 0` within each canvas.
|
||||
|
||||
**Table-only tabs** (no date pickers):
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "TABLE_WIDGET_V2",
|
||||
"parentId": "<canvas-widgetId>",
|
||||
"topRow": 0,
|
||||
"bottomRow": 58,
|
||||
"leftColumn": 0,
|
||||
"rightColumn": 62,
|
||||
"tableData": "{{<query_name>.data}}",
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
**Tabs with date pickers** (see Sales - Units Shipped for reference):
|
||||
|
||||
| Widget | topRow | bottomRow | leftColumn | rightColumn |
|
||||
|--------|--------|-----------|------------|-------------|
|
||||
| DateFrom | 0 | 7 | 0 | 5 |
|
||||
| DateTo | 0 | 7 | 5 | 10 |
|
||||
| Table | 7 | 58 | 0 | 62 |
|
||||
|
||||
Date pickers use `"type": "DATE_PICKER_WIDGET2"`, `"dateFormat": "YYYY-MM-DD HH:mm"`, and `"parentId"` set to the tab's canvas widgetId.
|
||||
|
||||
### 4. Create queries for each tab
|
||||
|
||||
Follow the **add-query-to-table** skill. Each query's `metadata.json` must have:
|
||||
- `"pageId"`: the page name (e.g. `"Pending POs"`)
|
||||
- `"gitSyncId"`: use the same 24-char hex prefix as the page, with a unique UUID suffix
|
||||
- `"runBehaviour": "AUTOMATIC"`
|
||||
|
||||
### 5. Delete the old standalone widget
|
||||
|
||||
If the page had a `widgets/Table1.json` (or similar) that the Tabs widget replaces, delete it.
|
||||
|
||||
### 6. Verify
|
||||
|
||||
- Each canvas `widgetId` in `children` matches the corresponding `widgetId` in `tabsObj`.
|
||||
- Each widget inside a tab has `parentId` set to its tab's canvas `widgetId`.
|
||||
- `defaultTab` matches one of the tab labels (not the tab ID).
|
||||
- All queries reference the correct `pageId`.
|
||||
- Run `git status` / `git diff` to confirm only intended changes.
|
||||
|
||||
## Canvas Widget Template
|
||||
|
||||
Each canvas child in the `children` array follows this template:
|
||||
|
||||
```json
|
||||
{
|
||||
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
|
||||
"bottomRow": 620,
|
||||
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}",
|
||||
"canExtend": true,
|
||||
"detachFromLayout": true,
|
||||
"dynamicBindingPathList": [
|
||||
{"key": "borderRadius"},
|
||||
{"key": "boxShadow"}
|
||||
],
|
||||
"dynamicHeight": "AUTO_HEIGHT",
|
||||
"dynamicTriggerPathList": [],
|
||||
"flexLayers": [],
|
||||
"isDisabled": false,
|
||||
"isLoading": false,
|
||||
"isVisible": true,
|
||||
"key": "<unique-key>",
|
||||
"leftColumn": 0,
|
||||
"maxDynamicHeight": 9000,
|
||||
"minDynamicHeight": 4,
|
||||
"minHeight": 150,
|
||||
"minWidth": 450,
|
||||
"mobileBottomRow": 150,
|
||||
"mobileLeftColumn": 0,
|
||||
"mobileRightColumn": 602.625,
|
||||
"mobileTopRow": 0,
|
||||
"needsErrorInfo": false,
|
||||
"parentColumnSpace": 1,
|
||||
"parentId": "<tabs-widgetId>",
|
||||
"parentRowSpace": 1,
|
||||
"renderMode": "CANVAS",
|
||||
"responsiveBehavior": "fill",
|
||||
"rightColumn": 602.625,
|
||||
"shouldScrollContents": false,
|
||||
"tabId": "<tab-id>",
|
||||
"tabName": "<tab-label>",
|
||||
"topRow": 0,
|
||||
"type": "CANVAS_WIDGET",
|
||||
"version": 1,
|
||||
"widgetId": "<unique-canvas-widgetId>",
|
||||
"widgetName": "Canvas<N>"
|
||||
}
|
||||
```
|
||||
|
||||
## Consolidating Multiple Pages into Tabs
|
||||
|
||||
When merging separate pages into one tabbed page:
|
||||
|
||||
1. **Choose the surviving page** (or create a new one via **create-new-page** skill).
|
||||
2. **Move each page's query** into the surviving page's `queries/` directory. Update `pageId` and generate new `gitSyncId` values (same 24-char prefix as the surviving page).
|
||||
3. **Create the Tabs widget** with one tab per former page, plus any additional tabs (e.g. "All").
|
||||
4. **Delete the old page directories** entirely.
|
||||
5. **Remove old navigation buttons** from all remaining pages (delete the button JSON files).
|
||||
6. **Update `application.json`** to remove entries for deleted pages.
|
||||
7. **Keep one nav button** pointing to the surviving page.
|
||||
|
||||
## Reference
|
||||
|
||||
| Item | Value |
|
||||
|------|-------|
|
||||
| Widget type | `TABS_WIDGET` version `3` |
|
||||
| Widget directory | `widgets/Tabs1/` |
|
||||
| Canvas parent | Tabs `widgetId` |
|
||||
| Widget-in-tab parent | Canvas `widgetId` for that tab |
|
||||
| `defaultTab` | Tab **label** (not tab ID) |
|
||||
| `tabsObj` keys | `tab1`, `tab2`, `tab3`, ... |
|
||||
| Existing examples | `Sales - Units Shipped`, `Pending POs` |
|
||||
119
.cursor/skills/create-new-page/SKILL.md
Normal file
119
.cursor/skills/create-new-page/SKILL.md
Normal file
@@ -0,0 +1,119 @@
|
||||
---
|
||||
name: create-new-page
|
||||
description: Creates a new page by cloning Sales - Capacity Planning, adds a nav button for it on every page, and sets the new page name for the button label and the page Heading. Use when the user asks to add a new page, clone a page, create a page from template, or add a new navigation page.
|
||||
---
|
||||
|
||||
# Create New Page (Clone + Navigation)
|
||||
|
||||
Creates a new page from the **Sales - Capacity Planning** template, registers it in the app, adds a navigation button on every page, and uses the **new page name** for both the nav button label and the Heading on the new page.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **New page name**: User must provide the exact display name (e.g. `Sales - Units Planning`, `Pending POs - SLx Pending`). Use it consistently for:
|
||||
- Page folder name: `pages/<New Page Name>/`
|
||||
- Page JSON filename: `<New Page Name>.json`
|
||||
- `unpublishedPage.name` and `unpublishedPage.slug` (slug = lowercase, spaces to hyphens, e.g. `pending-pos-slx-pending`)
|
||||
- Heading widget `text` on the new page
|
||||
- Nav button `text` on all pages
|
||||
- `navigateTo('<New Page Name>', {}, 'SAME_WINDOW')` in the new button's `onClick`
|
||||
- Follow the **navigation-button-add** skill for button styling and placement.
|
||||
- If the page belongs to a **new section** (e.g. "Pending POs" section distinct from "Sales"), follow the **navigation-section-add** skill to add the section label on all pages.
|
||||
|
||||
## Instructions
|
||||
|
||||
### 1. Clone the template page
|
||||
|
||||
- **Template**: `pages/Sales - Capacity Planning/`
|
||||
- **Target**: `pages/<New Page Name>/`
|
||||
- Copy the entire directory tree (widgets, queries, and the page JSON).
|
||||
- In the new folder, rename the page file to `<New Page Name>.json` and update inside it:
|
||||
- `unpublishedPage.name` -> `<New Page Name>`
|
||||
- `unpublishedPage.slug` -> slug form (e.g. `pending-pos-slx-pending`)
|
||||
- Replace all widget IDs in the new page's JSON files with new unique IDs (e.g. new UUIDs or unique strings) so the new page does not conflict with the template. Update any `parentId` references to the new IDs.
|
||||
- In every widget JSON under the new page, replace any reference to the old page name or slug with the new page name or slug (e.g. in queries or onClick that might point to the template page).
|
||||
|
||||
### 2. Add gitSyncId (CRITICAL)
|
||||
|
||||
Appsmith's git sync uses `gitSyncId` to track entities. **Without this field, Appsmith will auto-remove the page and query on the next pull/sync.**
|
||||
|
||||
- **Page JSON** (`<New Page Name>.json`): Add `"gitSyncId"` as the **first field** in the root object.
|
||||
- **Query metadata** (`queries/<query_name>/metadata.json`): Add `"gitSyncId"` as the **first field** in the root object.
|
||||
|
||||
**Format**: `"<24-char-hex>_<uuid-v4>"`
|
||||
|
||||
Generate unique IDs:
|
||||
```bash
|
||||
python3 -c "import uuid; print(uuid.uuid4().hex[:24] + '_' + str(uuid.uuid4()))"
|
||||
```
|
||||
|
||||
Use the **same 24-char hex prefix** for both the page and its queries (they share a creation context), but a **unique UUID suffix** for each.
|
||||
|
||||
**Example** (page JSON):
|
||||
```json
|
||||
{
|
||||
"gitSyncId": "3ac30122d2934ca9bf65e36a_54bc4b88-d468-4faa-bab0-80921aa88795",
|
||||
"unpublishedPage": { ... }
|
||||
}
|
||||
```
|
||||
|
||||
**Example** (query metadata):
|
||||
```json
|
||||
{
|
||||
"gitSyncId": "3ac30122d2934ca9bf65e36a_da6910cc-4263-496f-ba35-ffc773eddaae",
|
||||
"id": "PageName_query_name",
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Set the new page's Heading
|
||||
|
||||
- In `pages/<New Page Name>/widgets/Heading.json`, set:
|
||||
- `"text": "<New Page Name>"`
|
||||
so the heading always shows the page name (no dynamic binding; `appsmith.page` is undefined in this app).
|
||||
|
||||
### 4. Register the new page in the app
|
||||
|
||||
- In `application.json`, add to the `pages` array one entry:
|
||||
- `{"id": "<New Page Name>", "isDefault": false}`
|
||||
- Do not change `isDefault: true` on the existing default page unless the user asks to make the new page default.
|
||||
|
||||
### 5. Add the new nav button on every page
|
||||
|
||||
Apply the **navigation-button-add** skill:
|
||||
|
||||
- **Pages to update**: **All** existing pages **and** the new page.
|
||||
- **On each page**, in `widgets/Container1/`, add a new button:
|
||||
- `text`: short label for the page (e.g. "SLx Pending" for "Pending POs - SLx Pending").
|
||||
- `onClick`: `{{navigateTo('<New Page Name>', {}, 'SAME_WINDOW');}}`
|
||||
- `buttonColor`:
|
||||
- On the **new page only**: `{{appsmith.theme.colors.backgroundColor}}` and add `{"key":"buttonColor"}` to `dynamicBindingPathList`.
|
||||
- On all **other** pages: `#ffffff`, and do not add `buttonColor` to `dynamicBindingPathList`.
|
||||
- Place the new button below existing nav items within its section. Each button occupies 4 rows (e.g. topRow 22, bottomRow 26).
|
||||
- Assign a new unique `widgetId` and `widgetName` for the new button on each page.
|
||||
- Set `parentId` to the **Canvas widget ID** inside that page's `Container1.json` (the `widgetId` of the `CANVAS_WIDGET` child). This varies per page -- read `Container1.json` to find it.
|
||||
|
||||
### 6. Verify
|
||||
|
||||
- Run `git status` / `git diff` and confirm only intended files were added or changed.
|
||||
- Ensure no duplicate `widgetId` values across pages and that the new page's slug and name are used consistently.
|
||||
- Confirm `gitSyncId` is present in both the page JSON and every query metadata.
|
||||
|
||||
## Summary
|
||||
|
||||
| Item | Value |
|
||||
|------|--------|
|
||||
| Template | Sales - Capacity Planning |
|
||||
| New page folder | `pages/<New Page Name>/` |
|
||||
| gitSyncId | `<24-char-hex>_<uuid-v4>` in page JSON + query metadata |
|
||||
| Heading text | `<New Page Name>` |
|
||||
| Nav button text | Short label (e.g. "SLx Pending") |
|
||||
| Nav button onClick | `navigateTo('<New Page Name>', {}, 'SAME_WINDOW')` |
|
||||
| Highlight (new page) | `buttonColor`: `{{appsmith.theme.colors.backgroundColor}}` |
|
||||
| Inactive (other pages) | `buttonColor`: `#ffffff` |
|
||||
|
||||
## Reference
|
||||
|
||||
- Nav button behavior and layout: follow **navigation-button-add** skill.
|
||||
- New nav sections: follow **navigation-section-add** skill.
|
||||
- Slug format: lowercase, spaces to hyphens (e.g. `Pending POs - SLx Pending` -> `pending-pos-slx-pending`).
|
||||
- `gitSyncId` format: `<24-char-hex>_<uuid-v4>`. Without this, Appsmith auto-removes the entity on sync.
|
||||
54
.cursor/skills/navigation-button-add/SKILL.md
Normal file
54
.cursor/skills/navigation-button-add/SKILL.md
Normal file
@@ -0,0 +1,54 @@
|
||||
---
|
||||
name: navigation-button-add
|
||||
description: Implements a new navigation button with the existing highlight/default colors. Use when the user asks to add buttons to the nav block, mentions page navigation, or instructs to keep the highlight behavior consistent with the latest change.
|
||||
---
|
||||
|
||||
# Navigation Button Addition
|
||||
|
||||
## Instructions
|
||||
|
||||
1. **Understand the current navigation canvas** by checking `pages/*/widgets/Container1/`. Each page reuses the same container structure. The navigation is organized into **sections**, each with a Text label and one or more buttons:
|
||||
- **Sales** section: `Text1` (label), `Button1` (Capacity Planning), `Button1Copy` (Units Shipped), `Button1Copy2` (Units Ordered)
|
||||
- **Pending POs** section: `Text2` (label), `Button2` (SLx Pending)
|
||||
- Future sections follow the same pattern (`Text3`/`Button3`, etc.)
|
||||
2. **Follow the established color scheme**:
|
||||
- The highlighted button (current page) uses `appsmith.theme.colors.backgroundColor`.
|
||||
- The inactive buttons use `#ffffff`.
|
||||
3. **When adding a new navigation button**:
|
||||
- Copy one of the existing `Button*` definitions, adjusting `text`, `onClick`, `widgetId`, `widgetName`, `topRow`, `bottomRow`, and any other placement metadata so it fits below the existing items in its section.
|
||||
- Set `buttonColor` to `#ffffff` (inactive) unless the button is on its own page; then set its definition to `appsmith.theme.colors.backgroundColor` with `dynamicBindingPathList: [{"key": "buttonColor"}]`.
|
||||
- Ensure `dynamicBindingPathList` stays empty when `buttonColor` is static white.
|
||||
- Point `onClick` to `navigateTo('<Page Name>', {}, 'SAME_WINDOW')` and keep `placement`/`responsiveBehavior` matching other nav buttons.
|
||||
- Set `parentId` to the **Canvas widget ID** inside that page's `Container1.json` (the `widgetId` of the `CANVAS_WIDGET` child). This varies per page -- always read `Container1.json` to find the correct value.
|
||||
4. **Add the button on ALL pages** (not just pages in the same section). Every page gets the full navigation.
|
||||
5. **If adding a button to a new section**, follow the **navigation-section-add** skill first to create the section label.
|
||||
6. **Run git status/diff** to verify only the intended files changed before reporting back.
|
||||
|
||||
## Row Layout
|
||||
|
||||
Each widget occupies 4 rows. Sections are separated by a 2-row gap:
|
||||
|
||||
| Widget | topRow | bottomRow |
|
||||
|--------|--------|-----------|
|
||||
| Text1 "Sales" | 0 | 4 |
|
||||
| Button1 "Capacity Planning" | 4 | 8 |
|
||||
| Button1Copy "Units Shipped" | 8 | 12 |
|
||||
| Button1Copy2 "Units Ordered" | 12 | 16 |
|
||||
| *(2-row gap)* | 16 | 18 |
|
||||
| Text2 "Pending POs" | 18 | 22 |
|
||||
| Button2 "SLx Pending" | 22 | 26 |
|
||||
|
||||
When adding a new button within a section, increment from the last button's `bottomRow` by 4. When adding a new section after the last one, add 2 rows gap, then 4 for the label, then 4 for each button.
|
||||
|
||||
## Examples
|
||||
|
||||
- *Adding "Units Ordered" button to Sales section*:
|
||||
1. Copy `Button1Copy` (Units Shipped) in each page's `Container1`.
|
||||
2. Set `text` to "Units Ordered", `onClick` to `navigateTo('Sales - Units Ordered', {}, 'SAME_WINDOW');`.
|
||||
3. Assign `buttonColor` to `#ffffff` for inactive cases and to `appsmith.theme.colors.backgroundColor` inside the `Sales - Units Ordered` page definition for highlight.
|
||||
4. Set `topRow: 12`, `bottomRow: 16` (next slot after Units Shipped).
|
||||
|
||||
- *Adding a second button to Pending POs section*:
|
||||
1. Create `Button2Copy.json` in each page's `Container1`.
|
||||
2. Set `topRow: 26`, `bottomRow: 30` (next slot after SLx Pending at 22-26).
|
||||
3. Follow the same highlight/inactive pattern.
|
||||
124
.cursor/skills/navigation-section-add/SKILL.md
Normal file
124
.cursor/skills/navigation-section-add/SKILL.md
Normal file
@@ -0,0 +1,124 @@
|
||||
---
|
||||
name: navigation-section-add
|
||||
description: Adds a new navigation section (label + first button) to the sidebar on all pages. Use when the user asks to create a new section like "Pending POs", add a section header to navigation, or group pages under a new category separate from "Sales".
|
||||
---
|
||||
|
||||
# Navigation Section Addition
|
||||
|
||||
Adds a new **section** to the left-side navigation container on **all pages**. A section consists of a bold Text label (e.g. "Pending POs") followed by one or more page buttons. This skill covers adding the section label and first button; additional buttons follow the **navigation-button-add** skill.
|
||||
|
||||
## Current Sections
|
||||
|
||||
| Section | Label widget | Buttons | Row range |
|
||||
|---------|-------------|---------|-----------|
|
||||
| Sales | `Text1` | `Button1`, `Button1Copy`, `Button1Copy2` | 0-16 |
|
||||
| Pending POs | `Text2` | `Button2` | 18-26 |
|
||||
|
||||
## Instructions
|
||||
|
||||
### 1. Determine row placement
|
||||
|
||||
Sections are separated by a **2-row gap**. Find the last widget's `bottomRow` in the current navigation, add 2 for the gap, then place:
|
||||
- **Section label** (Text widget): 4 rows (e.g. topRow 18, bottomRow 22)
|
||||
- **First button**: 4 rows immediately after (e.g. topRow 22, bottomRow 26)
|
||||
|
||||
### 2. Choose widget names
|
||||
|
||||
Follow the naming sequence based on existing sections:
|
||||
- First section: `Text1`, `Button1` / `Button1Copy` / `Button1Copy2`
|
||||
- Second section: `Text2`, `Button2` / `Button2Copy` / ...
|
||||
- Third section: `Text3`, `Button3` / `Button3Copy` / ...
|
||||
|
||||
### 3. Create the Text label widget on ALL pages
|
||||
|
||||
Add a `Text<N>.json` file in `pages/<each page>/widgets/Container1/` with:
|
||||
|
||||
```json
|
||||
{
|
||||
"animateLoading": true,
|
||||
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
|
||||
"bottomRow": <calculated>,
|
||||
"dynamicBindingPathList": [
|
||||
{"key": "truncateButtonColor"},
|
||||
{"key": "fontFamily"},
|
||||
{"key": "borderRadius"}
|
||||
],
|
||||
"dynamicHeight": "AUTO_HEIGHT",
|
||||
"dynamicTriggerPathList": [],
|
||||
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
|
||||
"fontSize": "1rem",
|
||||
"fontStyle": "BOLD",
|
||||
"isLoading": false,
|
||||
"isVisible": true,
|
||||
"key": "<unique-key>",
|
||||
"leftColumn": 0,
|
||||
"maxDynamicHeight": 9000,
|
||||
"minDynamicHeight": 4,
|
||||
"minWidth": 450,
|
||||
"mobileBottomRow": <calculated>,
|
||||
"mobileLeftColumn": 10,
|
||||
"mobileRightColumn": 26,
|
||||
"mobileTopRow": <calculated>,
|
||||
"needsErrorInfo": false,
|
||||
"originalBottomRow": <calculated>,
|
||||
"originalTopRow": <calculated>,
|
||||
"overflow": "NONE",
|
||||
"parentColumnSpace": 3.841796875,
|
||||
"parentId": "<Canvas widgetId from Container1.json>",
|
||||
"parentRowSpace": 10,
|
||||
"renderMode": "CANVAS",
|
||||
"responsiveBehavior": "fill",
|
||||
"rightColumn": 64,
|
||||
"shouldTruncate": false,
|
||||
"text": "<Section Name>",
|
||||
"textAlign": "LEFT",
|
||||
"textColor": "#231F20",
|
||||
"topRow": <calculated>,
|
||||
"truncateButtonColor": "{{appsmith.theme.colors.primaryColor}}",
|
||||
"type": "TEXT_WIDGET",
|
||||
"version": 1,
|
||||
"widgetId": "<unique per page>",
|
||||
"widgetName": "Text<N>"
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Create the first button on ALL pages
|
||||
|
||||
Follow the **navigation-button-add** skill to add `Button<N>.json`. Key points:
|
||||
- On the page the button navigates to: `buttonColor` = `{{appsmith.theme.colors.backgroundColor}}` (highlighted), with `dynamicBindingPathList: [{"key": "buttonColor"}]`.
|
||||
- On all other pages: `buttonColor` = `#ffffff` (inactive), with empty `dynamicBindingPathList`.
|
||||
- `topRow` = label's `bottomRow`, `bottomRow` = topRow + 4.
|
||||
|
||||
### 5. parentId -- CRITICAL
|
||||
|
||||
Each page's Container1 has a **different Canvas widget ID**. The `parentId` for all nav widgets must match the `widgetId` of the `CANVAS_WIDGET` child inside that page's `Container1.json`.
|
||||
|
||||
**Always read** `pages/<PageName>/widgets/Container1/Container1.json` to find the Canvas `widgetId` before creating widgets. Do NOT copy parentId from another page.
|
||||
|
||||
Example Canvas widget IDs (verify these are still current):
|
||||
- Sales - Capacity Planning: `x3pc17vp6q`
|
||||
- Sales - Units Shipped: `wj6fxg5wpg`
|
||||
- Sales - Units Ordered: `wj6fxg5wpg`
|
||||
|
||||
### 6. Widget IDs -- must be unique
|
||||
|
||||
Every widget across all pages must have a **unique `widgetId`**. Use a short, unique string per widget per page. A practical pattern: `<page-prefix><widget-abbrev>` (e.g. `cp1txtpndpo` for Capacity Planning's "Pending POs" text label).
|
||||
|
||||
### 7. Apply to ALL pages
|
||||
|
||||
The section label and button must appear on **every page in the app** -- not just pages within the new section. This ensures the full navigation is visible regardless of which page the user is on.
|
||||
|
||||
## Example: Adding "Pending POs" section
|
||||
|
||||
Files created on each page's `widgets/Container1/`:
|
||||
- `Text2.json` -- "Pending POs" label, topRow 18, bottomRow 22
|
||||
- `Button2.json` -- "SLx Pending" button, topRow 22, bottomRow 26
|
||||
|
||||
On `Pending POs - SLx Pending` page: Button2 is highlighted.
|
||||
On all Sales pages: Button2 is white/inactive.
|
||||
|
||||
## Reference
|
||||
|
||||
- Button styling and highlight pattern: **navigation-button-add** skill.
|
||||
- Creating the page itself: **create-new-page** skill.
|
||||
- Row layout: 4 rows per widget, 2-row gap between sections.
|
||||
53
.cursor/skills/prepare-for-launch/SKILL.md
Normal file
53
.cursor/skills/prepare-for-launch/SKILL.md
Normal file
@@ -0,0 +1,53 @@
|
||||
---
|
||||
name: prepare-for-launch
|
||||
description: Prepares the app for production launch by moving from sandbox to production (GoLive) database and related steps. Use when the user asks to prepare for launch, move to production, switch to GoLive, or launch the app.
|
||||
---
|
||||
|
||||
# Prepare for Launch
|
||||
|
||||
Use this skill when preparing to launch the app: moving all data sources from sandbox to the production (xTuple_GoLive) database and performing launch checklist steps.
|
||||
|
||||
## Launch checklist (run in order)
|
||||
|
||||
1. **Switch all queries to xTuple_GoLive** (see below).
|
||||
2. Add or run any other launch steps you need (e.g. env/config, final checks).
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Move every query to xTuple_GoLive
|
||||
|
||||
Ensure every DB query in the app uses the production datasource `xTuple_GoLive` instead of `xTuple_Sandbox`.
|
||||
|
||||
### How to do it
|
||||
|
||||
1. **Find all query metadata files**
|
||||
Under `pages/`, each query has a `metadata.json` in `pages/<PageName>/queries/<query_name>/metadata.json`.
|
||||
|
||||
2. **In each `metadata.json`**, locate the datasource block inside `unpublishedAction`:
|
||||
```json
|
||||
"datasource": {
|
||||
"id": "xTuple_Sandbox",
|
||||
"isAutoGenerated": false,
|
||||
"name": "xTuple_Sandbox",
|
||||
"pluginId": "postgres-plugin"
|
||||
}
|
||||
```
|
||||
|
||||
3. **Replace** both `"id"` and `"name"` from `xTuple_Sandbox` to `xTuple_GoLive`:
|
||||
```json
|
||||
"datasource": {
|
||||
"id": "xTuple_GoLive",
|
||||
"isAutoGenerated": false,
|
||||
"name": "xTuple_GoLive",
|
||||
"pluginId": "postgres-plugin"
|
||||
}
|
||||
```
|
||||
|
||||
4. **Verify**
|
||||
- No query under `pages/**/queries/**/metadata.json` should reference `xTuple_Sandbox`.
|
||||
- Search the repo for `xTuple_Sandbox`; the only remaining references should be the datasource definition (e.g. `datasources/xTuple_Sandbox.json`) and any docs/skills that describe sandbox as the default (e.g. add-query skill).
|
||||
|
||||
### Scope
|
||||
|
||||
- Update **every** query that currently uses `xTuple_Sandbox`. Queries already using `xTuple_GoLive` can be left unchanged.
|
||||
- Do not change the add-query (or other) skills that say to use Sandbox by default; those are for day-to-day development. This skill is only for the one-time (or repeated) launch prep.
|
||||
Reference in New Issue
Block a user