@things-factory/inquiry 0.0.0-stage → 10.1.252

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 (106) hide show
  1. package/client/bootstrap.ts +27 -0
  2. package/client/components/inquiry-thread.ts +277 -0
  3. package/client/components/inquiry-todo.ts +36 -0
  4. package/client/components/inquiry-writer.ts +212 -0
  5. package/client/index.ts +2 -0
  6. package/client/inquiry-watch.ts +45 -0
  7. package/client/pages/inquiry-list.ts +218 -0
  8. package/client/route.ts +8 -0
  9. package/client/tsconfig.json +13 -0
  10. package/client/utils/inquiry-api.ts +103 -0
  11. package/client/utils/inquiry-badge.ts +46 -0
  12. package/client/utils/inquiry-words.ts +26 -0
  13. package/dist-client/bootstrap.d.ts +2 -0
  14. package/dist-client/bootstrap.js +25 -0
  15. package/dist-client/bootstrap.js.map +1 -0
  16. package/dist-client/components/inquiry-thread.d.ts +28 -0
  17. package/dist-client/components/inquiry-thread.js +303 -0
  18. package/dist-client/components/inquiry-thread.js.map +1 -0
  19. package/dist-client/components/inquiry-todo.d.ts +9 -0
  20. package/dist-client/components/inquiry-todo.js +43 -0
  21. package/dist-client/components/inquiry-todo.js.map +1 -0
  22. package/dist-client/components/inquiry-writer.d.ts +25 -0
  23. package/dist-client/components/inquiry-writer.js +233 -0
  24. package/dist-client/components/inquiry-writer.js.map +1 -0
  25. package/dist-client/index.d.ts +2 -0
  26. package/dist-client/index.js +3 -0
  27. package/dist-client/index.js.map +1 -0
  28. package/dist-client/inquiry-watch.d.ts +2 -0
  29. package/dist-client/inquiry-watch.js +41 -0
  30. package/dist-client/inquiry-watch.js.map +1 -0
  31. package/dist-client/pages/inquiry-list.d.ts +56 -0
  32. package/dist-client/pages/inquiry-list.js +241 -0
  33. package/dist-client/pages/inquiry-list.js.map +1 -0
  34. package/dist-client/route.d.ts +1 -0
  35. package/dist-client/route.js +9 -0
  36. package/dist-client/route.js.map +1 -0
  37. package/dist-client/tsconfig.tsbuildinfo +1 -0
  38. package/dist-client/utils/inquiry-api.d.ts +73 -0
  39. package/dist-client/utils/inquiry-api.js +34 -0
  40. package/dist-client/utils/inquiry-api.js.map +1 -0
  41. package/dist-client/utils/inquiry-badge.d.ts +4 -0
  42. package/dist-client/utils/inquiry-badge.js +47 -0
  43. package/dist-client/utils/inquiry-badge.js.map +1 -0
  44. package/dist-client/utils/inquiry-words.d.ts +13 -0
  45. package/dist-client/utils/inquiry-words.js +22 -0
  46. package/dist-client/utils/inquiry-words.js.map +1 -0
  47. package/dist-server/attachment-reader.d.ts +1 -0
  48. package/dist-server/attachment-reader.js +21 -0
  49. package/dist-server/attachment-reader.js.map +1 -0
  50. package/dist-server/index.d.ts +2 -0
  51. package/dist-server/index.js +22 -0
  52. package/dist-server/index.js.map +1 -0
  53. package/dist-server/inquiry-activity.d.ts +38 -0
  54. package/dist-server/inquiry-activity.js +38 -0
  55. package/dist-server/inquiry-activity.js.map +1 -0
  56. package/dist-server/role-tasks.d.ts +2 -0
  57. package/dist-server/role-tasks.js +14 -0
  58. package/dist-server/role-tasks.js.map +1 -0
  59. package/dist-server/service/index.d.ts +7 -0
  60. package/dist-server/service/index.js +24 -0
  61. package/dist-server/service/index.js.map +1 -0
  62. package/dist-server/service/inquiry/index.d.ts +4 -0
  63. package/dist-server/service/inquiry/index.js +8 -0
  64. package/dist-server/service/inquiry/index.js.map +1 -0
  65. package/dist-server/service/inquiry/inquiry-resolver.d.ts +29 -0
  66. package/dist-server/service/inquiry/inquiry-resolver.js +407 -0
  67. package/dist-server/service/inquiry/inquiry-resolver.js.map +1 -0
  68. package/dist-server/service/inquiry/inquiry-rules.d.ts +88 -0
  69. package/dist-server/service/inquiry/inquiry-rules.js +157 -0
  70. package/dist-server/service/inquiry/inquiry-rules.js.map +1 -0
  71. package/dist-server/service/inquiry/inquiry-service.d.ts +35 -0
  72. package/dist-server/service/inquiry/inquiry-service.js +143 -0
  73. package/dist-server/service/inquiry/inquiry-service.js.map +1 -0
  74. package/dist-server/service/inquiry/inquiry-types.d.ts +18 -0
  75. package/dist-server/service/inquiry/inquiry-types.js +69 -0
  76. package/dist-server/service/inquiry/inquiry-types.js.map +1 -0
  77. package/dist-server/service/inquiry/inquiry.d.ts +48 -0
  78. package/dist-server/service/inquiry/inquiry.js +232 -0
  79. package/dist-server/service/inquiry/inquiry.js.map +1 -0
  80. package/dist-server/tsconfig.tsbuildinfo +1 -0
  81. package/package.json +51 -4
  82. package/server/attachment-reader.ts +18 -0
  83. package/server/index.ts +18 -0
  84. package/server/inquiry-activity.ts +35 -0
  85. package/server/role-tasks.ts +13 -0
  86. package/server/service/index.ts +19 -0
  87. package/server/service/inquiry/index.ts +5 -0
  88. package/server/service/inquiry/inquiry-resolver.ts +321 -0
  89. package/server/service/inquiry/inquiry-rules.ts +173 -0
  90. package/server/service/inquiry/inquiry-service.ts +143 -0
  91. package/server/service/inquiry/inquiry-types.ts +45 -0
  92. package/server/service/inquiry/inquiry.ts +180 -0
  93. package/server/tsconfig.json +11 -0
  94. package/test/inquiry-persistence.test.ts +218 -0
  95. package/test/inquiry-rules.test.ts +121 -0
  96. package/test/open-doors.test.ts +44 -0
  97. package/test/translations.test.ts +53 -0
  98. package/test/writer-popup-standard.test.ts +40 -0
  99. package/things-factory.config.js +8 -0
  100. package/translations/en.json +93 -0
  101. package/translations/es.json +93 -0
  102. package/translations/ja.json +93 -0
  103. package/translations/ko.json +93 -0
  104. package/translations/ms.json +93 -0
  105. package/translations/zh.json +93 -0
  106. package/README.md +0 -3
@@ -0,0 +1,303 @@
1
+ import { __decorate, __metadata } from "tslib";
2
+ /*
3
+ * One inquiry as its thread (ADR-0184 decisions 2-4): what was asked, from which screen and application, the files, and
4
+ * the replies between its writer and the handlers. Below it, the reply box and the moves the person may make.
5
+ */
6
+ import '@material/web/icon/icon.js';
7
+ import '@material/web/button/filled-button.js';
8
+ import '@material/web/button/outlined-button.js';
9
+ import '@material/web/button/text-button.js';
10
+ import '@operato/input/ox-input-file.js';
11
+ import { css, html, LitElement, nothing } from 'lit';
12
+ import { customElement, property, state } from 'lit/decorators.js';
13
+ import { i18next, localize } from '@operato/i18n';
14
+ import { notify } from '@operato/layout';
15
+ import { ButtonStyles, FormFieldStyles } from '@operato/styles';
16
+ import { inquiriesChanged, inquiryApi } from '../utils/inquiry-api.js';
17
+ import { KIND_ICONS, kindWord, reasonOf, statusWord, t, visibilityWord, when } from '../utils/inquiry-words.js';
18
+ /** The words of a move button - a move to CLOSED by its writer and by a handler read the same. */
19
+ const moveWord = (status) => t(`inquiry.move.${status}`, { defaultValue: statusWord(status) });
20
+ let InquiryThread = class InquiryThread extends localize(i18next)(LitElement) {
21
+ constructor() {
22
+ super(...arguments);
23
+ this.inquiryId = '';
24
+ this.one = null;
25
+ this.missing = false;
26
+ this.text = '';
27
+ this.files = [];
28
+ this.busy = false;
29
+ }
30
+ static { this.styles = [
31
+ FormFieldStyles,
32
+ ButtonStyles,
33
+ css `
34
+ :host {
35
+ display: flex;
36
+ flex-direction: column;
37
+ gap: var(--spacing-medium, 8px);
38
+ color: var(--md-sys-color-on-surface);
39
+ }
40
+ h2 {
41
+ margin: 0;
42
+ font-size: var(--ui-t-title, 16px);
43
+ }
44
+ .meta {
45
+ display: flex;
46
+ flex-wrap: wrap;
47
+ align-items: center;
48
+ gap: var(--spacing-small, 4px) var(--spacing-medium, 8px);
49
+ font-size: var(--ui-t-small, 12px);
50
+ color: var(--ui-muted, var(--md-sys-color-on-surface-variant));
51
+ }
52
+ .chip {
53
+ display: inline-flex;
54
+ align-items: center;
55
+ gap: 2px;
56
+ padding: 0 var(--spacing-small, 4px);
57
+ border-radius: var(--border-radius, 4px);
58
+ border: 1px solid var(--ui-line-soft, var(--md-sys-color-outline-variant));
59
+ line-height: 1.6;
60
+ }
61
+ .chip md-icon {
62
+ --md-icon-size: 14px;
63
+ }
64
+ .chip.status {
65
+ color: var(--md-sys-color-primary);
66
+ border-color: currentColor;
67
+ }
68
+ .text {
69
+ white-space: pre-wrap;
70
+ word-break: break-word;
71
+ line-height: 1.6;
72
+ }
73
+ .context {
74
+ font-size: var(--ui-t-small, 12px);
75
+ color: var(--ui-muted, var(--md-sys-color-on-surface-variant));
76
+ word-break: break-all;
77
+ }
78
+ .context a {
79
+ color: inherit;
80
+ }
81
+ ul.files {
82
+ margin: 0;
83
+ padding: 0;
84
+ list-style: none;
85
+ display: flex;
86
+ flex-wrap: wrap;
87
+ gap: var(--spacing-small, 4px) var(--spacing-medium, 8px);
88
+ font-size: var(--ui-t-small, 12px);
89
+ }
90
+ ul.files a {
91
+ display: inline-flex;
92
+ align-items: center;
93
+ gap: 2px;
94
+ color: var(--md-sys-color-primary);
95
+ }
96
+ ul.files md-icon {
97
+ --md-icon-size: 14px;
98
+ }
99
+ ol.replies {
100
+ margin: 0;
101
+ padding: 0;
102
+ list-style: none;
103
+ display: flex;
104
+ flex-direction: column;
105
+ gap: var(--spacing-medium, 8px);
106
+ }
107
+ ol.replies li {
108
+ padding: var(--spacing-medium, 8px) var(--spacing-large, 12px);
109
+ border-radius: var(--border-radius, 4px);
110
+ border: 1px solid var(--ui-line-soft, var(--md-sys-color-outline-variant));
111
+ display: flex;
112
+ flex-direction: column;
113
+ gap: 4px;
114
+ }
115
+ ol.replies li.handler {
116
+ background: var(--ui-panel-2, var(--md-sys-color-surface-container));
117
+ }
118
+ ol.replies li.answer {
119
+ border-color: var(--md-sys-color-primary);
120
+ }
121
+ .move {
122
+ font-size: var(--ui-t-small, 12px);
123
+ color: var(--md-sys-color-primary);
124
+ font-weight: 600;
125
+ }
126
+ .box {
127
+ display: flex;
128
+ flex-direction: column;
129
+ gap: var(--spacing-small, 4px);
130
+ }
131
+ textarea {
132
+ min-height: 96px;
133
+ font-family: inherit;
134
+ box-sizing: border-box;
135
+ width: 100%;
136
+ }
137
+ .hint {
138
+ font-size: var(--ui-t-small, 12px);
139
+ color: var(--ui-muted, var(--md-sys-color-on-surface-variant));
140
+ }
141
+ .row {
142
+ display: flex;
143
+ flex-wrap: wrap;
144
+ align-items: center;
145
+ gap: var(--spacing-small, 4px) var(--spacing-medium, 8px);
146
+ }
147
+ .row .spacer {
148
+ flex: 1;
149
+ }
150
+ `
151
+ ]; }
152
+ updated(changes) {
153
+ if (changes.has('inquiryId'))
154
+ this.load(true);
155
+ }
156
+ async load(opened = false) {
157
+ if (!this.inquiryId)
158
+ return;
159
+ try {
160
+ this.one = await inquiryApi.one(this.inquiryId);
161
+ this.missing = !this.one;
162
+ /* Its writer opening it reads the handlers' words - the badge counts again. */
163
+ if (opened && this.one?.mine && this.one.isNew) {
164
+ await inquiryApi.read(this.one.id);
165
+ inquiriesChanged();
166
+ }
167
+ }
168
+ catch (error) {
169
+ notify({ level: 'error', message: reasonOf(error) });
170
+ }
171
+ }
172
+ render() {
173
+ if (this.missing)
174
+ return html `<p class="hint">${t('inquiry.text.not-found')}</p>`;
175
+ const one = this.one;
176
+ if (!one)
177
+ return nothing;
178
+ const replies = one.replies ?? [];
179
+ const handlerMoves = (one.moves ?? []).filter(status => status !== 'CLOSED');
180
+ return html `
181
+ <div class="meta">
182
+ <span class="chip"><md-icon>${KIND_ICONS[one.kind] ?? 'help'}</md-icon>${kindWord(one.kind)}</span>
183
+ <span class="chip status">${statusWord(one.status)}</span>
184
+ ${one.visibility === 'PRIVATE' ? html `<span class="chip"><md-icon>lock</md-icon>${visibilityWord(one.visibility)}</span>` : nothing}
185
+ <span>${one.creator?.name ?? ''} · ${when(one.createdAt)}</span>
186
+ ${one.assignee ? html `<span>${t('inquiry.text.assignee', { name: one.assignee.name ?? '' })}</span>` : nothing}
187
+ </div>
188
+ <h2>${one.title}</h2>
189
+ ${one.body ? html `<div class="text">${one.body}</div>` : nothing} ${this.renderFiles(one.attachments)}
190
+ <div class="context">
191
+ ${one.pageUrl ? html `<div>${t('inquiry.field.page')}: ${/^https?:\/\//i.test(one.pageUrl) ? html `<a href=${one.pageUrl} target="_blank" rel="noopener">${one.pageUrl}</a>` : one.pageUrl}</div>` : nothing}
192
+ ${one.appName ? html `<div>${t('inquiry.field.app')}: ${one.appName}${one.appVersion ? ` ${one.appVersion}` : ''}</div>` : nothing}
193
+ </div>
194
+
195
+ ${replies.length
196
+ ? html `<ol class="replies">
197
+ ${replies.map(reply => html `<li class=${[reply.byHandler ? 'handler' : '', reply.answer ? 'answer' : ''].join(' ')}>
198
+ <div class="meta">
199
+ <span>${reply.creator?.name ?? ''}</span>
200
+ ${reply.byHandler ? html `<span class="chip">${t('inquiry.text.handler')}</span>` : nothing}
201
+ ${reply.answer ? html `<span class="chip status"><md-icon>check</md-icon>${t('inquiry.text.answer')}</span>` : nothing}
202
+ <span>${when(reply.createdAt)}</span>
203
+ </div>
204
+ ${reply.status && !reply.answer ? html `<span class="move">${t('inquiry.text.moved', { status: statusWord(reply.status) })}</span>` : nothing}
205
+ ${reply.body ? html `<div class="text">${reply.body}</div>` : nothing} ${this.renderFiles(reply.attachments)}
206
+ </li>`)}
207
+ </ol>`
208
+ : nothing}
209
+ ${one.canReply ? this.renderBox(one, handlerMoves) : one.status === 'CLOSED' ? html `<p class="hint">${t('inquiry.text.closed-note')}</p>` : nothing}
210
+ `;
211
+ }
212
+ renderFiles(files) {
213
+ if (!files?.length)
214
+ return nothing;
215
+ return html `<ul class="files">
216
+ ${files.map(file => html `<li><a href=${file.fullpath} target="_blank" rel="noopener" download=${file.name}><md-icon>attach_file</md-icon>${file.name}</a></li>`)}
217
+ </ul>`;
218
+ }
219
+ renderBox(one, handlerMoves) {
220
+ const said = !!this.text.trim() || this.files.length > 0;
221
+ const canClose = (one.moves ?? []).includes('CLOSED');
222
+ return html `<div class="box">
223
+ <textarea .value=${this.text} placeholder=${t(one.asHandler ? 'inquiry.text.reply-placeholder-handler' : 'inquiry.text.reply-placeholder')} @input=${(e) => (this.text = e.target.value)}></textarea>
224
+ <div class="row">
225
+ ${this.files.map(file => html `<span class="hint">${file.name}</span>`)}
226
+ <ox-input-file quiet hide-filelist multiple icon="attach_file" .progress=${this.progress} @change=${(e) => this.pick(e)}></ox-input-file>
227
+ <span class="spacer"></span>
228
+ ${one.asHandler && !one.assignee ? html `<md-text-button ?disabled=${this.busy} @click=${() => this.act(() => inquiryApi.take(one.id))}>${t('inquiry.button.take')}</md-text-button>` : nothing}
229
+ ${one.asHandler
230
+ ? handlerMoves.map(status => html `<md-text-button ?disabled=${this.busy} @click=${() => this.move(one, status)}>${moveWord(status)}</md-text-button>`)
231
+ : nothing}
232
+ ${canClose ? html `<md-text-button ?disabled=${this.busy} @click=${() => this.move(one, 'CLOSED')}>${moveWord('CLOSED')}</md-text-button>` : nothing}
233
+ <md-outlined-button ?disabled=${!said || this.busy} @click=${() => this.reply(one, false)}>${t('inquiry.button.reply')}</md-outlined-button>
234
+ ${one.asHandler ? html `<md-filled-button ?disabled=${!said || this.busy} @click=${() => this.reply(one, true)}>${t('inquiry.button.answer')}</md-filled-button>` : nothing}
235
+ </div>
236
+ <span class="hint">${t('inquiry.text.move-hint')}</span>
237
+ </div>`;
238
+ }
239
+ pick(e) {
240
+ const picked = [...(e.detail ?? [])];
241
+ if (!picked.length)
242
+ return;
243
+ e.target.reset?.();
244
+ this.files = [...this.files, ...picked];
245
+ }
246
+ async act(work) {
247
+ this.busy = true;
248
+ try {
249
+ await work();
250
+ this.text = '';
251
+ this.files = [];
252
+ inquiriesChanged();
253
+ await this.load();
254
+ }
255
+ catch (error) {
256
+ notify({ level: 'error', message: reasonOf(error) });
257
+ }
258
+ finally {
259
+ this.busy = false;
260
+ this.progress = undefined;
261
+ }
262
+ }
263
+ reply(one, asAnswer) {
264
+ return this.act(() => inquiryApi.reply(one.id, this.text.trim(), asAnswer, this.files, fraction => (this.progress = fraction)));
265
+ }
266
+ /* What is written in the box goes with the move as its reason. */
267
+ move(one, status) {
268
+ return this.act(() => inquiryApi.move(one.id, status, this.text.trim() || undefined));
269
+ }
270
+ };
271
+ __decorate([
272
+ property(),
273
+ __metadata("design:type", Object)
274
+ ], InquiryThread.prototype, "inquiryId", void 0);
275
+ __decorate([
276
+ state(),
277
+ __metadata("design:type", Object)
278
+ ], InquiryThread.prototype, "one", void 0);
279
+ __decorate([
280
+ state(),
281
+ __metadata("design:type", Object)
282
+ ], InquiryThread.prototype, "missing", void 0);
283
+ __decorate([
284
+ state(),
285
+ __metadata("design:type", Object)
286
+ ], InquiryThread.prototype, "text", void 0);
287
+ __decorate([
288
+ state(),
289
+ __metadata("design:type", Array)
290
+ ], InquiryThread.prototype, "files", void 0);
291
+ __decorate([
292
+ state(),
293
+ __metadata("design:type", Object)
294
+ ], InquiryThread.prototype, "busy", void 0);
295
+ __decorate([
296
+ state(),
297
+ __metadata("design:type", Number)
298
+ ], InquiryThread.prototype, "progress", void 0);
299
+ InquiryThread = __decorate([
300
+ customElement('inquiry-thread')
301
+ ], InquiryThread);
302
+ export { InquiryThread };
303
+ //# sourceMappingURL=inquiry-thread.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inquiry-thread.js","sourceRoot":"","sources":["../../client/components/inquiry-thread.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * One inquiry as its thread (ADR-0184 decisions 2-4): what was asked, from which screen and application, the files, and\n * the replies between its writer and the handlers. Below it, the reply box and the moves the person may make.\n */\nimport '@material/web/icon/icon.js'\nimport '@material/web/button/filled-button.js'\nimport '@material/web/button/outlined-button.js'\nimport '@material/web/button/text-button.js'\nimport '@operato/input/ox-input-file.js'\n\nimport { css, html, LitElement, nothing } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\n\nimport { i18next, localize } from '@operato/i18n'\nimport { notify } from '@operato/layout'\nimport { ButtonStyles, FormFieldStyles } from '@operato/styles'\n\nimport { inquiriesChanged, inquiryApi, type FileRow, type InquiryItem } from '../utils/inquiry-api.js'\nimport { KIND_ICONS, kindWord, reasonOf, statusWord, t, visibilityWord, when } from '../utils/inquiry-words.js'\n\n/** The words of a move button - a move to CLOSED by its writer and by a handler read the same. */\nconst moveWord = (status: string) => t(`inquiry.move.${status}`, { defaultValue: statusWord(status) })\n\n@customElement('inquiry-thread')\nexport class InquiryThread extends localize(i18next)(LitElement) {\n static styles = [\n FormFieldStyles,\n ButtonStyles,\n css`\n :host {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-medium, 8px);\n color: var(--md-sys-color-on-surface);\n }\n h2 {\n margin: 0;\n font-size: var(--ui-t-title, 16px);\n }\n .meta {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: var(--spacing-small, 4px) var(--spacing-medium, 8px);\n font-size: var(--ui-t-small, 12px);\n color: var(--ui-muted, var(--md-sys-color-on-surface-variant));\n }\n .chip {\n display: inline-flex;\n align-items: center;\n gap: 2px;\n padding: 0 var(--spacing-small, 4px);\n border-radius: var(--border-radius, 4px);\n border: 1px solid var(--ui-line-soft, var(--md-sys-color-outline-variant));\n line-height: 1.6;\n }\n .chip md-icon {\n --md-icon-size: 14px;\n }\n .chip.status {\n color: var(--md-sys-color-primary);\n border-color: currentColor;\n }\n .text {\n white-space: pre-wrap;\n word-break: break-word;\n line-height: 1.6;\n }\n .context {\n font-size: var(--ui-t-small, 12px);\n color: var(--ui-muted, var(--md-sys-color-on-surface-variant));\n word-break: break-all;\n }\n .context a {\n color: inherit;\n }\n ul.files {\n margin: 0;\n padding: 0;\n list-style: none;\n display: flex;\n flex-wrap: wrap;\n gap: var(--spacing-small, 4px) var(--spacing-medium, 8px);\n font-size: var(--ui-t-small, 12px);\n }\n ul.files a {\n display: inline-flex;\n align-items: center;\n gap: 2px;\n color: var(--md-sys-color-primary);\n }\n ul.files md-icon {\n --md-icon-size: 14px;\n }\n ol.replies {\n margin: 0;\n padding: 0;\n list-style: none;\n display: flex;\n flex-direction: column;\n gap: var(--spacing-medium, 8px);\n }\n ol.replies li {\n padding: var(--spacing-medium, 8px) var(--spacing-large, 12px);\n border-radius: var(--border-radius, 4px);\n border: 1px solid var(--ui-line-soft, var(--md-sys-color-outline-variant));\n display: flex;\n flex-direction: column;\n gap: 4px;\n }\n ol.replies li.handler {\n background: var(--ui-panel-2, var(--md-sys-color-surface-container));\n }\n ol.replies li.answer {\n border-color: var(--md-sys-color-primary);\n }\n .move {\n font-size: var(--ui-t-small, 12px);\n color: var(--md-sys-color-primary);\n font-weight: 600;\n }\n .box {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-small, 4px);\n }\n textarea {\n min-height: 96px;\n font-family: inherit;\n box-sizing: border-box;\n width: 100%;\n }\n .hint {\n font-size: var(--ui-t-small, 12px);\n color: var(--ui-muted, var(--md-sys-color-on-surface-variant));\n }\n .row {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: var(--spacing-small, 4px) var(--spacing-medium, 8px);\n }\n .row .spacer {\n flex: 1;\n }\n `\n ]\n\n @property() inquiryId = ''\n @state() one: InquiryItem | null = null\n @state() missing = false\n @state() text = ''\n @state() files: File[] = []\n @state() busy = false\n /* Set only while files go up - the picker shows its bar then. */\n @state() progress?: number\n\n updated(changes: Map<string, unknown>) {\n if (changes.has('inquiryId')) this.load(true)\n }\n\n async load(opened = false) {\n if (!this.inquiryId) return\n try {\n this.one = await inquiryApi.one(this.inquiryId)\n this.missing = !this.one\n /* Its writer opening it reads the handlers' words - the badge counts again. */\n if (opened && this.one?.mine && this.one.isNew) {\n await inquiryApi.read(this.one.id)\n inquiriesChanged()\n }\n } catch (error) {\n notify({ level: 'error', message: reasonOf(error) })\n }\n }\n\n render() {\n if (this.missing) return html`<p class=\"hint\">${t('inquiry.text.not-found')}</p>`\n const one = this.one\n if (!one) return nothing\n const replies = one.replies ?? []\n const handlerMoves = (one.moves ?? []).filter(status => status !== 'CLOSED')\n return html`\n <div class=\"meta\">\n <span class=\"chip\"><md-icon>${KIND_ICONS[one.kind] ?? 'help'}</md-icon>${kindWord(one.kind)}</span>\n <span class=\"chip status\">${statusWord(one.status)}</span>\n ${one.visibility === 'PRIVATE' ? html`<span class=\"chip\"><md-icon>lock</md-icon>${visibilityWord(one.visibility)}</span>` : nothing}\n <span>${one.creator?.name ?? ''} · ${when(one.createdAt)}</span>\n ${one.assignee ? html`<span>${t('inquiry.text.assignee', { name: one.assignee.name ?? '' })}</span>` : nothing}\n </div>\n <h2>${one.title}</h2>\n ${one.body ? html`<div class=\"text\">${one.body}</div>` : nothing} ${this.renderFiles(one.attachments)}\n <div class=\"context\">\n ${one.pageUrl ? html`<div>${t('inquiry.field.page')}: ${/^https?:\\/\\//i.test(one.pageUrl) ? html`<a href=${one.pageUrl} target=\"_blank\" rel=\"noopener\">${one.pageUrl}</a>` : one.pageUrl}</div>` : nothing}\n ${one.appName ? html`<div>${t('inquiry.field.app')}: ${one.appName}${one.appVersion ? ` ${one.appVersion}` : ''}</div>` : nothing}\n </div>\n\n ${replies.length\n ? html`<ol class=\"replies\">\n ${replies.map(\n reply => html`<li class=${[reply.byHandler ? 'handler' : '', reply.answer ? 'answer' : ''].join(' ')}>\n <div class=\"meta\">\n <span>${reply.creator?.name ?? ''}</span>\n ${reply.byHandler ? html`<span class=\"chip\">${t('inquiry.text.handler')}</span>` : nothing}\n ${reply.answer ? html`<span class=\"chip status\"><md-icon>check</md-icon>${t('inquiry.text.answer')}</span>` : nothing}\n <span>${when(reply.createdAt)}</span>\n </div>\n ${reply.status && !reply.answer ? html`<span class=\"move\">${t('inquiry.text.moved', { status: statusWord(reply.status) })}</span>` : nothing}\n ${reply.body ? html`<div class=\"text\">${reply.body}</div>` : nothing} ${this.renderFiles(reply.attachments)}\n </li>`\n )}\n </ol>`\n : nothing}\n ${one.canReply ? this.renderBox(one, handlerMoves) : one.status === 'CLOSED' ? html`<p class=\"hint\">${t('inquiry.text.closed-note')}</p>` : nothing}\n `\n }\n\n private renderFiles(files?: FileRow[]) {\n if (!files?.length) return nothing\n return html`<ul class=\"files\">\n ${files.map(file => html`<li><a href=${file.fullpath} target=\"_blank\" rel=\"noopener\" download=${file.name}><md-icon>attach_file</md-icon>${file.name}</a></li>`)}\n </ul>`\n }\n\n private renderBox(one: InquiryItem, handlerMoves: string[]) {\n const said = !!this.text.trim() || this.files.length > 0\n const canClose = (one.moves ?? []).includes('CLOSED')\n return html`<div class=\"box\">\n <textarea .value=${this.text} placeholder=${t(one.asHandler ? 'inquiry.text.reply-placeholder-handler' : 'inquiry.text.reply-placeholder')} @input=${(e: any) => (this.text = e.target.value)}></textarea>\n <div class=\"row\">\n ${this.files.map(file => html`<span class=\"hint\">${file.name}</span>`)}\n <ox-input-file quiet hide-filelist multiple icon=\"attach_file\" .progress=${this.progress} @change=${(e: CustomEvent) => this.pick(e)}></ox-input-file>\n <span class=\"spacer\"></span>\n ${one.asHandler && !one.assignee ? html`<md-text-button ?disabled=${this.busy} @click=${() => this.act(() => inquiryApi.take(one.id))}>${t('inquiry.button.take')}</md-text-button>` : nothing}\n ${one.asHandler\n ? handlerMoves.map(status => html`<md-text-button ?disabled=${this.busy} @click=${() => this.move(one, status)}>${moveWord(status)}</md-text-button>`)\n : nothing}\n ${canClose ? html`<md-text-button ?disabled=${this.busy} @click=${() => this.move(one, 'CLOSED')}>${moveWord('CLOSED')}</md-text-button>` : nothing}\n <md-outlined-button ?disabled=${!said || this.busy} @click=${() => this.reply(one, false)}>${t('inquiry.button.reply')}</md-outlined-button>\n ${one.asHandler ? html`<md-filled-button ?disabled=${!said || this.busy} @click=${() => this.reply(one, true)}>${t('inquiry.button.answer')}</md-filled-button>` : nothing}\n </div>\n <span class=\"hint\">${t('inquiry.text.move-hint')}</span>\n </div>`\n }\n\n private pick(e: CustomEvent) {\n const picked = [...((e.detail as File[]) ?? [])]\n if (!picked.length) return\n ;(e.target as any).reset?.()\n this.files = [...this.files, ...picked]\n }\n\n private async act(work: () => Promise<unknown>) {\n this.busy = true\n try {\n await work()\n this.text = ''\n this.files = []\n inquiriesChanged()\n await this.load()\n } catch (error) {\n notify({ level: 'error', message: reasonOf(error) })\n } finally {\n this.busy = false\n this.progress = undefined\n }\n }\n\n private reply(one: InquiryItem, asAnswer: boolean) {\n return this.act(() => inquiryApi.reply(one.id, this.text.trim(), asAnswer, this.files, fraction => (this.progress = fraction)))\n }\n\n /* What is written in the box goes with the move as its reason. */\n private move(one: InquiryItem, status: string) {\n return this.act(() => inquiryApi.move(one.id, status, this.text.trim() || undefined))\n }\n}\n"]}
@@ -0,0 +1,9 @@
1
+ import '@material/web/button/filled-button.js';
2
+ import { LitElement } from 'lit';
3
+ export declare class InquiryTodo extends LitElement {
4
+ static styles: import("lit").CSSResult;
5
+ input: {
6
+ inquiryId?: string;
7
+ };
8
+ render(): import("lit-html").TemplateResult<1>;
9
+ }
@@ -0,0 +1,43 @@
1
+ import { __decorate, __metadata } from "tslib";
2
+ import '@material/web/button/filled-button.js';
3
+ import { css, html, LitElement } from 'lit';
4
+ import { customElement, property } from 'lit/decorators.js';
5
+ import { i18next } from '@operato/i18n';
6
+ import { navigate } from '@operato/shell';
7
+ /*
8
+ * 문의 처리의 할 일 (ADR-0184 decision 7) - what the worklist to-do shows: the way to the inquiry. Answering, deciding or
9
+ * closing it there closes the to-do; nothing is answered here.
10
+ */
11
+ let InquiryTodo = class InquiryTodo extends LitElement {
12
+ constructor() {
13
+ super(...arguments);
14
+ this.input = {};
15
+ }
16
+ static { this.styles = css `
17
+ :host {
18
+ display: block;
19
+ padding: var(--spacing-large, 12px);
20
+ font-size: var(--ui-t-body, 13px);
21
+ line-height: 1.6;
22
+ color: var(--ui-ink);
23
+ }
24
+
25
+ p {
26
+ margin: 0 0 var(--spacing-medium, 8px);
27
+ }
28
+ `; }
29
+ render() {
30
+ const id = this.input?.inquiryId ?? '';
31
+ return html `<p>${i18next.t('inquiry.text.todo-body')}</p>
32
+ <md-filled-button ?disabled=${!id} @click=${() => navigate(`inquiry-list?id=${id}`)}>${i18next.t('inquiry.button.open')}</md-filled-button>`;
33
+ }
34
+ };
35
+ __decorate([
36
+ property({ attribute: false }),
37
+ __metadata("design:type", Object)
38
+ ], InquiryTodo.prototype, "input", void 0);
39
+ InquiryTodo = __decorate([
40
+ customElement('inquiry-todo')
41
+ ], InquiryTodo);
42
+ export { InquiryTodo };
43
+ //# sourceMappingURL=inquiry-todo.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inquiry-todo.js","sourceRoot":"","sources":["../../client/components/inquiry-todo.ts"],"names":[],"mappings":";AAAA,OAAO,uCAAuC,CAAA;AAE9C,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,KAAK,CAAA;AAC3C,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AAE3D,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AACvC,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAEzC;;;GAGG;AAEI,IAAM,WAAW,GAAjB,MAAM,WAAY,SAAQ,UAAU;IAApC;;QAe2B,UAAK,GAA2B,EAAE,CAAA;IAOpE,CAAC;aArBQ,WAAM,GAAG,GAAG,CAAA;;;;;;;;;;;;GAYlB,AAZY,CAYZ;IAID,MAAM;QACJ,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,EAAE,SAAS,IAAI,EAAE,CAAA;QACtC,OAAO,IAAI,CAAA,MAAM,OAAO,CAAC,CAAC,CAAC,wBAAwB,CAAC;oCACpB,CAAC,EAAE,WAAW,GAAG,EAAE,CAAC,QAAQ,CAAC,mBAAmB,EAAE,EAAE,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,qBAAqB,CAAC,qBAAqB,CAAA;IAChJ,CAAC;;AAN+B;IAA/B,QAAQ,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;;0CAAmC;AAfvD,WAAW;IADvB,aAAa,CAAC,cAAc,CAAC;GACjB,WAAW,CAsBvB","sourcesContent":["import '@material/web/button/filled-button.js'\n\nimport { css, html, LitElement } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\n\nimport { i18next } from '@operato/i18n'\nimport { navigate } from '@operato/shell'\n\n/*\n * 문의 처리의 할 일 (ADR-0184 decision 7) - what the worklist to-do shows: the way to the inquiry. Answering, deciding or\n * closing it there closes the to-do; nothing is answered here.\n */\n@customElement('inquiry-todo')\nexport class InquiryTodo extends LitElement {\n static styles = css`\n :host {\n display: block;\n padding: var(--spacing-large, 12px);\n font-size: var(--ui-t-body, 13px);\n line-height: 1.6;\n color: var(--ui-ink);\n }\n\n p {\n margin: 0 0 var(--spacing-medium, 8px);\n }\n `\n\n @property({ attribute: false }) input: { inquiryId?: string } = {}\n\n render() {\n const id = this.input?.inquiryId ?? ''\n return html`<p>${i18next.t('inquiry.text.todo-body')}</p>\n <md-filled-button ?disabled=${!id} @click=${() => navigate(`inquiry-list?id=${id}`)}>${i18next.t('inquiry.button.open')}</md-filled-button>`\n }\n}\n"]}
@@ -0,0 +1,25 @@
1
+ import '@material/web/icon/icon.js';
2
+ import '@material/web/button/filled-button.js';
3
+ import '@material/web/button/text-button.js';
4
+ import '@operato/input/ox-input-file.js';
5
+ import '@operato/input/ox-input-select-buttons.js';
6
+ import '@operato/popup/ox-popup-actions.js';
7
+ import { LitElement } from 'lit';
8
+ /** Opens the window, carrying the screen it is opened on. */
9
+ export declare function openInquiryWriter(kind?: string): any;
10
+ declare const InquiryWriter_base: (new (...args: any[]) => LitElement) & typeof LitElement;
11
+ export declare class InquiryWriter extends InquiryWriter_base {
12
+ static styles: import("lit").CSSResult[];
13
+ kind: string;
14
+ pageUrl: string;
15
+ title: string;
16
+ body: string;
17
+ privately: boolean;
18
+ files: File[];
19
+ sending: boolean;
20
+ progress?: number;
21
+ render(): import("lit-html").TemplateResult<1>;
22
+ private pick;
23
+ private send;
24
+ }
25
+ export {};
@@ -0,0 +1,233 @@
1
+ import { __decorate, __metadata } from "tslib";
2
+ /*
3
+ * 문의 · 제안 보내기 (ADR-0184 decisions 1, 4, 5, 9) - the window the avatar menu opens on any screen: the kind, the title,
4
+ * the words, files, and who reads it. The screen it was opened on goes with it; the application, its version, the
5
+ * writer and the domain are the server's to take.
6
+ */
7
+ import '@material/web/icon/icon.js';
8
+ import '@material/web/button/filled-button.js';
9
+ import '@material/web/button/text-button.js';
10
+ import '@operato/input/ox-input-file.js';
11
+ import '@operato/input/ox-input-select-buttons.js';
12
+ import '@operato/popup/ox-popup-actions.js';
13
+ import { css, html, LitElement, nothing } from 'lit';
14
+ import { customElement, property, state } from 'lit/decorators.js';
15
+ import { i18next, localize } from '@operato/i18n';
16
+ import { notify, openPopup } from '@operato/layout';
17
+ import { CommonPopupStyles, FormFieldStyles, PopupBodyStyles, PopupFieldStyles } from '@operato/styles';
18
+ import { inquiriesChanged, inquiryApi } from '../utils/inquiry-api.js';
19
+ import { KINDS, kindWord, reasonOf, t } from '../utils/inquiry-words.js';
20
+ const MAX_TITLE = 200;
21
+ /** Opens the window, carrying the screen it is opened on. */
22
+ export function openInquiryWriter(kind = 'QUESTION') {
23
+ const pageUrl = location.href;
24
+ const handle = openPopup(html `<inquiry-writer .kind=${kind} .pageUrl=${pageUrl} @close=${() => handle.close()}></inquiry-writer>`, {
25
+ backdrop: true,
26
+ size: 'large',
27
+ title: t('inquiry.title.write')
28
+ });
29
+ return handle;
30
+ }
31
+ let InquiryWriter = class InquiryWriter extends localize(i18next)(LitElement) {
32
+ constructor() {
33
+ super(...arguments);
34
+ this.kind = 'QUESTION';
35
+ this.pageUrl = '';
36
+ this.title = '';
37
+ this.body = '';
38
+ this.privately = false;
39
+ this.files = [];
40
+ this.sending = false;
41
+ }
42
+ /* The house shape of a popup (page-standard sections 7, 8): only the body scrolls, fields are labels above native
43
+ inputs, the one button row is `ox-popup-actions`. */
44
+ static { this.styles = [
45
+ CommonPopupStyles,
46
+ PopupBodyStyles,
47
+ FormFieldStyles,
48
+ PopupFieldStyles,
49
+ css `
50
+ ox-input-select-buttons {
51
+ align-self: flex-start;
52
+ }
53
+ textarea {
54
+ min-height: 160px;
55
+ resize: vertical;
56
+ }
57
+ .note {
58
+ color: var(--ui-muted);
59
+ font-size: var(--ui-t-small, 11.5px);
60
+ word-break: break-all;
61
+ }
62
+ .files {
63
+ display: flex;
64
+ flex-wrap: wrap;
65
+ gap: var(--spacing-small, 4px);
66
+ align-items: center;
67
+ color: var(--ui-ink);
68
+ }
69
+ .file {
70
+ display: inline-flex;
71
+ align-items: center;
72
+ gap: 2px;
73
+ }
74
+ .file md-icon {
75
+ --md-icon-size: 14px;
76
+ }
77
+ .file button {
78
+ border: 0;
79
+ background: transparent;
80
+ box-shadow: none;
81
+ padding: 0;
82
+ cursor: pointer;
83
+ color: var(--ui-muted);
84
+ }
85
+ .check {
86
+ display: flex;
87
+ align-items: center;
88
+ gap: var(--spacing-small, 4px);
89
+ color: var(--ui-ink);
90
+ }
91
+ /* The field rule stretches its inputs to the row; a checkbox keeps its own size. */
92
+ .field .check input {
93
+ width: auto;
94
+ margin: 0;
95
+ }
96
+ `
97
+ ]; }
98
+ render() {
99
+ const title = this.title.trim();
100
+ return html `
101
+ <div class="body">
102
+ <div class="field">
103
+ <ox-input-select-buttons
104
+ .value=${this.kind}
105
+ .options=${KINDS.map(code => ({ display: kindWord(code), value: code }))}
106
+ @change=${(e) => e.detail && (this.kind = e.detail)}
107
+ ></ox-input-select-buttons>
108
+ <span>${t(`inquiry.text.kind-hint-${this.kind.toLowerCase()}`)}</span>
109
+ </div>
110
+ <label class="field">
111
+ <span
112
+ >${t('inquiry.field.title')} ·
113
+ ${t('inquiry.text.title-limit', { count: this.title.length, max: MAX_TITLE })}</span
114
+ >
115
+ <input .value=${this.title} maxlength=${MAX_TITLE} @input=${(e) => (this.title = e.target.value)} />
116
+ </label>
117
+ <label class="field">
118
+ <span>${t('inquiry.field.body')}</span>
119
+ <textarea
120
+ .value=${this.body}
121
+ placeholder=${t(`inquiry.text.body-placeholder-${this.kind.toLowerCase()}`)}
122
+ @input=${(e) => (this.body = e.target.value)}
123
+ ></textarea>
124
+ </label>
125
+ <div class="files">
126
+ ${this.files.map((file, index) => html `<span class="file"
127
+ ><md-icon>attach_file</md-icon>${file.name}<button
128
+ aria-label=${t('inquiry.button.remove')}
129
+ @click=${() => (this.files = this.files.filter((_, i) => i !== index))}
130
+ >
131
+ <md-icon>close</md-icon>
132
+ </button></span
133
+ >`)}
134
+ <ox-input-file
135
+ quiet
136
+ hide-filelist
137
+ multiple
138
+ icon="attach_file"
139
+ .progress=${this.progress}
140
+ @change=${(e) => this.pick(e)}
141
+ ></ox-input-file>
142
+ </div>
143
+ <div class="field">
144
+ <label class="check"
145
+ ><input
146
+ type="checkbox"
147
+ .checked=${this.privately}
148
+ @change=${(e) => (this.privately = e.target.checked)}
149
+ />${t('inquiry.text.private-label')}</label
150
+ >
151
+ <span>${this.privately ? t('inquiry.text.private-hint') : t('inquiry.text.domain-hint')}</span>
152
+ </div>
153
+ ${this.pageUrl ? html `<div class="note">${t('inquiry.text.page-hint', { page: this.pageUrl })}</div>` : nothing}
154
+ </div>
155
+ <ox-popup-actions>
156
+ <md-text-button slot="cancel" @click=${() => this.dispatchEvent(new CustomEvent('close'))}
157
+ >${t('inquiry.button.cancel')}</md-text-button
158
+ >
159
+ <md-filled-button slot="confirm" ?disabled=${!title || this.sending} @click=${() => this.send()}
160
+ >${t('inquiry.button.submit')}</md-filled-button
161
+ >
162
+ </ox-popup-actions>
163
+ `;
164
+ }
165
+ pick(e) {
166
+ const picked = [...(e.detail ?? [])];
167
+ /* The picker's own reset says `change` again, with nothing - that one is let go. */
168
+ if (!picked.length)
169
+ return;
170
+ e.target.reset?.();
171
+ this.files = [...this.files, ...picked];
172
+ }
173
+ async send() {
174
+ this.sending = true;
175
+ try {
176
+ await inquiryApi.create({
177
+ kind: this.kind,
178
+ title: this.title.trim(),
179
+ body: this.body,
180
+ visibility: this.privately ? 'PRIVATE' : 'DOMAIN',
181
+ pageUrl: this.pageUrl
182
+ }, this.files, fraction => (this.progress = fraction));
183
+ inquiriesChanged();
184
+ notify({ message: t('inquiry.text.sent') });
185
+ /* The writer stays on the screen they were on - the answer comes as a notice and on the menu's badge. */
186
+ this.dispatchEvent(new CustomEvent('close'));
187
+ }
188
+ catch (error) {
189
+ notify({ level: 'error', message: reasonOf(error) });
190
+ }
191
+ finally {
192
+ this.sending = false;
193
+ this.progress = undefined;
194
+ }
195
+ }
196
+ };
197
+ __decorate([
198
+ property(),
199
+ __metadata("design:type", Object)
200
+ ], InquiryWriter.prototype, "kind", void 0);
201
+ __decorate([
202
+ property(),
203
+ __metadata("design:type", Object)
204
+ ], InquiryWriter.prototype, "pageUrl", void 0);
205
+ __decorate([
206
+ state(),
207
+ __metadata("design:type", Object)
208
+ ], InquiryWriter.prototype, "title", void 0);
209
+ __decorate([
210
+ state(),
211
+ __metadata("design:type", Object)
212
+ ], InquiryWriter.prototype, "body", void 0);
213
+ __decorate([
214
+ state(),
215
+ __metadata("design:type", Object)
216
+ ], InquiryWriter.prototype, "privately", void 0);
217
+ __decorate([
218
+ state(),
219
+ __metadata("design:type", Array)
220
+ ], InquiryWriter.prototype, "files", void 0);
221
+ __decorate([
222
+ state(),
223
+ __metadata("design:type", Object)
224
+ ], InquiryWriter.prototype, "sending", void 0);
225
+ __decorate([
226
+ state(),
227
+ __metadata("design:type", Number)
228
+ ], InquiryWriter.prototype, "progress", void 0);
229
+ InquiryWriter = __decorate([
230
+ customElement('inquiry-writer')
231
+ ], InquiryWriter);
232
+ export { InquiryWriter };
233
+ //# sourceMappingURL=inquiry-writer.js.map