@things-factory/ops-master 10.1.33 → 10.1.35

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 (150) hide show
  1. package/dist-client/index.d.ts +1 -0
  2. package/dist-client/index.js +1 -0
  3. package/dist-client/index.js.map +1 -1
  4. package/dist-client/master-change-approval.d.ts +14 -0
  5. package/dist-client/master-change-approval.js +36 -0
  6. package/dist-client/master-change-approval.js.map +1 -0
  7. package/dist-client/pages/master/material-definition.d.ts +4 -11
  8. package/dist-client/pages/master/material-definition.js +5 -53
  9. package/dist-client/pages/master/material-definition.js.map +1 -1
  10. package/dist-client/pages/master/material-revision-fields.d.ts +2 -0
  11. package/dist-client/pages/master/material-revision-fields.js +43 -0
  12. package/dist-client/pages/master/material-revision-fields.js.map +1 -0
  13. package/dist-client/pages/master/recipe-definition.d.ts +4 -135
  14. package/dist-client/pages/master/recipe-definition.js +7 -397
  15. package/dist-client/pages/master/recipe-definition.js.map +1 -1
  16. package/dist-client/pages/master/revision-comparison.d.ts +48 -0
  17. package/dist-client/pages/master/revision-comparison.js +553 -0
  18. package/dist-client/pages/master/revision-comparison.js.map +1 -0
  19. package/dist-client/pages/master/revision-view.d.ts +19 -0
  20. package/dist-client/pages/master/revision-view.js +133 -0
  21. package/dist-client/pages/master/revision-view.js.map +1 -0
  22. package/dist-client/pages/master/revision-workspace.d.ts +50 -0
  23. package/dist-client/pages/master/revision-workspace.js +752 -0
  24. package/dist-client/pages/master/revision-workspace.js.map +1 -0
  25. package/dist-client/pages/master/test-specification.d.ts +4 -92
  26. package/dist-client/pages/master/test-specification.js +5 -440
  27. package/dist-client/pages/master/test-specification.js.map +1 -1
  28. package/dist-client/tsconfig.tsbuildinfo +1 -1
  29. package/dist-server/service/asset/asset-master-resolver.js +38 -34
  30. package/dist-server/service/asset/asset-master-resolver.js.map +1 -1
  31. package/dist-server/service/asset/index.d.ts +2 -2
  32. package/dist-server/service/equipment/equipment-master-resolver.js +54 -49
  33. package/dist-server/service/equipment/equipment-master-resolver.js.map +1 -1
  34. package/dist-server/service/equipment/index.d.ts +2 -2
  35. package/dist-server/service/index.d.ts +18 -2
  36. package/dist-server/service/index.js +18 -0
  37. package/dist-server/service/index.js.map +1 -1
  38. package/dist-server/service/location/index.d.ts +1 -1
  39. package/dist-server/service/location/location-master-resolver.js +34 -31
  40. package/dist-server/service/location/location-master-resolver.js.map +1 -1
  41. package/dist-server/service/master-axis.js +1 -1
  42. package/dist-server/service/master-axis.js.map +1 -1
  43. package/dist-server/service/master-change-policy.d.ts +23 -0
  44. package/dist-server/service/master-change-policy.js +134 -0
  45. package/dist-server/service/master-change-policy.js.map +1 -0
  46. package/dist-server/service/master-conformance-resolver.js +11 -1
  47. package/dist-server/service/master-conformance-resolver.js.map +1 -1
  48. package/dist-server/service/master-crud.js +9 -0
  49. package/dist-server/service/master-crud.js.map +1 -1
  50. package/dist-server/service/master-list.js +6 -0
  51. package/dist-server/service/master-list.js.map +1 -1
  52. package/dist-server/service/material/index.d.ts +3 -2
  53. package/dist-server/service/material/index.js +4 -2
  54. package/dist-server/service/material/index.js.map +1 -1
  55. package/dist-server/service/material/material-axis-status.d.ts +8 -0
  56. package/dist-server/service/material/material-axis-status.js +19 -0
  57. package/dist-server/service/material/material-axis-status.js.map +1 -0
  58. package/dist-server/service/material/material-definition.d.ts +3 -0
  59. package/dist-server/service/material/material-definition.js +13 -0
  60. package/dist-server/service/material/material-definition.js.map +1 -1
  61. package/dist-server/service/material/material-effective-list.d.ts +39 -0
  62. package/dist-server/service/material/material-effective-list.js +54 -0
  63. package/dist-server/service/material/material-effective-list.js.map +1 -0
  64. package/dist-server/service/material/material-master-resolver.js +59 -61
  65. package/dist-server/service/material/material-master-resolver.js.map +1 -1
  66. package/dist-server/service/material/material-query.js +9 -2
  67. package/dist-server/service/material/material-query.js.map +1 -1
  68. package/dist-server/service/material/material-revision-content.d.ts +10 -0
  69. package/dist-server/service/material/material-revision-content.js +92 -0
  70. package/dist-server/service/material/material-revision-content.js.map +1 -0
  71. package/dist-server/service/material/material-revision-resolver.d.ts +54 -0
  72. package/dist-server/service/material/material-revision-resolver.js +76 -0
  73. package/dist-server/service/material/material-revision-resolver.js.map +1 -0
  74. package/dist-server/service/material/material-revision-service.d.ts +144 -0
  75. package/dist-server/service/material/material-revision-service.js +534 -0
  76. package/dist-server/service/material/material-revision-service.js.map +1 -0
  77. package/dist-server/service/material/material-revision.d.ts +84 -0
  78. package/dist-server/service/material/material-revision.js +339 -0
  79. package/dist-server/service/material/material-revision.js.map +1 -0
  80. package/dist-server/service/operation/index.d.ts +1 -1
  81. package/dist-server/service/operation/operation-definition.js +1 -0
  82. package/dist-server/service/operation/operation-definition.js.map +1 -1
  83. package/dist-server/service/operation/operation-master-resolver.js +30 -30
  84. package/dist-server/service/operation/operation-master-resolver.js.map +1 -1
  85. package/dist-server/service/personnel/index.d.ts +2 -2
  86. package/dist-server/service/personnel/personnel-master-resolver.js +38 -34
  87. package/dist-server/service/personnel/personnel-master-resolver.js.map +1 -1
  88. package/dist-server/service/quality/nonconformance-cause-resolver.js +17 -14
  89. package/dist-server/service/quality/nonconformance-cause-resolver.js.map +1 -1
  90. package/dist-server/service/recipe/index.d.ts +3 -2
  91. package/dist-server/service/recipe/index.js +4 -2
  92. package/dist-server/service/recipe/index.js.map +1 -1
  93. package/dist-server/service/recipe/recipe-definition.d.ts +4 -0
  94. package/dist-server/service/recipe/recipe-definition.js +14 -1
  95. package/dist-server/service/recipe/recipe-definition.js.map +1 -1
  96. package/dist-server/service/recipe/recipe-effective-list.d.ts +49 -0
  97. package/dist-server/service/recipe/recipe-effective-list.js +96 -0
  98. package/dist-server/service/recipe/recipe-effective-list.js.map +1 -0
  99. package/dist-server/service/recipe/recipe-master-resolver.js +32 -28
  100. package/dist-server/service/recipe/recipe-master-resolver.js.map +1 -1
  101. package/dist-server/service/recipe/recipe-query.js +9 -2
  102. package/dist-server/service/recipe/recipe-query.js.map +1 -1
  103. package/dist-server/service/recipe/recipe-revision-content.d.ts +32 -0
  104. package/dist-server/service/recipe/recipe-revision-content.js +65 -0
  105. package/dist-server/service/recipe/recipe-revision-content.js.map +1 -0
  106. package/dist-server/service/recipe/recipe-revision-line.d.ts +30 -0
  107. package/dist-server/service/recipe/recipe-revision-line.js +95 -0
  108. package/dist-server/service/recipe/recipe-revision-line.js.map +1 -0
  109. package/dist-server/service/recipe/recipe-revision-resolver.d.ts +59 -0
  110. package/dist-server/service/recipe/recipe-revision-resolver.js +76 -0
  111. package/dist-server/service/recipe/recipe-revision-resolver.js.map +1 -0
  112. package/dist-server/service/recipe/recipe-revision-service.d.ts +147 -0
  113. package/dist-server/service/recipe/recipe-revision-service.js +591 -0
  114. package/dist-server/service/recipe/recipe-revision-service.js.map +1 -0
  115. package/dist-server/service/recipe/recipe-revision.d.ts +100 -0
  116. package/dist-server/service/recipe/recipe-revision.js +373 -0
  117. package/dist-server/service/recipe/recipe-revision.js.map +1 -0
  118. package/dist-server/service/recipe/route-definition.js +1 -0
  119. package/dist-server/service/recipe/route-definition.js.map +1 -1
  120. package/dist-server/service/resource/class-members.js +9 -0
  121. package/dist-server/service/resource/class-members.js.map +1 -1
  122. package/dist-server/service/status/master-status.js +2 -1
  123. package/dist-server/service/status/master-status.js.map +1 -1
  124. package/dist-server/service/test-spec/index.d.ts +3 -2
  125. package/dist-server/service/test-spec/index.js +4 -2
  126. package/dist-server/service/test-spec/index.js.map +1 -1
  127. package/dist-server/service/test-spec/test-spec-effective-list.d.ts +34 -0
  128. package/dist-server/service/test-spec/test-spec-effective-list.js +42 -0
  129. package/dist-server/service/test-spec/test-spec-effective-list.js.map +1 -0
  130. package/dist-server/service/test-spec/test-spec-master-resolver.d.ts +4 -14
  131. package/dist-server/service/test-spec/test-spec-master-resolver.js +12 -83
  132. package/dist-server/service/test-spec/test-spec-master-resolver.js.map +1 -1
  133. package/dist-server/service/test-spec/test-spec-query.js +7 -1
  134. package/dist-server/service/test-spec/test-spec-query.js.map +1 -1
  135. package/dist-server/service/test-spec/test-spec-revision-resolver.d.ts +53 -0
  136. package/dist-server/service/test-spec/test-spec-revision-resolver.js +76 -0
  137. package/dist-server/service/test-spec/test-spec-revision-resolver.js.map +1 -0
  138. package/dist-server/service/test-spec/test-spec-revision-service.d.ts +138 -0
  139. package/dist-server/service/test-spec/test-spec-revision-service.js +538 -0
  140. package/dist-server/service/test-spec/test-spec-revision-service.js.map +1 -0
  141. package/dist-server/service/test-spec/test-spec-revision.d.ts +84 -0
  142. package/dist-server/service/test-spec/test-spec-revision.js +334 -0
  143. package/dist-server/service/test-spec/test-spec-revision.js.map +1 -0
  144. package/dist-server/service/test-spec/test-specification.d.ts +6 -0
  145. package/dist-server/service/test-spec/test-specification.js +15 -2
  146. package/dist-server/service/test-spec/test-specification.js.map +1 -1
  147. package/dist-server/tsconfig.tsbuildinfo +1 -1
  148. package/package.json +10 -6
  149. package/translations/en.json +148 -0
  150. package/translations/ko.json +148 -0
@@ -1,402 +1,12 @@
1
- import { __decorate, __metadata } from "tslib";
2
- import { css, html } from 'lit';
3
- import { customElement, state } from 'lit/decorators.js';
4
- import gql from 'graphql-tag';
5
- import { client } from '@operato/graphql';
6
- import { i18next } from '@operato/i18n';
7
- import { notify } from '@operato/layout';
8
- import { navigate } from '@operato/shell';
9
- import { EFFECTIVE, IDENTITY, MasterPageBase } from './master-page-base.js';
10
- /**
11
- * 제조법 — **BOM 이 사는 자리.**
12
- *
13
- * ── 왜 이 화면이 있어야 하나 ───────────────────────────────────────────────
14
- * 서버에는 축도 목록 질의도 수정 연산도 있는데 **화면이 없었다.** 그래서 「이 완제품 한 대에 무엇이
15
- * 몇 개 들어가나」를 제품 안에서 볼 길이 없었다. 품목 화면은 이름과 등급과 유효기간만 보이고,
16
- * 소요는 여기 있는데 여기로 오는 문이 없었다.
17
- *
18
- * ── 왜 두 단인가 ───────────────────────────────────────────────────────────
19
- * 레시피 하나의 투입이 마흔아홉 줄이다(군산 ECU). 그것을 레시피 목록의 한 칸에 넣으면 아무것도
20
- * 읽을 수 없고, 레시피와 투입을 한 표에 섞으면 **무엇이 레시피이고 무엇이 그 안의 줄인지** 사라진다.
21
- *
22
- * 그래서 로케이션과 같은 결로 둔다 — 목록에서 하나를 고르면 그 안으로 들어가고, 어디에 있는지는
23
- * 빵가루가 말한다. **자리의 주인은 URL 이다**(`pageUpdated`). 그래야 붙여 보낼 수 있고, 뒤로 가기가
24
- * 한 단 올라가고, 새로 고쳐도 그 자리에 남는다.
25
- *
26
- * ── 투입 자재를 여기서 고친다 ──────────────────────────────────────────────
27
- * 안에 들어가면 그 레시피의 투입이 줄로 선다. 고치면 **레시피 한 건의 `inputs` 를 통째로 다시 쓴다** —
28
- * `RecipePart` 는 자기 식별자가 없어서 줄 단위로 저장할 수 없다. 그래서 저장은 언제나 그 레시피
29
- * 하나에 대한 한 번의 쓰기다.
30
- *
31
- * ── 무엇을 만드는가는 고정 칸이다 ──────────────────────────────────────────
32
- * `materialDefinitionName` 이 이 레시피가 만드는 품목이고, `route` 가 그것이 타고 갈 라우팅이다. 이
33
- * 둘이 **품목과 라우팅을 잇는 유일한 자리**다 — 품목에서 라우팅으로 바로 가는 길은 없고, 있어서도 안 된다.
34
- * 같은 품목을 다른 라우팅으로 만드는 일이 있기 때문이다(라인 증설 · 시제품).
35
- */
36
- let OpsMasterRecipeDefinition = class OpsMasterRecipeDefinition extends MasterPageBase {
37
- static { this.styles = [
38
- ...MasterPageBase.styles,
39
- css `
40
- .scope {
41
- display: flex;
42
- align-items: center;
43
- flex-wrap: wrap;
44
- gap: 2px;
45
- min-height: 26px;
46
- }
47
-
48
- .scope button {
49
- display: flex;
50
- align-items: center;
51
- gap: 2px;
52
- border: none;
53
- background: none;
54
- padding: 2px 6px;
55
- cursor: pointer;
56
- color: var(--md-sys-color-primary);
57
- font: inherit;
58
- }
59
-
60
- .scope .sep {
61
- opacity: 0.4;
62
- }
63
-
64
- .scope .here {
65
- padding: 2px 6px;
66
- font-weight: 600;
67
- }
68
-
69
- .scope .makes {
70
- margin-left: 8px;
71
- opacity: 0.7;
72
- font-size: 0.9em;
73
- }
74
- `
75
- ]; }
76
- get spec() {
77
- /* 안에 들어와 있으면 투입을 보이고, 아니면 레시피 목록을 보인다. */
78
- return this.opened ? this.partsSpec() : this.listSpec();
79
- }
80
- /** 레시피 목록 — 무엇을 만드나 · 어느 라우팅으로 · 투입이 몇 줄인가. */
81
- listSpec() {
82
- return {
83
- key: 'recipeDefinition',
84
- title: 'recipe',
85
- help: 'ops-master/recipe-definition',
86
- query: 'recipeDefinitions',
87
- mutation: 'updateMultipleRecipeDefinition',
88
- columns: [
89
- ...IDENTITY,
90
- {
91
- name: 'materialDefinitionName',
92
- label: 'makes',
93
- type: 'reference',
94
- reference: { queryName: 'materialDefinitions' },
95
- width: 180
96
- },
97
- { name: 'route', label: 'route', type: 'reference', reference: { queryName: 'routeDefinitions' }, width: 160 },
98
- /* 이 자리에 「투입 자재」 문이 들어간다 — 실제 칸은 `configOf` 가 갈아 끼운다. */
99
- { name: 'inputCount', label: 'inputs', type: 'number', width: 96, readonly: true },
100
- ...EFFECTIVE
101
- ]
102
- };
103
- }
104
- /**
105
- * 이 제조법에 들어가는 자재들 — 안으로 들어왔을 때의 표.
106
- *
107
- * 저장은 이 화면이 스스로 한다(`save` 를 덮어쓴다). `key` 와 `mutation` 은 그대로 두어 화면
108
- * 골격이 같은 축을 가리키게 한다.
109
- */
110
- /**
111
- * 「투입 자재」 칸을 목록에 끼운다 — **스펙이 아니라 여기서.**
112
- *
113
- * `MasterColumn` 은 렌더러를 모르는 좁은 타입이다. 자리 화면도 같은 이유로 여기서 끼운다
114
- * (`operational-location` 의 `configOf`). 스펙에 억지로 넣으면 타입을 속여야 하고, 그러면 다음
115
- * 사람이 스펙에 무엇을 넣을 수 있는지 알 수 없게 된다.
116
- */
117
- configOf() {
118
- const config = super.configOf();
119
- /* 안에 들어와 있으면 투입 자재 표다 — 그 표에는 이 칸이 없다. */
120
- if (this.opened) {
121
- return config;
122
- }
123
- const columns = [...config.columns];
124
- const at = columns.findIndex((column) => column?.name === 'inputCount');
125
- if (at >= 0) {
126
- columns[at] = this.inputsColumn();
127
- }
128
- return { ...config, columns };
129
- }
130
- /**
131
- * ── 개수는 **문이다.** 죽은 숫자가 아니다 ───────────────────────────────
132
- *
133
- * 처음에는 그냥 못 고치는 숫자였다. 사용자가 물었다(2026-09-05): 「이게 뭐고 왜 편집이 안 되나」.
134
- * 둘 다 맞는 물음이었다 — 고칠 수 없는 칸이 **왜** 고칠 수 없는지 화면이 말하지 않으면, 보는
135
- * 사람은 그것을 망가진 칸으로 읽는다.
136
- *
137
- * 이 수는 값이 아니라 안에 든 것의 수다. 바꾸려면 안에 들어가 자재를 더하거나 빼야 한다. 그러니
138
- * 그 길을 이 칸이 직접 내야 한다 — 자리 화면의 「하위」가 이미 그렇게 한다.
139
- *
140
- * ```
141
- * 투입 자재
142
- * 3 › 셋이 들어간다. 누르면 그리로 간다
143
- * ― 아직 안 정했다. 누를 데가 없다
144
- * ```
145
- */
146
- inputsColumn() {
147
- return {
148
- type: 'string',
149
- name: 'inputCount',
150
- width: 96,
151
- /* 폭을 끌 것이 없고, 개수로 정렬하려면 서버가 그 축을 내야 한다. */
152
- resizable: false,
153
- sortable: false,
154
- header: i18next.t('field.inputs'),
155
- record: {
156
- align: 'center',
157
- renderer: (_value, _column, record) => {
158
- /* 표 끝의 빈 줄에는 그리지 않는다 — 갈 곳이 없다. */
159
- if (!record?.id)
160
- return html ``;
161
- const count = record.inputCount ?? 0;
162
- /*
163
- * 비었으면 아무것도 그리지 않는다. 선택해도 빈 화면이 나오는 버튼은 없는 것이 낫고, 비우면
164
- * **아직 자재를 안 정한 제조법**이 눈에 띈다 — 그게 이 칸이 답하는 물음이다.
165
- */
166
- if (!count)
167
- return html ``;
168
- return html `<button
169
- class="icon wide"
170
- title=${i18next.t('button.enter-recipe')}
171
- @click=${(e) => {
172
- e.stopPropagation();
173
- this.enter(record);
174
- }}
175
- >
176
- <span>${count}</span>
177
- <md-icon>chevron_right</md-icon>
178
- </button>`;
179
- }
180
- }
181
- };
182
- }
183
- partsSpec() {
184
- return {
185
- key: 'recipeDefinition',
186
- title: 'recipe',
187
- help: 'ops-master/recipe-definition',
188
- query: 'recipeDefinitions',
189
- mutation: 'updateMultipleRecipeDefinition',
190
- columns: [
191
- {
192
- name: 'material',
193
- label: 'material',
194
- type: 'reference',
195
- reference: { queryName: 'materialDefinitions' },
196
- width: 220,
197
- fixed: true
198
- },
199
- { name: 'qty', label: 'quantity', type: 'number', width: 100 },
200
- /*
201
- * 어느 공정에서 들어가나 — **BOM 이 말하지 않는 것.**
202
- *
203
- * ERP 의 BOM 은 「한 대에 몇 개」만 말한다. 그것이 언제 빠지는지는 작업표준서에 있고, 둘을
204
- * 붙여야 재고가 언제 줄어드는지가 정해진다. 붙이지 않으면 소요는 있는데 빠지는 시각이 없다.
205
- */
206
- {
207
- name: 'operation',
208
- label: 'operation',
209
- type: 'reference',
210
- reference: { queryName: 'operationDefinitions' },
211
- width: 200
212
- },
213
- { name: 'kind', label: 'part-kind', type: 'string', width: 90, readonly: true }
214
- ]
215
- };
216
- }
217
- /* 이 화면은 주소 조각을 **파고들기**에 쓴다 — 목록 좁히기로 겹쳐 쓰지 않는다. */
218
- get pathSegmentNarrowsList() {
219
- return false;
220
- }
221
- async pageInitialized() {
222
- /*
223
- * 주소에 레시피가 담겨 있으면 여기서도 연다.
224
- *
225
- * `pageUpdated` 만 믿으면 주소로 바로 들어온 경우가 빠진다 — 그 호출이 이 함수보다 먼저 지나간다.
226
- * 로케이션 화면이 같은 것에 물렸다(2026-09-01).
227
- */
228
- await this.syncOpened(this.lifecycle?.resourceId);
229
- await super.pageInitialized();
230
- }
231
- async pageUpdated(changes, after, before) {
232
- if (after?.resourceId !== before?.resourceId) {
233
- await this.syncOpened(after?.resourceId);
234
- }
235
- await super.pageUpdated();
236
- }
237
- /** 주소가 가리키는 레시피를 연다. 없으면 목록이다. */
238
- async syncOpened(key) {
239
- this.opened = key ? await this.lookup(key) : undefined;
240
- }
241
- /** 레시피 하나를 찾는다. 아이디로 찾고, 없으면 이름으로 찾는다 — 주소에는 사람이 읽는 이름이 온다. */
242
- async lookup(key) {
243
- for (const field of ['id', 'name']) {
244
- const { records } = await this.fetchRecipes({
245
- page: 1,
246
- limit: 1,
247
- filters: [{ name: field, operator: 'eq', value: key }]
248
- });
249
- if (records.length) {
250
- return records[0];
251
- }
252
- }
253
- return undefined;
254
- }
255
- /** 레시피 자체를 읽는다 — 안에 들어와 있어도 이 질의는 레시피를 본다. */
256
- async fetchRecipes(option) {
257
- const { page = 1, limit = 50, sortings = [], filters = [] } = option;
258
- const response = await client.query({
259
- query: gql `
260
- query ($filters: [Filter!], $pagination: Pagination, $sortings: [Sorting!]) {
261
- recipeDefinitions(filters: $filters, pagination: $pagination, sortings: $sortings) {
262
- items {
263
- id
264
- name
265
- description
266
- materialDefinitionName
267
- route
268
- inputs
269
- outputs
270
- effectiveStart
271
- effectiveEnd
272
- }
273
- total
274
- }
275
- }
276
- `,
277
- variables: { filters, pagination: { page, limit }, sortings },
278
- fetchPolicy: 'no-cache'
279
- });
280
- const payload = response.data?.recipeDefinitions;
281
- return { total: payload?.total ?? 0, records: (payload?.items ?? []) };
282
- }
283
- /**
284
- * 목록일 때는 레시피를, 안에서는 그 레시피의 투입과 산출을 낸다.
285
- *
286
- * **투입 자재 줄에는 서버가 준 식별자가 없다.** `RecipePart` 는 자기 아이디가 없어서, 화면이 순번으로
287
- * 만들어 준다 — 그 값은 저장할 때 쓰지 않고 표가 줄을 가리키는 데만 쓴다.
288
- */
289
- async fetchHandler(option) {
290
- if (!this.opened) {
291
- const { total, records } = await this.fetchRecipes(option);
292
- return { total, records: records.map(one => ({ ...one, inputCount: (one.inputs ?? []).length })) };
293
- }
294
- /* 안에서는 다시 읽는다 — 저장한 뒤 이 화면이 스스로 새로 고칠 수 있어야 한다. */
295
- const fresh = (await this.lookup(this.opened.name)) ?? this.opened;
296
- this.opened = fresh;
297
- const rows = [
298
- ...(fresh.inputs ?? []).map((part, at) => ({ id: `in-${at}`, kind: 'input', ...part })),
299
- ...(fresh.outputs ?? []).map((part, at) => ({ id: `out-${at}`, kind: 'output', ...part }))
300
- ];
301
- return { total: rows.length, records: rows };
302
- }
303
- renderScope() {
304
- if (!this.opened) {
305
- return html ``;
306
- }
307
- return html `
308
- <div class="scope">
309
- <button @click=${() => this.enter(null)}>
310
- <md-icon>home</md-icon>
311
- ${i18next.t('label.all-recipes')}
312
- </button>
313
- <span class="sep">/</span><span class="here">${this.opened.name}</span>
314
- ${this.opened.materialDefinitionName
315
- ? html `<span class="makes"
316
- >${i18next.t('label.makes')} ${this.opened.materialDefinitionName}${this.opened.route
317
- ? ` · ${this.opened.route}`
318
- : ''}</span
319
- >`
320
- : ''}
321
- </div>
322
- `;
323
- }
324
- /**
325
- * 그 레시피로 들어간다. `null` 이면 목록으로 돌아간다.
326
- *
327
- * **주소만 바꾼다.** 셸이 주소를 물고 `pageUpdated` 를 부르고, 거기서 하나로 다시 선다.
328
- */
329
- enter(recipe) {
330
- /* 아직 저장하지 않은 줄로는 들어갈 수 없다. 표 끝의 빈 줄에도 이 버튼이 붙는다. */
331
- if (recipe && !recipe.id) {
332
- return;
333
- }
334
- navigate(recipe ? `${this.lifecycle?.page}/${recipe.name || recipe.id}` : `${this.lifecycle?.page}`);
335
- }
336
- /**
337
- * 저장 — **투입 자재 줄에는 자기 식별자가 없다.**
338
- *
339
- * `RecipePart` 는 아이디를 갖지 않아서 줄 단위로 저장할 수 없다. 그래서 안에서 고친 것은 표에
340
- * 서 있는 줄 전부를 모아 **그 레시피 한 건의 `inputs`·`outputs` 를 통째로 다시 쓴다.**
341
- *
342
- * 통째로 쓰는 것이 위험해 보이지만 그 반대가 더 위험하다 — 줄마다 쓰면 「셋째 줄」이 무엇인지
343
- * 서버가 알 방법이 없어서, 두 사람이 같은 레시피를 고칠 때 어느 줄이 어느 줄을 덮는지 정할 수
344
- * 없다. 한 번의 쓰기면 뒤엣사람이 앞엣사람의 것을 통째로 덮는 것이 눈에 보인다.
345
- *
346
- * 목록에서는 화면 골격의 저장을 그대로 쓴다 — 거기서는 레시피 한 줄이 곧 한 행이다.
347
- */
348
- async save() {
349
- if (!this.opened) {
350
- return await super.save();
351
- }
352
- if (!this.grist.dirtyRecords?.length) {
353
- notify({ message: i18next.t('text.nothing-changed'), level: 'info' });
354
- return;
355
- }
356
- /* 표에 서 있는 모든 줄 — 고친 것만이 아니라 전부다. 통째로 다시 쓰기 때문이다. */
357
- const rows = this.grist.dirtyData?.records ?? [];
358
- const part = (row) => ({
359
- material: String(row.material ?? '').trim(),
360
- qty: Number(row.qty ?? 0),
361
- ...(String(row.operation ?? '').trim() ? { operation: String(row.operation).trim() } : {})
362
- });
363
- const nameless = rows.filter(row => !String(row.material ?? '').trim());
364
- if (nameless.length) {
365
- /* 품목 없는 줄은 소요가 아니다. 하나라도 있으면 보내지 않는다. 절반만 저장되면 화면과 달라진다. */
366
- notify({ message: i18next.t('error.recipe-part-needs-material'), level: 'error' });
367
- return;
368
- }
369
- const response = await client.mutate({
370
- mutation: gql `
371
- mutation ($patches: [Object!]!) {
372
- updateMultipleRecipeDefinition(patches: $patches) {
373
- id
374
- }
375
- }
376
- `,
377
- variables: {
378
- patches: [
379
- {
380
- id: this.opened.id,
381
- name: this.opened.name,
382
- cuFlag: 'M',
383
- inputs: rows.filter(row => row.kind !== 'output').map(part),
384
- outputs: rows.filter(row => row.kind === 'output').map(part)
385
- }
386
- ]
387
- }
388
- });
389
- /* 이 클라이언트는 서버가 거절해도 던지지 않는다. 사유는 앱의 공통 오류 처리가 띄운다. */
390
- if (response?.errors?.length) {
391
- return;
392
- }
393
- this.grist.fetch();
1
+ import { __decorate } from "tslib";
2
+ import { customElement } from 'lit/decorators.js';
3
+ import { RevisionWorkspace } from './revision-workspace.js';
4
+ let OpsMasterRecipeDefinition = class OpsMasterRecipeDefinition extends RevisionWorkspace {
5
+ constructor() {
6
+ super(...arguments);
7
+ this.axis = 'recipeDefinition';
394
8
  }
395
9
  };
396
- __decorate([
397
- state(),
398
- __metadata("design:type", Object)
399
- ], OpsMasterRecipeDefinition.prototype, "opened", void 0);
400
10
  OpsMasterRecipeDefinition = __decorate([
401
11
  customElement('ops-master-recipe-definition')
402
12
  ], OpsMasterRecipeDefinition);
@@ -1 +1 @@
1
- {"version":3,"file":"recipe-definition.js","sourceRoot":"","sources":["../../../client/pages/master/recipe-definition.ts"],"names":[],"mappings":"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","sourcesContent":["import { css, html } from 'lit'\nimport { customElement, state } from 'lit/decorators.js'\n\nimport gql from 'graphql-tag'\n\nimport { FetchOption } from '@operato/data-grist'\nimport { client } from '@operato/graphql'\nimport { i18next } from '@operato/i18n'\nimport { notify } from '@operato/layout'\nimport { navigate } from '@operato/shell'\n\nimport { EFFECTIVE, IDENTITY, MasterPageBase, MasterSpec } from './master-page-base.js'\n\n/** 서버가 주는 레시피 한 줄. */\ntype RecipeRecord = Record<string, any> & {\n id: string\n name: string\n materialDefinitionName?: string\n route?: string\n inputs?: RecipePart[]\n outputs?: RecipePart[]\n}\n\n/** 투입 · 산출 한 줄 — 무엇을 얼마나, 그리고 어느 공정에서. */\ntype RecipePart = { material?: string; qty?: number; operation?: string }\n\n/**\n * 제조법 — **BOM 이 사는 자리.**\n *\n * ── 왜 이 화면이 있어야 하나 ───────────────────────────────────────────────\n * 서버에는 축도 목록 질의도 수정 연산도 있는데 **화면이 없었다.** 그래서 「이 완제품 한 대에 무엇이\n * 몇 개 들어가나」를 제품 안에서 볼 길이 없었다. 품목 화면은 이름과 등급과 유효기간만 보이고,\n * 소요는 여기 있는데 여기로 오는 문이 없었다.\n *\n * ── 왜 두 단인가 ───────────────────────────────────────────────────────────\n * 레시피 하나의 투입이 마흔아홉 줄이다(군산 ECU). 그것을 레시피 목록의 한 칸에 넣으면 아무것도\n * 읽을 수 없고, 레시피와 투입을 한 표에 섞으면 **무엇이 레시피이고 무엇이 그 안의 줄인지** 사라진다.\n *\n * 그래서 로케이션과 같은 결로 둔다 — 목록에서 하나를 고르면 그 안으로 들어가고, 어디에 있는지는\n * 빵가루가 말한다. **자리의 주인은 URL 이다**(`pageUpdated`). 그래야 붙여 보낼 수 있고, 뒤로 가기가\n * 한 단 올라가고, 새로 고쳐도 그 자리에 남는다.\n *\n * ── 투입 자재를 여기서 고친다 ──────────────────────────────────────────────\n * 안에 들어가면 그 레시피의 투입이 줄로 선다. 고치면 **레시피 한 건의 `inputs` 를 통째로 다시 쓴다** —\n * `RecipePart` 는 자기 식별자가 없어서 줄 단위로 저장할 수 없다. 그래서 저장은 언제나 그 레시피\n * 하나에 대한 한 번의 쓰기다.\n *\n * ── 무엇을 만드는가는 고정 칸이다 ──────────────────────────────────────────\n * `materialDefinitionName` 이 이 레시피가 만드는 품목이고, `route` 가 그것이 타고 갈 라우팅이다. 이\n * 둘이 **품목과 라우팅을 잇는 유일한 자리**다 — 품목에서 라우팅으로 바로 가는 길은 없고, 있어서도 안 된다.\n * 같은 품목을 다른 라우팅으로 만드는 일이 있기 때문이다(라인 증설 · 시제품).\n */\n@customElement('ops-master-recipe-definition')\nexport class OpsMasterRecipeDefinition extends MasterPageBase {\n static styles = [\n ...MasterPageBase.styles,\n css`\n .scope {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: 2px;\n min-height: 26px;\n }\n\n .scope button {\n display: flex;\n align-items: center;\n gap: 2px;\n border: none;\n background: none;\n padding: 2px 6px;\n cursor: pointer;\n color: var(--md-sys-color-primary);\n font: inherit;\n }\n\n .scope .sep {\n opacity: 0.4;\n }\n\n .scope .here {\n padding: 2px 6px;\n font-weight: 600;\n }\n\n .scope .makes {\n margin-left: 8px;\n opacity: 0.7;\n font-size: 0.9em;\n }\n `\n ]\n\n /**\n * 지금 열어 본 레시피. 비어 있으면 목록을 보고 있다.\n *\n * **이 값의 주인은 URL 이다.** 여기 직접 넣지 않는다 — `enter` 는 주소만 바꾸고, 바뀐 주소를\n * `pageUpdated` 가 받아 이 값을 다시 세운다.\n */\n @state() private opened?: RecipeRecord\n\n protected get spec(): MasterSpec {\n /* 안에 들어와 있으면 투입을 보이고, 아니면 레시피 목록을 보인다. */\n return this.opened ? this.partsSpec() : this.listSpec()\n }\n\n /** 레시피 목록 — 무엇을 만드나 · 어느 라우팅으로 · 투입이 몇 줄인가. */\n private listSpec(): MasterSpec {\n return {\n key: 'recipeDefinition',\n title: 'recipe',\n help: 'ops-master/recipe-definition',\n query: 'recipeDefinitions',\n mutation: 'updateMultipleRecipeDefinition',\n columns: [\n ...IDENTITY,\n {\n name: 'materialDefinitionName',\n label: 'makes',\n type: 'reference',\n reference: { queryName: 'materialDefinitions' },\n width: 180\n },\n { name: 'route', label: 'route', type: 'reference', reference: { queryName: 'routeDefinitions' }, width: 160 },\n /* 이 자리에 「투입 자재」 문이 들어간다 — 실제 칸은 `configOf` 가 갈아 끼운다. */\n { name: 'inputCount', label: 'inputs', type: 'number', width: 96, readonly: true },\n ...EFFECTIVE\n ]\n }\n }\n\n /**\n * 이 제조법에 들어가는 자재들 — 안으로 들어왔을 때의 표.\n *\n * 저장은 이 화면이 스스로 한다(`save` 를 덮어쓴다). `key` 와 `mutation` 은 그대로 두어 화면\n * 골격이 같은 축을 가리키게 한다.\n */\n /**\n * 「투입 자재」 칸을 목록에 끼운다 — **스펙이 아니라 여기서.**\n *\n * `MasterColumn` 은 렌더러를 모르는 좁은 타입이다. 자리 화면도 같은 이유로 여기서 끼운다\n * (`operational-location` 의 `configOf`). 스펙에 억지로 넣으면 타입을 속여야 하고, 그러면 다음\n * 사람이 스펙에 무엇을 넣을 수 있는지 알 수 없게 된다.\n */\n protected configOf() {\n const config = super.configOf()\n\n /* 안에 들어와 있으면 투입 자재 표다 — 그 표에는 이 칸이 없다. */\n if (this.opened) {\n return config\n }\n\n const columns = [...config.columns]\n const at = columns.findIndex((column: any) => column?.name === 'inputCount')\n\n if (at >= 0) {\n columns[at] = this.inputsColumn()\n }\n\n return { ...config, columns }\n }\n\n /**\n * ── 개수는 **문이다.** 죽은 숫자가 아니다 ───────────────────────────────\n *\n * 처음에는 그냥 못 고치는 숫자였다. 사용자가 물었다(2026-09-05): 「이게 뭐고 왜 편집이 안 되나」.\n * 둘 다 맞는 물음이었다 — 고칠 수 없는 칸이 **왜** 고칠 수 없는지 화면이 말하지 않으면, 보는\n * 사람은 그것을 망가진 칸으로 읽는다.\n *\n * 이 수는 값이 아니라 안에 든 것의 수다. 바꾸려면 안에 들어가 자재를 더하거나 빼야 한다. 그러니\n * 그 길을 이 칸이 직접 내야 한다 — 자리 화면의 「하위」가 이미 그렇게 한다.\n *\n * ```\n * 투입 자재\n * 3 › 셋이 들어간다. 누르면 그리로 간다\n * ― 아직 안 정했다. 누를 데가 없다\n * ```\n */\n private inputsColumn() {\n return {\n type: 'string',\n name: 'inputCount',\n width: 96,\n /* 폭을 끌 것이 없고, 개수로 정렬하려면 서버가 그 축을 내야 한다. */\n resizable: false,\n sortable: false,\n header: i18next.t('field.inputs'),\n record: {\n align: 'center',\n renderer: (_value: any, _column: any, record: RecipeRecord) => {\n /* 표 끝의 빈 줄에는 그리지 않는다 — 갈 곳이 없다. */\n if (!record?.id) return html``\n\n const count = (record as any).inputCount ?? 0\n\n /*\n * 비었으면 아무것도 그리지 않는다. 선택해도 빈 화면이 나오는 버튼은 없는 것이 낫고, 비우면\n * **아직 자재를 안 정한 제조법**이 눈에 띈다 — 그게 이 칸이 답하는 물음이다.\n */\n if (!count) return html``\n\n return html`<button\n class=\"icon wide\"\n title=${i18next.t('button.enter-recipe')}\n @click=${(e: Event) => {\n e.stopPropagation()\n this.enter(record)\n }}\n >\n <span>${count}</span>\n <md-icon>chevron_right</md-icon>\n </button>`\n }\n }\n }\n }\n\n private partsSpec(): MasterSpec {\n return {\n key: 'recipeDefinition',\n title: 'recipe',\n help: 'ops-master/recipe-definition',\n query: 'recipeDefinitions',\n mutation: 'updateMultipleRecipeDefinition',\n columns: [\n {\n name: 'material',\n label: 'material',\n type: 'reference',\n reference: { queryName: 'materialDefinitions' },\n width: 220,\n fixed: true\n },\n { name: 'qty', label: 'quantity', type: 'number', width: 100 },\n /*\n * 어느 공정에서 들어가나 — **BOM 이 말하지 않는 것.**\n *\n * ERP 의 BOM 은 「한 대에 몇 개」만 말한다. 그것이 언제 빠지는지는 작업표준서에 있고, 둘을\n * 붙여야 재고가 언제 줄어드는지가 정해진다. 붙이지 않으면 소요는 있는데 빠지는 시각이 없다.\n */\n {\n name: 'operation',\n label: 'operation',\n type: 'reference',\n reference: { queryName: 'operationDefinitions' },\n width: 200\n },\n { name: 'kind', label: 'part-kind', type: 'string', width: 90, readonly: true }\n ]\n }\n }\n\n /* 이 화면은 주소 조각을 **파고들기**에 쓴다 — 목록 좁히기로 겹쳐 쓰지 않는다. */\n protected get pathSegmentNarrowsList(): boolean {\n return false\n }\n\n async pageInitialized() {\n /*\n * 주소에 레시피가 담겨 있으면 여기서도 연다.\n *\n * `pageUpdated` 만 믿으면 주소로 바로 들어온 경우가 빠진다 — 그 호출이 이 함수보다 먼저 지나간다.\n * 로케이션 화면이 같은 것에 물렸다(2026-09-01).\n */\n await this.syncOpened(this.lifecycle?.resourceId)\n\n await super.pageInitialized()\n }\n\n async pageUpdated(changes: any, after: any, before: any) {\n if (after?.resourceId !== before?.resourceId) {\n await this.syncOpened(after?.resourceId)\n }\n\n await super.pageUpdated()\n }\n\n /** 주소가 가리키는 레시피를 연다. 없으면 목록이다. */\n private async syncOpened(key?: string) {\n this.opened = key ? await this.lookup(key) : undefined\n }\n\n /** 레시피 하나를 찾는다. 아이디로 찾고, 없으면 이름으로 찾는다 — 주소에는 사람이 읽는 이름이 온다. */\n private async lookup(key: string): Promise<RecipeRecord | undefined> {\n for (const field of ['id', 'name']) {\n const { records } = await this.fetchRecipes({\n page: 1,\n limit: 1,\n filters: [{ name: field, operator: 'eq', value: key }]\n } as FetchOption)\n\n if (records.length) {\n return records[0]\n }\n }\n\n return undefined\n }\n\n /** 레시피 자체를 읽는다 — 안에 들어와 있어도 이 질의는 레시피를 본다. */\n private async fetchRecipes(option: FetchOption): Promise<{ total: number; records: RecipeRecord[] }> {\n const { page = 1, limit = 50, sortings = [], filters = [] } = option\n const response = await client.query({\n query: gql`\n query ($filters: [Filter!], $pagination: Pagination, $sortings: [Sorting!]) {\n recipeDefinitions(filters: $filters, pagination: $pagination, sortings: $sortings) {\n items {\n id\n name\n description\n materialDefinitionName\n route\n inputs\n outputs\n effectiveStart\n effectiveEnd\n }\n total\n }\n }\n `,\n variables: { filters, pagination: { page, limit }, sortings },\n fetchPolicy: 'no-cache'\n })\n\n const payload = response.data?.recipeDefinitions\n\n return { total: payload?.total ?? 0, records: (payload?.items ?? []) as RecipeRecord[] }\n }\n\n /**\n * 목록일 때는 레시피를, 안에서는 그 레시피의 투입과 산출을 낸다.\n *\n * **투입 자재 줄에는 서버가 준 식별자가 없다.** `RecipePart` 는 자기 아이디가 없어서, 화면이 순번으로\n * 만들어 준다 — 그 값은 저장할 때 쓰지 않고 표가 줄을 가리키는 데만 쓴다.\n */\n async fetchHandler(option: FetchOption) {\n if (!this.opened) {\n const { total, records } = await this.fetchRecipes(option)\n\n return { total, records: records.map(one => ({ ...one, inputCount: (one.inputs ?? []).length })) }\n }\n\n /* 안에서는 다시 읽는다 — 저장한 뒤 이 화면이 스스로 새로 고칠 수 있어야 한다. */\n const fresh = (await this.lookup(this.opened.name)) ?? this.opened\n this.opened = fresh\n\n const rows = [\n ...(fresh.inputs ?? []).map((part, at) => ({ id: `in-${at}`, kind: 'input', ...part })),\n ...(fresh.outputs ?? []).map((part, at) => ({ id: `out-${at}`, kind: 'output', ...part }))\n ]\n\n return { total: rows.length, records: rows }\n }\n\n protected renderScope() {\n if (!this.opened) {\n return html``\n }\n\n return html`\n <div class=\"scope\">\n <button @click=${() => this.enter(null)}>\n <md-icon>home</md-icon>\n ${i18next.t('label.all-recipes')}\n </button>\n <span class=\"sep\">/</span><span class=\"here\">${this.opened.name}</span>\n ${this.opened.materialDefinitionName\n ? html`<span class=\"makes\"\n >${i18next.t('label.makes')} ${this.opened.materialDefinitionName}${this.opened.route\n ? ` · ${this.opened.route}`\n : ''}</span\n >`\n : ''}\n </div>\n `\n }\n\n /**\n * 그 레시피로 들어간다. `null` 이면 목록으로 돌아간다.\n *\n * **주소만 바꾼다.** 셸이 주소를 물고 `pageUpdated` 를 부르고, 거기서 하나로 다시 선다.\n */\n private enter(recipe: RecipeRecord | null) {\n /* 아직 저장하지 않은 줄로는 들어갈 수 없다. 표 끝의 빈 줄에도 이 버튼이 붙는다. */\n if (recipe && !recipe.id) {\n return\n }\n\n navigate(recipe ? `${this.lifecycle?.page}/${recipe.name || recipe.id}` : `${this.lifecycle?.page}`)\n }\n\n /**\n * 저장 — **투입 자재 줄에는 자기 식별자가 없다.**\n *\n * `RecipePart` 는 아이디를 갖지 않아서 줄 단위로 저장할 수 없다. 그래서 안에서 고친 것은 표에\n * 서 있는 줄 전부를 모아 **그 레시피 한 건의 `inputs`·`outputs` 를 통째로 다시 쓴다.**\n *\n * 통째로 쓰는 것이 위험해 보이지만 그 반대가 더 위험하다 — 줄마다 쓰면 「셋째 줄」이 무엇인지\n * 서버가 알 방법이 없어서, 두 사람이 같은 레시피를 고칠 때 어느 줄이 어느 줄을 덮는지 정할 수\n * 없다. 한 번의 쓰기면 뒤엣사람이 앞엣사람의 것을 통째로 덮는 것이 눈에 보인다.\n *\n * 목록에서는 화면 골격의 저장을 그대로 쓴다 — 거기서는 레시피 한 줄이 곧 한 행이다.\n */\n protected async save() {\n if (!this.opened) {\n return await super.save()\n }\n\n if (!this.grist.dirtyRecords?.length) {\n notify({ message: i18next.t('text.nothing-changed'), level: 'info' })\n return\n }\n\n /* 표에 서 있는 모든 줄 — 고친 것만이 아니라 전부다. 통째로 다시 쓰기 때문이다. */\n const rows: any[] = this.grist.dirtyData?.records ?? []\n const part = (row: any) => ({\n material: String(row.material ?? '').trim(),\n qty: Number(row.qty ?? 0),\n ...(String(row.operation ?? '').trim() ? { operation: String(row.operation).trim() } : {})\n })\n\n const nameless = rows.filter(row => !String(row.material ?? '').trim())\n\n if (nameless.length) {\n /* 품목 없는 줄은 소요가 아니다. 하나라도 있으면 보내지 않는다. 절반만 저장되면 화면과 달라진다. */\n notify({ message: i18next.t('error.recipe-part-needs-material'), level: 'error' })\n return\n }\n\n const response = await client.mutate({\n mutation: gql`\n mutation ($patches: [Object!]!) {\n updateMultipleRecipeDefinition(patches: $patches) {\n id\n }\n }\n `,\n variables: {\n patches: [\n {\n id: this.opened.id,\n name: this.opened.name,\n cuFlag: 'M',\n inputs: rows.filter(row => row.kind !== 'output').map(part),\n outputs: rows.filter(row => row.kind === 'output').map(part)\n }\n ]\n }\n })\n\n /* 이 클라이언트는 서버가 거절해도 던지지 않는다. 사유는 앱의 공통 오류 처리가 띄운다. */\n if (response?.errors?.length) {\n return\n }\n\n this.grist.fetch()\n }\n}\n"]}
1
+ {"version":3,"file":"recipe-definition.js","sourceRoot":"","sources":["../../../client/pages/master/recipe-definition.ts"],"names":[],"mappings":";AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AACjD,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAA;AAIpD,IAAM,yBAAyB,GAA/B,MAAM,yBAA0B,SAAQ,iBAAiB;IAAzD;;QACK,SAAI,GAAiB,kBAAkB,CAAA;IACnD,CAAC;CAAA,CAAA;AAFY,yBAAyB;IADrC,aAAa,CAAC,8BAA8B,CAAC;GACjC,yBAAyB,CAErC","sourcesContent":["import { customElement } from 'lit/decorators.js'\nimport { RevisionWorkspace } from './revision-workspace.js'\nimport { RevisionAxis } from './revision-view.js'\n\n@customElement('ops-master-recipe-definition')\nexport class OpsMasterRecipeDefinition extends RevisionWorkspace {\n protected axis: RevisionAxis = 'recipeDefinition'\n}\n"]}
@@ -0,0 +1,48 @@
1
+ import { LitElement } from 'lit';
2
+ import { RevisionAxis, RevisionSide } from './revision-view.js';
3
+ export declare const revisionText: (key: string, params?: any) => string;
4
+ export declare const revisionDate: (value?: string) => string;
5
+ export declare const revisionStyles: import("lit").CSSResult;
6
+ declare const MasterRevisionComparison_base: (new (...args: any[]) => LitElement) & typeof LitElement;
7
+ export declare class MasterRevisionComparison extends MasterRevisionComparison_base {
8
+ static styles: import("lit").CSSResult[];
9
+ axis: RevisionAxis;
10
+ before: any;
11
+ after: any;
12
+ private display;
13
+ render(): import("lit-html").TemplateResult<1>;
14
+ }
15
+ declare const MasterRevisionReview_base: (new (...args: any[]) => LitElement) & typeof LitElement;
16
+ /** Uses the existing Worklist custom-element slot. No Worklist dependency in the master module. */
17
+ export declare class MasterRevisionReview extends MasterRevisionReview_base {
18
+ static styles: import("lit").CSSResult[];
19
+ input: any;
20
+ activityApproval: any;
21
+ private detailError;
22
+ private static dockOwner;
23
+ private static activeReview?;
24
+ private static enabled;
25
+ private static detailPath;
26
+ private static detailRevision;
27
+ private static onAddress;
28
+ connectedCallback(): void;
29
+ disconnectedCallback(): void;
30
+ updated(changes: any): void;
31
+ private syncDetail;
32
+ private closeDetail;
33
+ private showDetail;
34
+ private openDetail;
35
+ private entityLink;
36
+ render(): import("lit-html").TemplateResult<1>;
37
+ }
38
+ export declare function revisionContents(value: any, axis: RevisionAxis): import("lit-html").TemplateResult<1> | import("lit-html").TemplateResult<1>[];
39
+ declare const MasterRevision360_base: (new (...args: any[]) => LitElement) & typeof LitElement;
40
+ /** Read-only revision contents. Layout, scrolling and closing belong to the common dock. */
41
+ export declare class MasterRevision360 extends MasterRevision360_base {
42
+ static styles: import("lit").CSSResult[];
43
+ side: RevisionSide;
44
+ input: any;
45
+ activityApproval: any;
46
+ render(): import("lit-html").TemplateResult<1>;
47
+ }
48
+ export {};