@things-factory/announcement 0.0.0-stage → 10.1.222
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/announcement-watch.ts +109 -0
- package/client/bootstrap.ts +9 -0
- package/client/components/announcement-popup.ts +92 -0
- package/client/components/announcement-receipts.ts +196 -0
- package/client/components/announcement-sheet.ts +214 -0
- package/client/components/audience-picker.ts +221 -0
- package/client/index.ts +1 -0
- package/client/pages/announcement-editor.ts +505 -0
- package/client/pages/announcement-list.ts +200 -0
- package/client/pages/announcement-manage-list.ts +113 -0
- package/client/pages/announcement-view.ts +172 -0
- package/client/route.ts +17 -0
- package/client/tsconfig.json +13 -0
- package/client/utils/announcement-api.ts +142 -0
- package/client/utils/announcement-badge.ts +46 -0
- package/client/utils/announcement-words.ts +26 -0
- package/dist-client/announcement-watch.d.ts +6 -0
- package/dist-client/announcement-watch.js +105 -0
- package/dist-client/announcement-watch.js.map +1 -0
- package/dist-client/bootstrap.d.ts +1 -0
- package/dist-client/bootstrap.js +8 -0
- package/dist-client/bootstrap.js.map +1 -0
- package/dist-client/components/announcement-popup.d.ts +20 -0
- package/dist-client/components/announcement-popup.js +98 -0
- package/dist-client/components/announcement-popup.js.map +1 -0
- package/dist-client/components/announcement-receipts.d.ts +30 -0
- package/dist-client/components/announcement-receipts.js +220 -0
- package/dist-client/components/announcement-receipts.js.map +1 -0
- package/dist-client/components/announcement-sheet.d.ts +17 -0
- package/dist-client/components/announcement-sheet.js +224 -0
- package/dist-client/components/announcement-sheet.js.map +1 -0
- package/dist-client/components/audience-picker.d.ts +32 -0
- package/dist-client/components/audience-picker.js +223 -0
- package/dist-client/components/audience-picker.js.map +1 -0
- package/dist-client/index.d.ts +1 -0
- package/dist-client/index.js +2 -0
- package/dist-client/index.js.map +1 -0
- package/dist-client/pages/announcement-editor.d.ts +59 -0
- package/dist-client/pages/announcement-editor.js +523 -0
- package/dist-client/pages/announcement-editor.js.map +1 -0
- package/dist-client/pages/announcement-list.d.ts +54 -0
- package/dist-client/pages/announcement-list.js +209 -0
- package/dist-client/pages/announcement-list.js.map +1 -0
- package/dist-client/pages/announcement-manage-list.d.ts +46 -0
- package/dist-client/pages/announcement-manage-list.js +115 -0
- package/dist-client/pages/announcement-manage-list.js.map +1 -0
- package/dist-client/pages/announcement-view.d.ts +30 -0
- package/dist-client/pages/announcement-view.js +182 -0
- package/dist-client/pages/announcement-view.js.map +1 -0
- package/dist-client/route.d.ts +1 -0
- package/dist-client/route.js +18 -0
- package/dist-client/route.js.map +1 -0
- package/dist-client/tsconfig.tsbuildinfo +1 -0
- package/dist-client/utils/announcement-api.d.ts +149 -0
- package/dist-client/utils/announcement-api.js +52 -0
- package/dist-client/utils/announcement-api.js.map +1 -0
- package/dist-client/utils/announcement-badge.d.ts +4 -0
- package/dist-client/utils/announcement-badge.js +47 -0
- package/dist-client/utils/announcement-badge.js.map +1 -0
- package/dist-client/utils/announcement-words.d.ts +11 -0
- package/dist-client/utils/announcement-words.js +23 -0
- package/dist-client/utils/announcement-words.js.map +1 -0
- package/dist-server/announcement-read.d.ts +6 -0
- package/dist-server/announcement-read.js +45 -0
- package/dist-server/announcement-read.js.map +1 -0
- package/dist-server/attachment-reader.d.ts +1 -0
- package/dist-server/attachment-reader.js +13 -0
- package/dist-server/attachment-reader.js.map +1 -0
- package/dist-server/index.d.ts +2 -0
- package/dist-server/index.js +20 -0
- package/dist-server/index.js.map +1 -0
- package/dist-server/role-tasks.d.ts +2 -0
- package/dist-server/role-tasks.js +13 -0
- package/dist-server/role-tasks.js.map +1 -0
- package/dist-server/service/announcement/announcement-resolver.d.ts +38 -0
- package/dist-server/service/announcement/announcement-resolver.js +664 -0
- package/dist-server/service/announcement/announcement-resolver.js.map +1 -0
- package/dist-server/service/announcement/announcement-rules.d.ts +150 -0
- package/dist-server/service/announcement/announcement-rules.js +227 -0
- package/dist-server/service/announcement/announcement-rules.js.map +1 -0
- package/dist-server/service/announcement/announcement-service.d.ts +54 -0
- package/dist-server/service/announcement/announcement-service.js +248 -0
- package/dist-server/service/announcement/announcement-service.js.map +1 -0
- package/dist-server/service/announcement/announcement-types.d.ts +54 -0
- package/dist-server/service/announcement/announcement-types.js +203 -0
- package/dist-server/service/announcement/announcement-types.js.map +1 -0
- package/dist-server/service/announcement/announcement.d.ts +124 -0
- package/dist-server/service/announcement/announcement.js +462 -0
- package/dist-server/service/announcement/announcement.js.map +1 -0
- package/dist-server/service/announcement/index.d.ts +4 -0
- package/dist-server/service/announcement/index.js +8 -0
- package/dist-server/service/announcement/index.js.map +1 -0
- package/dist-server/service/index.d.ts +7 -0
- package/dist-server/service/index.js +23 -0
- package/dist-server/service/index.js.map +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -0
- package/package.json +51 -4
- package/server/announcement-read.ts +44 -0
- package/server/attachment-reader.ts +11 -0
- package/server/index.ts +16 -0
- package/server/role-tasks.ts +12 -0
- package/server/service/announcement/announcement-resolver.ts +506 -0
- package/server/service/announcement/announcement-rules.ts +239 -0
- package/server/service/announcement/announcement-service.ts +240 -0
- package/server/service/announcement/announcement-types.ts +138 -0
- package/server/service/announcement/announcement.ts +367 -0
- package/server/service/announcement/index.ts +5 -0
- package/server/service/index.ts +19 -0
- package/server/tsconfig.json +11 -0
- package/test/announcement-persistence.test.ts +271 -0
- package/test/announcement-read.test.ts +49 -0
- package/test/announcement-rules.test.ts +217 -0
- package/test/open-doors.test.ts +45 -0
- package/test/translations.test.ts +54 -0
- package/things-factory.config.js +13 -0
- package/translations/en.json +169 -0
- package/translations/es.json +169 -0
- package/translations/ja.json +169 -0
- package/translations/ko.json +169 -0
- package/translations/ms.json +169 -0
- package/translations/zh.json +169 -0
- package/README.md +0 -3
|
@@ -0,0 +1,200 @@
|
|
|
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
|
+
|
|
7
|
+
import gql from 'graphql-tag'
|
|
8
|
+
import { css, html } from 'lit'
|
|
9
|
+
import { customElement, query, state } from 'lit/decorators.js'
|
|
10
|
+
|
|
11
|
+
import { DataGrist, FetchOption } from '@operato/data-grist'
|
|
12
|
+
import { client } from '@operato/graphql'
|
|
13
|
+
import { i18next, localize } from '@operato/i18n'
|
|
14
|
+
import { navigate, PageView } from '@operato/shell'
|
|
15
|
+
import { CommonGristStyles, CommonHeaderStyles, ScrollbarStyles, WorkSurfacePageStyles } from '@operato/styles'
|
|
16
|
+
import { isMobileDevice } from '@operato/utils'
|
|
17
|
+
import { hasPrivilege } from '@things-factory/auth-base/dist-client'
|
|
18
|
+
|
|
19
|
+
import { LIST_FIELDS } from '../utils/announcement-api.js'
|
|
20
|
+
import { CATEGORIES, categoryWord, importanceWord, readerStateWord, t, when } from '../utils/announcement-words.js'
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 공지 — the announcements addressed to the person: the current ones, pinned, urgent and important first, or the past
|
|
24
|
+
* ones (ADR-0163 decision 5). Those not read yet are bold; a row opens the announcement.
|
|
25
|
+
*/
|
|
26
|
+
@customElement('announcement-list')
|
|
27
|
+
export class AnnouncementList extends localize(i18next)(PageView) {
|
|
28
|
+
static styles = [
|
|
29
|
+
WorkSurfacePageStyles,
|
|
30
|
+
CommonGristStyles,
|
|
31
|
+
CommonHeaderStyles,
|
|
32
|
+
ScrollbarStyles,
|
|
33
|
+
css`
|
|
34
|
+
.filters {
|
|
35
|
+
display: flex;
|
|
36
|
+
flex-wrap: wrap;
|
|
37
|
+
align-items: center;
|
|
38
|
+
gap: var(--spacing-medium, 8px);
|
|
39
|
+
}
|
|
40
|
+
ox-select {
|
|
41
|
+
min-width: 140px;
|
|
42
|
+
}
|
|
43
|
+
`
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
@state() gristConfig: any
|
|
47
|
+
@state() period: 'current' | 'past' = 'current'
|
|
48
|
+
/* A writer manages and writes from here - the menu offers one entry, and a reader sees no group of one. */
|
|
49
|
+
@state() writer = false
|
|
50
|
+
@query('ox-grist') private grist!: DataGrist
|
|
51
|
+
|
|
52
|
+
private onChanged = () => this.grist?.fetch()
|
|
53
|
+
|
|
54
|
+
get context() {
|
|
55
|
+
return {
|
|
56
|
+
title: t('announcement.title.list'),
|
|
57
|
+
search: {
|
|
58
|
+
handler: (search: string) => {
|
|
59
|
+
this.grist.searchText = search
|
|
60
|
+
},
|
|
61
|
+
value: this.grist?.searchText || ''
|
|
62
|
+
},
|
|
63
|
+
filter: {
|
|
64
|
+
handler: () => {
|
|
65
|
+
this.grist.toggleHeadroom()
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
actions: this.writer
|
|
69
|
+
? [
|
|
70
|
+
{ title: t('announcement.title.manage'), icon: 'edit_note', action: () => navigate('announcement-manage-list') },
|
|
71
|
+
{ title: t('announcement.button.new'), icon: 'add', emphasis: { raised: true }, action: () => navigate('announcement-editor') }
|
|
72
|
+
]
|
|
73
|
+
: []
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
render() {
|
|
78
|
+
const periods = [
|
|
79
|
+
{ value: 'current', display: t('announcement.text.period-current') },
|
|
80
|
+
{ value: 'past', display: t('announcement.text.period-past') }
|
|
81
|
+
]
|
|
82
|
+
return html`<ox-grist class="page-fill" search-mode="term" .mode=${isMobileDevice() ? 'CARD' : 'GRID'} .config=${this.gristConfig} .fetchHandler=${this.fetchHandler.bind(this)}>
|
|
83
|
+
<div slot="headroom" class="header">
|
|
84
|
+
<div class="filters">
|
|
85
|
+
<ox-select
|
|
86
|
+
.value=${this.period}
|
|
87
|
+
aria-label=${t('announcement.text.period')}
|
|
88
|
+
@change=${(e: CustomEvent) => {
|
|
89
|
+
const next = e.detail === 'past' ? 'past' : 'current'
|
|
90
|
+
if (next === this.period) return
|
|
91
|
+
this.period = next
|
|
92
|
+
this.grist.fetch()
|
|
93
|
+
}}
|
|
94
|
+
>
|
|
95
|
+
<span slot="label">${periods.find(one => one.value === this.period)?.display}</span>
|
|
96
|
+
<ox-popup-list align-left nowrap with-search>${periods.map(one => html`<div option value=${one.value}>${one.display}</div>`)}</ox-popup-list>
|
|
97
|
+
</ox-select>
|
|
98
|
+
<ox-filters-form without-search autofocus></ox-filters-form>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
</ox-grist>`
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
connectedCallback() {
|
|
105
|
+
super.connectedCallback()
|
|
106
|
+
window.addEventListener('announcements-changed', this.onChanged)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
disconnectedCallback() {
|
|
110
|
+
window.removeEventListener('announcements-changed', this.onChanged)
|
|
111
|
+
super.disconnectedCallback()
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async pageUpdated(changes: any) {
|
|
115
|
+
if (this.active && changes.active && this.gristConfig) this.grist?.fetch()
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async pageInitialized() {
|
|
119
|
+
hasPrivilege({ category: 'announcement', privilege: 'mutation', domainOwnerGranted: true }).then(writer => {
|
|
120
|
+
this.writer = !!writer
|
|
121
|
+
this.updateContext()
|
|
122
|
+
})
|
|
123
|
+
const open = (record: any) => record?.id && navigate(`announcement-view?id=${record.id}`)
|
|
124
|
+
const owed = (record: any) => record?.readerState === 'unread' || record?.readerState === 'to-acknowledge'
|
|
125
|
+
this.gristConfig = {
|
|
126
|
+
rows: { appendable: false, selectable: false, handlers: { click: (_c: any, _d: any, _col: any, record: any) => open(record) } },
|
|
127
|
+
pagination: { limit: 50, pages: [50, 100] },
|
|
128
|
+
emptyNote: { title: t('announcement.text.empty'), description: t('announcement.text.empty-description') },
|
|
129
|
+
list: { fields: ['title', 'readerState'], details: ['category', 'publishedAt'] },
|
|
130
|
+
columns: [
|
|
131
|
+
{
|
|
132
|
+
type: 'string',
|
|
133
|
+
name: 'pinned',
|
|
134
|
+
header: '',
|
|
135
|
+
width: 34,
|
|
136
|
+
record: {
|
|
137
|
+
editable: false,
|
|
138
|
+
renderer: (_v: any, _c: any, record: any) =>
|
|
139
|
+
record?.pinned && !this.isPast()
|
|
140
|
+
? html`<md-icon style="--md-icon-size:16px" title=${t('announcement.field.pinned')}>push_pin</md-icon>`
|
|
141
|
+
: record?.importance === 'URGENT'
|
|
142
|
+
? html`<md-icon style="--md-icon-size:16px;color:var(--md-sys-color-error)" title=${importanceWord('URGENT')}>priority_high</md-icon>`
|
|
143
|
+
: ''
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
type: 'string',
|
|
148
|
+
name: 'category',
|
|
149
|
+
header: t('announcement.field.category'),
|
|
150
|
+
width: 90,
|
|
151
|
+
record: { editable: false, renderer: (value: string) => categoryWord(value) },
|
|
152
|
+
filter: { type: 'select', operator: 'eq', options: [{ display: '', value: '' }, ...CATEGORIES.map(code => ({ display: categoryWord(code), value: code }))] }
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
type: 'string',
|
|
156
|
+
name: 'title',
|
|
157
|
+
header: t('announcement.field.title'),
|
|
158
|
+
width: 320,
|
|
159
|
+
record: {
|
|
160
|
+
editable: false,
|
|
161
|
+
renderer: (value: string, _c: any, record: any) =>
|
|
162
|
+
html`<span style=${owed(record) ? 'font-weight:600' : ''}>${record?.importance === 'IMPORTANT' ? `[${importanceWord('IMPORTANT')}] ` : ''}${value}</span>`
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
type: 'string',
|
|
167
|
+
name: 'readerState',
|
|
168
|
+
header: t('announcement.field.reader-state'),
|
|
169
|
+
width: 100,
|
|
170
|
+
record: { editable: false, renderer: (value: string) => html`<span style=${value === 'unread' || value === 'to-acknowledge' ? 'color:var(--md-sys-color-primary);font-weight:600' : ''}>${readerStateWord(value)}</span>` }
|
|
171
|
+
},
|
|
172
|
+
{ type: 'string', name: 'publisher', header: t('announcement.field.publisher'), width: 110, record: { editable: false, renderer: (value: any) => value?.name ?? '' } },
|
|
173
|
+
{ type: 'string', name: 'publishedAt', header: t('announcement.field.published-at'), width: 110, record: { editable: false, renderer: (value: string) => when(value, false) } },
|
|
174
|
+
{ type: 'string', name: 'endsAt', header: t('announcement.field.ends-at'), width: 110, record: { editable: false, renderer: (value: string) => when(value, false) } }
|
|
175
|
+
]
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private isPast() {
|
|
180
|
+
return this.period === 'past'
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async fetchHandler({ page = 1, limit = 50, filters = [], search }: FetchOption) {
|
|
184
|
+
const response = await client.query({
|
|
185
|
+
query: gql`
|
|
186
|
+
query ($filters: [Filter!], $pagination: Pagination, $search: String) {
|
|
187
|
+
responses: announcements(filters: $filters, pagination: $pagination, search: $search) {
|
|
188
|
+
items { ${LIST_FIELDS} }
|
|
189
|
+
total
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
`,
|
|
193
|
+
/* The period is the reader's own filter - the assistant narrows the same list the same way. */
|
|
194
|
+
variables: { filters: [...filters, { name: 'period', operator: 'eq', value: this.period }], pagination: { page, limit }, search: search?.trim() || undefined },
|
|
195
|
+
fetchPolicy: 'network-only'
|
|
196
|
+
})
|
|
197
|
+
if (response.errors?.length) throw response
|
|
198
|
+
return { total: response.data.responses.total, records: response.data.responses.items }
|
|
199
|
+
}
|
|
200
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import '@operato/data-grist/ox-grist.js'
|
|
2
|
+
import '@operato/data-grist/ox-filters-form.js'
|
|
3
|
+
|
|
4
|
+
import gql from 'graphql-tag'
|
|
5
|
+
import { html } from 'lit'
|
|
6
|
+
import { customElement, query, state } from 'lit/decorators.js'
|
|
7
|
+
|
|
8
|
+
import { DataGrist, FetchOption } from '@operato/data-grist'
|
|
9
|
+
import { client } from '@operato/graphql'
|
|
10
|
+
import { i18next, localize } from '@operato/i18n'
|
|
11
|
+
import { navigate, PageView } from '@operato/shell'
|
|
12
|
+
import { CommonGristStyles, CommonHeaderStyles, ScrollbarStyles, WorkSurfacePageStyles } from '@operato/styles'
|
|
13
|
+
import { isMobileDevice } from '@operato/utils'
|
|
14
|
+
|
|
15
|
+
import { CATEGORIES, categoryWord, importanceWord, statusWord, t, when } from '../utils/announcement-words.js'
|
|
16
|
+
|
|
17
|
+
const STATUSES = ['DRAFT', 'SCHEDULED', 'PUBLISHED', 'WITHDRAWN']
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 공지 관리 — every announcement of the domain for its writers: the ones out, scheduled and withdrawn, and one's own
|
|
21
|
+
* drafts, with how many each reached and how many read it (ADR-0163 decision 4). A draft opens in the editor; the rest
|
|
22
|
+
* open as their readers see them, with the writer's actions.
|
|
23
|
+
*/
|
|
24
|
+
@customElement('announcement-manage-list')
|
|
25
|
+
export class AnnouncementManageList extends localize(i18next)(PageView) {
|
|
26
|
+
static styles = [WorkSurfacePageStyles, CommonGristStyles, CommonHeaderStyles, ScrollbarStyles]
|
|
27
|
+
|
|
28
|
+
@state() gristConfig: any
|
|
29
|
+
@query('ox-grist') private grist!: DataGrist
|
|
30
|
+
|
|
31
|
+
get context() {
|
|
32
|
+
return {
|
|
33
|
+
title: t('announcement.title.manage'),
|
|
34
|
+
/* Opened from 공지사항; the menu marks it. */
|
|
35
|
+
menu: 'announcement-list',
|
|
36
|
+
search: {
|
|
37
|
+
handler: (search: string) => {
|
|
38
|
+
this.grist.searchText = search
|
|
39
|
+
},
|
|
40
|
+
value: this.grist?.searchText || ''
|
|
41
|
+
},
|
|
42
|
+
filter: {
|
|
43
|
+
handler: () => {
|
|
44
|
+
this.grist.toggleHeadroom()
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
actions: [{ title: t('announcement.button.new'), icon: 'add', emphasis: { raised: true }, action: () => navigate('announcement-editor') }],
|
|
48
|
+
exportable: { name: t('announcement.title.manage'), data: () => this.grist.exportRecords() }
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
render() {
|
|
53
|
+
return html`<ox-grist class="page-fill" search-mode="term" .mode=${isMobileDevice() ? 'CARD' : 'GRID'} .config=${this.gristConfig} .fetchHandler=${this.fetchHandler.bind(this)}>
|
|
54
|
+
<div slot="headroom" class="header">
|
|
55
|
+
<div class="filters"><ox-filters-form without-search autofocus></ox-filters-form></div>
|
|
56
|
+
</div>
|
|
57
|
+
</ox-grist>`
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async pageUpdated(changes: any) {
|
|
61
|
+
if (this.active && changes.active && this.gristConfig) this.grist?.fetch()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async pageInitialized() {
|
|
65
|
+
const open = (record: any) => record?.id && navigate(record.status === 'DRAFT' ? `announcement-editor?id=${record.id}` : `announcement-view?id=${record.id}`)
|
|
66
|
+
const readonly = (type: string, name: string, header: string, width: number, extra: any = {}) => ({ type, name, header, width, sortable: true, ...extra, record: { editable: false, ...extra.record } })
|
|
67
|
+
this.gristConfig = {
|
|
68
|
+
rows: { appendable: false, selectable: false, handlers: { click: (_c: any, _d: any, _col: any, record: any) => open(record) } },
|
|
69
|
+
pagination: { limit: 50, pages: [50, 100, 500] },
|
|
70
|
+
emptyNote: { title: t('announcement.text.manage-empty'), description: t('announcement.text.manage-empty-description') },
|
|
71
|
+
list: { fields: ['title', 'status'], details: ['publishedAt', 'readCount'] },
|
|
72
|
+
columns: [
|
|
73
|
+
readonly('string', 'status', t('announcement.field.status'), 80, {
|
|
74
|
+
record: { renderer: (value: string) => statusWord(value) },
|
|
75
|
+
filter: { type: 'select', operator: 'eq', options: [{ display: '', value: '' }, ...STATUSES.map(code => ({ display: statusWord(code), value: code }))] }
|
|
76
|
+
}),
|
|
77
|
+
readonly('string', 'category', t('announcement.field.category'), 90, {
|
|
78
|
+
record: { renderer: (value: string) => categoryWord(value) },
|
|
79
|
+
filter: { type: 'select', operator: 'eq', options: [{ display: '', value: '' }, ...CATEGORIES.map(code => ({ display: categoryWord(code), value: code }))] }
|
|
80
|
+
}),
|
|
81
|
+
readonly('string', 'importance', t('announcement.field.importance'), 70, { record: { renderer: (value: string) => importanceWord(value) } }),
|
|
82
|
+
readonly('string', 'title', t('announcement.field.title'), 320),
|
|
83
|
+
readonly('string', 'startsAt', t('announcement.field.starts-at'), 140, { record: { renderer: (value: string) => when(value) } }),
|
|
84
|
+
readonly('string', 'endsAt', t('announcement.field.ends-at'), 140, { record: { renderer: (value: string) => when(value) } }),
|
|
85
|
+
readonly('integer', 'audienceSize', t('announcement.field.audience-size'), 70, { sortable: false, record: { align: 'right' } }),
|
|
86
|
+
readonly('integer', 'readCount', t('announcement.field.read-count'), 70, { sortable: false, record: { align: 'right' } }),
|
|
87
|
+
readonly('integer', 'acknowledgedCount', t('announcement.field.acknowledged-count'), 70, {
|
|
88
|
+
sortable: false,
|
|
89
|
+
record: { align: 'right', renderer: (value: number, _c: any, record: any) => (record?.requiresAcknowledgement ? (value ?? '') : '') }
|
|
90
|
+
}),
|
|
91
|
+
readonly('string', 'updatedAt', t('announcement.field.updated-at'), 140, { record: { renderer: (value: string) => when(value) } })
|
|
92
|
+
],
|
|
93
|
+
sorters: [{ name: 'updatedAt', desc: true }]
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async fetchHandler({ page = 1, limit = 50, sortings = [], filters = [], search }: FetchOption) {
|
|
98
|
+
const response = await client.query({
|
|
99
|
+
query: gql`
|
|
100
|
+
query ($filters: [Filter!], $pagination: Pagination, $sortings: [Sorting!], $search: String) {
|
|
101
|
+
responses: managedAnnouncements(filters: $filters, pagination: $pagination, sortings: $sortings, search: $search) {
|
|
102
|
+
items { id title category importance status startsAt endsAt requiresAcknowledgement publishedAt audienceSize readCount acknowledgedCount updatedAt }
|
|
103
|
+
total
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
`,
|
|
107
|
+
variables: { filters, pagination: { page, limit }, sortings, search: search?.trim() || undefined },
|
|
108
|
+
fetchPolicy: 'network-only'
|
|
109
|
+
})
|
|
110
|
+
if (response.errors?.length) throw response
|
|
111
|
+
return { total: response.data.responses.total, records: response.data.responses.items }
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import '@operato/list-state/ox-page-state.js'
|
|
2
|
+
import '../components/announcement-sheet.js'
|
|
3
|
+
import '../components/announcement-receipts.js'
|
|
4
|
+
|
|
5
|
+
import { css, html, nothing } from 'lit'
|
|
6
|
+
import { customElement, state } from 'lit/decorators.js'
|
|
7
|
+
|
|
8
|
+
import { i18next, localize } from '@operato/i18n'
|
|
9
|
+
import { notify, openPopup } from '@operato/layout'
|
|
10
|
+
import { navigate, PageView } from '@operato/shell'
|
|
11
|
+
import { DocumentPageStyles } from '@operato/styles'
|
|
12
|
+
|
|
13
|
+
import { announcementApi, type AnnouncementItem } from '../utils/announcement-api.js'
|
|
14
|
+
import { reasonOf, statusWord, t, when } from '../utils/announcement-words.js'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 공지 보기 — one announcement. Opening it reads it for its audience (ADR-0163 decision 5). Its writers also see whom it
|
|
18
|
+
* reached and who read it, and change, remind or withdraw it from here (decision 4).
|
|
19
|
+
*/
|
|
20
|
+
@customElement('announcement-view')
|
|
21
|
+
export class AnnouncementView extends localize(i18next)(PageView) {
|
|
22
|
+
static styles = [
|
|
23
|
+
DocumentPageStyles,
|
|
24
|
+
css`
|
|
25
|
+
.notice {
|
|
26
|
+
padding: var(--spacing-medium, 8px) var(--spacing-large, 12px);
|
|
27
|
+
border-radius: var(--border-radius, 4px);
|
|
28
|
+
background: var(--md-sys-color-surface-container, rgba(0, 0, 0, 0.04));
|
|
29
|
+
font-size: var(--ui-t-body, 14px);
|
|
30
|
+
}
|
|
31
|
+
.audience {
|
|
32
|
+
display: flex;
|
|
33
|
+
flex-wrap: wrap;
|
|
34
|
+
gap: var(--spacing-small, 4px) var(--spacing-medium, 8px);
|
|
35
|
+
font-size: var(--ui-t-small, 12px);
|
|
36
|
+
color: var(--ui-muted, var(--md-sys-color-on-surface-variant));
|
|
37
|
+
}
|
|
38
|
+
ul.deliveries {
|
|
39
|
+
margin: var(--spacing-small, 4px) 0 0;
|
|
40
|
+
padding: 0;
|
|
41
|
+
list-style: none;
|
|
42
|
+
font-size: var(--ui-t-small, 12px);
|
|
43
|
+
color: var(--ui-muted, var(--md-sys-color-on-surface-variant));
|
|
44
|
+
}
|
|
45
|
+
h2 {
|
|
46
|
+
margin: 0;
|
|
47
|
+
font-size: var(--ui-t-title-large, 22px);
|
|
48
|
+
}
|
|
49
|
+
`
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
@state() announcement: AnnouncementItem | null = null
|
|
53
|
+
@state() loading = false
|
|
54
|
+
@state() error: unknown = null
|
|
55
|
+
|
|
56
|
+
get context() {
|
|
57
|
+
const one = this.announcement
|
|
58
|
+
const actions: any[] = []
|
|
59
|
+
if (one?.editable) {
|
|
60
|
+
actions.push({ title: t('announcement.button.edit'), icon: 'edit', action: () => navigate(`announcement-editor?id=${one.id}`) })
|
|
61
|
+
if (one.status === 'PUBLISHED') {
|
|
62
|
+
actions.push({ title: t('announcement.button.receipts'), icon: 'groups', action: () => this.openReceipts() })
|
|
63
|
+
actions.push({ title: t('announcement.button.remind'), icon: 'notifications_active', action: () => this.remind() })
|
|
64
|
+
}
|
|
65
|
+
if (one.status === 'PUBLISHED' || one.status === 'SCHEDULED') actions.push({ title: t('announcement.button.withdraw'), icon: 'unpublished', action: () => this.withdraw() })
|
|
66
|
+
actions.push({ title: t('announcement.button.copy'), icon: 'content_copy', action: () => navigate(`announcement-editor?from=${one.id}`) })
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
title: t('announcement.title.view'),
|
|
70
|
+
...(one && one.status !== 'PUBLISHED' ? { description: statusWord(one.status) } : {}),
|
|
71
|
+
frame: 'document',
|
|
72
|
+
/* Opened from the list of announcements; the menu marks it. */
|
|
73
|
+
menu: 'announcement-list',
|
|
74
|
+
actions
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async pageUpdated(changes: any, lifecycle: any) {
|
|
79
|
+
if (!this.active || !(changes.active || changes.params)) return
|
|
80
|
+
const id = lifecycle?.params?.id
|
|
81
|
+
if (id) await this.load(id)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
render() {
|
|
85
|
+
const one = this.announcement
|
|
86
|
+
return html`<ox-page-state .loading=${this.loading} .error=${this.error} @retry=${() => one && this.load(one.id)}>
|
|
87
|
+
${one
|
|
88
|
+
? html`<div class="page">
|
|
89
|
+
<section class="page-section">
|
|
90
|
+
<h2>${one.title}</h2>
|
|
91
|
+
${one.status === 'WITHDRAWN' ? html`<p class="notice">${t('announcement.text.withdrawn', { when: when(one.withdrawnAt), reason: one.withdrawReason ?? '' })}</p>` : nothing}
|
|
92
|
+
${one.status === 'SCHEDULED' ? html`<p class="notice">${t('announcement.text.scheduled', { when: when(one.startsAt) })}</p>` : nothing}
|
|
93
|
+
<announcement-sheet .announcement=${one} @announcement-acknowledged=${() => this.load(one.id)}></announcement-sheet>
|
|
94
|
+
</section>
|
|
95
|
+
${one.editable && one.audiences
|
|
96
|
+
? html`<section class="page-section">
|
|
97
|
+
<header class="page-section-header"><h3>${t('announcement.field.audience')}</h3></header>
|
|
98
|
+
<div class="audience">
|
|
99
|
+
${one.audiences.map(target => html`<span>${t(`announcement.scope.${target.scope}`)} · ${target.targetName ?? target.targetId}</span>`)}
|
|
100
|
+
</div>
|
|
101
|
+
${(one.deliveries ?? []).length
|
|
102
|
+
? html`<ul class="deliveries">
|
|
103
|
+
${[...(one.deliveries ?? [])].reverse().map(
|
|
104
|
+
sent => html`<li>
|
|
105
|
+
${when(sent.at)} · ${t(`announcement.delivery.${sent.kind}`)} ·
|
|
106
|
+
${sent.failed ? t('announcement.text.delivery-not-sent', { count: sent.receivers }) : t('announcement.text.delivery-app', { count: sent.receivers })}
|
|
107
|
+
${sent.email.sent || sent.email.failed ? html` · ${t('announcement.text.delivery-email', { sent: sent.email.sent, failed: sent.email.failed })}` : nothing}
|
|
108
|
+
${sent.teams.sent || sent.teams.failed ? html` · ${t('announcement.text.delivery-teams', { sent: sent.teams.sent, failed: sent.teams.failed })}` : nothing}
|
|
109
|
+
</li>`
|
|
110
|
+
)}
|
|
111
|
+
</ul>`
|
|
112
|
+
: nothing}
|
|
113
|
+
${one.status === 'PUBLISHED'
|
|
114
|
+
? html`<p class="audience">
|
|
115
|
+
${t('announcement.text.reach', { size: one.audienceSize ?? 0, read: one.readCount ?? 0 })}
|
|
116
|
+
${one.requiresAcknowledgement ? t('announcement.text.reach-acknowledged', { acknowledged: one.acknowledgedCount ?? 0 }) : ''}
|
|
117
|
+
</p>`
|
|
118
|
+
: nothing}
|
|
119
|
+
</section>`
|
|
120
|
+
: nothing}
|
|
121
|
+
</div>`
|
|
122
|
+
: nothing}
|
|
123
|
+
</ox-page-state>`
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private async load(id: string) {
|
|
127
|
+
this.loading = true
|
|
128
|
+
this.error = null
|
|
129
|
+
try {
|
|
130
|
+
let one = await announcementApi.one(id)
|
|
131
|
+
if (!one) throw new Error(t('announcement.refusal.unknown-announcement'))
|
|
132
|
+
/* Opening it reads it - asked again after, so its writer's counts include this reading. */
|
|
133
|
+
if (one.addressed && !one.mine?.readAt) {
|
|
134
|
+
await announcementApi.read(id)
|
|
135
|
+
one = (await announcementApi.one(id)) ?? one
|
|
136
|
+
}
|
|
137
|
+
this.announcement = one
|
|
138
|
+
} catch (error) {
|
|
139
|
+
this.announcement = null
|
|
140
|
+
this.error = error
|
|
141
|
+
} finally {
|
|
142
|
+
this.loading = false
|
|
143
|
+
this.updateContext()
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private openReceipts() {
|
|
148
|
+
const one = this.announcement!
|
|
149
|
+
const popup: any = openPopup(html`<announcement-receipts .announcement=${one} @done=${() => popup.close()}></announcement-receipts>`, {
|
|
150
|
+
backdrop: true,
|
|
151
|
+
size: 'large',
|
|
152
|
+
title: `${t('announcement.button.receipts')} · ${one.title}`
|
|
153
|
+
})
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private async remind() {
|
|
157
|
+
try {
|
|
158
|
+
const told = await announcementApi.remind(this.announcement!.id)
|
|
159
|
+
notify({ message: t('announcement.text.reminded', { count: told }) })
|
|
160
|
+
} catch (error) {
|
|
161
|
+
notify({ level: 'error', message: reasonOf(error) })
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private withdraw() {
|
|
166
|
+
const one = this.announcement!
|
|
167
|
+
const popup: any = openPopup(
|
|
168
|
+
html`<announcement-withdraw-form .announcement=${one} @announcement-withdrawn=${() => this.load(one.id)} @done=${() => popup.close()}></announcement-withdraw-form>`,
|
|
169
|
+
{ backdrop: true, size: 'small', title: `${t('announcement.button.withdraw')} · ${one.title}` }
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
}
|
package/client/route.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/* The pages this module brings to an application that puts them in its menu (ADR-0163 decision 9). */
|
|
2
|
+
export default function route(page: string) {
|
|
3
|
+
switch (page) {
|
|
4
|
+
case 'announcement-list':
|
|
5
|
+
import('./pages/announcement-list.js')
|
|
6
|
+
return page
|
|
7
|
+
case 'announcement-view':
|
|
8
|
+
import('./pages/announcement-view.js')
|
|
9
|
+
return page
|
|
10
|
+
case 'announcement-manage-list':
|
|
11
|
+
import('./pages/announcement-manage-list.js')
|
|
12
|
+
return page
|
|
13
|
+
case 'announcement-editor':
|
|
14
|
+
import('./pages/announcement-editor.js')
|
|
15
|
+
return page
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -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,142 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The announcement doors as the screens call them (ADR-0163). 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 Audience = { scope: 'DOMAIN' | 'DEPARTMENT' | 'ROLE' | 'USER' | string; targetId: string; targetName?: string | null }
|
|
9
|
+
export type AnnouncementLink = { label: string; url: string }
|
|
10
|
+
export type FileRow = { id: string; name: string; fullpath: string; size?: number; mimetype?: string }
|
|
11
|
+
export type Receipt = { readAt?: string | null; acknowledgedAt?: string | null }
|
|
12
|
+
export type Delivery = { at: string; kind: string; receivers: number; email: { sent: number; failed: number }; teams: { sent: number; failed: number }; failed?: boolean }
|
|
13
|
+
|
|
14
|
+
export interface AnnouncementItem {
|
|
15
|
+
id: string
|
|
16
|
+
title: string
|
|
17
|
+
body?: string | null
|
|
18
|
+
category: string
|
|
19
|
+
importance: 'NORMAL' | 'IMPORTANT' | 'URGENT' | string
|
|
20
|
+
status: 'DRAFT' | 'SCHEDULED' | 'PUBLISHED' | 'WITHDRAWN' | string
|
|
21
|
+
startsAt?: string | null
|
|
22
|
+
endsAt?: string | null
|
|
23
|
+
pinned: boolean
|
|
24
|
+
requiresAcknowledgement: boolean
|
|
25
|
+
acknowledgeBy?: string | null
|
|
26
|
+
popup: boolean
|
|
27
|
+
sendEmail?: boolean
|
|
28
|
+
deliveries?: Delivery[] | null
|
|
29
|
+
links?: AnnouncementLink[] | null
|
|
30
|
+
version: number
|
|
31
|
+
publishedAt?: string | null
|
|
32
|
+
audienceSize?: number | null
|
|
33
|
+
withdrawnAt?: string | null
|
|
34
|
+
withdrawReason?: string | null
|
|
35
|
+
readerState?: 'unread' | 'read' | 'to-acknowledge' | 'acknowledged'
|
|
36
|
+
readCount?: number | null
|
|
37
|
+
acknowledgedCount?: number | null
|
|
38
|
+
editable?: boolean
|
|
39
|
+
addressed?: boolean
|
|
40
|
+
mine?: Receipt | null
|
|
41
|
+
publisher?: { id: string; name?: string } | null
|
|
42
|
+
creator?: { id: string; name?: string } | null
|
|
43
|
+
audiences?: Audience[]
|
|
44
|
+
attachments?: FileRow[]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface AnnouncementInput {
|
|
48
|
+
title: string
|
|
49
|
+
body?: string
|
|
50
|
+
category?: string
|
|
51
|
+
importance?: string
|
|
52
|
+
startsAt?: string | null
|
|
53
|
+
endsAt?: string | null
|
|
54
|
+
pinned?: boolean
|
|
55
|
+
requiresAcknowledgement?: boolean
|
|
56
|
+
acknowledgeBy?: string | null
|
|
57
|
+
popup?: boolean
|
|
58
|
+
sendEmail?: boolean
|
|
59
|
+
links?: AnnouncementLink[]
|
|
60
|
+
audiences?: Audience[]
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The fields a list row needs. */
|
|
64
|
+
export const LIST_FIELDS = `id title category importance status startsAt endsAt pinned requiresAcknowledgement acknowledgeBy popup publishedAt version readerState publisher { id name }`
|
|
65
|
+
|
|
66
|
+
/** Everything one announcement shows. */
|
|
67
|
+
export const FULL_FIELDS = `${LIST_FIELDS} body links sendEmail deliveries audienceSize withdrawnAt withdrawReason editable addressed mine { readAt acknowledgedAt }
|
|
68
|
+
creator { id name } audiences { scope targetId targetName } attachments { id name fullpath size mimetype }`
|
|
69
|
+
|
|
70
|
+
function answer<T>(response: any, field: string): T {
|
|
71
|
+
if (response.errors?.length) throw new Error(response.errors.map((one: any) => one.message).join(' · '))
|
|
72
|
+
return response.data?.[field] as T
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function ask<T>(query: string, variables: Record<string, unknown>, field: string): Promise<T> {
|
|
76
|
+
return answer<T>(await client.query({ query: gql(query), variables, fetchPolicy: 'network-only' }), field)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function change<T>(mutation: string, variables: Record<string, unknown>, field: string, uploading?: (fraction: number) => void): Promise<T> {
|
|
80
|
+
return answer<T>(await client.mutate({ mutation: gql(mutation), variables, ...(uploading ? { context: uploadContext(uploading) } : {}) }), field)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Tells the badge and an open list that a reading changed. */
|
|
84
|
+
export const announcementsChanged = () => window.dispatchEvent(new CustomEvent('announcements-changed'))
|
|
85
|
+
|
|
86
|
+
export const announcementApi = {
|
|
87
|
+
one: (id: string) => ask<AnnouncementItem | null>(`query ($id: String!) { announcement(id: $id) { ${FULL_FIELDS} readCount acknowledgedCount } }`, { id }, 'announcement'),
|
|
88
|
+
summary: () => ask<{ unread: number; toAcknowledge: number; pending: number }>(`query { announcementSummary { unread toAcknowledge pending } }`, {}, 'announcementSummary'),
|
|
89
|
+
popups: () => ask<AnnouncementItem[]>(`query { announcementPopups { ${FULL_FIELDS} } }`, {}, 'announcementPopups'),
|
|
90
|
+
read: async (id: string) => {
|
|
91
|
+
const one = await change<AnnouncementItem>(`mutation ($id: String!) { readAnnouncement(id: $id) { id readerState mine { readAt acknowledgedAt } } }`, { id }, 'readAnnouncement')
|
|
92
|
+
announcementsChanged()
|
|
93
|
+
return one
|
|
94
|
+
},
|
|
95
|
+
acknowledge: async (id: string) => {
|
|
96
|
+
const one = await change<AnnouncementItem>(`mutation ($id: String!) { acknowledgeAnnouncement(id: $id) { id readerState mine { readAt acknowledgedAt } } }`, { id }, 'acknowledgeAnnouncement')
|
|
97
|
+
announcementsChanged()
|
|
98
|
+
return one
|
|
99
|
+
},
|
|
100
|
+
receipts: (id: string) =>
|
|
101
|
+
ask<{ userId: string; name?: string; email?: string; listed: boolean; notifiedAt?: string; readAt?: string; acknowledgedAt?: string }[]>(
|
|
102
|
+
`query ($id: String!) { announcementReceipts(id: $id) { userId name email listed notifiedAt readAt acknowledgedAt } }`,
|
|
103
|
+
{ id },
|
|
104
|
+
'announcementReceipts'
|
|
105
|
+
),
|
|
106
|
+
revisions: (id: string) =>
|
|
107
|
+
ask<{ version: number; title: string; body?: string; rest?: any; createdAt: string; creator?: { name?: string } }[]>(
|
|
108
|
+
`query ($id: String!) { announcementRevisions(id: $id) { version title body rest createdAt creator { name } } }`,
|
|
109
|
+
{ id },
|
|
110
|
+
'announcementRevisions'
|
|
111
|
+
),
|
|
112
|
+
choices: () =>
|
|
113
|
+
ask<{ domainId: string; domainName: string; departments: { id: string; name: string; note?: string }[]; roles: { id: string; name: string; note?: string }[] }>(
|
|
114
|
+
`query { announcementAudienceChoices { domainId domainName departments { id name note } roles { id name note } } }`,
|
|
115
|
+
{},
|
|
116
|
+
'announcementAudienceChoices'
|
|
117
|
+
),
|
|
118
|
+
people: (text: string) => ask<{ id: string; name: string; note?: string }[]>(`query ($text: String!) { announcementPeople(text: $text) { id name note } }`, { text }, 'announcementPeople'),
|
|
119
|
+
audienceSize: (audiences: Audience[]) =>
|
|
120
|
+
ask<number>(`query ($audiences: [AnnouncementAudienceInput!]!) { announcementAudienceSize(audiences: $audiences) }`, { audiences }, 'announcementAudienceSize'),
|
|
121
|
+
create: (input: AnnouncementInput, attach: File[], uploading?: (fraction: number) => void) =>
|
|
122
|
+
change<AnnouncementItem>(
|
|
123
|
+
`mutation ($input: AnnouncementInput!, $attach: [Upload!]) { createAnnouncement(input: $input, attach: $attach) { id } }`,
|
|
124
|
+
{ input, attach },
|
|
125
|
+
'createAnnouncement',
|
|
126
|
+
attach.length ? uploading ?? (() => {}) : undefined
|
|
127
|
+
),
|
|
128
|
+
update: (id: string, input: AnnouncementInput, attach: File[], remove: string[], notifyAgain: boolean, uploading?: (fraction: number) => void) =>
|
|
129
|
+
change<AnnouncementItem>(
|
|
130
|
+
`mutation ($id: String!, $input: AnnouncementInput!, $attach: [Upload!], $remove: [String!], $notifyAgain: Boolean) {
|
|
131
|
+
updateAnnouncement(id: $id, input: $input, attach: $attach, remove: $remove, notifyAgain: $notifyAgain) { id status }
|
|
132
|
+
}`,
|
|
133
|
+
{ id, input, attach, remove, notifyAgain },
|
|
134
|
+
'updateAnnouncement',
|
|
135
|
+
attach.length ? uploading ?? (() => {}) : undefined
|
|
136
|
+
),
|
|
137
|
+
publish: (id: string) => change<AnnouncementItem>(`mutation ($id: String!) { publishAnnouncement(id: $id) { id status startsAt } }`, { id }, 'publishAnnouncement'),
|
|
138
|
+
withdraw: (id: string, reason: string) =>
|
|
139
|
+
change<AnnouncementItem>(`mutation ($id: String!, $reason: String!) { withdrawAnnouncement(id: $id, reason: $reason) { id status } }`, { id, reason }, 'withdrawAnnouncement'),
|
|
140
|
+
remove: (id: string) => change<boolean>(`mutation ($id: String!) { deleteAnnouncement(id: $id) }`, { id }, 'deleteAnnouncement'),
|
|
141
|
+
remind: (id: string) => change<number>(`mutation ($id: String!) { remindAnnouncement(id: $id) }`, { id }, 'remindAnnouncement')
|
|
142
|
+
}
|