@motor-cms/ui-admin 1.0.4 → 1.1.0-alpha.3
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/app/pages/search.vue +259 -67
- package/package.json +2 -2
package/app/pages/search.vue
CHANGED
|
@@ -1,91 +1,283 @@
|
|
|
1
1
|
<!-- app/pages/search.vue -->
|
|
2
2
|
<script setup lang="ts">
|
|
3
3
|
import type { SearchGridRow } from '@motor-cms/ui-core/app/types/search'
|
|
4
|
-
import type {
|
|
4
|
+
import type { PaginatedResponse, PaginationMeta } from '@motor-cms/ui-core/app/types/grid'
|
|
5
|
+
import { watchDebounced } from '@vueuse/core'
|
|
5
6
|
|
|
6
7
|
const { t } = useI18n()
|
|
8
|
+
const route = useRoute()
|
|
9
|
+
const router = useRouter()
|
|
10
|
+
const client = useSanctumClient()
|
|
11
|
+
const toast = useToast()
|
|
7
12
|
|
|
8
13
|
const ALL_MODULES = '_all'
|
|
9
14
|
const moduleFilter = ref(ALL_MODULES)
|
|
15
|
+
const searchInput = ref((route.query.search as string) ?? '')
|
|
16
|
+
const page = ref(1)
|
|
17
|
+
const perPage = ref(25)
|
|
18
|
+
const loading = ref(false)
|
|
19
|
+
const results = ref<SearchGridRow[]>([])
|
|
20
|
+
const meta = ref<PaginationMeta | null>(null)
|
|
10
21
|
|
|
11
22
|
const moduleFacets = useModuleFacets()
|
|
12
|
-
|
|
13
|
-
|
|
23
|
+
|
|
24
|
+
if (route.query.module && typeof route.query.module === 'string') {
|
|
25
|
+
moduleFilter.value = route.query.module
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
watch(() => route.query.search, (newSearch) => {
|
|
29
|
+
const val = (newSearch as string) ?? ''
|
|
30
|
+
if (val !== searchInput.value) {
|
|
31
|
+
searchInput.value = val
|
|
32
|
+
page.value = 1
|
|
33
|
+
doSearch(true)
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
watch(() => route.query.module, (newModule) => {
|
|
38
|
+
const val = (newModule as string) ?? ALL_MODULES
|
|
39
|
+
if (val !== moduleFilter.value) {
|
|
40
|
+
moduleFilter.value = val || ALL_MODULES
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const facetTabs = computed(() => {
|
|
45
|
+
const tabs = [{ label: t('motor-core.search.filter_all_modules'), value: ALL_MODULES, count: 0 }]
|
|
46
|
+
let totalCount = 0
|
|
14
47
|
for (const [key, count] of Object.entries(moduleFacets.value)) {
|
|
15
|
-
|
|
48
|
+
totalCount += count
|
|
49
|
+
tabs.push({ label: resolveModuleLabel(key, t), value: key, count })
|
|
16
50
|
}
|
|
17
|
-
|
|
51
|
+
tabs[0]!.count = totalCount
|
|
52
|
+
return tabs
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
async function refreshFacets() {
|
|
56
|
+
if (!searchInput.value || searchInput.value.length < 2) return
|
|
57
|
+
const fetcher = fetchSearchGrid(t, undefined, client)
|
|
58
|
+
await fetcher({ page: 1, per_page: 1, search: searchInput.value })
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function doSearch(updateFacets = false) {
|
|
62
|
+
if (!searchInput.value || searchInput.value.length < 2) {
|
|
63
|
+
results.value = []
|
|
64
|
+
meta.value = null
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
loading.value = true
|
|
69
|
+
try {
|
|
70
|
+
const activeModule = moduleFilter.value === ALL_MODULES ? undefined : moduleFilter.value
|
|
71
|
+
|
|
72
|
+
if (updateFacets && activeModule) {
|
|
73
|
+
await refreshFacets()
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const fetcher = fetchSearchGrid(t, activeModule, client)
|
|
77
|
+
const response: PaginatedResponse<SearchGridRow> = await fetcher({
|
|
78
|
+
page: page.value,
|
|
79
|
+
per_page: perPage.value,
|
|
80
|
+
search: searchInput.value
|
|
81
|
+
})
|
|
82
|
+
results.value = response.data
|
|
83
|
+
meta.value = response.meta
|
|
84
|
+
} catch {
|
|
85
|
+
results.value = []
|
|
86
|
+
meta.value = null
|
|
87
|
+
} finally {
|
|
88
|
+
loading.value = false
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
watchDebounced(searchInput, () => {
|
|
93
|
+
page.value = 1
|
|
94
|
+
doSearch(true)
|
|
95
|
+
router.replace({ query: { ...route.query, search: searchInput.value || undefined } })
|
|
96
|
+
}, { debounce: 300 })
|
|
97
|
+
|
|
98
|
+
watch(moduleFilter, () => {
|
|
99
|
+
page.value = 1
|
|
100
|
+
doSearch()
|
|
101
|
+
router.replace({
|
|
102
|
+
query: {
|
|
103
|
+
...route.query,
|
|
104
|
+
module: moduleFilter.value === ALL_MODULES ? undefined : moduleFilter.value
|
|
105
|
+
}
|
|
106
|
+
})
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
watch(page, () => doSearch())
|
|
110
|
+
watch(perPage, () => {
|
|
111
|
+
page.value = 1
|
|
112
|
+
doSearch()
|
|
18
113
|
})
|
|
19
114
|
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
{
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
115
|
+
// Builder page quicklinker
|
|
116
|
+
const quicklinkerOpen = ref(false)
|
|
117
|
+
const quicklinkerPageUuid = ref('')
|
|
118
|
+
const quicklinkerPageName = ref('')
|
|
119
|
+
|
|
120
|
+
async function handleCardAction(key: string, id: number | string | null, meta: Record<string, unknown> | undefined) {
|
|
121
|
+
if (key === 'link-navigation' && meta?.uuid) {
|
|
122
|
+
quicklinkerPageUuid.value = meta.uuid as string
|
|
123
|
+
quicklinkerPageName.value = (meta.name as string) ?? ''
|
|
124
|
+
quicklinkerOpen.value = true
|
|
125
|
+
} else if (key === 'publish' && id) {
|
|
126
|
+
try {
|
|
127
|
+
await client(`/api/v2/builder-pages/${id}/publication`, {
|
|
128
|
+
method: 'PUT',
|
|
129
|
+
body: { is_published: true }
|
|
130
|
+
})
|
|
131
|
+
toast.add({
|
|
132
|
+
title: t('motor-builder.builder_pages.toast_published'),
|
|
133
|
+
color: 'success',
|
|
134
|
+
icon: 'i-lucide-globe'
|
|
135
|
+
})
|
|
136
|
+
} catch {
|
|
137
|
+
toast.add({
|
|
138
|
+
title: t('motor-builder.builder_pages.toast_publish_error'),
|
|
139
|
+
color: 'error',
|
|
140
|
+
icon: 'i-lucide-alert-circle'
|
|
141
|
+
})
|
|
142
|
+
}
|
|
45
143
|
}
|
|
46
|
-
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
onMounted(() => {
|
|
147
|
+
if (searchInput.value) doSearch(true)
|
|
148
|
+
})
|
|
47
149
|
</script>
|
|
48
150
|
|
|
49
151
|
<template>
|
|
50
|
-
<
|
|
51
|
-
:title
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
152
|
+
<div class="flex flex-col h-full">
|
|
153
|
+
<!-- Sticky header: title + search + toolbar -->
|
|
154
|
+
<div class="sticky top-0 z-10 bg-[var(--ui-bg)] px-6 pt-6 pb-4 border-b border-[var(--ui-border)]">
|
|
155
|
+
<div class="flex items-center gap-2 mb-3">
|
|
156
|
+
<UDashboardSidebarToggle class="lg:hidden shrink-0 -ml-2" />
|
|
157
|
+
</div>
|
|
158
|
+
|
|
159
|
+
<div class="flex items-baseline gap-3 mb-4">
|
|
160
|
+
<h1 class="text-xl font-semibold">
|
|
161
|
+
{{ t('motor-core.search.title') }}
|
|
162
|
+
</h1>
|
|
163
|
+
<span class="text-sm text-dimmed hidden sm:inline">
|
|
164
|
+
{{ t('motor-core.search.subtitle') }}
|
|
165
|
+
</span>
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
<div class="mb-4">
|
|
169
|
+
<UInput
|
|
170
|
+
v-model="searchInput"
|
|
171
|
+
:placeholder="t('motor-core.search.placeholder')"
|
|
172
|
+
icon="i-lucide-search"
|
|
173
|
+
size="lg"
|
|
174
|
+
autofocus
|
|
175
|
+
class="w-full"
|
|
176
|
+
:ui="{ base: 'w-full' }"
|
|
72
177
|
/>
|
|
73
|
-
</
|
|
178
|
+
</div>
|
|
74
179
|
|
|
75
|
-
<
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
180
|
+
<div class="flex items-start gap-3">
|
|
181
|
+
<!-- Left: facet tabs -->
|
|
182
|
+
<div
|
|
183
|
+
v-if="facetTabs.length > 2"
|
|
184
|
+
class="flex flex-wrap gap-1.5 flex-1 min-w-0"
|
|
185
|
+
>
|
|
186
|
+
<UButton
|
|
187
|
+
v-for="tab in facetTabs"
|
|
188
|
+
:key="tab.value"
|
|
189
|
+
:label="tab.count > 0 ? `${tab.label} (${tab.count})` : tab.label"
|
|
190
|
+
size="sm"
|
|
191
|
+
:variant="moduleFilter === tab.value ? 'solid' : 'ghost'"
|
|
192
|
+
:color="moduleFilter === tab.value ? 'primary' : 'neutral'"
|
|
193
|
+
@click="moduleFilter = tab.value"
|
|
194
|
+
/>
|
|
195
|
+
</div>
|
|
196
|
+
|
|
197
|
+
<!-- Right: pagination -->
|
|
198
|
+
<GridPagination
|
|
199
|
+
compact
|
|
200
|
+
:meta="meta"
|
|
201
|
+
:per-page-options="[25, 50, 100]"
|
|
202
|
+
@update:page="page = $event"
|
|
203
|
+
@update:per-page="perPage = $event"
|
|
204
|
+
/>
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
|
|
208
|
+
<!-- Results -->
|
|
209
|
+
<div class="flex flex-col gap-4 px-6 pt-4 pb-8">
|
|
210
|
+
<!-- Loading skeleton -->
|
|
211
|
+
<div
|
|
212
|
+
v-if="loading"
|
|
213
|
+
class="flex flex-col gap-2"
|
|
214
|
+
>
|
|
215
|
+
<USkeleton
|
|
216
|
+
v-for="i in 5"
|
|
217
|
+
:key="i"
|
|
218
|
+
class="h-[76px] w-full rounded-[var(--ui-radius)]"
|
|
219
|
+
/>
|
|
220
|
+
</div>
|
|
221
|
+
|
|
222
|
+
<!-- Empty: no search term -->
|
|
223
|
+
<div
|
|
224
|
+
v-else-if="!searchInput || searchInput.length < 2"
|
|
225
|
+
class="flex flex-col items-center justify-center py-12 gap-3"
|
|
226
|
+
>
|
|
227
|
+
<UIcon
|
|
228
|
+
name="i-lucide-search"
|
|
229
|
+
class="size-8 text-[var(--ui-text-muted)]"
|
|
230
|
+
/>
|
|
231
|
+
<p class="text-sm font-medium text-[var(--ui-text-muted)]">
|
|
232
|
+
{{ t('motor-core.search.min_chars') }}
|
|
233
|
+
</p>
|
|
234
|
+
</div>
|
|
235
|
+
|
|
236
|
+
<!-- Empty: no results -->
|
|
237
|
+
<div
|
|
238
|
+
v-else-if="results.length === 0"
|
|
239
|
+
class="flex flex-col items-center justify-center py-12 gap-3"
|
|
240
|
+
>
|
|
241
|
+
<UIcon
|
|
242
|
+
name="i-lucide-search-x"
|
|
243
|
+
class="size-8 text-[var(--ui-text-muted)]"
|
|
244
|
+
/>
|
|
245
|
+
<p class="text-sm font-medium text-[var(--ui-text-muted)]">
|
|
246
|
+
{{ t('motor-core.search.no_results') }}
|
|
247
|
+
</p>
|
|
248
|
+
<p class="text-xs text-[var(--ui-text-dimmed)]">
|
|
249
|
+
{{ t('motor-core.search.no_results_hint') }}
|
|
250
|
+
</p>
|
|
251
|
+
</div>
|
|
252
|
+
|
|
253
|
+
<!-- Result cards -->
|
|
254
|
+
<template v-else>
|
|
255
|
+
<div class="flex flex-col gap-2 mb-6">
|
|
256
|
+
<SearchResultCard
|
|
257
|
+
v-for="row in results"
|
|
258
|
+
:key="`${row.raw_module}-${row.raw_index}-${row.id}`"
|
|
259
|
+
:title="row.title"
|
|
260
|
+
:excerpt="row.excerpt"
|
|
261
|
+
:icon="row.icon"
|
|
262
|
+
:thumbnail-url="row.thumbnail_url"
|
|
263
|
+
:module="row.module"
|
|
264
|
+
:index-label="row.index_label"
|
|
265
|
+
:to="row.to"
|
|
266
|
+
:actions="row.actions"
|
|
267
|
+
:file-id="row.raw_index === 'files' ? row.id : undefined"
|
|
268
|
+
:entity-id="row.id"
|
|
269
|
+
:entity-meta="row.entity_meta"
|
|
270
|
+
@action="handleCardAction"
|
|
80
271
|
/>
|
|
81
|
-
<p class="text-sm font-medium text-muted">
|
|
82
|
-
{{ t('motor-core.search.min_chars') }}
|
|
83
|
-
</p>
|
|
84
|
-
<p class="text-xs text-dimmed">
|
|
85
|
-
{{ t('motor-core.search.no_results_hint') }}
|
|
86
|
-
</p>
|
|
87
272
|
</div>
|
|
88
273
|
</template>
|
|
89
|
-
</
|
|
90
|
-
|
|
274
|
+
</div>
|
|
275
|
+
|
|
276
|
+
<LazyBuilderNavigationQuickLinker
|
|
277
|
+
v-if="quicklinkerOpen"
|
|
278
|
+
v-model:open="quicklinkerOpen"
|
|
279
|
+
:page-uuid="quicklinkerPageUuid"
|
|
280
|
+
:page-name="quicklinkerPageName"
|
|
281
|
+
/>
|
|
282
|
+
</div>
|
|
91
283
|
</template>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@motor-cms/ui-admin",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.1.0-alpha.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./nuxt.config.ts",
|
|
6
6
|
"files": [
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"@vueuse/core": "^14.0.0",
|
|
18
18
|
"sortablejs": "^1.15.0",
|
|
19
19
|
"zod": "^4.0.0",
|
|
20
|
-
"@motor-cms/ui-core": "1.0.
|
|
20
|
+
"@motor-cms/ui-core": "1.1.0-alpha.3"
|
|
21
21
|
},
|
|
22
22
|
"peerDependencies": {
|
|
23
23
|
"nuxt": "^4.0.0",
|