@webx-ui/module-blog 0.8.2 → 0.8.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{ArticleEditorPage-DViFCNvm.js → ArticleEditorPage-Bkb5YrLy.js} +2 -2
- package/dist/{ArticleEditorPage-DViFCNvm.js.map → ArticleEditorPage-Bkb5YrLy.js.map} +1 -1
- package/dist/{ArticlesPage-CI-Nwhpy.js → ArticlesPage-BnHi8gF0.js} +173 -154
- package/dist/ArticlesPage-BnHi8gF0.js.map +1 -0
- package/dist/{AuthorsPage-BN6sRqyE.js → AuthorsPage-DSLsX44M.js} +2 -2
- package/dist/{AuthorsPage-BN6sRqyE.js.map → AuthorsPage-DSLsX44M.js.map} +1 -1
- package/dist/{TagsPage-rFfpCqPs.js → TagsPage-BnLVnx5a.js} +2 -2
- package/dist/{TagsPage-rFfpCqPs.js.map → TagsPage-BnLVnx5a.js.map} +1 -1
- package/dist/{index-DqN5h0Ej.js → index-ChbP89HC.js} +418 -417
- package/dist/index-ChbP89HC.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/style.css +1 -1
- package/package.json +5 -5
- package/dist/ArticlesPage-CI-Nwhpy.js.map +0 -1
- package/dist/index-DqN5h0Ej.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ArticlesPage-BnHi8gF0.js","sources":["../src/ArticlesPage.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, ref, useTemplateRef, watch } from 'vue'\nimport { useRoute, useRouter, type LocationQueryRaw } from 'vue-router'\nimport {\n useAdmin,\n useErrorText,\n useTranslate,\n WxDate,\n WxListScreen,\n WxFilterChips,\n rowMenuWidth,\n WxPreviewThumb,\n WxRowMenu,\n type AppliedFilter,\n type RowAction,\n type ScreenAction,\n WxAddressNote,\n} from '@webx-ui/module-admin'\nimport {\n confirm,\n createModal,\n toast,\n useElementWidth,\n WxBadge,\n WxButton,\n WxEntityCard,\n WxFormItem,\n WxIcon,\n WxSelect,\n WxTable,\n WxText,\n WxTooltip,\n type TabItem,\n type TableColumn,\n type TableState,\n type TabValue,\n} from '@webx-ui/core'\nimport ArticleCreateDialog from './ArticleCreateDialog.vue'\nimport { createBlogApi } from './api'\nimport { useBlogMessages } from './i18n'\nimport type { ArticleRow, ArticleStatus, ArticlesPage } from './types'\n\n/**\n * The section: a page of articles with the filters of §10 over it.\n *\n * A paginator rather than a tree, which is the whole difference from `Pages`: articles all sit\n * at the same depth and there are a hundred of them, so the screen is twenty rows, a filter bar\n * and a page number.\n *\n * Below {@link CARDS} the row becomes a card, and the card is not the table's own stack of\n * labelled lines: at 375px five of those are four hundred pixels for one article. It is the\n * cover on the left and everything else beside it, which is 96px and ten articles on a screen\n * instead of two (§10). The table draws whatever columns it is given, so the switch is made\n * here — one column at that width, seven above it — and both halves agree on the number because\n * it is the same constant.\n */\nconst props = withDefaults(defineProps<{ base?: string }>(), { base: '/blog' })\n\n/** Where the row stops being a row. Below it the table draws cards and this screen draws one. */\nconst CARDS = 640\n\nconst context = useAdmin()\nconst api = createBlogApi(context)\nconst route = useRoute()\nconst router = useRouter()\nuseBlogMessages()\n\nconst t = useTranslate('webx-blog')\n/* The two words a list needs as soon as it has filters belong to the panel, not to the blog. */\nconst admin = useTranslate('webx-admin')\n/* Not the server's `message`: the panel says how a request failed in its own words. */\nconst message = useErrorText()\n\nconst root = useTemplateRef<HTMLElement>('root')\nconst width = useElementWidth(root)\n\nconst page = ref<ArticlesPage | null>(null)\nconst loading = ref(true)\n\nconst create = createModal<ArticleRow, Record<string, never>>(ArticleCreateDialog)\n\nconst canManage = computed(() => context.can('blog.articles.manage'))\n\nconst title = computed(\n () =>\n context.state.manifest?.modules.find((module) => module.id === 'articles')?.title ??\n t('module.articles'),\n)\n\n/**\n * Everything the list is looking at lives in the address.\n *\n * Coming back from an article has to land on the same filter and the same page, or the section\n * costs the reader their place every time they open something.\n */\nconst view = computed<TabValue>({\n get: () => (typeof route.query.view === 'string' ? route.query.view : ''),\n set: (value) => {\n // A new view is a new list: the page it was on belongs to the view it was on.\n void router.replace({\n query: { ...route.query, view: value === '' ? undefined : String(value), page: undefined },\n })\n },\n})\n\nconst inBin = computed(() => view.value === 'trashed')\n\nconst query = computed(() => ({\n q: typeof route.query.q === 'string' ? route.query.q : '',\n rubric: number(route.query.rubric),\n tag: number(route.query.tag),\n author: number(route.query.author),\n sort: typeof route.query.sort === 'string' ? route.query.sort : null,\n page: Number(route.query.page ?? 1) || 1,\n}))\n\n/*\n * The covers of the rows on screen, large: a click on one opens them all, the full pictures, in\n * the order of the list (`useLightbox`).\n */\nconst covered = computed(() => (page.value?.data ?? []).filter((one) => one.cover?.thumb))\nconst covers = computed(() =>\n covered.value.map((one) => ({\n src: one.cover!.url ?? one.cover!.thumb!,\n thumb: one.cover!.thumb ?? undefined,\n alt: one.title,\n caption: one.title,\n })),\n)\n\nfunction coverOf(row: ArticleRow): number {\n return covered.value.indexOf(row)\n}\n\nfunction number(value: unknown): number | null {\n return typeof value === 'string' && value !== '' ? Number(value) || null : null\n}\n\n/** The views of the same list, as tabs over the card. The bin rides in the same row (§10). */\nconst views = computed<TabItem[]>(() => [\n { value: '', label: t('panel.view-all') },\n { value: 'published', label: t('panel.view-published') },\n { value: 'scheduled', label: t('panel.view-scheduled') },\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 filters = computed(() => page.value?.filters ?? { rubrics: [], tags: [], authors: [] })\n\nconst asCards = computed(() => width.value > 0 && width.value < CARDS)\n\n/**\n * Seven columns, or one.\n *\n * Every column but the title carries a width and the table is laid out `fixed`, so the ones\n * that drop below a breakpoint give what they had to the title rather than to whichever cell\n * happened to hold the longest string. They go in the order they can be spared — who wrote it,\n * then where it belongs, then when — and what is left at the bottom is the cover, the title and\n * the state, which is what identifies an article and what an editor is looking for.\n */\nconst columns = computed<TableColumn<ArticleRow>[]>(() => {\n if (asCards.value) {\n return [{ key: 'card', label: '' }]\n }\n\n return [\n { key: 'cover', label: '', width: 68 },\n /* The one that takes what the others leave, and the only one without a number. */\n { key: 'title', label: t('panel.column-title'), minWidth: 220 },\n { key: 'rubrics', label: t('panel.column-rubrics'), width: 190, hideBelow: 1000 },\n { key: 'author', label: t('panel.column-author'), width: 160, hideBelow: 1200 },\n {\n key: 'date',\n label: inBin.value ? t('panel.column-deleted') : t('panel.column-date'),\n width: 120,\n hideBelow: 700,\n // \"today at 16:22\" is three words the table will break over two lines given half a\n // chance, and a date read down a column has to be one line to be read at all.\n cellClass: 'wx-articles__when',\n },\n { key: 'status', label: t('panel.column-status'), width: 170 },\n { key: 'actions', label: '', width: rowMenuWidth, align: 'right' },\n ]\n})\n\nconst emptyText = computed(() => {\n if (query.value.q !== '') return t('panel.empty-search')\n\n return inBin.value ? t('panel.empty-bin') : t('panel.empty')\n})\n\n/**\n * What the three dropdowns are set to, said in the reader's words.\n *\n * They live behind the funnel now, and a shut panel says nothing about itself: a list narrowed\n * by something nobody can see is a list that looks wrong. The chips are the answer, and taking\n * one off is the same `narrow()` the dropdown itself calls.\n */\nconst applied = computed<AppliedFilter[]>(() => {\n const chips: AppliedFilter[] = []\n\n const rubric = filters.value.rubrics.find((item) => item.id === query.value.rubric)\n if (rubric) {\n chips.push({\n key: 'rubric',\n label: `${t('panel.filter-rubric')}: ${rubric.title}`,\n clear: () => narrow('rubric', undefined),\n })\n }\n\n const tag = filters.value.tags.find((item) => item.id === query.value.tag)\n if (tag) {\n chips.push({\n key: 'tag',\n label: `${t('panel.filter-tag')}: ${tag.title}`,\n clear: () => narrow('tag', undefined),\n })\n }\n\n const author = filters.value.authors.find((item) => item.id === query.value.author)\n if (author) {\n chips.push({\n key: 'author',\n label: `${t('panel.filter-author')}: ${author.title}`,\n clear: () => narrow('author', undefined),\n })\n }\n\n return chips\n})\n\n/** All three at once, from inside the panel they were set in. */\nfunction clearFilters(): void {\n void router.replace({\n query: {\n ...route.query,\n rubric: undefined,\n tag: undefined,\n author: undefined,\n page: undefined,\n },\n })\n}\n\nasync function load(state?: TableState): Promise<void> {\n loading.value = true\n\n try {\n page.value = await api.articles({\n q: state?.search ?? query.value.q,\n status: inBin.value ? '' : (view.value as ArticleStatus | ''),\n trashed: inBin.value,\n rubric: query.value.rubric,\n tag: query.value.tag,\n author: query.value.author,\n sort: state?.sort ? `${state.sort.order === 'desc' ? '-' : ''}${state.sort.key}` : null,\n page: state?.page ?? query.value.page,\n per_page: state?.perPage,\n })\n } catch (error) {\n toast.danger(message(error))\n } finally {\n loading.value = false\n }\n}\n\n/**\n * The table reports everything it knows in one event, and that is the one place the address is\n * written from — so a page, a sort and a search all survive opening an article and coming back,\n * and none of them has its own half of the bookkeeping.\n */\nfunction onState(state: TableState): void {\n const wanted: LocationQueryRaw = {\n ...route.query,\n q: state.search === '' ? undefined : state.search,\n sort:\n state.sort === null\n ? undefined\n : `${state.sort.order === 'desc' ? '-' : ''}${state.sort.key}`,\n page: state.page === 1 ? undefined : String(state.page),\n }\n\n void router.replace({ query: wanted })\n void load(state)\n}\n\n/** One of the three dropdowns moved: a new list, from its first page. */\nfunction narrow(name: 'rubric' | 'tag' | 'author', value: unknown): void {\n void router.replace({\n query: {\n ...route.query,\n [name]: typeof value === 'number' ? String(value) : undefined,\n page: undefined,\n },\n })\n}\n\n/* One source per value, not one getter returning an array: `query` is rebuilt on every change of\n the address, so an array is new each time and the watcher would fire on a page turn too —\n a second request, without `per_page`, racing the one `onState` already sent. */\nwatch(\n [view, () => query.value.rubric, () => query.value.tag, () => query.value.author],\n () => void load(),\n)\n\nvoid load()\n\n/**\n * The editor of an article arrives with session C; until it does, the row opens nothing rather\n * than pushing the router at a route that is not there.\n *\n * The bin never reaches here: the table is told the rows lead nowhere, and it withholds the\n * click along with the pointer and the highlight (CLAUDE.md §4).\n */\nfunction open(article: ArticleRow): void {\n const path = `${props.base}/articles/${article.id}`\n\n if (router.resolve(path).matched.length > 0) void router.push({ path, query: { ...route.query } })\n}\n\n/** A new article: the dialog asks for a title, the list shows it as a draft, the editor opens. */\nasync function add(): Promise<void> {\n const article = await create({})\n\n if (!article) return\n\n await load()\n open(article)\n}\n\nfunction actionsFor(article: ArticleRow): 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(article),\n },\n ]\n }\n\n const actions: RowAction[] = [\n { key: 'open', icon: 'edit', label: t('panel.open'), run: () => open(article) },\n ]\n\n if (article.url && article.status !== 'draft') {\n actions.push({\n key: 'site',\n icon: 'external-link',\n label: t('panel.open-on-site'),\n href: article.url,\n })\n }\n\n if (article.path !== null) {\n actions.push({\n key: 'copy',\n icon: 'copy',\n label: t('panel.copy-address'),\n run: () => void copyAddress(article),\n })\n }\n\n actions.push({\n key: 'pin',\n icon: 'star',\n label: article.pinned ? t('panel.unpin') : t('panel.pin'),\n run: () => void setPinned(article, !article.pinned),\n })\n\n actions.push(\n article.status === 'published' ||\n article.status === 'modified' ||\n article.status === 'scheduled'\n ? {\n key: 'unpublish',\n icon: 'eye-off',\n label: t('panel.unpublish'),\n run: () => void unpublish(article),\n }\n : {\n key: 'publish',\n icon: 'upload',\n label: t('panel.publish'),\n run: () => void publish(article),\n },\n )\n\n actions.push({\n key: 'delete',\n icon: 'trash',\n label: t('panel.delete'),\n danger: true,\n run: () => void remove(article),\n })\n\n return actions\n}\n\nasync function publish(article: ArticleRow): Promise<void> {\n await run(() => api.publish(article.id), t('panel.published'))\n}\n\nasync function unpublish(article: ArticleRow): Promise<void> {\n await run(() => api.unpublish(article.id), t('panel.unpublished-done'))\n}\n\nasync function setPinned(article: ArticleRow, pinned: boolean): Promise<void> {\n await run(\n () => api.save(article.id, { values: { pinned }, revision: article.revision }),\n pinned ? t('panel.pinned-done') : t('panel.unpinned'),\n )\n}\n\nasync function remove(article: ArticleRow): Promise<void> {\n const agreed = await confirm({\n title: t('panel.delete-title', { title: article.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(article.id), t('panel.deleted'))\n}\n\nasync function restore(article: ArticleRow): Promise<void> {\n await run(() => api.restore(article.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 // Either way: a refused publication has left the row saying what it was, and one that\n // worked has changed an order the answer does not carry row by row.\n await load()\n }\n}\n\nasync function copyAddress(article: ArticleRow): Promise<void> {\n if (!article.url) return\n\n try {\n await navigator.clipboard.writeText(article.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\n // screen and can be copied from there.\n }\n}\n\n/**\n * Live and live-with-edits are the same green: the article is on the site, and what is waiting\n * beside it is said by a chip of its own rather than by a different colour. An editor scanning\n * the column is asking \"is this up\", and two greens answer it once.\n */\nfunction badge(status: ArticleStatus): 'default' | 'success' | 'warning' | 'danger' {\n if (status === 'published' || status === 'modified') return 'success'\n if (status === 'scheduled') return 'warning'\n\n return 'default'\n}\n\n/** The address under the title, and the two different nothings that can stand there instead. */\nfunction address(article: ArticleRow): string {\n if (inBin.value) return '—'\n\n return article.path === null ? t('panel.no-address') : `/${article.path}`\n}\n\n/** The date a row is about: when it was published, or when it was deleted in the bin. */\nfunction dateOf(article: ArticleRow): string | null {\n return inBin.value ? article.deleted_at : article.published_at\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 ref=\"root\" class=\"wx-articles\">\n <wx-list-screen v-model:view=\"view\" :title=\"title\" :views=\"views\" :actions=\"actions\">\n <wx-table\n :data=\"page\"\n :columns=\"columns\"\n row-key=\"id\"\n searchable\n :clickable=\"!inBin\"\n :hover=\"!inBin\"\n flush\n :loading=\"loading\"\n layout=\"fixed\"\n :cards-below=\"CARDS\"\n :filters-count=\"applied.length\"\n :filters-label=\"admin('filters.title')\"\n :search-placeholder=\"t('panel.search')\"\n :empty-text=\"emptyText\"\n :aria-label=\"title\"\n @row-click=\"open\"\n @state-change=\"onState\"\n >\n <!--\n The three dropdowns live behind the funnel, and what they are set to comes back as\n chips beside it. Standing open they were three controls of chrome above the first\n article, and on a phone — where the table stacks everything in its head into a column —\n three lines of it before any data.\n -->\n <template #filters>\n <wx-form-item :label=\"t('panel.filter-rubric')\">\n <wx-select\n :model-value=\"query.rubric\"\n :options=\"filters.rubrics.map((item) => ({ value: item.id, label: item.title }))\"\n :placeholder=\"t('panel.any-rubric')\"\n clearable\n size=\"sm\"\n @update:model-value=\"(value: unknown) => narrow('rubric', value)\"\n />\n </wx-form-item>\n\n <wx-form-item :label=\"t('panel.filter-tag')\">\n <wx-select\n :model-value=\"query.tag\"\n :options=\"filters.tags.map((item) => ({ value: item.id, label: item.title }))\"\n :placeholder=\"t('panel.any-tag')\"\n clearable\n filterable\n size=\"sm\"\n @update:model-value=\"(value: unknown) => narrow('tag', value)\"\n />\n </wx-form-item>\n\n <wx-form-item :label=\"t('panel.filter-author')\">\n <wx-select\n :model-value=\"query.author\"\n :options=\"filters.authors.map((item) => ({ value: item.id, label: item.title }))\"\n :placeholder=\"t('panel.any-author')\"\n clearable\n size=\"sm\"\n @update:model-value=\"(value: unknown) => narrow('author', value)\"\n />\n </wx-form-item>\n\n <!-- Taking all of them off belongs with the fields it resets: beside the chips it\n reads as one more chip, and it is the only control there that does not take\n exactly one filter away. -->\n <wx-button v-if=\"applied.length > 0\" variant=\"text\" size=\"sm\" block @click=\"clearFilters\">\n <template #icon><wx-icon name=\"close\" /></template>\n {{ admin('filters.reset') }}\n </wx-button>\n </template>\n\n <template #applied>\n <wx-filter-chips :filters=\"applied\" />\n </template>\n\n <template #cell-cover=\"{ row }\">\n <span class=\"wx-articles__cover\" :class=\"{ 'is-empty': !row.cover }\">\n <wx-preview-thumb v-if=\"row.cover?.thumb\" fill :items=\"covers\" :start=\"coverOf(row)\">\n <img :src=\"row.cover.thumb\" alt=\"\" loading=\"lazy\" />\n </wx-preview-thumb>\n <wx-icon v-else name=\"image\" size=\"sm\" />\n </span>\n </template>\n\n <template #cell-title=\"{ row }\">\n <div class=\"wx-articles__name\">\n <wx-tooltip v-if=\"row.pinned\" :content=\"t('panel.pinned')\">\n <wx-icon class=\"wx-articles__pin\" name=\"star\" size=\"sm\" />\n </wx-tooltip>\n <div class=\"wx-articles__name-text\">\n <span class=\"wx-articles__title\">{{ row.title }}</span>\n <!-- An article in the bin holds no address — the registry let it go when it went\n in — and \"the address appears when it is published\" would be a promise about\n something nobody can publish without bringing it back first. -->\n <span class=\"wx-articles__address\">\n {{ address(row) }}\n <wx-address-note v-if=\"!inBin\" :locale=\"row.address_locale\" />\n </span>\n </div>\n </div>\n </template>\n\n <template #cell-rubrics=\"{ row }\">\n <span class=\"wx-articles__chips\">\n <wx-badge v-for=\"rubric in row.rubrics\" :key=\"rubric.id\" size=\"sm\" round>\n {{ rubric.title }}\n </wx-badge>\n </span>\n </template>\n\n <template #cell-author=\"{ row }\">\n <wx-text size=\"sm\" tone=\"muted\" truncate>\n {{ row.author ? row.author.name : '—' }}\n </wx-text>\n </template>\n\n <template #cell-date=\"{ row }\">\n <wx-date v-if=\"dateOf(row)\" :value=\"dateOf(row)\" compact />\n <wx-text v-else size=\"sm\" tone=\"muted\">—</wx-text>\n </template>\n\n <template #cell-status=\"{ row }\">\n <span class=\"wx-articles__state\">\n <wx-badge :type=\"badge(row.status)\" dot size=\"sm\">\n {{ t(`panel.status-${row.status}`) }}\n </wx-badge>\n <!-- What is waiting, said beside the state rather than instead of it: the article\n is on the site, and there is something that is not. -->\n <wx-badge v-if=\"row.status === 'modified'\" type=\"primary\" size=\"sm\" round>\n {{ t('panel.edits') }}\n </wx-badge>\n </span>\n </template>\n\n <!--\n The card: the cover on the left, everything else beside it. One field and no label,\n because the table's own stack of \"Title: … / Author: … / Date: …\" is five lines and\n four hundred pixels for one article (§10).\n -->\n <!--\n `WxEntityCard`, `plain` so that the box around it stays the table's own: a picture, a\n name, its address under it and a row of facts is exactly what the component is, and\n writing that by hand once per section is how five sections end up with five different\n cards.\n -->\n <template #cell-card=\"{ row }\">\n <wx-entity-card\n class=\"wx-articles__entity\"\n variant=\"plain\"\n :title=\"row.title\"\n :image=\"row.cover?.thumb ?? undefined\"\n image-size=\"56px\"\n :title-lines=\"2\"\n :subtitle=\"address(row)\"\n >\n <template #title>\n <wx-icon v-if=\"row.pinned\" class=\"wx-articles__pin\" name=\"star\" size=\"sm\" />\n {{ row.title }}\n </template>\n\n <template #meta>\n <wx-badge v-if=\"row.rubrics[0]\" size=\"sm\" round>{{ row.rubrics[0].title }}</wx-badge>\n <wx-badge :type=\"badge(row.status)\" dot size=\"sm\">\n {{ t(`panel.status-${row.status}`) }}\n </wx-badge>\n <wx-date v-if=\"dateOf(row)\" :value=\"dateOf(row)\" />\n </template>\n </wx-entity-card>\n </template>\n\n <!--\n The menu goes in the card's own top strip, beside the checkbox: that line is there\n either way as soon as rows can be picked, and a `···` inside the entity takes width\n off a title that is already one line short.\n -->\n <template #card-actions=\"{ row }\">\n <wx-row-menu :actions=\"actionsFor(row)\" :label=\"row.title\" />\n </template>\n\n <template #cell-actions=\"{ row }\">\n <wx-row-menu :actions=\"actionsFor(row)\" :label=\"row.title\" />\n </template>\n </wx-table>\n </wx-list-screen>\n </div>\n</template>\n\n<style scoped>\n/* The list screen inside it lays the screen out; this only keeps a wide table from pushing the\n column it sits in. */\n.wx-articles {\n min-width: 0;\n}\n\n/*\n * The cover is the picture's box, never the picture: `<img>` has `min-width: auto`, which is\n * its natural width, so a 1600px photograph would take the column and the ones beside it\n * (CLAUDE.md §4).\n */\n.wx-articles__cover {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 44px;\n height: 32px;\n overflow: hidden;\n /* The smallest step in the scale, and what `WxEntityCard` gives its own thumbnail: on a box\n 32px tall the next one up is 12, which is over a third of it. */\n border-radius: var(--wx-radius-xs);\n background: var(--wx-bg-subtle);\n color: var(--wx-text-muted);\n}\n\n.wx-articles__cover.is-empty {\n border: 1px dashed var(--wx-border-default);\n background: var(--wx-bg-surface);\n}\n\n.wx-articles__cover img {\n width: 100%;\n height: 100%;\n min-width: 0;\n object-fit: cover;\n}\n\n.wx-articles__name {\n display: flex;\n align-items: center;\n gap: var(--wx-space-8);\n min-width: 0;\n}\n\n.wx-articles__name-text {\n display: flex;\n flex-direction: column;\n min-width: 0;\n}\n\n.wx-articles__pin {\n flex: none;\n color: var(--wx-color-primary);\n}\n\n.wx-articles__title {\n overflow: hidden;\n font-weight: var(--wx-font-weight-medium);\n white-space: nowrap;\n text-overflow: ellipsis;\n}\n\n.wx-articles__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-articles__chips,\n.wx-articles__state {\n display: flex;\n align-items: center;\n gap: var(--wx-space-4);\n min-width: 0;\n overflow: hidden;\n}\n\n/* `:deep()` because the cell is the table's element and the class is ours — a scoped rule\n would be looking for our attribute on somebody else's markup (CLAUDE.md §4). */\n.wx-articles :deep(.wx-articles__when) {\n white-space: nowrap;\n}\n</style>\n"],"names":["CARDS","props","__props","context","useAdmin","api","createBlogApi","route","useRoute","router","useRouter","useBlogMessages","useTranslate","admin","message","useErrorText","root","useTemplateRef","width","useElementWidth","page","ref","loading","create","createModal","ArticleCreateDialog","canManage","computed","title","module","view","value","inBin","query","number","covered","one","covers","coverOf","row","views","filters","asCards","columns","rowMenuWidth","emptyText","applied","chips","rubric","item","narrow","tag","author","clearFilters","load","state","error","toast","onState","wanted","name","watch","open","article","path","add","actionsFor","restore","actions","copyAddress","setPinned","unpublish","publish","remove","run","pinned","confirm","request","said","badge","status","address","dateOf","_createElementBlock","_createVNode","_unref","WxListScreen","$event","WxTable","WxFormItem","WxSelect","_cache","_createBlock","WxButton","WxIcon","_createTextVNode","WxFilterChips","_withCtx","_createElementVNode","_normalizeClass","WxPreviewThumb","_hoisted_2","WxTooltip","_hoisted_3","_hoisted_4","_toDisplayString","_hoisted_5","WxAddressNote","_hoisted_6","_openBlock","_Fragment","_renderList","WxBadge","WxText","WxDate","_hoisted_7","WxEntityCard","WxRowMenu"],"mappings":"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"}
|
|
@@ -2,7 +2,7 @@ import { defineComponent as M, ref as C, computed as _, watch as T, onBeforeUnmo
|
|
|
2
2
|
import { useRoute as G, useRouter as H } from "vue-router";
|
|
3
3
|
import { WxCard as J, WxListDetail as K, WxEmpty as N, WxInput as O, WxIcon as E, WxSkeleton as Q, WxAvatar as X, WxBadge as I, toast as Y } from "@webx-ui/core";
|
|
4
4
|
import { useAdmin as Z, useTranslate as ee, useErrorText as te, WxListScreen as oe } from "@webx-ui/module-admin";
|
|
5
|
-
import { d as ae, u as se, A as le, a as ne } from "./index-
|
|
5
|
+
import { d as ae, u as se, A as le, a as ne } from "./index-ChbP89HC.js";
|
|
6
6
|
const re = { class: "wx-authors__bar" }, ue = ["onClick"], ie = { class: "wx-author-row__body" }, ce = { class: "wx-author-row__name" }, de = { class: "wx-author-row__meta" }, pe = { key: 0 }, he = { class: "wx-author-row__facts" }, me = /* @__PURE__ */ M({
|
|
7
7
|
__name: "AuthorsPage",
|
|
8
8
|
props: {
|
|
@@ -194,4 +194,4 @@ const re = { class: "wx-authors__bar" }, ue = ["onClick"], ie = { class: "wx-aut
|
|
|
194
194
|
export {
|
|
195
195
|
ge as default
|
|
196
196
|
};
|
|
197
|
-
//# sourceMappingURL=AuthorsPage-
|
|
197
|
+
//# sourceMappingURL=AuthorsPage-DSLsX44M.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AuthorsPage-BN6sRqyE.js","sources":["../src/AuthorsPage.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, ref, watch } from 'vue'\nimport { useRoute, useRouter, type LocationQueryRaw } from 'vue-router'\nimport {\n toast,\n WxAvatar,\n WxBadge,\n WxCard,\n WxEmpty,\n WxIcon,\n WxInput,\n WxListDetail,\n WxSkeleton,\n} from '@webx-ui/core'\nimport { useAdmin, useErrorText, useTranslate, WxListScreen } from '@webx-ui/module-admin'\nimport AuthorPane from './AuthorPane.vue'\nimport { createAuthorsApi, type AuthorRow } from './authors'\nimport { useBlogMessages } from './i18n'\n\n/**\n * The authors: every profile on the left, the one being written on the right (§19.5 of the blog\n * spec) — the shape of the team, without its order and its bin: the people who write one site are\n * a short list, and a profile that goes takes nothing with it but a name on a page.\n *\n * The search and the open profile live in the address, so a link to an author — the one the panel\n * on the site and the article editor make — is a link to them in their list.\n */\nwithDefaults(defineProps<{ base?: string }>(), { base: '/blog' })\n\nconst context = useAdmin()\nconst api = createAuthorsApi(context)\nconst route = useRoute()\nconst router = useRouter()\nuseBlogMessages()\n\nconst t = useTranslate('webx-blog')\n/* Not the server's `message`: the panel says how a request failed in its own words. */\nconst message = useErrorText()\n\nconst rows = ref<AuthorRow[]>([])\nconst loading = ref(true)\n\nconst canManage = computed(() => context.can('blog.authors.manage'))\n\nconst title = computed(\n () =>\n context.state.manifest?.modules.find((module) => module.id === 'authors')?.title ??\n t('module.authors'),\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\n/** The open profile: a number, `'new'` for one not yet saved, or nothing. */\nconst selected = computed<number | 'new' | null>(() => {\n const raw = route.query.author\n\n if (raw === 'new') return canManage.value ? 'new' : null\n\n return typeof raw === 'string' && raw !== '' ? Number(raw) || null : null\n})\n\n/* Open is \"a profile is chosen\", so the address, the highlight and the drawer always agree. */\nconst open = computed({\n get: () => selected.value !== null,\n set: (value) => {\n if (!value) close()\n },\n})\n\nasync function load(): Promise<void> {\n loading.value = true\n\n try {\n rows.value = await api.list(q.value)\n } catch (error) {\n toast.danger(message(error))\n } finally {\n loading.value = false\n }\n}\n\nwatch(q, () => void load())\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\n/* `replace` and not `push`: going through the authors is not a step of history each. */\nfunction choose(id: number | 'new'): void {\n const query: LocationQueryRaw = { ...route.query, author: String(id) }\n\n // The account the article editor asked a profile for belongs to that one new profile only.\n if (id !== 'new') delete query.account\n void router.replace({ query })\n}\n\nfunction close(): void {\n const query = { ...route.query }\n\n delete query.author\n delete query.account\n void router.replace({ query })\n}\n\n/** A new profile was saved: it is a record now, and the list has it. */\nfunction created(author: AuthorRow): void {\n choose(author.id)\n void load()\n}\n\nfunction removed(id: number): void {\n if (selected.value === id) close()\n\n void load()\n}\n</script>\n\n<template>\n <wx-list-screen :title=\"title\" :card=\"false\">\n <wx-card class=\"wx-authors\" padding=\"none\">\n <wx-list-detail\n v-model:open=\"open\"\n class=\"wx-authors__panes\"\n :list-width=\"360\"\n :detail-min=\"460\"\n :detail-label=\"t('module.authors')\"\n >\n <template #list>\n <div class=\"wx-authors__bar\">\n <wx-input\n v-model=\"typed\"\n class=\"wx-authors__search\"\n size=\"sm\"\n clearable\n :placeholder=\"t('author.search')\"\n :aria-label=\"t('author.search')\"\n >\n <template #prefix><wx-icon name=\"search\" size=\"sm\" /></template>\n </wx-input>\n </div>\n\n <button\n v-if=\"canManage\"\n type=\"button\"\n class=\"wx-authors__new\"\n :class=\"{ 'is-current': selected === 'new' }\"\n @click=\"choose('new')\"\n >\n <wx-icon name=\"plus\" size=\"sm\" />\n {{ t('author.new') }}\n </button>\n\n <wx-skeleton v-if=\"loading && rows.length === 0\" class=\"wx-authors__loading\" :rows=\"4\" />\n\n <wx-empty\n v-else-if=\"rows.length === 0\"\n :title=\"q === '' ? t('author.empty') : t('author.empty-search')\"\n :description=\"q === '' ? t('author.empty-help') : undefined\"\n />\n\n <ul v-else class=\"wx-authors__list\" :class=\"{ 'is-loading': loading }\">\n <li v-for=\"item in rows\" :key=\"item.id\">\n <button\n type=\"button\"\n class=\"wx-author-row\"\n :class=\"{ 'is-current': selected === item.id }\"\n @click=\"choose(item.id)\"\n >\n <wx-avatar\n class=\"wx-author-row__photo\"\n :src=\"item.photo ?? undefined\"\n :name=\"item.name\"\n size=\"md\"\n />\n\n <span class=\"wx-author-row__body\">\n <span class=\"wx-author-row__name\">{{ item.name }}</span>\n\n <span class=\"wx-author-row__meta\">\n <span v-if=\"item.user\">{{ item.user.name }}</span>\n <span>{{ t('author.articles', { count: item.articles_count }) }}</span>\n </span>\n\n <span class=\"wx-author-row__facts\">\n <wx-badge v-if=\"item.open\" dot size=\"sm\" type=\"success\">\n {{ t('author.page-open') }}\n </wx-badge>\n <wx-badge v-else dot size=\"sm\">{{ t('author.page-closed') }}</wx-badge>\n </span>\n </span>\n </button>\n </li>\n </ul>\n </template>\n\n <template #detail=\"{ inline, back }\">\n <author-pane\n v-if=\"selected !== null\"\n :id=\"selected === 'new' ? null : selected\"\n :key=\"String(selected)\"\n :inline=\"inline\"\n @back=\"back\"\n @saved=\"load\"\n @created=\"created\"\n @removed=\"removed\"\n />\n </template>\n\n <template #empty>\n <wx-empty\n :title=\"t('author.choose')\"\n :description=\"canManage ? t('author.choose-help') : undefined\"\n />\n </template>\n </wx-list-detail>\n </wx-card>\n </wx-list-screen>\n</template>\n\n<style>\n/*\n * What scrolls here is the page, as on every other list of the panel: `clip`, not `hidden`, which\n * would make the card a scroll container and pin everything sticky inside it (CLAUDE.md §4).\n */\n.wx-authors > .wx-card__body {\n display: flex;\n flex-direction: column;\n overflow: clip;\n border-radius: inherit;\n}\n</style>\n\n<style scoped>\n.wx-authors__bar {\n display: flex;\n gap: var(--wx-space-8);\n padding: var(--wx-space-12) var(--wx-space-12) var(--wx-space-8);\n}\n\n.wx-authors__search {\n flex: 1 1 180px;\n min-width: 0;\n}\n\n.wx-authors__new {\n display: flex;\n align-items: center;\n gap: var(--wx-space-8);\n width: calc(100% - 2 * var(--wx-space-8));\n margin: 0 var(--wx-space-8) var(--wx-space-4);\n padding: var(--wx-space-8);\n border: 1px dashed var(--wx-border-default);\n border-radius: var(--wx-radius-sm);\n background: none;\n font: inherit;\n color: var(--wx-color-primary);\n text-align: start;\n cursor: pointer;\n}\n\n.wx-authors__new:hover,\n.wx-authors__new.is-current {\n background: var(--wx-bg-subtle);\n}\n\n.wx-authors__loading {\n padding: var(--wx-space-12);\n}\n\n.wx-authors__list {\n margin: 0;\n padding: 0 var(--wx-space-8) var(--wx-space-8);\n list-style: none;\n}\n\n.wx-authors__list.is-loading {\n opacity: 0.6;\n}\n\n.wx-author-row {\n display: flex;\n align-items: flex-start;\n gap: var(--wx-space-10);\n width: 100%;\n min-width: 0;\n padding: var(--wx-space-8) var(--wx-space-4);\n border: 0;\n border-radius: var(--wx-radius-sm);\n background: none;\n font: inherit;\n color: inherit;\n text-align: start;\n cursor: pointer;\n}\n\n.wx-author-row:hover,\n.wx-author-row.is-current {\n background: var(--wx-bg-subtle);\n}\n\n.wx-author-row__photo {\n flex: none;\n}\n\n.wx-author-row__body {\n display: flex;\n flex: 1 1 auto;\n flex-direction: column;\n gap: var(--wx-space-2);\n min-width: 0;\n}\n\n.wx-author-row__name {\n min-width: 0;\n overflow: hidden;\n font-weight: var(--wx-font-weight-medium);\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.wx-author-row.is-current .wx-author-row__name {\n color: var(--wx-color-primary);\n}\n\n.wx-author-row__meta {\n display: flex;\n flex-wrap: wrap;\n gap: var(--wx-space-2) var(--wx-space-8);\n color: var(--wx-text-muted);\n font-size: var(--wx-font-size-sm);\n}\n\n.wx-author-row__facts {\n display: flex;\n margin-top: var(--wx-space-2);\n}\n</style>\n"],"names":["context","useAdmin","api","createAuthorsApi","route","useRoute","router","useRouter","useBlogMessages","t","useTranslate","message","useErrorText","rows","ref","loading","canManage","computed","title","module","q","typed","selected","raw","open","value","close","load","error","toast","watch","pause","onBeforeUnmount","choose","id","query","created","author","removed","_createBlock","_unref","WxListScreen","_createVNode","WxCard","WxListDetail","$event","_createElementVNode","_hoisted_1","WxInput","WxIcon","_createElementBlock","_normalizeClass","_createTextVNode","WxSkeleton","WxEmpty","_Fragment","_renderList","item","WxAvatar","_hoisted_3","_hoisted_4","_toDisplayString","_hoisted_5","_openBlock","_hoisted_6","_hoisted_7","WxBadge","_withCtx","inline","back","AuthorPane"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AuthorsPage-DSLsX44M.js","sources":["../src/AuthorsPage.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, ref, watch } from 'vue'\nimport { useRoute, useRouter, type LocationQueryRaw } from 'vue-router'\nimport {\n toast,\n WxAvatar,\n WxBadge,\n WxCard,\n WxEmpty,\n WxIcon,\n WxInput,\n WxListDetail,\n WxSkeleton,\n} from '@webx-ui/core'\nimport { useAdmin, useErrorText, useTranslate, WxListScreen } from '@webx-ui/module-admin'\nimport AuthorPane from './AuthorPane.vue'\nimport { createAuthorsApi, type AuthorRow } from './authors'\nimport { useBlogMessages } from './i18n'\n\n/**\n * The authors: every profile on the left, the one being written on the right (§19.5 of the blog\n * spec) — the shape of the team, without its order and its bin: the people who write one site are\n * a short list, and a profile that goes takes nothing with it but a name on a page.\n *\n * The search and the open profile live in the address, so a link to an author — the one the panel\n * on the site and the article editor make — is a link to them in their list.\n */\nwithDefaults(defineProps<{ base?: string }>(), { base: '/blog' })\n\nconst context = useAdmin()\nconst api = createAuthorsApi(context)\nconst route = useRoute()\nconst router = useRouter()\nuseBlogMessages()\n\nconst t = useTranslate('webx-blog')\n/* Not the server's `message`: the panel says how a request failed in its own words. */\nconst message = useErrorText()\n\nconst rows = ref<AuthorRow[]>([])\nconst loading = ref(true)\n\nconst canManage = computed(() => context.can('blog.authors.manage'))\n\nconst title = computed(\n () =>\n context.state.manifest?.modules.find((module) => module.id === 'authors')?.title ??\n t('module.authors'),\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\n/** The open profile: a number, `'new'` for one not yet saved, or nothing. */\nconst selected = computed<number | 'new' | null>(() => {\n const raw = route.query.author\n\n if (raw === 'new') return canManage.value ? 'new' : null\n\n return typeof raw === 'string' && raw !== '' ? Number(raw) || null : null\n})\n\n/* Open is \"a profile is chosen\", so the address, the highlight and the drawer always agree. */\nconst open = computed({\n get: () => selected.value !== null,\n set: (value) => {\n if (!value) close()\n },\n})\n\nasync function load(): Promise<void> {\n loading.value = true\n\n try {\n rows.value = await api.list(q.value)\n } catch (error) {\n toast.danger(message(error))\n } finally {\n loading.value = false\n }\n}\n\nwatch(q, () => void load())\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\n/* `replace` and not `push`: going through the authors is not a step of history each. */\nfunction choose(id: number | 'new'): void {\n const query: LocationQueryRaw = { ...route.query, author: String(id) }\n\n // The account the article editor asked a profile for belongs to that one new profile only.\n if (id !== 'new') delete query.account\n void router.replace({ query })\n}\n\nfunction close(): void {\n const query = { ...route.query }\n\n delete query.author\n delete query.account\n void router.replace({ query })\n}\n\n/** A new profile was saved: it is a record now, and the list has it. */\nfunction created(author: AuthorRow): void {\n choose(author.id)\n void load()\n}\n\nfunction removed(id: number): void {\n if (selected.value === id) close()\n\n void load()\n}\n</script>\n\n<template>\n <wx-list-screen :title=\"title\" :card=\"false\">\n <wx-card class=\"wx-authors\" padding=\"none\">\n <wx-list-detail\n v-model:open=\"open\"\n class=\"wx-authors__panes\"\n :list-width=\"360\"\n :detail-min=\"460\"\n :detail-label=\"t('module.authors')\"\n >\n <template #list>\n <div class=\"wx-authors__bar\">\n <wx-input\n v-model=\"typed\"\n class=\"wx-authors__search\"\n size=\"sm\"\n clearable\n :placeholder=\"t('author.search')\"\n :aria-label=\"t('author.search')\"\n >\n <template #prefix><wx-icon name=\"search\" size=\"sm\" /></template>\n </wx-input>\n </div>\n\n <button\n v-if=\"canManage\"\n type=\"button\"\n class=\"wx-authors__new\"\n :class=\"{ 'is-current': selected === 'new' }\"\n @click=\"choose('new')\"\n >\n <wx-icon name=\"plus\" size=\"sm\" />\n {{ t('author.new') }}\n </button>\n\n <wx-skeleton v-if=\"loading && rows.length === 0\" class=\"wx-authors__loading\" :rows=\"4\" />\n\n <wx-empty\n v-else-if=\"rows.length === 0\"\n :title=\"q === '' ? t('author.empty') : t('author.empty-search')\"\n :description=\"q === '' ? t('author.empty-help') : undefined\"\n />\n\n <ul v-else class=\"wx-authors__list\" :class=\"{ 'is-loading': loading }\">\n <li v-for=\"item in rows\" :key=\"item.id\">\n <button\n type=\"button\"\n class=\"wx-author-row\"\n :class=\"{ 'is-current': selected === item.id }\"\n @click=\"choose(item.id)\"\n >\n <wx-avatar\n class=\"wx-author-row__photo\"\n :src=\"item.photo ?? undefined\"\n :name=\"item.name\"\n size=\"md\"\n />\n\n <span class=\"wx-author-row__body\">\n <span class=\"wx-author-row__name\">{{ item.name }}</span>\n\n <span class=\"wx-author-row__meta\">\n <span v-if=\"item.user\">{{ item.user.name }}</span>\n <span>{{ t('author.articles', { count: item.articles_count }) }}</span>\n </span>\n\n <span class=\"wx-author-row__facts\">\n <wx-badge v-if=\"item.open\" dot size=\"sm\" type=\"success\">\n {{ t('author.page-open') }}\n </wx-badge>\n <wx-badge v-else dot size=\"sm\">{{ t('author.page-closed') }}</wx-badge>\n </span>\n </span>\n </button>\n </li>\n </ul>\n </template>\n\n <template #detail=\"{ inline, back }\">\n <author-pane\n v-if=\"selected !== null\"\n :id=\"selected === 'new' ? null : selected\"\n :key=\"String(selected)\"\n :inline=\"inline\"\n @back=\"back\"\n @saved=\"load\"\n @created=\"created\"\n @removed=\"removed\"\n />\n </template>\n\n <template #empty>\n <wx-empty\n :title=\"t('author.choose')\"\n :description=\"canManage ? t('author.choose-help') : undefined\"\n />\n </template>\n </wx-list-detail>\n </wx-card>\n </wx-list-screen>\n</template>\n\n<style>\n/*\n * What scrolls here is the page, as on every other list of the panel: `clip`, not `hidden`, which\n * would make the card a scroll container and pin everything sticky inside it (CLAUDE.md §4).\n */\n.wx-authors > .wx-card__body {\n display: flex;\n flex-direction: column;\n overflow: clip;\n border-radius: inherit;\n}\n</style>\n\n<style scoped>\n.wx-authors__bar {\n display: flex;\n gap: var(--wx-space-8);\n padding: var(--wx-space-12) var(--wx-space-12) var(--wx-space-8);\n}\n\n.wx-authors__search {\n flex: 1 1 180px;\n min-width: 0;\n}\n\n.wx-authors__new {\n display: flex;\n align-items: center;\n gap: var(--wx-space-8);\n width: calc(100% - 2 * var(--wx-space-8));\n margin: 0 var(--wx-space-8) var(--wx-space-4);\n padding: var(--wx-space-8);\n border: 1px dashed var(--wx-border-default);\n border-radius: var(--wx-radius-sm);\n background: none;\n font: inherit;\n color: var(--wx-color-primary);\n text-align: start;\n cursor: pointer;\n}\n\n.wx-authors__new:hover,\n.wx-authors__new.is-current {\n background: var(--wx-bg-subtle);\n}\n\n.wx-authors__loading {\n padding: var(--wx-space-12);\n}\n\n.wx-authors__list {\n margin: 0;\n padding: 0 var(--wx-space-8) var(--wx-space-8);\n list-style: none;\n}\n\n.wx-authors__list.is-loading {\n opacity: 0.6;\n}\n\n.wx-author-row {\n display: flex;\n align-items: flex-start;\n gap: var(--wx-space-10);\n width: 100%;\n min-width: 0;\n padding: var(--wx-space-8) var(--wx-space-4);\n border: 0;\n border-radius: var(--wx-radius-sm);\n background: none;\n font: inherit;\n color: inherit;\n text-align: start;\n cursor: pointer;\n}\n\n.wx-author-row:hover,\n.wx-author-row.is-current {\n background: var(--wx-bg-subtle);\n}\n\n.wx-author-row__photo {\n flex: none;\n}\n\n.wx-author-row__body {\n display: flex;\n flex: 1 1 auto;\n flex-direction: column;\n gap: var(--wx-space-2);\n min-width: 0;\n}\n\n.wx-author-row__name {\n min-width: 0;\n overflow: hidden;\n font-weight: var(--wx-font-weight-medium);\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.wx-author-row.is-current .wx-author-row__name {\n color: var(--wx-color-primary);\n}\n\n.wx-author-row__meta {\n display: flex;\n flex-wrap: wrap;\n gap: var(--wx-space-2) var(--wx-space-8);\n color: var(--wx-text-muted);\n font-size: var(--wx-font-size-sm);\n}\n\n.wx-author-row__facts {\n display: flex;\n margin-top: var(--wx-space-2);\n}\n</style>\n"],"names":["context","useAdmin","api","createAuthorsApi","route","useRoute","router","useRouter","useBlogMessages","t","useTranslate","message","useErrorText","rows","ref","loading","canManage","computed","title","module","q","typed","selected","raw","open","value","close","load","error","toast","watch","pause","onBeforeUnmount","choose","id","query","created","author","removed","_createBlock","_unref","WxListScreen","_createVNode","WxCard","WxListDetail","$event","_createElementVNode","_hoisted_1","WxInput","WxIcon","_createElementBlock","_normalizeClass","_createTextVNode","WxSkeleton","WxEmpty","_Fragment","_renderList","item","WxAvatar","_hoisted_3","_hoisted_4","_toDisplayString","_hoisted_5","_openBlock","_hoisted_6","_hoisted_7","WxBadge","_withCtx","inline","back","AuthorPane"],"mappings":"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"}
|
|
@@ -2,7 +2,7 @@ import { defineComponent as H, useId as de, ref as S, useTemplateRef as ge, onMo
|
|
|
2
2
|
import { useRoute as he, useRouter as _e } from "vue-router";
|
|
3
3
|
import { useTranslate as I, useAdmin as ke, useErrorText as Te, rowMenuWidth as qe, WxListScreen as We, WxRowMenu as j } from "@webx-ui/module-admin";
|
|
4
4
|
import { useModal as Se, WxDialog as Be, WxFormItem as Me, WxInput as Re, WxSpace as $e, WxButton as D, createModal as A, confirm as G, toast as x, WxTable as Ie, WxTooltip as Ve, WxText as T, WxActionBar as Ce, WxIcon as ze } from "@webx-ui/core";
|
|
5
|
-
import { u as J, c as Ne, b as Ae, T as De, a as Pe } from "./index-
|
|
5
|
+
import { u as J, c as Ne, b as Ae, T as De, a as Pe } from "./index-ChbP89HC.js";
|
|
6
6
|
const Ue = ["id"], Ee = /* @__PURE__ */ H({
|
|
7
7
|
__name: "TagRenameDialog",
|
|
8
8
|
props: {
|
|
@@ -460,4 +460,4 @@ const Ue = ["id"], Ee = /* @__PURE__ */ H({
|
|
|
460
460
|
export {
|
|
461
461
|
Je as default
|
|
462
462
|
};
|
|
463
|
-
//# sourceMappingURL=TagsPage-
|
|
463
|
+
//# sourceMappingURL=TagsPage-BnLVnx5a.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TagsPage-rFfpCqPs.js","sources":["../src/TagRenameDialog.vue","../src/TagsPage.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { nextTick, onMounted, ref, useId, useTemplateRef } from 'vue'\nimport { useTranslate } from '@webx-ui/module-admin'\nimport { useModal, WxButton, WxDialog, WxFormItem, WxInput, WxSpace } from '@webx-ui/core'\nimport { useBlogMessages } from './i18n'\n\n/**\n * Renaming a tag: one field, one button, and nothing changes until it is pressed.\n *\n * It used to be done in the cell — the name turned into a box when it was clicked. Two things\n * were wrong with that. A name that is quietly an `<input>` reads as a name: nothing says it can\n * be typed in, and a stray click on a row is a rename nobody asked for. And the box saved itself\n * on `blur`, which never fired: `blur` does not bubble, so the listener on the field's wrapper\n * heard nothing and clicking away simply lost what had been typed.\n *\n * The address deliberately does not follow the word. A tag respelled three times before lunch\n * would otherwise leave three aliases behind a decision nobody made; the address has a menu item\n * of its own.\n */\nconst props = defineProps<{ name: string }>()\n\nconst { open, resolve, dismiss } = useModal<string>()\n\nuseBlogMessages()\n\nconst t = useTranslate('webx-blog')\n/* 'Save' is the panel's word, translated once for every module that has a form. */\nconst panel = useTranslate('webx-admin')\n\n/* The footer button lives outside the form, so it says which form it submits. */\nconst formId = useId()\n\nconst draft = ref(props.name)\nconst field = useTemplateRef<HTMLFormElement>('field')\n\nonMounted(async () => {\n await nextTick()\n\n const input = field.value?.querySelector('input')\n\n // Selected rather than merely focused: renaming is usually replacing, not appending.\n input?.focus()\n input?.select()\n})\n\nfunction submit(): void {\n const next = draft.value.trim()\n\n // An empty name is not a rename, it is a tag with nothing to call it by.\n if (next === '' || next === props.name) {\n dismiss()\n\n return\n }\n\n resolve(next)\n}\n</script>\n\n<template>\n <wx-dialog v-model:open=\"open\" :title=\"t('tag.rename')\" :width=\"420\">\n <!-- A real form, so that Enter does what the button does. A listener on the field is not\n the same thing: a form with one text field submits on Enter by itself, which is what a\n one-field dialog is for. -->\n <form :id=\"formId\" ref=\"field\" @submit.prevent=\"submit\">\n <wx-form-item :label=\"t('tag.field-title')\" required>\n <!-- The key as well as the form. Implicit submission is a browser behaviour and this\n field lives inside a dialog that takes the keyboard over; measured here, a real\n Enter reached the input, was not prevented by anybody, and submitted nothing. -->\n <wx-input\n v-model=\"draft\"\n :aria-label=\"t('tag.field-title')\"\n @keydown.enter.prevent=\"submit\"\n />\n </wx-form-item>\n </form>\n\n <template #footer>\n <wx-space size=\"sm\">\n <wx-button variant=\"outline\" @click=\"dismiss()\">{{ t('tag.cancel') }}</wx-button>\n <wx-button\n type=\"primary\"\n native-type=\"submit\"\n :form=\"formId\"\n :disabled=\"draft.trim() === ''\"\n >\n {{ panel('editor.save') }}\n </wx-button>\n </wx-space>\n </template>\n </wx-dialog>\n</template>\n","<script setup lang=\"ts\">\nimport { computed, ref, watch } from 'vue'\nimport { useRoute, useRouter, type LocationQueryRaw } from 'vue-router'\nimport {\n useAdmin,\n useErrorText,\n useTranslate,\n WxListScreen,\n rowMenuWidth,\n WxRowMenu,\n type RowAction,\n type ScreenAction,\n} from '@webx-ui/module-admin'\nimport {\n confirm,\n createModal,\n toast,\n WxActionBar,\n WxButton,\n WxIcon,\n WxTable,\n WxText,\n WxTooltip,\n type RowKey,\n type TabItem,\n type TableColumn,\n type TableSort,\n type TableState,\n type TabValue,\n} from '@webx-ui/core'\nimport TagCreateDialog from './TagCreateDialog.vue'\nimport TagRenameDialog from './TagRenameDialog.vue'\nimport TagMergeDialog from './TagMergeDialog.vue'\nimport { createBlogApi } from './api'\nimport { useBlogMessages } from './i18n'\nimport type { TagIndexing, TagMerged, TagQuery, TagRow, TagsPage } from './types'\n\n/**\n * Tags, and the raking over of them (§10).\n *\n * Entered from the article form by the hundred, which is what the whole screen is shaped by:\n * within six months the table holds \"belts\", \"belt\" and \"drive belts\", and all three are right.\n * So renaming is done in place — a tag is one word, and a form to change one word is a form\n * nobody opens — and everything else is done to a pile of rows at once.\n *\n * It stays a table at every width, with columns dropping out by `hideBelow` until the word and\n * the `···` are left. Cards would be 250px each for a word and a number (CLAUDE.md §4), and\n * this is a screen somebody scrolls through sixty rows of.\n *\n * \"Not indexed\" is the one filter that is not a `where`: it is the answer of the same rule the\n * rendered page follows, rules in the SEO section included, so the column, the tab's count and\n * the filter are one answer worked out on the server (§12).\n */\nconst props = withDefaults(defineProps<{ base?: string }>(), { base: '/blog' })\n\nconst context = useAdmin()\nconst api = createBlogApi(context)\nconst route = useRoute()\nconst router = useRouter()\nuseBlogMessages()\n\nconst t = useTranslate('webx-blog')\nconst panel = useTranslate('webx-admin')\n/* Not the server's `message`: the panel says how a request failed in its own words. */\nconst message = useErrorText()\n\nconst page = ref<TagsPage | null>(null)\nconst loading = ref(true)\nconst selected = ref<RowKey[]>([])\nconst working = ref(false)\n\nconst rename = createModal<string, { name: string }>(TagRenameDialog)\n\nconst create = createModal<TagRow, Record<string, never>>(TagCreateDialog)\nconst merge = createModal<TagMerged, { tags: TagRow[] }>(TagMergeDialog)\n\nconst canManage = computed(() => context.can('blog.taxonomy.manage'))\n\nconst title = computed(\n () =>\n context.state.manifest?.modules.find((module) => module.id === 'tags')?.title ??\n t('module.tags'),\n)\n\n/** Everything the list is looking at lives in the address, so a link to it is a link. */\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), page: undefined },\n })\n },\n})\n\nconst counts = computed(() => page.value?.filters ?? { total: 0, empty: 0, noindex: 0 })\n\nconst views = computed<TabItem[]>(() => [\n { value: '', label: t('tag.view-all'), badge: counts.value.total },\n { value: 'empty', label: t('tag.view-empty'), badge: counts.value.empty },\n { value: 'noindex', label: t('tag.view-noindex'), badge: counts.value.noindex },\n])\n\nconst rows = computed(() => page.value?.data ?? [])\n\nconst chosen = computed(() => rows.value.filter((tag) => selected.value.includes(tag.id as RowKey)))\n\n/**\n * Five columns, and never cards.\n *\n * The word and the menu stay at every width; the address goes first, because it is the one\n * thing a tag's name already tells you, then the state of the index. What is left on a phone is\n * a word, a number and a `···`, which is the screen this is (CLAUDE.md §4).\n */\nconst columns = computed<TableColumn<TagRow>[]>(() => [\n { key: 'title', label: t('tag.column-title'), minWidth: 150, sortable: true },\n // The number is what the other columns and the narrowest the title may be add up to without\n // this one: below it the address is what does not fit, and a fixed table does not shrink —\n // it scrolls sideways and takes the ··· with it (CLAUDE.md §4). Measured, not guessed: at\n // 375px the row is the word, the number and the menu, and nothing hangs off the edge.\n { key: 'path', label: t('tag.column-address'), width: 180, hideBelow: 760 },\n {\n key: 'articles_count',\n label: t('tag.column-articles'),\n width: 80,\n align: 'right',\n sortable: true,\n },\n // Wide enough for the longest of the three states on one line, in every language: measured\n // at 180px of text, and a cell keeps 32 of its own. Narrower, the ruled row wrapped and\n // stood eighteen pixels taller than the ones around it.\n { key: 'indexing', label: t('tag.column-indexing'), width: 230, hideBelow: 560 },\n { key: 'actions', label: '', width: rowMenuWidth, align: 'right' },\n])\n\nfunction query(state?: TableState): TagQuery {\n return {\n q: state?.search ?? (typeof route.query.q === 'string' ? route.query.q : ''),\n empty: view.value === 'empty',\n noindex: view.value === 'noindex',\n // Only when it is not the default: most used first is what the list is, and a query\n // string that repeats the default is a link that outlives a change of mind about it.\n sort: sortOf(state),\n page: state?.page ?? (Number(route.query.page ?? 1) || 1),\n per_page: state?.perPage,\n }\n}\n\nasync function load(state?: TableState): Promise<void> {\n loading.value = true\n\n try {\n page.value = await api.tagsPage(query(state))\n } catch (error) {\n toast.danger(message(error))\n } finally {\n loading.value = false\n }\n}\n\n/**\n * The order the table asks for, in the words the server knows.\n *\n * Two keys and a direction: the word from A, or the used ones first — and a minus turns either\n * of them around. The default is most used first, so the query string leaves it out: a link that\n * repeats the default outlives a change of mind about it.\n */\nfunction sortOf(state?: TableState): TagQuery['sort'] {\n const from = state?.sort ?? sortFromRoute()\n\n if (!from) return undefined\n\n const key = from.key === 'title' ? 'name' : 'articles'\n const sort = `${from.order === 'desc' ? '-' : ''}${key}` as NonNullable<TagQuery['sort']>\n\n return sort === 'articles' ? undefined : sort\n}\n\n/** What the address says the order is, for a list arrived at by a link. */\nfunction sortFromRoute(): TableSort | null {\n const value = typeof route.query.sort === 'string' ? route.query.sort : ''\n\n if (value === '') return null\n\n const desc = value.startsWith('-')\n const key = (desc ? value.slice(1) : value) === 'name' ? 'title' : 'articles_count'\n\n return { key, order: desc ? 'desc' : 'asc' }\n}\n\n/** The table reports everything it knows in one event, and the address is written from it. */\nfunction onState(state: TableState): void {\n const wanted: LocationQueryRaw = {\n ...route.query,\n q: state.search === '' ? undefined : state.search,\n sort: sortOf(state),\n page: state.page === 1 ? undefined : String(state.page),\n }\n\n void router.replace({ query: wanted })\n void load(state)\n}\n\nwatch(view, () => {\n // A new view is a new list, and a selection made in the old one is a selection of rows\n // nobody can see any more.\n selected.value = []\n void load()\n})\n\nvoid load()\n\nasync function add(): Promise<void> {\n if (await create({})) await load()\n}\n\n/**\n * Renaming, as an act rather than a side effect.\n *\n * A dialog with one field, because a name that is quietly an `<input>` reads as a name: nothing\n * says it can be typed in, and a stray click on a row is a rename nobody asked for. The address\n * does not move with the word — a tag respelled three times before lunch would leave three\n * aliases behind a decision nobody made — so it has a menu item of its own.\n */\nasync function startRename(tag: TagRow): Promise<void> {\n const title = await rename({ name: tag.title })\n\n if (title) await patch(tag, { title }, t('tag.renamed'))\n}\n\nasync function setIndexing(tag: TagRow, noindex: boolean): Promise<void> {\n await patch(tag, { noindex }, noindex ? t('tag.kept-out') : t('tag.indexed'))\n}\n\n/**\n * One tag changed, and the row it changed in.\n *\n * The answer replaces the row rather than reloading the page: the list is sixty words long and\n * somebody is halfway down it, and a reload after every rename would take their place away.\n */\nasync function patch(\n tag: TagRow,\n input: { title?: string; noindex?: boolean },\n said: string,\n): Promise<void> {\n try {\n const saved = await api.saveTag(tag.id, input)\n\n if (page.value) {\n page.value = {\n ...page.value,\n data: page.value.data.map((row) => (row.id === saved.id ? saved : row)),\n }\n }\n\n toast.success(said)\n } catch (error) {\n toast.danger(message(error))\n }\n}\n\nasync function remove(tag: TagRow): Promise<void> {\n const agreed = await confirm({\n title: t('tag.delete-title', { title: tag.title }),\n message:\n tag.articles_count > 0\n ? t('tag.delete-text-used', { count: tag.articles_count })\n : t('tag.delete-text'),\n confirmText: t('tag.delete'),\n cancelText: t('tag.cancel'),\n tone: 'danger',\n })\n\n if (!agreed) return\n\n try {\n await api.removeTag(tag.id)\n toast.success(t('tag.deleted'))\n selected.value = selected.value.filter((key) => key !== tag.id)\n await load()\n } catch (error) {\n toast.danger(message(error))\n }\n}\n\n/**\n * What can be done to the pile, behind the same `···` a single row has.\n *\n * Three buttons in the bar were three lines of chrome on a phone and the same three words every\n * row already offers; merging stays a button because it is the one thing here that cannot be\n * done to a row on its own.\n */\nconst massActions = computed<RowAction[]>(() => [\n {\n key: 'index',\n icon: 'eye',\n label: t('tag.index'),\n disabled: working.value,\n run: () => void massSelected('index'),\n },\n {\n key: 'noindex',\n icon: 'eye-off',\n label: t('tag.noindex'),\n disabled: working.value,\n run: () => void massSelected('noindex'),\n },\n {\n key: 'delete',\n icon: 'trash',\n label: t('tag.delete'),\n danger: true,\n disabled: working.value,\n run: () => void massSelected('delete'),\n },\n])\n\nfunction actionsFor(tag: TagRow): RowAction[] {\n if (!canManage.value) return []\n\n const actions: RowAction[] = [\n { key: 'rename', icon: 'edit', label: t('tag.rename'), run: () => void startRename(tag) },\n ]\n\n if (tag.url) {\n actions.push({\n key: 'site',\n icon: 'external-link',\n label: t('tag.open-on-site'),\n href: tag.url,\n })\n }\n\n actions.push({\n key: 'articles',\n icon: 'list',\n label: t('tag.show-articles'),\n run: () => void router.push({ path: `${props.base}/articles`, query: { tag: String(tag.id) } }),\n })\n\n actions.push(\n tag.noindex\n ? {\n key: 'index',\n icon: 'eye',\n label: t('tag.index'),\n run: () => void setIndexing(tag, false),\n }\n : {\n key: 'noindex',\n icon: 'eye-off',\n label: t('tag.noindex'),\n run: () => void setIndexing(tag, true),\n },\n )\n\n actions.push({\n key: 'delete',\n icon: 'trash',\n label: t('tag.delete'),\n danger: true,\n run: () => void remove(tag),\n })\n\n return actions\n}\n\n/** The colour the state of the index is said in: green for in, quiet for out (§12). */\nfunction tone(indexing: TagIndexing): 'success' | 'muted' {\n return indexing === 'noindex' ? 'muted' : 'success'\n}\n\nasync function mergeSelected(): Promise<void> {\n if (chosen.value.length < 2) return\n\n const merged = await merge({ tags: chosen.value })\n\n if (!merged) return\n\n toast.success(t('tag.merged', { tag: merged.tag.title, count: merged.articles_count }))\n selected.value = []\n await load()\n}\n\nasync function massSelected(action: 'index' | 'noindex' | 'delete'): Promise<void> {\n const ids = chosen.value.map((tag) => tag.id)\n\n if (ids.length === 0 || working.value) return\n\n if (action === 'delete') {\n const held = chosen.value.reduce((sum, tag) => sum + tag.articles_count, 0)\n\n const agreed = await confirm({\n title: t('tag.delete-many-title', { count: ids.length }),\n message:\n held > 0 ? t('tag.delete-many-text-used', { count: held }) : t('tag.delete-many-text'),\n confirmText: t('tag.delete'),\n cancelText: t('tag.cancel'),\n tone: 'danger',\n })\n\n if (!agreed) return\n }\n\n working.value = true\n\n try {\n const affected = await api.massTags(ids, action)\n\n toast.success(\n action === 'delete'\n ? t('tag.deleted-many', { count: affected })\n : action === 'index'\n ? t('tag.indexed-many', { count: affected })\n : t('tag.kept-out-many', { count: affected }),\n )\n selected.value = []\n await load()\n } catch (error) {\n toast.danger(message(error))\n } finally {\n working.value = false\n }\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('tag.new'), icon: 'plus', primary: true, run: () => void add() }]\n : [],\n)\n</script>\n\n<template>\n <div class=\"wx-tags\">\n <wx-list-screen v-model:view=\"view\" :title=\"title\" :views=\"views\" :actions=\"actions\">\n <wx-table\n v-model:selected=\"selected\"\n :data=\"page\"\n :columns=\"columns\"\n row-key=\"id\"\n searchable\n hover\n flush\n layout=\"fixed\"\n :loading=\"loading\"\n :selectable=\"canManage\"\n :select-row-label=\"panel('filters.select-row')\"\n :select-all-label=\"panel('filters.select-all')\"\n :cards-below=\"0\"\n :search-placeholder=\"t('tag.search')\"\n :empty-text=\"rows.length === 0 && !loading ? t('tag.empty') : undefined\"\n :aria-label=\"title\"\n @state-change=\"onState\"\n >\n <template #cell-title=\"{ row }\">\n <wx-text truncate>{{ row.title }}</wx-text>\n </template>\n\n <template #cell-path=\"{ row }\">\n <wx-text size=\"sm\" tone=\"muted\" mono truncate>\n {{ row.path ?? t('tag.no-address') }}\n </wx-text>\n </template>\n\n <template #cell-articles_count=\"{ row }\">\n <wx-text :tone=\"row.articles_count === 0 ? 'muted' : 'default'\">\n {{ row.articles_count }}\n </wx-text>\n </template>\n\n <!--\n Three states and not two. A rule in the SEO section opens the page whatever the flag\n says, and an editor who wrote that rule must not be left looking at a row that says\n `noindex` and disagreeing with it (§12).\n -->\n <template #cell-indexing=\"{ row }\">\n <wx-tooltip v-if=\"row.indexing === 'rule'\" :content=\"t('tag.indexing-rule-help')\">\n <wx-text size=\"sm\" :tone=\"tone(row.indexing)\">{{ t('tag.indexing-rule') }}</wx-text>\n </wx-tooltip>\n <wx-text v-else size=\"sm\" :tone=\"tone(row.indexing)\">\n {{ t(`tag.indexing-${row.indexing}`) }}\n </wx-text>\n </template>\n\n <template #cell-actions=\"{ row }\">\n <wx-row-menu :actions=\"actionsFor(row)\" :label=\"row.title\" />\n </template>\n </wx-table>\n </wx-list-screen>\n\n <!--\n The selection bar, at the bottom of the screen and only while something is selected.\n `WxActionBar` rather than a floating pill of its own: the panel already has one shape for\n \"what can be done from here\", and a second one would be a second one to keep in step.\n -->\n <wx-action-bar v-if=\"chosen.length > 0\" class=\"wx-tags__bar\" sticky>\n <template #state>\n <wx-text weight=\"medium\">{{ t('tag.selected', { count: chosen.length }) }}</wx-text>\n </template>\n\n <!--\n One button and a menu. Merging is what this bar exists for — it is the only thing here\n that cannot be done to a row on its own — and the other three are the row's own menu\n applied to several rows at once, which is where a reader already looks for them.\n\n The icon is a slot and not a prop: `WxButton` takes `#icon`, and `icon=\"…\"` lands on the\n element as an attribute and draws nothing at all (CLAUDE.md §4).\n -->\n <wx-button type=\"primary\" :disabled=\"chosen.length < 2 || working\" @click=\"mergeSelected\">\n <template #icon><wx-icon name=\"link\" /></template>\n {{ t('tag.merge') }}\n </wx-button>\n\n <!--\n No way out of the selection here. Untick the rows, or untick them all from the box in\n the heading — a × beside a red \"Delete\" is a button whose whole job is to undo something\n harmless, standing where the dangerous one is, and it read as a way to close the bar.\n\n `lg` because the menu stands beside a button rather than at the end of a row: a row menu\n is 30px, a button is 42, and the pair read as a control and a leftover. In a bar the two\n are the same two things and have to be the same size.\n -->\n <wx-row-menu\n size=\"lg\"\n :actions=\"massActions\"\n :label=\"t('tag.selected', { count: chosen.length })\"\n />\n </wx-action-bar>\n </div>\n</template>\n\n<style scoped>\n.wx-tags {\n display: flex;\n flex-direction: column;\n gap: var(--wx-space-12);\n min-width: 0;\n}\n\n.wx-tags__rename {\n display: block;\n min-width: 0;\n}\n\n.wx-tags__sort {\n display: flex;\n gap: var(--wx-space-4);\n min-width: 0;\n}\n\n/*\n * The name is the rename: a tag is one word, and a form to change one word is a form nobody\n * opens. It reads as text until it is hovered, because a table of sixty underlined words is a\n * table of sixty links to nowhere.\n */\n.wx-tags__name {\n max-width: 100%;\n overflow: hidden;\n padding: 0;\n border: 0;\n background: none;\n color: inherit;\n font: inherit;\n text-align: start;\n text-overflow: ellipsis;\n white-space: nowrap;\n cursor: text;\n}\n\n.wx-tags__name:hover {\n text-decoration: underline;\n text-decoration-style: dotted;\n}\n\n/*\n * One line on a phone, not two.\n *\n * The bar keeps its state box at 220px so that a sentence about a draft — \"Saved · goes out on\n * 25 September\" — has room to be read before the buttons take a line of their own. What stands\n * here is two words and a number, and asking for 220 of them pushed \"Merge\" and the ··· onto a\n * second line at every phone width. `:deep()` because the box is the bar's element and the rule\n * is ours. Measured at 375: the words 89px, the button and the ··· 203, and 41px of air left\n * between them.\n */\n.wx-tags__bar :deep(.wx-action-bar__state) {\n flex: 0 1 auto;\n}\n</style>\n"],"names":["props","__props","open","resolve","dismiss","useModal","useBlogMessages","t","useTranslate","panel","formId","useId","draft","ref","field","useTemplateRef","onMounted","nextTick","input","submit","next","_createBlock","_unref","WxDialog","$event","_createVNode","WxSpace","WxButton","_createElementVNode","WxFormItem","WxInput","context","useAdmin","api","createBlogApi","route","useRoute","router","useRouter","message","useErrorText","page","loading","selected","working","rename","createModal","TagRenameDialog","create","TagCreateDialog","merge","TagMergeDialog","canManage","computed","title","module","view","value","counts","views","rows","chosen","tag","columns","rowMenuWidth","query","state","sortOf","load","error","toast","from","sortFromRoute","key","sort","desc","onState","wanted","watch","add","startRename","patch","setIndexing","noindex","said","saved","row","remove","confirm","massActions","massSelected","actionsFor","actions","tone","indexing","mergeSelected","merged","action","ids","held","sum","affected","_openBlock","_createElementBlock","_hoisted_1","WxListScreen","WxTable","_withCtx","WxText","_createTextVNode","_toDisplayString","WxTooltip","WxRowMenu","WxActionBar","WxIcon"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"TagsPage-BnLVnx5a.js","sources":["../src/TagRenameDialog.vue","../src/TagsPage.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { nextTick, onMounted, ref, useId, useTemplateRef } from 'vue'\nimport { useTranslate } from '@webx-ui/module-admin'\nimport { useModal, WxButton, WxDialog, WxFormItem, WxInput, WxSpace } from '@webx-ui/core'\nimport { useBlogMessages } from './i18n'\n\n/**\n * Renaming a tag: one field, one button, and nothing changes until it is pressed.\n *\n * It used to be done in the cell — the name turned into a box when it was clicked. Two things\n * were wrong with that. A name that is quietly an `<input>` reads as a name: nothing says it can\n * be typed in, and a stray click on a row is a rename nobody asked for. And the box saved itself\n * on `blur`, which never fired: `blur` does not bubble, so the listener on the field's wrapper\n * heard nothing and clicking away simply lost what had been typed.\n *\n * The address deliberately does not follow the word. A tag respelled three times before lunch\n * would otherwise leave three aliases behind a decision nobody made; the address has a menu item\n * of its own.\n */\nconst props = defineProps<{ name: string }>()\n\nconst { open, resolve, dismiss } = useModal<string>()\n\nuseBlogMessages()\n\nconst t = useTranslate('webx-blog')\n/* 'Save' is the panel's word, translated once for every module that has a form. */\nconst panel = useTranslate('webx-admin')\n\n/* The footer button lives outside the form, so it says which form it submits. */\nconst formId = useId()\n\nconst draft = ref(props.name)\nconst field = useTemplateRef<HTMLFormElement>('field')\n\nonMounted(async () => {\n await nextTick()\n\n const input = field.value?.querySelector('input')\n\n // Selected rather than merely focused: renaming is usually replacing, not appending.\n input?.focus()\n input?.select()\n})\n\nfunction submit(): void {\n const next = draft.value.trim()\n\n // An empty name is not a rename, it is a tag with nothing to call it by.\n if (next === '' || next === props.name) {\n dismiss()\n\n return\n }\n\n resolve(next)\n}\n</script>\n\n<template>\n <wx-dialog v-model:open=\"open\" :title=\"t('tag.rename')\" :width=\"420\">\n <!-- A real form, so that Enter does what the button does. A listener on the field is not\n the same thing: a form with one text field submits on Enter by itself, which is what a\n one-field dialog is for. -->\n <form :id=\"formId\" ref=\"field\" @submit.prevent=\"submit\">\n <wx-form-item :label=\"t('tag.field-title')\" required>\n <!-- The key as well as the form. Implicit submission is a browser behaviour and this\n field lives inside a dialog that takes the keyboard over; measured here, a real\n Enter reached the input, was not prevented by anybody, and submitted nothing. -->\n <wx-input\n v-model=\"draft\"\n :aria-label=\"t('tag.field-title')\"\n @keydown.enter.prevent=\"submit\"\n />\n </wx-form-item>\n </form>\n\n <template #footer>\n <wx-space size=\"sm\">\n <wx-button variant=\"outline\" @click=\"dismiss()\">{{ t('tag.cancel') }}</wx-button>\n <wx-button\n type=\"primary\"\n native-type=\"submit\"\n :form=\"formId\"\n :disabled=\"draft.trim() === ''\"\n >\n {{ panel('editor.save') }}\n </wx-button>\n </wx-space>\n </template>\n </wx-dialog>\n</template>\n","<script setup lang=\"ts\">\nimport { computed, ref, watch } from 'vue'\nimport { useRoute, useRouter, type LocationQueryRaw } from 'vue-router'\nimport {\n useAdmin,\n useErrorText,\n useTranslate,\n WxListScreen,\n rowMenuWidth,\n WxRowMenu,\n type RowAction,\n type ScreenAction,\n} from '@webx-ui/module-admin'\nimport {\n confirm,\n createModal,\n toast,\n WxActionBar,\n WxButton,\n WxIcon,\n WxTable,\n WxText,\n WxTooltip,\n type RowKey,\n type TabItem,\n type TableColumn,\n type TableSort,\n type TableState,\n type TabValue,\n} from '@webx-ui/core'\nimport TagCreateDialog from './TagCreateDialog.vue'\nimport TagRenameDialog from './TagRenameDialog.vue'\nimport TagMergeDialog from './TagMergeDialog.vue'\nimport { createBlogApi } from './api'\nimport { useBlogMessages } from './i18n'\nimport type { TagIndexing, TagMerged, TagQuery, TagRow, TagsPage } from './types'\n\n/**\n * Tags, and the raking over of them (§10).\n *\n * Entered from the article form by the hundred, which is what the whole screen is shaped by:\n * within six months the table holds \"belts\", \"belt\" and \"drive belts\", and all three are right.\n * So renaming is done in place — a tag is one word, and a form to change one word is a form\n * nobody opens — and everything else is done to a pile of rows at once.\n *\n * It stays a table at every width, with columns dropping out by `hideBelow` until the word and\n * the `···` are left. Cards would be 250px each for a word and a number (CLAUDE.md §4), and\n * this is a screen somebody scrolls through sixty rows of.\n *\n * \"Not indexed\" is the one filter that is not a `where`: it is the answer of the same rule the\n * rendered page follows, rules in the SEO section included, so the column, the tab's count and\n * the filter are one answer worked out on the server (§12).\n */\nconst props = withDefaults(defineProps<{ base?: string }>(), { base: '/blog' })\n\nconst context = useAdmin()\nconst api = createBlogApi(context)\nconst route = useRoute()\nconst router = useRouter()\nuseBlogMessages()\n\nconst t = useTranslate('webx-blog')\nconst panel = useTranslate('webx-admin')\n/* Not the server's `message`: the panel says how a request failed in its own words. */\nconst message = useErrorText()\n\nconst page = ref<TagsPage | null>(null)\nconst loading = ref(true)\nconst selected = ref<RowKey[]>([])\nconst working = ref(false)\n\nconst rename = createModal<string, { name: string }>(TagRenameDialog)\n\nconst create = createModal<TagRow, Record<string, never>>(TagCreateDialog)\nconst merge = createModal<TagMerged, { tags: TagRow[] }>(TagMergeDialog)\n\nconst canManage = computed(() => context.can('blog.taxonomy.manage'))\n\nconst title = computed(\n () =>\n context.state.manifest?.modules.find((module) => module.id === 'tags')?.title ??\n t('module.tags'),\n)\n\n/** Everything the list is looking at lives in the address, so a link to it is a link. */\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), page: undefined },\n })\n },\n})\n\nconst counts = computed(() => page.value?.filters ?? { total: 0, empty: 0, noindex: 0 })\n\nconst views = computed<TabItem[]>(() => [\n { value: '', label: t('tag.view-all'), badge: counts.value.total },\n { value: 'empty', label: t('tag.view-empty'), badge: counts.value.empty },\n { value: 'noindex', label: t('tag.view-noindex'), badge: counts.value.noindex },\n])\n\nconst rows = computed(() => page.value?.data ?? [])\n\nconst chosen = computed(() => rows.value.filter((tag) => selected.value.includes(tag.id as RowKey)))\n\n/**\n * Five columns, and never cards.\n *\n * The word and the menu stay at every width; the address goes first, because it is the one\n * thing a tag's name already tells you, then the state of the index. What is left on a phone is\n * a word, a number and a `···`, which is the screen this is (CLAUDE.md §4).\n */\nconst columns = computed<TableColumn<TagRow>[]>(() => [\n { key: 'title', label: t('tag.column-title'), minWidth: 150, sortable: true },\n // The number is what the other columns and the narrowest the title may be add up to without\n // this one: below it the address is what does not fit, and a fixed table does not shrink —\n // it scrolls sideways and takes the ··· with it (CLAUDE.md §4). Measured, not guessed: at\n // 375px the row is the word, the number and the menu, and nothing hangs off the edge.\n { key: 'path', label: t('tag.column-address'), width: 180, hideBelow: 760 },\n {\n key: 'articles_count',\n label: t('tag.column-articles'),\n width: 80,\n align: 'right',\n sortable: true,\n },\n // Wide enough for the longest of the three states on one line, in every language: measured\n // at 180px of text, and a cell keeps 32 of its own. Narrower, the ruled row wrapped and\n // stood eighteen pixels taller than the ones around it.\n { key: 'indexing', label: t('tag.column-indexing'), width: 230, hideBelow: 560 },\n { key: 'actions', label: '', width: rowMenuWidth, align: 'right' },\n])\n\nfunction query(state?: TableState): TagQuery {\n return {\n q: state?.search ?? (typeof route.query.q === 'string' ? route.query.q : ''),\n empty: view.value === 'empty',\n noindex: view.value === 'noindex',\n // Only when it is not the default: most used first is what the list is, and a query\n // string that repeats the default is a link that outlives a change of mind about it.\n sort: sortOf(state),\n page: state?.page ?? (Number(route.query.page ?? 1) || 1),\n per_page: state?.perPage,\n }\n}\n\nasync function load(state?: TableState): Promise<void> {\n loading.value = true\n\n try {\n page.value = await api.tagsPage(query(state))\n } catch (error) {\n toast.danger(message(error))\n } finally {\n loading.value = false\n }\n}\n\n/**\n * The order the table asks for, in the words the server knows.\n *\n * Two keys and a direction: the word from A, or the used ones first — and a minus turns either\n * of them around. The default is most used first, so the query string leaves it out: a link that\n * repeats the default outlives a change of mind about it.\n */\nfunction sortOf(state?: TableState): TagQuery['sort'] {\n const from = state?.sort ?? sortFromRoute()\n\n if (!from) return undefined\n\n const key = from.key === 'title' ? 'name' : 'articles'\n const sort = `${from.order === 'desc' ? '-' : ''}${key}` as NonNullable<TagQuery['sort']>\n\n return sort === 'articles' ? undefined : sort\n}\n\n/** What the address says the order is, for a list arrived at by a link. */\nfunction sortFromRoute(): TableSort | null {\n const value = typeof route.query.sort === 'string' ? route.query.sort : ''\n\n if (value === '') return null\n\n const desc = value.startsWith('-')\n const key = (desc ? value.slice(1) : value) === 'name' ? 'title' : 'articles_count'\n\n return { key, order: desc ? 'desc' : 'asc' }\n}\n\n/** The table reports everything it knows in one event, and the address is written from it. */\nfunction onState(state: TableState): void {\n const wanted: LocationQueryRaw = {\n ...route.query,\n q: state.search === '' ? undefined : state.search,\n sort: sortOf(state),\n page: state.page === 1 ? undefined : String(state.page),\n }\n\n void router.replace({ query: wanted })\n void load(state)\n}\n\nwatch(view, () => {\n // A new view is a new list, and a selection made in the old one is a selection of rows\n // nobody can see any more.\n selected.value = []\n void load()\n})\n\nvoid load()\n\nasync function add(): Promise<void> {\n if (await create({})) await load()\n}\n\n/**\n * Renaming, as an act rather than a side effect.\n *\n * A dialog with one field, because a name that is quietly an `<input>` reads as a name: nothing\n * says it can be typed in, and a stray click on a row is a rename nobody asked for. The address\n * does not move with the word — a tag respelled three times before lunch would leave three\n * aliases behind a decision nobody made — so it has a menu item of its own.\n */\nasync function startRename(tag: TagRow): Promise<void> {\n const title = await rename({ name: tag.title })\n\n if (title) await patch(tag, { title }, t('tag.renamed'))\n}\n\nasync function setIndexing(tag: TagRow, noindex: boolean): Promise<void> {\n await patch(tag, { noindex }, noindex ? t('tag.kept-out') : t('tag.indexed'))\n}\n\n/**\n * One tag changed, and the row it changed in.\n *\n * The answer replaces the row rather than reloading the page: the list is sixty words long and\n * somebody is halfway down it, and a reload after every rename would take their place away.\n */\nasync function patch(\n tag: TagRow,\n input: { title?: string; noindex?: boolean },\n said: string,\n): Promise<void> {\n try {\n const saved = await api.saveTag(tag.id, input)\n\n if (page.value) {\n page.value = {\n ...page.value,\n data: page.value.data.map((row) => (row.id === saved.id ? saved : row)),\n }\n }\n\n toast.success(said)\n } catch (error) {\n toast.danger(message(error))\n }\n}\n\nasync function remove(tag: TagRow): Promise<void> {\n const agreed = await confirm({\n title: t('tag.delete-title', { title: tag.title }),\n message:\n tag.articles_count > 0\n ? t('tag.delete-text-used', { count: tag.articles_count })\n : t('tag.delete-text'),\n confirmText: t('tag.delete'),\n cancelText: t('tag.cancel'),\n tone: 'danger',\n })\n\n if (!agreed) return\n\n try {\n await api.removeTag(tag.id)\n toast.success(t('tag.deleted'))\n selected.value = selected.value.filter((key) => key !== tag.id)\n await load()\n } catch (error) {\n toast.danger(message(error))\n }\n}\n\n/**\n * What can be done to the pile, behind the same `···` a single row has.\n *\n * Three buttons in the bar were three lines of chrome on a phone and the same three words every\n * row already offers; merging stays a button because it is the one thing here that cannot be\n * done to a row on its own.\n */\nconst massActions = computed<RowAction[]>(() => [\n {\n key: 'index',\n icon: 'eye',\n label: t('tag.index'),\n disabled: working.value,\n run: () => void massSelected('index'),\n },\n {\n key: 'noindex',\n icon: 'eye-off',\n label: t('tag.noindex'),\n disabled: working.value,\n run: () => void massSelected('noindex'),\n },\n {\n key: 'delete',\n icon: 'trash',\n label: t('tag.delete'),\n danger: true,\n disabled: working.value,\n run: () => void massSelected('delete'),\n },\n])\n\nfunction actionsFor(tag: TagRow): RowAction[] {\n if (!canManage.value) return []\n\n const actions: RowAction[] = [\n { key: 'rename', icon: 'edit', label: t('tag.rename'), run: () => void startRename(tag) },\n ]\n\n if (tag.url) {\n actions.push({\n key: 'site',\n icon: 'external-link',\n label: t('tag.open-on-site'),\n href: tag.url,\n })\n }\n\n actions.push({\n key: 'articles',\n icon: 'list',\n label: t('tag.show-articles'),\n run: () => void router.push({ path: `${props.base}/articles`, query: { tag: String(tag.id) } }),\n })\n\n actions.push(\n tag.noindex\n ? {\n key: 'index',\n icon: 'eye',\n label: t('tag.index'),\n run: () => void setIndexing(tag, false),\n }\n : {\n key: 'noindex',\n icon: 'eye-off',\n label: t('tag.noindex'),\n run: () => void setIndexing(tag, true),\n },\n )\n\n actions.push({\n key: 'delete',\n icon: 'trash',\n label: t('tag.delete'),\n danger: true,\n run: () => void remove(tag),\n })\n\n return actions\n}\n\n/** The colour the state of the index is said in: green for in, quiet for out (§12). */\nfunction tone(indexing: TagIndexing): 'success' | 'muted' {\n return indexing === 'noindex' ? 'muted' : 'success'\n}\n\nasync function mergeSelected(): Promise<void> {\n if (chosen.value.length < 2) return\n\n const merged = await merge({ tags: chosen.value })\n\n if (!merged) return\n\n toast.success(t('tag.merged', { tag: merged.tag.title, count: merged.articles_count }))\n selected.value = []\n await load()\n}\n\nasync function massSelected(action: 'index' | 'noindex' | 'delete'): Promise<void> {\n const ids = chosen.value.map((tag) => tag.id)\n\n if (ids.length === 0 || working.value) return\n\n if (action === 'delete') {\n const held = chosen.value.reduce((sum, tag) => sum + tag.articles_count, 0)\n\n const agreed = await confirm({\n title: t('tag.delete-many-title', { count: ids.length }),\n message:\n held > 0 ? t('tag.delete-many-text-used', { count: held }) : t('tag.delete-many-text'),\n confirmText: t('tag.delete'),\n cancelText: t('tag.cancel'),\n tone: 'danger',\n })\n\n if (!agreed) return\n }\n\n working.value = true\n\n try {\n const affected = await api.massTags(ids, action)\n\n toast.success(\n action === 'delete'\n ? t('tag.deleted-many', { count: affected })\n : action === 'index'\n ? t('tag.indexed-many', { count: affected })\n : t('tag.kept-out-many', { count: affected }),\n )\n selected.value = []\n await load()\n } catch (error) {\n toast.danger(message(error))\n } finally {\n working.value = false\n }\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('tag.new'), icon: 'plus', primary: true, run: () => void add() }]\n : [],\n)\n</script>\n\n<template>\n <div class=\"wx-tags\">\n <wx-list-screen v-model:view=\"view\" :title=\"title\" :views=\"views\" :actions=\"actions\">\n <wx-table\n v-model:selected=\"selected\"\n :data=\"page\"\n :columns=\"columns\"\n row-key=\"id\"\n searchable\n hover\n flush\n layout=\"fixed\"\n :loading=\"loading\"\n :selectable=\"canManage\"\n :select-row-label=\"panel('filters.select-row')\"\n :select-all-label=\"panel('filters.select-all')\"\n :cards-below=\"0\"\n :search-placeholder=\"t('tag.search')\"\n :empty-text=\"rows.length === 0 && !loading ? t('tag.empty') : undefined\"\n :aria-label=\"title\"\n @state-change=\"onState\"\n >\n <template #cell-title=\"{ row }\">\n <wx-text truncate>{{ row.title }}</wx-text>\n </template>\n\n <template #cell-path=\"{ row }\">\n <wx-text size=\"sm\" tone=\"muted\" mono truncate>\n {{ row.path ?? t('tag.no-address') }}\n </wx-text>\n </template>\n\n <template #cell-articles_count=\"{ row }\">\n <wx-text :tone=\"row.articles_count === 0 ? 'muted' : 'default'\">\n {{ row.articles_count }}\n </wx-text>\n </template>\n\n <!--\n Three states and not two. A rule in the SEO section opens the page whatever the flag\n says, and an editor who wrote that rule must not be left looking at a row that says\n `noindex` and disagreeing with it (§12).\n -->\n <template #cell-indexing=\"{ row }\">\n <wx-tooltip v-if=\"row.indexing === 'rule'\" :content=\"t('tag.indexing-rule-help')\">\n <wx-text size=\"sm\" :tone=\"tone(row.indexing)\">{{ t('tag.indexing-rule') }}</wx-text>\n </wx-tooltip>\n <wx-text v-else size=\"sm\" :tone=\"tone(row.indexing)\">\n {{ t(`tag.indexing-${row.indexing}`) }}\n </wx-text>\n </template>\n\n <template #cell-actions=\"{ row }\">\n <wx-row-menu :actions=\"actionsFor(row)\" :label=\"row.title\" />\n </template>\n </wx-table>\n </wx-list-screen>\n\n <!--\n The selection bar, at the bottom of the screen and only while something is selected.\n `WxActionBar` rather than a floating pill of its own: the panel already has one shape for\n \"what can be done from here\", and a second one would be a second one to keep in step.\n -->\n <wx-action-bar v-if=\"chosen.length > 0\" class=\"wx-tags__bar\" sticky>\n <template #state>\n <wx-text weight=\"medium\">{{ t('tag.selected', { count: chosen.length }) }}</wx-text>\n </template>\n\n <!--\n One button and a menu. Merging is what this bar exists for — it is the only thing here\n that cannot be done to a row on its own — and the other three are the row's own menu\n applied to several rows at once, which is where a reader already looks for them.\n\n The icon is a slot and not a prop: `WxButton` takes `#icon`, and `icon=\"…\"` lands on the\n element as an attribute and draws nothing at all (CLAUDE.md §4).\n -->\n <wx-button type=\"primary\" :disabled=\"chosen.length < 2 || working\" @click=\"mergeSelected\">\n <template #icon><wx-icon name=\"link\" /></template>\n {{ t('tag.merge') }}\n </wx-button>\n\n <!--\n No way out of the selection here. Untick the rows, or untick them all from the box in\n the heading — a × beside a red \"Delete\" is a button whose whole job is to undo something\n harmless, standing where the dangerous one is, and it read as a way to close the bar.\n\n `lg` because the menu stands beside a button rather than at the end of a row: a row menu\n is 30px, a button is 42, and the pair read as a control and a leftover. In a bar the two\n are the same two things and have to be the same size.\n -->\n <wx-row-menu\n size=\"lg\"\n :actions=\"massActions\"\n :label=\"t('tag.selected', { count: chosen.length })\"\n />\n </wx-action-bar>\n </div>\n</template>\n\n<style scoped>\n.wx-tags {\n display: flex;\n flex-direction: column;\n gap: var(--wx-space-12);\n min-width: 0;\n}\n\n.wx-tags__rename {\n display: block;\n min-width: 0;\n}\n\n.wx-tags__sort {\n display: flex;\n gap: var(--wx-space-4);\n min-width: 0;\n}\n\n/*\n * The name is the rename: a tag is one word, and a form to change one word is a form nobody\n * opens. It reads as text until it is hovered, because a table of sixty underlined words is a\n * table of sixty links to nowhere.\n */\n.wx-tags__name {\n max-width: 100%;\n overflow: hidden;\n padding: 0;\n border: 0;\n background: none;\n color: inherit;\n font: inherit;\n text-align: start;\n text-overflow: ellipsis;\n white-space: nowrap;\n cursor: text;\n}\n\n.wx-tags__name:hover {\n text-decoration: underline;\n text-decoration-style: dotted;\n}\n\n/*\n * One line on a phone, not two.\n *\n * The bar keeps its state box at 220px so that a sentence about a draft — \"Saved · goes out on\n * 25 September\" — has room to be read before the buttons take a line of their own. What stands\n * here is two words and a number, and asking for 220 of them pushed \"Merge\" and the ··· onto a\n * second line at every phone width. `:deep()` because the box is the bar's element and the rule\n * is ours. Measured at 375: the words 89px, the button and the ··· 203, and 41px of air left\n * between them.\n */\n.wx-tags__bar :deep(.wx-action-bar__state) {\n flex: 0 1 auto;\n}\n</style>\n"],"names":["props","__props","open","resolve","dismiss","useModal","useBlogMessages","t","useTranslate","panel","formId","useId","draft","ref","field","useTemplateRef","onMounted","nextTick","input","submit","next","_createBlock","_unref","WxDialog","$event","_createVNode","WxSpace","WxButton","_createElementVNode","WxFormItem","WxInput","context","useAdmin","api","createBlogApi","route","useRoute","router","useRouter","message","useErrorText","page","loading","selected","working","rename","createModal","TagRenameDialog","create","TagCreateDialog","merge","TagMergeDialog","canManage","computed","title","module","view","value","counts","views","rows","chosen","tag","columns","rowMenuWidth","query","state","sortOf","load","error","toast","from","sortFromRoute","key","sort","desc","onState","wanted","watch","add","startRename","patch","setIndexing","noindex","said","saved","row","remove","confirm","massActions","massSelected","actionsFor","actions","tone","indexing","mergeSelected","merged","action","ids","held","sum","affected","_openBlock","_createElementBlock","_hoisted_1","WxListScreen","WxTable","_withCtx","WxText","_createTextVNode","_toDisplayString","WxTooltip","WxRowMenu","WxActionBar","WxIcon"],"mappings":"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"}
|