@things-factory/contact 8.0.0-beta.9 → 8.0.2

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.
@@ -0,0 +1,434 @@
1
+ import '@operato/data-grist'
2
+ import '@operato/context/ox-context-page-toolbar.js'
3
+
4
+ import { CommonGristStyles, CommonHeaderStyles, ScrollbarStyles } from '@operato/styles'
5
+ import { PageView, store } from '@operato/shell'
6
+ import { css, html } from 'lit'
7
+ import { customElement, property, query } from 'lit/decorators.js'
8
+ import { ScopedElementsMixin } from '@open-wc/scoped-elements'
9
+ import { DataGrist, FetchOption } from '@operato/data-grist'
10
+ import { client } from '@operato/graphql'
11
+ import { i18next, localize } from '@operato/i18n'
12
+ import { notify, openPopup } from '@operato/layout'
13
+ import { isMobileDevice } from '@operato/utils'
14
+
15
+ import { connect } from 'pwa-helpers/connect-mixin'
16
+ import gql from 'graphql-tag'
17
+
18
+ import '../components/contact-popup'
19
+ import { ContactImporter } from './contact-importer'
20
+
21
+ @customElement('contact-list-page')
22
+ export class ContactListPage extends connect(store)(localize(i18next)(ScopedElementsMixin(PageView))) {
23
+ static styles = [
24
+ ScrollbarStyles,
25
+ CommonGristStyles,
26
+ CommonHeaderStyles,
27
+ css`
28
+ :host {
29
+ display: flex;
30
+
31
+ width: 100%;
32
+
33
+ --ox-pfp-size: 40px;
34
+
35
+ --grid-record-emphasized-background-color: #8b0000;
36
+ --grid-record-emphasized-color: #ff6b6b;
37
+ }
38
+
39
+ ox-grist {
40
+ overflow-y: auto;
41
+ flex: 1;
42
+ }
43
+
44
+ .header {
45
+ grid-template-areas: 'filters actions';
46
+ }
47
+ `
48
+ ]
49
+
50
+ static get scopedElements() {
51
+ return {
52
+ 'contact-importer': ContactImporter
53
+ }
54
+ }
55
+
56
+ @property({ type: Object }) gristConfig: any
57
+ @property({ type: String }) mode: 'CARD' | 'GRID' | 'LIST' = isMobileDevice() ? 'CARD' : 'GRID'
58
+
59
+ @query('ox-grist') private grist!: DataGrist
60
+
61
+ get context() {
62
+ return {
63
+ title: i18next.t('title.contact list'),
64
+ search: {
65
+ handler: (search: string) => {
66
+ this.grist.searchText = search
67
+ },
68
+ value: this.grist?.searchText || ''
69
+ },
70
+ filter: {
71
+ handler: () => {
72
+ this.grist.toggleHeadroom()
73
+ }
74
+ },
75
+ help: 'contact/contact',
76
+ actions: [
77
+ {
78
+ icon: 'add',
79
+ title: i18next.t('button.add'),
80
+ action: this.onCreateContact.bind(this)
81
+ },
82
+ {
83
+ icon: 'delete',
84
+ title: i18next.t('button.delete'),
85
+ action: this.onDeleteContact.bind(this),
86
+ emphasis: {
87
+ danger: true
88
+ }
89
+ }
90
+ ],
91
+ exportable: {
92
+ name: i18next.t('title.contact list'),
93
+ data: this.exportHandler.bind(this)
94
+ },
95
+ importable: {
96
+ handler: this.importHandler.bind(this)
97
+ },
98
+ toolbar: false
99
+ }
100
+ }
101
+
102
+ render() {
103
+ const mode = this.mode || (isMobileDevice() ? 'CARD' : 'GRID')
104
+
105
+ return html`
106
+ <ox-grist .mode=${mode} .config=${this.gristConfig} .fetchHandler=${this.fetchHandler.bind(this)}>
107
+ <div slot="headroom" class="header">
108
+ <ox-context-page-toolbar class="actions" .context=${this.context}></ox-context-page-toolbar>
109
+ </div>
110
+ </ox-grist>
111
+ `
112
+ }
113
+
114
+ async pageInitialized(lifecycle: any) {
115
+ this.gristConfig = {
116
+ pagination: { pages: [100, 200, 500] },
117
+ list: {
118
+ thumbnail: 'profile',
119
+ fields: ['name', 'company'],
120
+ details: ['email', 'updatedAt']
121
+ },
122
+ columns: [
123
+ { type: 'gutter', gutterName: 'sequence' },
124
+ { type: 'gutter', gutterName: 'row-selector', multiple: true },
125
+ {
126
+ type: 'image',
127
+ name: 'profile',
128
+ header: i18next.t('button.edit-contact'),
129
+ width: 80,
130
+ record: {
131
+ align: 'center',
132
+ renderer: function (value, column, record, rowIndex, field) {
133
+ return html`<ox-pfp-view .profile=${record.profile} .name=${record.name || '+'}></ox-pfp-view>`
134
+ }
135
+ }
136
+ },
137
+ {
138
+ type: 'string',
139
+ name: 'name',
140
+ header: i18next.t('field.name'),
141
+ record: {
142
+ editable: false
143
+ },
144
+ filter: 'search',
145
+ sortable: true,
146
+ width: 100
147
+ },
148
+ {
149
+ type: 'string',
150
+ name: 'company',
151
+ header: i18next.t('field.company'),
152
+ record: {
153
+ editable: false
154
+ },
155
+ filter: 'search',
156
+ sortable: true,
157
+ width: 135
158
+ },
159
+ {
160
+ type: 'string',
161
+ name: 'department',
162
+ header: i18next.t('field.department'),
163
+ record: {
164
+ editable: false
165
+ },
166
+ sortable: false,
167
+ width: 135
168
+ },
169
+ {
170
+ type: 'string',
171
+ name: 'email',
172
+ header: i18next.t('field.email'),
173
+ record: {
174
+ editable: false
175
+ },
176
+ sortable: false,
177
+ width: 190
178
+ },
179
+ {
180
+ type: 'string',
181
+ name: 'phone',
182
+ header: i18next.t('field.phone'),
183
+ record: {
184
+ editable: false
185
+ },
186
+ sortable: false,
187
+ width: 120
188
+ },
189
+ {
190
+ type: 'string',
191
+ name: 'address',
192
+ header: i18next.t('field.address'),
193
+ record: {
194
+ editable: false
195
+ },
196
+ sortable: false,
197
+ width: 390
198
+ },
199
+ {
200
+ type: 'resource-object',
201
+ name: 'updater',
202
+ header: i18next.t('field.updater'),
203
+ record: {
204
+ editable: false
205
+ },
206
+ sortable: false,
207
+ width: 90
208
+ },
209
+ {
210
+ type: 'datetime',
211
+ name: 'updatedAt',
212
+ header: i18next.t('field.updated_at'),
213
+ record: {
214
+ editable: false
215
+ },
216
+ sortable: true,
217
+ width: 180
218
+ },
219
+ {
220
+ type: 'image',
221
+ name: 'profile',
222
+ record: {
223
+ renderer: function (value, column, record, rowIndex, field) {
224
+ return html`<ox-pfp-view
225
+ style="height:90%; width: unset; aspect-ratio: 1 / 1;"
226
+ .profile=${record.profile}
227
+ .name=${record.name}
228
+ ></ox-pfp-view>`
229
+ }
230
+ },
231
+ hidden: true
232
+ }
233
+ ],
234
+ rows: {
235
+ handlers: {
236
+ click: (columns, data, column, record, rowIndex, target) => {
237
+ this.openContactPopup(record)
238
+ }
239
+ },
240
+ appendable: false,
241
+ selectable: {
242
+ multiple: true
243
+ }
244
+ },
245
+ sorters: [
246
+ {
247
+ name: 'company'
248
+ },
249
+ {
250
+ name: 'name'
251
+ }
252
+ ]
253
+ }
254
+ }
255
+
256
+ async pageUpdated(changes: any, lifecycle: any) {
257
+ if (this.active) {
258
+ // do something here when this page just became as active
259
+ }
260
+ }
261
+
262
+ async fetchHandler({ page = 1, limit = 100, sortings = [], filters = [] }: FetchOption) {
263
+ const response = await client.query({
264
+ query: gql`
265
+ query ($filters: [Filter!], $pagination: Pagination, $sortings: [Sorting!]) {
266
+ responses: contacts(filters: $filters, pagination: $pagination, sortings: $sortings) {
267
+ items {
268
+ id
269
+ name
270
+ company
271
+ email
272
+ phone
273
+ address
274
+ department
275
+ note
276
+ profile {
277
+ left
278
+ top
279
+ zoom
280
+ picture
281
+ }
282
+ updater {
283
+ id
284
+ name
285
+ }
286
+ updatedAt
287
+ }
288
+ total
289
+ }
290
+ }
291
+ `,
292
+ variables: {
293
+ filters,
294
+ pagination: { page, limit },
295
+ sortings
296
+ }
297
+ })
298
+
299
+ return {
300
+ total: response.data.responses.total || 0,
301
+ records: response.data.responses.items || []
302
+ }
303
+ }
304
+
305
+ async onDeleteContact() {
306
+ if (confirm(i18next.t('text.sure_to_x', { x: i18next.t('text.delete') }))) {
307
+ const ids = this.grist.selected.map(record => record.id)
308
+ if (ids && ids.length > 0) {
309
+ const response = await client.mutate({
310
+ mutation: gql`
311
+ mutation ($ids: [String!]!) {
312
+ deleteContacts(ids: $ids)
313
+ }
314
+ `,
315
+ variables: {
316
+ ids
317
+ }
318
+ })
319
+
320
+ if (!response.errors) {
321
+ this.grist.fetch()
322
+ notify({
323
+ message: i18next.t('text.info_x_successfully', { x: i18next.t('text.delete') })
324
+ })
325
+ }
326
+ }
327
+ }
328
+ }
329
+
330
+ async _updateContact() {
331
+ let patches = this.grist.dirtyRecords
332
+ if (patches && patches.length) {
333
+ patches = patches.map(patch => {
334
+ let patchField: any = patch.id ? { id: patch.id } : {}
335
+ const dirtyFields = patch.__dirtyfields__
336
+ for (let key in dirtyFields) {
337
+ patchField[key] = dirtyFields[key].after
338
+ }
339
+ patchField.cuFlag = patch.__dirty__
340
+
341
+ return patchField
342
+ })
343
+
344
+ const response = await client.mutate({
345
+ mutation: gql`
346
+ mutation ($patches: [ContactPatch!]!) {
347
+ updateMultipleContact(patches: $patches) {
348
+ name
349
+ company
350
+ email
351
+ profile {
352
+ left
353
+ top
354
+ zoom
355
+ picture
356
+ }
357
+ updater {
358
+ id
359
+ name
360
+ }
361
+ updatedAt
362
+ }
363
+ }
364
+ `,
365
+ variables: {
366
+ patches
367
+ }
368
+ })
369
+
370
+ if (!response.errors) {
371
+ this.grist.fetch()
372
+ }
373
+ }
374
+ }
375
+
376
+ async exportHandler() {
377
+ const exportTargets = this.grist.selected.length ? this.grist.selected : this.grist.dirtyData.records
378
+ const targetFieldSet = new Set(['id', 'name', 'company', 'email', 'phone', 'address', 'note'])
379
+
380
+ return exportTargets.map(contact => {
381
+ let tempObj = {}
382
+ for (const field of targetFieldSet) {
383
+ tempObj[field] = contact[field]
384
+ }
385
+
386
+ return tempObj
387
+ })
388
+ }
389
+
390
+ async importHandler(records) {
391
+ const popup = openPopup(
392
+ html`
393
+ <contact-importer
394
+ .contacts=${records}
395
+ @imported=${() => {
396
+ history.back()
397
+ this.grist.fetch()
398
+ }}
399
+ ></contact-importer>
400
+ `,
401
+ {
402
+ backdrop: true,
403
+ size: 'large',
404
+ title: i18next.t('title.import contact')
405
+ }
406
+ )
407
+
408
+ popup.onclosed = () => {
409
+ this.grist.fetch()
410
+ }
411
+ }
412
+
413
+ async openContactPopup(record: any) {
414
+ const popup = openPopup(html` <contact-popup .contactId=${record.id}></contact-popup> `, {
415
+ backdrop: true,
416
+ size: 'large',
417
+ title: i18next.t('title.contact')
418
+ })
419
+ popup.onclosed = () => {
420
+ this.grist.fetch()
421
+ }
422
+ }
423
+
424
+ async onCreateContact() {
425
+ const popup = openPopup(html` <contact-popup edit-mode></contact-popup> `, {
426
+ backdrop: true,
427
+ size: 'large',
428
+ title: i18next.t('title.contact')
429
+ })
430
+ popup.onclosed = () => {
431
+ this.grist.fetch()
432
+ }
433
+ }
434
+ }
@@ -0,0 +1,7 @@
1
+ export default function route(page: string) {
2
+ switch (page) {
3
+ case 'contact-list':
4
+ import('./pages/contact-list-page')
5
+ return page
6
+ }
7
+ }
@@ -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
+ }