@mixd-id/web-scaffold 0.1.2301231350 → 0.1.2301231352

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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mixd-id/web-scaffold",
3
3
  "private": false,
4
- "version": "0.1.2301231350",
4
+ "version": "0.1.2301231352",
5
5
  "scripts": {
6
6
  "dev": "vite serve",
7
7
  "build": "vite build",
@@ -30,7 +30,12 @@ export default {
30
30
  default: "auto" // bottom-left, bottom-right, top-left, top-right, bottom-center, top-center default: bottom-left
31
31
  },
32
32
 
33
- bodyClass: String
33
+ bodyClass: String,
34
+
35
+ dismiss: {
36
+ type: Boolean,
37
+ default: true
38
+ }
34
39
 
35
40
  },
36
41
 
@@ -87,23 +92,14 @@ export default {
87
92
  }, 77)
88
93
  },
89
94
 
90
- onDismiss(){
95
+ onDismiss(e){
91
96
  if(typeof window === 'undefined') return
92
97
  if(!this.$refs.contextMenu) return
93
98
 
94
- const transitionEnd = () => {
95
- window.removeEventListener('click', this.onDismiss)
96
- window.removeEventListener('scroll', this.onDismiss)
97
- if(this.$refs.contextMenu){
98
- this.$refs.contextMenu.removeEventListener('transitionend', transitionEnd)
99
- }
100
- this.computedStyle = null
101
- this.$emit('dismiss')
102
- this.isOpen = false
103
- }
99
+ if(e.target.closest('.' + this.$style.contextMenu) && !this.dismiss)
100
+ return
104
101
 
105
- this.$refs.contextMenu.addEventListener('transitionend', transitionEnd)
106
- this.$refs.contextMenu.classList.remove(this.$style.active)
102
+ this.close()
107
103
  },
108
104
 
109
105
  open(caller, context){
@@ -186,6 +182,21 @@ export default {
186
182
  console.warn('Invalid context menu caller', this.caller)
187
183
  }
188
184
 
185
+ },
186
+
187
+ close(){
188
+ const transitionEnd = () => {
189
+ window.removeEventListener('click', this.onDismiss)
190
+ window.removeEventListener('scroll', this.onDismiss)
191
+ if(this.$refs.contextMenu){
192
+ this.$refs.contextMenu.removeEventListener('transitionend', transitionEnd)
193
+ }
194
+ this.computedStyle = null
195
+ this.isOpen = false
196
+ this.$emit('dismiss')
197
+ }
198
+ this.$refs.contextMenu.addEventListener('transitionend', transitionEnd)
199
+ this.$refs.contextMenu.classList.remove(this.$style.active)
189
200
  }
190
201
 
191
202
  }
@@ -2,17 +2,17 @@
2
2
  <Suspense @resolve="patch">
3
3
  <div :class="$style.comp">
4
4
 
5
- <div class="flex-1 flex flex-col">
5
+ <div class="flex-1 flex flex-col p-6">
6
6
 
7
- <div class="p-6 pb-0 flex flex-row gap-3 items-center">
7
+ <div class="pb-0 flex flex-row gap-3 items-center mb-6">
8
8
  <slot v-if="$slots['lp-start']" name="lp-start" :preset="preset"/>
9
9
  <div v-else class="mr-6 hidden md:block">
10
10
  <h2>{{ title }}</h2>
11
11
  </div>
12
12
  <div class="flex-1"></div>
13
- <div class="overflow-hidden mr-4 hidden md:block">
13
+ <div class="overflow-hidden hidden md:block">
14
14
  <slot v-if="$slots['lp-search']" name="lp-search" :preset="preset"/>
15
- <Textbox v-else :clearable="true" placeholder="Cari..." class="w-[200px]" v-model="preset.search"
15
+ <Textbox v-else :clearable="true" :placeholder="$t('Search...')" class="w-[200px]" v-model="preset.search"
16
16
  @clear="clearSearch" @submit="load">
17
17
  <template #start>
18
18
  <div class="px-2">
@@ -36,13 +36,9 @@
36
36
  </div>
37
37
  </div>
38
38
  </Dropdown>
39
- <button class="border-[1px] border-text-200 bg-base-50 hover:border-text-300 rounded-lg p-2"
40
- @click="openPreset">
41
- <svg width="20.5" height="20.5" viewBox="0 0 24 24" class="fill-text-300 hover:fill-primary" xmlns="http://www.w3.org/2000/svg">
42
- <path fill-rule="evenodd" clip-rule="evenodd" d="M5 13.5C5.82843 13.5 6.5 12.8284 6.5 12C6.5 11.1716 5.82843 10.5 5 10.5C4.17157 10.5 3.5 11.1716 3.5 12C3.5 12.8284 4.17157 13.5 5 13.5ZM5 15C6.65685 15 8 13.6569 8 12C8 10.3431 6.65685 9 5 9C3.34315 9 2 10.3431 2 12C2 13.6569 3.34315 15 5 15Z" />
43
- <path fill-rule="evenodd" clip-rule="evenodd" d="M12 13.5C12.8284 13.5 13.5 12.8284 13.5 12C13.5 11.1716 12.8284 10.5 12 10.5C11.1716 10.5 10.5 11.1716 10.5 12C10.5 12.8284 11.1716 13.5 12 13.5ZM12 15C13.6569 15 15 13.6569 15 12C15 10.3431 13.6569 9 12 9C10.3431 9 9 10.3431 9 12C9 13.6569 10.3431 15 12 15Z" />
44
- <path fill-rule="evenodd" clip-rule="evenodd" d="M19 13.5C19.8284 13.5 20.5 12.8284 20.5 12C20.5 11.1716 19.8284 10.5 19 10.5C18.1716 10.5 17.5 11.1716 17.5 12C17.5 12.8284 18.1716 13.5 19 13.5ZM19 15C20.6569 15 22 13.6569 22 12C22 10.3431 20.6569 9 19 9C17.3431 9 16 10.3431 16 12C16 13.6569 17.3431 15 19 15Z" />
45
- </svg>
39
+ <button class="border-[1px] border-text-200 bg-base-50 hover:border-text-300 rounded-lg p-3"
40
+ @click="openPreset()">
41
+ <svg width="15" height="15" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" class="fill-text-300 hover:fill-primary"><!--! Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M64 256V160H224v96H64zm0 64H224v96H64V320zm224 96V320H448v96H288zM448 256H288V160H448v96zM64 32C28.7 32 0 60.7 0 96V416c0 35.3 28.7 64 64 64H448c35.3 0 64-28.7 64-64V96c0-35.3-28.7-64-64-64H64z"/></svg>
46
42
  </button>
47
43
  </div>
48
44
  </div>
@@ -56,29 +52,86 @@
56
52
  </template>
57
53
  </VirtualScroll>
58
54
 
59
- <VirtualTable v-else class="flex-1 m-6" :columns="presetColumns" :items="items"
55
+ <VirtualTable v-else class="flex-1" :columns="presetColumns" :items="items"
60
56
  :appearances="appearances" @scroll-end="loadNext" :pinned="pinned">
61
57
  <template v-for="column in presetColumns" #[colOf(column.key)]="{}">
62
- <div :class="getHeader(column)">
63
- <div class="flex-1 p-2 cursor-pointer" @click="$refs[column.key][0].open($event.target)">
58
+ <div :class="getHeader(column)" @click="openColumnOptions(column.key, $event.target.closest('.' + $style.header))">
59
+ <div>
64
60
  {{ column.label }}
65
61
  </div>
66
- <div>
62
+ <div class="absolute top-0 right-0 p-2 bg-base-500" v-if="sortedColumns[column.key] === 'asc'">
63
+ <svg width="19" height="19" viewBox="0 0 24 24" class="fill-text-400" xmlns="http://www.w3.org/2000/svg">
64
+ <path d="M6.75 5C6.75 4.58579 6.41421 4.25 6 4.25C5.58579 4.25 5.25 4.58579 5.25 5V17.6893L3.53033 15.9697C3.23744 15.6768 2.76256 15.6768 2.46967 15.9697C2.17678 16.2626 2.17678 16.7374 2.46967 17.0303L4.76256 19.3232C5.44598 20.0066 6.55402 20.0066 7.23744 19.3232L9.53033 17.0303C9.82322 16.7374 9.82322 16.2626 9.53033 15.9697C9.23744 15.6768 8.76256 15.6768 8.46967 15.9697L6.75 17.6893V5Z"/>
65
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M12.25 17C12.25 17.4142 12.5858 17.75 13 17.75H21C21.4142 17.75 21.75 17.4142 21.75 17C21.75 16.5858 21.4142 16.25 21 16.25H13C12.5858 16.25 12.25 16.5858 12.25 17Z"/>
66
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M12.25 12C12.25 11.5858 12.5858 11.25 13 11.25H18C18.4142 11.25 18.75 11.5858 18.75 12C18.75 12.4142 18.4142 12.75 18 12.75H13C12.5858 12.75 12.25 12.4142 12.25 12Z"/>
67
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M12.25 7C12.25 6.58579 12.5858 6.25 13 6.25H15C15.4142 6.25 15.75 6.58579 15.75 7C15.75 7.41421 15.4142 7.75 15 7.75H13C12.5858 7.75 12.25 7.41421 12.25 7Z"/>
68
+ </svg>
69
+ </div>
70
+ <div class="absolute top-0 right-0 p-2 bg-base-500" v-else-if="sortedColumns[column.key] === 'desc'">
71
+ <svg width="19" height="19" viewBox="0 0 24 24" class="fill-text-400" xmlns="http://www.w3.org/2000/svg">
72
+ <path d="M6.75 5C6.75 4.58579 6.41421 4.25 6 4.25C5.58579 4.25 5.25 4.58579 5.25 5V17.6893L3.53033 15.9697C3.23744 15.6768 2.76256 15.6768 2.46967 15.9697C2.17678 16.2626 2.17678 16.7374 2.46967 17.0303L4.76256 19.3232C5.44598 20.0066 6.55402 20.0066 7.23744 19.3232L9.53033 17.0303C9.82322 16.7374 9.82322 16.2626 9.53033 15.9697C9.23744 15.6768 8.76256 15.6768 8.46967 15.9697L6.75 17.6893V5Z"/>
73
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M12.25 7C12.25 6.58579 12.5858 6.25 13 6.25H21C21.4142 6.25 21.75 6.58579 21.75 7C21.75 7.41421 21.4142 7.75 21 7.75H13C12.5858 7.75 12.25 7.41421 12.25 7Z"/>
74
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M12.25 12C12.25 12.4142 12.5858 12.75 13 12.75H18C18.4142 12.75 18.75 12.4142 18.75 12C18.75 11.5858 18.4142 11.25 18 11.25H13C12.5858 11.25 12.25 11.5858 12.25 12Z"/>
75
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M12.25 17C12.25 17.4142 12.5858 17.75 13 17.75H15C15.4142 17.75 15.75 17.4142 15.75 17C15.75 16.5858 15.4142 16.25 15 16.25H13C12.5858 16.25 12.25 16.5858 12.25 17Z"/>
76
+ </svg>
67
77
  </div>
68
78
  </div>
69
79
  </template>
70
- <template v-for="(_, slot) in $slots" #[slot]="{ item, index }">
80
+ <template v-for="(_, slot) in headerSlots" #[slot]="{ item, index }">
81
+ <div :class="getHeader(slot.replace('col-', ''))" @click="openColumnOptions(slot.replace('col-', ''), $event.target.closest('.' + $style.header))">
82
+ <slot :name="slot" :item="item" :index="index"></slot>
83
+ </div>
84
+ </template>
85
+ <template v-for="(_, slot) in contentSlots" #[slot]="{ item, index }">
71
86
  <slot :name="slot" :item="item" :index="index"></slot>
72
87
  </template>
73
88
  </VirtualTable>
74
89
 
75
- <ContextMenu v-for="column in presetColumns" :ref="column.key">
76
- <div class="p-4 min-w-[200px] max-w-[300px] min-h-[200px] flex flex-col gap-1">
77
- <h5>{{ column.label }}</h5>
78
- <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec nec metus nisi. Etiam faucibus tortor ut faucibus scelerisque. In vel odio mollis, facilisis arcu et, tristique dolor. Morbi sapien velit, porttitor ut neque id, facilisis tristique mi. Donec sed odio felis. Mauris id urna tempor, sagittis libero ornare, pulvinar nulla. </p>
90
+ <ContextMenu ref="columnMenu" :dismiss="false">
91
+ <div class="flex-1 flex flex-col w-[270px] p-3">
92
+ <div class="flex flex-col">
93
+ <div class="flex flex-row gap-2 items-center">
94
+ <div class="p-2 text-text-300 flex-1">Sort By</div>
95
+ <div class="text-primary cursor-pointer text-sm" @click="openPreset('sort');$refs.columnMenu.close()">Sort Options</div>
96
+ </div>
97
+ <div class="p-2 cursor-pointer" :class="$style.hoverable" @click="setSortCurrent(1)">Sort Ascending</div>
98
+ <div class="p-2 cursor-pointer" :class="$style.hoverable" @click="setSortCurrent(2)">Sort Descending</div>
99
+ </div>
100
+ <div class="h-[1px] bg-text-50 my-2"></div>
101
+ <div class="flex flex-col">
102
+ <div class="p-2 cursor-pointer" :class="$style.hoverable" @click="hide">Hide</div>
103
+ <div class="p-2 cursor-pointer" :class="$style.hoverable" @click="openPreset();$refs.columnMenu.close()">Column Options</div>
104
+ </div>
105
+ <div class="h-[1px] bg-text-50 my-2"></div>
106
+ <div class="flex flex-col">
107
+ <div class="flex flex-row gap-2 items-center">
108
+ <div class="p-2 text-text-300 flex-1">Filters</div>
109
+ <div class="text-primary cursor-pointer text-sm" @click="openPreset('filter');$refs.columnMenu.close()">Filter Options</div>
110
+ </div>
111
+ <div v-if="currentFilters.length > 0">
112
+ <ListPage1Filter v-if="preset.filters" v-for="filter in currentFilters"
113
+ :filter="filter" :column="columns[filter.key]"
114
+ @remove="removeFilter(filter)" @change="load" />
115
+ </div>
116
+ <div v-else>
117
+ <div class="p-2 cursor-pointer" :class="$style.hoverable" @click.stop="addCurrentFilter">Add Filter</div>
118
+ </div>
119
+ </div>
79
120
  </div>
80
121
  </ContextMenu>
81
122
 
123
+ <div v-if="count > 0 && !summaryTeleport" class="flex flex-row bg-base-500 divide-x divide-text-50 border-[1px] border-text-50">
124
+ <div class="p-2 px-3 text-text-400">
125
+ Count: {{ count }}
126
+ </div>
127
+ <div></div>
128
+ </div>
129
+ <Teleport v-else-if="count > 0" to=".footer-wrap">
130
+ <div>
131
+ Count: {{ count }}
132
+ </div>
133
+ </Teleport>
134
+
82
135
  </div>
83
136
 
84
137
  <div v-if="layout.presetOpen" class="w-[360px] border-l-[1px] border-text-50 bg-base-500 flex flex-col">
@@ -98,8 +151,8 @@
98
151
  :caller="layout.presetContextMenu ? layout.presetContextMenu.caller : null"
99
152
  @dismiss="layout.presetContextMenu = null" position="bottom-right">
100
153
  <div class="min-w-[200px]">
101
- <button class="w-full p-3 hover:bg-text-50" @click="copyPreset">Copy this Preset</button>
102
- <button class="w-full p-3 text-red-500 hover:text-red-700" @click="removePreset" v-if="presets.length > 1">Remove</button>
154
+ <button type="button" class="w-full p-3 hover:bg-text-50 text-left" @click="copyPreset">Copy this Preset</button>
155
+ <button type="button" class="w-full p-3 text-red-600 hover:text-red-500 text-left" @click="removePreset" v-if="presets.length > 1">Remove</button>
103
156
  </div>
104
157
  </ContextMenu>
105
158
  <button @click="closePreset">
@@ -110,7 +163,7 @@
110
163
  </div>
111
164
 
112
165
  <div class="flex justify-center">
113
- <Tabs v-model="layout.presetTab" :items="[{text:'Kolom',value:'column'},{text:'Filter',value:'filter'},{text:'Urutan',value:'sort'}]" />
166
+ <Tabs v-model="layout.presetTab" :items="[{text:'Columns',value:'column'},{text:'Filters',value:'filter'},{text:'Sorts',value:'sort'}]" />
114
167
  </div>
115
168
  </div>
116
169
 
@@ -139,10 +192,9 @@
139
192
  <ListPage1Filter v-if="preset.filters" v-for="filter in preset.filters"
140
193
  :filter="filter" :column="columns[filter.key]"
141
194
  @remove="removeFilter(filter)" @change="load"/>
142
-
143
195
  <div class="py-8">
144
196
  <Dropdown @change="addFilter" v-model="layout.presetFilterSelector">
145
- <option value="" disabled selected>Tambah Filter</option>
197
+ <option value="" disabled selected>{{ $t('Add Filter')}}</option>
146
198
  <option v-for="column in filterableColumns" :value="column.key">{{ column.label }}</option>
147
199
  </Dropdown>
148
200
  </div>
@@ -160,7 +212,7 @@
160
212
  <div class="flex flex-row items-center gap-2">
161
213
  <div class="flex-1">
162
214
  <Dropdown v-model="sort.key" @change="load">
163
- <option value="" disabled selected>Tambah Urutan</option>
215
+ <option value="" disabled selected>{{ $t('Add Sort') }}</option>
164
216
  <option v-for="column in sortableColumns" :value="column.key">{{ column.label }}</option>
165
217
  </Dropdown>
166
218
  </div>
@@ -177,12 +229,10 @@
177
229
  </button>
178
230
  </div>
179
231
  </div>
180
-
181
232
  <div v-if="preset.sorts && preset.sorts.length > 0" class="h-[1px] my-4 bg-text-100"></div>
182
-
183
233
  <div class="py-4">
184
234
  <Dropdown @change="addSort" v-model="layout.presetSortSelector">
185
- <option value="" disabled selected>Tambah Urutan</option>
235
+ <option value="" disabled selected>{{ $t('Add Sort') }}</option>
186
236
  <option v-for="column in sortableColumns" :value="column.key">{{ column.label }}</option>
187
237
  </Dropdown>
188
238
  </div>
@@ -224,7 +274,9 @@ export default{
224
274
 
225
275
  title: String,
226
276
 
227
- pinned: Function
277
+ pinned: Function,
278
+
279
+ summaryTeleport: String
228
280
 
229
281
  },
230
282
 
@@ -235,8 +287,9 @@ export default{
235
287
  },
236
288
 
237
289
  presetColumns(){
290
+ if(!this.preset.columns) return []
238
291
 
239
- for(let i = 0 ; i < (this.preset.columns ?? []).length ; i++){
292
+ for(let i = 0 ; i < (this.preset.columns).length ; i++){
240
293
  const presetColumn = this.preset.columns[i]
241
294
  const column = this.columns[presetColumn.key] ?? {}
242
295
  for(let key in column){
@@ -246,7 +299,7 @@ export default{
246
299
  }
247
300
  }
248
301
 
249
- return this.preset.columns ?? []
302
+ return this.preset.columns
250
303
  },
251
304
 
252
305
  appearances(){
@@ -261,8 +314,37 @@ export default{
261
314
  return Object.values(this.columns).filter((_) => _.sortable)
262
315
  },
263
316
 
317
+ sortedColumns(){
318
+ const c = {};
319
+ (this.preset.sorts ?? []).forEach((_) => c[_.key] = _.type);
320
+ return c
321
+ },
322
+
264
323
  computedDataSource(){
265
324
  return this.datasource ?? this.model
325
+ },
326
+
327
+ headerSlots(){
328
+ const slots = {}
329
+ for(let key in this.$slots){
330
+ if(key.startsWith('col-'))
331
+ slots[key] = this.$slots[key]
332
+ }
333
+ return slots
334
+ },
335
+
336
+ contentSlots(){
337
+ const slots = {}
338
+ for(let key in this.$slots){
339
+ if(!key.startsWith('col-'))
340
+ slots[key] = this.$slots[key]
341
+ }
342
+ return slots
343
+ },
344
+
345
+ currentFilters(){
346
+ if(!this.preset.filters) return []
347
+ return this.preset.filters.filter((_) => _.key === this.selectedColumn)
266
348
  }
267
349
 
268
350
  },
@@ -278,9 +360,11 @@ export default{
278
360
  },
279
361
  name: null,
280
362
  columns: [],
363
+ selectedColumn: null,
281
364
  presets: [],
282
365
  presetIdx: -1,
283
366
  items: [],
367
+ count: null,
284
368
  hasNext: false,
285
369
  modal: null
286
370
  }
@@ -294,6 +378,9 @@ export default{
294
378
  unmounted() {
295
379
  window.removeEventListener('focus', this.onFocus)
296
380
  this.socket.offAny(this.onHooks)
381
+
382
+ if(this.name)
383
+ this.socketEmit(this.name + '.unsubscribe', { name:this.name })
297
384
  },
298
385
 
299
386
  methods:{
@@ -303,15 +390,21 @@ export default{
303
390
  this.socketEmit(`${this.computedDataSource}.patch`, {
304
391
  ...(urlQuery().reset ? { reset:1 } : {})
305
392
  }, (res) => {
306
- this.name = res.name
307
- this.columns = res.columns
308
- this.presets = res.presets
309
- this.presetIdx = res.presetIdx
393
+ const data = res.data ? res.data : res
394
+ //console.log('patch', data)
395
+ this.name = data.name
396
+ this.columns = data.columns
397
+ this.presets = data.presets
398
+ this.presetIdx = data.presetIdx
310
399
  this.patchPresets()
311
400
  this.$nextTick(() => {
312
- this.items = res.items
313
- this.hasNext = res.hasNext
401
+ this.items = data.items
402
+ this.hasNext = data.hasNext
403
+ this.count = data.count
314
404
  })
405
+
406
+ if(this.name)
407
+ this.socketEmit(this.name + '.subscribe', { name:this.name })
315
408
  })
316
409
  },
317
410
 
@@ -355,12 +448,12 @@ export default{
355
448
  },
356
449
 
357
450
  load(){
358
- console.log('load')
359
451
  this.socketEmit(`${this.computedDataSource}.load`, {
360
452
  preset: this.preset
361
453
  }, (res) => {
362
- this.items = res.items
363
- this.hasNext = res.hasNext
454
+ const data = res.data ? res.data : res
455
+ //console.log('load', data)
456
+ Object.assign(this.$data, data)
364
457
  })
365
458
  },
366
459
 
@@ -397,10 +490,12 @@ export default{
397
490
 
398
491
  selectPreset(preset){
399
492
  this.presetIdx = this.presets.findIndex((_) => _ === preset)
493
+ this.load()
400
494
  },
401
495
 
402
- openPreset(){
496
+ openPreset(presetTab = 'column'){
403
497
  this.layout.presetOpen = true
498
+ this.layout.presetTab = presetTab
404
499
  },
405
500
 
406
501
  closePreset(){
@@ -486,6 +581,7 @@ export default{
486
581
  }
487
582
 
488
583
  this.preset.filters.push({
584
+ enabled: true,
489
585
  key: column.key,
490
586
  label: column.label,
491
587
  type: column.type,
@@ -496,6 +592,10 @@ export default{
496
592
  this.layout.presetFilterSelector = null
497
593
  },
498
594
 
595
+ addCurrentFilter(){
596
+ this.addFilter(this.selectedColumn)
597
+ },
598
+
499
599
  addSort(key){
500
600
 
501
601
  const column = this.preset.columns.filter((_) => _.key === key).pop()
@@ -535,10 +635,33 @@ export default{
535
635
 
536
636
  return [
537
637
  this.$style.header,
538
- (this.preset.filters ?? []).findIndex((_) => _.key === column.key) >= 0 ?
638
+ (this.preset.filters ?? []).findIndex((_) => _.key === column.key && _.enabled) >= 0 ?
539
639
  this.$style.headerSelected : ''
540
640
  ]
541
641
  .join(' ')
642
+ },
643
+
644
+ openColumnOptions(key, target){
645
+ this.selectedColumn = key
646
+ this.$refs.columnMenu.open(target)
647
+ },
648
+
649
+ setSortCurrent(sortType){
650
+ this.preset.sorts = [
651
+ {
652
+ key: this.selectedColumn,
653
+ label: this.columns[this.selectedColumn].label,
654
+ type: sortType === 2 ? 'desc' : 'asc'
655
+ }
656
+ ]
657
+ this.load()
658
+ this.$refs.columnMenu.close()
659
+ },
660
+
661
+ hide(){
662
+ const idx = this.preset.columns.findIndex((_) => _.key === this.selectedColumn)
663
+ this.preset.columns[idx].visible = false
664
+ this.$refs.columnMenu.close()
542
665
  }
543
666
 
544
667
  },
@@ -566,11 +689,18 @@ export default{
566
689
  }
567
690
 
568
691
  .header{
569
- @apply flex flex-row;
692
+ @apply p-2 cursor-pointer border-b-[2px] border-transparent overflow-hidden;
693
+ }
694
+ .header>*:first-child{
695
+ @apply text-ellipsis whitespace-nowrap overflow-x-hidden;
570
696
  }
571
697
 
572
698
  .headerSelected{
573
- @apply bg-primary-700 border-[1px] border-primary-700;
699
+ @apply border-primary;
700
+ }
701
+
702
+ .hoverable{
703
+ @apply hover:bg-primary hover:text-white;
574
704
  }
575
705
 
576
706
  </style>
@@ -1,52 +1,49 @@
1
1
  <template>
2
- <div class="py-8">
3
- <div class="flex flex-row items-center gap-2 py-2">
2
+ <div :class="$style.comp">
3
+ <div class="flex flex-row items-center gap-2">
4
+ <Checkbox v-model="filter.enabled" @change="onFilterChange(item)"/>
4
5
  <div class="flex-1">
5
- <label>{{ filter.label }}</label>
6
+ <strong class="block text-ellipsis overflow-x-hidden whitespace-nowrap">{{ filter.label }}</strong>
6
7
  </div>
7
- <button @click="removeFilter(filter)" class="text-primary">
8
+ <div class="w-[60px]">
9
+ <Dropdown v-model="filter.operand" @change="onFilterChange(item)">
10
+ <option value="">And</option>
11
+ <option value="or">Or</option>
12
+ </Dropdown>
13
+ </div>
14
+ <button @click.stop="removeFilter(filter)" class="text-primary">
8
15
  <svg width="21" height="21" class="fill-text-100 hover:fill-text-300" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
9
16
  <path class="secondary" fill-rule="evenodd" d="M15.78 14.36a1 1 0 0 1-1.42 1.42l-2.82-2.83-2.83 2.83a1 1 0 1 1-1.42-1.42l2.83-2.82L7.3 8.7a1 1 0 0 1 1.42-1.42l2.83 2.83 2.82-2.83a1 1 0 0 1 1.42 1.42l-2.83 2.83 2.83 2.82z"/>
10
17
  </svg>
11
18
  </button>
12
19
  </div>
13
- <div>
20
+ <div class="flex flex-col gap-3 py-3">
14
21
 
15
22
  <div v-for="(item, idx) in filter.filters">
16
23
 
17
24
  <div v-if="column && column.type === 'date'">
18
- <div class="flex flex-row items-start gap-2 my-2">
19
- <button v-if="filter.filters.length > 1" @click="filter.filters.splice(filter.filters.indexOf(item), 1)"
25
+
26
+ <div class="flex flex-row items-start gap-2">
27
+ <Dropdown v-model="item.operator" @change="onFilterChange(item)" class="flex-1 min-w-[100px]">
28
+ <option value="">Select</option>
29
+ <option value="today">Today</option>
30
+ <option value="thisWeek">This Week</option>
31
+ <option value="thisMonth">This Month</option>
32
+ <option value="lastMonth">Last Month</option>
33
+ <option value="thisMonth">This Year</option>
34
+ <option value="lastYear">Last Year</option>
35
+ <option value="between">Between</option>
36
+ </Dropdown>
37
+ <Datepicker v-if="item.operator === 'between'" v-model="item.value[0]" mode="popup" @change="onFilterChange(item)"/>
38
+ <Datepicker v-if="item.operator === 'between'" v-model="item.value[1]" mode="popup" @change="onFilterChange(item)"/>
39
+ <button v-if="filter.filters.length > 1" @click.stop="filter.filters.splice(filter.filters.indexOf(item), 1);onFilterChange(item)"
20
40
  class="py-2">
21
41
  <svg width="21" height="21" class="fill-text-100 hover:fill-text-300" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
22
42
  <path class="secondary" fill-rule="evenodd" d="M15.78 14.36a1 1 0 0 1-1.42 1.42l-2.82-2.83-2.83 2.83a1 1 0 1 1-1.42-1.42l2.83-2.82L7.3 8.7a1 1 0 0 1 1.42-1.42l2.83 2.83 2.82-2.83a1 1 0 0 1 1.42 1.42l-2.83 2.83 2.83 2.82z"/>
23
43
  </svg>
24
44
  </button>
25
- <div class="w-[60px]">
26
- <Dropdown v-if="idx === 0" v-model="item.operand" @change="onFilterChange(item)">
27
- <option value="">And</option>
28
- <option value="or">Or</option>
29
- </Dropdown>
30
- </div>
31
- <div class="flex-1 flex flex-col gap-2">
32
- <Dropdown v-model="item.operator" @change="onFilterChange(item)">
33
- <option value="">Select</option>
34
- <option value="today">Today</option>
35
- <option value="thisWeek">This Week</option>
36
- <option value="thisMonth">This Month</option>
37
- <option value="lastMonth">Last Month</option>
38
- <option value="thisMonth">This Year</option>
39
- <option value="lastYear">Last Year</option>
40
- <option value="between">Between</option>
41
- </Dropdown>
42
-
43
- <Datepicker v-if="item.operator === 'between'" v-model="item.value[0]" mode="popup" @change="onFilterChange(item)"/>
44
-
45
- <Datepicker v-if="item.operator === 'between'" v-model="item.value[1]" mode="popup" @change="onFilterChange(item)"/>
46
-
47
- <button v-if="idx === filter.filters.length - 1" @click="addMore" class="text-primary">Add More</button>
48
- </div>
49
45
  </div>
46
+
50
47
  </div>
51
48
 
52
49
  <div v-else-if="column && column.type === 'enum'" class="flex flex-col">
@@ -57,70 +54,57 @@
57
54
  </div>
58
55
 
59
56
  <div v-else-if="column && [ 'currency', 'number' ].includes(column.type)">
60
- <div class="flex flex-row items-start gap-2 my-2">
61
- <button v-if="filter.filters.length > 1" @click="filter.filters.splice(filter.filters.indexOf(item), 1)"
57
+
58
+ <div class="flex flex-row items-start gap-2">
59
+ <Dropdown v-model="item.operator" @change="onFilterChange(item)">
60
+ <option value="">Select</option>
61
+ <option value="=">=</option>
62
+ <option value=">">&gt;</option>
63
+ <option value=">=">&gt;=</option>
64
+ <option value="<">&lt;</option>
65
+ <option value="<=">&lt;=</option>
66
+ </Dropdown>
67
+ <Textbox class="flex-1" v-model="item.value" @keyup.enter="onFilterChange(item)"/>
68
+ <button v-if="filter.filters.length > 1" @click.stop="filter.filters.splice(filter.filters.indexOf(item), 1);onFilterChange(item)"
62
69
  class="py-2">
63
70
  <svg width="21" height="21" class="fill-text-100 hover:fill-text-300" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
64
71
  <path class="secondary" fill-rule="evenodd" d="M15.78 14.36a1 1 0 0 1-1.42 1.42l-2.82-2.83-2.83 2.83a1 1 0 1 1-1.42-1.42l2.83-2.82L7.3 8.7a1 1 0 0 1 1.42-1.42l2.83 2.83 2.82-2.83a1 1 0 0 1 1.42 1.42l-2.83 2.83 2.83 2.82z"/>
65
72
  </svg>
66
73
  </button>
67
- <div class="w-[60px]">
68
- <Dropdown v-if="idx === 0" v-model="item.operand" @change="onFilterChange(item)">
69
- <option value="">And</option>
70
- <option value="or">Or</option>
71
- </Dropdown>
72
- </div>
73
- <div class="flex-1 flex flex-col gap-2">
74
- <Dropdown v-model="item.operator" @change="onFilterChange(item)">
75
- <option value="">Select</option>
76
- <option value="=">=</option>
77
- <option value=">">&gt;</option>
78
- <option value=">=">&gt;=</option>
79
- <option value="<">&lt;</option>
80
- <option value="<=">&lt;=</option>
81
- </Dropdown>
82
-
83
- <Textbox v-model="item.value" />
84
-
85
- <button v-if="idx === filter.filters.length - 1" @click="addMore" class="text-primary">Add More</button>
86
- </div>
87
74
  </div>
75
+
88
76
  </div>
89
77
 
90
78
  <div v-else>
91
- <div class="flex flex-row items-start gap-2 my-2">
92
- <button v-if="filter.filters.length > 1" @click="filter.filters.splice(filter.filters.indexOf(item), 1);onFilterChange(item)"
93
- class="py-2">
79
+
80
+ <div class="flex flex-row items-start gap-2">
81
+ <Dropdown v-model="item.operator" @change="onFilterChange(item)" class="flex-1 min-w-[100px]">
82
+ <option value="">{{ $t('Select') }}</option>
83
+ <option value="startsWith">{{ $t('Starts with') }}</option>
84
+ <option value="endsWith">{{ $t('Ends with') }}</option>
85
+ <option value="contains">{{ $t('Contains') }}</option>
86
+ <option value="=">{{ $t('Equal') }}</option>
87
+ <option value="empty">{{ $t('Empty')}}</option>
88
+ <option value="notEmpty">{{ $t('Not empty')}}</option>
89
+ <option value="regex">{{ $t('Regexp')}}</option>
90
+ </Dropdown>
91
+ <Textbox v-if="![ 'empty', 'notEmpty' ].includes(item.operator)" class="w-[150px]" v-model="item.value" @keyup.enter="onFilterChange(item)"/>
92
+ <button v-if="filter.filters.length > 1" @click.stop="filter.filters.splice(filter.filters.indexOf(item), 1);onFilterChange(item)"
93
+ class="py-2">
94
94
  <svg width="21" height="21" class="fill-text-100 hover:fill-text-300" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
95
95
  <path class="secondary" fill-rule="evenodd" d="M15.78 14.36a1 1 0 0 1-1.42 1.42l-2.82-2.83-2.83 2.83a1 1 0 1 1-1.42-1.42l2.83-2.82L7.3 8.7a1 1 0 0 1 1.42-1.42l2.83 2.83 2.82-2.83a1 1 0 0 1 1.42 1.42l-2.83 2.83 2.83 2.82z"/>
96
96
  </svg>
97
97
  </button>
98
- <div class="w-[60px]">
99
- <Dropdown v-if="idx === 0" v-model="item.operand" @change="onFilterChange(item)">
100
- <option value="">And</option>
101
- <option value="or">Or</option>
102
- </Dropdown>
103
- </div>
104
- <div class="flex-1 flex flex-col gap-2">
105
- <Dropdown v-model="item.operator" @change="onFilterChange(item)">
106
- <option value="">Select</option>
107
- <option value="startsWith">Dimulai</option>
108
- <option value="endsWith">Diakhiri</option>
109
- <option value="contains">Berisi</option>
110
- <option value="=">=</option>
111
- </Dropdown>
112
-
113
- <Textbox v-model="item.value" @keyup.enter="onFilterChange(item)"/>
114
-
115
- <button v-if="idx === filter.filters.length - 1" @click="addMore" class="text-primary">Add More</button>
116
- </div>
117
98
  </div>
118
99
 
119
-
120
100
  </div>
121
101
 
122
102
  </div>
123
103
 
104
+ <div class="text-center">
105
+ <button @click="addMore" class="text-primary">{{ $t('Add More') }}</button>
106
+ </div>
107
+
124
108
  </div>
125
109
 
126
110
  </div>
@@ -145,14 +129,12 @@ export default{
145
129
  case 'date':
146
130
  case 'enum':
147
131
  this.filter.filters.push({
148
- operand: 'or',
149
132
  value: []
150
133
  })
151
134
  break
152
135
 
153
136
  default:
154
137
  this.filter.filters.push({
155
- operand: 'or'
156
138
  })
157
139
  break
158
140
  }
@@ -180,7 +162,7 @@ export default{
180
162
  <style module>
181
163
 
182
164
  .comp{
183
-
165
+ @apply py-4;
184
166
  }
185
167
 
186
168
  </style>
@@ -262,7 +262,10 @@ export default{
262
262
  const x2 = e.touches ? e.touches[0].clientX : e.clientX
263
263
  const d = x2 - x1
264
264
  x1 = x2
265
- this.columns[idx].width = parseInt(this.columns[idx].width ?? _DEFAULT_COLUMN_WIDTH) + d
265
+
266
+ let width = parseInt(this.columns[idx].width ?? _DEFAULT_COLUMN_WIDTH) + d
267
+ if(width < 20) width = 20
268
+ this.columns[idx].width = width
266
269
  }
267
270
 
268
271
  const onMouseUp = (e) => {
package/src/index.js CHANGED
@@ -129,6 +129,52 @@ const popPreloads = () => {
129
129
  return link
130
130
  }
131
131
 
132
+ const util = {
133
+
134
+ push: (arr, item, opt = { key:"id" }) => {
135
+ const index = arr.findIndex((_) => _[opt.key] === item[opt.key])
136
+ if(index >= 0){
137
+ Object.assign(arr[index], item)
138
+ }
139
+ else{
140
+ arr.push(item)
141
+ }
142
+ },
143
+
144
+ onEvent: (arr, updates, opt = {}) => {
145
+
146
+ const [ model, event, items ] = updates
147
+
148
+ if(opt.model && opt.model !== model)
149
+ return
150
+
151
+ items.forEach((item) => {
152
+
153
+ const index = arr.findIndex((_) => _.id === item.id)
154
+
155
+ switch(event){
156
+
157
+ case 'create':
158
+ case 'update':
159
+ if(index >= 0){
160
+ Object.assign(arr[index], item)
161
+ }
162
+ else{
163
+ arr.push(item)
164
+ }
165
+ break
166
+
167
+ case 'destroy':
168
+ if(index >= 0){
169
+ arr.splice(index, 1)
170
+ }
171
+ break
172
+ }
173
+ })
174
+ }
175
+
176
+ }
177
+
132
178
  export default{
133
179
 
134
180
  install: (app, options) => {
@@ -148,6 +194,7 @@ export default{
148
194
  app.config.globalProperties.$download = download
149
195
  app.config.globalProperties.$preload = preload
150
196
  app.config.globalProperties.$popPreloads = popPreloads
197
+ app.config.globalProperties.$util = util
151
198
 
152
199
  app.component('Alert', defineAsyncComponent(() => import("./components/Alert.vue")))
153
200
  app.component('Button', defineAsyncComponent(() => import("./components/Button.vue")))
@@ -1,6 +1,7 @@
1
1
  const { Op } = require('sequelize')
2
- const util = require("util");
2
+ const Sequelize = require('sequelize')
3
3
  const dayjs = require("dayjs");
4
+ const util = require("util");
4
5
 
5
6
  let ListPage1 = {
6
7
 
@@ -9,7 +10,7 @@ let ListPage1 = {
9
10
  model: '',
10
11
  channel: '',
11
12
 
12
- async patch(params){
13
+ async patch(params, socket){
13
14
 
14
15
  const { reset } = params
15
16
 
@@ -80,22 +81,14 @@ let ListPage1 = {
80
81
  ]
81
82
 
82
83
  let attributes = { id:1, updatedAt:1 }
83
- let modelAttributes = this.model.getAttributes()
84
- preset.columns.forEach((column) => {
85
- if(column.visible && modelAttributes[column.key]){
86
- switch(column.key){
87
-
88
- case 'item':
89
- case 'option':
90
- // Ignored
91
- break
92
-
93
- default:
94
- attributes[column.key] = 1
95
- break
84
+ if(preset.columns){
85
+ let modelAttributes = this.model.getAttributes();
86
+ preset.columns.forEach((column) => {
87
+ if(column.visible && modelAttributes[column.key]){
88
+ attributes[column.key] = 1
96
89
  }
97
- }
98
- })
90
+ })
91
+ }
99
92
  attributes = Object.keys(attributes)
100
93
 
101
94
  if(preset.filters){
@@ -129,12 +122,21 @@ let ListPage1 = {
129
122
  }
130
123
  }
131
124
 
132
- const items = await this.model.findAll({
125
+ let attributeIncludes
126
+ if(this.getAttributeIncludes){
127
+ attributeIncludes = this.getAttributeIncludes()
128
+ }
129
+
130
+ const { rows:items, count } = await this.model.findAndCountAll({
133
131
  where,
134
- attributes,
132
+ attributes: {
133
+ ...attributes,
134
+ include: attributeIncludes
135
+ },
135
136
  order,
136
137
  limit: itemsPerPage + 1,
137
138
  replacements,
139
+ include: this.getModelIncludes ? this.getModelIncludes() : undefined
138
140
  })
139
141
 
140
142
  const hasNext = items.length > itemsPerPage
@@ -148,7 +150,22 @@ let ListPage1 = {
148
150
 
149
151
  return {
150
152
  items,
151
- hasNext
153
+ hasNext,
154
+ count
155
+ }
156
+ },
157
+
158
+ async subscribe(params, socket){
159
+ const { name } = params
160
+ if(name){
161
+ socket.join(this.name)
162
+ }
163
+ },
164
+
165
+ async unsubscribe(params, socket){
166
+ const { name } = params
167
+ if(name){
168
+ socket.leave(this.name)
152
169
  }
153
170
  },
154
171
 
@@ -163,17 +180,20 @@ let ListPage1 = {
163
180
 
164
181
  preset.filters.forEach((filter) => {
165
182
 
183
+ if(!filter.enabled) return
184
+
166
185
  const key = filter.key
167
186
  const type = filter.type
187
+ const operand = filter.operand === 'or' ? 'or' : 'and'
168
188
  const items = filter.filters ?? []
169
- const operand = items[0] && items[0].operand && items[0].operand === 'or' && items.length > 1 ?
170
- 'or' : 'and'
171
189
 
172
190
  let whereValue = []
173
191
  switch(type){
174
192
 
175
193
  case 'date':
176
194
  items.forEach((item) => {
195
+ if(!('operator' in item) || (item.operator === 'between' && !('value' in item))) return
196
+
177
197
  const operator = item.operator
178
198
  const value = item.value
179
199
 
@@ -238,6 +258,8 @@ let ListPage1 = {
238
258
 
239
259
  case 'enum':
240
260
  items.forEach((item) => {
261
+ if(!('value' in item)) return
262
+
241
263
  const value = item.value ?? []
242
264
  whereValue = {
243
265
  ...whereValue,
@@ -249,6 +271,7 @@ let ListPage1 = {
249
271
  case 'number':
250
272
  case 'currency':
251
273
  items.forEach((item) => {
274
+ if(!('operator' in item) || !('value' in item)) return
252
275
 
253
276
  const operator = item.operator
254
277
  const value = item.value
@@ -295,6 +318,9 @@ let ListPage1 = {
295
318
 
296
319
  default:
297
320
  items.forEach((item) => {
321
+ if(!('operator' in item) ||
322
+ ([ 'startsWith', 'endsWith', 'contains', '=' ].includes(item.operator) && !'value' in item))
323
+ return
298
324
 
299
325
  const operator = item.operator
300
326
  const value = item.value
@@ -324,6 +350,33 @@ let ListPage1 = {
324
350
  [Op.eq]: value
325
351
  })
326
352
  break
353
+
354
+ case 'empty':
355
+ whereValue.push({
356
+ [Op.or]: [
357
+ { [Op.eq]: null },
358
+ { [Op.eq]: '' },
359
+ ]
360
+ })
361
+ break
362
+
363
+ case 'notEmpty':
364
+ whereValue.push({
365
+ [Op.and]: [
366
+ { [Op.ne]: null },
367
+ { [Op.ne]: '' },
368
+ ]
369
+ })
370
+ break
371
+
372
+ case 'length':
373
+ break
374
+
375
+ case 'regex':
376
+ whereValue.push({
377
+ [Op.regexp]: value
378
+ })
379
+ break
327
380
  }
328
381
  })
329
382
  break
@@ -352,6 +405,8 @@ let ListPage1 = {
352
405
  })
353
406
  }
354
407
 
408
+ //console.log(util.inspect(where, false, null, true /* enable colors */))
409
+
355
410
  /*console.log(util.inspect(preset.filters, false, null, true /!* enable colors *!/))
356
411
  console.log(where)
357
412
  console.log(replacements)*/
@@ -416,28 +471,15 @@ let ListPage1 = {
416
471
  return sortWhere
417
472
  },
418
473
 
419
- async onHooks(model, event, items, io){
474
+ async onHooks(model, event, items, socket){
420
475
 
421
476
  switch(event){
422
-
423
477
  case 'destroy':
424
478
  case 'remove':
425
- io.to(model).emit(model, event, items)
426
- break
427
-
428
479
  default:
429
- const instances = await this.model.findAll({
430
- where: {
431
- id: {
432
- [Op.in]: items.map((_) => _.id)
433
- }
434
- }
435
- })
436
- io.to(model).emit(`${model}${event}`, instances)
480
+ socket.emit(model, event, items)
437
481
  break
438
-
439
482
  }
440
-
441
483
  }
442
484
 
443
485
  }