@michaelthielemann/kestrel 4.1.2 → 5.0.0
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/README.md +26 -201
- package/dist/boot.d.ts +45 -0
- package/dist/boot.js +274 -0
- package/dist/cast.d.ts +3 -0
- package/dist/cast.js +6 -0
- package/dist/catalogue.d.ts +11 -0
- package/dist/catalogue.js +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +55 -0
- package/dist/context.d.ts +62 -0
- package/dist/context.js +61 -0
- package/dist/dataflow.d.ts +3 -0
- package/dist/dataflow.js +65 -0
- package/dist/defineConfig.d.ts +157 -0
- package/dist/defineConfig.js +41 -0
- package/dist/defineContract.d.ts +12 -0
- package/dist/defineContract.js +16 -0
- package/dist/defineModule.d.ts +75 -0
- package/dist/defineModule.js +9 -0
- package/dist/definePipeline.d.ts +7 -0
- package/dist/definePipeline.js +11 -0
- package/dist/errors.d.ts +26 -0
- package/dist/errors.js +48 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +14 -0
- package/dist/load.d.ts +7 -0
- package/dist/load.js +79 -0
- package/dist/logger.d.ts +18 -0
- package/dist/logger.js +36 -0
- package/dist/registry.d.ts +14 -0
- package/dist/registry.js +87 -0
- package/dist/result.d.ts +19 -0
- package/dist/result.js +19 -0
- package/dist/runner.d.ts +29 -0
- package/dist/runner.js +133 -0
- package/dist/schema.d.ts +7 -0
- package/dist/schema.js +142 -0
- package/dist/sort.d.ts +2 -0
- package/dist/sort.js +50 -0
- package/dist/testing/runPipeline.d.ts +9 -0
- package/dist/testing/runPipeline.js +22 -0
- package/dist/triggers/cron.d.ts +10 -0
- package/dist/triggers/cron.js +44 -0
- package/dist/triggers/cronMatch.d.ts +10 -0
- package/dist/triggers/cronMatch.js +68 -0
- package/dist/triggers/http.d.ts +58 -0
- package/dist/triggers/http.js +292 -0
- package/package.json +25 -151
- package/NOTICE +0 -12
- package/kestrel.config.ts +0 -28
- package/layers/access/nuxt.config.ts +0 -5
- package/layers/access/server/middleware/00.ip-allowlist.ts +0 -52
- package/layers/access/server/middleware/access-guard.ts +0 -32
- package/layers/access/server/types.d.ts +0 -7
- package/layers/admin/app/components/AdminAccount.vue +0 -212
- package/layers/admin/app/components/AdminNav.vue +0 -44
- package/layers/admin/app/components/BlockFields.vue +0 -67
- package/layers/admin/app/components/BlockPreview.vue +0 -413
- package/layers/admin/app/components/BlockTree.vue +0 -433
- package/layers/admin/app/components/BlocksBody.vue +0 -163
- package/layers/admin/app/components/CollectionDeleteDialog.vue +0 -38
- package/layers/admin/app/components/CollectionEditor.vue +0 -324
- package/layers/admin/app/components/CollectionList.vue +0 -429
- package/layers/admin/app/components/CollectionListBulkBar.vue +0 -62
- package/layers/admin/app/components/CollectionListColumnsPanel.vue +0 -37
- package/layers/admin/app/components/CollectionListFilterPanel.vue +0 -145
- package/layers/admin/app/components/CollectionListPager.vue +0 -70
- package/layers/admin/app/components/CollectionListTable.vue +0 -347
- package/layers/admin/app/components/EditorStatus.vue +0 -211
- package/layers/admin/app/components/EditorUnsupported.vue +0 -28
- package/layers/admin/app/components/FieldsBody.vue +0 -23
- package/layers/admin/app/components/LocaleBar.vue +0 -107
- package/layers/admin/app/components/PageFields.vue +0 -129
- package/layers/admin/app/components/PageFieldsPane.vue +0 -39
- package/layers/admin/app/components/SeoFields.vue +0 -210
- package/layers/admin/app/components/SingletonEditor.vue +0 -93
- package/layers/admin/app/composables/useAuth.ts +0 -68
- package/layers/admin/app/composables/useBlockTree.ts +0 -95
- package/layers/admin/app/composables/useBlocks.ts +0 -7
- package/layers/admin/app/composables/useCachedResource.ts +0 -35
- package/layers/admin/app/composables/useCollectionOps.ts +0 -102
- package/layers/admin/app/composables/useCollections.ts +0 -6
- package/layers/admin/app/composables/useContentLocales.ts +0 -23
- package/layers/admin/app/composables/useEditForm.ts +0 -402
- package/layers/admin/app/composables/useLinkResolver.ts +0 -40
- package/layers/admin/app/composables/useListBatchActions.ts +0 -78
- package/layers/admin/app/composables/useListColumns.ts +0 -53
- package/layers/admin/app/composables/useListFilterDraft.ts +0 -129
- package/layers/admin/app/composables/useListRows.ts +0 -75
- package/layers/admin/app/composables/useListSelection.ts +0 -28
- package/layers/admin/app/composables/useListUrlState.ts +0 -53
- package/layers/admin/app/composables/usePendingPublishPoll.ts +0 -15
- package/layers/admin/app/composables/usePreviewViewport.ts +0 -30
- package/layers/admin/app/composables/usePublishStatus.ts +0 -107
- package/layers/admin/app/composables/useRailCollapsed.ts +0 -14
- package/layers/admin/app/composables/useTheme.ts +0 -36
- package/layers/admin/app/composables/useToolbarPanel.ts +0 -38
- package/layers/admin/app/composables/useUnsavedGuard.ts +0 -22
- package/layers/admin/app/layouts/admin.vue +0 -266
- package/layers/admin/app/middleware/admin-auth.ts +0 -4
- package/layers/admin/app/pages/admin/[collection]/[id].vue +0 -203
- package/layers/admin/app/pages/admin/[collection]/index.vue +0 -62
- package/layers/admin/app/pages/admin/index.vue +0 -118
- package/layers/admin/app/pages/admin/login.vue +0 -68
- package/layers/admin/app/pages/admin/media.vue +0 -28
- package/layers/admin/app/pages/admin/references.vue +0 -146
- package/layers/admin/app/plugins/admin-style-guard.client.ts +0 -76
- package/layers/admin/app/plugins/reauth.client.ts +0 -20
- package/layers/admin/app/utils/admin-style-guard.ts +0 -20
- package/layers/admin/app/utils/block-tree.ts +0 -214
- package/layers/admin/app/utils/collection-ops.ts +0 -69
- package/layers/admin/app/utils/dead-refs.ts +0 -30
- package/layers/admin/app/utils/edit-form.ts +0 -267
- package/layers/admin/app/utils/editor-expose.ts +0 -36
- package/layers/admin/app/utils/editor-form-context.ts +0 -82
- package/layers/admin/app/utils/editor-registry.ts +0 -20
- package/layers/admin/app/utils/field-keys.ts +0 -11
- package/layers/admin/app/utils/list-cell.ts +0 -46
- package/layers/admin/app/utils/list-columns.ts +0 -142
- package/layers/admin/app/utils/list-query.ts +0 -96
- package/layers/admin/app/utils/nav-active.ts +0 -9
- package/layers/admin/app/utils/populate-blocks.ts +0 -288
- package/layers/admin/app/utils/preview-channel.ts +0 -62
- package/layers/admin/app/utils/preview-viewport.ts +0 -80
- package/layers/admin/app/utils/reauth.ts +0 -67
- package/layers/admin/app/utils/record-title.ts +0 -15
- package/layers/admin/app/utils/register-builtin-editors.ts +0 -9
- package/layers/admin/app/utils/safe-redirect.ts +0 -12
- package/layers/admin/nuxt.config.ts +0 -20
- package/layers/auth/nuxt.config.ts +0 -1
- package/layers/auth/server/plugins/01.register-auth-pipelines.ts +0 -7
- package/layers/collections/nuxt.config.ts +0 -1
- package/layers/collections/server/plugins/01.register.ts +0 -29
- package/layers/collections/server/plugins/02.register-relation-populate.ts +0 -18
- package/layers/core/app/composables/layouts.ts +0 -5
- package/layers/core/app/utils/field-factories.ts +0 -129
- package/layers/core/app/utils/layouts.ts +0 -38
- package/layers/core/modules/auto-discovery/extract-block.ts +0 -179
- package/layers/core/modules/auto-discovery/index.ts +0 -112
- package/layers/core/modules/auto-discovery/package-registry.ts +0 -19
- package/layers/core/modules/auto-discovery/scan.ts +0 -170
- package/layers/core/modules/auto-discovery/virtual.d.ts +0 -20
- package/layers/core/modules/component-namespace/index.ts +0 -44
- package/layers/core/modules/component-namespace/shared.ts +0 -24
- package/layers/core/modules/kestrel/app-shell.ts +0 -55
- package/layers/core/modules/kestrel/auto-imports.ts +0 -44
- package/layers/core/modules/kestrel/index.ts +0 -144
- package/layers/core/modules/plugin-order/index.ts +0 -69
- package/layers/core/modules/plugin-order/plugin-order.ts +0 -273
- package/layers/core/nuxt.config.ts +0 -10
- package/layers/core/server/api/[...path].ts +0 -98
- package/layers/core/server/plugins/00.config.ts +0 -12
- package/layers/core/server/plugins/00.migrate.ts +0 -12
- package/layers/core/server/plugins/01.register-introspection-pipeline.ts +0 -5
- package/layers/core/server/plugins/01.register-openapi-pipeline.ts +0 -5
- package/layers/core/server/plugins/01.register-tooling-pipelines.ts +0 -5
- package/layers/core/server/plugins/02.schema-sync.ts +0 -58
- package/layers/core/server/plugins/04.outbox-worker.ts +0 -31
- package/layers/core/server/plugins/05.reindex-refs.ts +0 -5
- package/layers/core/server/routes/__kestrel/dashboard.get.ts +0 -29
- package/layers/core/server/tasks/db/migrate-module.ts +0 -83
- package/layers/core/server/tasks/db/migrate-revisions.ts +0 -20
- package/layers/core/server/tasks/db/migrate.ts +0 -50
- package/layers/core/server/utils/server-config.ts +0 -81
- package/layers/fields/nuxt.config.ts +0 -1
- package/layers/fields/server/plugins/01.register-blocks-pipeline.ts +0 -7
- package/layers/fields/server/plugins/01.register-field-populate.ts +0 -11
- package/layers/media/app/components/KestrelImg.vue +0 -94
- package/layers/media/app/components/MediaConflictDialog.vue +0 -35
- package/layers/media/app/components/MediaDeleteDialog.vue +0 -52
- package/layers/media/app/components/MediaGrid.vue +0 -113
- package/layers/media/app/components/MediaImage.vue +0 -70
- package/layers/media/app/components/MediaLibrary.vue +0 -456
- package/layers/media/app/components/MediaNewFolderDialog.vue +0 -32
- package/layers/media/app/components/MediaPathBar.vue +0 -163
- package/layers/media/app/components/MediaPicker.vue +0 -19
- package/layers/media/app/components/MediaRenameDialog.vue +0 -32
- package/layers/media/app/components/MediaTable.vue +0 -75
- package/layers/media/app/components/MediaThumb.vue +0 -25
- package/layers/media/app/components/MediaToolbar.vue +0 -90
- package/layers/media/app/components/MediaUploadDialog.vue +0 -87
- package/layers/media/app/components/MediaViewer.vue +0 -137
- package/layers/media/app/components/field/Media.vue +0 -263
- package/layers/media/app/composables/useBlurUp.ts +0 -55
- package/layers/media/app/composables/useMediaClipboard.ts +0 -14
- package/layers/media/app/composables/useMediaContextMenu.ts +0 -56
- package/layers/media/app/composables/useMediaDnd.ts +0 -56
- package/layers/media/app/composables/useMediaLibrary.ts +0 -152
- package/layers/media/app/composables/useMediaOps.ts +0 -50
- package/layers/media/app/composables/useMediaRelocate.ts +0 -39
- package/layers/media/app/composables/useMediaResolver.ts +0 -50
- package/layers/media/app/composables/useMediaUpload.ts +0 -93
- package/layers/media/app/composables/useMediaVariant.ts +0 -34
- package/layers/media/app/plugins/field-media.client.ts +0 -6
- package/layers/media/app/utils/ai-disclosure.ts +0 -19
- package/layers/media/app/utils/dnd.ts +0 -69
- package/layers/media/app/utils/library.ts +0 -81
- package/layers/media/app/utils/ops.ts +0 -60
- package/layers/media/app/utils/picture-model.ts +0 -125
- package/layers/media/app/utils/thumbhash.ts +0 -19
- package/layers/media/app/utils/variant-name.ts +0 -20
- package/layers/media/nuxt.config.ts +0 -7
- package/layers/media/server/middleware/ondemand-variants.ts +0 -38
- package/layers/media/server/plugins/01.register-media-pipelines.ts +0 -7
- package/layers/media/server/plugins/02.register-media.ts +0 -22
- package/layers/media/server/plugins/04.variant-capture.ts +0 -27
- package/layers/media/server/plugins/05.media-cleanup.ts +0 -5
- package/layers/media/server/tasks/media/backfill.ts +0 -17
- package/layers/public/app/app.vue +0 -5
- package/layers/public/app/components/BlockRenderer.vue +0 -56
- package/layers/public/app/components/KestrelLink.vue +0 -26
- package/layers/public/app/components/KestrelPreviewBridge.vue +0 -133
- package/layers/public/app/composables/public-page.ts +0 -24
- package/layers/public/app/error.vue +0 -12
- package/layers/public/app/layouts/default.vue +0 -5
- package/layers/public/app/pages/[...slug].vue +0 -239
- package/layers/public/app/pages/__kestrel/preview.vue +0 -46
- package/layers/public/app/utils/block-edit-context.ts +0 -16
- package/layers/public/app/utils/json-ld.ts +0 -139
- package/layers/public/app/utils/link-href.ts +0 -46
- package/layers/public/app/utils/page-head.ts +0 -84
- package/layers/public/app/utils/page-layout.ts +0 -18
- package/layers/public/app/utils/preview-protocol.ts +0 -117
- package/layers/public/app/utils/public-route.ts +0 -46
- package/layers/public/app/utils/site-head.ts +0 -43
- package/layers/public/modules/deploy-output/index.ts +0 -80
- package/layers/public/modules/prerender-routes/discover.ts +0 -37
- package/layers/public/modules/prerender-routes/index.ts +0 -88
- package/layers/public/modules/prerender-routes/routes.ts +0 -16
- package/layers/public/modules/prune-media/index.ts +0 -48
- package/layers/public/modules/prune-media/prune-media.ts +0 -81
- package/layers/public/nuxt.config.ts +0 -14
- package/layers/public/server/delivery-live/serve.ts +0 -111
- package/layers/public/server/middleware/delivery-live-catchall.ts +0 -7
- package/layers/public/server/pipelines/index.ts +0 -23
- package/layers/public/server/plugins/00.ensure-snapshot-triggers.ts +0 -14
- package/layers/public/server/plugins/01.register-public-pipelines.ts +0 -7
- package/layers/public/server/plugins/02.register-links.ts +0 -16
- package/layers/public/server/plugins/03.redirects.ts +0 -55
- package/layers/public/server/plugins/05.plan-publish.ts +0 -5
- package/layers/public/server/plugins/zz.publish.ts +0 -127
- package/layers/public/server/routes/llms-full.txt.get.ts +0 -99
- package/layers/public/server/routes/llms.txt.get.ts +0 -66
- package/layers/public/server/routes/redirects.json.get.ts +0 -58
- package/layers/public/server/routes/robots.txt.get.ts +0 -11
- package/layers/public/server/routes/sitemap.xml.get.ts +0 -79
- package/layers/public/server/tasks/publish/run.ts +0 -32
- package/layers/public/server/utils/publish/render-live.ts +0 -26
- package/layers/ui/app/assets/scss/_base.scss +0 -16
- package/layers/ui/app/assets/scss/_datepicker.scss +0 -82
- package/layers/ui/app/assets/scss/_mixins.scss +0 -50
- package/layers/ui/app/assets/scss/_reset.scss +0 -65
- package/layers/ui/app/assets/scss/_tokens.scss +0 -183
- package/layers/ui/app/assets/scss/main.scss +0 -4
- package/layers/ui/app/components/field/Boolean.vue +0 -30
- package/layers/ui/app/components/field/Choice.vue +0 -73
- package/layers/ui/app/components/field/Datetime.vue +0 -98
- package/layers/ui/app/components/field/DeadRefNote.vue +0 -28
- package/layers/ui/app/components/field/Json.vue +0 -62
- package/layers/ui/app/components/field/Layout.vue +0 -138
- package/layers/ui/app/components/field/Link.vue +0 -112
- package/layers/ui/app/components/field/LinkInternalPicker.vue +0 -95
- package/layers/ui/app/components/field/Number.vue +0 -30
- package/layers/ui/app/components/field/Relation.vue +0 -49
- package/layers/ui/app/components/field/Renderer.vue +0 -30
- package/layers/ui/app/components/field/Repeater.vue +0 -460
- package/layers/ui/app/components/field/Richtext.vue +0 -26
- package/layers/ui/app/components/field/Slug.vue +0 -65
- package/layers/ui/app/components/field/Text.vue +0 -35
- package/layers/ui/app/components/field/Unsupported.vue +0 -34
- package/layers/ui/app/components/ui/Alert.vue +0 -50
- package/layers/ui/app/components/ui/Brand.vue +0 -22
- package/layers/ui/app/components/ui/Button.vue +0 -116
- package/layers/ui/app/components/ui/ButtonGroup.vue +0 -96
- package/layers/ui/app/components/ui/Checkbox.vue +0 -53
- package/layers/ui/app/components/ui/CheckboxGroup.vue +0 -51
- package/layers/ui/app/components/ui/Combobox.vue +0 -345
- package/layers/ui/app/components/ui/DatePicker.vue +0 -92
- package/layers/ui/app/components/ui/DateRangePicker.vue +0 -100
- package/layers/ui/app/components/ui/Dialog.vue +0 -107
- package/layers/ui/app/components/ui/EmptyState.vue +0 -46
- package/layers/ui/app/components/ui/Field.vue +0 -58
- package/layers/ui/app/components/ui/Fieldset.vue +0 -65
- package/layers/ui/app/components/ui/Icon.vue +0 -44
- package/layers/ui/app/components/ui/Menu.vue +0 -54
- package/layers/ui/app/components/ui/NumberInput.vue +0 -175
- package/layers/ui/app/components/ui/Richtext.vue +0 -141
- package/layers/ui/app/components/ui/RichtextToolbar.vue +0 -251
- package/layers/ui/app/components/ui/Select.vue +0 -52
- package/layers/ui/app/components/ui/TextInput.vue +0 -111
- package/layers/ui/app/components/ui/Textarea.vue +0 -39
- package/layers/ui/app/components/ui/TimeInput.vue +0 -18
- package/layers/ui/app/components/ui/Toasts.vue +0 -103
- package/layers/ui/app/components/ui/Tooltip.vue +0 -45
- package/layers/ui/app/composables/useAdminLang.ts +0 -8
- package/layers/ui/app/composables/useDragReorder.ts +0 -41
- package/layers/ui/app/composables/useEchoGuard.ts +0 -28
- package/layers/ui/app/composables/useLinkField.ts +0 -70
- package/layers/ui/app/composables/useRecordOptions.ts +0 -84
- package/layers/ui/app/composables/useRepeater.ts +0 -99
- package/layers/ui/app/composables/useT.ts +0 -32
- package/layers/ui/app/composables/useToast.ts +0 -46
- package/layers/ui/app/i18n/de.ts +0 -432
- package/layers/ui/app/i18n/en.ts +0 -434
- package/layers/ui/app/utils/boolean-field.ts +0 -10
- package/layers/ui/app/utils/field-component.ts +0 -22
- package/layers/ui/app/utils/field-empty.ts +0 -21
- package/layers/ui/app/utils/field-registry.ts +0 -36
- package/layers/ui/app/utils/humanize.ts +0 -14
- package/layers/ui/app/utils/icons.ts +0 -143
- package/layers/ui/app/utils/iso-date.ts +0 -18
- package/layers/ui/app/utils/localized.ts +0 -13
- package/layers/ui/app/utils/record-context.ts +0 -15
- package/layers/ui/app/utils/reorder.ts +0 -8
- package/layers/ui/app/utils/richtext-preserve-class.ts +0 -31
- package/layers/ui/app/utils/useFieldA11y.ts +0 -40
- package/layers/ui/nuxt.config.ts +0 -9
- package/nuxt.config.ts +0 -108
- package/scripts/consumer-template-ci.mjs +0 -160
- package/scripts/copy-create-payload.mjs +0 -49
- package/scripts/coverage-all.mjs +0 -163
- package/scripts/dashboard.mjs +0 -89
- package/scripts/hash-password.mjs +0 -29
- package/scripts/kestrel.mjs +0 -240
- package/scripts/lib/cli.mjs +0 -129
- package/scripts/lib/coverage-model.mjs +0 -51
- package/scripts/lib/password.mjs +0 -19
- package/scripts/lib/scaffold.mjs +0 -205
- package/scripts/port-graph.mjs +0 -350
- package/scripts/typecheck.mjs +0 -62
- package/templates/starter/README.md +0 -65
- package/templates/starter/_env.example +0 -17
- package/templates/starter/_gitignore +0 -26
- package/templates/starter/_package.json +0 -22
- package/templates/starter/app/app.vue +0 -7
- package/templates/starter/app/blocks/Prose.vue +0 -13
- package/templates/starter/app/layouts/default.vue +0 -6
- package/templates/starter/nuxt.config.ts +0 -23
- package/templates/starter/pnpm-workspace.yaml +0 -8
- package/templates/starter/tsconfig.json +0 -3
|
@@ -1,429 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import type { SerializedCollection } from '@michaelthielemann/kestrel-core'
|
|
3
|
-
import { resolveLocalized } from '../../../ui/app/utils/localized'
|
|
4
|
-
import { bulkCustomActions, recordCustomActions } from '../utils/collection-ops'
|
|
5
|
-
|
|
6
|
-
// `locale` is set only for a translatable collection: it filters the list to that content locale and
|
|
7
|
-
// shows a locale switcher. New records / row links carry it so they open in the same locale.
|
|
8
|
-
const props = defineProps<{ schema: SerializedCollection; locale?: string }>()
|
|
9
|
-
|
|
10
|
-
const { t, lang } = useT()
|
|
11
|
-
const { locales } = useContentLocales()
|
|
12
|
-
|
|
13
|
-
const collection = computed(() => props.schema.name)
|
|
14
|
-
const label = computed(() => resolveLocalized(props.schema.label?.singular, lang.value) ?? props.schema.name)
|
|
15
|
-
// Prefer the collection's complete per-locale "create" phrase; fall back to the generic template.
|
|
16
|
-
const newLabel = computed(() => resolveLocalized(props.schema.label?.new, lang.value) ?? t('common.new', { label: label.value }))
|
|
17
|
-
const localeQuery = computed(() => (props.locale ? `?locale=${props.locale}` : ''))
|
|
18
|
-
|
|
19
|
-
// Configurable columns (persisted per-collection). The select + row-action columns are CHROME — fixed
|
|
20
|
-
// cells rendered outside this model, never sortable/filterable/hideable/persisted.
|
|
21
|
-
const { available, visibleColumns, visibleKeys, toggle: toggleColumn, reset: resetColumns, isDefault: columnsAreDefault } =
|
|
22
|
-
useListColumns(toRef(props, 'schema'))
|
|
23
|
-
const filterableColumns = computed(() => available.value.filter((c) => c.filterable))
|
|
24
|
-
|
|
25
|
-
// The URL is the single source of truth for committed list state. `sort`/`page`/`perPage`/`filter` are
|
|
26
|
-
// derived (read-only) computeds off `route.query`; `effectiveQuery` is the sanitized `$fetch` query; the
|
|
27
|
-
// setters navigate (page → push so Back walks pages, the rest → replace). The composable also owns the
|
|
28
|
-
// per-page cookie (a density DEFAULT that seeds perPage when the URL omits it).
|
|
29
|
-
const { sort, page, perPage, filter, effectiveQuery, setSort, setPage, clampPage, setPerPage, setFilter } =
|
|
30
|
-
useListUrlState(available)
|
|
31
|
-
|
|
32
|
-
// `filter` (above, from the URL) = the committed clauses that drive the query; the panel's pending edits
|
|
33
|
-
// live in `draft` inside the composable, which also owns the debounce and the chip model.
|
|
34
|
-
const filterDraft = useListFilterDraft({ available, filterableColumns, filter, setFilter })
|
|
35
|
-
const activeFilters = filterDraft.activeFilters
|
|
36
|
-
|
|
37
|
-
const { rows, total, quarantinedCount, error, totalPages, fetchRows } = useListRows({
|
|
38
|
-
collection,
|
|
39
|
-
effectiveQuery,
|
|
40
|
-
locale: () => props.locale,
|
|
41
|
-
page,
|
|
42
|
-
perPage,
|
|
43
|
-
clampPage,
|
|
44
|
-
// Selection is PAGE-SCOPED: any (re)fetch — page, filter, sort, per-page — replaces the visible rows, so
|
|
45
|
-
// the previous selection no longer maps to what's on screen. (A locale switch remounts the whole
|
|
46
|
-
// component, so that case clears too.)
|
|
47
|
-
onLoaded: () => clearSelection(),
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
const { selected, allSelected, headerIndeterminate, toggleRow, toggleAll, clear: clearSelection } =
|
|
51
|
-
useListSelection(rows)
|
|
52
|
-
|
|
53
|
-
// Row action = 1 id, bulk bar = N ids; the server does the batching.
|
|
54
|
-
const { busy: opsBusy, error: opsError, deleteOpen, deleteReport, askDelete, confirmDelete, duplicate: onDuplicate, setStatus, runAction } =
|
|
55
|
-
useListBatchActions(collection, fetchRows)
|
|
56
|
-
|
|
57
|
-
// Schema-driven actions beyond the built-in delete/duplicate/publish: a consumer's `definePipeline` shows
|
|
58
|
-
// up here without any UI code.
|
|
59
|
-
const bulkActions = computed(() => bulkCustomActions(props.schema.actions ?? []))
|
|
60
|
-
const rowActions = computed(() => recordCustomActions(props.schema.actions ?? []))
|
|
61
|
-
|
|
62
|
-
// Toolbar popovers (filter / columns), including the outside-click and Escape-restores-focus behaviour.
|
|
63
|
-
const { container: toolsRef, open: openPanel, toggle: togglePanel } = useToolbarPanel<'filter' | 'columns'>()
|
|
64
|
-
|
|
65
|
-
// Polite status announced after every (re)fetch so filter/sort/page changes report their result count
|
|
66
|
-
// to screen readers — WCAG 4.1.3 Status Messages.
|
|
67
|
-
const resultsLabel = computed(() => t(total.value === 1 ? 'list.result' : 'list.results', { total: total.value }))
|
|
68
|
-
// Quarantined rows failed their select schema and would otherwise be silent data loss — a chip in
|
|
69
|
-
// the header surfaces the count whenever it's non-zero.
|
|
70
|
-
const quarantinedLabel = computed(() => t(quarantinedCount.value === 1 ? 'list.quarantinedOne' : 'list.quarantinedCount', { n: quarantinedCount.value }))
|
|
71
|
-
// The ONE permanent live region. A live region only announces MUTATIONS to a node already in the tree, so
|
|
72
|
-
// the selection count is folded in here (mutating this region's text) rather than toggling a second
|
|
73
|
-
// role="status" node into the DOM — inserting an already-populated live node is typically not announced,
|
|
74
|
-
// which would silence the first selection. Selection takes precedence; with none, it reports the count.
|
|
75
|
-
const srStatus = computed(() => (selected.size ? t('list.selected', { n: selected.size }) : resultsLabel.value))
|
|
76
|
-
|
|
77
|
-
// Exactly ONE refetch per navigation: the effective (sanitized) query is stringified so an identical or
|
|
78
|
-
// junk-only URL change never refetches, and a single change fires a single fetch. Every URL-driven refetch
|
|
79
|
-
// clears the page-scoped selection inside `fetchRows`.
|
|
80
|
-
watch(() => JSON.stringify(effectiveQuery.value), fetchRows)
|
|
81
|
-
// The only host (pages/admin/[collection]/index.vue) keys this component on collection + locale, so a
|
|
82
|
-
// collection- or locale-change fully remounts it (resetting committed filter/draft/page/selection from the
|
|
83
|
-
// clean target URL) while a sort/page/perPage/filter query change does NOT remount — the derived computeds
|
|
84
|
-
// recompute in place, keeping the filter panel open and focus intact. The initial load is the
|
|
85
|
-
// `await fetchRows()` below; subsequent loads flow through the effective-query watch above.
|
|
86
|
-
await fetchRows()
|
|
87
|
-
</script>
|
|
88
|
-
|
|
89
|
-
<template>
|
|
90
|
-
<div class="list">
|
|
91
|
-
<div class="list__bar">
|
|
92
|
-
<div v-if="locale" class="list__locales" role="group" :aria-label="t('a11y.contentLocale')">
|
|
93
|
-
<NuxtLink
|
|
94
|
-
v-for="loc in locales"
|
|
95
|
-
:key="loc"
|
|
96
|
-
:to="`/admin/${collection}?locale=${loc}`"
|
|
97
|
-
class="list__locale"
|
|
98
|
-
:class="{ 'list__locale--active': loc === locale }"
|
|
99
|
-
:aria-current="loc === locale ? 'true' : undefined"
|
|
100
|
-
>{{ loc.toUpperCase() }}</NuxtLink>
|
|
101
|
-
</div>
|
|
102
|
-
|
|
103
|
-
<div ref="toolsRef" class="list__tools">
|
|
104
|
-
<div class="list__tool">
|
|
105
|
-
<KestrelUiButton
|
|
106
|
-
type="button"
|
|
107
|
-
size="sm"
|
|
108
|
-
icon="filter"
|
|
109
|
-
aria-controls="list-filter-panel"
|
|
110
|
-
:aria-expanded="openPanel === 'filter'"
|
|
111
|
-
class="list__tool-btn"
|
|
112
|
-
@click="togglePanel('filter', $event)"
|
|
113
|
-
>
|
|
114
|
-
{{ t('list.filter') }}<span v-if="activeFilters.length" class="list__tool-count">{{ activeFilters.length }}</span>
|
|
115
|
-
</KestrelUiButton>
|
|
116
|
-
<KestrelCollectionListFilterPanel
|
|
117
|
-
v-if="openPanel === 'filter'"
|
|
118
|
-
:columns="filterableColumns"
|
|
119
|
-
:filter="filterDraft"
|
|
120
|
-
:has-active="activeFilters.length > 0"
|
|
121
|
-
/>
|
|
122
|
-
</div>
|
|
123
|
-
|
|
124
|
-
<div class="list__tool">
|
|
125
|
-
<KestrelUiButton
|
|
126
|
-
type="button"
|
|
127
|
-
size="sm"
|
|
128
|
-
icon="columns"
|
|
129
|
-
aria-controls="list-columns-panel"
|
|
130
|
-
:aria-expanded="openPanel === 'columns'"
|
|
131
|
-
class="list__tool-btn"
|
|
132
|
-
@click="togglePanel('columns', $event)"
|
|
133
|
-
>{{ t('list.columns') }}</KestrelUiButton>
|
|
134
|
-
<KestrelCollectionListColumnsPanel
|
|
135
|
-
v-if="openPanel === 'columns'"
|
|
136
|
-
:columns="available"
|
|
137
|
-
:visible-keys="visibleKeys"
|
|
138
|
-
:is-default="columnsAreDefault"
|
|
139
|
-
@toggle="toggleColumn"
|
|
140
|
-
@reset="resetColumns"
|
|
141
|
-
/>
|
|
142
|
-
</div>
|
|
143
|
-
</div>
|
|
144
|
-
|
|
145
|
-
<span v-if="quarantinedCount > 0" class="list__quarantine-chip">
|
|
146
|
-
<KestrelUiIcon name="triangle-alert" :size="14" />
|
|
147
|
-
{{ quarantinedLabel }}
|
|
148
|
-
</span>
|
|
149
|
-
|
|
150
|
-
<NuxtLink :to="`/admin/${collection}/new${localeQuery}`" class="list__new">{{ newLabel }}</NuxtLink>
|
|
151
|
-
</div>
|
|
152
|
-
|
|
153
|
-
<KestrelCollectionListBulkBar
|
|
154
|
-
v-if="selected.size"
|
|
155
|
-
:count="selected.size"
|
|
156
|
-
:has-status="!!schema.status"
|
|
157
|
-
:busy="opsBusy"
|
|
158
|
-
:actions="bulkActions"
|
|
159
|
-
@set-status="(status) => setStatus([...selected], status)"
|
|
160
|
-
@delete="askDelete([...selected])"
|
|
161
|
-
@run-action="(action) => runAction(action, [...selected])"
|
|
162
|
-
@clear="clearSelection"
|
|
163
|
-
/>
|
|
164
|
-
|
|
165
|
-
<div v-if="activeFilters.length" class="list__chips">
|
|
166
|
-
<span v-for="f in activeFilters" :key="f.key" class="list__chip">
|
|
167
|
-
<span class="list__chip-label">{{ f.label }} {{ f.opLabel }}:</span> {{ f.display }}
|
|
168
|
-
<button type="button" class="list__chip-x" :aria-label="t('list.removeFilter', { name: f.label })" @click="filterDraft.clearFilter(f.key)">
|
|
169
|
-
<KestrelUiIcon name="x" :size="12" />
|
|
170
|
-
</button>
|
|
171
|
-
</span>
|
|
172
|
-
</div>
|
|
173
|
-
|
|
174
|
-
<KestrelUiAlert v-if="error" variant="error" class="list__error">
|
|
175
|
-
{{ error }}
|
|
176
|
-
<KestrelUiButton type="button" variant="secondary" size="sm" class="list__retry" @click="fetchRows">{{ t('common.retry') }}</KestrelUiButton>
|
|
177
|
-
</KestrelUiAlert>
|
|
178
|
-
|
|
179
|
-
<!-- The one scroll region: the table + its alternate empty-states scroll here while the bar, chips,
|
|
180
|
-
error and pager stay fixed (see the admin layout's scroll-ownership convention). -->
|
|
181
|
-
<div class="list__scroll">
|
|
182
|
-
<KestrelCollectionListTable
|
|
183
|
-
:rows="rows"
|
|
184
|
-
:columns="visibleColumns"
|
|
185
|
-
:collection="collection"
|
|
186
|
-
:locale-query="localeQuery"
|
|
187
|
-
:sort="sort"
|
|
188
|
-
:busy="opsBusy"
|
|
189
|
-
:selected="selected"
|
|
190
|
-
:all-selected="allSelected"
|
|
191
|
-
:header-indeterminate="headerIndeterminate"
|
|
192
|
-
:actions="rowActions"
|
|
193
|
-
@sort="setSort"
|
|
194
|
-
@toggle-row="toggleRow"
|
|
195
|
-
@toggle-all="toggleAll"
|
|
196
|
-
@duplicate="onDuplicate"
|
|
197
|
-
@delete="askDelete"
|
|
198
|
-
@run-action="(action, id) => runAction(action, [id])"
|
|
199
|
-
/>
|
|
200
|
-
|
|
201
|
-
<KestrelUiEmptyState
|
|
202
|
-
v-if="!rows.length && !error && activeFilters.length"
|
|
203
|
-
icon="filter"
|
|
204
|
-
:title="t('list.noMatch.title')"
|
|
205
|
-
:description="t('list.noMatch.desc', { name: label })"
|
|
206
|
-
>
|
|
207
|
-
<template #action>
|
|
208
|
-
<button type="button" class="list__new" @click="filterDraft.clearAll">{{ t('list.filterClearAll') }}</button>
|
|
209
|
-
</template>
|
|
210
|
-
</KestrelUiEmptyState>
|
|
211
|
-
<KestrelUiEmptyState
|
|
212
|
-
v-else-if="!rows.length && !error"
|
|
213
|
-
icon="file-text"
|
|
214
|
-
:title="t('list.empty.title')"
|
|
215
|
-
:description="t('list.empty.desc', { name: label })"
|
|
216
|
-
>
|
|
217
|
-
<template #action>
|
|
218
|
-
<NuxtLink :to="`/admin/${collection}/new${localeQuery}`" class="list__new">{{ newLabel }}</NuxtLink>
|
|
219
|
-
</template>
|
|
220
|
-
</KestrelUiEmptyState>
|
|
221
|
-
</div>
|
|
222
|
-
|
|
223
|
-
<p class="list__sr-status" role="status" aria-live="polite">{{ srStatus }}</p>
|
|
224
|
-
|
|
225
|
-
<KestrelCollectionListPager
|
|
226
|
-
:page="page"
|
|
227
|
-
:total-pages="totalPages"
|
|
228
|
-
:total="total"
|
|
229
|
-
:per-page="perPage"
|
|
230
|
-
@update:page="setPage"
|
|
231
|
-
@update:per-page="setPerPage"
|
|
232
|
-
/>
|
|
233
|
-
|
|
234
|
-
<KestrelCollectionDeleteDialog
|
|
235
|
-
:open="deleteOpen"
|
|
236
|
-
:report="deleteReport"
|
|
237
|
-
:busy="opsBusy"
|
|
238
|
-
:error="opsError"
|
|
239
|
-
@update:open="deleteOpen = $event"
|
|
240
|
-
@confirm="confirmDelete"
|
|
241
|
-
/>
|
|
242
|
-
</div>
|
|
243
|
-
</template>
|
|
244
|
-
|
|
245
|
-
<style lang="scss">
|
|
246
|
-
.list__retry {
|
|
247
|
-
margin-inline-start: var(--space-3);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// Visually-hidden polite status region (mirrors the ui sr-only mixin; admin can't @use a ui-layer scss).
|
|
251
|
-
.list__sr-status {
|
|
252
|
-
position: absolute;
|
|
253
|
-
width: 1px;
|
|
254
|
-
height: 1px;
|
|
255
|
-
padding: 0;
|
|
256
|
-
margin: -1px;
|
|
257
|
-
overflow: hidden;
|
|
258
|
-
clip: rect(0, 0, 0, 0);
|
|
259
|
-
white-space: nowrap;
|
|
260
|
-
border: 0;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// Fills the height handed down and stays fixed itself; scroll is delegated to .list__scroll. Overflow is
|
|
264
|
-
// left VISIBLE (not a scroll container) so the inline Filter/Columns popovers are never clipped.
|
|
265
|
-
.list {
|
|
266
|
-
// Frozen-column token: the actions column's sticky `left` must equal the select column's width
|
|
267
|
-
// (1.25rem checkbox + 2×var(--space-3) cell padding). Read by CollectionListTable's frozen columns —
|
|
268
|
-
// keep in sync if the checkbox size or padding change.
|
|
269
|
-
--list-col-select: 2.75rem;
|
|
270
|
-
display: flex;
|
|
271
|
-
flex-direction: column;
|
|
272
|
-
gap: var(--space-4);
|
|
273
|
-
flex: 1 1 auto;
|
|
274
|
-
min-height: 0;
|
|
275
|
-
|
|
276
|
-
// Fixed chrome around the one scroll region.
|
|
277
|
-
&__bar,
|
|
278
|
-
&__bulkbar,
|
|
279
|
-
&__chips,
|
|
280
|
-
&__error,
|
|
281
|
-
&__pager {
|
|
282
|
-
flex: 0 0 auto;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// The only scroller: the table (+ its empty-states) scroll here; the bar/chips/error/pager stay put.
|
|
286
|
-
&__scroll {
|
|
287
|
-
flex: 1 1 auto;
|
|
288
|
-
min-height: 0;
|
|
289
|
-
overflow: auto;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
&__bar {
|
|
293
|
-
display: flex;
|
|
294
|
-
align-items: center;
|
|
295
|
-
gap: var(--space-3);
|
|
296
|
-
}
|
|
297
|
-
&__locales {
|
|
298
|
-
display: flex;
|
|
299
|
-
gap: var(--space-1);
|
|
300
|
-
}
|
|
301
|
-
&__locale {
|
|
302
|
-
padding: var(--space-1) var(--space-2);
|
|
303
|
-
border: 1px solid var(--color-border);
|
|
304
|
-
border-radius: var(--radius-sm);
|
|
305
|
-
font-size: var(--text-sm);
|
|
306
|
-
font-weight: var(--weight-medium);
|
|
307
|
-
color: var(--color-text-muted);
|
|
308
|
-
text-decoration: none;
|
|
309
|
-
|
|
310
|
-
&--active {
|
|
311
|
-
background: var(--color-surface);
|
|
312
|
-
color: var(--color-text);
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
// Filter / Columns toolbar. The panels themselves live in CollectionListFilterPanel /
|
|
317
|
-
// CollectionListColumnsPanel; the shared popover chrome below styles both.
|
|
318
|
-
&__tools {
|
|
319
|
-
display: flex;
|
|
320
|
-
align-items: center;
|
|
321
|
-
gap: var(--space-2);
|
|
322
|
-
}
|
|
323
|
-
&__tool {
|
|
324
|
-
position: relative;
|
|
325
|
-
}
|
|
326
|
-
&__tool-count {
|
|
327
|
-
margin-left: var(--space-1);
|
|
328
|
-
padding: 0 var(--space-1);
|
|
329
|
-
border-radius: var(--radius-sm);
|
|
330
|
-
background: var(--color-primary-soft);
|
|
331
|
-
color: var(--color-primary);
|
|
332
|
-
font-size: var(--text-xs);
|
|
333
|
-
font-weight: var(--weight-medium);
|
|
334
|
-
}
|
|
335
|
-
&__panel {
|
|
336
|
-
position: absolute;
|
|
337
|
-
z-index: 20;
|
|
338
|
-
top: calc(100% + var(--space-1));
|
|
339
|
-
left: 0;
|
|
340
|
-
min-width: 16rem;
|
|
341
|
-
max-height: 60vh;
|
|
342
|
-
overflow-y: auto;
|
|
343
|
-
display: flex;
|
|
344
|
-
flex-direction: column;
|
|
345
|
-
gap: var(--space-2);
|
|
346
|
-
padding: var(--space-3);
|
|
347
|
-
border: 1px solid var(--color-border);
|
|
348
|
-
border-radius: var(--radius-md);
|
|
349
|
-
background: var(--color-surface);
|
|
350
|
-
box-shadow: var(--shadow-md, 0 6px 24px rgb(0 0 0 / 12%));
|
|
351
|
-
}
|
|
352
|
-
&__panel-empty {
|
|
353
|
-
margin: 0;
|
|
354
|
-
color: var(--color-text-muted);
|
|
355
|
-
font-size: var(--text-sm);
|
|
356
|
-
}
|
|
357
|
-
&__panel-action {
|
|
358
|
-
align-self: flex-start;
|
|
359
|
-
margin-top: var(--space-1);
|
|
360
|
-
padding: 0;
|
|
361
|
-
border: 0;
|
|
362
|
-
background: none;
|
|
363
|
-
color: var(--color-primary);
|
|
364
|
-
font: inherit;
|
|
365
|
-
font-size: var(--text-sm);
|
|
366
|
-
cursor: pointer;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
// Quarantine count chip — icon + text (not color alone, WCAG 1.4.1) so it reads even without color.
|
|
370
|
-
&__quarantine-chip {
|
|
371
|
-
display: inline-flex;
|
|
372
|
-
align-items: center;
|
|
373
|
-
gap: var(--space-1);
|
|
374
|
-
padding: var(--space-1) var(--space-2);
|
|
375
|
-
border-radius: var(--radius-sm);
|
|
376
|
-
background: var(--color-danger-soft, var(--color-surface));
|
|
377
|
-
color: var(--color-danger);
|
|
378
|
-
font-size: var(--text-sm);
|
|
379
|
-
font-weight: var(--weight-medium);
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
&__new {
|
|
383
|
-
margin-left: auto;
|
|
384
|
-
padding: var(--space-2) var(--space-4);
|
|
385
|
-
border-radius: var(--radius-md);
|
|
386
|
-
background: var(--color-primary);
|
|
387
|
-
color: var(--color-on-primary);
|
|
388
|
-
text-decoration: none;
|
|
389
|
-
font-size: var(--text-sm);
|
|
390
|
-
font-weight: var(--weight-medium);
|
|
391
|
-
|
|
392
|
-
&:hover {
|
|
393
|
-
background: var(--color-primary-hover);
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
// Active-filter chips
|
|
398
|
-
&__chips {
|
|
399
|
-
display: flex;
|
|
400
|
-
flex-wrap: wrap;
|
|
401
|
-
gap: var(--space-2);
|
|
402
|
-
}
|
|
403
|
-
&__chip {
|
|
404
|
-
display: inline-flex;
|
|
405
|
-
align-items: center;
|
|
406
|
-
gap: var(--space-1);
|
|
407
|
-
padding: var(--space-1) var(--space-2);
|
|
408
|
-
border: 1px solid var(--color-border);
|
|
409
|
-
border-radius: var(--radius-sm);
|
|
410
|
-
background: var(--color-surface);
|
|
411
|
-
font-size: var(--text-sm);
|
|
412
|
-
}
|
|
413
|
-
&__chip-label {
|
|
414
|
-
color: var(--color-text-muted);
|
|
415
|
-
}
|
|
416
|
-
&__chip-x {
|
|
417
|
-
display: inline-flex;
|
|
418
|
-
padding: 0;
|
|
419
|
-
border: 0;
|
|
420
|
-
background: none;
|
|
421
|
-
color: var(--color-text-muted);
|
|
422
|
-
cursor: pointer;
|
|
423
|
-
|
|
424
|
-
&:hover {
|
|
425
|
-
color: var(--color-text);
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
</style>
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
// Chrome (flex: 0 0 auto) that appears when ≥1 row is selected, shrinking the one scroll region rather
|
|
3
|
-
// than overlaying it. Publish/Unpublish are schema-driven (only when the collection has a status).
|
|
4
|
-
// NOT a live region: the selection count is announced through the list's permanent .list__sr-status
|
|
5
|
-
// region; a v-if-toggled role="status" here would double up and stay silent on first show.
|
|
6
|
-
import { resolveLocalized } from '../../../ui/app/utils/localized'
|
|
7
|
-
import type { SerializedAction } from '@michaelthielemann/kestrel-core'
|
|
8
|
-
|
|
9
|
-
const props = defineProps<{ count: number; hasStatus: boolean; busy: boolean; actions?: SerializedAction[] }>()
|
|
10
|
-
const emit = defineEmits<{ setStatus: [status: 'published' | 'draft']; delete: []; runAction: [action: SerializedAction]; clear: [] }>()
|
|
11
|
-
|
|
12
|
-
const { t, lang } = useT()
|
|
13
|
-
const actionLabel = (action: SerializedAction) => resolveLocalized(action.label, lang.value) ?? action.name
|
|
14
|
-
</script>
|
|
15
|
-
|
|
16
|
-
<template>
|
|
17
|
-
<div class="list__bulkbar">
|
|
18
|
-
<span class="list__bulk-count">{{ t('list.selected', { n: count }) }}</span>
|
|
19
|
-
<div class="list__bulk-actions">
|
|
20
|
-
<template v-if="hasStatus">
|
|
21
|
-
<KestrelUiButton type="button" size="sm" variant="secondary" :disabled="busy" @click="emit('setStatus', 'published')">{{ t('list.bulkPublish') }}</KestrelUiButton>
|
|
22
|
-
<KestrelUiButton type="button" size="sm" variant="secondary" :disabled="busy" @click="emit('setStatus', 'draft')">{{ t('list.bulkUnpublish') }}</KestrelUiButton>
|
|
23
|
-
</template>
|
|
24
|
-
<KestrelUiButton
|
|
25
|
-
v-for="action in props.actions"
|
|
26
|
-
:key="action.name"
|
|
27
|
-
type="button"
|
|
28
|
-
size="sm"
|
|
29
|
-
variant="secondary"
|
|
30
|
-
:disabled="busy"
|
|
31
|
-
:data-action="action.name"
|
|
32
|
-
@click="emit('runAction', action)"
|
|
33
|
-
>{{ actionLabel(action) }}</KestrelUiButton>
|
|
34
|
-
<KestrelUiButton type="button" size="sm" variant="danger" :disabled="busy" @click="emit('delete')">{{ t('list.bulkDelete') }}</KestrelUiButton>
|
|
35
|
-
<KestrelUiButton type="button" size="sm" variant="ghost" @click="emit('clear')">{{ t('list.clearSelection') }}</KestrelUiButton>
|
|
36
|
-
</div>
|
|
37
|
-
</div>
|
|
38
|
-
</template>
|
|
39
|
-
|
|
40
|
-
<style lang="scss">
|
|
41
|
-
.list {
|
|
42
|
-
&__bulkbar {
|
|
43
|
-
display: flex;
|
|
44
|
-
align-items: center;
|
|
45
|
-
gap: var(--space-3);
|
|
46
|
-
padding: var(--space-2) var(--space-3);
|
|
47
|
-
border: 1px solid var(--color-border);
|
|
48
|
-
border-radius: var(--radius-md);
|
|
49
|
-
background: var(--color-surface);
|
|
50
|
-
}
|
|
51
|
-
&__bulk-count {
|
|
52
|
-
font-size: var(--text-sm);
|
|
53
|
-
font-weight: var(--weight-medium);
|
|
54
|
-
}
|
|
55
|
-
&__bulk-actions {
|
|
56
|
-
display: flex;
|
|
57
|
-
align-items: center;
|
|
58
|
-
gap: var(--space-2);
|
|
59
|
-
margin-inline-start: auto;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
</style>
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import { columnLabel } from '../utils/list-cell'
|
|
3
|
-
import type { ListColumn } from '../utils/list-columns'
|
|
4
|
-
|
|
5
|
-
defineProps<{ columns: ListColumn[]; visibleKeys: string[]; isDefault: boolean }>()
|
|
6
|
-
const emit = defineEmits<{ toggle: [key: string]; reset: [] }>()
|
|
7
|
-
|
|
8
|
-
const { t } = useT()
|
|
9
|
-
const colLabel = (col: ListColumn) => columnLabel(col, t)
|
|
10
|
-
</script>
|
|
11
|
-
|
|
12
|
-
<template>
|
|
13
|
-
<div id="list-columns-panel" class="list__panel">
|
|
14
|
-
<!-- eslint-disable-next-line vuejs-accessibility/label-has-for -- native wrapping label around a custom UiCheckbox; no `for`/`id` pair needed, invisible to static analysis -->
|
|
15
|
-
<label v-for="c in columns" :key="c.key" class="list__col-row">
|
|
16
|
-
<KestrelUiCheckbox
|
|
17
|
-
class="list__col-check"
|
|
18
|
-
:data-col="c.key"
|
|
19
|
-
:model-value="visibleKeys.includes(c.key)"
|
|
20
|
-
:disabled="visibleKeys.length === 1 && visibleKeys.includes(c.key)"
|
|
21
|
-
@update:model-value="() => emit('toggle', c.key)"
|
|
22
|
-
/>
|
|
23
|
-
<span>{{ colLabel(c) }}</span>
|
|
24
|
-
</label>
|
|
25
|
-
<button v-if="!isDefault" type="button" class="list__panel-action" @click="emit('reset')">{{ t('list.columnsReset') }}</button>
|
|
26
|
-
</div>
|
|
27
|
-
</template>
|
|
28
|
-
|
|
29
|
-
<style lang="scss">
|
|
30
|
-
.list__col-row {
|
|
31
|
-
display: flex;
|
|
32
|
-
align-items: center;
|
|
33
|
-
gap: var(--space-2);
|
|
34
|
-
font-size: var(--text-sm);
|
|
35
|
-
cursor: pointer;
|
|
36
|
-
}
|
|
37
|
-
</style>
|
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import type { FilterOp } from '@michaelthielemann/kestrel-core'
|
|
3
|
-
import type { ListFilterDraft } from '../composables/useListFilterDraft'
|
|
4
|
-
import { columnLabel } from '../utils/list-cell'
|
|
5
|
-
import type { ListColumn } from '../utils/list-columns'
|
|
6
|
-
|
|
7
|
-
// The whole draft controller arrives as ONE prop: its ten members are a single cohesive thing, and the
|
|
8
|
-
// panel must share the parent's draft state (the chip row renders off the same clauses).
|
|
9
|
-
const props = defineProps<{ columns: ListColumn[]; filter: ListFilterDraft; hasActive: boolean }>()
|
|
10
|
-
|
|
11
|
-
const { t } = useT()
|
|
12
|
-
const colLabel = (col: ListColumn) => columnLabel(col, t)
|
|
13
|
-
const draft = computed(() => props.filter.draft)
|
|
14
|
-
</script>
|
|
15
|
-
|
|
16
|
-
<template>
|
|
17
|
-
<div id="list-filter-panel" class="list__panel list__panel--filter">
|
|
18
|
-
<p v-if="!columns.length" class="list__panel-empty">{{ t('list.noFilters') }}</p>
|
|
19
|
-
<div v-for="c in columns" :key="c.key" class="list__filter-row">
|
|
20
|
-
<span class="list__filter-label">{{ colLabel(c) }}</span>
|
|
21
|
-
<div class="list__filter-controls">
|
|
22
|
-
<select
|
|
23
|
-
v-if="filter.opsFor(c).length > 1"
|
|
24
|
-
class="list__filter-op"
|
|
25
|
-
:data-filter-op="c.key"
|
|
26
|
-
:aria-label="`${t('filter.operator')} — ${colLabel(c)}`"
|
|
27
|
-
:value="draft[c.key]?.op"
|
|
28
|
-
@change="filter.setOp(c.key, ($event.target as HTMLSelectElement).value as FilterOp)"
|
|
29
|
-
>
|
|
30
|
-
<option v-for="op in filter.opsFor(c)" :key="op" :value="op">{{ filter.opLabel(c.filterKind ?? 'text', op) }}</option>
|
|
31
|
-
</select>
|
|
32
|
-
|
|
33
|
-
<input
|
|
34
|
-
v-if="c.filterKind === 'datetime'"
|
|
35
|
-
type="date"
|
|
36
|
-
class="list__filter"
|
|
37
|
-
:data-filter="c.key"
|
|
38
|
-
:aria-label="colLabel(c)"
|
|
39
|
-
:value="draft[c.key]?.value ?? ''"
|
|
40
|
-
@input="filter.setValue(c.key, ($event.target as HTMLInputElement).value)"
|
|
41
|
-
>
|
|
42
|
-
<input
|
|
43
|
-
v-else-if="c.filterKind === 'number' || c.filterKind === 'ref' || c.filterKind === 'idSet'"
|
|
44
|
-
type="number"
|
|
45
|
-
class="list__filter"
|
|
46
|
-
:data-filter="c.key"
|
|
47
|
-
:aria-label="colLabel(c)"
|
|
48
|
-
:value="draft[c.key]?.value ?? ''"
|
|
49
|
-
@input="filter.setValue(c.key, ($event.target as HTMLInputElement).value)"
|
|
50
|
-
>
|
|
51
|
-
<select
|
|
52
|
-
v-else-if="c.filterKind === 'boolean'"
|
|
53
|
-
class="list__filter"
|
|
54
|
-
:data-filter="c.key"
|
|
55
|
-
:aria-label="colLabel(c)"
|
|
56
|
-
:value="draft[c.key]?.value ?? ''"
|
|
57
|
-
@change="filter.setValue(c.key, ($event.target as HTMLSelectElement).value)"
|
|
58
|
-
>
|
|
59
|
-
<option value="">—</option>
|
|
60
|
-
<option value="true">{{ t('filter.bool.true') }}</option>
|
|
61
|
-
<option value="false">{{ t('filter.bool.false') }}</option>
|
|
62
|
-
</select>
|
|
63
|
-
<!-- Single choice (`enum`) and multi choice (`stringSet`) both pick a stored VALUE from the
|
|
64
|
-
field's choices; a free-text box would let the user type the visible LABEL and match nothing. -->
|
|
65
|
-
<select
|
|
66
|
-
v-else-if="c.filterKind === 'enum' || c.filterKind === 'stringSet'"
|
|
67
|
-
class="list__filter"
|
|
68
|
-
:data-filter="c.key"
|
|
69
|
-
:aria-label="colLabel(c)"
|
|
70
|
-
:value="draft[c.key]?.value ?? ''"
|
|
71
|
-
@change="filter.setValue(c.key, ($event.target as HTMLSelectElement).value)"
|
|
72
|
-
>
|
|
73
|
-
<option value="">—</option>
|
|
74
|
-
<option v-for="o in filter.enumOptions(c)" :key="o.value" :value="o.value">{{ o.label }}</option>
|
|
75
|
-
</select>
|
|
76
|
-
<input
|
|
77
|
-
v-else
|
|
78
|
-
type="text"
|
|
79
|
-
class="list__filter"
|
|
80
|
-
:data-filter="c.key"
|
|
81
|
-
:aria-label="colLabel(c)"
|
|
82
|
-
:value="draft[c.key]?.value ?? ''"
|
|
83
|
-
:placeholder="t('list.filterPlaceholder', { name: colLabel(c) })"
|
|
84
|
-
@input="filter.setValue(c.key, ($event.target as HTMLInputElement).value)"
|
|
85
|
-
>
|
|
86
|
-
</div>
|
|
87
|
-
</div>
|
|
88
|
-
<button v-if="hasActive" type="button" class="list__panel-action" @click="filter.clearAll">{{ t('list.filterClearAll') }}</button>
|
|
89
|
-
</div>
|
|
90
|
-
</template>
|
|
91
|
-
|
|
92
|
-
<style lang="scss">
|
|
93
|
-
.list {
|
|
94
|
-
// The inline filter rows need room for name + operator + value; the columns panel stays compact.
|
|
95
|
-
&__panel--filter {
|
|
96
|
-
min-width: 30rem;
|
|
97
|
-
}
|
|
98
|
-
// Name, operator and value on ONE line: a stacked label doubled the panel's height for no information.
|
|
99
|
-
&__filter-row {
|
|
100
|
-
display: flex;
|
|
101
|
-
align-items: center;
|
|
102
|
-
gap: var(--space-2);
|
|
103
|
-
font-size: var(--text-sm);
|
|
104
|
-
}
|
|
105
|
-
// Fixed basis so the names form an aligned column and the controls all start at the same x.
|
|
106
|
-
&__filter-label {
|
|
107
|
-
flex: 0 0 6.5rem;
|
|
108
|
-
min-width: 0;
|
|
109
|
-
overflow: hidden;
|
|
110
|
-
text-overflow: ellipsis;
|
|
111
|
-
white-space: nowrap;
|
|
112
|
-
color: var(--color-text-muted);
|
|
113
|
-
}
|
|
114
|
-
// Operator select + value control; the value control takes the remaining width.
|
|
115
|
-
&__filter-controls {
|
|
116
|
-
flex: 1 1 auto;
|
|
117
|
-
min-width: 0;
|
|
118
|
-
display: flex;
|
|
119
|
-
gap: var(--space-1);
|
|
120
|
-
align-items: center;
|
|
121
|
-
}
|
|
122
|
-
&__filter-op {
|
|
123
|
-
flex: 0 0 auto;
|
|
124
|
-
max-width: 45%;
|
|
125
|
-
padding: var(--space-1) var(--space-2);
|
|
126
|
-
border: 1px solid var(--color-border);
|
|
127
|
-
border-radius: var(--radius-sm);
|
|
128
|
-
background: var(--color-surface);
|
|
129
|
-
color: var(--color-text-muted);
|
|
130
|
-
font: inherit;
|
|
131
|
-
font-size: var(--text-sm);
|
|
132
|
-
}
|
|
133
|
-
&__filter {
|
|
134
|
-
width: 100%;
|
|
135
|
-
padding: var(--space-1) var(--space-2);
|
|
136
|
-
border: 1px solid var(--color-border);
|
|
137
|
-
border-radius: var(--radius-sm);
|
|
138
|
-
font: inherit;
|
|
139
|
-
}
|
|
140
|
-
&__filter-controls .list__filter {
|
|
141
|
-
flex: 1 1 auto;
|
|
142
|
-
min-width: 0;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
</style>
|