@things-factory/inquiry 0.0.0-stage → 10.1.251
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/bootstrap.ts +27 -0
- package/client/components/inquiry-thread.ts +277 -0
- package/client/components/inquiry-todo.ts +36 -0
- package/client/components/inquiry-writer.ts +208 -0
- package/client/index.ts +2 -0
- package/client/inquiry-watch.ts +45 -0
- package/client/pages/inquiry-list.ts +218 -0
- package/client/route.ts +8 -0
- package/client/tsconfig.json +13 -0
- package/client/utils/inquiry-api.ts +103 -0
- package/client/utils/inquiry-badge.ts +46 -0
- package/client/utils/inquiry-words.ts +26 -0
- package/dist-client/bootstrap.d.ts +2 -0
- package/dist-client/bootstrap.js +25 -0
- package/dist-client/bootstrap.js.map +1 -0
- package/dist-client/components/inquiry-thread.d.ts +28 -0
- package/dist-client/components/inquiry-thread.js +303 -0
- package/dist-client/components/inquiry-thread.js.map +1 -0
- package/dist-client/components/inquiry-todo.d.ts +9 -0
- package/dist-client/components/inquiry-todo.js +43 -0
- package/dist-client/components/inquiry-todo.js.map +1 -0
- package/dist-client/components/inquiry-writer.d.ts +23 -0
- package/dist-client/components/inquiry-writer.js +230 -0
- package/dist-client/components/inquiry-writer.js.map +1 -0
- package/dist-client/index.d.ts +2 -0
- package/dist-client/index.js +3 -0
- package/dist-client/index.js.map +1 -0
- package/dist-client/inquiry-watch.d.ts +2 -0
- package/dist-client/inquiry-watch.js +41 -0
- package/dist-client/inquiry-watch.js.map +1 -0
- package/dist-client/pages/inquiry-list.d.ts +56 -0
- package/dist-client/pages/inquiry-list.js +241 -0
- package/dist-client/pages/inquiry-list.js.map +1 -0
- package/dist-client/route.d.ts +1 -0
- package/dist-client/route.js +9 -0
- package/dist-client/route.js.map +1 -0
- package/dist-client/tsconfig.tsbuildinfo +1 -0
- package/dist-client/utils/inquiry-api.d.ts +73 -0
- package/dist-client/utils/inquiry-api.js +34 -0
- package/dist-client/utils/inquiry-api.js.map +1 -0
- package/dist-client/utils/inquiry-badge.d.ts +4 -0
- package/dist-client/utils/inquiry-badge.js +47 -0
- package/dist-client/utils/inquiry-badge.js.map +1 -0
- package/dist-client/utils/inquiry-words.d.ts +13 -0
- package/dist-client/utils/inquiry-words.js +22 -0
- package/dist-client/utils/inquiry-words.js.map +1 -0
- package/dist-server/attachment-reader.d.ts +1 -0
- package/dist-server/attachment-reader.js +21 -0
- package/dist-server/attachment-reader.js.map +1 -0
- package/dist-server/index.d.ts +2 -0
- package/dist-server/index.js +22 -0
- package/dist-server/index.js.map +1 -0
- package/dist-server/inquiry-activity.d.ts +38 -0
- package/dist-server/inquiry-activity.js +38 -0
- package/dist-server/inquiry-activity.js.map +1 -0
- package/dist-server/role-tasks.d.ts +2 -0
- package/dist-server/role-tasks.js +14 -0
- package/dist-server/role-tasks.js.map +1 -0
- package/dist-server/service/index.d.ts +7 -0
- package/dist-server/service/index.js +24 -0
- package/dist-server/service/index.js.map +1 -0
- package/dist-server/service/inquiry/index.d.ts +4 -0
- package/dist-server/service/inquiry/index.js +8 -0
- package/dist-server/service/inquiry/index.js.map +1 -0
- package/dist-server/service/inquiry/inquiry-resolver.d.ts +29 -0
- package/dist-server/service/inquiry/inquiry-resolver.js +407 -0
- package/dist-server/service/inquiry/inquiry-resolver.js.map +1 -0
- package/dist-server/service/inquiry/inquiry-rules.d.ts +88 -0
- package/dist-server/service/inquiry/inquiry-rules.js +157 -0
- package/dist-server/service/inquiry/inquiry-rules.js.map +1 -0
- package/dist-server/service/inquiry/inquiry-service.d.ts +35 -0
- package/dist-server/service/inquiry/inquiry-service.js +143 -0
- package/dist-server/service/inquiry/inquiry-service.js.map +1 -0
- package/dist-server/service/inquiry/inquiry-types.d.ts +18 -0
- package/dist-server/service/inquiry/inquiry-types.js +69 -0
- package/dist-server/service/inquiry/inquiry-types.js.map +1 -0
- package/dist-server/service/inquiry/inquiry.d.ts +48 -0
- package/dist-server/service/inquiry/inquiry.js +232 -0
- package/dist-server/service/inquiry/inquiry.js.map +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -0
- package/package.json +51 -4
- package/server/attachment-reader.ts +18 -0
- package/server/index.ts +18 -0
- package/server/inquiry-activity.ts +35 -0
- package/server/role-tasks.ts +13 -0
- package/server/service/index.ts +19 -0
- package/server/service/inquiry/index.ts +5 -0
- package/server/service/inquiry/inquiry-resolver.ts +321 -0
- package/server/service/inquiry/inquiry-rules.ts +173 -0
- package/server/service/inquiry/inquiry-service.ts +143 -0
- package/server/service/inquiry/inquiry-types.ts +45 -0
- package/server/service/inquiry/inquiry.ts +180 -0
- package/server/tsconfig.json +11 -0
- package/test/inquiry-persistence.test.ts +218 -0
- package/test/inquiry-rules.test.ts +121 -0
- package/test/open-doors.test.ts +44 -0
- package/test/translations.test.ts +53 -0
- package/things-factory.config.js +8 -0
- package/translations/en.json +93 -0
- package/translations/es.json +93 -0
- package/translations/ja.json +93 -0
- package/translations/ko.json +93 -0
- package/translations/ms.json +93 -0
- package/translations/zh.json +93 -0
- package/README.md +0 -3
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import '@material/web/icon/icon.js'
|
|
2
|
+
import '@operato/data-grist/ox-grist.js'
|
|
3
|
+
import '@operato/data-grist/ox-filters-form.js'
|
|
4
|
+
import '@operato/input/ox-select.js'
|
|
5
|
+
import '@operato/popup/ox-popup-list.js'
|
|
6
|
+
import '../components/inquiry-thread.js'
|
|
7
|
+
|
|
8
|
+
import gql from 'graphql-tag'
|
|
9
|
+
import { css, html, nothing } from 'lit'
|
|
10
|
+
import { customElement, query, state } from 'lit/decorators.js'
|
|
11
|
+
|
|
12
|
+
import { DataGrist, FetchOption } from '@operato/data-grist'
|
|
13
|
+
import { client } from '@operato/graphql'
|
|
14
|
+
import { i18next, localize } from '@operato/i18n'
|
|
15
|
+
import { navigate, PageView } from '@operato/shell'
|
|
16
|
+
import { CommonGristStyles, CommonHeaderStyles, ScrollbarStyles, WorkSurfacePageStyles } from '@operato/styles'
|
|
17
|
+
import { isMobileDevice } from '@operato/utils'
|
|
18
|
+
|
|
19
|
+
import { openInquiryWriter } from '../components/inquiry-writer.js'
|
|
20
|
+
import type { InquiryThread } from '../components/inquiry-thread.js'
|
|
21
|
+
import { inquiryApi, LIST_FIELDS } from '../utils/inquiry-api.js'
|
|
22
|
+
import { KIND_ICONS, KINDS, kindWord, STATUSES, statusWord, t, visibilityWord, when } from '../utils/inquiry-words.js'
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 문의 · 제안 — the questions, bug reports and proposals of the domain that the person may read, and with `?id=` one of
|
|
26
|
+
* them as its thread (ADR-0184 decision 8). Handlers work from here too: they open on those waiting on them.
|
|
27
|
+
*/
|
|
28
|
+
@customElement('inquiry-list')
|
|
29
|
+
export class InquiryList extends localize(i18next)(PageView) {
|
|
30
|
+
static styles = [
|
|
31
|
+
WorkSurfacePageStyles,
|
|
32
|
+
CommonGristStyles,
|
|
33
|
+
CommonHeaderStyles,
|
|
34
|
+
ScrollbarStyles,
|
|
35
|
+
css`
|
|
36
|
+
.filters {
|
|
37
|
+
display: flex;
|
|
38
|
+
flex-wrap: wrap;
|
|
39
|
+
align-items: center;
|
|
40
|
+
gap: var(--spacing-medium, 8px);
|
|
41
|
+
}
|
|
42
|
+
ox-select {
|
|
43
|
+
min-width: 140px;
|
|
44
|
+
}
|
|
45
|
+
.thread {
|
|
46
|
+
overflow: auto;
|
|
47
|
+
padding: var(--spacing-large, 12px) var(--spacing-huge, 16px);
|
|
48
|
+
max-width: 880px;
|
|
49
|
+
width: 100%;
|
|
50
|
+
box-sizing: border-box;
|
|
51
|
+
}
|
|
52
|
+
`
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
@state() gristConfig: any
|
|
56
|
+
@state() scope = 'all'
|
|
57
|
+
@state() handler = false
|
|
58
|
+
/* The inquiry shown as its thread - none shows the list. */
|
|
59
|
+
@state() openId = ''
|
|
60
|
+
@query('ox-grist') private grist!: DataGrist
|
|
61
|
+
@query('inquiry-thread') private thread!: InquiryThread
|
|
62
|
+
|
|
63
|
+
private onChanged = () => {
|
|
64
|
+
if (this.openId) return
|
|
65
|
+
this.grist?.fetch()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
get context() {
|
|
69
|
+
return {
|
|
70
|
+
title: t('inquiry.title.list'),
|
|
71
|
+
...(this.openId
|
|
72
|
+
? {}
|
|
73
|
+
: {
|
|
74
|
+
search: {
|
|
75
|
+
handler: (search: string) => {
|
|
76
|
+
this.grist.searchText = search
|
|
77
|
+
},
|
|
78
|
+
value: this.grist?.searchText || ''
|
|
79
|
+
},
|
|
80
|
+
filter: {
|
|
81
|
+
handler: () => {
|
|
82
|
+
this.grist.toggleHeadroom()
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}),
|
|
86
|
+
actions: [
|
|
87
|
+
...(this.openId ? [{ title: t('inquiry.button.back'), icon: 'arrow_back', action: () => navigate('inquiry-list') }] : []),
|
|
88
|
+
{ title: t('inquiry.button.send'), icon: 'add', emphasis: { raised: true }, action: () => openInquiryWriter() }
|
|
89
|
+
]
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
render() {
|
|
94
|
+
if (this.openId) return html`<div class="thread"><inquiry-thread .inquiryId=${this.openId}></inquiry-thread></div>`
|
|
95
|
+
const scopes = [
|
|
96
|
+
{ value: 'all', display: t('inquiry.scope.all') },
|
|
97
|
+
{ value: 'mine', display: t('inquiry.scope.mine') },
|
|
98
|
+
...(this.handler ? [{ value: 'handlers-turn', display: t('inquiry.scope.handlers-turn') }] : [])
|
|
99
|
+
]
|
|
100
|
+
return html`<ox-grist class="page-fill" search-mode="term" .mode=${isMobileDevice() ? 'CARD' : 'GRID'} .config=${this.gristConfig} .fetchHandler=${this.fetchHandler.bind(this)}>
|
|
101
|
+
<div slot="headroom" class="header">
|
|
102
|
+
<div class="filters">
|
|
103
|
+
<ox-select
|
|
104
|
+
.value=${this.scope}
|
|
105
|
+
aria-label=${t('inquiry.text.scope')}
|
|
106
|
+
@change=${(e: CustomEvent) => {
|
|
107
|
+
const next = String(e.detail ?? 'all')
|
|
108
|
+
if (next === this.scope) return
|
|
109
|
+
this.scope = next
|
|
110
|
+
this.grist.fetch()
|
|
111
|
+
}}
|
|
112
|
+
>
|
|
113
|
+
<span slot="label">${scopes.find(one => one.value === this.scope)?.display}</span>
|
|
114
|
+
<ox-popup-list align-left nowrap>${scopes.map(one => html`<div option value=${one.value}>${one.display}</div>`)}</ox-popup-list>
|
|
115
|
+
</ox-select>
|
|
116
|
+
<ox-filters-form without-search autofocus></ox-filters-form>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
</ox-grist>`
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
connectedCallback() {
|
|
123
|
+
super.connectedCallback()
|
|
124
|
+
window.addEventListener('inquiries-changed', this.onChanged)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
disconnectedCallback() {
|
|
128
|
+
window.removeEventListener('inquiries-changed', this.onChanged)
|
|
129
|
+
super.disconnectedCallback()
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async pageUpdated(changes: any, lifecycle: any) {
|
|
133
|
+
if (!this.active || !(changes.active || changes.params)) return
|
|
134
|
+
const id = String(lifecycle?.params?.id ?? '')
|
|
135
|
+
if (id !== this.openId) {
|
|
136
|
+
this.openId = id
|
|
137
|
+
this.updateContext()
|
|
138
|
+
} else if (id) this.thread?.load(true)
|
|
139
|
+
else if (this.gristConfig) this.grist?.fetch()
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async pageInitialized() {
|
|
143
|
+
try {
|
|
144
|
+
this.handler = (await inquiryApi.summary()).handler
|
|
145
|
+
/* A handler opens on what waits on them. */
|
|
146
|
+
if (this.handler) this.scope = 'handlers-turn'
|
|
147
|
+
} catch {
|
|
148
|
+
this.handler = false
|
|
149
|
+
}
|
|
150
|
+
const open = (record: any) => record?.id && navigate(`inquiry-list?id=${record.id}`)
|
|
151
|
+
this.gristConfig = {
|
|
152
|
+
rows: { appendable: false, selectable: false, handlers: { click: (_c: any, _d: any, _col: any, record: any) => open(record) } },
|
|
153
|
+
pagination: { limit: 50, pages: [50, 100] },
|
|
154
|
+
emptyNote: { title: t('inquiry.text.empty'), description: t('inquiry.text.empty-description') },
|
|
155
|
+
list: { fields: ['title', 'status'], details: ['kind', 'createdAt'] },
|
|
156
|
+
columns: [
|
|
157
|
+
{
|
|
158
|
+
type: 'string',
|
|
159
|
+
name: 'kind',
|
|
160
|
+
header: t('inquiry.field.kind'),
|
|
161
|
+
width: 110,
|
|
162
|
+
record: {
|
|
163
|
+
editable: false,
|
|
164
|
+
renderer: (value: string) => html`<span style="display:inline-flex;align-items:center;gap:2px"><md-icon style="--md-icon-size:16px">${KIND_ICONS[value] ?? 'help'}</md-icon>${kindWord(value)}</span>`
|
|
165
|
+
},
|
|
166
|
+
filter: { type: 'select', operator: 'eq', options: [{ display: '', value: '' }, ...KINDS.map(code => ({ display: kindWord(code), value: code }))] }
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
type: 'string',
|
|
170
|
+
name: 'title',
|
|
171
|
+
header: t('inquiry.field.title'),
|
|
172
|
+
width: 320,
|
|
173
|
+
record: {
|
|
174
|
+
editable: false,
|
|
175
|
+
renderer: (value: string, _c: any, record: any) =>
|
|
176
|
+
html`<span style=${record?.isNew ? 'font-weight:600' : ''}
|
|
177
|
+
>${record?.visibility === 'PRIVATE' ? html`<md-icon style="--md-icon-size:14px" title=${visibilityWord('PRIVATE')}>lock</md-icon> ` : nothing}${value}${record?.replyCount
|
|
178
|
+
? ` (${record.replyCount})`
|
|
179
|
+
: ''}</span
|
|
180
|
+
>`
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
type: 'string',
|
|
185
|
+
name: 'status',
|
|
186
|
+
header: t('inquiry.field.status'),
|
|
187
|
+
width: 100,
|
|
188
|
+
record: {
|
|
189
|
+
editable: false,
|
|
190
|
+
renderer: (value: string, _c: any, record: any) =>
|
|
191
|
+
html`<span style=${record?.isNew ? 'color:var(--md-sys-color-primary);font-weight:600' : ''}>${statusWord(value)}${record?.isNew ? ` · ${t('inquiry.text.new')}` : ''}</span>`
|
|
192
|
+
},
|
|
193
|
+
filter: { type: 'select', operator: 'eq', options: [{ display: '', value: '' }, ...STATUSES.map(code => ({ display: statusWord(code), value: code }))] }
|
|
194
|
+
},
|
|
195
|
+
{ type: 'string', name: 'creator', header: t('inquiry.field.writer'), width: 110, record: { editable: false, renderer: (value: any) => value?.name ?? '' } },
|
|
196
|
+
{ type: 'string', name: 'assignee', header: t('inquiry.field.assignee'), width: 110, record: { editable: false, renderer: (value: any) => value?.name ?? '' } },
|
|
197
|
+
{ type: 'string', name: 'createdAt', header: t('inquiry.field.created-at'), width: 130, record: { editable: false, renderer: (value: string) => when(value) } }
|
|
198
|
+
]
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async fetchHandler({ page = 1, limit = 50, filters = [], search }: FetchOption) {
|
|
203
|
+
const response = await client.query({
|
|
204
|
+
query: gql`
|
|
205
|
+
query ($filters: [Filter!], $pagination: Pagination, $search: String, $scope: String) {
|
|
206
|
+
responses: inquiries(filters: $filters, pagination: $pagination, search: $search, scope: $scope) {
|
|
207
|
+
items { ${LIST_FIELDS} }
|
|
208
|
+
total
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
`,
|
|
212
|
+
variables: { filters, pagination: { page, limit }, search: search?.trim() || undefined, scope: this.scope },
|
|
213
|
+
fetchPolicy: 'network-only'
|
|
214
|
+
})
|
|
215
|
+
if (response.errors?.length) throw response
|
|
216
|
+
return { total: response.data.responses.total, records: response.data.responses.items }
|
|
217
|
+
}
|
|
218
|
+
}
|
package/client/route.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig-base.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"experimentalDecorators": true,
|
|
5
|
+
"skipLibCheck": true,
|
|
6
|
+
"strict": true,
|
|
7
|
+
"declaration": true,
|
|
8
|
+
"module": "esnext",
|
|
9
|
+
"outDir": "../dist-client",
|
|
10
|
+
"baseUrl": "./"
|
|
11
|
+
},
|
|
12
|
+
"include": ["./**/*"]
|
|
13
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The inquiry doors as the screens call them (ADR-0184). Every call throws the server's words when it is refused.
|
|
3
|
+
*/
|
|
4
|
+
import gql from 'graphql-tag'
|
|
5
|
+
|
|
6
|
+
import { client, uploadContext } from '@operato/graphql'
|
|
7
|
+
|
|
8
|
+
export type FileRow = { id: string; name: string; fullpath: string; size?: number; mimetype?: string }
|
|
9
|
+
export type Person = { id: string; name?: string | null } | null
|
|
10
|
+
|
|
11
|
+
export interface InquiryReplyItem {
|
|
12
|
+
id: string
|
|
13
|
+
body?: string | null
|
|
14
|
+
byHandler: boolean
|
|
15
|
+
answer: boolean
|
|
16
|
+
status?: string | null
|
|
17
|
+
createdAt: string
|
|
18
|
+
creator?: Person
|
|
19
|
+
attachments?: FileRow[]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface InquiryItem {
|
|
23
|
+
id: string
|
|
24
|
+
kind: 'QUESTION' | 'BUG' | 'PROPOSAL' | string
|
|
25
|
+
title: string
|
|
26
|
+
body?: string | null
|
|
27
|
+
status: string
|
|
28
|
+
visibility: 'DOMAIN' | 'PRIVATE' | string
|
|
29
|
+
pageUrl?: string | null
|
|
30
|
+
appName?: string | null
|
|
31
|
+
appVersion?: string | null
|
|
32
|
+
replyCount: number
|
|
33
|
+
createdAt: string
|
|
34
|
+
answeredAt?: string | null
|
|
35
|
+
closedAt?: string | null
|
|
36
|
+
mine?: boolean
|
|
37
|
+
isNew?: boolean
|
|
38
|
+
canReply?: boolean
|
|
39
|
+
asHandler?: boolean
|
|
40
|
+
moves?: string[]
|
|
41
|
+
creator?: Person
|
|
42
|
+
assignee?: Person
|
|
43
|
+
attachments?: FileRow[]
|
|
44
|
+
replies?: InquiryReplyItem[]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface InquiryInput {
|
|
48
|
+
kind: string
|
|
49
|
+
title: string
|
|
50
|
+
body?: string
|
|
51
|
+
visibility?: string
|
|
52
|
+
pageUrl?: string
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export type InquirySummary = { newAnswers: number; unhandled: number; handler: boolean; badge: number }
|
|
56
|
+
|
|
57
|
+
/** The fields a list row needs. */
|
|
58
|
+
export const LIST_FIELDS = `id kind title status visibility replyCount createdAt mine isNew creator { id name } assignee { id name }`
|
|
59
|
+
|
|
60
|
+
const FILE = `attachments { id name fullpath size mimetype }`
|
|
61
|
+
|
|
62
|
+
/** Everything one inquiry shows. */
|
|
63
|
+
export const FULL_FIELDS = `${LIST_FIELDS} body pageUrl appName appVersion answeredAt closedAt canReply asHandler moves ${FILE}
|
|
64
|
+
replies { id body byHandler answer status createdAt creator { id name } ${FILE} }`
|
|
65
|
+
|
|
66
|
+
function answer<T>(response: any, field: string): T {
|
|
67
|
+
if (response.errors?.length) throw new Error(response.errors.map((one: any) => one.message).join(' · '))
|
|
68
|
+
return response.data?.[field] as T
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function ask<T>(query: string, variables: Record<string, unknown>, field: string): Promise<T> {
|
|
72
|
+
return answer<T>(await client.query({ query: gql(query), variables, fetchPolicy: 'network-only' }), field)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function change<T>(mutation: string, variables: Record<string, unknown>, field: string, uploading?: (fraction: number) => void): Promise<T> {
|
|
76
|
+
return answer<T>(await client.mutate({ mutation: gql(mutation), variables, ...(uploading ? { context: uploadContext(uploading) } : {}) }), field)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Tells the badge and an open list that an inquiry changed. */
|
|
80
|
+
export const inquiriesChanged = () => window.dispatchEvent(new CustomEvent('inquiries-changed'))
|
|
81
|
+
|
|
82
|
+
export const inquiryApi = {
|
|
83
|
+
one: (id: string) => ask<InquiryItem | null>(`query ($id: String!) { inquiry(id: $id) { ${FULL_FIELDS} } }`, { id }, 'inquiry'),
|
|
84
|
+
summary: () => ask<InquirySummary>(`query { inquirySummary { newAnswers unhandled handler badge } }`, {}, 'inquirySummary'),
|
|
85
|
+
create: (input: InquiryInput, attachments: File[], uploading?: (fraction: number) => void) =>
|
|
86
|
+
change<InquiryItem>(
|
|
87
|
+
`mutation ($input: InquiryInput!, $attachments: [Upload!]) { createInquiry(input: $input, attachments: $attachments) { id } }`,
|
|
88
|
+
{ input, attachments: attachments.length ? attachments : null },
|
|
89
|
+
'createInquiry',
|
|
90
|
+
attachments.length ? uploading : undefined
|
|
91
|
+
),
|
|
92
|
+
reply: (id: string, body: string, asAnswer: boolean, attachments: File[], uploading?: (fraction: number) => void) =>
|
|
93
|
+
change<InquiryItem>(
|
|
94
|
+
`mutation ($id: String!, $body: String, $answer: Boolean, $attachments: [Upload!]) { replyInquiry(id: $id, body: $body, answer: $answer, attachments: $attachments) { id } }`,
|
|
95
|
+
{ id, body, answer: asAnswer, attachments: attachments.length ? attachments : null },
|
|
96
|
+
'replyInquiry',
|
|
97
|
+
attachments.length ? uploading : undefined
|
|
98
|
+
),
|
|
99
|
+
move: (id: string, status: string, reason?: string) =>
|
|
100
|
+
change<InquiryItem>(`mutation ($id: String!, $status: String!, $reason: String) { moveInquiry(id: $id, status: $status, reason: $reason) { id } }`, { id, status, reason }, 'moveInquiry'),
|
|
101
|
+
take: (id: string) => change<InquiryItem>(`mutation ($id: String!) { takeInquiry(id: $id) { id } }`, { id }, 'takeInquiry'),
|
|
102
|
+
read: (id: string) => change<InquiryItem>(`mutation ($id: String!) { readInquiry(id: $id) { id } }`, { id }, 'readInquiry')
|
|
103
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The count the menu shows on 문의 · 제안 (ADR-0184 decision 8) - new words on one's own inquiries, and for a handler
|
|
3
|
+
* those waiting on the handlers. An application puts `generateInquiryBadge` on its menu entry; the count is asked again
|
|
4
|
+
* whenever an inquiry notice arrives or an inquiry changes on a screen.
|
|
5
|
+
*/
|
|
6
|
+
import { inquiryApi } from './inquiry-api.js'
|
|
7
|
+
|
|
8
|
+
let count: number | undefined
|
|
9
|
+
/* One more each time the count is asked again - a badge that was busy drawing the last one still sees the next. */
|
|
10
|
+
let version = 0
|
|
11
|
+
const waiting = new Set<() => void>()
|
|
12
|
+
|
|
13
|
+
/** Asks the server again and tells every badge. A count that could not be asked is not zero: it draws nothing. */
|
|
14
|
+
export async function refreshInquiryBadge(): Promise<void> {
|
|
15
|
+
try {
|
|
16
|
+
count = (await inquiryApi.summary()).badge
|
|
17
|
+
} catch {
|
|
18
|
+
count = undefined
|
|
19
|
+
}
|
|
20
|
+
version++
|
|
21
|
+
for (const wake of [...waiting]) wake()
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let generation = 0
|
|
25
|
+
|
|
26
|
+
/** One badge's counts. The menu asks for a new one when it draws again; the one before it then ends, as worklist's do. */
|
|
27
|
+
export async function* generateInquiryBadge() {
|
|
28
|
+
const mine = ++generation
|
|
29
|
+
for (const wake of [...waiting]) wake()
|
|
30
|
+
let seen = version
|
|
31
|
+
yield count
|
|
32
|
+
while (true) {
|
|
33
|
+
if (seen === version) {
|
|
34
|
+
await new Promise<void>(resolve => {
|
|
35
|
+
const wake = () => {
|
|
36
|
+
waiting.delete(wake)
|
|
37
|
+
resolve()
|
|
38
|
+
}
|
|
39
|
+
waiting.add(wake)
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
if (mine !== generation) return
|
|
43
|
+
seen = version
|
|
44
|
+
yield count
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The words and marks of inquiries - one place for the list, the thread, the writing window and the to-do.
|
|
3
|
+
*/
|
|
4
|
+
import { i18next } from '@operato/i18n'
|
|
5
|
+
|
|
6
|
+
export const t = (key: string, options: Record<string, unknown> = {}) => String(i18next.t(key, options))
|
|
7
|
+
|
|
8
|
+
export const KINDS = ['QUESTION', 'BUG', 'PROPOSAL']
|
|
9
|
+
export const VISIBILITIES = ['DOMAIN', 'PRIVATE']
|
|
10
|
+
export const STATUSES = ['RECEIVED', 'IN_PROGRESS', 'ANSWERED', 'PLANNED', 'DECLINED', 'CLOSED']
|
|
11
|
+
|
|
12
|
+
export const kindWord = (code?: string | null) => (code ? t(`inquiry.kind.${code}`, { defaultValue: code }) : '')
|
|
13
|
+
export const statusWord = (code?: string | null) => (code ? t(`inquiry.status.${code}`, { defaultValue: code }) : '')
|
|
14
|
+
export const visibilityWord = (code?: string | null) => (code ? t(`inquiry.visibility.${code}`, { defaultValue: code }) : '')
|
|
15
|
+
|
|
16
|
+
/** The icon of each kind. */
|
|
17
|
+
export const KIND_ICONS: Record<string, string> = { QUESTION: 'help', BUG: 'bug_report', PROPOSAL: 'lightbulb' }
|
|
18
|
+
|
|
19
|
+
/** A day and time as the reader's locale writes it. */
|
|
20
|
+
export function when(value?: string | Date | null): string {
|
|
21
|
+
if (!value) return ''
|
|
22
|
+
return new Date(value).toLocaleString(i18next.language, { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The words of a refusal the server gave, or the fallback. */
|
|
26
|
+
export const reasonOf = (error: unknown) => (error as Error)?.message || t('inquiry.text.failed')
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import './components/inquiry-todo.js';
|
|
2
|
+
import { store } from '@operato/shell';
|
|
3
|
+
import { auth } from '@things-factory/auth-base/dist-client/auth.js';
|
|
4
|
+
import { ADD_USER_MENU } from '@things-factory/apptool-base/client';
|
|
5
|
+
import { openInquiryWriter } from './components/inquiry-writer.js';
|
|
6
|
+
import { startInquiryWatch, stopInquiryWatch } from './inquiry-watch.js';
|
|
7
|
+
/*
|
|
8
|
+
* The way to write one is in the menu the header's avatar opens (ADR-0184 decision 9) - on every screen of every
|
|
9
|
+
* application, carrying the screen it was opened on. The badge follows the person.
|
|
10
|
+
*/
|
|
11
|
+
export default function bootstrap() {
|
|
12
|
+
store.dispatch({
|
|
13
|
+
type: ADD_USER_MENU,
|
|
14
|
+
item: {
|
|
15
|
+
name: 'inquiry',
|
|
16
|
+
icon: 'contact_support',
|
|
17
|
+
label: 'inquiry.button.send',
|
|
18
|
+
order: 30,
|
|
19
|
+
action: () => openInquiryWriter()
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
auth.on('profile', () => startInquiryWatch());
|
|
23
|
+
auth.on('presignout', () => stopInquiryWatch());
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=bootstrap.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bootstrap.js","sourceRoot":"","sources":["../client/bootstrap.ts"],"names":[],"mappings":"AAAA,OAAO,8BAA8B,CAAA;AAErC,OAAO,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAA;AACtC,OAAO,EAAE,IAAI,EAAE,MAAM,+CAA+C,CAAA;AACpE,OAAO,EAAE,aAAa,EAAE,MAAM,qCAAqC,CAAA;AAEnE,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAA;AAClE,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AAExE;;;GAGG;AACH,MAAM,CAAC,OAAO,UAAU,SAAS;IAC/B,KAAK,CAAC,QAAQ,CAAC;QACb,IAAI,EAAE,aAAa;QACnB,IAAI,EAAE;YACJ,IAAI,EAAE,SAAS;YACf,IAAI,EAAE,iBAAiB;YACvB,KAAK,EAAE,qBAAqB;YAC5B,KAAK,EAAE,EAAE;YACT,MAAM,EAAE,GAAG,EAAE,CAAC,iBAAiB,EAAE;SAClC;KACF,CAAC,CAAA;IACF,IAAI,CAAC,EAAE,CAAC,SAAS,EAAE,GAAG,EAAE,CAAC,iBAAiB,EAAE,CAAC,CAAA;IAC7C,IAAI,CAAC,EAAE,CAAC,YAAY,EAAE,GAAG,EAAE,CAAC,gBAAgB,EAAE,CAAC,CAAA;AACjD,CAAC","sourcesContent":["import './components/inquiry-todo.js'\n\nimport { store } from '@operato/shell'\nimport { auth } from '@things-factory/auth-base/dist-client/auth.js'\nimport { ADD_USER_MENU } from '@things-factory/apptool-base/client'\n\nimport { openInquiryWriter } from './components/inquiry-writer.js'\nimport { startInquiryWatch, stopInquiryWatch } from './inquiry-watch.js'\n\n/*\n * The way to write one is in the menu the header's avatar opens (ADR-0184 decision 9) - on every screen of every\n * application, carrying the screen it was opened on. The badge follows the person.\n */\nexport default function bootstrap() {\n store.dispatch({\n type: ADD_USER_MENU,\n item: {\n name: 'inquiry',\n icon: 'contact_support',\n label: 'inquiry.button.send',\n order: 30,\n action: () => openInquiryWriter()\n }\n })\n auth.on('profile', () => startInquiryWatch())\n auth.on('presignout', () => stopInquiryWatch())\n}\n"]}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import '@material/web/icon/icon.js';
|
|
2
|
+
import '@material/web/button/filled-button.js';
|
|
3
|
+
import '@material/web/button/outlined-button.js';
|
|
4
|
+
import '@material/web/button/text-button.js';
|
|
5
|
+
import '@operato/input/ox-input-file.js';
|
|
6
|
+
import { LitElement, nothing } from 'lit';
|
|
7
|
+
import { type InquiryItem } from '../utils/inquiry-api.js';
|
|
8
|
+
declare const InquiryThread_base: (new (...args: any[]) => LitElement) & typeof LitElement;
|
|
9
|
+
export declare class InquiryThread extends InquiryThread_base {
|
|
10
|
+
static styles: import("lit").CSSResult[];
|
|
11
|
+
inquiryId: string;
|
|
12
|
+
one: InquiryItem | null;
|
|
13
|
+
missing: boolean;
|
|
14
|
+
text: string;
|
|
15
|
+
files: File[];
|
|
16
|
+
busy: boolean;
|
|
17
|
+
progress?: number;
|
|
18
|
+
updated(changes: Map<string, unknown>): void;
|
|
19
|
+
load(opened?: boolean): Promise<void>;
|
|
20
|
+
render(): import("lit-html").TemplateResult<1> | typeof nothing;
|
|
21
|
+
private renderFiles;
|
|
22
|
+
private renderBox;
|
|
23
|
+
private pick;
|
|
24
|
+
private act;
|
|
25
|
+
private reply;
|
|
26
|
+
private move;
|
|
27
|
+
}
|
|
28
|
+
export {};
|