@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.
- package/.env +1 -0
- package/.env.dev +1 -0
- package/README.md +55 -20
- package/{agent.md → agents.md} +2 -0
- package/bin/cli.js +6 -6
- package/edge/components/auth/login.vue +384 -0
- package/edge/components/auth/register.vue +396 -0
- package/edge/components/auth.vue +108 -0
- package/edge/components/autoFileUpload.vue +215 -0
- package/edge/components/billing.vue +8 -0
- package/edge/components/buttonDivider.vue +14 -0
- package/edge/components/chip.vue +34 -0
- package/edge/components/clipboardButton.vue +42 -0
- package/edge/components/cms/block.vue +529 -0
- package/edge/components/cms/blockApi.vue +212 -0
- package/edge/components/cms/blockEditor.vue +725 -0
- package/edge/components/cms/blockInput.vue +66 -0
- package/edge/components/cms/blockPicker.vue +486 -0
- package/edge/components/cms/blockRender.vue +78 -0
- package/edge/components/cms/blockSheetContent.vue +28 -0
- package/edge/components/cms/codeEditor.vue +466 -0
- package/edge/components/cms/fontUpload.vue +327 -0
- package/edge/components/cms/htmlContent.vue +807 -0
- package/edge/components/cms/init_blocks/api_with_subarrays.html +17 -0
- package/edge/components/cms/init_blocks/array_with_collection.html +7 -0
- package/edge/components/cms/init_blocks/array_with_objects.html +7 -0
- package/edge/components/cms/init_blocks/carousel.html +103 -0
- package/edge/components/cms/init_blocks/contact_us.html +69 -0
- package/edge/components/cms/init_blocks/content_with_left_image.html +27 -0
- package/edge/components/cms/init_blocks/footer.html +24 -0
- package/edge/components/cms/init_blocks/header_divider.html +7 -0
- package/edge/components/cms/init_blocks/hero.html +35 -0
- package/edge/components/cms/init_blocks/hero_carousel.html +52 -0
- package/edge/components/cms/init_blocks/newsletter.html +117 -0
- package/edge/components/cms/init_blocks/post_content.html +7 -0
- package/edge/components/cms/init_blocks/post_title_header.html +21 -0
- package/edge/components/cms/init_blocks/posts_list.html +20 -0
- package/edge/components/cms/init_blocks/properties_showcase.html +100 -0
- package/edge/components/cms/init_blocks/property_carousel.html +59 -0
- package/edge/components/cms/init_blocks/property_detail.html +112 -0
- package/edge/components/cms/init_blocks/property_detail_header.html +34 -0
- package/edge/components/cms/init_blocks/property_results.html +137 -0
- package/edge/components/cms/init_blocks/property_search.html +75 -0
- package/edge/components/cms/init_blocks/simple_array.html +7 -0
- package/edge/components/cms/mediaCard.vue +116 -0
- package/edge/components/cms/mediaManager.vue +386 -0
- package/edge/components/cms/menu.vue +1103 -0
- package/edge/components/cms/optionsSelect.vue +107 -0
- package/edge/components/cms/page.vue +1785 -0
- package/edge/components/cms/posts.vue +1083 -0
- package/edge/components/cms/site.vue +1298 -0
- package/edge/components/cms/themeDefaultMenu.vue +548 -0
- package/edge/components/cms/themeEditor.vue +426 -0
- package/edge/components/dashboard.vue +776 -0
- package/edge/components/editor.vue +671 -0
- package/edge/components/fileTree.vue +72 -0
- package/edge/components/files.vue +89 -0
- package/edge/components/formSubtypes/myOrgs.vue +214 -0
- package/edge/components/formSubtypes/users.vue +336 -0
- package/edge/components/functionChips.vue +57 -0
- package/edge/components/gError.vue +98 -0
- package/edge/components/gHelper.vue +67 -0
- package/edge/components/gInput.vue +1331 -0
- package/edge/components/loggingIn.vue +41 -0
- package/edge/components/menu.vue +137 -0
- package/edge/components/menuContent.vue +132 -0
- package/edge/components/myAccount.vue +317 -0
- package/edge/components/myOrganizations.vue +75 -0
- package/edge/components/myProfile.vue +122 -0
- package/edge/components/orgSwitcher.vue +25 -0
- package/edge/components/organizationMembers.vue +522 -0
- package/edge/components/organizationSettings.vue +271 -0
- package/edge/components/shad/breadcrumbs.vue +35 -0
- package/edge/components/shad/button.vue +43 -0
- package/edge/components/shad/checkbox.vue +73 -0
- package/edge/components/shad/combobox.vue +238 -0
- package/edge/components/shad/datepicker.vue +184 -0
- package/edge/components/shad/dialog.vue +32 -0
- package/edge/components/shad/dropdownMenu.vue +54 -0
- package/edge/components/shad/dropdownMenuItem.vue +21 -0
- package/edge/components/shad/form.vue +59 -0
- package/edge/components/shad/html.vue +877 -0
- package/edge/components/shad/input.vue +139 -0
- package/edge/components/shad/number.vue +109 -0
- package/edge/components/shad/select.vue +151 -0
- package/edge/components/shad/selectTags.vue +278 -0
- package/edge/components/shad/switch.vue +67 -0
- package/edge/components/shad/tags.vue +137 -0
- package/edge/components/shad/textarea.vue +102 -0
- package/edge/components/shad/typeMoney.vue +167 -0
- package/edge/components/sideBar.vue +288 -0
- package/edge/components/sideBarContent.vue +268 -0
- package/edge/components/sidebarProvider.vue +33 -0
- package/edge/components/tooltip.vue +16 -0
- package/edge/components/userMenu.vue +148 -0
- package/edge/components/v/alert.vue +59 -0
- package/edge/components/v/alertTitle.vue +18 -0
- package/edge/components/v/card.vue +53 -0
- package/edge/components/v/cardActions.vue +18 -0
- package/edge/components/v/cardText.vue +18 -0
- package/edge/components/v/cardTitle.vue +20 -0
- package/edge/components/v/col.vue +56 -0
- package/edge/components/v/list.vue +46 -0
- package/edge/components/v/listItem.vue +26 -0
- package/edge/components/v/listItemTitle.vue +18 -0
- package/edge/components/v/row.vue +42 -0
- package/edge/components/v/toolbar.vue +24 -0
- package/edge/composables/global.ts +519 -0
- package/edge-pull.sh +2 -0
- package/edge-push.sh +1 -0
- package/edge-status.sh +14 -0
- package/firebase.json +5 -2
- package/firebase_init.sh +21 -6
- package/package.json +1 -1
- package/plugins/firebase.client.ts +1 -0
- 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>
|