@webx-ui/module-services 0.1.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/LICENSE +21 -0
- package/README.md +55 -0
- package/dist/ServiceCreateDialog.d.ts +4 -0
- package/dist/ServiceEditorPage.d.ts +17 -0
- package/dist/ServiceHistory.d.ts +2 -0
- package/dist/ServicesPage.d.ts +21 -0
- package/dist/api.d.ts +33 -0
- package/dist/editor.d.ts +19 -0
- package/dist/i18n.d.ts +2 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +1034 -0
- package/dist/index.js.map +1 -0
- package/dist/messages.d.ts +7 -0
- package/dist/module.d.ts +22 -0
- package/dist/style.css +1 -0
- package/dist/types.d.ts +96 -0
- package/package.json +66 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/api.ts","../src/editor.ts","../src/messages.ts","../src/i18n.ts","../src/ServiceEditorPage.vue","../src/ServiceHistory.vue","../src/ServiceCreateDialog.vue","../src/ServicesPage.vue","../src/module.ts"],"sourcesContent":["import { reorderItems, type AdminContext } from '@webx-ui/module-admin'\nimport type {\n ServiceDetail,\n ServiceInput,\n ServiceQuery,\n ServiceRow,\n ServiceSave,\n ServicesList,\n ServiceVersion,\n} from './types'\n\nexport interface ServicesApi {\n /** Every service, with the categories the list can be narrowed to. */\n list(query?: ServiceQuery): Promise<ServicesList>\n /** One service as its editor opens it: the record, the screen's values and the revision. */\n get(id: number): Promise<ServiceDetail>\n create(input: ServiceInput): Promise<ServiceRow>\n /**\n * Save the draft. Refused with a 409 when the revision is stale — the body is a\n * {@link ServiceConflict} with the service as it now is.\n */\n save(id: number, input: ServiceSave): Promise<ServiceDetail>\n /** Throw away what is waiting and keep what the site is showing. */\n discard(id: number): Promise<ServiceDetail>\n publish(id: number): Promise<ServiceRow>\n unpublish(id: number): Promise<ServiceRow>\n remove(id: number): Promise<void>\n restore(id: number): Promise<ServiceRow>\n /** The publications, newest first. */\n versions(id: number): Promise<ServiceVersion[]>\n /** An old publication becomes the draft; putting it on the site is a separate step. */\n restoreVersion(id: number, number: number): Promise<ServiceDetail>\n /** The order on screen: of the whole list, or — with `category` — of that category alone. */\n reorder(ids: number[], category?: number | null): Promise<void>\n}\n\n/** The path of the list under the panel's API, and of its order (`CategoryRoutes::items()`). */\nexport const SERVICES_API = 'services'\n\n/**\n * Everything under `/services`, below the panel's API path — except the categories, which are the\n * panel's shared ones and are asked for with `createCategoriesApi(admin, 'services/categories')`.\n */\nexport function createServicesApi(admin: AdminContext): ServicesApi {\n const base = `${admin.apiPath}/${SERVICES_API}`\n const data = <T>(body: { data: T }): T => body.data\n\n return {\n list: (query = {}) => {\n const search = new URLSearchParams()\n\n // Only what was asked for: `category=` would ask for the services of no category.\n if (query.q) search.set('q', query.q)\n if (query.category != null) search.set('category', String(query.category))\n if (query.status) search.set('status', query.status)\n if (query.trashed) search.set('trashed', '1')\n\n return admin.http.get<ServicesList>(search.size > 0 ? `${base}?${search}` : base)\n },\n get: (id) => admin.http.get<{ data: ServiceDetail }>(`${base}/${id}`).then(data),\n create: (input) => admin.http.post<{ data: ServiceRow }>(base, input).then(data),\n save: (id, input) => admin.http.put<{ data: ServiceDetail }>(`${base}/${id}`, input).then(data),\n discard: (id) =>\n admin.http.post<{ data: ServiceDetail }>(`${base}/${id}/discard`, {}).then(data),\n publish: (id) => admin.http.post<{ data: ServiceRow }>(`${base}/${id}/publish`, {}).then(data),\n unpublish: (id) =>\n admin.http.post<{ data: ServiceRow }>(`${base}/${id}/unpublish`, {}).then(data),\n remove: (id) => admin.http.delete<void>(`${base}/${id}`).then(() => undefined),\n restore: (id) => admin.http.post<{ data: ServiceRow }>(`${base}/${id}/restore`, {}).then(data),\n versions: (id) =>\n admin.http.get<{ data: ServiceVersion[] }>(`${base}/${id}/versions`).then(data),\n restoreVersion: (id, number) =>\n admin.http\n .post<{ data: ServiceDetail }>(`${base}/${id}/versions/${number}/restore`, {})\n .then(data),\n reorder: (ids, category = null) => reorderItems(admin, SERVICES_API, ids, category),\n }\n}\n","import { inject, provide, type InjectionKey, type Ref } from 'vue'\nimport type { ServiceRow } from './types'\n\n/**\n * What the editor knows and the nodes of `services.form` do not: the service being edited, and a\n * way to read it again. The history needs both — its list is keyed by the service, and a restored\n * version has to reach the fields the form is bound to.\n */\nexport interface ServiceEditorContext {\n /** The service as the server last answered it, or `null` while the first request is out. */\n service: Ref<ServiceRow | null>\n /** Whether this administrator may write at all. */\n canManage: boolean\n /** Ask the server for the service again — after a restore, a publication, a discard. */\n reload(): Promise<void>\n}\n\nexport const serviceEditorKey: InjectionKey<ServiceEditorContext> = Symbol('wx-service-editor')\n\nexport function provideServiceEditor(editor: ServiceEditorContext): void {\n provide(serviceEditorKey, editor)\n}\n\n/** The editor above this node, or `null` outside one — a demo or a test draws nothing. */\nexport function useServiceEditor(): ServiceEditorContext | null {\n return inject(serviceEditorKey, null)\n}\n","import type { Messages } from '@webx-ui/module-admin'\n\n/**\n * The English this package speaks on its own, before the server's dictionary arrives, or without\n * one. The server ships the same lines in ten languages under `webx-services::` and overrides\n * these; `messages.test.ts` keeps the two sets of keys equal.\n */\nexport const servicesMessages: Record<string, Messages> = {\n module: {\n group: 'Services',\n services: 'Services',\n categories: 'Categories',\n },\n panel: {\n new: 'New service',\n 'new-title': 'New service',\n 'field-title': 'Title',\n create: 'Create',\n cancel: 'Cancel',\n search: 'Search by title or address',\n empty: 'No services yet.',\n 'empty-help': 'A service is a page of the site made of blocks, filed under categories.',\n 'empty-search': 'Nothing matches that.',\n 'empty-bin': 'The bin is empty.',\n\n 'status-draft': 'Draft',\n 'status-published': 'Live',\n 'status-modified': 'Live',\n 'status-unpublished': 'Off the site',\n edits: 'edits',\n 'no-address': 'No address in this language',\n 'no-category': 'In no category',\n 'main-category': 'The main category: it goes in the breadcrumbs',\n\n 'view-all': 'All',\n 'view-published': 'Live',\n 'view-draft': 'Drafts',\n 'view-unpublished': 'Off the site',\n bin: 'Bin',\n\n 'filter-category': 'Category',\n 'any-category': 'Any category',\n\n open: 'Open',\n 'open-on-site': 'Open on the site',\n 'copy-address': 'Copy the address',\n 'address-copied': 'The address is on the clipboard.',\n publish: 'Publish',\n unpublish: 'Take off the site',\n delete: 'Delete',\n restore: 'Restore',\n 'delete-title': 'Delete “:title”?',\n 'delete-text': 'It goes to the bin and comes off the site, and its address is free again.',\n deleted: 'The service is in the bin.',\n restored: 'The service is back.',\n published: 'The service is on the site.',\n 'unpublished-done': 'The service is off the site.',\n 'reorder-failed': 'The new order was not saved.',\n },\n service: {\n trail: 'Where this service sits',\n untitled: 'Untitled',\n save: 'Save',\n 'save-failed': 'The service was not saved.',\n 'publish-title': 'Put “:title” on the site?',\n 'publish-text': 'It answers at :address from the moment you do, for everyone.',\n 'publish-nowhere': 'It has no address in this language yet, so nothing will answer.',\n preview: 'Preview',\n discard: 'Discard changes',\n 'discard-title': 'Discard what is waiting?',\n 'discard-text':\n 'The service goes back to what the site is showing. What was written since is not listed anywhere.',\n discarded: 'The service is back to what is published.',\n 'conflict-title': 'The service changed while you were editing',\n 'conflict-mine': 'Keep mine',\n 'conflict-theirs': 'Take the newer version',\n 'conflict-theirs-title': 'Give up what you wrote?',\n 'conflict-theirs-text':\n 'The service is read again as it now is, and what you have typed since goes.',\n leave: 'Leave',\n 'leave-title': 'Leave without saving?',\n 'leave-text': 'The service could not be saved, and what you wrote is not on the server.',\n 'live-since': 'On the site since :date',\n 'address-moving':\n 'The address is changing. The old one keeps working and leads to the new one.',\n 'history-empty': 'This service has never been published.',\n version: '#:number',\n 'version-live': 'On the site',\n 'source-panel': 'From the panel',\n 'source-mcp': 'By an agent',\n 'source-import': 'Imported',\n 'restore-title': 'Restore version :number?',\n 'restore-text':\n 'It becomes the draft. The site keeps showing what is published until you publish this.',\n 'restore-version': 'Restore',\n 'restored-version': 'Version :number is now the draft.',\n },\n category: {\n 'tab-content': 'Content',\n 'tab-blocks': 'Blocks',\n 'tab-cover': 'Image',\n 'tab-seo': 'SEO',\n 'field-title': 'Title',\n 'field-slug': 'Address',\n 'field-lead': 'Introduction',\n 'lead-help': 'Above the list of services on the category page.',\n visible: 'Shown on the site',\n 'visible-help':\n 'Hidden, its page answers 404 and it drops out of the index; its services go on answering at their own addresses.',\n 'cover-help':\n 'On the category card, and in social networks when the SEO card has no picture of its own.',\n new: 'New category',\n empty: 'No categories yet.',\n 'empty-help': 'A category is a section of the catalogue with a page of its own.',\n order: 'Drag to change the order of the catalogue on the site.',\n hidden: 'Hidden from the site',\n 'no-address': 'No address in this language',\n services: 'Services: :count',\n 'show-services': 'Show its services',\n 'delete-blocked': 'This category still holds services. Move them first.',\n 'delete-text': 'It goes to the bin and its page comes off the site. Its services stay.',\n deleted: 'The category is in the bin.',\n saved: 'The category is saved.',\n 'address-moving':\n 'The address is changing. The old one keeps working and leads to the new one.',\n },\n}\n","import { useAdmin } from '@webx-ui/module-admin'\nimport { servicesMessages } from './messages'\n\nconst seeded = new WeakSet<object>()\n\n/** Puts this package's English under the panel's dictionary, once per panel. */\nexport function useServicesMessages(): void {\n try {\n const { i18n } = useAdmin()\n\n if (!seeded.has(i18n)) {\n i18n.defaults('webx-services', servicesMessages)\n seeded.add(i18n)\n }\n } catch {\n // Outside a panel there is no dictionary to seed, and `useTranslate` falls back on its own.\n }\n}\n","<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'\nimport { onBeforeRouteLeave, useRoute, useRouter } from 'vue-router'\nimport {\n provideRecordAddress,\n useAdmin,\n useDates,\n useErrorText,\n useTranslate,\n WxSaveState,\n WxScreen,\n WxScreenHead,\n type ScreenAction,\n} from '@webx-ui/module-admin'\nimport { provideBlocksPreview } from '@webx-ui/module-blocks'\nimport {\n confirm,\n localizedValue,\n toast,\n useLocales,\n WxActionBar,\n WxAlert,\n WxBadge,\n WxBreadcrumb,\n WxBreadcrumbItem,\n WxButton,\n WxCard,\n WxSkeleton,\n type LocalizedValue,\n} from '@webx-ui/core'\nimport type { ScreenModel } from '@webx-ui/schema'\nimport { createServicesApi } from './api'\nimport { provideServiceEditor } from './editor'\nimport { useServicesMessages } from './i18n'\nimport type { ServiceConflict, ServiceDetail, ServiceRow } from './types'\n\n/**\n * The editor of one service: a head that stays put, the described screen under it, and the bar\n * along the bottom where it is saved and published (§4.6).\n *\n * Everything between the head and the bar is `services.form`, so `module-seo` adds its card and a\n * project its price with a patch rather than a fork of this file. Saving is by autosave — a pause\n * after the last keystroke, and the moment a field is left — into the draft; the site changes\n * only on \"Publish\". Edits are guarded by a revision: a save over somebody else's is refused with\n * a 409 and the question of which version the site gets.\n */\nconst props = withDefaults(defineProps<{ base?: string }>(), { base: '/services' })\n\nconst context = useAdmin()\nconst api = createServicesApi(context)\nconst route = useRoute()\nconst router = useRouter()\nconst locales = useLocales()\nconst dates = useDates()\nuseServicesMessages()\n\nconst t = useTranslate('webx-services')\n/* Not the server's `message`: the panel says how a request failed in its own words. */\nconst message = useErrorText()\n\n/** How long after the last keystroke the draft goes to the server. */\nconst PAUSE = 1200\n\nconst id = computed(() => Number(route.params.id))\nconst list = computed(() => props.base)\n\nconst service = ref<ServiceRow | null>(null)\nconst values = ref<ScreenModel>({})\nconst revision = ref('')\nconst prefix = ref<string | null>(null)\nconst previewUrl = ref<string | null>(null)\n\nconst loading = ref(true)\nconst saving = ref(false)\nconst working = ref(false)\nconst errors = ref<Record<string, string[]>>({})\nconst conflict = ref<ServiceConflict | null>(null)\n\nconst snapshot = ref('')\nconst reloadToken = ref(0)\n\nconst canManage = computed(() => context.can('services.manage'))\n\n/** Closed for writing: no permission, or a publication in flight. */\nconst locked = computed(() => !canManage.value || working.value)\n\nconst current = computed(() => JSON.stringify(values.value))\nconst dirty = computed(() => snapshot.value !== '' && current.value !== snapshot.value)\n\nconst state = computed<'saving' | 'unsaved' | 'saved'>(() => {\n if (saving.value) return 'saving'\n\n return dirty.value ? 'unsaved' : 'saved'\n})\n\n/** The name follows the field rather than the answer: a rename shows at the top as it is typed. */\nconst title = computed(() => {\n const written = values.value.title as LocalizedValue | string | null | undefined\n\n return localizedValue(written, locales.active.value, service.value?.title ?? '')\n})\n\n/** Since when it is on the site, under the name — the one fact the badge beside it cannot carry. */\nconst publication = computed(() => {\n const row = service.value\n\n if (!row || (row.status !== 'published' && row.status !== 'modified')) return ''\n\n return t('service.live-since', { date: dates.short(row.published_at) })\n})\n\n/* The constructor shows the draft as a page of the site. The link is the editor's to hand over:\n only this screen knows which entity it is editing. */\nprovideBlocksPreview({ url: computed(() => previewUrl.value), reload: reloadToken })\n\n/* The address field is the panel's shared one (`wx-slug`), and this is what it prints. */\nprovideRecordAddress({\n values,\n prefix,\n path: computed(() => service.value?.path),\n moving: () => t('service.address-moving'),\n})\n\nprovideServiceEditor({ service, canManage: canManage.value, reload: () => load(true) })\n\nfunction take(detail: ServiceDetail): void {\n service.value = detail.service\n values.value = detail.values\n revision.value = detail.revision\n prefix.value = detail.prefix\n previewUrl.value = detail.preview_url\n snapshot.value = JSON.stringify(detail.values)\n conflict.value = null\n}\n\n/**\n * Read the service again — quietly unless this is the first time: the skeleton replaces the whole\n * screen, and a publication answered with it would throw the editor back onto the first tab.\n */\nasync function load(silent = false): Promise<void> {\n if (!silent) loading.value = true\n\n try {\n take(await api.get(id.value))\n } catch (error) {\n toast.danger(message(error))\n void router.push(list.value)\n } finally {\n loading.value = false\n }\n}\n\n/* One pending save at a time, and one pending pause. */\nlet timer: ReturnType<typeof setTimeout> | undefined\n\nfunction schedule(): void {\n if (!canManage.value || conflict.value || !dirty.value) return\n\n clearTimeout(timer)\n timer = setTimeout(() => void save(), PAUSE)\n}\n\n/** A field was left: write now rather than at the end of a pause nobody is waiting through. */\nfunction onFocusOut(): void {\n if (!canManage.value || conflict.value || !dirty.value || saving.value) return\n\n clearTimeout(timer)\n void save()\n}\n\nasync function save(): Promise<void> {\n if (!service.value || saving.value || !canManage.value) return\n\n clearTimeout(timer)\n\n // What is being sent, so that whatever is typed while the request is in flight stays dirty and\n // gets its own save rather than being marked as written.\n const sending = current.value\n const sent = values.value\n\n saving.value = true\n errors.value = {}\n\n try {\n const detail = await api.save(service.value.id, { values: sent, revision: revision.value })\n\n service.value = detail.service\n revision.value = detail.revision\n prefix.value = detail.prefix\n previewUrl.value = detail.preview_url\n conflict.value = null\n\n if (current.value === sending) snapshot.value = sending\n\n // The preview is rendered from the draft, and the draft is what was just written.\n reloadToken.value += 1\n } catch (error) {\n const failure = error as {\n status?: number\n body?: ServiceConflict & { errors?: Record<string, string[]> }\n }\n\n if (failure.status === 409 && failure.body?.data) {\n conflict.value = failure.body\n } else if (failure.body?.errors) {\n errors.value = failure.body.errors\n toast.danger(t('service.save-failed'))\n } else {\n toast.danger(message(error))\n }\n } finally {\n saving.value = false\n }\n}\n\n/** Give up what was typed and take the service as it now is. */\nasync function takeTheirs(): Promise<void> {\n const agreed = await confirm({\n title: t('service.conflict-theirs-title'),\n message: t('service.conflict-theirs-text'),\n confirmText: t('service.conflict-theirs'),\n cancelText: t('panel.cancel'),\n tone: 'danger',\n })\n\n if (!agreed) return\n\n await load(true)\n}\n\n/** Keep what was typed and write it over the other version — which is still in the history. */\nasync function keepMine(): Promise<void> {\n const theirs = conflict.value\n\n if (!theirs) return\n\n revision.value = theirs.data.revision\n conflict.value = null\n\n await save()\n}\n\n/** Throw away what is waiting — asked about, because it is the one button here that loses writing. */\nasync function discard(): Promise<void> {\n if (!service.value) return\n\n const agreed = await confirm({\n title: t('service.discard-title'),\n message: t('service.discard-text'),\n confirmText: t('service.discard'),\n cancelText: t('panel.cancel'),\n tone: 'danger',\n })\n\n if (!agreed) return\n\n working.value = true\n\n try {\n take(await api.discard(service.value.id))\n toast.success(t('service.discarded'))\n } catch (error) {\n toast.danger(message(error))\n } finally {\n working.value = false\n }\n}\n\n/** Publishing is asked about, because it is the one action here that visitors see. */\nasync function publish(): Promise<void> {\n const row = service.value\n\n if (!row) return\n\n const agreed = await confirm({\n title: t('service.publish-title', { title: title.value || row.title }),\n message:\n row.path === null\n ? t('service.publish-nowhere')\n : t('service.publish-text', { address: `/${row.path}` }),\n confirmText: t('panel.publish'),\n cancelText: t('panel.cancel'),\n })\n\n if (!agreed) return\n\n if (dirty.value) await save()\n if (conflict.value) return\n\n working.value = true\n\n try {\n await api.publish(row.id)\n await load(true)\n toast.success(t('panel.published'))\n } catch (error) {\n toast.danger(message(error))\n } finally {\n working.value = false\n }\n}\n\nfunction badge(): 'default' | 'success' {\n const status = service.value?.status\n\n return status === 'published' || status === 'modified' ? 'success' : 'default'\n}\n\n/* The browser's own guard: it cannot wait for a request, so all it can do is ask. */\nfunction leaveGuard(event: BeforeUnloadEvent): void {\n if (dirty.value) event.preventDefault()\n}\n\nwatch(current, schedule)\nwatch(id, () => void load())\n\nonMounted(() => {\n void load()\n window.addEventListener('beforeunload', leaveGuard)\n})\n\nonBeforeUnmount(() => {\n clearTimeout(timer)\n window.removeEventListener('beforeunload', leaveGuard)\n})\n\n/**\n * Leaving flushes the pause rather than asking about it; the question is asked only when the save\n * did not go through — a conflict, a refused field, a server that is not there.\n */\nonBeforeRouteLeave(async () => {\n if (!dirty.value || !canManage.value) return true\n\n await save()\n\n if (!dirty.value) return true\n\n return await confirm({\n title: t('service.leave-title'),\n message: t('service.leave-text'),\n confirmText: t('service.leave'),\n cancelText: t('panel.cancel'),\n tone: 'danger',\n })\n})\n\n/* What leads away from the service. On a phone the head folds them into the ···. */\nconst actions = computed<ScreenAction[]>(() => {\n const leads: ScreenAction[] = []\n\n if (previewUrl.value) {\n leads.push({ key: 'preview', label: t('service.preview'), icon: 'eye', href: previewUrl.value })\n }\n\n if (service.value?.url && service.value.status !== 'draft') {\n leads.push({\n key: 'site',\n label: t('panel.open-on-site'),\n icon: 'link',\n href: service.value.url,\n })\n }\n\n // Throwing away writing lives in the ···, never beside \"publish\", and only while there is a\n // difference between what is written and what is on the site.\n if (service.value?.status === 'modified') {\n leads.push({\n key: 'discard',\n label: t('service.discard'),\n icon: 'refresh',\n danger: true,\n menu: true,\n run: () => void discard(),\n })\n }\n\n return leads\n})\n</script>\n\n<template>\n <div class=\"wx-service-editor\" @focusout=\"onFocusOut\">\n <template v-if=\"loading || !service\">\n <wx-skeleton class=\"wx-service-editor__ghost\" title :rows=\"1\" />\n <wx-card><wx-skeleton :rows=\"8\" /></wx-card>\n </template>\n\n <template v-else>\n <wx-screen-head\n divider\n :back=\"list\"\n :back-label=\"t('module.services')\"\n :title=\"title || t('service.untitled')\"\n :subtitle=\"publication\"\n :actions=\"actions\"\n >\n <template #trail>\n <wx-breadcrumb size=\"sm\" :label=\"t('service.trail')\">\n <wx-breadcrumb-item :as=\"'router-link'\" :to=\"list\">\n {{ t('module.services') }}\n </wx-breadcrumb-item>\n <wx-breadcrumb-item current>{{ title || t('service.untitled') }}</wx-breadcrumb-item>\n </wx-breadcrumb>\n </template>\n\n <template #title-after>\n <wx-badge :type=\"badge()\" dot>{{ t(`panel.status-${service.status}`) }}</wx-badge>\n <wx-badge v-if=\"service.status === 'modified'\" type=\"primary\" round>\n {{ t('panel.edits') }}\n </wx-badge>\n </template>\n </wx-screen-head>\n\n <!-- Somebody else wrote while this editor was typing. Both versions still exist, so the\n question is which one the site gets — a question, not a toast that disappears. -->\n <wx-alert\n v-if=\"conflict\"\n type=\"warning\"\n :title=\"t('service.conflict-title')\"\n :description=\"conflict.message\"\n >\n <template #actions>\n <wx-button size=\"sm\" variant=\"outline\" @click=\"takeTheirs\">\n {{ t('service.conflict-theirs') }}\n </wx-button>\n <wx-button size=\"sm\" type=\"primary\" @click=\"keepMine\">\n {{ t('service.conflict-mine') }}\n </wx-button>\n </template>\n </wx-alert>\n\n <div class=\"wx-service-editor__screen\">\n <wx-screen v-model=\"values\" name=\"services.form\" :errors=\"errors\" :disabled=\"locked\" />\n </div>\n\n <wx-action-bar v-if=\"canManage\">\n <template #state>\n <wx-save-state :state=\"state\" />\n </template>\n\n <wx-button variant=\"outline\" :loading=\"saving\" :disabled=\"!dirty\" @click=\"save\">\n {{ t('service.save') }}\n </wx-button>\n\n <wx-button\n type=\"primary\"\n :loading=\"working\"\n :disabled=\"service.status === 'published' && !dirty\"\n @click=\"publish\"\n >\n {{ t('panel.publish') }}\n </wx-button>\n </wx-action-bar>\n </template>\n </div>\n</template>\n\n<style scoped>\n.wx-service-editor {\n display: flex;\n flex-direction: column;\n gap: var(--wx-gap, var(--wx-space-16));\n height: 100%;\n min-height: 0;\n container-type: inline-size;\n}\n\n.wx-service-editor__ghost {\n max-width: 420px;\n}\n\n.wx-service-editor__screen {\n display: flex;\n flex-direction: column;\n flex: 1;\n min-height: 0;\n}\n\n/*\n * The height travels through the tabs to the constructor: every box between the page and it is a\n * flex column that may shrink. `:not([hidden])` is load-bearing — the tabs are kept alive, and a\n * `display: flex` that reached the hidden ones would split the height between all four.\n */\n.wx-service-editor__screen :deep(.wx-screen-host),\n.wx-service-editor__screen :deep(.wx-screen),\n.wx-service-editor__screen :deep(.wx-tabs),\n.wx-service-editor__screen :deep(.wx-tabs__layout),\n.wx-service-editor__screen :deep(.wx-tabs__panels),\n.wx-service-editor__screen :deep(.wx-tab:not([hidden])) {\n display: flex;\n flex-direction: column;\n flex: 1;\n min-height: 0;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed, ref, watch } from 'vue'\nimport { useAdmin, useErrorText, useTranslate, WxDate } from '@webx-ui/module-admin'\nimport { confirm, toast, WxBadge, WxButton, WxEmpty, WxSkeleton, WxText } from '@webx-ui/core'\nimport { createServicesApi } from './api'\nimport { useServiceEditor } from './editor'\nimport { useServicesMessages } from './i18n'\nimport type { ServiceVersion } from './types'\n\n/**\n * What was published, when, by whom and from where — publications only; the autosaves are\n * insurance, not history.\n *\n * Restoring makes the old version the draft. Publishing it is the same separate step it always\n * is, so nothing here changes the site by itself.\n */\ndefineOptions({ name: 'WxServiceHistory' })\n\nconst context = useAdmin()\nconst api = createServicesApi(context)\nconst editor = useServiceEditor()\nuseServicesMessages()\n\nconst t = useTranslate('webx-services')\n/* Not the server's `message`: the panel says how a request failed in its own words. */\nconst message = useErrorText()\n\nconst versions = ref<ServiceVersion[] | null>(null)\nconst working = ref(false)\n\nconst service = computed(() => editor?.service.value ?? null)\nconst canManage = computed(() => editor?.canManage ?? false)\n\n/** The newest publication is the site — while the service is on it at all. */\nconst live = computed(() =>\n service.value?.status === 'published' || service.value?.status === 'modified'\n ? (versions.value?.[0]?.number ?? null)\n : null,\n)\n\nasync function load(): Promise<void> {\n const current = service.value\n\n if (!current) return\n\n try {\n versions.value = await api.versions(current.id)\n } catch (error) {\n versions.value = []\n toast.danger(message(error))\n }\n}\n\nasync function restore(version: ServiceVersion): Promise<void> {\n const current = service.value\n\n if (!current) return\n\n const agreed = await confirm({\n title: t('service.restore-title', { number: version.number }),\n message: t('service.restore-text'),\n confirmText: t('service.restore-version'),\n cancelText: t('panel.cancel'),\n })\n\n if (!agreed) return\n\n working.value = true\n\n try {\n await api.restoreVersion(current.id, version.number)\n // Through the editor rather than with the answer: the values it holds are what the form is\n // bound to, and a version restored underneath it has to reach the fields.\n await editor?.reload()\n toast.success(t('service.restored-version', { number: version.number }))\n } catch (error) {\n toast.danger(message(error))\n } finally {\n working.value = false\n }\n}\n\n// The service is read again after every publication and every restore, so its stamp is the\n// signal that there is something new to list.\nwatch(\n () => [service.value?.id, service.value?.published_at, service.value?.updated_at],\n () => void load(),\n { immediate: true },\n)\n</script>\n\n<template>\n <div class=\"wx-service-history\">\n <wx-skeleton v-if=\"versions === null\" class=\"wx-service-history__ghost\" :rows=\"3\" />\n <wx-empty v-else-if=\"versions.length === 0\" :description=\"t('service.history-empty')\" />\n <div\n v-for=\"version in versions\"\n v-else\n :key=\"version.number\"\n class=\"wx-service-history__row\"\n :class=\"{ 'is-live': version.number === live }\"\n >\n <code class=\"wx-service-history__number\">{{\n t('service.version', { number: version.number })\n }}</code>\n <div class=\"wx-service-history__who\">\n <wx-date v-if=\"version.created_at\" :value=\"version.created_at\" size=\"md\" tone=\"default\" />\n <wx-text size=\"sm\" tone=\"muted\">\n {{ version.author ?? t(`service.source-${version.source}`) }}\n <template v-if=\"version.comment\"> · {{ version.comment }}</template>\n </wx-text>\n </div>\n <wx-badge v-if=\"version.number === live\" type=\"success\" dot>{{\n t('service.version-live')\n }}</wx-badge>\n <wx-button\n v-if=\"canManage && version.number !== live\"\n size=\"sm\"\n variant=\"outline\"\n :loading=\"working\"\n @click=\"restore(version)\"\n >\n {{ t('service.restore-version') }}\n </wx-button>\n </div>\n </div>\n</template>\n\n<style scoped>\n.wx-service-history {\n display: flex;\n flex-direction: column;\n border: 1px solid var(--wx-border-default);\n border-radius: var(--wx-radius-md);\n background: var(--wx-bg-surface);\n overflow: hidden;\n}\n\n.wx-service-history__ghost {\n padding: var(--wx-space-10) var(--wx-space-12);\n}\n\n.wx-service-history__row {\n display: flex;\n align-items: center;\n gap: var(--wx-space-12);\n padding: var(--wx-space-10) var(--wx-space-12);\n border-block-end: 1px solid var(--wx-border-default);\n flex-wrap: wrap;\n}\n\n.wx-service-history__row:last-child {\n border-block-end: 0;\n}\n\n.wx-service-history__number {\n width: 40px;\n font-family: var(--wx-font-family-mono);\n font-size: var(--wx-font-size-sm);\n color: var(--wx-text-muted);\n}\n\n.wx-service-history__who {\n display: flex;\n flex-direction: column;\n flex: 1;\n min-width: 160px;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { nextTick, onMounted, ref, useTemplateRef } from 'vue'\nimport { useAdmin, useTranslate } from '@webx-ui/module-admin'\nimport { useModal, WxButton, WxDialog, WxFormItem, WxInput, WxSpace } from '@webx-ui/core'\nimport { createServicesApi } from './api'\nimport { useServicesMessages } from './i18n'\nimport type { ServiceRow } from './types'\n\n/**\n * A new service: what it is called, and nothing else. The server makes the address out of the\n * title; the address field on the settings tab is where there is room to say what changing it\n * costs.\n */\nconst { open, resolve, dismiss } = useModal<ServiceRow>()\n\nconst context = useAdmin()\nconst api = createServicesApi(context)\nuseServicesMessages()\n\nconst t = useTranslate('webx-services')\n\nconst title = ref('')\nconst saving = ref(false)\nconst errors = ref<Record<string, string[]>>({})\nconst field = useTemplateRef<HTMLElement>('field')\n\nonMounted(async () => {\n await nextTick()\n field.value?.querySelector('input')?.focus()\n})\n\nasync function submit(): Promise<void> {\n if (title.value.trim() === '' || saving.value) return\n\n saving.value = true\n errors.value = {}\n\n try {\n resolve(await api.create({ title: title.value.trim() }))\n } catch (error) {\n // The address is what can be refused here, and it is refused under the title that made it:\n // the registry sees the categories and the pages too, so \"taken\" can mean taken by either.\n errors.value = (error as { body?: { errors?: Record<string, string[]> } }).body?.errors ?? {}\n } finally {\n saving.value = false\n }\n}\n</script>\n\n<template>\n <wx-dialog v-model:open=\"open\" :title=\"t('panel.new-title')\" :width=\"440\">\n <wx-form-item\n :label=\"t('panel.field-title')\"\n :error=\"errors.title?.[0] ?? errors.slug?.[0]\"\n required\n >\n <div ref=\"field\">\n <wx-input v-model=\"title\" :aria-label=\"t('panel.field-title')\" @keyup.enter=\"submit\" />\n </div>\n </wx-form-item>\n\n <template #footer>\n <wx-space size=\"sm\">\n <wx-button variant=\"outline\" @click=\"dismiss()\">{{ t('panel.cancel') }}</wx-button>\n <wx-button type=\"primary\" :loading=\"saving\" :disabled=\"title.trim() === ''\" @click=\"submit\">\n {{ t('panel.create') }}\n </wx-button>\n </wx-space>\n </template>\n </wx-dialog>\n</template>\n","<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, ref, watch } from 'vue'\nimport { useRoute, useRouter } from 'vue-router'\nimport {\n useAdmin,\n useErrorText,\n useItemOrder,\n useTranslate,\n WxListScreen,\n WxRowMenu,\n type RowAction,\n type ScreenAction,\n} from '@webx-ui/module-admin'\nimport {\n confirm,\n createModal,\n toast,\n WxAlert,\n WxBadge,\n WxEmpty,\n WxIcon,\n WxInput,\n WxSelect,\n WxSkeleton,\n WxSortableList,\n WxTooltip,\n type TabItem,\n type TabValue,\n} from '@webx-ui/core'\nimport { createServicesApi, SERVICES_API } from './api'\nimport { useServicesMessages } from './i18n'\nimport ServiceCreateDialog from './ServiceCreateDialog.vue'\nimport type { ServiceCategoryRef, ServiceRow, ServiceStatus } from './types'\n\n/**\n * The catalogue: every service at once, in the order the site shows them (§4.6).\n *\n * No paginator and no table. There are dozens of services, not thousands, and this list is where\n * they are put in order — a drag cannot cross a page boundary. The order is decision 5 of the\n * spec: without a filter the rows are the whole list and a drag moves the common order; narrowed\n * to one category they come in that category's own order and a drag moves only it; any other\n * narrowing (a search, a status, the bin) shows a selection with invisible gaps in it, so the\n * grips go away and the line under the list says why.\n *\n * A row is a card rather than a line of cells, which is what makes the same list work on a\n * phone: the picture, the name and the address on one side, the categories and the state beside\n * them on a wide screen and under them on a narrow one.\n */\nconst props = withDefaults(defineProps<{ base?: string }>(), { base: '/services' })\n\nconst context = useAdmin()\nconst api = createServicesApi(context)\nconst route = useRoute()\nconst router = useRouter()\nuseServicesMessages()\n\nconst t = useTranslate('webx-services')\n/* Not the server's `message`: the panel says how a request failed in its own words. */\nconst message = useErrorText()\n\nconst rows = ref<ServiceRow[]>([])\nconst categories = ref<ServiceCategoryRef[]>([])\nconst loading = ref(true)\n\nconst create = createModal<ServiceRow, Record<string, never>>(ServiceCreateDialog)\n\nconst canManage = computed(() => context.can('services.manage'))\n\nconst title = computed(\n () =>\n context.state.manifest?.modules.find((module) => module.id === 'services')?.title ??\n t('module.services'),\n)\n\n/*\n * Everything the list is looking at lives in the address, so coming back from a service lands on\n * the same view, the same category and the same search.\n */\nconst view = computed<TabValue>({\n get: () => (typeof route.query.view === 'string' ? route.query.view : ''),\n set: (value) => {\n void router.replace({\n query: { ...route.query, view: value === '' ? undefined : String(value) },\n })\n },\n})\n\nconst inBin = computed(() => view.value === 'trashed')\n\nconst category = computed<number | null>(() => {\n const raw = route.query.category\n\n return typeof raw === 'string' && raw !== '' ? Number(raw) || null : null\n})\n\nconst q = computed(() => (typeof route.query.q === 'string' ? route.query.q : ''))\n\n/* What is typed, ahead of the address: the address follows it after a pause. */\nconst typed = ref(q.value)\n\nconst views = computed<TabItem[]>(() => [\n { value: '', label: t('panel.view-all') },\n { value: 'published', label: t('panel.view-published') },\n { value: 'draft', label: t('panel.view-draft') },\n { value: 'unpublished', label: t('panel.view-unpublished') },\n { value: 'trashed', label: t('panel.bin'), icon: 'trash' },\n])\n\nconst order = useItemOrder(SERVICES_API, () => ({\n q: q.value,\n category: category.value,\n filtered: view.value !== '',\n}))\n\n/* Dragged only by somebody who may write, and only in a list that is a whole order. */\nconst sortable = computed(() => canManage.value && order.sortable.value)\n\nconst emptyText = computed(() => {\n if (q.value !== '') return t('panel.empty-search')\n\n return inBin.value ? t('panel.empty-bin') : t('panel.empty')\n})\n\nasync function load(): Promise<void> {\n loading.value = true\n\n try {\n const answer = await api.list({\n q: q.value,\n category: category.value,\n status: inBin.value ? '' : (view.value as ServiceStatus | ''),\n trashed: inBin.value,\n })\n\n rows.value = answer.data\n categories.value = answer.filters.categories\n } catch (error) {\n toast.danger(message(error))\n } finally {\n loading.value = false\n }\n}\n\nwatch(\n () => [view.value, category.value, q.value] as const,\n () => void load(),\n)\nvoid load()\n\nlet pause: ReturnType<typeof setTimeout> | undefined\n\nwatch(typed, (value) => {\n clearTimeout(pause)\n pause = setTimeout(() => {\n void router.replace({ query: { ...route.query, q: value.trim() === '' ? undefined : value } })\n }, 300)\n})\n\nonBeforeUnmount(() => clearTimeout(pause))\n\nfunction narrow(value: unknown): void {\n void router.replace({\n query: { ...route.query, category: typeof value === 'number' ? String(value) : undefined },\n })\n}\n\nfunction open(service: ServiceRow): void {\n void router.push({ path: `${props.base}/${service.id}`, query: { ...route.query } })\n}\n\n/** A new service: the dialog asks for a title, the editor opens. */\nasync function add(): Promise<void> {\n const made = await create({})\n\n if (made) open(made)\n}\n\n/**\n * The order on screen, written as a whole — the list or the category's, whichever this is. The\n * list is already in its new order (`WxSortableList` reorders the model before it says so), so a\n * failure has to put it back rather than leave the screen disagreeing with the database.\n */\nasync function reorder(): Promise<void> {\n try {\n await order.move(rows.value.map((row) => row.id))\n } catch (error) {\n toast.danger(message(error, t('panel.reorder-failed')))\n await load()\n }\n}\n\nfunction actionsFor(service: ServiceRow): RowAction[] {\n if (!canManage.value) return []\n\n if (inBin.value) {\n return [\n {\n key: 'restore',\n icon: 'refresh',\n label: t('panel.restore'),\n run: () => void restore(service),\n },\n ]\n }\n\n const actions: RowAction[] = [\n { key: 'open', icon: 'edit', label: t('panel.open'), run: () => open(service) },\n ]\n\n if (service.url && service.status !== 'draft') {\n actions.push({\n key: 'site',\n icon: 'external-link',\n label: t('panel.open-on-site'),\n href: service.url,\n })\n }\n\n if (service.url) {\n actions.push({\n key: 'copy',\n icon: 'copy',\n label: t('panel.copy-address'),\n run: () => void copyAddress(service),\n })\n }\n\n actions.push(\n service.status === 'published' || service.status === 'modified'\n ? {\n key: 'unpublish',\n icon: 'eye-off',\n label: t('panel.unpublish'),\n run: () => void run(() => api.unpublish(service.id), t('panel.unpublished-done')),\n }\n : {\n key: 'publish',\n icon: 'upload',\n label: t('panel.publish'),\n run: () => void run(() => api.publish(service.id), t('panel.published')),\n },\n )\n\n actions.push({\n key: 'delete',\n icon: 'trash',\n label: t('panel.delete'),\n danger: true,\n run: () => void remove(service),\n })\n\n return actions\n}\n\nasync function remove(service: ServiceRow): Promise<void> {\n const agreed = await confirm({\n title: t('panel.delete-title', { title: service.title }),\n message: t('panel.delete-text'),\n confirmText: t('panel.delete'),\n cancelText: t('panel.cancel'),\n tone: 'danger',\n })\n\n if (!agreed) return\n\n await run(() => api.remove(service.id), t('panel.deleted'))\n}\n\nasync function restore(service: ServiceRow): Promise<void> {\n await run(() => api.restore(service.id), t('panel.restored'))\n}\n\n/** One request, one sentence about it, and the list as the server now sees it. */\nasync function run(request: () => Promise<unknown>, said: string): Promise<void> {\n try {\n await request()\n toast.success(said)\n } catch (error) {\n toast.danger(message(error))\n } finally {\n await load()\n }\n}\n\nasync function copyAddress(service: ServiceRow): Promise<void> {\n if (!service.url) return\n\n try {\n await navigator.clipboard.writeText(service.url)\n toast.success(t('panel.address-copied'))\n } catch {\n // A browser that refuses the clipboard without a gesture it trusts; the address is on screen.\n }\n}\n\n/* Live and live-with-edits are the same green; what is waiting has a chip of its own. */\nfunction badge(status: ServiceStatus): 'default' | 'success' {\n return status === 'published' || status === 'modified' ? 'success' : 'default'\n}\n\nfunction address(service: ServiceRow): string {\n if (inBin.value) return '—'\n\n return service.path === null ? t('panel.no-address') : `/${service.path}`\n}\n\n/* What the section offers. Declared, because on a phone the head folds it into the ···. */\nconst actions = computed<ScreenAction[]>(() =>\n canManage.value\n ? [{ key: 'new', label: t('panel.new'), icon: 'plus', primary: true, run: () => void add() }]\n : [],\n)\n</script>\n\n<template>\n <div class=\"wx-services\">\n <wx-list-screen\n v-model:view=\"view\"\n :title=\"title\"\n :views=\"views\"\n :actions=\"actions\"\n padding=\"sm\"\n >\n <div class=\"wx-services__bar\">\n <wx-input\n v-model=\"typed\"\n class=\"wx-services__search\"\n size=\"sm\"\n clearable\n :placeholder=\"t('panel.search')\"\n :aria-label=\"t('panel.search')\"\n >\n <template #prefix><wx-icon name=\"search\" size=\"sm\" /></template>\n </wx-input>\n\n <!-- Always in sight rather than behind a funnel: it is the one filter that decides which\n order a drag writes, and the list says so right under it. -->\n <wx-select\n class=\"wx-services__category\"\n :model-value=\"category\"\n :options=\"categories.map((item) => ({ value: item.id, label: item.title }))\"\n :placeholder=\"t('panel.any-category')\"\n :aria-label=\"t('panel.filter-category')\"\n clearable\n size=\"sm\"\n @update:model-value=\"narrow\"\n />\n </div>\n\n <wx-skeleton v-if=\"loading && rows.length === 0\" class=\"wx-services__loading\" :rows=\"5\" />\n\n <wx-empty\n v-else-if=\"rows.length === 0\"\n :title=\"emptyText\"\n :description=\"\n view === '' && q === '' && category === null ? t('panel.empty-help') : undefined\n \"\n />\n\n <template v-else>\n <!--\n A grip and not the whole row: the row opens a service, and a row that both opens and\n drags is one where one of the two happens by accident. No grips at all where the list\n is a selection — a handle that moves rows past invisible ones would be lying.\n -->\n <wx-sortable-list\n v-model=\"rows\"\n class=\"wx-services__list\"\n :class=\"{ 'is-loading': loading }\"\n plain\n item-key=\"id\"\n :handle=\"sortable ? 'grip' : 'row'\"\n :item-label=\"(item: ServiceRow) => item.title\"\n :disabled=\"!sortable\"\n @move=\"reorder\"\n >\n <template #default=\"{ item }\">\n <component\n :is=\"inBin ? 'div' : 'router-link'\"\n class=\"wx-service-row\"\n v-bind=\"inBin ? {} : { to: { path: `${props.base}/${item.id}`, query: route.query } }\"\n >\n <!-- The picture's box, never the picture: `<img>` has `min-width: auto`, and a\n photograph would take the row (CLAUDE.md §4). -->\n <span class=\"wx-service-row__cover\" :class=\"{ 'is-empty': !item.cover }\">\n <img v-if=\"item.cover?.thumb\" :src=\"item.cover.thumb\" alt=\"\" loading=\"lazy\" />\n <wx-icon v-else name=\"image\" size=\"sm\" />\n </span>\n\n <span class=\"wx-service-row__name\">\n <span class=\"wx-service-row__title\">{{ item.title }}</span>\n <span class=\"wx-service-row__address\">{{ address(item) }}</span>\n </span>\n\n <span class=\"wx-service-row__facts\">\n <span class=\"wx-service-row__chips\">\n <template v-for=\"(chip, index) in item.categories\" :key=\"chip.id\">\n <wx-tooltip v-if=\"index === 0\" :content=\"t('panel.main-category')\">\n <wx-badge size=\"sm\" round type=\"primary\">{{ chip.title }}</wx-badge>\n </wx-tooltip>\n <wx-badge v-else size=\"sm\" round>{{ chip.title }}</wx-badge>\n </template>\n </span>\n\n <span class=\"wx-service-row__state\">\n <wx-badge :type=\"badge(item.status)\" dot size=\"sm\">\n {{ t(`panel.status-${item.status}`) }}\n </wx-badge>\n <wx-badge v-if=\"item.status === 'modified'\" type=\"primary\" size=\"sm\" round>\n {{ t('panel.edits') }}\n </wx-badge>\n </span>\n </span>\n </component>\n </template>\n\n <template #actions=\"{ item }\">\n <wx-row-menu :actions=\"actionsFor(item)\" :label=\"item.title\" />\n </template>\n </wx-sortable-list>\n\n <!-- What a drag does here, or why there is nothing to drag — under the rows it is about. -->\n <wx-alert\n v-if=\"canManage && !inBin\"\n class=\"wx-services__note\"\n type=\"info\"\n :description=\"order.hint.value\"\n />\n </template>\n </wx-list-screen>\n </div>\n</template>\n\n<style scoped>\n.wx-services {\n min-width: 0;\n container-type: inline-size;\n}\n\n.wx-services__bar {\n display: flex;\n flex-wrap: wrap;\n gap: var(--wx-space-8);\n padding: var(--wx-space-4) var(--wx-space-8) var(--wx-space-8);\n}\n\n.wx-services__search {\n flex: 1 1 240px;\n min-width: 0;\n}\n\n/* The select is a fragment (it carries its dropdown beside it), so our scope attribute never\n reaches it: styled from our own wrapper (CLAUDE.md §4). */\n.wx-services__bar > :deep(.wx-select) {\n flex: 1 1 160px;\n max-width: 280px;\n}\n\n.wx-services__loading {\n padding: var(--wx-space-8);\n}\n\n.wx-services__list.is-loading {\n opacity: 0.6;\n}\n\n.wx-services__list :deep(.wx-sortable-list__row) {\n padding-inline: var(--wx-space-8);\n}\n\n.wx-services__list :deep(.wx-sortable-list__row:hover) {\n background: var(--wx-bg-subtle);\n border-radius: var(--wx-radius-sm);\n}\n\n.wx-services__note {\n margin-block-start: var(--wx-space-8);\n}\n\n.wx-service-row {\n display: grid;\n grid-template-columns: auto minmax(0, 1fr) auto;\n grid-template-areas: 'cover name facts';\n align-items: center;\n gap: var(--wx-space-4) var(--wx-space-12);\n flex: 1 1 auto;\n min-width: 0;\n padding-block: var(--wx-space-4);\n color: inherit;\n text-decoration: none;\n}\n\n.wx-service-row__cover {\n grid-area: cover;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 44px;\n height: 32px;\n overflow: hidden;\n border-radius: var(--wx-radius-xs);\n background: var(--wx-bg-subtle);\n color: var(--wx-text-muted);\n}\n\n.wx-service-row__cover.is-empty {\n border: 1px dashed var(--wx-border-default);\n background: var(--wx-bg-surface);\n}\n\n.wx-service-row__cover img {\n width: 100%;\n height: 100%;\n min-width: 0;\n object-fit: cover;\n}\n\n.wx-service-row__name {\n grid-area: name;\n display: flex;\n flex-direction: column;\n min-width: 0;\n}\n\n.wx-service-row__title {\n overflow: hidden;\n font-weight: var(--wx-font-weight-medium);\n white-space: nowrap;\n text-overflow: ellipsis;\n}\n\n.wx-service-row__address {\n overflow: hidden;\n color: var(--wx-text-muted);\n font-family: var(--wx-font-family-mono);\n font-size: var(--wx-font-size-xs);\n white-space: nowrap;\n text-overflow: ellipsis;\n}\n\n.wx-service-row__facts {\n grid-area: facts;\n display: flex;\n align-items: center;\n gap: var(--wx-space-12);\n min-width: 0;\n}\n\n.wx-service-row__chips,\n.wx-service-row__state {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: var(--wx-space-4);\n min-width: 0;\n}\n\n.wx-service-row__chips {\n justify-content: flex-end;\n max-width: 320px;\n}\n\n/*\n * Narrow: the facts go under the name rather than beside it, so the name keeps the width. By the\n * width of the list, not of the window — the panel's sidebar decides how much the list has.\n */\n@container (max-width: 640px) {\n .wx-service-row {\n grid-template-columns: auto minmax(0, 1fr);\n grid-template-areas:\n 'cover name'\n 'cover facts';\n align-items: start;\n }\n\n .wx-service-row__facts {\n flex-wrap: wrap;\n gap: var(--wx-space-4);\n }\n\n /* One wrapping line of badges rather than two boxes side by side: squeezed next to each\n other, each box shrank to its widest badge and stacked the categories one per line. */\n .wx-service-row__chips,\n .wx-service-row__state {\n display: contents;\n }\n\n /* Every pixel of a 375px row goes to the name: the grip and the menu keep their size, the\n gaps between them do not. */\n .wx-service-row {\n column-gap: var(--wx-space-8);\n }\n\n .wx-services__list :deep(.wx-sortable-list__row) {\n gap: var(--wx-space-8);\n padding-inline: var(--wx-space-4);\n }\n\n .wx-services__bar {\n padding-inline: var(--wx-space-4);\n }\n\n .wx-services__bar > :deep(.wx-select) {\n max-width: none;\n }\n}\n</style>\n","import { categoryRoutes, type AdminModule, type CategoriesOptions } from '@webx-ui/module-admin'\nimport ServiceEditorPage from './ServiceEditorPage.vue'\nimport ServiceHistory from './ServiceHistory.vue'\nimport ServicesPage from './ServicesPage.vue'\n\nexport interface ServicesOptions {\n /** Where the catalogue lives inside the panel. The categories sit under it. */\n path?: string\n}\n\n/**\n * The categories of services as the panel's shared category screens see them: where they answer,\n * what they are edited on and what this module calls them. Exported so that a panel mounting the\n * screens somewhere of its own does not have to repeat the words.\n *\n * Only the words that name the thing are this module's — \"services\" — and the rest (\"Edit\",\n * \"Cancel\", \"Leave without saving?\") is the panel's, said the same way in every module.\n */\nexport function serviceCategoriesOptions(path = '/services'): CategoriesOptions {\n return {\n api: 'services/categories',\n path: `${path}/categories`,\n name: 'webx.services.categories',\n module: 'service-categories',\n screen: 'services.category-form',\n manage: 'services.categories.manage',\n count: 'services_count',\n // The list narrowed to one category is also where its own order is dragged.\n items: (id) => ({ path, query: { category: String(id) } }),\n words: {\n new: 'webx-services::category.new',\n empty: 'webx-services::category.empty',\n 'empty-help': 'webx-services::category.empty-help',\n order: 'webx-services::category.order',\n hidden: 'webx-services::category.hidden',\n 'no-address': 'webx-services::category.no-address',\n count: 'webx-services::category.services',\n 'show-items': 'webx-services::category.show-services',\n 'delete-blocked': 'webx-services::category.delete-blocked',\n 'delete-text': 'webx-services::category.delete-text',\n deleted: 'webx-services::category.deleted',\n saved: 'webx-services::category.saved',\n 'field-title': 'webx-services::category.field-title',\n 'field-slug': 'webx-services::category.field-slug',\n 'address-moving': 'webx-services::category.address-moving',\n },\n }\n}\n\n/**\n * The catalogue as sections of the panel: services, and their categories (§4.6).\n *\n * Two modules rather than one, because the navigation is one entry per module; the server puts\n * both in the `services` group, which is what draws them under one heading. A section whose\n * server half is not installed never appears — the entry is built from the manifest.\n */\nexport function services(options: ServicesOptions = {}): AdminModule[] {\n const path = options.path ?? '/services'\n\n return [\n {\n id: 'services',\n path,\n routes: [\n { path, name: 'webx.services', component: ServicesPage, props: { base: path } },\n {\n path: `${path}/:id(\\\\d+)`,\n name: 'webx.services.edit',\n component: ServiceEditorPage,\n props: { base: path },\n },\n ],\n /*\n * The one part of `services.form` only this module can draw. Everything else on it is the\n * panel's (`wx-slug`, `wx-categories`, `wx-media`, `wx-blocks`) — the address field in\n * particular is shared, and reads the prefix from the editor hosting the screen.\n */\n types: {\n 'wx-service-history': { component: ServiceHistory, kind: 'display' },\n },\n },\n {\n id: 'service-categories',\n path: `${path}/categories`,\n routes: categoryRoutes(serviceCategoriesOptions(path)),\n },\n ]\n}\n"],"names":["SERVICES_API","createServicesApi","admin","base","data","body","query","search","id","input","number","ids","category","reorderItems","serviceEditorKey","provideServiceEditor","editor","provide","useServiceEditor","inject","servicesMessages","seeded","useServicesMessages","i18n","useAdmin","PAUSE","props","__props","context","api","route","useRoute","router","useRouter","locales","useLocales","dates","useDates","t","useTranslate","message","useErrorText","computed","list","service","ref","values","revision","prefix","previewUrl","loading","saving","working","errors","conflict","snapshot","reloadToken","canManage","locked","current","dirty","state","title","written","localizedValue","publication","row","provideBlocksPreview","provideRecordAddress","load","take","detail","silent","error","toast","timer","schedule","save","onFocusOut","sending","sent","failure","takeTheirs","confirm","keepMine","theirs","discard","publish","badge","status","leaveGuard","event","watch","onMounted","onBeforeUnmount","onBeforeRouteLeave","actions","leads","_createElementBlock","_Fragment","_createVNode","_unref","WxSkeleton","WxCard","WxScreenHead","WxBreadcrumb","WxBreadcrumbItem","_createTextVNode","_toDisplayString","WxBadge","_createBlock","WxAlert","WxButton","_createElementVNode","_hoisted_1","WxScreen","$event","WxActionBar","WxSaveState","versions","live","restore","version","_openBlock","WxEmpty","_renderList","_hoisted_2","_hoisted_3","WxDate","WxText","open","resolve","dismiss","useModal","field","useTemplateRef","nextTick","submit","WxDialog","WxSpace","WxFormItem","WxInput","rows","categories","create","createModal","ServiceCreateDialog","module","view","value","inBin","raw","q","typed","views","order","useItemOrder","sortable","emptyText","answer","pause","narrow","add","made","reorder","actionsFor","copyAddress","run","remove","request","said","address","WxListScreen","WxIcon","WxSelect","item","WxSortableList","_normalizeClass","_withCtx","_resolveDynamicComponent","_mergeProps","_hoisted_4","_hoisted_5","_hoisted_6","_hoisted_7","_hoisted_8","chip","index","WxTooltip","_hoisted_9","WxRowMenu","serviceCategoriesOptions","path","services","options","ServicesPage","ServiceEditorPage","ServiceHistory","categoryRoutes"],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Messages } from '@webx-ui/module-admin';
|
|
2
|
+
/**
|
|
3
|
+
* The English this package speaks on its own, before the server's dictionary arrives, or without
|
|
4
|
+
* one. The server ships the same lines in ten languages under `webx-services::` and overrides
|
|
5
|
+
* these; `messages.test.ts` keeps the two sets of keys equal.
|
|
6
|
+
*/
|
|
7
|
+
export declare const servicesMessages: Record<string, Messages>;
|
package/dist/module.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { AdminModule, CategoriesOptions } from '@webx-ui/module-admin';
|
|
2
|
+
export interface ServicesOptions {
|
|
3
|
+
/** Where the catalogue lives inside the panel. The categories sit under it. */
|
|
4
|
+
path?: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* The categories of services as the panel's shared category screens see them: where they answer,
|
|
8
|
+
* what they are edited on and what this module calls them. Exported so that a panel mounting the
|
|
9
|
+
* screens somewhere of its own does not have to repeat the words.
|
|
10
|
+
*
|
|
11
|
+
* Only the words that name the thing are this module's — "services" — and the rest ("Edit",
|
|
12
|
+
* "Cancel", "Leave without saving?") is the panel's, said the same way in every module.
|
|
13
|
+
*/
|
|
14
|
+
export declare function serviceCategoriesOptions(path?: string): CategoriesOptions;
|
|
15
|
+
/**
|
|
16
|
+
* The catalogue as sections of the panel: services, and their categories (§4.6).
|
|
17
|
+
*
|
|
18
|
+
* Two modules rather than one, because the navigation is one entry per module; the server puts
|
|
19
|
+
* both in the `services` group, which is what draws them under one heading. A section whose
|
|
20
|
+
* server half is not installed never appears — the entry is built from the manifest.
|
|
21
|
+
*/
|
|
22
|
+
export declare function services(options?: ServicesOptions): AdminModule[];
|
package/dist/style.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.wx-service-editor[data-v-c1092685]{display:flex;flex-direction:column;gap:var(--wx-gap, var(--wx-space-16));height:100%;min-height:0;container-type:inline-size}.wx-service-editor__ghost[data-v-c1092685]{max-width:420px}.wx-service-editor__screen[data-v-c1092685]{display:flex;flex-direction:column;flex:1;min-height:0}.wx-service-editor__screen[data-v-c1092685] .wx-screen-host,.wx-service-editor__screen[data-v-c1092685] .wx-screen,.wx-service-editor__screen[data-v-c1092685] .wx-tabs,.wx-service-editor__screen[data-v-c1092685] .wx-tabs__layout,.wx-service-editor__screen[data-v-c1092685] .wx-tabs__panels,.wx-service-editor__screen[data-v-c1092685] .wx-tab:not([hidden]){display:flex;flex-direction:column;flex:1;min-height:0}.wx-service-history[data-v-72dccbb1]{display:flex;flex-direction:column;border:1px solid var(--wx-border-default);border-radius:var(--wx-radius-md);background:var(--wx-bg-surface);overflow:hidden}.wx-service-history__ghost[data-v-72dccbb1]{padding:var(--wx-space-10) var(--wx-space-12)}.wx-service-history__row[data-v-72dccbb1]{display:flex;align-items:center;gap:var(--wx-space-12);padding:var(--wx-space-10) var(--wx-space-12);border-block-end:1px solid var(--wx-border-default);flex-wrap:wrap}.wx-service-history__row[data-v-72dccbb1]:last-child{border-block-end:0}.wx-service-history__number[data-v-72dccbb1]{width:40px;font-family:var(--wx-font-family-mono);font-size:var(--wx-font-size-sm);color:var(--wx-text-muted)}.wx-service-history__who[data-v-72dccbb1]{display:flex;flex-direction:column;flex:1;min-width:160px}.wx-services[data-v-f99d37d6]{min-width:0;container-type:inline-size}.wx-services__bar[data-v-f99d37d6]{display:flex;flex-wrap:wrap;gap:var(--wx-space-8);padding:var(--wx-space-4) var(--wx-space-8) var(--wx-space-8)}.wx-services__search[data-v-f99d37d6]{flex:1 1 240px;min-width:0}.wx-services__bar[data-v-f99d37d6]>.wx-select{flex:1 1 160px;max-width:280px}.wx-services__loading[data-v-f99d37d6]{padding:var(--wx-space-8)}.wx-services__list.is-loading[data-v-f99d37d6]{opacity:.6}.wx-services__list[data-v-f99d37d6] .wx-sortable-list__row{padding-inline:var(--wx-space-8)}.wx-services__list[data-v-f99d37d6] .wx-sortable-list__row:hover{background:var(--wx-bg-subtle);border-radius:var(--wx-radius-sm)}.wx-services__note[data-v-f99d37d6]{margin-block-start:var(--wx-space-8)}.wx-service-row[data-v-f99d37d6]{display:grid;grid-template-columns:auto minmax(0,1fr) auto;grid-template-areas:"cover name facts";align-items:center;gap:var(--wx-space-4) var(--wx-space-12);flex:1 1 auto;min-width:0;padding-block:var(--wx-space-4);color:inherit;text-decoration:none}.wx-service-row__cover[data-v-f99d37d6]{grid-area:cover;display:flex;align-items:center;justify-content:center;width:44px;height:32px;overflow:hidden;border-radius:var(--wx-radius-xs);background:var(--wx-bg-subtle);color:var(--wx-text-muted)}.wx-service-row__cover.is-empty[data-v-f99d37d6]{border:1px dashed var(--wx-border-default);background:var(--wx-bg-surface)}.wx-service-row__cover img[data-v-f99d37d6]{width:100%;height:100%;min-width:0;object-fit:cover}.wx-service-row__name[data-v-f99d37d6]{grid-area:name;display:flex;flex-direction:column;min-width:0}.wx-service-row__title[data-v-f99d37d6]{overflow:hidden;font-weight:var(--wx-font-weight-medium);white-space:nowrap;text-overflow:ellipsis}.wx-service-row__address[data-v-f99d37d6]{overflow:hidden;color:var(--wx-text-muted);font-family:var(--wx-font-family-mono);font-size:var(--wx-font-size-xs);white-space:nowrap;text-overflow:ellipsis}.wx-service-row__facts[data-v-f99d37d6]{grid-area:facts;display:flex;align-items:center;gap:var(--wx-space-12);min-width:0}.wx-service-row__chips[data-v-f99d37d6],.wx-service-row__state[data-v-f99d37d6]{display:flex;flex-wrap:wrap;align-items:center;gap:var(--wx-space-4);min-width:0}.wx-service-row__chips[data-v-f99d37d6]{justify-content:flex-end;max-width:320px}@container (max-width: 640px){.wx-service-row[data-v-f99d37d6]{grid-template-columns:auto minmax(0,1fr);grid-template-areas:"cover name" "cover facts";align-items:start}.wx-service-row__facts[data-v-f99d37d6]{flex-wrap:wrap;gap:var(--wx-space-4)}.wx-service-row__chips[data-v-f99d37d6],.wx-service-row__state[data-v-f99d37d6]{display:contents}.wx-service-row[data-v-f99d37d6]{column-gap:var(--wx-space-8)}.wx-services__list[data-v-f99d37d6] .wx-sortable-list__row{gap:var(--wx-space-8);padding-inline:var(--wx-space-4)}.wx-services__bar[data-v-f99d37d6]{padding-inline:var(--wx-space-4)}.wx-services__bar[data-v-f99d37d6]>.wx-select{max-width:none}}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { ScreenModel } from '@webx-ui/schema';
|
|
2
|
+
/**
|
|
3
|
+
* Never published · on the site · on the site with edits waiting · taken off it (§4.6).
|
|
4
|
+
*
|
|
5
|
+
* No "scheduled": a service has no date to be published under. "Taken off" and "draft" both mean
|
|
6
|
+
* it is not on the site, and only its history tells them apart.
|
|
7
|
+
*/
|
|
8
|
+
export type ServiceStatus = 'draft' | 'published' | 'modified' | 'unpublished';
|
|
9
|
+
/** A category as a row of the list names one. The first on a service is the main one. */
|
|
10
|
+
export interface ServiceCategoryRef {
|
|
11
|
+
id: number;
|
|
12
|
+
title: string;
|
|
13
|
+
slug?: string;
|
|
14
|
+
}
|
|
15
|
+
/** The cover, with the two addresses `module-media` works out on every read and never stores. */
|
|
16
|
+
export interface ServiceCover {
|
|
17
|
+
id: number;
|
|
18
|
+
path: string;
|
|
19
|
+
url: string;
|
|
20
|
+
thumb: string | null;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* One service as the section lists it: the draft's title beside the registry's address, which is
|
|
24
|
+
* why a renamed service shows its new name at its old address until it is published.
|
|
25
|
+
*/
|
|
26
|
+
export interface ServiceRow {
|
|
27
|
+
id: number;
|
|
28
|
+
title: string;
|
|
29
|
+
slug: string;
|
|
30
|
+
lead: string;
|
|
31
|
+
/** `null` — no address in the language the panel is open in. */
|
|
32
|
+
path: string | null;
|
|
33
|
+
url: string | null;
|
|
34
|
+
status: ServiceStatus;
|
|
35
|
+
/** Its place in the whole list. */
|
|
36
|
+
position: number;
|
|
37
|
+
published_at: string | null;
|
|
38
|
+
updated_at: string | null;
|
|
39
|
+
deleted_at: string | null;
|
|
40
|
+
cover: ServiceCover | null;
|
|
41
|
+
/** In the order they were dragged into: the first is the main one. */
|
|
42
|
+
categories: ServiceCategoryRef[];
|
|
43
|
+
/** What this service was when it was read, so a save can be refused rather than written over. */
|
|
44
|
+
revision: string;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The whole catalogue — no pages: the list is where services are put in order, and a drag cannot
|
|
48
|
+
* cross a page boundary (§4.6). The categories travel with it for the filter.
|
|
49
|
+
*/
|
|
50
|
+
export interface ServicesList {
|
|
51
|
+
data: ServiceRow[];
|
|
52
|
+
filters: {
|
|
53
|
+
categories: ServiceCategoryRef[];
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export interface ServiceQuery {
|
|
57
|
+
/** Title or address, in any language the site has. */
|
|
58
|
+
q?: string;
|
|
59
|
+
/** One category: the list comes in its own order, and a drag writes that order. */
|
|
60
|
+
category?: number | null;
|
|
61
|
+
status?: ServiceStatus | '';
|
|
62
|
+
/** What was deleted. The only way back to a service in the bin is through this list. */
|
|
63
|
+
trashed?: boolean;
|
|
64
|
+
}
|
|
65
|
+
export interface ServiceInput {
|
|
66
|
+
title: string;
|
|
67
|
+
slug?: string;
|
|
68
|
+
}
|
|
69
|
+
/** The values of `services.form` keyed by field name, and the revision they were read at. */
|
|
70
|
+
export interface ServiceSave {
|
|
71
|
+
values: ScreenModel;
|
|
72
|
+
revision?: string;
|
|
73
|
+
}
|
|
74
|
+
/** One service as its editor opens it. */
|
|
75
|
+
export interface ServiceDetail {
|
|
76
|
+
service: ServiceRow;
|
|
77
|
+
values: ScreenModel;
|
|
78
|
+
revision: string;
|
|
79
|
+
/** The first segment of every services address; `''` when the catalogue is at the root. */
|
|
80
|
+
prefix: string;
|
|
81
|
+
preview_url: string | null;
|
|
82
|
+
}
|
|
83
|
+
/** A 409: somebody wrote in between. `data` is the service as it now is. */
|
|
84
|
+
export interface ServiceConflict {
|
|
85
|
+
message: string;
|
|
86
|
+
data: ServiceDetail;
|
|
87
|
+
}
|
|
88
|
+
/** One publication in the history. */
|
|
89
|
+
export interface ServiceVersion {
|
|
90
|
+
number: number;
|
|
91
|
+
created_at: string | null;
|
|
92
|
+
author: string | null;
|
|
93
|
+
source: 'panel' | 'mcp' | 'import' | string;
|
|
94
|
+
comment: string | null;
|
|
95
|
+
is_pinned: boolean;
|
|
96
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@webx-ui/module-services",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Services for the WebX UI admin panel: a catalogue of services made of blocks, flat categories with pages of their own, ordered by hand.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": [
|
|
8
|
+
"*.css"
|
|
9
|
+
],
|
|
10
|
+
"keywords": [
|
|
11
|
+
"webx-ui",
|
|
12
|
+
"vue",
|
|
13
|
+
"admin",
|
|
14
|
+
"cms",
|
|
15
|
+
"services"
|
|
16
|
+
],
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/webx-ui/webx-ui.git",
|
|
20
|
+
"directory": "packages/module-services"
|
|
21
|
+
},
|
|
22
|
+
"homepage": "https://webx-ui.github.io/webx-ui/guide/services.html",
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist"
|
|
28
|
+
],
|
|
29
|
+
"main": "./dist/index.js",
|
|
30
|
+
"module": "./dist/index.js",
|
|
31
|
+
"types": "./dist/index.d.ts",
|
|
32
|
+
"exports": {
|
|
33
|
+
".": {
|
|
34
|
+
"types": "./dist/index.d.ts",
|
|
35
|
+
"import": "./dist/index.js"
|
|
36
|
+
},
|
|
37
|
+
"./style.css": "./dist/style.css",
|
|
38
|
+
"./dist/style.css": "./dist/style.css",
|
|
39
|
+
"./package.json": "./package.json"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"vue": "^3.5.0",
|
|
43
|
+
"vue-router": "^4.5.0"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@webx-ui/core": "^0.33.0",
|
|
47
|
+
"@webx-ui/module-admin": "^0.16.0",
|
|
48
|
+
"@webx-ui/module-blocks": "^0.8.5",
|
|
49
|
+
"@webx-ui/schema": "^0.6.0"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/node": "^24.10.1",
|
|
53
|
+
"@vitejs/plugin-vue": "^6.0.1",
|
|
54
|
+
"typescript": "^5.9.3",
|
|
55
|
+
"vite": "^7.1.14",
|
|
56
|
+
"vite-plugin-dts": "^4.5.4",
|
|
57
|
+
"vue": "^3.5.24",
|
|
58
|
+
"vue-router": "^4.5.1",
|
|
59
|
+
"vue-tsc": "^3.1.3"
|
|
60
|
+
},
|
|
61
|
+
"scripts": {
|
|
62
|
+
"build": "vite build",
|
|
63
|
+
"dev": "vite build --watch",
|
|
64
|
+
"typecheck": "vue-tsc -p tsconfig.json --noEmit"
|
|
65
|
+
}
|
|
66
|
+
}
|