@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
@@ -578,6 +578,8 @@ export class FigureModellerPage extends FigureModellerPageBase {
578
578
  ?collapse-left=${this.collapsed.left}
579
579
  ?collapse-right=${this.collapsed.right}
580
580
  @figure-rename=${(e: CustomEvent) => this.rename(e.detail.name)}
581
+ @figure-released=${(e: CustomEvent) => this.released(e.detail.figure)}
582
+ @figure-reverted=${() => this.figure?.id && this.load(this.figure.id)}
581
583
  @add-part=${(e: CustomEvent) => this.addPart(e.detail.primitive)}
582
584
  @select-part=${(e: CustomEvent) => (this.selected = e.detail.index)}
583
585
  @remove-part=${(e: CustomEvent) => this.removePart(e.detail.index)}
@@ -638,6 +640,10 @@ export class FigureModellerPage extends FigureModellerPageBase {
638
640
 
639
641
  <figure-side
640
642
  .figureName=${this.figure?.name ?? ''}
643
+ .figureId=${this.figure?.id ?? ''}
644
+ .figureState=${this.figure?.state}
645
+ .figureVersion=${this.figure?.version ?? 0}
646
+ .dirty=${this.dirty}
641
647
  .board=${this.board}
642
648
  .parts=${this.parts}
643
649
  .source=${source}
@@ -1070,6 +1076,22 @@ export class FigureModellerPage extends FigureModellerPageBase {
1070
1076
  * 그림만 보낸다. 정본을 건드리지 않으므로 미저장 표시도 켜지 않는다 — 열어 봤다는 것이 고쳤다는
1071
1077
  * 뜻이 되면 저작자가 저장할지 말지를 매번 고민한다.
1072
1078
  */
1079
+ /**
1080
+ * 발행되었다 — **정본을 든 자리가 그 사실을 받는다.**
1081
+ *
1082
+ * 이력 패널은 발행을 시키고 결과를 알릴 뿐, 이 페이지의 상태를 고쳐 쓰지 않는다. 판 번호와 상태가
1083
+ * 여기서 갱신되어야 머리줄·이력 탭이 같은 것을 본다.
1084
+ *
1085
+ * 정본(source)은 발행이 건드리지 않으므로 다시 불러오지 않는다 — 저작 중인 화면을 되감지 않는다.
1086
+ */
1087
+ private released(figure: Figure): void {
1088
+ if (!this.figure || figure?.id !== this.figure.id) {
1089
+ return
1090
+ }
1091
+
1092
+ this.figure = { ...this.figure, version: figure.version, state: figure.state, updatedAt: figure.updatedAt }
1093
+ }
1094
+
1073
1095
  private async fillThumbnail(id: string): Promise<void> {
1074
1096
  await this.updateComplete
1075
1097
 
package/client/types.ts CHANGED
@@ -30,6 +30,23 @@ export interface Figure {
30
30
  version?: number
31
31
  }
32
32
 
33
+ /**
34
+ * 발행된 판 하나.
35
+ *
36
+ * 저작 중 저장은 여기 오지 않는다 — 판본은 발행에서만 생긴다.
37
+ */
38
+ export interface FigureVersion {
39
+ version: number
40
+ /** 발행하며 남긴 말. */
41
+ comment?: string
42
+ state?: 'draft' | 'released'
43
+ updatedAt?: string
44
+ updater?: { id: string; name: string }
45
+ score?: number
46
+ triangles?: number
47
+ groups?: number
48
+ }
49
+
33
50
  export interface FigureListResult {
34
51
  items: Figure[]
35
52
  total: number
@@ -1,10 +1,19 @@
1
- import type { Figure, FigureFinding, FigureListResult, FigurePatch, FigureProposal, NewFigure } from '../types.js';
1
+ import type { Figure, FigureFinding, FigureListResult, FigurePatch, FigureProposal, FigureVersion, NewFigure } from '../types.js';
2
+ /**
3
+ * 목록을 가져온다.
4
+ *
5
+ * 걸러 보기·정렬·쪽 나누기를 **그대로 넘긴다.** 화면이 `search`·`state` 같은 이름을 따로 만들어
6
+ * filter 로 옮기던 것을 그만두었다 — 그 이름들은 격자(ox-grist)가 이미 컬럼 설정에서 만들어 주고,
7
+ * 중간에 한 벌 더 두면 격자가 아는 조건과 서버에 가는 조건이 어긋난다.
8
+ */
2
9
  export declare function fetchFigureList(params: {
3
10
  page?: number;
4
11
  limit?: number;
5
- search?: string;
6
- category?: string;
7
- state?: string;
12
+ filters?: unknown[];
13
+ sortings?: {
14
+ name: string;
15
+ desc?: boolean;
16
+ }[];
8
17
  }): Promise<FigureListResult>;
9
18
  /** 저작면에 필요한 전부 — `source` 를 포함한다. */
10
19
  export declare function fetchFigure(id: string): Promise<Figure>;
@@ -39,3 +48,14 @@ export declare function proposeFigure(request: {
39
48
  palette: string[];
40
49
  image?: File;
41
50
  }): Promise<FigureProposal>;
51
+ /**
52
+ * 발행한다 — 판 번호가 오르고, 그 순간이 판본으로 남는다.
53
+ *
54
+ * 이미 발행된 것을 다시 발행하면 서버가 거절한다. 고쳐서 다시 내려면 저장이 먼저이고, 저장은
55
+ * 발행을 푼다(초안으로 돌아온다).
56
+ */
57
+ export declare function releaseFigure(id: string, comment?: string): Promise<Figure>;
58
+ /** 옛 판을 초안으로 되살린다. 정본·속성·그림이 돌아오고 이름·설명은 그대로다. */
59
+ export declare function revertFigureVersion(id: string, version: number): Promise<SaveResult>;
60
+ /** 발행된 판들 — 최근 것부터 열 개. */
61
+ export declare function fetchFigureVersions(id: string): Promise<FigureVersion[]>;
@@ -33,6 +33,7 @@ const FIGURE_LIST_FIELDS = `
33
33
  description
34
34
  category
35
35
  state
36
+ version
36
37
  score
37
38
  triangles
38
39
  groups
@@ -41,12 +42,14 @@ const FIGURE_LIST_FIELDS = `
41
42
  updatedAt
42
43
  updater { id name }
43
44
  `;
45
+ /**
46
+ * 목록을 가져온다.
47
+ *
48
+ * 걸러 보기·정렬·쪽 나누기를 **그대로 넘긴다.** 화면이 `search`·`state` 같은 이름을 따로 만들어
49
+ * filter 로 옮기던 것을 그만두었다 — 그 이름들은 격자(ox-grist)가 이미 컬럼 설정에서 만들어 주고,
50
+ * 중간에 한 벌 더 두면 격자가 아는 조건과 서버에 가는 조건이 어긋난다.
51
+ */
44
52
  export async function fetchFigureList(params) {
45
- const filters = [];
46
- if (params.category)
47
- filters.push({ name: 'category', operator: 'eq', value: params.category });
48
- if (params.state)
49
- filters.push({ name: 'state', operator: 'eq', value: params.state });
50
53
  const response = await client.query({
51
54
  query: gql `
52
55
  query ($filters: [Filter!], $pagination: Pagination, $sortings: [Sorting!]) {
@@ -57,9 +60,9 @@ export async function fetchFigureList(params) {
57
60
  }
58
61
  `,
59
62
  variables: {
60
- filters,
63
+ filters: params.filters ?? [],
61
64
  pagination: { page: params.page ?? 1, limit: params.limit ?? 30 },
62
- sortings: [{ name: 'updatedAt', desc: true }]
65
+ sortings: params.sortings?.length ? params.sortings : [{ name: 'updatedAt', desc: true }]
63
66
  },
64
67
  fetchPolicy: 'network-only'
65
68
  });
@@ -74,7 +77,6 @@ export async function fetchFigure(id) {
74
77
  ${FIGURE_LIST_FIELDS}
75
78
  source
76
79
  properties
77
- version
78
80
  }
79
81
  }
80
82
  `,
@@ -119,7 +121,7 @@ export async function updateFigure(id, patch) {
119
121
  mutation: gql `
120
122
  mutation ($id: String!, $patch: FigurePatch!) {
121
123
  updateFigure(id: $id, patch: $patch) {
122
- figure { ${FIGURE_LIST_FIELDS} source version }
124
+ figure { ${FIGURE_LIST_FIELDS} source }
123
125
  violations { code message at }
124
126
  }
125
127
  }
@@ -170,4 +172,63 @@ export async function proposeFigure(request) {
170
172
  });
171
173
  return unwrap(response, 'proposeFigure');
172
174
  }
175
+ /**
176
+ * 발행한다 — 판 번호가 오르고, 그 순간이 판본으로 남는다.
177
+ *
178
+ * 이미 발행된 것을 다시 발행하면 서버가 거절한다. 고쳐서 다시 내려면 저장이 먼저이고, 저장은
179
+ * 발행을 푼다(초안으로 돌아온다).
180
+ */
181
+ export async function releaseFigure(id, comment) {
182
+ const response = await client.mutate({
183
+ mutation: gql `
184
+ mutation ReleaseFigure($id: String!, $comment: String) {
185
+ releaseFigure(id: $id, comment: $comment) {
186
+ id
187
+ version
188
+ state
189
+ updatedAt
190
+ }
191
+ }
192
+ `,
193
+ variables: { id, comment }
194
+ });
195
+ return unwrap(response, 'releaseFigure');
196
+ }
197
+ /** 옛 판을 초안으로 되살린다. 정본·속성·그림이 돌아오고 이름·설명은 그대로다. */
198
+ export async function revertFigureVersion(id, version) {
199
+ const response = await client.mutate({
200
+ mutation: gql `
201
+ mutation RevertFigureVersion($id: String!, $version: Float!) {
202
+ revertFigureVersion(id: $id, version: $version) {
203
+ figure { ${FIGURE_LIST_FIELDS} source }
204
+ violations { code message at }
205
+ }
206
+ }
207
+ `,
208
+ variables: { id, version }
209
+ });
210
+ return unwrap(response, 'revertFigureVersion');
211
+ }
212
+ /** 발행된 판들 — 최근 것부터 열 개. */
213
+ export async function fetchFigureVersions(id) {
214
+ const response = await client.query({
215
+ query: gql `
216
+ query FigureVersions($id: String!) {
217
+ figureVersions(id: $id) {
218
+ version
219
+ comment
220
+ state
221
+ updatedAt
222
+ updater { id name }
223
+ score
224
+ triangles
225
+ groups
226
+ }
227
+ }
228
+ `,
229
+ variables: { id },
230
+ fetchPolicy: 'network-only'
231
+ });
232
+ return unwrap(response, 'figureVersions') ?? [];
233
+ }
173
234
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../client/graphql/index.ts"],"names":[],"mappings":"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","sourcesContent":["import gql from 'graphql-tag'\nimport { client } from '@operato/graphql'\n\nimport type { Figure, FigureFinding, FigureListResult, FigurePatch, FigureProposal, NewFigure } from '../types.js'\n\n/**\n * 응답에서 값을 꺼낸다. **서버가 말한 사유를 그대로 올린다.**\n *\n * 전에는 `response.data.figures` 처럼 바로 꺼냈다. 서버가 오류를 돌려주면 `data` 가\n * 없으므로 `Cannot read properties of undefined (reading 'figures')` 가 났고, 화면에는\n * 그 문장이 그대로 떴다. 사용자는 자기가 무엇을 잘못했는지 알 수 없다 — 실제로 AI\n * 요청에서 그렇게 났다.\n *\n * 값이 `null` 인 것은 오류가 아니다. 지운 것을 물었을 때처럼 없는 것이 답인 경우가 있다.\n * 여기서 막는 것은 **`data` 자체가 없는 것**이다.\n */\nfunction unwrap<T>(response: { data?: Record<string, unknown>; errors?: readonly { message: string }[] }, field: string): T {\n const said = response.errors?.[0]?.message\n if (said) throw new Error(said)\n\n if (!response.data) {\n throw new Error(`서버가 ${field} 를 돌려주지 않았습니다.`)\n }\n\n return response.data[field] as T\n}\n\n\n/**\n * 목록에 필요한 필드만 가져온다 — `source` 는 무겁고 목록에서 쓰지 않는다.\n *\n * `score`·`triangles` 를 함께 받는 이유는 카드에 그것을 보여 주기 때문이다. 저작자가\n * 목록에서 「이 Figure 가 무거운가」를 바로 알아야 한다.\n */\nconst FIGURE_LIST_FIELDS = `\n id\n type\n name\n description\n category\n state\n score\n triangles\n groups\n thumbnail\n thumbnailUpdatedAt\n updatedAt\n updater { id name }\n`\n\nexport async function fetchFigureList(params: {\n page?: number\n limit?: number\n search?: string\n category?: string\n state?: string\n}): Promise<FigureListResult> {\n const filters: { name: string; operator: string; value: unknown }[] = []\n if (params.category) filters.push({ name: 'category', operator: 'eq', value: params.category })\n if (params.state) filters.push({ name: 'state', operator: 'eq', value: params.state })\n\n const response = await client.query({\n query: gql`\n query ($filters: [Filter!], $pagination: Pagination, $sortings: [Sorting!]) {\n figures(filters: $filters, pagination: $pagination, sortings: $sortings) {\n items { ${FIGURE_LIST_FIELDS} }\n total\n }\n }\n `,\n variables: {\n filters,\n pagination: { page: params.page ?? 1, limit: params.limit ?? 30 },\n sortings: [{ name: 'updatedAt', desc: true }]\n },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<FigureListResult>(response, 'figures')\n}\n\n/** 저작면에 필요한 전부 — `source` 를 포함한다. */\nexport async function fetchFigure(id: string): Promise<Figure> {\n const response = await client.query({\n query: gql`\n query ($id: String!) {\n figure(id: $id) {\n ${FIGURE_LIST_FIELDS}\n source\n properties\n version\n }\n }\n `,\n variables: { id },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<Figure>(response, 'figure')\n}\n\n/**\n * 이미 쓰이고 있는 타입 이름.\n *\n * `type` 은 만든 뒤에 고칠 수 없으므로 **만들기 전에** 알려 줘야 한다. 저장을 눌러\n * 거절당하고 나서 아는 것은 늦다.\n */\nexport async function fetchFigureTypeNames(): Promise<string[]> {\n const response = await client.query({\n query: gql`\n query {\n figureTypeNames\n }\n `,\n fetchPolicy: 'network-only'\n })\n\n return unwrap<string[] | null>(response, 'figureTypeNames') ?? []\n}\n\nexport interface SaveResult {\n figure: Figure\n /** 막지 않는 발견. 저장은 됐지만 화면이 보여 줘야 한다. */\n violations: FigureFinding[]\n}\n\nexport async function createFigure(figure: NewFigure): Promise<SaveResult> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($figure: NewFigure!) {\n createFigure(figure: $figure) {\n figure { ${FIGURE_LIST_FIELDS} source }\n violations { code message at }\n }\n }\n `,\n variables: { figure }\n })\n\n return unwrap<SaveResult>(response, 'createFigure')\n}\n\nexport async function updateFigure(id: string, patch: FigurePatch): Promise<SaveResult> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($id: String!, $patch: FigurePatch!) {\n updateFigure(id: $id, patch: $patch) {\n figure { ${FIGURE_LIST_FIELDS} source version }\n violations { code message at }\n }\n }\n `,\n variables: { id, patch }\n })\n\n return unwrap<SaveResult>(response, 'updateFigure')\n}\n\nexport async function deleteFigure(id: string): Promise<boolean> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($id: String!) {\n deleteFigure(id: $id)\n }\n `,\n variables: { id }\n })\n\n return unwrap<boolean>(response, 'deleteFigure')\n}\n\n/**\n * 저작 보조에게 후보를 시킨다.\n *\n * 팔레트를 **클라이언트가 보낸다.** 서버는 팔레트를 모른다 — things-scene 이 갖고\n * 있는데 그 패키지의 node 조건은 팔레트가 없는 번들로 간다. 그리는 쪽이 어떤 토큰을\n * 풀 수 있는지 아는 유일한 자리이므로 그쪽이 보낸다.\n *\n * 돌아오는 것은 **후보**다. 저장되지 않았다 — 저작자가 받아야 정본이 된다.\n */\nexport async function proposeFigure(request: {\n prompt: string\n base?: string\n type?: string\n palette: string[]\n image?: File\n}): Promise<FigureProposal> {\n const response = await client.mutate({\n mutation: gql`\n mutation ProposeFigure($request: ProposeRequest!) {\n proposeFigure(request: $request) {\n source\n score\n grade\n triangles\n groups\n attempts\n violations {\n code\n message\n }\n }\n }\n `,\n variables: { request }\n })\n\n return unwrap<FigureProposal>(response, 'proposeFigure')\n}\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../client/graphql/index.ts"],"names":[],"mappings":"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","sourcesContent":["import gql from 'graphql-tag'\nimport { client } from '@operato/graphql'\n\nimport type {\n Figure,\n FigureFinding,\n FigureListResult,\n FigurePatch,\n FigureProposal,\n FigureVersion,\n NewFigure\n} from '../types.js'\n\n/**\n * 응답에서 값을 꺼낸다. **서버가 말한 사유를 그대로 올린다.**\n *\n * 전에는 `response.data.figures` 처럼 바로 꺼냈다. 서버가 오류를 돌려주면 `data` 가\n * 없으므로 `Cannot read properties of undefined (reading 'figures')` 가 났고, 화면에는\n * 그 문장이 그대로 떴다. 사용자는 자기가 무엇을 잘못했는지 알 수 없다 — 실제로 AI\n * 요청에서 그렇게 났다.\n *\n * 값이 `null` 인 것은 오류가 아니다. 지운 것을 물었을 때처럼 없는 것이 답인 경우가 있다.\n * 여기서 막는 것은 **`data` 자체가 없는 것**이다.\n */\nfunction unwrap<T>(response: { data?: Record<string, unknown>; errors?: readonly { message: string }[] }, field: string): T {\n const said = response.errors?.[0]?.message\n if (said) throw new Error(said)\n\n if (!response.data) {\n throw new Error(`서버가 ${field} 를 돌려주지 않았습니다.`)\n }\n\n return response.data[field] as T\n}\n\n\n/**\n * 목록에 필요한 필드만 가져온다 — `source` 는 무겁고 목록에서 쓰지 않는다.\n *\n * `score`·`triangles` 를 함께 받는 이유는 카드에 그것을 보여 주기 때문이다. 저작자가\n * 목록에서 「이 Figure 가 무거운가」를 바로 알아야 한다.\n */\nconst FIGURE_LIST_FIELDS = `\n id\n type\n name\n description\n category\n state\n version\n score\n triangles\n groups\n thumbnail\n thumbnailUpdatedAt\n updatedAt\n updater { id name }\n`\n\n/**\n * 목록을 가져온다.\n *\n * 걸러 보기·정렬·쪽 나누기를 **그대로 넘긴다.** 화면이 `search`·`state` 같은 이름을 따로 만들어\n * filter 로 옮기던 것을 그만두었다 — 그 이름들은 격자(ox-grist)가 이미 컬럼 설정에서 만들어 주고,\n * 중간에 한 벌 더 두면 격자가 아는 조건과 서버에 가는 조건이 어긋난다.\n */\nexport async function fetchFigureList(params: {\n page?: number\n limit?: number\n filters?: unknown[]\n sortings?: { name: string; desc?: boolean }[]\n}): Promise<FigureListResult> {\n const response = await client.query({\n query: gql`\n query ($filters: [Filter!], $pagination: Pagination, $sortings: [Sorting!]) {\n figures(filters: $filters, pagination: $pagination, sortings: $sortings) {\n items { ${FIGURE_LIST_FIELDS} }\n total\n }\n }\n `,\n variables: {\n filters: params.filters ?? [],\n pagination: { page: params.page ?? 1, limit: params.limit ?? 30 },\n sortings: params.sortings?.length ? params.sortings : [{ name: 'updatedAt', desc: true }]\n },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<FigureListResult>(response, 'figures')\n}\n\n/** 저작면에 필요한 전부 — `source` 를 포함한다. */\nexport async function fetchFigure(id: string): Promise<Figure> {\n const response = await client.query({\n query: gql`\n query ($id: String!) {\n figure(id: $id) {\n ${FIGURE_LIST_FIELDS}\n source\n properties\n }\n }\n `,\n variables: { id },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<Figure>(response, 'figure')\n}\n\n/**\n * 이미 쓰이고 있는 타입 이름.\n *\n * `type` 은 만든 뒤에 고칠 수 없으므로 **만들기 전에** 알려 줘야 한다. 저장을 눌러\n * 거절당하고 나서 아는 것은 늦다.\n */\nexport async function fetchFigureTypeNames(): Promise<string[]> {\n const response = await client.query({\n query: gql`\n query {\n figureTypeNames\n }\n `,\n fetchPolicy: 'network-only'\n })\n\n return unwrap<string[] | null>(response, 'figureTypeNames') ?? []\n}\n\nexport interface SaveResult {\n figure: Figure\n /** 막지 않는 발견. 저장은 됐지만 화면이 보여 줘야 한다. */\n violations: FigureFinding[]\n}\n\nexport async function createFigure(figure: NewFigure): Promise<SaveResult> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($figure: NewFigure!) {\n createFigure(figure: $figure) {\n figure { ${FIGURE_LIST_FIELDS} source }\n violations { code message at }\n }\n }\n `,\n variables: { figure }\n })\n\n return unwrap<SaveResult>(response, 'createFigure')\n}\n\nexport async function updateFigure(id: string, patch: FigurePatch): Promise<SaveResult> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($id: String!, $patch: FigurePatch!) {\n updateFigure(id: $id, patch: $patch) {\n figure { ${FIGURE_LIST_FIELDS} source }\n violations { code message at }\n }\n }\n `,\n variables: { id, patch }\n })\n\n return unwrap<SaveResult>(response, 'updateFigure')\n}\n\nexport async function deleteFigure(id: string): Promise<boolean> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($id: String!) {\n deleteFigure(id: $id)\n }\n `,\n variables: { id }\n })\n\n return unwrap<boolean>(response, 'deleteFigure')\n}\n\n/**\n * 저작 보조에게 후보를 시킨다.\n *\n * 팔레트를 **클라이언트가 보낸다.** 서버는 팔레트를 모른다 — things-scene 이 갖고\n * 있는데 그 패키지의 node 조건은 팔레트가 없는 번들로 간다. 그리는 쪽이 어떤 토큰을\n * 풀 수 있는지 아는 유일한 자리이므로 그쪽이 보낸다.\n *\n * 돌아오는 것은 **후보**다. 저장되지 않았다 — 저작자가 받아야 정본이 된다.\n */\nexport async function proposeFigure(request: {\n prompt: string\n base?: string\n type?: string\n palette: string[]\n image?: File\n}): Promise<FigureProposal> {\n const response = await client.mutate({\n mutation: gql`\n mutation ProposeFigure($request: ProposeRequest!) {\n proposeFigure(request: $request) {\n source\n score\n grade\n triangles\n groups\n attempts\n violations {\n code\n message\n }\n }\n }\n `,\n variables: { request }\n })\n\n return unwrap<FigureProposal>(response, 'proposeFigure')\n}\n\n/**\n * 발행한다 — 판 번호가 오르고, 그 순간이 판본으로 남는다.\n *\n * 이미 발행된 것을 다시 발행하면 서버가 거절한다. 고쳐서 다시 내려면 저장이 먼저이고, 저장은\n * 발행을 푼다(초안으로 돌아온다).\n */\nexport async function releaseFigure(id: string, comment?: string): Promise<Figure> {\n const response = await client.mutate({\n mutation: gql`\n mutation ReleaseFigure($id: String!, $comment: String) {\n releaseFigure(id: $id, comment: $comment) {\n id\n version\n state\n updatedAt\n }\n }\n `,\n variables: { id, comment }\n })\n\n return unwrap<Figure>(response, 'releaseFigure')\n}\n\n/** 옛 판을 초안으로 되살린다. 정본·속성·그림이 돌아오고 이름·설명은 그대로다. */\nexport async function revertFigureVersion(id: string, version: number): Promise<SaveResult> {\n const response = await client.mutate({\n mutation: gql`\n mutation RevertFigureVersion($id: String!, $version: Float!) {\n revertFigureVersion(id: $id, version: $version) {\n figure { ${FIGURE_LIST_FIELDS} source }\n violations { code message at }\n }\n }\n `,\n variables: { id, version }\n })\n\n return unwrap<SaveResult>(response, 'revertFigureVersion')\n}\n\n/** 발행된 판들 — 최근 것부터 열 개. */\nexport async function fetchFigureVersions(id: string): Promise<FigureVersion[]> {\n const response = await client.query({\n query: gql`\n query FigureVersions($id: String!) {\n figureVersions(id: $id) {\n version\n comment\n state\n updatedAt\n updater { id name }\n score\n triangles\n groups\n }\n }\n `,\n variables: { id },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<FigureVersion[]>(response, 'figureVersions') ?? []\n}\n"]}
@@ -0,0 +1,42 @@
1
+ import '@material/web/icon/icon.js';
2
+ import { LitElement } from 'lit';
3
+ declare const FigureHistoryPanel_base: (new (...args: any[]) => LitElement) & typeof LitElement;
4
+ /**
5
+ * 이력 — **발행하는 자리이자, 발행된 판들을 보는 자리.**
6
+ *
7
+ * ## 왜 저장 옆이 아니라 여기인가
8
+ *
9
+ * 저장은 하루에 수십 번이고 발행은 드물다. 두 단추를 나란히 두면 저장하려다 발행을 누른다.
10
+ * 그리고 발행은 **소비처에게 하는 약속**이라, 무엇을 약속하는지(지금 몇 판인가 · 저장 안 한 것이
11
+ * 있나 · 지난 판이 무엇이었나)를 함께 보여 주는 자리에 있어야 한다.
12
+ *
13
+ * ## 저장하지 않은 것이 있으면 발행하지 않는다
14
+ *
15
+ * 발행은 **저장된 것**을 내보낸다. 화면에 고친 것이 남아 있는 채로 발행하면, 저작자가 보고 있는
16
+ * 것과 약속한 것이 다르다. 그래서 막고, 왜 막혔는지 적는다.
17
+ *
18
+ * ## 되돌리기는 초안으로 돌아온다
19
+ *
20
+ * 옛 판의 정본·속성·그림이 돌아오고 이름·설명은 그대로다. 되살렸다고 그 판이 다시 발행되지는
21
+ * 않는다 — 다시 내보내려면 발행을 한 번 더 하고, 그때 번호는 이미 쓴 다음 번호를 받는다.
22
+ */
23
+ export declare class FigureHistoryPanel extends FigureHistoryPanel_base {
24
+ static styles: import("lit").CSSResult;
25
+ /** 지금 열려 있는 도형. 없으면(새로 만드는 중) 발행할 것이 없다. */
26
+ figureId?: string;
27
+ figureState?: 'draft' | 'released';
28
+ figureVersion: number;
29
+ /** 저장하지 않은 것이 있나 — 있으면 발행을 막는다. */
30
+ dirty: boolean;
31
+ private versions;
32
+ private busy;
33
+ private failure;
34
+ updated(changes: Map<string, unknown>): void;
35
+ private reload;
36
+ render(): import("lit-html").TemplateResult<1>;
37
+ private renderRelease;
38
+ private renderVersion;
39
+ private release;
40
+ private revert;
41
+ }
42
+ export {};
@@ -0,0 +1,343 @@
1
+ import { __decorate, __metadata } from "tslib";
2
+ import '@material/web/icon/icon.js';
3
+ import { css, html, LitElement, nothing } from 'lit';
4
+ import { customElement, property, state } from 'lit/decorators.js';
5
+ import { i18next, localize } from '@operato/i18n';
6
+ import { fetchFigureVersions, releaseFigure, revertFigureVersion } from '../graphql/index.js';
7
+ /**
8
+ * 이력 — **발행하는 자리이자, 발행된 판들을 보는 자리.**
9
+ *
10
+ * ## 왜 저장 옆이 아니라 여기인가
11
+ *
12
+ * 저장은 하루에 수십 번이고 발행은 드물다. 두 단추를 나란히 두면 저장하려다 발행을 누른다.
13
+ * 그리고 발행은 **소비처에게 하는 약속**이라, 무엇을 약속하는지(지금 몇 판인가 · 저장 안 한 것이
14
+ * 있나 · 지난 판이 무엇이었나)를 함께 보여 주는 자리에 있어야 한다.
15
+ *
16
+ * ## 저장하지 않은 것이 있으면 발행하지 않는다
17
+ *
18
+ * 발행은 **저장된 것**을 내보낸다. 화면에 고친 것이 남아 있는 채로 발행하면, 저작자가 보고 있는
19
+ * 것과 약속한 것이 다르다. 그래서 막고, 왜 막혔는지 적는다.
20
+ *
21
+ * ## 되돌리기는 초안으로 돌아온다
22
+ *
23
+ * 옛 판의 정본·속성·그림이 돌아오고 이름·설명은 그대로다. 되살렸다고 그 판이 다시 발행되지는
24
+ * 않는다 — 다시 내보내려면 발행을 한 번 더 하고, 그때 번호는 이미 쓴 다음 번호를 받는다.
25
+ */
26
+ let FigureHistoryPanel = class FigureHistoryPanel extends localize(i18next)(LitElement) {
27
+ constructor() {
28
+ super(...arguments);
29
+ this.figureVersion = 0;
30
+ /** 저장하지 않은 것이 있나 — 있으면 발행을 막는다. */
31
+ this.dirty = false;
32
+ this.versions = [];
33
+ this.busy = false;
34
+ this.failure = '';
35
+ }
36
+ static { this.styles = css `
37
+ :host {
38
+ display: flex;
39
+ flex-direction: column;
40
+ min-height: 0;
41
+ overflow-y: auto;
42
+ background-color: var(--md-sys-color-surface-container-lowest);
43
+ color: var(--md-sys-color-on-surface);
44
+ font: var(--label-font, inherit);
45
+ font-size: 0.76rem;
46
+ }
47
+
48
+ :host([off]) {
49
+ display: none;
50
+ }
51
+
52
+ section {
53
+ padding: var(--spacing-medium, 8px) var(--spacing-large, 12px);
54
+ border-bottom: 1px solid var(--md-sys-color-outline-variant);
55
+ }
56
+
57
+ h3 {
58
+ margin: 0 0 6px 0;
59
+ font-size: var(--ui-t-micro, 11px);
60
+ font-weight: 700;
61
+ letter-spacing: 0.04em;
62
+ text-transform: uppercase;
63
+ color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));
64
+ }
65
+
66
+ /* 지금 무엇인가 — 발행을 누르기 전에 읽는 한 줄 */
67
+ div[now] {
68
+ display: flex;
69
+ align-items: center;
70
+ gap: 6px;
71
+ margin-bottom: 8px;
72
+ }
73
+
74
+ span[state] {
75
+ padding: 2px 8px;
76
+ border-radius: 10px;
77
+ font-size: 0.7rem;
78
+ font-weight: 700;
79
+ color: var(--md-sys-color-on-secondary-container);
80
+ background-color: var(--md-sys-color-secondary-container);
81
+ }
82
+
83
+ span[state][released] {
84
+ color: var(--md-sys-color-on-primary);
85
+ background-color: var(--md-sys-color-primary);
86
+ }
87
+
88
+ span[ver] {
89
+ font-family: var(--mono-font, monospace);
90
+ color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));
91
+ }
92
+
93
+ input,
94
+ button {
95
+ font: var(--input-field-font, inherit);
96
+ font-size: 0.76rem;
97
+ }
98
+
99
+ input[note] {
100
+ width: 100%;
101
+ box-sizing: border-box;
102
+ padding: 5px 8px;
103
+ color: var(--md-sys-color-on-surface);
104
+ background-color: var(--md-sys-color-surface-container-lowest);
105
+ border: 1px solid var(--md-sys-color-outline-variant);
106
+ border-radius: 6px;
107
+ }
108
+
109
+ button[release] {
110
+ width: 100%;
111
+ margin-top: 6px;
112
+ padding: 6px 0;
113
+ font-weight: 700;
114
+ color: var(--ui-on-accent, #fff);
115
+ background-color: var(--md-sys-color-primary);
116
+ border: 0;
117
+ border-radius: 6px;
118
+ cursor: pointer;
119
+ }
120
+
121
+ button[release][disabled] {
122
+ opacity: 0.45;
123
+ cursor: default;
124
+ }
125
+
126
+ /* 왜 못 누르는지 — 눌러 보고 알게 하지 않는다 */
127
+ p[why] {
128
+ margin: 6px 0 0 0;
129
+ font-size: 0.7rem;
130
+ line-height: 1.5;
131
+ color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));
132
+ }
133
+
134
+ p[failed] {
135
+ margin: 6px 0 0 0;
136
+ font-size: 0.7rem;
137
+ line-height: 1.5;
138
+ color: var(--md-sys-color-error);
139
+ }
140
+
141
+ ul {
142
+ margin: 0;
143
+ padding: 0;
144
+ list-style: none;
145
+ }
146
+
147
+ li {
148
+ display: grid;
149
+ grid-template-columns: auto 1fr auto;
150
+ gap: 4px 8px;
151
+ align-items: baseline;
152
+ padding: 7px 0;
153
+ border-bottom: 1px dashed var(--md-sys-color-outline-variant);
154
+ }
155
+
156
+ li:last-child {
157
+ border-bottom: 0;
158
+ }
159
+
160
+ li span[no] {
161
+ font-family: var(--mono-font, monospace);
162
+ font-weight: 700;
163
+ }
164
+
165
+ li span[when] {
166
+ grid-column: 2 / 4;
167
+ font-size: 0.7rem;
168
+ color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));
169
+ }
170
+
171
+ li span[comment] {
172
+ grid-column: 1 / 4;
173
+ line-height: 1.5;
174
+ }
175
+
176
+ li button[revert] {
177
+ padding: 2px 8px;
178
+ color: var(--md-sys-color-on-surface);
179
+ background: none;
180
+ border: 1px solid var(--md-sys-color-outline-variant);
181
+ border-radius: 6px;
182
+ cursor: pointer;
183
+ }
184
+
185
+ li button[revert]:hover {
186
+ color: var(--md-sys-color-on-primary-container);
187
+ background-color: var(--md-sys-color-primary-container);
188
+ border-color: transparent;
189
+ }
190
+
191
+ p[quiet] {
192
+ margin: 0;
193
+ line-height: 1.6;
194
+ color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));
195
+ }
196
+ `; }
197
+ updated(changes) {
198
+ if (changes.has('figureId') || changes.has('figureVersion')) {
199
+ void this.reload();
200
+ }
201
+ }
202
+ async reload() {
203
+ if (!this.figureId) {
204
+ this.versions = [];
205
+ return;
206
+ }
207
+ try {
208
+ this.versions = await fetchFigureVersions(this.figureId);
209
+ this.failure = '';
210
+ }
211
+ catch (e) {
212
+ this.failure = e.message;
213
+ }
214
+ }
215
+ render() {
216
+ return html `
217
+ <section>
218
+ <h3>${i18next.t('label.release')}</h3>
219
+ ${this.figureId ? this.renderRelease() : html `<p quiet>${i18next.t('text.save-the-figure-before-releasing')}</p>`}
220
+ </section>
221
+
222
+ <section>
223
+ <h3>${i18next.t('label.released-versions')}</h3>
224
+ ${this.versions.length
225
+ ? html `<ul>
226
+ ${this.versions.map(version => this.renderVersion(version))}
227
+ </ul>`
228
+ : html `<p quiet>${i18next.t('text.nothing-released-yet')}</p>`}
229
+ </section>
230
+ `;
231
+ }
232
+ renderRelease() {
233
+ const released = this.figureState === 'released';
234
+ /* 막는 이유는 하나씩 말한다. 「발행할 수 없습니다」로 뭉치면 무엇을 하면 되는지 모른다. */
235
+ const why = this.dirty
236
+ ? i18next.t('text.release-needs-a-save-first')
237
+ : released
238
+ ? i18next.t('text.already-released-edit-to-release-again')
239
+ : '';
240
+ return html `
241
+ <div now>
242
+ <span state ?released=${released}>${i18next.t(released ? 'label.released' : 'label.draft')}</span>
243
+ <span ver>${i18next.t('text.version-number', { version: this.figureVersion ?? 0 })}</span>
244
+ </div>
245
+
246
+ <input note placeholder=${i18next.t('text.what-changed-in-this-release')} ?disabled=${!!why || this.busy} />
247
+
248
+ <button release ?disabled=${!!why || this.busy} @click=${() => this.release()}>
249
+ ${this.busy ? i18next.t('text.releasing') : i18next.t('button.release')}
250
+ </button>
251
+
252
+ ${why ? html `<p why>${why}</p>` : nothing}
253
+ ${this.failure ? html `<p failed>${this.failure}</p>` : nothing}
254
+ `;
255
+ }
256
+ renderVersion(version) {
257
+ return html `
258
+ <li>
259
+ <span no>${i18next.t('text.version-number', { version: version.version })}</span>
260
+ <span when>
261
+ ${version.updatedAt ? new Date(version.updatedAt).toLocaleString() : ''}
262
+ ${version.updater?.name ? ` · ${version.updater.name}` : ''}
263
+ </span>
264
+ <button revert ?disabled=${this.busy} @click=${() => this.revert(version.version)}>
265
+ ${i18next.t('button.revert')}
266
+ </button>
267
+ ${version.comment ? html `<span comment>${version.comment}</span>` : nothing}
268
+ </li>
269
+ `;
270
+ }
271
+ async release() {
272
+ const input = this.renderRoot.querySelector('input[note]');
273
+ const comment = input?.value?.trim();
274
+ this.busy = true;
275
+ this.failure = '';
276
+ try {
277
+ const figure = await releaseFigure(this.figureId, comment || undefined);
278
+ if (input)
279
+ input.value = '';
280
+ /* 정본을 든 자리가 갱신한다 — 여기서 페이지 상태를 고쳐 쓰지 않는다. */
281
+ this.dispatchEvent(new CustomEvent('figure-released', { detail: { figure }, bubbles: true, composed: true }));
282
+ await this.reload();
283
+ }
284
+ catch (e) {
285
+ this.failure = e.message;
286
+ }
287
+ finally {
288
+ this.busy = false;
289
+ }
290
+ }
291
+ async revert(version) {
292
+ /* 되돌아갈 수 없는 조작은 아니지만(판본은 남는다), 지금 화면의 것이 덮인다 — 이름으로 확인한다. */
293
+ if (!confirm(i18next.t('text.revert-to-version-confirm', { version }))) {
294
+ return;
295
+ }
296
+ this.busy = true;
297
+ this.failure = '';
298
+ try {
299
+ const result = await revertFigureVersion(this.figureId, version);
300
+ this.dispatchEvent(new CustomEvent('figure-reverted', { detail: result, bubbles: true, composed: true }));
301
+ await this.reload();
302
+ }
303
+ catch (e) {
304
+ this.failure = e.message;
305
+ }
306
+ finally {
307
+ this.busy = false;
308
+ }
309
+ }
310
+ };
311
+ __decorate([
312
+ property({ type: String }),
313
+ __metadata("design:type", String)
314
+ ], FigureHistoryPanel.prototype, "figureId", void 0);
315
+ __decorate([
316
+ property({ type: String }),
317
+ __metadata("design:type", String)
318
+ ], FigureHistoryPanel.prototype, "figureState", void 0);
319
+ __decorate([
320
+ property({ type: Number }),
321
+ __metadata("design:type", Object)
322
+ ], FigureHistoryPanel.prototype, "figureVersion", void 0);
323
+ __decorate([
324
+ property({ type: Boolean }),
325
+ __metadata("design:type", Object)
326
+ ], FigureHistoryPanel.prototype, "dirty", void 0);
327
+ __decorate([
328
+ state(),
329
+ __metadata("design:type", Array)
330
+ ], FigureHistoryPanel.prototype, "versions", void 0);
331
+ __decorate([
332
+ state(),
333
+ __metadata("design:type", Object)
334
+ ], FigureHistoryPanel.prototype, "busy", void 0);
335
+ __decorate([
336
+ state(),
337
+ __metadata("design:type", Object)
338
+ ], FigureHistoryPanel.prototype, "failure", void 0);
339
+ FigureHistoryPanel = __decorate([
340
+ customElement('figure-history-panel')
341
+ ], FigureHistoryPanel);
342
+ export { FigureHistoryPanel };
343
+ //# sourceMappingURL=figure-history-panel.js.map