@things-factory/figure-ui 10.1.4 → 10.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/client/graphql/index.ts +87 -12
  2. package/client/modeller/figure-history-panel.ts +324 -0
  3. package/client/modeller/figure-side.ts +19 -1
  4. package/client/pages/figure-list-page.ts +363 -226
  5. package/client/pages/figure-modeller-page.ts +22 -0
  6. package/client/types.ts +17 -0
  7. package/dist-client/graphql/index.d.ts +24 -4
  8. package/dist-client/graphql/index.js +70 -9
  9. package/dist-client/graphql/index.js.map +1 -1
  10. package/dist-client/modeller/figure-history-panel.d.ts +42 -0
  11. package/dist-client/modeller/figure-history-panel.js +343 -0
  12. package/dist-client/modeller/figure-history-panel.js.map +1 -0
  13. package/dist-client/modeller/figure-side.d.ts +6 -0
  14. package/dist-client/modeller/figure-side.js +31 -0
  15. package/dist-client/modeller/figure-side.js.map +1 -1
  16. package/dist-client/pages/figure-list-page.d.ts +44 -21
  17. package/dist-client/pages/figure-list-page.js +351 -246
  18. package/dist-client/pages/figure-list-page.js.map +1 -1
  19. package/dist-client/pages/figure-modeller-page.d.ts +9 -0
  20. package/dist-client/pages/figure-modeller-page.js +20 -0
  21. package/dist-client/pages/figure-modeller-page.js.map +1 -1
  22. package/dist-client/tsconfig.tsbuildinfo +1 -1
  23. package/dist-client/types.d.ts +19 -0
  24. package/dist-client/types.js.map +1 -1
  25. package/package.json +4 -2
  26. package/translations/en.json +15 -0
  27. package/translations/ja.json +17 -2
  28. package/translations/ko.json +17 -2
  29. package/translations/ms.json +17 -2
  30. package/translations/zh.json +17 -2
  31. package/client/viewparts/figure-card.ts +0 -226
  32. package/dist-client/viewparts/figure-card.d.ts +0 -24
  33. package/dist-client/viewparts/figure-card.js +0 -221
  34. package/dist-client/viewparts/figure-card.js.map +0 -1
@@ -1,7 +1,15 @@
1
1
  import gql from 'graphql-tag'
2
2
  import { client } from '@operato/graphql'
3
3
 
4
- import type { Figure, FigureFinding, FigureListResult, FigurePatch, FigureProposal, NewFigure } from '../types.js'
4
+ import type {
5
+ Figure,
6
+ FigureFinding,
7
+ FigureListResult,
8
+ FigurePatch,
9
+ FigureProposal,
10
+ FigureVersion,
11
+ NewFigure
12
+ } from '../types.js'
5
13
 
6
14
  /**
7
15
  * 응답에서 값을 꺼낸다. **서버가 말한 사유를 그대로 올린다.**
@@ -39,6 +47,7 @@ const FIGURE_LIST_FIELDS = `
39
47
  description
40
48
  category
41
49
  state
50
+ version
42
51
  score
43
52
  triangles
44
53
  groups
@@ -48,17 +57,19 @@ const FIGURE_LIST_FIELDS = `
48
57
  updater { id name }
49
58
  `
50
59
 
60
+ /**
61
+ * 목록을 가져온다.
62
+ *
63
+ * 걸러 보기·정렬·쪽 나누기를 **그대로 넘긴다.** 화면이 `search`·`state` 같은 이름을 따로 만들어
64
+ * filter 로 옮기던 것을 그만두었다 — 그 이름들은 격자(ox-grist)가 이미 컬럼 설정에서 만들어 주고,
65
+ * 중간에 한 벌 더 두면 격자가 아는 조건과 서버에 가는 조건이 어긋난다.
66
+ */
51
67
  export async function fetchFigureList(params: {
52
68
  page?: number
53
69
  limit?: number
54
- search?: string
55
- category?: string
56
- state?: string
70
+ filters?: unknown[]
71
+ sortings?: { name: string; desc?: boolean }[]
57
72
  }): Promise<FigureListResult> {
58
- const filters: { name: string; operator: string; value: unknown }[] = []
59
- if (params.category) filters.push({ name: 'category', operator: 'eq', value: params.category })
60
- if (params.state) filters.push({ name: 'state', operator: 'eq', value: params.state })
61
-
62
73
  const response = await client.query({
63
74
  query: gql`
64
75
  query ($filters: [Filter!], $pagination: Pagination, $sortings: [Sorting!]) {
@@ -69,9 +80,9 @@ export async function fetchFigureList(params: {
69
80
  }
70
81
  `,
71
82
  variables: {
72
- filters,
83
+ filters: params.filters ?? [],
73
84
  pagination: { page: params.page ?? 1, limit: params.limit ?? 30 },
74
- sortings: [{ name: 'updatedAt', desc: true }]
85
+ sortings: params.sortings?.length ? params.sortings : [{ name: 'updatedAt', desc: true }]
75
86
  },
76
87
  fetchPolicy: 'network-only'
77
88
  })
@@ -88,7 +99,6 @@ export async function fetchFigure(id: string): Promise<Figure> {
88
99
  ${FIGURE_LIST_FIELDS}
89
100
  source
90
101
  properties
91
- version
92
102
  }
93
103
  }
94
104
  `,
@@ -145,7 +155,7 @@ export async function updateFigure(id: string, patch: FigurePatch): Promise<Save
145
155
  mutation: gql`
146
156
  mutation ($id: String!, $patch: FigurePatch!) {
147
157
  updateFigure(id: $id, patch: $patch) {
148
- figure { ${FIGURE_LIST_FIELDS} source version }
158
+ figure { ${FIGURE_LIST_FIELDS} source }
149
159
  violations { code message at }
150
160
  }
151
161
  }
@@ -207,3 +217,68 @@ export async function proposeFigure(request: {
207
217
 
208
218
  return unwrap<FigureProposal>(response, 'proposeFigure')
209
219
  }
220
+
221
+ /**
222
+ * 발행한다 — 판 번호가 오르고, 그 순간이 판본으로 남는다.
223
+ *
224
+ * 이미 발행된 것을 다시 발행하면 서버가 거절한다. 고쳐서 다시 내려면 저장이 먼저이고, 저장은
225
+ * 발행을 푼다(초안으로 돌아온다).
226
+ */
227
+ export async function releaseFigure(id: string, comment?: string): Promise<Figure> {
228
+ const response = await client.mutate({
229
+ mutation: gql`
230
+ mutation ReleaseFigure($id: String!, $comment: String) {
231
+ releaseFigure(id: $id, comment: $comment) {
232
+ id
233
+ version
234
+ state
235
+ updatedAt
236
+ }
237
+ }
238
+ `,
239
+ variables: { id, comment }
240
+ })
241
+
242
+ return unwrap<Figure>(response, 'releaseFigure')
243
+ }
244
+
245
+ /** 옛 판을 초안으로 되살린다. 정본·속성·그림이 돌아오고 이름·설명은 그대로다. */
246
+ export async function revertFigureVersion(id: string, version: number): Promise<SaveResult> {
247
+ const response = await client.mutate({
248
+ mutation: gql`
249
+ mutation RevertFigureVersion($id: String!, $version: Float!) {
250
+ revertFigureVersion(id: $id, version: $version) {
251
+ figure { ${FIGURE_LIST_FIELDS} source }
252
+ violations { code message at }
253
+ }
254
+ }
255
+ `,
256
+ variables: { id, version }
257
+ })
258
+
259
+ return unwrap<SaveResult>(response, 'revertFigureVersion')
260
+ }
261
+
262
+ /** 발행된 판들 — 최근 것부터 열 개. */
263
+ export async function fetchFigureVersions(id: string): Promise<FigureVersion[]> {
264
+ const response = await client.query({
265
+ query: gql`
266
+ query FigureVersions($id: String!) {
267
+ figureVersions(id: $id) {
268
+ version
269
+ comment
270
+ state
271
+ updatedAt
272
+ updater { id name }
273
+ score
274
+ triangles
275
+ groups
276
+ }
277
+ }
278
+ `,
279
+ variables: { id },
280
+ fetchPolicy: 'network-only'
281
+ })
282
+
283
+ return unwrap<FigureVersion[]>(response, 'figureVersions') ?? []
284
+ }
@@ -0,0 +1,324 @@
1
+ import '@material/web/icon/icon.js'
2
+
3
+ import { css, html, LitElement, nothing } from 'lit'
4
+ import { customElement, property, state } from 'lit/decorators.js'
5
+
6
+ import { i18next, localize } from '@operato/i18n'
7
+
8
+ import { fetchFigureVersions, releaseFigure, revertFigureVersion } from '../graphql/index.js'
9
+ import type { FigureVersion } from '../types.js'
10
+
11
+ /**
12
+ * 이력 — **발행하는 자리이자, 발행된 판들을 보는 자리.**
13
+ *
14
+ * ## 왜 저장 옆이 아니라 여기인가
15
+ *
16
+ * 저장은 하루에 수십 번이고 발행은 드물다. 두 단추를 나란히 두면 저장하려다 발행을 누른다.
17
+ * 그리고 발행은 **소비처에게 하는 약속**이라, 무엇을 약속하는지(지금 몇 판인가 · 저장 안 한 것이
18
+ * 있나 · 지난 판이 무엇이었나)를 함께 보여 주는 자리에 있어야 한다.
19
+ *
20
+ * ## 저장하지 않은 것이 있으면 발행하지 않는다
21
+ *
22
+ * 발행은 **저장된 것**을 내보낸다. 화면에 고친 것이 남아 있는 채로 발행하면, 저작자가 보고 있는
23
+ * 것과 약속한 것이 다르다. 그래서 막고, 왜 막혔는지 적는다.
24
+ *
25
+ * ## 되돌리기는 초안으로 돌아온다
26
+ *
27
+ * 옛 판의 정본·속성·그림이 돌아오고 이름·설명은 그대로다. 되살렸다고 그 판이 다시 발행되지는
28
+ * 않는다 — 다시 내보내려면 발행을 한 번 더 하고, 그때 번호는 이미 쓴 다음 번호를 받는다.
29
+ */
30
+ @customElement('figure-history-panel')
31
+ export class FigureHistoryPanel extends localize(i18next)(LitElement) {
32
+ static styles = css`
33
+ :host {
34
+ display: flex;
35
+ flex-direction: column;
36
+ min-height: 0;
37
+ overflow-y: auto;
38
+ background-color: var(--md-sys-color-surface-container-lowest);
39
+ color: var(--md-sys-color-on-surface);
40
+ font: var(--label-font, inherit);
41
+ font-size: 0.76rem;
42
+ }
43
+
44
+ :host([off]) {
45
+ display: none;
46
+ }
47
+
48
+ section {
49
+ padding: var(--spacing-medium, 8px) var(--spacing-large, 12px);
50
+ border-bottom: 1px solid var(--md-sys-color-outline-variant);
51
+ }
52
+
53
+ h3 {
54
+ margin: 0 0 6px 0;
55
+ font-size: var(--ui-t-micro, 11px);
56
+ font-weight: 700;
57
+ letter-spacing: 0.04em;
58
+ text-transform: uppercase;
59
+ color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));
60
+ }
61
+
62
+ /* 지금 무엇인가 — 발행을 누르기 전에 읽는 한 줄 */
63
+ div[now] {
64
+ display: flex;
65
+ align-items: center;
66
+ gap: 6px;
67
+ margin-bottom: 8px;
68
+ }
69
+
70
+ span[state] {
71
+ padding: 2px 8px;
72
+ border-radius: 10px;
73
+ font-size: 0.7rem;
74
+ font-weight: 700;
75
+ color: var(--md-sys-color-on-secondary-container);
76
+ background-color: var(--md-sys-color-secondary-container);
77
+ }
78
+
79
+ span[state][released] {
80
+ color: var(--md-sys-color-on-primary);
81
+ background-color: var(--md-sys-color-primary);
82
+ }
83
+
84
+ span[ver] {
85
+ font-family: var(--mono-font, monospace);
86
+ color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));
87
+ }
88
+
89
+ input,
90
+ button {
91
+ font: var(--input-field-font, inherit);
92
+ font-size: 0.76rem;
93
+ }
94
+
95
+ input[note] {
96
+ width: 100%;
97
+ box-sizing: border-box;
98
+ padding: 5px 8px;
99
+ color: var(--md-sys-color-on-surface);
100
+ background-color: var(--md-sys-color-surface-container-lowest);
101
+ border: 1px solid var(--md-sys-color-outline-variant);
102
+ border-radius: 6px;
103
+ }
104
+
105
+ button[release] {
106
+ width: 100%;
107
+ margin-top: 6px;
108
+ padding: 6px 0;
109
+ font-weight: 700;
110
+ color: var(--ui-on-accent, #fff);
111
+ background-color: var(--md-sys-color-primary);
112
+ border: 0;
113
+ border-radius: 6px;
114
+ cursor: pointer;
115
+ }
116
+
117
+ button[release][disabled] {
118
+ opacity: 0.45;
119
+ cursor: default;
120
+ }
121
+
122
+ /* 왜 못 누르는지 — 눌러 보고 알게 하지 않는다 */
123
+ p[why] {
124
+ margin: 6px 0 0 0;
125
+ font-size: 0.7rem;
126
+ line-height: 1.5;
127
+ color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));
128
+ }
129
+
130
+ p[failed] {
131
+ margin: 6px 0 0 0;
132
+ font-size: 0.7rem;
133
+ line-height: 1.5;
134
+ color: var(--md-sys-color-error);
135
+ }
136
+
137
+ ul {
138
+ margin: 0;
139
+ padding: 0;
140
+ list-style: none;
141
+ }
142
+
143
+ li {
144
+ display: grid;
145
+ grid-template-columns: auto 1fr auto;
146
+ gap: 4px 8px;
147
+ align-items: baseline;
148
+ padding: 7px 0;
149
+ border-bottom: 1px dashed var(--md-sys-color-outline-variant);
150
+ }
151
+
152
+ li:last-child {
153
+ border-bottom: 0;
154
+ }
155
+
156
+ li span[no] {
157
+ font-family: var(--mono-font, monospace);
158
+ font-weight: 700;
159
+ }
160
+
161
+ li span[when] {
162
+ grid-column: 2 / 4;
163
+ font-size: 0.7rem;
164
+ color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));
165
+ }
166
+
167
+ li span[comment] {
168
+ grid-column: 1 / 4;
169
+ line-height: 1.5;
170
+ }
171
+
172
+ li button[revert] {
173
+ padding: 2px 8px;
174
+ color: var(--md-sys-color-on-surface);
175
+ background: none;
176
+ border: 1px solid var(--md-sys-color-outline-variant);
177
+ border-radius: 6px;
178
+ cursor: pointer;
179
+ }
180
+
181
+ li button[revert]:hover {
182
+ color: var(--md-sys-color-on-primary-container);
183
+ background-color: var(--md-sys-color-primary-container);
184
+ border-color: transparent;
185
+ }
186
+
187
+ p[quiet] {
188
+ margin: 0;
189
+ line-height: 1.6;
190
+ color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));
191
+ }
192
+ `
193
+
194
+ /** 지금 열려 있는 도형. 없으면(새로 만드는 중) 발행할 것이 없다. */
195
+ @property({ type: String }) figureId?: string
196
+ @property({ type: String }) figureState?: 'draft' | 'released'
197
+ @property({ type: Number }) figureVersion = 0
198
+ /** 저장하지 않은 것이 있나 — 있으면 발행을 막는다. */
199
+ @property({ type: Boolean }) dirty = false
200
+
201
+ @state() private versions: FigureVersion[] = []
202
+ @state() private busy = false
203
+ @state() private failure = ''
204
+
205
+ updated(changes: Map<string, unknown>) {
206
+ if (changes.has('figureId') || changes.has('figureVersion')) {
207
+ void this.reload()
208
+ }
209
+ }
210
+
211
+ private async reload() {
212
+ if (!this.figureId) {
213
+ this.versions = []
214
+ return
215
+ }
216
+
217
+ try {
218
+ this.versions = await fetchFigureVersions(this.figureId)
219
+ this.failure = ''
220
+ } catch (e) {
221
+ this.failure = (e as Error).message
222
+ }
223
+ }
224
+
225
+ render() {
226
+ return html`
227
+ <section>
228
+ <h3>${i18next.t('label.release')}</h3>
229
+ ${this.figureId ? this.renderRelease() : html`<p quiet>${i18next.t('text.save-the-figure-before-releasing')}</p>`}
230
+ </section>
231
+
232
+ <section>
233
+ <h3>${i18next.t('label.released-versions')}</h3>
234
+ ${this.versions.length
235
+ ? html`<ul>
236
+ ${this.versions.map(version => this.renderVersion(version))}
237
+ </ul>`
238
+ : html`<p quiet>${i18next.t('text.nothing-released-yet')}</p>`}
239
+ </section>
240
+ `
241
+ }
242
+
243
+ private renderRelease() {
244
+ const released = this.figureState === 'released'
245
+ /* 막는 이유는 하나씩 말한다. 「발행할 수 없습니다」로 뭉치면 무엇을 하면 되는지 모른다. */
246
+ const why = this.dirty
247
+ ? i18next.t('text.release-needs-a-save-first')
248
+ : released
249
+ ? i18next.t('text.already-released-edit-to-release-again')
250
+ : ''
251
+
252
+ return html`
253
+ <div now>
254
+ <span state ?released=${released}>${i18next.t(released ? 'label.released' : 'label.draft')}</span>
255
+ <span ver>${i18next.t('text.version-number', { version: this.figureVersion ?? 0 })}</span>
256
+ </div>
257
+
258
+ <input note placeholder=${i18next.t('text.what-changed-in-this-release')} ?disabled=${!!why || this.busy} />
259
+
260
+ <button release ?disabled=${!!why || this.busy} @click=${() => this.release()}>
261
+ ${this.busy ? i18next.t('text.releasing') : i18next.t('button.release')}
262
+ </button>
263
+
264
+ ${why ? html`<p why>${why}</p>` : nothing}
265
+ ${this.failure ? html`<p failed>${this.failure}</p>` : nothing}
266
+ `
267
+ }
268
+
269
+ private renderVersion(version: FigureVersion) {
270
+ return html`
271
+ <li>
272
+ <span no>${i18next.t('text.version-number', { version: version.version })}</span>
273
+ <span when>
274
+ ${version.updatedAt ? new Date(version.updatedAt).toLocaleString() : ''}
275
+ ${version.updater?.name ? ` · ${version.updater.name}` : ''}
276
+ </span>
277
+ <button revert ?disabled=${this.busy} @click=${() => this.revert(version.version)}>
278
+ ${i18next.t('button.revert')}
279
+ </button>
280
+ ${version.comment ? html`<span comment>${version.comment}</span>` : nothing}
281
+ </li>
282
+ `
283
+ }
284
+
285
+ private async release() {
286
+ const input = this.renderRoot.querySelector('input[note]') as HTMLInputElement
287
+ const comment = input?.value?.trim()
288
+
289
+ this.busy = true
290
+ this.failure = ''
291
+
292
+ try {
293
+ const figure = await releaseFigure(this.figureId!, comment || undefined)
294
+ if (input) input.value = ''
295
+ /* 정본을 든 자리가 갱신한다 — 여기서 페이지 상태를 고쳐 쓰지 않는다. */
296
+ this.dispatchEvent(new CustomEvent('figure-released', { detail: { figure }, bubbles: true, composed: true }))
297
+ await this.reload()
298
+ } catch (e) {
299
+ this.failure = (e as Error).message
300
+ } finally {
301
+ this.busy = false
302
+ }
303
+ }
304
+
305
+ private async revert(version: number) {
306
+ /* 되돌아갈 수 없는 조작은 아니지만(판본은 남는다), 지금 화면의 것이 덮인다 — 이름으로 확인한다. */
307
+ if (!confirm(i18next.t('text.revert-to-version-confirm', { version }))) {
308
+ return
309
+ }
310
+
311
+ this.busy = true
312
+ this.failure = ''
313
+
314
+ try {
315
+ const result = await revertFigureVersion(this.figureId!, version)
316
+ this.dispatchEvent(new CustomEvent('figure-reverted', { detail: result, bubbles: true, composed: true }))
317
+ await this.reload()
318
+ } catch (e) {
319
+ this.failure = (e as Error).message
320
+ } finally {
321
+ this.busy = false
322
+ }
323
+ }
324
+ }
@@ -1,6 +1,7 @@
1
1
  import './figure-inspector.js'
2
2
  import './figure-report.js'
3
3
  import './figure-settings.js'
4
+ import './figure-history-panel.js'
4
5
 
5
6
  import { css, html, LitElement } from 'lit'
6
7
  import { customElement, property, state } from 'lit/decorators.js'
@@ -12,7 +13,7 @@ import type { BoardModel, PartModel } from './figure-source.js'
12
13
  import { DEFAULT_VIEW } from './figure-view.js'
13
14
  import type { ViewSettings } from './figure-view.js'
14
15
 
15
- type Tab = 'properties' | 'verdict' | 'view'
16
+ type Tab = 'properties' | 'verdict' | 'view' | 'history'
16
17
 
17
18
  /**
18
19
  * 저작면 오른쪽 칸 — 속성과 판정을 탭으로 겹쳐 둔다.
@@ -117,6 +118,11 @@ export class FigureSide extends localize(i18next)(LitElement) {
117
118
  곳이라 변환된 FigureSource 를 받는다. 변환은 모델러 페이지가 한 번만 해서 내려보낸다.
118
119
  */
119
120
  @property({ type: String }) figureName = ''
121
+ /** 이력 탭이 발행하고 되돌리는 데 필요한 것들. 정본은 저작면이 든다. */
122
+ @property({ type: String }) figureId?: string
123
+ @property({ type: String }) figureState?: 'draft' | 'released'
124
+ @property({ type: Number }) figureVersion = 0
125
+ @property({ type: Boolean }) dirty = false
120
126
 
121
127
  @property({ type: Object }) board?: BoardModel
122
128
  @property({ type: Array }) parts: PartModel[] = []
@@ -135,6 +141,7 @@ export class FigureSide extends localize(i18next)(LitElement) {
135
141
  ${this.renderTab('properties', i18next.t('label.properties'))}
136
142
  ${this.renderTab('verdict', i18next.t('label.maturity'), this.instances)}
137
143
  ${this.renderTab('view', i18next.t('label.view'))}
144
+ ${this.renderTab('history', i18next.t('label.history'))}
138
145
  </div>
139
146
 
140
147
  <figure-inspector
@@ -152,6 +159,17 @@ export class FigureSide extends localize(i18next)(LitElement) {
152
159
  ></figure-report>
153
160
 
154
161
  <figure-settings ?off=${this.tab !== 'view'} .view=${this.view}></figure-settings>
162
+
163
+ <!--
164
+ 이력은 감춰도 DOM 에 남긴다 — 다른 탭들과 같은 규칙이다. 떼었다 붙이면 판본을 다시 부른다.
165
+ -->
166
+ <figure-history-panel
167
+ ?off=${this.tab !== 'history'}
168
+ .figureId=${this.figureId}
169
+ .figureState=${this.figureState}
170
+ .figureVersion=${this.figureVersion}
171
+ .dirty=${this.dirty}
172
+ ></figure-history-panel>
155
173
  `
156
174
  }
157
175