@things-factory/announcement 10.1.255 → 10.1.257
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/components/announcement-receipts.ts +31 -36
- package/client/components/announcement-sheet.ts +18 -15
- package/client/components/audience-picker.ts +41 -28
- package/client/pages/announcement-editor.ts +125 -87
- package/client/pages/announcement-view.ts +38 -36
- package/client/utils/announcement-api.ts +3 -50
- package/dist-client/components/announcement-receipts.d.ts +6 -5
- package/dist-client/components/announcement-receipts.js +24 -52
- package/dist-client/components/announcement-receipts.js.map +1 -1
- package/dist-client/components/announcement-sheet.d.ts +2 -1
- package/dist-client/components/announcement-sheet.js +15 -21
- package/dist-client/components/announcement-sheet.js.map +1 -1
- package/dist-client/components/audience-picker.d.ts +8 -4
- package/dist-client/components/audience-picker.js +35 -35
- package/dist-client/components/audience-picker.js.map +1 -1
- package/dist-client/pages/announcement-editor.d.ts +22 -12
- package/dist-client/pages/announcement-editor.js +116 -130
- package/dist-client/pages/announcement-editor.js.map +1 -1
- package/dist-client/pages/announcement-view.d.ts +7 -4
- package/dist-client/pages/announcement-view.js +34 -48
- package/dist-client/pages/announcement-view.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/utils/announcement-api.d.ts +0 -47
- package/dist-client/utils/announcement-api.js +3 -21
- package/dist-client/utils/announcement-api.js.map +1 -1
- package/dist-server/index.d.ts +1 -0
- package/dist-server/index.js +1 -0
- package/dist-server/index.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/dist-server/work-doors.d.ts +1 -0
- package/dist-server/work-doors.js +11 -0
- package/dist-server/work-doors.js.map +1 -0
- package/package.json +11 -11
- package/server/index.ts +1 -0
- package/server/work-doors.ts +9 -0
- package/translations/en.json +2 -1
- package/translations/es.json +2 -1
- package/translations/ja.json +2 -1
- package/translations/ko.json +2 -1
- package/translations/ms.json +2 -1
- package/translations/zh.json +2 -1
|
@@ -6,17 +6,19 @@ import '@material/web/button/filled-button.js'
|
|
|
6
6
|
import '@material/web/button/text-button.js'
|
|
7
7
|
import '@operato/popup/ox-popup-actions.js'
|
|
8
8
|
|
|
9
|
+
import gql from 'graphql-tag'
|
|
9
10
|
import { css, html, LitElement, nothing } from 'lit'
|
|
10
11
|
import { customElement, property, state } from 'lit/decorators.js'
|
|
11
12
|
|
|
13
|
+
import { MutationController, QueryController } from '@operato/graphql'
|
|
12
14
|
import { i18next, localize } from '@operato/i18n'
|
|
13
15
|
import { notify } from '@operato/layout'
|
|
14
16
|
import { CommonPopupStyles, PopupBodyStyles } from '@operato/styles'
|
|
15
17
|
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
+
import { type AnnouncementItem } from '../utils/announcement-api.js'
|
|
19
|
+
import { t, when } from '../utils/announcement-words.js'
|
|
18
20
|
|
|
19
|
-
type Row =
|
|
21
|
+
type Row = { userId: string; name?: string; email?: string; listed: boolean; notifiedAt?: string; readAt?: string; acknowledgedAt?: string }
|
|
20
22
|
|
|
21
23
|
const FormStyles = css`
|
|
22
24
|
:host {
|
|
@@ -74,21 +76,25 @@ const FormStyles = css`
|
|
|
74
76
|
export class AnnouncementReceipts extends localize(i18next)(LitElement) {
|
|
75
77
|
static styles = [CommonPopupStyles, PopupBodyStyles, FormStyles]
|
|
76
78
|
@property({ attribute: false }) announcement!: AnnouncementItem
|
|
77
|
-
@state() private rows: Row[] = []
|
|
78
79
|
@state() private which: 'all' | 'unread' | 'unacknowledged' | 'later' = 'all'
|
|
79
|
-
@state() private busy = false
|
|
80
80
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
this
|
|
81
|
+
/* Whom it reached and what each did with it, read for the announcement shown (ADR-0190). */
|
|
82
|
+
private receipts = new QueryController(
|
|
83
|
+
this,
|
|
84
|
+
gql`query ($id: String!) { announcementReceipts(id: $id) { userId name email listed notifiedAt readAt acknowledgedAt } }`,
|
|
85
|
+
{ variables: () => (this.announcement?.id ? { id: this.announcement.id } : undefined) }
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
private reminder = new MutationController(this, gql`mutation ($id: String!) { remindAnnouncement(id: $id) }`, {
|
|
89
|
+
refetch: () => [this.receipts]
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
private get rows(): Row[] {
|
|
93
|
+
return this.receipts.data?.announcementReceipts ?? []
|
|
84
94
|
}
|
|
85
95
|
|
|
86
|
-
private
|
|
87
|
-
|
|
88
|
-
this.rows = await announcementApi.receipts(this.announcement.id)
|
|
89
|
-
} catch (error) {
|
|
90
|
-
notify({ level: 'error', message: reasonOf(error) })
|
|
91
|
-
}
|
|
96
|
+
private get busy() {
|
|
97
|
+
return this.reminder.pending
|
|
92
98
|
}
|
|
93
99
|
|
|
94
100
|
render() {
|
|
@@ -140,16 +146,8 @@ export class AnnouncementReceipts extends localize(i18next)(LitElement) {
|
|
|
140
146
|
}
|
|
141
147
|
|
|
142
148
|
private async remind() {
|
|
143
|
-
this.
|
|
144
|
-
|
|
145
|
-
const told = await announcementApi.remind(this.announcement.id)
|
|
146
|
-
notify({ message: t('announcement.text.reminded', { count: told }) })
|
|
147
|
-
await this.load()
|
|
148
|
-
} catch (error) {
|
|
149
|
-
notify({ level: 'error', message: reasonOf(error) })
|
|
150
|
-
} finally {
|
|
151
|
-
this.busy = false
|
|
152
|
-
}
|
|
149
|
+
const done = await this.reminder.run({ id: this.announcement.id })
|
|
150
|
+
if (done) notify({ message: t('announcement.text.reminded', { count: done.remindAnnouncement }) })
|
|
153
151
|
}
|
|
154
152
|
|
|
155
153
|
private close() {
|
|
@@ -163,7 +161,11 @@ export class AnnouncementWithdrawForm extends localize(i18next)(LitElement) {
|
|
|
163
161
|
static styles = [CommonPopupStyles, PopupBodyStyles, FormStyles]
|
|
164
162
|
@property({ attribute: false }) announcement!: AnnouncementItem
|
|
165
163
|
@state() private reason = ''
|
|
166
|
-
|
|
164
|
+
private withdrawal = new MutationController(this, gql`mutation ($id: String!, $reason: String!) { withdrawAnnouncement(id: $id, reason: $reason) { id status } }`)
|
|
165
|
+
|
|
166
|
+
private get busy() {
|
|
167
|
+
return this.withdrawal.pending
|
|
168
|
+
}
|
|
167
169
|
|
|
168
170
|
render() {
|
|
169
171
|
return html`<div class="body">
|
|
@@ -177,17 +179,10 @@ export class AnnouncementWithdrawForm extends localize(i18next)(LitElement) {
|
|
|
177
179
|
}
|
|
178
180
|
|
|
179
181
|
private async withdraw() {
|
|
180
|
-
this.
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
this.dispatchEvent(new CustomEvent('announcement-withdrawn', { bubbles: true, composed: true }))
|
|
185
|
-
this.close()
|
|
186
|
-
} catch (error) {
|
|
187
|
-
notify({ level: 'error', message: reasonOf(error) })
|
|
188
|
-
} finally {
|
|
189
|
-
this.busy = false
|
|
190
|
-
}
|
|
182
|
+
if (!(await this.withdrawal.run({ id: this.announcement.id, reason: this.reason }))) return
|
|
183
|
+
notify({ message: t('announcement.text.withdrawn-done') })
|
|
184
|
+
this.dispatchEvent(new CustomEvent('announcement-withdrawn', { bubbles: true, composed: true }))
|
|
185
|
+
this.close()
|
|
191
186
|
}
|
|
192
187
|
|
|
193
188
|
private close() {
|
|
@@ -5,17 +5,18 @@
|
|
|
5
5
|
import '@material/web/icon/icon.js'
|
|
6
6
|
import '@material/web/button/filled-button.js'
|
|
7
7
|
|
|
8
|
+
import gql from 'graphql-tag'
|
|
8
9
|
import { css, html, LitElement, nothing } from 'lit'
|
|
9
|
-
import { customElement, property
|
|
10
|
+
import { customElement, property } from 'lit/decorators.js'
|
|
10
11
|
import { unsafeHTML } from 'lit/directives/unsafe-html.js'
|
|
11
12
|
|
|
13
|
+
import { MutationController } from '@operato/graphql'
|
|
12
14
|
import { i18next, localize } from '@operato/i18n'
|
|
13
|
-
import { notify } from '@operato/layout'
|
|
14
15
|
import { navigate } from '@operato/shell'
|
|
15
16
|
import { renderMarkdown } from '@things-factory/ai-assistant/dist-client/index.js'
|
|
16
17
|
|
|
17
|
-
import {
|
|
18
|
-
import { categoryWord, importanceWord,
|
|
18
|
+
import { announcementsChanged, type AnnouncementItem } from '../utils/announcement-api.js'
|
|
19
|
+
import { categoryWord, importanceWord, t, when } from '../utils/announcement-words.js'
|
|
19
20
|
|
|
20
21
|
/** A screen of the application opens in place; an https page opens apart. */
|
|
21
22
|
export function followLink(url: string) {
|
|
@@ -143,7 +144,14 @@ export class AnnouncementSheet extends localize(i18next)(LitElement) {
|
|
|
143
144
|
`
|
|
144
145
|
|
|
145
146
|
@property({ attribute: false }) announcement!: AnnouncementItem
|
|
146
|
-
|
|
147
|
+
private acknowledgement = new MutationController(
|
|
148
|
+
this,
|
|
149
|
+
gql`mutation ($id: String!) { acknowledgeAnnouncement(id: $id) { id readerState mine { readAt acknowledgedAt } } }`
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
private get busy() {
|
|
153
|
+
return this.acknowledgement.pending
|
|
154
|
+
}
|
|
147
155
|
|
|
148
156
|
render() {
|
|
149
157
|
const one = this.announcement
|
|
@@ -200,15 +208,10 @@ export class AnnouncementSheet extends localize(i18next)(LitElement) {
|
|
|
200
208
|
}
|
|
201
209
|
|
|
202
210
|
private async acknowledge() {
|
|
203
|
-
this.
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
} catch (error) {
|
|
209
|
-
notify({ level: 'error', message: reasonOf(error) })
|
|
210
|
-
} finally {
|
|
211
|
-
this.busy = false
|
|
212
|
-
}
|
|
211
|
+
const done = await this.acknowledgement.run({ id: this.announcement.id })
|
|
212
|
+
if (!done) return
|
|
213
|
+
announcementsChanged()
|
|
214
|
+
this.announcement = { ...this.announcement, ...done.acknowledgeAnnouncement }
|
|
215
|
+
this.dispatchEvent(new CustomEvent('announcement-acknowledged', { bubbles: true, composed: true, detail: this.announcement }))
|
|
213
216
|
}
|
|
214
217
|
}
|
|
@@ -6,15 +6,18 @@ import '@material/web/icon/icon.js'
|
|
|
6
6
|
import '@operato/input/ox-select.js'
|
|
7
7
|
import '@operato/popup/ox-popup-list.js'
|
|
8
8
|
|
|
9
|
+
import gql from 'graphql-tag'
|
|
9
10
|
import { css, html, LitElement, nothing } from 'lit'
|
|
10
11
|
import { customElement, property, state } from 'lit/decorators.js'
|
|
11
12
|
|
|
13
|
+
import { QueryController } from '@operato/graphql'
|
|
12
14
|
import { i18next, localize } from '@operato/i18n'
|
|
13
15
|
|
|
14
|
-
import {
|
|
16
|
+
import { type Audience } from '../utils/announcement-api.js'
|
|
15
17
|
import { t } from '../utils/announcement-words.js'
|
|
16
18
|
|
|
17
|
-
type Choices =
|
|
19
|
+
type Choices = { domainId: string; domainName: string; departments: { id: string; name: string; note?: string }[]; roles: { id: string; name: string; note?: string }[] }
|
|
20
|
+
type Person = { id: string; name: string; note?: string }
|
|
18
21
|
|
|
19
22
|
const ICON: Record<string, string> = { DOMAIN: 'domain', DEPARTMENT: 'account_tree', ROLE: 'badge', USER: 'person' }
|
|
20
23
|
|
|
@@ -113,20 +116,41 @@ export class AudiencePicker extends localize(i18next)(LitElement) {
|
|
|
113
116
|
|
|
114
117
|
@property({ attribute: false }) value: Audience[] = []
|
|
115
118
|
@property({ type: Boolean }) disabled = false
|
|
116
|
-
@state() private choices?: Choices
|
|
117
119
|
@state() private text = ''
|
|
118
|
-
|
|
119
|
-
@state() private
|
|
120
|
+
/* The words searched for and the audience counted - each a moment after it stops changing. */
|
|
121
|
+
@state() private searched = ''
|
|
122
|
+
@state() private counted: Audience[] = []
|
|
120
123
|
private searching?: ReturnType<typeof setTimeout>
|
|
121
124
|
private counting?: ReturnType<typeof setTimeout>
|
|
122
125
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
126
|
+
/* What may be chosen, the people the words find, and how many the audience names now (ADR-0190). */
|
|
127
|
+
private choicesRead = new QueryController(
|
|
128
|
+
this,
|
|
129
|
+
gql`query { announcementAudienceChoices { domainId domainName departments { id name note } roles { id name note } } }`
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
private people = new QueryController(this, gql`query ($text: String!) { announcementPeople(text: $text) { id name note } }`, {
|
|
133
|
+
variables: () => (this.searched ? { text: this.searched } : undefined),
|
|
134
|
+
keepPrevious: true
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
private sizeRead = new QueryController(this, gql`query ($audiences: [AnnouncementAudienceInput!]!) { announcementAudienceSize(audiences: $audiences) }`, {
|
|
138
|
+
variables: () => (this.counted.length ? { audiences: this.counted } : undefined),
|
|
139
|
+
keepPrevious: true
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
private get choices(): Choices | undefined {
|
|
143
|
+
if (this.choicesRead.error) return { domainId: '', domainName: '', departments: [], roles: [] }
|
|
144
|
+
return this.choicesRead.data?.announcementAudienceChoices
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private get found(): Person[] {
|
|
148
|
+
return (this.searched && !this.people.error && this.people.data?.announcementPeople) || []
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private get size(): number | undefined {
|
|
152
|
+
if (!this.value.length) return 0
|
|
153
|
+
return this.sizeRead.error ? undefined : this.sizeRead.data?.announcementAudienceSize
|
|
130
154
|
}
|
|
131
155
|
|
|
132
156
|
updated(changes: Map<string, unknown>) {
|
|
@@ -183,8 +207,9 @@ export class AudiencePicker extends localize(i18next)(LitElement) {
|
|
|
183
207
|
}
|
|
184
208
|
|
|
185
209
|
private give(target: Audience) {
|
|
210
|
+
clearTimeout(this.searching)
|
|
186
211
|
this.text = ''
|
|
187
|
-
this.
|
|
212
|
+
this.searched = ''
|
|
188
213
|
this.tell([...this.value, target])
|
|
189
214
|
}
|
|
190
215
|
|
|
@@ -200,28 +225,16 @@ export class AudiencePicker extends localize(i18next)(LitElement) {
|
|
|
200
225
|
private search(text: string) {
|
|
201
226
|
this.text = text
|
|
202
227
|
clearTimeout(this.searching)
|
|
203
|
-
this.searching = setTimeout(
|
|
204
|
-
try {
|
|
205
|
-
this.found = text.trim() ? await announcementApi.people(text.trim()) : []
|
|
206
|
-
} catch {
|
|
207
|
-
this.found = []
|
|
208
|
-
}
|
|
209
|
-
}, 250)
|
|
228
|
+
this.searching = setTimeout(() => (this.searched = text.trim()), 250)
|
|
210
229
|
}
|
|
211
230
|
|
|
212
231
|
/** How many the audience names now - asked a moment after it stops changing. */
|
|
213
232
|
private count() {
|
|
214
233
|
clearTimeout(this.counting)
|
|
215
234
|
if (!this.value.length) {
|
|
216
|
-
this.
|
|
235
|
+
this.counted = []
|
|
217
236
|
return
|
|
218
237
|
}
|
|
219
|
-
this.counting = setTimeout(
|
|
220
|
-
try {
|
|
221
|
-
this.size = await announcementApi.audienceSize(this.value.map(({ scope, targetId }) => ({ scope, targetId })))
|
|
222
|
-
} catch {
|
|
223
|
-
this.size = undefined
|
|
224
|
-
}
|
|
225
|
-
}, 300)
|
|
238
|
+
this.counting = setTimeout(() => (this.counted = this.value.map(({ scope, targetId }) => ({ scope, targetId }))), 300)
|
|
226
239
|
}
|
|
227
240
|
}
|
|
@@ -3,20 +3,22 @@ import '@operato/input/ox-input-file.js'
|
|
|
3
3
|
import '@operato/list-state/ox-page-state.js'
|
|
4
4
|
import '../components/audience-picker.js'
|
|
5
5
|
|
|
6
|
+
import gql from 'graphql-tag'
|
|
6
7
|
import { css, html, nothing } from 'lit'
|
|
7
8
|
import { customElement, state } from 'lit/decorators.js'
|
|
8
9
|
import { unsafeHTML } from 'lit/directives/unsafe-html.js'
|
|
9
10
|
|
|
11
|
+
import { MutationController, QueryController, uploadContext } from '@operato/graphql'
|
|
10
12
|
import { i18next, localize } from '@operato/i18n'
|
|
11
13
|
import { OxPrompt } from '@operato/popup/ox-prompt.js'
|
|
12
14
|
import { notify } from '@operato/layout'
|
|
13
|
-
import { navigate, PageView } from '@operato/shell'
|
|
15
|
+
import { navigate, PageView, DraftController } from '@operato/shell'
|
|
14
16
|
import { renderMarkdown } from '@things-factory/ai-assistant/dist-client/index.js'
|
|
15
17
|
import { ButtonStyles, DocumentPageStyles, FormFieldStyles } from '@operato/styles'
|
|
16
18
|
|
|
17
19
|
import { chooser } from '../components/audience-picker.js'
|
|
18
|
-
import {
|
|
19
|
-
import { CATEGORIES, categoryWord, IMPORTANCES, importanceWord,
|
|
20
|
+
import { FULL_FIELDS, type AnnouncementInput, type AnnouncementItem, type AnnouncementLink, type Audience, type FileRow } from '../utils/announcement-api.js'
|
|
21
|
+
import { CATEGORIES, categoryWord, IMPORTANCES, importanceWord, statusWord, t } from '../utils/announcement-words.js'
|
|
20
22
|
|
|
21
23
|
type Form = {
|
|
22
24
|
title: string
|
|
@@ -32,6 +34,10 @@ type Form = {
|
|
|
32
34
|
sendEmail: boolean
|
|
33
35
|
links: AnnouncementLink[]
|
|
34
36
|
audiences: Audience[]
|
|
37
|
+
/** Files picked to attach, attached ones to take off, and whether a changed one tells everyone again. */
|
|
38
|
+
added: File[]
|
|
39
|
+
removed: string[]
|
|
40
|
+
notifyAgain: boolean
|
|
35
41
|
}
|
|
36
42
|
|
|
37
43
|
const blank = (): Form => ({
|
|
@@ -47,7 +53,10 @@ const blank = (): Form => ({
|
|
|
47
53
|
popup: false,
|
|
48
54
|
sendEmail: false,
|
|
49
55
|
links: [],
|
|
50
|
-
audiences: []
|
|
56
|
+
audiences: [],
|
|
57
|
+
added: [],
|
|
58
|
+
removed: [],
|
|
59
|
+
notifyAgain: false
|
|
51
60
|
})
|
|
52
61
|
|
|
53
62
|
/** `datetime-local` speaks the reader's own clock. */
|
|
@@ -74,7 +83,10 @@ function formOf(one: AnnouncementItem): Form {
|
|
|
74
83
|
popup: one.popup,
|
|
75
84
|
sendEmail: !!one.sendEmail,
|
|
76
85
|
links: (one.links ?? []).map(link => ({ ...link })),
|
|
77
|
-
audiences: (one.audiences ?? []).map(({ scope, targetId, targetName }) => ({ scope, targetId, targetName }))
|
|
86
|
+
audiences: (one.audiences ?? []).map(({ scope, targetId, targetName }) => ({ scope, targetId, targetName })),
|
|
87
|
+
added: [],
|
|
88
|
+
removed: [],
|
|
89
|
+
notifyAgain: false
|
|
78
90
|
}
|
|
79
91
|
}
|
|
80
92
|
|
|
@@ -200,17 +212,74 @@ export class AnnouncementEditor extends localize(i18next)(PageView) {
|
|
|
200
212
|
`
|
|
201
213
|
]
|
|
202
214
|
|
|
203
|
-
|
|
204
|
-
@state()
|
|
205
|
-
@state() files: FileRow[] = []
|
|
206
|
-
@state() added: File[] = []
|
|
207
|
-
@state() removed: string[] = []
|
|
208
|
-
@state() notifyAgain = false
|
|
215
|
+
/** What the address opens: an announcement by id, a new one copied `from` another, or a new one. */
|
|
216
|
+
@state() private address?: { id?: string; from?: string }
|
|
209
217
|
@state() preview = false
|
|
210
218
|
@state() progress?: number
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
219
|
+
|
|
220
|
+
/* The announcement edited, or the one a new one is copied from (ADR-0190). */
|
|
221
|
+
private record = new QueryController(this, gql(`query ($id: String!) { announcement(id: $id) { ${FULL_FIELDS} readCount acknowledgedCount } }`), {
|
|
222
|
+
variables: () => {
|
|
223
|
+
const id = this.address?.id || this.address?.from
|
|
224
|
+
return id ? { id } : undefined
|
|
225
|
+
}
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
/* What is typed over the announcement read - a copy starts without its days, a new one blank. */
|
|
229
|
+
private draft = new DraftController<Form>(this, {
|
|
230
|
+
read: () => {
|
|
231
|
+
if (!this.address) return undefined
|
|
232
|
+
if (!this.address.id && !this.address.from) return blank()
|
|
233
|
+
const one = this.record.data?.announcement
|
|
234
|
+
if (!one) return undefined
|
|
235
|
+
return this.address.from && !this.address.id ? { ...formOf(one), startsAt: '', endsAt: '' } : formOf(one)
|
|
236
|
+
},
|
|
237
|
+
key: () => this.address
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
/* A change reads the announcement again once it is done; one that goes out or away leaves the editor. */
|
|
241
|
+
private saves = {
|
|
242
|
+
create: new MutationController(this, gql`mutation ($input: AnnouncementInput!, $attach: [Upload!]) { createAnnouncement(input: $input, attach: $attach) { id } }`),
|
|
243
|
+
update: new MutationController(
|
|
244
|
+
this,
|
|
245
|
+
gql`
|
|
246
|
+
mutation ($id: String!, $input: AnnouncementInput!, $attach: [Upload!], $remove: [String!], $notifyAgain: Boolean) {
|
|
247
|
+
updateAnnouncement(id: $id, input: $input, attach: $attach, remove: $remove, notifyAgain: $notifyAgain) { id status }
|
|
248
|
+
}
|
|
249
|
+
`,
|
|
250
|
+
{ refetch: () => [this.record] }
|
|
251
|
+
),
|
|
252
|
+
publish: new MutationController(this, gql`mutation ($id: String!) { publishAnnouncement(id: $id) { id status startsAt } }`),
|
|
253
|
+
remove: new MutationController(this, gql`mutation ($id: String!) { deleteAnnouncement(id: $id) }`)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
get announcement(): AnnouncementItem | null {
|
|
257
|
+
return (this.address?.id && this.record.data?.announcement) || null
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** The form as it shows: each field's edit, else what was read (blank while it is read). */
|
|
261
|
+
get form(): Form {
|
|
262
|
+
return Object.fromEntries(Object.entries(blank()).map(([name, given]) => [name, this.draft.value(name as keyof Form) ?? given])) as Form
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** The files attached so far - a copy starts with none. */
|
|
266
|
+
get files(): FileRow[] {
|
|
267
|
+
return this.announcement?.attachments ?? []
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** A failed read, or an announcement the address names that is not there. */
|
|
271
|
+
get error(): unknown {
|
|
272
|
+
if (this.record.error) return this.record.error
|
|
273
|
+
return this.record.data && !this.record.data.announcement ? new Error(t('announcement.refusal.unknown-announcement')) : null
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
get loading() {
|
|
277
|
+
return this.record.loading
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
get busy() {
|
|
281
|
+
return Object.values(this.saves).some(save => save.pending)
|
|
282
|
+
}
|
|
214
283
|
|
|
215
284
|
get out() {
|
|
216
285
|
return this.announcement?.status === 'PUBLISHED'
|
|
@@ -241,52 +310,20 @@ export class AnnouncementEditor extends localize(i18next)(PageView) {
|
|
|
241
310
|
async pageUpdated(changes: any, lifecycle: any) {
|
|
242
311
|
if (!this.active || !(changes.active || changes.params)) return
|
|
243
312
|
const { id, from } = lifecycle?.params ?? {}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
await this.load(from, true)
|
|
248
|
-
} else {
|
|
249
|
-
this.announcement = null
|
|
250
|
-
this.form = blank()
|
|
251
|
-
this.files = []
|
|
252
|
-
this.reset()
|
|
253
|
-
this.updateContext()
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
private reset() {
|
|
258
|
-
this.added = []
|
|
259
|
-
this.removed = []
|
|
260
|
-
this.notifyAgain = false
|
|
313
|
+
const address = id ? { id } : from ? { from } : {}
|
|
314
|
+
if (JSON.stringify(address) === JSON.stringify(this.address)) return
|
|
315
|
+
this.address = address
|
|
261
316
|
this.preview = false
|
|
262
317
|
}
|
|
263
318
|
|
|
264
|
-
private async load(id: string, asCopy: boolean) {
|
|
265
|
-
this.loading = true
|
|
266
|
-
this.error = null
|
|
267
|
-
try {
|
|
268
|
-
const one = await announcementApi.one(id)
|
|
269
|
-
if (!one) throw new Error(t('announcement.refusal.unknown-announcement'))
|
|
270
|
-
this.announcement = asCopy ? null : one
|
|
271
|
-
this.form = asCopy ? { ...formOf(one), startsAt: '', endsAt: '' } : formOf(one)
|
|
272
|
-
this.files = asCopy ? [] : (one.attachments ?? [])
|
|
273
|
-
this.reset()
|
|
274
|
-
} catch (error) {
|
|
275
|
-
this.error = error
|
|
276
|
-
} finally {
|
|
277
|
-
this.loading = false
|
|
278
|
-
this.updateContext()
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
|
|
282
319
|
render() {
|
|
283
320
|
const form = this.form
|
|
284
321
|
const set = (patch: Partial<Form>) => {
|
|
285
|
-
this.
|
|
322
|
+
for (const [name, value] of Object.entries(patch)) this.draft.set(name as keyof Form, value)
|
|
286
323
|
this.updateContext()
|
|
287
324
|
}
|
|
288
325
|
const editable = !this.busy && (!this.announcement || !!this.announcement.editable)
|
|
289
|
-
return html`<ox-page-state .loading=${this.loading} .error=${this.error} @retry=${() => this.
|
|
326
|
+
return html`<ox-page-state .loading=${this.loading} .error=${this.error} @retry=${() => this.record.refetch()}>
|
|
290
327
|
<div class="page">
|
|
291
328
|
<section class="page-section">
|
|
292
329
|
<header class="page-section-header"><h3>${t('announcement.text.section-what')}</h3></header>
|
|
@@ -374,19 +411,19 @@ export class AnnouncementEditor extends localize(i18next)(PageView) {
|
|
|
374
411
|
<header class="page-section-header"><h3>${t('announcement.field.attachments')}</h3></header>
|
|
375
412
|
<ul class="files">
|
|
376
413
|
${this.files.map(
|
|
377
|
-
file => html`<li class=${
|
|
414
|
+
file => html`<li class=${form.removed.includes(file.id) ? 'removed' : ''}>
|
|
378
415
|
<md-icon>attach_file</md-icon>${file.name}
|
|
379
416
|
${editable
|
|
380
|
-
?
|
|
381
|
-
? html`<button @click=${() => (
|
|
382
|
-
: html`<button aria-label=${t('announcement.button.remove')} @click=${() => (
|
|
417
|
+
? form.removed.includes(file.id)
|
|
418
|
+
? html`<button @click=${() => set({ removed: form.removed.filter(id => id !== file.id) })}><md-icon>undo</md-icon></button>`
|
|
419
|
+
: html`<button aria-label=${t('announcement.button.remove')} @click=${() => set({ removed: [...form.removed, file.id] })}><md-icon>close</md-icon></button>`
|
|
383
420
|
: nothing}
|
|
384
421
|
</li>`
|
|
385
422
|
)}
|
|
386
|
-
${
|
|
423
|
+
${form.added.map(
|
|
387
424
|
(file, index) => html`<li>
|
|
388
425
|
<md-icon>upload_file</md-icon>${file.name}
|
|
389
|
-
<button aria-label=${t('announcement.button.remove')} @click=${() => (
|
|
426
|
+
<button aria-label=${t('announcement.button.remove')} @click=${() => set({ added: form.added.filter((_, i) => i !== index) })}><md-icon>close</md-icon></button>
|
|
390
427
|
</li>`
|
|
391
428
|
)}
|
|
392
429
|
</ul>
|
|
@@ -424,7 +461,7 @@ export class AnnouncementEditor extends localize(i18next)(PageView) {
|
|
|
424
461
|
|
|
425
462
|
${this.out
|
|
426
463
|
? html`<section class="page-section">
|
|
427
|
-
<label class="check"><input type="checkbox" .checked=${
|
|
464
|
+
<label class="check"><input type="checkbox" .checked=${form.notifyAgain} @change=${(e: any) => set({ notifyAgain: e.target.checked })} />${t('announcement.text.notify-again')}</label>
|
|
428
465
|
<span class="hint">${t('announcement.text.new-version', { version: (this.announcement?.version ?? 1) + 1 })}</span>
|
|
429
466
|
</section>`
|
|
430
467
|
: nothing}
|
|
@@ -437,7 +474,7 @@ export class AnnouncementEditor extends localize(i18next)(PageView) {
|
|
|
437
474
|
/* The picker's own reset says `change` again, with nothing - that one is let go. */
|
|
438
475
|
if (!picked.length) return
|
|
439
476
|
;(e.target as any).reset?.()
|
|
440
|
-
this.added
|
|
477
|
+
this.draft.set('added', [...this.form.added, ...picked])
|
|
441
478
|
}
|
|
442
479
|
|
|
443
480
|
private input(): AnnouncementInput {
|
|
@@ -459,31 +496,36 @@ export class AnnouncementEditor extends localize(i18next)(PageView) {
|
|
|
459
496
|
}
|
|
460
497
|
}
|
|
461
498
|
|
|
462
|
-
/**
|
|
499
|
+
/**
|
|
500
|
+
* Keeps it, then sends it out when asked - a draft that cannot go out stays kept, opened as one. Failures are told by
|
|
501
|
+
* the GraphQL link.
|
|
502
|
+
*/
|
|
463
503
|
private async save(publish: boolean) {
|
|
464
|
-
this.busy
|
|
504
|
+
if (this.busy) return
|
|
505
|
+
const id = this.announcement?.id
|
|
506
|
+
const { added, removed, notifyAgain } = this.form
|
|
507
|
+
const context = added.length ? uploadContext(fraction => (this.progress = fraction)) : undefined
|
|
508
|
+
const saving = this.draft.submit(() =>
|
|
509
|
+
id
|
|
510
|
+
? this.saves.update.run({ id, input: this.input(), attach: added, remove: removed, notifyAgain }, { context })
|
|
511
|
+
: this.saves.create.run({ input: this.input(), attach: added }, { context })
|
|
512
|
+
)
|
|
513
|
+
/* The actions wait while it is out. */
|
|
514
|
+
this.updateContext()
|
|
515
|
+
const saved = await saving
|
|
516
|
+
this.progress = undefined
|
|
465
517
|
this.updateContext()
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
notify({ message: sent.status === 'SCHEDULED' ? t('announcement.text.scheduled-done') : t('announcement.text.published-done') })
|
|
474
|
-
navigate(`announcement-view?id=${id}`)
|
|
518
|
+
if (!saved) return
|
|
519
|
+
const kept = id ?? saved.createAnnouncement.id
|
|
520
|
+
if (publish) {
|
|
521
|
+
const sent = await this.saves.publish.run({ id: kept })
|
|
522
|
+
if (sent) {
|
|
523
|
+
notify({ message: sent.publishAnnouncement.status === 'SCHEDULED' ? t('announcement.text.scheduled-done') : t('announcement.text.published-done') })
|
|
524
|
+
navigate(`announcement-view?id=${kept}`)
|
|
475
525
|
return
|
|
476
526
|
}
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
else navigate(`announcement-editor?id=${id}`)
|
|
480
|
-
} catch (error) {
|
|
481
|
-
notify({ level: 'error', message: reasonOf(error) })
|
|
482
|
-
} finally {
|
|
483
|
-
this.progress = undefined
|
|
484
|
-
this.busy = false
|
|
485
|
-
this.updateContext()
|
|
486
|
-
}
|
|
527
|
+
} else notify({ message: t('announcement.text.saved') })
|
|
528
|
+
if (!id) navigate(`announcement-editor?id=${kept}`)
|
|
487
529
|
}
|
|
488
530
|
|
|
489
531
|
private async deleteDraft() {
|
|
@@ -494,12 +536,8 @@ export class AnnouncementEditor extends localize(i18next)(PageView) {
|
|
|
494
536
|
cancelButton: { text: t('announcement.button.cancel') }
|
|
495
537
|
})
|
|
496
538
|
if (!sure) return
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
navigate('announcement-manage-list')
|
|
501
|
-
} catch (error) {
|
|
502
|
-
notify({ level: 'error', message: reasonOf(error) })
|
|
503
|
-
}
|
|
539
|
+
if (!(await this.saves.remove.run({ id: this.announcement!.id }))) return
|
|
540
|
+
notify({ message: t('announcement.text.deleted') })
|
|
541
|
+
navigate('announcement-manage-list')
|
|
504
542
|
}
|
|
505
543
|
}
|