@things-factory/integration-ui 10.0.0-zeta.8 → 10.0.0
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/pages/scenario.ts +125 -3
- package/client/viewparts/scenario-instance-log-view.ts +26 -3
- package/client/viewparts/scenario-monitor.ts +20 -7
- package/client/viewparts/scenario-versions.ts +334 -0
- package/dist-client/pages/scenario.d.ts +4 -0
- package/dist-client/pages/scenario.js +123 -5
- package/dist-client/pages/scenario.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/viewparts/scenario-instance-log-view.js +26 -3
- package/dist-client/viewparts/scenario-instance-log-view.js.map +1 -1
- package/dist-client/viewparts/scenario-monitor.d.ts +1 -1
- package/dist-client/viewparts/scenario-monitor.js +20 -7
- package/dist-client/viewparts/scenario-monitor.js.map +1 -1
- package/dist-client/viewparts/scenario-versions.d.ts +31 -0
- package/dist-client/viewparts/scenario-versions.js +331 -0
- package/dist-client/viewparts/scenario-versions.js.map +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +18 -18
- package/translations/en.json +22 -1
- package/translations/ja.json +22 -1
- package/translations/ko.json +22 -1
- package/translations/ms.json +22 -1
- package/translations/zh.json +22 -1
package/client/pages/scenario.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import '@material/web/icon/icon.js'
|
|
1
2
|
import '@operato/data-grist'
|
|
2
3
|
import './scenario-detail.js'
|
|
3
4
|
import './scenario-importer.js'
|
|
5
|
+
import '../viewparts/scenario-versions.js'
|
|
4
6
|
|
|
5
7
|
import gql from 'graphql-tag'
|
|
6
8
|
import { css, html } from 'lit'
|
|
@@ -9,6 +11,7 @@ import { DataGrist } from '@operato/data-grist/ox-grist.js'
|
|
|
9
11
|
import { client } from '@operato/graphql'
|
|
10
12
|
import { i18next, localize } from '@operato/i18n'
|
|
11
13
|
import { notify, openPopup } from '@operato/layout'
|
|
14
|
+
import { OxPrompt } from '@operato/popup/ox-prompt.js'
|
|
12
15
|
import { navigate, PageView } from '@operato/shell'
|
|
13
16
|
import { CommonButtonStyles, CommonGristStyles, ScrollbarStyles } from '@operato/styles'
|
|
14
17
|
import { isMobileDevice } from '@operato/utils'
|
|
@@ -249,6 +252,69 @@ export class Scenario extends p13n(localize(i18next)(PageView)) { static get s
|
|
|
249
252
|
sortable: true,
|
|
250
253
|
width: 60
|
|
251
254
|
},
|
|
255
|
+
{
|
|
256
|
+
type: 'string',
|
|
257
|
+
name: 'publishState',
|
|
258
|
+
header: i18next.t('field.publish state'),
|
|
259
|
+
record: {
|
|
260
|
+
editable: false,
|
|
261
|
+
align: 'center',
|
|
262
|
+
// released → 라벨, draft → 'release' 버튼(클릭 시 릴리즈). 별도 버튼 컬럼 없이 상태 컬럼에서 처리.
|
|
263
|
+
renderer: (value, column, record) => {
|
|
264
|
+
if (!record || !record.id) return html``
|
|
265
|
+
if (value === 'released') {
|
|
266
|
+
return html`
|
|
267
|
+
<span
|
|
268
|
+
style="display:inline-flex;align-items:center;gap:3px;color:var(--md-sys-color-primary);font-size:11px;opacity:0.85;"
|
|
269
|
+
>
|
|
270
|
+
<md-icon style="font-size:15px;">check_circle</md-icon>${i18next.t('label.released')}
|
|
271
|
+
</span>
|
|
272
|
+
`
|
|
273
|
+
}
|
|
274
|
+
return html`
|
|
275
|
+
<button
|
|
276
|
+
style="cursor:pointer;display:inline-flex;align-items:center;gap:3px;font:inherit;font-size:11px;color:var(--md-sys-color-on-primary);background:var(--md-sys-color-primary);border:none;border-radius:999px;padding:2px 11px;"
|
|
277
|
+
title=${i18next.t('button.release')}
|
|
278
|
+
@click=${(e: Event) => {
|
|
279
|
+
e.stopPropagation()
|
|
280
|
+
this.releaseScenario(record)
|
|
281
|
+
}}
|
|
282
|
+
>
|
|
283
|
+
<md-icon style="font-size:15px;">publish</md-icon>${i18next.t('button.release')}
|
|
284
|
+
</button>
|
|
285
|
+
`
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
width: 100
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
type: 'number',
|
|
292
|
+
name: 'version',
|
|
293
|
+
header: i18next.t('field.version'),
|
|
294
|
+
record: {
|
|
295
|
+
editable: false,
|
|
296
|
+
align: 'center',
|
|
297
|
+
// 버전 번호 옆에 history 아이콘 — 클릭 시 버전 이력 열기(별도 컬럼 없이 version 컬럼 재사용).
|
|
298
|
+
renderer: (value, column, record) => {
|
|
299
|
+
if (!record || !record.id) return html``
|
|
300
|
+
return html`
|
|
301
|
+
<span style="display:inline-flex;align-items:center;justify-content:center;gap:4px;">
|
|
302
|
+
v${value ?? 0}
|
|
303
|
+
<md-icon
|
|
304
|
+
style="cursor:pointer;font-size:17px;color:var(--md-sys-color-primary);"
|
|
305
|
+
title=${i18next.t('button.version history')}
|
|
306
|
+
@click=${(e: Event) => {
|
|
307
|
+
e.stopPropagation()
|
|
308
|
+
this.openVersionHistory(record)
|
|
309
|
+
}}
|
|
310
|
+
>history</md-icon
|
|
311
|
+
>
|
|
312
|
+
</span>
|
|
313
|
+
`
|
|
314
|
+
}
|
|
315
|
+
},
|
|
316
|
+
width: 82
|
|
317
|
+
},
|
|
252
318
|
{ type: 'object',
|
|
253
319
|
name: 'updater',
|
|
254
320
|
header: i18next.t('field.updater'),
|
|
@@ -286,6 +352,8 @@ export class Scenario extends p13n(localize(i18next)(PageView)) { static get s
|
|
|
286
352
|
description
|
|
287
353
|
active
|
|
288
354
|
state
|
|
355
|
+
publishState
|
|
356
|
+
version
|
|
289
357
|
iteration
|
|
290
358
|
schedule
|
|
291
359
|
scheduleId
|
|
@@ -316,7 +384,8 @@ export class Scenario extends p13n(localize(i18next)(PageView)) { static get s
|
|
|
316
384
|
variables: { filters,
|
|
317
385
|
pagination: { page, limit },
|
|
318
386
|
sortings
|
|
319
|
-
}
|
|
387
|
+
},
|
|
388
|
+
fetchPolicy: 'no-cache'
|
|
320
389
|
})
|
|
321
390
|
|
|
322
391
|
return { total: response.data.responses.total || 0,
|
|
@@ -406,13 +475,66 @@ export class Scenario extends p13n(localize(i18next)(PageView)) { static get s
|
|
|
406
475
|
}
|
|
407
476
|
}
|
|
408
477
|
|
|
478
|
+
async releaseScenario(record) {
|
|
479
|
+
// 릴리즈 코멘트(선택) — OxPrompt.prompt(모달, native prompt 대체). 비우면 서버가 diff 요약으로 자동 채움.
|
|
480
|
+
const input = await OxPrompt.prompt({
|
|
481
|
+
type: 'question',
|
|
482
|
+
title: String(i18next.t('button.release')),
|
|
483
|
+
text: String(i18next.t('text.release comment optional')),
|
|
484
|
+
multiline: true,
|
|
485
|
+
confirmButton: { text: String(i18next.t('button.release')) },
|
|
486
|
+
cancelButton: { text: String(i18next.t('button.cancel')) }
|
|
487
|
+
})
|
|
488
|
+
if (input === null) return
|
|
489
|
+
const comment = input.trim() || null
|
|
490
|
+
|
|
491
|
+
const { errors } = await client.mutate({
|
|
492
|
+
mutation: gql`
|
|
493
|
+
mutation ($id: String!, $comment: String) {
|
|
494
|
+
releaseScenario(id: $id, comment: $comment) {
|
|
495
|
+
id
|
|
496
|
+
version
|
|
497
|
+
publishState
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
`,
|
|
501
|
+
variables: { id: record.id, comment: comment || null }
|
|
502
|
+
})
|
|
503
|
+
|
|
504
|
+
if (!errors) {
|
|
505
|
+
notify({ message: i18next.t('text.info_x_successfully', { x: i18next.t('button.release') }) })
|
|
506
|
+
this.grist.fetch()
|
|
507
|
+
} else {
|
|
508
|
+
notify({ level: 'error', message: errors.map(error => error.message).join('\n') })
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
openVersionHistory(record) {
|
|
513
|
+
openPopup(
|
|
514
|
+
html`
|
|
515
|
+
<scenario-versions
|
|
516
|
+
.scenarioId=${record.id}
|
|
517
|
+
.scenarioName=${record.name}
|
|
518
|
+
.onReverted=${() => this.grist.fetch()}
|
|
519
|
+
@reverted=${() => this.grist.fetch()}
|
|
520
|
+
></scenario-versions>
|
|
521
|
+
`,
|
|
522
|
+
{
|
|
523
|
+
backdrop: true,
|
|
524
|
+
size: 'medium',
|
|
525
|
+
title: i18next.t('title.version history')
|
|
526
|
+
}
|
|
527
|
+
)
|
|
528
|
+
}
|
|
529
|
+
|
|
409
530
|
async startScenario(record) { var { data, errors } = await client.mutate({ mutation: gql`
|
|
410
|
-
mutation ($scenarioName: String!, $instanceName: String) { startScenario(scenarioName: $scenarioName, instanceName: $instanceName) { state
|
|
531
|
+
mutation ($scenarioName: String!, $instanceName: String, $mode: String) { startScenario(scenarioName: $scenarioName, instanceName: $instanceName, mode: $mode) { state
|
|
411
532
|
}
|
|
412
533
|
}
|
|
413
534
|
`,
|
|
414
535
|
variables: { scenarioName: record.name,
|
|
415
|
-
instanceName: record.name
|
|
536
|
+
instanceName: record.name,
|
|
537
|
+
mode: 'released'
|
|
416
538
|
}
|
|
417
539
|
})
|
|
418
540
|
|
|
@@ -6,6 +6,7 @@ import gql from 'graphql-tag'
|
|
|
6
6
|
import { css, html, LitElement, nothing } from 'lit'
|
|
7
7
|
import { customElement, property, query } from 'lit/decorators.js'
|
|
8
8
|
import { subscribe } from '@operato/graphql'
|
|
9
|
+
import { i18next } from '@operato/i18n'
|
|
9
10
|
import { ScrollbarStyles } from '@operato/styles'
|
|
10
11
|
|
|
11
12
|
@customElement('scenario-instance-log-view')
|
|
@@ -51,6 +52,14 @@ export class ScenarioInstanceLogView extends LitElement {
|
|
|
51
52
|
bottom: 15px;
|
|
52
53
|
right: 16px;
|
|
53
54
|
text-decoration: auto;
|
|
55
|
+
display: flex;
|
|
56
|
+
flex-direction: column-reverse;
|
|
57
|
+
gap: 10px;
|
|
58
|
+
align-items: center;
|
|
59
|
+
}
|
|
60
|
+
#start [draft] {
|
|
61
|
+
--md-fab-container-color: var(--md-sys-color-secondary-container);
|
|
62
|
+
--md-fab-icon-color: var(--md-sys-color-on-secondary-container);
|
|
54
63
|
}
|
|
55
64
|
`
|
|
56
65
|
]
|
|
@@ -87,9 +96,23 @@ export class ScenarioInstanceLogView extends LitElement {
|
|
|
87
96
|
)}
|
|
88
97
|
</div>
|
|
89
98
|
${this.startable
|
|
90
|
-
? html
|
|
91
|
-
<
|
|
92
|
-
|
|
99
|
+
? html`
|
|
100
|
+
<div id="start">
|
|
101
|
+
<md-fab
|
|
102
|
+
title=${i18next.t('button.run released version')}
|
|
103
|
+
@click=${() => this.dispatchEvent(new CustomEvent('start', { detail: { mode: 'released' } }))}
|
|
104
|
+
>
|
|
105
|
+
<md-icon slot="icon">play_arrow</md-icon>
|
|
106
|
+
</md-fab>
|
|
107
|
+
<md-fab
|
|
108
|
+
draft
|
|
109
|
+
title=${i18next.t('button.run draft for debugging')}
|
|
110
|
+
@click=${() => this.dispatchEvent(new CustomEvent('start', { detail: { mode: 'draft' } }))}
|
|
111
|
+
>
|
|
112
|
+
<md-icon slot="icon">bug_report</md-icon>
|
|
113
|
+
</md-fab>
|
|
114
|
+
</div>
|
|
115
|
+
`
|
|
93
116
|
: nothing}
|
|
94
117
|
`
|
|
95
118
|
}
|
|
@@ -243,7 +243,19 @@ export class ScenarioMonitor extends localize(i18next)(LitElement) {
|
|
|
243
243
|
<div buttons ?detail=${this.mode == 'detail'}>
|
|
244
244
|
<md-icon @click=${e => this.popupLogView(scenario)}>text_snippet</md-icon>
|
|
245
245
|
${!scenarioInstance
|
|
246
|
-
? html`
|
|
246
|
+
? html`
|
|
247
|
+
<md-icon
|
|
248
|
+
title=${i18next.t('button.run released version')}
|
|
249
|
+
@click=${e => this.startScenario(scenario, 'released')}
|
|
250
|
+
>play_circle</md-icon
|
|
251
|
+
>
|
|
252
|
+
<md-icon
|
|
253
|
+
title=${i18next.t('button.run draft for debugging')}
|
|
254
|
+
@click=${e => this.startScenario(scenario, 'draft')}
|
|
255
|
+
debug
|
|
256
|
+
>bug_report</md-icon
|
|
257
|
+
>
|
|
258
|
+
`
|
|
247
259
|
: html` <md-icon @click=${e => this.stopScenario(scenario)} stop>pause_circle</md-icon> `}
|
|
248
260
|
</div>
|
|
249
261
|
`
|
|
@@ -254,7 +266,7 @@ export class ScenarioMonitor extends localize(i18next)(LitElement) {
|
|
|
254
266
|
html`
|
|
255
267
|
<scenario-instance-log-view
|
|
256
268
|
.scenarioName=${scenario.name}
|
|
257
|
-
@start=${() => this.startScenario(scenario)}
|
|
269
|
+
@start=${(e: CustomEvent) => this.startScenario(scenario, e.detail?.mode || 'released')}
|
|
258
270
|
startable
|
|
259
271
|
></scenario-instance-log-view>
|
|
260
272
|
`,
|
|
@@ -265,23 +277,24 @@ export class ScenarioMonitor extends localize(i18next)(LitElement) {
|
|
|
265
277
|
)
|
|
266
278
|
}
|
|
267
279
|
|
|
268
|
-
async startScenario(scenario) {
|
|
280
|
+
async startScenario(scenario, mode: string = 'released') {
|
|
269
281
|
var response = await client.mutate({
|
|
270
282
|
mutation: gql`
|
|
271
|
-
mutation ($scenarioName: String
|
|
272
|
-
startScenario(scenarioName: $scenarioName) {
|
|
283
|
+
mutation ($scenarioName: String!, $mode: String) {
|
|
284
|
+
startScenario(scenarioName: $scenarioName, mode: $mode) {
|
|
273
285
|
state
|
|
274
286
|
}
|
|
275
287
|
}
|
|
276
288
|
`,
|
|
277
289
|
variables: {
|
|
278
|
-
scenarioName: scenario.name
|
|
290
|
+
scenarioName: scenario.name,
|
|
291
|
+
mode
|
|
279
292
|
}
|
|
280
293
|
})
|
|
281
294
|
|
|
282
295
|
notify({
|
|
283
296
|
level: 'info',
|
|
284
|
-
message: `${!IS_SCENARIO_RUNNING(response.data.startScenario.state) ? 'fail' : 'success'} to start instance : ${scenario.name}`
|
|
297
|
+
message: `${!IS_SCENARIO_RUNNING(response.data.startScenario.state) ? 'fail' : 'success'} to start instance : ${scenario.name} (${mode})`
|
|
285
298
|
})
|
|
286
299
|
}
|
|
287
300
|
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import '@material/web/icon/icon.js'
|
|
2
|
+
import '@material/web/chips/assist-chip.js'
|
|
3
|
+
import '@operato/data-grist/ox-empty-note.js'
|
|
4
|
+
|
|
5
|
+
import gql from 'graphql-tag'
|
|
6
|
+
import { css, html, LitElement } from 'lit'
|
|
7
|
+
import { customElement, property, state } from 'lit/decorators.js'
|
|
8
|
+
|
|
9
|
+
import { i18next } from '@operato/i18n'
|
|
10
|
+
import { client } from '@operato/graphql'
|
|
11
|
+
import { notify } from '@operato/layout'
|
|
12
|
+
import { OxPrompt } from '@operato/popup/ox-prompt.js'
|
|
13
|
+
import { ScrollbarStyles } from '@operato/styles'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 시나리오 버전 히스토리 패널.
|
|
17
|
+
*
|
|
18
|
+
* - 릴리즈된 버전 목록(버전번호·코멘트·작성자·시각)
|
|
19
|
+
* - 각 버전을 현재 draft 와 비교하는 diff 뷰
|
|
20
|
+
* - 과거 버전을 현재 draft 로 복사(revert)
|
|
21
|
+
*
|
|
22
|
+
* board-ui 의 board-versions 를 시나리오(스텝 배열 diff)에 맞춰 확장한 형태.
|
|
23
|
+
*/
|
|
24
|
+
@customElement('scenario-versions')
|
|
25
|
+
export class ScenarioVersions extends LitElement {
|
|
26
|
+
static styles = [
|
|
27
|
+
ScrollbarStyles,
|
|
28
|
+
css`
|
|
29
|
+
:host {
|
|
30
|
+
display: flex;
|
|
31
|
+
flex-direction: column;
|
|
32
|
+
overflow: auto;
|
|
33
|
+
background-color: var(--md-sys-color-surface);
|
|
34
|
+
color: var(--md-sys-color-on-surface);
|
|
35
|
+
font: var(--label-font);
|
|
36
|
+
padding: var(--spacing-medium, 12px);
|
|
37
|
+
gap: var(--spacing-medium, 12px);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
div[card] {
|
|
41
|
+
position: relative;
|
|
42
|
+
border: 1px solid var(--md-sys-color-outline-variant, rgba(0, 0, 0, 0.12));
|
|
43
|
+
border-radius: var(--border-radius, 6px);
|
|
44
|
+
padding: var(--spacing-medium, 12px);
|
|
45
|
+
text-align: start;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
div[head] {
|
|
49
|
+
display: flex;
|
|
50
|
+
align-items: center;
|
|
51
|
+
gap: var(--spacing-small, 6px);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
div[head] [ver] {
|
|
55
|
+
font: var(--subtitle-font, bold 14px sans-serif);
|
|
56
|
+
color: var(--md-sys-color-primary);
|
|
57
|
+
display: inline-flex;
|
|
58
|
+
align-items: center;
|
|
59
|
+
gap: 3px;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
div[head] [meta] {
|
|
63
|
+
margin-left: auto;
|
|
64
|
+
display: inline-flex;
|
|
65
|
+
align-items: center;
|
|
66
|
+
gap: 5px;
|
|
67
|
+
opacity: 0.75;
|
|
68
|
+
font-size: var(--fontsize-small, 12px);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
div[comment] {
|
|
72
|
+
margin-top: 6px;
|
|
73
|
+
white-space: pre-wrap;
|
|
74
|
+
color: var(--md-sys-color-on-surface-variant, inherit);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
div[actions] {
|
|
78
|
+
display: flex;
|
|
79
|
+
gap: var(--spacing-small, 6px);
|
|
80
|
+
margin-top: 8px;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
md-icon {
|
|
84
|
+
font-size: var(--fontsize-large, 18px);
|
|
85
|
+
vertical-align: middle;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
button {
|
|
89
|
+
cursor: pointer;
|
|
90
|
+
border: 1px solid var(--md-sys-color-outline, rgba(0, 0, 0, 0.2));
|
|
91
|
+
background: var(--md-sys-color-surface);
|
|
92
|
+
color: inherit;
|
|
93
|
+
border-radius: var(--border-radius, 6px);
|
|
94
|
+
padding: 4px 10px;
|
|
95
|
+
display: inline-flex;
|
|
96
|
+
align-items: center;
|
|
97
|
+
gap: 4px;
|
|
98
|
+
font: inherit;
|
|
99
|
+
}
|
|
100
|
+
button[primary] {
|
|
101
|
+
background: var(--md-sys-color-primary);
|
|
102
|
+
color: var(--md-sys-color-on-primary);
|
|
103
|
+
border-color: transparent;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
div[diff] {
|
|
107
|
+
margin-top: 8px;
|
|
108
|
+
border-top: 1px dashed var(--md-sys-color-outline-variant, rgba(0, 0, 0, 0.12));
|
|
109
|
+
padding-top: 8px;
|
|
110
|
+
font-size: var(--fontsize-small, 12px);
|
|
111
|
+
}
|
|
112
|
+
div[diff] [group] {
|
|
113
|
+
font-weight: bold;
|
|
114
|
+
margin: 4px 0 2px;
|
|
115
|
+
}
|
|
116
|
+
[change] {
|
|
117
|
+
display: block;
|
|
118
|
+
padding: 1px 0;
|
|
119
|
+
}
|
|
120
|
+
[change][added] {
|
|
121
|
+
color: var(--md-sys-color-primary, green);
|
|
122
|
+
}
|
|
123
|
+
[change][removed] {
|
|
124
|
+
color: var(--md-sys-color-error, #b00020);
|
|
125
|
+
text-decoration: line-through;
|
|
126
|
+
opacity: 0.8;
|
|
127
|
+
}
|
|
128
|
+
[change][modified] {
|
|
129
|
+
color: var(--status-warning-color, #b26a00);
|
|
130
|
+
}
|
|
131
|
+
[change] small {
|
|
132
|
+
opacity: 0.7;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
ox-empty-note {
|
|
136
|
+
padding: 24px;
|
|
137
|
+
opacity: 0.7;
|
|
138
|
+
}
|
|
139
|
+
`
|
|
140
|
+
]
|
|
141
|
+
|
|
142
|
+
@property({ type: String }) scenarioId?: string
|
|
143
|
+
@property({ type: String }) scenarioName?: string
|
|
144
|
+
/** revert(과거→draft 복사) 성공 시 호출 — 목록 자동 갱신용 콜백(속성 바인딩이라 팝업 경계에서도 확실히 동작). */
|
|
145
|
+
@property({ attribute: false }) onReverted?: () => void
|
|
146
|
+
|
|
147
|
+
@state() versions: any[] = []
|
|
148
|
+
/** version → diff 결과(펼쳐진 경우만 존재) */
|
|
149
|
+
@state() diffs: { [version: number]: any } = {}
|
|
150
|
+
|
|
151
|
+
render() {
|
|
152
|
+
return html`
|
|
153
|
+
${this.versions.length == 0
|
|
154
|
+
? /* 빈 상태는 프레임워크 공통 부품(ox-empty-note) 사용 */
|
|
155
|
+
html`<ox-empty-note icon="history" title=${i18next.t('text.no released version information available')}></ox-empty-note>`
|
|
156
|
+
: this.versions.map(
|
|
157
|
+
(version, index) => html`
|
|
158
|
+
<div card>
|
|
159
|
+
<div head>
|
|
160
|
+
<span ver><md-icon>sell</md-icon> v${version.version}</span>
|
|
161
|
+
<span meta>
|
|
162
|
+
<md-icon>person</md-icon> ${version.updater?.name || 'anonymous'}
|
|
163
|
+
<md-icon>schedule</md-icon> ${new Date(version.updatedAt).toLocaleString()}
|
|
164
|
+
</span>
|
|
165
|
+
</div>
|
|
166
|
+
|
|
167
|
+
${version.comment ? html`<div comment>${version.comment}</div>` : ''}
|
|
168
|
+
|
|
169
|
+
<div actions>
|
|
170
|
+
<button @click=${() => this.toggleDiff(version.version, this.versions[index + 1]?.version)}>
|
|
171
|
+
<md-icon>difference</md-icon>${i18next.t('button.diff with previous')}
|
|
172
|
+
</button>
|
|
173
|
+
<button primary @click=${() => this.revert(version.version)}>
|
|
174
|
+
<md-icon>settings_backup_restore</md-icon>${i18next.t('button.copy to draft')}
|
|
175
|
+
</button>
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
${this.diffs[version.version] ? this.renderDiff(this.diffs[version.version]) : ''}
|
|
179
|
+
</div>
|
|
180
|
+
`
|
|
181
|
+
)}
|
|
182
|
+
`
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private renderDiff(diff: any) {
|
|
186
|
+
if (diff.earliest) {
|
|
187
|
+
return html`<div diff>${i18next.t('text.initial version all new')}</div>`
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const fieldDiffs = diff.fieldDiffs || []
|
|
191
|
+
const stepDiffs = diff.stepDiffs || []
|
|
192
|
+
|
|
193
|
+
if (fieldDiffs.length == 0 && stepDiffs.length == 0) {
|
|
194
|
+
return html`<div diff>${i18next.t('text.no difference from previous')}</div>`
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const fmt = (v: any) => (v === null || v === undefined ? '∅' : typeof v === 'object' ? JSON.stringify(v) : String(v))
|
|
198
|
+
|
|
199
|
+
return html`
|
|
200
|
+
<div diff>
|
|
201
|
+
${fieldDiffs.length
|
|
202
|
+
? html`
|
|
203
|
+
<div group>${i18next.t('label.scenario fields')}</div>
|
|
204
|
+
${fieldDiffs.map(
|
|
205
|
+
(f: any) => html`<span change modified>${f.field}: <small>${fmt(f.before)} → ${fmt(f.after)}</small></span>`
|
|
206
|
+
)}
|
|
207
|
+
`
|
|
208
|
+
: ''}
|
|
209
|
+
${stepDiffs.length
|
|
210
|
+
? html`
|
|
211
|
+
<div group>${i18next.t('label.steps')}</div>
|
|
212
|
+
${stepDiffs.map((s: any) => {
|
|
213
|
+
if (s.changeType == 'added') {
|
|
214
|
+
return html`<span change added>+ ${s.name || '(unnamed)'}</span>`
|
|
215
|
+
}
|
|
216
|
+
if (s.changeType == 'removed') {
|
|
217
|
+
return html`<span change removed>− ${s.name || '(unnamed)'}</span>`
|
|
218
|
+
}
|
|
219
|
+
const changed = (s.fieldDiffs || []).map((fd: any) => fd.field).join(', ')
|
|
220
|
+
return html`<span change modified>~ ${s.name || '(unnamed)'} <small>(${changed})</small></span>`
|
|
221
|
+
})}
|
|
222
|
+
`
|
|
223
|
+
: ''}
|
|
224
|
+
</div>
|
|
225
|
+
`
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
firstUpdated() {
|
|
229
|
+
this.refresh()
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async refresh() {
|
|
233
|
+
if (!this.scenarioId) {
|
|
234
|
+
return
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const response = (
|
|
238
|
+
await client.query({
|
|
239
|
+
query: gql`
|
|
240
|
+
query FetchScenarioVersions($id: String!) {
|
|
241
|
+
scenarioVersions(id: $id) {
|
|
242
|
+
id
|
|
243
|
+
version
|
|
244
|
+
comment
|
|
245
|
+
updater {
|
|
246
|
+
name
|
|
247
|
+
}
|
|
248
|
+
updatedAt
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
`,
|
|
252
|
+
variables: { id: this.scenarioId },
|
|
253
|
+
fetchPolicy: 'no-cache'
|
|
254
|
+
})
|
|
255
|
+
).data
|
|
256
|
+
|
|
257
|
+
this.versions = response.scenarioVersions || []
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async toggleDiff(version: number, prevVersion?: number) {
|
|
261
|
+
if (this.diffs[version]) {
|
|
262
|
+
const { [version]: _removed, ...rest } = this.diffs
|
|
263
|
+
this.diffs = rest
|
|
264
|
+
return
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// 앞 버전과 비교 = "이 버전이 무엇을 바꿨나"(체인지로그). 최초 버전은 앞 버전이 없어 전체 신규.
|
|
268
|
+
if (prevVersion === undefined || prevVersion === null) {
|
|
269
|
+
this.diffs = { ...this.diffs, [version]: { earliest: true } }
|
|
270
|
+
return
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const response = await client.query({
|
|
274
|
+
query: gql`
|
|
275
|
+
query ScenarioVersionDiff($id: String!, $base: Int!, $to: Int!) {
|
|
276
|
+
scenarioVersionDiff(id: $id, version: $base, toVersion: $to) {
|
|
277
|
+
fromVersion
|
|
278
|
+
toVersion
|
|
279
|
+
fieldDiffs {
|
|
280
|
+
field
|
|
281
|
+
before
|
|
282
|
+
after
|
|
283
|
+
}
|
|
284
|
+
stepDiffs {
|
|
285
|
+
name
|
|
286
|
+
changeType
|
|
287
|
+
fieldDiffs {
|
|
288
|
+
field
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
`,
|
|
294
|
+
variables: { id: this.scenarioId, base: prevVersion, to: version },
|
|
295
|
+
fetchPolicy: 'no-cache'
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
this.diffs = { ...this.diffs, [version]: response.data.scenarioVersionDiff }
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async revert(version: number) {
|
|
302
|
+
const ok = await OxPrompt.open({
|
|
303
|
+
type: 'question',
|
|
304
|
+
title: String(i18next.t('button.copy to draft')),
|
|
305
|
+
text: String(i18next.t('text.sure to copy version to draft', { version })),
|
|
306
|
+
confirmButton: { text: String(i18next.t('button.confirm')) },
|
|
307
|
+
cancelButton: { text: String(i18next.t('button.cancel')) }
|
|
308
|
+
})
|
|
309
|
+
if (!ok) {
|
|
310
|
+
return
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const response = await client.mutate({
|
|
314
|
+
mutation: gql`
|
|
315
|
+
mutation RevertScenarioVersion($id: String!, $version: Int!) {
|
|
316
|
+
revertScenarioVersion(id: $id, version: $version) {
|
|
317
|
+
id
|
|
318
|
+
version
|
|
319
|
+
publishState
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
`,
|
|
323
|
+
variables: { id: this.scenarioId, version }
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
if (!response.errors) {
|
|
327
|
+
notify({ message: i18next.t('text.info_x_successfully', { x: i18next.t('text.copy') }) })
|
|
328
|
+
this.onReverted?.()
|
|
329
|
+
this.dispatchEvent(new CustomEvent('reverted', { bubbles: true, composed: true }))
|
|
330
|
+
} else {
|
|
331
|
+
notify({ level: 'error', message: response.errors.map(e => e.message).join('\n') })
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import '@material/web/icon/icon.js';
|
|
1
2
|
import '@operato/data-grist';
|
|
2
3
|
import './scenario-detail.js';
|
|
3
4
|
import './scenario-importer.js';
|
|
5
|
+
import '../viewparts/scenario-versions.js';
|
|
4
6
|
import { DataGrist } from '@operato/data-grist/ox-grist.js';
|
|
5
7
|
import { PageView } from '@operato/shell';
|
|
6
8
|
import { FetchOption } from '@operato/data-grist';
|
|
@@ -53,6 +55,8 @@ export declare class Scenario extends Scenario_base {
|
|
|
53
55
|
_deleteScenario(): Promise<void>;
|
|
54
56
|
_copyScenario(): Promise<void>;
|
|
55
57
|
_updateScenario(): Promise<void>;
|
|
58
|
+
releaseScenario(record: any): Promise<void>;
|
|
59
|
+
openVersionHistory(record: any): void;
|
|
56
60
|
startScenario(record: any): Promise<void>;
|
|
57
61
|
stopScenario(record: any): Promise<void>;
|
|
58
62
|
startScenarioSchedule(record: any): Promise<void>;
|