@things-factory/figure-ui 10.1.79 → 10.1.82
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-assistant.ts +2 -2
- package/client/graphql/index.ts +99 -10
- package/client/graphql/reached.ts +25 -0
- package/client/graphql/store.ts +27 -4
- package/client/modeller/ai-proposal.ts +74 -9
- package/client/modeller/align.ts +2 -3
- package/client/modeller/canvas.ts +18 -5
- package/client/modeller/command-preview.ts +113 -0
- package/client/modeller/editor.ts +826 -54
- package/client/modeller/fields.ts +12 -4
- package/client/modeller/figure-facts.ts +2 -2
- package/client/modeller/figure-first-save.ts +36 -14
- package/client/modeller/figure-history-panel.ts +133 -14
- package/client/modeller/figure-links.ts +385 -68
- package/client/modeller/figure-parts.ts +56 -2
- package/client/modeller/figure-preview.ts +19 -1
- package/client/modeller/figure-properties.ts +3 -3
- package/client/modeller/figure-revert-choice.ts +49 -0
- package/client/modeller/first-save.ts +16 -0
- package/client/modeller/gizmo.ts +0 -16
- package/client/modeller/links.ts +142 -15
- package/client/modeller/motion-ends.ts +83 -0
- package/client/modeller/occupancy.ts +28 -2
- package/client/modeller/product-session.ts +154 -18
- package/client/modeller/refusals.ts +15 -0
- package/client/modeller/revert-request.ts +26 -0
- package/client/modeller/side-panel-styles.ts +12 -0
- package/client/modeller/stage-model.ts +25 -3
- package/client/pages/figure-modeller-page.ts +68 -29
- package/dist-client/figure-assistant.js +2 -2
- package/dist-client/figure-assistant.js.map +1 -1
- package/dist-client/graphql/index.d.ts +39 -2
- package/dist-client/graphql/index.js +63 -9
- package/dist-client/graphql/index.js.map +1 -1
- package/dist-client/graphql/reached.d.ts +10 -0
- package/dist-client/graphql/reached.js +28 -0
- package/dist-client/graphql/reached.js.map +1 -0
- package/dist-client/graphql/store.js +27 -4
- package/dist-client/graphql/store.js.map +1 -1
- package/dist-client/modeller/ai-proposal.d.ts +31 -2
- package/dist-client/modeller/ai-proposal.js +65 -6
- package/dist-client/modeller/ai-proposal.js.map +1 -1
- package/dist-client/modeller/align.d.ts +1 -1
- package/dist-client/modeller/align.js +0 -2
- package/dist-client/modeller/align.js.map +1 -1
- package/dist-client/modeller/canvas.d.ts +11 -0
- package/dist-client/modeller/canvas.js +33 -4
- package/dist-client/modeller/canvas.js.map +1 -1
- package/dist-client/modeller/clips.d.ts +1 -1
- package/dist-client/modeller/command-preview.d.ts +35 -0
- package/dist-client/modeller/command-preview.js +95 -0
- package/dist-client/modeller/command-preview.js.map +1 -0
- package/dist-client/modeller/editor.d.ts +113 -1
- package/dist-client/modeller/editor.js +850 -50
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/fields.d.ts +3 -2
- package/dist-client/modeller/fields.js +10 -3
- package/dist-client/modeller/fields.js.map +1 -1
- package/dist-client/modeller/figure-facts.js +2 -2
- package/dist-client/modeller/figure-facts.js.map +1 -1
- package/dist-client/modeller/figure-first-save.d.ts +3 -4
- package/dist-client/modeller/figure-first-save.js +41 -14
- package/dist-client/modeller/figure-first-save.js.map +1 -1
- package/dist-client/modeller/figure-history-panel.d.ts +31 -0
- package/dist-client/modeller/figure-history-panel.js +130 -14
- package/dist-client/modeller/figure-history-panel.js.map +1 -1
- package/dist-client/modeller/figure-links.d.ts +51 -1
- package/dist-client/modeller/figure-links.js +392 -72
- package/dist-client/modeller/figure-links.js.map +1 -1
- package/dist-client/modeller/figure-parts.d.ts +7 -0
- package/dist-client/modeller/figure-parts.js +55 -2
- package/dist-client/modeller/figure-parts.js.map +1 -1
- package/dist-client/modeller/figure-preview.d.ts +6 -0
- package/dist-client/modeller/figure-preview.js +22 -1
- package/dist-client/modeller/figure-preview.js.map +1 -1
- package/dist-client/modeller/figure-properties.js +3 -3
- package/dist-client/modeller/figure-properties.js.map +1 -1
- package/dist-client/modeller/figure-revert-choice.d.ts +17 -0
- package/dist-client/modeller/figure-revert-choice.js +59 -0
- package/dist-client/modeller/figure-revert-choice.js.map +1 -0
- package/dist-client/modeller/first-save.d.ts +7 -0
- package/dist-client/modeller/first-save.js +17 -0
- package/dist-client/modeller/first-save.js.map +1 -1
- package/dist-client/modeller/gizmo.d.ts +0 -6
- package/dist-client/modeller/gizmo.js +0 -18
- package/dist-client/modeller/gizmo.js.map +1 -1
- package/dist-client/modeller/links.d.ts +49 -4
- package/dist-client/modeller/links.js +141 -15
- package/dist-client/modeller/links.js.map +1 -1
- package/dist-client/modeller/motion-ends.d.ts +12 -0
- package/dist-client/modeller/motion-ends.js +65 -0
- package/dist-client/modeller/motion-ends.js.map +1 -0
- package/dist-client/modeller/occupancy.d.ts +14 -1
- package/dist-client/modeller/occupancy.js +14 -1
- package/dist-client/modeller/occupancy.js.map +1 -1
- package/dist-client/modeller/product-session.d.ts +43 -2
- package/dist-client/modeller/product-session.js +146 -12
- package/dist-client/modeller/product-session.js.map +1 -1
- package/dist-client/modeller/refusals.d.ts +13 -0
- package/dist-client/modeller/refusals.js +16 -0
- package/dist-client/modeller/refusals.js.map +1 -1
- package/dist-client/modeller/revert-request.d.ts +16 -0
- package/dist-client/modeller/revert-request.js +11 -0
- package/dist-client/modeller/revert-request.js.map +1 -0
- package/dist-client/modeller/side-panel-styles.js +12 -0
- package/dist-client/modeller/side-panel-styles.js.map +1 -1
- package/dist-client/modeller/stage-model.d.ts +18 -2
- package/dist-client/modeller/stage-model.js +21 -3
- package/dist-client/modeller/stage-model.js.map +1 -1
- package/dist-client/pages/figure-modeller-page.d.ts +8 -1
- package/dist-client/pages/figure-modeller-page.js +72 -34
- package/dist-client/pages/figure-modeller-page.js.map +1 -1
- package/dist-client/route.d.ts +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +6 -6
- package/test/ai-proposal.test.ts +2 -2
- package/test/align.test.ts +50 -5
- package/test/browser/acceptance-page.js +187 -0
- package/test/browser/build.mjs +5 -1
- package/test/browser/revert-page.js +47 -0
- package/test/browser/serve-acceptance.mjs +85 -0
- package/test/browser/serve-revert.mjs +119 -0
- package/test/clip-ai-proposal.test.ts +104 -0
- package/test/clip-consent.test.ts +88 -0
- package/test/command-preview.test.ts +288 -0
- package/test/fields.test.ts +4 -3
- package/test/figure-assistant-surface.test.ts +4 -1
- package/test/first-minute.test.ts +96 -0
- package/test/first-save-suggest.test.ts +42 -0
- package/test/gizmo.test.ts +20 -16
- package/test/joint-fan.test.ts +2 -1
- package/test/layout.test.ts +9 -2
- package/test/links.test.ts +7 -4
- package/test/proposal-record.test.ts +126 -0
- package/test/release-entry.test.ts +28 -0
- package/test/release-panel.test.ts +3 -2
- package/test/resize-with-placement.test.ts +225 -0
- package/test/revert-choice.test.ts +39 -0
- package/test/revert-keeping.test.ts +102 -0
- package/test/save-landed.test.ts +198 -0
- package/translations/en.json +222 -10
- package/translations/ja.json +223 -11
- package/translations/ko.json +223 -11
- package/translations/ms.json +222 -10
- package/translations/zh.json +223 -11
|
@@ -14,7 +14,8 @@ export type Axis3 = 'x' | 'y' | 'z'
|
|
|
14
14
|
const AXES: readonly Axis3[] = ['x', 'y', 'z']
|
|
15
15
|
|
|
16
16
|
export type FieldGroup =
|
|
17
|
-
|
|
17
|
+
/* A cell is missing where a rule sets that axis instead of a number: a floor part's height position (ADR-0097). */
|
|
18
|
+
| { kind: 'vector'; name: 'position' | 'rotation' | 'size'; cells: Partial<Record<Axis3, FieldLike>> }
|
|
18
19
|
| { kind: 'single'; name: string; field: FieldLike }
|
|
19
20
|
|
|
20
21
|
/** The name a single value goes by: its id without the part, `dimension.` and corner numbers read as words. */
|
|
@@ -24,9 +25,14 @@ function nameOf(rest: string): string {
|
|
|
24
25
|
|
|
25
26
|
/**
|
|
26
27
|
* The part's own values (`<part>.…`), or with no part the figure's (`size.x` …). Position, rotation and size come
|
|
27
|
-
* first, each
|
|
28
|
+
* first, each as one row when any of its axes is there -- an axis a rule sets shows as held, not as a stray single
|
|
29
|
+
* further down; the rest follow in the order they are declared.
|
|
28
30
|
*/
|
|
29
31
|
export function groupFields(fields: readonly FieldLike[], part?: string): { vectors: FieldGroup[]; singles: FieldGroup[] } {
|
|
32
|
+
/*
|
|
33
|
+
A part is always placed and turned, so its position and turn rows stand even when a rule sets every axis (a
|
|
34
|
+
fastened part): the screen shows the numbers the rule gives. Its size row stands only when it has a field of its own.
|
|
35
|
+
*/
|
|
30
36
|
const prefix = part ? `${part}.` : ''
|
|
31
37
|
const own = fields.filter(f => (part ? f.id.startsWith(prefix) : !f.id.includes('.') || /^size\.[xyz]$/.test(f.id)))
|
|
32
38
|
const byId = new Map(own.map(f => [f.id, f]))
|
|
@@ -34,9 +40,11 @@ export function groupFields(fields: readonly FieldLike[], part?: string): { vect
|
|
|
34
40
|
const vectors: FieldGroup[] = []
|
|
35
41
|
for (const name of ['position', 'rotation', 'size'] as const) {
|
|
36
42
|
const ids = AXES.map(axis => `${prefix}${name}.${axis}`)
|
|
37
|
-
if (!ids.
|
|
43
|
+
if (!ids.some(id => byId.has(id)) && !(part && name !== 'size')) continue
|
|
38
44
|
ids.forEach(id => used.add(id))
|
|
39
|
-
|
|
45
|
+
const cells: Partial<Record<Axis3, FieldLike>> = {}
|
|
46
|
+
AXES.forEach((axis, i) => byId.has(ids[i]!) && (cells[axis] = byId.get(ids[i]!)!))
|
|
47
|
+
vectors.push({ kind: 'vector', name, cells })
|
|
40
48
|
}
|
|
41
49
|
const singles: FieldGroup[] = own.filter(f => !used.has(f.id)).map(field => ({ kind: 'single', name: nameOf(field.id.slice(prefix.length)), field }))
|
|
42
50
|
return { vectors, singles }
|
|
@@ -38,14 +38,14 @@ export function figureFigureFacts(source: string): FigureFigureFacts {
|
|
|
38
38
|
name: asset.name,
|
|
39
39
|
parts,
|
|
40
40
|
declared: !!asset.occupancy,
|
|
41
|
-
placement: asset.
|
|
41
|
+
placement: asset.placement as Placement | undefined,
|
|
42
42
|
mounting: found ? { part: found[1], face: found[2] as FigureFace } : undefined,
|
|
43
43
|
suggested: suggestedMountOf(asset, parts)
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
function suggestedMountOf(asset: FigureAsset, parts: string[]): { part: string; face: FigureFace } | undefined {
|
|
48
|
-
const placement = (asset.
|
|
48
|
+
const placement = (asset.placement ?? 'floor') as Placement
|
|
49
49
|
if (placement === 'center' || !parts.length) return undefined
|
|
50
50
|
let boxes
|
|
51
51
|
try {
|
|
@@ -7,12 +7,15 @@ import { CommonPopupStyles, FormFieldStyles } from '@operato/styles'
|
|
|
7
7
|
import '@material/web/button/filled-button.js'
|
|
8
8
|
import '@material/web/button/text-button.js'
|
|
9
9
|
|
|
10
|
-
import { firstSaveRefusalOf, releasableType, typeOf, type FirstSave } from './first-save.js'
|
|
10
|
+
import { firstSaveRefusalOf, releasableType, suggestTypeOf, typeOf, type FirstSave } from './first-save.js'
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* The first save of a new figure: its type and its name, asked once, in the suite's popup look.
|
|
14
14
|
* Closes with the answer, or with nothing when cancelled -- then nothing is saved.
|
|
15
15
|
*/
|
|
16
|
+
/** The shape of an identifier the modeller offers (`suggestTypeOf`'s random form). */
|
|
17
|
+
const OFFERED = /^FIG_[0-9A-Z]{6}$/
|
|
18
|
+
|
|
16
19
|
@customElement('figure-first-save')
|
|
17
20
|
export class FigureFirstSave extends LitElement {
|
|
18
21
|
static styles = [
|
|
@@ -47,16 +50,41 @@ export class FigureFirstSave extends LitElement {
|
|
|
47
50
|
/** Type names already used in this domain. */
|
|
48
51
|
@property({ attribute: false }) taken: string[] = []
|
|
49
52
|
@state() private refusal: { key: string; type?: string } | null = null
|
|
53
|
+
/** The identifier offered, while the author has not changed it. */
|
|
54
|
+
@state() private suggested = ''
|
|
55
|
+
|
|
56
|
+
/*
|
|
57
|
+
Asked for a name first, with a valid identifier already there to keep or change (Q-D3). The identifier is not the
|
|
58
|
+
name: renaming does not change it, and a retried first save opens on the one answered before (the page keeps it).
|
|
59
|
+
Only an offered one found taken -- told by the server, or in the list -- is offered again; one the author typed is
|
|
60
|
+
kept as typed, and its refusal said.
|
|
61
|
+
*/
|
|
62
|
+
willUpdate(changed: Map<string, unknown>) {
|
|
63
|
+
if (!(changed.has('type') || changed.has('taken'))) return
|
|
64
|
+
const offeredAndTaken = OFFERED.test(this.type) && this.taken.includes(this.type)
|
|
65
|
+
if (!this.type || offeredAndTaken) this.type = this.suggested = suggestTypeOf(this.name, this.taken)
|
|
66
|
+
else if (OFFERED.test(this.type)) this.suggested = this.type
|
|
67
|
+
}
|
|
50
68
|
|
|
51
69
|
render() {
|
|
52
70
|
return html`
|
|
53
71
|
<div class="what">${i18next.t('figure.text.save-new-figure-desc')}</div>
|
|
54
72
|
<div field>
|
|
55
|
-
<label for="
|
|
73
|
+
<label for="name">${i18next.t('figure.label.name')}</label>
|
|
74
|
+
<input
|
|
75
|
+
id="name"
|
|
76
|
+
autofocus
|
|
77
|
+
.value=${this.name}
|
|
78
|
+
placeholder=${i18next.t('figure.text.figure-name-example')}
|
|
79
|
+
@input=${(e: Event) => (this.name = (e.target as HTMLInputElement).value)}
|
|
80
|
+
@keydown=${this.onKey}
|
|
81
|
+
/>
|
|
82
|
+
</div>
|
|
83
|
+
<div field>
|
|
84
|
+
<label for="type">${i18next.t('figure.label.figure-identifier')}</label>
|
|
56
85
|
<input
|
|
57
86
|
id="type"
|
|
58
87
|
type-field
|
|
59
|
-
autofocus
|
|
60
88
|
.value=${this.type}
|
|
61
89
|
placeholder=${i18next.t('figure.text.figure-type-example')}
|
|
62
90
|
@input=${(e: Event) => {
|
|
@@ -66,17 +94,11 @@ export class FigureFirstSave extends LitElement {
|
|
|
66
94
|
}}
|
|
67
95
|
@keydown=${this.onKey}
|
|
68
96
|
/>
|
|
69
|
-
${this.type && !releasableType(this.type)
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
id="name"
|
|
75
|
-
.value=${this.name}
|
|
76
|
-
placeholder=${i18next.t('figure.text.figure-name-example')}
|
|
77
|
-
@input=${(e: Event) => (this.name = (e.target as HTMLInputElement).value)}
|
|
78
|
-
@keydown=${this.onKey}
|
|
79
|
-
/>
|
|
97
|
+
${this.type && !releasableType(this.type)
|
|
98
|
+
? html`<div class="note">${i18next.t('figure.text.type-name-will-not-release')}</div>`
|
|
99
|
+
: this.suggested && this.type === this.suggested
|
|
100
|
+
? html`<div class="note">${i18next.t('figure.text.identifier-suggested')}</div>`
|
|
101
|
+
: nothing}
|
|
80
102
|
</div>
|
|
81
103
|
${this.refusal ? html`<div class="failure" role="alert">${i18next.t(this.refusal.key, { type: this.refusal.type })}</div>` : nothing}
|
|
82
104
|
<ox-popup-actions>
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { openPopup } from '@operato/layout'
|
|
2
|
+
import './figure-revert-choice.js'
|
|
3
|
+
import type { RevertChoice } from './figure-revert-choice.js'
|
|
1
4
|
import '@material/web/icon/icon.js'
|
|
2
5
|
|
|
3
6
|
import { gateFindingStyles, renderGateFinding } from './gate-findings.js'
|
|
@@ -7,7 +10,10 @@ import { customElement, property, state } from 'lit/decorators.js'
|
|
|
7
10
|
import { i18next, localize } from '@operato/i18n'
|
|
8
11
|
import { ScrollbarStyles } from '@operato/styles'
|
|
9
12
|
|
|
10
|
-
import { fetchFigureVersions, inspectFigure, releaseFigure, revertFigureVersion } from '../graphql/index.js'
|
|
13
|
+
import { fetchFigureVersions, fetchReplacedDrafts, inspectFigure, releaseFigure, revertFigureVersion, undoFigureRevert } from '../graphql/index.js'
|
|
14
|
+
import type { KeptDraft } from '../graphql/index.js'
|
|
15
|
+
import { afterRevertAnswer, revertOutcomeUnknown, revertRequestFor } from './revert-request.js'
|
|
16
|
+
import type { RevertRequest } from './revert-request.js'
|
|
11
17
|
import type { FigureGateFinding, FigureInspection, FigureVersion } from '../types.js'
|
|
12
18
|
import { OxPrompt } from '@operato/popup'
|
|
13
19
|
|
|
@@ -29,6 +35,10 @@ import { OxPrompt } from '@operato/popup'
|
|
|
29
35
|
*
|
|
30
36
|
* 옛 판의 정본·속성·그림이 돌아오고 이름·설명은 그대로다. 되살렸다고 그 판이 다시 발행되지는
|
|
31
37
|
* 않는다 — 다시 내보내려면 발행을 한 번 더 하고, 그때 번호는 이미 쓴 다음 번호를 받는다.
|
|
38
|
+
*
|
|
39
|
+
* 덮이는 초안은 서버가 같은 트랜잭션에서 보관하고, 「되돌리기 전 초안」에 남는다(ADR-0097 §5.6, 디자이너 2026-09-27).
|
|
40
|
+
* 발행 판과 섞지 않는다. 다시 접속해도 거기서 찾고, 가장 최근 것은 「되돌리기 취소」로 되살린다 — 되돌린 뒤 모델이
|
|
41
|
+
* 바뀌었으면 서버가 거절하고 보관본은 남는다.
|
|
32
42
|
*/
|
|
33
43
|
@customElement('figure-history-panel')
|
|
34
44
|
export class FigureHistoryPanel extends localize(i18next)(LitElement) {
|
|
@@ -209,6 +219,10 @@ export class FigureHistoryPanel extends localize(i18next)(LitElement) {
|
|
|
209
219
|
@property({ type: Number }) figureVersion = 0
|
|
210
220
|
/** 저장하지 않은 것이 있나 — 있으면 발행을 막는다. */
|
|
211
221
|
@property({ type: Boolean }) dirty = false
|
|
222
|
+
/** Saves the edits in hand before reverting; `ok` only when the save is known to have happened. */
|
|
223
|
+
@property({ attribute: false }) saveFirst?: () => Promise<{ ok: boolean; reason?: string }>
|
|
224
|
+
/** The source the server is confirmed to hold: a revert goes on only while the server still holds it. */
|
|
225
|
+
@property({ attribute: false }) serverSource?: () => string | undefined
|
|
212
226
|
/**
|
|
213
227
|
* 행의 타입. 판정 하나(`type-is-placeholder`)가 이것을 본다 — 자산은 타입을 원본에 적지 않아서, 타입을 바꾸면
|
|
214
228
|
* 원본도 dirty 도 그대로다. 그래서 이 값이 바뀌면 판정을 다시 묻는다. 안 그러면 타입을 정한 뒤에도 「임시 타입」
|
|
@@ -217,6 +231,13 @@ export class FigureHistoryPanel extends localize(i18next)(LitElement) {
|
|
|
217
231
|
@property({ type: String }) figureType?: string
|
|
218
232
|
|
|
219
233
|
@state() private versions: FigureVersion[] = []
|
|
234
|
+
/** Drafts reverts kept, newest first -- apart from the released versions. */
|
|
235
|
+
@state() private kept: KeptDraft[] = []
|
|
236
|
+
/**
|
|
237
|
+
* A revert or undo whose answer never came back. Asked again, it goes with the same request id, so the server
|
|
238
|
+
* answers with what it did rather than doing it twice. Any answer -- done or refused -- lets it go.
|
|
239
|
+
*/
|
|
240
|
+
private pending?: RevertRequest
|
|
220
241
|
@state() private busy = false
|
|
221
242
|
@state() private failure = ''
|
|
222
243
|
/**
|
|
@@ -257,20 +278,29 @@ export class FigureHistoryPanel extends localize(i18next)(LitElement) {
|
|
|
257
278
|
private async reload() {
|
|
258
279
|
if (!this.figureId) {
|
|
259
280
|
this.versions = []
|
|
281
|
+
this.kept = []
|
|
260
282
|
return
|
|
261
283
|
}
|
|
262
284
|
|
|
263
285
|
try {
|
|
264
|
-
this.versions = await fetchFigureVersions(this.figureId)
|
|
286
|
+
;[this.versions, this.kept] = await Promise.all([fetchFigureVersions(this.figureId), fetchReplacedDrafts(this.figureId)])
|
|
265
287
|
this.failure = ''
|
|
266
288
|
} catch (e) {
|
|
267
289
|
this.failure = (e as Error).message
|
|
268
290
|
}
|
|
269
291
|
}
|
|
270
292
|
|
|
293
|
+
/** The release section in view, focus on it: where the top bar's "release" leads (ADR-0097 §5.6, Q-D2). */
|
|
294
|
+
async showRelease() {
|
|
295
|
+
await this.updateComplete
|
|
296
|
+
const section = this.renderRoot.querySelector<HTMLElement>('section[release]')
|
|
297
|
+
section?.scrollIntoView({ block: 'start', behavior: 'smooth' })
|
|
298
|
+
;(section?.querySelector<HTMLElement>('input[note]:not([disabled]), button[release]:not([disabled])') ?? section)?.focus({ preventScroll: true })
|
|
299
|
+
}
|
|
300
|
+
|
|
271
301
|
render() {
|
|
272
302
|
return html`
|
|
273
|
-
<section>
|
|
303
|
+
<section release tabindex="-1">
|
|
274
304
|
<h3>${i18next.t('figure.label.release')}</h3>
|
|
275
305
|
${this.figureId ? this.renderRelease() : html`<p quiet>${i18next.t('figure.text.save-the-figure-before-releasing')}</p>`}
|
|
276
306
|
</section>
|
|
@@ -283,6 +313,15 @@ export class FigureHistoryPanel extends localize(i18next)(LitElement) {
|
|
|
283
313
|
</ul>`
|
|
284
314
|
: html`<p quiet>${i18next.t('figure.text.nothing-released-yet')}</p>`}
|
|
285
315
|
</section>
|
|
316
|
+
|
|
317
|
+
${this.kept.length
|
|
318
|
+
? html`<section kept>
|
|
319
|
+
<h3>${i18next.t('figure.label.kept-drafts')}</h3>
|
|
320
|
+
<ul>
|
|
321
|
+
${this.kept.map((kept, index) => this.renderKept(kept, index === 0))}
|
|
322
|
+
</ul>
|
|
323
|
+
</section>`
|
|
324
|
+
: nothing}
|
|
286
325
|
`
|
|
287
326
|
}
|
|
288
327
|
|
|
@@ -397,6 +436,31 @@ export class FigureHistoryPanel extends localize(i18next)(LitElement) {
|
|
|
397
436
|
`
|
|
398
437
|
}
|
|
399
438
|
|
|
439
|
+
/*
|
|
440
|
+
Only the newest can be taken back: an undo holds only while the figure is what that revert wrote, and a later
|
|
441
|
+
revert or undo has written something else since. The older ones stay listed -- found, not offered.
|
|
442
|
+
*/
|
|
443
|
+
private renderKept(kept: KeptDraft, newest: boolean) {
|
|
444
|
+
const what =
|
|
445
|
+
kept.kind === 'undo'
|
|
446
|
+
? i18next.t('figure.text.kept-before-undo')
|
|
447
|
+
: i18next.t('figure.text.kept-before-revert', { version: String(kept.revertedToVersion ?? '') })
|
|
448
|
+
return html`
|
|
449
|
+
<li>
|
|
450
|
+
<span no>${what}</span>
|
|
451
|
+
<span when>
|
|
452
|
+
${kept.createdAt ? new Date(kept.createdAt).toLocaleString() : ''} ${kept.creator?.name ? ` · ${kept.creator.name}` : ''}
|
|
453
|
+
${kept.restoredAt ? ` · ${i18next.t('figure.text.kept-restored')}` : ''}
|
|
454
|
+
</span>
|
|
455
|
+
${newest && !kept.restoredAt
|
|
456
|
+
? html`<button revert undo ?disabled=${this.busy} @click=${() => this.undo(kept)}>
|
|
457
|
+
${i18next.t(kept.kind === 'undo' ? 'figure.button.redo-revert' : 'figure.button.undo-revert')}
|
|
458
|
+
</button>`
|
|
459
|
+
: nothing}
|
|
460
|
+
</li>
|
|
461
|
+
`
|
|
462
|
+
}
|
|
463
|
+
|
|
400
464
|
private async release() {
|
|
401
465
|
const input = this.renderRoot.querySelector('input[note]') as HTMLInputElement
|
|
402
466
|
const comment = input?.value?.trim()
|
|
@@ -418,24 +482,71 @@ export class FigureHistoryPanel extends localize(i18next)(LitElement) {
|
|
|
418
482
|
}
|
|
419
483
|
}
|
|
420
484
|
|
|
485
|
+
/**
|
|
486
|
+
* Edits not saved yet: saved first, let go, or kept -- asked with the three (designer 2026-09-27, Q-D1). Reverting and
|
|
487
|
+
* taking a revert back both reopen the figure from the server, so edits not saved would go without a word. `true` to
|
|
488
|
+
* go on.
|
|
489
|
+
*/
|
|
490
|
+
private async settleUnsaved(purpose: 'revert' | 'undo', version = 0): Promise<boolean> {
|
|
491
|
+
const choice = await openPopup<RevertChoice>(
|
|
492
|
+
html`<figure-revert-choice .version=${version} .purpose=${purpose}></figure-revert-choice>`,
|
|
493
|
+
{ backdrop: true, size: 'small', title: i18next.t(purpose === 'undo' ? 'figure.button.undo-revert' : 'figure.button.revert') }
|
|
494
|
+
).result
|
|
495
|
+
if (!choice) return false
|
|
496
|
+
if (choice === 'save') {
|
|
497
|
+
/* Saved, and known to be: a save that failed or whose outcome is not known goes no further. */
|
|
498
|
+
const saved = await this.saveFirst?.()
|
|
499
|
+
if (!saved?.ok) {
|
|
500
|
+
this.failure = i18next.t('figure.text.revert-held-unsaved', { reason: saved?.reason ?? '' })
|
|
501
|
+
return false
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
return true
|
|
505
|
+
}
|
|
506
|
+
|
|
421
507
|
private async revert(version: number) {
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
508
|
+
this.failure = ''
|
|
509
|
+
if (this.dirty) {
|
|
510
|
+
if (!(await this.settleUnsaved('revert', version))) return
|
|
511
|
+
} else {
|
|
512
|
+
/* 지금 초안은 서버가 보관하지만, 화면의 것이 그 판으로 바뀐다 — 이름으로 확인한다. */
|
|
513
|
+
const agreed = await OxPrompt.open({
|
|
514
|
+
type: 'warning',
|
|
515
|
+
title: i18next.t('figure.button.revert'),
|
|
516
|
+
text: i18next.t('figure.text.revert-to-version-confirm', { version }),
|
|
517
|
+
confirmButton: { text: i18next.t('figure.button.revert') },
|
|
518
|
+
cancelButton: { text: i18next.t('figure.button.keep-editing') }
|
|
519
|
+
})
|
|
520
|
+
if (!agreed) {
|
|
521
|
+
return
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/* Read after any save above: what the server is now confirmed to hold. */
|
|
526
|
+
const expected = this.serverSource?.()
|
|
527
|
+
if (expected === undefined) {
|
|
528
|
+
this.failure = i18next.t('figure.text.revert-needs-server-source')
|
|
431
529
|
return
|
|
432
530
|
}
|
|
531
|
+
await this.send(`revert:${version}:${expected}`, requestId => revertFigureVersion(this.figureId!, version, requestId, expected))
|
|
532
|
+
}
|
|
433
533
|
|
|
534
|
+
private async undo(kept: KeptDraft) {
|
|
535
|
+
this.failure = ''
|
|
536
|
+
if (this.dirty && !(await this.settleUnsaved('undo'))) return
|
|
537
|
+
await this.send(`undo:${kept.id}`, requestId => undoFigureRevert(kept.id, requestId))
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/** One revert or undo: a new request id, or the one whose answer was lost when the same is asked again. */
|
|
541
|
+
private async send(key: string, request: (requestId: string) => Promise<unknown>) {
|
|
542
|
+
const sent = revertRequestFor(this.pending, key)
|
|
543
|
+
this.pending = sent
|
|
434
544
|
this.busy = true
|
|
435
545
|
this.failure = ''
|
|
436
546
|
|
|
437
547
|
try {
|
|
438
|
-
const result = await
|
|
548
|
+
const result = await request(sent.requestId)
|
|
549
|
+
this.pending = undefined
|
|
439
550
|
this.dispatchEvent(new CustomEvent('figure-reverted', { detail: result, bubbles: true, composed: true }))
|
|
440
551
|
await this.reload()
|
|
441
552
|
/*
|
|
@@ -445,7 +556,15 @@ export class FigureHistoryPanel extends localize(i18next)(LitElement) {
|
|
|
445
556
|
*/
|
|
446
557
|
await this.inspect()
|
|
447
558
|
} catch (e) {
|
|
448
|
-
|
|
559
|
+
const code = (e as { code?: string })?.code
|
|
560
|
+
/* Not known whether it happened: kept, so the next press asks with the same id. Anything the server said: let go. */
|
|
561
|
+
this.pending = afterRevertAnswer(sent, code)
|
|
562
|
+
if (revertOutcomeUnknown(code)) this.failure = i18next.t('figure.text.revert-unconfirmed')
|
|
563
|
+
else {
|
|
564
|
+
/* Refused: the kept list may have changed under us (another session's undo), so it is read again. */
|
|
565
|
+
await this.reload()
|
|
566
|
+
this.failure = (e as Error).message
|
|
567
|
+
}
|
|
449
568
|
} finally {
|
|
450
569
|
this.busy = false
|
|
451
570
|
}
|