@things-factory/figure-library 10.1.153 → 10.1.155

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 (60) hide show
  1. package/dist-client/pages/figure-library-page.d.ts +20 -58
  2. package/dist-client/pages/figure-library-page.js +189 -672
  3. package/dist-client/pages/figure-library-page.js.map +1 -1
  4. package/dist-client/tsconfig.tsbuildinfo +1 -1
  5. package/dist-client/viewparts/figure-card-meta.d.ts +19 -0
  6. package/dist-client/viewparts/figure-card-meta.js +122 -0
  7. package/dist-client/viewparts/figure-card-meta.js.map +1 -0
  8. package/dist-client/viewparts/figure-releases-popup.d.ts +34 -0
  9. package/dist-client/viewparts/figure-releases-popup.js +400 -0
  10. package/dist-client/viewparts/figure-releases-popup.js.map +1 -0
  11. package/dist-client/viewparts/release-pair.d.ts +9 -0
  12. package/dist-client/viewparts/release-pair.js +15 -0
  13. package/dist-client/viewparts/release-pair.js.map +1 -0
  14. package/dist-server/service/figure-library-release/figure-library-release.d.ts +5 -3
  15. package/dist-server/service/figure-library-release/figure-library-release.js +13 -3
  16. package/dist-server/service/figure-library-release/figure-library-release.js.map +1 -1
  17. package/dist-server/service/figure-library-resolver.d.ts +9 -9
  18. package/dist-server/service/figure-library-resolver.js +66 -69
  19. package/dist-server/service/figure-library-resolver.js.map +1 -1
  20. package/dist-server/service/figure-library-types.d.ts +17 -4
  21. package/dist-server/service/figure-library-types.js +63 -15
  22. package/dist-server/service/figure-library-types.js.map +1 -1
  23. package/dist-server/service/figure-type-map/figure-type-map-entry.d.ts +5 -4
  24. package/dist-server/service/figure-type-map/figure-type-map-entry.js +8 -14
  25. package/dist-server/service/figure-type-map/figure-type-map-entry.js.map +1 -1
  26. package/dist-server/service/index.d.ts +2 -3
  27. package/dist-server/service/index.js +2 -3
  28. package/dist-server/service/index.js.map +1 -1
  29. package/dist-server/service/library/blueprints.d.ts +3 -8
  30. package/dist-server/service/library/blueprints.js +5 -6
  31. package/dist-server/service/library/blueprints.js.map +1 -1
  32. package/dist-server/service/library/carriers.d.ts +8 -11
  33. package/dist-server/service/library/carriers.js +8 -14
  34. package/dist-server/service/library/carriers.js.map +1 -1
  35. package/dist-server/service/library/derived.d.ts +5 -3
  36. package/dist-server/service/library/derived.js +4 -3
  37. package/dist-server/service/library/derived.js.map +1 -1
  38. package/dist-server/service/library/figures.d.ts +46 -0
  39. package/dist-server/service/library/figures.js +91 -0
  40. package/dist-server/service/library/figures.js.map +1 -0
  41. package/dist-server/service/library/import.d.ts +1 -1
  42. package/dist-server/service/library/import.js +5 -3
  43. package/dist-server/service/library/import.js.map +1 -1
  44. package/dist-server/service/library/inspection.d.ts +1 -48
  45. package/dist-server/service/library/inspection.js +3 -80
  46. package/dist-server/service/library/inspection.js.map +1 -1
  47. package/dist-server/service/library/type-map.d.ts +10 -22
  48. package/dist-server/service/library/type-map.js +44 -108
  49. package/dist-server/service/library/type-map.js.map +1 -1
  50. package/dist-server/tsconfig.tsbuildinfo +1 -1
  51. package/package.json +11 -10
  52. package/translations/en.json +26 -47
  53. package/translations/es.json +26 -47
  54. package/translations/ja.json +26 -47
  55. package/translations/ko.json +26 -47
  56. package/translations/ms.json +26 -47
  57. package/translations/zh.json +26 -47
  58. package/dist-server/service/figure-type-map/figure-type-map.d.ts +0 -34
  59. package/dist-server/service/figure-type-map/figure-type-map.js +0 -120
  60. package/dist-server/service/figure-type-map/figure-type-map.js.map +0 -1
@@ -1,72 +1,66 @@
1
1
  import { __decorate, __metadata } from "tslib";
2
- import '@operato/layout';
3
- import '@operato/list-state/ox-page-state.js';
2
+ import '@operato/data-grist';
3
+ import '../viewparts/figure-card-meta.js';
4
+ import '@things-factory/figure-viewer/dist-client/figure-thumb-mark.js';
5
+ import '../viewparts/figure-releases-popup.js';
4
6
  import gql from 'graphql-tag';
5
7
  import { css, html, nothing } from 'lit';
6
- import { customElement, state } from 'lit/decorators.js';
8
+ import { customElement, query, state } from 'lit/decorators.js';
9
+ import { DataGrist } from '@operato/data-grist';
7
10
  import { client } from '@operato/graphql';
8
11
  import { i18next, localize } from '@operato/i18n';
12
+ import { openPopup } from '@operato/popup';
9
13
  import { PageView } from '@operato/shell';
10
- import { actionHeight, DocumentPageStyles, FormFieldStyles, ScrollbarStyles } from '@operato/styles';
14
+ import { CommonGristStyles, CommonHeaderStyles, ScrollbarStyles, WorkSurfacePageStyles } from '@operato/styles';
11
15
  import { getPathInfo } from '@operato/utils';
12
16
  /*
13
- * Screen layout (operato-application/design/ui/page-standard.md): document frame, the tab in the address
14
- * (ox-url-tabs), one section per thing the operator looks at, and the decisions of each tab in the shell's
15
- * action bar - upload on the library tab; save, check, preview and activate on the type map tab. Activate
16
- * stays off until the candidate has been checked, and the button says so.
17
+ * The board server's figures (ADR-0114, amended 2026-10-12), as a card catalogue: one card per imported figure, its
18
+ * thumbnail first, the release it draws with and the latest imported. A card whose figure does not draw with its latest
19
+ * says so. Clicking a card opens its releases side by side to compare them and choose the one it draws with.
17
20
  *
18
- * The board server's figures (ADR-0114): the library of imported releases, and the domain type map that picks the
19
- * release each figure draws with. Importing puts a release into the candidate; the candidate is checked against the
20
- * domain's boards and activated; an archived map is activated again to roll back.
21
+ * The library is expected to grow to many figures, so the list pages from the server and the filters (category,
22
+ * capability) are drop-down lists in the filter bar beside the name search. Upload, the decision of the screen, sits in
23
+ * the shell's action bar; what it did opens in a popup.
21
24
  */
22
- const RELEASES = gql `
23
- query {
24
- figureLibraryReleases(pagination: { page: 1, limit: 1000 }, sortings: [{ name: "ref" }]) {
25
+ const FIGURES = gql `
26
+ query ($filters: [Filter!], $pagination: Pagination, $sortings: [Sorting!]) {
27
+ responses: figureLibraryFigures(filters: $filters, pagination: $pagination, sortings: $sortings) {
25
28
  items {
26
- id
27
29
  name
28
- ref
29
- namespace
30
- type
31
- version
32
- formatSupported
33
- deprecated
34
- supersededBy
35
- dependencies
36
- sourceUrl
37
- createdAt
30
+ latestVersion
31
+ releaseCount
32
+ release {
33
+ id
34
+ name
35
+ version
36
+ category
37
+ capabilities
38
+ formatSupported
39
+ deprecated
40
+ dependencies
41
+ }
42
+ latest {
43
+ id
44
+ name
45
+ version
46
+ formatSupported
47
+ dependencies
48
+ }
38
49
  }
39
50
  total
40
51
  }
41
52
  }
42
53
  `;
43
- const MAPS = gql `
54
+ const FACETS = gql `
44
55
  query {
45
- figureTypeMaps {
46
- version
47
- name
48
- state
49
- revision
50
- activatedAt
56
+ figureLibraryFacets {
57
+ categories
58
+ uncategorized
59
+ capabilities
60
+ incapable
51
61
  }
52
62
  }
53
63
  `;
54
- const LINES = gql `
55
- query ($version: Int!) {
56
- figureTypeMapEntries(version: $version) {
57
- name
58
- release {
59
- id
60
- ref
61
- }
62
- }
63
- }
64
- `;
65
- const CHECK = gql `
66
- query {
67
- figureTypeMapCheck
68
- }
69
- `;
70
64
  const IMPORT = gql `
71
65
  mutation ($file: Upload!) {
72
66
  importFigurePackage(file: $file) {
@@ -83,698 +77,221 @@ const IMPORT = gql `
83
77
  }
84
78
  }
85
79
  `;
86
- const SAVE = gql `
87
- mutation ($lines: [FigureTypeMapLineInput!]!) {
88
- saveFigureTypeMapCandidate(lines: $lines) {
89
- version
90
- revision
91
- }
92
- }
93
- `;
94
- const ACTIVATE = gql `
95
- mutation ($version: Int!) {
96
- activateFigureTypeMap(version: $version) {
97
- version
98
- state
99
- }
100
- }
101
- `;
102
- const fullName = (release) => `${release.namespace}/${release.type}`;
103
- /** A time in the reader's language (a bare toLocaleString follows the browser, not the app). */
104
- const when = (value) => (value ? new Date(value).toLocaleString(i18next.language) : '');
105
- /**
106
- * A contract change in words. figure-model gives a code and values; `detail` is English for logs and is
107
- * the sentence only when this module has no words for a new code (open question P8 in figure-library.md).
108
- */
109
- const changeText = (change) => change.code
110
- ? i18next.t(`figure-library.contract.${change.code}`, { ...(change.values ?? {}), defaultValue: change.detail ?? change.code })
111
- : change.detail ?? '';
80
+ /** The filter value for a figure with no category, or one that declares no capability. */
81
+ const NONE = '-';
112
82
  let FigureLibraryPage = class FigureLibraryPage extends localize(i18next)(PageView) {
113
83
  constructor() {
114
84
  super(...arguments);
115
- this.tab = 'library';
116
- this.releases = [];
117
- this.maps = [];
118
- this.candidate = new Map();
119
- /** The candidate's lines as last saved — to tell an unsaved choice from a saved one. */
120
- this.saved = new Map();
121
- this.busy = false;
122
- this.loading = true;
123
- /** The first read failed; nothing is drawn yet, so the whole screen says it could not read. */
124
- this.loadError = null;
125
- /** A later step failed (save, activate, a re-read); what is drawn stays, this line says why. */
126
- this.failure = '';
85
+ this.fetchHandler = async ({ page = 1, limit = 24, filters = [] }) => {
86
+ const response = await client.query({
87
+ query: FIGURES,
88
+ variables: {
89
+ /* The name search arrives as %text%; the server matches parts itself. */
90
+ filters: filters
91
+ .filter(filter => filter.value !== undefined && filter.value !== '' && !(Array.isArray(filter.value) && !filter.value.length))
92
+ .map(filter => ({ ...filter, value: typeof filter.value === 'string' ? filter.value.replace(/^%|%$/g, '') : filter.value })),
93
+ pagination: { page, limit }
94
+ },
95
+ fetchPolicy: 'network-only'
96
+ });
97
+ return { total: response.data.responses.total || 0, records: response.data.responses.items || [] };
98
+ };
127
99
  this.importFile = async (event) => {
128
100
  const input = event.target;
129
101
  const file = input.files?.[0];
130
102
  if (!file)
131
103
  return;
132
- this.busy = true;
104
+ let result;
133
105
  try {
134
106
  const response = await client.mutate({ mutation: IMPORT, variables: { file }, context: { hasUpload: true } });
135
- this.imported = response.data.importFigurePackage;
136
- await this.refresh();
137
- }
138
- catch (e) {
139
- this.imported = { problems: [{ code: e?.message ?? 'error' }], outcomes: [] };
140
- }
141
- finally {
142
- this.busy = false;
143
- input.value = '';
144
- }
145
- };
146
- /* A window of its own draws the candidate: its figures never mix with the active map of this one (ADR-0114 결정 12). */
147
- this.preview = () => {
148
- const { contextPath } = getPathInfo(location.pathname);
149
- window.open(`${contextPath}/board-list?figure-map=candidate`, '_blank', 'noopener');
150
- };
151
- this.saveCandidate = async () => {
152
- this.busy = true;
153
- try {
154
- const lines = [...this.candidate].map(([name, releaseId]) => ({ name, releaseId }));
155
- await client.mutate({ mutation: SAVE, variables: { lines } });
156
- this.report = undefined;
157
- await this.refresh();
158
- }
159
- catch (e) {
160
- this.failure = e?.message ?? '';
161
- }
162
- finally {
163
- this.busy = false;
164
- }
165
- };
166
- this.check = async () => {
167
- this.busy = true;
168
- try {
169
- const response = await client.query({ query: CHECK, fetchPolicy: 'network-only' });
170
107
  if (response.errors?.length)
171
108
  throw new Error(response.errors[0].message);
172
- this.report = response.data.figureTypeMapCheck;
173
- this.failure = '';
109
+ result = response.data.importFigurePackage;
174
110
  }
175
111
  catch (e) {
176
- this.failure = e?.message ?? '';
112
+ result = { problems: [{ code: e?.message ?? 'error' }], outcomes: [] };
177
113
  }
178
114
  finally {
179
- this.busy = false;
115
+ input.value = '';
180
116
  }
117
+ await this.configure();
118
+ this.grist.fetch();
119
+ openPopup(this.renderImported(result), { backdrop: true, size: 'small', closable: true, title: i18next.t('figure-library.title.import-result') });
181
120
  };
182
121
  }
183
122
  static { this.styles = [
123
+ WorkSurfacePageStyles,
184
124
  ScrollbarStyles,
185
- DocumentPageStyles,
186
- FormFieldStyles,
125
+ CommonGristStyles,
126
+ CommonHeaderStyles,
187
127
  css `
188
- :host {
189
- color: var(--ui-ink, var(--md-sys-color-on-surface));
190
- font-size: var(--ui-t-body, 13px);
191
- line-height: 1.5;
192
- }
193
-
194
- /* 표 — 격자와 같은 모양(머리글 · 줄 여백 · 경계선이 --grid-* 토큰). */
195
- table {
196
- width: 100%;
197
- border-collapse: collapse;
198
- font-size: var(--grid-record-fontsize, 14px);
199
- }
200
-
201
- th {
202
- padding: var(--spacing-small, 4px) var(--spacing-medium, 8px);
203
- border-top: var(--grid-header-top-border, 2px solid var(--md-sys-color-primary));
204
- border-bottom: var(--grid-header-bottom-border, 1px solid var(--md-sys-color-outline-variant));
205
- background: var(--grid-header-background-color, var(--md-sys-color-surface-container-low));
206
- font: var(--grid-header-font, 500 14px inherit);
207
- color: var(--grid-header-color, var(--md-sys-color-primary));
208
- text-align: left;
209
- white-space: nowrap;
210
- }
211
-
212
- td {
213
- padding: var(--grid-record-padding, 4px 8px);
214
- border-bottom: var(--grid-record-border-bottom, 1px solid var(--md-sys-color-outline-variant));
215
- vertical-align: middle;
216
- word-break: keep-all;
217
- overflow-wrap: anywhere;
218
- }
219
-
220
- td.thumb {
221
- width: 64px;
222
- }
223
-
224
- img[thumb] {
225
- display: block;
226
- width: 48px;
227
- height: 36px;
228
- object-fit: contain;
229
- }
230
-
231
- /* 식별자(정식 이름 · 발행본 참조)는 고정폭 숫자, 줄을 바꾸지 않는다. */
232
- .ref {
233
- font-family: var(--ui-mono, ui-monospace, Menlo, monospace);
234
- font-size: var(--ui-t-small, 11.5px);
235
- white-space: nowrap;
236
- }
237
-
238
- .sub {
239
- display: block;
240
- color: var(--ui-muted, var(--md-sys-color-on-surface-variant));
241
- font-size: var(--ui-t-small, 11.5px);
242
- }
243
-
244
- .when {
245
- font-variant-numeric: tabular-nums;
246
- white-space: nowrap;
247
- }
248
-
249
- /* 상태 표 — 같은 줄에 둘 이상 붙을 수 있다(형식 미지원 · 퇴역). */
250
- .marks {
251
- display: flex;
252
- flex-wrap: wrap;
253
- gap: var(--spacing-small, 4px);
254
- }
255
-
256
- .mark {
257
- padding: 1px var(--spacing-medium, 8px);
258
- border: 1px solid var(--ui-line, var(--md-sys-color-outline-variant));
259
- border-radius: 999px;
260
- color: var(--ui-muted, var(--md-sys-color-on-surface-variant));
261
- font-size: var(--ui-t-small, 11.5px);
262
- white-space: nowrap;
263
- }
264
-
265
- .mark.crit {
266
- border-color: color-mix(in srgb, var(--ui-crit, var(--md-sys-color-error)) 40%, transparent);
267
- color: var(--ui-crit, var(--md-sys-color-error));
268
- }
269
-
270
- .mark.on {
271
- border-color: var(--ui-accent, var(--md-sys-color-primary));
272
- color: var(--ui-accent, var(--md-sys-color-primary));
273
- font-weight: 600;
274
- }
275
-
276
- /* 찾아낸 것 · 거부 — 왼쪽 선 하나. */
277
- .finding {
278
- margin: 0;
279
- padding: var(--spacing-medium, 8px) 0 var(--spacing-medium, 8px) var(--spacing-large, 12px);
280
- border-left: 2px solid var(--ui-crit, var(--md-sys-color-error));
281
- }
282
-
283
- .outcomes,
284
- .report-list {
285
- margin: 0;
286
- padding: 0;
287
- list-style: none;
288
- }
289
-
290
- .outcomes > li,
291
- .report-list > li {
292
- display: flex;
293
- flex-wrap: wrap;
294
- align-items: baseline;
295
- gap: var(--spacing-small, 4px) var(--spacing-large, 12px);
296
- padding: var(--spacing-medium, 8px) 0;
297
- border-top: 1px solid var(--ui-line-soft, var(--md-sys-color-surface-container-high));
298
- }
299
-
300
- .outcomes > li:first-child,
301
- .report-list > li:first-child {
302
- padding-top: 0;
303
- border-top: none;
304
- }
305
-
306
- .outcome-refused {
307
- color: var(--ui-crit, var(--md-sys-color-error));
308
- font-weight: 600;
309
- }
310
-
311
- .changes {
312
- flex-basis: 100%;
313
- margin: 0;
314
- padding-left: 1.2em;
315
- color: var(--ui-muted, var(--md-sys-color-on-surface-variant));
316
- }
317
-
318
- .changes .breaking {
319
- color: var(--ui-crit, var(--md-sys-color-error));
320
- }
321
-
322
- .report-group + .report-group {
323
- margin-top: var(--spacing-huge, 24px);
324
- }
325
-
326
- .report-group h4 {
327
- margin: 0 0 var(--spacing-medium, 8px);
328
- font-size: var(--ui-t-body, 13px);
329
- font-weight: 600;
330
- }
331
-
332
- select {
333
- height: var(--form-element-height-medium, 30px);
334
- min-width: 240px;
335
- font-size: var(--ui-t-body, 13px);
336
- }
337
-
338
- .empty {
339
- margin: 0;
340
- padding: var(--spacing-huge, 24px);
341
- border: 1px dashed var(--ui-line, var(--md-sys-color-outline-variant));
342
- border-radius: var(--ui-r, 10px);
343
- color: var(--ui-muted, var(--md-sys-color-on-surface-variant));
344
- text-align: center;
345
- }
346
-
347
- /* Row buttons take the action-bar look until @operato/styles ButtonStyles is installed (ADR-0117). */
348
- td button {
349
- box-sizing: border-box;
350
- height: ${actionHeight('--cell-action-height')};
351
- padding: 0 var(--spacing-medium, 8px);
352
- border: 1px solid var(--md-sys-color-outline-variant);
353
- border-radius: var(--md-sys-shape-corner-small);
354
- background: var(--md-sys-color-surface-container-lowest);
355
- color: var(--md-sys-color-on-surface);
356
- font: inherit;
357
- font-size: var(--ui-t-body, 13px);
358
- font-weight: 600;
359
- white-space: nowrap;
360
- cursor: pointer;
128
+ ox-filters-form {
129
+ flex: 1;
361
130
  }
362
131
 
363
- td button:hover:not(:disabled) {
364
- background: var(--md-sys-color-secondary-container);
365
- border-color: var(--md-sys-color-secondary);
132
+ ox-grist {
133
+ /* Cards of a row stand at one height; a card with an extra fact row does not stick out below the others. */
134
+ --data-card-record-card-height: auto;
135
+ --data-card-thumbnail-aspect: 4 / 3;
136
+ --data-card-thumbnail-background: var(--md-sys-color-surface-container-lowest);
366
137
  }
367
138
 
368
- td button:disabled {
369
- opacity: 0.38;
370
- cursor: default;
371
- }
372
-
373
- td.end {
374
- text-align: right;
375
- }
376
139
  `
377
140
  ]; }
378
141
  get context() {
379
- const candidate = this.maps.find(map => map.state === 'CANDIDATE');
380
- const dirty = this.isDirty();
381
- const actions = this.tab === 'library'
382
- ? [
142
+ return {
143
+ title: i18next.t('figure-library.title'),
144
+ search: {
145
+ placeholder: i18next.t('figure-library.text.search'),
146
+ handler: (search) => {
147
+ this.grist.searchText = search;
148
+ },
149
+ value: this.grist?.searchText || ''
150
+ },
151
+ actions: [
383
152
  {
384
153
  title: i18next.t('figure-library.button.upload'),
385
154
  icon: 'upload_file',
386
155
  action: () => this.chooseFile(),
387
- disabled: this.busy,
388
156
  emphasis: { raised: true }
389
157
  }
390
158
  ]
391
- : [
159
+ };
160
+ }
161
+ render() {
162
+ return html `
163
+ <input id="file" type="file" accept=".figpkg,.zip" hidden @change=${this.importFile} />
164
+ <ox-grist class="page-fill" mode="CARD" .config=${this.gristConfig} .fetchHandler=${this.fetchHandler}>
165
+ <div slot="headroom">
166
+ <ox-filters-form autofocus without-search></ox-filters-form>
167
+ </div>
168
+ </ox-grist>
169
+ `;
170
+ }
171
+ async pageInitialized() {
172
+ await this.configure();
173
+ }
174
+ /*
175
+ * The filter options are read once here (and again after an import), never per render: an async options function
176
+ * runs on every render of the filter bar and replaces the open list's options under the pointer.
177
+ */
178
+ async configure() {
179
+ const { categories, capabilities } = await this.facets();
180
+ this.gristConfig = {
181
+ list: {
182
+ thumbnail: 'thumbnail',
183
+ fields: ['title', 'meta'],
184
+ details: []
185
+ },
186
+ columns: [
392
187
  {
393
- title: i18next.t('figure-library.button.save-candidate'),
394
- icon: 'save',
395
- action: () => this.saveCandidate(),
396
- /* With no candidate yet, saving is how one is made from the active map — so it stays on. */
397
- disabled: this.busy || (!dirty && !!candidate)
188
+ type: 'image',
189
+ name: 'thumbnail',
190
+ record: { editable: false, renderEmpty: true, renderer: (_value, _column, record) => this.thumbnailOf(record) }
398
191
  },
399
192
  {
400
- title: i18next.t('figure-library.button.check'),
401
- icon: 'fact_check',
402
- action: () => this.check(),
403
- disabled: this.busy || !candidate || dirty,
404
- disabledTitle: dirty ? i18next.t('figure-library.text.save-before-check') : ''
193
+ type: 'string',
194
+ name: 'title',
195
+ record: {
196
+ editable: false,
197
+ renderer: (_value, _column, record) => (record.release ?? record.latest).name
198
+ }
405
199
  },
406
200
  {
407
- title: i18next.t('figure-library.button.preview'),
408
- icon: 'open_in_new',
409
- action: () => this.preview(),
410
- disabled: !candidate || dirty,
411
- disabledTitle: dirty ? i18next.t('figure-library.text.save-before-check') : ''
201
+ type: 'string',
202
+ name: 'meta',
203
+ record: { editable: false, renderer: (_value, _column, record) => html `<figure-card-meta .figure=${record}></figure-card-meta>` }
412
204
  },
205
+ { type: 'string', name: 'name', hidden: true, record: { editable: false }, filter: 'search' },
413
206
  {
414
- title: i18next.t('figure-library.button.activate'),
415
- icon: 'published_with_changes',
416
- action: () => candidate && this.activate(candidate.version),
417
- disabled: this.busy || !candidate || dirty || !this.report,
418
- disabledTitle: i18next.t('figure-library.text.check-before-activate'),
419
- emphasis: { raised: true }
207
+ type: 'select',
208
+ name: 'category',
209
+ header: i18next.t('figure-library.label.category'),
210
+ hidden: true,
211
+ record: { editable: false },
212
+ filter: { operator: 'eq', options: categories }
213
+ },
214
+ {
215
+ type: 'select',
216
+ name: 'capability',
217
+ header: i18next.t('figure-library.label.capability'),
218
+ hidden: true,
219
+ record: { editable: false },
220
+ filter: { operator: 'eq', options: capabilities }
420
221
  }
421
- ];
422
- return {
423
- title: i18next.t('figure-library.title'),
424
- description: i18next.t('figure-library.description'),
425
- frame: 'document',
426
- actions
222
+ ],
223
+ rows: {
224
+ selectable: false,
225
+ appendable: false,
226
+ handlers: {
227
+ click: (_columns, _data, _column, record) => record && this.openReleases(record)
228
+ }
229
+ },
230
+ pagination: { pages: [24, 48, 96] }
427
231
  };
428
232
  }
429
- /*
430
- * Opened straight from the address, pageUpdated runs before the page stands and the first read never
431
- * starts - the screen sat on "loading" (before ox-page-state, on empty tables). pageInitialized reads too.
432
- */
433
- async pageInitialized() {
434
- await this.enter();
435
- }
436
- async pageUpdated(changes, lifecycle) {
233
+ async pageUpdated() {
437
234
  if (this.active)
438
- await this.enter();
439
- }
440
- async enter() {
441
- /* ox-url-tabs reads the address on connect but announces only later changes; the body follows the same address. */
442
- this.tab = new URLSearchParams(location.search).get('tab') === 'type-map' ? 'type-map' : 'library';
443
- await this.refresh();
235
+ this.grist?.fetch();
444
236
  }
445
- updated(changes) {
446
- /* The action bar follows the tab and what can be done now (dirty · checked · busy). */
447
- if (['tab', 'busy', 'report', 'maps', 'candidate', 'saved'].some(key => changes.has(key)))
448
- this.updateContext();
449
- }
450
- isDirty() {
451
- if (this.candidate.size !== this.saved.size)
452
- return true;
453
- for (const [name, id] of this.candidate)
454
- if (this.saved.get(name) !== id)
455
- return true;
456
- return false;
457
- }
458
- async refresh() {
459
- try {
460
- const [releases, maps] = await Promise.all([
461
- client.query({ query: RELEASES, fetchPolicy: 'network-only' }),
462
- client.query({ query: MAPS, fetchPolicy: 'network-only' })
463
- ]);
464
- if (releases.errors?.length || maps.errors?.length)
465
- throw releases.errors?.length ? releases.errors : maps.errors;
466
- this.releases = releases.data.figureLibraryReleases.items;
467
- this.maps = maps.data.figureTypeMaps;
468
- const candidate = this.maps.find(map => map.state === 'CANDIDATE') ?? this.maps.find(map => map.state === 'ACTIVE');
469
- let lines = new Map();
470
- if (candidate) {
471
- const entries = await client.query({ query: LINES, variables: { version: candidate.version }, fetchPolicy: 'network-only' });
472
- lines = new Map(entries.data.figureTypeMapEntries.map((line) => [line.name, line.release?.id]));
473
- }
474
- this.candidate = lines;
475
- this.saved = new Map(lines);
476
- this.failure = '';
477
- this.loadError = null;
478
- }
479
- catch (e) {
480
- if (this.loading)
481
- this.loadError = e;
482
- else
483
- this.failure = i18next.t('figure-library.text.load-failed');
484
- }
485
- finally {
486
- this.loading = false;
487
- }
488
- }
489
- render() {
490
- return html `
491
- <ox-page-state .loading=${this.loading} .error=${this.loadError} @retry=${() => this.refresh()}>
492
- <div class="page">
493
- <ox-url-tabs
494
- .tabs=${[
495
- { key: 'library', label: i18next.t('figure-library.tab.library') },
496
- { key: 'type-map', label: i18next.t('figure-library.tab.type-map') }
497
- ]}
498
- @tab-change=${(e) => (this.tab = e.detail.key)}
499
- ></ox-url-tabs>
500
- <input id="file" type="file" accept=".figpkg,.zip" hidden @change=${this.importFile} />
501
- ${this.failure ? html `<p class="finding" role="alert">${this.failure}</p>` : nothing}
502
- ${this.tab === 'library' ? this.renderLibrary() : this.renderTypeMap()}
503
- </div>
504
- </ox-page-state>
505
- `;
237
+ /** The filter options, from what the figures that draw carry. */
238
+ async facets() {
239
+ const response = await client.query({ query: FACETS, fetchPolicy: 'network-only' });
240
+ const facets = response.data?.figureLibraryFacets ?? { categories: [], capabilities: [] };
241
+ return {
242
+ categories: [
243
+ ...facets.categories.map((value) => ({ display: value, value })),
244
+ ...(facets.uncategorized ? [{ display: i18next.t('figure-library.text.no-category'), value: NONE }] : [])
245
+ ],
246
+ capabilities: [
247
+ ...facets.capabilities.map((value) => ({ display: i18next.t(`figure.label.role-${value}`, { defaultValue: value }), value })),
248
+ ...(facets.incapable ? [{ display: i18next.t('figure-library.text.no-capability'), value: NONE }] : [])
249
+ ]
250
+ };
506
251
  }
507
- thumbnailOf(release) {
508
- const hash = release.dependencies?.find(d => d.role === 'thumbnail')?.hash;
252
+ thumbnailOf(record) {
253
+ const hash = (record?.release ?? record?.latest)?.dependencies?.find((d) => d.role === 'thumbnail')?.hash;
509
254
  if (!hash)
510
- return nothing;
255
+ return html `<figure-thumb-mark .label=${i18next.t('figure-library.text.no-thumbnail')}></figure-thumb-mark>`;
511
256
  const { contextPath } = getPathInfo(location.pathname);
512
- return html `<img thumb alt="" src=${`${contextPath}/figure-library-file/${hash}`} />`;
257
+ return html `<img alt="" src=${`${contextPath}/figure-library-file/${hash}`} />`;
258
+ }
259
+ openReleases(record) {
260
+ openPopup(html `<figure-releases-popup .name=${record.name} .drawing=${record.release?.id ?? ''} @saved=${() => this.grist.fetch()}></figure-releases-popup>`, {
261
+ backdrop: true,
262
+ size: 'large',
263
+ title: i18next.t('figure-library.title.releases-of', { name: (record.release ?? record.latest).name, count: record.releaseCount })
264
+ });
513
265
  }
514
266
  chooseFile() {
515
267
  ;
516
268
  this.renderRoot.querySelector('#file')?.click();
517
269
  }
518
- renderLibrary() {
519
- return html `
520
- ${this.imported ? this.renderImported(this.imported) : nothing}
521
-
522
- <section class="page-section">
523
- <header class="page-section-header">
524
- <h3>${i18next.t('figure-library.title.releases')}</h3>
525
- </header>
526
- ${this.releases.length
527
- ? html `<p class="page-section-note">${i18next.t('figure-library.text.release-count', { count: this.releases.length })}</p>
528
- <table>
529
- <thead>
530
- <tr>
531
- <th></th>
532
- <th>${i18next.t('figure-library.label.name')}</th>
533
- <th>${i18next.t('figure-library.label.release')}</th>
534
- <th>${i18next.t('figure-library.label.source')}</th>
535
- <th>${i18next.t('figure-library.label.imported-at')}</th>
536
- <th>${i18next.t('figure-library.label.state')}</th>
537
- </tr>
538
- </thead>
539
- <tbody>
540
- ${this.releases.map(release => html `
541
- <tr>
542
- <td class="thumb">${this.thumbnailOf(release)}</td>
543
- <td>${release.name}<span class="sub ref">${fullName(release)}</span></td>
544
- <td><span class="ref">${release.ref}</span></td>
545
- <td>${release.sourceUrl ?? ''}</td>
546
- <td class="when">${when(release.createdAt)}</td>
547
- <td>
548
- <span class="marks">
549
- ${release.formatSupported
550
- ? nothing
551
- : html `<span class="mark crit">${i18next.t('figure-library.text.format-unsupported')}</span>`}
552
- ${release.deprecated ? html `<span class="mark">${i18next.t('figure-library.text.deprecated')}</span>` : nothing}
553
- </span>
554
- </td>
555
- </tr>
556
- `)}
557
- </tbody>
558
- </table>`
559
- : html `<p class="empty">${i18next.t('figure-library.text.no-releases')}</p>`}
560
- </section>
561
- `;
562
- }
563
270
  renderImported(result) {
564
271
  return html `
565
- <section class="page-section">
566
- <header class="page-section-header">
567
- <h3>${i18next.t('figure-library.title.import-result')}</h3>
568
- </header>
272
+ <div style="padding: 16px; display: flex; flex-direction: column; gap: 8px; font-size: var(--ui-t-body, 13px);">
569
273
  ${result.problems.length
570
- ? html `<p class="finding" role="alert">
274
+ ? html `<p style="margin: 0; color: var(--ui-crit, var(--md-sys-color-error));" role="alert">
571
275
  ${i18next.t('figure-library.text.package-refused', { code: result.problems.map(p => p.code).join(', ') })}
572
276
  </p>`
573
- : html `<div class="page-card">
574
- <ul class="outcomes">
575
- ${result.outcomes.map(o => html `<li>
576
- <span class="ref">${o.ref}</span>
577
- <span class=${o.outcome === 'refused' ? 'outcome-refused' : ''}>${i18next.t(`figure-library.outcome.${o.outcome}`)}</span>
578
- ${o.differs?.length
579
- ? html `<span class="sub">${i18next.t('figure-library.text.differs', { fields: o.differs.join(', ') })}</span>`
580
- : nothing}
581
- ${o.outcome === 'refused' && o.code ? html `<span class="sub">${o.code}</span>` : nothing}
582
- </li>`)}
583
- </ul>
584
- </div>`}
585
- </section>
586
- `;
587
- }
588
- renderTypeMap() {
589
- const candidate = this.maps.find(map => map.state === 'CANDIDATE');
590
- const names = [...new Set(this.releases.map(fullName))].sort();
591
- const dirty = this.isDirty();
592
- return html `
593
- <section class="page-section">
594
- <header class="page-section-header">
595
- <h3>${candidate ? i18next.t('figure-library.text.candidate', { name: candidate.name }) : i18next.t('figure-library.text.new-candidate')}</h3>
596
- </header>
597
- <p class="page-section-note">
598
- ${dirty
599
- ? i18next.t('figure-library.text.candidate-unsaved')
600
- : this.report
601
- ? i18next.t('figure-library.text.candidate-checked')
602
- : i18next.t('figure-library.text.check-before-activate')}
603
- </p>
604
- ${names.length
605
- ? html `<table>
606
- <thead>
607
- <tr>
608
- <th>${i18next.t('figure-library.label.figure')}</th>
609
- <th>${i18next.t('figure-library.label.release')}</th>
610
- </tr>
611
- </thead>
612
- <tbody>
613
- ${names.map(name => {
614
- const versions = this.releases.filter(release => fullName(release) === name).sort((a, b) => b.version - a.version);
615
- const chosen = this.candidate.get(name) ?? '';
616
- return html `
617
- <tr>
618
- <td><span class="ref">${name}</span></td>
619
- <td>
620
- <select @change=${(e) => this.choose(name, e.target.value)}>
621
- <option value="" ?selected=${!chosen}>${i18next.t('figure-library.text.not-in-map')}</option>
622
- ${versions.map(release => html `<option value=${release.id} ?selected=${chosen === release.id}>${release.ref}</option>`)}
623
- </select>
624
- </td>
625
- </tr>
626
- `;
627
- })}
628
- </tbody>
629
- </table>`
630
- : html `<p class="empty">${i18next.t('figure-library.text.no-releases')}</p>`}
631
- </section>
632
-
633
- ${this.report ? this.renderReport(this.report) : nothing}
634
-
635
- <section class="page-section">
636
- <header class="page-section-header">
637
- <h3>${i18next.t('figure-library.title.map-releases')}</h3>
638
- </header>
639
- ${this.maps.length
640
- ? html `<table>
641
- <thead>
642
- <tr>
643
- <th>${i18next.t('figure-library.label.type-map')}</th>
644
- <th>${i18next.t('figure-library.label.state')}</th>
645
- <th>${i18next.t('figure-library.label.activated-at')}</th>
646
- <th></th>
647
- </tr>
648
- </thead>
649
- <tbody>
650
- ${this.maps.map(map => html `
651
- <tr>
652
- <td>${map.name}</td>
653
- <td>
654
- <span class="mark ${map.state === 'ACTIVE' ? 'on' : ''}">${i18next.t(`figure-library.state.${map.state.toLowerCase()}`)}</span>
655
- </td>
656
- <td class="when">${when(map.activatedAt)}</td>
657
- <td class="end">
658
- ${map.state === 'ARCHIVED'
659
- ? html `<button ?disabled=${this.busy} @click=${() => this.activate(map.version)}>
660
- ${i18next.t('figure-library.button.activate-again')}
661
- </button>`
662
- : nothing}
663
- </td>
664
- </tr>
665
- `)}
666
- </tbody>
667
- </table>`
668
- : html `<p class="empty">${i18next.t('figure-library.text.no-maps')}</p>`}
669
- </section>
670
- `;
671
- }
672
- choose(name, releaseId) {
673
- const next = new Map(this.candidate);
674
- if (releaseId)
675
- next.set(name, releaseId);
676
- else
677
- next.delete(name);
678
- this.candidate = next;
679
- this.report = undefined;
680
- }
681
- async activate(version) {
682
- this.busy = true;
683
- try {
684
- await client.mutate({ mutation: ACTIVATE, variables: { version } });
685
- this.report = undefined;
686
- await this.refresh();
687
- }
688
- catch (e) {
689
- this.failure = e?.message ?? '';
690
- }
691
- finally {
692
- this.busy = false;
693
- }
694
- }
695
- renderReport(report) {
696
- const reach = (item) => html `<span class="sub">${i18next.t('figure-library.text.reach', { boards: item.boards, nodes: item.nodes })}</span>`;
697
- const group = (key, items, line) => items?.length
698
- ? html `<div class="report-group">
699
- <h4>${i18next.t(`figure-library.check.${key}`)}</h4>
700
- <ul class="report-list">
701
- ${items.map(item => html `<li>${line(item)}</li>`)}
702
- </ul>
703
- </div>`
704
- : nothing;
705
- const nothingToReport = ['missing', 'changed', 'added', 'deprecated', 'unsupported', 'dependencies', 'carrierConflicts'].every(key => !report[key]?.length);
706
- return html `
707
- <section class="page-section">
708
- <header class="page-section-header">
709
- <h3>${i18next.t('figure-library.title.check-report')}</h3>
710
- </header>
711
- <div class="page-card">
712
- ${nothingToReport ? html `<p class="sub">${i18next.t('figure-library.check.nothing')}</p>` : nothing}
713
- ${group('missing', report.missing, item => html `<span class="ref">${item.name}</span> ${reach(item)}`)}
714
- ${group('changed', report.changed, item => html `<span class="ref">${item.from}</span> → <span class="ref">${item.to}</span> ${reach(item)}
715
- ${item.contract.checked
716
- ? item.contract.changes.length
717
- ? html `<ul class="changes">
718
- ${item.contract.changes.map((change) => html `<li class=${change.blocking ? 'breaking' : ''}>
719
- ${changeText(change)}${change.blocking ? html ` · ${i18next.t('figure-library.text.breaking')}` : nothing}
720
- </li>`)}
721
- </ul>`
722
- : nothing
723
- : html `<span class="sub">${i18next.t('figure-library.check.uncheckable')}</span>`}`)}
724
- ${group('added', report.added, item => html `<span class="ref">${item.ref}</span>`)}
725
- ${group('deprecated', report.deprecated, item => html `<span class="ref">${item.ref}</span> ${reach(item)}`)}
726
- ${group('unsupported', report.unsupported, item => html `<span class="ref">${item.ref}</span> ${reach(item)}`)}
727
- ${group('dependencies', report.dependencies, item => html `<span class="ref">${item.ref}</span> ${reach(item)}`)}
728
- ${group('carrierConflicts', report.carrierConflicts, item => html `${item.carrierType} ${item.lines.map((line) => html `<span class="ref">${line.ref}</span> `)}`)}
729
- </div>
730
- </section>
277
+ : result.outcomes.map(o => html `<div>
278
+ <span style="font-family: var(--ui-mono, monospace);">${o.ref}</span> ·
279
+ <strong style=${o.outcome === 'refused' ? 'color: var(--ui-crit, var(--md-sys-color-error))' : ''}>${i18next.t(`figure-library.outcome.${o.outcome}`)}</strong>
280
+ ${o.differs?.length ? html `<div>${i18next.t('figure-library.text.differs', { fields: o.differs.join(', ') })}</div>` : nothing}
281
+ ${o.outcome === 'refused' && o.code ? html `<div>${o.code}</div>` : nothing}
282
+ </div>`)}
283
+ </div>
731
284
  `;
732
285
  }
733
286
  };
734
- __decorate([
735
- state(),
736
- __metadata("design:type", String)
737
- ], FigureLibraryPage.prototype, "tab", void 0);
738
- __decorate([
739
- state(),
740
- __metadata("design:type", Array)
741
- ], FigureLibraryPage.prototype, "releases", void 0);
742
- __decorate([
743
- state(),
744
- __metadata("design:type", Array)
745
- ], FigureLibraryPage.prototype, "maps", void 0);
746
- __decorate([
747
- state(),
748
- __metadata("design:type", Map)
749
- ], FigureLibraryPage.prototype, "candidate", void 0);
750
- __decorate([
751
- state(),
752
- __metadata("design:type", Map)
753
- ], FigureLibraryPage.prototype, "saved", void 0);
754
287
  __decorate([
755
288
  state(),
756
289
  __metadata("design:type", Object)
757
- ], FigureLibraryPage.prototype, "imported", void 0);
290
+ ], FigureLibraryPage.prototype, "gristConfig", void 0);
758
291
  __decorate([
759
- state(),
760
- __metadata("design:type", Object)
761
- ], FigureLibraryPage.prototype, "report", void 0);
762
- __decorate([
763
- state(),
764
- __metadata("design:type", Object)
765
- ], FigureLibraryPage.prototype, "busy", void 0);
766
- __decorate([
767
- state(),
768
- __metadata("design:type", Object)
769
- ], FigureLibraryPage.prototype, "loading", void 0);
770
- __decorate([
771
- state(),
772
- __metadata("design:type", Object)
773
- ], FigureLibraryPage.prototype, "loadError", void 0);
774
- __decorate([
775
- state(),
776
- __metadata("design:type", Object)
777
- ], FigureLibraryPage.prototype, "failure", void 0);
292
+ query('ox-grist'),
293
+ __metadata("design:type", DataGrist)
294
+ ], FigureLibraryPage.prototype, "grist", void 0);
778
295
  FigureLibraryPage = __decorate([
779
296
  customElement('figure-library-page')
780
297
  ], FigureLibraryPage);