logisheets-core 1.14.0 → 1.15.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/field/index.d.ts +0 -26
- package/dist/field/index.js +23 -68
- package/dist/format/index.d.ts +1 -1
- package/dist/ops/index.d.ts +475 -3
- package/dist/ops/index.js +895 -40
- package/dist/permissions/index.d.ts +0 -12
- package/dist/permissions/index.js +6 -9
- package/package.json +2 -2
package/dist/field/index.d.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import type { Value } from 'logisheets-web';
|
|
2
|
-
import type { Violation } from '../validation/index.js';
|
|
3
1
|
export type FieldTypeEnum = 'unspecified' | 'enum' | 'multiSelect' | 'datetime' | 'boolean' | 'string' | 'number' | 'image' | 'fieldRef' | 'multiSelectRef';
|
|
4
2
|
export interface EnumValue {
|
|
5
3
|
id: string;
|
|
@@ -25,27 +23,3 @@ export interface FieldSetting {
|
|
|
25
23
|
refBlockId?: number;
|
|
26
24
|
refFieldName?: string;
|
|
27
25
|
}
|
|
28
|
-
export interface CellRef {
|
|
29
|
-
sheetIdx: number;
|
|
30
|
-
row: number;
|
|
31
|
-
col: number;
|
|
32
|
-
}
|
|
33
|
-
/** A field column: its constraints plus the data cells that belong to it. */
|
|
34
|
-
export interface FieldColumn {
|
|
35
|
-
field: Pick<FieldSetting, 'name' | 'required' | 'unique'>;
|
|
36
|
-
cells: readonly CellRef[];
|
|
37
|
-
/**
|
|
38
|
-
* Allowed values for membership-constrained fields (enum / multiSelect /
|
|
39
|
-
* fieldRef / multiSelectRef). When provided, every non-empty cell value
|
|
40
|
-
* must appear here. The caller resolves the candidate set — for enums it
|
|
41
|
-
* is the enum labels; for fieldRef it is the referenced field's values.
|
|
42
|
-
* Leave undefined for fields with no membership constraint.
|
|
43
|
-
*/
|
|
44
|
-
allowed?: readonly string[];
|
|
45
|
-
}
|
|
46
|
-
/**
|
|
47
|
-
* Check `required` and `unique` across every field column and return all
|
|
48
|
-
* violating cells. Pure: the caller supplies `getValue` (WorkbookOps wraps the
|
|
49
|
-
* engine), so this runs identically in the browser and on Node.
|
|
50
|
-
*/
|
|
51
|
-
export declare function checkFieldConstraints(columns: readonly FieldColumn[], getValue: (sheetIdx: number, row: number, col: number) => Value): Violation[];
|
package/dist/field/index.js
CHANGED
|
@@ -1,70 +1,25 @@
|
|
|
1
|
-
//
|
|
1
|
+
// The field AUTHORING model — what a composer UI edits before it becomes a
|
|
2
|
+
// schema field.
|
|
2
3
|
//
|
|
3
|
-
//
|
|
4
|
-
// the
|
|
5
|
-
//
|
|
6
|
-
//
|
|
4
|
+
// This is the shape the browser's block-composer works in, shared so the App,
|
|
5
|
+
// the data-gateway craft and a Node runtime agree on what a field is while it
|
|
6
|
+
// is being authored. It is NOT the source of truth for what a field IS: that is
|
|
7
|
+
// the engine schema, which carries the declaration and persists it (see
|
|
8
|
+
// design/block-field-semantics.md). This model is what you fill in to produce
|
|
9
|
+
// one.
|
|
7
10
|
//
|
|
8
|
-
// Formula-based validation lives in ../validation
|
|
9
|
-
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Check `required` and `unique` across every field column and return all
|
|
25
|
-
* violating cells. Pure: the caller supplies `getValue` (WorkbookOps wraps the
|
|
26
|
-
* engine), so this runs identically in the browser and on Node.
|
|
27
|
-
*/
|
|
28
|
-
export function checkFieldConstraints(columns, getValue) {
|
|
29
|
-
const out = [];
|
|
30
|
-
for (const { field, cells, allowed } of columns) {
|
|
31
|
-
const seen = new Map();
|
|
32
|
-
const allowedSet = allowed ? new Set(allowed) : undefined;
|
|
33
|
-
for (const cell of cells) {
|
|
34
|
-
const v = getValue(cell.sheetIdx, cell.row, cell.col);
|
|
35
|
-
const empty = isEmpty(v);
|
|
36
|
-
if (field.required && empty) {
|
|
37
|
-
out.push({
|
|
38
|
-
...cell,
|
|
39
|
-
kind: 'required',
|
|
40
|
-
message: `Field "${field.name}" is required`,
|
|
41
|
-
});
|
|
42
|
-
continue;
|
|
43
|
-
}
|
|
44
|
-
if (empty)
|
|
45
|
-
continue;
|
|
46
|
-
if (allowedSet && !allowedSet.has(valueText(v))) {
|
|
47
|
-
out.push({
|
|
48
|
-
...cell,
|
|
49
|
-
kind: 'membership',
|
|
50
|
-
message: `Value "${valueText(v)}" is not an allowed option for field "${field.name}"`,
|
|
51
|
-
});
|
|
52
|
-
continue;
|
|
53
|
-
}
|
|
54
|
-
if (field.unique) {
|
|
55
|
-
const key = valueKey(v);
|
|
56
|
-
if (seen.has(key)) {
|
|
57
|
-
out.push({
|
|
58
|
-
...cell,
|
|
59
|
-
kind: 'duplicate',
|
|
60
|
-
message: `Duplicate value in unique field "${field.name}"`,
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
else {
|
|
64
|
-
seen.set(key, cell);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
return out;
|
|
70
|
-
}
|
|
11
|
+
// Formula-based validation lives in ../validation.
|
|
12
|
+
export {};
|
|
13
|
+
// The `required` / `unique` / membership CHECKS used to live here as a second,
|
|
14
|
+
// independent implementation over an injected value port. They are gone: a
|
|
15
|
+
// field that declares `required`, `unique`, `enum`/`multiSelect` or `fieldRef`
|
|
16
|
+
// now has the matching rule generated by the engine from the declaration
|
|
17
|
+
// (crates/controller/src/block_manager/derived_rules.rs), installed as a
|
|
18
|
+
// per-record validation shadow, and reported through the same channel as every
|
|
19
|
+
// other rule — the warning marker, `list_violations`, and the
|
|
20
|
+
// `overrideValidation` write gate.
|
|
21
|
+
//
|
|
22
|
+
// Two implementations of one constraint is how this area went wrong: they did
|
|
23
|
+
// not even agree on what "the same value" meant (`type:value` here,
|
|
24
|
+
// COUNTIF's stringified value in the engine), so the number 1 and the text "1"
|
|
25
|
+
// collided in one and not the other. See design/block-field-semantics.md §2.3.
|
package/dist/format/index.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export interface FontStyle {
|
|
|
6
6
|
color?: string;
|
|
7
7
|
size?: number;
|
|
8
8
|
strike?: boolean;
|
|
9
|
-
/** Font family name, e.g. "Arial", "Times New Roman", "
|
|
9
|
+
/** Font family name, e.g. "Arial", "Times New Roman", "Microsoft YaHei". */
|
|
10
10
|
name?: string;
|
|
11
11
|
}
|
|
12
12
|
export declare function generateFontPayload(sheetIdx: number, data: SelectedData, update: FontStyle): readonly Payload[];
|
package/dist/ops/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Payload, ActionEffect, SelectedData, Alignment, StPatternType, Value, SheetCellId } from 'logisheets-web';
|
|
2
2
|
import type { Client } from '../port.js';
|
|
3
3
|
import { type ValidationRule, type Violation } from '../validation/index.js';
|
|
4
|
-
import {
|
|
4
|
+
import type { FieldTypeEnum } from '../field/index.js';
|
|
5
5
|
import { type FontStyle, type BorderBatchUpdate } from '../format/index.js';
|
|
6
6
|
/**
|
|
7
7
|
* Tells WorkbookOps whether to mark transactions temp (speculative). The
|
|
@@ -17,10 +17,203 @@ export interface FormBlockField {
|
|
|
17
17
|
renderId: string;
|
|
18
18
|
/** Per-field value-formula template (#FIELD("X") / #KEY); '' if free-form. */
|
|
19
19
|
valueFormula?: string;
|
|
20
|
+
/**
|
|
21
|
+
* Per-field validation template (#PLACEHOLDER for the value under test,
|
|
22
|
+
* #FIELD("X") for a same-row sibling); '' when the field has no rule.
|
|
23
|
+
*
|
|
24
|
+
* This goes into the schema rather than staying host-side so the engine
|
|
25
|
+
* installs the per-record shadow itself — on bind AND on every row added
|
|
26
|
+
* later — and so one answer serves every reader: the warning marker, the
|
|
27
|
+
* `overrideValidation` write gate, and any other host.
|
|
28
|
+
*/
|
|
29
|
+
validationFormula?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Per-field editability template — FALSE installs a `UserEditable` lock the
|
|
32
|
+
* host permission layer reads.
|
|
33
|
+
*
|
|
34
|
+
* Carried here because a schema re-bind replaces the field wholesale: the
|
|
35
|
+
* three bind sites below used to send `editabilityFormulas: []` ("all
|
|
36
|
+
* None"), so editing a block through this layer silently dropped any
|
|
37
|
+
* editability template it had.
|
|
38
|
+
*/
|
|
39
|
+
editabilityFormula?: string;
|
|
20
40
|
/** Whether the field renders via a host-drawn (DIY) overlay. */
|
|
21
41
|
diyRender: boolean;
|
|
22
42
|
/** Number format applied to the field's render info. */
|
|
23
43
|
numFmt?: string;
|
|
44
|
+
/** Declared type. Omitted reads as unspecified (free-form). */
|
|
45
|
+
fieldType?: FieldTypeEnum;
|
|
46
|
+
/** Enum set backing a `fieldType` of 'enum' / 'multiSelect'. */
|
|
47
|
+
enumSetId?: string;
|
|
48
|
+
/** Target of a `fieldType` of 'fieldRef' / 'multiSelectRef'. */
|
|
49
|
+
refTarget?: {
|
|
50
|
+
sheetId: number;
|
|
51
|
+
blockId: number;
|
|
52
|
+
fieldName: string;
|
|
53
|
+
};
|
|
54
|
+
/** What the field means, in prose, for whoever reads the block next. */
|
|
55
|
+
description?: string;
|
|
56
|
+
/** Every record must carry a value here. */
|
|
57
|
+
required?: boolean;
|
|
58
|
+
/** No two records may carry the same value here. */
|
|
59
|
+
unique?: boolean;
|
|
60
|
+
/** What a newly-added record starts with. */
|
|
61
|
+
defaultValue?: string;
|
|
62
|
+
/**
|
|
63
|
+
* Who may write to this field's cells: `'inherit'` (the block's own owner
|
|
64
|
+
* rules) | `'ownerOnly'` | `'anyone'`.
|
|
65
|
+
*
|
|
66
|
+
* Declared on the schema so every host reads the same answer — it was the
|
|
67
|
+
* last field-level rule that lived only in the browser's own field store,
|
|
68
|
+
* as a tri-state `userEditable` boolean. The engine persists and answers;
|
|
69
|
+
* it does not enforce, because it does not know who is writing.
|
|
70
|
+
*/
|
|
71
|
+
writePolicy?: 'inherit' | 'ownerOnly' | 'anyone';
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* An option list a field can draw from, as the engine stores it.
|
|
75
|
+
*
|
|
76
|
+
* Ids and labels only. A variant's COLOUR is presentation and stays in the
|
|
77
|
+
* host, keyed by variant id — the engine needs the options in order to judge a
|
|
78
|
+
* value and needs nothing else.
|
|
79
|
+
*/
|
|
80
|
+
export interface EnumSetDecl {
|
|
81
|
+
id: string;
|
|
82
|
+
name?: string;
|
|
83
|
+
variants: ReadonlyArray<{
|
|
84
|
+
id: string;
|
|
85
|
+
label?: string;
|
|
86
|
+
}>;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* How an analysis field aggregates the field it reads. Every one lowers to
|
|
90
|
+
* `FUNC(BLOCKREFSB(...))` in the engine, which is why adding one is a one-line
|
|
91
|
+
* change there and none here.
|
|
92
|
+
*
|
|
93
|
+
* `COUNT` counts numbers, `COUNTA` counts values that are simply THERE — over
|
|
94
|
+
* `10, 20, "n/a", <blank>` they say 2 and 3. In a pivot, COUNT counts matching
|
|
95
|
+
* records and COUNTA counts the ones whose measure is filled in.
|
|
96
|
+
*/
|
|
97
|
+
export type AggFunc = 'SUM' | 'COUNT' | 'COUNTA' | 'AVERAGE' | 'MIN' | 'MAX';
|
|
98
|
+
/** One field of an analysis block: what it aggregates, and how. */
|
|
99
|
+
export interface AnalysisAggregate {
|
|
100
|
+
/** Field name of the SOURCE block. */
|
|
101
|
+
field: string;
|
|
102
|
+
func: AggFunc;
|
|
103
|
+
}
|
|
104
|
+
/** One column of the block being analysed, as the analysis needs to see it. */
|
|
105
|
+
export interface AnalysisSourceField {
|
|
106
|
+
name: string;
|
|
107
|
+
/**
|
|
108
|
+
* Whether the source DECLARES this field a number. Not whether its cells
|
|
109
|
+
* currently hold numbers: guessing from the data totals an id column, and
|
|
110
|
+
* leaves a still-empty column out of a total it belongs in.
|
|
111
|
+
*/
|
|
112
|
+
isNumber: boolean;
|
|
113
|
+
/**
|
|
114
|
+
* The source column's number format, carried onto the total so a sum of
|
|
115
|
+
* currency reads as currency.
|
|
116
|
+
*/
|
|
117
|
+
numFmt?: string;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Whether the result of `func` over a column is still measured in that
|
|
121
|
+
* column's units, and so should carry its number format.
|
|
122
|
+
*
|
|
123
|
+
* A sum, an average, a minimum or a maximum of a currency column is currency.
|
|
124
|
+
* The counts are not: they answer "how many", so formatting one like the
|
|
125
|
+
* column it counts would print "$3" for three orders. These are the aggregates
|
|
126
|
+
* whose result changes what is being measured.
|
|
127
|
+
*/
|
|
128
|
+
export declare function aggregateKeepsFormat(func: AggFunc): boolean;
|
|
129
|
+
/** The block being analysed. */
|
|
130
|
+
export interface AnalysisSource {
|
|
131
|
+
sheetIdx: number;
|
|
132
|
+
blockId: number;
|
|
133
|
+
/** Its ref name — used in the new block's description, not in a formula. */
|
|
134
|
+
refName: string;
|
|
135
|
+
rowStart: number;
|
|
136
|
+
rowCnt: number;
|
|
137
|
+
colStart: number;
|
|
138
|
+
/** Fields in COLUMN order, so the analysis's columns line up with them. */
|
|
139
|
+
fields: readonly AnalysisSourceField[];
|
|
140
|
+
/** Which column is the key — where the label goes. */
|
|
141
|
+
keyIdx: number;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* SUM over every field the source declares a number.
|
|
145
|
+
*
|
|
146
|
+
* A default is only possible because the declaration exists: this is what the
|
|
147
|
+
* field-type work in `design/block-field-semantics.md` bought — before it, a
|
|
148
|
+
* caller could only guess from the data.
|
|
149
|
+
*/
|
|
150
|
+
export declare function defaultAnalysisAggregates(fields: readonly AnalysisSourceField[]): AnalysisAggregate[];
|
|
151
|
+
/**
|
|
152
|
+
* How a pivot orders the distinct values it turns into rows and columns.
|
|
153
|
+
*
|
|
154
|
+
* `custom` takes the sequence from `orderValues`; values it does not mention
|
|
155
|
+
* follow in ascending order rather than disappearing, because a hidden group
|
|
156
|
+
* is the failure a pivot must never commit silently.
|
|
157
|
+
*/
|
|
158
|
+
export type DimOrder = 'ascending' | 'firstSeen' | 'custom';
|
|
159
|
+
/** One condition a source record must meet to be counted by a pivot. */
|
|
160
|
+
export interface PivotFilter {
|
|
161
|
+
field: string;
|
|
162
|
+
/** Spreadsheet condition syntax: `">100"`, `"East"`, `"<>closed"`. */
|
|
163
|
+
criteria: string;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* A pivot column that is declared rather than derived.
|
|
167
|
+
*
|
|
168
|
+
* A plain cross-tab needs none of these: each column's name IS the dimension
|
|
169
|
+
* value and the block's recipe supplies the rest. These exist for the two
|
|
170
|
+
* things that cannot express — a ROW TOTAL (`colValue: null`, spanning every
|
|
171
|
+
* value) and a SECOND MEASURE (`measure` / `func` of its own).
|
|
172
|
+
*
|
|
173
|
+
* A refresh leaves declared columns alone: they were never derived from the
|
|
174
|
+
* data, so the data cannot justify removing them.
|
|
175
|
+
*/
|
|
176
|
+
export interface PivotColumnSpec {
|
|
177
|
+
/** Column name in the pivot. */
|
|
178
|
+
name: string;
|
|
179
|
+
/** Which column-dimension value it filters on; `null` spans every one. */
|
|
180
|
+
colValue: string | null;
|
|
181
|
+
measure?: string;
|
|
182
|
+
func?: AggFunc;
|
|
183
|
+
}
|
|
184
|
+
/** The block a pivot analyses, as the create needs to see it. */
|
|
185
|
+
export interface PivotSource {
|
|
186
|
+
sheetIdx: number;
|
|
187
|
+
blockId: number;
|
|
188
|
+
/** Its ref name — used in the new block's description, not in a formula. */
|
|
189
|
+
refName: string;
|
|
190
|
+
rowStart: number;
|
|
191
|
+
rowCnt: number;
|
|
192
|
+
colStart: number;
|
|
193
|
+
/**
|
|
194
|
+
* Number format per SOURCE field name, for the pivot to inherit: a cell of
|
|
195
|
+
* a pivot is an aggregate of one source column, so a SUM of a currency
|
|
196
|
+
* column should read as currency rather than as a bare number.
|
|
197
|
+
*
|
|
198
|
+
* Optional — omit it and the pivot is left unformatted, which is what
|
|
199
|
+
* every caller got before this existed.
|
|
200
|
+
*/
|
|
201
|
+
numFmts?: Readonly<Record<string, string | undefined>>;
|
|
202
|
+
}
|
|
203
|
+
/** What a refresh actually changed. */
|
|
204
|
+
export interface PivotRefresh {
|
|
205
|
+
/** Groups that appeared in the source and now have a row. */
|
|
206
|
+
addedKeys: string[];
|
|
207
|
+
/** Rows the source no longer justifies. */
|
|
208
|
+
removedKeys: string[];
|
|
209
|
+
addedFields: string[];
|
|
210
|
+
removedFields: string[];
|
|
211
|
+
/**
|
|
212
|
+
* Source records with a blank dimension. They are in NO cell of the pivot,
|
|
213
|
+
* so its grand total is short by their measure — a refresh cannot fix
|
|
214
|
+
* that, only report it.
|
|
215
|
+
*/
|
|
216
|
+
unassignedRecords: number;
|
|
24
217
|
}
|
|
25
218
|
/**
|
|
26
219
|
* High-level workbook operations bound to one engine {@link Client}.
|
|
@@ -141,6 +334,8 @@ export declare class WorkbookOps {
|
|
|
141
334
|
refName: string;
|
|
142
335
|
keyIdx: number;
|
|
143
336
|
fields: readonly FormBlockField[];
|
|
337
|
+
/** Option lists the fields reference; written in the same transaction. */
|
|
338
|
+
enumSets?: readonly EnumSetDecl[];
|
|
144
339
|
}): Promise<void>;
|
|
145
340
|
/**
|
|
146
341
|
* Turn an EXISTING cell region into a form-backed block in place: like
|
|
@@ -158,6 +353,8 @@ export declare class WorkbookOps {
|
|
|
158
353
|
refName: string;
|
|
159
354
|
keyIdx: number;
|
|
160
355
|
fields: readonly FormBlockField[];
|
|
356
|
+
/** Option lists the fields reference; written in the same transaction. */
|
|
357
|
+
enumSets?: readonly EnumSetDecl[];
|
|
161
358
|
}): Promise<void>;
|
|
162
359
|
/**
|
|
163
360
|
* Edit an EXISTING form-backed block: rename it, re-type / re-formula its
|
|
@@ -187,7 +384,284 @@ export declare class WorkbookOps {
|
|
|
187
384
|
refName: string;
|
|
188
385
|
keyIdx: number;
|
|
189
386
|
fields: readonly FormBlockField[];
|
|
387
|
+
/** Option lists the fields reference; written in the same transaction. */
|
|
388
|
+
enumSets?: readonly EnumSetDecl[];
|
|
389
|
+
}): Promise<void>;
|
|
390
|
+
/**
|
|
391
|
+
* Rewrite ONE kind of per-field rule on a block, leaving the others alone.
|
|
392
|
+
*
|
|
393
|
+
* `formulas` is one entry per field, in the schema's field order — a rule
|
|
394
|
+
* or `''` for none. The other two rule kinds are sent empty, which the
|
|
395
|
+
* engine reads as "don't touch these", so editing a validation rule cannot
|
|
396
|
+
* clear the value formulas standing next to it.
|
|
397
|
+
*
|
|
398
|
+
* Every existing row is re-materialized from the new rule, so this is also
|
|
399
|
+
* how a rule is removed: pass `''` for that field.
|
|
400
|
+
*/
|
|
401
|
+
setFieldRules(opts: {
|
|
402
|
+
sheetIdx: number;
|
|
403
|
+
blockId: number;
|
|
404
|
+
kind: 'value' | 'validation' | 'editability';
|
|
405
|
+
formulas: readonly string[];
|
|
190
406
|
}): Promise<void>;
|
|
407
|
+
/**
|
|
408
|
+
* Create the block that analyses `source`: a totals row placed directly
|
|
409
|
+
* below it, declaring what it analyses and how each of its fields
|
|
410
|
+
* aggregates. See `design/block-analysis.md`.
|
|
411
|
+
*
|
|
412
|
+
* **No formula is sent.** The engine generates each aggregate field's
|
|
413
|
+
* formula from the declaration, which is what makes renaming a source
|
|
414
|
+
* field rebuild the total instead of silently zeroing it. A field with no
|
|
415
|
+
* aggregate stays an ordinary cell — that is the label column, and the
|
|
416
|
+
* label is also the key the result is addressed by
|
|
417
|
+
* (`BLOCKREF(refName, label, field)`).
|
|
418
|
+
*
|
|
419
|
+
* One transaction, so it is one undo, and so a reader between two payloads
|
|
420
|
+
* never sees a one-row table it would mistake for a record.
|
|
421
|
+
*
|
|
422
|
+
* Returns what it aggregated, in column order, for the caller to report.
|
|
423
|
+
*/
|
|
424
|
+
createAnalysisBlock(opts: {
|
|
425
|
+
source: AnalysisSource;
|
|
426
|
+
blockId: number;
|
|
427
|
+
refName: string;
|
|
428
|
+
label: string;
|
|
429
|
+
/**
|
|
430
|
+
* Which source fields to aggregate and how. Omit for
|
|
431
|
+
* {@link defaultAnalysisAggregates} — SUM over every field the source
|
|
432
|
+
* DECLARES as a number.
|
|
433
|
+
*/
|
|
434
|
+
aggregates?: readonly AnalysisAggregate[];
|
|
435
|
+
}): Promise<readonly AnalysisAggregate[]>;
|
|
436
|
+
/**
|
|
437
|
+
* Change WHAT an existing analysis block computes — which source fields it
|
|
438
|
+
* aggregates, with which function, and the label its row is addressed by.
|
|
439
|
+
*
|
|
440
|
+
* The shape never changes: an analysis block is one row with one column
|
|
441
|
+
* per source field, so an edit is a re-bind and nothing else. That is the
|
|
442
|
+
* whole difference from {@link editPivot}, where changing the recipe
|
|
443
|
+
* changes how many rows and columns there are.
|
|
444
|
+
*
|
|
445
|
+
* Exists so an analysis can be arrived at in STEPS. The first guess (sum
|
|
446
|
+
* every number) is often nearly right and occasionally wrong in one
|
|
447
|
+
* column, and without this the only remedy was to delete the block and
|
|
448
|
+
* build it again — losing its ref name, and with it every formula pointing
|
|
449
|
+
* at it.
|
|
450
|
+
*/
|
|
451
|
+
editAnalysisBlock(opts: {
|
|
452
|
+
sheetIdx: number;
|
|
453
|
+
/** The analysis block being edited. */
|
|
454
|
+
blockId: number;
|
|
455
|
+
/** Its ref name, which the re-bind has to restate. */
|
|
456
|
+
refName: string;
|
|
457
|
+
/** The block it analyses, for its field list and number formats. */
|
|
458
|
+
source: AnalysisSource;
|
|
459
|
+
aggregates: readonly AnalysisAggregate[];
|
|
460
|
+
/**
|
|
461
|
+
* A new row label, or omit to leave it alone. Rewriting it re-aims
|
|
462
|
+
* nothing — unlike a pivot key, an analysis block's label is just the
|
|
463
|
+
* name its single row is addressed by.
|
|
464
|
+
*/
|
|
465
|
+
label?: string;
|
|
466
|
+
/** Where the block sits, needed only when `label` is given. */
|
|
467
|
+
rowStart?: number;
|
|
468
|
+
colStart?: number;
|
|
469
|
+
}): Promise<readonly AnalysisAggregate[]>;
|
|
470
|
+
/**
|
|
471
|
+
* The label row a pivot carries directly above itself, as `cellInput`
|
|
472
|
+
* payloads.
|
|
473
|
+
*
|
|
474
|
+
* A pivot's column names are DATA — they are the column dimension's own
|
|
475
|
+
* values — but they live on the schema, which means the raw sheet shows a
|
|
476
|
+
* grid of numbers with nothing to say what the columns are. Everything
|
|
477
|
+
* that is not our own UI sees it that way: another tool, a person reading
|
|
478
|
+
* the file, and Excel, whose pivot tables require the labels to be in
|
|
479
|
+
* cells. So the pivot writes them there.
|
|
480
|
+
*
|
|
481
|
+
* The row sits OUTSIDE the block, immediately above it. Inside would break
|
|
482
|
+
* what a block is: every row of a block is a record addressed by its key,
|
|
483
|
+
* so a header row would become a record keyed "region" — captured by
|
|
484
|
+
* `#KEY`, counted by the key-uniqueness guard, and aggregated by anything
|
|
485
|
+
* reading the block.
|
|
486
|
+
*
|
|
487
|
+
* Rewritten in full whenever the column set can have changed, because a
|
|
488
|
+
* label left behind from a previous shape is worse than no label: it names
|
|
489
|
+
* a column that is now something else.
|
|
490
|
+
*/
|
|
491
|
+
private pivotHeaderRow;
|
|
492
|
+
/**
|
|
493
|
+
* The number format each of a pivot's columns should carry, in the order
|
|
494
|
+
* the columns are declared (`[key, ...value columns]`).
|
|
495
|
+
*
|
|
496
|
+
* Every column of a pivot aggregates ONE source column, so it is formatted
|
|
497
|
+
* like that column. Which source column differs per kind:
|
|
498
|
+
*
|
|
499
|
+
* - the key column holds the row dimension's own values;
|
|
500
|
+
* - a derived column is `func(measure)` from the recipe;
|
|
501
|
+
* - a declared column (a row total, a second measure) names its own, and
|
|
502
|
+
* falls back to the recipe's for whatever it leaves out.
|
|
503
|
+
*
|
|
504
|
+
* A COUNT column is deliberately plain — see {@link aggregateKeepsFormat}.
|
|
505
|
+
*
|
|
506
|
+
* Returned as `''` rather than `undefined` for "no format", because the
|
|
507
|
+
* payload is also how a format is CLEARED, and a refresh re-states every
|
|
508
|
+
* column: `refreshPivot` reassigns render ids by position, so a column
|
|
509
|
+
* that appears shifts the ids after it, and only restating all of them
|
|
510
|
+
* keeps each format on the column it belongs to.
|
|
511
|
+
*/
|
|
512
|
+
private pivotColumnFormats;
|
|
513
|
+
/**
|
|
514
|
+
* Create a pivot of `source`: a cross-tab whose rows are the distinct
|
|
515
|
+
* values of `rowDim`, whose columns are the distinct values of `colDim`,
|
|
516
|
+
* and whose cells are `func` over `measure`.
|
|
517
|
+
*
|
|
518
|
+
* **One transaction**, so it is one undo — which is why it asks the engine
|
|
519
|
+
* for the shape BEFORE creating the block (`pivotPlanFor`). Creating first
|
|
520
|
+
* and reshaping after would leave an empty declared pivot as an
|
|
521
|
+
* intermediate state and take two undos to remove.
|
|
522
|
+
*
|
|
523
|
+
* No formula is sent. The engine generates every cell from the recipe plus
|
|
524
|
+
* the cell's own row key and field name, which is what makes renaming a
|
|
525
|
+
* source field rebuild the pivot instead of breaking it.
|
|
526
|
+
*
|
|
527
|
+
* Returns the shape it created, for the caller to report.
|
|
528
|
+
*/
|
|
529
|
+
createPivot(opts: {
|
|
530
|
+
source: PivotSource;
|
|
531
|
+
blockId: number;
|
|
532
|
+
refName: string;
|
|
533
|
+
rowDim: string;
|
|
534
|
+
/** Omit for a grouped pivot: one value column, no cross-tab. */
|
|
535
|
+
colDim?: string;
|
|
536
|
+
measure: string;
|
|
537
|
+
func: AggFunc;
|
|
538
|
+
order?: DimOrder;
|
|
539
|
+
/** The sequence for `order: 'custom'`. */
|
|
540
|
+
orderValues?: readonly string[];
|
|
541
|
+
/** Which source records to count at all. Omit to count every one. */
|
|
542
|
+
filters?: readonly PivotFilter[];
|
|
543
|
+
/**
|
|
544
|
+
* Columns beyond the derived ones — a row total, a second measure.
|
|
545
|
+
* Appended after the cross-tab's own columns.
|
|
546
|
+
*/
|
|
547
|
+
extraColumns?: readonly PivotColumnSpec[];
|
|
548
|
+
/**
|
|
549
|
+
* Name of the pivot's single value column when `colDim` is omitted.
|
|
550
|
+
* Ignored for a real cross-tab, whose column names ARE the dimension's
|
|
551
|
+
* values.
|
|
552
|
+
*/
|
|
553
|
+
valueColumn?: string;
|
|
554
|
+
}): Promise<{
|
|
555
|
+
keys: string[];
|
|
556
|
+
fields: string[];
|
|
557
|
+
unassignedRecords: number;
|
|
558
|
+
}>;
|
|
559
|
+
/**
|
|
560
|
+
* Bring a pivot's SHAPE back in line with its source: add rows for groups
|
|
561
|
+
* that appeared, drop rows for groups that are gone, same for columns.
|
|
562
|
+
*
|
|
563
|
+
* Its numbers were never stale — they are live formulas. Only the set of
|
|
564
|
+
* rows and columns needs this, because no formula can add a row.
|
|
565
|
+
*
|
|
566
|
+
* One transaction, and the payload order is not negotiable
|
|
567
|
+
* (`design/block-pivot.md` §6): grow, write the keys, bind, shrink. Keys
|
|
568
|
+
* before the bind because `#KEY` is captured at materialization; grow
|
|
569
|
+
* before the bind because a field cannot bind to a column that does not
|
|
570
|
+
* exist; shrink after, so nothing is left bound to a vanishing column.
|
|
571
|
+
*
|
|
572
|
+
* Returns what changed, or `null` when the pivot was already current — so
|
|
573
|
+
* a caller can say "nothing to do" instead of reporting an empty refresh.
|
|
574
|
+
*/
|
|
575
|
+
refreshPivot(opts: {
|
|
576
|
+
sheetIdx: number;
|
|
577
|
+
blockId: number;
|
|
578
|
+
/** The pivot's ref name, which the re-bind has to restate. */
|
|
579
|
+
refName: string;
|
|
580
|
+
/** Its key field's name — the column holding the row dimension. */
|
|
581
|
+
keyField: string;
|
|
582
|
+
/**
|
|
583
|
+
* The pivot's CURRENT schema fields, so a re-bind can restate any
|
|
584
|
+
* column that was declared by hand.
|
|
585
|
+
*
|
|
586
|
+
* Without them the re-bind would rewrite every column as a derived
|
|
587
|
+
* one, silently turning a row total into a column filtering on the
|
|
588
|
+
* literal value "Total" — which matches nothing and reads 0.
|
|
589
|
+
*/
|
|
590
|
+
currentFields?: ReadonlyArray<{
|
|
591
|
+
field: string;
|
|
592
|
+
pivotColValue?: string;
|
|
593
|
+
pivotMeasure?: string;
|
|
594
|
+
pivotFunc?: string;
|
|
595
|
+
}>;
|
|
596
|
+
/**
|
|
597
|
+
* Number format per SOURCE field name, and the recipe the pivot runs,
|
|
598
|
+
* so a column the refresh ADDS is formatted like the columns beside it
|
|
599
|
+
* instead of arriving as a bare number.
|
|
600
|
+
*
|
|
601
|
+
* Both or neither: without the recipe there is no way to know which
|
|
602
|
+
* source column a derived column aggregates. Omit to leave every
|
|
603
|
+
* format alone.
|
|
604
|
+
*/
|
|
605
|
+
formats?: {
|
|
606
|
+
numFmts: Readonly<Record<string, string | undefined>>;
|
|
607
|
+
measure: string;
|
|
608
|
+
func: AggFunc;
|
|
609
|
+
};
|
|
610
|
+
}): Promise<PivotRefresh | null>;
|
|
611
|
+
/**
|
|
612
|
+
* Change a pivot's RECIPE — what it groups by, what it measures, how, in
|
|
613
|
+
* what order, over which records — and reshape it to match, in one
|
|
614
|
+
* transaction.
|
|
615
|
+
*
|
|
616
|
+
* A recipe is not editable in place by hand: a pivot's numbers are
|
|
617
|
+
* generated from it, its column names ARE the column dimension's values,
|
|
618
|
+
* and its key column holds the row dimension's. Changing any of those by
|
|
619
|
+
* typing produces a table that says one thing and computes another —
|
|
620
|
+
* editing a row label does not re-aim the row, it relabels a group.
|
|
621
|
+
*
|
|
622
|
+
* Deliberately does NOT ask for the current plan. The commonest reason to
|
|
623
|
+
* edit is that the recipe has stopped resolving (a source field renamed
|
|
624
|
+
* out from under it, say), and planning the OLD recipe would fail exactly
|
|
625
|
+
* then. The caller states the block's present size instead, which it can
|
|
626
|
+
* always see.
|
|
627
|
+
*
|
|
628
|
+
* Payload order, for the same reasons as §6 with one addition: the RECIPE
|
|
629
|
+
* goes before the bind, because the bind is what regenerates every cell
|
|
630
|
+
* from it — set it after and the block re-materializes from the old one.
|
|
631
|
+
*
|
|
632
|
+
* grow (if growing) → recipe → keys → bind → shrink (only if shrinking)
|
|
633
|
+
*
|
|
634
|
+
* Returns the shape it produced.
|
|
635
|
+
*/
|
|
636
|
+
editPivot(opts: {
|
|
637
|
+
sheetIdx: number;
|
|
638
|
+
/** The pivot being edited. */
|
|
639
|
+
blockId: number;
|
|
640
|
+
/** Its ref name, which the re-bind has to restate. */
|
|
641
|
+
refName: string;
|
|
642
|
+
/** The block it analyses: what the new recipe is planned against. */
|
|
643
|
+
source: PivotSource;
|
|
644
|
+
/**
|
|
645
|
+
* Its present size, so the reshape knows whether it grows or shrinks.
|
|
646
|
+
* Asked for rather than planned, so a broken recipe can still be
|
|
647
|
+
* fixed — which is most of the point of this method.
|
|
648
|
+
*/
|
|
649
|
+
currentRowCnt: number;
|
|
650
|
+
currentColCnt: number;
|
|
651
|
+
rowDim: string;
|
|
652
|
+
colDim?: string;
|
|
653
|
+
measure: string;
|
|
654
|
+
func: AggFunc;
|
|
655
|
+
order?: DimOrder;
|
|
656
|
+
orderValues?: readonly string[];
|
|
657
|
+
filters?: readonly PivotFilter[];
|
|
658
|
+
extraColumns?: readonly PivotColumnSpec[];
|
|
659
|
+
valueColumn?: string;
|
|
660
|
+
}): Promise<{
|
|
661
|
+
keys: string[];
|
|
662
|
+
fields: string[];
|
|
663
|
+
unassignedRecords: number;
|
|
664
|
+
}>;
|
|
191
665
|
/**
|
|
192
666
|
* Apply a caller-built payload list as one transaction (at the host's
|
|
193
667
|
* temp-mode). Escape hatch for operations whose payload construction still
|
|
@@ -242,6 +716,4 @@ export declare class WorkbookOps {
|
|
|
242
716
|
* interpretValidation instead.
|
|
243
717
|
*/
|
|
244
718
|
checkValidations(rules: readonly ValidationRule[]): Promise<Violation[]>;
|
|
245
|
-
/** Check required / unique / membership field constraints. */
|
|
246
|
-
checkFieldConstraints(columns: readonly FieldColumn[]): Promise<Violation[]>;
|
|
247
719
|
}
|