@operato/layout 10.29.46 → 10.29.50

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.
Files changed (54) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/src/actions/entity360-door.d.ts +28 -0
  3. package/dist/src/actions/entity360-door.js +45 -0
  4. package/dist/src/actions/entity360-door.js.map +1 -1
  5. package/dist/src/actions/entity360-registry.d.ts +13 -3
  6. package/dist/src/actions/entity360-registry.js +49 -8
  7. package/dist/src/actions/entity360-registry.js.map +1 -1
  8. package/dist/src/actions/entity360.d.ts +2 -0
  9. package/dist/src/actions/entity360.js +4 -0
  10. package/dist/src/actions/entity360.js.map +1 -1
  11. package/dist/src/actions/id-lens.d.ts +67 -0
  12. package/dist/src/actions/id-lens.js +51 -0
  13. package/dist/src/actions/id-lens.js.map +1 -0
  14. package/dist/src/actions/layout.d.ts +5 -0
  15. package/dist/src/actions/layout.js.map +1 -1
  16. package/dist/src/components/ox-entity360-dock.d.ts +18 -2
  17. package/dist/src/components/ox-entity360-dock.js +39 -17
  18. package/dist/src/components/ox-entity360-dock.js.map +1 -1
  19. package/dist/src/components/ox-id-lens.d.ts +58 -0
  20. package/dist/src/components/ox-id-lens.js +362 -0
  21. package/dist/src/components/ox-id-lens.js.map +1 -0
  22. package/dist/src/index.d.ts +1 -0
  23. package/dist/src/index.js +1 -0
  24. package/dist/src/index.js.map +1 -1
  25. package/dist/src/layouts/ox-aside-bar.js +134 -6
  26. package/dist/src/layouts/ox-aside-bar.js.map +1 -1
  27. package/dist/test/entity360-companion.test.d.ts +1 -0
  28. package/dist/test/entity360-companion.test.js +127 -0
  29. package/dist/test/entity360-companion.test.js.map +1 -0
  30. package/dist/test/entity360-kind.test.js +136 -10
  31. package/dist/test/entity360-kind.test.js.map +1 -1
  32. package/dist/test/entity360-section.test.js +31 -1
  33. package/dist/test/entity360-section.test.js.map +1 -1
  34. package/dist/test/entity360.test.js +29 -6
  35. package/dist/test/entity360.test.js.map +1 -1
  36. package/dist/test/id-lens.test.d.ts +2 -0
  37. package/dist/test/id-lens.test.js +162 -0
  38. package/dist/test/id-lens.test.js.map +1 -0
  39. package/dist/tsconfig.tsbuildinfo +1 -1
  40. package/package.json +11 -6
  41. package/test/entity360-companion.test.ts +110 -0
  42. package/test/entity360-kind.test.ts +145 -11
  43. package/test/entity360-section.test.ts +34 -1
  44. package/test/entity360.test.ts +31 -6
  45. package/test/id-lens.test.ts +198 -0
  46. package/coverage/lcov-report/base.css +0 -224
  47. package/coverage/lcov-report/block-navigation.js +0 -87
  48. package/coverage/lcov-report/favicon.png +0 -0
  49. package/coverage/lcov-report/index.html +0 -1136
  50. package/coverage/lcov-report/prettify.css +0 -1
  51. package/coverage/lcov-report/prettify.js +0 -2
  52. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  53. package/coverage/lcov-report/sorter.js +0 -210
  54. package/coverage/lcov.info +0 -63268
package/CHANGELOG.md CHANGED
@@ -3,6 +3,32 @@
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.50](https://github.com/hatiolab/operato/compare/v10.29.49...v10.29.50) (2026-10-23)
7
+
8
+ **Note:** Version bump only for package @operato/layout
9
+
10
+
11
+
12
+
13
+
14
+ ### [10.29.49](https://github.com/hatiolab/operato/compare/v10.29.48...v10.29.49) (2026-10-23)
15
+
16
+
17
+ ### :bug: Bug Fix
18
+
19
+ * **layout:** opening the 360 already shown does not read it again ([2b975aa](https://github.com/hatiolab/operato/commit/2b975aaa2d493905eadc174ba7688dfa350b593d))
20
+
21
+
22
+ ### :rocket: New Features
23
+
24
+ * **layout:** one entity 360 per address, every read ends, entries survive refresh and moves ([1f41694](https://github.com/hatiolab/operato/commit/1f4169440ccc3aa3e7c030d408065f85c03d473b))
25
+ * **layout:** only the newest read of a 360 section answers (Entity360Reads) ([bc6399a](https://github.com/hatiolab/operato/commit/bc6399a212c30303e804df0af484ac20bddbff77))
26
+ * **layout:** preserve companion dock when Entity360 expands ([8ef8579](https://github.com/hatiolab/operato/commit/8ef8579ae2ae13f552939370ba09789cec3b8372))
27
+ * **layout:** the ID Lens placeholder may be read at each render ([58e1377](https://github.com/hatiolab/operato/commit/58e13775ca8a0b012500d14a5ceae115873535ad))
28
+ * **layout:** the ID Lens, shared by the products that open Entity 360 ([9908a52](https://github.com/hatiolab/operato/commit/9908a52e8f58f9105c04147ba3f534b7b8a80fa3))
29
+
30
+
31
+
6
32
  ### [10.29.46](https://github.com/hatiolab/operato/compare/v10.29.45...v10.29.46) (2026-10-20)
7
33
 
8
34
 
@@ -26,3 +26,31 @@ export declare function readEntity360Door<T>(response: {
26
26
  data?: any;
27
27
  errors?: readonly any[] | null;
28
28
  } | null | undefined, pick: (data: any) => T, isEmpty?: (value: T) => boolean): Entity360DoorAnswer<T>;
29
+ /**
30
+ * Ask a door and read its answer - `readEntity360Door` for a request still on its way (ADR-0067 decision 7).
31
+ *
32
+ * A request that throws (the network is down, the server is gone) is `failed` with its own message, so a section
33
+ * always comes to rest in one of the four states; nothing is left saying 「loading」 for good.
34
+ */
35
+ export declare function askEntity360Door<T>(request: Promise<{
36
+ data?: any;
37
+ errors?: readonly any[] | null;
38
+ } | null | undefined>, pick: (data: any) => T, isEmpty?: (value: T) => boolean): Promise<Entity360DoorAnswer<T>>;
39
+ /**
40
+ * One section's reads, of which only the newest may answer (ADR-0067 decision 7).
41
+ *
42
+ * A section reads again whenever the dock hands it a subject - another subject, or the same one after a refresh. An
43
+ * answer asked for before that is old: drawn, it would show the previous subject, or what stood before the refresh,
44
+ * under the new heading. Comparing ids does not catch the refresh (the id is the same), so each read takes a number
45
+ * and only the last one taken is let through. A request that throws comes back `failed` (`askEntity360Door`).
46
+ */
47
+ export declare class Entity360Reads {
48
+ private latest;
49
+ /** The answer, or `undefined` when a newer read began (or `forget` was called) while this one was on its way. */
50
+ ask<T>(request: () => Promise<{
51
+ data?: any;
52
+ errors?: readonly any[] | null;
53
+ } | null | undefined>, pick: (data: any) => T, isEmpty?: (value: T) => boolean): Promise<Entity360DoorAnswer<T> | undefined>;
54
+ /** There is nothing to ask: an answer still on its way is no longer wanted. */
55
+ forget(): void;
56
+ }
@@ -25,4 +25,49 @@ export function readEntity360Door(response, pick, isEmpty = value => value == nu
25
25
  const value = pick(response.data);
26
26
  return isEmpty(value) ? { state: 'none', data: value } : { state: 'ready', data: value };
27
27
  }
28
+ /**
29
+ * Ask a door and read its answer - `readEntity360Door` for a request still on its way (ADR-0067 decision 7).
30
+ *
31
+ * A request that throws (the network is down, the server is gone) is `failed` with its own message, so a section
32
+ * always comes to rest in one of the four states; nothing is left saying 「loading」 for good.
33
+ */
34
+ export async function askEntity360Door(request, pick, isEmpty) {
35
+ var _a;
36
+ let response;
37
+ try {
38
+ response = await request;
39
+ }
40
+ catch (error) {
41
+ /* A client set to throw on GraphQL errors still carries them: a refusal stays `forbidden`. */
42
+ const errors = error === null || error === void 0 ? void 0 : error.graphQLErrors;
43
+ if (errors === null || errors === void 0 ? void 0 : errors.length)
44
+ return readEntity360Door({ errors }, pick, isEmpty);
45
+ return { state: 'failed', message: String((_a = error === null || error === void 0 ? void 0 : error.message) !== null && _a !== void 0 ? _a : '') };
46
+ }
47
+ return readEntity360Door(response, pick, isEmpty);
48
+ }
49
+ /**
50
+ * One section's reads, of which only the newest may answer (ADR-0067 decision 7).
51
+ *
52
+ * A section reads again whenever the dock hands it a subject - another subject, or the same one after a refresh. An
53
+ * answer asked for before that is old: drawn, it would show the previous subject, or what stood before the refresh,
54
+ * under the new heading. Comparing ids does not catch the refresh (the id is the same), so each read takes a number
55
+ * and only the last one taken is let through. A request that throws comes back `failed` (`askEntity360Door`).
56
+ */
57
+ export class Entity360Reads {
58
+ constructor() {
59
+ this.latest = 0;
60
+ }
61
+ /** The answer, or `undefined` when a newer read began (or `forget` was called) while this one was on its way. */
62
+ async ask(request, pick, isEmpty) {
63
+ const mine = ++this.latest;
64
+ /* A request that throws before it returns a promise is a rejected one too. */
65
+ const answer = await askEntity360Door((async () => request())(), pick, isEmpty);
66
+ return mine === this.latest ? answer : undefined;
67
+ }
68
+ /** There is nothing to ask: an answer still on its way is no longer wanted. */
69
+ forget() {
70
+ this.latest++;
71
+ }
72
+ }
28
73
  //# sourceMappingURL=entity360-door.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"entity360-door.js","sourceRoot":"","sources":["../../../src/actions/entity360-door.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAWH;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAC/B,QAA2E,EAC3E,IAAsB,EACtB,UAAiC,KAAK,CAAC,EAAE,CAAC,KAAK,IAAI,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,CAAC;IAEvG,MAAM,MAAM,GAAG,CAAA,QAAQ,aAAR,QAAQ,uBAAR,QAAQ,CAAE,MAAM,KAAI,EAAE,CAAA;IAErC,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC,KAAU,EAAE,EAAE,WAAC,OAAA,CAAA,MAAA,KAAK,aAAL,KAAK,uBAAL,KAAK,CAAE,UAAU,0CAAE,IAAI,KAAI,WAAW,CAAA,EAAA,CAAC;QAAE,OAAO,EAAE,KAAK,EAAE,WAAW,EAAE,CAAA;IACtG,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,CAAA,QAAQ,aAAR,QAAQ,uBAAR,QAAQ,CAAE,IAAI,CAAA,EAAE,CAAC;QACzC,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,KAAU,EAAE,EAAE,CAAC,KAAK,aAAL,KAAK,uBAAL,KAAK,CAAE,OAAO,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAA;IAC3G,CAAC;IAED,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAA;IACjC,OAAO,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,CAAA;AAC1F,CAAC","sourcesContent":["/**\n * What a 360 section's read door answered - pure: a GraphQL response in, a state out.\n *\n * A section shows one of four things once it has asked (ADR-0067's rule for the 360):\n * - `ready` - there is something, and the section draws it\n * - `none` - the door answered and there is nothing: 「없음」\n * - `forbidden` - the door refused this viewer: 「볼 수 없음」. Not the same as `none` - saying\n * \"none\" to someone who may not look would state a fact the screen does not know.\n * - `failed` - something else went wrong; the server's own sentence is kept, since the section\n * does not know the cause and must not make one up.\n *\n * A refusal is told by `extensions.code == 'FORBIDDEN'`, which is what the @privilege directive sends.\n */\n\nexport type Entity360DoorState = 'loading' | 'ready' | 'none' | 'forbidden' | 'failed'\n\nexport interface Entity360DoorAnswer<T> {\n state: Exclude<Entity360DoorState, 'loading'>\n data?: T\n /** The server's sentence, for `failed`. */\n message?: string\n}\n\n/**\n * Read a response. `pick` takes what the section draws out of `data`; `isEmpty` says whether that is\n * nothing (by default: `null`, `undefined` or an empty array).\n */\nexport function readEntity360Door<T>(\n response: { data?: any; errors?: readonly any[] | null } | null | undefined,\n pick: (data: any) => T,\n isEmpty: (value: T) => boolean = value => value == null || (Array.isArray(value) && value.length === 0)\n): Entity360DoorAnswer<T> {\n const errors = response?.errors || []\n\n if (errors.some((error: any) => error?.extensions?.code == 'FORBIDDEN')) return { state: 'forbidden' }\n if (errors.length > 0 || !response?.data) {\n return { state: 'failed', message: errors.map((error: any) => error?.message).filter(Boolean).join(' ') }\n }\n\n const value = pick(response.data)\n return isEmpty(value) ? { state: 'none', data: value } : { state: 'ready', data: value }\n}\n"]}
1
+ {"version":3,"file":"entity360-door.js","sourceRoot":"","sources":["../../../src/actions/entity360-door.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What a 360 section's read door answered - pure: a GraphQL response in, a state out.\n *\n * A section shows one of four things once it has asked (ADR-0067's rule for the 360):\n * - `ready` - there is something, and the section draws it\n * - `none` - the door answered and there is nothing: 「없음」\n * - `forbidden` - the door refused this viewer: 「볼 수 없음」. Not the same as `none` - saying\n * \"none\" to someone who may not look would state a fact the screen does not know.\n * - `failed` - something else went wrong; the server's own sentence is kept, since the section\n * does not know the cause and must not make one up.\n *\n * A refusal is told by `extensions.code == 'FORBIDDEN'`, which is what the @privilege directive sends.\n */\n\nexport type Entity360DoorState = 'loading' | 'ready' | 'none' | 'forbidden' | 'failed'\n\nexport interface Entity360DoorAnswer<T> {\n state: Exclude<Entity360DoorState, 'loading'>\n data?: T\n /** The server's sentence, for `failed`. */\n message?: string\n}\n\n/**\n * Read a response. `pick` takes what the section draws out of `data`; `isEmpty` says whether that is\n * nothing (by default: `null`, `undefined` or an empty array).\n */\nexport function readEntity360Door<T>(\n response: { data?: any; errors?: readonly any[] | null } | null | undefined,\n pick: (data: any) => T,\n isEmpty: (value: T) => boolean = value => value == null || (Array.isArray(value) && value.length === 0)\n): Entity360DoorAnswer<T> {\n const errors = response?.errors || []\n\n if (errors.some((error: any) => error?.extensions?.code == 'FORBIDDEN')) return { state: 'forbidden' }\n if (errors.length > 0 || !response?.data) {\n return { state: 'failed', message: errors.map((error: any) => error?.message).filter(Boolean).join(' ') }\n }\n\n const value = pick(response.data)\n return isEmpty(value) ? { state: 'none', data: value } : { state: 'ready', data: value }\n}\n\n/**\n * Ask a door and read its answer - `readEntity360Door` for a request still on its way (ADR-0067 decision 7).\n *\n * A request that throws (the network is down, the server is gone) is `failed` with its own message, so a section\n * always comes to rest in one of the four states; nothing is left saying 「loading」 for good.\n */\nexport async function askEntity360Door<T>(\n request: Promise<{ data?: any; errors?: readonly any[] | null } | null | undefined>,\n pick: (data: any) => T,\n isEmpty?: (value: T) => boolean\n): Promise<Entity360DoorAnswer<T>> {\n let response\n try {\n response = await request\n } catch (error) {\n /* A client set to throw on GraphQL errors still carries them: a refusal stays `forbidden`. */\n const errors = (error as any)?.graphQLErrors\n if (errors?.length) return readEntity360Door({ errors }, pick, isEmpty)\n return { state: 'failed', message: String((error as any)?.message ?? '') }\n }\n return readEntity360Door(response, pick, isEmpty)\n}\n\n/**\n * One section's reads, of which only the newest may answer (ADR-0067 decision 7).\n *\n * A section reads again whenever the dock hands it a subject - another subject, or the same one after a refresh. An\n * answer asked for before that is old: drawn, it would show the previous subject, or what stood before the refresh,\n * under the new heading. Comparing ids does not catch the refresh (the id is the same), so each read takes a number\n * and only the last one taken is let through. A request that throws comes back `failed` (`askEntity360Door`).\n */\nexport class Entity360Reads {\n private latest = 0\n\n /** The answer, or `undefined` when a newer read began (or `forget` was called) while this one was on its way. */\n async ask<T>(\n request: () => Promise<{ data?: any; errors?: readonly any[] | null } | null | undefined>,\n pick: (data: any) => T,\n isEmpty?: (value: T) => boolean\n ): Promise<Entity360DoorAnswer<T> | undefined> {\n const mine = ++this.latest\n /* A request that throws before it returns a promise is a rejected one too. */\n const answer = await askEntity360Door((async () => request())(), pick, isEmpty)\n return mine === this.latest ? answer : undefined\n }\n\n /** There is nothing to ask: an answer still on its way is no longer wanted. */\n forget(): void {\n this.latest++\n }\n}\n"]}
@@ -24,6 +24,12 @@ export interface Entity360KindDeclaration {
24
24
  kind: string;
25
25
  /** The GraphQL `__typename`s whose values open this kind. A typename belongs to one kind. */
26
26
  typenames: string[];
27
+ /**
28
+ * The kind's one identifier for a value - what the address, the links and `subject.id` all carry (ADR-0067
29
+ * decision 6). A section reads its door by `subject.id` alone: a reload or a shared link brings nothing else.
30
+ * Without it, `value.id`.
31
+ */
32
+ id?(value: Record<string, any>): string | null | undefined;
27
33
  /** The dock's title for a subject - what a person recognises it by. */
28
34
  title(subject: Entity360Subject): string;
29
35
  /** A line under the title, if the kind has one. */
@@ -63,6 +69,8 @@ export declare function registerEntity360Section(kind: string, section: Entity36
63
69
  * the cache off) - the caller draws it as it always has.
64
70
  */
65
71
  export declare function entity360KindOf(value: unknown): string | undefined;
72
+ /** The identifier a kind gives a value (its declaration's `id`, else `value.id`); '' when there is none. */
73
+ export declare function entity360IdOf(kind: string, value: Record<string, any> | null | undefined): string;
66
74
  /** Whether a kind was declared - an identifier of an undeclared kind stays text. */
67
75
  export declare function entity360KindRegistered(kind: string): boolean;
68
76
  /** The sections hung on a kind, in order. */
@@ -89,10 +97,12 @@ source?: Element): void;
89
97
  /** Close the registry's 360 and take its step out of the address - for an app whose own state let go of it. */
90
98
  export declare function closeEntity360(): void;
91
99
  /**
92
- * Draw the shown 360 again - its title and context are read anew. For a kind whose title comes from
93
- * what its section has just read.
100
+ * What a section has read about its subject - its name, what the title and context are drawn from (ADR-0067
101
+ * decision 6). Opened from an address alone the value is `{ id }`; the section that reads who the subject is gives
102
+ * the rest here, the value is kept for going back to it, and the heading is drawn anew. The sections are left as
103
+ * they are. A subject no longer shown is only remembered.
94
104
  */
95
- export declare function redrawEntity360(): void;
105
+ export declare function setEntity360Value(subject: Pick<Entity360Subject, 'kind' | 'id'>, value: Record<string, any>): void;
96
106
  type Entity360Listener = (subject: Entity360Subject | undefined) => void;
97
107
  /**
98
108
  * Hear which subject the registry's 360 shows - on open, on back or forward, and `undefined` on close.
@@ -1,8 +1,9 @@
1
1
  import { html } from 'lit';
2
2
  import { i18next } from '@operato/i18n';
3
+ import { OxPrompt } from '@operato/popup/ox-prompt.js';
3
4
  import { addressController, writeAddress } from '@operato/utils/address.js';
4
5
  import { store, UPDATE_CONTEXT } from '@operato/shell';
5
- import { showEntity360, hideEntity360, canGoBackEntity360, isEntity360Shown, navigateEntity360 } from './entity360.js';
6
+ import { showEntity360, hideEntity360, canGoBackEntity360, isEntity360Shown, isEntity360Unkept, navigateEntity360 } from './entity360.js';
6
7
  const returnFocusByAddress = new Map();
7
8
  const kinds = new Map();
8
9
  const kindOfTypename = new Map();
@@ -56,6 +57,12 @@ export function entity360KindOf(value) {
56
57
  const typename = value === null || value === void 0 ? void 0 : value.__typename;
57
58
  return typeof typename === 'string' ? kindOfTypename.get(typename) : undefined;
58
59
  }
60
+ /** The identifier a kind gives a value (its declaration's `id`, else `value.id`); '' when there is none. */
61
+ export function entity360IdOf(kind, value) {
62
+ var _a, _b;
63
+ const declared = (_a = kinds.get(kind)) === null || _a === void 0 ? void 0 : _a.id;
64
+ return String((_b = (declared ? declared(value !== null && value !== void 0 ? value : {}) : value === null || value === void 0 ? void 0 : value.id)) !== null && _b !== void 0 ? _b : '');
65
+ }
59
66
  /** Whether a kind was declared - an identifier of an undeclared kind stays text. */
60
67
  export function entity360KindRegistered(kind) {
61
68
  return kinds.has(kind);
@@ -102,8 +109,7 @@ export function openEntity360For(value, returnFocus, source) {
102
109
  export function openEntity360Kind(kind, value, returnFocus,
103
110
  /** The element the request comes from; a link inside a popup names that popup as its origin. */
104
111
  source) {
105
- var _a;
106
- const subject = { kind, id: String((_a = value === null || value === void 0 ? void 0 : value.id) !== null && _a !== void 0 ? _a : ''), value: value !== null && value !== void 0 ? value : {} };
112
+ const subject = { kind, id: entity360IdOf(kind, value), value: value !== null && value !== void 0 ? value : {} };
107
113
  const address = addressOf(kind, subject.id);
108
114
  opened.set(address, subject.value);
109
115
  if (returnFocus)
@@ -116,8 +122,31 @@ source) {
116
122
  return;
117
123
  }
118
124
  }
125
+ /*
126
+ * Already shown - an app that wrote the address itself (twin's focus) opens it again right after the address drew
127
+ * it. Drawing again would hand the sections a new subject, which is how a refresh asks them to read.
128
+ */
129
+ if ((shown === null || shown === void 0 ? void 0 : shown.kind) === kind && shown.id === subject.id && isEntity360Shown(OWNER))
130
+ return;
119
131
  draw(subject, returnFocus);
120
132
  }
133
+ /*
134
+ * What a 360 section holds unsaved goes when its subject does. A draft keeps it (ADR-0098) - only an entry this
135
+ * browser cannot keep is asked about, with the page's own question, by the address layer's one guard.
136
+ */
137
+ addressController().guard({
138
+ discards: change => (change.focus || change.route) && isEntity360Unkept(OWNER),
139
+ kept: () => false,
140
+ approve: change => !((change.focus || change.route) && isEntity360Unkept(OWNER)) ||
141
+ OxPrompt.open({
142
+ type: 'warning',
143
+ title: i18next.t('title.unsaved-draft', { defaultValue: 'there is an unsaved edit' }),
144
+ text: i18next.t('text.unsaved-may-be-lost', { defaultValue: 'anything not saved is lost.' }),
145
+ confirmButton: { text: i18next.t('button.discard', { defaultValue: 'discard' }), destructive: true },
146
+ cancelButton: { text: i18next.t('button.cancel', { defaultValue: 'cancel' }) },
147
+ signal: change.signal
148
+ })
149
+ });
121
150
  /* Back and forward walk the address; the dock follows it and adds no step of its own. */
122
151
  addressController().subscribe(() => {
123
152
  var _a;
@@ -138,12 +167,19 @@ export function closeEntity360() {
138
167
  close(true);
139
168
  }
140
169
  /**
141
- * Draw the shown 360 again - its title and context are read anew. For a kind whose title comes from
142
- * what its section has just read.
170
+ * What a section has read about its subject - its name, what the title and context are drawn from (ADR-0067
171
+ * decision 6). Opened from an address alone the value is `{ id }`; the section that reads who the subject is gives
172
+ * the rest here, the value is kept for going back to it, and the heading is drawn anew. The sections are left as
173
+ * they are. A subject no longer shown is only remembered.
143
174
  */
144
- export function redrawEntity360() {
145
- if (shown && isEntity360Shown(OWNER))
175
+ export function setEntity360Value(subject, value) {
176
+ const address = addressOf(subject.kind, subject.id);
177
+ const merged = { ...opened.get(address), ...value, id: subject.id };
178
+ opened.set(address, merged);
179
+ if ((shown === null || shown === void 0 ? void 0 : shown.kind) === subject.kind && shown.id === subject.id && isEntity360Shown(OWNER)) {
180
+ shown.value = merged;
146
181
  draw(shown);
182
+ }
147
183
  }
148
184
  const listeners = new Set();
149
185
  /**
@@ -191,7 +227,12 @@ function draw(subject, returnFocus) {
191
227
  back: i18next.t('button.back', { defaultValue: 'back' })
192
228
  },
193
229
  onClose: () => close(true),
194
- onBack: canGoBackEntity360() ? () => history.back() : undefined
230
+ onBack: canGoBackEntity360() ? () => history.back() : undefined,
231
+ /* A new subject of the same kind and id: every section reads its door again and keeps what it holds. */
232
+ onRefresh: () => {
233
+ if (shown === subject)
234
+ draw({ ...subject });
235
+ }
195
236
  }, returnFocus);
196
237
  if (declaration === null || declaration === void 0 ? void 0 : declaration.assistantSubject)
197
238
  takeSubject({ type: kind, id: subject.id });
@@ -1 +1 @@
1
- {"version":3,"file":"entity360-registry.js","sourceRoot":"","sources":["../../../src/actions/entity360-registry.ts"],"names":[],"mappings":"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","sourcesContent":["import { html, type TemplateResult } from 'lit'\nimport { i18next } from '@operato/i18n'\nimport { addressController, writeAddress } from '@operato/utils/address.js'\n\nimport { store, UPDATE_CONTEXT } from '@operato/shell'\n\nimport { showEntity360, hideEntity360, canGoBackEntity360, isEntity360Shown, navigateEntity360 } from './entity360.js'\n\nconst returnFocusByAddress = new Map<string, () => void>()\n\n/**\n * Which kinds of thing open a 360, and what each module puts in it (ADR-0067, the person 360 ruling).\n *\n * An entity's name is a link to its 360 (ADR-0067 decision 1), and a grid shows names by the dozen -\n * `updater` alone is a person column on 65 framework screens. Writing an opener into every column\n * would never finish. Instead a kind is declared once with the GraphQL `__typename`s it claims, and\n * whatever draws a value can ask `entity360KindOf(value)`: registered, it is a link; not, it stays\n * text, exactly as before.\n *\n * This file knows no product words and imports no product module. A kind brings its own title; each\n * module hangs its own section with its own read door (`registerEntity360Section`), the way activity\n * panels hang on the activity detail. Opening a kind with no section says so in one line - never a\n * blank dock.\n */\n\n/** The thing a 360 is about: its kind, its id, and the value it was opened from. */\nexport interface Entity360Subject {\n kind: string\n id: string\n value: Record<string, any>\n}\n\nexport interface Entity360KindDeclaration {\n /** The name sections register under - 'person', 'role', … */\n kind: string\n /** The GraphQL `__typename`s whose values open this kind. A typename belongs to one kind. */\n typenames: string[]\n /** The dock's title for a subject - what a person recognises it by. */\n title(subject: Entity360Subject): string\n /** A line under the title, if the kind has one. */\n context?(subject: Entity360Subject): string\n /**\n * The open 360 is what the page talks about: while it is open the page context's `subject` is\n * `{ type: kind, id }`, so the AI dock follows it (ai-assistant registerSubjectSurface). Closing\n * gives the page its own subject back. Off by default - a kind the assistant has no surface for\n * would only take the page's subject away.\n */\n assistantSubject?: boolean\n}\n\nexport interface Entity360Section {\n /** Unique within its kind. */\n name: string\n /** Lower comes first; equal orders keep the order they were registered in. */\n order?: number\n /** The section's content - usually the module's own element, which reads its own door. */\n render(subject: Entity360Subject): TemplateResult\n}\n\nexport type Entity360Rejection = { kind: string; name?: string; reason: string }\n\nconst kinds = new Map<string, Entity360KindDeclaration>()\nconst kindOfTypename = new Map<string, string>()\nconst sections = new Map<string, (Entity360Section & { at: number })[]>()\nconst rejected: Entity360Rejection[] = []\nlet registered = 0\n\n/** The dock has one owner for everything opened through this registry. */\nconst OWNER = {}\n\n/**\n * Declare a kind. A kind named twice, or a typename another kind already claims, is refused and\n * kept in `entity360Rejections()` - silently taking the second would reroute links nobody asked to move.\n */\nexport function registerEntity360Kind(declaration: Entity360KindDeclaration): void {\n const kind = String(declaration?.kind ?? '').trim()\n\n if (!kind) return void rejected.push({ kind: '(no kind)', reason: 'no-kind' })\n if (kinds.has(kind)) return void rejected.push({ kind, reason: 'duplicate-kind' })\n if (typeof declaration.title !== 'function') return void rejected.push({ kind, reason: 'no-title' })\n\n const taken = (declaration.typenames || []).find(typename => kindOfTypename.has(typename))\n if (taken) return void rejected.push({ kind, name: taken, reason: 'typename-claimed' })\n\n kinds.set(kind, { ...declaration, kind })\n for (const typename of declaration.typenames || []) kindOfTypename.set(typename, kind)\n}\n\n/** Hang a section on a kind. A name already on that kind is refused. */\nexport function registerEntity360Section(kind: string, section: Entity360Section): void {\n const name = String(section?.name ?? '').trim()\n\n if (!name) return void rejected.push({ kind, reason: 'no-name' })\n if (typeof section.render !== 'function') return void rejected.push({ kind, name, reason: 'no-render' })\n\n const list = sections.get(kind) ?? []\n if (list.some(one => one.name === name)) return void rejected.push({ kind, name, reason: 'duplicate-name' })\n\n list.push({ ...section, name, at: registered++ })\n list.sort((a, b) => (a.order ?? 0) - (b.order ?? 0) || a.at - b.at)\n sections.set(kind, list)\n}\n\n/**\n * The kind a value opens, or `undefined`.\n *\n * `undefined` for anything not registered and for a value without `__typename` (a call that turned\n * the cache off) - the caller draws it as it always has.\n */\nexport function entity360KindOf(value: unknown): string | undefined {\n const typename = (value as any)?.__typename\n return typeof typename === 'string' ? kindOfTypename.get(typename) : undefined\n}\n\n/** Whether a kind was declared - an identifier of an undeclared kind stays text. */\nexport function entity360KindRegistered(kind: string): boolean {\n return kinds.has(kind)\n}\n\n/** The sections hung on a kind, in order. */\nexport function entity360Sections(kind: string): Entity360Section[] {\n return [...(sections.get(kind) ?? [])]\n}\n\n/** What was refused, and why - for a test or a boot check to read. */\nexport function entity360Rejections(): Entity360Rejection[] {\n return [...rejected]\n}\n\nlet shown: Entity360Subject | undefined\n/** Values opened in this page, by address - going back to one gives its section the value it was opened with. */\nconst opened = new Map<string, Record<string, any>>()\n/** The page's own subject while a 360 stands in for it, and the page it belongs to. */\nlet pageSubject: { path: string; subject: unknown; ours?: unknown } | undefined\n\n/** The address of a subject: `f=kind:id`. The id may hold colons (an EPC URN); the kind does not. */\nconst FOCUS = 'f'\nconst addressOf = (kind: string, id: string) => `${kind}:${id}`\n\nfunction subjectInAddress(): { kind: string; id: string } | undefined {\n const value = new URLSearchParams(location.search).get(FOCUS)\n const at = value ? value.indexOf(':') : -1\n return at > 0 ? { kind: value!.slice(0, at), id: value!.slice(at + 1) } : undefined\n}\n\n/**\n * Open the 360 of a value.\n *\n * A kind with sections shows them in order. A kind with none - or a value that opens no kind -\n * shows one line saying there is no 360 for it yet. The dock is never blank.\n */\nexport function openEntity360For(value: Record<string, any>, returnFocus?: () => void, source?: Element): void {\n openEntity360Kind(entity360KindOf(value) ?? '', value, returnFocus, source)\n}\n\n/**\n * Open the 360 of a kind directly - for a value known by its kind and id rather than by a GraphQL\n * typename: an identifier inside a row, or a relation inside another 360.\n *\n * Opening is a step in the address (`f=kind:id`, the same step twin's focus takes), so the dock's back\n * button and the browser's go to the 360 before, and a reload or a shared link opens the same one.\n */\nexport function openEntity360Kind(\n kind: string,\n value: Record<string, any>,\n returnFocus?: () => void,\n /** The element the request comes from; a link inside a popup names that popup as its origin. */\n source?: Element\n): void {\n const subject: Entity360Subject = { kind, id: String(value?.id ?? ''), value: value ?? {} }\n const address = addressOf(kind, subject.id)\n opened.set(address, subject.value)\n if (returnFocus) returnFocusByAddress.set(address, returnFocus)\n\n if (kind && subject.id) {\n const url = new URL(location.href)\n if (url.searchParams.get(FOCUS) !== address) {\n url.searchParams.set(FOCUS, address)\n void navigateEntity360(url.pathname + url.search + url.hash, source)\n return\n }\n }\n\n draw(subject, returnFocus)\n}\n\n/* Back and forward walk the address; the dock follows it and adds no step of its own. */\naddressController().subscribe(() => {\n const inAddress = subjectInAddress()\n\n if (inAddress && kinds.has(inAddress.kind)) {\n if (shown?.kind === inAddress.kind && shown.id === inAddress.id && isEntity360Shown(OWNER)) return\n const value = opened.get(addressOf(inAddress.kind, inAddress.id)) ?? { id: inAddress.id }\n draw({ ...inAddress, value }, returnFocusByAddress.get(addressOf(inAddress.kind, inAddress.id)))\n } else if (isEntity360Shown(OWNER)) {\n close(false)\n }\n}, false)\n\n/** Close the registry's 360 and take its step out of the address - for an app whose own state let go of it. */\nexport function closeEntity360(): void {\n if (isEntity360Shown(OWNER)) close(true)\n}\n\n/**\n * Draw the shown 360 again - its title and context are read anew. For a kind whose title comes from\n * what its section has just read.\n */\nexport function redrawEntity360(): void {\n if (shown && isEntity360Shown(OWNER)) draw(shown)\n}\n\ntype Entity360Listener = (subject: Entity360Subject | undefined) => void\nconst listeners = new Set<Entity360Listener>()\n\n/**\n * Hear which subject the registry's 360 shows - on open, on back or forward, and `undefined` on close.\n * An app with its own selection (twin's focus) follows the dock through this. Returns the unsubscribe.\n */\nexport function onEntity360Change(listener: Entity360Listener): () => void {\n listeners.add(listener)\n return () => listeners.delete(listener)\n}\n\nfunction tell(subject: Entity360Subject | undefined): void {\n listeners.forEach(listener => listener(subject))\n}\n\nfunction close(clearAddress: boolean): void {\n if (clearAddress && subjectInAddress()) {\n const url = new URL(location.href)\n url.searchParams.delete(FOCUS)\n void writeAddress(url, { replace: true, state: history.state })\n return\n }\n hideEntity360(OWNER)\n shown = undefined\n giveSubjectBack()\n tell(undefined)\n}\n\nfunction draw(subject: Entity360Subject, returnFocus?: () => void): void {\n const { kind, value } = subject\n const declaration = kinds.get(kind)\n const hung = entity360Sections(kind)\n shown = subject\n\n const content = hung.length\n ? html`${hung.map(section => section.render(subject))}`\n : html`<p data-not-yet>${i18next.t('text.entity360-not-yet', { defaultValue: 'there is no 360 for this kind yet.' })}</p>`\n\n showEntity360(\n OWNER,\n {\n key: addressOf(kind, subject.id),\n title: declaration ? declaration.title(subject) : String(value?.name ?? value?.id ?? ''),\n context: declaration?.context ? declaration.context(subject) : '',\n content,\n labels: {\n close: i18next.t('button.close', { defaultValue: 'close' }),\n expand: i18next.t('button.expand', { defaultValue: 'expand' }),\n collapse: i18next.t('button.collapse', { defaultValue: 'collapse' }),\n resize: i18next.t('label.resize', { defaultValue: 'resize' }),\n back: i18next.t('button.back', { defaultValue: 'back' })\n },\n onClose: () => close(true),\n onBack: canGoBackEntity360() ? () => history.back() : undefined\n },\n returnFocus\n )\n\n if (declaration?.assistantSubject) takeSubject({ type: kind, id: subject.id })\n else giveSubjectBack()\n\n tell(subject)\n}\n\nfunction routeContext(): any {\n return (store.getState() as any)?.route?.context ?? {}\n}\n\nfunction takeSubject(ours: { type: string; id: string }): void {\n const context = routeContext()\n if (!pageSubject || pageSubject.path !== location.pathname) pageSubject = { path: location.pathname, subject: context.subject }\n pageSubject.ours = ours\n store.dispatch({ type: UPDATE_CONTEXT, context: { ...context, subject: ours } })\n}\n\n/** Only on the same page, and only while the subject is still the one the 360 put there. */\nfunction giveSubjectBack(): void {\n const taken = pageSubject\n pageSubject = undefined\n if (!taken || taken.path !== location.pathname) return\n\n const context = routeContext()\n if (context.subject === taken.ours) store.dispatch({ type: UPDATE_CONTEXT, context: { ...context, subject: taken.subject } })\n}\n"]}
1
+ {"version":3,"file":"entity360-registry.js","sourceRoot":"","sources":["../../../src/actions/entity360-registry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAuB,MAAM,KAAK,CAAA;AAC/C,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AACvC,OAAO,EAAE,QAAQ,EAAE,MAAM,6BAA6B,CAAA;AACtD,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAA;AAE3E,OAAO,EAAE,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AAEtD,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAA;AAEzI,MAAM,oBAAoB,GAAG,IAAI,GAAG,EAAsB,CAAA;AA2D1D,MAAM,KAAK,GAAG,IAAI,GAAG,EAAoC,CAAA;AACzD,MAAM,cAAc,GAAG,IAAI,GAAG,EAAkB,CAAA;AAChD,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAiD,CAAA;AACzE,MAAM,QAAQ,GAAyB,EAAE,CAAA;AACzC,IAAI,UAAU,GAAG,CAAC,CAAA;AAElB,0EAA0E;AAC1E,MAAM,KAAK,GAAG,EAAE,CAAA;AAEhB;;;GAGG;AACH,MAAM,UAAU,qBAAqB,CAAC,WAAqC;;IACzE,MAAM,IAAI,GAAG,MAAM,CAAC,MAAA,WAAW,aAAX,WAAW,uBAAX,WAAW,CAAE,IAAI,mCAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAA;IAEnD,IAAI,CAAC,IAAI;QAAE,OAAO,KAAK,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,CAAA;IAC9E,IAAI,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC;QAAE,OAAO,KAAK,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,gBAAgB,EAAE,CAAC,CAAA;IAClF,IAAI,OAAO,WAAW,CAAC,KAAK,KAAK,UAAU;QAAE,OAAO,KAAK,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC,CAAA;IAEpG,MAAM,KAAK,GAAG,CAAC,WAAW,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,cAAc,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAA;IAC1F,IAAI,KAAK;QAAE,OAAO,KAAK,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,kBAAkB,EAAE,CAAC,CAAA;IAEvF,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,GAAG,WAAW,EAAE,IAAI,EAAE,CAAC,CAAA;IACzC,KAAK,MAAM,QAAQ,IAAI,WAAW,CAAC,SAAS,IAAI,EAAE;QAAE,cAAc,CAAC,GAAG,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAA;AACxF,CAAC;AAED,wEAAwE;AACxE,MAAM,UAAU,wBAAwB,CAAC,IAAY,EAAE,OAAyB;;IAC9E,MAAM,IAAI,GAAG,MAAM,CAAC,MAAA,OAAO,aAAP,OAAO,uBAAP,OAAO,CAAE,IAAI,mCAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAA;IAE/C,IAAI,CAAC,IAAI;QAAE,OAAO,KAAK,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,CAAA;IACjE,IAAI,OAAO,OAAO,CAAC,MAAM,KAAK,UAAU;QAAE,OAAO,KAAK,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC,CAAA;IAExG,MAAM,IAAI,GAAG,MAAA,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,mCAAI,EAAE,CAAA;IACrC,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,IAAI,KAAK,IAAI,CAAC;QAAE,OAAO,KAAK,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,gBAAgB,EAAE,CAAC,CAAA;IAE5G,IAAI,CAAC,IAAI,CAAC,EAAE,GAAG,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,UAAU,EAAE,EAAE,CAAC,CAAA;IACjD,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,eAAC,OAAA,CAAC,MAAA,CAAC,CAAC,KAAK,mCAAI,CAAC,CAAC,GAAG,CAAC,MAAA,CAAC,CAAC,KAAK,mCAAI,CAAC,CAAC,IAAI,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,EAAE,CAAA,EAAA,CAAC,CAAA;IACnE,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,CAAA;AAC1B,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAAC,KAAc;IAC5C,MAAM,QAAQ,GAAI,KAAa,aAAb,KAAK,uBAAL,KAAK,CAAU,UAAU,CAAA;IAC3C,OAAO,OAAO,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,cAAc,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;AAChF,CAAC;AAED,4GAA4G;AAC5G,MAAM,UAAU,aAAa,CAAC,IAAY,EAAE,KAA6C;;IACvF,MAAM,QAAQ,GAAG,MAAA,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,0CAAE,EAAE,CAAA;IACpC,OAAO,MAAM,CAAC,MAAA,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,aAAL,KAAK,cAAL,KAAK,GAAI,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,aAAL,KAAK,uBAAL,KAAK,CAAE,EAAE,CAAC,mCAAI,EAAE,CAAC,CAAA;AACrE,CAAC;AAED,oFAAoF;AACpF,MAAM,UAAU,uBAAuB,CAAC,IAAY;IAClD,OAAO,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;AACxB,CAAC;AAED,6CAA6C;AAC7C,MAAM,UAAU,iBAAiB,CAAC,IAAY;;IAC5C,OAAO,CAAC,GAAG,CAAC,MAAA,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,mCAAI,EAAE,CAAC,CAAC,CAAA;AACxC,CAAC;AAED,sEAAsE;AACtE,MAAM,UAAU,mBAAmB;IACjC,OAAO,CAAC,GAAG,QAAQ,CAAC,CAAA;AACtB,CAAC;AAED,IAAI,KAAmC,CAAA;AACvC,iHAAiH;AACjH,MAAM,MAAM,GAAG,IAAI,GAAG,EAA+B,CAAA;AACrD,uFAAuF;AACvF,IAAI,WAA2E,CAAA;AAE/E,qGAAqG;AACrG,MAAM,KAAK,GAAG,GAAG,CAAA;AACjB,MAAM,SAAS,GAAG,CAAC,IAAY,EAAE,EAAU,EAAE,EAAE,CAAC,GAAG,IAAI,IAAI,EAAE,EAAE,CAAA;AAE/D,SAAS,gBAAgB;IACvB,MAAM,KAAK,GAAG,IAAI,eAAe,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAA;IAC7D,MAAM,EAAE,GAAG,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IAC1C,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,KAAM,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,EAAE,EAAE,KAAM,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,CAAA;AACrF,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAA0B,EAAE,WAAwB,EAAE,MAAgB;;IACrG,iBAAiB,CAAC,MAAA,eAAe,CAAC,KAAK,CAAC,mCAAI,EAAE,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,CAAC,CAAA;AAC7E,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB,CAC/B,IAAY,EACZ,KAA0B,EAC1B,WAAwB;AACxB,gGAAgG;AAChG,MAAgB;IAEhB,MAAM,OAAO,GAAqB,EAAE,IAAI,EAAE,EAAE,EAAE,aAAa,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE,KAAK,EAAE,KAAK,aAAL,KAAK,cAAL,KAAK,GAAI,EAAE,EAAE,CAAA;IAC9F,MAAM,OAAO,GAAG,SAAS,CAAC,IAAI,EAAE,OAAO,CAAC,EAAE,CAAC,CAAA;IAC3C,MAAM,CAAC,GAAG,CAAC,OAAO,EAAE,OAAO,CAAC,KAAK,CAAC,CAAA;IAClC,IAAI,WAAW;QAAE,oBAAoB,CAAC,GAAG,CAAC,OAAO,EAAE,WAAW,CAAC,CAAA;IAE/D,IAAI,IAAI,IAAI,OAAO,CAAC,EAAE,EAAE,CAAC;QACvB,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAA;QAClC,IAAI,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,OAAO,EAAE,CAAC;YAC5C,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,KAAK,EAAE,OAAO,CAAC,CAAA;YACpC,KAAK,iBAAiB,CAAC,GAAG,CAAC,QAAQ,GAAG,GAAG,CAAC,MAAM,GAAG,GAAG,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;YACpE,OAAM;QACR,CAAC;IACH,CAAC;IAED;;;OAGG;IACH,IAAI,CAAA,KAAK,aAAL,KAAK,uBAAL,KAAK,CAAE,IAAI,MAAK,IAAI,IAAI,KAAK,CAAC,EAAE,KAAK,OAAO,CAAC,EAAE,IAAI,gBAAgB,CAAC,KAAK,CAAC;QAAE,OAAM;IAEtF,IAAI,CAAC,OAAO,EAAE,WAAW,CAAC,CAAA;AAC5B,CAAC;AAED;;;GAGG;AACH,iBAAiB,EAAE,CAAC,KAAK,CAAC;IACxB,QAAQ,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,IAAI,MAAM,CAAC,KAAK,CAAC,IAAI,iBAAiB,CAAC,KAAK,CAAC;IAC9E,IAAI,EAAE,GAAG,EAAE,CAAC,KAAK;IACjB,OAAO,EAAE,MAAM,CAAC,EAAE,CAChB,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,IAAI,MAAM,CAAC,KAAK,CAAC,IAAI,iBAAiB,CAAC,KAAK,CAAC,CAAC;QAC7D,QAAQ,CAAC,IAAI,CAAC;YACZ,IAAI,EAAE,SAAS;YACf,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,qBAAqB,EAAE,EAAE,YAAY,EAAE,0BAA0B,EAAE,CAAC;YACrF,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,0BAA0B,EAAE,EAAE,YAAY,EAAE,6BAA6B,EAAE,CAAC;YAC5F,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;YAC9E,MAAM,EAAE,MAAM,CAAC,MAAM;SACtB,CAAC;CACL,CAAC,CAAA;AAEF,yFAAyF;AACzF,iBAAiB,EAAE,CAAC,SAAS,CAAC,GAAG,EAAE;;IACjC,MAAM,SAAS,GAAG,gBAAgB,EAAE,CAAA;IAEpC,IAAI,SAAS,IAAI,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC;QAC3C,IAAI,CAAA,KAAK,aAAL,KAAK,uBAAL,KAAK,CAAE,IAAI,MAAK,SAAS,CAAC,IAAI,IAAI,KAAK,CAAC,EAAE,KAAK,SAAS,CAAC,EAAE,IAAI,gBAAgB,CAAC,KAAK,CAAC;YAAE,OAAM;QAClG,MAAM,KAAK,GAAG,MAAA,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,SAAS,CAAC,IAAI,EAAE,SAAS,CAAC,EAAE,CAAC,CAAC,mCAAI,EAAE,EAAE,EAAE,SAAS,CAAC,EAAE,EAAE,CAAA;QACzF,IAAI,CAAC,EAAE,GAAG,SAAS,EAAE,KAAK,EAAE,EAAE,oBAAoB,CAAC,GAAG,CAAC,SAAS,CAAC,SAAS,CAAC,IAAI,EAAE,SAAS,CAAC,EAAE,CAAC,CAAC,CAAC,CAAA;IAClG,CAAC;SAAM,IAAI,gBAAgB,CAAC,KAAK,CAAC,EAAE,CAAC;QACnC,KAAK,CAAC,KAAK,CAAC,CAAA;IACd,CAAC;AACH,CAAC,EAAE,KAAK,CAAC,CAAA;AAET,+GAA+G;AAC/G,MAAM,UAAU,cAAc;IAC5B,IAAI,gBAAgB,CAAC,KAAK,CAAC;QAAE,KAAK,CAAC,IAAI,CAAC,CAAA;AAC1C,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,iBAAiB,CAAC,OAA8C,EAAE,KAA0B;IAC1G,MAAM,OAAO,GAAG,SAAS,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,EAAE,CAAC,CAAA;IACnD,MAAM,MAAM,GAAG,EAAE,GAAG,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,GAAG,KAAK,EAAE,EAAE,EAAE,OAAO,CAAC,EAAE,EAAE,CAAA;IACnE,MAAM,CAAC,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,CAAA;IAC3B,IAAI,CAAA,KAAK,aAAL,KAAK,uBAAL,KAAK,CAAE,IAAI,MAAK,OAAO,CAAC,IAAI,IAAI,KAAK,CAAC,EAAE,KAAK,OAAO,CAAC,EAAE,IAAI,gBAAgB,CAAC,KAAK,CAAC,EAAE,CAAC;QACvF,KAAK,CAAC,KAAK,GAAG,MAAM,CAAA;QACpB,IAAI,CAAC,KAAK,CAAC,CAAA;IACb,CAAC;AACH,CAAC;AAGD,MAAM,SAAS,GAAG,IAAI,GAAG,EAAqB,CAAA;AAE9C;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAAC,QAA2B;IAC3D,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;IACvB,OAAO,GAAG,EAAE,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAA;AACzC,CAAC;AAED,SAAS,IAAI,CAAC,OAAqC;IACjD,SAAS,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAA;AAClD,CAAC;AAED,SAAS,KAAK,CAAC,YAAqB;IAClC,IAAI,YAAY,IAAI,gBAAgB,EAAE,EAAE,CAAC;QACvC,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAA;QAClC,GAAG,CAAC,YAAY,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;QAC9B,KAAK,YAAY,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,CAAC,CAAA;QAC/D,OAAM;IACR,CAAC;IACD,aAAa,CAAC,KAAK,CAAC,CAAA;IACpB,KAAK,GAAG,SAAS,CAAA;IACjB,eAAe,EAAE,CAAA;IACjB,IAAI,CAAC,SAAS,CAAC,CAAA;AACjB,CAAC;AAED,SAAS,IAAI,CAAC,OAAyB,EAAE,WAAwB;;IAC/D,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,OAAO,CAAA;IAC/B,MAAM,WAAW,GAAG,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;IACnC,MAAM,IAAI,GAAG,iBAAiB,CAAC,IAAI,CAAC,CAAA;IACpC,KAAK,GAAG,OAAO,CAAA;IAEf,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM;QACzB,CAAC,CAAC,IAAI,CAAA,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,EAAE;QACvD,CAAC,CAAC,IAAI,CAAA,mBAAmB,OAAO,CAAC,CAAC,CAAC,wBAAwB,EAAE,EAAE,YAAY,EAAE,oCAAoC,EAAE,CAAC,MAAM,CAAA;IAE5H,aAAa,CACX,KAAK,EACL;QACE,GAAG,EAAE,SAAS,CAAC,IAAI,EAAE,OAAO,CAAC,EAAE,CAAC;QAChC,KAAK,EAAE,WAAW,CAAC,CAAC,CAAC,WAAW,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,MAAA,MAAA,KAAK,aAAL,KAAK,uBAAL,KAAK,CAAE,IAAI,mCAAI,KAAK,aAAL,KAAK,uBAAL,KAAK,CAAE,EAAE,mCAAI,EAAE,CAAC;QACxF,OAAO,EAAE,CAAA,WAAW,aAAX,WAAW,uBAAX,WAAW,CAAE,OAAO,EAAC,CAAC,CAAC,WAAW,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE;QACjE,OAAO;QACP,MAAM,EAAE;YACN,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,cAAc,EAAE,EAAE,YAAY,EAAE,OAAO,EAAE,CAAC;YAC3D,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,eAAe,EAAE,EAAE,YAAY,EAAE,QAAQ,EAAE,CAAC;YAC9D,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC,iBAAiB,EAAE,EAAE,YAAY,EAAE,UAAU,EAAE,CAAC;YACpE,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,cAAc,EAAE,EAAE,YAAY,EAAE,QAAQ,EAAE,CAAC;YAC7D,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,aAAa,EAAE,EAAE,YAAY,EAAE,MAAM,EAAE,CAAC;SACzD;QACD,OAAO,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC;QAC1B,MAAM,EAAE,kBAAkB,EAAE,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,SAAS;QAC/D,wGAAwG;QACxG,SAAS,EAAE,GAAG,EAAE;YACd,IAAI,KAAK,KAAK,OAAO;gBAAE,IAAI,CAAC,EAAE,GAAG,OAAO,EAAE,CAAC,CAAA;QAC7C,CAAC;KACF,EACD,WAAW,CACZ,CAAA;IAED,IAAI,WAAW,aAAX,WAAW,uBAAX,WAAW,CAAE,gBAAgB;QAAE,WAAW,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,OAAO,CAAC,EAAE,EAAE,CAAC,CAAA;;QACzE,eAAe,EAAE,CAAA;IAEtB,IAAI,CAAC,OAAO,CAAC,CAAA;AACf,CAAC;AAED,SAAS,YAAY;;IACnB,OAAO,MAAA,MAAA,MAAC,KAAK,CAAC,QAAQ,EAAU,0CAAE,KAAK,0CAAE,OAAO,mCAAI,EAAE,CAAA;AACxD,CAAC;AAED,SAAS,WAAW,CAAC,IAAkC;IACrD,MAAM,OAAO,GAAG,YAAY,EAAE,CAAA;IAC9B,IAAI,CAAC,WAAW,IAAI,WAAW,CAAC,IAAI,KAAK,QAAQ,CAAC,QAAQ;QAAE,WAAW,GAAG,EAAE,IAAI,EAAE,QAAQ,CAAC,QAAQ,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,CAAA;IAC/H,WAAW,CAAC,IAAI,GAAG,IAAI,CAAA;IACvB,KAAK,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,EAAE,GAAG,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,CAAC,CAAA;AAClF,CAAC;AAED,4FAA4F;AAC5F,SAAS,eAAe;IACtB,MAAM,KAAK,GAAG,WAAW,CAAA;IACzB,WAAW,GAAG,SAAS,CAAA;IACvB,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,IAAI,KAAK,QAAQ,CAAC,QAAQ;QAAE,OAAM;IAEtD,MAAM,OAAO,GAAG,YAAY,EAAE,CAAA;IAC9B,IAAI,OAAO,CAAC,OAAO,KAAK,KAAK,CAAC,IAAI;QAAE,KAAK,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,EAAE,GAAG,OAAO,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,EAAE,CAAC,CAAA;AAC/H,CAAC","sourcesContent":["import { html, type TemplateResult } from 'lit'\nimport { i18next } from '@operato/i18n'\nimport { OxPrompt } from '@operato/popup/ox-prompt.js'\nimport { addressController, writeAddress } from '@operato/utils/address.js'\n\nimport { store, UPDATE_CONTEXT } from '@operato/shell'\n\nimport { showEntity360, hideEntity360, canGoBackEntity360, isEntity360Shown, isEntity360Unkept, navigateEntity360 } from './entity360.js'\n\nconst returnFocusByAddress = new Map<string, () => void>()\n\n/**\n * Which kinds of thing open a 360, and what each module puts in it (ADR-0067, the person 360 ruling).\n *\n * An entity's name is a link to its 360 (ADR-0067 decision 1), and a grid shows names by the dozen -\n * `updater` alone is a person column on 65 framework screens. Writing an opener into every column\n * would never finish. Instead a kind is declared once with the GraphQL `__typename`s it claims, and\n * whatever draws a value can ask `entity360KindOf(value)`: registered, it is a link; not, it stays\n * text, exactly as before.\n *\n * This file knows no product words and imports no product module. A kind brings its own title; each\n * module hangs its own section with its own read door (`registerEntity360Section`), the way activity\n * panels hang on the activity detail. Opening a kind with no section says so in one line - never a\n * blank dock.\n */\n\n/** The thing a 360 is about: its kind, its id, and the value it was opened from. */\nexport interface Entity360Subject {\n kind: string\n id: string\n value: Record<string, any>\n}\n\nexport interface Entity360KindDeclaration {\n /** The name sections register under - 'person', 'role', … */\n kind: string\n /** The GraphQL `__typename`s whose values open this kind. A typename belongs to one kind. */\n typenames: string[]\n /**\n * The kind's one identifier for a value - what the address, the links and `subject.id` all carry (ADR-0067\n * decision 6). A section reads its door by `subject.id` alone: a reload or a shared link brings nothing else.\n * Without it, `value.id`.\n */\n id?(value: Record<string, any>): string | null | undefined\n /** The dock's title for a subject - what a person recognises it by. */\n title(subject: Entity360Subject): string\n /** A line under the title, if the kind has one. */\n context?(subject: Entity360Subject): string\n /**\n * The open 360 is what the page talks about: while it is open the page context's `subject` is\n * `{ type: kind, id }`, so the AI dock follows it (ai-assistant registerSubjectSurface). Closing\n * gives the page its own subject back. Off by default - a kind the assistant has no surface for\n * would only take the page's subject away.\n */\n assistantSubject?: boolean\n}\n\nexport interface Entity360Section {\n /** Unique within its kind. */\n name: string\n /** Lower comes first; equal orders keep the order they were registered in. */\n order?: number\n /** The section's content - usually the module's own element, which reads its own door. */\n render(subject: Entity360Subject): TemplateResult\n}\n\nexport type Entity360Rejection = { kind: string; name?: string; reason: string }\n\nconst kinds = new Map<string, Entity360KindDeclaration>()\nconst kindOfTypename = new Map<string, string>()\nconst sections = new Map<string, (Entity360Section & { at: number })[]>()\nconst rejected: Entity360Rejection[] = []\nlet registered = 0\n\n/** The dock has one owner for everything opened through this registry. */\nconst OWNER = {}\n\n/**\n * Declare a kind. A kind named twice, or a typename another kind already claims, is refused and\n * kept in `entity360Rejections()` - silently taking the second would reroute links nobody asked to move.\n */\nexport function registerEntity360Kind(declaration: Entity360KindDeclaration): void {\n const kind = String(declaration?.kind ?? '').trim()\n\n if (!kind) return void rejected.push({ kind: '(no kind)', reason: 'no-kind' })\n if (kinds.has(kind)) return void rejected.push({ kind, reason: 'duplicate-kind' })\n if (typeof declaration.title !== 'function') return void rejected.push({ kind, reason: 'no-title' })\n\n const taken = (declaration.typenames || []).find(typename => kindOfTypename.has(typename))\n if (taken) return void rejected.push({ kind, name: taken, reason: 'typename-claimed' })\n\n kinds.set(kind, { ...declaration, kind })\n for (const typename of declaration.typenames || []) kindOfTypename.set(typename, kind)\n}\n\n/** Hang a section on a kind. A name already on that kind is refused. */\nexport function registerEntity360Section(kind: string, section: Entity360Section): void {\n const name = String(section?.name ?? '').trim()\n\n if (!name) return void rejected.push({ kind, reason: 'no-name' })\n if (typeof section.render !== 'function') return void rejected.push({ kind, name, reason: 'no-render' })\n\n const list = sections.get(kind) ?? []\n if (list.some(one => one.name === name)) return void rejected.push({ kind, name, reason: 'duplicate-name' })\n\n list.push({ ...section, name, at: registered++ })\n list.sort((a, b) => (a.order ?? 0) - (b.order ?? 0) || a.at - b.at)\n sections.set(kind, list)\n}\n\n/**\n * The kind a value opens, or `undefined`.\n *\n * `undefined` for anything not registered and for a value without `__typename` (a call that turned\n * the cache off) - the caller draws it as it always has.\n */\nexport function entity360KindOf(value: unknown): string | undefined {\n const typename = (value as any)?.__typename\n return typeof typename === 'string' ? kindOfTypename.get(typename) : undefined\n}\n\n/** The identifier a kind gives a value (its declaration's `id`, else `value.id`); '' when there is none. */\nexport function entity360IdOf(kind: string, value: Record<string, any> | null | undefined): string {\n const declared = kinds.get(kind)?.id\n return String((declared ? declared(value ?? {}) : value?.id) ?? '')\n}\n\n/** Whether a kind was declared - an identifier of an undeclared kind stays text. */\nexport function entity360KindRegistered(kind: string): boolean {\n return kinds.has(kind)\n}\n\n/** The sections hung on a kind, in order. */\nexport function entity360Sections(kind: string): Entity360Section[] {\n return [...(sections.get(kind) ?? [])]\n}\n\n/** What was refused, and why - for a test or a boot check to read. */\nexport function entity360Rejections(): Entity360Rejection[] {\n return [...rejected]\n}\n\nlet shown: Entity360Subject | undefined\n/** Values opened in this page, by address - going back to one gives its section the value it was opened with. */\nconst opened = new Map<string, Record<string, any>>()\n/** The page's own subject while a 360 stands in for it, and the page it belongs to. */\nlet pageSubject: { path: string; subject: unknown; ours?: unknown } | undefined\n\n/** The address of a subject: `f=kind:id`. The id may hold colons (an EPC URN); the kind does not. */\nconst FOCUS = 'f'\nconst addressOf = (kind: string, id: string) => `${kind}:${id}`\n\nfunction subjectInAddress(): { kind: string; id: string } | undefined {\n const value = new URLSearchParams(location.search).get(FOCUS)\n const at = value ? value.indexOf(':') : -1\n return at > 0 ? { kind: value!.slice(0, at), id: value!.slice(at + 1) } : undefined\n}\n\n/**\n * Open the 360 of a value.\n *\n * A kind with sections shows them in order. A kind with none - or a value that opens no kind -\n * shows one line saying there is no 360 for it yet. The dock is never blank.\n */\nexport function openEntity360For(value: Record<string, any>, returnFocus?: () => void, source?: Element): void {\n openEntity360Kind(entity360KindOf(value) ?? '', value, returnFocus, source)\n}\n\n/**\n * Open the 360 of a kind directly - for a value known by its kind and id rather than by a GraphQL\n * typename: an identifier inside a row, or a relation inside another 360.\n *\n * Opening is a step in the address (`f=kind:id`, the same step twin's focus takes), so the dock's back\n * button and the browser's go to the 360 before, and a reload or a shared link opens the same one.\n */\nexport function openEntity360Kind(\n kind: string,\n value: Record<string, any>,\n returnFocus?: () => void,\n /** The element the request comes from; a link inside a popup names that popup as its origin. */\n source?: Element\n): void {\n const subject: Entity360Subject = { kind, id: entity360IdOf(kind, value), value: value ?? {} }\n const address = addressOf(kind, subject.id)\n opened.set(address, subject.value)\n if (returnFocus) returnFocusByAddress.set(address, returnFocus)\n\n if (kind && subject.id) {\n const url = new URL(location.href)\n if (url.searchParams.get(FOCUS) !== address) {\n url.searchParams.set(FOCUS, address)\n void navigateEntity360(url.pathname + url.search + url.hash, source)\n return\n }\n }\n\n /*\n * Already shown - an app that wrote the address itself (twin's focus) opens it again right after the address drew\n * it. Drawing again would hand the sections a new subject, which is how a refresh asks them to read.\n */\n if (shown?.kind === kind && shown.id === subject.id && isEntity360Shown(OWNER)) return\n\n draw(subject, returnFocus)\n}\n\n/*\n * What a 360 section holds unsaved goes when its subject does. A draft keeps it (ADR-0098) - only an entry this\n * browser cannot keep is asked about, with the page's own question, by the address layer's one guard.\n */\naddressController().guard({\n discards: change => (change.focus || change.route) && isEntity360Unkept(OWNER),\n kept: () => false,\n approve: change =>\n !((change.focus || change.route) && isEntity360Unkept(OWNER)) ||\n OxPrompt.open({\n type: 'warning',\n title: i18next.t('title.unsaved-draft', { defaultValue: 'there is an unsaved edit' }),\n text: i18next.t('text.unsaved-may-be-lost', { defaultValue: 'anything not saved is lost.' }),\n confirmButton: { text: i18next.t('button.discard', { defaultValue: 'discard' }), destructive: true },\n cancelButton: { text: i18next.t('button.cancel', { defaultValue: 'cancel' }) },\n signal: change.signal\n })\n})\n\n/* Back and forward walk the address; the dock follows it and adds no step of its own. */\naddressController().subscribe(() => {\n const inAddress = subjectInAddress()\n\n if (inAddress && kinds.has(inAddress.kind)) {\n if (shown?.kind === inAddress.kind && shown.id === inAddress.id && isEntity360Shown(OWNER)) return\n const value = opened.get(addressOf(inAddress.kind, inAddress.id)) ?? { id: inAddress.id }\n draw({ ...inAddress, value }, returnFocusByAddress.get(addressOf(inAddress.kind, inAddress.id)))\n } else if (isEntity360Shown(OWNER)) {\n close(false)\n }\n}, false)\n\n/** Close the registry's 360 and take its step out of the address - for an app whose own state let go of it. */\nexport function closeEntity360(): void {\n if (isEntity360Shown(OWNER)) close(true)\n}\n\n/**\n * What a section has read about its subject - its name, what the title and context are drawn from (ADR-0067\n * decision 6). Opened from an address alone the value is `{ id }`; the section that reads who the subject is gives\n * the rest here, the value is kept for going back to it, and the heading is drawn anew. The sections are left as\n * they are. A subject no longer shown is only remembered.\n */\nexport function setEntity360Value(subject: Pick<Entity360Subject, 'kind' | 'id'>, value: Record<string, any>): void {\n const address = addressOf(subject.kind, subject.id)\n const merged = { ...opened.get(address), ...value, id: subject.id }\n opened.set(address, merged)\n if (shown?.kind === subject.kind && shown.id === subject.id && isEntity360Shown(OWNER)) {\n shown.value = merged\n draw(shown)\n }\n}\n\ntype Entity360Listener = (subject: Entity360Subject | undefined) => void\nconst listeners = new Set<Entity360Listener>()\n\n/**\n * Hear which subject the registry's 360 shows - on open, on back or forward, and `undefined` on close.\n * An app with its own selection (twin's focus) follows the dock through this. Returns the unsubscribe.\n */\nexport function onEntity360Change(listener: Entity360Listener): () => void {\n listeners.add(listener)\n return () => listeners.delete(listener)\n}\n\nfunction tell(subject: Entity360Subject | undefined): void {\n listeners.forEach(listener => listener(subject))\n}\n\nfunction close(clearAddress: boolean): void {\n if (clearAddress && subjectInAddress()) {\n const url = new URL(location.href)\n url.searchParams.delete(FOCUS)\n void writeAddress(url, { replace: true, state: history.state })\n return\n }\n hideEntity360(OWNER)\n shown = undefined\n giveSubjectBack()\n tell(undefined)\n}\n\nfunction draw(subject: Entity360Subject, returnFocus?: () => void): void {\n const { kind, value } = subject\n const declaration = kinds.get(kind)\n const hung = entity360Sections(kind)\n shown = subject\n\n const content = hung.length\n ? html`${hung.map(section => section.render(subject))}`\n : html`<p data-not-yet>${i18next.t('text.entity360-not-yet', { defaultValue: 'there is no 360 for this kind yet.' })}</p>`\n\n showEntity360(\n OWNER,\n {\n key: addressOf(kind, subject.id),\n title: declaration ? declaration.title(subject) : String(value?.name ?? value?.id ?? ''),\n context: declaration?.context ? declaration.context(subject) : '',\n content,\n labels: {\n close: i18next.t('button.close', { defaultValue: 'close' }),\n expand: i18next.t('button.expand', { defaultValue: 'expand' }),\n collapse: i18next.t('button.collapse', { defaultValue: 'collapse' }),\n resize: i18next.t('label.resize', { defaultValue: 'resize' }),\n back: i18next.t('button.back', { defaultValue: 'back' })\n },\n onClose: () => close(true),\n onBack: canGoBackEntity360() ? () => history.back() : undefined,\n /* A new subject of the same kind and id: every section reads its door again and keeps what it holds. */\n onRefresh: () => {\n if (shown === subject) draw({ ...subject })\n }\n },\n returnFocus\n )\n\n if (declaration?.assistantSubject) takeSubject({ type: kind, id: subject.id })\n else giveSubjectBack()\n\n tell(subject)\n}\n\nfunction routeContext(): any {\n return (store.getState() as any)?.route?.context ?? {}\n}\n\nfunction takeSubject(ours: { type: string; id: string }): void {\n const context = routeContext()\n if (!pageSubject || pageSubject.path !== location.pathname) pageSubject = { path: location.pathname, subject: context.subject }\n pageSubject.ours = ours\n store.dispatch({ type: UPDATE_CONTEXT, context: { ...context, subject: ours } })\n}\n\n/** Only on the same page, and only while the subject is still the one the 360 put there. */\nfunction giveSubjectBack(): void {\n const taken = pageSubject\n pageSubject = undefined\n if (!taken || taken.path !== location.pathname) return\n\n const context = routeContext()\n if (context.subject === taken.ours) store.dispatch({ type: UPDATE_CONTEXT, context: { ...context, subject: taken.subject } })\n}\n"]}
@@ -3,6 +3,8 @@ import { type Entity360View } from '../components/ox-entity360-dock.js';
3
3
  export declare function showEntity360(owner: object, view: Entity360View, returnFocus?: () => void): void;
4
4
  /** Whether the dock is open for this owner - closed by its button, by another owner or by navigation, it is not. */
5
5
  export declare function isEntity360Shown(owner: object): boolean;
6
+ /** Whether the owner's open 360 holds an entry no draft keeps (ADR-0067 decision 8). */
7
+ export declare function isEntity360Unkept(owner: object): boolean;
6
8
  export declare function hideEntity360(owner: object, restoreFocus?: boolean): void;
7
9
  /**
8
10
  * Providers validate the canonical axis and read scope before calling this function. `source` is the element the
@@ -30,6 +30,10 @@ export function showEntity360(owner, view, returnFocus) {
30
30
  export function isEntity360Shown(owner) {
31
31
  return (current === null || current === void 0 ? void 0 : current.owner) === owner;
32
32
  }
33
+ /** Whether the owner's open 360 holds an entry no draft keeps (ADR-0067 decision 8). */
34
+ export function isEntity360Unkept(owner) {
35
+ return (current === null || current === void 0 ? void 0 : current.owner) === owner && current.dock.unkept;
36
+ }
33
37
  export function hideEntity360(owner, restoreFocus = true) {
34
38
  if ((current === null || current === void 0 ? void 0 : current.owner) !== owner)
35
39
  return;
@@ -1 +1 @@
1
- {"version":3,"file":"entity360.js","sourceRoot":"","sources":["../../../src/actions/entity360.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,KAAK,CAAA;AAC1B,OAAO,EAAE,KAAK,EAAE,4BAA4B,EAAE,MAAM,gBAAgB,CAAA;AACpE,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAE/E,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAA;AAExD,MAAM,IAAI,GAAG,WAAW,CAAA;AACxB,IAAI,OAAqG,CAAA;AAEzG,mGAAmG;AACnG,KAAK,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,4BAA4B,EAAE,QAAQ,EAAE,GAAG,EAAE;QAClE,IAAI,OAAO,IAAI,OAAO,CAAC,IAAI,KAAK,QAAQ,CAAC,QAAQ;YAAE,aAAa,CAAC,OAAO,CAAC,KAAK,EAAE,KAAK,CAAC,CAAA;IACxF,CAAC,EAAE,CAAC,CAAA;AAEJ,+FAA+F;AAC/F,MAAM,UAAU,aAAa,CAAC,KAAa,EAAE,IAAmB,EAAE,WAAwB;IACxF,IAAI,CAAA,OAAO,aAAP,OAAO,uBAAP,OAAO,CAAE,KAAK,MAAK,KAAK,EAAE,CAAC;QAC7B,IAAI,OAAO;YAAE,aAAa,CAAC,OAAO,CAAC,KAAK,EAAE,KAAK,CAAC,CAAA;QAChD,MAAM,IAAI,GAAG,QAAQ,CAAC,aAAa,CAAC,mBAAmB,CAAoB,CAAA;QAC3E,OAAO,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,CAAC,QAAQ,EAAE,WAAW,EAAE,CAAA;QAC/D,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;QAChB,cAAc,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,iBAAiB,CAAC,QAAQ;YAC/D,QAAQ,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,CAAA,GAAG,IAAI,EAAE,EAAE,EAAE,CAAC,CAAA;IAC1E,CAAC;SAAM,CAAC;QACN,OAAO,CAAC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;QACxB,IAAI,WAAW;YAAE,OAAO,CAAC,WAAW,GAAG,WAAW,CAAA;IACpD,CAAC;AACH,CAAC;AAED,oHAAoH;AACpH,MAAM,UAAU,gBAAgB,CAAC,KAAa;IAC5C,OAAO,CAAA,OAAO,aAAP,OAAO,uBAAP,OAAO,CAAE,KAAK,MAAK,KAAK,CAAA;AACjC,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,KAAa,EAAE,YAAY,GAAG,IAAI;IAC9D,IAAI,CAAA,OAAO,aAAP,OAAO,uBAAP,OAAO,CAAE,KAAK,MAAK,KAAK;QAAE,OAAM;IACpC,MAAM,QAAQ,GAAG,OAAO,CAAA;IACxB,OAAO,GAAG,SAAS,CAAA;IACnB,cAAc,CAAC,IAAI,CAAC,CAAA;IACpB,IAAI,YAAY;QAAE,cAAc,CAAC,GAAG,EAAE,WAAC,OAAA,MAAA,QAAQ,CAAC,WAAW,wDAAI,CAAA,EAAA,CAAC,CAAA;AAClE,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAAC,OAAe,EAAE,MAAgB;IACjE,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,OAAO,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAA;IAC3C,IAAI,GAAG,CAAC,MAAM,KAAK,QAAQ,CAAC,MAAM;QAAE,MAAM,IAAI,KAAK,CAAC,6CAA6C,CAAC,CAAA;IAClG,IAAI,GAAG,CAAC,IAAI,KAAK,QAAQ,CAAC,IAAI;QAAE,OAAO,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;IAC5D,OAAO,YAAY,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,EAAE,GAAG,OAAO,CAAC,KAAK,EAAE,iBAAiB,EAAE,QAAQ,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,CAAC,CAAA;AACrG,CAAC;AAED,MAAM,UAAU,kBAAkB;;IAChC,MAAM,QAAQ,GAAG,MAAA,OAAO,CAAC,KAAK,0CAAE,iBAAiB,CAAA;IACjD,IAAI,OAAO,QAAQ,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAA;IAC9C,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,QAAQ,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAA;IAC5C,OAAO,GAAG,CAAC,QAAQ,KAAK,QAAQ,CAAC,QAAQ,IAAI,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;AACxE,CAAC","sourcesContent":["import { html } from 'lit'\nimport { store, REGISTER_NAVIGATION_CALLBACK } from '@operato/shell'\nimport { appendViewpart, removeViewpart, VIEWPART_POSITION } from './layout.js'\nimport { OxEntity360Dock, type Entity360View } from '../components/ox-entity360-dock.js'\nimport { writeAddress } from '@operato/utils/address.js'\n\nconst NAME = 'entity360'\nlet current: { owner: object; dock: OxEntity360Dock; path: string; returnFocus?: () => void } | undefined\n\n// Cached pages may stay connected after navigation; their details must not leak into another task.\nstore.dispatch({ type: REGISTER_NAVIGATION_CALLBACK, callback: () => {\n if (current && current.path !== location.pathname) hideEntity360(current.owner, false)\n} })\n\n/** One dock per application, independent of product controllers and their render lifetimes. */\nexport function showEntity360(owner: object, view: Entity360View, returnFocus?: () => void): void {\n if (current?.owner !== owner) {\n if (current) hideEntity360(current.owner, false)\n const dock = document.createElement('ox-entity360-dock') as OxEntity360Dock\n current = { owner, dock, path: location.pathname, returnFocus }\n dock.view = view\n appendViewpart({ name: NAME, position: VIEWPART_POSITION.ASIDEBAR,\n viewpart: { show: true, persistent: true, template: html`${dock}` } })\n } else {\n current.dock.view = view\n if (returnFocus) current.returnFocus = returnFocus\n }\n}\n\n/** Whether the dock is open for this owner - closed by its button, by another owner or by navigation, it is not. */\nexport function isEntity360Shown(owner: object): boolean {\n return current?.owner === owner\n}\n\nexport function hideEntity360(owner: object, restoreFocus = true): void {\n if (current?.owner !== owner) return\n const previous = current\n current = undefined\n removeViewpart(NAME)\n if (restoreFocus) queueMicrotask(() => previous.returnFocus?.())\n}\n\n/**\n * Providers validate the canonical axis and read scope before calling this function. `source` is the element the\n * request comes from: a link inside a popup names that popup as its origin.\n */\nexport function navigateEntity360(address: string, source?: Element): Promise<boolean> {\n const url = new URL(address, location.href)\n if (url.origin !== location.origin) throw new Error('Entity 360 must stay within the application')\n if (url.href === location.href) return Promise.resolve(true)\n return writeAddress(url, { state: { ...history.state, entity360Previous: location.href }, source })\n}\n\nexport function canGoBackEntity360(): boolean {\n const previous = history.state?.entity360Previous\n if (typeof previous !== 'string') return false\n const url = new URL(previous, location.href)\n return url.pathname === location.pathname && url.searchParams.has('f')\n}\n"]}
1
+ {"version":3,"file":"entity360.js","sourceRoot":"","sources":["../../../src/actions/entity360.ts"],"names":[],"mappings":"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","sourcesContent":["import { html } from 'lit'\nimport { store, REGISTER_NAVIGATION_CALLBACK } from '@operato/shell'\nimport { appendViewpart, removeViewpart, VIEWPART_POSITION } from './layout.js'\nimport { OxEntity360Dock, type Entity360View } from '../components/ox-entity360-dock.js'\nimport { writeAddress } from '@operato/utils/address.js'\n\nconst NAME = 'entity360'\nlet current: { owner: object; dock: OxEntity360Dock; path: string; returnFocus?: () => void } | undefined\n\n// Cached pages may stay connected after navigation; their details must not leak into another task.\nstore.dispatch({ type: REGISTER_NAVIGATION_CALLBACK, callback: () => {\n if (current && current.path !== location.pathname) hideEntity360(current.owner, false)\n} })\n\n/** One dock per application, independent of product controllers and their render lifetimes. */\nexport function showEntity360(owner: object, view: Entity360View, returnFocus?: () => void): void {\n if (current?.owner !== owner) {\n if (current) hideEntity360(current.owner, false)\n const dock = document.createElement('ox-entity360-dock') as OxEntity360Dock\n current = { owner, dock, path: location.pathname, returnFocus }\n dock.view = view\n appendViewpart({ name: NAME, position: VIEWPART_POSITION.ASIDEBAR,\n viewpart: { show: true, persistent: true, template: html`${dock}` } })\n } else {\n current.dock.view = view\n if (returnFocus) current.returnFocus = returnFocus\n }\n}\n\n/** Whether the dock is open for this owner - closed by its button, by another owner or by navigation, it is not. */\nexport function isEntity360Shown(owner: object): boolean {\n return current?.owner === owner\n}\n\n/** Whether the owner's open 360 holds an entry no draft keeps (ADR-0067 decision 8). */\nexport function isEntity360Unkept(owner: object): boolean {\n return current?.owner === owner && current.dock.unkept\n}\n\nexport function hideEntity360(owner: object, restoreFocus = true): void {\n if (current?.owner !== owner) return\n const previous = current\n current = undefined\n removeViewpart(NAME)\n if (restoreFocus) queueMicrotask(() => previous.returnFocus?.())\n}\n\n/**\n * Providers validate the canonical axis and read scope before calling this function. `source` is the element the\n * request comes from: a link inside a popup names that popup as its origin.\n */\nexport function navigateEntity360(address: string, source?: Element): Promise<boolean> {\n const url = new URL(address, location.href)\n if (url.origin !== location.origin) throw new Error('Entity 360 must stay within the application')\n if (url.href === location.href) return Promise.resolve(true)\n return writeAddress(url, { state: { ...history.state, entity360Previous: location.href }, source })\n}\n\nexport function canGoBackEntity360(): boolean {\n const previous = history.state?.entity360Previous\n if (typeof previous !== 'string') return false\n const url = new URL(previous, location.href)\n return url.pathname === location.pathname && url.searchParams.has('f')\n}\n"]}
@@ -0,0 +1,67 @@
1
+ /**
2
+ * The ID Lens's rules - what one look-up found, and what the lens does with it (ADR-0133 2-1 · 2-2).
3
+ *
4
+ * The lens is the products': each product hands it its own doors, one per kind of identifier it keeps. This
5
+ * module knows no kind, no rule and no privilege - a door asks its product's existing exact read door, under that
6
+ * door's own privilege, and says what came back. Kept free of lit so the rules read on their own.
7
+ */
8
+ /** One door's answer - the four states every 360 section speaks (`readEntity360Door`). */
9
+ export type IdLensDoorState = 'ready' | 'none' | 'forbidden' | 'failed';
10
+ export interface IdLensHit {
11
+ /** The Entity 360 kind name, as registered with `registerEntity360Kind` - no table maps one to the other. */
12
+ kind: string;
13
+ /** The identifier the 360 opens by. */
14
+ id: string;
15
+ name: string;
16
+ /** The kind's own state, already in the reader's words. The lens does not translate a product's states. */
17
+ status?: string;
18
+ /** What tells two hits apart besides the identifier. */
19
+ context?: string;
20
+ /**
21
+ * The work screen a hit of a kind without a 360 goes to (ADR-0133 2-2: only where a screen is known). A kind
22
+ * with a 360 opens that instead.
23
+ */
24
+ open?: () => void;
25
+ }
26
+ export interface IdLensDoorAnswer {
27
+ state: IdLensDoorState;
28
+ hit?: IdLensHit;
29
+ message?: string;
30
+ }
31
+ export interface IdLensDoor {
32
+ kind: string;
33
+ /** The product's word for the kind, read when it is said - so it follows the language. */
34
+ label: () => string;
35
+ ask: (text: string) => Promise<IdLensDoorAnswer>;
36
+ }
37
+ export interface IdLensOutcome {
38
+ text: string;
39
+ hits: IdLensHit[];
40
+ /** Kinds this person may not read. Said by kind only - whether such a thing exists is never said. */
41
+ forbidden: string[];
42
+ failed: {
43
+ kind: string;
44
+ message?: string;
45
+ }[];
46
+ }
47
+ /**
48
+ * The text a look-up asks with. Only the ends are trimmed: case, prefixes and digits stay as typed until a
49
+ * kind's own rule is known (ADR-0133 2-1).
50
+ */
51
+ export declare function lookupText(raw: string | null | undefined): string;
52
+ export declare function foldIdLensAnswers(text: string, answers: (IdLensDoorAnswer & {
53
+ kind: string;
54
+ })[]): IdLensOutcome;
55
+ /**
56
+ * Open the 360 straight away only when there is nothing to choose and nothing missing: exactly one hit, its
57
+ * kind registered, and every door answered. A door that failed might have held a second hit - opening the
58
+ * one we have would hide that.
59
+ */
60
+ export declare function idLensHitToOpen(outcome: IdLensOutcome, registered: (kind: string) => boolean): IdLensHit | undefined;
61
+ /** Ask every door at once. A door that threw did not answer - that is `failed`, not `none`. */
62
+ export declare function askIdLensDoors(doors: readonly IdLensDoor[], text: string): Promise<IdLensOutcome>;
63
+ /**
64
+ * A door's answer from its GraphQL response: `pick` takes the row out of `data`, `hitOf` says it as a hit. The
65
+ * states are `readEntity360Door`'s - a refusal is `forbidden`, nothing is `none`.
66
+ */
67
+ export declare function idLensAnswerOf<T>(response: any, pick: (data: any) => T | null | undefined, hitOf: (value: T) => IdLensHit | undefined): IdLensDoorAnswer;
@@ -0,0 +1,51 @@
1
+ import { readEntity360Door } from './entity360-door.js';
2
+ /**
3
+ * The text a look-up asks with. Only the ends are trimmed: case, prefixes and digits stay as typed until a
4
+ * kind's own rule is known (ADR-0133 2-1).
5
+ */
6
+ export function lookupText(raw) {
7
+ return String(raw !== null && raw !== void 0 ? raw : '').trim();
8
+ }
9
+ export function foldIdLensAnswers(text, answers) {
10
+ return {
11
+ text,
12
+ hits: answers.filter(one => one.state === 'ready' && one.hit).map(one => one.hit),
13
+ forbidden: answers.filter(one => one.state === 'forbidden').map(one => one.kind),
14
+ failed: answers.filter(one => one.state === 'failed').map(one => ({ kind: one.kind, message: one.message }))
15
+ };
16
+ }
17
+ /**
18
+ * Open the 360 straight away only when there is nothing to choose and nothing missing: exactly one hit, its
19
+ * kind registered, and every door answered. A door that failed might have held a second hit - opening the
20
+ * one we have would hide that.
21
+ */
22
+ export function idLensHitToOpen(outcome, registered) {
23
+ if (outcome.hits.length !== 1 || outcome.failed.length > 0)
24
+ return undefined;
25
+ const [only] = outcome.hits;
26
+ return registered(only.kind) ? only : undefined;
27
+ }
28
+ /** Ask every door at once. A door that threw did not answer - that is `failed`, not `none`. */
29
+ export async function askIdLensDoors(doors, text) {
30
+ const answers = await Promise.all(doors.map(async (door) => {
31
+ try {
32
+ return { ...(await door.ask(text)), kind: door.kind };
33
+ }
34
+ catch (error) {
35
+ return { kind: door.kind, state: 'failed', message: error === null || error === void 0 ? void 0 : error.message };
36
+ }
37
+ }));
38
+ return foldIdLensAnswers(text, answers);
39
+ }
40
+ /**
41
+ * A door's answer from its GraphQL response: `pick` takes the row out of `data`, `hitOf` says it as a hit. The
42
+ * states are `readEntity360Door`'s - a refusal is `forbidden`, nothing is `none`.
43
+ */
44
+ export function idLensAnswerOf(response, pick, hitOf) {
45
+ const answer = readEntity360Door(response, pick);
46
+ if (answer.state !== 'ready' || !answer.data)
47
+ return { state: answer.state, message: answer.message };
48
+ const hit = hitOf(answer.data);
49
+ return hit ? { state: 'ready', hit } : { state: 'none' };
50
+ }
51
+ //# sourceMappingURL=id-lens.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"id-lens.js","sourceRoot":"","sources":["../../../src/actions/id-lens.ts"],"names":[],"mappings":"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","sourcesContent":["import { readEntity360Door } from './entity360-door.js'\n\n/**\n * The ID Lens's rules - what one look-up found, and what the lens does with it (ADR-0133 2-1 · 2-2).\n *\n * The lens is the products': each product hands it its own doors, one per kind of identifier it keeps. This\n * module knows no kind, no rule and no privilege - a door asks its product's existing exact read door, under that\n * door's own privilege, and says what came back. Kept free of lit so the rules read on their own.\n */\n\n/** One door's answer - the four states every 360 section speaks (`readEntity360Door`). */\nexport type IdLensDoorState = 'ready' | 'none' | 'forbidden' | 'failed'\n\nexport interface IdLensHit {\n /** The Entity 360 kind name, as registered with `registerEntity360Kind` - no table maps one to the other. */\n kind: string\n /** The identifier the 360 opens by. */\n id: string\n name: string\n /** The kind's own state, already in the reader's words. The lens does not translate a product's states. */\n status?: string\n /** What tells two hits apart besides the identifier. */\n context?: string\n /**\n * The work screen a hit of a kind without a 360 goes to (ADR-0133 2-2: only where a screen is known). A kind\n * with a 360 opens that instead.\n */\n open?: () => void\n}\n\nexport interface IdLensDoorAnswer {\n state: IdLensDoorState\n hit?: IdLensHit\n message?: string\n}\n\nexport interface IdLensDoor {\n kind: string\n /** The product's word for the kind, read when it is said - so it follows the language. */\n label: () => string\n ask: (text: string) => Promise<IdLensDoorAnswer>\n}\n\nexport interface IdLensOutcome {\n text: string\n hits: IdLensHit[]\n /** Kinds this person may not read. Said by kind only - whether such a thing exists is never said. */\n forbidden: string[]\n failed: { kind: string; message?: string }[]\n}\n\n/**\n * The text a look-up asks with. Only the ends are trimmed: case, prefixes and digits stay as typed until a\n * kind's own rule is known (ADR-0133 2-1).\n */\nexport function lookupText(raw: string | null | undefined): string {\n return String(raw ?? '').trim()\n}\n\nexport function foldIdLensAnswers(text: string, answers: (IdLensDoorAnswer & { kind: string })[]): IdLensOutcome {\n return {\n text,\n hits: answers.filter(one => one.state === 'ready' && one.hit).map(one => one.hit!),\n forbidden: answers.filter(one => one.state === 'forbidden').map(one => one.kind),\n failed: answers.filter(one => one.state === 'failed').map(one => ({ kind: one.kind, message: one.message }))\n }\n}\n\n/**\n * Open the 360 straight away only when there is nothing to choose and nothing missing: exactly one hit, its\n * kind registered, and every door answered. A door that failed might have held a second hit - opening the\n * one we have would hide that.\n */\nexport function idLensHitToOpen(outcome: IdLensOutcome, registered: (kind: string) => boolean): IdLensHit | undefined {\n if (outcome.hits.length !== 1 || outcome.failed.length > 0) return undefined\n\n const [only] = outcome.hits\n return registered(only.kind) ? only : undefined\n}\n\n/** Ask every door at once. A door that threw did not answer - that is `failed`, not `none`. */\nexport async function askIdLensDoors(doors: readonly IdLensDoor[], text: string): Promise<IdLensOutcome> {\n const answers = await Promise.all(\n doors.map(async door => {\n try {\n return { ...(await door.ask(text)), kind: door.kind }\n } catch (error: any) {\n return { kind: door.kind, state: 'failed' as const, message: error?.message }\n }\n })\n )\n\n return foldIdLensAnswers(text, answers)\n}\n\n/**\n * A door's answer from its GraphQL response: `pick` takes the row out of `data`, `hitOf` says it as a hit. The\n * states are `readEntity360Door`'s - a refusal is `forbidden`, nothing is `none`.\n */\nexport function idLensAnswerOf<T>(response: any, pick: (data: any) => T | null | undefined, hitOf: (value: T) => IdLensHit | undefined): IdLensDoorAnswer {\n const answer = readEntity360Door(response, pick)\n if (answer.state !== 'ready' || !answer.data) return { state: answer.state, message: answer.message }\n\n const hit = hitOf(answer.data)\n return hit ? { state: 'ready', hit } : { state: 'none' }\n}\n"]}
@@ -18,6 +18,11 @@ export type Viewpart = {
18
18
  };
19
19
  backdrop?: boolean;
20
20
  temporary?: boolean;
21
+ /** An already open supporting dock can share the expanded entity workspace. No product content is owned here. */
22
+ expansionCompanion?: {
23
+ labelKey: string;
24
+ defaultLabel: string;
25
+ };
21
26
  persistent?: boolean;
22
27
  resizable?: boolean;
23
28
  movable?: boolean;