@operato/data-grist 2.0.0-alpha.87 → 2.0.0-alpha.88

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,553 @@
1
+ import '../src/index.js'
2
+ import '../src/filters/filters-form.js'
3
+ import '../src/sorters/sorters-control.js'
4
+ import '../src/record-view/record-creator.js'
5
+ import '@operato/popup/ox-popup-list.js'
6
+ import '@material/web/icon/icon.js'
7
+
8
+ import { html, TemplateResult } from 'lit'
9
+
10
+ import {
11
+ ColumnConfig,
12
+ FetchHandler,
13
+ GristClassifier,
14
+ GristEventHandlerSet,
15
+ GristRecord,
16
+ ValidationCallback
17
+ } from '../src/types.js'
18
+
19
+ const fetchHandler: FetchHandler = async ({ page, limit }) => {
20
+ var total = 120993
21
+ var start = (page! - 1) * limit!
22
+
23
+ await new Promise(resolve => setTimeout(resolve, 500))
24
+
25
+ return {
26
+ total,
27
+ records: Array(limit! * page! > total ? total % limit! : limit)
28
+ .fill('')
29
+ .map((item, idx) => {
30
+ return {
31
+ id: String(idx),
32
+ name: idx % 2 ? `shnam-${start + idx + 1}` : `heartyoh-${start + idx + 1}`,
33
+ description: idx % 2 ? `hatiolabmanager${start + idx + 1}1234567890` : `hatiosea manager-${start + idx + 1}`,
34
+ email: idx % 2 ? `shnam-${start + idx + 1}@gmail.com` : `heartyoh-${start + idx + 1}@gmail.com`,
35
+ active: Math.round(Math.random() * 2) % 2 ? true : false,
36
+ barcode: idx % 2 ? `1234567890${start + idx + 1}` : `0987654321${start + idx + 1}`,
37
+ company:
38
+ idx % 2
39
+ ? {
40
+ id: '2',
41
+ name: 'HatioLAB',
42
+ description: `경기도 성남시-${start + idx + 1}`
43
+ }
44
+ : {
45
+ id: '3',
46
+ name: 'HatioSEA',
47
+ description: `말레이시아 세티아알람-${start + idx + 1}`
48
+ },
49
+ thumbnail:
50
+ idx % 4 === 0
51
+ ? '' /* no source */
52
+ : idx % 4 === 1
53
+ ? `http://www.hatiolab.com/assets/img/operato-biz3.png`
54
+ : idx % 4 === 2
55
+ ? `http://www.hatiolab.com/assets/img/thingsboard-30.png`
56
+ : `http://www.hatiolab.com/wrong-url.png` /* wrong source */,
57
+ role: ['admin', 'worker', 'tester'][idx % 3],
58
+ color: idx % 2 ? `#87f018` : `#180f87`,
59
+ rate: Math.round(Math.random() * 100),
60
+ dynamicType: ['text', 'email', 'checkbox', 'color', 'progress', 'barcode'][idx % 5],
61
+ dynamicValue: ['abcdefghijkl', 'heartyoh@hatiolab.com', 'true', 'orange', '50', '1234567890'][idx % 5],
62
+ homepage:
63
+ idx % 2 ? `http://hatiolab.com/${start + idx + 1}` : `http://deadpool.hatiolab.com/${start + idx + 1}`,
64
+ json5: {
65
+ abc: 'abc',
66
+ value: 123
67
+ },
68
+ createdAt: Date.now(),
69
+ updatedAt: Date.now()
70
+ }
71
+ })
72
+ }
73
+ }
74
+
75
+ const config = {
76
+ list: {
77
+ thumbnail: 'thumbnail',
78
+ fields: ['name', 'description'],
79
+ details: ['role', 'email']
80
+ },
81
+ columns: [
82
+ {
83
+ type: 'gutter',
84
+ gutterName: 'dirty'
85
+ },
86
+ {
87
+ type: 'gutter',
88
+ gutterName: 'sequence'
89
+ },
90
+ {
91
+ type: 'gutter',
92
+ gutterName: 'row-selector',
93
+ multiple: true
94
+ },
95
+ {
96
+ type: 'gutter',
97
+ gutterName: 'button',
98
+ icon: 'edit',
99
+ title: 'edit',
100
+ handlers: {
101
+ click: function () {
102
+ console.log('clicked')
103
+ }
104
+ }
105
+ },
106
+ {
107
+ type: 'gutter',
108
+ gutterName: 'button',
109
+ icon: 'add',
110
+ title: 'add',
111
+ handlers: {
112
+ click: 'record-copy'
113
+ }
114
+ },
115
+ {
116
+ type: 'gutter',
117
+ gutterName: 'button',
118
+ icon: 'arrow_downward',
119
+ title: 'download',
120
+ handlers: {
121
+ click: 'move-down'
122
+ }
123
+ },
124
+ {
125
+ type: 'string',
126
+ name: 'id',
127
+ hidden: true
128
+ },
129
+ {
130
+ type: 'link',
131
+ name: 'name',
132
+ label: true,
133
+ fixed: true,
134
+ header: 'name',
135
+ record: {
136
+ editable: true,
137
+ options: {
138
+ // href: 'http://hatiolab.com',
139
+ href: function (column: ColumnConfig, record: GristRecord, rowIndex: number) {
140
+ return record['homepage']
141
+ },
142
+ target: '_blank'
143
+ }
144
+ },
145
+ filter: 'search',
146
+ sortable: true,
147
+ width: 120
148
+ },
149
+ {
150
+ type: 'string',
151
+ name: 'description',
152
+ header: 'description',
153
+ filter: 'search',
154
+ record: {
155
+ editable: true,
156
+ align: 'left'
157
+ },
158
+ width: 200,
159
+ handlers: {
160
+ click: (columns, data, column, record, rowIndex, target) => {
161
+ alert(`${column!.name} ${record![column!.name]}, row : ${rowIndex}`)
162
+ }
163
+ } as GristEventHandlerSet
164
+ },
165
+ {
166
+ type: 'email',
167
+ name: 'email',
168
+ label: true,
169
+ header: 'email',
170
+ record: {
171
+ editable: true
172
+ },
173
+ filter: 'search',
174
+ sortable: true,
175
+ width: 130,
176
+ validation: function (after, before, record, column) {
177
+ if (after.indexOf('@') == -1) {
178
+ document.dispatchEvent(
179
+ new CustomEvent('notify', {
180
+ detail: {
181
+ type: 'error',
182
+ message: `invalid value - ${after}`
183
+ }
184
+ })
185
+ )
186
+ return false
187
+ }
188
+ return true
189
+ } as ValidationCallback
190
+ },
191
+ {
192
+ type: 'boolean',
193
+ name: 'active',
194
+ header: 'active',
195
+ record: {
196
+ editable: true
197
+ },
198
+ filter: true,
199
+ handlers: {
200
+ dblclick: () => {
201
+ const grist = document.querySelector('ox-grist') as any
202
+ console.log(grist!.dirtyRecords)
203
+ }
204
+ },
205
+ sortable: true,
206
+ width: 60
207
+ },
208
+ {
209
+ type: 'select',
210
+ name: 'role',
211
+ label: true,
212
+ header: 'role',
213
+ record: {
214
+ options: ['', 'admin', 'worker', 'tester'],
215
+ editable: true
216
+ },
217
+ filter: true,
218
+ sortable: true,
219
+ width: 120
220
+ },
221
+ {
222
+ type: 'color',
223
+ name: 'color',
224
+ header: 'color',
225
+ record: {
226
+ editable: true
227
+ },
228
+ sortable: true,
229
+ width: 50
230
+ },
231
+ {
232
+ type: 'float',
233
+ name: 'rate',
234
+ header: 'rate',
235
+ record: {
236
+ align: 'right',
237
+ editable: true,
238
+ defaultValue: 10000.1
239
+ },
240
+ filter: {
241
+ operator: 'between',
242
+ value: [1, 100]
243
+ },
244
+ sortable: true,
245
+ width: 50
246
+ },
247
+ {
248
+ type: 'json5',
249
+ name: 'json5',
250
+ header: 'JSON5',
251
+ width: 200
252
+ },
253
+ {
254
+ type: 'image',
255
+ name: 'thumbnail',
256
+ header: 'thumbnail',
257
+ record: {
258
+ editable: true
259
+ },
260
+ width: 120
261
+ },
262
+ {
263
+ type: 'datetime',
264
+ name: 'updatedAt',
265
+ header: 'updated at',
266
+ record: {
267
+ editable: true,
268
+ defaultValue: {
269
+ name: 'now'
270
+ }
271
+ },
272
+ filter: {
273
+ operator: 'between',
274
+ type: 'datetime',
275
+ value: [
276
+ {
277
+ name: 'today',
278
+ params: {
279
+ type: 'datetime'
280
+ }
281
+ },
282
+ {
283
+ name: 'now',
284
+ params: {
285
+ type: 'datetime',
286
+ relativeDays: 1
287
+ }
288
+ }
289
+ ]
290
+ },
291
+ sortable: true,
292
+ width: 180
293
+ },
294
+ {
295
+ type: 'datetime',
296
+ name: 'createdAt',
297
+ header: 'created at',
298
+ record: {
299
+ editable: false
300
+ },
301
+ sortable: true,
302
+ width: 180
303
+ }
304
+ ],
305
+ rows: {
306
+ selectable: {
307
+ multiple: true
308
+ },
309
+ handlers: {
310
+ focus: 'select-row-toggle'
311
+ },
312
+ classifier: function (record, rowIndex) {
313
+ const rate = record['rate']
314
+ const emphasized =
315
+ rate < 10 ? ['black', 'white'] : rate < 25 ? ['yellow', 'blue'] : rate < 40 ? ['cyan', 'red'] : undefined
316
+ return {
317
+ emphasized
318
+ }
319
+ } as GristClassifier
320
+ },
321
+ sorters: [
322
+ {
323
+ name: 'name',
324
+ desc: true
325
+ },
326
+ {
327
+ name: 'email'
328
+ }
329
+ ],
330
+ pagination: {
331
+ pages: [20, 30, 50, 100, 200]
332
+ }
333
+ }
334
+
335
+ export default {
336
+ title: 'grid setting',
337
+ component: 'ox-grist',
338
+ argTypes: {
339
+ config: { control: 'object' },
340
+ mode: { control: 'select', options: ['GRID', 'LIST', 'CARD'] },
341
+ urlParamsSensitive: { control: 'boolean' }
342
+ }
343
+ }
344
+
345
+ interface Story<T> {
346
+ (args: T): TemplateResult
347
+ args?: Partial<T>
348
+ argTypes?: Record<string, unknown>
349
+ }
350
+
351
+ interface ArgTypes {
352
+ config: object
353
+ mode: string
354
+ urlParamsSensitive: boolean
355
+ fetchHandler: object
356
+ }
357
+
358
+ const Template: Story<ArgTypes> = ({ config, mode = 'GRID', urlParamsSensitive = false, fetchHandler }: ArgTypes) =>
359
+ html` <link
360
+ href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL@20..48,100..700,0..1"
361
+ rel="stylesheet"
362
+ />
363
+ <link
364
+ href="https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL@20..48,100..700,0..1"
365
+ rel="stylesheet"
366
+ />
367
+ <link
368
+ href="https://fonts.googleapis.com/css2?family=Material+Symbols+Sharp:opsz,wght,FILL@20..48,100..700,0..1"
369
+ rel="stylesheet"
370
+ />
371
+
372
+ <link href="/themes/app-theme.css" rel="stylesheet" />
373
+ <link href="/themes/oops-theme.css" rel="stylesheet" />
374
+ <link href="/themes/grist-theme.css" rel="stylesheet" />
375
+
376
+ <style>
377
+ ox-filters-form {
378
+ --input-gap-vertical: 8px;
379
+ --input-gap-horizontal: 16px;
380
+
381
+ --ox-filters-input-placeholder-color: var(--primary-color);
382
+
383
+ --ox-filters-input-border: 1px solid rgba(0, 0, 0, 0.2);
384
+ --ox-filters-input-focus-border: 1px solid var(--primary-color);
385
+ --ox-filters-input-font: normal 14px var(--theme-font);
386
+ --ox-filters-input-color: var(--primary-text-color);
387
+ --ox-filters-input-focus-color: var(--primary-color);
388
+ --ox-filters-label-font: normal 14px var(--theme-font);
389
+ --ox-filters-label-color: var(--primary-text-color);
390
+
391
+ --ox-filters-form-gap: var(--input-gap-vertical, 8px) var(--input-gap-horizontal, 16px);
392
+ --ox-filters-input-padding: 6px 2px;
393
+ }
394
+ </style>
395
+
396
+ <style>
397
+ ox-grist {
398
+ width: 100%;
399
+ height: 600px;
400
+ }
401
+
402
+ [slot='headroom'] {
403
+ display: flex;
404
+ flex-direction: row;
405
+ align-items: center;
406
+ padding: var(--padding-default) var(--padding-wide);
407
+ background-color: var(--theme-white-color);
408
+ box-shadow: var(--box-shadow);
409
+
410
+ --md-icon-size: 24px;
411
+ }
412
+ #sorters md-icon,
413
+ #modes md-icon {
414
+ --md-icon-size: 18px;
415
+ }
416
+ #sorters {
417
+ margin-left: auto;
418
+ margin-right: var(--margin-default);
419
+ padding-left: var(--padding-narrow);
420
+ border-bottom: var(--border-dark-color);
421
+ position: relative;
422
+ color: var(--secondary-color);
423
+ font-size: var(--fontsize-default);
424
+ user-select: none;
425
+ }
426
+
427
+ #sorters > * {
428
+ padding: var(--padding-narrow);
429
+ vertical-align: middle;
430
+ }
431
+
432
+ #modes > * {
433
+ padding: var(--padding-narrow);
434
+ opacity: 0.5;
435
+ color: var(--primary-text-color);
436
+ cursor: pointer;
437
+ }
438
+
439
+ #modes > md-icon[active] {
440
+ border-radius: 9px;
441
+ background-color: rgba(var(--primary-color-rgb), 0.05);
442
+ opacity: 1;
443
+ color: var(--secondary-text-color);
444
+ cursor: default;
445
+ }
446
+
447
+ #modes > md-icon:hover {
448
+ opacity: 1;
449
+ color: var(--secondary-text-color);
450
+ }
451
+
452
+ #add {
453
+ text-align: right;
454
+ }
455
+
456
+ #add button {
457
+ display: flex;
458
+ align-items: center;
459
+ justify-content: center;
460
+
461
+ background-color: var(--primary-color);
462
+ border: 0;
463
+ border-radius: 50%;
464
+ padding: 5px;
465
+ width: 32px;
466
+ height: 32px;
467
+ cursor: pointer;
468
+ }
469
+
470
+ #add button:hover {
471
+ background-color: var(--focus-background-color);
472
+ box-shadow: var(--box-shadow);
473
+ }
474
+
475
+ #add button md-icon {
476
+ font-size: 1.5em;
477
+ color: var(--theme-white-color);
478
+ }
479
+
480
+ #filters {
481
+ display: flex;
482
+ justify-content: center;
483
+ align-items: center;
484
+ }
485
+
486
+ #filters * {
487
+ margin-right: var(--margin-default);
488
+ }
489
+
490
+ [slot='setting'] {
491
+ --md-icon-size: 18px;
492
+ }
493
+
494
+ @media only screen and (max-width: 460px) {
495
+ #filters {
496
+ flex-direction: column;
497
+ }
498
+
499
+ #modes {
500
+ display: none;
501
+ }
502
+ }
503
+ </style>
504
+
505
+ <ox-grist
506
+ .config=${config}
507
+ .mode=${mode}
508
+ .fetchHandler=${fetchHandler}
509
+ ?url-params-sensitive=${urlParamsSensitive}
510
+ @filters-change=${(e: Event) => console.log('filters', (e.target as any).filters)}
511
+ >
512
+ <div slot="headroom">
513
+ <div id="filters">
514
+ <ox-filters-form autofocus></ox-filters-form>
515
+ </div>
516
+
517
+ <div id="sorters">
518
+ Sort
519
+ <md-icon
520
+ @click=${(e: Event) => {
521
+ const target = e.currentTarget as HTMLElement
522
+ ;(target.closest('#sorters')!.querySelector('#sorter-control') as any).open({
523
+ right: 0,
524
+ top: target.offsetTop + target.offsetHeight
525
+ })
526
+ }}
527
+ >expand_more</md-icon
528
+ >
529
+ <ox-popup id="sorter-control">
530
+ <ox-sorters-control> </ox-sorters-control>
531
+ </ox-popup>
532
+ </div>
533
+
534
+ <div id="modes">
535
+ <md-icon @click=${() => (mode = 'GRID')} ?active=${mode == 'GRID'}>grid_on</md-icon>
536
+ <md-icon @click=${() => (mode = 'LIST')} ?active=${mode == 'LIST'}>format_list_bulleted</md-icon>
537
+ <md-icon @click=${() => (mode = 'CARD')} ?active=${mode == 'CARD'}>apps</md-icon>
538
+ </div>
539
+
540
+ <ox-record-creator id="add" light-popup>
541
+ <button><md-icon>add</md-icon></button>
542
+ </ox-record-creator>
543
+ </div>
544
+
545
+ <md-icon slot="setting" @click=${(e: MouseEvent) => alert('setting clicked')}>tune</md-icon>
546
+ </ox-grist>`
547
+
548
+ export const Regular = Template.bind({})
549
+ Regular.args = {
550
+ config,
551
+ fetchHandler,
552
+ mode: 'GRID'
553
+ }