@actuate-media/cms-admin 0.15.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/AdminRoot.d.ts.map +1 -1
- package/dist/AdminRoot.js +148 -2
- package/dist/AdminRoot.js.map +1 -1
- package/dist/__tests__/components/posts-badges.test.d.ts +2 -0
- package/dist/__tests__/components/posts-badges.test.d.ts.map +1 -0
- package/dist/__tests__/components/posts-badges.test.js +68 -0
- package/dist/__tests__/components/posts-badges.test.js.map +1 -0
- package/dist/__tests__/layout/sidebar-posts.test.d.ts +2 -0
- package/dist/__tests__/layout/sidebar-posts.test.d.ts.map +1 -0
- package/dist/__tests__/layout/sidebar-posts.test.js +162 -0
- package/dist/__tests__/layout/sidebar-posts.test.js.map +1 -0
- package/dist/__tests__/lib/page-editor-service.test.d.ts +2 -0
- package/dist/__tests__/lib/page-editor-service.test.d.ts.map +1 -0
- package/dist/__tests__/lib/page-editor-service.test.js +137 -0
- package/dist/__tests__/lib/page-editor-service.test.js.map +1 -0
- package/dist/__tests__/lib/pages-service.test.d.ts +2 -0
- package/dist/__tests__/lib/pages-service.test.d.ts.map +1 -0
- package/dist/__tests__/lib/pages-service.test.js +140 -0
- package/dist/__tests__/lib/pages-service.test.js.map +1 -0
- package/dist/__tests__/lib/post-editor-service.test.d.ts +2 -0
- package/dist/__tests__/lib/post-editor-service.test.d.ts.map +1 -0
- package/dist/__tests__/lib/post-editor-service.test.js +185 -0
- package/dist/__tests__/lib/post-editor-service.test.js.map +1 -0
- package/dist/__tests__/lib/posts-service.test.d.ts +2 -0
- package/dist/__tests__/lib/posts-service.test.d.ts.map +1 -0
- package/dist/__tests__/lib/posts-service.test.js +244 -0
- package/dist/__tests__/lib/posts-service.test.js.map +1 -0
- package/dist/__tests__/views/posts-list-view.test.d.ts +2 -0
- package/dist/__tests__/views/posts-list-view.test.d.ts.map +1 -0
- package/dist/__tests__/views/posts-list-view.test.js +98 -0
- package/dist/__tests__/views/posts-list-view.test.js.map +1 -0
- package/dist/actuate-admin.css +1 -1
- package/dist/components/pages/tag-badges.d.ts +21 -0
- package/dist/components/pages/tag-badges.d.ts.map +1 -0
- package/dist/components/pages/tag-badges.js +56 -0
- package/dist/components/pages/tag-badges.js.map +1 -0
- package/dist/components/posts/badges.d.ts +25 -0
- package/dist/components/posts/badges.d.ts.map +1 -0
- package/dist/components/posts/badges.js +123 -0
- package/dist/components/posts/badges.js.map +1 -0
- package/dist/components/posts/icons.d.ts +12 -0
- package/dist/components/posts/icons.d.ts.map +1 -0
- package/dist/components/posts/icons.js +26 -0
- package/dist/components/posts/icons.js.map +1 -0
- package/dist/layout/Layout.js +36 -8
- package/dist/layout/Layout.js.map +1 -1
- package/dist/layout/Sidebar.d.ts +9 -0
- package/dist/layout/Sidebar.d.ts.map +1 -1
- package/dist/layout/Sidebar.js +100 -48
- package/dist/layout/Sidebar.js.map +1 -1
- package/dist/lib/page-editor-service.d.ts +45 -0
- package/dist/lib/page-editor-service.d.ts.map +1 -0
- package/dist/lib/page-editor-service.js +153 -0
- package/dist/lib/page-editor-service.js.map +1 -0
- package/dist/lib/pages-service.d.ts +143 -0
- package/dist/lib/pages-service.d.ts.map +1 -0
- package/dist/lib/pages-service.js +444 -0
- package/dist/lib/pages-service.js.map +1 -0
- package/dist/lib/post-editor-service.d.ts +57 -0
- package/dist/lib/post-editor-service.d.ts.map +1 -0
- package/dist/lib/post-editor-service.js +234 -0
- package/dist/lib/post-editor-service.js.map +1 -0
- package/dist/lib/posts-service.d.ts +73 -0
- package/dist/lib/posts-service.d.ts.map +1 -0
- package/dist/lib/posts-service.js +245 -0
- package/dist/lib/posts-service.js.map +1 -0
- package/dist/lib/preview-link.d.ts +32 -0
- package/dist/lib/preview-link.d.ts.map +1 -0
- package/dist/lib/preview-link.js +40 -0
- package/dist/lib/preview-link.js.map +1 -0
- package/dist/router/index.d.ts +1 -0
- package/dist/router/index.d.ts.map +1 -1
- package/dist/router/index.js +9 -1
- package/dist/router/index.js.map +1 -1
- package/dist/views/Dashboard.d.ts.map +1 -1
- package/dist/views/Dashboard.js +14 -3
- package/dist/views/Dashboard.js.map +1 -1
- package/dist/views/Pages/PageTagManager.d.ts +14 -0
- package/dist/views/Pages/PageTagManager.d.ts.map +1 -0
- package/dist/views/Pages/PageTagManager.js +145 -0
- package/dist/views/Pages/PageTagManager.js.map +1 -0
- package/dist/views/Pages/PagesListView.d.ts +18 -0
- package/dist/views/Pages/PagesListView.d.ts.map +1 -0
- package/dist/views/Pages/PagesListView.js +287 -0
- package/dist/views/Pages/PagesListView.js.map +1 -0
- package/dist/views/Posts/NewPostPicker.d.ts +16 -0
- package/dist/views/Posts/NewPostPicker.d.ts.map +1 -0
- package/dist/views/Posts/NewPostPicker.js +59 -0
- package/dist/views/Posts/NewPostPicker.js.map +1 -0
- package/dist/views/Posts/NewPostTypeInstructions.d.ts +18 -0
- package/dist/views/Posts/NewPostTypeInstructions.d.ts.map +1 -0
- package/dist/views/Posts/NewPostTypeInstructions.js +147 -0
- package/dist/views/Posts/NewPostTypeInstructions.js.map +1 -0
- package/dist/views/Posts/PostTypesView.d.ts +26 -0
- package/dist/views/Posts/PostTypesView.d.ts.map +1 -0
- package/dist/views/Posts/PostTypesView.js +52 -0
- package/dist/views/Posts/PostTypesView.js.map +1 -0
- package/dist/views/Posts/PostsListView.d.ts +25 -0
- package/dist/views/Posts/PostsListView.d.ts.map +1 -0
- package/dist/views/Posts/PostsListView.js +239 -0
- package/dist/views/Posts/PostsListView.js.map +1 -0
- package/dist/views/Posts/post-type-presets.d.ts +32 -0
- package/dist/views/Posts/post-type-presets.d.ts.map +1 -0
- package/dist/views/Posts/post-type-presets.js +72 -0
- package/dist/views/Posts/post-type-presets.js.map +1 -0
- package/dist/views/page-builder/Widgets.d.ts +13 -0
- package/dist/views/page-builder/Widgets.d.ts.map +1 -0
- package/dist/views/page-builder/Widgets.js +15 -0
- package/dist/views/page-builder/Widgets.js.map +1 -0
- package/dist/views/page-editor/AddSectionModal.d.ts +20 -0
- package/dist/views/page-editor/AddSectionModal.d.ts.map +1 -0
- package/dist/views/page-editor/AddSectionModal.js +22 -0
- package/dist/views/page-editor/AddSectionModal.js.map +1 -0
- package/dist/views/page-editor/EditorCanvas.d.ts +18 -0
- package/dist/views/page-editor/EditorCanvas.d.ts.map +1 -0
- package/dist/views/page-editor/EditorCanvas.js +64 -0
- package/dist/views/page-editor/EditorCanvas.js.map +1 -0
- package/dist/views/page-editor/EditorTopBar.d.ts +30 -0
- package/dist/views/page-editor/EditorTopBar.d.ts.map +1 -0
- package/dist/views/page-editor/EditorTopBar.js +42 -0
- package/dist/views/page-editor/EditorTopBar.js.map +1 -0
- package/dist/views/page-editor/PagePreview.d.ts +13 -0
- package/dist/views/page-editor/PagePreview.d.ts.map +1 -0
- package/dist/views/page-editor/PagePreview.js +37 -0
- package/dist/views/page-editor/PagePreview.js.map +1 -0
- package/dist/views/page-editor/PageSectionEditor.d.ts +16 -0
- package/dist/views/page-editor/PageSectionEditor.d.ts.map +1 -0
- package/dist/views/page-editor/PageSectionEditor.js +272 -0
- package/dist/views/page-editor/PageSectionEditor.js.map +1 -0
- package/dist/views/page-editor/PageSettingsModal.d.ts +12 -0
- package/dist/views/page-editor/PageSettingsModal.d.ts.map +1 -0
- package/dist/views/page-editor/PageSettingsModal.js +52 -0
- package/dist/views/page-editor/PageSettingsModal.js.map +1 -0
- package/dist/views/page-editor/SectionInspector.d.ts +17 -0
- package/dist/views/page-editor/SectionInspector.d.ts.map +1 -0
- package/dist/views/page-editor/SectionInspector.js +98 -0
- package/dist/views/page-editor/SectionInspector.js.map +1 -0
- package/dist/views/page-editor/SectionsPanel.d.ts +15 -0
- package/dist/views/page-editor/SectionsPanel.d.ts.map +1 -0
- package/dist/views/page-editor/SectionsPanel.js +43 -0
- package/dist/views/page-editor/SectionsPanel.js.map +1 -0
- package/dist/views/page-editor/ValidationSummary.d.ts +9 -0
- package/dist/views/page-editor/ValidationSummary.d.ts.map +1 -0
- package/dist/views/page-editor/ValidationSummary.js +10 -0
- package/dist/views/page-editor/ValidationSummary.js.map +1 -0
- package/dist/views/page-editor/section-icons.d.ts +4 -0
- package/dist/views/page-editor/section-icons.d.ts.map +1 -0
- package/dist/views/page-editor/section-icons.js +20 -0
- package/dist/views/page-editor/section-icons.js.map +1 -0
- package/dist/views/page-editor/section-types.d.ts +11 -0
- package/dist/views/page-editor/section-types.d.ts.map +1 -0
- package/dist/views/page-editor/section-types.js +10 -0
- package/dist/views/page-editor/section-types.js.map +1 -0
- package/dist/views/page-editor/sections/ArticleBodySection.d.ts +13 -0
- package/dist/views/page-editor/sections/ArticleBodySection.d.ts.map +1 -0
- package/dist/views/page-editor/sections/ArticleBodySection.js +23 -0
- package/dist/views/page-editor/sections/ArticleBodySection.js.map +1 -0
- package/dist/views/page-editor/sections/AuthorBioSection.d.ts +8 -0
- package/dist/views/page-editor/sections/AuthorBioSection.d.ts.map +1 -0
- package/dist/views/page-editor/sections/AuthorBioSection.js +21 -0
- package/dist/views/page-editor/sections/AuthorBioSection.js.map +1 -0
- package/dist/views/page-editor/sections/ByTheNumbersSection.d.ts +5 -0
- package/dist/views/page-editor/sections/ByTheNumbersSection.d.ts.map +1 -0
- package/dist/views/page-editor/sections/ByTheNumbersSection.js +10 -0
- package/dist/views/page-editor/sections/ByTheNumbersSection.js.map +1 -0
- package/dist/views/page-editor/sections/CtaBannerSection.d.ts +5 -0
- package/dist/views/page-editor/sections/CtaBannerSection.d.ts.map +1 -0
- package/dist/views/page-editor/sections/CtaBannerSection.js +10 -0
- package/dist/views/page-editor/sections/CtaBannerSection.js.map +1 -0
- package/dist/views/page-editor/sections/FeatureHighlightSection.d.ts +5 -0
- package/dist/views/page-editor/sections/FeatureHighlightSection.d.ts.map +1 -0
- package/dist/views/page-editor/sections/FeatureHighlightSection.js +18 -0
- package/dist/views/page-editor/sections/FeatureHighlightSection.js.map +1 -0
- package/dist/views/page-editor/sections/GallerySection.d.ts +6 -0
- package/dist/views/page-editor/sections/GallerySection.d.ts.map +1 -0
- package/dist/views/page-editor/sections/GallerySection.js +17 -0
- package/dist/views/page-editor/sections/GallerySection.js.map +1 -0
- package/dist/views/page-editor/sections/HeroBannerSection.d.ts +5 -0
- package/dist/views/page-editor/sections/HeroBannerSection.d.ts.map +1 -0
- package/dist/views/page-editor/sections/HeroBannerSection.js +11 -0
- package/dist/views/page-editor/sections/HeroBannerSection.js.map +1 -0
- package/dist/views/page-editor/sections/MissionSection.d.ts +6 -0
- package/dist/views/page-editor/sections/MissionSection.d.ts.map +1 -0
- package/dist/views/page-editor/sections/MissionSection.js +12 -0
- package/dist/views/page-editor/sections/MissionSection.js.map +1 -0
- package/dist/views/page-editor/sections/PullQuoteSection.d.ts +6 -0
- package/dist/views/page-editor/sections/PullQuoteSection.d.ts.map +1 -0
- package/dist/views/page-editor/sections/PullQuoteSection.js +13 -0
- package/dist/views/page-editor/sections/PullQuoteSection.js.map +1 -0
- package/dist/views/page-editor/sections/RelatedPostsSection.d.ts +12 -0
- package/dist/views/page-editor/sections/RelatedPostsSection.d.ts.map +1 -0
- package/dist/views/page-editor/sections/RelatedPostsSection.js +23 -0
- package/dist/views/page-editor/sections/RelatedPostsSection.js.map +1 -0
- package/dist/views/page-editor/sections/SectionRenderer.d.ts +22 -0
- package/dist/views/page-editor/sections/SectionRenderer.d.ts.map +1 -0
- package/dist/views/page-editor/sections/SectionRenderer.js +52 -0
- package/dist/views/page-editor/sections/SectionRenderer.js.map +1 -0
- package/dist/views/page-editor/sections/index.d.ts +14 -0
- package/dist/views/page-editor/sections/index.d.ts.map +1 -0
- package/dist/views/page-editor/sections/index.js +12 -0
- package/dist/views/page-editor/sections/index.js.map +1 -0
- package/dist/views/page-editor/sections/parts.d.ts +55 -0
- package/dist/views/page-editor/sections/parts.d.ts.map +1 -0
- package/dist/views/page-editor/sections/parts.js +60 -0
- package/dist/views/page-editor/sections/parts.js.map +1 -0
- package/dist/views/page-editor/viewports.d.ts +14 -0
- package/dist/views/page-editor/viewports.d.ts.map +1 -0
- package/dist/views/page-editor/viewports.js +12 -0
- package/dist/views/page-editor/viewports.js.map +1 -0
- package/dist/views/post-editor/PostEditorCanvas.d.ts +21 -0
- package/dist/views/post-editor/PostEditorCanvas.d.ts.map +1 -0
- package/dist/views/post-editor/PostEditorCanvas.js +58 -0
- package/dist/views/post-editor/PostEditorCanvas.js.map +1 -0
- package/dist/views/post-editor/PostFieldsModal.d.ts +12 -0
- package/dist/views/post-editor/PostFieldsModal.d.ts.map +1 -0
- package/dist/views/post-editor/PostFieldsModal.js +53 -0
- package/dist/views/post-editor/PostFieldsModal.js.map +1 -0
- package/dist/views/post-editor/PostHeader.d.ts +15 -0
- package/dist/views/post-editor/PostHeader.d.ts.map +1 -0
- package/dist/views/post-editor/PostHeader.js +27 -0
- package/dist/views/post-editor/PostHeader.js.map +1 -0
- package/dist/views/post-editor/PostHeaderPanel.d.ts +11 -0
- package/dist/views/post-editor/PostHeaderPanel.d.ts.map +1 -0
- package/dist/views/post-editor/PostHeaderPanel.js +21 -0
- package/dist/views/post-editor/PostHeaderPanel.js.map +1 -0
- package/dist/views/post-editor/PostPreview.d.ts +14 -0
- package/dist/views/post-editor/PostPreview.d.ts.map +1 -0
- package/dist/views/post-editor/PostPreview.js +53 -0
- package/dist/views/post-editor/PostPreview.js.map +1 -0
- package/dist/views/post-editor/PostSectionEditor.d.ts +17 -0
- package/dist/views/post-editor/PostSectionEditor.d.ts.map +1 -0
- package/dist/views/post-editor/PostSectionEditor.js +290 -0
- package/dist/views/post-editor/PostSectionEditor.js.map +1 -0
- package/dist/views/post-editor/PostTemplateEditor.d.ts +9 -0
- package/dist/views/post-editor/PostTemplateEditor.d.ts.map +1 -0
- package/dist/views/post-editor/PostTemplateEditor.js +144 -0
- package/dist/views/post-editor/PostTemplateEditor.js.map +1 -0
- package/package.json +2 -2
- package/src/AdminRoot.tsx +213 -2
- package/src/__tests__/components/posts-badges.test.tsx +77 -0
- package/src/__tests__/layout/sidebar-posts.test.ts +179 -0
- package/src/__tests__/lib/page-editor-service.test.ts +173 -0
- package/src/__tests__/lib/pages-service.test.ts +157 -0
- package/src/__tests__/lib/post-editor-service.test.ts +215 -0
- package/src/__tests__/lib/posts-service.test.ts +279 -0
- package/src/__tests__/views/posts-list-view.test.ts +110 -0
- package/src/components/pages/tag-badges.tsx +104 -0
- package/src/components/posts/badges.tsx +170 -0
- package/src/components/posts/icons.ts +40 -0
- package/src/layout/Layout.tsx +66 -10
- package/src/layout/Sidebar.tsx +116 -50
- package/src/lib/page-editor-service.ts +233 -0
- package/src/lib/pages-service.ts +599 -0
- package/src/lib/post-editor-service.ts +342 -0
- package/src/lib/posts-service.ts +366 -0
- package/src/lib/preview-link.ts +63 -0
- package/src/router/index.ts +10 -1
- package/src/views/Dashboard.tsx +15 -3
- package/src/views/Pages/PageTagManager.tsx +436 -0
- package/src/views/Pages/PagesListView.tsx +944 -0
- package/src/views/Posts/NewPostPicker.tsx +147 -0
- package/src/views/Posts/NewPostTypeInstructions.tsx +344 -0
- package/src/views/Posts/PostTypesView.tsx +194 -0
- package/src/views/Posts/PostsListView.tsx +869 -0
- package/src/views/Posts/post-type-presets.ts +105 -0
- package/src/views/page-builder/Widgets.tsx +44 -0
- package/src/views/page-editor/AddSectionModal.tsx +56 -0
- package/src/views/page-editor/EditorCanvas.tsx +127 -0
- package/src/views/page-editor/EditorTopBar.tsx +233 -0
- package/src/views/page-editor/PagePreview.tsx +96 -0
- package/src/views/page-editor/PageSectionEditor.tsx +476 -0
- package/src/views/page-editor/PageSettingsModal.tsx +154 -0
- package/src/views/page-editor/SectionInspector.tsx +599 -0
- package/src/views/page-editor/SectionsPanel.tsx +270 -0
- package/src/views/page-editor/ValidationSummary.tsx +53 -0
- package/src/views/page-editor/section-icons.ts +33 -0
- package/src/views/page-editor/section-types.ts +27 -0
- package/src/views/page-editor/sections/ArticleBodySection.tsx +57 -0
- package/src/views/page-editor/sections/AuthorBioSection.tsx +61 -0
- package/src/views/page-editor/sections/ByTheNumbersSection.tsx +41 -0
- package/src/views/page-editor/sections/CtaBannerSection.tsx +29 -0
- package/src/views/page-editor/sections/FeatureHighlightSection.tsx +63 -0
- package/src/views/page-editor/sections/GallerySection.tsx +54 -0
- package/src/views/page-editor/sections/HeroBannerSection.tsx +44 -0
- package/src/views/page-editor/sections/MissionSection.tsx +22 -0
- package/src/views/page-editor/sections/PullQuoteSection.tsx +27 -0
- package/src/views/page-editor/sections/RelatedPostsSection.tsx +55 -0
- package/src/views/page-editor/sections/SectionRenderer.tsx +102 -0
- package/src/views/page-editor/sections/index.ts +13 -0
- package/src/views/page-editor/sections/parts.tsx +113 -0
- package/src/views/page-editor/viewports.ts +25 -0
- package/src/views/post-editor/PostEditorCanvas.tsx +123 -0
- package/src/views/post-editor/PostFieldsModal.tsx +196 -0
- package/src/views/post-editor/PostHeader.tsx +87 -0
- package/src/views/post-editor/PostHeaderPanel.tsx +107 -0
- package/src/views/post-editor/PostPreview.tsx +114 -0
- package/src/views/post-editor/PostSectionEditor.tsx +506 -0
- package/src/views/post-editor/PostTemplateEditor.tsx +347 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { cmsApi } from './api.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Mint a signed, time-boxed preview token for a single document. The token is
|
|
5
|
+
* bound to the (collection, documentId) pair server-side, so it can only
|
|
6
|
+
* unlock the exact draft it was issued for. Used to build public preview URLs
|
|
7
|
+
* (`/<prefix>/<slug>?preview=<token>`) that render a draft through the
|
|
8
|
+
* consumer's real front-end — see {@link buildPublicPreviewUrl}.
|
|
9
|
+
*/
|
|
10
|
+
export async function createPreviewToken(
|
|
11
|
+
collection: string,
|
|
12
|
+
documentId: string,
|
|
13
|
+
ttlSeconds?: number,
|
|
14
|
+
): Promise<{ token: string; expiresAt: string; slug?: string | null }> {
|
|
15
|
+
const res = await cmsApi<{ token: string; expiresAt: string; slug?: string | null }>(
|
|
16
|
+
'/preview/token',
|
|
17
|
+
{
|
|
18
|
+
method: 'POST',
|
|
19
|
+
body: JSON.stringify({ collection, documentId, ttlSeconds }),
|
|
20
|
+
},
|
|
21
|
+
)
|
|
22
|
+
if (res.error || !res.data) {
|
|
23
|
+
throw new Error(res.error ?? 'Failed to create preview token')
|
|
24
|
+
}
|
|
25
|
+
return res.data
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface PublicPreviewUrlParts {
|
|
29
|
+
/** Absolute site origin, e.g. `https://example.com`. Trailing slash optional. */
|
|
30
|
+
siteUrl: string
|
|
31
|
+
/** Collection URL prefix (e.g. `blog`). Empty for top-level page collections. */
|
|
32
|
+
urlPrefix?: string | null
|
|
33
|
+
/** Document slug used in the public path. */
|
|
34
|
+
slug: string
|
|
35
|
+
/** Signed preview token from {@link createPreviewToken}. */
|
|
36
|
+
token: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Build the public URL a draft preview opens at — the SAME path the
|
|
41
|
+
* `/resolve` endpoint matches: `<urlPrefix>/<slug>` (or `/<slug>` for
|
|
42
|
+
* top-level pages), with the preview token on the query string so the
|
|
43
|
+
* public route can drop the `PUBLISHED` filter for this one document.
|
|
44
|
+
*/
|
|
45
|
+
export function buildPublicPreviewUrl({
|
|
46
|
+
siteUrl,
|
|
47
|
+
urlPrefix,
|
|
48
|
+
slug,
|
|
49
|
+
token,
|
|
50
|
+
}: PublicPreviewUrlParts): string {
|
|
51
|
+
const base = (siteUrl ?? '').replace(/\/+$/, '')
|
|
52
|
+
const prefix = (urlPrefix ?? '').replace(/^\/+|\/+$/g, '')
|
|
53
|
+
const cleanSlug = slug.replace(/^\/+|\/+$/g, '')
|
|
54
|
+
const path = prefix ? `/${prefix}/${cleanSlug}` : `/${cleanSlug}`
|
|
55
|
+
return `${base}${path}?preview=${encodeURIComponent(token)}`
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Resolve the public site origin: explicit config first, else the current tab's origin. */
|
|
59
|
+
export function resolveSiteUrl(configSiteUrl?: string | null): string {
|
|
60
|
+
if (configSiteUrl && configSiteUrl.trim()) return configSiteUrl.trim()
|
|
61
|
+
if (typeof window !== 'undefined') return window.location.origin
|
|
62
|
+
return ''
|
|
63
|
+
}
|
package/src/router/index.ts
CHANGED
|
@@ -44,6 +44,15 @@ export function useAdminRouter(basePath = '/admin', serverPath = '/') {
|
|
|
44
44
|
window.history.pushState({ adminPath: normalizedPath }, '', fullUrl)
|
|
45
45
|
}, [])
|
|
46
46
|
|
|
47
|
+
// Build an absolute (basePath-prefixed) admin URL without navigating. Used
|
|
48
|
+
// for links that must load a real document — e.g. opening a preview in a
|
|
49
|
+
// new browser tab, where `pushState` (which only mutates the current tab)
|
|
50
|
+
// is not an option.
|
|
51
|
+
const buildHref = useCallback((path: string) => {
|
|
52
|
+
const normalizedPath = path.startsWith('/') ? path : `/${path}`
|
|
53
|
+
return normalizedPath === '/' ? baseRef.current : `${baseRef.current}${normalizedPath}`
|
|
54
|
+
}, [])
|
|
55
|
+
|
|
47
56
|
const matchRoute = useCallback(
|
|
48
57
|
(pattern: string): RouteParams | null => {
|
|
49
58
|
const patternParts = pattern.split('/').filter(Boolean)
|
|
@@ -75,5 +84,5 @@ export function useAdminRouter(basePath = '/admin', serverPath = '/') {
|
|
|
75
84
|
[currentPath],
|
|
76
85
|
)
|
|
77
86
|
|
|
78
|
-
return { currentPath, navigate, matchRoute }
|
|
87
|
+
return { currentPath, navigate, matchRoute, buildHref }
|
|
79
88
|
}
|
package/src/views/Dashboard.tsx
CHANGED
|
@@ -358,12 +358,24 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
358
358
|
// dashboard's content-creation toolbar. Order mirrors the natural
|
|
359
359
|
// authoring flow (post → page → media → form → integration).
|
|
360
360
|
const quickActions = useMemo(() => {
|
|
361
|
-
const
|
|
362
|
-
const postsSlug = posts?.slug ?? 'posts'
|
|
361
|
+
const postTypes = collections.filter((c) => c.type === 'post')
|
|
363
362
|
const pages = collections.find((c) => c.slug === 'pages' || c.type === 'page')
|
|
364
363
|
const pagesSlug = pages?.slug ?? 'pages'
|
|
364
|
+
// "New Post": one Post Type → straight to its editor; several →
|
|
365
|
+
// the picker; none → the Post Type setup instructions (avoids a
|
|
366
|
+
// broken slug-based new URL).
|
|
367
|
+
const newPostPath =
|
|
368
|
+
postTypes.length === 1
|
|
369
|
+
? `/${postTypes[0]!.slug}/new`
|
|
370
|
+
: postTypes.length > 1
|
|
371
|
+
? '/posts/new'
|
|
372
|
+
: '/posts/types/new'
|
|
365
373
|
const items: { label: string; icon: LucideIcon; onClick: () => void }[] = [
|
|
366
|
-
{
|
|
374
|
+
{
|
|
375
|
+
label: 'New Post',
|
|
376
|
+
icon: Plus,
|
|
377
|
+
onClick: () => nav(newPostPath),
|
|
378
|
+
},
|
|
367
379
|
{ label: 'New Page', icon: Plus, onClick: () => nav(`/${pagesSlug}/new`) },
|
|
368
380
|
{ label: 'Upload Media', icon: Upload, onClick: () => nav('/media') },
|
|
369
381
|
{ label: 'New Form', icon: Plus, onClick: () => nav('/forms') },
|
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useMemo, useState } from 'react'
|
|
4
|
+
import { ArrowLeft, GitMerge, Loader2, Pencil, Plus, Trash2, X } from 'lucide-react'
|
|
5
|
+
import { toast } from 'sonner'
|
|
6
|
+
import {
|
|
7
|
+
createPageTag,
|
|
8
|
+
deletePageTagWithDetach,
|
|
9
|
+
fetchPages,
|
|
10
|
+
mergePageTags,
|
|
11
|
+
updatePageTag,
|
|
12
|
+
type PageTag,
|
|
13
|
+
} from '../../lib/pages-service.js'
|
|
14
|
+
import { PageTagPill, tagDotColor } from '../../components/pages/tag-badges.js'
|
|
15
|
+
import { ConfirmDialog } from '../../components/ui/ConfirmDialog.js'
|
|
16
|
+
|
|
17
|
+
const TAG_COLORS = ['purple', 'green', 'orange', 'red', 'blue', 'pink', 'gray'] as const
|
|
18
|
+
|
|
19
|
+
export interface PageTagManagerProps {
|
|
20
|
+
onNavigate: (path: string) => void
|
|
21
|
+
/** When set, the create form opens pre-expanded (route /pages/tags/new). */
|
|
22
|
+
startCreating?: boolean
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* /pages/tags — create, rename, recolor, merge, and delete page tags.
|
|
27
|
+
*
|
|
28
|
+
* Tags are documents in the hidden `page-tags` collection; pages
|
|
29
|
+
* reference them by slug in `data.tags`. Deleting a tag detaches it
|
|
30
|
+
* from every page first so no dangling slugs remain.
|
|
31
|
+
*/
|
|
32
|
+
export function PageTagManager({ onNavigate, startCreating = false }: PageTagManagerProps) {
|
|
33
|
+
const [tags, setTags] = useState<PageTag[]>([])
|
|
34
|
+
const [loading, setLoading] = useState(true)
|
|
35
|
+
const [error, setError] = useState<string | null>(null)
|
|
36
|
+
const [refreshToken, setRefreshToken] = useState(0)
|
|
37
|
+
|
|
38
|
+
const [creating, setCreating] = useState(startCreating)
|
|
39
|
+
const [pendingDelete, setPendingDelete] = useState<PageTag | null>(null)
|
|
40
|
+
|
|
41
|
+
const existingSlugs = useMemo(() => new Set(tags.map((t) => t.slug)), [tags])
|
|
42
|
+
|
|
43
|
+
const load = useCallback(async () => {
|
|
44
|
+
setLoading(true)
|
|
45
|
+
setError(null)
|
|
46
|
+
try {
|
|
47
|
+
// fetchPages returns tags with accurate page counts.
|
|
48
|
+
const res = await fetchPages({ pageSize: 1 })
|
|
49
|
+
setTags(res.tags)
|
|
50
|
+
} catch (err) {
|
|
51
|
+
setError(err instanceof Error ? err.message : 'Failed to load tags')
|
|
52
|
+
} finally {
|
|
53
|
+
setLoading(false)
|
|
54
|
+
}
|
|
55
|
+
}, [])
|
|
56
|
+
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
void load()
|
|
59
|
+
}, [load, refreshToken])
|
|
60
|
+
|
|
61
|
+
const refresh = () => setRefreshToken((t) => t + 1)
|
|
62
|
+
|
|
63
|
+
const handleDelete = async (tag: PageTag) => {
|
|
64
|
+
const promise = deletePageTagWithDetach(tag)
|
|
65
|
+
toast.promise(promise, {
|
|
66
|
+
loading: `Deleting "${tag.name}"…`,
|
|
67
|
+
success: () => `Deleted "${tag.name}"`,
|
|
68
|
+
error: 'Failed to delete tag',
|
|
69
|
+
})
|
|
70
|
+
await promise
|
|
71
|
+
setPendingDelete(null)
|
|
72
|
+
refresh()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<div className="p-4 pr-8 sm:p-6">
|
|
77
|
+
<button
|
|
78
|
+
type="button"
|
|
79
|
+
onClick={() => onNavigate('/pages')}
|
|
80
|
+
className="text-muted-foreground hover:text-foreground mb-4 inline-flex items-center gap-1.5 text-sm"
|
|
81
|
+
>
|
|
82
|
+
<ArrowLeft className="h-4 w-4" aria-hidden />
|
|
83
|
+
Back to Pages
|
|
84
|
+
</button>
|
|
85
|
+
|
|
86
|
+
<div className="mb-5 flex items-center justify-between">
|
|
87
|
+
<div>
|
|
88
|
+
<h1 className="text-foreground mb-1 text-xl font-medium tracking-tight sm:text-2xl">
|
|
89
|
+
Page Tags
|
|
90
|
+
</h1>
|
|
91
|
+
<p className="text-muted-foreground text-sm">
|
|
92
|
+
Organize pages with reusable, color-coded labels.
|
|
93
|
+
</p>
|
|
94
|
+
</div>
|
|
95
|
+
{!creating && (
|
|
96
|
+
<button
|
|
97
|
+
type="button"
|
|
98
|
+
onClick={() => setCreating(true)}
|
|
99
|
+
className="bg-primary text-primary-foreground hover:bg-primary/90 inline-flex items-center gap-1.5 rounded-lg px-3.5 py-1.5 text-sm font-medium shadow-sm transition-colors"
|
|
100
|
+
>
|
|
101
|
+
<Plus className="h-3.5 w-3.5" aria-hidden />
|
|
102
|
+
New tag
|
|
103
|
+
</button>
|
|
104
|
+
)}
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
{creating && (
|
|
108
|
+
<CreateTagForm
|
|
109
|
+
existingSlugs={existingSlugs}
|
|
110
|
+
onCancel={() => setCreating(false)}
|
|
111
|
+
onCreated={() => {
|
|
112
|
+
setCreating(false)
|
|
113
|
+
refresh()
|
|
114
|
+
}}
|
|
115
|
+
/>
|
|
116
|
+
)}
|
|
117
|
+
|
|
118
|
+
{loading ? (
|
|
119
|
+
<div className="flex h-48 items-center justify-center" role="status" aria-live="polite">
|
|
120
|
+
<Loader2 className="text-primary h-6 w-6 animate-spin" aria-hidden />
|
|
121
|
+
<span className="sr-only">Loading tags</span>
|
|
122
|
+
</div>
|
|
123
|
+
) : error ? (
|
|
124
|
+
<div className="border-border bg-card flex items-center gap-3 rounded-lg border p-3">
|
|
125
|
+
<span className="text-foreground flex-1 text-sm">{error}</span>
|
|
126
|
+
<button
|
|
127
|
+
type="button"
|
|
128
|
+
onClick={refresh}
|
|
129
|
+
className="border-border text-foreground hover:bg-accent rounded-md border px-3 py-1 text-sm"
|
|
130
|
+
>
|
|
131
|
+
Retry
|
|
132
|
+
</button>
|
|
133
|
+
</div>
|
|
134
|
+
) : tags.length === 0 ? (
|
|
135
|
+
<div className="border-border bg-card rounded-lg border p-8 text-center">
|
|
136
|
+
<h2 className="text-foreground mb-1 text-lg font-medium">No tags yet</h2>
|
|
137
|
+
<p className="text-muted-foreground text-sm">Create a tag to start organizing pages.</p>
|
|
138
|
+
</div>
|
|
139
|
+
) : (
|
|
140
|
+
<div className="bg-card border-border divide-border divide-y overflow-hidden rounded-xl border shadow-sm">
|
|
141
|
+
{tags.map((tag) => (
|
|
142
|
+
<TagRow
|
|
143
|
+
key={tag.docId}
|
|
144
|
+
tag={tag}
|
|
145
|
+
allTags={tags}
|
|
146
|
+
onChanged={refresh}
|
|
147
|
+
onRequestDelete={() => setPendingDelete(tag)}
|
|
148
|
+
/>
|
|
149
|
+
))}
|
|
150
|
+
</div>
|
|
151
|
+
)}
|
|
152
|
+
|
|
153
|
+
<ConfirmDialog
|
|
154
|
+
open={pendingDelete !== null}
|
|
155
|
+
title={`Delete "${pendingDelete?.name ?? ''}"?`}
|
|
156
|
+
description={
|
|
157
|
+
pendingDelete && pendingDelete.pageCount > 0
|
|
158
|
+
? `This tag is used on ${pendingDelete.pageCount} page${
|
|
159
|
+
pendingDelete.pageCount === 1 ? '' : 's'
|
|
160
|
+
}. It will be removed from those pages. This cannot be undone.`
|
|
161
|
+
: 'This tag has no pages. Deleting it cannot be undone.'
|
|
162
|
+
}
|
|
163
|
+
confirmLabel="Delete tag"
|
|
164
|
+
destructive
|
|
165
|
+
onClose={() => setPendingDelete(null)}
|
|
166
|
+
onConfirm={() => {
|
|
167
|
+
if (pendingDelete) void handleDelete(pendingDelete)
|
|
168
|
+
}}
|
|
169
|
+
/>
|
|
170
|
+
</div>
|
|
171
|
+
)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ─── Create form ─────────────────────────────────────────────────────
|
|
175
|
+
|
|
176
|
+
function CreateTagForm({
|
|
177
|
+
existingSlugs,
|
|
178
|
+
onCancel,
|
|
179
|
+
onCreated,
|
|
180
|
+
}: {
|
|
181
|
+
existingSlugs: Set<string>
|
|
182
|
+
onCancel: () => void
|
|
183
|
+
onCreated: () => void
|
|
184
|
+
}) {
|
|
185
|
+
const [name, setName] = useState('')
|
|
186
|
+
const [color, setColor] = useState<string>('purple')
|
|
187
|
+
const [submitting, setSubmitting] = useState(false)
|
|
188
|
+
|
|
189
|
+
const slug = name
|
|
190
|
+
.toLowerCase()
|
|
191
|
+
.trim()
|
|
192
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
193
|
+
.replace(/^-+|-+$/g, '')
|
|
194
|
+
const duplicate = slug.length > 0 && existingSlugs.has(slug)
|
|
195
|
+
const canSubmit = slug.length > 0 && !duplicate && !submitting
|
|
196
|
+
|
|
197
|
+
const submit = async () => {
|
|
198
|
+
if (!canSubmit) return
|
|
199
|
+
setSubmitting(true)
|
|
200
|
+
const res = await createPageTag({ name: name.trim(), color })
|
|
201
|
+
setSubmitting(false)
|
|
202
|
+
if (res.error) {
|
|
203
|
+
toast.error(res.error)
|
|
204
|
+
return
|
|
205
|
+
}
|
|
206
|
+
toast.success(`Created "${name.trim()}"`)
|
|
207
|
+
onCreated()
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return (
|
|
211
|
+
<div className="border-border bg-card mb-5 rounded-xl border p-4 shadow-sm">
|
|
212
|
+
<div className="flex flex-wrap items-end gap-3">
|
|
213
|
+
<label className="flex-1">
|
|
214
|
+
<span className="text-foreground mb-1 block text-sm font-medium">Tag name</span>
|
|
215
|
+
<input
|
|
216
|
+
type="text"
|
|
217
|
+
value={name}
|
|
218
|
+
autoFocus
|
|
219
|
+
onChange={(e) => setName(e.target.value)}
|
|
220
|
+
onKeyDown={(e) => {
|
|
221
|
+
if (e.key === 'Enter') void submit()
|
|
222
|
+
}}
|
|
223
|
+
placeholder="e.g. Marketing"
|
|
224
|
+
className="border-border bg-input-background text-foreground focus:border-primary focus:ring-primary/30 w-full rounded-lg border px-3 py-2 text-sm focus:ring-2 focus:outline-none"
|
|
225
|
+
/>
|
|
226
|
+
</label>
|
|
227
|
+
<ColorPicker value={color} onChange={setColor} />
|
|
228
|
+
<button
|
|
229
|
+
type="button"
|
|
230
|
+
onClick={() => void submit()}
|
|
231
|
+
disabled={!canSubmit}
|
|
232
|
+
className="bg-primary text-primary-foreground hover:bg-primary/90 inline-flex items-center gap-1.5 rounded-lg px-3.5 py-2 text-sm font-medium shadow-sm transition-colors disabled:cursor-not-allowed disabled:opacity-50"
|
|
233
|
+
>
|
|
234
|
+
{submitting ? <Loader2 className="h-4 w-4 animate-spin" aria-hidden /> : null}
|
|
235
|
+
Create
|
|
236
|
+
</button>
|
|
237
|
+
<button
|
|
238
|
+
type="button"
|
|
239
|
+
onClick={onCancel}
|
|
240
|
+
className="text-muted-foreground hover:text-foreground inline-flex items-center gap-1 px-2 py-2 text-sm"
|
|
241
|
+
>
|
|
242
|
+
<X className="h-4 w-4" aria-hidden />
|
|
243
|
+
Cancel
|
|
244
|
+
</button>
|
|
245
|
+
</div>
|
|
246
|
+
{duplicate && (
|
|
247
|
+
<p className="text-destructive mt-2 text-xs">A tag with that name already exists.</p>
|
|
248
|
+
)}
|
|
249
|
+
</div>
|
|
250
|
+
)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function ColorPicker({ value, onChange }: { value: string; onChange: (c: string) => void }) {
|
|
254
|
+
return (
|
|
255
|
+
<div>
|
|
256
|
+
<span className="text-foreground mb-1 block text-sm font-medium">Color</span>
|
|
257
|
+
<div className="flex items-center gap-1.5" role="radiogroup" aria-label="Tag color">
|
|
258
|
+
{TAG_COLORS.map((c) => (
|
|
259
|
+
<button
|
|
260
|
+
key={c}
|
|
261
|
+
type="button"
|
|
262
|
+
role="radio"
|
|
263
|
+
aria-checked={value === c}
|
|
264
|
+
aria-label={c}
|
|
265
|
+
onClick={() => onChange(c)}
|
|
266
|
+
className={`h-7 w-7 rounded-full ${tagDotColor(c)} ring-offset-background transition-shadow ${
|
|
267
|
+
value === c ? 'ring-ring ring-2 ring-offset-2' : ''
|
|
268
|
+
}`}
|
|
269
|
+
/>
|
|
270
|
+
))}
|
|
271
|
+
</div>
|
|
272
|
+
</div>
|
|
273
|
+
)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ─── Tag row ─────────────────────────────────────────────────────────
|
|
277
|
+
|
|
278
|
+
function TagRow({
|
|
279
|
+
tag,
|
|
280
|
+
allTags,
|
|
281
|
+
onChanged,
|
|
282
|
+
onRequestDelete,
|
|
283
|
+
}: {
|
|
284
|
+
tag: PageTag
|
|
285
|
+
allTags: PageTag[]
|
|
286
|
+
onChanged: () => void
|
|
287
|
+
onRequestDelete: () => void
|
|
288
|
+
}) {
|
|
289
|
+
const [editing, setEditing] = useState(false)
|
|
290
|
+
const [name, setName] = useState(tag.name)
|
|
291
|
+
const [color, setColor] = useState(tag.color ?? 'gray')
|
|
292
|
+
const [merging, setMerging] = useState(false)
|
|
293
|
+
|
|
294
|
+
const save = async () => {
|
|
295
|
+
const res = await updatePageTag(tag.docId, { name: name.trim(), color })
|
|
296
|
+
if (res.error) {
|
|
297
|
+
toast.error(res.error)
|
|
298
|
+
return
|
|
299
|
+
}
|
|
300
|
+
toast.success('Tag updated')
|
|
301
|
+
setEditing(false)
|
|
302
|
+
onChanged()
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const merge = async (targetSlug: string) => {
|
|
306
|
+
const target = allTags.find((t) => t.slug === targetSlug)
|
|
307
|
+
if (!target) return
|
|
308
|
+
const promise = mergePageTags(tag, target)
|
|
309
|
+
toast.promise(promise, {
|
|
310
|
+
loading: `Merging "${tag.name}" into "${target.name}"…`,
|
|
311
|
+
success: () => `Merged into "${target.name}"`,
|
|
312
|
+
error: 'Merge failed',
|
|
313
|
+
})
|
|
314
|
+
await promise
|
|
315
|
+
setMerging(false)
|
|
316
|
+
onChanged()
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (editing) {
|
|
320
|
+
return (
|
|
321
|
+
<div className="flex flex-wrap items-end gap-3 p-4">
|
|
322
|
+
<label className="flex-1">
|
|
323
|
+
<span className="sr-only">Tag name</span>
|
|
324
|
+
<input
|
|
325
|
+
type="text"
|
|
326
|
+
value={name}
|
|
327
|
+
onChange={(e) => setName(e.target.value)}
|
|
328
|
+
className="border-border bg-input-background text-foreground focus:border-primary focus:ring-primary/30 w-full rounded-lg border px-3 py-2 text-sm focus:ring-2 focus:outline-none"
|
|
329
|
+
/>
|
|
330
|
+
</label>
|
|
331
|
+
<ColorPicker value={color} onChange={setColor} />
|
|
332
|
+
<button
|
|
333
|
+
type="button"
|
|
334
|
+
onClick={() => void save()}
|
|
335
|
+
className="bg-primary text-primary-foreground hover:bg-primary/90 rounded-lg px-3.5 py-2 text-sm font-medium"
|
|
336
|
+
>
|
|
337
|
+
Save
|
|
338
|
+
</button>
|
|
339
|
+
<button
|
|
340
|
+
type="button"
|
|
341
|
+
onClick={() => {
|
|
342
|
+
setEditing(false)
|
|
343
|
+
setName(tag.name)
|
|
344
|
+
setColor(tag.color ?? 'gray')
|
|
345
|
+
}}
|
|
346
|
+
className="text-muted-foreground hover:text-foreground px-2 py-2 text-sm"
|
|
347
|
+
>
|
|
348
|
+
Cancel
|
|
349
|
+
</button>
|
|
350
|
+
</div>
|
|
351
|
+
)
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return (
|
|
355
|
+
<div className="flex items-center gap-3 p-4">
|
|
356
|
+
<div className="flex min-w-0 flex-1 items-center gap-3">
|
|
357
|
+
<PageTagPill name={tag.name} color={tag.color} />
|
|
358
|
+
<span className="text-muted-foreground text-sm tabular-nums">
|
|
359
|
+
{tag.pageCount} page{tag.pageCount === 1 ? '' : 's'}
|
|
360
|
+
</span>
|
|
361
|
+
</div>
|
|
362
|
+
|
|
363
|
+
{merging ? (
|
|
364
|
+
<div className="flex items-center gap-2">
|
|
365
|
+
<label className="text-foreground inline-flex items-center gap-1.5 text-sm">
|
|
366
|
+
<span className="text-muted-foreground">Merge into:</span>
|
|
367
|
+
<select
|
|
368
|
+
defaultValue=""
|
|
369
|
+
onChange={(e) => {
|
|
370
|
+
if (e.target.value) void merge(e.target.value)
|
|
371
|
+
}}
|
|
372
|
+
className="border-border bg-card text-foreground rounded-lg border px-2.5 py-1.5 text-sm"
|
|
373
|
+
>
|
|
374
|
+
<option value="" disabled>
|
|
375
|
+
Choose tag…
|
|
376
|
+
</option>
|
|
377
|
+
{allTags
|
|
378
|
+
.filter((t) => t.slug !== tag.slug)
|
|
379
|
+
.map((t) => (
|
|
380
|
+
<option key={t.slug} value={t.slug}>
|
|
381
|
+
{t.name}
|
|
382
|
+
</option>
|
|
383
|
+
))}
|
|
384
|
+
</select>
|
|
385
|
+
</label>
|
|
386
|
+
<button
|
|
387
|
+
type="button"
|
|
388
|
+
onClick={() => setMerging(false)}
|
|
389
|
+
className="text-muted-foreground hover:text-foreground px-2 py-1 text-sm"
|
|
390
|
+
>
|
|
391
|
+
Cancel
|
|
392
|
+
</button>
|
|
393
|
+
</div>
|
|
394
|
+
) : (
|
|
395
|
+
<div className="flex items-center gap-1">
|
|
396
|
+
<IconAction label={`Edit ${tag.name}`} onClick={() => setEditing(true)}>
|
|
397
|
+
<Pencil className="h-4 w-4" aria-hidden />
|
|
398
|
+
</IconAction>
|
|
399
|
+
{allTags.length > 1 && (
|
|
400
|
+
<IconAction label={`Merge ${tag.name}`} onClick={() => setMerging(true)}>
|
|
401
|
+
<GitMerge className="h-4 w-4" aria-hidden />
|
|
402
|
+
</IconAction>
|
|
403
|
+
)}
|
|
404
|
+
<IconAction label={`Delete ${tag.name}`} onClick={onRequestDelete} destructive>
|
|
405
|
+
<Trash2 className="h-4 w-4" aria-hidden />
|
|
406
|
+
</IconAction>
|
|
407
|
+
</div>
|
|
408
|
+
)}
|
|
409
|
+
</div>
|
|
410
|
+
)
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function IconAction({
|
|
414
|
+
label,
|
|
415
|
+
onClick,
|
|
416
|
+
destructive,
|
|
417
|
+
children,
|
|
418
|
+
}: {
|
|
419
|
+
label: string
|
|
420
|
+
onClick: () => void
|
|
421
|
+
destructive?: boolean
|
|
422
|
+
children: React.ReactNode
|
|
423
|
+
}) {
|
|
424
|
+
return (
|
|
425
|
+
<button
|
|
426
|
+
type="button"
|
|
427
|
+
onClick={onClick}
|
|
428
|
+
aria-label={label}
|
|
429
|
+
className={`hover:bg-accent inline-flex h-8 w-8 items-center justify-center rounded-md transition-colors ${
|
|
430
|
+
destructive ? 'text-destructive hover:bg-destructive/10' : 'text-muted-foreground'
|
|
431
|
+
}`}
|
|
432
|
+
>
|
|
433
|
+
{children}
|
|
434
|
+
</button>
|
|
435
|
+
)
|
|
436
|
+
}
|