@edgedev/create-edge-app 1.1.23 → 1.1.26

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.
Files changed (116) hide show
  1. package/.env +1 -0
  2. package/.env.dev +1 -0
  3. package/README.md +55 -20
  4. package/{agent.md → agents.md} +2 -0
  5. package/bin/cli.js +6 -6
  6. package/edge/components/auth/login.vue +384 -0
  7. package/edge/components/auth/register.vue +396 -0
  8. package/edge/components/auth.vue +108 -0
  9. package/edge/components/autoFileUpload.vue +215 -0
  10. package/edge/components/billing.vue +8 -0
  11. package/edge/components/buttonDivider.vue +14 -0
  12. package/edge/components/chip.vue +34 -0
  13. package/edge/components/clipboardButton.vue +42 -0
  14. package/edge/components/cms/block.vue +529 -0
  15. package/edge/components/cms/blockApi.vue +212 -0
  16. package/edge/components/cms/blockEditor.vue +725 -0
  17. package/edge/components/cms/blockInput.vue +66 -0
  18. package/edge/components/cms/blockPicker.vue +486 -0
  19. package/edge/components/cms/blockRender.vue +78 -0
  20. package/edge/components/cms/blockSheetContent.vue +28 -0
  21. package/edge/components/cms/codeEditor.vue +466 -0
  22. package/edge/components/cms/fontUpload.vue +327 -0
  23. package/edge/components/cms/htmlContent.vue +807 -0
  24. package/edge/components/cms/init_blocks/api_with_subarrays.html +17 -0
  25. package/edge/components/cms/init_blocks/array_with_collection.html +7 -0
  26. package/edge/components/cms/init_blocks/array_with_objects.html +7 -0
  27. package/edge/components/cms/init_blocks/carousel.html +103 -0
  28. package/edge/components/cms/init_blocks/contact_us.html +69 -0
  29. package/edge/components/cms/init_blocks/content_with_left_image.html +27 -0
  30. package/edge/components/cms/init_blocks/footer.html +24 -0
  31. package/edge/components/cms/init_blocks/header_divider.html +7 -0
  32. package/edge/components/cms/init_blocks/hero.html +35 -0
  33. package/edge/components/cms/init_blocks/hero_carousel.html +52 -0
  34. package/edge/components/cms/init_blocks/newsletter.html +117 -0
  35. package/edge/components/cms/init_blocks/post_content.html +7 -0
  36. package/edge/components/cms/init_blocks/post_title_header.html +21 -0
  37. package/edge/components/cms/init_blocks/posts_list.html +20 -0
  38. package/edge/components/cms/init_blocks/properties_showcase.html +100 -0
  39. package/edge/components/cms/init_blocks/property_carousel.html +59 -0
  40. package/edge/components/cms/init_blocks/property_detail.html +112 -0
  41. package/edge/components/cms/init_blocks/property_detail_header.html +34 -0
  42. package/edge/components/cms/init_blocks/property_results.html +137 -0
  43. package/edge/components/cms/init_blocks/property_search.html +75 -0
  44. package/edge/components/cms/init_blocks/simple_array.html +7 -0
  45. package/edge/components/cms/mediaCard.vue +116 -0
  46. package/edge/components/cms/mediaManager.vue +386 -0
  47. package/edge/components/cms/menu.vue +1103 -0
  48. package/edge/components/cms/optionsSelect.vue +107 -0
  49. package/edge/components/cms/page.vue +1785 -0
  50. package/edge/components/cms/posts.vue +1083 -0
  51. package/edge/components/cms/site.vue +1298 -0
  52. package/edge/components/cms/themeDefaultMenu.vue +548 -0
  53. package/edge/components/cms/themeEditor.vue +426 -0
  54. package/edge/components/dashboard.vue +776 -0
  55. package/edge/components/editor.vue +671 -0
  56. package/edge/components/fileTree.vue +72 -0
  57. package/edge/components/files.vue +89 -0
  58. package/edge/components/formSubtypes/myOrgs.vue +214 -0
  59. package/edge/components/formSubtypes/users.vue +336 -0
  60. package/edge/components/functionChips.vue +57 -0
  61. package/edge/components/gError.vue +98 -0
  62. package/edge/components/gHelper.vue +67 -0
  63. package/edge/components/gInput.vue +1331 -0
  64. package/edge/components/loggingIn.vue +41 -0
  65. package/edge/components/menu.vue +137 -0
  66. package/edge/components/menuContent.vue +132 -0
  67. package/edge/components/myAccount.vue +317 -0
  68. package/edge/components/myOrganizations.vue +75 -0
  69. package/edge/components/myProfile.vue +122 -0
  70. package/edge/components/orgSwitcher.vue +25 -0
  71. package/edge/components/organizationMembers.vue +522 -0
  72. package/edge/components/organizationSettings.vue +271 -0
  73. package/edge/components/shad/breadcrumbs.vue +35 -0
  74. package/edge/components/shad/button.vue +43 -0
  75. package/edge/components/shad/checkbox.vue +73 -0
  76. package/edge/components/shad/combobox.vue +238 -0
  77. package/edge/components/shad/datepicker.vue +184 -0
  78. package/edge/components/shad/dialog.vue +32 -0
  79. package/edge/components/shad/dropdownMenu.vue +54 -0
  80. package/edge/components/shad/dropdownMenuItem.vue +21 -0
  81. package/edge/components/shad/form.vue +59 -0
  82. package/edge/components/shad/html.vue +877 -0
  83. package/edge/components/shad/input.vue +139 -0
  84. package/edge/components/shad/number.vue +109 -0
  85. package/edge/components/shad/select.vue +151 -0
  86. package/edge/components/shad/selectTags.vue +278 -0
  87. package/edge/components/shad/switch.vue +67 -0
  88. package/edge/components/shad/tags.vue +137 -0
  89. package/edge/components/shad/textarea.vue +102 -0
  90. package/edge/components/shad/typeMoney.vue +167 -0
  91. package/edge/components/sideBar.vue +288 -0
  92. package/edge/components/sideBarContent.vue +268 -0
  93. package/edge/components/sidebarProvider.vue +33 -0
  94. package/edge/components/tooltip.vue +16 -0
  95. package/edge/components/userMenu.vue +148 -0
  96. package/edge/components/v/alert.vue +59 -0
  97. package/edge/components/v/alertTitle.vue +18 -0
  98. package/edge/components/v/card.vue +53 -0
  99. package/edge/components/v/cardActions.vue +18 -0
  100. package/edge/components/v/cardText.vue +18 -0
  101. package/edge/components/v/cardTitle.vue +20 -0
  102. package/edge/components/v/col.vue +56 -0
  103. package/edge/components/v/list.vue +46 -0
  104. package/edge/components/v/listItem.vue +26 -0
  105. package/edge/components/v/listItemTitle.vue +18 -0
  106. package/edge/components/v/row.vue +42 -0
  107. package/edge/components/v/toolbar.vue +24 -0
  108. package/edge/composables/global.ts +519 -0
  109. package/edge-pull.sh +2 -0
  110. package/edge-push.sh +1 -0
  111. package/edge-status.sh +14 -0
  112. package/firebase.json +5 -2
  113. package/firebase_init.sh +21 -6
  114. package/package.json +1 -1
  115. package/plugins/firebase.client.ts +1 -0
  116. package/edge-components-install.sh +0 -1
@@ -0,0 +1,776 @@
1
+ <script setup>
2
+ import { useElementVisibility } from '@vueuse/core'
3
+ import { cn } from '@/lib/utils'
4
+ const props = defineProps({
5
+ paginated: {
6
+ type: Boolean,
7
+ default: false,
8
+ },
9
+ paginatedQuery: {
10
+ type: Array,
11
+ default: () => [],
12
+ },
13
+ paginatedSort: {
14
+ type: Array,
15
+ default: () => [],
16
+ },
17
+ paginatedLimit: {
18
+ type: Number,
19
+ default: 100,
20
+ },
21
+ collection: {
22
+ type: String,
23
+ required: true,
24
+ },
25
+ class: {
26
+ type: String,
27
+ default: '',
28
+ },
29
+ filters: {
30
+ type: Array,
31
+ default: () => [],
32
+ },
33
+ filter: {
34
+ type: [String, Array],
35
+ default: '',
36
+ },
37
+ filterFields: {
38
+ type: Array,
39
+ default: () => ['name'],
40
+ },
41
+ sortField: {
42
+ type: String,
43
+ default: 'name',
44
+ },
45
+ sortDirection: {
46
+ type: String,
47
+ default: 'asc',
48
+ },
49
+ deleteTitle: {
50
+ type: String,
51
+ default: '',
52
+ },
53
+ deleteDescription: {
54
+ type: String,
55
+ default: '',
56
+ },
57
+ headerClass: {
58
+ type: String,
59
+ default: 'bg-secondary py-2',
60
+ },
61
+ footerClass: {
62
+ type: String,
63
+ default: 'justify-end py-2 bg-secondary',
64
+ },
65
+ searchFields: {
66
+ type: Array,
67
+ default: () => [],
68
+ },
69
+ queryField: {
70
+ type: String,
71
+ default: '',
72
+ },
73
+ queryValue: {
74
+ type: [String, Array, Boolean],
75
+ default: '',
76
+ },
77
+ queryOperator: {
78
+ type: String,
79
+ default: '==',
80
+ },
81
+ hideSearch: {
82
+ type: Boolean,
83
+ default: false,
84
+ },
85
+ defaultFilterFields: {
86
+ type: Array,
87
+ default: () => [],
88
+ },
89
+ loadFirstIfOne: {
90
+ type: Boolean,
91
+ default: false,
92
+ },
93
+ })
94
+
95
+ defineOptions({ name: 'EdgeDashboard' })
96
+
97
+ const target = ref(null)
98
+ const isVisible = useElementVisibility(target)
99
+
100
+ const edgeFirebase = inject('edgeFirebase')
101
+ const router = useRouter()
102
+ const route = useRoute()
103
+
104
+ const state = reactive({
105
+ form: false,
106
+ menu: false,
107
+ dialog: false,
108
+ apiKeys: [],
109
+ filter: '',
110
+ empty: false,
111
+ afterMount: false,
112
+ deleteDialog: false,
113
+ deleteItemName: '',
114
+ deleteItemDocId: '',
115
+ staticSearch: {},
116
+ paginatedResults: [],
117
+ loadingMore: false,
118
+ queryField: '',
119
+ queryValue: '',
120
+ queryOperator: '',
121
+ scrollPosition: 0,
122
+ staticCurrentPage: '',
123
+ searching: false,
124
+ filterFields: [],
125
+ })
126
+
127
+ const gotoSite = (docId) => {
128
+ router.push(`/app/dashboard/${props.collection}/${docId}`)
129
+ }
130
+
131
+ const capitalizeFirstLetter = (str) => {
132
+ return str.charAt(0).toUpperCase() + str.slice(1)
133
+ }
134
+
135
+ const singularize = (word) => {
136
+ if (word.endsWith('ies') && word.length > 4) {
137
+ return `${word.slice(0, -3)}y`
138
+ }
139
+ else if (word.endsWith('es') && word.length > 2) {
140
+ // if the word ends with one of the common "es" patterns, remove "es"
141
+ if (
142
+ word.endsWith('ches')
143
+ || word.endsWith('shes')
144
+ || word.endsWith('xes')
145
+ || word.endsWith('ses')
146
+ || word.endsWith('zes')
147
+ ) {
148
+ return word.slice(0, -2)
149
+ }
150
+ else {
151
+ // otherwise, the plural is likely just the singular plus "s"
152
+ return word.slice(0, -1)
153
+ }
154
+ }
155
+ else if (word.endsWith('s') && word.length > 1) {
156
+ return word.slice(0, -1)
157
+ }
158
+ else {
159
+ return word
160
+ }
161
+ }
162
+
163
+ const getByPath = (obj, path) => {
164
+ if (!obj || !path)
165
+ return undefined
166
+ return path.split('.').reduce((acc, key) => {
167
+ if (acc == null)
168
+ return undefined
169
+ return acc[key]
170
+ }, obj)
171
+ }
172
+
173
+ const snapShotQuery = computed(() => {
174
+ if (state.queryField && state.queryValue) {
175
+ // console.log('snapShotQuery', state.queryField, state.queryOperator, state.queryValue)
176
+ return [
177
+ { field: state.queryField, operator: state.queryOperator, value: state.queryValue },
178
+ ]
179
+ }
180
+ if (state.queryValue === false) {
181
+ return [
182
+ { field: state.queryField, operator: '==', value: state.queryValue },
183
+ ]
184
+ }
185
+ return []
186
+ })
187
+
188
+ const searchQuery = computed(() => {
189
+ const field = state.queryField
190
+ const rawVal = state.queryValue
191
+
192
+ // Bail early if field or value is missing/empty
193
+ if (!field || rawVal == null || (Array.isArray(rawVal) && rawVal.length === 0)) {
194
+ return []
195
+ }
196
+
197
+ // Normalize to an array of trimmed, non-empty strings
198
+ const values = (Array.isArray(rawVal) ? rawVal : [rawVal])
199
+ .map(v => (v == null ? '' : String(v).trim()))
200
+ .filter(Boolean)
201
+
202
+ if (values.length === 0)
203
+ return []
204
+
205
+ const searchField = props.searchFields.find(f => f.name === field)
206
+
207
+ // If this field has discrete choices, support multi-select with 'in'
208
+ if (searchField?.choices) {
209
+ if (values.length === 1) {
210
+ return [{ field, operator: '==', value: values[0] }]
211
+ }
212
+ // Firestore 'in' supports up to 10 values
213
+ return [{ field, operator: 'in', value: values.slice(0, 10) }]
214
+ }
215
+
216
+ // Prefix search: use the FIRST value (Firestore can't OR multiple prefix ranges)
217
+ const upper = values[0].toUpperCase()
218
+ return [
219
+ { field, operator: '>=', value: upper },
220
+ { field, operator: '<=', value: `${upper}\uF8FF` },
221
+ ]
222
+ })
223
+ const filterText = computed(() => {
224
+ if (props.filter) {
225
+ return props.filter
226
+ }
227
+ return state.filter
228
+ })
229
+
230
+ const allData = computed(() => {
231
+ if (!props.paginated) {
232
+ if (!edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/${props.collection}`]) {
233
+ return []
234
+ }
235
+ return Object.values(edgeFirebase.data[`${edgeGlobal.edgeState.organizationDocPath}/${props.collection}`])
236
+ }
237
+ return state.paginatedResults
238
+ })
239
+
240
+ const filtered = computed(() => {
241
+ let allData = []
242
+ if (!props.paginated) {
243
+ if (!edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/${props.collection}`]) {
244
+ return []
245
+ }
246
+ allData = Object.values(edgeFirebase.data[`${edgeGlobal.edgeState.organizationDocPath}/${props.collection}`])
247
+ }
248
+ else {
249
+ allData = state.paginatedResults
250
+ }
251
+
252
+ const qRaw = filterText.value
253
+ const fieldsToSearch = (Array.isArray(props.filterFields) && props.filterFields.length > 0) ? props.filterFields : ['name']
254
+
255
+ // Helper: does a "has value" check for strings/arrays
256
+ const hasValue = val => !(val == null || (typeof val === 'string' && val.trim() === '') || (Array.isArray(val) && val.length === 0))
257
+
258
+ // Helper: apply one filter group to a dataset
259
+ const applyFilterToData = (data, qRawLocal, fieldsLocal) => {
260
+ return data.filter((entry) => {
261
+ // If no query value, keep everything
262
+ if (!hasValue(qRawLocal))
263
+ return true
264
+
265
+ // If the filter text is an array, treat it as an "allowlist":
266
+ // include the entry if the field's value is IN that array (case-insensitive).
267
+ if (Array.isArray(qRawLocal)) {
268
+ const qSet = new Set(
269
+ qRawLocal.map(v => String(v).toLowerCase().trim()).filter(Boolean),
270
+ )
271
+ if (qSet.size === 0)
272
+ return true
273
+
274
+ return fieldsLocal.some((fieldPath) => {
275
+ const raw = getByPath(entry, fieldPath)
276
+ if (raw === undefined || raw === null)
277
+ return false
278
+
279
+ // If the field itself is an array, match on any overlap
280
+ if (Array.isArray(raw)) {
281
+ return raw.some(val => qSet.has(String(val).toLowerCase()))
282
+ }
283
+
284
+ const str = typeof raw === 'string' ? raw : JSON.stringify(raw)
285
+ return qSet.has(String(str).toLowerCase())
286
+ })
287
+ }
288
+
289
+ // Otherwise, treat it as a substring match on any of the fields
290
+ const q = String(qRawLocal).trim().toLowerCase()
291
+ return fieldsLocal.some((fieldPath) => {
292
+ const raw = getByPath(entry, fieldPath)
293
+ if (raw === undefined || raw === null)
294
+ return false
295
+
296
+ // If the field is an array, check if any element includes the substring
297
+ if (Array.isArray(raw)) {
298
+ return raw.some(val => String(val).toLowerCase().includes(q))
299
+ }
300
+
301
+ const str = typeof raw === 'string' ? raw : JSON.stringify(raw)
302
+ return String(str).toLowerCase().includes(q)
303
+ })
304
+ })
305
+ }
306
+
307
+ // Build the list of filter groups to apply in order.
308
+ // Group 1 (optional): props.filter + props.filterFields (if it "has value")
309
+ const filterGroups = []
310
+ if (hasValue(qRaw)) {
311
+ filterGroups.push({ filterFields: fieldsToSearch, value: qRaw })
312
+ }
313
+
314
+ // Additional groups: props.filters (array of { filterFields: string[], value: string | string[] })
315
+ if (Array.isArray(props.filters) && props.filters.length > 0) {
316
+ props.filters.forEach((g) => {
317
+ const gFields = (Array.isArray(g?.filterFields) && g.filterFields.length > 0) ? g.filterFields : ['name']
318
+ filterGroups.push({ filterFields: gFields, value: g?.value })
319
+ })
320
+ }
321
+
322
+ // Apply all groups sequentially; if none exist, keep all data
323
+ let filtered = allData
324
+ if (filterGroups.length > 0) {
325
+ for (const g of filterGroups) {
326
+ filtered = applyFilterToData(filtered, g.value, g.filterFields)
327
+ }
328
+ }
329
+
330
+ if (props.paginated) {
331
+ return filtered
332
+ }
333
+
334
+ if (state.filterFields.length > 0) {
335
+ state.filterFields.forEach((filter) => {
336
+ filtered = filtered.filter((item) => {
337
+ if (item[filter.field] === undefined) {
338
+ return false
339
+ }
340
+ if (typeof item[filter.field] === 'string') {
341
+ return item[filter.field].toLowerCase().includes(filter.value.toLowerCase())
342
+ }
343
+ return item[filter.field] === filter.value
344
+ })
345
+ })
346
+ }
347
+
348
+ return filtered.sort((a, b) => {
349
+ const field = props.sortField
350
+ const direction = props.sortDirection === 'asc' ? 1 : -1
351
+
352
+ if (a[field] < b[field]) {
353
+ return -1 * direction
354
+ }
355
+ if (a[field] > b[field]) {
356
+ return 1 * direction
357
+ }
358
+ return 0
359
+ })
360
+ })
361
+
362
+ const loadInitialData = async () => {
363
+ state.staticSearch = new edgeFirebase.SearchStaticData()
364
+ if (state.queryField === '') {
365
+ state.queryField = props.searchFields[0].name
366
+ }
367
+ let sortFields = [{ field: state.queryField, direction: 'asc' }]
368
+ if (!props.paginatedSort.some(sort => sort.field === state.queryField)) {
369
+ sortFields.push(...props.paginatedSort)
370
+ }
371
+
372
+ sortFields = sortFields.filter(
373
+ (item, index, self) =>
374
+ index === self.findIndex(t => t.field === item.field),
375
+ )
376
+
377
+ const finalSortFields = []
378
+ sortFields.forEach((sortField) => {
379
+ console.log(sortField)
380
+ const findPaginatedSort = props.paginatedSort.find(sort => sort.field === sortField.field)
381
+ console.log(findPaginatedSort)
382
+ if (findPaginatedSort) {
383
+ finalSortFields.push(findPaginatedSort)
384
+ }
385
+ else {
386
+ finalSortFields.push(sortField)
387
+ }
388
+ })
389
+
390
+ await state.staticSearch.getData(
391
+ `${edgeGlobal.edgeState.organizationDocPath}/${props.collection}`,
392
+ searchQuery.value,
393
+ finalSortFields,
394
+ props.paginatedLimit,
395
+ )
396
+ state.staticCurrentPage = state.staticSearch.results.staticCurrentPage
397
+ console.log(state.staticSearch.results)
398
+ const initialResults = state.staticSearch.results.data || {}
399
+ state.paginatedResults = Object.values(initialResults)
400
+ }
401
+
402
+ const loadMoreData = async () => {
403
+ if (state.staticSearch?.results) {
404
+ if (state.staticSearch && !state.staticSearch.results.staticIsLastPage && !state.loadingMore) {
405
+ state.loadingMore = true
406
+ await state.staticSearch.next()
407
+ const newResults = state.staticSearch.results.data || {}
408
+ console.log(newResults)
409
+ // Append new results to paginatedResults
410
+ if (state.staticCurrentPage !== state.staticSearch.results.staticCurrentPage) {
411
+ state.paginatedResults = [
412
+ ...state.paginatedResults,
413
+ ...Object.values(newResults),
414
+ ]
415
+ }
416
+
417
+ state.staticCurrentPage = state.staticSearch.results.staticCurrentPage
418
+ }
419
+ }
420
+ state.loadingMore = false
421
+ }
422
+
423
+ watch(isVisible, async (visible) => {
424
+ if (visible) {
425
+ await loadMoreData()
426
+ }
427
+ })
428
+
429
+ watch (
430
+ () => edgeGlobal.edgeState.organizationDocPath,
431
+ async () => {
432
+ state.afterMount = false
433
+ console.log('organizationDocPath changed')
434
+ if (!props.paginated) {
435
+ if (!state.searchField) {
436
+ state.queryField = props.queryField
437
+ }
438
+ if (!state.queryValue) {
439
+ state.queryValue = props.queryValue
440
+ }
441
+ if (!state.queryOperator) {
442
+ state.queryOperator = props.queryOperator
443
+ }
444
+ console.log('start snapshot')
445
+ console.log(snapShotQuery.value)
446
+ await edgeFirebase.stopSnapshot(`${edgeGlobal.edgeState.organizationDocPath}/${props.collection}`)
447
+ await edgeFirebase.startSnapshot(`${edgeGlobal.edgeState.organizationDocPath}/${props.collection}`, snapShotQuery.value)
448
+ }
449
+ else {
450
+ await loadInitialData()
451
+ }
452
+ state.afterMount = true
453
+ },
454
+ )
455
+
456
+ onBeforeMount(async () => {
457
+ console.log('before mount')
458
+ if (!props.paginated) {
459
+ if (!state.searchField) {
460
+ state.queryField = props.queryField
461
+ }
462
+ if (!state.queryValue) {
463
+ state.queryValue = props.queryValue
464
+ }
465
+ if (!state.queryOperator) {
466
+ state.queryOperator = props.queryOperator
467
+ }
468
+
469
+ if (!edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/${props.collection}`]) {
470
+ await edgeFirebase.stopSnapshot(`${edgeGlobal.edgeState.organizationDocPath}/${props.collection}`)
471
+ await edgeFirebase.startSnapshot(`${edgeGlobal.edgeState.organizationDocPath}/${props.collection}`, snapShotQuery.value)
472
+ }
473
+ }
474
+ else {
475
+ await loadInitialData()
476
+ }
477
+ state.filterFields = props.defaultFilterFields.map(field => ({
478
+ field: field.field,
479
+ value: field.value,
480
+ }))
481
+
482
+ console.log('after mount')
483
+ console.log(filtered.value)
484
+ if (props.loadFirstIfOne && filtered.value.length === 1) {
485
+ gotoSite(filtered.value[0].docId)
486
+ }
487
+ state.afterMount = true
488
+ })
489
+
490
+ const handleScroll = async (event) => {
491
+ if (props.paginated) {
492
+ state.scrollPosition = event.target.scrollTop
493
+ const scrollContainer = event.target
494
+ if (
495
+ scrollContainer.scrollTop + scrollContainer.clientHeight
496
+ >= scrollContainer.scrollHeight - 10
497
+ ) {
498
+ // Load more data when near the bottom of the scroll container
499
+ await loadMoreData()
500
+ }
501
+ }
502
+ }
503
+
504
+ const deleteItem = (docId) => {
505
+ state.deleteDialog = true
506
+ if (props.paginated) {
507
+ state.deleteItemName = state.paginatedResults.find(item => item.docId === docId).name
508
+ }
509
+ else {
510
+ state.deleteItemName = edgeFirebase.data[`${edgeGlobal.edgeState.organizationDocPath}/${props.collection}`][docId].name
511
+ }
512
+
513
+ state.deleteItemDocId = docId
514
+ }
515
+
516
+ const deleteAction = () => {
517
+ console.log('deleteAction', state.deleteItemDocId)
518
+ edgeFirebase.removeDoc(`${edgeGlobal.edgeState.organizationDocPath}/${props.collection}`, state.deleteItemDocId)
519
+ if (props.paginated) {
520
+ state.paginatedResults = state.paginatedResults.filter(item => item.docId !== state.deleteItemDocId)
521
+ }
522
+ state.deleteDialog = false
523
+ }
524
+
525
+ watch(searchQuery, async () => {
526
+ if (props.paginated) {
527
+ state.staticSearch = new edgeFirebase.SearchStaticData()
528
+ await loadInitialData()
529
+ }
530
+ })
531
+
532
+ watch (snapShotQuery, async () => {
533
+ if (state.afterMount) {
534
+ if (!props.paginated) {
535
+ console.log('snapShotQuery changed')
536
+ await edgeFirebase.stopSnapshot(`${edgeGlobal.edgeState.organizationDocPath}/${props.collection}`)
537
+ await edgeFirebase.startSnapshot(`${edgeGlobal.edgeState.organizationDocPath}/${props.collection}`, snapShotQuery.value)
538
+ }
539
+ }
540
+ })
541
+
542
+ const scrollContainerRef = ref(null)
543
+
544
+ // Restore the scroll position in the div
545
+ const restoreScrollPosition = async () => {
546
+ const cardText = document.querySelector('.scroll-area')
547
+ // console.log(cardText)
548
+ nextTick(() => {
549
+ if (cardText) {
550
+ cardText.scrollTop = state.scrollPosition
551
+ }
552
+ })
553
+ }
554
+
555
+ // When the component is activated (coming back to this route)
556
+ onActivated(() => {
557
+ console.log('activated dashboard')
558
+ restoreScrollPosition() // Restore the scroll position when the component is activated
559
+ if (props.paginated) {
560
+ const workingDoc = edgeGlobal.edgeState.lastPaginatedDoc
561
+ if (workingDoc) {
562
+ state.paginatedResults = state.paginatedResults.map((item) => {
563
+ if (item.docId === workingDoc.docId) {
564
+ return workingDoc
565
+ }
566
+ return item
567
+ })
568
+ }
569
+ }
570
+ })
571
+
572
+ const runSearch = (field, value) => {
573
+ state.afterMount = false
574
+ state.queryField = field
575
+ state.queryValue = value
576
+ nextTick(() => {
577
+ state.afterMount = true
578
+ })
579
+ }
580
+
581
+ const searchDropDown = computed(() => {
582
+ const searchField = props.searchFields.find(field => field.name === state.queryField)
583
+ if (searchField?.choices) {
584
+ const title = searchField.choices.title
585
+ const name = searchField.name
586
+ return searchField.choices.data.map(choice => ({
587
+ name: String(choice[name]),
588
+ title: String(choice[title]),
589
+ }))
590
+ }
591
+ return null
592
+ })
593
+
594
+ const removeFilter = (field) => {
595
+ state.filterFields = state.filterFields.filter(f => f.field !== field)
596
+ }
597
+
598
+ const addFilter = (field, value) => {
599
+ if (state.filterFields.some(f => f.field === field)) {
600
+ const existingFilterKey = state.filterFields.findIndex(f => f.field === field)
601
+ state.filterFields[existingFilterKey] = { field, value }
602
+ }
603
+ else {
604
+ state.filterFields.push({ field, value })
605
+ }
606
+ if (!value) {
607
+ removeFilter(field)
608
+ }
609
+ }
610
+ </script>
611
+
612
+ <template>
613
+ <Card v-if="state.afterMount" :class="cn('mx-auto bg-muted/50 w-full flex-1 border-none shadow-none pt-2', props.class)">
614
+ <slot name="header" :add-filter="addFilter" :icon="edgeGlobal.iconFromMenu(route)" :add-title="capitalizeFirstLetter(singularize(props.collection))" :title="capitalizeFirstLetter(props.collection).replaceAll('-', ' ')">
615
+ <edge-menu class="bg-primary text-foreground rounded-none sticky top-0" :class="props.headerClass">
616
+ <template #start>
617
+ <slot name="header-start" :record-count="allData.length" :add-filter="addFilter" :icon="edgeGlobal.iconFromMenu(route)" :title="capitalizeFirstLetter(props.collection).replaceAll('-', ' ')">
618
+ <component :is="edgeGlobal.iconFromMenu(route)" class="mr-2" />
619
+ <span class="capitalize">{{ capitalizeFirstLetter(props.collection).replaceAll('-', ' ') }}</span>
620
+ </slot>
621
+ </template>
622
+ <template #center>
623
+ <slot name="header-center" :add-filter="addFilter" :filter="state.filter">
624
+ <div v-if="!props.hideSearch" class="w-full px-6">
625
+ <edge-shad-form>
626
+ <edge-shad-input
627
+ v-if="props.searchFields.length === 0"
628
+ v-model="state.filter"
629
+ label=""
630
+ name="filter"
631
+ placeholder="Search..."
632
+ />
633
+ <div v-else class="py-0 flex gap-2 w-full">
634
+ <div class="w-48">
635
+ <edge-shad-select
636
+ v-model="state.queryField"
637
+ :items="props.searchFields"
638
+ class="uppercase bg-background text-foreground"
639
+ name="search"
640
+ />
641
+ </div>
642
+ <div v-if="props.searchFields.find(field => field.name === state.queryField)?.operators">
643
+ <edge-shad-select
644
+ v-model="state.queryOperator"
645
+ :items="props.searchFields.find(field => field.name === state.queryField)?.operators"
646
+ item-title="title"
647
+ item-value="operator"
648
+ name="operator"
649
+ class="uppercase bg-background text-foreground"
650
+ />
651
+ </div>
652
+ <div class="flex-grow">
653
+ <div v-if="searchDropDown" class="py-1">
654
+ <edge-shad-combobox
655
+ v-model="state.queryValue"
656
+ :items="searchDropDown"
657
+ name="filter"
658
+ placeholder="Search For..."
659
+ class="uppercase bg-background text-foreground w-full"
660
+ />
661
+ </div>
662
+ <div v-else-if="props.searchFields.find(field => field.name === state.queryField)?.fieldType === 'date'" class="py-1">
663
+ <edge-shad-datepicker
664
+ v-model="state.queryValue"
665
+ name="filter"
666
+ placeholder="Search For..."
667
+ class="!bg-yellow-900 !text-foreground"
668
+ />
669
+ </div>
670
+ <edge-shad-input
671
+ v-else
672
+ v-model="state.queryValue"
673
+ name="filter"
674
+ class="bg-background text-foreground"
675
+ placeholder="Search For..."
676
+ />
677
+ </div>
678
+ </div>
679
+ </edge-shad-form>
680
+ </div>
681
+ </slot>
682
+ </template>
683
+ <template #end>
684
+ <slot v-if="props.paginated" name="header-end" :add-filter="addFilter" :record-count="state.staticSearch?.results?.total" :title="singularize(props.collection)">
685
+ <edge-shad-button v-if="!props.paginated" class="uppercase bg-primary" :to="`/app/dashboard/${props.collection}/new`">
686
+ Add {{ singularize(props.collection).replaceAll('-', ' ') }}
687
+ </edge-shad-button>
688
+ <span v-else>
689
+ {{ state.staticSearch.results.total.toLocaleString() }} records
690
+ </span>
691
+ </slot>
692
+ <slot v-else name="header-end" :add-filter="addFilter" :record-count="filtered.length" :title="singularize(props.collection)">
693
+ <edge-shad-button v-if="!props.paginated" class="uppercase bg-primary" :to="`/app/dashboard/${props.collection}/new`">
694
+ Add {{ singularize(props.collection).replaceAll('-', ' ') }}
695
+ </edge-shad-button>
696
+ <span v-else>
697
+ {{ state.staticSearch.results.total.toLocaleString() }} records
698
+ </span>
699
+ </slot>
700
+ </template>
701
+ </edge-menu>
702
+ </slot>
703
+ <div v-if="$slots['list-header']" class="flex flex-wrap items-center py-0 mx-8 text-sm">
704
+ <slot name="list-header" />
705
+ </div>
706
+ <CardContent
707
+ ref="scrollContainerRef"
708
+ class="p-3 w-full overflow-y-auto scroll-area"
709
+ @scroll="handleScroll"
710
+ >
711
+ <div class="flex flex-wrap items-center py-0">
712
+ <slot name="list" :filtered="filtered" :delete-item="deleteItem" :run-search="runSearch" :loading-more="state.loadingMore">
713
+ <template v-for="item in filtered" :key="item.docId">
714
+ <slot name="list-item" :item="item" :delete-item="deleteItem" :run-search="runSearch" :goto-site="gotoSite">
715
+ <div class="cursor-pointer w-full flex justify-between items-center py-1 gap-3" @click="gotoSite(item.docId)">
716
+ <div>
717
+ <Avatar class="cursor-pointer p-0 h-8 w-8 mr-2">
718
+ <FilePenLine class="h-5 w-5" />
719
+ </Avatar>
720
+ </div>
721
+ <div class="grow">
722
+ <div class="text-lg">
723
+ {{ item.name }}
724
+ </div>
725
+ </div>
726
+ <div>
727
+ <edge-shad-button
728
+ size="icon"
729
+ class="bg-slate-600 h-7 w-7"
730
+ @click.stop="deleteItem(item.docId)"
731
+ >
732
+ <Trash class="h-5 w-5" />
733
+ </edge-shad-button>
734
+ </div>
735
+ </div>
736
+ <Separator class="dark:bg-slate-600" />
737
+ </slot>
738
+ </template>
739
+ </slot>
740
+ <div ref="target" />
741
+ </div>
742
+ </CardContent>
743
+ <edge-shad-dialog
744
+ v-model="state.deleteDialog"
745
+ >
746
+ <DialogContent class="pt-10">
747
+ <DialogHeader>
748
+ <DialogTitle v-if="!props.deleteTitle" class="text-left">
749
+ Are you sure you want to delete "{{ state.deleteItemName }}"?
750
+ </DialogTitle>
751
+ <DialogTitle v-else class="text-left">
752
+ {{ props.deleteTitle }}
753
+ </DialogTitle>
754
+ <DialogDescription v-if="!props.deleteDescription">
755
+ This action cannot be undone. {{ state.deleteItemName }} will be permanently deleted.
756
+ </DialogDescription>
757
+ <DialogDescription v-else>
758
+ {{ props.deleteDescription }}
759
+ </DialogDescription>
760
+ </DialogHeader>
761
+ <DialogFooter class="pt-2 flex justify-between">
762
+ <edge-shad-button class="text-white bg-slate-800 hover:bg-slate-400" @click="state.deleteDialog = false">
763
+ Cancel
764
+ </edge-shad-button>
765
+ <edge-shad-button variant="destructive" class="text-white w-full" @click="deleteAction()">
766
+ Delete
767
+ </edge-shad-button>
768
+ </DialogFooter>
769
+ </DialogContent>
770
+ </edge-shad-dialog>
771
+ </Card>
772
+ </template>
773
+
774
+ <style lang="scss" scoped>
775
+
776
+ </style>