@things-factory/figure-ui 10.1.224 → 10.1.225
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/modeller/editor-styles.ts +3 -2
- package/client/modeller/editor.ts +3 -1
- package/client/modeller/figure-capability-panel.ts +6 -0
- package/client/modeller/figure-clips.ts +1 -1
- package/client/modeller/figure-data-panel.ts +17 -14
- package/client/modeller/figure-preview.ts +46 -6
- package/client/modeller/product-session.ts +3 -2
- package/client/pages/figure-modeller-page.ts +2 -2
- package/dist-client/modeller/editor-styles.js +3 -2
- package/dist-client/modeller/editor-styles.js.map +1 -1
- package/dist-client/modeller/editor.js +5 -3
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/figure-capability-panel.js +6 -0
- package/dist-client/modeller/figure-capability-panel.js.map +1 -1
- package/dist-client/modeller/figure-clips.js +1 -1
- package/dist-client/modeller/figure-clips.js.map +1 -1
- package/dist-client/modeller/figure-data-panel.js +18 -14
- package/dist-client/modeller/figure-data-panel.js.map +1 -1
- package/dist-client/modeller/figure-preview.js +46 -6
- package/dist-client/modeller/figure-preview.js.map +1 -1
- package/dist-client/modeller/product-session.d.ts +4 -2
- package/dist-client/modeller/product-session.js +3 -2
- package/dist-client/modeller/product-session.js.map +1 -1
- package/dist-client/pages/figure-modeller-page.js +2 -2
- package/dist-client/pages/figure-modeller-page.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/test/ai-proposal.test.ts +2 -2
- package/test/browser/product-page.check.mjs +25 -0
- package/test/browser/side-panel-sweep.check.mjs +182 -0
- package/test/figure-assistant-surface.test.ts +1 -1
- package/test/first-save.test.ts +2 -2
- package/test/product-session.test.ts +5 -5
- package/test/proposal-record.test.ts +3 -3
- package/test/revert-keeping.test.ts +1 -1
- package/translations/en.json +1 -0
- package/translations/es.json +1 -0
- package/translations/ja.json +1 -0
- package/translations/ko.json +1 -0
- package/translations/ms.json +1 -0
- package/translations/zh.json +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@things-factory/figure-ui",
|
|
3
|
-
"version": "10.1.
|
|
3
|
+
"version": "10.1.225",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public",
|
|
6
6
|
"@things-factory:registry": "https://registry.npmjs.org"
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"scripts": {
|
|
19
19
|
"build": "npm run build:server && npm run build:client",
|
|
20
20
|
"test": "FIGURE_FREEZE_COMPILED=1 node --import ./test/resolve-ts-siblings.mjs --test test/*.test.ts",
|
|
21
|
-
"test:browser": "node --test test/browser/build.check.mjs test/browser/modeller.check.mjs test/browser/modeller-revision.check.mjs test/browser/editing.check.mjs test/browser/links.check.mjs test/browser/section.check.mjs test/browser/screen.check.mjs test/browser/product-page.check.mjs test/browser/parts.check.mjs test/browser/places-mesh.check.mjs test/browser/part-ids.check.mjs test/browser/fix-alignment.check.mjs test/browser/mesh-select.check.mjs test/browser/row-preview.check.mjs test/browser/detail-tabs.check.mjs test/browser/props.check.mjs test/browser/retire.check.mjs test/browser/namespace.check.mjs test/browser/public.check.mjs test/browser/category.check.mjs test/browser/property-section.check.mjs test/browser/list.check.mjs test/browser/item.check.mjs",
|
|
21
|
+
"test:browser": "node --test test/browser/build.check.mjs test/browser/modeller.check.mjs test/browser/modeller-revision.check.mjs test/browser/editing.check.mjs test/browser/links.check.mjs test/browser/section.check.mjs test/browser/screen.check.mjs test/browser/product-page.check.mjs test/browser/parts.check.mjs test/browser/places-mesh.check.mjs test/browser/part-ids.check.mjs test/browser/fix-alignment.check.mjs test/browser/mesh-select.check.mjs test/browser/row-preview.check.mjs test/browser/detail-tabs.check.mjs test/browser/props.check.mjs test/browser/retire.check.mjs test/browser/namespace.check.mjs test/browser/public.check.mjs test/browser/category.check.mjs test/browser/property-section.check.mjs test/browser/list.check.mjs test/browser/item.check.mjs test/browser/side-panel-sweep.check.mjs",
|
|
22
22
|
"copy:files": "copyfiles -e \"./client/**/*.{ts,js,json}\" -u 1 \"./client/**/*\" dist-client",
|
|
23
23
|
"build:client": "npm run copy:files && npx tsc --p ./client/tsconfig.json",
|
|
24
24
|
"build:server": "npx tsc --p ./server/tsconfig.json",
|
|
@@ -48,5 +48,5 @@
|
|
|
48
48
|
"@things-factory/setting-base": "^10.1.223",
|
|
49
49
|
"three": "^0.185.1"
|
|
50
50
|
},
|
|
51
|
-
"gitHead": "
|
|
51
|
+
"gitHead": "9929ca76f614ff84bea1f61dabe0e1b7f00cd18a"
|
|
52
52
|
}
|
package/test/ai-proposal.test.ts
CHANGED
|
@@ -157,7 +157,7 @@ test('the same new figure\'s card in another tab before the first save makes a s
|
|
|
157
157
|
})
|
|
158
158
|
const made = [{ kind: 'add-part', part: { id: 'lid', kind: 'box', dimensions: { width: 100, height: 10, depth: 100 } } }] as any[]
|
|
159
159
|
const first = tab()
|
|
160
|
-
first.startNew()
|
|
160
|
+
first.startNew({ name: 'New' })
|
|
161
161
|
const detail: FigureApplyDetail = { key: K, steps: made, create: true }
|
|
162
162
|
const here = { ...taker({}), offer: (d: FigureApplyDetail) => (takeFigureOffer(first, d, { onSavedFigure: false, say }), d) }
|
|
163
163
|
assert.equal((await runFigureProposal(detail, here, say)).verdict, 'accepted')
|
|
@@ -166,7 +166,7 @@ test('the same new figure\'s card in another tab before the first save makes a s
|
|
|
166
166
|
const there = {
|
|
167
167
|
...taker({}),
|
|
168
168
|
async openNew(s: FigureProposalStep[], key?: string) {
|
|
169
|
-
second.startNew()
|
|
169
|
+
second.startNew({ name: 'New' })
|
|
170
170
|
second.createUnder(key!)
|
|
171
171
|
second.applyProposal(s, second.read().revision!, second.read().context!, key)
|
|
172
172
|
return { applied: true }
|
|
@@ -173,6 +173,11 @@ describe('the first save of a new figure', () => {
|
|
|
173
173
|
return { first: popup.shadowRoot.querySelector('input')?.id, type: popup.shadowRoot.querySelector('#type').value, focused: active?.id }
|
|
174
174
|
})
|
|
175
175
|
|
|
176
|
+
test('a new figure is named in the screen\'s words, the tab too (sweep 2026-10-27 U8)', async () => {
|
|
177
|
+
assert.equal(await read(() => product.page().session.read().name), 'figure.text.new-figure-name')
|
|
178
|
+
assert.equal(await read(() => product.page().context.title), 'figure.text.new-figure-name')
|
|
179
|
+
})
|
|
180
|
+
|
|
176
181
|
test('before it is saved: the type can be written, held by the page, and the release waits', async () => {
|
|
177
182
|
const field = await read(() => { const input = product.page().shadowRoot.querySelector('label[type-field] input'); return input && { disabled: input.disabled } })
|
|
178
183
|
assert.deepEqual(field, { disabled: false })
|
|
@@ -242,6 +247,22 @@ describe('opening another figure', () => {
|
|
|
242
247
|
})
|
|
243
248
|
})
|
|
244
249
|
|
|
250
|
+
test('nothing open on the page: its properties say nothing of an empty figure (sweep 2026-10-27 U3)', async () => {
|
|
251
|
+
await open('a')
|
|
252
|
+
const empty = await read(async () => {
|
|
253
|
+
const page = product.page()
|
|
254
|
+
/* The state a page is in before it is told which figure, and after it is left: the session holds none. */
|
|
255
|
+
page.session.close()
|
|
256
|
+
const editor = product.deep('figure-editor')[0]
|
|
257
|
+
editor.requestUpdate()
|
|
258
|
+
await editor.updateComplete
|
|
259
|
+
return editor.shadowRoot.querySelectorAll('section[empty-figure]').length
|
|
260
|
+
})
|
|
261
|
+
/* Told another figure, the page opens it again: the checks after this one start from an open page. */
|
|
262
|
+
await open('b')
|
|
263
|
+
assert.equal(empty, 0)
|
|
264
|
+
})
|
|
265
|
+
|
|
245
266
|
test('the row is emptied first: the type of the one before does not stand while the next one loads', async () => {
|
|
246
267
|
const typeField = () => read(() => product.page().shadowRoot.querySelector('label[type-field] input')?.value)
|
|
247
268
|
await open('a')
|
|
@@ -258,6 +279,10 @@ describe('opening another figure', () => {
|
|
|
258
279
|
await until(() => !!window.letB, 'b asked for')
|
|
259
280
|
await read(() => product.page().updateComplete)
|
|
260
281
|
assert.equal(await typeField(), '')
|
|
282
|
+
/* Still loading, the figure is not an empty one: no 「add a part」 (sweep 2026-10-27 U3). */
|
|
283
|
+
await read(() => product.deep('figure-editor')[0].updateComplete)
|
|
284
|
+
assert.equal(await read(() => product.deep('figure-editor >>> section[empty-figure]').length), 0)
|
|
285
|
+
assert.equal(await read(() => product.page().context.title), 'figure.label.figure')
|
|
261
286
|
await read(() => window.letB())
|
|
262
287
|
await until(() => product.page().shadowRoot.querySelector('label[type-field] input')?.value === 'type-b', 'b\'s type')
|
|
263
288
|
})
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* What the modeller sweep of 2026-10-27 found on the side panel and the preview, in a real browser
|
|
3
|
+
* (operato-figure docs/audits/modeller-ux-sweep-2026-10-27.md, U1 U2 U4 U5 U6 U7): each as a person would meet it -- a
|
|
4
|
+
* box that reads as a box, a heading nothing is drawn over, a delete that looks like one, the empty animation said in
|
|
5
|
+
* its own words, a field added by Enter, the preview's values starting in one column and its help in the bar's look.
|
|
6
|
+
* The page loads no translations, so the words read are i18n keys.
|
|
7
|
+
*
|
|
8
|
+
* Run: node --test test/browser/side-panel-sweep.check.mjs
|
|
9
|
+
*/
|
|
10
|
+
import { test, describe, before, after } from 'node:test'
|
|
11
|
+
import assert from 'node:assert/strict'
|
|
12
|
+
|
|
13
|
+
import { addFigureBox, createFigureAsset, serializeFigureAsset } from '@hatiolab/figure-model'
|
|
14
|
+
|
|
15
|
+
import { buildPage } from './build.mjs'
|
|
16
|
+
import { openPage } from './serve.mjs'
|
|
17
|
+
|
|
18
|
+
const sleep = ms => new Promise(done => setTimeout(done, ms))
|
|
19
|
+
|
|
20
|
+
let opened
|
|
21
|
+
before(async () => {
|
|
22
|
+
opened = await openPage(await buildPage('links-page.js'))
|
|
23
|
+
})
|
|
24
|
+
after(async () => {
|
|
25
|
+
await opened?.close()
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
const page = () => opened.page
|
|
29
|
+
const read = (fn, ...args) => page().evaluate(fn, ...args)
|
|
30
|
+
const source = () => read(() => probe.source())
|
|
31
|
+
const settled = () => read(() => document.querySelector('figure-editor').updateComplete)
|
|
32
|
+
|
|
33
|
+
async function until(fn, args, what, ms = 8000) {
|
|
34
|
+
const deadline = Date.now() + ms
|
|
35
|
+
for (;;) {
|
|
36
|
+
const got = await read(fn, ...args)
|
|
37
|
+
if (got) return got
|
|
38
|
+
if (Date.now() > deadline) assert.fail(`waited for ${what}`)
|
|
39
|
+
await sleep(50)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async function mount(kind, text = null) {
|
|
43
|
+
await read((kind, text) => harness.mount(kind, text), kind, text)
|
|
44
|
+
await until(
|
|
45
|
+
() => {
|
|
46
|
+
try {
|
|
47
|
+
return probe.ids().some(id => !!probe.partOnPage(id))
|
|
48
|
+
} catch {
|
|
49
|
+
return false
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
[],
|
|
53
|
+
'the figure drawn in 3D',
|
|
54
|
+
15000
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
async function click(point, what) {
|
|
58
|
+
assert.ok(point, `${what} is on the page to click`)
|
|
59
|
+
await page().mouse.click(point.x, point.y)
|
|
60
|
+
await settled()
|
|
61
|
+
}
|
|
62
|
+
const tab = name => read(l => probe.tab(l), `figure.label.tab-short-${name}`)
|
|
63
|
+
|
|
64
|
+
/** Every element matching the selector through every shadow root, shown on the page. */
|
|
65
|
+
const DEEP = `(selector => {
|
|
66
|
+
const deep = (root = document) => [...root.querySelectorAll(selector)].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
|
|
67
|
+
return deep().filter(e => e.getBoundingClientRect().height > 0)
|
|
68
|
+
})`
|
|
69
|
+
|
|
70
|
+
describe('the side panel', () => {
|
|
71
|
+
before(() => mount('box'))
|
|
72
|
+
|
|
73
|
+
test('U1 the capability tab: the item box keeps its size beside its long note', async () => {
|
|
74
|
+
await click(await tab('capability'), 'the capability tab')
|
|
75
|
+
/* The note as Korean reads it: words that wrap (the key alone is one word that never does). */
|
|
76
|
+
const width = await read(DEEP => {
|
|
77
|
+
eval(DEEP)('label[item] em')[0].textContent = '홀더가 자리에 앉혀 나르는 물건의 모양입니다. 릴 · 트레이 · 매거진. 아이템 타입 표에서 이 피규어를 고를 수 있습니다.'
|
|
78
|
+
return eval(DEEP)('label[item] > input')[0]?.getBoundingClientRect().width
|
|
79
|
+
}, DEEP)
|
|
80
|
+
assert.ok(width >= 12, `the item box is ${width}px wide`)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test('U2 scrolled under the heading, the places chips are drawn below it, not over it', async () => {
|
|
84
|
+
/* A short window, so the panel scrolls. */
|
|
85
|
+
await page().setViewport({ width: 1400, height: 520 })
|
|
86
|
+
await click(await read(id => probe.row(id), 'box'), 'the box in the parts list')
|
|
87
|
+
await click(await tab('capability'), 'the capability tab')
|
|
88
|
+
const hit = await read(DEEP => {
|
|
89
|
+
const chips = eval(DEEP)('ox-input-select-buttons[places-by]')[0]
|
|
90
|
+
const subject = eval(DEEP)('div[subject]')[0]
|
|
91
|
+
if (!chips || !subject) return 'no chips or no heading'
|
|
92
|
+
let scroller = chips.parentNode
|
|
93
|
+
for (;;) {
|
|
94
|
+
if (!scroller) return 'nothing scrolls'
|
|
95
|
+
if (scroller instanceof ShadowRoot) scroller = scroller.host
|
|
96
|
+
else if (scroller instanceof Element && /auto|scroll/.test(getComputedStyle(scroller).overflowY) && scroller.scrollHeight > scroller.clientHeight) break
|
|
97
|
+
else scroller = scroller.parentNode
|
|
98
|
+
}
|
|
99
|
+
const head = subject.getBoundingClientRect()
|
|
100
|
+
const before = chips.getBoundingClientRect()
|
|
101
|
+
scroller.scrollTop += before.top - (head.top + head.height / 2) + 4
|
|
102
|
+
const at = chips.getBoundingClientRect()
|
|
103
|
+
const x = at.left + Math.min(40, at.width / 2),
|
|
104
|
+
y = at.top + 6
|
|
105
|
+
if (y > head.bottom || y < head.top) return `the chips stand at ${Math.round(y)}, the heading ${Math.round(head.top)}..${Math.round(head.bottom)}`
|
|
106
|
+
let el = document.elementFromPoint(x, y)
|
|
107
|
+
while (el?.shadowRoot) {
|
|
108
|
+
const inner = el.shadowRoot.elementFromPoint(x, y)
|
|
109
|
+
if (!inner || inner === el) break
|
|
110
|
+
el = inner
|
|
111
|
+
}
|
|
112
|
+
for (let n = el; n; n = n.parentNode instanceof ShadowRoot ? n.parentNode.host : n.parentNode) if (n === subject) return 'heading'
|
|
113
|
+
return el?.tagName.toLowerCase()
|
|
114
|
+
}, DEEP)
|
|
115
|
+
await page().setViewport({ width: 1400, height: 900 })
|
|
116
|
+
assert.equal(hit, 'heading')
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
test('U5 the motion tab: no animation yet is said in its own words, the controls\' words said once', async () => {
|
|
120
|
+
await click(await tab('motion'), 'the motion tab')
|
|
121
|
+
const hints = await read(DEEP => eval(DEEP)('p[hint]').map(p => p.textContent.trim()), DEEP)
|
|
122
|
+
assert.ok(hints.includes('figure.text.no-clips'), hints.join(' | '))
|
|
123
|
+
assert.equal(hints.filter(h => h === 'figure.text.no-controls').length, 1)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
test('U6 the data tab: a field name and Enter adds the field', async () => {
|
|
127
|
+
await click(await tab('data'), 'the data tab')
|
|
128
|
+
await click(await read(() => look.at('input[aria-label="figure.label.data-name"]')), 'the field name')
|
|
129
|
+
await page().keyboard.type('speed')
|
|
130
|
+
await page().keyboard.press('Enter')
|
|
131
|
+
await settled()
|
|
132
|
+
await until(() => probe.source().dataInterface?.schema?.some(f => f.name === 'speed'), [], 'the field added')
|
|
133
|
+
assert.equal((await read(DEEP => eval(DEEP)('input[aria-label="figure.label.data-name"]')[0].value, DEEP)), '')
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
test('U4 a field opened: its delete is the bin, not a cross that reads as close', async () => {
|
|
137
|
+
await click(await read(() => look.at('button', 'speed')), 'the speed field')
|
|
138
|
+
const icon = await until(DEEP => eval(DEEP)('button[title="figure.button.remove-data-field"] md-icon')[0]?.textContent.trim(), [DEEP], 'the field opened')
|
|
139
|
+
assert.equal(icon, 'delete')
|
|
140
|
+
assert.ok((await source()).dataInterface.schema.some(f => f.name === 'speed'))
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
describe('the preview', () => {
|
|
145
|
+
/* A number and a text, of names far apart in length, each read by a table: the preview gives each a field. */
|
|
146
|
+
const asset = addFigureBox(createFigureAsset('Probe'), { id: 'box', size: { x: 400, y: 400, z: 400 }, position: { x: 0, y: 200, z: 0 } })
|
|
147
|
+
asset.dataInterface = {
|
|
148
|
+
version: 'figure-data-interface-2',
|
|
149
|
+
schema: [
|
|
150
|
+
{ name: 'A', kind: 'number' },
|
|
151
|
+
{ name: 'CARRIERSTATUS', kind: 'text' }
|
|
152
|
+
],
|
|
153
|
+
bindings: [
|
|
154
|
+
{ id: 'a', accessor: 'A', rule: 'table', lookup: 'ranges', targets: { parts: ['box'] }, table: { '~10': { color: '#ff0000' }, default: { color: '#0000ff' } } },
|
|
155
|
+
{ id: 's', accessor: 'CARRIERSTATUS', rule: 'table', lookup: 'code', targets: { parts: ['box'] }, table: { X: { color: '#00ff00' } } }
|
|
156
|
+
]
|
|
157
|
+
}
|
|
158
|
+
before(async () => {
|
|
159
|
+
await mount('box', serializeFigureAsset(asset))
|
|
160
|
+
await click(await read(() => probe.modeButton('preview')), 'the preview')
|
|
161
|
+
await until(DEEP => eval(DEEP)('figure-preview')[0]?.shadowRoot?.querySelectorAll('div[clips] input').length === 2, [DEEP], 'the two values in the preview')
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
test('U7 a number\'s field and a text\'s start in one column, whatever the names\' lengths', async () => {
|
|
165
|
+
const lefts = await read(DEEP => [...eval(DEEP)('figure-preview')[0].shadowRoot.querySelectorAll('div[clips] input')].map(i => Math.round(i.getBoundingClientRect().left)), DEEP)
|
|
166
|
+
assert.equal(lefts[0], lefts[1], `the fields start at ${lefts.join(' and ')}`)
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
test('U7 the size help\'s choice and button are in the bar\'s look, not the browser\'s grey', async () => {
|
|
170
|
+
const looks = await read(DEEP => {
|
|
171
|
+
const root = eval(DEEP)('figure-preview')[0].shadowRoot
|
|
172
|
+
return ['div[setup] select', 'div[setup] button', 'div[clips] input'].map(s => {
|
|
173
|
+
const c = getComputedStyle(root.querySelector(s))
|
|
174
|
+
return { radius: c.borderTopLeftRadius, background: c.backgroundColor }
|
|
175
|
+
})
|
|
176
|
+
}, DEEP)
|
|
177
|
+
assert.deepEqual(looks[0], looks[1])
|
|
178
|
+
assert.deepEqual(looks[0], looks[2])
|
|
179
|
+
assert.equal(looks[0].radius, '5px')
|
|
180
|
+
assert.notEqual(looks[0].background, 'rgb(239, 239, 239)')
|
|
181
|
+
})
|
|
182
|
+
})
|
|
@@ -95,7 +95,7 @@ test('a new figure keeps its creation id through a new figure\'s proposal and th
|
|
|
95
95
|
return { id: q.id ?? q.creationId!, source: q.source }
|
|
96
96
|
}
|
|
97
97
|
})
|
|
98
|
-
session.startNew()
|
|
98
|
+
session.startNew({ name: 'New' })
|
|
99
99
|
const creationId = session.read().creationId
|
|
100
100
|
assert.match(creationId ?? '', /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/)
|
|
101
101
|
const create: FigureApplyDetail = { key: K1, steps: lid, create: true }
|
package/test/first-save.test.ts
CHANGED
|
@@ -47,7 +47,7 @@ function recording() {
|
|
|
47
47
|
test('타입은 새 피규어를 만드는 저장에만 실린다 — 이미 있는 피규어의 저장은 타입을 보내지 않는다', async () => {
|
|
48
48
|
const { store, requests } = recording()
|
|
49
49
|
const session = createFigureProductSession(store)
|
|
50
|
-
session.startNew()
|
|
50
|
+
session.startNew({ name: 'New' })
|
|
51
51
|
await session.save({ type: 'agv-lifter' })
|
|
52
52
|
assert.equal(requests[0]!.type, 'agv-lifter')
|
|
53
53
|
assert.equal(requests[0]!.id, undefined, '만드는 저장')
|
|
@@ -61,7 +61,7 @@ test('타입은 새 피규어를 만드는 저장에만 실린다 — 이미 있
|
|
|
61
61
|
test('타입을 주지 않으면 싣지 않는다 — 서버가 임시 이름을 붙인다', async () => {
|
|
62
62
|
const { store, requests } = recording()
|
|
63
63
|
const session = createFigureProductSession(store)
|
|
64
|
-
session.startNew()
|
|
64
|
+
session.startNew({ name: 'New' })
|
|
65
65
|
await session.save()
|
|
66
66
|
assert.equal('type' in requests[0]!, false)
|
|
67
67
|
})
|
|
@@ -28,7 +28,7 @@ test('existing model -> new clears geometry, ID, history and derived state', asy
|
|
|
28
28
|
const s = createFigureProductSession(store)
|
|
29
29
|
await s.open('A')
|
|
30
30
|
assert.equal(s.read().geometry.length, 1)
|
|
31
|
-
const v = s.startNew()
|
|
31
|
+
const v = s.startNew({ name: 'New' })
|
|
32
32
|
assert.equal(v.id, undefined)
|
|
33
33
|
assert.equal(v.geometry.length, 0)
|
|
34
34
|
assert.equal(v.canUndo, false)
|
|
@@ -46,7 +46,7 @@ test('late load cannot overwrite another model or a new figure', async () => {
|
|
|
46
46
|
const p = deferred<FigureProductRecord>(),
|
|
47
47
|
t = createFigureProductSession({ ...store, load: () => p.promise })
|
|
48
48
|
const loading = t.open('A')
|
|
49
|
-
t.startNew()
|
|
49
|
+
t.startNew({ name: 'New' })
|
|
50
50
|
p.resolve({ id: 'A', source: source() })
|
|
51
51
|
await loading
|
|
52
52
|
assert.equal(t.read().id, undefined)
|
|
@@ -95,10 +95,10 @@ test('save carries exact baseline and acknowledges only matching response', asyn
|
|
|
95
95
|
test('pending save locks edits/navigation and duplicate create', async () => {
|
|
96
96
|
const p = deferred<FigureProductRecord>(),
|
|
97
97
|
s = createFigureProductSession({ ...store, save: () => p.promise })
|
|
98
|
-
s.startNew()
|
|
98
|
+
s.startNew({ name: 'New' })
|
|
99
99
|
const text = s.source(),
|
|
100
100
|
saving = s.save()
|
|
101
|
-
assert.throws(() => s.startNew())
|
|
101
|
+
assert.throws(() => s.startNew({ name: 'New' }))
|
|
102
102
|
assert.throws(() => s.undo())
|
|
103
103
|
await assert.rejects(s.save())
|
|
104
104
|
p.resolve({ id: 'new', source: text })
|
|
@@ -124,7 +124,7 @@ test('retry after a lost create response reuses the same creation identity', asy
|
|
|
124
124
|
return { id: q.creationId!, source: q.source }
|
|
125
125
|
}
|
|
126
126
|
})
|
|
127
|
-
s.startNew()
|
|
127
|
+
s.startNew({ name: 'New' })
|
|
128
128
|
await assert.rejects(s.save())
|
|
129
129
|
await s.save()
|
|
130
130
|
assert.equal(keys.length, 2)
|
|
@@ -67,14 +67,14 @@ test('a save names the proposals its text holds: undone, not; redone, again; und
|
|
|
67
67
|
test('a new figure opened for a proposal is created under its key; one taken on the open new figure keeps its id', async () => {
|
|
68
68
|
const { store, saves } = server()
|
|
69
69
|
const s = createFigureProductSession(store)
|
|
70
|
-
s.startNew()
|
|
70
|
+
s.startNew({ name: 'New' })
|
|
71
71
|
s.createUnder(K1)
|
|
72
72
|
s.applyProposal(lid, s.read().revision!, s.read().context!, K1)
|
|
73
73
|
await s.save({ type: 'LIDDED' })
|
|
74
74
|
assert.equal(saves.at(-1)!.creationId, K1, 'created under the proposal key: taken twice, it meets the first figure')
|
|
75
75
|
assert.deepEqual(saves.at(-1)!.proposalKeys, [K1])
|
|
76
76
|
|
|
77
|
-
s.startNew({ discard: true })
|
|
77
|
+
s.startNew({ discard: true, name: 'New' })
|
|
78
78
|
const own = s.read().creationId
|
|
79
79
|
const detail: FigureApplyDetail = { key: K2, steps: lid, create: true }
|
|
80
80
|
assert.equal(takeFigureOffer(s, detail, { onSavedFigure: false, say }), true)
|
|
@@ -82,7 +82,7 @@ test('a new figure opened for a proposal is created under its key; one taken on
|
|
|
82
82
|
assert.deepEqual({ creationId: saves.at(-1)!.creationId, proposalKeys: saves.at(-1)!.proposalKeys }, { creationId: own, proposalKeys: [K2] })
|
|
83
83
|
|
|
84
84
|
/* A new figure the person has touched is theirs: the proposal is not put on it (NEW_FIGURE_UNSAVED), its id stays. */
|
|
85
|
-
s.startNew({ discard: true })
|
|
85
|
+
s.startNew({ discard: true, name: 'New' })
|
|
86
86
|
s.command({ kind: 'rename', name: 'Mine' } as never)
|
|
87
87
|
assert.throws(() => s.createUnder(K2))
|
|
88
88
|
/* Nor on a saved one. */
|
|
@@ -50,7 +50,7 @@ test('the source a revert expects is what the server is confirmed to hold -- not
|
|
|
50
50
|
return { id: q.id ?? 'a', source: row }
|
|
51
51
|
}
|
|
52
52
|
})
|
|
53
|
-
session.startNew()
|
|
53
|
+
session.startNew({ name: 'New' })
|
|
54
54
|
assert.equal(session.confirmedSource(), undefined, 'nothing saved: nothing to revert over')
|
|
55
55
|
await session.open('a')
|
|
56
56
|
const loaded = row
|
package/translations/en.json
CHANGED
|
@@ -640,6 +640,7 @@
|
|
|
640
640
|
"figure.label.controls": "Controls",
|
|
641
641
|
"figure.text.controls-explained": "A board or a connection sets these values to pose the figure. Move a slider to try one.",
|
|
642
642
|
"figure.text.no-controls": "No controls yet. Adding a motion to a part makes one.",
|
|
643
|
+
"figure.text.no-clips": "No motion to play yet.",
|
|
643
644
|
"figure.button.keep-pose": "Keep this pose as the default",
|
|
644
645
|
"figure.label.control-name": "Name",
|
|
645
646
|
"figure.text.control-starts-at": "Starts at {value} {unit} when placed on a board.",
|
package/translations/es.json
CHANGED
|
@@ -640,6 +640,7 @@
|
|
|
640
640
|
"figure.label.controls": "Controles",
|
|
641
641
|
"figure.text.controls-explained": "Un tablero o una conexión define estos valores para dar postura a la figura. Mueva un control deslizante para probar uno.",
|
|
642
642
|
"figure.text.no-controls": "Aún no hay controles. Al añadir un movimiento a una pieza se crea uno.",
|
|
643
|
+
"figure.text.no-clips": "Todavía no hay movimientos para reproducir.",
|
|
643
644
|
"figure.button.keep-pose": "Mantener esta postura como predeterminada",
|
|
644
645
|
"figure.label.control-name": "Nombre",
|
|
645
646
|
"figure.text.control-starts-at": "Empieza en {value} {unit} al colocarla en un tablero.",
|
package/translations/ja.json
CHANGED
|
@@ -640,6 +640,7 @@
|
|
|
640
640
|
"figure.label.controls": "操作値",
|
|
641
641
|
"figure.text.controls-explained": "ボードや連携がこの値を変えて図形の姿勢を動かします。スライダーで試せます。",
|
|
642
642
|
"figure.text.no-controls": "操作値はまだありません。部品の「動き」で動きを追加すると作られます。",
|
|
643
|
+
"figure.text.no-clips": "再生する動きがありません。",
|
|
643
644
|
"figure.button.keep-pose": "今の姿勢を既定にする",
|
|
644
645
|
"figure.label.control-name": "名前",
|
|
645
646
|
"figure.text.control-starts-at": "ボードに置くと {value} {unit} から始まります。",
|
package/translations/ko.json
CHANGED
|
@@ -640,6 +640,7 @@
|
|
|
640
640
|
"figure.label.controls": "조작값",
|
|
641
641
|
"figure.text.controls-explained": "보드나 연결이 이 값을 바꿔 피규어의 자세를 움직입니다. 슬라이더로 움직여 볼 수 있습니다.",
|
|
642
642
|
"figure.text.no-controls": "조작값이 없습니다. 부품의 「움직임」에서 움직임을 추가하면 생깁니다.",
|
|
643
|
+
"figure.text.no-clips": "재생할 움직임이 없습니다.",
|
|
643
644
|
"figure.button.keep-pose": "지금 자세를 기본으로",
|
|
644
645
|
"figure.label.control-name": "이름",
|
|
645
646
|
"figure.text.control-starts-at": "보드에 놓이면 {value} {unit} 에서 시작합니다.",
|
package/translations/ms.json
CHANGED
|
@@ -640,6 +640,7 @@
|
|
|
640
640
|
"figure.label.controls": "Kawalan",
|
|
641
641
|
"figure.text.controls-explained": "Papan atau sambungan menetapkan nilai ini untuk menggerakkan rajah. Gerakkan peluncur untuk mencuba.",
|
|
642
642
|
"figure.text.no-controls": "Belum ada kawalan. Menambah gerakan pada bahagian akan menciptanya.",
|
|
643
|
+
"figure.text.no-clips": "Belum ada gerakan untuk dimainkan.",
|
|
643
644
|
"figure.button.keep-pose": "Jadikan pose ini lalai",
|
|
644
645
|
"figure.label.control-name": "Nama",
|
|
645
646
|
"figure.text.control-starts-at": "Bermula pada {value} {unit} apabila diletakkan pada papan.",
|
package/translations/zh.json
CHANGED
|
@@ -640,6 +640,7 @@
|
|
|
640
640
|
"figure.label.controls": "控制值",
|
|
641
641
|
"figure.text.controls-explained": "看板或连接通过设置这些值来改变图形的姿态。拖动滑块即可试用。",
|
|
642
642
|
"figure.text.no-controls": "还没有控制值。在部件的“运动”中添加运动即可生成。",
|
|
643
|
+
"figure.text.no-clips": "还没有可播放的动作。",
|
|
643
644
|
"figure.button.keep-pose": "将当前姿态设为默认",
|
|
644
645
|
"figure.label.control-name": "名称",
|
|
645
646
|
"figure.text.control-starts-at": "放到看板上时从 {value} {unit} 开始。",
|