@operato/board 10.29.8 → 10.29.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@operato/board",
3
- "version": "10.29.8",
3
+ "version": "10.29.9",
4
4
  "description": "Webcomponent for board following open-wc recommendations",
5
5
  "author": "heartyoh",
6
6
  "type": "module",
@@ -120,18 +120,18 @@
120
120
  "dependencies": {
121
121
  "@material/web": "^2.0.0",
122
122
  "@open-wc/scoped-elements": "^2.1.3",
123
- "@operato/app": "^10.29.8",
124
- "@operato/data-grist": "^10.29.8",
125
- "@operato/font": "^10.29.8",
123
+ "@operato/app": "^10.29.9",
124
+ "@operato/data-grist": "^10.29.9",
125
+ "@operato/font": "^10.29.9",
126
126
  "@operato/graphql": "^10.29.5",
127
127
  "@operato/i18n": "^10.29.1",
128
- "@operato/input": "^10.29.1",
129
- "@operato/layout": "^10.29.7",
130
- "@operato/markdown": "^10.29.1",
131
- "@operato/popup": "^10.29.1",
132
- "@operato/property-editor": "^10.29.8",
133
- "@operato/property-panel": "^10.29.8",
134
- "@operato/styles": "^10.29.1",
128
+ "@operato/input": "^10.29.9",
129
+ "@operato/layout": "^10.29.9",
130
+ "@operato/markdown": "^10.29.9",
131
+ "@operato/popup": "^10.29.9",
132
+ "@operato/property-editor": "^10.29.9",
133
+ "@operato/property-panel": "^10.29.9",
134
+ "@operato/styles": "^10.29.9",
135
135
  "@operato/utils": "^10.29.1",
136
136
  "@types/file-saver": "^2.0.4",
137
137
  "@types/sortablejs": "^1.10.7",
@@ -144,7 +144,7 @@
144
144
  "devDependencies": {
145
145
  "@custom-elements-manifest/analyzer": "^0.10.0",
146
146
  "@hatiolab/prettier-config": "^1.0.0",
147
- "@hatiolab/things-scene": "^10.1.61",
147
+ "@hatiolab/things-scene": "^10.1.74",
148
148
  "@open-wc/eslint-config": "^12.0.3",
149
149
  "@open-wc/testing": "^4.0.0",
150
150
  "@rollup/plugin-image": "^3.0.3",
@@ -180,5 +180,5 @@
180
180
  "prettier --write"
181
181
  ]
182
182
  },
183
- "gitHead": "7afbba99cf2119ccb709b6cb0cff8b58d447ef88"
183
+ "gitHead": "24e4809ab0168eef48792291f72b11f75d8179ad"
184
184
  }
@@ -2,10 +2,17 @@ import { expect } from '@open-wc/testing'
2
2
 
3
3
  import { Component, hasFigure, registerTypeResolver, resetFigures, resolveUnknownTypes } from '@hatiolab/things-scene'
4
4
 
5
- import { createFigureResolver } from '../src/figure-provider.js'
5
+ import {
6
+ createFigureResolver,
7
+ FIGURE_TYPE_MAP_CHANGED,
8
+ figureMapReleaseOfPage,
9
+ resetFigureMapPage,
10
+ setFigureMapChoice,
11
+ type FigureBlueprintsAnswer
12
+ } from '../src/figure-provider.js'
6
13
 
7
14
  /**
8
- * The path a published figure takes to a board (ADR-0063 결정 2).
15
+ * The path a figure takes to a board (ADR-0063 결정 2, ADR-0114): one batch question by full name.
9
16
  *
10
17
  * things-scene holds the seat and knows nothing about figures; this package fills it. What is
11
18
  * checked here is the part that belongs to the board: which names are asked about, what happens to
@@ -56,166 +63,169 @@ const figureAssetOf = (type: string) => ({
56
63
  }
57
64
  })
58
65
 
59
- const figurePayloadOf = (type: string) => ({ type, asset: figureAssetOf(type) })
60
-
61
- /** Records what was asked for, and answers from a table. */
62
- function fetcherOf(published: Record<string, unknown>, failing: string[] = []) {
63
- const asked: string[] = []
64
-
66
+ /** A server that answers from a table, from one map release, and records each question. */
67
+ function serverOf(
68
+ published: Record<string, unknown>,
69
+ release: { version: number; revision: number } | null = { version: 1, revision: 1 },
70
+ refusals: Record<string, string> = {}
71
+ ) {
72
+ const asked: { types: string[]; map: string }[] = []
73
+ const state = { release }
65
74
  return {
66
75
  asked,
67
- fetch: async (type: string) => {
68
- asked.push(type)
69
- if (failing.includes(type)) {
70
- throw new Error(`no route to the door (${type})`)
71
- }
72
- return (published[type] ?? null) as never
76
+ state,
77
+ fetch: async (types: readonly string[], map: string): Promise<FigureBlueprintsAnswer> => {
78
+ asked.push({ types: [...types], map })
79
+ const items = types.filter(type => published[type]).map(type => ({ type, ref: `${type}@1`, asset: published[type] }))
80
+ const refused = types
81
+ .filter(type => !published[type])
82
+ .map(type => ({ type, code: 'figure-not-in-type-map', message: refusals[type] ?? `${type} is not in the type map` }))
83
+ return { release: state.release, items, refused }
73
84
  }
74
85
  }
75
86
  }
76
87
 
77
- describe('figure 공급자 — 보드가 발행된 Figure 를 받는 길', () => {
88
+ const name = (label: string) => `hatiolab.com/${label}-${Math.random().toString(36).slice(2, 8)}`
89
+
90
+ describe('figure 공급자 — 보드가 Figure 를 정식 이름으로 한 번에 묻는다', () => {
78
91
  afterEach(() => {
79
92
  resetFigures()
93
+ resetFigureMapPage()
80
94
  })
81
95
 
82
- it('대문자 이름만 서버에 묻는다 — 씬의 기본 type 과 오타는 문을 지나지 않는다', async () => {
83
- const { asked, fetch } = fetcherOf({ EQ_MIXER: figurePayloadOf('EQ_MIXER') })
96
+ it('등록되지 않은 type 을 대소문자와 관계없이 한 번에 묻는다 — 이미 등록된 것은 묻지 않는다', async () => {
97
+ const mixer = name('eq-mixer')
98
+ const { asked, fetch } = serverOf({ [mixer]: figureAssetOf(mixer) })
84
99
 
85
- await createFigureResolver(fetch)(['EQ_MIXER', 'rect', 'Conveyor', 'text', 'VEH_AGV_01'])
100
+ await createFigureResolver(fetch)([mixer, 'rect', 'Conveyor', 'text', 'VEH_AGV_01'])
86
101
 
87
- expect(asked).to.deep.equal(['EQ_MIXER', 'VEH_AGV_01'])
102
+ expect(asked).to.deep.equal([{ types: [mixer, 'Conveyor', 'VEH_AGV_01'], map: 'ACTIVE' }])
88
103
  })
89
104
 
90
- it('이미 등록된 type 은 묻지 않는다 — 그리고 있는 컴포넌트를 figure 가 덮지 않는다', async () => {
91
- const { asked, fetch } = fetcherOf({ EQ_TAKEN: figurePayloadOf('EQ_TAKEN') })
105
+ it('이미 등록된 이름은 묻지 않고, figure 가 그 컴포넌트를 덮지 않는다', async () => {
106
+ const taken = name('taken')
107
+ const { asked, fetch } = serverOf({ [taken]: figureAssetOf(taken) })
108
+ Component.register(taken, class Taken extends Component {} as never)
92
109
 
93
- /* Somebody else already draws this name — a plugin, an app's own component. */
94
- Component.register('EQ_TAKEN', class Taken extends Component {} as never)
95
-
96
- await createFigureResolver(fetch)(['EQ_TAKEN'])
110
+ await createFigureResolver(fetch)([taken])
97
111
 
98
112
  expect(asked).to.deep.equal([])
99
- expect(hasFigure('EQ_TAKEN')).to.equal(false)
113
+ expect(hasFigure(taken)).to.equal(false)
100
114
  })
101
115
 
102
- it('받은 자산을 등록한다 — 도면은 type 만 적으면 된다', async () => {
103
- const { fetch } = fetcherOf({ EQ_REGISTERED: figurePayloadOf('EQ_REGISTERED') })
116
+ it('받은 자산을 물은 이름으로 등록한다', async () => {
117
+ const conveyor = name('conveyor')
118
+ const { fetch } = serverOf({ [conveyor]: figureAssetOf(conveyor) })
104
119
 
105
- await createFigureResolver(fetch)(['EQ_REGISTERED'])
120
+ await createFigureResolver(fetch)([conveyor])
106
121
 
107
- expect(hasFigure('EQ_REGISTERED')).to.equal(true)
108
- /* The component type is registered with it — the model writes `type` and nothing else. */
109
- expect(Component.register('EQ_REGISTERED')).to.not.equal(undefined)
122
+ expect(hasFigure(conveyor)).to.equal(true)
123
+ expect(Component.register(conveyor)).to.not.equal(undefined)
110
124
  })
111
125
 
112
- it('자산은 씬의 문으로 등록한다 — 보드는 그것을 읽지 않고 씬에 넘긴다', async () => {
113
- const { fetch } = fetcherOf({ EQ: figurePayloadOf('EQ') })
126
+ it('★ 그리지 못한 이름은 서버가 준 이유를 그대로 돌려준다 — 읽을 수 없는 자산도 그 이름만 이유를 받는다', async () => {
127
+ const good = name('good')
128
+ const broken = name('broken')
129
+ const missing = name('missing')
130
+ const brokenAsset = figureAssetOf(broken)
131
+ brokenAsset.appearance = []
132
+ const { fetch } = serverOf({ [good]: figureAssetOf(good), [broken]: brokenAsset }, { version: 1, revision: 1 }, { [missing]: '타입 맵에 없습니다' })
114
133
 
115
- await createFigureResolver(fetch)(['EQ'])
134
+ const answer = await createFigureResolver(fetch)([good, broken, missing])
116
135
 
117
- expect(hasFigure('EQ')).to.equal(true)
118
- /* 도면은 type 하나만 적으면 그려진다. */
119
- expect(Component.register('EQ')).to.not.equal(undefined)
136
+ expect(hasFigure(good)).to.equal(true)
137
+ expect(hasFigure(broken)).to.equal(false)
138
+ expect(answer!.refused.map(r => r.type)).to.deep.equal([missing, broken])
139
+ expect(answer!.refused[0].message).to.equal('타입 맵에 없습니다')
120
140
  })
121
141
 
122
- it('자산이 없는 답은 그 type 만 실패로 남는다 — 옛 청사진을 그리지 않는다', async () => {
123
- const { fetch } = fetcherOf({ EQ_NEW: figurePayloadOf('EQ_NEW'), EQ_OLD: { version: 1, type: 'EQ_OLD', groups: [] } })
124
-
125
- let message = ''
126
- try {
127
- await createFigureResolver(fetch)(['EQ_NEW', 'EQ_OLD'])
128
- } catch (error) {
129
- message = error instanceof Error ? error.message : String(error)
142
+ it('물을 수 없으면 거절한다 — 씬은 실패로 모으고 보드는 열린다', async () => {
143
+ const fetch = async () => {
144
+ throw new Error('no route to the door')
130
145
  }
131
-
132
- expect(message).to.contain('not a figure asset: EQ_OLD')
133
- expect(hasFigure('EQ_NEW')).to.equal(true)
134
- expect(hasFigure('EQ_OLD')).to.equal(false)
135
- })
136
-
137
- it('읽을 수 없는 자산은 그 type 만 실패로 남는다', async () => {
138
- const broken = figurePayloadOf('EQ_ASSET_BROKEN')
139
- broken.asset.appearance = []
140
- const { fetch } = fetcherOf({ EQ_BESIDE: figurePayloadOf('EQ_BESIDE'), EQ_ASSET_BROKEN: broken })
141
-
142
146
  let message = ''
143
147
  try {
144
- await createFigureResolver(fetch)(['EQ_BESIDE', 'EQ_ASSET_BROKEN'])
148
+ await createFigureResolver(fetch as never)([name('x')])
145
149
  } catch (error) {
146
150
  message = error instanceof Error ? error.message : String(error)
147
151
  }
148
-
149
- expect(message).to.contain('EQ_ASSET_BROKEN')
150
- expect(hasFigure('EQ_BESIDE')).to.equal(true)
151
- expect(hasFigure('EQ_ASSET_BROKEN')).to.equal(false)
152
+ expect(message).to.contain('no route to the door')
152
153
  })
153
154
 
154
- it('발행된 것이 없으면 조용히 자리표시로 남는다 — 거절이 아니다', async () => {
155
- const { fetch } = fetcherOf({})
155
+ it('★ 페이지는 처음 받은 맵 릴리스를 기억하고, 후보의 revision 이 바뀐 답은 등록하지 않는다', async () => {
156
+ const first = name('first')
157
+ const later = name('later')
158
+ const server = serverOf({ [first]: figureAssetOf(first), [later]: figureAssetOf(later) }, { version: 2, revision: 3 })
159
+ const resolver = createFigureResolver(server.fetch)
160
+ setFigureMapChoice('CANDIDATE')
161
+
162
+ await resolver([first])
163
+ expect(figureMapReleaseOfPage()).to.deep.equal({ map: 'CANDIDATE', version: 2, revision: 3 })
156
164
 
157
- let rejected = false
165
+ let changed: any = null
166
+ const listen = (e: Event) => (changed = (e as CustomEvent).detail)
167
+ window.addEventListener(FIGURE_TYPE_MAP_CHANGED, listen)
158
168
  try {
159
- await createFigureResolver(fetch)(['EQ_NOT_PUBLISHED'])
160
- } catch {
161
- rejected = true
169
+ server.state.release = { version: 2, revision: 4 }
170
+ const answer = await resolver([later])
171
+ expect(hasFigure(later)).to.equal(false)
172
+ expect(answer!.refused.map(r => r.type)).to.deep.equal([later])
173
+ expect(changed).to.deep.equal({ was: { map: 'CANDIDATE', version: 2, revision: 3 }, now: { map: 'CANDIDATE', version: 2, revision: 4 } })
174
+ expect(figureMapReleaseOfPage()).to.deep.equal({ map: 'CANDIDATE', version: 2, revision: 3 })
175
+ } finally {
176
+ window.removeEventListener(FIGURE_TYPE_MAP_CHANGED, listen)
162
177
  }
163
-
164
- expect(rejected).to.equal(false)
165
- expect(hasFigure('EQ_NOT_PUBLISHED')).to.equal(false)
166
178
  })
167
179
 
168
- it('물을 수 없으면 거절한다. 그때도 답을 받은 것은 등록된다', async () => {
169
- const { fetch } = fetcherOf({ EQ_BESIDE_BROKEN: figurePayloadOf('EQ_BESIDE_BROKEN') }, ['EQ_BROKEN'])
180
+ it('★ 활성 맵과 후보 맵의 등록을 한 페이지에서 섞지 않는다', async () => {
181
+ const active = name('active')
182
+ const preview = name('preview')
183
+ const server = serverOf({ [active]: figureAssetOf(active), [preview]: figureAssetOf(preview) }, { version: 1, revision: 1 })
184
+ const resolver = createFigureResolver(server.fetch)
170
185
 
171
- let message = ''
172
- try {
173
- await createFigureResolver(fetch)(['EQ_BESIDE_BROKEN', 'EQ_BROKEN'])
174
- } catch (error) {
175
- message = error instanceof Error ? error.message : String(error)
176
- }
186
+ await resolver([active])
187
+ expect(figureMapReleaseOfPage()).to.deep.equal({ map: 'ACTIVE', version: 1, revision: 1 })
188
+
189
+ setFigureMapChoice('CANDIDATE')
190
+ const answer = await resolver([preview])
177
191
 
178
- expect(message).to.contain('EQ_BROKEN')
179
- expect(message).to.not.contain('EQ_BESIDE_BROKEN')
180
- expect(hasFigure('EQ_BESIDE_BROKEN')).to.equal(true)
192
+ expect(server.asked.map(a => a.map)).to.deep.equal(['ACTIVE', 'CANDIDATE'])
193
+ expect(hasFigure(preview)).to.equal(false)
194
+ expect(answer!.refused.map(r => r.type)).to.deep.equal([preview])
181
195
  })
182
196
 
183
- it('읽을 수 없는 자산은 그 type 만 실패로 남는다 — 다른 type 을 막지 않는다', async () => {
184
- const { fetch } = fetcherOf({
185
- EQ_BESIDE_MALFORMED: figurePayloadOf('EQ_BESIDE_MALFORMED'),
186
- EQ_MALFORMED: { type: 'EQ_MALFORMED', asset: { version: 'figure-asset-1' } }
187
- })
197
+ it('맵 릴리스가 없는 서버(저작 서버의 최신 정책)는 기억하지 않는다', async () => {
198
+ const a = name('a')
199
+ const b = name('b')
200
+ const server = serverOf({ [a]: figureAssetOf(a), [b]: figureAssetOf(b) }, null)
201
+ const resolver = createFigureResolver(server.fetch)
188
202
 
189
- let message = ''
190
- try {
191
- await createFigureResolver(fetch)(['EQ_BESIDE_MALFORMED', 'EQ_MALFORMED'])
192
- } catch (error) {
193
- message = error instanceof Error ? error.message : String(error)
194
- }
203
+ await resolver([a])
204
+ await resolver([b])
195
205
 
196
- expect(message).to.contain('EQ_MALFORMED')
197
- expect(hasFigure('EQ_BESIDE_MALFORMED')).to.equal(true)
198
- expect(hasFigure('EQ_MALFORMED')).to.equal(false)
206
+ expect(figureMapReleaseOfPage()).to.equal(null)
207
+ expect(hasFigure(a)).to.equal(true)
208
+ expect(hasFigure(b)).to.equal(true)
199
209
  })
200
210
 
201
- it('씬의 자리에 앉아 동작한다 — 모델의 모르는 type 이 해석 뒤에 남지 않는다', async () => {
202
- const { fetch } = fetcherOf({ EQ_ON_A_BOARD: figurePayloadOf('EQ_ON_A_BOARD') })
203
- const takeBack = registerTypeResolver(createFigureResolver(fetch))
211
+ it('씬의 자리에 앉아 동작한다 — 받은 것은 그려지고 나머지는 자리표시로 남는다', async () => {
212
+ const onBoard = name('on-board')
213
+ const never = name('never')
214
+ const { fetch } = serverOf({ [onBoard]: figureAssetOf(onBoard) })
215
+ const takeBack = registerTypeResolver(createFigureResolver(fetch) as never)
204
216
 
205
217
  try {
206
218
  const model = {
207
219
  type: 'container',
208
220
  components: [
209
- { type: 'EQ_ON_A_BOARD', left: 0, top: 0, width: 100, height: 100 },
210
- { type: 'EQ_NEVER_PUBLISHED', left: 0, top: 0, width: 100, height: 100 }
221
+ { type: onBoard, left: 0, top: 0, width: 100, height: 100 },
222
+ { type: never, left: 0, top: 0, width: 100, height: 100 }
211
223
  ]
212
224
  }
213
-
214
225
  const resolution = await resolveUnknownTypes(model)
215
-
216
- expect(resolution.unresolved).to.deep.equal(['EQ_NEVER_PUBLISHED'])
226
+ expect(resolution.unresolved).to.deep.equal([never])
217
227
  expect(resolution.failed).to.deep.equal([])
218
- expect(hasFigure('EQ_ON_A_BOARD')).to.equal(true)
228
+ expect(hasFigure(onBoard)).to.equal(true)
219
229
  } finally {
220
230
  takeBack()
221
231
  }
@@ -36,5 +36,6 @@
36
36
  "label.list": "list",
37
37
  "label.visibility-domain": "public in group",
38
38
  "label.visibility-private": "private",
39
- "label.visibility-public": "public"
39
+ "label.visibility-public": "public",
40
+ "text.figure-type-map-changed": "the type map changed. reopen to draw it."
40
41
  }
@@ -36,5 +36,6 @@
36
36
  "label.list": "一覧",
37
37
  "label.visibility-domain": "グループ内公開",
38
38
  "label.visibility-private": "プライベート",
39
- "label.visibility-public": "公開"
39
+ "label.visibility-public": "公開",
40
+ "text.figure-type-map-changed": "タイプマップが変わりました。開き直すと描画されます。"
40
41
  }
@@ -36,5 +36,6 @@
36
36
  "label.list": "목록",
37
37
  "label.visibility-domain": "그룹내 공개",
38
38
  "label.visibility-private": "비공개",
39
- "label.visibility-public": "공개"
39
+ "label.visibility-public": "공개",
40
+ "text.figure-type-map-changed": "타입 맵이 바뀌었습니다. 다시 열면 그려집니다."
40
41
  }