@operato/attachment 0.2.52

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,547 @@
1
+ // import './attachment-creation-card'
2
+ import '@operato/data-grist'
3
+
4
+ import gql from 'graphql-tag'
5
+ import i18next from 'i18next'
6
+ import { css, html, LitElement, PropertyValues } from 'lit'
7
+ import { customElement, property, query, state } from 'lit/decorators.js'
8
+
9
+ import { DataGrist, QueryFilter } from '@operato/data-grist'
10
+ import {
11
+ ColumnConfig,
12
+ GristData,
13
+ GristRecord,
14
+ PaginationConfig,
15
+ SortersConfig
16
+ } from '@operato/data-grist/dist/src/types'
17
+ import { buildArgs, client } from '@operato/graphql'
18
+ import { ScrollbarStyles, TooltipStyles } from '@operato/styles'
19
+
20
+ const FETCH_ATTACHMENT_LIST_GQL = (listParam: any) => {
21
+ return gql`
22
+ {
23
+ attachments(${buildArgs(listParam)}) {
24
+ items {
25
+ id
26
+ name
27
+ description
28
+ mimetype
29
+ encoding
30
+ category
31
+ fullpath
32
+ path
33
+ }
34
+ total
35
+ }
36
+ }
37
+ `
38
+ }
39
+
40
+ const DELETE_ATTACHMENT_GQL = gql`
41
+ mutation DeleteAttachment($id: String!) {
42
+ deleteAttachment(id: $id)
43
+ }
44
+ `
45
+
46
+ const CREATE_ATTACHMENTS_GQL = gql`
47
+ mutation ($attachments: [NewAttachment!]!) {
48
+ createAttachments(attachments: $attachments) {
49
+ id
50
+ name
51
+ description
52
+ mimetype
53
+ encoding
54
+ category
55
+ path
56
+ createdAt
57
+ updatedAt
58
+ }
59
+ }
60
+ `
61
+
62
+ @customElement('ox-attachment-list')
63
+ export class OxAttachmentList extends LitElement {
64
+ static styles = [
65
+ ScrollbarStyles,
66
+ TooltipStyles,
67
+ css`
68
+ :host {
69
+ display: flex;
70
+
71
+ width: 100%;
72
+
73
+ --grid-record-emphasized-background-color: red;
74
+ --grid-record-emphasized-color: yellow;
75
+ }
76
+
77
+ ox-grist {
78
+ flex: 1;
79
+ overflow-y: auto;
80
+
81
+ --grid-record-emphasized-background-color: red;
82
+ --grid-record-emphasized-color: yellow;
83
+ }
84
+
85
+ #headroom {
86
+ display: flex;
87
+ flex-direction: row;
88
+ align-items: center;
89
+ padding: var(--padding-default) var(--padding-wide);
90
+ border-top: 2px solid rgba(0, 0, 0, 0.2);
91
+ background-color: var(--theme-white-color);
92
+ box-shadow: var(--box-shadow);
93
+ }
94
+
95
+ #filters {
96
+ flex: 1;
97
+ --mdc-icon-size: 20px;
98
+ }
99
+ #mode {
100
+ width: 100px;
101
+ }
102
+ #add {
103
+ width: 50px;
104
+ text-align: right;
105
+ }
106
+
107
+ #modes > * {
108
+ padding: var(--padding-narrow);
109
+ font-size: 1em;
110
+ opacity: 0.5;
111
+ color: var(--primary-text-color);
112
+ cursor: pointer;
113
+ }
114
+
115
+ #modes > mwc-icon[active] {
116
+ border-radius: 9px;
117
+ background-color: rgba(var(--primary-color-rgb), 0.05);
118
+ opacity: 1;
119
+ color: var(--secondary-text-color);
120
+ cursor: default;
121
+ }
122
+ #modes > mwc-icon:hover {
123
+ opacity: 1;
124
+ color: var(--secondary-text-color);
125
+ }
126
+
127
+ #add button {
128
+ background-color: var(--status-success-color);
129
+ border: 0;
130
+ border-radius: 50%;
131
+ padding: 5px;
132
+ width: 36px;
133
+ height: 36px;
134
+ cursor: pointer;
135
+ }
136
+ #add button:hover {
137
+ background-color: var(--focus-background-color);
138
+ box-shadow: var(--box-shadow);
139
+ }
140
+ #add button mwc-icon {
141
+ font-size: 2em;
142
+ color: var(--theme-white-color);
143
+ }
144
+
145
+ #filters {
146
+ position: relative;
147
+ }
148
+ #filters [type='text'] {
149
+ background-color: transparent;
150
+ border: 0;
151
+ border-bottom: var(--border-dark-color);
152
+ padding: var(--padding-narrow) var(--padding-narrow) 7px 25px;
153
+ font-size: var(--fontsize-large);
154
+ }
155
+ #filters [type='text']:focus {
156
+ outline: none;
157
+ }
158
+ #filters mwc-icon {
159
+ position: absolute;
160
+ top: 3px;
161
+ color: var(--secondary-color);
162
+ }
163
+ #filters * {
164
+ margin-right: var(--margin-default);
165
+ }
166
+
167
+ @media only screen and (max-width: 460px) {
168
+ #modes {
169
+ display: none;
170
+ }
171
+ }
172
+ `
173
+ ]
174
+
175
+ @property({ type: Array }) categories: any[] = ['audio', 'video', 'image', 'text', 'application']
176
+ @property({ type: String }) mode: 'GRID' | 'LIST' | 'CARD' = 'CARD'
177
+ @property({ type: String }) category: string = ''
178
+ @property({ type: Boolean }) creatable: boolean = false
179
+
180
+ @state() private _total: number = 0
181
+ @state() private _page: number = 1
182
+
183
+ @query('ox-grist') private grist!: DataGrist
184
+
185
+ render() {
186
+ const mode = this.mode || 'CARD'
187
+
188
+ return html`
189
+ <ox-grist .config=${this.gristConfig} .mode=${mode} auto-fetch .fetchHandler=${this.fetchHandler.bind(this)}>
190
+ <div slot="headroom" id="headroom">
191
+ <div id="filters">
192
+ <mwc-icon>search</mwc-icon>
193
+ <input type="text" />
194
+
195
+ <select
196
+ @change=${(e: Event) => {
197
+ this.category = (e.currentTarget as HTMLInputElement)?.value
198
+ this.requestUpdate()
199
+ }}
200
+ >
201
+ <option value="">--${i18next.t('text.please choose a category')}--</option>
202
+ ${this.categories.map(
203
+ category =>
204
+ html` <option value=${category} ?selected=${this.category == category}>${category}</option> `
205
+ )}
206
+ </select>
207
+ </div>
208
+
209
+ <div id="modes">
210
+ <mwc-icon @click=${() => (this.mode = 'GRID')} ?active=${mode == 'GRID'}>grid_on</mwc-icon>
211
+ <mwc-icon @click=${() => (this.mode = 'LIST')} ?active=${mode == 'LIST'}>format_list_bulleted</mwc-icon>
212
+ <mwc-icon @click=${() => (this.mode = 'CARD')} ?active=${mode == 'CARD'}>apps</mwc-icon>
213
+ </div>
214
+
215
+ <!-- ${this.creatable
216
+ ? html`
217
+ <attachment-creation-card
218
+ class="card create"
219
+ .categories=${this.categories}
220
+ .defaultCategory=${this.category}
221
+ @create-attachment=${(e: CustomEvent) => this.onCreateAttachment(e)}
222
+ @file-drop=${(e: CustomEvent) => this.onAttachmentDropped(e)}
223
+ ></attachment-creation-card>
224
+ `
225
+ : undefined} -->
226
+ </div>
227
+ </ox-grist>
228
+ `
229
+ }
230
+
231
+ async fetchHandler({ page, limit, sorters = [] }: { page: number; limit: number; sorters: SortersConfig }) {
232
+ const { items: records, total } = await this.getAttachments({ page, limit, sorters })
233
+
234
+ return {
235
+ total,
236
+ records
237
+ }
238
+ }
239
+
240
+ get gristConfig() {
241
+ return {
242
+ list: {
243
+ thumbnail: function (record: GristRecord, rowIndex: number) {
244
+ return record.category == 'image'
245
+ ? html` <img src=${record.fullpath} style="width: 100%; height: 100%;" /> `
246
+ : record.category == 'video'
247
+ ? html` <video src=${record.fullpath} style="width: 100%; height: 100%;" controls></video> `
248
+ : html`
249
+ <div style="width: 100%; height: 100%;" etc>
250
+ <mwc-icon outlined>insert_drive_file</mwc-icon>
251
+ <span>${record.path.substr(record.path.lastIndexOf('.'))}</span>
252
+ </div>
253
+ `
254
+ },
255
+ fields: ['name'],
256
+ details: ['updatedAt']
257
+ },
258
+ columns: [
259
+ {
260
+ type: 'gutter',
261
+ gutterName: 'dirty'
262
+ },
263
+ {
264
+ type: 'gutter',
265
+ gutterName: 'sequence'
266
+ },
267
+ {
268
+ type: 'gutter',
269
+ gutterName: 'button',
270
+ icon: 'open_in_new',
271
+ handlers: {
272
+ click: (
273
+ columns: ColumnConfig[],
274
+ data: GristData,
275
+ column: ColumnConfig,
276
+ record: GristRecord,
277
+ rowIndex: number
278
+ ): void => {
279
+ window.open(record.fullpath, '_blank')
280
+ }
281
+ }
282
+ },
283
+ {
284
+ type: 'gutter',
285
+ gutterName: 'button',
286
+ icon: 'link',
287
+ handlers: {
288
+ click: async (
289
+ columns: ColumnConfig[],
290
+ data: GristData,
291
+ column: ColumnConfig,
292
+ record: GristRecord,
293
+ rowIndex: number
294
+ ): Promise<void> => {
295
+ var { protocol, hostname, port } = location
296
+ await navigator.clipboard.writeText(`${protocol}//${hostname}:${port}${record.fullpath}`)
297
+
298
+ // TODO how to access clicked target
299
+ // this.setAttribute('data-tooltip', 'url copied!')
300
+ // await sleep(2000)
301
+ // this.removeAttribute('data-tooltip')
302
+ }
303
+ }
304
+ },
305
+ {
306
+ type: 'gutter',
307
+ gutterName: 'button',
308
+ icon: 'delete',
309
+ handlers: {
310
+ click: async (
311
+ columns: ColumnConfig[],
312
+ data: GristData,
313
+ column: ColumnConfig,
314
+ record: GristRecord,
315
+ rowIndex: number
316
+ ): Promise<void> => {
317
+ await this.deleteAttachment(record.id!)
318
+ this.refreshAttachments()
319
+ }
320
+ }
321
+ },
322
+ {
323
+ type: 'gutter',
324
+ gutterName: 'button',
325
+ icon: 'save_alt',
326
+ handlers: {
327
+ click: (
328
+ columns: ColumnConfig[],
329
+ data: GristData,
330
+ column: ColumnConfig,
331
+ record: GristRecord,
332
+ rowIndex: number
333
+ ): void => {
334
+ const element = document.createElement('a')
335
+ element.setAttribute('href', record.fullpath)
336
+ element.setAttribute('download', record.name!)
337
+ document.body.appendChild(element)
338
+ element.click()
339
+ }
340
+ }
341
+ },
342
+ {
343
+ type: 'string',
344
+ name: 'id',
345
+ hidden: true
346
+ },
347
+ {
348
+ type: 'string',
349
+ name: 'name',
350
+ header: 'name',
351
+ record: {
352
+ editable: true,
353
+ align: 'left'
354
+ },
355
+ width: 200,
356
+ sortable: true
357
+ },
358
+ {
359
+ type: 'string',
360
+ name: 'description',
361
+ header: 'description',
362
+ record: {
363
+ editable: true,
364
+ align: 'left'
365
+ },
366
+ width: 200,
367
+ handlers: {
368
+ dblclick: (
369
+ columns: ColumnConfig[],
370
+ data: GristData,
371
+ column: ColumnConfig,
372
+ record: GristRecord,
373
+ rowIndex: number
374
+ ): void => {
375
+ alert(`${column.name} ${record[column.name]}, row : ${rowIndex}`)
376
+ }
377
+ }
378
+ },
379
+ {
380
+ type: 'datetime',
381
+ name: 'updatedAt',
382
+ header: 'updated at',
383
+ record: {
384
+ editable: true
385
+ },
386
+ sortable: true,
387
+ width: 180
388
+ },
389
+ {
390
+ type: 'datetime',
391
+ name: 'createdAt',
392
+ header: 'created at',
393
+ record: {
394
+ editable: true
395
+ },
396
+ sortable: true,
397
+ width: 180
398
+ }
399
+ ],
400
+ rows: {
401
+ selectable: {
402
+ multiple: true
403
+ },
404
+ handlers: {},
405
+ classifier: function (
406
+ record: GristRecord,
407
+ rowIndex: number
408
+ ): { emphasized?: boolean | string | string[]; [key: string]: any } | void {}
409
+ },
410
+ sorters: [
411
+ {
412
+ name: 'name',
413
+ desc: false
414
+ }
415
+ ],
416
+ pagination: {
417
+ pages: [20, 30, 50, 100, 200]
418
+ }
419
+ }
420
+ }
421
+
422
+ async firstUpdated() {
423
+ this.refreshAttachments()
424
+ }
425
+
426
+ updated(changes: PropertyValues<this>) {
427
+ if (changes.has('category')) {
428
+ this.refreshAttachments()
429
+ }
430
+ }
431
+
432
+ onClickSelect(attachment: any) {
433
+ this.dispatchEvent(
434
+ new CustomEvent('attachment-selected', {
435
+ composed: true,
436
+ bubbles: true,
437
+ detail: {
438
+ attachment
439
+ }
440
+ })
441
+ )
442
+ }
443
+
444
+ async onAttachmentDropped(e: CustomEvent) {
445
+ var files = e.detail
446
+
447
+ await this.createAttachments('', files)
448
+ this.refreshAttachments()
449
+ }
450
+
451
+ async onCreateAttachment(e: CustomEvent) {
452
+ var { category, files } = e.detail
453
+
454
+ files = Array.from(files)
455
+
456
+ await this.createAttachments(category, files)
457
+ this.refreshAttachments()
458
+ }
459
+
460
+ async onDeleteAttachment(id: string) {
461
+ await this.deleteAttachment(id)
462
+
463
+ this.refreshAttachments()
464
+ }
465
+
466
+ async refreshAttachments() {
467
+ this.grist.fetch()
468
+ // var attachments = await this.getAttachments()
469
+ // this.attachments = [...attachments]
470
+
471
+ // var creationCard = this.shadowRoot.querySelector('attachment-creation-card')
472
+ // if (creationCard) {
473
+ // creationCard.reset()
474
+ // }
475
+ }
476
+
477
+ async appendAttachments() {
478
+ // var attachments = await this.getAttachments({ page: this._page + 1 })
479
+ // this.attachments = [...this.attachments, ...attachments]
480
+ }
481
+
482
+ async getAttachments({
483
+ page = 1,
484
+ limit = 30,
485
+ sorters = []
486
+ }: { page?: number; limit?: number; sorters?: SortersConfig } = {}) {
487
+ var filters: QueryFilter[] = []
488
+ var sortings: SortersConfig = []
489
+ var pagination: PaginationConfig = {
490
+ limit,
491
+ page
492
+ }
493
+
494
+ if (this.category)
495
+ filters.push({
496
+ name: 'category',
497
+ operator: 'eq',
498
+ value: this.category
499
+ })
500
+
501
+ var params = {
502
+ filters,
503
+ sortings,
504
+ pagination
505
+ }
506
+
507
+ var attachmentListResponse = await client.query({
508
+ query: FETCH_ATTACHMENT_LIST_GQL(params)
509
+ })
510
+
511
+ if (!attachmentListResponse || !attachmentListResponse.data) {
512
+ return {}
513
+ }
514
+
515
+ return attachmentListResponse?.data?.attachments || {}
516
+ }
517
+
518
+ async createAttachments(category: string, files: File[]) {
519
+ /*
520
+ ref. https://github.com/jaydenseric/graphql-multipart-request-spec#client
521
+ - TODO support multiple file upload
522
+ */
523
+
524
+ await client.mutate({
525
+ mutation: CREATE_ATTACHMENTS_GQL,
526
+ variables: {
527
+ attachments: files.map(file => {
528
+ return { category, file }
529
+ })
530
+ },
531
+ context: {
532
+ hasUpload: true
533
+ }
534
+ })
535
+ }
536
+
537
+ async deleteAttachment(id: string) {
538
+ const response = await client.mutate({
539
+ mutation: DELETE_ATTACHMENT_GQL,
540
+ variables: {
541
+ id
542
+ }
543
+ })
544
+
545
+ return response.data
546
+ }
547
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "es2018",
4
+ "module": "esnext",
5
+ "moduleResolution": "node",
6
+ "noEmitOnError": true,
7
+ "lib": ["es2017", "dom"],
8
+ "strict": true,
9
+ "esModuleInterop": false,
10
+ "allowSyntheticDefaultImports": true,
11
+ "experimentalDecorators": true,
12
+ "importHelpers": true,
13
+ "outDir": "dist",
14
+ "sourceMap": true,
15
+ "inlineSources": true,
16
+ "rootDir": "./",
17
+ "declaration": true,
18
+ "incremental": true,
19
+ "types": ["node", "mocha"]
20
+ },
21
+ "include": ["**/*.ts"]
22
+ }
@@ -0,0 +1,27 @@
1
+ // import { hmrPlugin, presets } from '@open-wc/dev-server-hmr';
2
+
3
+ /** Use Hot Module replacement by adding --hmr to the start command */
4
+ const hmr = process.argv.includes('--hmr');
5
+
6
+ export default /** @type {import('@web/dev-server').DevServerConfig} */ ({
7
+ open: '/demo/',
8
+ /** Use regular watch mode if HMR is not enabled. */
9
+ watch: !hmr,
10
+ /** Resolve bare module imports */
11
+ nodeResolve: {
12
+ exportConditions: ['browser', 'development'],
13
+ },
14
+
15
+ /** Compile JS for older browsers. Requires @web/dev-server-esbuild plugin */
16
+ // esbuildTarget: 'auto'
17
+
18
+ /** Set appIndex to enable SPA routing */
19
+ // appIndex: 'demo/index.html',
20
+
21
+ plugins: [
22
+ /** Use Hot Module Replacement by uncommenting. Requires @open-wc/dev-server-hmr plugin */
23
+ // hmr && hmrPlugin({ exclude: ['**/*/node_modules/**/*'], presets: [presets.litElement] }),
24
+ ],
25
+
26
+ // See documentation for all available options
27
+ });
@@ -0,0 +1,41 @@
1
+ // import { playwrightLauncher } from '@web/test-runner-playwright';
2
+
3
+ const filteredLogs = ['Running in dev mode', 'lit-html is in dev mode'];
4
+
5
+ export default /** @type {import("@web/test-runner").TestRunnerConfig} */ ({
6
+ /** Test files to run */
7
+ files: 'dist/test/**/*.test.js',
8
+
9
+ /** Resolve bare module imports */
10
+ nodeResolve: {
11
+ exportConditions: ['browser', 'development'],
12
+ },
13
+
14
+ /** Filter out lit dev mode logs */
15
+ filterBrowserLogs(log) {
16
+ for (const arg of log.args) {
17
+ if (typeof arg === 'string' && filteredLogs.some(l => arg.includes(l))) {
18
+ return false;
19
+ }
20
+ }
21
+ return true;
22
+ },
23
+
24
+ /** Compile JS for older browsers. Requires @web/dev-server-esbuild plugin */
25
+ // esbuildTarget: 'auto',
26
+
27
+ /** Amount of browsers to run concurrently */
28
+ // concurrentBrowsers: 2,
29
+
30
+ /** Amount of test files per browser to test concurrently */
31
+ // concurrency: 1,
32
+
33
+ /** Browsers to run tests on */
34
+ // browsers: [
35
+ // playwrightLauncher({ product: 'chromium' }),
36
+ // playwrightLauncher({ product: 'firefox' }),
37
+ // playwrightLauncher({ product: 'webkit' }),
38
+ // ],
39
+
40
+ // See documentation for all available options
41
+ });