@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 @@
1
+ export default function bootstrap() {}
@@ -0,0 +1,220 @@
1
+ import '@material/web/icon/icon.js'
2
+ import '@operato/contact/ox-contact-view.js'
3
+ import '@operato/contact/ox-contact-edit.js'
4
+
5
+ import { html, css, LitElement, PropertyValues } from 'lit'
6
+ import { customElement, property, query, state } from 'lit/decorators.js'
7
+ import gql from 'graphql-tag'
8
+
9
+ import { client } from '@operato/graphql'
10
+ import { Contact } from '@operato/contact/ox-contact.js'
11
+ import { i18next } from '@operato/i18n'
12
+ import { CommonHeaderStyles, ScrollbarStyles } from '@operato/styles'
13
+ import { OxContactEdit } from '@operato/contact/ox-contact-edit.js'
14
+
15
+ @customElement('contact-popup')
16
+ export class ContactPopup extends LitElement {
17
+ static styles = [
18
+ ScrollbarStyles,
19
+ CommonHeaderStyles,
20
+ css`
21
+ :host {
22
+ display: flex;
23
+ flex-direction: column;
24
+ color: var(--popup-content-color);
25
+ background-color: var(--popup-content-background-color);
26
+ }
27
+
28
+ [content] {
29
+ flex: 1;
30
+ padding: var(--spacing-medium);
31
+ overflow: auto;
32
+ }
33
+ `
34
+ ]
35
+
36
+ @property({ type: String }) contactId?: string
37
+ @property({ type: Boolean, attribute: 'edit-mode' }) editMode?: boolean = false
38
+ @property({ type: Boolean }) detachable?: boolean = false
39
+ @property({ type: Object }) closeCallback?: (contact?: Contact | null) => void
40
+
41
+ @state() contact: Contact = {}
42
+
43
+ @query('ox-contact-edit') editor!: OxContactEdit
44
+
45
+ render() {
46
+ return html`
47
+ <div content>
48
+ ${this.editMode
49
+ ? html`<ox-contact-edit
50
+ .contact=${this.contact}
51
+ @edit-cancel=${() => (this.editMode = false)}
52
+ @edit-done=${(e: CustomEvent) => {
53
+ this.updateContact(e.detail)
54
+ this.editMode = false
55
+ }}
56
+ ></ox-contact-edit>`
57
+ : html` <ox-contact-view .contact=${this.contact}></ox-contact-view>`}
58
+ </div>
59
+
60
+ <div class="footer">
61
+ ${!this.editMode
62
+ ? html`
63
+ ${this.detachable
64
+ ? html`
65
+ <button
66
+ danger
67
+ @click=${() => {
68
+ this.dispatchEvent(new CustomEvent('detach'))
69
+ history.back()
70
+ }}
71
+ >
72
+ <md-icon>link_off</md-icon>
73
+ ${i18next.t('button.detach')}
74
+ </button>
75
+ `
76
+ : html``}
77
+ <div filler></div>
78
+ <button @click=${() => (this.editMode = true)}><md-icon>edit</md-icon>${i18next.t('button.edit')}</button>
79
+ <button @click=${this.onClose} ok><md-icon>close</md-icon>${i18next.t('button.close')}</button>
80
+ `
81
+ : html`
82
+ <button @click=${() => this.editor.reset()}>
83
+ <md-icon>restart_alt</md-icon>${i18next.t('button.reset')}
84
+ </button>
85
+ <div filler></div>
86
+ <button @click=${() => this.editor.cancel()}>
87
+ <md-icon>cancel</md-icon>${i18next.t('button.cancel')}
88
+ </button>
89
+ <button @click=${() => this.editor.save()} ok><md-icon>save</md-icon>${i18next.t('button.save')}</button>
90
+ `}
91
+ </div>
92
+ `
93
+ }
94
+
95
+ onClose() {
96
+ this.closeCallback && this.closeCallback(this.contact?.id ? this.contact : null)
97
+
98
+ history.back()
99
+ }
100
+
101
+ updated(changes: PropertyValues<this>) {
102
+ if (changes.has('contactId') && this.contactId) {
103
+ this.fetchContact()
104
+ }
105
+ }
106
+
107
+ async fetchContact() {
108
+ const response = await client.query({
109
+ query: gql`
110
+ query ($id: String!) {
111
+ contact(id: $id) {
112
+ id
113
+ name
114
+ company
115
+ department
116
+ email
117
+ phone
118
+ address
119
+ note
120
+ items {
121
+ type
122
+ label
123
+ value
124
+ }
125
+ profile {
126
+ left
127
+ top
128
+ zoom
129
+ picture
130
+ }
131
+ }
132
+ }
133
+ `,
134
+ variables: {
135
+ id: this.contactId
136
+ }
137
+ })
138
+
139
+ this.contact = response.data.contact
140
+ }
141
+
142
+ async updateContact(contact: Contact) {
143
+ if (contact.id) {
144
+ var { id, ...patch } = contact
145
+
146
+ var response = await client.mutate({
147
+ mutation: gql`
148
+ mutation ($id: String!, $patch: ContactPatch!) {
149
+ contact: updateContact(id: $id, patch: $patch) {
150
+ id
151
+ name
152
+ company
153
+ email
154
+ phone
155
+ note
156
+ items {
157
+ label
158
+ type
159
+ value
160
+ }
161
+ profile {
162
+ left
163
+ top
164
+ zoom
165
+ picture
166
+ }
167
+ }
168
+ }
169
+ `,
170
+ variables: {
171
+ id,
172
+ patch
173
+ },
174
+ context: {
175
+ hasUpload: true
176
+ }
177
+ })
178
+ } else {
179
+ var response = await client.mutate({
180
+ mutation: gql`
181
+ mutation ($contact: NewContact!) {
182
+ contact: createContact(contact: $contact) {
183
+ id
184
+ name
185
+ company
186
+ email
187
+ phone
188
+ note
189
+ items {
190
+ label
191
+ type
192
+ value
193
+ }
194
+ profile {
195
+ left
196
+ top
197
+ zoom
198
+ picture
199
+ }
200
+ }
201
+ }
202
+ `,
203
+ variables: {
204
+ contact
205
+ },
206
+ context: {
207
+ hasUpload: true
208
+ }
209
+ })
210
+ }
211
+
212
+ this.contact = response.data.contact
213
+
214
+ this.dispatchEvent(
215
+ new CustomEvent('change', {
216
+ detail: this.contact
217
+ })
218
+ )
219
+ }
220
+ }
@@ -0,0 +1,330 @@
1
+ import '@material/web/icon/icon.js'
2
+ import '@operato/data-grist'
3
+ import './contact-popup'
4
+
5
+ import gql from 'graphql-tag'
6
+ import { css, html, LitElement } from 'lit'
7
+ import { customElement, property, query } from 'lit/decorators.js'
8
+
9
+ import { DataGrist, FetchHandler, GristData, GristEventHandler, GristRecord, ZERO_DATA } from '@operato/data-grist'
10
+ import { client } from '@operato/graphql'
11
+ import { i18next } from '@operato/i18n'
12
+ import { closePopup } from '@operato/popup'
13
+ import { openPopup } from '@operato/layout'
14
+ import { isMobileDevice } from '@operato/utils'
15
+ import { CommonGristStyles, CommonHeaderStyles, ScrollbarStyles } from '@operato/styles'
16
+
17
+ @customElement('contact-selector')
18
+ export class ContactSelector extends LitElement {
19
+ static styles = [
20
+ ScrollbarStyles,
21
+ CommonGristStyles,
22
+ CommonHeaderStyles,
23
+ css`
24
+ :host {
25
+ display: flex;
26
+ flex-direction: column;
27
+
28
+ background-color: var(--md-sys-color-surface);
29
+
30
+ width: var(--overlay-center-normal-width, 50%);
31
+ height: var(--overlay-center-normal-height, 50%);
32
+ }
33
+
34
+ ox-grist {
35
+ flex: 1;
36
+ }
37
+
38
+ .header {
39
+ grid-template-areas: 'filters actions';
40
+ }
41
+ `
42
+ ]
43
+
44
+ @property({ type: String }) value?: string
45
+ @property({ type: Object }) config: any
46
+ @property({ type: Object }) data: GristData = ZERO_DATA
47
+ @property({ type: Object }) confirmCallback?: (record?: GristRecord | null) => void
48
+ @property({ type: Array }) selectedRecords: GristRecord[] = []
49
+
50
+ @property({ type: Object }) list: any
51
+
52
+ @query('ox-grist') grist!: DataGrist
53
+
54
+ render() {
55
+ return html`
56
+ <ox-grist
57
+ .mode=${isMobileDevice() ? 'LIST' : 'GRID'}
58
+ .config=${this.config}
59
+ .data=${this.data}
60
+ .fetchHandler=${this.fetchHandler.bind(this)}
61
+ .selectedRecords=${this.selectedRecords}
62
+ >
63
+ <div class="header" slot="headroom">
64
+ <div class="filters">
65
+ <ox-filters-form autofocus without-search></ox-filters-form>
66
+ </div>
67
+
68
+ <div class="actions">
69
+ <button @click=${this.onCreate.bind(this)} class="iconbtn">
70
+ <md-icon>add</md-icon>${i18next.t('button.new-contact')}
71
+ </button>
72
+ </div>
73
+ </div>
74
+ </ox-grist>
75
+
76
+ <div class="footer">
77
+ <button @click=${this.onEmpty.bind(this)}>
78
+ <md-icon>check_box_outline_blank</md-icon>${i18next.t('button.empty')}
79
+ </button>
80
+
81
+ <div filler></div>
82
+
83
+ <button @click=${this.onCancel.bind(this)}><md-icon>cancel</md-icon>${i18next.t('button.cancel')}</button>
84
+ <button @click=${this.onConfirm.bind(this)} ok><md-icon>done</md-icon>${i18next.t('button.confirm')}</button>
85
+ </div>
86
+ `
87
+ }
88
+
89
+ onEmpty() {
90
+ this.confirmCallback && this.confirmCallback()
91
+ closePopup(this)
92
+ }
93
+
94
+ onCancel() {
95
+ closePopup(this)
96
+ }
97
+
98
+ onConfirm() {
99
+ this.confirmCallback && this.confirmCallback(this.selected)
100
+ closePopup(this)
101
+ }
102
+
103
+ onCreate() {
104
+ var createdContact
105
+ const popup = openPopup(
106
+ html`
107
+ <contact-popup
108
+ edit-mode
109
+ .closeCallback=${contact => {
110
+ createdContact = contact
111
+ }}
112
+ ></contact-popup>
113
+ `,
114
+ {
115
+ backdrop: true,
116
+ size: 'large',
117
+ title: i18next.t('title.contact')
118
+ }
119
+ )
120
+
121
+ popup.onclosed = () => {
122
+ if (createdContact) {
123
+ this.confirmCallback && this.confirmCallback(createdContact)
124
+ closePopup(this)
125
+ } else {
126
+ this.grist.fetch()
127
+ }
128
+ }
129
+ }
130
+
131
+ fetchHandler: FetchHandler = async ({ page = 1, limit = 100, sortings = [], filters = [] }) => {
132
+ const response = await client.query({
133
+ query: gql`
134
+ query ($filters: [Filter!], $pagination: Pagination, $sortings: [Sorting!]) {
135
+ fetch: contacts(filters: $filters, pagination: $pagination, sortings: $sortings) {
136
+ items {
137
+ id
138
+ name
139
+ company
140
+ email
141
+ phone
142
+ address
143
+ department
144
+ note
145
+ profile {
146
+ left
147
+ top
148
+ zoom
149
+ picture
150
+ }
151
+ updater {
152
+ id
153
+ name
154
+ }
155
+ updatedAt
156
+ }
157
+ total
158
+ }
159
+ }
160
+ `,
161
+ variables: {
162
+ filters,
163
+ pagination: { page, limit },
164
+ sortings
165
+ }
166
+ })
167
+
168
+ if (!response.errors) {
169
+ const records = response.data.fetch.items.map((item: any) => {
170
+ if (this.value && this.value === item.id) {
171
+ this.selectedRecords = [item]
172
+ item['__selected__'] = true
173
+ }
174
+
175
+ return item
176
+ })
177
+ const total = response.data.fetch.total
178
+
179
+ return {
180
+ records,
181
+ total,
182
+ limit,
183
+ page
184
+ }
185
+ }
186
+ }
187
+
188
+ async firstUpdated() {
189
+ this.config = {
190
+ pagination: { pages: [100, 200, 500] },
191
+ list: {
192
+ thumbnail: 'profile',
193
+ fields: ['name', 'company'],
194
+ details: ['email', 'updatedAt']
195
+ },
196
+ columns: [
197
+ { type: 'gutter', gutterName: 'sequence' },
198
+ { type: 'gutter', gutterName: 'row-selector', multiple: false },
199
+ {
200
+ type: 'string',
201
+ name: 'name',
202
+ header: i18next.t('field.name'),
203
+ record: {
204
+ editable: false
205
+ },
206
+ filter: 'search',
207
+ sortable: true,
208
+ width: 100
209
+ },
210
+ {
211
+ type: 'string',
212
+ name: 'company',
213
+ header: i18next.t('field.company'),
214
+ record: {
215
+ editable: false
216
+ },
217
+ filter: 'search',
218
+ sortable: true,
219
+ width: 135
220
+ },
221
+ {
222
+ type: 'string',
223
+ name: 'department',
224
+ header: i18next.t('field.department'),
225
+ record: {
226
+ editable: false
227
+ },
228
+ // filter: 'search',
229
+ sortable: true,
230
+ width: 135
231
+ },
232
+ {
233
+ type: 'string',
234
+ name: 'email',
235
+ header: i18next.t('field.email'),
236
+ record: {
237
+ editable: false
238
+ },
239
+ // filter: 'search',
240
+ sortable: true,
241
+ width: 190
242
+ },
243
+ {
244
+ type: 'string',
245
+ name: 'phone',
246
+ header: i18next.t('field.phone'),
247
+ record: {
248
+ editable: false
249
+ },
250
+ // filter: 'search',
251
+ sortable: true,
252
+ width: 120
253
+ },
254
+ {
255
+ type: 'string',
256
+ name: 'address',
257
+ header: i18next.t('field.address'),
258
+ record: {
259
+ editable: false
260
+ },
261
+ // filter: 'search',
262
+ sortable: true,
263
+ width: 375
264
+ },
265
+ /*{
266
+ type: 'resource-object',
267
+ name: 'updater',
268
+ header: i18next.t('field.updater'),
269
+ record: {
270
+ editable: false
271
+ },
272
+ sortable: true,
273
+ width: 90
274
+ },
275
+ {
276
+ type: 'datetime',
277
+ name: 'updatedAt',
278
+ header: i18next.t('field.updated_at'),
279
+ record: {
280
+ editable: false
281
+ },
282
+ sortable: true,
283
+ width: 180
284
+ },*/
285
+ {
286
+ type: 'image',
287
+ name: 'profile',
288
+ record: {
289
+ renderer: function (value, column, record, rowIndex, field) {
290
+ return html`<ox-pfp-view
291
+ style="height:90%; width: unset; aspect-ratio: 1 / 1;"
292
+ .profile=${record.profile}
293
+ .name=${record.name}
294
+ ></ox-pfp-view>`
295
+ }
296
+ },
297
+ hidden: true
298
+ }
299
+ ],
300
+ rows: {
301
+ selectable: {
302
+ multiple: false
303
+ },
304
+ handlers: {
305
+ click: 'select-row',
306
+ dblclick: ((columns, data, column, record, rowIndex, field) => {
307
+ this.onConfirm()
308
+ }) as GristEventHandler
309
+ },
310
+ appendable: false
311
+ },
312
+ sorters: [
313
+ {
314
+ name: 'name'
315
+ },
316
+ {
317
+ name: 'company'
318
+ }
319
+ ]
320
+ }
321
+ }
322
+
323
+ get selected() {
324
+ var grist = this.renderRoot.querySelector('ox-grist') as DataGrist
325
+
326
+ var selected = grist.selected
327
+
328
+ return selected && selected.length > 0 ? selected[0] : undefined
329
+ }
330
+ }
@@ -0,0 +1,2 @@
1
+ export * from './components/contact-popup'
2
+ export * from './components/contact-selector'
@@ -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 ContactImporter 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 }) contacts: 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.contacts
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 importContacts($contacts: [ContactPatch!]!) {
77
+ importContacts(contacts: $contacts)
78
+ }
79
+ `,
80
+ variables: { contacts: this.contacts }
81
+ })
82
+
83
+ if (response.errors?.length) return
84
+
85
+ this.dispatchEvent(new CustomEvent('imported'))
86
+ }
87
+ }