@conduction/nextcloud-vue 0.1.0-beta.1
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/dist/nextcloud-vue.cjs.js +10710 -0
- package/dist/nextcloud-vue.cjs.js.map +1 -0
- package/dist/nextcloud-vue.css +803 -0
- package/dist/nextcloud-vue.esm.js +10665 -0
- package/dist/nextcloud-vue.esm.js.map +1 -0
- package/package.json +63 -0
- package/src/components/CnCardGrid/CnCardGrid.vue +152 -0
- package/src/components/CnCardGrid/index.js +1 -0
- package/src/components/CnCellRenderer/CnCellRenderer.vue +132 -0
- package/src/components/CnCellRenderer/index.js +1 -0
- package/src/components/CnConfigurationCard/CnConfigurationCard.vue +77 -0
- package/src/components/CnConfigurationCard/index.js +1 -0
- package/src/components/CnDataTable/CnDataTable.vue +354 -0
- package/src/components/CnDataTable/index.js +1 -0
- package/src/components/CnDetailViewLayout/CnDetailViewLayout.vue +88 -0
- package/src/components/CnDetailViewLayout/index.js +1 -0
- package/src/components/CnEmptyState/CnEmptyState.vue +78 -0
- package/src/components/CnEmptyState/index.js +1 -0
- package/src/components/CnFacetSidebar/CnFacetSidebar.vue +223 -0
- package/src/components/CnFacetSidebar/index.js +1 -0
- package/src/components/CnFilterBar/CnFilterBar.vue +152 -0
- package/src/components/CnFilterBar/index.js +1 -0
- package/src/components/CnIndexPage/CnIndexPage.vue +682 -0
- package/src/components/CnIndexPage/index.js +1 -0
- package/src/components/CnKpiGrid/CnKpiGrid.vue +89 -0
- package/src/components/CnKpiGrid/index.js +1 -0
- package/src/components/CnListViewLayout/CnListViewLayout.vue +80 -0
- package/src/components/CnListViewLayout/index.js +1 -0
- package/src/components/CnMassActionBar/CnMassActionBar.vue +160 -0
- package/src/components/CnMassActionBar/index.js +1 -0
- package/src/components/CnMassCopyDialog/CnMassCopyDialog.vue +320 -0
- package/src/components/CnMassCopyDialog/index.js +1 -0
- package/src/components/CnMassDeleteDialog/CnMassDeleteDialog.vue +238 -0
- package/src/components/CnMassDeleteDialog/index.js +1 -0
- package/src/components/CnMassExportDialog/CnMassExportDialog.vue +190 -0
- package/src/components/CnMassExportDialog/index.js +1 -0
- package/src/components/CnMassImportDialog/CnMassImportDialog.vue +491 -0
- package/src/components/CnMassImportDialog/index.js +1 -0
- package/src/components/CnObjectCard/CnObjectCard.vue +292 -0
- package/src/components/CnObjectCard/index.js +1 -0
- package/src/components/CnPagination/CnPagination.vue +252 -0
- package/src/components/CnPagination/index.js +1 -0
- package/src/components/CnRowActions/CnRowActions.vue +73 -0
- package/src/components/CnRowActions/index.js +1 -0
- package/src/components/CnSettingsCard/CnSettingsCard.vue +92 -0
- package/src/components/CnSettingsCard/index.js +1 -0
- package/src/components/CnSettingsSection/CnSettingsSection.vue +266 -0
- package/src/components/CnSettingsSection/index.js +1 -0
- package/src/components/CnStatsBlock/CnStatsBlock.vue +366 -0
- package/src/components/CnStatsBlock/index.js +1 -0
- package/src/components/CnStatusBadge/CnStatusBadge.vue +77 -0
- package/src/components/CnStatusBadge/index.js +1 -0
- package/src/components/CnVersionInfoCard/CnVersionInfoCard.vue +312 -0
- package/src/components/CnVersionInfoCard/index.js +1 -0
- package/src/components/CnViewModeToggle/CnViewModeToggle.vue +77 -0
- package/src/components/CnViewModeToggle/index.js +1 -0
- package/src/components/index.js +25 -0
- package/src/composables/index.js +3 -0
- package/src/composables/useDetailView.js +132 -0
- package/src/composables/useListView.js +153 -0
- package/src/composables/useSubResource.js +142 -0
- package/src/css/badge.css +51 -0
- package/src/css/card.css +128 -0
- package/src/css/detail.css +68 -0
- package/src/css/index.css +8 -0
- package/src/css/layout.css +90 -0
- package/src/css/pagination.css +72 -0
- package/src/css/table.css +143 -0
- package/src/css/utilities.css +46 -0
- package/src/index.js +50 -0
- package/src/store/createSubResourcePlugin.js +135 -0
- package/src/store/index.js +3 -0
- package/src/store/plugins/auditTrails.js +17 -0
- package/src/store/plugins/files.js +186 -0
- package/src/store/plugins/index.js +4 -0
- package/src/store/plugins/lifecycle.js +180 -0
- package/src/store/plugins/relations.js +68 -0
- package/src/store/useObjectStore.js +625 -0
- package/src/types/auditTrail.d.ts +32 -0
- package/src/types/file.d.ts +23 -0
- package/src/types/index.d.ts +35 -0
- package/src/types/notification.d.ts +36 -0
- package/src/types/object.d.ts +40 -0
- package/src/types/organisation.d.ts +41 -0
- package/src/types/register.d.ts +25 -0
- package/src/types/schema.d.ts +39 -0
- package/src/types/shared.d.ts +79 -0
- package/src/types/source.d.ts +14 -0
- package/src/types/task.d.ts +31 -0
- package/src/utils/errors.js +96 -0
- package/src/utils/headers.js +44 -0
- package/src/utils/index.js +3 -0
- package/src/utils/schema.js +287 -0
|
@@ -0,0 +1,682 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="cn-index-page">
|
|
3
|
+
<!-- Header -->
|
|
4
|
+
<div class="cn-index-page__header">
|
|
5
|
+
<div class="cn-index-page__title-area">
|
|
6
|
+
<h2 class="cn-index-page__title">{{ title }}</h2>
|
|
7
|
+
<span v-if="pagination && pagination.total > 0" class="cn-index-page__count">
|
|
8
|
+
{{ countText }}
|
|
9
|
+
</span>
|
|
10
|
+
</div>
|
|
11
|
+
<div class="cn-index-page__header-actions">
|
|
12
|
+
<!-- Mass actions dropdown (shows when items selected) -->
|
|
13
|
+
<CnMassActionBar
|
|
14
|
+
v-if="selectable"
|
|
15
|
+
:selected-ids="selectedIds"
|
|
16
|
+
:count="selectedIds.length"
|
|
17
|
+
:show-import="showMassImport"
|
|
18
|
+
:show-export="showMassExport"
|
|
19
|
+
:show-copy="showMassCopy"
|
|
20
|
+
:show-delete="showMassDelete"
|
|
21
|
+
@mass-import="showImportDialog = true"
|
|
22
|
+
@mass-export="showExportDialog = true"
|
|
23
|
+
@mass-copy="showCopyDialog = true"
|
|
24
|
+
@mass-delete="showDeleteDialog = true">
|
|
25
|
+
<template #actions="{ count: selCount, selectedIds: selIds }">
|
|
26
|
+
<slot name="mass-actions" :count="selCount" :selected-ids="selIds" />
|
|
27
|
+
</template>
|
|
28
|
+
</CnMassActionBar>
|
|
29
|
+
|
|
30
|
+
<CnViewModeToggle
|
|
31
|
+
v-if="showViewToggle"
|
|
32
|
+
:value="currentViewMode"
|
|
33
|
+
@input="onViewModeChange" />
|
|
34
|
+
<slot name="header-actions" />
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<!-- Mass delete dialog -->
|
|
39
|
+
<CnMassDeleteDialog
|
|
40
|
+
v-if="showDeleteDialog"
|
|
41
|
+
ref="deleteDialog"
|
|
42
|
+
:items="selectedObjects"
|
|
43
|
+
:name-field="massActionNameField"
|
|
44
|
+
@confirm="onMassDeleteConfirm"
|
|
45
|
+
@close="showDeleteDialog = false" />
|
|
46
|
+
|
|
47
|
+
<!-- Mass copy dialog -->
|
|
48
|
+
<CnMassCopyDialog
|
|
49
|
+
v-if="showCopyDialog"
|
|
50
|
+
ref="copyDialog"
|
|
51
|
+
:items="selectedObjects"
|
|
52
|
+
:name-field="massActionNameField"
|
|
53
|
+
@confirm="onMassCopyConfirm"
|
|
54
|
+
@close="showCopyDialog = false" />
|
|
55
|
+
|
|
56
|
+
<!-- Mass export dialog -->
|
|
57
|
+
<CnMassExportDialog
|
|
58
|
+
v-if="showExportDialog"
|
|
59
|
+
ref="exportDialog"
|
|
60
|
+
:formats="exportFormats"
|
|
61
|
+
@confirm="onMassExportConfirm"
|
|
62
|
+
@close="showExportDialog = false" />
|
|
63
|
+
|
|
64
|
+
<!-- Mass import dialog -->
|
|
65
|
+
<CnMassImportDialog
|
|
66
|
+
v-if="showImportDialog"
|
|
67
|
+
ref="importDialog"
|
|
68
|
+
:options="importOptions"
|
|
69
|
+
@confirm="onMassImportConfirm"
|
|
70
|
+
@close="showImportDialog = false">
|
|
71
|
+
<template v-if="$scopedSlots['import-fields']" #fields="{ file }">
|
|
72
|
+
<slot name="import-fields" :file="file" />
|
|
73
|
+
</template>
|
|
74
|
+
</CnMassImportDialog>
|
|
75
|
+
|
|
76
|
+
<!-- Body: sidebar + main content -->
|
|
77
|
+
<div class="cn-index-page__body" :class="{ 'cn-index-page__body--with-sidebar': showSidebar }">
|
|
78
|
+
<!-- Facet sidebar -->
|
|
79
|
+
<aside v-if="showSidebar" class="cn-index-page__sidebar">
|
|
80
|
+
<slot name="sidebar">
|
|
81
|
+
<CnFacetSidebar
|
|
82
|
+
v-if="schema"
|
|
83
|
+
:schema="schema"
|
|
84
|
+
:facet-data="facetData"
|
|
85
|
+
:active-filters="activeFilters"
|
|
86
|
+
:loading="facetLoading"
|
|
87
|
+
@filter-change="$emit('filter-change', $event)"
|
|
88
|
+
@clear-all="$emit('clear-filters')" />
|
|
89
|
+
</slot>
|
|
90
|
+
</aside>
|
|
91
|
+
|
|
92
|
+
<!-- Main content area -->
|
|
93
|
+
<div class="cn-index-page__main">
|
|
94
|
+
<!-- Search bar -->
|
|
95
|
+
<div v-if="showSearch" class="cn-index-page__search">
|
|
96
|
+
<CnFilterBar
|
|
97
|
+
:search-value="searchValue"
|
|
98
|
+
:search-placeholder="searchPlaceholder"
|
|
99
|
+
:filters="inlineFilters"
|
|
100
|
+
:show-clear-all="false"
|
|
101
|
+
@search="$emit('search', $event)"
|
|
102
|
+
@filter-change="$emit('filter-change', $event)" />
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<!-- Loading state -->
|
|
106
|
+
<div v-if="loading" class="cn-index-page__loading">
|
|
107
|
+
<NcLoadingIcon :size="32" />
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
<!-- Empty state -->
|
|
111
|
+
<div v-else-if="objects.length === 0" class="cn-index-page__empty">
|
|
112
|
+
<slot name="empty">
|
|
113
|
+
<NcEmptyContent :name="emptyText">
|
|
114
|
+
<template #icon>
|
|
115
|
+
<DatabaseSearch :size="64" />
|
|
116
|
+
</template>
|
|
117
|
+
</NcEmptyContent>
|
|
118
|
+
</slot>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
<!-- Table view -->
|
|
122
|
+
<CnDataTable
|
|
123
|
+
v-else-if="currentViewMode === 'table'"
|
|
124
|
+
:schema="schema"
|
|
125
|
+
:columns="columns"
|
|
126
|
+
:rows="objects"
|
|
127
|
+
:sort-key="sortKey"
|
|
128
|
+
:sort-order="sortOrder"
|
|
129
|
+
:selectable="selectable"
|
|
130
|
+
:selected-ids="selectedIds"
|
|
131
|
+
:row-key="rowKey"
|
|
132
|
+
:empty-text="emptyText"
|
|
133
|
+
:exclude-columns="excludeColumns"
|
|
134
|
+
:include-columns="includeColumns"
|
|
135
|
+
:column-overrides="columnOverrides"
|
|
136
|
+
:row-class="rowClass"
|
|
137
|
+
@sort="$emit('sort', $event)"
|
|
138
|
+
@select="$emit('select', $event)"
|
|
139
|
+
@row-click="$emit('row-click', $event)">
|
|
140
|
+
<!-- Pass through column slots -->
|
|
141
|
+
<template
|
|
142
|
+
v-for="col in slotColumns"
|
|
143
|
+
#[`column-${col}`]="{ row, value }">
|
|
144
|
+
<slot :name="'column-' + col" :row="row" :value="value" />
|
|
145
|
+
</template>
|
|
146
|
+
|
|
147
|
+
<!-- Row actions -->
|
|
148
|
+
<template v-if="hasRowActions" #row-actions="{ row }">
|
|
149
|
+
<slot name="row-actions" :row="row">
|
|
150
|
+
<CnRowActions
|
|
151
|
+
v-if="actions.length > 0"
|
|
152
|
+
:actions="actions"
|
|
153
|
+
:row="row"
|
|
154
|
+
@action="$emit('action', $event)" />
|
|
155
|
+
</slot>
|
|
156
|
+
</template>
|
|
157
|
+
</CnDataTable>
|
|
158
|
+
|
|
159
|
+
<!-- Card view -->
|
|
160
|
+
<CnCardGrid
|
|
161
|
+
v-else
|
|
162
|
+
:objects="objects"
|
|
163
|
+
:schema="schema"
|
|
164
|
+
:selectable="selectable"
|
|
165
|
+
:selected-ids="selectedIds"
|
|
166
|
+
:row-key="rowKey"
|
|
167
|
+
:empty-text="emptyText"
|
|
168
|
+
@click="$emit('row-click', $event)"
|
|
169
|
+
@select="$emit('select', $event)">
|
|
170
|
+
<template v-if="$scopedSlots.card" #card="{ object, selected }">
|
|
171
|
+
<slot name="card" :object="object" :selected="selected" />
|
|
172
|
+
</template>
|
|
173
|
+
<template v-if="hasRowActions" #card-actions="{ object }">
|
|
174
|
+
<slot name="row-actions" :row="object">
|
|
175
|
+
<CnRowActions
|
|
176
|
+
v-if="actions.length > 0"
|
|
177
|
+
:actions="actions"
|
|
178
|
+
:row="object"
|
|
179
|
+
@action="$emit('action', $event)" />
|
|
180
|
+
</slot>
|
|
181
|
+
</template>
|
|
182
|
+
</CnCardGrid>
|
|
183
|
+
|
|
184
|
+
<!-- Pagination -->
|
|
185
|
+
<CnPagination
|
|
186
|
+
v-if="pagination && pagination.pages > 1"
|
|
187
|
+
:current-page="pagination.page || 1"
|
|
188
|
+
:total-pages="pagination.pages || 1"
|
|
189
|
+
:total-items="pagination.total || 0"
|
|
190
|
+
:current-page-size="pagination.limit || 20"
|
|
191
|
+
class="cn-index-page__pagination"
|
|
192
|
+
@page-changed="$emit('page-changed', $event)"
|
|
193
|
+
@page-size-changed="$emit('page-size-changed', $event)" />
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
</template>
|
|
198
|
+
|
|
199
|
+
<script>
|
|
200
|
+
import { NcLoadingIcon, NcEmptyContent } from '@nextcloud/vue'
|
|
201
|
+
import DatabaseSearch from 'vue-material-design-icons/DatabaseSearch.vue'
|
|
202
|
+
import { CnDataTable } from '../CnDataTable/index.js'
|
|
203
|
+
import { CnCardGrid } from '../CnCardGrid/index.js'
|
|
204
|
+
import { CnPagination } from '../CnPagination/index.js'
|
|
205
|
+
import { CnFilterBar } from '../CnFilterBar/index.js'
|
|
206
|
+
import { CnFacetSidebar } from '../CnFacetSidebar/index.js'
|
|
207
|
+
import { CnViewModeToggle } from '../CnViewModeToggle/index.js'
|
|
208
|
+
import { CnRowActions } from '../CnRowActions/index.js'
|
|
209
|
+
import { CnMassActionBar } from '../CnMassActionBar/index.js'
|
|
210
|
+
import { CnMassDeleteDialog } from '../CnMassDeleteDialog/index.js'
|
|
211
|
+
import { CnMassCopyDialog } from '../CnMassCopyDialog/index.js'
|
|
212
|
+
import { CnMassExportDialog } from '../CnMassExportDialog/index.js'
|
|
213
|
+
import { CnMassImportDialog } from '../CnMassImportDialog/index.js'
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* CnIndexPage — Top-level schema-driven index page component.
|
|
217
|
+
*
|
|
218
|
+
* Assembles all sub-components (table, cards, pagination, search, faceted
|
|
219
|
+
* sidebar, view mode toggle) into a single zero-config page. Takes a schema
|
|
220
|
+
* and objects array, then auto-generates everything.
|
|
221
|
+
*
|
|
222
|
+
* @example Minimal usage
|
|
223
|
+
* <CnIndexPage
|
|
224
|
+
* title="Publications"
|
|
225
|
+
* :schema="schema"
|
|
226
|
+
* :objects="publications"
|
|
227
|
+
* :pagination="pagination"
|
|
228
|
+
* :loading="loading"
|
|
229
|
+
* :search-value="search"
|
|
230
|
+
* @search="onSearch"
|
|
231
|
+
* @row-click="openPublication"
|
|
232
|
+
* @page-changed="onPage" />
|
|
233
|
+
*
|
|
234
|
+
* @example Full usage with sidebar, actions, mass actions
|
|
235
|
+
* <CnIndexPage
|
|
236
|
+
* ref="indexPage"
|
|
237
|
+
* title="Cases"
|
|
238
|
+
* :schema="caseSchema"
|
|
239
|
+
* :objects="cases"
|
|
240
|
+
* :pagination="pagination"
|
|
241
|
+
* :loading="loading"
|
|
242
|
+
* :search-value="search"
|
|
243
|
+
* :selected-ids="selectedIds"
|
|
244
|
+
* :facet-data="facetData"
|
|
245
|
+
* :active-filters="filters"
|
|
246
|
+
* :actions="[{ label: 'Edit', handler: editCase }]"
|
|
247
|
+
* @search="onSearch"
|
|
248
|
+
* @select="selectedIds = $event"
|
|
249
|
+
* @row-click="openCase"
|
|
250
|
+
* @mass-delete="onMassDelete"
|
|
251
|
+
* @mass-copy="onMassCopy">
|
|
252
|
+
* <template #header-actions>
|
|
253
|
+
* <NcButton type="primary" @click="createCase">New case</NcButton>
|
|
254
|
+
* </template>
|
|
255
|
+
* <template #mass-actions="{ count, selectedIds }">
|
|
256
|
+
* <NcButton @click="exportSelected(selectedIds)">Export {{ count }}</NcButton>
|
|
257
|
+
* </template>
|
|
258
|
+
* </CnIndexPage>
|
|
259
|
+
*
|
|
260
|
+
* // In methods:
|
|
261
|
+
* async onMassDelete(ids) {
|
|
262
|
+
* try {
|
|
263
|
+
* await store.massDelete(ids)
|
|
264
|
+
* this.$refs.indexPage.setDeleteResult({ success: true })
|
|
265
|
+
* } catch (e) {
|
|
266
|
+
* this.$refs.indexPage.setDeleteResult({ error: e.message })
|
|
267
|
+
* }
|
|
268
|
+
* }
|
|
269
|
+
* async onMassCopy({ ids, getName }) {
|
|
270
|
+
* try {
|
|
271
|
+
* for (const obj of this.selectedObjects) {
|
|
272
|
+
* await store.copyObject(obj.id, { title: getName(obj) })
|
|
273
|
+
* }
|
|
274
|
+
* this.$refs.indexPage.setCopyResult({ success: true })
|
|
275
|
+
* } catch (e) {
|
|
276
|
+
* this.$refs.indexPage.setCopyResult({ error: e.message })
|
|
277
|
+
* }
|
|
278
|
+
* }
|
|
279
|
+
*/
|
|
280
|
+
export default {
|
|
281
|
+
name: 'CnIndexPage',
|
|
282
|
+
|
|
283
|
+
components: {
|
|
284
|
+
NcLoadingIcon,
|
|
285
|
+
NcEmptyContent,
|
|
286
|
+
DatabaseSearch,
|
|
287
|
+
CnDataTable,
|
|
288
|
+
CnCardGrid,
|
|
289
|
+
CnPagination,
|
|
290
|
+
CnFilterBar,
|
|
291
|
+
CnFacetSidebar,
|
|
292
|
+
CnViewModeToggle,
|
|
293
|
+
CnRowActions,
|
|
294
|
+
CnMassActionBar,
|
|
295
|
+
CnMassDeleteDialog,
|
|
296
|
+
CnMassCopyDialog,
|
|
297
|
+
CnMassExportDialog,
|
|
298
|
+
CnMassImportDialog,
|
|
299
|
+
},
|
|
300
|
+
|
|
301
|
+
props: {
|
|
302
|
+
/** Page title */
|
|
303
|
+
title: {
|
|
304
|
+
type: String,
|
|
305
|
+
required: true,
|
|
306
|
+
},
|
|
307
|
+
/** Schema definition */
|
|
308
|
+
schema: {
|
|
309
|
+
type: Object,
|
|
310
|
+
default: null,
|
|
311
|
+
},
|
|
312
|
+
/** Manual column definitions (used instead of schema when provided) */
|
|
313
|
+
columns: {
|
|
314
|
+
type: Array,
|
|
315
|
+
default: () => [],
|
|
316
|
+
},
|
|
317
|
+
/** Object/row data array */
|
|
318
|
+
objects: {
|
|
319
|
+
type: Array,
|
|
320
|
+
default: () => [],
|
|
321
|
+
},
|
|
322
|
+
/** Pagination state: { page, pages, total, limit } */
|
|
323
|
+
pagination: {
|
|
324
|
+
type: Object,
|
|
325
|
+
default: null,
|
|
326
|
+
},
|
|
327
|
+
/** Whether data is loading */
|
|
328
|
+
loading: {
|
|
329
|
+
type: Boolean,
|
|
330
|
+
default: false,
|
|
331
|
+
},
|
|
332
|
+
/** Current search term */
|
|
333
|
+
searchValue: {
|
|
334
|
+
type: String,
|
|
335
|
+
default: '',
|
|
336
|
+
},
|
|
337
|
+
/** Search input placeholder */
|
|
338
|
+
searchPlaceholder: {
|
|
339
|
+
type: String,
|
|
340
|
+
default: 'Search...',
|
|
341
|
+
},
|
|
342
|
+
/** Inline filter definitions (shown in the search bar) */
|
|
343
|
+
inlineFilters: {
|
|
344
|
+
type: Array,
|
|
345
|
+
default: () => [],
|
|
346
|
+
},
|
|
347
|
+
/** Facet data from API: { fieldName: { values: [{value, count}] } } */
|
|
348
|
+
facetData: {
|
|
349
|
+
type: Object,
|
|
350
|
+
default: null,
|
|
351
|
+
},
|
|
352
|
+
/** Current active facet filters: { fieldName: [values] } */
|
|
353
|
+
activeFilters: {
|
|
354
|
+
type: Object,
|
|
355
|
+
default: () => ({}),
|
|
356
|
+
},
|
|
357
|
+
/** Whether facet data is loading */
|
|
358
|
+
facetLoading: {
|
|
359
|
+
type: Boolean,
|
|
360
|
+
default: false,
|
|
361
|
+
},
|
|
362
|
+
/** Whether rows/cards can be selected */
|
|
363
|
+
selectable: {
|
|
364
|
+
type: Boolean,
|
|
365
|
+
default: true,
|
|
366
|
+
},
|
|
367
|
+
/** Currently selected IDs */
|
|
368
|
+
selectedIds: {
|
|
369
|
+
type: Array,
|
|
370
|
+
default: () => [],
|
|
371
|
+
},
|
|
372
|
+
/** View mode: 'table' or 'cards' */
|
|
373
|
+
viewMode: {
|
|
374
|
+
type: String,
|
|
375
|
+
default: 'table',
|
|
376
|
+
validator: (v) => ['table', 'cards'].includes(v),
|
|
377
|
+
},
|
|
378
|
+
/** Current sort key */
|
|
379
|
+
sortKey: {
|
|
380
|
+
type: String,
|
|
381
|
+
default: null,
|
|
382
|
+
},
|
|
383
|
+
/** Current sort order */
|
|
384
|
+
sortOrder: {
|
|
385
|
+
type: String,
|
|
386
|
+
default: 'asc',
|
|
387
|
+
},
|
|
388
|
+
/** Unique row identifier property */
|
|
389
|
+
rowKey: {
|
|
390
|
+
type: String,
|
|
391
|
+
default: 'id',
|
|
392
|
+
},
|
|
393
|
+
/** Columns to exclude in schema mode */
|
|
394
|
+
excludeColumns: {
|
|
395
|
+
type: Array,
|
|
396
|
+
default: () => [],
|
|
397
|
+
},
|
|
398
|
+
/** Columns to include in schema mode (whitelist) */
|
|
399
|
+
includeColumns: {
|
|
400
|
+
type: Array,
|
|
401
|
+
default: null,
|
|
402
|
+
},
|
|
403
|
+
/** Per-column overrides in schema mode */
|
|
404
|
+
columnOverrides: {
|
|
405
|
+
type: Object,
|
|
406
|
+
default: () => ({}),
|
|
407
|
+
},
|
|
408
|
+
/** Row action definitions */
|
|
409
|
+
actions: {
|
|
410
|
+
type: Array,
|
|
411
|
+
default: () => [],
|
|
412
|
+
},
|
|
413
|
+
/** Text shown when no items found */
|
|
414
|
+
emptyText: {
|
|
415
|
+
type: String,
|
|
416
|
+
default: 'No items found',
|
|
417
|
+
},
|
|
418
|
+
/** Whether to show the view mode toggle */
|
|
419
|
+
showViewToggle: {
|
|
420
|
+
type: Boolean,
|
|
421
|
+
default: true,
|
|
422
|
+
},
|
|
423
|
+
/** Whether to show the search bar */
|
|
424
|
+
showSearch: {
|
|
425
|
+
type: Boolean,
|
|
426
|
+
default: true,
|
|
427
|
+
},
|
|
428
|
+
/** Function returning CSS class(es) for a row */
|
|
429
|
+
rowClass: {
|
|
430
|
+
type: Function,
|
|
431
|
+
default: null,
|
|
432
|
+
},
|
|
433
|
+
/** Whether to show the built-in mass Import action */
|
|
434
|
+
showMassImport: {
|
|
435
|
+
type: Boolean,
|
|
436
|
+
default: true,
|
|
437
|
+
},
|
|
438
|
+
/** Whether to show the built-in mass Export action */
|
|
439
|
+
showMassExport: {
|
|
440
|
+
type: Boolean,
|
|
441
|
+
default: true,
|
|
442
|
+
},
|
|
443
|
+
/** Whether to show the built-in mass Copy button */
|
|
444
|
+
showMassCopy: {
|
|
445
|
+
type: Boolean,
|
|
446
|
+
default: true,
|
|
447
|
+
},
|
|
448
|
+
/** Whether to show the built-in mass Delete button */
|
|
449
|
+
showMassDelete: {
|
|
450
|
+
type: Boolean,
|
|
451
|
+
default: true,
|
|
452
|
+
},
|
|
453
|
+
/** Property name used to display item names in mass action dialogs */
|
|
454
|
+
massActionNameField: {
|
|
455
|
+
type: String,
|
|
456
|
+
default: 'title',
|
|
457
|
+
},
|
|
458
|
+
/** Available export formats for the export dialog */
|
|
459
|
+
exportFormats: {
|
|
460
|
+
type: Array,
|
|
461
|
+
default: () => [
|
|
462
|
+
{ id: 'excel', label: 'Excel (.xlsx)' },
|
|
463
|
+
{ id: 'csv', label: 'CSV (.csv)' },
|
|
464
|
+
],
|
|
465
|
+
},
|
|
466
|
+
/** Import option definitions for the import dialog */
|
|
467
|
+
importOptions: {
|
|
468
|
+
type: Array,
|
|
469
|
+
default: () => [],
|
|
470
|
+
},
|
|
471
|
+
},
|
|
472
|
+
|
|
473
|
+
data() {
|
|
474
|
+
return {
|
|
475
|
+
currentViewMode: this.viewMode,
|
|
476
|
+
showDeleteDialog: false,
|
|
477
|
+
showCopyDialog: false,
|
|
478
|
+
showExportDialog: false,
|
|
479
|
+
showImportDialog: false,
|
|
480
|
+
}
|
|
481
|
+
},
|
|
482
|
+
|
|
483
|
+
computed: {
|
|
484
|
+
countText() {
|
|
485
|
+
if (!this.pagination) return ''
|
|
486
|
+
return `Showing ${this.objects.length} of ${this.pagination.total}`
|
|
487
|
+
},
|
|
488
|
+
|
|
489
|
+
showSidebar() {
|
|
490
|
+
return this.$scopedSlots.sidebar || this.facetData !== null
|
|
491
|
+
},
|
|
492
|
+
|
|
493
|
+
hasRowActions() {
|
|
494
|
+
return this.$scopedSlots['row-actions'] || this.actions.length > 0
|
|
495
|
+
},
|
|
496
|
+
|
|
497
|
+
/** Whether all visible items are selected */
|
|
498
|
+
allSelected() {
|
|
499
|
+
if (this.objects.length === 0 || this.selectedIds.length === 0) return false
|
|
500
|
+
return this.objects.every((o) => this.selectedIds.includes(o[this.rowKey]))
|
|
501
|
+
},
|
|
502
|
+
|
|
503
|
+
/** Full objects for the selected IDs (used by mass action dialogs) */
|
|
504
|
+
selectedObjects() {
|
|
505
|
+
return this.objects.filter((o) => this.selectedIds.includes(o[this.rowKey]))
|
|
506
|
+
},
|
|
507
|
+
|
|
508
|
+
/** Column slot names that the parent has provided (for pass-through) */
|
|
509
|
+
slotColumns() {
|
|
510
|
+
return Object.keys(this.$scopedSlots)
|
|
511
|
+
.filter((name) => name.startsWith('column-'))
|
|
512
|
+
.map((name) => name.replace('column-', ''))
|
|
513
|
+
},
|
|
514
|
+
},
|
|
515
|
+
|
|
516
|
+
watch: {
|
|
517
|
+
viewMode(val) {
|
|
518
|
+
this.currentViewMode = val
|
|
519
|
+
},
|
|
520
|
+
},
|
|
521
|
+
|
|
522
|
+
methods: {
|
|
523
|
+
onViewModeChange(mode) {
|
|
524
|
+
this.currentViewMode = mode
|
|
525
|
+
this.$emit('view-mode-change', mode)
|
|
526
|
+
},
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Handle mass delete confirm. Emits 'mass-delete' with the IDs.
|
|
530
|
+
* Parent should call `this.$refs.indexPage.setDeleteResult(...)` when done.
|
|
531
|
+
* @param {Array} ids Array of item IDs to delete
|
|
532
|
+
*/
|
|
533
|
+
onMassDeleteConfirm(ids) {
|
|
534
|
+
this.$emit('mass-delete', ids)
|
|
535
|
+
},
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Handle mass copy confirm. Emits 'mass-copy' with the payload.
|
|
539
|
+
* Parent should call `this.$refs.indexPage.setCopyResult(...)` when done.
|
|
540
|
+
* @param {{ ids: Array, getName: Function }} payload
|
|
541
|
+
*/
|
|
542
|
+
onMassCopyConfirm(payload) {
|
|
543
|
+
this.$emit('mass-copy', payload)
|
|
544
|
+
},
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Set the result of a mass delete operation. Call from parent after API call.
|
|
548
|
+
* @param {{ success?: boolean, error?: string }} resultData
|
|
549
|
+
* @public
|
|
550
|
+
*/
|
|
551
|
+
setDeleteResult(resultData) {
|
|
552
|
+
if (this.$refs.deleteDialog) {
|
|
553
|
+
this.$refs.deleteDialog.setResult(resultData)
|
|
554
|
+
}
|
|
555
|
+
},
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Set the result of a mass copy operation. Call from parent after API call.
|
|
559
|
+
* @param {{ success?: boolean, error?: string }} resultData
|
|
560
|
+
* @public
|
|
561
|
+
*/
|
|
562
|
+
setCopyResult(resultData) {
|
|
563
|
+
if (this.$refs.copyDialog) {
|
|
564
|
+
this.$refs.copyDialog.setResult(resultData)
|
|
565
|
+
}
|
|
566
|
+
},
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Handle mass export confirm.
|
|
570
|
+
* @param {{ format: string }} payload
|
|
571
|
+
*/
|
|
572
|
+
onMassExportConfirm(payload) {
|
|
573
|
+
this.$emit('mass-export', payload)
|
|
574
|
+
},
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Handle mass import confirm.
|
|
578
|
+
* @param {{ file: File, options: object }} payload
|
|
579
|
+
*/
|
|
580
|
+
onMassImportConfirm(payload) {
|
|
581
|
+
this.$emit('mass-import', payload)
|
|
582
|
+
},
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Set the result of a mass export operation.
|
|
586
|
+
* @param {{ success?: boolean, error?: string }} resultData
|
|
587
|
+
* @public
|
|
588
|
+
*/
|
|
589
|
+
setExportResult(resultData) {
|
|
590
|
+
if (this.$refs.exportDialog) {
|
|
591
|
+
this.$refs.exportDialog.setResult(resultData)
|
|
592
|
+
}
|
|
593
|
+
},
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Set the result of a mass import operation.
|
|
597
|
+
* @param {{ success?: boolean, error?: string, summary?: object }} resultData
|
|
598
|
+
* @public
|
|
599
|
+
*/
|
|
600
|
+
setImportResult(resultData) {
|
|
601
|
+
if (this.$refs.importDialog) {
|
|
602
|
+
this.$refs.importDialog.setResult(resultData)
|
|
603
|
+
}
|
|
604
|
+
},
|
|
605
|
+
},
|
|
606
|
+
}
|
|
607
|
+
</script>
|
|
608
|
+
|
|
609
|
+
<style scoped>
|
|
610
|
+
.cn-index-page {
|
|
611
|
+
padding: 20px;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
.cn-index-page__header {
|
|
615
|
+
display: flex;
|
|
616
|
+
justify-content: space-between;
|
|
617
|
+
align-items: center;
|
|
618
|
+
margin-bottom: 16px;
|
|
619
|
+
flex-wrap: wrap;
|
|
620
|
+
gap: 12px;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
.cn-index-page__title-area {
|
|
624
|
+
display: flex;
|
|
625
|
+
align-items: baseline;
|
|
626
|
+
gap: 8px;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
.cn-index-page__title {
|
|
630
|
+
margin: 0;
|
|
631
|
+
font-size: 22px;
|
|
632
|
+
font-weight: 700;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
.cn-index-page__count {
|
|
636
|
+
font-size: 14px;
|
|
637
|
+
color: var(--color-text-maxcontrast);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
.cn-index-page__header-actions {
|
|
641
|
+
display: flex;
|
|
642
|
+
align-items: center;
|
|
643
|
+
gap: 8px;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
.cn-index-page__body {
|
|
647
|
+
display: flex;
|
|
648
|
+
gap: 0;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
.cn-index-page__body--with-sidebar {
|
|
652
|
+
gap: 0;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
.cn-index-page__sidebar {
|
|
656
|
+
flex-shrink: 0;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
.cn-index-page__main {
|
|
660
|
+
flex: 1;
|
|
661
|
+
min-width: 0;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
.cn-index-page__search {
|
|
665
|
+
margin-bottom: 16px;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
.cn-index-page__loading {
|
|
669
|
+
display: flex;
|
|
670
|
+
justify-content: center;
|
|
671
|
+
padding: 60px;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
.cn-index-page__empty {
|
|
675
|
+
padding: 40px 20px;
|
|
676
|
+
text-align: center;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
.cn-index-page__pagination {
|
|
680
|
+
margin-top: 16px;
|
|
681
|
+
}
|
|
682
|
+
</style>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as CnIndexPage } from './CnIndexPage.vue'
|