@things-factory/figure-ui 10.1.177 → 10.1.179
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/figure-released.ts +6 -0
- package/client/graphql/index.ts +101 -25
- package/client/graphql/store.ts +2 -1
- package/client/index.ts +1 -0
- package/client/modeller/editor.ts +10 -2
- package/client/modeller/figure-first-save.ts +23 -3
- package/client/modeller/figure-parts.ts +53 -23
- package/client/modeller/figure-properties.ts +41 -7
- package/client/modeller/first-save.ts +2 -0
- package/client/modeller/product-session.ts +5 -3
- package/client/modeller/property-sections.ts +45 -0
- package/client/pages/figure-list-page.ts +52 -10
- package/client/pages/figure-modeller-page.ts +15 -3
- package/client/setting-let/figure-namespace-setting-let.ts +113 -31
- package/client/types.ts +8 -0
- package/client/viewparts/figure-detail-dock.ts +124 -9
- package/client/viewparts/figure-thumb.ts +6 -3
- package/dist-client/figure-released.d.ts +3 -0
- package/dist-client/figure-released.js +5 -0
- package/dist-client/figure-released.js.map +1 -0
- package/dist-client/graphql/index.d.ts +25 -13
- package/dist-client/graphql/index.js +85 -16
- package/dist-client/graphql/index.js.map +1 -1
- package/dist-client/graphql/store.js +2 -1
- package/dist-client/graphql/store.js.map +1 -1
- package/dist-client/index.d.ts +1 -0
- package/dist-client/index.js +1 -0
- package/dist-client/index.js.map +1 -1
- package/dist-client/modeller/editor.d.ts +2 -0
- package/dist-client/modeller/editor.js +13 -2
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/figure-first-save.d.ts +8 -0
- package/dist-client/modeller/figure-first-save.js +28 -3
- package/dist-client/modeller/figure-first-save.js.map +1 -1
- package/dist-client/modeller/figure-links.d.ts +1 -1
- package/dist-client/modeller/figure-parts.d.ts +5 -0
- package/dist-client/modeller/figure-parts.js +54 -22
- package/dist-client/modeller/figure-parts.js.map +1 -1
- package/dist-client/modeller/figure-properties.d.ts +4 -0
- package/dist-client/modeller/figure-properties.js +45 -7
- package/dist-client/modeller/figure-properties.js.map +1 -1
- package/dist-client/modeller/first-save.d.ts +2 -0
- package/dist-client/modeller/first-save.js.map +1 -1
- package/dist-client/modeller/product-session.d.ts +4 -1
- package/dist-client/modeller/product-session.js +3 -3
- package/dist-client/modeller/product-session.js.map +1 -1
- package/dist-client/modeller/property-sections.d.ts +26 -0
- package/dist-client/modeller/property-sections.js +17 -0
- package/dist-client/modeller/property-sections.js.map +1 -0
- package/dist-client/pages/figure-list-page.d.ts +4 -1
- package/dist-client/pages/figure-list-page.js +56 -10
- package/dist-client/pages/figure-list-page.js.map +1 -1
- package/dist-client/pages/figure-modeller-page.d.ts +2 -0
- package/dist-client/pages/figure-modeller-page.js +19 -3
- package/dist-client/pages/figure-modeller-page.js.map +1 -1
- package/dist-client/route.d.ts +1 -1
- package/dist-client/setting-let/figure-namespace-setting-let.d.ts +16 -5
- package/dist-client/setting-let/figure-namespace-setting-let.js +129 -31
- package/dist-client/setting-let/figure-namespace-setting-let.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/types.d.ts +12 -0
- package/dist-client/types.js.map +1 -1
- package/dist-client/viewparts/figure-detail-dock.d.ts +33 -2
- package/dist-client/viewparts/figure-detail-dock.js +171 -11
- package/dist-client/viewparts/figure-detail-dock.js.map +1 -1
- package/dist-client/viewparts/figure-thumb.d.ts +2 -0
- package/dist-client/viewparts/figure-thumb.js +13 -3
- package/dist-client/viewparts/figure-thumb.js.map +1 -1
- package/package.json +8 -7
- package/test/browser/category-page.js +28 -0
- package/test/browser/category.check.mjs +54 -0
- package/test/browser/mesh-select.check.mjs +1 -1
- package/test/browser/namespace-page.js +75 -0
- package/test/browser/namespace.check.mjs +104 -0
- package/test/browser/parts-page.js +8 -2
- package/test/browser/places-mesh.check.mjs +30 -2
- package/test/browser/property-section-page.js +45 -0
- package/test/browser/property-section.check.mjs +48 -0
- package/test/browser/props.check.mjs +13 -2
- package/test/browser/public-page.js +39 -0
- package/test/browser/public.check.mjs +79 -0
- package/test/browser/retire-page.js +37 -0
- package/test/browser/retire.check.mjs +96 -0
- package/test/figure-released.test.ts +54 -0
- package/test/modeller-route.test.ts +2 -1
- package/translations/en.json +20 -5
- package/translations/es.json +20 -5
- package/translations/ja.json +20 -5
- package/translations/ko.json +20 -5
- package/translations/ms.json +20 -5
- package/translations/zh.json +20 -5
|
@@ -3,20 +3,22 @@ import '@material/web/icon/icon.js'
|
|
|
3
3
|
import '../viewparts/figure-thumb.js'
|
|
4
4
|
|
|
5
5
|
import { css, html } from 'lit'
|
|
6
|
-
import { customElement, property, query } from 'lit/decorators.js'
|
|
6
|
+
import { customElement, property, query, state } from 'lit/decorators.js'
|
|
7
7
|
|
|
8
8
|
import type { DataGrist, FetchOption } from '@operato/data-grist'
|
|
9
9
|
import { i18next, localize } from '@operato/i18n'
|
|
10
10
|
import { p13n } from '@operato/p13n'
|
|
11
11
|
import { auth } from '@things-factory/auth-base/dist-client'
|
|
12
12
|
import { navigate, PageView } from '@operato/shell'
|
|
13
|
+
import { notify } from '@operato/layout'
|
|
13
14
|
import { CardCatalogStyles, CommonGristStyles, CommonHeaderStyles, ScrollbarStyles, WorkSurfacePageStyles } from '@operato/styles'
|
|
14
15
|
|
|
15
16
|
import { capacityFrom } from '../modeller/scene-budget.js'
|
|
16
17
|
import { deleteFigure, fetchFigureList } from '../graphql/index.js'
|
|
17
18
|
import type { Figure } from '../types.js'
|
|
19
|
+
import { hasBeenReleased } from '../figure-released.js'
|
|
18
20
|
import { modellerPathOf } from '../modeller/modeller-route.js'
|
|
19
|
-
import { FIGURE_THUMBNAIL_CHANGED, openFigureDetail } from '../viewparts/figure-detail-dock.js'
|
|
21
|
+
import { FIGURE_PUBLIC_CHANGED, FIGURE_RETIREMENT_CHANGED, FIGURE_THUMBNAIL_CHANGED, openFigureDetail } from '../viewparts/figure-detail-dock.js'
|
|
20
22
|
|
|
21
23
|
/**
|
|
22
24
|
* 이 값 아래면 점수를 색으로 눈에 띄게 한다.
|
|
@@ -95,6 +97,23 @@ export class FigureListPage extends FigureListPageBase {
|
|
|
95
97
|
카드 · 그림 칸 · 만들기 FAB 는 모든 카드 카탈로그와 같다(@operato/styles CardCatalogStyles,
|
|
96
98
|
ADR-0117 개정 2026-10-13). 여기에는 이 화면에만 있는 것만 둔다.
|
|
97
99
|
*/
|
|
100
|
+
/* Which figures are listed: quiet words beside the filters, coloured when they are on. */
|
|
101
|
+
button[scope] {
|
|
102
|
+
font: inherit;
|
|
103
|
+
padding: 0 8px;
|
|
104
|
+
border: none;
|
|
105
|
+
background: none;
|
|
106
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
107
|
+
cursor: pointer;
|
|
108
|
+
white-space: nowrap;
|
|
109
|
+
}
|
|
110
|
+
button[scope]:hover {
|
|
111
|
+
color: var(--md-sys-color-on-surface);
|
|
112
|
+
}
|
|
113
|
+
button[scope][aria-pressed='true'] {
|
|
114
|
+
color: var(--md-sys-color-primary);
|
|
115
|
+
font-weight: 600;
|
|
116
|
+
}
|
|
98
117
|
ox-grist {
|
|
99
118
|
/* 상태와 성숙도는 짧은 표식이다 — 두 줄로 세우지 않고 한 줄에 뉜다 */
|
|
100
119
|
--data-card-detail-direction: row;
|
|
@@ -108,6 +127,9 @@ export class FigureListPage extends FigureListPageBase {
|
|
|
108
127
|
|
|
109
128
|
@query('ox-grist') private grist!: DataGrist
|
|
110
129
|
|
|
130
|
+
/** The domain's own figures, or other domains' public ones (LV figure-server design G2), read only. */
|
|
131
|
+
@state() private scope: 'own' | 'public' = 'own'
|
|
132
|
+
|
|
111
133
|
private listen?: () => void
|
|
112
134
|
|
|
113
135
|
get context() {
|
|
@@ -149,6 +171,7 @@ export class FigureListPage extends FigureListPageBase {
|
|
|
149
171
|
<div class="filters">
|
|
150
172
|
<ox-filters-form autofocus without-search></ox-filters-form>
|
|
151
173
|
</div>
|
|
174
|
+
<button scope aria-pressed=${String(this.scope === 'public')} @click=${() => this.toggleScope()}>${i18next.t('figure.button.public-figures')}</button>
|
|
152
175
|
</div>
|
|
153
176
|
|
|
154
177
|
<ox-grist-personalizer slot="setting"></ox-grist-personalizer>
|
|
@@ -427,7 +450,7 @@ export class FigureListPage extends FigureListPageBase {
|
|
|
427
450
|
wide: true,
|
|
428
451
|
renderEmpty: true,
|
|
429
452
|
renderer: (_value: unknown, _column: any, record: Figure) => html`
|
|
430
|
-
<figure-thumb .figure=${record} @remove=${() => this.removeFigure(record)}></figure-thumb>
|
|
453
|
+
<figure-thumb .figure=${record} ?readonly=${this.scope === 'public'} @remove=${() => this.removeFigure(record)}></figure-thumb>
|
|
431
454
|
`
|
|
432
455
|
},
|
|
433
456
|
hidden: true,
|
|
@@ -466,7 +489,7 @@ export class FigureListPage extends FigureListPageBase {
|
|
|
466
489
|
handlers: {
|
|
467
490
|
click: (_columns: any, _data: any, _column: any, record: Figure) => {
|
|
468
491
|
if (record?.id) {
|
|
469
|
-
openFigureDetail(record)
|
|
492
|
+
openFigureDetail(record, this.scope === 'public')
|
|
470
493
|
}
|
|
471
494
|
}
|
|
472
495
|
}
|
|
@@ -493,10 +516,12 @@ export class FigureListPage extends FigureListPageBase {
|
|
|
493
516
|
}
|
|
494
517
|
}
|
|
495
518
|
auth.on('profile', this.listen)
|
|
496
|
-
/* A picture chosen in the preview: the cards are read again
|
|
497
|
-
window.addEventListener(FIGURE_THUMBNAIL_CHANGED, this.
|
|
519
|
+
/* A picture chosen in the preview, or a figure retired or brought back: the cards are read again. */
|
|
520
|
+
window.addEventListener(FIGURE_THUMBNAIL_CHANGED, this.cardsChanged)
|
|
521
|
+
window.addEventListener(FIGURE_RETIREMENT_CHANGED, this.cardsChanged)
|
|
522
|
+
window.addEventListener(FIGURE_PUBLIC_CHANGED, this.cardsChanged)
|
|
498
523
|
}
|
|
499
|
-
private
|
|
524
|
+
private cardsChanged = () => {
|
|
500
525
|
if (this.active) this.grist?.fetch()
|
|
501
526
|
}
|
|
502
527
|
|
|
@@ -505,7 +530,9 @@ export class FigureListPage extends FigureListPageBase {
|
|
|
505
530
|
auth.off?.('profile', this.listen)
|
|
506
531
|
this.listen = undefined
|
|
507
532
|
}
|
|
508
|
-
window.removeEventListener(FIGURE_THUMBNAIL_CHANGED, this.
|
|
533
|
+
window.removeEventListener(FIGURE_THUMBNAIL_CHANGED, this.cardsChanged)
|
|
534
|
+
window.removeEventListener(FIGURE_RETIREMENT_CHANGED, this.cardsChanged)
|
|
535
|
+
window.removeEventListener(FIGURE_PUBLIC_CHANGED, this.cardsChanged)
|
|
509
536
|
super.disconnectedCallback()
|
|
510
537
|
}
|
|
511
538
|
|
|
@@ -522,7 +549,7 @@ export class FigureListPage extends FigureListPageBase {
|
|
|
522
549
|
* 받아 화면에 낸다 — 목록이 비어 있는 것과 못 불러온 것은 다른 사실이다.
|
|
523
550
|
*/
|
|
524
551
|
async fetchHandler({ page = 1, limit = 24, sortings = [], filters = [] }: FetchOption) {
|
|
525
|
-
const { items, total } = await fetchFigureList({ page, limit, filters, sortings })
|
|
552
|
+
const { items, total } = await fetchFigureList({ page, limit, filters, sortings, scope: this.scope })
|
|
526
553
|
|
|
527
554
|
return { total: total || 0, records: items || [] }
|
|
528
555
|
}
|
|
@@ -532,15 +559,30 @@ export class FigureListPage extends FigureListPageBase {
|
|
|
532
559
|
navigate(modellerPathOf(figure))
|
|
533
560
|
}
|
|
534
561
|
|
|
562
|
+
private toggleScope() {
|
|
563
|
+
this.scope = this.scope === 'public' ? 'own' : 'public'
|
|
564
|
+
this.grist?.fetch()
|
|
565
|
+
}
|
|
566
|
+
|
|
535
567
|
/** `remove` 는 HTMLElement 의 메서드다 — 가리면 조용한 결함이 된다. */
|
|
536
568
|
private async removeFigure(figure: Figure) {
|
|
569
|
+
if (this.scope === 'public') return
|
|
537
570
|
// 되돌릴 수 없는 조작이므로 무엇이 사라지는지 이름으로 확인한다
|
|
571
|
+
if (hasBeenReleased(figure)) {
|
|
572
|
+
notify({ level: 'info', message: i18next.t('figure.text.released-figure-not-deletable', { name: figure.name }) })
|
|
573
|
+
return
|
|
574
|
+
}
|
|
538
575
|
const message = i18next.t('figure.text.delete-figure-confirm', { name: figure.name })
|
|
539
576
|
if (!confirm(message)) {
|
|
540
577
|
return
|
|
541
578
|
}
|
|
542
579
|
|
|
543
|
-
|
|
580
|
+
try {
|
|
581
|
+
await deleteFigure(figure.id)
|
|
582
|
+
} catch (e) {
|
|
583
|
+
notify({ level: 'error', message: (e as Error).message })
|
|
584
|
+
return
|
|
585
|
+
}
|
|
544
586
|
this.grist.fetch()
|
|
545
587
|
}
|
|
546
588
|
}
|
|
@@ -6,7 +6,7 @@ import { modellerPathOf } from '../modeller/modeller-route.js'
|
|
|
6
6
|
import { createFigureProductSession } from '../modeller/product-session.js'
|
|
7
7
|
import { figureProductStore } from '../graphql/store.js'
|
|
8
8
|
import { i18next } from '@operato/i18n'
|
|
9
|
-
import { fetchFigure, fetchFigureTypeNames, updateFigure } from '../graphql/index.js'
|
|
9
|
+
import { fetchFigure, fetchFigureCategories, fetchFigureNamespaces, fetchFigureTypeNames, updateFigure } from '../graphql/index.js'
|
|
10
10
|
import { figureProposalOutcome } from '../figure-assistant.js'
|
|
11
11
|
import { availableRecipes, recipeGuide } from '../modeller/recipe-source.js'
|
|
12
12
|
import type { RecipeTemplate } from '../modeller/recipes.js'
|
|
@@ -137,6 +137,8 @@ export class FigureModellerPage extends PageView {
|
|
|
137
137
|
* `figure-modeller` 가 이 화면으로 바뀌면 저작자는 발행을 할 수 없었다. 새 도형은 공간을 선언하기 전에는 전부
|
|
138
138
|
* 발행 판정에 막히는데 그 선언을 할 단추도 없었다(2026-09-24 확인). 그래서 발행 패널을 건다.
|
|
139
139
|
*/
|
|
140
|
+
/** The categories the server allows, or null when a category is free text (G5). */
|
|
141
|
+
@state() private categories: string[] | null = null
|
|
140
142
|
@state() private row: { state?: 'draft' | 'released'; version: number; namespace?: string; type?: string; category?: string; tags?: string[] } = {
|
|
141
143
|
version: 0
|
|
142
144
|
}
|
|
@@ -398,6 +400,10 @@ export class FigureModellerPage extends PageView {
|
|
|
398
400
|
}
|
|
399
401
|
connectedCallback() {
|
|
400
402
|
super.connectedCallback()
|
|
403
|
+
/* Read once: the server's category vocabulary does not change while it runs. Free text when it has none or cannot say. */
|
|
404
|
+
fetchFigureCategories()
|
|
405
|
+
.then(codes => (this.categories = codes))
|
|
406
|
+
.catch(() => (this.categories = null))
|
|
401
407
|
window.addEventListener('figure-apply', this.applyAI)
|
|
402
408
|
window.addEventListener('beforeunload', this.holdUnsaved)
|
|
403
409
|
}
|
|
@@ -481,8 +487,13 @@ export class FigureModellerPage extends PageView {
|
|
|
481
487
|
}
|
|
482
488
|
/** The first save's question, over the names this domain already uses. */
|
|
483
489
|
private askFirstSave = async (name: string): Promise<FirstSave | undefined> => {
|
|
484
|
-
const taken = await
|
|
485
|
-
|
|
490
|
+
const [taken, namespaces] = await Promise.all([
|
|
491
|
+
fetchFigureTypeNames().catch(() => [] as string[]),
|
|
492
|
+
fetchFigureNamespaces()
|
|
493
|
+
.then(all => all.filter(namespace => namespace.state === 'active'))
|
|
494
|
+
.catch(() => [])
|
|
495
|
+
])
|
|
496
|
+
const answer = await openPopup<FirstSave>(html`<figure-first-save .type=${this.row.type ?? ''} .name=${name} .taken=${taken} .namespaces=${namespaces}></figure-first-save>`, {
|
|
486
497
|
backdrop: true,
|
|
487
498
|
size: 'small',
|
|
488
499
|
title: i18next.t('figure.title.save-new-figure')
|
|
@@ -522,6 +533,7 @@ export class FigureModellerPage extends PageView {
|
|
|
522
533
|
.revision=${this.session.revisionNow()}
|
|
523
534
|
.figureId=${view.id}
|
|
524
535
|
.figureCategory=${this.row.category}
|
|
536
|
+
.figureCategories=${this.categories}
|
|
525
537
|
.figureTags=${this.row.tags}
|
|
526
538
|
@command=${this.onCommand}
|
|
527
539
|
@catalog=${this.onCatalog}
|
|
@@ -1,17 +1,24 @@
|
|
|
1
1
|
import '@material/web/textfield/filled-text-field.js'
|
|
2
|
+
import '@material/web/button/text-button.js'
|
|
2
3
|
import '@things-factory/setting-base'
|
|
3
4
|
|
|
4
|
-
import { css, html, LitElement } from 'lit'
|
|
5
|
-
import { customElement, state } from 'lit/decorators.js'
|
|
5
|
+
import { css, html, LitElement, nothing } from 'lit'
|
|
6
|
+
import { customElement, property, state } from 'lit/decorators.js'
|
|
6
7
|
|
|
7
8
|
import { i18next, localize } from '@operato/i18n'
|
|
8
9
|
|
|
9
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
createFigureNamespace,
|
|
12
|
+
fetchFigureNamespaces,
|
|
13
|
+
setDefaultFigureNamespace,
|
|
14
|
+
setFigureNamespaceState,
|
|
15
|
+
type FigureNamespace
|
|
16
|
+
} from '../graphql/index.js'
|
|
10
17
|
|
|
11
18
|
/**
|
|
12
|
-
* The domain
|
|
13
|
-
*
|
|
14
|
-
*
|
|
19
|
+
* The namespaces the domain owns (LV figure-server design G1): the `{namespace}` of every full name `{namespace}/{type}`
|
|
20
|
+
* a board stores. A namespace is added once and never renamed; the default is the one new figures are created under;
|
|
21
|
+
* a suspended one takes no new figure and no release. The server refuses what it must and says why.
|
|
15
22
|
*/
|
|
16
23
|
@customElement('figure-namespace-setting-let')
|
|
17
24
|
export class FigureNamespaceSettingLet extends localize(i18next)(LitElement) {
|
|
@@ -23,63 +30,138 @@ export class FigureNamespaceSettingLet extends localize(i18next)(LitElement) {
|
|
|
23
30
|
gap: var(--spacing-small, 4px);
|
|
24
31
|
text-transform: none;
|
|
25
32
|
}
|
|
26
|
-
|
|
27
|
-
|
|
33
|
+
ul {
|
|
34
|
+
list-style: none;
|
|
35
|
+
margin: 0;
|
|
36
|
+
padding: 0;
|
|
37
|
+
display: grid;
|
|
38
|
+
gap: 2px;
|
|
39
|
+
}
|
|
40
|
+
li {
|
|
41
|
+
display: flex;
|
|
42
|
+
flex-wrap: wrap;
|
|
43
|
+
align-items: center;
|
|
44
|
+
gap: 4px 12px;
|
|
45
|
+
min-height: 32px;
|
|
28
46
|
}
|
|
29
|
-
|
|
30
|
-
font: var(--
|
|
47
|
+
code {
|
|
48
|
+
font-family: var(--ui-mono, ui-monospace, monospace);
|
|
31
49
|
color: var(--md-sys-color-on-surface);
|
|
32
50
|
}
|
|
33
|
-
|
|
34
|
-
margin: 0;
|
|
51
|
+
[mark] {
|
|
35
52
|
font-size: 0.8rem;
|
|
36
53
|
color: var(--md-sys-color-on-surface-variant);
|
|
37
54
|
}
|
|
55
|
+
li[suspended] code {
|
|
56
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
57
|
+
}
|
|
58
|
+
[actions] {
|
|
59
|
+
margin-left: auto;
|
|
60
|
+
display: flex;
|
|
61
|
+
gap: 4px;
|
|
62
|
+
}
|
|
63
|
+
[add] {
|
|
64
|
+
display: flex;
|
|
65
|
+
align-items: center;
|
|
66
|
+
gap: 8px;
|
|
67
|
+
}
|
|
68
|
+
md-filled-text-field {
|
|
69
|
+
flex: 1;
|
|
70
|
+
}
|
|
38
71
|
`
|
|
39
72
|
]
|
|
40
73
|
|
|
41
|
-
|
|
74
|
+
/* The server's calls; a check gives its own. */
|
|
75
|
+
@property({ attribute: false }) list: () => Promise<FigureNamespace[]> = fetchFigureNamespaces
|
|
76
|
+
@property({ attribute: false }) add: (name: string) => Promise<FigureNamespace> = name => createFigureNamespace(name)
|
|
77
|
+
@property({ attribute: false }) makeDefault: (name: string) => Promise<FigureNamespace> = setDefaultFigureNamespace
|
|
78
|
+
@property({ attribute: false }) setState: (name: string, state: 'active' | 'suspended') => Promise<FigureNamespace> = setFigureNamespaceState
|
|
79
|
+
|
|
80
|
+
@state() private namespaces?: FigureNamespace[]
|
|
81
|
+
@state() private draft = ''
|
|
42
82
|
@state() private refused = ''
|
|
83
|
+
@state() private busy = false
|
|
43
84
|
|
|
44
85
|
render() {
|
|
45
|
-
const
|
|
86
|
+
const namespaces = this.namespaces
|
|
46
87
|
return html`
|
|
47
88
|
<setting-let>
|
|
48
89
|
<span slot="title">${i18next.t('figure.title.namespace-setting')}</span>
|
|
49
90
|
<div slot="content">
|
|
50
|
-
${
|
|
51
|
-
? html
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
91
|
+
${namespaces?.length
|
|
92
|
+
? html`<ul>
|
|
93
|
+
${namespaces.map(
|
|
94
|
+
namespace => html`<li ?suspended=${namespace.state !== 'active'}>
|
|
95
|
+
<code>${namespace.name}</code>
|
|
96
|
+
${namespace.isDefault ? html`<span mark>${i18next.t('figure.label.namespace-default')}</span>` : nothing}
|
|
97
|
+
${namespace.state !== 'active' ? html`<span mark>${i18next.t('figure.label.namespace-suspended')}</span>` : nothing}
|
|
98
|
+
${namespace.isDefault
|
|
99
|
+
? nothing
|
|
100
|
+
: html`<span actions>
|
|
101
|
+
${namespace.state === 'active'
|
|
102
|
+
? html`<md-text-button ?disabled=${this.busy} @click=${() => this.run(() => this.makeDefault(namespace.name!))}
|
|
103
|
+
>${i18next.t('figure.button.namespace-make-default')}</md-text-button
|
|
104
|
+
><md-text-button ?disabled=${this.busy} @click=${() => this.run(() => this.setState(namespace.name!, 'suspended'))}
|
|
105
|
+
>${i18next.t('figure.button.namespace-suspend')}</md-text-button
|
|
106
|
+
>`
|
|
107
|
+
: html`<md-text-button ?disabled=${this.busy} @click=${() => this.run(() => this.setState(namespace.name!, 'active'))}
|
|
108
|
+
>${i18next.t('figure.button.namespace-activate')}</md-text-button
|
|
109
|
+
>`}
|
|
110
|
+
</span>`}
|
|
111
|
+
</li>`
|
|
112
|
+
)}
|
|
113
|
+
</ul>`
|
|
114
|
+
: nothing}
|
|
115
|
+
<div add>
|
|
116
|
+
<md-filled-text-field
|
|
117
|
+
.label=${i18next.t('figure.label.namespace')}
|
|
118
|
+
.value=${this.draft}
|
|
119
|
+
?error=${!!this.refused}
|
|
120
|
+
error-text=${this.refused}
|
|
121
|
+
@input=${(e: Event) => {
|
|
122
|
+
this.draft = (e.target as HTMLInputElement).value
|
|
123
|
+
this.refused = ''
|
|
124
|
+
}}
|
|
125
|
+
@keydown=${(e: KeyboardEvent) => e.key === 'Enter' && !e.isComposing && this.addDraft()}
|
|
126
|
+
></md-filled-text-field>
|
|
127
|
+
<md-text-button ?disabled=${this.busy || !this.draft.trim()} @click=${() => this.addDraft()}>${i18next.t('figure.button.namespace-add')}</md-text-button>
|
|
128
|
+
</div>
|
|
64
129
|
</div>
|
|
65
130
|
</setting-let>
|
|
66
131
|
`
|
|
67
132
|
}
|
|
68
133
|
|
|
69
134
|
async firstUpdated() {
|
|
135
|
+
await this.reload()
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private async reload() {
|
|
70
139
|
try {
|
|
71
|
-
this.
|
|
140
|
+
this.namespaces = await this.list()
|
|
72
141
|
} catch (e) {
|
|
73
142
|
this.refused = (e as Error).message
|
|
74
143
|
}
|
|
75
144
|
}
|
|
76
145
|
|
|
77
|
-
private async
|
|
146
|
+
private async addDraft() {
|
|
147
|
+
const name = this.draft.trim()
|
|
148
|
+
if (!name || this.busy) return
|
|
149
|
+
if (await this.run(() => this.add(name))) this.draft = ''
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** One change, then the list read again. False when the server refused it; its reason is shown. */
|
|
153
|
+
private async run(change: () => Promise<unknown>): Promise<boolean> {
|
|
154
|
+
this.busy = true
|
|
78
155
|
try {
|
|
79
|
-
|
|
156
|
+
await change()
|
|
80
157
|
this.refused = ''
|
|
158
|
+
await this.reload()
|
|
159
|
+
return true
|
|
81
160
|
} catch (e) {
|
|
82
161
|
this.refused = (e as Error).message
|
|
162
|
+
return false
|
|
163
|
+
} finally {
|
|
164
|
+
this.busy = false
|
|
83
165
|
}
|
|
84
166
|
}
|
|
85
167
|
}
|
package/client/types.ts
CHANGED
|
@@ -28,6 +28,14 @@ export interface Figure {
|
|
|
28
28
|
source?: string
|
|
29
29
|
properties?: string
|
|
30
30
|
version?: number
|
|
31
|
+
/** Retired: still released and served, no longer offered for new use. */
|
|
32
|
+
deprecated?: boolean
|
|
33
|
+
/** The type to use instead of a retired figure, in the same domain. */
|
|
34
|
+
supersededBy?: string | null
|
|
35
|
+
/** Public: other domains read its releases and draw it by full name (LV figure-server design G2). */
|
|
36
|
+
public?: boolean
|
|
37
|
+
/** The domain it belongs to; carried by another domain's public figure, so a screen says whose it is. */
|
|
38
|
+
domain?: { id: string; name?: string; subdomain?: string } | null
|
|
31
39
|
}
|
|
32
40
|
|
|
33
41
|
/**
|
|
@@ -9,7 +9,8 @@ import { ASSISTANT_DOCK_VIEWPART } from '@things-factory/ai-assistant/dist-clien
|
|
|
9
9
|
import { figureCostOf, parseFigureAsset, type FigureAsset } from '@hatiolab/figure-model'
|
|
10
10
|
import { capacityFrom } from '../modeller/scene-budget.js'
|
|
11
11
|
import { SidePanelStyles } from '@things-factory/figure-viewer/dist-client/side-panel-styles.js'
|
|
12
|
-
import { fetchFigure, fetchReleasedFigure, updateFigure } from '../graphql/index.js'
|
|
12
|
+
import { fetchFigure, fetchFigureTypeNames, fetchReleasedFigure, setFigureDeprecation, setFigurePublic, updateFigure } from '../graphql/index.js'
|
|
13
|
+
import { hasBeenReleased } from '../figure-released.js'
|
|
13
14
|
import { modellerPathOf } from '../modeller/modeller-route.js'
|
|
14
15
|
import { leftOpeningPage } from './detail-route.js'
|
|
15
16
|
import type { Figure } from '../types.js'
|
|
@@ -19,9 +20,15 @@ import '@things-factory/figure-viewer/dist-client/figure-facts-list.js'
|
|
|
19
20
|
const VIEWPART = 'figure-detail'
|
|
20
21
|
/** Said on window when a figure's card picture was replaced from the preview: `{ detail: { id } }`. */
|
|
21
22
|
export const FIGURE_THUMBNAIL_CHANGED = 'figure-thumbnail-changed'
|
|
23
|
+
/** Said on window when a figure was retired or brought back: `{ detail: { id } }`. */
|
|
24
|
+
export const FIGURE_RETIREMENT_CHANGED = 'figure-retirement-changed'
|
|
25
|
+
/** Said on window when a figure was made public or private: `{ detail: { id } }`. */
|
|
26
|
+
export const FIGURE_PUBLIC_CHANGED = 'figure-public-changed'
|
|
22
27
|
export interface FigureDetailRecord {
|
|
23
28
|
saved: Figure
|
|
24
29
|
released?: { version: number; source: string }
|
|
30
|
+
/** Another domain's public figure (G2): only its release is shown, and nothing on it is changed from here. */
|
|
31
|
+
foreign?: boolean
|
|
25
32
|
}
|
|
26
33
|
|
|
27
34
|
/** Catalogue details stay while the catalogue is shown; choosing a size or a pose has no write path. */
|
|
@@ -68,6 +75,16 @@ export class FigureDetailDock extends localize(i18next)(LitElement) {
|
|
|
68
75
|
p { margin: 0; line-height: 1.5; }
|
|
69
76
|
[description] { padding: 10px 16px 0; color: var(--md-sys-color-on-surface-variant); }
|
|
70
77
|
[status] { display: grid; gap: 8px; justify-items: start; padding: 12px 16px; }
|
|
78
|
+
/* Retiring: quiet words in the line under what is shown, like the source switch above it. */
|
|
79
|
+
[retire], [publish] { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 12px; color: var(--md-sys-color-on-surface-variant); }
|
|
80
|
+
[retire] button, [publish] button { padding: 0; border: none; background: none; color: var(--md-sys-color-on-surface-variant); }
|
|
81
|
+
[retire] button:hover:not(:disabled), [publish] button:hover:not(:disabled) { color: var(--md-sys-color-on-surface); }
|
|
82
|
+
[publish] [published] { color: var(--md-sys-color-on-surface); font-weight: 600; }
|
|
83
|
+
[retire] button[primary] { color: var(--md-sys-color-primary); font-weight: 600; }
|
|
84
|
+
[retire] input { font: inherit; min-width: 0; flex: 1 1 160px; padding: 2px 6px; border: 1px solid var(--md-sys-color-outline-variant);
|
|
85
|
+
border-radius: 6px; background: var(--md-sys-color-surface); color: var(--md-sys-color-on-surface); }
|
|
86
|
+
[retire] [retired] { color: var(--md-sys-color-on-surface); font-weight: 600; }
|
|
87
|
+
[retire-error] { color: var(--md-sys-color-error); }
|
|
71
88
|
@media (max-width: 1439.98px) {
|
|
72
89
|
:host { position: fixed; inset: var(--figure-detail-top, 44px) 0 0 auto; height: calc(100dvh - var(--figure-detail-top, 44px)); z-index: 11; box-shadow: -4px 0 16px rgb(0 0 0 / 20%); }
|
|
73
90
|
}
|
|
@@ -85,6 +102,19 @@ export class FigureDetailDock extends localize(i18next)(LitElement) {
|
|
|
85
102
|
await updateFigure(id, { thumbnail: dataUrl })
|
|
86
103
|
window.dispatchEvent(new CustomEvent(FIGURE_THUMBNAIL_CHANGED, { detail: { id } }))
|
|
87
104
|
}
|
|
105
|
+
/** Retires a released figure or brings it back (`setFigureDeprecation`); a check gives its own. */
|
|
106
|
+
@property({ attribute: false }) retire: (id: string, deprecated: boolean, supersededBy: string | null) => Promise<Figure> = setFigureDeprecation
|
|
107
|
+
/** The types a retired figure may name instead: the domain's other figures. */
|
|
108
|
+
@property({ attribute: false }) typeNamesOf: () => Promise<string[]> = fetchFigureTypeNames
|
|
109
|
+
/** Makes a released figure public or private (`setFigurePublic`); a check gives its own. */
|
|
110
|
+
@property({ attribute: false }) publish: (id: string, isPublic: boolean) => Promise<Figure> = setFigurePublic
|
|
111
|
+
/** Another domain's public figure as the catalogue listed it, with its latest release; its draft is not this domain's to open. */
|
|
112
|
+
@property({ attribute: false }) loadPublic: (row: Figure) => Promise<FigureDetailRecord> = async row => ({
|
|
113
|
+
saved: row,
|
|
114
|
+
released: await fetchReleasedFigure(row.id),
|
|
115
|
+
foreign: true
|
|
116
|
+
})
|
|
117
|
+
@state() private publishError = ''
|
|
88
118
|
@state() private figureId = ''
|
|
89
119
|
@state() private record?: FigureDetailRecord
|
|
90
120
|
@state() private asset?: FigureAsset
|
|
@@ -92,12 +122,17 @@ export class FigureDetailDock extends localize(i18next)(LitElement) {
|
|
|
92
122
|
@state() private loading = false
|
|
93
123
|
@state() private error = ''
|
|
94
124
|
@state() private cost?: ReturnType<typeof figureCostOf>
|
|
125
|
+
@state() private retiring = false
|
|
126
|
+
@state() private instead = ''
|
|
127
|
+
@state() private typeNames: string[] = []
|
|
128
|
+
@state() private retireError = ''
|
|
129
|
+
@state() private retireBusy = false
|
|
95
130
|
private ticket = 0
|
|
96
131
|
private unsubscribe?: () => void
|
|
97
132
|
private boundary?: Element
|
|
98
133
|
private boundaryObserver?: ResizeObserver
|
|
99
134
|
|
|
100
|
-
async show(id: string) {
|
|
135
|
+
async show(id: string, publicRow?: Figure) {
|
|
101
136
|
const ticket = ++this.ticket
|
|
102
137
|
this.figureId = id
|
|
103
138
|
this.record = undefined
|
|
@@ -105,8 +140,11 @@ export class FigureDetailDock extends localize(i18next)(LitElement) {
|
|
|
105
140
|
this.cost = undefined
|
|
106
141
|
this.loading = true
|
|
107
142
|
this.error = ''
|
|
143
|
+
this.retiring = false
|
|
144
|
+
this.retireError = ''
|
|
145
|
+
this.publishError = ''
|
|
108
146
|
try {
|
|
109
|
-
const record = await this.load(id)
|
|
147
|
+
const record = publicRow ? await this.loadPublic(publicRow) : await this.load(id)
|
|
110
148
|
if (ticket !== this.ticket) return
|
|
111
149
|
this.record = record
|
|
112
150
|
this.choose(record.released ? 'released' : 'saved')
|
|
@@ -168,6 +206,78 @@ export class FigureDetailDock extends localize(i18next)(LitElement) {
|
|
|
168
206
|
private escape = (e: KeyboardEvent) => {
|
|
169
207
|
if (e.key === 'Escape') hideDetail()
|
|
170
208
|
}
|
|
209
|
+
/**
|
|
210
|
+
* Retiring a released figure (it stays released and served; boards already using it keep it), naming the type to use
|
|
211
|
+
* instead, or bringing it back. A figure never released is deleted from the catalogue instead.
|
|
212
|
+
*/
|
|
213
|
+
private retireRow(saved: Figure) {
|
|
214
|
+
if (saved.deprecated) {
|
|
215
|
+
return html`<div retire>
|
|
216
|
+
<span retired>${saved.supersededBy
|
|
217
|
+
? i18next.t('figure.text.retired-instead', { type: saved.supersededBy })
|
|
218
|
+
: i18next.t('figure.label.retired')}</span>
|
|
219
|
+
<button ?disabled=${this.retireBusy} @click=${() => this.setRetired(saved, false)}>${i18next.t('figure.button.unretire')}</button>
|
|
220
|
+
</div>
|
|
221
|
+
${this.retireError ? html`<p retire-error role="alert">${this.retireError}</p>` : nothing}`
|
|
222
|
+
}
|
|
223
|
+
if (!this.retiring) {
|
|
224
|
+
return html`<div retire><button @click=${() => this.openRetire(saved)}>${i18next.t('figure.button.retire')}</button></div>`
|
|
225
|
+
}
|
|
226
|
+
return html`<div retire>
|
|
227
|
+
<input list="figure-retire-instead" aria-label=${i18next.t('figure.label.superseded-by')}
|
|
228
|
+
placeholder=${i18next.t('figure.label.superseded-by')} .value=${this.instead}
|
|
229
|
+
@input=${(e: Event) => (this.instead = (e.target as HTMLInputElement).value)} />
|
|
230
|
+
<datalist id="figure-retire-instead">${this.typeNames.map(name => html`<option value=${name}></option>`)}</datalist>
|
|
231
|
+
<button primary ?disabled=${this.retireBusy} @click=${() => this.setRetired(saved, true)}>${i18next.t('figure.button.retire')}</button>
|
|
232
|
+
<button ?disabled=${this.retireBusy} @click=${() => { this.retiring = false; this.retireError = '' }}>${i18next.t('figure.button.cancel')}</button>
|
|
233
|
+
</div>
|
|
234
|
+
${this.retireError ? html`<p retire-error role="alert">${this.retireError}</p>` : nothing}`
|
|
235
|
+
}
|
|
236
|
+
/** Public to other domains, or not (G2): they read its releases and draw it by full name; its draft stays here. */
|
|
237
|
+
private publishRow(saved: Figure) {
|
|
238
|
+
return html`<div publish>
|
|
239
|
+
${saved.public ? html`<span published>${i18next.t('figure.label.public')}</span>` : nothing}
|
|
240
|
+
<button ?disabled=${this.retireBusy} @click=${() => this.setPublic(saved, !saved.public)}>${i18next.t(saved.public ? 'figure.button.make-private' : 'figure.button.make-public')}</button>
|
|
241
|
+
</div>
|
|
242
|
+
${this.publishError ? html`<p retire-error role="alert">${this.publishError}</p>` : nothing}`
|
|
243
|
+
}
|
|
244
|
+
private async setPublic(saved: Figure, isPublic: boolean) {
|
|
245
|
+
this.retireBusy = true
|
|
246
|
+
this.publishError = ''
|
|
247
|
+
try {
|
|
248
|
+
const figure = await this.publish(saved.id, isPublic)
|
|
249
|
+
if (this.record?.saved.id === saved.id) this.record = { ...this.record, saved: { ...this.record.saved, ...figure } }
|
|
250
|
+
window.dispatchEvent(new CustomEvent(FIGURE_PUBLIC_CHANGED, { detail: { id: saved.id } }))
|
|
251
|
+
} catch (e) {
|
|
252
|
+
this.publishError = (e as Error).message
|
|
253
|
+
} finally {
|
|
254
|
+
this.retireBusy = false
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
private async openRetire(saved: Figure) {
|
|
258
|
+
this.retiring = true
|
|
259
|
+
this.instead = ''
|
|
260
|
+
this.retireError = ''
|
|
261
|
+
try {
|
|
262
|
+
this.typeNames = (await this.typeNamesOf()).filter(name => name !== saved.type)
|
|
263
|
+
} catch {
|
|
264
|
+
this.typeNames = []
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
private async setRetired(saved: Figure, deprecated: boolean) {
|
|
268
|
+
this.retireBusy = true
|
|
269
|
+
this.retireError = ''
|
|
270
|
+
try {
|
|
271
|
+
const figure = await this.retire(saved.id, deprecated, deprecated ? this.instead.trim() || null : null)
|
|
272
|
+
if (this.record?.saved.id === saved.id) this.record = { ...this.record, saved: { ...this.record.saved, ...figure } }
|
|
273
|
+
this.retiring = false
|
|
274
|
+
window.dispatchEvent(new CustomEvent(FIGURE_RETIREMENT_CHANGED, { detail: { id: saved.id } }))
|
|
275
|
+
} catch (e) {
|
|
276
|
+
this.retireError = (e as Error).message
|
|
277
|
+
} finally {
|
|
278
|
+
this.retireBusy = false
|
|
279
|
+
}
|
|
280
|
+
}
|
|
171
281
|
render() {
|
|
172
282
|
const saved = this.record?.saved
|
|
173
283
|
const released = this.record?.released
|
|
@@ -179,19 +289,21 @@ export class FigureDetailDock extends localize(i18next)(LitElement) {
|
|
|
179
289
|
return html`<header>
|
|
180
290
|
<div title-block><h2 title=${saved?.name || ''}>${saved?.name || i18next.t('figure.title.detail')}</h2>
|
|
181
291
|
${saved ? html`<code title=${fullName(saved)}>${fullName(saved)}</code>` : nothing}</div>
|
|
182
|
-
|
|
292
|
+
${this.record?.foreign ? nothing : html`<button icon edit aria-label=${i18next.t('figure.button.detail-edit')} title=${i18next.t('figure.button.detail-edit')} ?disabled=${!saved}
|
|
183
293
|
@click=${() => {
|
|
184
294
|
// The modeller shows the figure itself; a preview of it beside the editor only covers the work.
|
|
185
295
|
hideDetail()
|
|
186
296
|
navigate(modellerPathOf(saved))
|
|
187
|
-
}}><md-icon>edit</md-icon></button
|
|
297
|
+
}}><md-icon>edit</md-icon></button>`}
|
|
188
298
|
<button icon aria-label=${i18next.t('figure.button.close')} title=${i18next.t('figure.button.close')} @click=${() => hideDetail()}><md-icon>close</md-icon></button>
|
|
189
299
|
</header>
|
|
190
300
|
<div body>
|
|
191
301
|
${this.loading ? html`<p status role="status">${i18next.t('figure.text.detail-loading')}</p>` : nothing}
|
|
192
302
|
${this.error ? html`<div status role="alert"><p>${this.error}</p><button act @click=${() => this.show(this.figureId)}><md-icon>refresh</md-icon>${i18next.t('figure.button.detail-retry')}</button></div>` : nothing}
|
|
193
303
|
${saved ? html`<section meta>
|
|
194
|
-
${released
|
|
304
|
+
${released && this.record?.foreign
|
|
305
|
+
? html`<div source><span>${i18next.t('figure.label.detail-released', { version: released.version })}</span>${saved.domain?.name ? html`<span owner>${saved.domain.name}</span>` : nothing}</div>`
|
|
306
|
+
: released
|
|
195
307
|
? html`<div source role="group" aria-label=${i18next.t('figure.label.detail-source')}>
|
|
196
308
|
<button aria-pressed=${String(this.sourceKind === 'released')} @click=${() => this.choose('released')}>${i18next.t('figure.label.detail-released', { version: released.version })}</button>
|
|
197
309
|
<button aria-pressed=${String(this.sourceKind === 'saved')} @click=${() => this.choose('saved')}>${savedLabel}</button>
|
|
@@ -205,9 +317,11 @@ export class FigureDetailDock extends localize(i18next)(LitElement) {
|
|
|
205
317
|
<div><dt>${i18next.t('figure.label.budget-groups')}</dt><dd>${cost?.groups ?? '—'}</dd></div>
|
|
206
318
|
<div><dt>${i18next.t('figure.label.budget-parts')}</dt><dd>${this.asset.appearance.length}</dd></div>
|
|
207
319
|
</dl>` : nothing}
|
|
320
|
+
${hasBeenReleased(saved) && !this.record?.foreign ? this.retireRow(saved) : nothing}
|
|
321
|
+
${hasBeenReleased(saved) && !this.record?.foreign ? this.publishRow(saved) : nothing}
|
|
208
322
|
</section>` : nothing}
|
|
209
323
|
${this.asset ? html`<figure-asset-viewer .asset=${this.asset}
|
|
210
|
-
.keepShot=${saved?.id ? (dataUrl: string) => this.keepThumbnail(saved.id, dataUrl) : undefined}>
|
|
324
|
+
.keepShot=${saved?.id && !this.record?.foreign ? (dataUrl: string) => this.keepThumbnail(saved.id, dataUrl) : undefined}>
|
|
211
325
|
${saved?.description ? html`<p slot="facts" description>${saved.description}</p>` : nothing}
|
|
212
326
|
${saved ? html`<figure-facts-list slot="facts" .asset=${this.asset} .row=${saved}></figure-facts-list>` : nothing}
|
|
213
327
|
</figure-asset-viewer>` : nothing}
|
|
@@ -236,7 +350,8 @@ let openedOn: string | undefined
|
|
|
236
350
|
function hideDetail() {
|
|
237
351
|
store.dispatch({ type: UPDATE_VIEWPART, name: VIEWPART, override: { show: false } })
|
|
238
352
|
}
|
|
239
|
-
|
|
353
|
+
/** Opens the preview of a figure; `publicRow` for another domain's public figure, shown by its release alone. */
|
|
354
|
+
export function openFigureDetail(figure: Figure, publicRow = false) {
|
|
240
355
|
if (!dock) {
|
|
241
356
|
dock = document.createElement('figure-detail-dock') as FigureDetailDock
|
|
242
357
|
}
|
|
@@ -244,5 +359,5 @@ export function openFigureDetail(figure: Figure) {
|
|
|
244
359
|
store.dispatch({ type: UPDATE_VIEWPART, name: ASSISTANT_DOCK_VIEWPART, override: { show: false } })
|
|
245
360
|
appendViewpart({ name: VIEWPART, position: VIEWPART_POSITION.ASIDEBAR,
|
|
246
361
|
viewpart: { show: true, persistent: true, level: VIEWPART_LEVEL.TOPMOST, template: html`${dock}` } })
|
|
247
|
-
void dock.show(figure.id)
|
|
362
|
+
void dock.show(figure.id, publicRow ? figure : undefined)
|
|
248
363
|
}
|