logisheets-core 1.1.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/dist/craft-interactions/index.d.ts +140 -0
- package/dist/craft-interactions/index.js +462 -0
- package/dist/field/index.d.ts +51 -0
- package/dist/field/index.js +70 -0
- package/dist/format/index.d.ts +21 -0
- package/dist/format/index.js +454 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +17 -0
- package/dist/ops/index.d.ts +155 -0
- package/dist/ops/index.js +335 -0
- package/dist/permissions/index.d.ts +32 -0
- package/dist/permissions/index.js +74 -0
- package/dist/port.d.ts +9 -0
- package/dist/port.js +14 -0
- package/dist/selection/index.d.ts +1 -0
- package/dist/selection/index.js +1 -0
- package/dist/selection/model.d.ts +10 -0
- package/dist/selection/model.js +38 -0
- package/dist/strings/case.d.ts +2 -0
- package/dist/strings/case.js +6 -0
- package/dist/strings/char-code.d.ts +422 -0
- package/dist/strings/char-code.js +424 -0
- package/dist/strings/common_length.d.ts +4 -0
- package/dist/strings/common_length.js +28 -0
- package/dist/strings/contain.d.ts +4 -0
- package/dist/strings/contain.js +60 -0
- package/dist/strings/index.d.ts +5 -0
- package/dist/strings/index.js +5 -0
- package/dist/strings/judges.d.ts +3 -0
- package/dist/strings/judges.js +38 -0
- package/dist/strings/surrogate.d.ts +16 -0
- package/dist/strings/surrogate.js +30 -0
- package/dist/structured/index.d.ts +41 -0
- package/dist/structured/index.js +50 -0
- package/dist/transaction/index.d.ts +8 -0
- package/dist/transaction/index.js +11 -0
- package/dist/type-guard/index.d.ts +3 -0
- package/dist/type-guard/index.js +3 -0
- package/dist/type-guard/propterty.d.ts +1 -0
- package/dist/type-guard/propterty.js +3 -0
- package/dist/type-guard/string.d.ts +1 -0
- package/dist/type-guard/string.js +3 -0
- package/dist/type-guard/u8.d.ts +1 -0
- package/dist/type-guard/u8.js +3 -0
- package/dist/utils/a1notation.d.ts +18 -0
- package/dist/utils/a1notation.js +76 -0
- package/dist/utils/array.d.ts +2 -0
- package/dist/utils/array.js +12 -0
- package/dist/utils/clone.d.ts +2 -0
- package/dist/utils/clone.js +25 -0
- package/dist/utils/const.d.ts +7 -0
- package/dist/utils/const.js +8 -0
- package/dist/utils/equal.d.ts +1 -0
- package/dist/utils/equal.js +11 -0
- package/dist/utils/index.d.ts +6 -0
- package/dist/utils/index.js +6 -0
- package/dist/utils/uuid.d.ts +1 -0
- package/dist/utils/uuid.js +4 -0
- package/dist/validation/index.d.ts +34 -0
- package/dist/validation/index.js +60 -0
- package/dist/value/index.d.ts +9 -0
- package/dist/value/index.js +49 -0
- package/package.json +54 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import type { Payload, ActionEffect, SelectedData, Alignment, StPatternType, Value } from 'logisheets-web';
|
|
2
|
+
import type { Client } from '../port.js';
|
|
3
|
+
import { type ValidationRule, type Violation } from '../validation/index.js';
|
|
4
|
+
import { type FieldColumn } from '../field/index.js';
|
|
5
|
+
import { type FontStyle, type BorderBatchUpdate } from '../format/index.js';
|
|
6
|
+
/**
|
|
7
|
+
* Tells WorkbookOps whether to mark transactions temp (speculative). The
|
|
8
|
+
* browser injects its global temp-mode toggle; the Node runtime leaves it at
|
|
9
|
+
* the default (always committed).
|
|
10
|
+
*/
|
|
11
|
+
export type TempModeProvider = () => boolean;
|
|
12
|
+
/** One field of a form-backed block, resolved by the host. */
|
|
13
|
+
export interface FormBlockField {
|
|
14
|
+
/** Display name. */
|
|
15
|
+
name: string;
|
|
16
|
+
/** Engine render id, allocated by the host's FieldManager. */
|
|
17
|
+
renderId: string;
|
|
18
|
+
/** Per-field value-formula template (#FIELD("X") / #KEY); '' if free-form. */
|
|
19
|
+
valueFormula?: string;
|
|
20
|
+
/** Whether the field renders via a host-drawn (DIY) overlay. */
|
|
21
|
+
diyRender: boolean;
|
|
22
|
+
/** Number format applied to the field's render info. */
|
|
23
|
+
numFmt?: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* High-level workbook operations bound to one engine {@link Client}.
|
|
27
|
+
* Construct one per workbook and share it across the host.
|
|
28
|
+
*/
|
|
29
|
+
export declare class WorkbookOps {
|
|
30
|
+
private readonly client;
|
|
31
|
+
private readonly tempMode;
|
|
32
|
+
/** Monotonic id for the throwaway ephemeral cells used by evalFormula. */
|
|
33
|
+
private ephemeralSeq;
|
|
34
|
+
constructor(client: Client, tempMode?: TempModeProvider);
|
|
35
|
+
/**
|
|
36
|
+
* Build a transaction at the host's current temp-mode, send it, and return
|
|
37
|
+
* the engine's effect. Throws on an engine ErrorMessage so callers can use
|
|
38
|
+
* normal try/catch instead of inspecting a union.
|
|
39
|
+
*/
|
|
40
|
+
private apply;
|
|
41
|
+
/** Write a value or formula into a cell. */
|
|
42
|
+
inputCell(sheetIdx: number, row: number, col: number, content: string, undoable?: boolean): Promise<ActionEffect>;
|
|
43
|
+
/** Write a value into a cell addressed within a block's coordinate space. */
|
|
44
|
+
inputBlockCell(sheetIdx: number, blockId: number, row: number, col: number, input: string, undoable?: boolean): Promise<ActionEffect>;
|
|
45
|
+
/**
|
|
46
|
+
* Write several block cells in one transaction. Used when an interaction
|
|
47
|
+
* changes a group of related cells at once (e.g. redistributing a percent
|
|
48
|
+
* allocation across a pool).
|
|
49
|
+
*/
|
|
50
|
+
inputBlockCells(inputs: ReadonlyArray<{
|
|
51
|
+
sheetIdx: number;
|
|
52
|
+
blockId: number;
|
|
53
|
+
row: number;
|
|
54
|
+
col: number;
|
|
55
|
+
input: string;
|
|
56
|
+
}>, undoable?: boolean): Promise<ActionEffect>;
|
|
57
|
+
/**
|
|
58
|
+
* Set a cell's content to an image URL, optionally widening the column
|
|
59
|
+
* and/or heightening the row to fit — all in one transaction. The host
|
|
60
|
+
* computes the target `colWidth`/`rowHeight` (in engine units) and passes
|
|
61
|
+
* them only when an adjustment is actually needed.
|
|
62
|
+
*/
|
|
63
|
+
setCellImage(sheetIdx: number, row: number, col: number, url: string, opts?: {
|
|
64
|
+
colWidth?: number;
|
|
65
|
+
rowHeight?: number;
|
|
66
|
+
}): Promise<ActionEffect>;
|
|
67
|
+
/** Create a new sheet named `name` at index `idx`. */
|
|
68
|
+
createSheet(name: string, idx: number): Promise<ActionEffect>;
|
|
69
|
+
/** Rename a sheet. */
|
|
70
|
+
renameSheet(oldName: string, newName: string): Promise<ActionEffect>;
|
|
71
|
+
/** Delete the sheet at index `idx`. */
|
|
72
|
+
deleteSheet(idx: number): Promise<ActionEffect>;
|
|
73
|
+
/** Set a sheet tab's color (ARGB string; empty clears it). */
|
|
74
|
+
setSheetColor(idx: number, color: string): Promise<ActionEffect>;
|
|
75
|
+
/** Insert `cnt` rows into a block, starting at block-row `start`. */
|
|
76
|
+
insertRowsInBlock(sheetIdx: number, blockId: number, start: number, cnt?: number): Promise<ActionEffect>;
|
|
77
|
+
/** Delete a block. */
|
|
78
|
+
removeBlock(sheetIdx: number, blockId: number): Promise<ActionEffect>;
|
|
79
|
+
/** Apply font styling (bold/italic/underline/strike/color/size). */
|
|
80
|
+
setFont(sheetIdx: number, data: SelectedData, update: FontStyle): Promise<void>;
|
|
81
|
+
/** Apply horizontal/vertical alignment. */
|
|
82
|
+
setAlignment(sheetIdx: number, data: SelectedData, alignment: Alignment): Promise<void>;
|
|
83
|
+
/** Toggle wrap-text. */
|
|
84
|
+
setWrapText(sheetIdx: number, data: SelectedData, wrapText: boolean): Promise<void>;
|
|
85
|
+
/** Apply a number format. */
|
|
86
|
+
setNumFmt(sheetIdx: number, data: SelectedData, numFmt: string): Promise<void>;
|
|
87
|
+
/** Apply a pattern fill (foreground/background color + pattern). */
|
|
88
|
+
setPatternFill(sheetIdx: number, data: SelectedData, opts: {
|
|
89
|
+
fgColor?: string;
|
|
90
|
+
bgColor?: string;
|
|
91
|
+
pattern?: StPatternType;
|
|
92
|
+
}): Promise<void>;
|
|
93
|
+
/** Apply borders to the selection per the batch directive. */
|
|
94
|
+
setBorder(sheetIdx: number, data: SelectedData, update: BorderBatchUpdate): Promise<void>;
|
|
95
|
+
/** Apply a generated payload list, skipping the round-trip when empty. */
|
|
96
|
+
private applyGenerated;
|
|
97
|
+
/**
|
|
98
|
+
* Create a form-backed block: the `createBlock` + `bindFormSchema` +
|
|
99
|
+
* per-field `upsertFieldRenderInfo` payloads, in one transaction.
|
|
100
|
+
*
|
|
101
|
+
* The host resolves each field first (type → FieldInfo, validation
|
|
102
|
+
* formulas, and the engine render id from its FieldManager), then hands
|
|
103
|
+
* the flattened list here. Field/formula composition and FieldManager
|
|
104
|
+
* registration stay in the host because they touch engine-side render
|
|
105
|
+
* state that isn't part of the Client seam.
|
|
106
|
+
*/
|
|
107
|
+
createFormBlock(opts: {
|
|
108
|
+
sheetIdx: number;
|
|
109
|
+
blockId: number;
|
|
110
|
+
masterRow: number;
|
|
111
|
+
masterCol: number;
|
|
112
|
+
refName: string;
|
|
113
|
+
keyIdx: number;
|
|
114
|
+
fields: readonly FormBlockField[];
|
|
115
|
+
}): Promise<void>;
|
|
116
|
+
/**
|
|
117
|
+
* Apply a caller-built payload list as one transaction (at the host's
|
|
118
|
+
* temp-mode). Escape hatch for operations whose payload construction still
|
|
119
|
+
* lives in the host — e.g. the toolbar's format/border generators. Prefer
|
|
120
|
+
* a named method above when one exists; this exists so no caller has to
|
|
121
|
+
* reach past WorkbookOps to `client.handleTransaction` directly.
|
|
122
|
+
*/
|
|
123
|
+
applyPayloads(payloads: readonly Payload[], undoable?: boolean): Promise<ActionEffect>;
|
|
124
|
+
/** Commit the workbook's temp (speculative) branch into the main branch. */
|
|
125
|
+
commitTempStatus(): Promise<void>;
|
|
126
|
+
/** Discard the workbook's temp (speculative) branch. */
|
|
127
|
+
cleanupTempStatus(): Promise<void>;
|
|
128
|
+
/**
|
|
129
|
+
* Establish (or refresh) the validation rule for a cell.
|
|
130
|
+
*
|
|
131
|
+
* A validation rule is an Excel formula (no leading `=`) that should
|
|
132
|
+
* evaluate to a boolean. We park it in the cell's *shadow* cell so the
|
|
133
|
+
* engine evaluates it reactively; the host reads the shadow value back and
|
|
134
|
+
* renders the result (see logisheets-core's `interpretValidation`).
|
|
135
|
+
*
|
|
136
|
+
* Lifted verbatim out of the browser's ValidationCell component so the
|
|
137
|
+
* Node runtime gets the same operation.
|
|
138
|
+
*/
|
|
139
|
+
setValidationRule(sheetIdx: number, row: number, col: number, formula: string): Promise<void>;
|
|
140
|
+
/**
|
|
141
|
+
* Evaluate an Excel formula (no leading `=`) in a sheet and return its
|
|
142
|
+
* Value. Parks the formula in a throwaway ephemeral cell, reads the result
|
|
143
|
+
* back, and leaves no committed change — the same mechanism the browser
|
|
144
|
+
* uses for shadow cells, here for one-shot evaluation.
|
|
145
|
+
*/
|
|
146
|
+
evalFormula(sheetIdx: number, formula: string): Promise<Value>;
|
|
147
|
+
/**
|
|
148
|
+
* Evaluate formula-based validation rules and return the violating cells.
|
|
149
|
+
* Headless batch check — the per-cell browser path uses setValidationRule +
|
|
150
|
+
* interpretValidation instead.
|
|
151
|
+
*/
|
|
152
|
+
checkValidations(rules: readonly ValidationRule[]): Promise<Violation[]>;
|
|
153
|
+
/** Check required / unique / membership field constraints. */
|
|
154
|
+
checkFieldConstraints(columns: readonly FieldColumn[]): Promise<Violation[]>;
|
|
155
|
+
}
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
// WorkbookOps — the operation layer.
|
|
2
|
+
//
|
|
3
|
+
// This is the single home for high-level, engine-neutral workbook
|
|
4
|
+
// *operations* (input a cell, add a sheet, insert a block row, set a
|
|
5
|
+
// validation rule, ...). Each operation is an async method that does the full
|
|
6
|
+
// orchestration — resolve ids, build payloads, send the transaction, surface
|
|
7
|
+
// errors — on top of the injected {@link Client} seam (see ../port).
|
|
8
|
+
//
|
|
9
|
+
// Both hosts are thin shells over this layer:
|
|
10
|
+
// - browser app -> injects logisheets-engine's worker-backed client
|
|
11
|
+
// - node runtime -> injects an async client built over the Node WASM engine
|
|
12
|
+
//
|
|
13
|
+
// Operations are written async against the full Client. The browser client is
|
|
14
|
+
// already async; the Node runtime adapts its synchronous handle() into an
|
|
15
|
+
// async Client, so this one implementation runs unchanged on both.
|
|
16
|
+
//
|
|
17
|
+
// UI side effects (refocus, toasts, event buses, closing dialogs) stay in the
|
|
18
|
+
// host — only the engine-facing operation lives here.
|
|
19
|
+
import { makeTransaction } from '../transaction/index.js';
|
|
20
|
+
import { checkValidations as checkValidationsPure, } from '../validation/index.js';
|
|
21
|
+
import { checkFieldConstraints as checkFieldConstraintsPure, } from '../field/index.js';
|
|
22
|
+
import { generateFontPayload, generateAlgnmentPayload, generateWrapTextPayload, generateNumFmtPayload, generatePatternFillPayload, generateBorderPayloads, } from '../format/index.js';
|
|
23
|
+
function isErrorMessage(v) {
|
|
24
|
+
return (typeof v === 'object' &&
|
|
25
|
+
v !== null &&
|
|
26
|
+
'msg' in v);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* High-level workbook operations bound to one engine {@link Client}.
|
|
30
|
+
* Construct one per workbook and share it across the host.
|
|
31
|
+
*/
|
|
32
|
+
export class WorkbookOps {
|
|
33
|
+
constructor(client, tempMode = () => false) {
|
|
34
|
+
this.client = client;
|
|
35
|
+
this.tempMode = tempMode;
|
|
36
|
+
/** Monotonic id for the throwaway ephemeral cells used by evalFormula. */
|
|
37
|
+
this.ephemeralSeq = 1;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Build a transaction at the host's current temp-mode, send it, and return
|
|
41
|
+
* the engine's effect. Throws on an engine ErrorMessage so callers can use
|
|
42
|
+
* normal try/catch instead of inspecting a union.
|
|
43
|
+
*/
|
|
44
|
+
async apply(payloads, undoable) {
|
|
45
|
+
const transaction = makeTransaction(payloads, undoable, this.tempMode());
|
|
46
|
+
const res = await this.client.handleTransaction({ transaction });
|
|
47
|
+
if (isErrorMessage(res)) {
|
|
48
|
+
throw new Error('Transaction failed: ' + res.msg);
|
|
49
|
+
}
|
|
50
|
+
return res;
|
|
51
|
+
}
|
|
52
|
+
// ---- cell / block input --------------------------------------------
|
|
53
|
+
/** Write a value or formula into a cell. */
|
|
54
|
+
inputCell(sheetIdx, row, col, content, undoable = true) {
|
|
55
|
+
return this.apply([{ type: 'cellInput', value: { sheetIdx, row, col, content } }], undoable);
|
|
56
|
+
}
|
|
57
|
+
/** Write a value into a cell addressed within a block's coordinate space. */
|
|
58
|
+
inputBlockCell(sheetIdx, blockId, row, col, input, undoable = true) {
|
|
59
|
+
return this.apply([
|
|
60
|
+
{
|
|
61
|
+
type: 'blockInput',
|
|
62
|
+
value: { sheetIdx, blockId, row, col, input },
|
|
63
|
+
},
|
|
64
|
+
], undoable);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Write several block cells in one transaction. Used when an interaction
|
|
68
|
+
* changes a group of related cells at once (e.g. redistributing a percent
|
|
69
|
+
* allocation across a pool).
|
|
70
|
+
*/
|
|
71
|
+
inputBlockCells(inputs, undoable = true) {
|
|
72
|
+
return this.apply(inputs.map((i) => ({ type: 'blockInput', value: i })), undoable);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Set a cell's content to an image URL, optionally widening the column
|
|
76
|
+
* and/or heightening the row to fit — all in one transaction. The host
|
|
77
|
+
* computes the target `colWidth`/`rowHeight` (in engine units) and passes
|
|
78
|
+
* them only when an adjustment is actually needed.
|
|
79
|
+
*/
|
|
80
|
+
setCellImage(sheetIdx, row, col, url, opts) {
|
|
81
|
+
const payloads = [
|
|
82
|
+
{ type: 'cellInput', value: { sheetIdx, row, col, content: url } },
|
|
83
|
+
];
|
|
84
|
+
if (opts?.colWidth !== undefined) {
|
|
85
|
+
payloads.push({
|
|
86
|
+
type: 'setColWidth',
|
|
87
|
+
value: { sheetIdx, col, width: opts.colWidth },
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
if (opts?.rowHeight !== undefined) {
|
|
91
|
+
payloads.push({
|
|
92
|
+
type: 'setRowHeight',
|
|
93
|
+
value: { sheetIdx, row, height: opts.rowHeight },
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
return this.apply(payloads, true);
|
|
97
|
+
}
|
|
98
|
+
// ---- sheets ---------------------------------------------------------
|
|
99
|
+
/** Create a new sheet named `name` at index `idx`. */
|
|
100
|
+
createSheet(name, idx) {
|
|
101
|
+
return this.apply([{ type: 'createSheet', value: { idx, newName: name } }], true);
|
|
102
|
+
}
|
|
103
|
+
/** Rename a sheet. */
|
|
104
|
+
renameSheet(oldName, newName) {
|
|
105
|
+
return this.apply([{ type: 'sheetRename', value: { oldName, newName } }], true);
|
|
106
|
+
}
|
|
107
|
+
/** Delete the sheet at index `idx`. */
|
|
108
|
+
deleteSheet(idx) {
|
|
109
|
+
return this.apply([{ type: 'deleteSheet', value: { idx } }], true);
|
|
110
|
+
}
|
|
111
|
+
/** Set a sheet tab's color (ARGB string; empty clears it). */
|
|
112
|
+
setSheetColor(idx, color) {
|
|
113
|
+
return this.apply([{ type: 'setSheetColor', value: { idx, color } }], true);
|
|
114
|
+
}
|
|
115
|
+
// ---- blocks ---------------------------------------------------------
|
|
116
|
+
/** Insert `cnt` rows into a block, starting at block-row `start`. */
|
|
117
|
+
insertRowsInBlock(sheetIdx, blockId, start, cnt = 1) {
|
|
118
|
+
return this.apply([
|
|
119
|
+
{
|
|
120
|
+
type: 'insertRowsInBlock',
|
|
121
|
+
value: { sheetIdx, blockId, start, cnt },
|
|
122
|
+
},
|
|
123
|
+
], true);
|
|
124
|
+
}
|
|
125
|
+
/** Delete a block. */
|
|
126
|
+
removeBlock(sheetIdx, blockId) {
|
|
127
|
+
return this.apply([{ type: 'removeBlock', value: { sheetIdx, id: blockId } }], true);
|
|
128
|
+
}
|
|
129
|
+
// ---- formatting -----------------------------------------------------
|
|
130
|
+
//
|
|
131
|
+
// Each method turns the current sheet + selection into style-update
|
|
132
|
+
// payloads (logic in ../format) and applies them. The host supplies the
|
|
133
|
+
// sheet index (a view concern) and the selection.
|
|
134
|
+
/** Apply font styling (bold/italic/underline/strike/color/size). */
|
|
135
|
+
async setFont(sheetIdx, data, update) {
|
|
136
|
+
await this.applyGenerated(generateFontPayload(sheetIdx, data, update));
|
|
137
|
+
}
|
|
138
|
+
/** Apply horizontal/vertical alignment. */
|
|
139
|
+
async setAlignment(sheetIdx, data, alignment) {
|
|
140
|
+
await this.applyGenerated(generateAlgnmentPayload(sheetIdx, data, alignment));
|
|
141
|
+
}
|
|
142
|
+
/** Toggle wrap-text. */
|
|
143
|
+
async setWrapText(sheetIdx, data, wrapText) {
|
|
144
|
+
await this.applyGenerated(generateWrapTextPayload(sheetIdx, data, wrapText));
|
|
145
|
+
}
|
|
146
|
+
/** Apply a number format. */
|
|
147
|
+
async setNumFmt(sheetIdx, data, numFmt) {
|
|
148
|
+
await this.applyGenerated(generateNumFmtPayload(sheetIdx, data, numFmt));
|
|
149
|
+
}
|
|
150
|
+
/** Apply a pattern fill (foreground/background color + pattern). */
|
|
151
|
+
async setPatternFill(sheetIdx, data, opts) {
|
|
152
|
+
await this.applyGenerated(generatePatternFillPayload(sheetIdx, data, opts.fgColor, opts.bgColor, opts.pattern));
|
|
153
|
+
}
|
|
154
|
+
/** Apply borders to the selection per the batch directive. */
|
|
155
|
+
async setBorder(sheetIdx, data, update) {
|
|
156
|
+
await this.applyGenerated(generateBorderPayloads(sheetIdx, data, update));
|
|
157
|
+
}
|
|
158
|
+
/** Apply a generated payload list, skipping the round-trip when empty. */
|
|
159
|
+
async applyGenerated(payloads) {
|
|
160
|
+
if (payloads.length === 0)
|
|
161
|
+
return;
|
|
162
|
+
await this.apply(payloads, true);
|
|
163
|
+
}
|
|
164
|
+
// ---- structured blocks ---------------------------------------------
|
|
165
|
+
/**
|
|
166
|
+
* Create a form-backed block: the `createBlock` + `bindFormSchema` +
|
|
167
|
+
* per-field `upsertFieldRenderInfo` payloads, in one transaction.
|
|
168
|
+
*
|
|
169
|
+
* The host resolves each field first (type → FieldInfo, validation
|
|
170
|
+
* formulas, and the engine render id from its FieldManager), then hands
|
|
171
|
+
* the flattened list here. Field/formula composition and FieldManager
|
|
172
|
+
* registration stay in the host because they touch engine-side render
|
|
173
|
+
* state that isn't part of the Client seam.
|
|
174
|
+
*/
|
|
175
|
+
async createFormBlock(opts) {
|
|
176
|
+
const { sheetIdx, blockId, masterRow, masterCol, refName, keyIdx, fields, } = opts;
|
|
177
|
+
const payloads = [
|
|
178
|
+
{
|
|
179
|
+
type: 'createBlock',
|
|
180
|
+
value: {
|
|
181
|
+
sheetIdx,
|
|
182
|
+
id: blockId,
|
|
183
|
+
masterRow,
|
|
184
|
+
masterCol,
|
|
185
|
+
rowCnt: 1,
|
|
186
|
+
colCnt: fields.length,
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
type: 'bindFormSchema',
|
|
191
|
+
value: {
|
|
192
|
+
refName,
|
|
193
|
+
sheetIdx,
|
|
194
|
+
blockId,
|
|
195
|
+
fieldFrom: 0,
|
|
196
|
+
row: true,
|
|
197
|
+
keyIdx: keyIdx < 0 ? 0 : keyIdx,
|
|
198
|
+
fields: fields.map((f) => f.name),
|
|
199
|
+
renderIds: fields.map((f) => f.renderId),
|
|
200
|
+
fieldFormulas: fields.map((f) => f.valueFormula ?? ''),
|
|
201
|
+
validationFormulas: [],
|
|
202
|
+
editabilityFormulas: [],
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
...fields.map((f) => ({
|
|
206
|
+
type: 'upsertFieldRenderInfo',
|
|
207
|
+
value: {
|
|
208
|
+
renderId: f.renderId,
|
|
209
|
+
diyRender: f.diyRender,
|
|
210
|
+
styleUpdate: { setNumFmt: f.numFmt ?? '' },
|
|
211
|
+
},
|
|
212
|
+
})),
|
|
213
|
+
];
|
|
214
|
+
await this.apply(payloads, true);
|
|
215
|
+
}
|
|
216
|
+
// ---- generic / temp-branch -----------------------------------------
|
|
217
|
+
/**
|
|
218
|
+
* Apply a caller-built payload list as one transaction (at the host's
|
|
219
|
+
* temp-mode). Escape hatch for operations whose payload construction still
|
|
220
|
+
* lives in the host — e.g. the toolbar's format/border generators. Prefer
|
|
221
|
+
* a named method above when one exists; this exists so no caller has to
|
|
222
|
+
* reach past WorkbookOps to `client.handleTransaction` directly.
|
|
223
|
+
*/
|
|
224
|
+
applyPayloads(payloads, undoable = true) {
|
|
225
|
+
return this.apply(payloads, undoable);
|
|
226
|
+
}
|
|
227
|
+
/** Commit the workbook's temp (speculative) branch into the main branch. */
|
|
228
|
+
async commitTempStatus() {
|
|
229
|
+
const res = await this.client.commitTempStatus();
|
|
230
|
+
if (isErrorMessage(res)) {
|
|
231
|
+
throw new Error('Failed to commit temp status: ' + res.msg);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
/** Discard the workbook's temp (speculative) branch. */
|
|
235
|
+
async cleanupTempStatus() {
|
|
236
|
+
const res = await this.client.cleanupTempStatus();
|
|
237
|
+
if (isErrorMessage(res)) {
|
|
238
|
+
throw new Error('Failed to clean up temp status: ' + res.msg);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
// ---- validation -----------------------------------------------------
|
|
242
|
+
/**
|
|
243
|
+
* Establish (or refresh) the validation rule for a cell.
|
|
244
|
+
*
|
|
245
|
+
* A validation rule is an Excel formula (no leading `=`) that should
|
|
246
|
+
* evaluate to a boolean. We park it in the cell's *shadow* cell so the
|
|
247
|
+
* engine evaluates it reactively; the host reads the shadow value back and
|
|
248
|
+
* renders the result (see logisheets-core's `interpretValidation`).
|
|
249
|
+
*
|
|
250
|
+
* Lifted verbatim out of the browser's ValidationCell component so the
|
|
251
|
+
* Node runtime gets the same operation.
|
|
252
|
+
*/
|
|
253
|
+
async setValidationRule(sheetIdx, row, col, formula) {
|
|
254
|
+
const shadow = await this.client.getShadowCellId({
|
|
255
|
+
sheetIdx,
|
|
256
|
+
rowIdx: row,
|
|
257
|
+
colIdx: col,
|
|
258
|
+
});
|
|
259
|
+
if (isErrorMessage(shadow)) {
|
|
260
|
+
throw new Error('Failed to get shadow cell id: ' + shadow.msg);
|
|
261
|
+
}
|
|
262
|
+
await this.apply([
|
|
263
|
+
{
|
|
264
|
+
type: 'ephemeralCellInput',
|
|
265
|
+
value: {
|
|
266
|
+
id: shadow.cellId.value,
|
|
267
|
+
sheetIdx,
|
|
268
|
+
content: `=${formula}`,
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
], false);
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Evaluate an Excel formula (no leading `=`) in a sheet and return its
|
|
275
|
+
* Value. Parks the formula in a throwaway ephemeral cell, reads the result
|
|
276
|
+
* back, and leaves no committed change — the same mechanism the browser
|
|
277
|
+
* uses for shadow cells, here for one-shot evaluation.
|
|
278
|
+
*/
|
|
279
|
+
async evalFormula(sheetIdx, formula) {
|
|
280
|
+
const id = this.ephemeralSeq++;
|
|
281
|
+
await this.apply([
|
|
282
|
+
{
|
|
283
|
+
type: 'ephemeralCellInput',
|
|
284
|
+
value: { id, sheetIdx, content: '=' + formula },
|
|
285
|
+
},
|
|
286
|
+
], false);
|
|
287
|
+
const sheetId = await this.client.getSheetId({ sheetIdx });
|
|
288
|
+
if (isErrorMessage(sheetId)) {
|
|
289
|
+
throw new Error('Failed to resolve sheet id: ' + sheetId.msg);
|
|
290
|
+
}
|
|
291
|
+
const infos = await this.client.batchGetCellInfoById({
|
|
292
|
+
ids: [{ sheetId, cellId: { type: 'ephemeralCell', value: id } }],
|
|
293
|
+
});
|
|
294
|
+
if (isErrorMessage(infos)) {
|
|
295
|
+
throw new Error('Failed to read ephemeral cell: ' + infos.msg);
|
|
296
|
+
}
|
|
297
|
+
return infos[0].value;
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Evaluate formula-based validation rules and return the violating cells.
|
|
301
|
+
* Headless batch check — the per-cell browser path uses setValidationRule +
|
|
302
|
+
* interpretValidation instead.
|
|
303
|
+
*/
|
|
304
|
+
async checkValidations(rules) {
|
|
305
|
+
const values = new Map();
|
|
306
|
+
for (const rule of rules) {
|
|
307
|
+
values.set(rule.formula, await this.evalFormula(rule.sheetIdx, rule.formula));
|
|
308
|
+
}
|
|
309
|
+
return checkValidationsPure(rules, (_sheetIdx, formula) => values.get(formula));
|
|
310
|
+
}
|
|
311
|
+
/** Check required / unique / membership field constraints. */
|
|
312
|
+
async checkFieldConstraints(columns) {
|
|
313
|
+
const values = new Map();
|
|
314
|
+
for (const { cells } of columns) {
|
|
315
|
+
for (const c of cells) {
|
|
316
|
+
const key = `${c.sheetIdx}:${c.row}:${c.col}`;
|
|
317
|
+
if (values.has(key))
|
|
318
|
+
continue;
|
|
319
|
+
const v = await this.client.getValue({
|
|
320
|
+
sheetIdx: c.sheetIdx,
|
|
321
|
+
row: c.row,
|
|
322
|
+
col: c.col,
|
|
323
|
+
});
|
|
324
|
+
if (isErrorMessage(v)) {
|
|
325
|
+
throw new Error('Failed to read cell value: ' + v.msg);
|
|
326
|
+
}
|
|
327
|
+
values.set(key, v);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return checkFieldConstraintsPure(columns, (sheetIdx, row, col) => {
|
|
331
|
+
const v = values.get(`${sheetIdx}:${row}:${col}`);
|
|
332
|
+
return (v ?? 'empty');
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
declare class CallerRegistry {
|
|
2
|
+
private _entries;
|
|
3
|
+
private _blockOwners;
|
|
4
|
+
private _fieldPositions;
|
|
5
|
+
getUserUuid(): string;
|
|
6
|
+
getCraftUuid(craftId: string): string;
|
|
7
|
+
isUser(uuid: string | undefined): boolean;
|
|
8
|
+
registerBlockOwner(sheetIdx: number, blockId: number, callerUuid: string): void;
|
|
9
|
+
getBlockOwner(sheetIdx: number, blockId: number): string | undefined;
|
|
10
|
+
/**
|
|
11
|
+
* Register the field at a specific block-relative position.
|
|
12
|
+
* `axis = 'col'` — column-oriented form (one field per column);
|
|
13
|
+
* `axis = 'row'` — row-oriented form (one field per row).
|
|
14
|
+
*/
|
|
15
|
+
registerFieldPosition(sheetIdx: number, blockId: number, axis: 'col' | 'row', offset: number, renderId: string): void;
|
|
16
|
+
getFieldRenderId(sheetIdx: number, blockId: number, blockRow: number, blockCol: number): string | undefined;
|
|
17
|
+
private _getOrAssign;
|
|
18
|
+
}
|
|
19
|
+
export declare const callerRegistry: CallerRegistry;
|
|
20
|
+
/** Minimal shape needed to decide static editability — a subset of the
|
|
21
|
+
* engine's FieldInfo. `userEditable` may be a boolean or a formula string. */
|
|
22
|
+
export interface FieldEditableInfo {
|
|
23
|
+
userEditable?: boolean | string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Whether a field permits user edits based on its static declaration alone
|
|
27
|
+
* (ignoring any dynamic formula). `userEditable === false` blocks; `true`,
|
|
28
|
+
* `undefined`, or a formula string permits (the formula is enforced
|
|
29
|
+
* downstream via a shadow value).
|
|
30
|
+
*/
|
|
31
|
+
export declare function isFieldUserEditable(fi: FieldEditableInfo | undefined): boolean;
|
|
32
|
+
export {};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Permissions — caller identity registry + the pure editability predicate.
|
|
2
|
+
//
|
|
3
|
+
// The CallerRegistry is plain in-process state (no engine, no UI): it maps
|
|
4
|
+
// caller identities to uuids, blocks to owners, and block-relative field
|
|
5
|
+
// positions to renderIds. It's the shared foundation the App's permission glue
|
|
6
|
+
// (patch.ts, field-editable.ts) builds on — moving it here makes it usable from
|
|
7
|
+
// the Node runtime too.
|
|
8
|
+
//
|
|
9
|
+
// The engine-integration parts (monkey-patching the WorkbookClient, toast
|
|
10
|
+
// feedback, resolving FieldInfo via the live engine) stay in the App: they are
|
|
11
|
+
// UI/runtime glue, not portable logic.
|
|
12
|
+
import { simpleUuid } from '../utils/index.js';
|
|
13
|
+
const USER_KEY = '__user__';
|
|
14
|
+
class CallerRegistry {
|
|
15
|
+
constructor() {
|
|
16
|
+
this._entries = new Map();
|
|
17
|
+
this._blockOwners = new Map();
|
|
18
|
+
// (sheetIdx, blockId, block-relative col) → field renderId. Populated
|
|
19
|
+
// when patch.ts observes a bindFormSchema payload. Lets the cellInput
|
|
20
|
+
// validator look up the FieldInfo for any block cell to enforce
|
|
21
|
+
// FieldInfo.userEditable.
|
|
22
|
+
this._fieldPositions = new Map();
|
|
23
|
+
}
|
|
24
|
+
getUserUuid() {
|
|
25
|
+
return this._getOrAssign(USER_KEY);
|
|
26
|
+
}
|
|
27
|
+
getCraftUuid(craftId) {
|
|
28
|
+
if (craftId === USER_KEY) {
|
|
29
|
+
throw new Error(`invalid craftId: ${craftId}`);
|
|
30
|
+
}
|
|
31
|
+
return this._getOrAssign(craftId);
|
|
32
|
+
}
|
|
33
|
+
isUser(uuid) {
|
|
34
|
+
return uuid === this._entries.get(USER_KEY);
|
|
35
|
+
}
|
|
36
|
+
registerBlockOwner(sheetIdx, blockId, callerUuid) {
|
|
37
|
+
this._blockOwners.set(`${sheetIdx}-${blockId}`, callerUuid);
|
|
38
|
+
}
|
|
39
|
+
getBlockOwner(sheetIdx, blockId) {
|
|
40
|
+
return this._blockOwners.get(`${sheetIdx}-${blockId}`);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Register the field at a specific block-relative position.
|
|
44
|
+
* `axis = 'col'` — column-oriented form (one field per column);
|
|
45
|
+
* `axis = 'row'` — row-oriented form (one field per row).
|
|
46
|
+
*/
|
|
47
|
+
registerFieldPosition(sheetIdx, blockId, axis, offset, renderId) {
|
|
48
|
+
this._fieldPositions.set(`${sheetIdx}-${blockId}-${axis}-${offset}`, renderId);
|
|
49
|
+
}
|
|
50
|
+
getFieldRenderId(sheetIdx, blockId, blockRow, blockCol) {
|
|
51
|
+
// Try column-oriented form first, then row-oriented. A block
|
|
52
|
+
// shouldn't be bound to both — first hit wins.
|
|
53
|
+
return (this._fieldPositions.get(`${sheetIdx}-${blockId}-col-${blockCol}`) ??
|
|
54
|
+
this._fieldPositions.get(`${sheetIdx}-${blockId}-row-${blockRow}`));
|
|
55
|
+
}
|
|
56
|
+
_getOrAssign(key) {
|
|
57
|
+
const existing = this._entries.get(key);
|
|
58
|
+
if (existing)
|
|
59
|
+
return existing;
|
|
60
|
+
const uuid = simpleUuid();
|
|
61
|
+
this._entries.set(key, uuid);
|
|
62
|
+
return uuid;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export const callerRegistry = new CallerRegistry();
|
|
66
|
+
/**
|
|
67
|
+
* Whether a field permits user edits based on its static declaration alone
|
|
68
|
+
* (ignoring any dynamic formula). `userEditable === false` blocks; `true`,
|
|
69
|
+
* `undefined`, or a formula string permits (the formula is enforced
|
|
70
|
+
* downstream via a shadow value).
|
|
71
|
+
*/
|
|
72
|
+
export function isFieldUserEditable(fi) {
|
|
73
|
+
return fi?.userEditable !== false;
|
|
74
|
+
}
|
package/dist/port.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Client } from 'logisheets-web';
|
|
2
|
+
export type { Client };
|
|
3
|
+
/**
|
|
4
|
+
* Anything in logisheets-core that needs to reach the engine takes one of
|
|
5
|
+
* these instead of importing a concrete client. Tests pass a mock.
|
|
6
|
+
*/
|
|
7
|
+
export interface HasClient {
|
|
8
|
+
readonly client: Client;
|
|
9
|
+
}
|
package/dist/port.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// The dependency seam.
|
|
2
|
+
//
|
|
3
|
+
// logisheets-core is pure logic and must not bundle a backend. It talks to
|
|
4
|
+
// the engine ONLY through the `Client` interface, which it imports as a TYPE
|
|
5
|
+
// from logisheets-web. `import type` is erased at compile time, so this file
|
|
6
|
+
// produces NO runtime dependency on logisheets-web (or on logisheets-node).
|
|
7
|
+
//
|
|
8
|
+
// The concrete Client is INJECTED by the host:
|
|
9
|
+
// - browser app -> logisheets-engine's worker-backed WorkbookClient
|
|
10
|
+
// - node runtime -> logisheets' synchronous handle()-based client
|
|
11
|
+
//
|
|
12
|
+
// Both implement the same `Client` interface (node's source is a copy of
|
|
13
|
+
// web's), so logisheets-core never needs to know which one it got.
|
|
14
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './model.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './model.js';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare class SelectionModel<T> {
|
|
2
|
+
#private;
|
|
3
|
+
readonly multi: boolean;
|
|
4
|
+
readonly data: readonly T[];
|
|
5
|
+
constructor(multi?: boolean, data?: readonly T[]);
|
|
6
|
+
get selected(): readonly T[];
|
|
7
|
+
toggle(data: T): void;
|
|
8
|
+
select(data: T): void;
|
|
9
|
+
deSelect(data: T): void;
|
|
10
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
|
2
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
|
3
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
4
|
+
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
5
|
+
};
|
|
6
|
+
var _SelectionModel_selected;
|
|
7
|
+
export class SelectionModel {
|
|
8
|
+
constructor(multi = false, data = []) {
|
|
9
|
+
this.multi = multi;
|
|
10
|
+
this.data = data;
|
|
11
|
+
_SelectionModel_selected.set(this, new Map());
|
|
12
|
+
data.forEach((d) => {
|
|
13
|
+
__classPrivateFieldGet(this, _SelectionModel_selected, "f").set(d, true);
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
get selected() {
|
|
17
|
+
const selecteds = [];
|
|
18
|
+
__classPrivateFieldGet(this, _SelectionModel_selected, "f").forEach((checked, d) => {
|
|
19
|
+
if (!checked)
|
|
20
|
+
return;
|
|
21
|
+
selecteds.push(d);
|
|
22
|
+
});
|
|
23
|
+
return selecteds;
|
|
24
|
+
}
|
|
25
|
+
toggle(data) {
|
|
26
|
+
if (__classPrivateFieldGet(this, _SelectionModel_selected, "f").has(data))
|
|
27
|
+
__classPrivateFieldGet(this, _SelectionModel_selected, "f").set(data, true);
|
|
28
|
+
else
|
|
29
|
+
__classPrivateFieldGet(this, _SelectionModel_selected, "f").set(data, false);
|
|
30
|
+
}
|
|
31
|
+
select(data) {
|
|
32
|
+
__classPrivateFieldGet(this, _SelectionModel_selected, "f").set(data, true);
|
|
33
|
+
}
|
|
34
|
+
deSelect(data) {
|
|
35
|
+
__classPrivateFieldGet(this, _SelectionModel_selected, "f").set(data, false);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
_SelectionModel_selected = new WeakMap();
|