@things-factory/calendar 8.0.0-beta.8 → 8.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/client/bootstrap.ts +1 -0
  2. package/client/index.ts +0 -0
  3. package/client/pages/attendee/attendee-importer.ts +87 -0
  4. package/client/pages/attendee/attendee-list-page.ts +324 -0
  5. package/client/pages/calendar/calendar-importer.ts +87 -0
  6. package/client/pages/calendar/calendar-list-page.ts +325 -0
  7. package/client/pages/calendar/calendar-page.ts +128 -0
  8. package/client/pages/event/event-importer.ts +87 -0
  9. package/client/pages/event/event-list-page.ts +324 -0
  10. package/client/route.ts +19 -0
  11. package/client/tsconfig.json +13 -0
  12. package/dist-client/tsconfig.tsbuildinfo +1 -1
  13. package/dist-server/tsconfig.tsbuildinfo +1 -1
  14. package/package.json +7 -7
  15. package/server/controllers/index.ts +0 -0
  16. package/server/index.ts +4 -0
  17. package/server/middlewares/index.ts +3 -0
  18. package/server/migrations/index.ts +9 -0
  19. package/server/routes.ts +28 -0
  20. package/server/service/attendee/attendee-mutation.ts +122 -0
  21. package/server/service/attendee/attendee-query.ts +31 -0
  22. package/server/service/attendee/attendee-type.ts +44 -0
  23. package/server/service/attendee/attendee.ts +37 -0
  24. package/server/service/attendee/index.ts +7 -0
  25. package/server/service/calendar/calendar-mutation.ts +133 -0
  26. package/server/service/calendar/calendar-query.ts +48 -0
  27. package/server/service/calendar/calendar-type.ts +55 -0
  28. package/server/service/calendar/calendar.ts +82 -0
  29. package/server/service/calendar/index.ts +7 -0
  30. package/server/service/event/event-mutation.ts +125 -0
  31. package/server/service/event/event-query.ts +38 -0
  32. package/server/service/event/event-type.ts +61 -0
  33. package/server/service/event/event.ts +85 -0
  34. package/server/service/event/index.ts +7 -0
  35. package/server/service/index.ts +32 -0
  36. package/server/tsconfig.json +10 -0
@@ -0,0 +1,325 @@
1
+ import '@operato/data-grist'
2
+
3
+ import { CommonButtonStyles, CommonHeaderStyles, CommonGristStyles, ScrollbarStyles } from '@operato/styles'
4
+ import { PageView, store } from '@operato/shell'
5
+ import { css, html } from 'lit'
6
+ import { customElement, property, query } from 'lit/decorators.js'
7
+ import { ScopedElementsMixin } from '@open-wc/scoped-elements'
8
+ import { ColumnConfig, DataGrist, FetchOption, SortersControl } from '@operato/data-grist'
9
+ import { client } from '@operato/graphql'
10
+ import { i18next, localize } from '@operato/i18n'
11
+ import { notify, openPopup } from '@operato/layout'
12
+ import { OxPopup } from '@operato/popup'
13
+ import { isMobileDevice } from '@operato/utils'
14
+
15
+ import { connect } from 'pwa-helpers/connect-mixin'
16
+ import gql from 'graphql-tag'
17
+
18
+ import { CalendarImporter } from './calendar-importer'
19
+
20
+ @customElement('calendar-list-page')
21
+ export class CalendarListPage extends connect(store)(localize(i18next)(ScopedElementsMixin(PageView))) {
22
+ static styles = [
23
+ ScrollbarStyles,
24
+ CommonGristStyles,
25
+ CommonHeaderStyles,
26
+ css`
27
+ :host {
28
+ display: flex;
29
+
30
+ width: 100%;
31
+
32
+ --grid-record-emphasized-background-color: #8b0000;
33
+ --grid-record-emphasized-color: #ff6b6b;
34
+ }
35
+
36
+ ox-grist {
37
+ overflow-y: auto;
38
+ flex: 1;
39
+ }
40
+
41
+ ox-filters-form {
42
+ flex: 1;
43
+ }
44
+ `
45
+ ]
46
+
47
+ static get scopedElements() {
48
+ return {
49
+ 'calendar-importer': CalendarImporter
50
+ }
51
+ }
52
+
53
+ @property({ type: Object }) gristConfig: any
54
+ @property({ type: String }) mode: 'CARD' | 'GRID' | 'LIST' = isMobileDevice() ? 'CARD' : 'GRID'
55
+
56
+ @query('ox-grist') private grist!: DataGrist
57
+
58
+ get context() {
59
+ return {
60
+ title: i18next.t('title.calendar list'),
61
+ search: {
62
+ handler: (search: string) => {
63
+ this.grist.searchText = search
64
+ },
65
+ value: this.grist?.searchText || '',
66
+ autofocus: true
67
+ },
68
+ filter: {
69
+ handler: () => {
70
+ this.grist.toggleHeadroom()
71
+ }
72
+ },
73
+ help: 'calendar/calendar',
74
+ actions: [
75
+ {
76
+ title: i18next.t('button.save'),
77
+ action: this._updateCalendar.bind(this),
78
+ ...CommonButtonStyles.save
79
+ },
80
+ {
81
+ title: i18next.t('button.delete'),
82
+ action: this._deleteCalendar.bind(this),
83
+ ...CommonButtonStyles.delete
84
+ }
85
+ ],
86
+ exportable: {
87
+ name: i18next.t('title.calendar list'),
88
+ data: this.exportHandler.bind(this)
89
+ },
90
+ importable: {
91
+ handler: this.importHandler.bind(this)
92
+ }
93
+ }
94
+ }
95
+
96
+ render() {
97
+ const mode = this.mode || (isMobileDevice() ? 'CARD' : 'GRID')
98
+
99
+ return html`
100
+ <ox-grist .mode=${mode} .config=${this.gristConfig} .fetchHandler=${this.fetchHandler.bind(this)}>
101
+ <div slot="headroom" class="header">
102
+ <div class="filters">
103
+ <ox-filters-form autofocus></ox-filters-form>
104
+
105
+ <div id="modes">
106
+ <md-icon @click=${() => (this.mode = 'GRID')} ?active=${mode == 'GRID'}>grid_on</md-icon>
107
+ <md-icon @click=${() => (this.mode = 'LIST')} ?active=${mode == 'LIST'}>format_list_bulleted</md-icon>
108
+ <md-icon @click=${() => (this.mode = 'CARD')} ?active=${mode == 'CARD'}>apps</md-icon>
109
+ </div>
110
+ </div>
111
+ </div>
112
+ </ox-grist>
113
+ `
114
+ }
115
+
116
+ async pageInitialized(lifecycle: any) {
117
+ this.gristConfig = {
118
+ list: {
119
+ fields: ['name', 'description'],
120
+ details: ['active', 'updatedAt']
121
+ },
122
+ columns: [
123
+ { type: 'gutter', gutterName: 'sequence' },
124
+ { type: 'gutter', gutterName: 'row-selector', multiple: true },
125
+ {
126
+ type: 'string',
127
+ name: 'name',
128
+ header: i18next.t('field.name'),
129
+ record: {
130
+ editable: true
131
+ },
132
+ filter: 'search',
133
+ sortable: true,
134
+ width: 150
135
+ },
136
+ {
137
+ type: 'string',
138
+ name: 'description',
139
+ header: i18next.t('field.description'),
140
+ record: {
141
+ editable: true
142
+ },
143
+ filter: 'search',
144
+ width: 200
145
+ },
146
+ {
147
+ type: 'checkbox',
148
+ name: 'active',
149
+ label: true,
150
+ header: i18next.t('field.active'),
151
+ record: {
152
+ editable: true
153
+ },
154
+ filter: true,
155
+ sortable: true,
156
+ width: 60
157
+ },
158
+ {
159
+ type: 'resource-object',
160
+ name: 'updater',
161
+ header: i18next.t('field.updater'),
162
+ record: {
163
+ editable: false
164
+ },
165
+ sortable: true,
166
+ width: 120
167
+ },
168
+ {
169
+ type: 'datetime',
170
+ name: 'updatedAt',
171
+ header: i18next.t('field.updated_at'),
172
+ record: {
173
+ editable: false
174
+ },
175
+ sortable: true,
176
+ width: 180
177
+ }
178
+ ],
179
+ rows: {
180
+ selectable: {
181
+ multiple: true
182
+ }
183
+ },
184
+ sorters: [
185
+ {
186
+ name: 'name'
187
+ }
188
+ ]
189
+ }
190
+ }
191
+
192
+ async pageUpdated(changes: any, lifecycle: any) {
193
+ if (this.active) {
194
+ // do something here when this page just became as active
195
+ }
196
+ }
197
+
198
+ async fetchHandler({ page = 1, limit = 100, sortings = [], filters = [] }: FetchOption) {
199
+ const response = await client.query({
200
+ query: gql`
201
+ query ($filters: [Filter!], $pagination: Pagination, $sortings: [Sorting!]) {
202
+ responses: calendars(filters: $filters, pagination: $pagination, sortings: $sortings) {
203
+ items {
204
+ id
205
+ name
206
+ description
207
+ active
208
+ updater {
209
+ id
210
+ name
211
+ }
212
+ updatedAt
213
+ }
214
+ total
215
+ }
216
+ }
217
+ `,
218
+ variables: {
219
+ filters,
220
+ pagination: { page, limit },
221
+ sortings
222
+ }
223
+ })
224
+
225
+ return {
226
+ total: response.data.responses.total || 0,
227
+ records: response.data.responses.items || []
228
+ }
229
+ }
230
+
231
+ async _deleteCalendar() {
232
+ if (confirm(i18next.t('text.sure_to_x', { x: i18next.t('text.delete') }))) {
233
+ const ids = this.grist.selected.map(record => record.id)
234
+ if (ids && ids.length > 0) {
235
+ const response = await client.mutate({
236
+ mutation: gql`
237
+ mutation ($ids: [String!]!) {
238
+ deleteCalendars(ids: $ids)
239
+ }
240
+ `,
241
+ variables: {
242
+ ids
243
+ }
244
+ })
245
+
246
+ if (!response.errors) {
247
+ this.grist.fetch()
248
+ notify({
249
+ message: i18next.t('text.info_x_successfully', { x: i18next.t('text.delete') })
250
+ })
251
+ }
252
+ }
253
+ }
254
+ }
255
+
256
+ async _updateCalendar() {
257
+ let patches = this.grist.dirtyRecords
258
+ if (patches && patches.length) {
259
+ patches = patches.map(patch => {
260
+ let patchField: any = patch.id ? { id: patch.id } : {}
261
+ const dirtyFields = patch.__dirtyfields__
262
+ for (let key in dirtyFields) {
263
+ patchField[key] = dirtyFields[key].after
264
+ }
265
+ patchField.cuFlag = patch.__dirty__
266
+
267
+ return patchField
268
+ })
269
+
270
+ const response = await client.mutate({
271
+ mutation: gql`
272
+ mutation ($patches: [CalendarPatch!]!) {
273
+ updateMultipleCalendar(patches: $patches) {
274
+ name
275
+ }
276
+ }
277
+ `,
278
+ variables: {
279
+ patches
280
+ }
281
+ })
282
+
283
+ if (!response.errors) {
284
+ this.grist.fetch()
285
+ }
286
+ }
287
+ }
288
+
289
+ async exportHandler() {
290
+ const exportTargets = this.grist.selected.length ? this.grist.selected : this.grist.dirtyData.records
291
+ const targetFieldSet = new Set(['id', 'name', 'description', 'active'])
292
+
293
+ return exportTargets.map(calendar => {
294
+ let tempObj = {}
295
+ for (const field of targetFieldSet) {
296
+ tempObj[field] = calendar[field]
297
+ }
298
+
299
+ return tempObj
300
+ })
301
+ }
302
+
303
+ async importHandler(records) {
304
+ const popup = openPopup(
305
+ html`
306
+ <calendar-importer
307
+ .calendars=${records}
308
+ @imported=${() => {
309
+ history.back()
310
+ this.grist.fetch()
311
+ }}
312
+ ></calendar-importer>
313
+ `,
314
+ {
315
+ backdrop: true,
316
+ size: 'large',
317
+ title: i18next.t('title.import calendar')
318
+ }
319
+ )
320
+
321
+ popup.onclosed = () => {
322
+ this.grist.fetch()
323
+ }
324
+ }
325
+ }
@@ -0,0 +1,128 @@
1
+ import '@operato/event-view/ox-event-view.js'
2
+
3
+ import { PropertyValues, html, css } from 'lit'
4
+ import { customElement, property, query, state } from 'lit/decorators.js'
5
+ import { connect } from 'pwa-helpers/connect-mixin.js'
6
+ import { store, PageView } from '@operato/shell'
7
+ import { BizEvent, CALENDAR, EventProvider } from '@operato/event-view/types.js'
8
+ import { OxEventView } from '@operato/event-view'
9
+ import { ScrollbarStyles } from '@operato/styles'
10
+
11
+ @customElement('calendar-page')
12
+ export class CalendarPage extends connect(store)(PageView) {
13
+ static styles = [
14
+ ScrollbarStyles,
15
+ css`
16
+ :host {
17
+ display: flex;
18
+ }
19
+
20
+ ox-event-view {
21
+ flex: 1;
22
+ padding: 10px;
23
+ overflow: auto;
24
+ }
25
+ `
26
+ ]
27
+
28
+ @property({ type: String }) itemId?: string
29
+ @property({ type: Object }) params: any
30
+
31
+ @query('ox-event-view') eventView!: OxEventView
32
+
33
+ render() {
34
+ return html` <ox-event-view mode="monthly" .eventProvider=${this}></ox-event-view> `
35
+ }
36
+
37
+ updated(changes: PropertyValues<this>) {
38
+ /*
39
+ * If this page properties are changed, this callback will be invoked.
40
+ * This callback will be called back only when this page is activated.
41
+ */
42
+ if (changes.has('itemId') || changes.has('params')) {
43
+ /* do something */
44
+ }
45
+ }
46
+
47
+ fetchEventsForCalendar(calendar: CALENDAR): Map<Date, BizEvent[]> {
48
+ const eventMap = new Map<Date, BizEvent[]>()
49
+
50
+ calendar.forEach(({ date }) => {
51
+ eventMap.set(date, [])
52
+ })
53
+
54
+ return eventMap
55
+ }
56
+
57
+ stateChanged(state: any) {
58
+ /*
59
+ * application wide state changed
60
+ *
61
+ */
62
+ }
63
+
64
+ /*
65
+ * page lifecycle
66
+ *
67
+ * - pageInitialized(lifecycle)
68
+ * - pageUpdated(changes, lifecycle, changedBefore)
69
+ * - pageDisposed(lifecycle)
70
+ *
71
+ * lifecycle value has
72
+ * - active : this page is activated
73
+ * - page : first path of href
74
+ * - resourceId : second path of href
75
+ * - params : search params object of href
76
+ * - initialized : initialized state of this page
77
+ *
78
+ * you can update lifecycle values, or add custom values
79
+ * by calling this.pageUpdate({ ...values }, force)
80
+ * If lifecycle values changed by this.pageUpdate(...),
81
+ * this.pageUpdated(...) will be called back right after.
82
+ * If you want to invoke this.pageUpdated(...) callback,
83
+ * set force argument to true.
84
+ *
85
+ * you can re-initialize this page
86
+ * by calling this.pageReset().
87
+ * this.pageInitialized(...) followed by this.pageDisposed(...) will be invoked
88
+ * by calling this.pageReset().
89
+ *
90
+ * you can invoke this.pageDisposed()
91
+ * by calling this.pageDispose()
92
+ */
93
+
94
+ pageInitialized(lifecycle: any) {
95
+ /*
96
+ * This page is initialized.
97
+ * It's right time to configure of this page.
98
+ *
99
+ * - called before when this page activated first
100
+ * - called when i18next resource is updated (loaded, changed, ..)
101
+ * - called right after this.pageReset()
102
+ */
103
+ }
104
+
105
+ pageUpdated(changes: any, lifecycle: any, before: any) {
106
+ if (this.active) {
107
+ /*
108
+ * this page is activated
109
+ */
110
+ this.itemId = lifecycle.resourceId
111
+ this.params = lifecycle.params
112
+ } else {
113
+ /* this page is deactivated */
114
+ }
115
+ }
116
+
117
+ pageDisposed(lifecycle: any) {
118
+ /*
119
+ * This page is disposed.
120
+ * It's right time to release system resources.
121
+ *
122
+ * - called just before (re)pageInitialized
123
+ * - called right after when i18next resource updated (loaded, changed, ..)
124
+ * - called right after this.pageReset()
125
+ * - called right after this.pageDispose()
126
+ */
127
+ }
128
+ }
@@ -0,0 +1,87 @@
1
+ import '@material/web/icon/icon.js'
2
+ import '@operato/data-grist'
3
+
4
+ import gql from 'graphql-tag'
5
+ import { css, html, LitElement } from 'lit'
6
+ import { property } from 'lit/decorators.js'
7
+
8
+ import { client } from '@operato/graphql'
9
+ import { i18next } from '@operato/i18n'
10
+ import { isMobileDevice } from '@operato/utils'
11
+ import { CommonHeaderStyles } from '@operato/styles'
12
+
13
+ export class EventImporter extends LitElement {
14
+ static styles = [
15
+ CommonHeaderStyles,
16
+ css`
17
+ :host {
18
+ display: flex;
19
+ flex-direction: column;
20
+
21
+ background-color: var(--md-sys-color-surface);
22
+ }
23
+
24
+ ox-grist {
25
+ flex: 1;
26
+ }
27
+ `
28
+ ]
29
+
30
+ @property({ type: Array }) events: any[] = []
31
+ @property({ type: Object }) columns = {
32
+ list: { fields: ['name', 'description'] },
33
+ pagination: { infinite: true },
34
+ columns: [
35
+ {
36
+ type: 'string',
37
+ name: 'name',
38
+ header: i18next.t('field.name'),
39
+ width: 150
40
+ },
41
+ {
42
+ type: 'string',
43
+ name: 'description',
44
+ header: i18next.t('field.description'),
45
+ width: 200
46
+ },
47
+ {
48
+ type: 'checkbox',
49
+ name: 'active',
50
+ header: i18next.t('field.active'),
51
+ width: 60
52
+ }
53
+ ]
54
+ }
55
+
56
+ render() {
57
+ return html`
58
+ <ox-grist
59
+ .mode=${isMobileDevice() ? 'LIST' : 'GRID'}
60
+ .config=${this.columns}
61
+ .data=${{
62
+ records: this.events
63
+ }}
64
+ ></ox-grist>
65
+
66
+ <div class="footer">
67
+ <div filler></div>
68
+ <button @click=${this.save.bind(this)} done><md-icon>save</md-icon>${i18next.t('button.save')}</button>
69
+ </div>
70
+ `
71
+ }
72
+
73
+ async save() {
74
+ const response = await client.mutate({
75
+ mutation: gql`
76
+ mutation importEvents($events: [EventPatch!]!) {
77
+ importEvents(events: $events)
78
+ }
79
+ `,
80
+ variables: { events: this.events }
81
+ })
82
+
83
+ if (response.errors?.length) return
84
+
85
+ this.dispatchEvent(new CustomEvent('imported'))
86
+ }
87
+ }