# How to define a form A recipe for the metadata rows that produce a working module + form in BACK and FROUNT. Use this when you want to add a new screen without writing Java. ## What you'll insert Three rows, in order, in three tables. Plus optional rows for fields, permissions, and document numbering. ### 1. The module — `gdsmodule` One row registers the module's existence. Required columns: | Column | Value | |---|---| | `sId` | unique ID — use the `IdGen` next-id helper, or any 32-char string not already taken | | `sName` | the URL fragment, e.g. `/indexPage/commonList` (use a shared page-template URL — see [Slice 3](../../slices/03-report.md#why-this-module)) | | `sChinese` / `sEnglish` / `sBig5` | display name in three languages | | `sParentId` | parent module's `sId` — places this module in the menu tree | | `sBrandsId` / `sSubsidiaryId` | tenant scope — should be your tenant's IDs (or `'1111111111'` if standard / system-level) | | `sVersionFlowId` | the product edition this module belongs to (look up in `sisversionflow`) | | `bVisible` | `1` to show in the menu | | `bInvalid` | `0` for active | Leave `sSaveProName`, `sDeleteProName`, `sCalcProName`, `sProcName`, `sSaveProNameBefore` empty unless you need custom CRUD (see [the runtime reference](../maintainer/runtime.md)). ### 2. The form-master — `gdsconfigformmaster` One row per form per module. Required: | Column | Value | |---|---| | `sId` | unique form ID | | `sParentId` | the module's `sId` from step 1 | | `sTbName` | the backing object's name — a real table, view, or proc | | `sType` | `'table'`, `'view'`, or `'proc'` | | `sSqlStr` | the read SQL skeleton; usually `SELECT ... FROM {sTbName} WHERE ...` | | `sWhere` | default WHERE predicates (the runtime adds tenant filters automatically) | | `sOrder` | default ORDER BY | | `iPageSize` | rows per page in the grid | | `bGrd` | `1` if a grid layout, `0` if a single-record form | | `sBrandsId` / `sSubsidiaryId` | tenant scope | For a read-only report, use `sType = 'view'` and point `sTbName` at a [`viw_*` view](../../slices/03-report.md). The framework will not render save/delete buttons. ### 3. The fields — `gdsconfigformslave`, one row per field Most-used columns (the table has 60+, most have sane defaults): | Column | Value | |---|---| | `sId` | unique field ID | | `sParentId` | the form's `sId` from step 2 | | `iOrder` | sort order in the grid | | `sName` | the column name on the backing object | | `sChinese` / `sEnglish` / `sBig5` | display label | | `sControlName` | UI control type — `文本框` (textbox), `下拉框` (dropdown), `日期` (date), `数字` (number), … | | `bVisible` | `1` to show | | `bNotEmpty` | `1` to require | | `bReadonly` | `1` for display-only | | `bCanInput` | `1` for editable | | `iColValue` | column-span in form layout | | `sDefault` | default value | | `sChineseDropDown` | dropdown SQL (if applicable) — `SELECT sId AS sValue, sChinese AS sText FROM …` | | `sActiveId` / `sActiveKey` | for popup-lookup controls — refers to another module | ## Optional layers ### Document numbering — `sysbillnosettings` If the module's records need auto-generated bill numbers (work-order numbers, quotation numbers, etc.), add a row here keyed by the form's `sId`. The runtime returns this in the `getModelBysId` response under `billnosetting`. ### Permissions — `gdsjurisdiction` See [How to set permissions](permissions.md). One row per (module, button-or-data, role) tuple. ### Tenant overrides A tenant who wants a different view of this form inserts rows into `gdsconfigformpersonalize` (form-level override) or `gdsconfigformcustomslave` (field-level override). See [Slice 4](../../slices/04-custom-field.md). ## Verifying Once all rows are inserted, the SPA's next request to `getModelBysId/{your-module-sId}` should return your form's metadata. Click the new sidebar item — the form should render. If it doesn't: 1. Check `bVisible = 1` on `gdsmodule`. 2. Check that `gdsroute` has an entry for the URL (one is auto-added in most deployments). 3. Check `sBrandsId`/`sSubsidiaryId` on every row match the user's tenant. 4. Check that the form's `sParentId` matches the module's `sId` exactly — semantic FK, no cross-row check at write time. ## Cache invalidation After inserting, the runtime's cache holds the *previous* (empty) state until something clears it. When BACK saves the change, the save service synchronously calls `BusinessCleanRedisData.delCleanRedisData*`, which fires `@CacheEvict` on the relevant cache regions in `CleanRedisServiceImpl`. If you're inserting via raw SQL, **no eviction runs** — you'll need to either invoke `BusinessCleanRedisDataImpl` methods directly from inside the application, bounce the running services, or wait for the TTL to expire. (The JMS path with the similarly-named `ConsumerChangeGdsModuleThread` does base-data merging via stored proc, NOT cache invalidation despite the name — see [Cache invalidation on metadata change](../maintainer/cache-invalidation.md).)