@fui-org/fui-cli 2.2.1 → 2.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/{fui-xg17j3f9.js → fui-8tyckpke.js} +160 -159
- package/dist/fui.js +1 -1
- package/package.json +1 -1
- package/skills/fui/SKILL.md +13 -6
- package/skills/fui/references/INDEX.md +1 -1
- package/skills/fui/references/component-quickref.md +5 -3
- package/skills/fui/references/db-table-design.md +1 -1
- package/skills/fui/references/db-workflow.md +13 -13
- package/skills/fui/references/new-session.md +26 -6
- package/skills/fui/references/script-map.md +1 -1
- package/skills/fui/references/system-design.md +1 -1
- package/skills/fui/references/tapi-reference.md +27 -12
- package/skills/fui/references/tools-registry.md +4 -3
- package/skills/fui/scripts/fechart.js +56 -12
package/dist/fui.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{enableCompileCache as a}from"node:module";a?.();var{run:e}=await import("./fui-
|
|
2
|
+
import{enableCompileCache as a}from"node:module";a?.();var{run:e}=await import("./fui-8tyckpke.js");await e();
|
package/package.json
CHANGED
package/skills/fui/SKILL.md
CHANGED
|
@@ -72,13 +72,13 @@ A request to create, design, fix or update a module authorizes the agent to publ
|
|
|
72
72
|
|
|
73
73
|
**Imports**
|
|
74
74
|
|
|
75
|
-
-
|
|
75
|
+
- Before using an existing control, check `defaultControlAttr[control].import` in `scripts/component.js` (V2) or `scripts/component-3.0.js` (V3). Treat that array and its order as the source of truth for required JS/CSS; then check imports on every add or remove. → [component-quickref.md](references/component-quickref.md)
|
|
76
76
|
- **NEVER** add CDN for bundled libs (jQuery, lodash, moment, numeral, Vuetify, toast, jquery-confirm...); list: [script-map.md](references/script-map.md) §0.
|
|
77
77
|
- That ban is `HTMLOnly=false` only. `HTMLOnly: true` bundles **nothing** (no Vue, Vuetify, jQuery, lodash, MDI, `projectdefaultstyle.css`, `#fastProjectAPP`): CDNs are **required**, **per HTMLOnly module** (project level has no effect). → [module-structure.md](references/module-structure.md) §HTMLOnly=true
|
|
78
78
|
|
|
79
79
|
**Database** (enforced in code; details under "Writes need the user")
|
|
80
80
|
|
|
81
|
-
- **NEVER** `DROP
|
|
81
|
+
- **NEVER** `DROP DATABASE` or drop a persistent table, anywhere, incl. SP scripts. `DROP TABLE #name` (one local temporary table) is allowed; `##name`, schema-qualified names and a mixed drop list remain blocked.
|
|
82
82
|
- `ALTER TABLE` (structure OK, data loss not): **free** `ADD` (column/constraint), `DROP CONSTRAINT` · **needs `--confirm-write`** `ALTER COLUMN` · **hard-blocked** `DROP COLUMN`, `SWITCH`, all else (fail-closed). Checked **per statement**: an `ADD` doesn't cover a later `DROP COLUMN` in the batch; hard-block wins.
|
|
83
83
|
- **NEVER** `DELETE`/`UPDATE` without `WHERE`; show the SQL for the user to run.
|
|
84
84
|
- **Never test on the live system**; the line is **writes**, not network. Verify = read definition + reason + cross-check; **never run an SP that writes**, and never call a real API/SP/DML "to test". Levels:
|
|
@@ -225,7 +225,7 @@ Before writing `apiMocks` for an SP that exists, run `fui sp verify <name>`. It
|
|
|
225
225
|
- Writes to real data exit `3` until you add `--confirm-write`. That covers `INSERT`, `UPDATE`, `DELETE`, `MERGE` and `TRUNCATE` in `query` or `exec` (also inside `EXEC('...')` or `sp_executesql`), `SELECT ... INTO` a real table, `DROP PROCEDURE`/`FUNCTION`/`VIEW`, and `ALTER TABLE ... ALTER COLUMN`.
|
|
226
226
|
- Add `--confirm-write` **only after the user approved that exact write in chat**, never just to make it run. → [db-workflow.md](references/db-workflow.md)
|
|
227
227
|
- Always blocked, with no flag to unlock them. Show the SQL to the user to run by hand:
|
|
228
|
-
- `DROP TABLE` and `DROP DATABASE`
|
|
228
|
+
- `DROP TABLE` for a persistent/global table and `DROP DATABASE` (a single local `#name` temporary table is allowed)
|
|
229
229
|
- `ALTER TABLE ... DROP COLUMN` or `SWITCH`
|
|
230
230
|
- `xp_cmdshell`
|
|
231
231
|
- `DELETE`/`UPDATE` without `WHERE`, in `query` and `exec`
|
|
@@ -257,6 +257,12 @@ fui query "SELECT TOP 5 ID, Name FROM tblX" --json | jq -r '.[].Name'
|
|
|
257
257
|
|
|
258
258
|
## Connections and secrets
|
|
259
259
|
|
|
260
|
+
For any DB exploration, schema, SQL, or API/SP design, start with `fui db list -p <project>` and use a saved connection with `--db`. These operations use its **dbToken**. A `userToken` is only for the `acc` management lookup and end-user API calls; it cannot authenticate SQL requests.
|
|
261
|
+
|
|
262
|
+
One `userToken` is reusable across projects in the same SSO zone (the parent domain, e.g. `*.lhu.edu.vn`). When a project has no token, the CLI looks for one on the exact API host first, then on another host in that zone. Do not ask for a new login when there is one unique token in the zone. A different zone needs its own token; if several distinct tokens exist in the zone, use `--user-token-from <project/connectionName>` for `db alias list` or `db add-by-name` to select an already saved token without exposing it. → [db-workflow.md](references/db-workflow.md) §6a.
|
|
263
|
+
|
|
264
|
+
If no connection exists, find the project's tAPI domain with `fui project sync -p <project>` when needed, then run `fui db alias list -p <project>` to see databases the account may manage. Connect the chosen alias with `fui db add-by-name <apiName> -p <project>`. This discovers SQLServer/DatabaseName through `acc`, reuses a SQL UID/PWD pair from a saved dbToken (same apiDomain preferred; another domain works when it is the only login), builds a new dbToken, tests it against the target DB, and fetches schema. With several source logins, select one with `--borrow-from <project/connectionName>`. If there is no saved SQL login, use `--uid <login> --pwd -` or `--pwd-env VAR` with `add-by-name`. A management userToken alone cannot reveal the SQL password; never treat the `Password` field from `acc` as one. Then use `--db <project/connectionName>` for DB work.
|
|
265
|
+
|
|
260
266
|
```bash
|
|
261
267
|
printf %s "$TOKEN" | fui db add docs -p lhu-docs --token - --api-domain tapi.lhu.edu.vn --user-token-env UT
|
|
262
268
|
fui db add local -p demo --conn-env SQLITE_URL # sqlite:// connects directly (MSSQL goes through tAPI)
|
|
@@ -268,11 +274,12 @@ fui token decode --db lhu-docs/docs # never shows the password
|
|
|
268
274
|
Secrets are accepted only from stdin (`-`) or `--*-env VAR`. Never echo or paste token values.
|
|
269
275
|
|
|
270
276
|
- Never `cat`, `jq` or otherwise print `_db/_connections.json` or `~/.config/fui/config.json`. Use `fui db list`, `fui token decode` or `fui config`; they never print secrets.
|
|
271
|
-
-
|
|
272
|
-
- Know the alias but not the connection string? `fui db alias list`, then `fui db add-by-name <apiName>`: it
|
|
277
|
+
- Need a management userToken for the `acc` lookup? `fui db user-token` lists fingerprints of tokens already in the workspace; copy one with `--from <project/alias>`. This does not create a dbToken. The value never appears.
|
|
278
|
+
- Know the alias but not the connection string? `fui db alias list`, then `fui db add-by-name <apiName>`: it reuses a saved SQL UID/PWD pair and combines it with the target server/database returned by `acc`.
|
|
273
279
|
- `db add` / `db add-by-name` take `--name <connectionName>`; omitted, it is suggested from apiName, then apiName_zone. The same server + database re-added keeps its name, and folders are never renamed.
|
|
280
|
+
- `fui db remove <project>/<connectionName>` removes one local connection from `_connections.json`. It does not change the server; cached schema and SQL files under `_db/<connectionName>/` remain available.
|
|
274
281
|
- `right *` and `db alias new` write to the shared acc system. Ask the user first, especially for `delete`.
|
|
275
|
-
- `_db/_connections.json`
|
|
282
|
+
- `_db/_connections.json` has no `version` field. Existing files with `"version": 2` or connections missing `apiName` are normalized on first read (the old file goes to `_history/`); `fui db migrate` does this for every project at once.
|
|
276
283
|
|
|
277
284
|
## FUI conventions
|
|
278
285
|
|
|
@@ -49,7 +49,7 @@ Load: `fui skill ref references/a.md references/b.md` (several at once OK).
|
|
|
49
49
|
|
|
50
50
|
- SP naming, params, URL, response, errors, wiring → `tapi-reference.md`
|
|
51
51
|
- 200 but empty/wrong data, or changing/removing params of an SP a module calls: lenient binding (missing → `NULL`, extra → ignored, never errors) → `tapi-reference.md` §2
|
|
52
|
-
-
|
|
52
|
+
- Remote address in SP: `@sys_RemoteIP varchar(15)` from `REMOTE_ADDR`; HTTP headers (`User-Agent`, `Cookie`, `X-Forwarded-For`, device/browser): `@sys_header_*`, each `-` → `_`, except `Authorization` → `tapi-reference.md` §2
|
|
53
53
|
- SP permission checks (5 patterns, `@sys_SystemRight`, `getSystemRight`) → `tapi-permission-patterns.md`
|
|
54
54
|
- File upload/download/view (`spAPIFILE_`, `fileContent`, `tblFileData`) → `tapi-file-api.md`
|
|
55
55
|
- SQL CLR: httpCall, sendMail, ImageResize, FileReader, RegexMatch… → `sql-clr-functions.md`
|
|
@@ -41,23 +41,25 @@ Prefer `f-*`; use `v-*` only when no FUI equivalent exists.
|
|
|
41
41
|
|
|
42
42
|
## Components needing JS/CSS imports
|
|
43
43
|
|
|
44
|
-
**When adding or removing
|
|
44
|
+
**Before using any built-in control, inspect `defaultControlAttr[control].import` in `scripts/component.js` for V2 or `scripts/component-3.0.js` for V3.** That ordered array is the source of truth for JS/CSS URLs. The table below is a quick reference, not an exhaustive list. When adding or removing a control with imports, run `fui import list` and add/remove the matching imports. Do not substitute a similar CDN URL without checking the selected framework's source.
|
|
45
45
|
|
|
46
46
|
| Component | Required imports |
|
|
47
47
|
| ----------------- | -------------------------------------------------------------------------------------------------------- |
|
|
48
48
|
| `f-pdfmake` | `/include/pdfmake/pdfmake.min.js` · `/include/pdfmake/print.min.js` · `/include/pdfmake/vfs_fonts.js` |
|
|
49
49
|
| `f-file-upload` | `/include/plupload-2.3.7/plupload.full.min.js` |
|
|
50
|
-
| `f-image-update` | `/include/vue-advanced-cropper/index.umd.js` · `/include/vue-advanced-cropper/style.css`
|
|
50
|
+
| `f-image-update` | V2: `/include/vue-advanced-cropper/index.umd.js` · `/include/vue-advanced-cropper/style.css`; V3: use the two `unpkg.com/vue-advanced-cropper` URLs in `component-3.0.js` |
|
|
51
51
|
| `f-qrcode` | `/include/qrcode/qrcode.min.js` |
|
|
52
52
|
| `f-qrcode-reader` | `/include/qrcode/qrcode-reader.min.js` |
|
|
53
53
|
| `f-echart` | `/include/chart/echarts.min.js` · `/include/chart/fechart.js` (**in this order**) — Apache ECharts |
|
|
54
54
|
| `f-sheet` | `/include/ag-grid/ag-grid-community.min.js` · `/include/ag-grid/fsheet.js` (**in this order**) — AG Grid |
|
|
55
55
|
| `f-editor` | `/include/ckeditor5/build/ckeditor.js` |
|
|
56
56
|
| `f-editor-dialog` | `/include/ckeditor5/build/ckeditor.js` |
|
|
57
|
+
| `f-audio`, `f-audio-recorder` | `/include/audio/vueaudio.js` |
|
|
58
|
+
| `f-q-and-a`, `f-etest-review` | MathJax, Q&A CSS, context menu, audio and QandA.js; `f-q-and-a` also needs CKEditor and Plupload. V3 `f-q-and-a` additionally loads Sortable and vuedraggable. Read the complete ordered array in the matching `component*.js`. |
|
|
57
59
|
|
|
58
60
|
> **Bundled, NO import needed:** `v-*` (Vuetify) · `f-table` · `f-dialog` · `f-button` ·
|
|
59
61
|
> `f-date` · `f-time` · `f-search` · `f-menu` · `f-radiobox` · `f-box` · `f-label` · `f-header` ·
|
|
60
|
-
> `f-title` · `f-slider` · `f-excel-reader
|
|
62
|
+
> `f-title` · `f-slider` · `f-excel-reader`. Other components may also declare `import` in `defaultControlAttr`; check the source before assuming none is needed.
|
|
61
63
|
|
|
62
64
|
**Workflow when adding a component with imports** (see `fui import --help`):
|
|
63
65
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> Owns: **table design rules: naming, PK/FK, audit columns, data types**. Connection/SP workflow: [db-workflow.md](db-workflow.md). Load when designing a new schema.
|
|
4
4
|
|
|
5
|
-
> **DDL — structure changes OK, data loss not:** `ALTER TABLE ... ADD` (column/constraint) and `DROP
|
|
5
|
+
> **DDL — structure changes OK, data loss not:** `ALTER TABLE ... ADD` (column/constraint), `DROP CONSTRAINT`, and `DROP TABLE #name` for one local temporary table run directly via fui. `ALTER COLUMN` (type change) runs but **needs `--confirm-write`** — narrowing numeric/time precision silently loses data. `CREATE TABLE`, `DROP COLUMN`, and dropping persistent or global (`##name`) tables are **hard-blocked**; the developer runs them. Conventions below are documentation — present DDL to the user for approval before running.
|
|
6
6
|
>
|
|
7
7
|
> DB connection, dbToken, schema, SP workflow: [db-workflow.md](db-workflow.md). Multi-table/multi-module app design: [system-design.md](system-design.md).
|
|
8
8
|
|
|
@@ -33,7 +33,6 @@ One `_db/` subfolder PER connection. The folder name is a short, readable `conne
|
|
|
33
33
|
|
|
34
34
|
```json
|
|
35
35
|
{
|
|
36
|
-
"version": 2,
|
|
37
36
|
"default": "docs",
|
|
38
37
|
"connections": {
|
|
39
38
|
"docs": {
|
|
@@ -59,20 +58,19 @@ One `_db/` subfolder PER connection. The folder name is a short, readable `conne
|
|
|
59
58
|
|
|
60
59
|
| Field | Req | Meaning |
|
|
61
60
|
| ----------- | --- | ------------------------------------------------------------------ |
|
|
62
|
-
| `version` | ✅ | Always `2` for this layout |
|
|
63
61
|
| key | ✅ | `connectionName`, also the folder name (`docs`, `docs_lhbs`) |
|
|
64
62
|
| `apiName` | ✅ | tAPI alias in `"API": "/{apiName}/Ten"`; may repeat across zones |
|
|
65
63
|
| `apiDomain` | ✅ | Host only, NO alias, NO trailing `/` (`api.example3.vn`) |
|
|
66
64
|
| `sqlServer` | ✅ | SQL Server host; with `database`, identifies the physical database |
|
|
67
65
|
| `database` | ✅ | Database name; with `sqlServer`, prevents duplicate connections |
|
|
68
66
|
| `dbToken` | ✅ | Base64 SQL Server connection string |
|
|
69
|
-
| `userToken` | ❌ | Bearer
|
|
67
|
+
| `userToken` | ❌ | Bearer for `acc` management lookup and end-user `spAPI_*` calls; never a SQL credential |
|
|
70
68
|
|
|
71
69
|
Without `--sql-server`/`--database`, `fui db add` reads both from the connection string in the token.
|
|
72
70
|
|
|
73
71
|
**Naming:** pass `--name <connectionName>` for a specific name. Omitted, fui suggests `apiName` first; if taken, adds the zone from `apiDomain` (`docs` → `docs_lhbs`); still taken, adds the database and a number. Names take letters, digits, `.`, `_`, `-` and are lowercased. The same server + database added again keeps its existing name.
|
|
74
72
|
|
|
75
|
-
**
|
|
73
|
+
**Older files, no folder moves:** a record without `apiName` reads as `apiName = connectionName = key`. A legacy `"version": 2` marker is accepted. On first read, fui normalizes the file without a version field (old file kept in `_history/`). It never renames old SQL folders.
|
|
76
74
|
|
|
77
75
|
> **CRITICAL — `apiDomain` has two meanings:** `_connections.json` → bare host (`api.example3.vn`); `project.json` `data.apiDomain` → full base URL with apiName + trailing `/` (`https://tapi.example.vn/acc/`). Top bug: bare host + apiName without `/` → `tapi.lhu.edu.vnts` instead of `tapi.lhu.edu.vn/ts`. See [tapi-reference.md](tapi-reference.md) §3. Don't build URLs; copy the `url (reference only, not called; copy verbatim)` line from `fui sp verify`/`fui sp help`.
|
|
78
76
|
|
|
@@ -107,11 +105,12 @@ Without `--sql-server`/`--database`, `fui db add` reads both from the connection
|
|
|
107
105
|
Never extract UID/PWD by hand (PowerShell etc.) — exposes credentials.
|
|
108
106
|
|
|
109
107
|
1. `fui db alias list` — no args: tAPI injects `@sys_UserID` from the Bearer token, so `SM_Modules_ManagerSelect` returns only DBs you're granted. Columns `APIName | SQLServer | DatabaseName | ModuleName | Enable`; `--search` filters client-side (APIName/DatabaseName/ModuleName, case-insensitive).
|
|
110
|
-
2. `fui db add-by-name <apiName>` — looks up SQLServer/DatabaseName on acc → borrows the UID/PWD pair of an existing connection → builds dbToken → tests `SELECT 1` before saving anything → saves + fetches schema to `_db/{connectionName}/`. Never prints dbToken/UID/PWD.
|
|
108
|
+
2. `fui db add-by-name <apiName>` — looks up SQLServer/DatabaseName on acc → borrows the UID/PWD pair of an existing connection, or takes `--uid <login> --pwd -` / `--pwd-env VAR` → builds dbToken → tests `SELECT 1` before saving anything → saves + fetches schema to `_db/{connectionName}/`. Never prints dbToken/UID/PWD.
|
|
111
109
|
|
|
112
|
-
-
|
|
110
|
+
- Reuse the UID/PWD pair from a saved connection; target SQLServer and DatabaseName come from `acc` and may differ from the source. Prefer sources on the same apiDomain; if none exist, a unique login elsewhere in the workspace is tried against the target DB. Several different logins → menu; pass `--borrow-from <projectId>/<connectionName>`. Credentials are never printed.
|
|
113
111
|
- "apiName not found" = alias doesn't exist OR you're not granted it. Don't assume a typo; run `fui db alias list`.
|
|
114
|
-
-
|
|
112
|
+
- A project with no connection can use apiDomain from local `project.json`. A project whose connection lacks userToken can use that connection's apiDomain. In either case fui looks for a token on the exact API host first, then another host in the same SSO zone (auto only if one distinct token; otherwise use `--user-token-from <project/connectionName>`). Else pass `--api-domain` + `--user-token-env VAR`.
|
|
113
|
+
- `userToken` authorizes only the `acc` lookup. It cannot become a `dbToken` or reveal a SQL password. If no saved dbToken contains a usable SQL login, supply the SQL login and password through `add-by-name`, or supply a dbToken through `fui db add`.
|
|
115
114
|
|
|
116
115
|
> ⚠️ `Password` in `SM_Modules_ManagerSelect` is a tAPI internal hash, not the SQL password — unusable for dbToken. fui never prints it.
|
|
117
116
|
|
|
@@ -127,7 +126,7 @@ Never extract UID/PWD by hand (PowerShell etc.) — exposes credentials.
|
|
|
127
126
|
5. Module uses other aliases? Repeat 4 per alias. Added connections do NOT become default (use --default).
|
|
128
127
|
```
|
|
129
128
|
|
|
130
|
-
Already have a dbToken → skip 3–4, use `fui db add`.
|
|
129
|
+
Already have a dbToken → skip 3–4, use `fui db add`. Already connected → skip discovery and use `--db <project/connectionName>` for schema, SQL, SP and API design.
|
|
131
130
|
|
|
132
131
|
## 3. Building a dbToken
|
|
133
132
|
|
|
@@ -178,19 +177,19 @@ UID/PWD changed, Server/Database unchanged:
|
|
|
178
177
|
|
|
179
178
|
```
|
|
180
179
|
dbToken → SQL Server connection to run schema/SP
|
|
181
|
-
userToken → call tAPI REST API as an end-user
|
|
180
|
+
userToken → discover managed aliases through acc, or call tAPI REST API as an end-user
|
|
182
181
|
```
|
|
183
182
|
|
|
184
|
-
- Shareable across projects
|
|
183
|
+
- Shareable across projects in the same SSO zone: `fui db user-token --db <connectionName> --from <project/connectionName>` copies a specific token without exposing its value. `fui db user-token` with no source lists workspace tokens. `GroupName` alone does not prove that two projects share an SSO zone.
|
|
185
184
|
- Stored PER CONNECTION in `_connections.json`. Multi-DB → one token per connection; pass `--db <connectionName>` (omitted → default).
|
|
186
|
-
- `fui right …` calls `acc` with the userToken of the connection matching the current apiName (else default);
|
|
185
|
+
- `fui right …` calls `acc` with the userToken of the connection matching the current apiName (else default); if absent, fui searches saved tokens in the same SSO zone.
|
|
187
186
|
- Not set at `fui db add` → `fui db user-token --db <connectionName> --token -`.
|
|
188
187
|
|
|
189
188
|
## 6a. Domain SSO — same parent domain: token works, don't ask
|
|
190
189
|
|
|
191
190
|
SSO is by parent domain: all APIs under e.g. `*.lhu.edu.vn` (`tapi.lhu.edu.vn`, `api.lhu.edu.vn`, `capnhatluong.lhu.edu.vn`…) accept the same `userToken`.
|
|
192
191
|
|
|
193
|
-
- Have a token for that parent domain → use it for other projects/aliases there. Do
|
|
192
|
+
- Have a token for that parent domain → use it for other projects/aliases there. Fui prefers an exact API host match, then a unique token from the zone. If several distinct tokens qualify, use `--user-token-from <project/connectionName>` for the `acc` lookup. Do not ask the user to log in again when the right token is already saved.
|
|
194
193
|
- Scanning the workspace for tokens (`fui db user-token` without a token) is valid and recommended.
|
|
195
194
|
|
|
196
195
|
| Case | Action |
|
|
@@ -248,7 +247,7 @@ Prints `📋 Archived orphaned local SP file(s)...` when archiving. Name match i
|
|
|
248
247
|
- `--db` omitted → inferred from SP name (schema + local `.sql` per connection). One match → silent; zero or several (same name in two DBs) → connection menu, pick and re-run, don't guess.
|
|
249
248
|
- `fui sp save` for a brand-new SP can't infer: multi-connection projects must pass `--db` on first save.
|
|
250
249
|
|
|
251
|
-
> `fui sp verify` reads the real body, not the name; hard error if unreadable. Write-API testing with `fui module simulate`: [tools-registry.md](tools-registry.md). Guards on `DROP TABLE
|
|
250
|
+
> `fui sp verify` reads the real body, not the name; hard error if unreadable. Write-API testing with `fui module simulate`: [tools-registry.md](tools-registry.md). Guards on persistent/global `DROP TABLE` (a single local `#name` temporary table is allowed), `DROP DATABASE`/`ALTER TABLE`, `xp_cmdshell`, `DELETE`/`UPDATE` without `WHERE` are enforced in code, no bypass. `sp deploy` also refuses a file that is not exactly one definition of the named object. Writes via `fui query`/`fui exec` need `--confirm-write`.
|
|
252
251
|
|
|
253
252
|
**Rollback:** read `{workspace}/_history/{projectId}/_db/{connectionName}/{name}_{timestamp}.sql` → `fui sp save <name> -f <old file>` → `fui sp deploy <name>`.
|
|
254
253
|
|
|
@@ -285,6 +284,7 @@ EXEC XDATA23.Calendar2015.dbo.spIO_IN_DeleteMonHocInfo @MonHocID, @ErrorDKMH OUT
|
|
|
285
284
|
| Alias only, no dbToken | `fui db alias list` → `fui db add-by-name` |
|
|
286
285
|
| Which DBs am I granted | `fui db alias list` |
|
|
287
286
|
| Another DB (new alias) | `fui db add-by-name` again |
|
|
287
|
+
| Remove one local connection | `fui db remove <project>/<connectionName>`; server alias and cached schema/SQL files remain |
|
|
288
288
|
| Connections + default | `fui db list` |
|
|
289
289
|
| UID/PWD changed | `fui db token-rebuild` (`--db` if several) |
|
|
290
290
|
| Set userToken | `fui db user-token` (`--db` if several) |
|
|
@@ -26,19 +26,19 @@ Session for **one project** → `{workspaceRoot}/{projectId}`; general work →
|
|
|
26
26
|
**Windows (PowerShell)** — most common:
|
|
27
27
|
|
|
28
28
|
```powershell
|
|
29
|
-
powershell -Command "Start-Process powershell -ArgumentList '-NoExit', '-Command', 'cd ''<THƯ-MỤC>''; claude --rc'"
|
|
29
|
+
powershell -Command "Start-Process powershell -ArgumentList '-NoExit', '-Command', 'Remove-Item Env:\CLAUDE_CODE_CHILD_SESSION -ErrorAction SilentlyContinue; cd ''<THƯ-MỤC>''; claude --rc'"
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
**macOS
|
|
32
|
+
**macOS** (by analogy, not yet verified on a Mac — see §4):
|
|
33
33
|
|
|
34
34
|
```bash
|
|
35
|
-
osascript -e 'tell app "Terminal" to do script "cd \"<THƯ-MỤC>\" && claude --rc"'
|
|
35
|
+
osascript -e 'tell app "Terminal" to do script "unset CLAUDE_CODE_CHILD_SESSION; cd \"<THƯ-MỤC>\" && claude --rc"'
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
-
**Linux** (swap `gnome-terminal` for the terminal in use):
|
|
38
|
+
**Linux** (swap `gnome-terminal` for the terminal in use; not yet verified):
|
|
39
39
|
|
|
40
40
|
```bash
|
|
41
|
-
gnome-terminal -- bash -c 'cd "<THƯ-MỤC>" && claude --rc; exec bash'
|
|
41
|
+
gnome-terminal -- bash -c 'unset CLAUDE_CODE_CHILD_SESSION; cd "<THƯ-MỤC>" && claude --rc; exec bash'
|
|
42
42
|
```
|
|
43
43
|
|
|
44
44
|
Team default is **`claude --rc`**. If the user gives other flags (`-c`, `-r`, `--resume <id>`…), substitute them.
|
|
@@ -46,7 +46,7 @@ Team default is **`claude --rc`**. If the user gives other flags (`-c`, `-r`, `-
|
|
|
46
46
|
**With an opening prompt ("open a new session and do X") → the prompt goes BEFORE `--rc`, not after.** `--rc` consumes the token right after it as its own value (see the warning in §4), so placing it first opens an idle session instead of running the prompt. Correct order:
|
|
47
47
|
|
|
48
48
|
```powershell
|
|
49
|
-
powershell -Command "Start-Process powershell -ArgumentList '-NoExit', '-Command', 'cd ''<THƯ-MỤC>''; claude ''X'' --rc'"
|
|
49
|
+
powershell -Command "Start-Process powershell -ArgumentList '-NoExit', '-Command', 'Remove-Item Env:\CLAUDE_CODE_CHILD_SESSION -ErrorAction SilentlyContinue; cd ''<THƯ-MỤC>''; claude ''X'' --rc'"
|
|
50
50
|
```
|
|
51
51
|
|
|
52
52
|
---
|
|
@@ -80,6 +80,26 @@ Report **PID + folder**. No new process ⇒ say it failed; **never report succes
|
|
|
80
80
|
|
|
81
81
|
**Windows: go through `powershell`.** `claude` is usually `claude.ps1` (not `.exe`), so `cmd` can't run it.
|
|
82
82
|
|
|
83
|
+
**🔴 Clear `CLAUDE_CODE_CHILD_SESSION` first, before `cd` — otherwise the new session can never be resumed.** The session running this command carries `CLAUDE_CODE_CHILD_SESSION=1`, and `Start-Process` / `osascript` / `bash -c` inherit the parent environment. The new `claude` sees the marker, treats itself as a child session and **turns transcript saving off**: it works normally but never appears in `claude --resume` (not even with Ctrl+A / all folders), however it is closed. A window the user opens by hand has no such variable, which is the only difference — not the folder, not how the window was closed. Verified on Windows (2026-10-17): the status line "Transcript saving is off — inherited CLAUDE_CODE_CHILD_SESSION marker" disappears once `Remove-Item Env:\CLAUDE_CODE_CHILD_SESSION -ErrorAction SilentlyContinue;` runs first.
|
|
84
|
+
|
|
85
|
+
> In auto mode, the classifier **blocks this command the first time** ("Auto-Mode Bypass"). It goes through once the user has this allow rule in `~/.claude/settings.json` (verified — not a hard deny):
|
|
86
|
+
> ```json
|
|
87
|
+
> "permissions": {
|
|
88
|
+
> "allow": [
|
|
89
|
+
> "PowerShell(Start-Process powershell -ArgumentList '-NoExit', '-Command', 'Remove-Item Env:\\CLAUDE_CODE_CHILD_SESSION*)"
|
|
90
|
+
> ]
|
|
91
|
+
> }
|
|
92
|
+
> ```
|
|
93
|
+
> The rule is per machine (it does not ship with the skill). If it is missing and the command is blocked, tell the user to add it. **Do not route around the block, and do not drop the `Remove-Item` step to get past it** — that brings back the unresumable session.
|
|
94
|
+
>
|
|
95
|
+
> **Offer to add the rule yourself.** After the block, explain in one or two sentences why the rule is needed (so the new session can be resumed, not to bypass oversight), then ask the user whether you should add it. Only on an explicit yes:
|
|
96
|
+
> 1. Read `~/.claude/settings.json` first; create it only if it does not exist.
|
|
97
|
+
> 2. Append the rule to `permissions.allow`, keeping every existing entry (merge, never replace the array).
|
|
98
|
+
> 3. The rule must end with `*)` — a missing `)` fails settings validation ("Malformed Tool(content) rule").
|
|
99
|
+
> 4. Retry the open command once. If it is still blocked, stop and tell the user; do not try other variants.
|
|
100
|
+
>
|
|
101
|
+
> Never add the rule without the user's yes, and never add it pre-emptively before a block has actually happened.
|
|
102
|
+
|
|
83
103
|
---
|
|
84
104
|
|
|
85
105
|
## 5. The new session is an INDEPENDENT process
|
|
@@ -75,7 +75,7 @@ numeral(value).format("0,0.0"); // 1,234,567.8
|
|
|
75
75
|
numeral(_.sumBy(items, key)).format("0,0"); // tổng cột trong bảng
|
|
76
76
|
```
|
|
77
77
|
|
|
78
|
-
> `f-*`
|
|
78
|
+
> Before using any `f-*` control, check `defaultControlAttr[control].import` in the matching V2/V3 `component*.js` file. Its ordered URLs determine required JS/CSS; [component-quickref.md](component-quickref.md) is a quick reference.
|
|
79
79
|
|
|
80
80
|
## 1) scripts/fastproject.js — V3: fastproject-3.0.js
|
|
81
81
|
|
|
@@ -36,7 +36,7 @@ Before any module.json:
|
|
|
36
36
|
|
|
37
37
|
1. Tables + PK + FK for **all** entities. Per [db-table-design.md](db-table-design.md): `tbl` + PascalCase, PK `[Entity]ID`, audit `CreateUser/CreateTime/UpdateUser/UpdateTime`.
|
|
38
38
|
2. n-n → junction table (e.g. `tblUserRole`).
|
|
39
|
-
3. **`CREATE TABLE` is run by the developer** — fui has no guard for it, so do not run it yourself; present DDL for review. Existing tables, gated by data loss: `ALTER TABLE ... ADD` and `DROP
|
|
39
|
+
3. **`CREATE TABLE` is run by the developer** — fui has no guard for it, so do not run it yourself; present DDL for review. Existing tables, gated by data loss: `ALTER TABLE ... ADD`, `DROP CONSTRAINT`, and `DROP TABLE #name` for a local temporary table run directly; `ALTER COLUMN` needs `--confirm-write`; `DROP COLUMN`/persistent or global `DROP TABLE`/`SWITCH` hard-blocked.
|
|
40
40
|
4. Design lookup/catalog tables early (shared dropdowns).
|
|
41
41
|
|
|
42
42
|
**Output:** tables + relations, user-confirmed.
|
|
@@ -65,10 +65,11 @@ https://api.domain.vn/app/FunctionName/value1/value2
|
|
|
65
65
|
| `@sys_SystemRight` | int | Tiered right (1=low, 9=high) |
|
|
66
66
|
| `@sys_FunctionRight` | varchar(2000) | Function rights as `[AD][RPT][MGR]` |
|
|
67
67
|
| `@sys_SessionID` | varchar(50) | Auth session ID |
|
|
68
|
+
| `@sys_RemoteIP` | varchar(15) | `Request.ServerVariables("REMOTE_ADDR")`; the address seen by the server |
|
|
68
69
|
| `@sys_RawData` | nvarchar(max) | Full client JSON body |
|
|
69
|
-
| `@sys_header_{HeaderName}` | varchar(n) |
|
|
70
|
+
| `@sys_header_{HeaderName}` | varchar(n) | Request header except `Authorization`; missing header becomes `NULL`. Naming rule below |
|
|
70
71
|
|
|
71
|
-
> **CRITICAL — never put `@sys_
|
|
72
|
+
> **CRITICAL — never put recognized `@sys_*` params in module.json `IN`.** tAPI assigns them from authenticated user data, the HTTP request, or the JSON body; a same-named client field is ignored.
|
|
72
73
|
>
|
|
73
74
|
> ```json
|
|
74
75
|
> // ❌ SAI — không đưa sys_ vào IN
|
|
@@ -78,9 +79,21 @@ https://api.domain.vn/app/FunctionName/value1/value2
|
|
|
78
79
|
> "IN": { "ModuleID": "sModuleID" }
|
|
79
80
|
> ```
|
|
80
81
|
|
|
82
|
+
To read the remote address in an API procedure, declare the parameter with this exact name and type. tAPI reads `REMOTE_ADDR`; do not send it in `IN`. `varchar(15)` fits IPv4 addresses, so check the deployment's IPv6 and proxy behavior before using it for a wider address range:
|
|
83
|
+
|
|
84
|
+
```sql
|
|
85
|
+
CREATE PROCEDURE spAPI_AuditVisit
|
|
86
|
+
@sys_UserID varchar(9),
|
|
87
|
+
@sys_RemoteIP varchar(15)
|
|
88
|
+
AS
|
|
89
|
+
BEGIN
|
|
90
|
+
SELECT @sys_UserID AS UserID, @sys_RemoteIP AS RemoteIP;
|
|
91
|
+
END
|
|
92
|
+
```
|
|
93
|
+
|
|
81
94
|
#### `@sys_header_*` — read HTTP headers: replace each `-` with `_`
|
|
82
95
|
|
|
83
|
-
Works for **
|
|
96
|
+
Works for request headers **except `Authorization`**. The suffix after `@sys_header_` must be nonempty. tAPI lowercases the SQL parameter name, takes the suffix, replaces every `_` with `-`, then looks it up in `Request.Headers`. `-` is invalid in SQL Server param names, so the literal name won't compile:
|
|
84
97
|
|
|
85
98
|
```sql
|
|
86
99
|
-- ❌ SAI — lỗi cú pháp ngay lúc CREATE PROCEDURE, không phải lỗi runtime
|
|
@@ -88,7 +101,7 @@ Works for **any** header. `-` is invalid in SQL Server param names, so the liter
|
|
|
88
101
|
```
|
|
89
102
|
|
|
90
103
|
- **No quoting works**: `@[sys_header_User-Agent]` and `@"sys_header_User-Agent"` are both invalid. Renaming is the **only** way.
|
|
91
|
-
- **Each `-` → one `_
|
|
104
|
+
- **Each `-` → one `_` in the SP declaration.** tAPI reverses every `_` in the suffix to `-`; a header whose actual name contains `_` cannot be represented exactly by this convention.
|
|
92
105
|
|
|
93
106
|
| HTTP header | SP param |
|
|
94
107
|
| ------------------ | ---------------------------------------- |
|
|
@@ -99,26 +112,28 @@ Works for **any** header. `-` is invalid in SQL Server param names, so the liter
|
|
|
99
112
|
| `Cookie` | `@sys_header_Cookie` — no `-`, unchanged |
|
|
100
113
|
|
|
101
114
|
- Reverse (reading an existing SP): drop `@sys_header_`, turn remaining `_` into `-` — `@sys_header_Accept_Language` → `Accept-Language`.
|
|
102
|
-
- Matching is **case-insensitive** (prefix and
|
|
115
|
+
- Matching is **case-insensitive** (prefix and header lookup): `@sys_header_User_Agent`, `@sys_Header_User_Agent`, `@sys_header_USER_AGENT` request the same header. Use `_` for each `-`; tAPI does not insert separators, so `@sys_Header_RequestID` requests `RequestID`, not `Request-ID`.
|
|
116
|
+
- `@sys_header_Authorization` is **not exposed**: the code skips assignment for that header. It does not explicitly set the SQL parameter to `NULL` in this branch; do not rely on its value.
|
|
117
|
+
- A missing header is assigned `DBNull.Value`; exceptions during header lookup are caught without a reported error, so diagnose a missing value against the actual incoming request.
|
|
103
118
|
|
|
104
119
|
```sql
|
|
105
120
|
CREATE PROCEDURE spAPI_AuditLog_Insert
|
|
106
121
|
@Action nvarchar(100),
|
|
107
122
|
@sys_UserID varchar(9),
|
|
123
|
+
@sys_RemoteIP varchar(15),
|
|
108
124
|
@sys_header_User_Agent nvarchar(500),
|
|
109
125
|
@sys_header_X_Forwarded_For varchar(200)
|
|
110
126
|
AS
|
|
111
127
|
BEGIN
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
GETDATE());
|
|
128
|
+
SELECT @Action AS ActionName,
|
|
129
|
+
@sys_UserID AS UserID,
|
|
130
|
+
@sys_RemoteIP AS RemoteIP,
|
|
131
|
+
@sys_header_User_Agent AS UserAgent,
|
|
132
|
+
@sys_header_X_Forwarded_For AS ForwardedFor;
|
|
118
133
|
END
|
|
119
134
|
```
|
|
120
135
|
|
|
121
|
-
- **Still `@sys_*`:** auto-filled, **not** in `IN`, **no default** (`= null` is a hard error; `fui sp deploy` blocks it, see below).
|
|
136
|
+
- **Still recognized `@sys_*`:** auto-filled, **not** in `IN`, **no default** (`= null` is a hard error; `fui sp deploy` blocks it, see below).
|
|
122
137
|
- **Missing header → `NULL`** (not `''`) — normal, e.g. script/server-to-server calls often lack `User-Agent`. Wrap in `ISNULL(...)` at use: comparisons give UNKNOWN (`WHERE @sys_header_User_Agent <> 'bot'` **drops** the row), and `+` concatenation gives `NULL` (blanks the whole log string).
|
|
123
138
|
- **Untrusted (client-supplied, spoofable).** Log/stats only, never authorization; take rights from `@sys_UserID`/`@sys_SystemRight` (from the verified token).
|
|
124
139
|
|
|
@@ -53,7 +53,7 @@ Users name commands loosely, often in Vietnamese.
|
|
|
53
53
|
| "let someone edit this project" / "cấp quyền admin project", "thêm người vào project" | `fui user search` → `fui user add` |
|
|
54
54
|
| "who can edit this project" / "ai được sửa project này" | `fui user list` |
|
|
55
55
|
| "update project.json", "edit the project menu" / "sửa menu project" | edit `{projectId}/project.json`, then `fui project publish-config` |
|
|
56
|
-
| "connect the DB but I only know the alias" / "không có dbToken" | `fui db alias list` → `fui db add-by-name`
|
|
56
|
+
| "connect the DB but I only know the alias" / "không có dbToken" | `fui db list` → `fui db alias list` → `fui db add-by-name`; if no saved SQL login, supply `--uid` and `--pwd -` / `--pwd-env` |
|
|
57
57
|
| "sync / refresh the project" / "đồng bộ project", "lấy project.json" | `fui project sync` |
|
|
58
58
|
| "get / pull / clone / refresh the module" / "lấy module về", "pull mới nhất" | `fui module get` |
|
|
59
59
|
| "new module" / "tạo module mới" | `fui module new` |
|
|
@@ -79,7 +79,7 @@ Users name commands loosely, often in Vietnamese.
|
|
|
79
79
|
| "DB password changed" / "đổi password DB", "rebuild dbToken" | `fui db token-rebuild` |
|
|
80
80
|
| "which DBs does this project use" / "project này đang nối những DB nào" | `fui db list` |
|
|
81
81
|
| "connect one more database" / "nối thêm một database nữa" | `fui db add` again with the new apiName |
|
|
82
|
-
| **"sync the db", "refresh schema"** / **"đồng bộ db", "cập nhật schema", "làm mới schema"** | `fui
|
|
82
|
+
| **"sync the db", "refresh schema"** / **"đồng bộ db", "cập nhật schema", "làm mới schema"** | `fui db list` → existing `--db`: `fui schema pull`. No connection: `fui db alias list` → `fui db add-by-name` (automatically fetches schema) |
|
|
83
83
|
| "list designs" / "có style nào" · "use style X" / "dùng style X" | `fui design list` · `fui design read <brand>` |
|
|
84
84
|
|
|
85
85
|
## Commands
|
|
@@ -152,6 +152,7 @@ Scope, for every component command:
|
|
|
152
152
|
| `db list`, `db test`, `token decode` | R | ✅ | never print secrets |
|
|
153
153
|
| `db alias list` | R | ✅ | databases your token may manage on `acc`; run it when an alias is unknown or `db add-by-name` finds nothing |
|
|
154
154
|
| `db add`, `db add-by-name` | M | ✅ | test, save, fetch the schema. `add-by-name` borrows a SQL login from a connection on the same `apiDomain`; "not found" means the alias does not exist **or** you may not manage it |
|
|
155
|
+
| `db remove <project>/<connectionName>` | M | ⚠️ | removes only the named local connection and updates the default; server alias and cached `_db/<connectionName>/` files remain |
|
|
155
156
|
| `db user-token`, `db default`, `db migrate` | M | ✅ | local only |
|
|
156
157
|
| `db token-rebuild` | M | ⚠️ | overwrites the stored token; `--db` is required when the project has several connections |
|
|
157
158
|
| `db alias new` | M | ⚠️ | registers an existing database as a tAPI alias on `acc`; the login must be the **application** account (execute-only), never a `db_owner` dev login. Ask the user first |
|
|
@@ -199,7 +200,7 @@ Guarding with a right code that is not defined fails silently: `rightTest()` is
|
|
|
199
200
|
## Database rules (enforced in code)
|
|
200
201
|
|
|
201
202
|
- `query` / `exec`: say what the SQL does and what data it touches, and wait for the user. Writes (INSERT, UPDATE…SET, DELETE, MERGE, TRUNCATE, `SELECT … INTO` a real table, a write inside `EXEC('…')` or `sp_executesql`, `DROP PROCEDURE`/`FUNCTION`/`VIEW`, ALTER COLUMN) exit 3 until `--confirm-write`, added only after the user approved that exact write. It is a confirmation gate, not an obstacle to work around.
|
|
202
|
-
- Table DDL is judged by data loss, **per statement**: free `ALTER TABLE … ADD` and `DROP
|
|
203
|
+
- Table DDL is judged by data loss, **per statement**: free `ALTER TABLE … ADD`, `DROP CONSTRAINT`, and `DROP TABLE #name` for one local temporary table · `--confirm-write` for `ALTER COLUMN` (narrowing numeric or date precision loses data silently) · always blocked `DROP TABLE` on persistent or global (`##name`) tables, mixed drop lists, `DROP DATABASE`, `DROP COLUMN`, `SWITCH`, `xp_cmdshell` and any action fui cannot parse. A blocked statement wins over a confirmable one. `DELETE`/`UPDATE` without `WHERE` is blocked too, in `query` and `exec` (a `MERGE` is scoped by its `ON`). A `--` or a quote inside a `[name]` or `"name"` does not hide the statements after it. Show blocked SQL for the user to run by hand; no flag unlocks it.
|
|
203
204
|
- Never call a live API SP or run sub-queries "to test" it: read its body (`fui sp get`) and reason, or `fui sp verify <name|Fn>`. `sp verify` calls no endpoint; its only network reads are metadata (`OBJECT_DEFINITION` when no local `.sql`, permissions with `--check-grant`). An unreadable body is a hard error: it never guesses from the name.
|
|
204
205
|
- `--mocks` for an SP that **exists** must be the snippet `fui sp verify` prints, never a guessed shape. On `🚨 UNVERIFIED MOCK SHAPE` stop, run `fui sp verify`, copy the snippet. Invented data is right only for an SP that does not exist yet.
|
|
205
206
|
- `fui sp deploy` asks the server whether the object exists, so `CREATE PROCEDURE` for an existing SP is fine: do not edit the file to `ALTER` and redeploy. tAPI parameters with a default value are hard-blocked.
|
|
@@ -119,7 +119,8 @@ const LAYOUT = {
|
|
|
119
119
|
lineHeight: 1.4, // hệ số quy cỡ chữ → chiều cao dòng thực tế
|
|
120
120
|
legendRowHeight: 24, // một hàng legend (chữ + đệm của item); config.legendRows nếu nhiều hàng
|
|
121
121
|
axisLabelBand: 26, // nhãn trục hoành vẽ NGOÀI grid, phải chừa thêm ở dưới
|
|
122
|
-
axisTopBand: 10
|
|
122
|
+
axisTopBand: 10, // nửa chữ của nhãn trục tung cao nhất nhô lên trên mép grid
|
|
123
|
+
seriesLabelBand: 18 // label của bar/line có thể nhô lên trên grid (font 12px × line-height 1.4)
|
|
123
124
|
};
|
|
124
125
|
|
|
125
126
|
|
|
@@ -656,10 +657,34 @@ Vue.component('f-echart', {
|
|
|
656
657
|
return (hasText ? line(fs) : 0) + (hasSub ? gap + line(sfs) : 0);
|
|
657
658
|
},
|
|
658
659
|
|
|
660
|
+
// Đổi inset dạng px / '%' về px để có thể kiểm tra khoảng hở tối thiểu.
|
|
661
|
+
// Các từ khoá ngữ nghĩa như 'middle' không thể quy đổi an toàn nên trả null.
|
|
662
|
+
insetPx(value) {
|
|
663
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
664
|
+
if (typeof value === 'string') {
|
|
665
|
+
const s = value.trim();
|
|
666
|
+
if (/^-?\d+(\.\d+)?px$/.test(s)) return parseFloat(s);
|
|
667
|
+
if (/^-?\d+(\.\d+)?%$/.test(s)) {
|
|
668
|
+
const rawHeight = (this.$el && this.$el.clientHeight) || this.chartHeight || 0;
|
|
669
|
+
const h = typeof rawHeight === 'number' ? rawHeight : parseFloat(rawHeight);
|
|
670
|
+
return h ? h * parseFloat(s) / 100 : null;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
return null;
|
|
674
|
+
},
|
|
675
|
+
|
|
676
|
+
// Đặt block không sớm hơn con trỏ hiện tại. Nhờ vậy top: 0 / '2%' của
|
|
677
|
+
// module không thể kéo legend hoặc vùng vẽ đè lên block đứng trước nó.
|
|
678
|
+
enforceInset(block, edge, minimum) {
|
|
679
|
+
const px = this.insetPx(block[edge]);
|
|
680
|
+
if (block[edge] == null || (px != null && px < minimum)) block[edge] = minimum;
|
|
681
|
+
return px == null ? minimum : Math.max(px, minimum);
|
|
682
|
+
},
|
|
683
|
+
|
|
659
684
|
// Xếp chỗ theo chiều dọc: đi từ mép trên xuống và từ mép dưới lên, mỗi khối
|
|
660
685
|
// hiện diện lấy đúng chiều cao của mình + một `gap`; phần chưa ai lấy là vùng vẽ.
|
|
661
|
-
// Giá trị do module tự khai
|
|
662
|
-
//
|
|
686
|
+
// Giá trị do module tự khai được giữ khi đủ rộng; nếu quá nhỏ thì được nâng lên
|
|
687
|
+
// mức tối thiểu để không block nào phá gap 12px với block đứng trước.
|
|
663
688
|
applyLayout(opt, hints) {
|
|
664
689
|
const P = LAYOUT.padding, G = LAYOUT.gap;
|
|
665
690
|
const title = Array.isArray(opt.title) ? opt.title[0] : opt.title;
|
|
@@ -668,9 +693,14 @@ Vue.component('f-echart', {
|
|
|
668
693
|
|
|
669
694
|
const titleH = this.measureTitle(title);
|
|
670
695
|
if (titleH) {
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
696
|
+
const titleAnchor = title.bottom != null && title.top == null ? 'bottom' : 'top';
|
|
697
|
+
if (titleAnchor === 'bottom') {
|
|
698
|
+
const y = this.enforceInset(title, 'bottom', bottom);
|
|
699
|
+
bottom = y + titleH + G;
|
|
700
|
+
} else {
|
|
701
|
+
const y = this.enforceInset(title, 'top', top);
|
|
702
|
+
top = y + titleH + G;
|
|
703
|
+
}
|
|
674
704
|
}
|
|
675
705
|
|
|
676
706
|
if (legend && legend.show !== false) {
|
|
@@ -679,11 +709,11 @@ Vue.component('f-echart', {
|
|
|
679
709
|
: legend.bottom != null ? 'bottom'
|
|
680
710
|
: (hints.legendAnchor || 'top');
|
|
681
711
|
if (anchor === 'top') {
|
|
682
|
-
|
|
683
|
-
top =
|
|
712
|
+
const y = this.enforceInset(legend, 'top', top);
|
|
713
|
+
top = y + legendH + G;
|
|
684
714
|
} else {
|
|
685
|
-
|
|
686
|
-
bottom =
|
|
715
|
+
const y = this.enforceInset(legend, 'bottom', bottom);
|
|
716
|
+
bottom = y + legendH + G;
|
|
687
717
|
}
|
|
688
718
|
}
|
|
689
719
|
|
|
@@ -700,8 +730,22 @@ Vue.component('f-echart', {
|
|
|
700
730
|
if (Array.isArray(opt.grid)) return; // multi-grid: module tự lo bố cục
|
|
701
731
|
const dg = _.isPlainObject(declared.grid) ? declared.grid : {};
|
|
702
732
|
const g = _.isPlainObject(opt.grid) ? opt.grid : (opt.grid = {});
|
|
703
|
-
|
|
704
|
-
|
|
733
|
+
const series = Array.isArray(opt.series) ? opt.series : (opt.series ? [opt.series] : []);
|
|
734
|
+
const hasOuterTopLabel = series.some(s => {
|
|
735
|
+
if (!s || !s.label || s.label.show !== true) return false;
|
|
736
|
+
const pos = s.label.position;
|
|
737
|
+
return pos == null || pos === 'top' || pos === 'outside';
|
|
738
|
+
});
|
|
739
|
+
const topBand = Math.max(LAYOUT.axisTopBand, hasOuterTopLabel ? LAYOUT.seriesLabelBand : 0);
|
|
740
|
+
const minTop = Math.max(LAYOUT.gridTop, top + topBand);
|
|
741
|
+
const minBottom = Math.max(LAYOUT.gridBottom, bottom + LAYOUT.axisLabelBand);
|
|
742
|
+
|
|
743
|
+
// Custom grid vẫn được giữ nếu rộng hơn; nếu quá sát thì nâng lên mức tối thiểu.
|
|
744
|
+
// Đây là điểm trước kia làm chart có grid.top nhỏ phá vỡ gap 12px.
|
|
745
|
+
const declaredTop = this.insetPx(dg.top);
|
|
746
|
+
const declaredBottom = this.insetPx(dg.bottom);
|
|
747
|
+
g.top = declaredTop == null ? minTop : Math.max(declaredTop, minTop);
|
|
748
|
+
g.bottom = declaredBottom == null ? minBottom : Math.max(declaredBottom, minBottom);
|
|
705
749
|
if (dg.left == null) g.left = LAYOUT.gridLeft;
|
|
706
750
|
if (dg.right == null) g.right = LAYOUT.gridRight;
|
|
707
751
|
return;
|