logisheets-core 1.8.0 → 1.9.0

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 ADDED
@@ -0,0 +1,72 @@
1
+ # logisheets-core
2
+
3
+ UI-free LogiSheets logic — the portable core shared by every host. It holds all
4
+ the high-level workbook operations (styling, blocks, fields, validation, crafts,
5
+ transactions, permissions) with **no rendering and no runtime of its own**, so
6
+ the exact same code runs in the browser app and in a headless Node runtime.
7
+
8
+ It depends on [`logisheets-web`](https://www.npmjs.com/package/logisheets-web)
9
+ for **types only** — the concrete engine `Client` is injected by the host:
10
+
11
+ - the **browser app** injects the worker-backed client from `logisheets-engine`;
12
+ - [`logisheets-runtime`](https://www.npmjs.com/package/logisheets-runtime)
13
+ injects a synchronous client built on the Node WASM engine.
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ npm install logisheets-core logisheets-web
19
+ ```
20
+
21
+ `logisheets-web` is a peer dependency (used for types). This package is
22
+ ESM-only.
23
+
24
+ ## Usage
25
+
26
+ You rarely construct the engine `Client` yourself — a host normally provides it.
27
+ Given a `Client`, `WorkbookOps` is the high-level operation layer:
28
+
29
+ ```ts
30
+ import {WorkbookOps} from 'logisheets-core'
31
+ import type {Client} from 'logisheets-web'
32
+
33
+ declare const client: Client // supplied by the host (browser worker or Node)
34
+
35
+ const ops = new WorkbookOps(client)
36
+
37
+ await ops.inputCell(0, 0, 0, 'Hello') // sheet 0, cell A1
38
+ await ops.createSheet(1, 'Summary')
39
+ await ops.setSheetColor(0, '#4472C4')
40
+ ```
41
+
42
+ `WorkbookOps` covers cell input (including block cells and cell images), sheet
43
+ management (create / delete / rename / color), block operations (move, remove,
44
+ insert rows), and style payload generation (borders, fills, alignment, wrap).
45
+
46
+ ## Subpath exports
47
+
48
+ Focused utilities are available without pulling in the whole surface:
49
+
50
+ | Import | Contents |
51
+ |--------|----------|
52
+ | `logisheets-core` | Full surface: ops, format, crafts, validation, fields, values, transactions, permissions |
53
+ | `logisheets-core/strings` | String helpers |
54
+ | `logisheets-core/type-guard` | Runtime type guards for engine types |
55
+ | `logisheets-core/selection` | Selection helpers |
56
+ | `logisheets-core/utils` | General utilities |
57
+ | `logisheets-core/value` | Cell-value helpers |
58
+
59
+ ## Where it fits
60
+
61
+ ```
62
+ logisheets-web / logisheets (WASM engine, per host)
63
+ │ Client (types only)
64
+
65
+ logisheets-core ← you are here (UI-free logic)
66
+ ┌───────┴────────┐
67
+ Browser App logisheets-runtime (Node)
68
+ ```
69
+
70
+ ## License
71
+
72
+ MIT — part of the [LogiSheets](https://github.com/logisky/LogiSheets) project.
@@ -83,6 +83,28 @@ export declare class WorkbookOps {
83
83
  * clear (see the drag-to-move overlay, which cancels on collision).
84
84
  */
85
85
  moveBlock(sheetIdx: number, blockId: number, newMasterRow: number, newMasterCol: number): Promise<ActionEffect>;
86
+ /**
87
+ * Sort a block's records by the named field. Rows are reordered for a
88
+ * row-schema block, columns for a col-schema block. The engine computes a
89
+ * type-aware order (numbers numerically, text lexicographically, blanks
90
+ * last); the reorder is one undoable transaction.
91
+ *
92
+ * Throws for random-schema blocks (no fields) or an unknown field name.
93
+ */
94
+ sortBlock(sheetIdx: number, blockId: number, field: string, asc?: boolean, undoable?: boolean): Promise<ActionEffect>;
95
+ /**
96
+ * Move a single block line (column or row) from one position to another —
97
+ * positional `remove(from)` then `insert(to)` on the block's line order
98
+ * (`MoveBlockLine`). For a row-schema form block, a FIELD is a column, so
99
+ * reordering fields is `isRow = false` with block-inner column indices.
100
+ *
101
+ * `to` is the index in the post-removal frame (matching the engine's
102
+ * `move_line`). The field's stable line id keeps its cell data, and the
103
+ * schema's per-field `idx` is re-derived from the new position on the next
104
+ * read — so both data and field order follow the move automatically; no
105
+ * re-`bindFormSchema` is needed.
106
+ */
107
+ moveBlockLine(sheetIdx: number, blockId: number, from: number, to: number, isRow?: boolean, undoable?: boolean): Promise<ActionEffect>;
86
108
  /** Apply font styling (bold/italic/underline/strike/color/size). */
87
109
  setFont(sheetIdx: number, data: SelectedData, update: FontStyle): Promise<void>;
88
110
  /** Apply horizontal/vertical alignment. */
@@ -137,6 +159,35 @@ export declare class WorkbookOps {
137
159
  keyIdx: number;
138
160
  fields: readonly FormBlockField[];
139
161
  }): Promise<void>;
162
+ /**
163
+ * Edit an EXISTING form-backed block: rename it, re-type / re-formula its
164
+ * existing fields, and/or append new fields — in one transaction. The v1
165
+ * contract is **fields are never removed**, so the column count is
166
+ * monotonically non-decreasing: `fields.length >= currentColCnt`. That
167
+ * keeps this safe with only a *tail* `resizeBlock` (new columns are
168
+ * appended, existing field columns — and the cells/formulas that
169
+ * reference them — are untouched, and no schema entry is ever orphaned).
170
+ *
171
+ * `fields` is the FULL field list (existing fields first, in their
172
+ * current order, followed by any newly-added fields). Existing fields
173
+ * MUST keep their original `renderId` so the block's cells stay wired to
174
+ * their render/type metadata; the host reconstructs the list from
175
+ * `BlockInfo.schema.fields[i].renderId`.
176
+ *
177
+ * Order matters: the `resizeBlock` runs BEFORE `bindFormSchema` so the
178
+ * appended field columns exist when the schema binds to them. Validation
179
+ * / editability formulas are carried on `FieldInfo` (the field type) via
180
+ * the host's FieldManager, same as `createFormBlock`, so this only needs
181
+ * to round-trip each field's value-formula template.
182
+ */
183
+ editFormBlock(opts: {
184
+ sheetIdx: number;
185
+ blockId: number;
186
+ currentColCnt: number;
187
+ refName: string;
188
+ keyIdx: number;
189
+ fields: readonly FormBlockField[];
190
+ }): Promise<void>;
140
191
  /**
141
192
  * Apply a caller-built payload list as one transaction (at the host's
142
193
  * temp-mode). Escape hatch for operations whose payload construction still
package/dist/ops/index.js CHANGED
@@ -140,6 +140,56 @@ export class WorkbookOps {
140
140
  },
141
141
  ], true);
142
142
  }
143
+ /**
144
+ * Sort a block's records by the named field. Rows are reordered for a
145
+ * row-schema block, columns for a col-schema block. The engine computes a
146
+ * type-aware order (numbers numerically, text lexicographically, blanks
147
+ * last); the reorder is one undoable transaction.
148
+ *
149
+ * Throws for random-schema blocks (no fields) or an unknown field name.
150
+ */
151
+ async sortBlock(sheetIdx, blockId, field, asc = true, undoable = true) {
152
+ const order = await this.client.getBlockSortOrder({
153
+ sheetIdx,
154
+ blockId,
155
+ field,
156
+ asc,
157
+ });
158
+ if (isErrorMessage(order)) {
159
+ throw new Error('Sort failed: ' + order.msg);
160
+ }
161
+ return this.apply([
162
+ {
163
+ type: 'reorderBlockLines',
164
+ value: {
165
+ sheetIdx,
166
+ blockId,
167
+ isRow: order.isRow,
168
+ newOrder: order.newOrder,
169
+ },
170
+ },
171
+ ], undoable);
172
+ }
173
+ /**
174
+ * Move a single block line (column or row) from one position to another —
175
+ * positional `remove(from)` then `insert(to)` on the block's line order
176
+ * (`MoveBlockLine`). For a row-schema form block, a FIELD is a column, so
177
+ * reordering fields is `isRow = false` with block-inner column indices.
178
+ *
179
+ * `to` is the index in the post-removal frame (matching the engine's
180
+ * `move_line`). The field's stable line id keeps its cell data, and the
181
+ * schema's per-field `idx` is re-derived from the new position on the next
182
+ * read — so both data and field order follow the move automatically; no
183
+ * re-`bindFormSchema` is needed.
184
+ */
185
+ moveBlockLine(sheetIdx, blockId, from, to, isRow = false, undoable = true) {
186
+ return this.apply([
187
+ {
188
+ type: 'moveBlockLine',
189
+ value: { sheetIdx, blockId, from, to, isRow },
190
+ },
191
+ ], undoable);
192
+ }
143
193
  // ---- formatting -----------------------------------------------------
144
194
  //
145
195
  // Each method turns the current sheet + selection into style-update
@@ -274,6 +324,71 @@ export class WorkbookOps {
274
324
  ];
275
325
  await this.apply(payloads, true);
276
326
  }
327
+ /**
328
+ * Edit an EXISTING form-backed block: rename it, re-type / re-formula its
329
+ * existing fields, and/or append new fields — in one transaction. The v1
330
+ * contract is **fields are never removed**, so the column count is
331
+ * monotonically non-decreasing: `fields.length >= currentColCnt`. That
332
+ * keeps this safe with only a *tail* `resizeBlock` (new columns are
333
+ * appended, existing field columns — and the cells/formulas that
334
+ * reference them — are untouched, and no schema entry is ever orphaned).
335
+ *
336
+ * `fields` is the FULL field list (existing fields first, in their
337
+ * current order, followed by any newly-added fields). Existing fields
338
+ * MUST keep their original `renderId` so the block's cells stay wired to
339
+ * their render/type metadata; the host reconstructs the list from
340
+ * `BlockInfo.schema.fields[i].renderId`.
341
+ *
342
+ * Order matters: the `resizeBlock` runs BEFORE `bindFormSchema` so the
343
+ * appended field columns exist when the schema binds to them. Validation
344
+ * / editability formulas are carried on `FieldInfo` (the field type) via
345
+ * the host's FieldManager, same as `createFormBlock`, so this only needs
346
+ * to round-trip each field's value-formula template.
347
+ */
348
+ async editFormBlock(opts) {
349
+ const { sheetIdx, blockId, currentColCnt, refName, keyIdx, fields } = opts;
350
+ const newColCnt = fields.length;
351
+ if (newColCnt < currentColCnt) {
352
+ throw new Error(`editFormBlock cannot remove fields: got ${newColCnt} field(s) ` +
353
+ `for a block with ${currentColCnt} column(s).`);
354
+ }
355
+ const payloads = [];
356
+ if (newColCnt !== currentColCnt) {
357
+ payloads.push({
358
+ type: 'resizeBlock',
359
+ value: {
360
+ sheetIdx,
361
+ id: blockId,
362
+ newColCnt,
363
+ },
364
+ });
365
+ }
366
+ payloads.push({
367
+ type: 'bindFormSchema',
368
+ value: {
369
+ refName,
370
+ sheetIdx,
371
+ blockId,
372
+ fieldFrom: 0,
373
+ row: true,
374
+ keyIdx: keyIdx < 0 ? 0 : keyIdx,
375
+ fields: fields.map((f) => f.name),
376
+ renderIds: fields.map((f) => f.renderId),
377
+ fieldFormulas: fields.map((f) => f.valueFormula ?? ''),
378
+ validationFormulas: [],
379
+ editabilityFormulas: [],
380
+ },
381
+ });
382
+ payloads.push(...fields.map((f) => ({
383
+ type: 'upsertFieldRenderInfo',
384
+ value: {
385
+ renderId: f.renderId,
386
+ diyRender: f.diyRender,
387
+ styleUpdate: { setNumFmt: f.numFmt ?? '' },
388
+ },
389
+ })));
390
+ await this.apply(payloads, true);
391
+ }
277
392
  // ---- generic / temp-branch -----------------------------------------
278
393
  /**
279
394
  * Apply a caller-built payload list as one transaction (at the host's
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "logisheets-core",
3
- "version": "1.8.0",
3
+ "version": "1.9.0",
4
4
  "description": "UI-free LogiSheets logic — runs in the browser or on Node. Depends on logisheets-web only for types; the engine Client is injected by the host.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -44,11 +44,11 @@
44
44
  "author": "Jeremy He",
45
45
  "license": "MIT",
46
46
  "peerDependencies": {
47
- "logisheets-web": "^1.8.0"
47
+ "logisheets-web": "^1.9.0"
48
48
  },
49
49
  "devDependencies": {
50
50
  "logisheets-web": "workspace:*",
51
51
  "typescript": "^5.5.0",
52
- "vitest": "^2.0.0"
52
+ "vitest": "3.2.6"
53
53
  }
54
54
  }