# 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` / `sVersionFlowCode` | product-edition catalogue tags (look up in `sisversionflow` where populated); live menu visibility is still gated by the licence-derived module list |
| `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).)
