@operato/shell 10.29.68 → 10.29.70
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/CHANGELOG.md +33 -0
- package/dist/src/app/pages/page-view.d.ts +2 -1
- package/dist/src/app/pages/page-view.js +25 -7
- package/dist/src/app/pages/page-view.js.map +1 -1
- package/dist/src/drafts/draft-controller.d.ts +74 -0
- package/dist/src/drafts/draft-controller.js +164 -0
- package/dist/src/drafts/draft-controller.js.map +1 -0
- package/dist/src/drafts/draft-owner.d.ts +6 -0
- package/dist/src/drafts/draft-owner.js +10 -0
- package/dist/src/drafts/draft-owner.js.map +1 -0
- package/dist/src/drafts/unsaved-draft-controller.d.ts +5 -0
- package/dist/src/drafts/unsaved-draft-controller.js +19 -0
- package/dist/src/drafts/unsaved-draft-controller.js.map +1 -1
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +2 -0
- package/dist/src/index.js.map +1 -1
- package/dist/test/draft-controller.test.d.ts +1 -0
- package/dist/test/draft-controller.test.js +168 -0
- package/dist/test/draft-controller.test.js.map +1 -0
- package/dist/test/draft-owner.test.d.ts +1 -0
- package/dist/test/draft-owner.test.js +100 -0
- package/dist/test/draft-owner.test.js.map +1 -0
- package/dist/test/page-drafts.test.d.ts +1 -0
- package/dist/test/page-drafts.test.js +24 -0
- package/dist/test/page-drafts.test.js.map +1 -0
- package/dist/test/page-refresh-queries.test.d.ts +1 -0
- package/dist/test/page-refresh-queries.test.js +89 -0
- package/dist/test/page-refresh-queries.test.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +6 -6
- package/test/draft-controller.test.ts +186 -0
- package/test/draft-owner.test.ts +109 -0
- package/test/page-drafts.test.ts +23 -0
- package/test/page-refresh-queries.test.ts +96 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,39 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
### [10.29.70](https://github.com/hatiolab/operato/compare/v10.29.69...v10.29.70) (2026-11-07)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @operato/shell
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### [10.29.69](https://github.com/hatiolab/operato/compare/v10.29.68...v10.29.69) (2026-11-07)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### :rocket: New Features
|
|
18
|
+
|
|
19
|
+
* **data-grist:** a grid read gets a signal that its replacement aborts ([d2286fd](https://github.com/hatiolab/operato/commit/d2286fd8bd3c8997d3e517173c4b3685a2cefa61))
|
|
20
|
+
* **graphql:** an edit form is a draft over what was read ([500b6cd](https://github.com/hatiolab/operato/commit/500b6cda8ab8375c1ce4889e70bb4a14e592ea76))
|
|
21
|
+
* **graphql:** query and mutation controllers - a screen declares what it reads ([4b58869](https://github.com/hatiolab/operato/commit/4b5886914613fb81aac5e19166969fde9ce6bd87))
|
|
22
|
+
* **shell:** a draft keeps its edits in this browser - DraftController takes over keep/restore ([2f608f1](https://github.com/hatiolab/operato/commit/2f608f14d66dfe7a60968aae403adb70626cd841))
|
|
23
|
+
* **shell:** a page says its context again when a query it declared answered ([9eba986](https://github.com/hatiolab/operato/commit/9eba9864f4d54543d8cf333109d756cd0af4bc5b))
|
|
24
|
+
* **shell:** a page shown again reads again what it declared ([a7086f7](https://github.com/hatiolab/operato/commit/a7086f7b2bd3f5adce207190f9ec689f1e1f5c3a))
|
|
25
|
+
* **shell:** a read's failure goes to its own callback, under the same guard as its answer ([94c1eb9](https://github.com/hatiolab/operato/commit/94c1eb98dbd6fd231d010a70fee1fee0646da2a6))
|
|
26
|
+
* **shell:** draftOwner - whose drafts this browser keeps, named in one place (ADR-0190) ([c6f5f5f](https://github.com/hatiolab/operato/commit/c6f5f5f5af0985d896afa957e0b8dc947e5ff331))
|
|
27
|
+
* **shell:** page reads and saves - only the last read lands, a save keeps its target ([3f0b357](https://github.com/hatiolab/operato/commit/3f0b3573d2550a63be9f0332e3f46149bc0ba79b))
|
|
28
|
+
* **shell:** page-reads.js subpath - a bundle that needs only PageReads does not take the shell ([5944511](https://github.com/hatiolab/operato/commit/5944511cdff3964a468c789d359343e5b4eccfb0))
|
|
29
|
+
* **utils:** isAbort and quietAbort - an aborted read is the layer's own doing, never an error ([4b10603](https://github.com/hatiolab/operato/commit/4b106034e6afa44d3c0bd6826163fc91c13f3b1f))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
### :bug: Bug Fix
|
|
33
|
+
|
|
34
|
+
* **shell:** a save answering after the form moved clears only the record it was sent from ([4029e45](https://github.com/hatiolab/operato/commit/4029e4528f4da7060468c084f9f5f6d9a8a75422))
|
|
35
|
+
* **shell:** a value typed while a save is out stays an edit, even back to what was read ([1e7b30b](https://github.com/hatiolab/operato/commit/1e7b30b8c5656b7eeafc4a84b0a7dfc09119e4ac))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
6
39
|
### [10.29.68](https://github.com/hatiolab/operato/compare/v10.29.67...v10.29.68) (2026-11-06)
|
|
7
40
|
|
|
8
41
|
**Note:** Version bump only for package @operato/shell
|
|
@@ -21,6 +21,7 @@ export declare class PageView extends LitElement {
|
|
|
21
21
|
/** True only for the current edit's completed IndexedDB write; an unwritten edit still needs protection. */
|
|
22
22
|
get addressEditsKept(): boolean;
|
|
23
23
|
private queryChanged;
|
|
24
|
+
private queryAnswered;
|
|
24
25
|
connectedCallback(): void;
|
|
25
26
|
disconnectedCallback(): void;
|
|
26
27
|
/**
|
|
@@ -41,7 +42,7 @@ export declare class PageView extends LitElement {
|
|
|
41
42
|
get deactivation(): boolean | Parameters<typeof OxPrompt.open>[0];
|
|
42
43
|
canDeactivate(signal?: AbortSignal): Promise<boolean>;
|
|
43
44
|
protected get addressDiscardText(): string;
|
|
44
|
-
/** The page's own unsaved state, apart from its grids (a modeller's model
|
|
45
|
+
/** The page's own unsaved state, apart from its grids: edits in its drafts no browser store keeps (ADR-0190), or a modeller's model. */
|
|
45
46
|
get addressDirty(): boolean;
|
|
46
47
|
protected flushAddressEdits(): void;
|
|
47
48
|
/**
|
|
@@ -3,12 +3,15 @@ import { LitElement } from 'lit';
|
|
|
3
3
|
import { property } from 'lit/decorators.js';
|
|
4
4
|
import isEqual from 'lodash-es/isEqual.js';
|
|
5
5
|
import { i18next } from '@operato/i18n';
|
|
6
|
+
import { QUERY_ANSWERED, refetchWithin } from '@operato/graphql';
|
|
6
7
|
import { OxPrompt } from '@operato/popup/ox-prompt.js';
|
|
7
8
|
import { UPDATE_CONTEXT } from '../../actions/const.js';
|
|
8
9
|
import { store } from '../../store.js';
|
|
9
10
|
import { getPathInfo } from '@operato/utils';
|
|
10
11
|
import { addressController } from '@operato/utils/address.js';
|
|
11
12
|
import { UnsavedDraftController } from '../../drafts/unsaved-draft-controller.js';
|
|
13
|
+
import { draftsOn } from '../../drafts/draft-controller.js';
|
|
14
|
+
import { draftOwner } from '../../drafts/draft-owner.js';
|
|
12
15
|
function diff(after, before) {
|
|
13
16
|
var changed = false;
|
|
14
17
|
var changes = {};
|
|
@@ -39,16 +42,14 @@ export class PageView extends LitElement {
|
|
|
39
42
|
binding = undefined;
|
|
40
43
|
}
|
|
41
44
|
if (!binding && action !== 'leave' && action !== 'discard') {
|
|
42
|
-
const
|
|
43
|
-
const user = state.auth?.user?.id;
|
|
45
|
+
const user = draftOwner();
|
|
44
46
|
const target = JSON.stringify([
|
|
45
47
|
source.id || Array.from(this.renderRoot.querySelectorAll('ox-grist')).indexOf(source),
|
|
46
48
|
query
|
|
47
49
|
]);
|
|
48
50
|
const controller = new UnsavedDraftController(source, {
|
|
49
51
|
address: () => {
|
|
50
|
-
|
|
51
|
-
return user && current.auth?.user?.id === user
|
|
52
|
+
return user && draftOwner() === user
|
|
52
53
|
? { user, domain: getPathInfo(pathname).domain || '', screen: pathname, target }
|
|
53
54
|
: undefined;
|
|
54
55
|
},
|
|
@@ -81,6 +82,12 @@ export class PageView extends LitElement {
|
|
|
81
82
|
if (this.active)
|
|
82
83
|
this.updateContext();
|
|
83
84
|
};
|
|
85
|
+
/* A query the page declared answered (ADR-0190): its title and actions may come from it. */
|
|
86
|
+
this.queryAnswered = async () => {
|
|
87
|
+
await this.updateComplete;
|
|
88
|
+
if (this.active)
|
|
89
|
+
this.updateContext();
|
|
90
|
+
};
|
|
84
91
|
/**
|
|
85
92
|
* Indicates whether the page is currently active.
|
|
86
93
|
*/
|
|
@@ -109,6 +116,7 @@ export class PageView extends LitElement {
|
|
|
109
116
|
super.connectedCallback();
|
|
110
117
|
this.addEventListener('query-state-change', this.queryChanged);
|
|
111
118
|
this.addEventListener('list-draft', this.listDraft);
|
|
119
|
+
this.addEventListener(QUERY_ANSWERED, this.queryAnswered);
|
|
112
120
|
}
|
|
113
121
|
disconnectedCallback() {
|
|
114
122
|
super.disconnectedCallback();
|
|
@@ -120,6 +128,7 @@ export class PageView extends LitElement {
|
|
|
120
128
|
}
|
|
121
129
|
this.listDrafts.clear();
|
|
122
130
|
this.removeEventListener('list-draft', this.listDraft);
|
|
131
|
+
this.removeEventListener(QUERY_ANSWERED, this.queryAnswered);
|
|
123
132
|
}
|
|
124
133
|
/**
|
|
125
134
|
* Whether `change` would discard unsaved edits, so the shell asks first (address-state-ownership §6.4,
|
|
@@ -163,9 +172,9 @@ export class PageView extends LitElement {
|
|
|
163
172
|
get addressDiscardText() {
|
|
164
173
|
return i18next.t('text.unsaved-may-be-lost', { defaultValue: 'anything not saved is lost.' });
|
|
165
174
|
}
|
|
166
|
-
/** The page's own unsaved state, apart from its grids (a modeller's model
|
|
175
|
+
/** The page's own unsaved state, apart from its grids: edits in its drafts no browser store keeps (ADR-0190), or a modeller's model. */
|
|
167
176
|
get addressDirty() {
|
|
168
|
-
return
|
|
177
|
+
return draftsOn(this).some(draft => draft.dirty && !draft.kept);
|
|
169
178
|
}
|
|
170
179
|
flushAddressEdits() {
|
|
171
180
|
const grids = Array.from(this.renderRoot.querySelectorAll('ox-grist'));
|
|
@@ -176,6 +185,7 @@ export class PageView extends LitElement {
|
|
|
176
185
|
* A list query change resets only the url-bound grids: the others keep their edits.
|
|
177
186
|
*/
|
|
178
187
|
discardAddressEdits(change) {
|
|
188
|
+
draftsOn(this).forEach(draft => draft.reset());
|
|
179
189
|
this.listGrids(!!change && !change.route)
|
|
180
190
|
.filter(grid => grid.dirtyRecords?.length)
|
|
181
191
|
.forEach(grid => grid.reset());
|
|
@@ -280,6 +290,10 @@ export class PageView extends LitElement {
|
|
|
280
290
|
await this.pageUpdated(changed, after, before);
|
|
281
291
|
/* active page인 경우에는, page Context 갱신도 필요할 것이다. */
|
|
282
292
|
after.active && this.updateContext();
|
|
293
|
+
/* Shown again: what the page and its sections declared they read (ADR-0190) is read again, as a page reading on
|
|
294
|
+
activation did. */
|
|
295
|
+
if (changed.active === true)
|
|
296
|
+
void refetchWithin(this);
|
|
283
297
|
}
|
|
284
298
|
}
|
|
285
299
|
/**
|
|
@@ -321,7 +335,11 @@ export class PageView extends LitElement {
|
|
|
321
335
|
return true;
|
|
322
336
|
}
|
|
323
337
|
async readAgain(grids) {
|
|
324
|
-
await Promise.all(
|
|
338
|
+
await Promise.all([
|
|
339
|
+
...grids.filter(({ grid, reads }) => reads > 0 && grid.reads === reads).map(({ grid }) => grid.fetch()),
|
|
340
|
+
/* What the page and its sections declared they read (ADR-0190) is read again too. */
|
|
341
|
+
refetchWithin(this)
|
|
342
|
+
]);
|
|
325
343
|
}
|
|
326
344
|
/**
|
|
327
345
|
* Resets the page. Subclasses can override this method to perform custom reset logic.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"page-view.js","sourceRoot":"","sources":["../../../../src/app/pages/page-view.ts"],"names":[],"mappings":";AAAA,OAAO,EAAE,UAAU,EAA+C,MAAM,KAAK,CAAA;AAC7E,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AAC5C,OAAO,OAAO,MAAM,sBAAsB,CAAA;AAC1C,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AACvC,OAAO,EAAE,QAAQ,EAAE,MAAM,6BAA6B,CAAA;AAEtD,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAA;AACvD,OAAO,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAA;AACtC,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAC5C,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAA;AAC7D,OAAO,EAAE,sBAAsB,EAAE,MAAM,0CAA0C,CAAA;AAoBjF,SAAS,IAAI,CAAC,KAAU,EAAE,MAAW;IACnC,IAAI,OAAO,GAAG,KAAK,CAAA;IACnB,IAAI,OAAO,GAA2B,EAAE,CAAA;IAExC,MAAM,CAAC,mBAAmB,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,UAAU,GAAG;QACrD,IAAI,UAAU,GAAG,MAAM,CAAC,GAAG,CAAC,CAAA;QAC5B,IAAI,SAAS,GAAG,KAAK,CAAC,GAAG,CAAC,CAAA;QAE1B,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,SAAS,CAAC,EAAE,CAAC;YACpC,OAAO,CAAC,GAAG,CAAC,GAAG,SAAS,CAAA;YACxB,OAAO,GAAG,IAAI,CAAA;QAChB,CAAC;IACH,CAAC,CAAC,CAAA;IAEF,OAAO,OAAO,IAAI,OAAO,CAAA;AAC3B,CAAC;AAED;;;GAGG;AACH,MAAM,OAAO,QAAS,SAAQ,UAAU;IAAxC;;QAWU,eAAU,GAAG,IAAI,GAAG,EAAmF,CAAA;QAEvG,cAAS,GAAG,CAAC,KAAY,EAAE,EAAE;YACnC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,GACvC,KAMD,CAAC,MAAM,CAAA;YACR,IAAI,OAAO,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;YACzC,IAAI,OAAO,IAAI,OAAO,CAAC,KAAK,KAAK,KAAK,EAAE,CAAC;gBACvC,OAAO,CAAC,UAAU,CAAC,OAAO,EAAE,CAAA;gBAC5B,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;gBAC9B,OAAO,GAAG,SAAS,CAAA;YACrB,CAAC;YACD,IAAI,CAAC,OAAO,IAAI,MAAM,KAAK,OAAO,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;gBAC3D,MAAM,KAAK,GAAG,KAAK,CAAC,QAAQ,EAA2C,CAAA;gBACvE,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,CAAA;gBACjC,MAAM,MAAM,GAAG,IAAI,CAAC,SAAS,CAAC;oBAC5B,MAAM,CAAC,EAAE,IAAI,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,gBAAgB,CAAC,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC;oBACrF,KAAK;iBACN,CAAC,CAAA;gBACF,MAAM,UAAU,GAAG,IAAI,sBAAsB,CAAC,MAAM,EAAE;oBACpD,OAAO,EAAE,GAAG,EAAE;wBACZ,MAAM,OAAO,GAAG,KAAK,CAAC,QAAQ,EAA2C,CAAA;wBACzE,OAAO,IAAI,IAAI,OAAO,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,KAAK,IAAI;4BAC5C,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,CAAC,QAAQ,CAAC,CAAC,MAAM,IAAI,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE;4BAChF,CAAC,CAAC,SAAS,CAAA;oBACf,CAAC;oBACD,QAAQ,EAAE,GAAG,EAAE;wBACb,MAAM,CAAC,UAAU,EAAE,CAAA;wBACnB,OAAO,MAAM,CAAC,aAAa,CAAA;oBAC7B,CAAC;oBACD,aAAa,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,aAAa;oBACzC,OAAO,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC,MAAM,CAAC,mBAAmB,CAAC,KAAK,EAAE,IAAI,CAAC;iBAClE,CAAC,CAAA;gBACF,OAAO,GAAG,EAAE,KAAK,EAAE,UAAU,EAAE,CAAA;gBAC/B,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;YACtC,CAAC;YACD,IAAI,CAAC,OAAO;gBAAE,OAAM;YACpB,IAAI,MAAM,KAAK,SAAS;gBAAE,KAAK,OAAO,CAAC,UAAU,CAAC,OAAO,EAAE,CAAA;YAC3D,IAAI,MAAM,KAAK,MAAM;gBAAE,KAAK,OAAO,CAAC,UAAU,CAAC,KAAK,EAAE,CAAA;YACtD,IAAI,MAAM,KAAK,QAAQ,IAAI,IAAI,CAAC,MAAM,IAAI,QAAQ,KAAK,QAAQ,CAAC,QAAQ;gBAAE,KAAK,OAAO,CAAC,UAAU,CAAC,KAAK,EAAE,CAAA;YACzG,IAAI,MAAM,KAAK,OAAO,EAAE,CAAC;gBACvB,IAAI,MAAM,CAAC,aAAa;oBAAE,KAAK,OAAO,CAAC,UAAU,CAAC,KAAK,EAAE,CAAA;gBACzD,OAAO,CAAC,UAAU,CAAC,OAAO,EAAE,CAAA;gBAC5B,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;YAChC,CAAC;QACH,CAAC,CAAA;QAUO,iBAAY,GAAG,GAAG,EAAE;YAC1B,IAAI,IAAI,CAAC,MAAM;gBAAE,IAAI,CAAC,aAAa,EAAE,CAAA;QACvC,CAAC,CAAA;QAwHD;;WAEG;QAC0B,WAAM,GAAY,KAAK,CAAA;IA4MtD,CAAC;IA/YC;;;OAGG;IACH,KAAK,CAAC,YAAY;QAChB,IAAI,CAAC,IAAI,CAAC,YAAY,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC;YAAE,OAAO,IAAI,CAAA;QAC7D,IAAI,CAAC,CAAC,MAAM,IAAI,CAAC,aAAa,EAAE,CAAC;YAAE,OAAO,KAAK,CAAA;QAC/C,IAAI,CAAC,mBAAmB,EAAE,CAAA;QAC1B,OAAO,IAAI,CAAA;IACb,CAAC;IAqDD,4GAA4G;IAC5G,IAAI,gBAAgB;QAClB,IAAI,CAAC,iBAAiB,EAAE,CAAA;QACxB,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,gBAAgB,CAAC,UAAU,CAAC,CAAsB,CAAA;QAC3F,MAAM,KAAK,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,KAAK,EAAE,GAAG,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,CAAA;QAC/G,OAAO,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,UAAU,CAAC,KAAK,CAAC,CAAA;IACjG,CAAC;IAMD,iBAAiB;QACf,KAAK,CAAC,iBAAiB,EAAE,CAAA;QACzB,IAAI,CAAC,gBAAgB,CAAC,oBAAoB,EAAE,IAAI,CAAC,YAAY,CAAC,CAAA;QAC9D,IAAI,CAAC,gBAAgB,CAAC,YAAY,EAAE,IAAI,CAAC,SAAS,CAAC,CAAA;IACrD,CAAC;IAED,oBAAoB;QAClB,KAAK,CAAC,oBAAoB,EAAE,CAAA;QAC5B,IAAI,CAAC,mBAAmB,CAAC,oBAAoB,EAAE,IAAI,CAAC,YAAY,CAAC,CAAA;QACjE,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;YAChD,IAAI,MAAM,CAAC,aAAa;gBAAE,KAAK,OAAO,CAAC,UAAU,CAAC,KAAK,EAAE,CAAA;YACzD,OAAO,CAAC,UAAU,CAAC,OAAO,EAAE,CAAA;QAC9B,CAAC;QACD,IAAI,CAAC,UAAU,CAAC,KAAK,EAAE,CAAA;QACvB,IAAI,CAAC,mBAAmB,CAAC,YAAY,EAAE,IAAI,CAAC,SAAS,CAAC,CAAA;IACxD,CAAC;IACD;;;;;;OAMG;IACH,QAAQ,CAAC,MAA0C,EAAE,KAAc;QACjE,IAAI,CAAC,MAAM,CAAC,KAAK,IAAI,CAAC,MAAM,CAAC,KAAK;YAAE,OAAO,KAAK,CAAA;QAChD,IAAI,IAAI,CAAC,YAAY;YAAE,OAAO,IAAI,CAAA;QAClC,OAAO,KAAK,IAAI,IAAI,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;IAC/C,CAAC;IAED,qGAAqG;IAC3F,SAAS,CAAC,QAAiB;QACnC,OAAO,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,YAAY,EAAE,MAAM,CAAC,CAAA;IACzE,CAAC;IAEO,SAAS,CAAC,QAAiB;QACjC,IAAI,CAAC,iBAAiB,EAAE,CAAA;QACxB,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,gBAAgB,CAAC,UAAU,CAAC,CAIjE,CAAA;QACJ,OAAO,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC,CAAC,CAAC,KAAK,CAAA;IACzE,CAAC;IAED,4GAA4G;IAC5G,IAAI,YAAY;QACd,IAAI,CAAC,IAAI,CAAC,YAAY,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC;YAAE,OAAO,IAAI,CAAA;QAC7D,OAAO;YACL,IAAI,EAAE,SAAS;YACf,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,qBAAqB,EAAE,EAAE,YAAY,EAAE,0BAA0B,EAAE,CAAC;YACrF,IAAI,EAAE,IAAI,CAAC,kBAAkB;YAC7B,aAAa,EAAE,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,gBAAgB,EAAE,EAAE,YAAY,EAAE,SAAS,EAAE,CAAC,EAAE,WAAW,EAAE,IAAI,EAAE;YACpG,YAAY,EAAE,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,eAAe,EAAE,EAAE,YAAY,EAAE,QAAQ,EAAE,CAAC,EAAE;SAC/E,CAAA;IACH,CAAC;IAED,KAAK,CAAC,aAAa,CAAC,MAAoB;QACtC,MAAM,QAAQ,GAAG,IAAI,CAAC,YAAY,CAAA;QAClC,OAAO,OAAO,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,GAAG,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAA;IAC1F,CAAC;IAED,IAAc,kBAAkB;QAC9B,OAAO,OAAO,CAAC,CAAC,CAAC,0BAA0B,EAAE,EAAE,YAAY,EAAE,6BAA6B,EAAE,CAAC,CAAA;IAC/F,CAAC;IAED,+EAA+E;IAC/E,IAAI,YAAY;QACd,OAAO,KAAK,CAAA;IACd,CAAC;IAES,iBAAiB;QACzB,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,gBAAgB,CAAC,UAAU,CAAC,CAEjE,CAAA;QACJ,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,UAAU,EAAE,EAAE,CAAC,CAAA;IAC5C,CAAC;IAED;;;OAGG;IACH,mBAAmB,CAAC,MAA2B;QAC7C,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC;aACtC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,YAAY,EAAE,MAAM,CAAC;aACzC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,CAAA;IAClC,CAAC;IAED;;;;;OAKG;IACH,YAAY,CAAC,OAA6B;QACxC,IAAI,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,MAAM,CAAA;QAC1C,IAAI,EAAE,MAAM,EAAE,SAAS,GAAG,KAAK,EAAE,GAAG,IAAI,CAAC,kBAAkB,IAAI,EAAE,CAAA;QAEjE;;;;YAII;QACJ,IAAI,MAAM,EAAE,CAAC;YACX,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAA;YAC1B,IAAI,CAAC,MAAM,GAAG,SAAS,CAAA;YACvB,MAAM,MAAM,GAAG,IAAI,CAAC,UAAU,CAAC;gBAC7B,MAAM;aACP,CAAC,CAAA;YACF,IAAI,MAAM;gBAAE,KAAK,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAA;QAC5D,CAAC;aAAM,IAAI,SAAS,EAAE,CAAC;YACrB,IAAI,CAAC,UAAU,CAAC;gBACd,MAAM;aACP,CAAC,CAAA;QACJ,CAAC;QAED,OAAO,MAAM,CAAA;IACf,CAAC;IAwBO,SAAS,CAAC,MAA2B;QAC3C,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,OAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAA;QACnD,GAAG,CAAC,MAAM,GAAG,IAAI,eAAe,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE,CAAA;QACnD,OAAO,iBAAiB,EAAE,CAAC,MAAM,CAAC,EAAE,GAAG,iBAAiB,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,CAAC,CAAA;IAC1E,CAAC;IAED;;;;;OAKG;IACH,KAAK,CAAC,UAAU,CAAC,UAAe,EAAE,EAAE,QAAiB,KAAK;QACxD,IAAI,MAAM,GAAG,IAAI,CAAC,kBAAkB,IAAI,EAAE,CAAA;QAE1C,IAAI,KAAK,GAAG;YACV,GAAG,MAAM;YACT,GAAG,IAAI,CAAC,SAAS;YACjB,WAAW,EAAE,IAAI,CAAC,WAAW;YAC7B,GAAG,OAAO;SACX,CAAA;QAED;;;;WAIG;QACH,IAAI,KAAK,CAAC,MAAM;YAAE,IAAI,CAAC,OAAO,GAAG,QAAQ,CAAC,QAAQ,CAAA;QAClD,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACjB,IAAI,MAAM,CAAC,MAAM;gBAAE,MAAM,GAAG,EAAE,GAAG,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAA;YAChF,IAAI,KAAK,CAAC,MAAM;gBAAE,KAAK,GAAG,EAAE,GAAG,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAA;QAC9E,CAAC;QAED,IAAI,CAAC,CAAC,aAAa,IAAI,OAAO,CAAC,IAAI,KAAK,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC;YACvE,KAAK,CAAC,WAAW,GAAG,IAAI,CAAA;QAC1B,CAAC;QAED,IAAI,KAAK,EAAE,CAAC;YACV,KAAK,CAAC,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA;QAC5B,CAAC;QAED,IAAI,OAAO,GAAG,IAAI,CAAC,KAAK,EAAE,MAAM,CAAC,CAAA;QACjC,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,OAAM;QACR,CAAC;QAED,IAAI,CAAC,kBAAkB,GAAG,KAAK,CAAA;QAE/B,+DAA+D;QAC/D,IAAI,MAAM,CAAC,WAAW,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;YAC9C,MAAM,IAAI,CAAC,SAAS,EAAE,CAAA;YACtB,OAAM;QACR,CAAC;QAED,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;YACxB,MAAM,IAAI,CAAC,eAAe,CAAC,KAAK,CAAC,CAAA;QACnC,CAAC;QAED,IAAI,aAAa,IAAI,OAAO,EAAE,CAAC;YAC7B,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;gBACxB;;;mBAGG;gBACH,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,qBAAqB,CAAC,OAAO,CAAC,CAAC,CAAA;gBAC5D,MAAM,IAAI,CAAC,WAAW,CAAC,OAAO,EAAE,KAAK,EAAE,MAAM,CAAC,CAAA;gBAC9C,kDAAkD;gBAClD,KAAK,CAAC,MAAM,IAAI,IAAI,CAAC,aAAa,EAAE,CAAA;YACtC,CAAC;iBAAM,CAAC;gBACN,MAAM,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,CAAA;YAChC,CAAC;QACH,CAAC;aAAM,CAAC;YACN,MAAM,IAAI,CAAC,WAAW,CAAC,OAAO,EAAE,KAAK,EAAE,MAAM,CAAC,CAAA;YAC9C,kDAAkD;YAClD,KAAK,CAAC,MAAM,IAAI,IAAI,CAAC,aAAa,EAAE,CAAA;QACtC,CAAC;IACH,CAAC;IAED;;;;;;;;;;;;;;;OAeG;IACH,KAAK,CAAC,OAAO,CAAC,EAAE,GAAG,GAAG,IAAI,KAAwB,EAAE;QAClD,IAAI,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,YAAY,IAAI,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;YAAE,OAAO,KAAK,CAAA;QACtE,IAAI,IAAI,CAAC,YAAY,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,gBAAgB,CAAC,EAAE,CAAC;YAC3E,IAAI,CAAC,CAAC,MAAM,IAAI,CAAC,aAAa,EAAE,CAAC;gBAAE,OAAO,KAAK,CAAA;YAC/C,IAAI,CAAC,mBAAmB,EAAE,CAAA;QAC5B,CAAC;QAED,MAAM,KAAK,GAAI,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,gBAAgB,CAAC,UAAU,CAAC,CAAgB,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YAClG,IAAI;YACJ,KAAK,EAAE,IAAI,CAAC,KAAK,IAAI,CAAC;SACvB,CAAC,CAAC,CAAA;QAEH,MAAM,IAAI,CAAC,WAAW,EAAE,CAAA;QACxB,IAAI,CAAC,kBAAkB,GAAG,SAAS,CAAA;QACnC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;YACjB,IAAI,CAAC,MAAM,GAAG,KAAK,CAAA;YACnB,OAAO,IAAI,CAAA;QACb,CAAC;QAED,MAAM,IAAI,CAAC,UAAU,EAAE,CAAA;QACvB,MAAM,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAA;QAC3B,OAAO,IAAI,CAAA;IACb,CAAC;IAKO,KAAK,CAAC,SAAS,CAAC,KAA0C;QAChE,MAAM,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,KAAK,GAAG,CAAC,IAAI,IAAI,CAAC,KAAK,KAAK,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,CAAA;IACzH,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,SAAS;QACb,IAAI,EAAE,WAAW,EAAE,GAAG,IAAI,CAAC,kBAAkB,IAAI,EAAE,CAAA;QAEnD,IAAI,WAAW,EAAE,CAAC;YAChB,MAAM,IAAI,CAAC,WAAW,EAAE,CAAA;YACxB,MAAM,IAAI,CAAC,UAAU,CAAC,EAAE,EAAE,IAAI,CAAC,CAAA;QACjC,CAAC;IACH,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,WAAW;QACf,MAAM,IAAI,CAAC,UAAU,CAAC;YACpB,WAAW,EAAE,KAAK;SACnB,CAAC,CAAA;IACJ,CAAC;IAED;;;OAGG;IACH,eAAe,CAAC,QAAa,IAAG,CAAC;IAEjC;;;;;;OAMG;IACH,WAAW,CAAC,OAAY,EAAE,KAAU,EAAE,MAAW,IAAG,CAAC;IAErD;;;;OAIG;IACH,YAAY,CAAC,QAAa,IAAG,CAAC;IAE9B,aAAa;IACb,aAAa,CAAC,QAAc;QAC1B,KAAK,CAAC,QAAQ,CAAC;YACb,IAAI,EAAE,cAAc;YACpB,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC,EAAE,GAAG,IAAI,CAAC,OAAO,EAAE,GAAG,QAAQ,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO;SACpE,CAAC,CAAA;IACJ,CAAC;IAED;;;;;OAKG;IACH,IAAI,OAAO;QACT,OAAO,EAAE,CAAA;IACX,CAAC;CACF;AA5M8B;IAA5B,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;wCAAwB;AAKxB;IAA3B,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;2CAA0B;AAKE;IAAtD,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,cAAc,EAAE,CAAC;6CAAqB;AAoM7E,oHAAoH;AACpH,MAAM,UAAU,OAAO,CAAC,IAAc,EAAE,KAAU,EAAE,MAAW;IAC7D,IAAI,KAAK,CAAC,MAAM,KAAK,MAAM,CAAC,MAAM,IAAI,KAAK,CAAC,QAAQ,KAAK,MAAM,CAAC,QAAQ;QAAE,OAAO,IAAI,CAAA;IACrF,MAAM,IAAI,GAAG,kBAAkB,CAAC,WAAW,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,aAAa,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAA;IACvG,OAAO,CAAC,CAAC,IAAI,IAAI,IAAI,CAAC,YAAY,CAAC,WAAW,CAAC,KAAK,IAAI,CAAA;AAC1D,CAAC","sourcesContent":["import { LitElement, PropertyValues, type ReactiveControllerHost } from 'lit'\nimport { property } from 'lit/decorators.js'\nimport isEqual from 'lodash-es/isEqual.js'\nimport { i18next } from '@operato/i18n'\nimport { OxPrompt } from '@operato/popup/ox-prompt.js'\n\nimport { UPDATE_CONTEXT } from '../../actions/const.js'\nimport { store } from '../../store.js'\nimport { getPathInfo } from '@operato/utils'\nimport { addressController } from '@operato/utils/address.js'\nimport { UnsavedDraftController } from '../../drafts/unsaved-draft-controller.js'\n\ntype ListDraftSource = HTMLElement &\n ReactiveControllerHost & {\n draftSnapshot?: { value: unknown; base: string | number | null }\n draftRevision: string | number | null\n dirtyRecords?: unknown[]\n restoreUnsavedDraft(value: unknown, base: string | number | null): void\n flushEdits(): void\n }\n\ntype ReadGrid = HTMLElement & { reads?: number; fetch(): Promise<void> }\n\nexport type PageLifecycle = {\n active: boolean\n params: object\n resourceId?: string\n page?: string\n}\n\nfunction diff(after: any, before: any): any {\n var changed = false\n var changes: { [key: string]: any } = {}\n\n Object.getOwnPropertyNames(after).forEach(function (key) {\n let before_val = before[key]\n let after_val = after[key]\n\n if (!isEqual(before_val, after_val)) {\n changes[key] = after_val\n changed = true\n }\n })\n\n return changed && changes\n}\n\n/**\n * PageView is a base class for creating page elements with lifecycle management.\n * Subclasses can extend PageView to define custom behavior and handle page lifecycle events.\n */\nexport class PageView extends LitElement {\n /**\n * Closing this page instance (an MDI tab) destroys it, so its unsaved edits are asked about first: the bar\n * removes the tab only on yes. A page switch only caches the instance and asks nothing.\n */\n async confirmClose(): Promise<boolean> {\n if (!this.addressDirty && !this.listDirty(false)) return true\n if (!(await this.canDeactivate())) return false\n this.discardAddressEdits()\n return true\n }\n private listDrafts = new Map<ListDraftSource, { query: string; controller: UnsavedDraftController<unknown> }>()\n\n private listDraft = (event: Event) => {\n const { source, query, action, pathname } = (\n event as CustomEvent<{\n source: ListDraftSource\n query: string\n pathname: string\n action: 'edit' | 'loaded' | 'leave' | 'discard'\n }>\n ).detail\n let binding = this.listDrafts.get(source)\n if (binding && binding.query !== query) {\n binding.controller.dispose()\n this.listDrafts.delete(source)\n binding = undefined\n }\n if (!binding && action !== 'leave' && action !== 'discard') {\n const state = store.getState() as { auth?: { user?: { id?: string } } }\n const user = state.auth?.user?.id\n const target = JSON.stringify([\n source.id || Array.from(this.renderRoot.querySelectorAll('ox-grist')).indexOf(source),\n query\n ])\n const controller = new UnsavedDraftController(source, {\n address: () => {\n const current = store.getState() as { auth?: { user?: { id?: string } } }\n return user && current.auth?.user?.id === user\n ? { user, domain: getPathInfo(pathname).domain || '', screen: pathname, target }\n : undefined\n },\n snapshot: () => {\n source.flushEdits()\n return source.draftSnapshot\n },\n savedRevision: () => source.draftRevision,\n restore: (value, base) => source.restoreUnsavedDraft(value, base)\n })\n binding = { query, controller }\n this.listDrafts.set(source, binding)\n }\n if (!binding) return\n if (action === 'discard') void binding.controller.settled()\n if (action === 'edit') void binding.controller.flush()\n if (action === 'loaded' && this.active && pathname === location.pathname) void binding.controller.offer()\n if (action === 'leave') {\n if (source.draftSnapshot) void binding.controller.flush()\n binding.controller.dispose()\n this.listDrafts.delete(source)\n }\n }\n\n /** True only for the current edit's completed IndexedDB write; an unwritten edit still needs protection. */\n get addressEditsKept(): boolean {\n this.flushAddressEdits()\n const grids = Array.from(this.renderRoot.querySelectorAll('ox-grist')) as ListDraftSource[]\n const dirty = [...new Set([...grids, ...this.listDrafts.keys()])].filter(source => source.dirtyRecords?.length)\n return dirty.length > 0 && dirty.every(source => this.listDrafts.get(source)?.controller.keeps)\n }\n\n private queryChanged = () => {\n if (this.active) this.updateContext()\n }\n\n connectedCallback() {\n super.connectedCallback()\n this.addEventListener('query-state-change', this.queryChanged)\n this.addEventListener('list-draft', this.listDraft)\n }\n\n disconnectedCallback() {\n super.disconnectedCallback()\n this.removeEventListener('query-state-change', this.queryChanged)\n for (const [source, binding] of this.listDrafts) {\n if (source.draftSnapshot) void binding.controller.flush()\n binding.controller.dispose()\n }\n this.listDrafts.clear()\n this.removeEventListener('list-draft', this.listDraft)\n }\n /**\n * Whether `change` would discard unsaved edits, so the shell asks first (address-state-ownership §6.4,\n * 2026-10-19). `stays`: this page instance stays on screen. Grid edits stay in the cached page when another\n * page is shown; they are lost when this page gets new route arguments (every grid reloads) or a new list\n * query (the url-bound grids reload). A page's own unsaved state (`addressDirty`) is asked about whenever the\n * page is left or changed, as `canDeactivate` was before 2026-10-17.\n */\n discards(change: { route: boolean; query: boolean }, stays: boolean): boolean {\n if (!change.route && !change.query) return false\n if (this.addressDirty) return true\n return stays && this.listDirty(!change.route)\n }\n\n /** Whether the page's grids hold unsaved edits - only those bound to the address when `urlBound`. */\n protected listDirty(urlBound: boolean): boolean {\n return this.listGrids(urlBound).some(grid => grid.dirtyRecords?.length)\n }\n\n private listGrids(urlBound: boolean) {\n this.flushAddressEdits()\n const grids = Array.from(this.renderRoot.querySelectorAll('ox-grist')) as (HTMLElement & {\n dirtyRecords?: unknown[]\n urlParamsSensitive?: boolean\n reset(): void\n })[]\n return urlBound ? grids.filter(grid => grid.urlParamsSensitive) : grids\n }\n\n /** The question asked when `discards` says so: asked before an app request, or at a traversal's arrival. */\n get deactivation(): boolean | Parameters<typeof OxPrompt.open>[0] {\n if (!this.addressDirty && !this.listDirty(false)) return true\n return {\n type: 'warning',\n title: i18next.t('title.unsaved-draft', { defaultValue: 'there is an unsaved edit' }),\n text: this.addressDiscardText,\n confirmButton: { text: i18next.t('button.discard', { defaultValue: 'discard' }), destructive: true },\n cancelButton: { text: i18next.t('button.cancel', { defaultValue: 'cancel' }) }\n }\n }\n\n async canDeactivate(signal?: AbortSignal): Promise<boolean> {\n const decision = this.deactivation\n return typeof decision === 'boolean' ? decision : OxPrompt.open({ ...decision, signal })\n }\n\n protected get addressDiscardText(): string {\n return i18next.t('text.unsaved-may-be-lost', { defaultValue: 'anything not saved is lost.' })\n }\n\n /** The page's own unsaved state, apart from its grids (a modeller's model). */\n get addressDirty(): boolean {\n return false\n }\n\n protected flushAddressEdits(): void {\n const grids = Array.from(this.renderRoot.querySelectorAll('ox-grist')) as (HTMLElement & {\n flushEdits?: () => void\n })[]\n grids.forEach(grid => grid.flushEdits?.())\n }\n\n /**\n * Runs only once a change `discards` approved has committed, never while a question is pending or canceled.\n * A list query change resets only the url-bound grids: the others keep their edits.\n */\n discardAddressEdits(change?: { route: boolean }): void {\n this.listGrids(!!change && !change.route)\n .filter(grid => grid.dirtyRecords?.length)\n .forEach(grid => grid.reset())\n }\n\n /**\n * Determines whether the page should update. This method is called whenever there are\n * changes to the page's properties.\n * @param changes - A map of changed property values.\n * @returns True if the page should update, or false otherwise.\n */\n shouldUpdate(changes: PropertyValues<this>) {\n var active = String(this.active) == 'true'\n var { active: oldActive = false } = this._oldLifecycleInfo$ || {}\n\n /*\n * page lifecycle\n * case 1. page가 새로 activate 되었다.\n * case 2. page가 active 상태에서 lifecycle 정보가 바뀌었다.\n **/\n if (active) {\n const unread = this.unread\n this.unread = undefined\n const played = this.pageUpdate({\n active\n })\n if (unread) void played.then(() => this.readAgain(unread))\n } else if (oldActive) {\n this.pageUpdate({\n active\n })\n }\n\n return active\n }\n\n /**\n * Indicates whether the page is currently active.\n */\n @property({ type: Boolean }) active: boolean = false\n\n /**\n * Stores information about the page's lifecycle.\n */\n @property({ type: Object }) lifecycle!: PageLifecycle\n\n /**\n * The context path for the page.\n */\n @property({ type: String, attribute: 'context-path' }) contextPath?: string\n\n _oldLifecycleInfo$: any\n\n /* lifecycle */\n\n /** The path this page is shown at: its route arguments are what that path's claims leave of the address. */\n private shownAt?: string\n\n private unclaimed(params: Record<string, any>): Record<string, string> {\n const url = new URL(this.shownAt!, location.origin)\n url.search = new URLSearchParams(params).toString()\n return addressController().params({ ...addressController().entry, url })\n }\n\n /**\n * Handles page updates and lifecycle events. Subclasses can override this method\n * to implement custom logic for initializing, updating, and disposing of the page.\n * @param changes - A map of changed properties.\n * @param force - If true, forces an update of the page.\n */\n async pageUpdate(changes: any = {}, force: boolean = false) {\n var before = this._oldLifecycleInfo$ || {}\n\n var after = {\n ...before,\n ...this.lifecycle,\n contextPath: this.contextPath,\n ...changes\n }\n\n /*\n * Route arguments are compared under today's claims. A grid or tab claims its keys only after the page is drawn,\n * so the page opened by an address with `filters` stored them as an argument; on the next route dispatch they\n * are none. Counting that as a change made a screen read its list again and drop the edits it held.\n */\n if (after.active) this.shownAt = location.pathname\n if (this.shownAt) {\n if (before.params) before = { ...before, params: this.unclaimed(before.params) }\n if (after.params) after = { ...after, params: this.unclaimed(after.params) }\n }\n\n if (!('initialized' in changes) && after.active && !before.initialized) {\n after.initialized = true\n }\n\n if (force) {\n after.updated = Date.now()\n }\n\n var changed = diff(after, before)\n if (!changed) {\n return\n }\n\n this._oldLifecycleInfo$ = after\n\n /* page의 이미 초기화된 상태에서 contextPath가 바뀐다면, 무조건 page가 리셋되어야 한다. */\n if (before.initialized && changed.contextPath) {\n await this.pageReset()\n return\n }\n\n if (changed.initialized) {\n await this.pageInitialized(after)\n }\n\n if ('initialized' in changed) {\n if (changed.initialized) {\n /*\n * 방금 초기화된 경우라면, 엘리먼트들이 만들어지지 않았을 가능성이 있으므로,\n * 다음 animationFrame에서 pageUpdated 콜백을 호출한다.\n */\n await new Promise(resolve => requestAnimationFrame(resolve))\n await this.pageUpdated(changed, after, before)\n /* active page인 경우에는, page Context 갱신도 필요할 것이다. */\n after.active && this.updateContext()\n } else {\n await this.pageDisposed(after)\n }\n } else {\n await this.pageUpdated(changed, after, before)\n /* active page인 경우에는, page Context 갱신도 필요할 것이다. */\n after.active && this.updateContext()\n }\n }\n\n /**\n * Reads the page again, as if it had just been opened at the address it shows (ADR-0174). The tab menu's\n * refresh calls this; a page draws no refresh control of its own.\n *\n * Unsaved edits follow the rule of a route change on the page on screen: grid edits a draft keeps are offered\n * back once the grid has read again; anything else is asked about first, and on \"keep\" nothing happens.\n *\n * The lifecycle is played from nothing, so `pageInitialized` runs and `pageUpdated` sees `initialized`,\n * `active`, `params` and `resourceId` all changed. A page not on screen plays it when it is next shown.\n * A grid that had been read and that the page did not read again in that play reads again itself.\n *\n * A refresh nobody asked for (a record changed elsewhere) passes `ask: false`: it shows nothing the user did not\n * ask for, so with unsaved edits of any kind - even ones a draft keeps - it reads nothing.\n *\n * @returns false when the user kept the unsaved edits, or when `ask: false` met unsaved edits.\n */\n async refresh({ ask = true }: { ask?: boolean } = {}): Promise<boolean> {\n if (!ask && (this.addressDirty || this.listDirty(false))) return false\n if (this.addressDirty || (this.listDirty(false) && !this.addressEditsKept)) {\n if (!(await this.canDeactivate())) return false\n this.discardAddressEdits()\n }\n\n const grids = (Array.from(this.renderRoot.querySelectorAll('ox-grist')) as ReadGrid[]).map(grid => ({\n grid,\n reads: grid.reads || 0\n }))\n\n await this.pageDispose()\n this._oldLifecycleInfo$ = undefined\n if (!this.active) {\n this.unread = grids\n return true\n }\n\n await this.pageUpdate()\n await this.readAgain(grids)\n return true\n }\n\n /** The grids of a refresh asked for while the page was not on screen: read once it is shown. */\n private unread?: { grid: ReadGrid; reads: number }[]\n\n private async readAgain(grids: { grid: ReadGrid; reads: number }[]) {\n await Promise.all(grids.filter(({ grid, reads }) => reads > 0 && grid.reads === reads).map(({ grid }) => grid.fetch()))\n }\n\n /**\n * Resets the page. Subclasses can override this method to perform custom reset logic.\n */\n async pageReset() {\n var { initialized } = this._oldLifecycleInfo$ || {}\n\n if (initialized) {\n await this.pageDispose()\n await this.pageUpdate({}, true)\n }\n }\n\n /**\n * Disposes of the page. Subclasses can override this method to perform custom disposal logic.\n */\n async pageDispose() {\n await this.pageUpdate({\n initialized: false\n })\n }\n\n /**\n * Initializes the page. Subclasses can override this method to perform custom initialization logic.\n * @param pageInfo - Information about the page's state.\n */\n pageInitialized(pageInfo: any) {}\n\n /**\n * Handles page updates and changes in properties.\n * Subclasses can override this method to implement custom update logic.\n * @param changes - A map of changed properties.\n * @param after - The current state of the page.\n * @param before - The previous state of the page.\n */\n pageUpdated(changes: any, after: any, before: any) {}\n\n /**\n * Handles the disposal of the page. Subclasses can override this method\n * to implement custom disposal logic.\n * @param pageInfo - Information about the page's state.\n */\n pageDisposed(pageInfo: any) {}\n\n /* context */\n updateContext(override?: any) {\n store.dispatch({\n type: UPDATE_CONTEXT,\n context: override ? { ...this.context, ...override } : this.context\n })\n }\n\n /**\n * Updates the context of the page. Subclasses can override the `context` getter\n * to provide specific context information for the page. The context will be updated\n * using the `updateContext` method inherited from PageView.\n * @param override - An optional object with context properties to override.\n */\n get context() {\n return {}\n }\n}\n\n/** Whether the page shown at `before` stays on screen at `after`: the same path, or a path naming the same page. */\nexport function staysOn(page: PageView, after: URL, before: URL): boolean {\n if (after.origin === before.origin && after.pathname === before.pathname) return true\n const name = decodeURIComponent(getPathInfo(after.pathname).path || '').match(/^\\/([^\\/]+)/)?.[1] || ''\n return !!name && page.getAttribute('data-page') === name\n}\n"]}
|
|
1
|
+
{"version":3,"file":"page-view.js","sourceRoot":"","sources":["../../../../src/app/pages/page-view.ts"],"names":[],"mappings":"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","sourcesContent":["import { LitElement, PropertyValues, type ReactiveControllerHost } from 'lit'\nimport { property } from 'lit/decorators.js'\nimport isEqual from 'lodash-es/isEqual.js'\nimport { i18next } from '@operato/i18n'\nimport { QUERY_ANSWERED, refetchWithin } from '@operato/graphql'\nimport { OxPrompt } from '@operato/popup/ox-prompt.js'\n\nimport { UPDATE_CONTEXT } from '../../actions/const.js'\nimport { store } from '../../store.js'\nimport { getPathInfo } from '@operato/utils'\nimport { addressController } from '@operato/utils/address.js'\nimport { UnsavedDraftController } from '../../drafts/unsaved-draft-controller.js'\nimport { draftsOn } from '../../drafts/draft-controller.js'\nimport { draftOwner } from '../../drafts/draft-owner.js'\n\ntype ListDraftSource = HTMLElement &\n ReactiveControllerHost & {\n draftSnapshot?: { value: unknown; base: string | number | null }\n draftRevision: string | number | null\n dirtyRecords?: unknown[]\n restoreUnsavedDraft(value: unknown, base: string | number | null): void\n flushEdits(): void\n }\n\ntype ReadGrid = HTMLElement & { reads?: number; fetch(): Promise<void> }\n\nexport type PageLifecycle = {\n active: boolean\n params: object\n resourceId?: string\n page?: string\n}\n\nfunction diff(after: any, before: any): any {\n var changed = false\n var changes: { [key: string]: any } = {}\n\n Object.getOwnPropertyNames(after).forEach(function (key) {\n let before_val = before[key]\n let after_val = after[key]\n\n if (!isEqual(before_val, after_val)) {\n changes[key] = after_val\n changed = true\n }\n })\n\n return changed && changes\n}\n\n/**\n * PageView is a base class for creating page elements with lifecycle management.\n * Subclasses can extend PageView to define custom behavior and handle page lifecycle events.\n */\nexport class PageView extends LitElement {\n /**\n * Closing this page instance (an MDI tab) destroys it, so its unsaved edits are asked about first: the bar\n * removes the tab only on yes. A page switch only caches the instance and asks nothing.\n */\n async confirmClose(): Promise<boolean> {\n if (!this.addressDirty && !this.listDirty(false)) return true\n if (!(await this.canDeactivate())) return false\n this.discardAddressEdits()\n return true\n }\n private listDrafts = new Map<ListDraftSource, { query: string; controller: UnsavedDraftController<unknown> }>()\n\n private listDraft = (event: Event) => {\n const { source, query, action, pathname } = (\n event as CustomEvent<{\n source: ListDraftSource\n query: string\n pathname: string\n action: 'edit' | 'loaded' | 'leave' | 'discard'\n }>\n ).detail\n let binding = this.listDrafts.get(source)\n if (binding && binding.query !== query) {\n binding.controller.dispose()\n this.listDrafts.delete(source)\n binding = undefined\n }\n if (!binding && action !== 'leave' && action !== 'discard') {\n const user = draftOwner()\n const target = JSON.stringify([\n source.id || Array.from(this.renderRoot.querySelectorAll('ox-grist')).indexOf(source),\n query\n ])\n const controller = new UnsavedDraftController(source, {\n address: () => {\n return user && draftOwner() === user\n ? { user, domain: getPathInfo(pathname).domain || '', screen: pathname, target }\n : undefined\n },\n snapshot: () => {\n source.flushEdits()\n return source.draftSnapshot\n },\n savedRevision: () => source.draftRevision,\n restore: (value, base) => source.restoreUnsavedDraft(value, base)\n })\n binding = { query, controller }\n this.listDrafts.set(source, binding)\n }\n if (!binding) return\n if (action === 'discard') void binding.controller.settled()\n if (action === 'edit') void binding.controller.flush()\n if (action === 'loaded' && this.active && pathname === location.pathname) void binding.controller.offer()\n if (action === 'leave') {\n if (source.draftSnapshot) void binding.controller.flush()\n binding.controller.dispose()\n this.listDrafts.delete(source)\n }\n }\n\n /** True only for the current edit's completed IndexedDB write; an unwritten edit still needs protection. */\n get addressEditsKept(): boolean {\n this.flushAddressEdits()\n const grids = Array.from(this.renderRoot.querySelectorAll('ox-grist')) as ListDraftSource[]\n const dirty = [...new Set([...grids, ...this.listDrafts.keys()])].filter(source => source.dirtyRecords?.length)\n return dirty.length > 0 && dirty.every(source => this.listDrafts.get(source)?.controller.keeps)\n }\n\n private queryChanged = () => {\n if (this.active) this.updateContext()\n }\n\n /* A query the page declared answered (ADR-0190): its title and actions may come from it. */\n private queryAnswered = async () => {\n await this.updateComplete\n if (this.active) this.updateContext()\n }\n\n connectedCallback() {\n super.connectedCallback()\n this.addEventListener('query-state-change', this.queryChanged)\n this.addEventListener('list-draft', this.listDraft)\n this.addEventListener(QUERY_ANSWERED, this.queryAnswered)\n }\n\n disconnectedCallback() {\n super.disconnectedCallback()\n this.removeEventListener('query-state-change', this.queryChanged)\n for (const [source, binding] of this.listDrafts) {\n if (source.draftSnapshot) void binding.controller.flush()\n binding.controller.dispose()\n }\n this.listDrafts.clear()\n this.removeEventListener('list-draft', this.listDraft)\n this.removeEventListener(QUERY_ANSWERED, this.queryAnswered)\n }\n /**\n * Whether `change` would discard unsaved edits, so the shell asks first (address-state-ownership §6.4,\n * 2026-10-19). `stays`: this page instance stays on screen. Grid edits stay in the cached page when another\n * page is shown; they are lost when this page gets new route arguments (every grid reloads) or a new list\n * query (the url-bound grids reload). A page's own unsaved state (`addressDirty`) is asked about whenever the\n * page is left or changed, as `canDeactivate` was before 2026-10-17.\n */\n discards(change: { route: boolean; query: boolean }, stays: boolean): boolean {\n if (!change.route && !change.query) return false\n if (this.addressDirty) return true\n return stays && this.listDirty(!change.route)\n }\n\n /** Whether the page's grids hold unsaved edits - only those bound to the address when `urlBound`. */\n protected listDirty(urlBound: boolean): boolean {\n return this.listGrids(urlBound).some(grid => grid.dirtyRecords?.length)\n }\n\n private listGrids(urlBound: boolean) {\n this.flushAddressEdits()\n const grids = Array.from(this.renderRoot.querySelectorAll('ox-grist')) as (HTMLElement & {\n dirtyRecords?: unknown[]\n urlParamsSensitive?: boolean\n reset(): void\n })[]\n return urlBound ? grids.filter(grid => grid.urlParamsSensitive) : grids\n }\n\n /** The question asked when `discards` says so: asked before an app request, or at a traversal's arrival. */\n get deactivation(): boolean | Parameters<typeof OxPrompt.open>[0] {\n if (!this.addressDirty && !this.listDirty(false)) return true\n return {\n type: 'warning',\n title: i18next.t('title.unsaved-draft', { defaultValue: 'there is an unsaved edit' }),\n text: this.addressDiscardText,\n confirmButton: { text: i18next.t('button.discard', { defaultValue: 'discard' }), destructive: true },\n cancelButton: { text: i18next.t('button.cancel', { defaultValue: 'cancel' }) }\n }\n }\n\n async canDeactivate(signal?: AbortSignal): Promise<boolean> {\n const decision = this.deactivation\n return typeof decision === 'boolean' ? decision : OxPrompt.open({ ...decision, signal })\n }\n\n protected get addressDiscardText(): string {\n return i18next.t('text.unsaved-may-be-lost', { defaultValue: 'anything not saved is lost.' })\n }\n\n /** The page's own unsaved state, apart from its grids: edits in its drafts no browser store keeps (ADR-0190), or a modeller's model. */\n get addressDirty(): boolean {\n return draftsOn(this).some(draft => draft.dirty && !draft.kept)\n }\n\n protected flushAddressEdits(): void {\n const grids = Array.from(this.renderRoot.querySelectorAll('ox-grist')) as (HTMLElement & {\n flushEdits?: () => void\n })[]\n grids.forEach(grid => grid.flushEdits?.())\n }\n\n /**\n * Runs only once a change `discards` approved has committed, never while a question is pending or canceled.\n * A list query change resets only the url-bound grids: the others keep their edits.\n */\n discardAddressEdits(change?: { route: boolean }): void {\n draftsOn(this).forEach(draft => draft.reset())\n this.listGrids(!!change && !change.route)\n .filter(grid => grid.dirtyRecords?.length)\n .forEach(grid => grid.reset())\n }\n\n /**\n * Determines whether the page should update. This method is called whenever there are\n * changes to the page's properties.\n * @param changes - A map of changed property values.\n * @returns True if the page should update, or false otherwise.\n */\n shouldUpdate(changes: PropertyValues<this>) {\n var active = String(this.active) == 'true'\n var { active: oldActive = false } = this._oldLifecycleInfo$ || {}\n\n /*\n * page lifecycle\n * case 1. page가 새로 activate 되었다.\n * case 2. page가 active 상태에서 lifecycle 정보가 바뀌었다.\n **/\n if (active) {\n const unread = this.unread\n this.unread = undefined\n const played = this.pageUpdate({\n active\n })\n if (unread) void played.then(() => this.readAgain(unread))\n } else if (oldActive) {\n this.pageUpdate({\n active\n })\n }\n\n return active\n }\n\n /**\n * Indicates whether the page is currently active.\n */\n @property({ type: Boolean }) active: boolean = false\n\n /**\n * Stores information about the page's lifecycle.\n */\n @property({ type: Object }) lifecycle!: PageLifecycle\n\n /**\n * The context path for the page.\n */\n @property({ type: String, attribute: 'context-path' }) contextPath?: string\n\n _oldLifecycleInfo$: any\n\n /* lifecycle */\n\n /** The path this page is shown at: its route arguments are what that path's claims leave of the address. */\n private shownAt?: string\n\n private unclaimed(params: Record<string, any>): Record<string, string> {\n const url = new URL(this.shownAt!, location.origin)\n url.search = new URLSearchParams(params).toString()\n return addressController().params({ ...addressController().entry, url })\n }\n\n /**\n * Handles page updates and lifecycle events. Subclasses can override this method\n * to implement custom logic for initializing, updating, and disposing of the page.\n * @param changes - A map of changed properties.\n * @param force - If true, forces an update of the page.\n */\n async pageUpdate(changes: any = {}, force: boolean = false) {\n var before = this._oldLifecycleInfo$ || {}\n\n var after = {\n ...before,\n ...this.lifecycle,\n contextPath: this.contextPath,\n ...changes\n }\n\n /*\n * Route arguments are compared under today's claims. A grid or tab claims its keys only after the page is drawn,\n * so the page opened by an address with `filters` stored them as an argument; on the next route dispatch they\n * are none. Counting that as a change made a screen read its list again and drop the edits it held.\n */\n if (after.active) this.shownAt = location.pathname\n if (this.shownAt) {\n if (before.params) before = { ...before, params: this.unclaimed(before.params) }\n if (after.params) after = { ...after, params: this.unclaimed(after.params) }\n }\n\n if (!('initialized' in changes) && after.active && !before.initialized) {\n after.initialized = true\n }\n\n if (force) {\n after.updated = Date.now()\n }\n\n var changed = diff(after, before)\n if (!changed) {\n return\n }\n\n this._oldLifecycleInfo$ = after\n\n /* page의 이미 초기화된 상태에서 contextPath가 바뀐다면, 무조건 page가 리셋되어야 한다. */\n if (before.initialized && changed.contextPath) {\n await this.pageReset()\n return\n }\n\n if (changed.initialized) {\n await this.pageInitialized(after)\n }\n\n if ('initialized' in changed) {\n if (changed.initialized) {\n /*\n * 방금 초기화된 경우라면, 엘리먼트들이 만들어지지 않았을 가능성이 있으므로,\n * 다음 animationFrame에서 pageUpdated 콜백을 호출한다.\n */\n await new Promise(resolve => requestAnimationFrame(resolve))\n await this.pageUpdated(changed, after, before)\n /* active page인 경우에는, page Context 갱신도 필요할 것이다. */\n after.active && this.updateContext()\n } else {\n await this.pageDisposed(after)\n }\n } else {\n await this.pageUpdated(changed, after, before)\n /* active page인 경우에는, page Context 갱신도 필요할 것이다. */\n after.active && this.updateContext()\n /* Shown again: what the page and its sections declared they read (ADR-0190) is read again, as a page reading on\n activation did. */\n if (changed.active === true) void refetchWithin(this)\n }\n }\n\n /**\n * Reads the page again, as if it had just been opened at the address it shows (ADR-0174). The tab menu's\n * refresh calls this; a page draws no refresh control of its own.\n *\n * Unsaved edits follow the rule of a route change on the page on screen: grid edits a draft keeps are offered\n * back once the grid has read again; anything else is asked about first, and on \"keep\" nothing happens.\n *\n * The lifecycle is played from nothing, so `pageInitialized` runs and `pageUpdated` sees `initialized`,\n * `active`, `params` and `resourceId` all changed. A page not on screen plays it when it is next shown.\n * A grid that had been read and that the page did not read again in that play reads again itself.\n *\n * A refresh nobody asked for (a record changed elsewhere) passes `ask: false`: it shows nothing the user did not\n * ask for, so with unsaved edits of any kind - even ones a draft keeps - it reads nothing.\n *\n * @returns false when the user kept the unsaved edits, or when `ask: false` met unsaved edits.\n */\n async refresh({ ask = true }: { ask?: boolean } = {}): Promise<boolean> {\n if (!ask && (this.addressDirty || this.listDirty(false))) return false\n if (this.addressDirty || (this.listDirty(false) && !this.addressEditsKept)) {\n if (!(await this.canDeactivate())) return false\n this.discardAddressEdits()\n }\n\n const grids = (Array.from(this.renderRoot.querySelectorAll('ox-grist')) as ReadGrid[]).map(grid => ({\n grid,\n reads: grid.reads || 0\n }))\n\n await this.pageDispose()\n this._oldLifecycleInfo$ = undefined\n if (!this.active) {\n this.unread = grids\n return true\n }\n\n await this.pageUpdate()\n await this.readAgain(grids)\n return true\n }\n\n /** The grids of a refresh asked for while the page was not on screen: read once it is shown. */\n private unread?: { grid: ReadGrid; reads: number }[]\n\n private async readAgain(grids: { grid: ReadGrid; reads: number }[]) {\n await Promise.all([\n ...grids.filter(({ grid, reads }) => reads > 0 && grid.reads === reads).map(({ grid }) => grid.fetch()),\n /* What the page and its sections declared they read (ADR-0190) is read again too. */\n refetchWithin(this)\n ])\n }\n\n /**\n * Resets the page. Subclasses can override this method to perform custom reset logic.\n */\n async pageReset() {\n var { initialized } = this._oldLifecycleInfo$ || {}\n\n if (initialized) {\n await this.pageDispose()\n await this.pageUpdate({}, true)\n }\n }\n\n /**\n * Disposes of the page. Subclasses can override this method to perform custom disposal logic.\n */\n async pageDispose() {\n await this.pageUpdate({\n initialized: false\n })\n }\n\n /**\n * Initializes the page. Subclasses can override this method to perform custom initialization logic.\n * @param pageInfo - Information about the page's state.\n */\n pageInitialized(pageInfo: any) {}\n\n /**\n * Handles page updates and changes in properties.\n * Subclasses can override this method to implement custom update logic.\n * @param changes - A map of changed properties.\n * @param after - The current state of the page.\n * @param before - The previous state of the page.\n */\n pageUpdated(changes: any, after: any, before: any) {}\n\n /**\n * Handles the disposal of the page. Subclasses can override this method\n * to implement custom disposal logic.\n * @param pageInfo - Information about the page's state.\n */\n pageDisposed(pageInfo: any) {}\n\n /* context */\n updateContext(override?: any) {\n store.dispatch({\n type: UPDATE_CONTEXT,\n context: override ? { ...this.context, ...override } : this.context\n })\n }\n\n /**\n * Updates the context of the page. Subclasses can override the `context` getter\n * to provide specific context information for the page. The context will be updated\n * using the `updateContext` method inherited from PageView.\n * @param override - An optional object with context properties to override.\n */\n get context() {\n return {}\n }\n}\n\n/** Whether the page shown at `before` stays on screen at `after`: the same path, or a path naming the same page. */\nexport function staysOn(page: PageView, after: URL, before: URL): boolean {\n if (after.origin === before.origin && after.pathname === before.pathname) return true\n const name = decodeURIComponent(getPathInfo(after.pathname).path || '').match(/^\\/([^\\/]+)/)?.[1] || ''\n return !!name && page.getAttribute('data-page') === name\n}\n"]}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { ReactiveController, ReactiveControllerHost } from 'lit';
|
|
2
|
+
import { type DraftAddress } from './draft-store.js';
|
|
3
|
+
/** Dispatched (bubbling, composed) when a draft's edits can or can no longer be kept in this browser. */
|
|
4
|
+
export declare const DRAFT_UNKEPT = "draft-unkept";
|
|
5
|
+
export type DraftControllerOptions<T> = {
|
|
6
|
+
/** What was read - the values a field shows until it is edited. */
|
|
7
|
+
read: () => T | undefined;
|
|
8
|
+
/** Which record the form edits (the variables of its read): edits are dropped when it changes. */
|
|
9
|
+
key?: () => unknown;
|
|
10
|
+
/**
|
|
11
|
+
* Keep the edits in this browser (ADR-0098) until they are saved or thrown away, and offer them back when the record
|
|
12
|
+
* is read again: where they are kept, and the saved revision they began from (null when saves have no revision rule).
|
|
13
|
+
*/
|
|
14
|
+
keep?: {
|
|
15
|
+
address: () => DraftAddress | undefined;
|
|
16
|
+
revision?: () => string | number | null;
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
/** The drafts a host holds - PageView asks them whether anything typed would be lost. */
|
|
20
|
+
export declare function draftsOn(host: object): DraftController<any>[];
|
|
21
|
+
/**
|
|
22
|
+
* An edit form over what a screen read (ADR-0190 decision 2): the read stays the QueryController's, the typed values
|
|
23
|
+
* live here, and a field shows its edit if it has one, else what was read. A save sends a snapshot of the edits; once
|
|
24
|
+
* it is done, only the edits still as sent are cleared - what was typed while it was out stays. Moving to another
|
|
25
|
+
* record (a new `key`) drops the edits; with `keep` they are written for that record first, and offered back when it
|
|
26
|
+
* is read again.
|
|
27
|
+
*/
|
|
28
|
+
export declare class DraftController<T extends Record<string, any> = Record<string, any>> implements ReactiveController {
|
|
29
|
+
private host;
|
|
30
|
+
private options;
|
|
31
|
+
private changes;
|
|
32
|
+
private keyed?;
|
|
33
|
+
/** How many times the form moved to another record - a save that answers after a move belongs to the record left. */
|
|
34
|
+
private moves;
|
|
35
|
+
/** Saves sent and not answered yet - while one is out, every value typed is an edit. */
|
|
36
|
+
private sending;
|
|
37
|
+
private started;
|
|
38
|
+
private keeper?;
|
|
39
|
+
/** The address the edits are kept under - the record's until the form has moved to the next one. */
|
|
40
|
+
private keptAt?;
|
|
41
|
+
private offeredFor?;
|
|
42
|
+
private unkept;
|
|
43
|
+
private announcing;
|
|
44
|
+
constructor(host: ReactiveControllerHost & EventTarget, options: DraftControllerOptions<T>);
|
|
45
|
+
hostConnected(): void;
|
|
46
|
+
hostDisconnected(): void;
|
|
47
|
+
hostUpdate(): void;
|
|
48
|
+
/** The record is read: a kept draft of it is offered back - once per record, and never over edits typed now. */
|
|
49
|
+
private answered;
|
|
50
|
+
/** The value a field shows: its edit, else what was read. */
|
|
51
|
+
value<K extends keyof T>(name: K): T[K] | undefined;
|
|
52
|
+
/**
|
|
53
|
+
* A value put back to what was read is no edit - unless a save is out: what it sent may be saved already, so a field
|
|
54
|
+
* typed back to an earlier value is still an edit, and the answer leaves it.
|
|
55
|
+
*/
|
|
56
|
+
set<K extends keyof T>(name: K, value: T[K]): void;
|
|
57
|
+
/** The edits not saved yet. */
|
|
58
|
+
get edits(): Partial<T>;
|
|
59
|
+
get dirty(): boolean;
|
|
60
|
+
/** Whether the edits are written in this browser (`keep`), so leaving does not lose them. */
|
|
61
|
+
get kept(): boolean;
|
|
62
|
+
/**
|
|
63
|
+
* Sends the edits as they are now; once `save` comes back with an answer, the edits still as sent are cleared. An
|
|
64
|
+
* answer that comes after the form moved to another record clears only what was kept for the record it was sent
|
|
65
|
+
* from - the edits shown now are another record's, or the same record's opened again.
|
|
66
|
+
* @returns the save's answer, or undefined when it failed (the edits stay).
|
|
67
|
+
*/
|
|
68
|
+
submit<R>(save: (sent: Partial<T>) => Promise<R | undefined>): Promise<R | undefined>;
|
|
69
|
+
/** Drops every edit - leaving the record, or a discard the person agreed to. */
|
|
70
|
+
reset(): void;
|
|
71
|
+
private edited;
|
|
72
|
+
/** Only where this browser keeps no drafts, and only while something is typed, the edits can be lost on leaving. */
|
|
73
|
+
private announce;
|
|
74
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { QUERY_ANSWERED } from '@operato/graphql';
|
|
2
|
+
import { draftKey, draftsKept } from './draft-store.js';
|
|
3
|
+
import { UnsavedDraftController } from './unsaved-draft-controller.js';
|
|
4
|
+
/** Dispatched (bubbling, composed) when a draft's edits can or can no longer be kept in this browser. */
|
|
5
|
+
export const DRAFT_UNKEPT = 'draft-unkept';
|
|
6
|
+
/** The drafts on each host, so a page knows whether it holds unsaved edits (PageView `addressDirty`). */
|
|
7
|
+
const hosted = new WeakMap();
|
|
8
|
+
/** The drafts a host holds - PageView asks them whether anything typed would be lost. */
|
|
9
|
+
export function draftsOn(host) {
|
|
10
|
+
return [...(hosted.get(host) ?? [])];
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* An edit form over what a screen read (ADR-0190 decision 2): the read stays the QueryController's, the typed values
|
|
14
|
+
* live here, and a field shows its edit if it has one, else what was read. A save sends a snapshot of the edits; once
|
|
15
|
+
* it is done, only the edits still as sent are cleared - what was typed while it was out stays. Moving to another
|
|
16
|
+
* record (a new `key`) drops the edits; with `keep` they are written for that record first, and offered back when it
|
|
17
|
+
* is read again.
|
|
18
|
+
*/
|
|
19
|
+
export class DraftController {
|
|
20
|
+
constructor(host, options) {
|
|
21
|
+
this.host = host;
|
|
22
|
+
this.options = options;
|
|
23
|
+
this.changes = {};
|
|
24
|
+
/** How many times the form moved to another record - a save that answers after a move belongs to the record left. */
|
|
25
|
+
this.moves = 0;
|
|
26
|
+
/** Saves sent and not answered yet - while one is out, every value typed is an edit. */
|
|
27
|
+
this.sending = 0;
|
|
28
|
+
this.started = false;
|
|
29
|
+
this.unkept = false;
|
|
30
|
+
this.announcing = 0;
|
|
31
|
+
/** The record is read: a kept draft of it is offered back - once per record, and never over edits typed now. */
|
|
32
|
+
this.answered = () => {
|
|
33
|
+
if (!this.keeper || this.offeredFor === this.keyed)
|
|
34
|
+
return;
|
|
35
|
+
this.offeredFor = this.keyed;
|
|
36
|
+
this.keptAt = this.options.keep.address();
|
|
37
|
+
if (!this.dirty)
|
|
38
|
+
void this.keeper.offer();
|
|
39
|
+
};
|
|
40
|
+
host.addController(this);
|
|
41
|
+
if (options.keep) {
|
|
42
|
+
this.keeper = new UnsavedDraftController(host, {
|
|
43
|
+
address: () => this.keptAt,
|
|
44
|
+
snapshot: () => (this.dirty ? { value: this.changes, base: options.keep.revision?.() ?? null } : undefined),
|
|
45
|
+
savedRevision: () => options.keep.revision?.() ?? null,
|
|
46
|
+
restore: value => {
|
|
47
|
+
this.changes = { ...value };
|
|
48
|
+
this.edited();
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
hostConnected() {
|
|
54
|
+
let drafts = hosted.get(this.host);
|
|
55
|
+
if (!drafts)
|
|
56
|
+
hosted.set(this.host, (drafts = new Set()));
|
|
57
|
+
drafts.add(this);
|
|
58
|
+
if (this.keeper)
|
|
59
|
+
this.host.addEventListener(QUERY_ANSWERED, this.answered);
|
|
60
|
+
}
|
|
61
|
+
hostDisconnected() {
|
|
62
|
+
hosted.get(this.host)?.delete(this);
|
|
63
|
+
this.host.removeEventListener(QUERY_ANSWERED, this.answered);
|
|
64
|
+
}
|
|
65
|
+
hostUpdate() {
|
|
66
|
+
const key = this.options.key ? JSON.stringify(this.options.key() ?? null) : undefined;
|
|
67
|
+
if (this.started && key === this.keyed)
|
|
68
|
+
return;
|
|
69
|
+
if (this.started) {
|
|
70
|
+
/* Written under the record left - the snapshot and its address are taken before anything is cleared. */
|
|
71
|
+
if (this.keeper && this.dirty)
|
|
72
|
+
void this.keeper.flush();
|
|
73
|
+
this.changes = {};
|
|
74
|
+
this.moves++;
|
|
75
|
+
void this.announce();
|
|
76
|
+
}
|
|
77
|
+
this.started = true;
|
|
78
|
+
this.keyed = key;
|
|
79
|
+
this.keptAt = this.options.keep?.address();
|
|
80
|
+
}
|
|
81
|
+
/** The value a field shows: its edit, else what was read. */
|
|
82
|
+
value(name) {
|
|
83
|
+
return name in this.changes ? this.changes[name] : this.options.read()?.[name];
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* A value put back to what was read is no edit - unless a save is out: what it sent may be saved already, so a field
|
|
87
|
+
* typed back to an earlier value is still an edit, and the answer leaves it.
|
|
88
|
+
*/
|
|
89
|
+
set(name, value) {
|
|
90
|
+
const { [name]: _, ...others } = this.changes;
|
|
91
|
+
const putBack = this.sending === 0 && value === this.options.read()?.[name];
|
|
92
|
+
this.changes = (putBack ? others : { ...others, [name]: value });
|
|
93
|
+
this.edited();
|
|
94
|
+
}
|
|
95
|
+
/** The edits not saved yet. */
|
|
96
|
+
get edits() {
|
|
97
|
+
return this.changes;
|
|
98
|
+
}
|
|
99
|
+
get dirty() {
|
|
100
|
+
return Object.keys(this.changes).length > 0;
|
|
101
|
+
}
|
|
102
|
+
/** Whether the edits are written in this browser (`keep`), so leaving does not lose them. */
|
|
103
|
+
get kept() {
|
|
104
|
+
return !!this.keeper && this.keeper.keeps;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Sends the edits as they are now; once `save` comes back with an answer, the edits still as sent are cleared. An
|
|
108
|
+
* answer that comes after the form moved to another record clears only what was kept for the record it was sent
|
|
109
|
+
* from - the edits shown now are another record's, or the same record's opened again.
|
|
110
|
+
* @returns the save's answer, or undefined when it failed (the edits stay).
|
|
111
|
+
*/
|
|
112
|
+
async submit(save) {
|
|
113
|
+
const sent = { ...this.changes };
|
|
114
|
+
const moves = this.moves;
|
|
115
|
+
const sentAt = this.keptAt;
|
|
116
|
+
this.sending++;
|
|
117
|
+
let answer;
|
|
118
|
+
try {
|
|
119
|
+
answer = await save(sent);
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
this.sending--;
|
|
123
|
+
}
|
|
124
|
+
if (answer !== undefined && moves !== this.moves) {
|
|
125
|
+
if (this.keeper && sentAt) {
|
|
126
|
+
await this.keeper.forget(sentAt);
|
|
127
|
+
if (this.dirty && this.keptAt && draftKey(this.keptAt) === draftKey(sentAt))
|
|
128
|
+
await this.keeper.flush();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
else if (answer !== undefined) {
|
|
132
|
+
this.changes = Object.fromEntries(Object.entries(this.changes).filter(([name, value]) => !(name in sent) || sent[name] !== value));
|
|
133
|
+
this.edited();
|
|
134
|
+
}
|
|
135
|
+
return answer;
|
|
136
|
+
}
|
|
137
|
+
/** Drops every edit - leaving the record, or a discard the person agreed to. */
|
|
138
|
+
reset() {
|
|
139
|
+
this.changes = {};
|
|
140
|
+
this.edited();
|
|
141
|
+
}
|
|
142
|
+
edited() {
|
|
143
|
+
this.host.requestUpdate();
|
|
144
|
+
if (!this.keeper)
|
|
145
|
+
return;
|
|
146
|
+
if (this.dirty)
|
|
147
|
+
this.keeper.changed();
|
|
148
|
+
else
|
|
149
|
+
void this.keeper.settled();
|
|
150
|
+
void this.announce();
|
|
151
|
+
}
|
|
152
|
+
/** Only where this browser keeps no drafts, and only while something is typed, the edits can be lost on leaving. */
|
|
153
|
+
async announce() {
|
|
154
|
+
if (!this.keeper)
|
|
155
|
+
return;
|
|
156
|
+
const mine = ++this.announcing;
|
|
157
|
+
const unkept = this.dirty && !(await draftsKept());
|
|
158
|
+
if (mine !== this.announcing || unkept === this.unkept)
|
|
159
|
+
return;
|
|
160
|
+
this.unkept = unkept;
|
|
161
|
+
this.host.dispatchEvent(new CustomEvent(DRAFT_UNKEPT, { detail: { unkept }, bubbles: true, composed: true }));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
//# sourceMappingURL=draft-controller.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"draft-controller.js","sourceRoot":"","sources":["../../../src/drafts/draft-controller.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ReactiveController, ReactiveControllerHost } from 'lit'\nimport { QUERY_ANSWERED } from '@operato/graphql'\n\nimport { draftKey, draftsKept, type DraftAddress } from './draft-store.js'\nimport { UnsavedDraftController } from './unsaved-draft-controller.js'\n\n/** Dispatched (bubbling, composed) when a draft's edits can or can no longer be kept in this browser. */\nexport const DRAFT_UNKEPT = 'draft-unkept'\n\nexport type DraftControllerOptions<T> = {\n /** What was read - the values a field shows until it is edited. */\n read: () => T | undefined\n /** Which record the form edits (the variables of its read): edits are dropped when it changes. */\n key?: () => unknown\n /**\n * Keep the edits in this browser (ADR-0098) until they are saved or thrown away, and offer them back when the record\n * is read again: where they are kept, and the saved revision they began from (null when saves have no revision rule).\n */\n keep?: { address: () => DraftAddress | undefined; revision?: () => string | number | null }\n}\n\n/** The drafts on each host, so a page knows whether it holds unsaved edits (PageView `addressDirty`). */\nconst hosted = new WeakMap<object, Set<DraftController<any>>>()\n\n/** The drafts a host holds - PageView asks them whether anything typed would be lost. */\nexport function draftsOn(host: object): DraftController<any>[] {\n return [...(hosted.get(host) ?? [])]\n}\n\n/**\n * An edit form over what a screen read (ADR-0190 decision 2): the read stays the QueryController's, the typed values\n * live here, and a field shows its edit if it has one, else what was read. A save sends a snapshot of the edits; once\n * it is done, only the edits still as sent are cleared - what was typed while it was out stays. Moving to another\n * record (a new `key`) drops the edits; with `keep` they are written for that record first, and offered back when it\n * is read again.\n */\nexport class DraftController<T extends Record<string, any> = Record<string, any>> implements ReactiveController {\n private changes: Partial<T> = {}\n private keyed?: string\n /** How many times the form moved to another record - a save that answers after a move belongs to the record left. */\n private moves = 0\n /** Saves sent and not answered yet - while one is out, every value typed is an edit. */\n private sending = 0\n private started = false\n private keeper?: UnsavedDraftController<Partial<T>>\n /** The address the edits are kept under - the record's until the form has moved to the next one. */\n private keptAt?: DraftAddress\n private offeredFor?: string\n private unkept = false\n private announcing = 0\n\n constructor(\n private host: ReactiveControllerHost & EventTarget,\n private options: DraftControllerOptions<T>\n ) {\n host.addController(this)\n if (options.keep) {\n this.keeper = new UnsavedDraftController<Partial<T>>(host, {\n address: () => this.keptAt,\n snapshot: () => (this.dirty ? { value: this.changes, base: options.keep!.revision?.() ?? null } : undefined),\n savedRevision: () => options.keep!.revision?.() ?? null,\n restore: value => {\n this.changes = { ...value }\n this.edited()\n }\n })\n }\n }\n\n hostConnected() {\n let drafts = hosted.get(this.host)\n if (!drafts) hosted.set(this.host, (drafts = new Set()))\n drafts.add(this)\n if (this.keeper) this.host.addEventListener(QUERY_ANSWERED, this.answered)\n }\n\n hostDisconnected() {\n hosted.get(this.host)?.delete(this)\n this.host.removeEventListener(QUERY_ANSWERED, this.answered)\n }\n\n hostUpdate() {\n const key = this.options.key ? JSON.stringify(this.options.key() ?? null) : undefined\n if (this.started && key === this.keyed) return\n if (this.started) {\n /* Written under the record left - the snapshot and its address are taken before anything is cleared. */\n if (this.keeper && this.dirty) void this.keeper.flush()\n this.changes = {}\n this.moves++\n void this.announce()\n }\n this.started = true\n this.keyed = key\n this.keptAt = this.options.keep?.address()\n }\n\n /** The record is read: a kept draft of it is offered back - once per record, and never over edits typed now. */\n private answered = () => {\n if (!this.keeper || this.offeredFor === this.keyed) return\n this.offeredFor = this.keyed\n this.keptAt = this.options.keep!.address()\n if (!this.dirty) void this.keeper.offer()\n }\n\n /** The value a field shows: its edit, else what was read. */\n value<K extends keyof T>(name: K): T[K] | undefined {\n return name in this.changes ? (this.changes[name] as T[K]) : this.options.read()?.[name]\n }\n\n /**\n * A value put back to what was read is no edit - unless a save is out: what it sent may be saved already, so a field\n * typed back to an earlier value is still an edit, and the answer leaves it.\n */\n set<K extends keyof T>(name: K, value: T[K]) {\n const { [name]: _, ...others } = this.changes\n const putBack = this.sending === 0 && value === this.options.read()?.[name]\n this.changes = (putBack ? others : { ...others, [name]: value }) as Partial<T>\n this.edited()\n }\n\n /** The edits not saved yet. */\n get edits(): Partial<T> {\n return this.changes\n }\n\n get dirty(): boolean {\n return Object.keys(this.changes).length > 0\n }\n\n /** Whether the edits are written in this browser (`keep`), so leaving does not lose them. */\n get kept(): boolean {\n return !!this.keeper && this.keeper.keeps\n }\n\n /**\n * Sends the edits as they are now; once `save` comes back with an answer, the edits still as sent are cleared. An\n * answer that comes after the form moved to another record clears only what was kept for the record it was sent\n * from - the edits shown now are another record's, or the same record's opened again.\n * @returns the save's answer, or undefined when it failed (the edits stay).\n */\n async submit<R>(save: (sent: Partial<T>) => Promise<R | undefined>): Promise<R | undefined> {\n const sent = { ...this.changes }\n const moves = this.moves\n const sentAt = this.keptAt\n this.sending++\n let answer: R | undefined\n try {\n answer = await save(sent)\n } finally {\n this.sending--\n }\n if (answer !== undefined && moves !== this.moves) {\n if (this.keeper && sentAt) {\n await this.keeper.forget(sentAt)\n if (this.dirty && this.keptAt && draftKey(this.keptAt) === draftKey(sentAt)) await this.keeper.flush()\n }\n } else if (answer !== undefined) {\n this.changes = Object.fromEntries(\n Object.entries(this.changes).filter(([name, value]) => !(name in sent) || sent[name] !== value)\n ) as Partial<T>\n this.edited()\n }\n return answer\n }\n\n /** Drops every edit - leaving the record, or a discard the person agreed to. */\n reset() {\n this.changes = {}\n this.edited()\n }\n\n private edited() {\n this.host.requestUpdate()\n if (!this.keeper) return\n if (this.dirty) this.keeper.changed()\n else void this.keeper.settled()\n void this.announce()\n }\n\n /** Only where this browser keeps no drafts, and only while something is typed, the edits can be lost on leaving. */\n private async announce() {\n if (!this.keeper) return\n const mine = ++this.announcing\n const unkept = this.dirty && !(await draftsKept())\n if (mine !== this.announcing || unkept === this.unkept) return\n this.unkept = unkept\n this.host.dispatchEvent(new CustomEvent(DRAFT_UNKEPT, { detail: { unkept }, bubbles: true, composed: true }))\n }\n}\n"]}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whose unsaved drafts this browser keeps (ADR-0098): the signed-in person's id, as the profile gives it
|
|
3
|
+
* (`auth.user`). Undefined while no one is signed in - a draft with no owner is never written. Every screen that
|
|
4
|
+
* keeps drafts, and sign-out that takes them away, names the owner here (ADR-0190).
|
|
5
|
+
*/
|
|
6
|
+
export declare function draftOwner(): string | undefined;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { store } from '../store.js';
|
|
2
|
+
/**
|
|
3
|
+
* Whose unsaved drafts this browser keeps (ADR-0098): the signed-in person's id, as the profile gives it
|
|
4
|
+
* (`auth.user`). Undefined while no one is signed in - a draft with no owner is never written. Every screen that
|
|
5
|
+
* keeps drafts, and sign-out that takes them away, names the owner here (ADR-0190).
|
|
6
|
+
*/
|
|
7
|
+
export function draftOwner() {
|
|
8
|
+
return store.getState().auth?.user?.id || undefined;
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=draft-owner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"draft-owner.js","sourceRoot":"","sources":["../../../src/drafts/draft-owner.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAA;AAEnC;;;;GAIG;AACH,MAAM,UAAU,UAAU;IACxB,OAAQ,KAAK,CAAC,QAAQ,EAAmD,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,IAAI,SAAS,CAAA;AACvG,CAAC","sourcesContent":["import { store } from '../store.js'\n\n/**\n * Whose unsaved drafts this browser keeps (ADR-0098): the signed-in person's id, as the profile gives it\n * (`auth.user`). Undefined while no one is signed in - a draft with no owner is never written. Every screen that\n * keeps drafts, and sign-out that takes them away, names the owner here (ADR-0190).\n */\nexport function draftOwner(): string | undefined {\n return (store.getState() as { auth?: { user?: { id?: string } | null } }).auth?.user?.id || undefined\n}\n"]}
|
|
@@ -44,6 +44,11 @@ export declare class UnsavedDraftController<T> implements ReactiveController {
|
|
|
44
44
|
changed(): void;
|
|
45
45
|
/** Saved, or discarded on purpose: there is nothing to keep. */
|
|
46
46
|
settled(): Promise<void>;
|
|
47
|
+
/**
|
|
48
|
+
* A save sent from a record the screen has since left answered: what was kept for that record is gone. What the
|
|
49
|
+
* screen edits now is not touched - if it is that record again, the caller writes it anew.
|
|
50
|
+
*/
|
|
51
|
+
forget(address: DraftAddress): Promise<void>;
|
|
47
52
|
/** Capture before yielding: a query switch must never store its edit under the next query's key. */
|
|
48
53
|
flush(): Promise<void>;
|
|
49
54
|
/**
|