diff --git a/.cursor/rules/no-docs-directory.mdc b/.cursor/rules/no-docs-directory.mdc new file mode 100644 index 0000000..e4a7d9d --- /dev/null +++ b/.cursor/rules/no-docs-directory.mdc @@ -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. diff --git a/.cursor/skills/.DS_Store b/.cursor/skills/.DS_Store new file mode 100644 index 0000000..db9afce Binary files /dev/null and b/.cursor/skills/.DS_Store differ diff --git a/.cursor/skills/add-query-to-table/SKILL.md b/.cursor/skills/add-query-to-table/SKILL.md new file mode 100644 index 0000000..afe3cd7 --- /dev/null +++ b/.cursor/skills/add-query-to-table/SKILL.md @@ -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//queries//` +- **Files**: + - **`.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>_"`. 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`**: `"_"` (e.g. `"Pending POs - SLx Pending_pending_pos_slx_pending"`). +- **`name`**: `""` (must match the folder and the name used in bindings). +- **`unpublishedAction.actionConfiguration.body`**: The exact SQL from `.txt`, escaped for JSON (newlines -> `\n`, `"` -> `\"`). +- **`unpublishedAction.datasource`**: Use the same as other queries on the page (e.g. `xTuple_Sandbox`). +- **`unpublishedAction.pageId`**: `""`. +- **`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`**: `"{{.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//queries//`. +2. Add `.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 `{{.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>_`. Without this, Appsmith auto-removes the query on sync. +- **Optional date range**: NULLIF + IS NULL + OR + BETWEEN as above. +- **Table data binding**: `{{.data}}`. +- **Column keys**: Must match query SELECT aliases (e.g. `"Product"`, `"Qty Ordered"`). diff --git a/.cursor/skills/add-tabs-to-page/SKILL.md b/.cursor/skills/add-tabs-to-page/SKILL.md new file mode 100644 index 0000000..c77efde --- /dev/null +++ b/.cursor/skills/add-tabs-to-page/SKILL.md @@ -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//widgets/Tabs1/ +├── Tabs1.json # The TABS_WIDGET itself +├── .json # Table inside tab 1 +├── .json # Table inside tab 2 +├── .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": "" + }, + "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": "