@prenta/admin 2.6.0 → 2.7.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/CHANGELOG.md +104 -0
- package/design-system/tokens.css +179 -85
- package/design-system/tokens.json +276 -85
- package/design-system/tokens.scss +148 -44
- package/design-system/tokens.tailwind.cjs +50 -1
- package/dist/AdminRoot.d.ts.map +1 -1
- package/dist/AdminRoot.js +42 -20
- package/dist/AdminRoot.js.map +1 -1
- package/dist/LoginRoot.d.ts.map +1 -1
- package/dist/LoginRoot.js +2 -1
- package/dist/LoginRoot.js.map +1 -1
- package/dist/__tests__/components/author-avatar.test.js +3 -17
- package/dist/__tests__/components/author-avatar.test.js.map +1 -1
- package/dist/__tests__/components/notification-bell.test.js +86 -15
- package/dist/__tests__/components/notification-bell.test.js.map +1 -1
- package/dist/__tests__/components/responsive-tiers.render.test.d.ts +2 -0
- package/dist/__tests__/components/responsive-tiers.render.test.d.ts.map +1 -0
- package/dist/__tests__/components/responsive-tiers.render.test.js +313 -0
- package/dist/__tests__/components/responsive-tiers.render.test.js.map +1 -0
- package/dist/__tests__/components/serp-preview.render.test.js +6 -3
- package/dist/__tests__/components/serp-preview.render.test.js.map +1 -1
- package/dist/__tests__/components/shell-primitives.render.test.d.ts +2 -0
- package/dist/__tests__/components/shell-primitives.render.test.d.ts.map +1 -0
- package/dist/__tests__/components/shell-primitives.render.test.js +716 -0
- package/dist/__tests__/components/shell-primitives.render.test.js.map +1 -0
- package/dist/__tests__/components/theme-provider-accent.test.js +49 -7
- package/dist/__tests__/components/theme-provider-accent.test.js.map +1 -1
- package/dist/__tests__/components/toggle.test.js +10 -3
- package/dist/__tests__/components/toggle.test.js.map +1 -1
- package/dist/__tests__/components/ui-primitives.test.js +3 -7
- package/dist/__tests__/components/ui-primitives.test.js.map +1 -1
- package/dist/__tests__/layout/build-nav-items.test.js +1 -1
- package/dist/__tests__/layout/build-nav-items.test.js.map +1 -1
- package/dist/__tests__/layout/shell-nav.render.test.d.ts +2 -0
- package/dist/__tests__/layout/shell-nav.render.test.d.ts.map +1 -0
- package/dist/__tests__/layout/shell-nav.render.test.js +736 -0
- package/dist/__tests__/layout/shell-nav.render.test.js.map +1 -0
- package/dist/__tests__/layout/sidebar-forms-submenu.render.test.js +8 -4
- package/dist/__tests__/layout/sidebar-forms-submenu.render.test.js.map +1 -1
- package/dist/__tests__/layout/sidebar-groups.render.test.d.ts +2 -0
- package/dist/__tests__/layout/sidebar-groups.render.test.d.ts.map +1 -0
- package/dist/__tests__/layout/sidebar-groups.render.test.js +56 -0
- package/dist/__tests__/layout/sidebar-groups.render.test.js.map +1 -0
- package/dist/__tests__/layout/sidebar-menus.render.test.d.ts +2 -0
- package/dist/__tests__/layout/sidebar-menus.render.test.d.ts.map +1 -0
- package/dist/__tests__/layout/sidebar-menus.render.test.js +82 -0
- package/dist/__tests__/layout/sidebar-menus.render.test.js.map +1 -0
- package/dist/__tests__/layout/sidebar-plugin-nav.test.js +1 -1
- package/dist/__tests__/layout/sidebar-plugin-nav.test.js.map +1 -1
- package/dist/__tests__/layout/sidebar-posts.test.js +48 -38
- package/dist/__tests__/layout/sidebar-posts.test.js.map +1 -1
- package/dist/__tests__/layout/sidebar-submenu.render.test.js +26 -10
- package/dist/__tests__/layout/sidebar-submenu.render.test.js.map +1 -1
- package/dist/__tests__/layout/topbar.render.test.d.ts +2 -0
- package/dist/__tests__/layout/topbar.render.test.d.ts.map +1 -0
- package/dist/__tests__/layout/topbar.render.test.js +406 -0
- package/dist/__tests__/layout/topbar.render.test.js.map +1 -0
- package/dist/__tests__/lib/api.test.js +2 -0
- package/dist/__tests__/lib/api.test.js.map +1 -1
- package/dist/__tests__/lib/job-drawer.test.d.ts +2 -0
- package/dist/__tests__/lib/job-drawer.test.d.ts.map +1 -0
- package/dist/__tests__/lib/job-drawer.test.js +393 -0
- package/dist/__tests__/lib/job-drawer.test.js.map +1 -0
- package/dist/__tests__/lib/job-view.test.d.ts +2 -0
- package/dist/__tests__/lib/job-view.test.d.ts.map +1 -0
- package/dist/__tests__/lib/job-view.test.js +387 -0
- package/dist/__tests__/lib/job-view.test.js.map +1 -0
- package/dist/__tests__/lib/jobs-service.test.d.ts +2 -0
- package/dist/__tests__/lib/jobs-service.test.d.ts.map +1 -0
- package/dist/__tests__/lib/jobs-service.test.js +88 -0
- package/dist/__tests__/lib/jobs-service.test.js.map +1 -0
- package/dist/__tests__/router/admin-route-order.test.d.ts +2 -0
- package/dist/__tests__/router/admin-route-order.test.d.ts.map +1 -0
- package/dist/__tests__/router/admin-route-order.test.js +36 -0
- package/dist/__tests__/router/admin-route-order.test.js.map +1 -0
- package/dist/__tests__/styles/ai-gradient-contrast.test.d.ts +2 -0
- package/dist/__tests__/styles/ai-gradient-contrast.test.d.ts.map +1 -0
- package/dist/__tests__/styles/ai-gradient-contrast.test.js +110 -0
- package/dist/__tests__/styles/ai-gradient-contrast.test.js.map +1 -0
- package/dist/__tests__/styles/breakpoint-order.test.d.ts +2 -0
- package/dist/__tests__/styles/breakpoint-order.test.d.ts.map +1 -0
- package/dist/__tests__/styles/breakpoint-order.test.js +52 -0
- package/dist/__tests__/styles/breakpoint-order.test.js.map +1 -0
- package/dist/__tests__/styles/responsive-layer.test.js +303 -12
- package/dist/__tests__/styles/responsive-layer.test.js.map +1 -1
- package/dist/__tests__/styles/tailwind-preset.test.d.ts +2 -0
- package/dist/__tests__/styles/tailwind-preset.test.d.ts.map +1 -0
- package/dist/__tests__/styles/tailwind-preset.test.js +147 -0
- package/dist/__tests__/styles/tailwind-preset.test.js.map +1 -0
- package/dist/__tests__/styles/theme-token-scope.test.js +9 -3
- package/dist/__tests__/styles/theme-token-scope.test.js.map +1 -1
- package/dist/__tests__/styles/tiers.test.d.ts +2 -0
- package/dist/__tests__/styles/tiers.test.d.ts.map +1 -0
- package/dist/__tests__/styles/tiers.test.js +121 -0
- package/dist/__tests__/styles/tiers.test.js.map +1 -0
- package/dist/__tests__/styles/tokens-drift.test.js +57 -6
- package/dist/__tests__/styles/tokens-drift.test.js.map +1 -1
- package/dist/__tests__/views/admin-lazy-route.render.test.js +27 -1
- package/dist/__tests__/views/admin-lazy-route.render.test.js.map +1 -1
- package/dist/__tests__/views/ai-settings.render.test.js +4 -4
- package/dist/__tests__/views/appearance-menus-route.render.test.d.ts +2 -0
- package/dist/__tests__/views/appearance-menus-route.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/appearance-menus-route.render.test.js +50 -0
- package/dist/__tests__/views/appearance-menus-route.render.test.js.map +1 -0
- package/dist/__tests__/views/appearance-settings.render.test.js +8 -6
- package/dist/__tests__/views/appearance-settings.render.test.js.map +1 -1
- package/dist/__tests__/views/appearance-theme-provider.render.test.d.ts +2 -0
- package/dist/__tests__/views/appearance-theme-provider.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/appearance-theme-provider.render.test.js +286 -0
- package/dist/__tests__/views/appearance-theme-provider.render.test.js.map +1 -0
- package/dist/__tests__/views/dashboard.test.js +37 -0
- package/dist/__tests__/views/dashboard.test.js.map +1 -1
- package/dist/__tests__/views/document-edit-navigation.render.test.d.ts +2 -0
- package/dist/__tests__/views/document-edit-navigation.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/document-edit-navigation.render.test.js +101 -0
- package/dist/__tests__/views/document-edit-navigation.render.test.js.map +1 -0
- package/dist/__tests__/views/document-edit-routes.render.test.d.ts +2 -0
- package/dist/__tests__/views/document-edit-routes.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/document-edit-routes.render.test.js +113 -0
- package/dist/__tests__/views/document-edit-routes.render.test.js.map +1 -0
- package/dist/__tests__/views/email-settings.render.test.js +2 -2
- package/dist/__tests__/views/form-entries.render.test.js +15 -0
- package/dist/__tests__/views/form-entries.render.test.js.map +1 -1
- package/dist/__tests__/views/form-submissions.render.test.js +18 -0
- package/dist/__tests__/views/form-submissions.render.test.js.map +1 -1
- package/dist/__tests__/views/forms-list.render.test.js +12 -0
- package/dist/__tests__/views/forms-list.render.test.js.map +1 -1
- package/dist/__tests__/views/general-settings.render.test.js +26 -10
- package/dist/__tests__/views/general-settings.render.test.js.map +1 -1
- package/dist/__tests__/views/jobs-drawer.render.test.d.ts +2 -0
- package/dist/__tests__/views/jobs-drawer.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/jobs-drawer.render.test.js +908 -0
- package/dist/__tests__/views/jobs-drawer.render.test.js.map +1 -0
- package/dist/__tests__/views/jobs-list.render.test.d.ts +2 -0
- package/dist/__tests__/views/jobs-list.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/jobs-list.render.test.js +695 -0
- package/dist/__tests__/views/jobs-list.render.test.js.map +1 -0
- package/dist/__tests__/views/kpi-grid.test.d.ts +2 -0
- package/dist/__tests__/views/kpi-grid.test.d.ts.map +1 -0
- package/dist/__tests__/views/kpi-grid.test.js +26 -0
- package/dist/__tests__/views/kpi-grid.test.js.map +1 -0
- package/dist/__tests__/views/mcp-connect.render.test.js +1 -0
- package/dist/__tests__/views/mcp-connect.render.test.js.map +1 -1
- package/dist/__tests__/views/post-section-editor.test.js +3 -1
- package/dist/__tests__/views/post-section-editor.test.js.map +1 -1
- package/dist/__tests__/views/script-tags.render.test.js +25 -0
- package/dist/__tests__/views/script-tags.render.test.js.map +1 -1
- package/dist/__tests__/views/security-settings.render.test.js +9 -8
- package/dist/__tests__/views/security-settings.render.test.js.map +1 -1
- package/dist/__tests__/views/seo-settings.render.test.js +7 -7
- package/dist/__tests__/views/seo-settings.render.test.js.map +1 -1
- package/dist/__tests__/views/settings-save-bar.render.test.d.ts +2 -0
- package/dist/__tests__/views/settings-save-bar.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/settings-save-bar.render.test.js +68 -0
- package/dist/__tests__/views/settings-save-bar.render.test.js.map +1 -0
- package/dist/__tests__/views/settings-save-footer.render.test.js +23 -15
- package/dist/__tests__/views/settings-save-footer.render.test.js.map +1 -1
- package/dist/__tests__/views/settings-tabs.render.test.d.ts +2 -0
- package/dist/__tests__/views/settings-tabs.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/settings-tabs.render.test.js +319 -0
- package/dist/__tests__/views/settings-tabs.render.test.js.map +1 -0
- package/dist/__tests__/views/updates-tab.render.test.js +4 -3
- package/dist/__tests__/views/updates-tab.render.test.js.map +1 -1
- package/dist/components/Breadcrumbs.d.ts +8 -1
- package/dist/components/Breadcrumbs.d.ts.map +1 -1
- package/dist/components/Breadcrumbs.js +6 -0
- package/dist/components/Breadcrumbs.js.map +1 -1
- package/dist/components/CommandPalette.d.ts.map +1 -1
- package/dist/components/CommandPalette.js +40 -4
- package/dist/components/CommandPalette.js.map +1 -1
- package/dist/components/LocaleSwitcher.d.ts.map +1 -1
- package/dist/components/LocaleSwitcher.js +1 -1
- package/dist/components/LocaleSwitcher.js.map +1 -1
- package/dist/components/NotificationBell.d.ts.map +1 -1
- package/dist/components/NotificationBell.js +44 -3
- package/dist/components/NotificationBell.js.map +1 -1
- package/dist/components/OfflineStatus.js +1 -1
- package/dist/components/OfflineStatus.js.map +1 -1
- package/dist/components/PresenceIndicator.d.ts.map +1 -1
- package/dist/components/PresenceIndicator.js +2 -1
- package/dist/components/PresenceIndicator.js.map +1 -1
- package/dist/components/ReflowMeter.js +2 -2
- package/dist/components/ReflowMeter.js.map +1 -1
- package/dist/components/SEOPanel.js +2 -2
- package/dist/components/SEOPanel.js.map +1 -1
- package/dist/components/ThemeProvider.d.ts +24 -4
- package/dist/components/ThemeProvider.d.ts.map +1 -1
- package/dist/components/ThemeProvider.js +80 -22
- package/dist/components/ThemeProvider.js.map +1 -1
- package/dist/components/TipTapEditor.d.ts.map +1 -1
- package/dist/components/TipTapEditor.js +3 -4
- package/dist/components/TipTapEditor.js.map +1 -1
- package/dist/components/forms/EmbedPanel.js +1 -1
- package/dist/components/forms/EmbedPanel.js.map +1 -1
- package/dist/components/forms/FormEntryDetailPane.js +6 -1
- package/dist/components/forms/FormEntryDetailPane.js.map +1 -1
- package/dist/components/forms/ShortcodeGlossary.d.ts.map +1 -1
- package/dist/components/forms/ShortcodeGlossary.js +6 -1
- package/dist/components/forms/ShortcodeGlossary.js.map +1 -1
- package/dist/components/forms/primitives.d.ts +15 -5
- package/dist/components/forms/primitives.d.ts.map +1 -1
- package/dist/components/forms/primitives.js +11 -11
- package/dist/components/forms/primitives.js.map +1 -1
- package/dist/components/pages/tag-badges.d.ts +2 -1
- package/dist/components/pages/tag-badges.d.ts.map +1 -1
- package/dist/components/pages/tag-badges.js +4 -3
- package/dist/components/pages/tag-badges.js.map +1 -1
- package/dist/components/seo/PageAuditPanel.js +2 -2
- package/dist/components/seo/PageAuditPanel.js.map +1 -1
- package/dist/components/seo/SerpPreview.d.ts +4 -4
- package/dist/components/seo/SerpPreview.js +1 -1
- package/dist/components/seo/SerpPreview.js.map +1 -1
- package/dist/components/seo/primitives.js +2 -2
- package/dist/components/seo/primitives.js.map +1 -1
- package/dist/components/ui/AuthorAvatar.d.ts +4 -4
- package/dist/components/ui/AuthorAvatar.d.ts.map +1 -1
- package/dist/components/ui/AuthorAvatar.js +11 -10
- package/dist/components/ui/AuthorAvatar.js.map +1 -1
- package/dist/components/ui/Badge.d.ts +1 -1
- package/dist/components/ui/Badge.js +1 -1
- package/dist/components/ui/Badge.js.map +1 -1
- package/dist/components/ui/Breadcrumb.d.ts +27 -0
- package/dist/components/ui/Breadcrumb.d.ts.map +1 -0
- package/dist/components/ui/Breadcrumb.js +41 -0
- package/dist/components/ui/Breadcrumb.js.map +1 -0
- package/dist/components/ui/Button.d.ts +2 -2
- package/dist/components/ui/Button.d.ts.map +1 -1
- package/dist/components/ui/Button.js +31 -16
- package/dist/components/ui/Button.js.map +1 -1
- package/dist/components/ui/Card.d.ts +8 -4
- package/dist/components/ui/Card.d.ts.map +1 -1
- package/dist/components/ui/Card.js +21 -13
- package/dist/components/ui/Card.js.map +1 -1
- package/dist/components/ui/CardListControls.d.ts +34 -0
- package/dist/components/ui/CardListControls.d.ts.map +1 -0
- package/dist/components/ui/CardListControls.js +25 -0
- package/dist/components/ui/CardListControls.js.map +1 -0
- package/dist/components/ui/CountChip.d.ts +20 -0
- package/dist/components/ui/CountChip.d.ts.map +1 -0
- package/dist/components/ui/CountChip.js +48 -0
- package/dist/components/ui/CountChip.js.map +1 -0
- package/dist/components/ui/FilterPill.d.ts +2 -1
- package/dist/components/ui/FilterPill.d.ts.map +1 -1
- package/dist/components/ui/FilterPill.js +3 -2
- package/dist/components/ui/FilterPill.js.map +1 -1
- package/dist/components/ui/Input.d.ts +1 -1
- package/dist/components/ui/InspectorPane.d.ts +1 -1
- package/dist/components/ui/InspectorPane.d.ts.map +1 -1
- package/dist/components/ui/InspectorPane.js +15 -4
- package/dist/components/ui/InspectorPane.js.map +1 -1
- package/dist/components/ui/Modal.d.ts.map +1 -1
- package/dist/components/ui/Modal.js +10 -2
- package/dist/components/ui/Modal.js.map +1 -1
- package/dist/components/ui/PageHeader.d.ts +28 -0
- package/dist/components/ui/PageHeader.d.ts.map +1 -0
- package/dist/components/ui/PageHeader.js +18 -0
- package/dist/components/ui/PageHeader.js.map +1 -0
- package/dist/components/ui/RowActions.d.ts +5 -0
- package/dist/components/ui/RowActions.d.ts.map +1 -1
- package/dist/components/ui/RowActions.js +7 -2
- package/dist/components/ui/RowActions.js.map +1 -1
- package/dist/components/ui/Segmented.d.ts +21 -0
- package/dist/components/ui/Segmented.d.ts.map +1 -0
- package/dist/components/ui/Segmented.js +41 -0
- package/dist/components/ui/Segmented.js.map +1 -0
- package/dist/components/ui/Toaster.d.ts +12 -0
- package/dist/components/ui/Toaster.d.ts.map +1 -0
- package/dist/components/ui/Toaster.js +103 -0
- package/dist/components/ui/Toaster.js.map +1 -0
- package/dist/components/ui/Toggle.d.ts +4 -3
- package/dist/components/ui/Toggle.d.ts.map +1 -1
- package/dist/components/ui/Toggle.js +16 -12
- package/dist/components/ui/Toggle.js.map +1 -1
- package/dist/components/ui/UnderlineTabs.d.ts +56 -0
- package/dist/components/ui/UnderlineTabs.d.ts.map +1 -0
- package/dist/components/ui/UnderlineTabs.js +205 -0
- package/dist/components/ui/UnderlineTabs.js.map +1 -0
- package/dist/components/ui/UnsavedBar.d.ts +25 -0
- package/dist/components/ui/UnsavedBar.d.ts.map +1 -0
- package/dist/components/ui/UnsavedBar.js +22 -0
- package/dist/components/ui/UnsavedBar.js.map +1 -0
- package/dist/components/ui/admin-links.d.ts +11 -0
- package/dist/components/ui/admin-links.d.ts.map +1 -0
- package/dist/components/ui/admin-links.js +10 -0
- package/dist/components/ui/admin-links.js.map +1 -0
- package/dist/components/ui/card-table.d.ts +43 -0
- package/dist/components/ui/card-table.d.ts.map +1 -0
- package/dist/components/ui/card-table.js +31 -0
- package/dist/components/ui/card-table.js.map +1 -0
- package/dist/components/virtual-window.d.ts +7 -0
- package/dist/components/virtual-window.d.ts.map +1 -1
- package/dist/components/virtual-window.js +5 -2
- package/dist/components/virtual-window.js.map +1 -1
- package/dist/fields/RelationshipField.js +1 -1
- package/dist/fields/RelationshipField.js.map +1 -1
- package/dist/files/dm-mono-latin-400-normal.woff +0 -0
- package/dist/files/dm-mono-latin-400-normal.woff2 +0 -0
- package/dist/files/dm-mono-latin-500-normal.woff +0 -0
- package/dist/files/dm-mono-latin-500-normal.woff2 +0 -0
- package/dist/files/dm-mono-latin-ext-400-normal.woff +0 -0
- package/dist/files/dm-mono-latin-ext-400-normal.woff2 +0 -0
- package/dist/files/dm-mono-latin-ext-500-normal.woff +0 -0
- package/dist/files/dm-mono-latin-ext-500-normal.woff2 +0 -0
- package/dist/files/dm-sans-latin-400-normal.woff +0 -0
- package/dist/files/dm-sans-latin-400-normal.woff2 +0 -0
- package/dist/files/dm-sans-latin-500-normal.woff +0 -0
- package/dist/files/dm-sans-latin-500-normal.woff2 +0 -0
- package/dist/files/dm-sans-latin-600-normal.woff +0 -0
- package/dist/files/dm-sans-latin-600-normal.woff2 +0 -0
- package/dist/files/dm-sans-latin-700-normal.woff +0 -0
- package/dist/files/dm-sans-latin-700-normal.woff2 +0 -0
- package/dist/files/dm-sans-latin-ext-400-normal.woff +0 -0
- package/dist/files/dm-sans-latin-ext-400-normal.woff2 +0 -0
- package/dist/files/dm-sans-latin-ext-500-normal.woff +0 -0
- package/dist/files/dm-sans-latin-ext-500-normal.woff2 +0 -0
- package/dist/files/dm-sans-latin-ext-600-normal.woff +0 -0
- package/dist/files/dm-sans-latin-ext-600-normal.woff2 +0 -0
- package/dist/files/dm-sans-latin-ext-700-normal.woff +0 -0
- package/dist/files/dm-sans-latin-ext-700-normal.woff2 +0 -0
- package/dist/hooks/useAppearanceBootstrap.d.ts.map +1 -1
- package/dist/hooks/useAppearanceBootstrap.js +9 -3
- package/dist/hooks/useAppearanceBootstrap.js.map +1 -1
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/layout/Header.d.ts +14 -5
- package/dist/layout/Header.d.ts.map +1 -1
- package/dist/layout/Header.js +78 -13
- package/dist/layout/Header.js.map +1 -1
- package/dist/layout/Layout.d.ts +5 -3
- package/dist/layout/Layout.d.ts.map +1 -1
- package/dist/layout/Layout.js +45 -54
- package/dist/layout/Layout.js.map +1 -1
- package/dist/layout/MobileTabBar.d.ts +20 -0
- package/dist/layout/MobileTabBar.d.ts.map +1 -0
- package/dist/layout/MobileTabBar.js +29 -0
- package/dist/layout/MobileTabBar.js.map +1 -0
- package/dist/layout/Sidebar.d.ts +8 -26
- package/dist/layout/Sidebar.d.ts.map +1 -1
- package/dist/layout/Sidebar.js +309 -345
- package/dist/layout/Sidebar.js.map +1 -1
- package/dist/layout/nav-config.d.ts +101 -0
- package/dist/layout/nav-config.d.ts.map +1 -0
- package/dist/layout/nav-config.js +297 -0
- package/dist/layout/nav-config.js.map +1 -0
- package/dist/layout/primitives/AdminShell.d.ts +10 -2
- package/dist/layout/primitives/AdminShell.d.ts.map +1 -1
- package/dist/layout/primitives/AdminShell.js +114 -6
- package/dist/layout/primitives/AdminShell.js.map +1 -1
- package/dist/layout/topbar-classes.d.ts +8 -0
- package/dist/layout/topbar-classes.d.ts.map +1 -0
- package/dist/layout/topbar-classes.js +8 -0
- package/dist/layout/topbar-classes.js.map +1 -0
- package/dist/layout/use-nav-items.d.ts +10 -0
- package/dist/layout/use-nav-items.d.ts.map +1 -0
- package/dist/layout/use-nav-items.js +39 -0
- package/dist/layout/use-nav-items.js.map +1 -0
- package/dist/lib/api.d.ts +6 -0
- package/dist/lib/api.d.ts.map +1 -1
- package/dist/lib/api.js +1 -1
- package/dist/lib/api.js.map +1 -1
- package/dist/lib/appearance-api.d.ts +3 -1
- package/dist/lib/appearance-api.d.ts.map +1 -1
- package/dist/lib/appearance-api.js +1 -0
- package/dist/lib/appearance-api.js.map +1 -1
- package/dist/lib/job-drawer.d.ts +128 -0
- package/dist/lib/job-drawer.d.ts.map +1 -0
- package/dist/lib/job-drawer.js +351 -0
- package/dist/lib/job-drawer.js.map +1 -0
- package/dist/lib/job-view.d.ts +187 -0
- package/dist/lib/job-view.d.ts.map +1 -0
- package/dist/lib/job-view.js +363 -0
- package/dist/lib/job-view.js.map +1 -0
- package/dist/lib/jobs-service.d.ts +134 -0
- package/dist/lib/jobs-service.d.ts.map +1 -0
- package/dist/lib/jobs-service.js +179 -0
- package/dist/lib/jobs-service.js.map +1 -0
- package/dist/lib/sheet-focus.d.ts +24 -0
- package/dist/lib/sheet-focus.d.ts.map +1 -0
- package/dist/lib/sheet-focus.js +61 -0
- package/dist/lib/sheet-focus.js.map +1 -0
- package/dist/lib/tiers.d.ts +30 -0
- package/dist/lib/tiers.d.ts.map +1 -0
- package/dist/lib/tiers.js +43 -0
- package/dist/lib/tiers.js.map +1 -0
- package/dist/prenta-admin.css +1 -1
- package/dist/router/admin-routes.d.ts.map +1 -1
- package/dist/router/admin-routes.js +8 -0
- package/dist/router/admin-routes.js.map +1 -1
- package/dist/router/index.d.ts +5 -1
- package/dist/router/index.d.ts.map +1 -1
- package/dist/router/index.js +7 -0
- package/dist/router/index.js.map +1 -1
- package/dist/views/ApiKeys.js +2 -2
- package/dist/views/ApiKeys.js.map +1 -1
- package/dist/views/AppearanceChrome.d.ts +6 -0
- package/dist/views/AppearanceChrome.d.ts.map +1 -0
- package/dist/views/AppearanceChrome.js +25 -0
- package/dist/views/AppearanceChrome.js.map +1 -0
- package/dist/views/CollectionList.d.ts.map +1 -1
- package/dist/views/CollectionList.js +73 -53
- package/dist/views/CollectionList.js.map +1 -1
- package/dist/views/Dashboard.d.ts.map +1 -1
- package/dist/views/Dashboard.js +14 -8
- package/dist/views/Dashboard.js.map +1 -1
- package/dist/views/DocumentEdit.d.ts +6 -1
- package/dist/views/DocumentEdit.d.ts.map +1 -1
- package/dist/views/DocumentEdit.js +44 -8
- package/dist/views/DocumentEdit.js.map +1 -1
- package/dist/views/FormDeliveries.d.ts.map +1 -1
- package/dist/views/FormDeliveries.js +7 -1
- package/dist/views/FormDeliveries.js.map +1 -1
- package/dist/views/FormEntries.d.ts.map +1 -1
- package/dist/views/FormEntries.js +11 -2
- package/dist/views/FormEntries.js.map +1 -1
- package/dist/views/FormSubmissions.d.ts +1 -1
- package/dist/views/FormSubmissions.d.ts.map +1 -1
- package/dist/views/FormSubmissions.js +15 -4
- package/dist/views/FormSubmissions.js.map +1 -1
- package/dist/views/Forms.d.ts.map +1 -1
- package/dist/views/Forms.js +6 -4
- package/dist/views/Forms.js.map +1 -1
- package/dist/views/FormsSettings.d.ts +2 -0
- package/dist/views/FormsSettings.d.ts.map +1 -0
- package/dist/views/FormsSettings.js +37 -0
- package/dist/views/FormsSettings.js.map +1 -0
- package/dist/views/Jobs/BusyButton.d.ts +15 -0
- package/dist/views/Jobs/BusyButton.d.ts.map +1 -0
- package/dist/views/Jobs/BusyButton.js +26 -0
- package/dist/views/Jobs/BusyButton.js.map +1 -0
- package/dist/views/Jobs/CancelJobDialog.d.ts +21 -0
- package/dist/views/Jobs/CancelJobDialog.d.ts.map +1 -0
- package/dist/views/Jobs/CancelJobDialog.js +66 -0
- package/dist/views/Jobs/CancelJobDialog.js.map +1 -0
- package/dist/views/Jobs/DrawerActivity.d.ts +16 -0
- package/dist/views/Jobs/DrawerActivity.d.ts.map +1 -0
- package/dist/views/Jobs/DrawerActivity.js +59 -0
- package/dist/views/Jobs/DrawerActivity.js.map +1 -0
- package/dist/views/Jobs/DrawerOverview.d.ts +28 -0
- package/dist/views/Jobs/DrawerOverview.d.ts.map +1 -0
- package/dist/views/Jobs/DrawerOverview.js +32 -0
- package/dist/views/Jobs/DrawerOverview.js.map +1 -0
- package/dist/views/Jobs/DrawerResults.d.ts +25 -0
- package/dist/views/Jobs/DrawerResults.d.ts.map +1 -0
- package/dist/views/Jobs/DrawerResults.js +50 -0
- package/dist/views/Jobs/DrawerResults.js.map +1 -0
- package/dist/views/Jobs/DrawerReview.d.ts +44 -0
- package/dist/views/Jobs/DrawerReview.d.ts.map +1 -0
- package/dist/views/Jobs/DrawerReview.js +110 -0
- package/dist/views/Jobs/DrawerReview.js.map +1 -0
- package/dist/views/Jobs/JobDrawer.d.ts +31 -0
- package/dist/views/Jobs/JobDrawer.d.ts.map +1 -0
- package/dist/views/Jobs/JobDrawer.js +353 -0
- package/dist/views/Jobs/JobDrawer.js.map +1 -0
- package/dist/views/Jobs/JobsTable.d.ts +19 -0
- package/dist/views/Jobs/JobsTable.d.ts.map +1 -0
- package/dist/views/Jobs/JobsTable.js +80 -0
- package/dist/views/Jobs/JobsTable.js.map +1 -0
- package/dist/views/Jobs/WorkerStrip.d.ts +22 -0
- package/dist/views/Jobs/WorkerStrip.d.ts.map +1 -0
- package/dist/views/Jobs/WorkerStrip.js +63 -0
- package/dist/views/Jobs/WorkerStrip.js.map +1 -0
- package/dist/views/Jobs/atoms.d.ts +19 -0
- package/dist/views/Jobs/atoms.d.ts.map +1 -0
- package/dist/views/Jobs/atoms.js +52 -0
- package/dist/views/Jobs/atoms.js.map +1 -0
- package/dist/views/Jobs/focus.d.ts +23 -0
- package/dist/views/Jobs/focus.d.ts.map +1 -0
- package/dist/views/Jobs/focus.js +54 -0
- package/dist/views/Jobs/focus.js.map +1 -0
- package/dist/views/Jobs/icons.d.ts +22 -0
- package/dist/views/Jobs/icons.d.ts.map +1 -0
- package/dist/views/Jobs/icons.js +23 -0
- package/dist/views/Jobs/icons.js.map +1 -0
- package/dist/views/Jobs/placeholders.d.ts +6 -0
- package/dist/views/Jobs/placeholders.d.ts.map +1 -0
- package/dist/views/Jobs/placeholders.js +20 -0
- package/dist/views/Jobs/placeholders.js.map +1 -0
- package/dist/views/Jobs/useJobDetail.d.ts +21 -0
- package/dist/views/Jobs/useJobDetail.d.ts.map +1 -0
- package/dist/views/Jobs/useJobDetail.js +39 -0
- package/dist/views/Jobs/useJobDetail.js.map +1 -0
- package/dist/views/Jobs/useJobsFeed.d.ts +35 -0
- package/dist/views/Jobs/useJobsFeed.d.ts.map +1 -0
- package/dist/views/Jobs/useJobsFeed.js +231 -0
- package/dist/views/Jobs/useJobsFeed.js.map +1 -0
- package/dist/views/Jobs/useReviewItems.d.ts +39 -0
- package/dist/views/Jobs/useReviewItems.d.ts.map +1 -0
- package/dist/views/Jobs/useReviewItems.js +140 -0
- package/dist/views/Jobs/useReviewItems.js.map +1 -0
- package/dist/views/Jobs.d.ts +10 -1
- package/dist/views/Jobs.d.ts.map +1 -1
- package/dist/views/Jobs.js +126 -35
- package/dist/views/Jobs.js.map +1 -1
- package/dist/views/MediaBrowser.d.ts.map +1 -1
- package/dist/views/MediaBrowser.js +37 -9
- package/dist/views/MediaBrowser.js.map +1 -1
- package/dist/views/Pages/PagesListView.d.ts +1 -1
- package/dist/views/Pages/PagesListView.d.ts.map +1 -1
- package/dist/views/Pages/PagesListView.js +34 -5
- package/dist/views/Pages/PagesListView.js.map +1 -1
- package/dist/views/Posts/NewPostTypeInstructions.d.ts.map +1 -1
- package/dist/views/Posts/NewPostTypeInstructions.js +12 -2
- package/dist/views/Posts/NewPostTypeInstructions.js.map +1 -1
- package/dist/views/Posts/PostTypesView.d.ts.map +1 -1
- package/dist/views/Posts/PostTypesView.js +10 -1
- package/dist/views/Posts/PostTypesView.js.map +1 -1
- package/dist/views/Posts/PostsListView.d.ts +1 -2
- package/dist/views/Posts/PostsListView.d.ts.map +1 -1
- package/dist/views/Posts/PostsListView.js +31 -3
- package/dist/views/Posts/PostsListView.js.map +1 -1
- package/dist/views/SEO.d.ts.map +1 -1
- package/dist/views/SEO.js +3 -3
- package/dist/views/SEO.js.map +1 -1
- package/dist/views/ScriptTags.d.ts.map +1 -1
- package/dist/views/ScriptTags.js +20 -15
- package/dist/views/ScriptTags.js.map +1 -1
- package/dist/views/Settings.d.ts.map +1 -1
- package/dist/views/Settings.js +115 -51
- package/dist/views/Settings.js.map +1 -1
- package/dist/views/SpamFolder.d.ts +2 -0
- package/dist/views/SpamFolder.d.ts.map +1 -0
- package/dist/views/SpamFolder.js +14 -0
- package/dist/views/SpamFolder.js.map +1 -0
- package/dist/views/Trash.d.ts +6 -1
- package/dist/views/Trash.d.ts.map +1 -1
- package/dist/views/Trash.js +7 -2
- package/dist/views/Trash.js.map +1 -1
- package/dist/views/Users.js +2 -2
- package/dist/views/Users.js.map +1 -1
- package/dist/views/WidgetsPlaceholder.d.ts +2 -0
- package/dist/views/WidgetsPlaceholder.d.ts.map +1 -0
- package/dist/views/WidgetsPlaceholder.js +15 -0
- package/dist/views/WidgetsPlaceholder.js.map +1 -0
- package/dist/views/dashboard/kpi-grid.d.ts +18 -0
- package/dist/views/dashboard/kpi-grid.d.ts.map +1 -0
- package/dist/views/dashboard/kpi-grid.js +36 -0
- package/dist/views/dashboard/kpi-grid.js.map +1 -0
- package/dist/views/lazy-views.d.ts +4 -0
- package/dist/views/lazy-views.d.ts.map +1 -1
- package/dist/views/lazy-views.js +4 -0
- package/dist/views/lazy-views.js.map +1 -1
- package/dist/views/page-editor/PageTab.js +1 -1
- package/dist/views/page-editor/PageTab.js.map +1 -1
- package/dist/views/page-editor/ReviewBar.js +1 -1
- package/dist/views/page-editor/ReviewBar.js.map +1 -1
- package/dist/views/page-editor/VersionHistoryPanel.js +1 -1
- package/dist/views/page-editor/VersionHistoryPanel.js.map +1 -1
- package/dist/views/seo/AuditTab.js +1 -1
- package/dist/views/seo/AuditTab.js.map +1 -1
- package/dist/views/seo/OverviewTab.d.ts.map +1 -1
- package/dist/views/seo/OverviewTab.js +2 -2
- package/dist/views/seo/OverviewTab.js.map +1 -1
- package/dist/views/seo/ProposalsTab.js +1 -1
- package/dist/views/seo/ProposalsTab.js.map +1 -1
- package/dist/views/settings/AISettingsTab.d.ts.map +1 -1
- package/dist/views/settings/AISettingsTab.js +4 -2
- package/dist/views/settings/AISettingsTab.js.map +1 -1
- package/dist/views/settings/AccessProtectionCard.js +1 -1
- package/dist/views/settings/AccessProtectionCard.js.map +1 -1
- package/dist/views/settings/AdminThemeCard.d.ts.map +1 -1
- package/dist/views/settings/AdminThemeCard.js +63 -15
- package/dist/views/settings/AdminThemeCard.js.map +1 -1
- package/dist/views/settings/AppearanceTab.d.ts.map +1 -1
- package/dist/views/settings/AppearanceTab.js +4 -2
- package/dist/views/settings/AppearanceTab.js.map +1 -1
- package/dist/views/settings/BrandPreviewCard.js +1 -1
- package/dist/views/settings/BrandPreviewCard.js.map +1 -1
- package/dist/views/settings/BrandVoiceCard.js +1 -1
- package/dist/views/settings/BrandVoiceCard.js.map +1 -1
- package/dist/views/settings/EmailSettingsTab.d.ts.map +1 -1
- package/dist/views/settings/EmailSettingsTab.js +5 -3
- package/dist/views/settings/EmailSettingsTab.js.map +1 -1
- package/dist/views/settings/GeneralSettingsTab.d.ts.map +1 -1
- package/dist/views/settings/GeneralSettingsTab.js +4 -2
- package/dist/views/settings/GeneralSettingsTab.js.map +1 -1
- package/dist/views/settings/McpConnectTab.d.ts.map +1 -1
- package/dist/views/settings/McpConnectTab.js +1 -1
- package/dist/views/settings/McpConnectTab.js.map +1 -1
- package/dist/views/settings/ProviderConnectionCard.js +1 -1
- package/dist/views/settings/ProviderConnectionCard.js.map +1 -1
- package/dist/views/settings/SearchDataCard.js +1 -1
- package/dist/views/settings/SearchDataCard.js.map +1 -1
- package/dist/views/settings/SecurityStatusCard.js +2 -2
- package/dist/views/settings/SecurityStatusCard.js.map +1 -1
- package/dist/views/settings/SecurityTab.d.ts.map +1 -1
- package/dist/views/settings/SecurityTab.js +4 -2
- package/dist/views/settings/SecurityTab.js.map +1 -1
- package/dist/views/settings/SeoAutopilotCard.js +2 -2
- package/dist/views/settings/SeoAutopilotCard.js.map +1 -1
- package/dist/views/settings/SeoSettingsTab.d.ts.map +1 -1
- package/dist/views/settings/SeoSettingsTab.js +5 -3
- package/dist/views/settings/SeoSettingsTab.js.map +1 -1
- package/dist/views/settings/SiteIndexStatusCard.js +1 -1
- package/dist/views/settings/SiteIndexStatusCard.js.map +1 -1
- package/dist/views/settings/SiteThemeCard.js +1 -1
- package/dist/views/settings/UpdatesTab.d.ts.map +1 -1
- package/dist/views/settings/UpdatesTab.js +6 -5
- package/dist/views/settings/UpdatesTab.js.map +1 -1
- package/dist/views/settings/components.d.ts +7 -11
- package/dist/views/settings/components.d.ts.map +1 -1
- package/dist/views/settings/components.js +18 -12
- package/dist/views/settings/components.js.map +1 -1
- package/dist/views/settings/useAppearanceSettings.d.ts +7 -1
- package/dist/views/settings/useAppearanceSettings.d.ts.map +1 -1
- package/dist/views/settings/useAppearanceSettings.js +89 -22
- package/dist/views/settings/useAppearanceSettings.js.map +1 -1
- package/dist/views/users/UserDetailDrawer.js +1 -1
- package/dist/views/users/UserDetailDrawer.js.map +1 -1
- package/package.json +11 -9
- package/src/AdminRoot.tsx +88 -30
- package/src/LoginRoot.tsx +2 -1
- package/src/__tests__/components/author-avatar.test.tsx +3 -20
- package/src/__tests__/components/notification-bell.test.tsx +146 -14
- package/src/__tests__/components/responsive-tiers.render.test.tsx +461 -0
- package/src/__tests__/components/serp-preview.render.test.tsx +6 -3
- package/src/__tests__/components/shell-primitives.render.test.tsx +951 -0
- package/src/__tests__/components/theme-provider-accent.test.tsx +83 -8
- package/src/__tests__/components/toggle.test.tsx +11 -3
- package/src/__tests__/components/ui-primitives.test.tsx +5 -8
- package/src/__tests__/layout/build-nav-items.test.ts +1 -1
- package/src/__tests__/layout/shell-nav.render.test.tsx +983 -0
- package/src/__tests__/layout/sidebar-forms-submenu.render.test.tsx +8 -4
- package/src/__tests__/layout/sidebar-groups.render.test.tsx +116 -0
- package/src/__tests__/layout/sidebar-menus.render.test.tsx +116 -0
- package/src/__tests__/layout/sidebar-plugin-nav.test.ts +1 -1
- package/src/__tests__/layout/sidebar-posts.test.ts +48 -40
- package/src/__tests__/layout/sidebar-submenu.render.test.tsx +35 -10
- package/src/__tests__/layout/topbar.render.test.tsx +464 -0
- package/src/__tests__/lib/api.test.ts +2 -0
- package/src/__tests__/lib/job-drawer.test.ts +493 -0
- package/src/__tests__/lib/job-view.test.ts +437 -0
- package/src/__tests__/lib/jobs-service.test.ts +98 -0
- package/src/__tests__/router/admin-route-order.test.ts +39 -0
- package/src/__tests__/styles/ai-gradient-contrast.test.ts +135 -0
- package/src/__tests__/styles/breakpoint-order.test.ts +68 -0
- package/src/__tests__/styles/responsive-layer.test.ts +367 -12
- package/src/__tests__/styles/tailwind-preset.test.ts +154 -0
- package/src/__tests__/styles/theme-token-scope.test.ts +10 -3
- package/src/__tests__/styles/tiers.test.ts +136 -0
- package/src/__tests__/styles/tokens-drift.test.ts +67 -6
- package/src/__tests__/views/admin-lazy-route.render.test.tsx +41 -1
- package/src/__tests__/views/ai-settings.render.test.tsx +4 -4
- package/src/__tests__/views/appearance-menus-route.render.test.tsx +58 -0
- package/src/__tests__/views/appearance-settings.render.test.tsx +10 -8
- package/src/__tests__/views/appearance-theme-provider.render.test.tsx +347 -0
- package/src/__tests__/views/dashboard.test.tsx +48 -0
- package/src/__tests__/views/document-edit-navigation.render.test.tsx +130 -0
- package/src/__tests__/views/document-edit-routes.render.test.tsx +130 -0
- package/src/__tests__/views/email-settings.render.test.tsx +2 -2
- package/src/__tests__/views/form-entries.render.test.tsx +22 -0
- package/src/__tests__/views/form-submissions.render.test.tsx +22 -0
- package/src/__tests__/views/forms-list.render.test.tsx +13 -0
- package/src/__tests__/views/general-settings.render.test.tsx +38 -11
- package/src/__tests__/views/jobs-drawer.render.test.tsx +1047 -0
- package/src/__tests__/views/jobs-list.render.test.tsx +850 -0
- package/src/__tests__/views/kpi-grid.test.ts +29 -0
- package/src/__tests__/views/mcp-connect.render.test.tsx +1 -0
- package/src/__tests__/views/post-section-editor.test.tsx +5 -1
- package/src/__tests__/views/script-tags.render.test.tsx +29 -0
- package/src/__tests__/views/security-settings.render.test.tsx +9 -10
- package/src/__tests__/views/seo-settings.render.test.tsx +7 -9
- package/src/__tests__/views/settings-save-bar.render.test.tsx +91 -0
- package/src/__tests__/views/settings-save-footer.render.test.tsx +25 -15
- package/src/__tests__/views/settings-tabs.render.test.tsx +371 -0
- package/src/__tests__/views/updates-tab.render.test.tsx +4 -3
- package/src/components/Breadcrumbs.tsx +7 -1
- package/src/components/CommandPalette.tsx +51 -3
- package/src/components/LocaleSwitcher.tsx +5 -6
- package/src/components/NotificationBell.tsx +74 -18
- package/src/components/OfflineStatus.tsx +1 -1
- package/src/components/PresenceIndicator.tsx +2 -1
- package/src/components/ReflowMeter.tsx +3 -3
- package/src/components/SEOPanel.tsx +2 -2
- package/src/components/ThemeProvider.tsx +122 -42
- package/src/components/TipTapEditor.tsx +3 -4
- package/src/components/forms/EmbedPanel.tsx +1 -1
- package/src/components/forms/FormEntryDetailPane.tsx +7 -1
- package/src/components/forms/ShortcodeGlossary.tsx +7 -1
- package/src/components/forms/primitives.tsx +37 -21
- package/src/components/pages/tag-badges.tsx +4 -3
- package/src/components/seo/PageAuditPanel.tsx +2 -2
- package/src/components/seo/SerpPreview.tsx +5 -5
- package/src/components/seo/primitives.tsx +2 -2
- package/src/components/ui/AuthorAvatar.tsx +11 -10
- package/src/components/ui/Badge.tsx +1 -1
- package/src/components/ui/Breadcrumb.tsx +109 -0
- package/src/components/ui/Button.tsx +40 -15
- package/src/components/ui/Card.tsx +35 -32
- package/src/components/ui/CardListControls.tsx +90 -0
- package/src/components/ui/CountChip.tsx +90 -0
- package/src/components/ui/FilterPill.tsx +5 -3
- package/src/components/ui/InspectorPane.tsx +21 -7
- package/src/components/ui/Modal.tsx +20 -6
- package/src/components/ui/PageHeader.tsx +63 -0
- package/src/components/ui/RowActions.tsx +7 -2
- package/src/components/ui/Segmented.tsx +97 -0
- package/src/components/ui/Toaster.tsx +139 -0
- package/src/components/ui/Toggle.tsx +18 -14
- package/src/components/ui/UnderlineTabs.tsx +373 -0
- package/src/components/ui/UnsavedBar.tsx +119 -0
- package/src/components/ui/admin-links.tsx +20 -0
- package/src/components/ui/card-table.ts +34 -0
- package/src/components/virtual-window.tsx +13 -2
- package/src/fields/RelationshipField.tsx +1 -1
- package/src/hooks/useAppearanceBootstrap.ts +9 -3
- package/src/index.ts +19 -0
- package/src/layout/Header.tsx +210 -135
- package/src/layout/Layout.tsx +61 -108
- package/src/layout/MobileTabBar.tsx +92 -0
- package/src/layout/Sidebar.tsx +555 -593
- package/src/layout/nav-config.tsx +425 -0
- package/src/layout/primitives/AdminShell.tsx +154 -6
- package/src/layout/topbar-classes.ts +8 -0
- package/src/layout/use-nav-items.ts +40 -0
- package/src/lib/api.ts +7 -1
- package/src/lib/appearance-api.ts +4 -0
- package/src/lib/job-drawer.ts +424 -0
- package/src/lib/job-view.ts +456 -0
- package/src/lib/jobs-service.ts +290 -0
- package/src/lib/sheet-focus.ts +71 -0
- package/src/lib/tiers.ts +43 -0
- package/src/router/admin-routes.ts +8 -0
- package/src/router/index.ts +9 -1
- package/src/styles/prenta-tokens.css +263 -82
- package/src/styles/responsive.css +460 -22
- package/src/styles/theme.css +177 -28
- package/src/views/ApiKeys.tsx +2 -2
- package/src/views/AppearanceChrome.tsx +74 -0
- package/src/views/CollectionList.tsx +264 -205
- package/src/views/Dashboard.tsx +26 -13
- package/src/views/DocumentEdit.tsx +111 -56
- package/src/views/FormDeliveries.tsx +30 -17
- package/src/views/FormEntries.tsx +28 -20
- package/src/views/FormSubmissions.tsx +32 -34
- package/src/views/Forms.tsx +55 -34
- package/src/views/FormsSettings.tsx +94 -0
- package/src/views/Jobs/BusyButton.tsx +56 -0
- package/src/views/Jobs/CancelJobDialog.tsx +127 -0
- package/src/views/Jobs/DrawerActivity.tsx +142 -0
- package/src/views/Jobs/DrawerOverview.tsx +139 -0
- package/src/views/Jobs/DrawerResults.tsx +155 -0
- package/src/views/Jobs/DrawerReview.tsx +464 -0
- package/src/views/Jobs/JobDrawer.tsx +556 -0
- package/src/views/Jobs/JobsTable.tsx +260 -0
- package/src/views/Jobs/WorkerStrip.tsx +144 -0
- package/src/views/Jobs/atoms.tsx +93 -0
- package/src/views/Jobs/focus.ts +56 -0
- package/src/views/Jobs/icons.tsx +111 -0
- package/src/views/Jobs/placeholders.tsx +66 -0
- package/src/views/Jobs/useJobDetail.ts +64 -0
- package/src/views/Jobs/useJobsFeed.ts +249 -0
- package/src/views/Jobs/useReviewItems.ts +187 -0
- package/src/views/Jobs.tsx +282 -105
- package/src/views/MediaBrowser.tsx +71 -21
- package/src/views/Pages/PagesListView.tsx +115 -39
- package/src/views/Posts/NewPostTypeInstructions.tsx +15 -10
- package/src/views/Posts/PostTypesView.tsx +28 -6
- package/src/views/Posts/PostsListView.tsx +130 -40
- package/src/views/SEO.tsx +23 -8
- package/src/views/ScriptTags.tsx +233 -153
- package/src/views/Settings.tsx +215 -256
- package/src/views/SpamFolder.tsx +32 -0
- package/src/views/Trash.tsx +12 -1
- package/src/views/Users.tsx +2 -2
- package/src/views/WidgetsPlaceholder.tsx +33 -0
- package/src/views/dashboard/kpi-grid.ts +38 -0
- package/src/views/lazy-views.tsx +12 -0
- package/src/views/page-editor/PageTab.tsx +2 -2
- package/src/views/page-editor/ReviewBar.tsx +1 -1
- package/src/views/page-editor/VersionHistoryPanel.tsx +1 -1
- package/src/views/seo/AuditTab.tsx +1 -1
- package/src/views/seo/OverviewTab.tsx +6 -5
- package/src/views/seo/ProposalsTab.tsx +1 -1
- package/src/views/settings/AISettingsTab.tsx +10 -6
- package/src/views/settings/AccessProtectionCard.tsx +1 -1
- package/src/views/settings/AdminThemeCard.tsx +228 -35
- package/src/views/settings/AppearanceTab.tsx +10 -6
- package/src/views/settings/BrandPreviewCard.tsx +1 -1
- package/src/views/settings/BrandVoiceCard.tsx +1 -1
- package/src/views/settings/EmailSettingsTab.tsx +11 -7
- package/src/views/settings/GeneralSettingsTab.tsx +10 -6
- package/src/views/settings/McpConnectTab.tsx +2 -1
- package/src/views/settings/ProviderConnectionCard.tsx +1 -1
- package/src/views/settings/SearchDataCard.tsx +1 -1
- package/src/views/settings/SecurityStatusCard.tsx +2 -2
- package/src/views/settings/SecurityTab.tsx +10 -6
- package/src/views/settings/SeoAutopilotCard.tsx +2 -2
- package/src/views/settings/SeoSettingsTab.tsx +13 -9
- package/src/views/settings/SiteIndexStatusCard.tsx +1 -1
- package/src/views/settings/SiteThemeCard.tsx +1 -1
- package/src/views/settings/UpdatesTab.tsx +107 -101
- package/src/views/settings/components.tsx +26 -62
- package/src/views/settings/useAppearanceSettings.ts +116 -25
- package/src/views/users/UserDetailDrawer.tsx +1 -1
- package/dist/components/ui/StatPill.d.ts +0 -18
- package/dist/components/ui/StatPill.d.ts.map +0 -1
- package/dist/components/ui/StatPill.js +0 -5
- package/dist/components/ui/StatPill.js.map +0 -1
- package/src/components/ui/StatPill.tsx +0 -37
|
@@ -0,0 +1,908 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
// @vitest-environment happy-dom
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { act, fireEvent, render, screen, waitFor, within } from '@testing-library/react';
|
|
5
|
+
/*
|
|
6
|
+
* The job drawer (P-18): a modal dialog with Overview, Review or Results,
|
|
7
|
+
* Activity and Details, on the real endpoints. The list's refresh is
|
|
8
|
+
* answered by the `fetch` stub; the drawer's reads and decisions by the
|
|
9
|
+
* `cmsApi` mock. Both read `server` below.
|
|
10
|
+
*/
|
|
11
|
+
const MIN = 60_000;
|
|
12
|
+
const server = {
|
|
13
|
+
jobs: [],
|
|
14
|
+
details: {},
|
|
15
|
+
events: {},
|
|
16
|
+
items: {},
|
|
17
|
+
reviewError: null,
|
|
18
|
+
decide: () => ({ error: 'not stubbed', status: 500 }),
|
|
19
|
+
approveAll: () => ({ error: 'not stubbed', status: 500 }),
|
|
20
|
+
calls: [],
|
|
21
|
+
};
|
|
22
|
+
function ok(data, extra = {}) {
|
|
23
|
+
return { data, body: { data, ...extra }, status: 200 };
|
|
24
|
+
}
|
|
25
|
+
function cmsApiStub(endpoint, init = {}) {
|
|
26
|
+
const method = (init.method ?? 'GET').toUpperCase();
|
|
27
|
+
const body = typeof init.body === 'string' ? JSON.parse(init.body) : undefined;
|
|
28
|
+
server.calls.push({ endpoint, method, body });
|
|
29
|
+
const url = new URL(endpoint, 'http://x');
|
|
30
|
+
const [, , jobId, part, itemId] = url.pathname.split('/');
|
|
31
|
+
const job = server.jobs.find((entry) => entry.id === jobId);
|
|
32
|
+
if (method === 'GET' && part === undefined) {
|
|
33
|
+
return job ? ok({ ...job, ...server.details[job.id] }) : { error: 'Job not found', status: 404 };
|
|
34
|
+
}
|
|
35
|
+
if (method === 'GET' && part === 'events')
|
|
36
|
+
return ok({ events: server.events[jobId] ?? [] });
|
|
37
|
+
if (method === 'GET' && part === 'review') {
|
|
38
|
+
if (server.reviewError)
|
|
39
|
+
return { error: server.reviewError.message, status: server.reviewError.status };
|
|
40
|
+
const limit = Number(url.searchParams.get('limit'));
|
|
41
|
+
const cursor = url.searchParams.get('cursor');
|
|
42
|
+
const all = [...(server.items[jobId] ?? [])].sort((a, b) => a.id.localeCompare(b.id));
|
|
43
|
+
const after = cursor ? all.filter((item) => item.id > cursor) : all;
|
|
44
|
+
const items = after.slice(0, limit);
|
|
45
|
+
const last = items.at(-1);
|
|
46
|
+
return ok({ items, nextCursor: items.length === limit && last ? last.id : null });
|
|
47
|
+
}
|
|
48
|
+
if (method === 'POST' && part === 'review' && itemId === 'approve-all')
|
|
49
|
+
return server.approveAll(jobId);
|
|
50
|
+
if (method === 'POST' && part === 'review')
|
|
51
|
+
return server.decide(jobId, itemId, body);
|
|
52
|
+
return { error: `unexpected ${method} ${endpoint}`, status: 404 };
|
|
53
|
+
}
|
|
54
|
+
vi.mock('../../lib/api.js', () => ({
|
|
55
|
+
getApiBase: () => '/api/cms',
|
|
56
|
+
cmsApi: vi.fn(async (endpoint, init) => cmsApiStub(endpoint, init)),
|
|
57
|
+
}));
|
|
58
|
+
const toast = vi.hoisted(() => Object.assign(vi.fn(), { error: vi.fn() }));
|
|
59
|
+
vi.mock('sonner', () => ({ toast }));
|
|
60
|
+
/** The list's refresh reads through fetch (it needs response headers). */
|
|
61
|
+
function fetchStub(url) {
|
|
62
|
+
const endpoint = url.replace(/^\/api\/cms/, '');
|
|
63
|
+
const data = endpoint.endsWith('/jobs/worker')
|
|
64
|
+
? { lastTickAt: null, lastTickMs: null, nextTickAt: null, budgetMs: 20000, schedules: [] }
|
|
65
|
+
: { jobs: server.jobs };
|
|
66
|
+
return Promise.resolve({
|
|
67
|
+
ok: true,
|
|
68
|
+
status: 200,
|
|
69
|
+
headers: { get: () => null },
|
|
70
|
+
json: async () => ({ data }),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
const { Jobs } = await import('../../views/Jobs.js');
|
|
74
|
+
function iso(offsetMs) {
|
|
75
|
+
return new Date(Date.now() + offsetMs).toISOString();
|
|
76
|
+
}
|
|
77
|
+
function batch(overrides) {
|
|
78
|
+
const { done = 19, total = 42, ...rest } = overrides;
|
|
79
|
+
return {
|
|
80
|
+
id: 'job_417',
|
|
81
|
+
type: 'batch-ai',
|
|
82
|
+
status: 'running',
|
|
83
|
+
title: 'Rewrite meta descriptions',
|
|
84
|
+
scope: '42 posts with a missing or over-length description',
|
|
85
|
+
runAt: iso(0),
|
|
86
|
+
attempts: 0,
|
|
87
|
+
maxAttempts: 3,
|
|
88
|
+
progress: { done, total, unit: 'documents', cursor: String(done), completed: done },
|
|
89
|
+
lastError: null,
|
|
90
|
+
explain: null,
|
|
91
|
+
summary: null,
|
|
92
|
+
lockedAt: iso(-5_000),
|
|
93
|
+
cancelRequestedAt: null,
|
|
94
|
+
cancelledBy: null,
|
|
95
|
+
createdBy: { id: 'u1', name: 'Brandon West', initials: 'BW' },
|
|
96
|
+
createdAt: iso(-19 * MIN),
|
|
97
|
+
updatedAt: iso(0),
|
|
98
|
+
finishedAt: null,
|
|
99
|
+
usage: { tokensIn: 11_500, tokensOut: 5_800 },
|
|
100
|
+
payload: {
|
|
101
|
+
operation: 'meta',
|
|
102
|
+
instruction: 'Plain, specific, under 155 characters.',
|
|
103
|
+
source: 'blog',
|
|
104
|
+
statusFilter: 'all',
|
|
105
|
+
documentIds: ['d1', 'd2', 'd3'],
|
|
106
|
+
review: 'review',
|
|
107
|
+
onApprove: 'draft',
|
|
108
|
+
},
|
|
109
|
+
review: { pending: 0, approved: 0, rejected: 0 },
|
|
110
|
+
...rest,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function item(id, overrides = {}) {
|
|
114
|
+
return {
|
|
115
|
+
id,
|
|
116
|
+
jobId: 'job_417',
|
|
117
|
+
documentId: `doc-${id}`,
|
|
118
|
+
documentTitle: `Post ${id}`,
|
|
119
|
+
url: `/blog/${id}`,
|
|
120
|
+
field: 'metaDescription',
|
|
121
|
+
before: '',
|
|
122
|
+
after: `Suggested description for ${id}.`,
|
|
123
|
+
editedAfter: null,
|
|
124
|
+
status: 'pending',
|
|
125
|
+
decidedBy: null,
|
|
126
|
+
decidedAt: null,
|
|
127
|
+
appliedRevisionId: null,
|
|
128
|
+
...overrides,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/** Items r01…rNN on a job, all pending. */
|
|
132
|
+
function pending(jobId, count) {
|
|
133
|
+
return Array.from({ length: count }, (_, index) => item(`r${String(index + 1).padStart(2, '0')}`, { jobId }));
|
|
134
|
+
}
|
|
135
|
+
function decided(entry, status) {
|
|
136
|
+
return {
|
|
137
|
+
...entry,
|
|
138
|
+
status,
|
|
139
|
+
decidedAt: iso(0),
|
|
140
|
+
decidedBy: { id: 'u1', name: 'Brandon West', initials: 'BW' },
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
beforeEach(() => {
|
|
144
|
+
vi.stubGlobal('fetch', vi.fn(fetchStub));
|
|
145
|
+
Object.assign(server, {
|
|
146
|
+
jobs: [],
|
|
147
|
+
details: {},
|
|
148
|
+
events: {},
|
|
149
|
+
items: {},
|
|
150
|
+
reviewError: null,
|
|
151
|
+
calls: [],
|
|
152
|
+
approveAll: () => ({ error: 'not stubbed', status: 500 }),
|
|
153
|
+
});
|
|
154
|
+
// By default a decision goes through: the change takes the new status.
|
|
155
|
+
server.decide = (jobId, itemId, body) => {
|
|
156
|
+
const list = server.items[jobId] ?? [];
|
|
157
|
+
const index = list.findIndex((entry) => entry.id === itemId);
|
|
158
|
+
const current = list[index];
|
|
159
|
+
const next = body.action === 'undo'
|
|
160
|
+
? { ...current, status: 'pending', decidedAt: null, decidedBy: null }
|
|
161
|
+
: decided(body.after ? { ...current, editedAfter: String(body.after) } : current, body.action === 'approve' ? 'approved' : 'rejected');
|
|
162
|
+
list[index] = next;
|
|
163
|
+
return ok(next, { documentStatus: body.onApprove === 'publish' ? 'PUBLISHED' : 'DRAFT' });
|
|
164
|
+
};
|
|
165
|
+
toast.mockClear();
|
|
166
|
+
toast.error.mockClear();
|
|
167
|
+
});
|
|
168
|
+
afterEach(() => {
|
|
169
|
+
vi.useRealTimers();
|
|
170
|
+
vi.unstubAllGlobals();
|
|
171
|
+
});
|
|
172
|
+
function rowButton(id) {
|
|
173
|
+
const button = document.getElementById(`job-open-${id}`);
|
|
174
|
+
if (!button)
|
|
175
|
+
throw new Error(`no row ${id}`);
|
|
176
|
+
return button;
|
|
177
|
+
}
|
|
178
|
+
async function renderJobs(onNavigate = () => { }) {
|
|
179
|
+
render(_jsx(Jobs, { onNavigate: onNavigate }));
|
|
180
|
+
await screen.findByRole('list', { name: 'Jobs' });
|
|
181
|
+
}
|
|
182
|
+
async function openJob(id, tab) {
|
|
183
|
+
const row = rowButton(id);
|
|
184
|
+
row.focus();
|
|
185
|
+
fireEvent.click(row);
|
|
186
|
+
const dialog = await screen.findByRole('dialog');
|
|
187
|
+
if (tab)
|
|
188
|
+
fireEvent.click(within(dialog).getByRole('tab', { name: new RegExp(`^${tab}`) }));
|
|
189
|
+
return dialog;
|
|
190
|
+
}
|
|
191
|
+
function card(id) {
|
|
192
|
+
const element = document.querySelector(`[data-review-item="${id}"]`);
|
|
193
|
+
if (!element)
|
|
194
|
+
throw new Error(`no change ${id}`);
|
|
195
|
+
return element;
|
|
196
|
+
}
|
|
197
|
+
/** Radix hides its positioned menu in happy-dom, so the items are queried with `hidden`. */
|
|
198
|
+
async function approveWith(entry, option) {
|
|
199
|
+
const trigger = within(entry).getByRole('button', { name: /^Approve/ });
|
|
200
|
+
fireEvent.keyDown(trigger, { key: 'Enter' });
|
|
201
|
+
await waitFor(() => expect(trigger.getAttribute('aria-expanded')).toBe('true'));
|
|
202
|
+
fireEvent.click(screen.getByRole('menuitem', { name: option, hidden: true }));
|
|
203
|
+
}
|
|
204
|
+
async function menuOptions(entry) {
|
|
205
|
+
const trigger = within(entry).getByRole('button', { name: /^Approve/ });
|
|
206
|
+
fireEvent.keyDown(trigger, { key: 'Enter' });
|
|
207
|
+
await waitFor(() => expect(trigger.getAttribute('aria-expanded')).toBe('true'));
|
|
208
|
+
const options = screen
|
|
209
|
+
.getAllByRole('menuitem', { hidden: true })
|
|
210
|
+
.map((option) => option.textContent ?? '');
|
|
211
|
+
fireEvent.keyDown(document.activeElement ?? document.body, { key: 'Escape' });
|
|
212
|
+
await waitFor(() => expect(trigger.getAttribute('aria-expanded')).toBe('false'));
|
|
213
|
+
return options;
|
|
214
|
+
}
|
|
215
|
+
const posts = (method, part) => server.calls.filter((call) => call.method === method && call.endpoint.includes(part));
|
|
216
|
+
/** A change's note (not a live region). */
|
|
217
|
+
function noteOf(entry) {
|
|
218
|
+
return entry.querySelector('[data-review-note]')?.textContent ?? '';
|
|
219
|
+
}
|
|
220
|
+
/** What the drawer's live region last announced. */
|
|
221
|
+
function announced() {
|
|
222
|
+
return document.querySelector('[data-job-drawer] [aria-live="polite"]')?.textContent ?? '';
|
|
223
|
+
}
|
|
224
|
+
/** Let "Approve all" act: it ignores activations for 600ms after Review opens. */
|
|
225
|
+
async function waitOutArming() {
|
|
226
|
+
await act(async () => {
|
|
227
|
+
await vi.advanceTimersByTimeAsync(700);
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
describe('Jobs drawer', () => {
|
|
231
|
+
it('opens from the row and returns focus to it when Escape closes it', async () => {
|
|
232
|
+
server.jobs = [batch({})];
|
|
233
|
+
await renderJobs();
|
|
234
|
+
const row = rowButton('job_417');
|
|
235
|
+
expect(row.getAttribute('aria-haspopup')).toBe('dialog');
|
|
236
|
+
const dialog = await openJob('job_417');
|
|
237
|
+
expect(dialog.getAttribute('aria-modal')).toBe('true');
|
|
238
|
+
expect(screen.getByRole('dialog', { name: 'Rewrite meta descriptions' })).toBe(dialog);
|
|
239
|
+
expect(row.closest('li')?.className).toContain('bg-acc-l');
|
|
240
|
+
fireEvent.keyDown(document.activeElement ?? document, { key: 'Escape' });
|
|
241
|
+
await waitFor(() => expect(screen.queryByRole('dialog')).toBeNull());
|
|
242
|
+
await waitFor(() => expect(document.activeElement).toBe(row));
|
|
243
|
+
expect(row.closest('li')?.className).not.toContain('bg-acc-l');
|
|
244
|
+
});
|
|
245
|
+
it('focuses the row when the opener never took focus (Safari clicks)', async () => {
|
|
246
|
+
server.jobs = [batch({})];
|
|
247
|
+
await renderJobs();
|
|
248
|
+
const row = rowButton('job_417');
|
|
249
|
+
document.activeElement?.blur();
|
|
250
|
+
fireEvent.click(row);
|
|
251
|
+
await screen.findByRole('dialog');
|
|
252
|
+
fireEvent.click(screen.getByRole('button', { name: 'Close' }));
|
|
253
|
+
await waitFor(() => expect(document.activeElement).toBe(row));
|
|
254
|
+
});
|
|
255
|
+
it('shows the overview: status sentence, progress, facts and instruction', async () => {
|
|
256
|
+
server.jobs = [batch({ review: { pending: 6, approved: 0, rejected: 0 } })];
|
|
257
|
+
server.items.job_417 = pending('job_417', 6);
|
|
258
|
+
await renderJobs();
|
|
259
|
+
const dialog = await openJob('job_417');
|
|
260
|
+
expect(within(dialog).getByRole('tab', { name: 'Overview' }).getAttribute('aria-selected')).toBe('true');
|
|
261
|
+
expect(dialog.textContent).toContain('Running slice 20 of 42 now. Each slice applies the operation to one document.');
|
|
262
|
+
expect(dialog.textContent).toContain('19 / 42 documents');
|
|
263
|
+
const facts = within(dialog).getByText('Attempts').closest('div');
|
|
264
|
+
expect(facts.textContent).toBe('Attempts1 of 3');
|
|
265
|
+
expect(within(dialog).getByText('17,300 tokens')).toBeTruthy();
|
|
266
|
+
expect(within(dialog).getByText('Rewrite meta descriptions', { selector: 'dd' })).toBeTruthy();
|
|
267
|
+
expect(dialog.textContent).toContain('“Plain, specific, under 155 characters.”');
|
|
268
|
+
expect(within(dialog).getByRole('button', { name: 'Review 6 changes' })).toBeTruthy();
|
|
269
|
+
});
|
|
270
|
+
it('shows the full error with Copy and the AI explanation on a failed job', async () => {
|
|
271
|
+
const stack = 'ProviderError: 429 Too Many Requests\n at callModel (lib/ai/provider.ts:88)';
|
|
272
|
+
server.jobs = [
|
|
273
|
+
batch({
|
|
274
|
+
id: 'job_415',
|
|
275
|
+
status: 'dead',
|
|
276
|
+
attempts: 3,
|
|
277
|
+
done: 7,
|
|
278
|
+
total: 30,
|
|
279
|
+
lockedAt: null,
|
|
280
|
+
lastError: 'ProviderError: 429 Too Many Requests',
|
|
281
|
+
explain: 'Your AI provider was rate-limiting requests.',
|
|
282
|
+
}),
|
|
283
|
+
];
|
|
284
|
+
server.details.job_415 = { lastError: stack };
|
|
285
|
+
const writeText = vi.fn(async () => { });
|
|
286
|
+
Object.defineProperty(navigator, 'clipboard', { value: { writeText }, configurable: true });
|
|
287
|
+
await renderJobs();
|
|
288
|
+
const dialog = await openJob('job_415');
|
|
289
|
+
expect(dialog.textContent).toContain('Stopped after 3 attempts at document 8.');
|
|
290
|
+
await waitFor(() => expect(dialog.querySelector('pre')?.textContent).toBe(stack));
|
|
291
|
+
expect(within(dialog).getByText('What happened')).toBeTruthy();
|
|
292
|
+
fireEvent.click(within(dialog).getByRole('button', { name: 'Copy' }));
|
|
293
|
+
await waitFor(() => expect(toast).toHaveBeenCalledWith('Error copied'));
|
|
294
|
+
expect(writeText).toHaveBeenCalledWith(stack);
|
|
295
|
+
});
|
|
296
|
+
it('counts from the API: badge and footer use review.pending, and changes load 20 at a time', async () => {
|
|
297
|
+
server.jobs = [batch({ review: { pending: 30, approved: 0, rejected: 0 } })];
|
|
298
|
+
server.items.job_417 = pending('job_417', 30);
|
|
299
|
+
await renderJobs();
|
|
300
|
+
const dialog = await openJob('job_417', 'Review');
|
|
301
|
+
await waitFor(() => expect(dialog.querySelectorAll('[data-review-item]')).toHaveLength(20));
|
|
302
|
+
expect(within(dialog).getByRole('tab', { name: /^Review/ }).textContent).toBe('Review30 waiting');
|
|
303
|
+
expect(within(dialog).getByRole('button', { name: 'Approve all 30' })).toBeTruthy();
|
|
304
|
+
expect(dialog.textContent).toContain('30 waiting · 0 approved · 0 rejected');
|
|
305
|
+
const reads = posts('GET', '/review');
|
|
306
|
+
expect(reads.at(-1)?.endpoint).toBe('/jobs/job_417/review?limit=20');
|
|
307
|
+
fireEvent.click(within(dialog).getByRole('button', { name: 'Show 10 more' }));
|
|
308
|
+
// Loading and drawing 10 more changes takes about 0.2s here, but CI runs
|
|
309
|
+
// the suite 9-13 times slower with coverage on, past waitFor's 1s default.
|
|
310
|
+
await waitFor(() => expect(dialog.querySelectorAll('[data-review-item]')).toHaveLength(30), {
|
|
311
|
+
timeout: 5_000,
|
|
312
|
+
});
|
|
313
|
+
expect(posts('GET', '/review').at(-1)?.endpoint).toBe('/jobs/job_417/review?limit=20&cursor=r20');
|
|
314
|
+
expect(within(dialog).queryByRole('button', { name: /^Show/ })).toBeNull();
|
|
315
|
+
await waitFor(() => expect(document.activeElement).toBe(card('r21')), { timeout: 5_000 });
|
|
316
|
+
});
|
|
317
|
+
it('keeps focus on the footer button as Review n changes becomes Approve all', async () => {
|
|
318
|
+
server.jobs = [batch({ review: { pending: 2, approved: 0, rejected: 0 } })];
|
|
319
|
+
server.items.job_417 = pending('job_417', 2);
|
|
320
|
+
await renderJobs();
|
|
321
|
+
const dialog = await openJob('job_417');
|
|
322
|
+
const footer = within(dialog).getByRole('button', { name: 'Review 2 changes' });
|
|
323
|
+
footer.focus();
|
|
324
|
+
fireEvent.click(footer);
|
|
325
|
+
await waitFor(() => expect(footer.textContent).toBe('Approve all 2'));
|
|
326
|
+
expect(document.activeElement).toBe(footer);
|
|
327
|
+
expect(within(dialog)
|
|
328
|
+
.getByRole('tab', { name: /^Review/ })
|
|
329
|
+
.getAttribute('aria-selected')).toBe('true');
|
|
330
|
+
});
|
|
331
|
+
it('offers Save as draft and Publish, and publishes when asked', async () => {
|
|
332
|
+
server.jobs = [batch({ review: { pending: 2, approved: 0, rejected: 0 } })];
|
|
333
|
+
server.items.job_417 = pending('job_417', 2);
|
|
334
|
+
await renderJobs();
|
|
335
|
+
await openJob('job_417', 'Review');
|
|
336
|
+
await waitFor(() => card('r01'));
|
|
337
|
+
expect(await menuOptions(card('r01'))).toEqual(['Save as draft', 'Publish']);
|
|
338
|
+
await approveWith(card('r01'), 'Publish');
|
|
339
|
+
const undo = await within(card('r01')).findByRole('button', { name: 'Undo' });
|
|
340
|
+
expect(posts('POST', '/review/r01').at(-1)?.body).toEqual({
|
|
341
|
+
action: 'approve',
|
|
342
|
+
onApprove: 'publish',
|
|
343
|
+
});
|
|
344
|
+
expect(within(card('r01')).getByText('Approved')).toBeTruthy();
|
|
345
|
+
await waitFor(() => expect(document.activeElement).toBe(undo));
|
|
346
|
+
});
|
|
347
|
+
it('reads Keep current status on a published document and leaves its change in review', async () => {
|
|
348
|
+
server.jobs = [batch({ review: { pending: 2, approved: 0, rejected: 0 } })];
|
|
349
|
+
server.items.job_417 = pending('job_417', 2);
|
|
350
|
+
server.decide = (_jobId, _itemId, body) => body.onApprove === 'publish'
|
|
351
|
+
? ok(decided(server.items.job_417[0], 'approved'), { documentStatus: 'PUBLISHED' })
|
|
352
|
+
: {
|
|
353
|
+
error: 'This document is published, so the change stays in review. Approve it with Publish to apply it live.',
|
|
354
|
+
status: 409,
|
|
355
|
+
};
|
|
356
|
+
await renderJobs();
|
|
357
|
+
await openJob('job_417', 'Review');
|
|
358
|
+
await waitFor(() => card('r01'));
|
|
359
|
+
await approveWith(card('r01'), 'Save as draft');
|
|
360
|
+
await waitFor(() => expect(noteOf(card('r01'))).toContain('so the change stays in review'));
|
|
361
|
+
// The note isn't a live region (it appears with its text); the drawer announces it.
|
|
362
|
+
expect(card('r01').querySelector('[role="status"], [role="alert"]')).toBeNull();
|
|
363
|
+
expect(announced()).toBe('Post r01: This document is published, so the change stays in review. Approve it with Publish to apply it live.');
|
|
364
|
+
expect(within(card('r01')).queryByText('Approved')).toBeNull();
|
|
365
|
+
expect(await menuOptions(card('r01'))).toEqual(['Keep current status', 'Publish']);
|
|
366
|
+
// Another document isn't known to be published.
|
|
367
|
+
expect(await menuOptions(card('r02'))).toEqual(['Save as draft', 'Publish']);
|
|
368
|
+
});
|
|
369
|
+
it('assumes published documents when the job only took published ones', async () => {
|
|
370
|
+
server.jobs = [
|
|
371
|
+
batch({
|
|
372
|
+
review: { pending: 1, approved: 0, rejected: 0 },
|
|
373
|
+
payload: { ...batch({}).payload, statusFilter: 'published' },
|
|
374
|
+
}),
|
|
375
|
+
];
|
|
376
|
+
server.items.job_417 = pending('job_417', 1);
|
|
377
|
+
await renderJobs();
|
|
378
|
+
await openJob('job_417', 'Review');
|
|
379
|
+
await waitFor(() => card('r01'));
|
|
380
|
+
expect(await menuOptions(card('r01'))).toEqual(['Keep current status', 'Publish']);
|
|
381
|
+
});
|
|
382
|
+
it.each([
|
|
383
|
+
[
|
|
384
|
+
'changed while saving',
|
|
385
|
+
'The document changed while this was being saved, so nothing was written. Try again.',
|
|
386
|
+
],
|
|
387
|
+
[
|
|
388
|
+
'the publish state moved',
|
|
389
|
+
'This document is published, so the change stays in review. Approve it with Publish to apply it live.',
|
|
390
|
+
],
|
|
391
|
+
])('explains a 409 when %s, and keeps the change pending', async (_, message) => {
|
|
392
|
+
server.jobs = [batch({ review: { pending: 1, approved: 0, rejected: 0 } })];
|
|
393
|
+
server.items.job_417 = pending('job_417', 1);
|
|
394
|
+
server.decide = () => ({ error: message, status: 409 });
|
|
395
|
+
await renderJobs();
|
|
396
|
+
await openJob('job_417', 'Review');
|
|
397
|
+
await waitFor(() => card('r01'));
|
|
398
|
+
await approveWith(card('r01'), 'Save as draft');
|
|
399
|
+
await waitFor(() => expect(card('r01').textContent).toContain(message));
|
|
400
|
+
expect(within(card('r01')).getByRole('button', { name: /^Approve/ })).toBeTruthy();
|
|
401
|
+
});
|
|
402
|
+
it('shows link changes as text, and explains a link whose page is gone', async () => {
|
|
403
|
+
const markup = 'Compare <b>plans</b> & pricing <img src=x onerror=alert(1)> on the page.';
|
|
404
|
+
server.jobs = [
|
|
405
|
+
batch({
|
|
406
|
+
id: 'job_413',
|
|
407
|
+
status: 'succeeded',
|
|
408
|
+
lockedAt: null,
|
|
409
|
+
title: 'Add internal links',
|
|
410
|
+
review: { pending: 1, approved: 0, rejected: 1 },
|
|
411
|
+
}),
|
|
412
|
+
];
|
|
413
|
+
server.items.job_413 = [
|
|
414
|
+
item('l1', {
|
|
415
|
+
jobId: 'job_413',
|
|
416
|
+
field: 'body',
|
|
417
|
+
before: 'Learn more about us before you get in touch.',
|
|
418
|
+
after: 'Learn more <a href="/about">about us</a> before you get in touch.',
|
|
419
|
+
}),
|
|
420
|
+
item('l2', {
|
|
421
|
+
jobId: 'job_413',
|
|
422
|
+
field: 'body',
|
|
423
|
+
status: 'rejected',
|
|
424
|
+
before: markup,
|
|
425
|
+
after: markup.replace('the page', '<a href="/platform">the page</a>'),
|
|
426
|
+
}),
|
|
427
|
+
];
|
|
428
|
+
server.decide = () => ({
|
|
429
|
+
error: "The page this links to, /about, isn't published at that address any more.",
|
|
430
|
+
status: 409,
|
|
431
|
+
});
|
|
432
|
+
await renderJobs();
|
|
433
|
+
await openJob('job_413', 'Review');
|
|
434
|
+
await waitFor(() => card('l2'));
|
|
435
|
+
expect(card('l2').querySelector('img, b, a[href="/platform"]')).toBeNull();
|
|
436
|
+
expect(card('l2').textContent).toContain('<b>plans</b>');
|
|
437
|
+
expect(card('l1').querySelector('mark')?.textContent).toBe('about us');
|
|
438
|
+
expect(card('l1').textContent).toContain('→ /about');
|
|
439
|
+
expect(within(card('l1')).queryByRole('button', { name: 'Edit' })).toBeNull();
|
|
440
|
+
await approveWith(card('l1'), 'Publish');
|
|
441
|
+
await waitFor(() => expect(noteOf(card('l1'))).toContain("isn't published at that address any more"));
|
|
442
|
+
expect(announced()).toContain("isn't published at that address any more");
|
|
443
|
+
});
|
|
444
|
+
it('shows the notice when undo leaves a document published', async () => {
|
|
445
|
+
const approvedItem = decided(item('r01'), 'approved');
|
|
446
|
+
server.jobs = [batch({ review: { pending: 0, approved: 1, rejected: 0 } })];
|
|
447
|
+
server.items.job_417 = [approvedItem];
|
|
448
|
+
const notice = 'The document stays published. Undo put back the previous text, and the page shows it now.';
|
|
449
|
+
server.decide = () => ok({ ...approvedItem, status: 'pending' }, { documentStatus: 'PUBLISHED', notice });
|
|
450
|
+
await renderJobs();
|
|
451
|
+
await openJob('job_417', 'Review');
|
|
452
|
+
await waitFor(() => card('r01'));
|
|
453
|
+
fireEvent.click(within(card('r01')).getByRole('button', { name: 'Undo' }));
|
|
454
|
+
await waitFor(() => expect(noteOf(card('r01'))).toContain(notice));
|
|
455
|
+
expect(announced()).toBe(`Post r01 is back in review. ${notice}`);
|
|
456
|
+
await waitFor(() => expect(document.activeElement).toBe(within(card('r01')).getByRole('button', { name: /^Approve/ })));
|
|
457
|
+
// The document is now known to be published.
|
|
458
|
+
expect(await menuOptions(card('r01'))).toEqual(['Keep current status', 'Publish']);
|
|
459
|
+
});
|
|
460
|
+
it('keeps an edit in progress across a poll and a tab switch, then approves the edit', async () => {
|
|
461
|
+
vi.useFakeTimers({ shouldAdvanceTime: true });
|
|
462
|
+
server.jobs = [batch({ review: { pending: 2, approved: 0, rejected: 0 } })];
|
|
463
|
+
server.items.job_417 = pending('job_417', 2);
|
|
464
|
+
await renderJobs();
|
|
465
|
+
const dialog = await openJob('job_417', 'Review');
|
|
466
|
+
await waitFor(() => card('r01'));
|
|
467
|
+
fireEvent.click(within(card('r01')).getByRole('button', { name: 'Edit' }));
|
|
468
|
+
const box = within(card('r01')).getByRole('textbox', { name: 'Edit suggestion for Post r01' });
|
|
469
|
+
await waitFor(() => expect(document.activeElement).toBe(box));
|
|
470
|
+
fireEvent.change(box, { target: { value: 'Edited by hand.' } });
|
|
471
|
+
// A slice adds a suggestion: the list refresh sees the job change and the changes are read again.
|
|
472
|
+
server.items.job_417.push(item('r03'));
|
|
473
|
+
server.jobs = [
|
|
474
|
+
batch({ updatedAt: iso(5_000), review: { pending: 3, approved: 0, rejected: 0 } }),
|
|
475
|
+
];
|
|
476
|
+
const before = posts('GET', '/review').length;
|
|
477
|
+
await act(async () => {
|
|
478
|
+
await vi.advanceTimersByTimeAsync(6_000);
|
|
479
|
+
});
|
|
480
|
+
await waitFor(() => expect(posts('GET', '/review').length).toBeGreaterThan(before));
|
|
481
|
+
await waitFor(() => card('r03'));
|
|
482
|
+
expect(within(card('r01')).getByRole('textbox').value).toBe('Edited by hand.');
|
|
483
|
+
fireEvent.click(within(dialog).getByRole('tab', { name: 'Overview' }));
|
|
484
|
+
fireEvent.click(within(dialog).getByRole('tab', { name: /^Review/ }));
|
|
485
|
+
await waitFor(() => card('r01'));
|
|
486
|
+
expect(within(card('r01')).getByRole('textbox').value).toBe('Edited by hand.');
|
|
487
|
+
await approveWith(card('r01'), 'Save as draft');
|
|
488
|
+
await within(card('r01')).findByRole('button', { name: 'Undo' });
|
|
489
|
+
expect(posts('POST', '/review/r01').at(-1)?.body).toEqual({
|
|
490
|
+
action: 'approve',
|
|
491
|
+
onApprove: 'draft',
|
|
492
|
+
after: 'Edited by hand.',
|
|
493
|
+
});
|
|
494
|
+
expect(card('r01').textContent).toContain('Edited by hand.');
|
|
495
|
+
});
|
|
496
|
+
it('re-reads the changes when one was decided elsewhere', async () => {
|
|
497
|
+
server.jobs = [batch({ review: { pending: 1, approved: 0, rejected: 0 } })];
|
|
498
|
+
server.items.job_417 = pending('job_417', 1);
|
|
499
|
+
let answer = () => { };
|
|
500
|
+
server.decide = () => new Promise((resolve) => {
|
|
501
|
+
answer = resolve;
|
|
502
|
+
});
|
|
503
|
+
await renderJobs();
|
|
504
|
+
await openJob('job_417', 'Review');
|
|
505
|
+
await waitFor(() => card('r01'));
|
|
506
|
+
await approveWith(card('r01'), 'Save as draft');
|
|
507
|
+
const trigger = within(card('r01')).getByRole('button', { name: /^Approve/ });
|
|
508
|
+
await waitFor(() => expect(document.activeElement).toBe(trigger));
|
|
509
|
+
server.items.job_417[0] = decided(server.items.job_417[0], 'rejected');
|
|
510
|
+
answer({ error: 'This change was already decided.', status: 409 });
|
|
511
|
+
await waitFor(() => expect(within(card('r01')).getByText('Rejected')).toBeTruthy());
|
|
512
|
+
expect(card('r01').textContent).toContain('This change was already decided.');
|
|
513
|
+
expect(announced()).toBe('Post r01: This change was already decided.');
|
|
514
|
+
// Approve went with the re-read; focus goes to the Undo that replaced it.
|
|
515
|
+
await waitFor(() => expect(document.activeElement).toBe(within(card('r01')).getByRole('button', { name: 'Undo' })));
|
|
516
|
+
});
|
|
517
|
+
it('puts focus on the panel when the change pressed is gone', async () => {
|
|
518
|
+
server.jobs = [batch({ review: { pending: 2, approved: 0, rejected: 0 } })];
|
|
519
|
+
server.items.job_417 = pending('job_417', 2);
|
|
520
|
+
server.decide = () => {
|
|
521
|
+
server.items.job_417 = server.items.job_417.filter((entry) => entry.id !== 'r01');
|
|
522
|
+
return { error: 'This change no longer exists.', status: 409 };
|
|
523
|
+
};
|
|
524
|
+
await renderJobs();
|
|
525
|
+
const dialog = await openJob('job_417', 'Review');
|
|
526
|
+
await waitFor(() => card('r01'));
|
|
527
|
+
const reject = within(card('r01')).getByRole('button', { name: 'Reject' });
|
|
528
|
+
reject.focus();
|
|
529
|
+
fireEvent.click(reject);
|
|
530
|
+
await waitFor(() => expect(dialog.querySelector('[data-review-item="r01"]')).toBeNull());
|
|
531
|
+
await waitFor(() => expect(document.activeElement).toBe(within(dialog).getByRole('tabpanel')));
|
|
532
|
+
});
|
|
533
|
+
it('leaves focus where the reviewer moved it while a decision was out', async () => {
|
|
534
|
+
server.jobs = [batch({ review: { pending: 2, approved: 0, rejected: 0 } })];
|
|
535
|
+
server.items.job_417 = pending('job_417', 2);
|
|
536
|
+
let answer = () => { };
|
|
537
|
+
server.decide = () => new Promise((resolve) => {
|
|
538
|
+
answer = resolve;
|
|
539
|
+
});
|
|
540
|
+
await renderJobs();
|
|
541
|
+
await openJob('job_417', 'Review');
|
|
542
|
+
await waitFor(() => card('r01'));
|
|
543
|
+
await approveWith(card('r01'), 'Publish');
|
|
544
|
+
// The closed menu hands focus back to Approve (on a timer), while the request is out.
|
|
545
|
+
const trigger = within(card('r01')).getByRole('button', { name: /^Approve/ });
|
|
546
|
+
await waitFor(() => expect(document.activeElement).toBe(trigger));
|
|
547
|
+
// Meanwhile they start editing the next change.
|
|
548
|
+
fireEvent.click(within(card('r02')).getByRole('button', { name: 'Edit' }));
|
|
549
|
+
const box = within(card('r02')).getByRole('textbox');
|
|
550
|
+
await waitFor(() => expect(document.activeElement).toBe(box));
|
|
551
|
+
answer(ok(decided(server.items.job_417[0], 'approved'), { documentStatus: 'PUBLISHED' }));
|
|
552
|
+
await within(card('r01')).findByRole('button', { name: 'Undo' });
|
|
553
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
554
|
+
expect(document.activeElement).toBe(box);
|
|
555
|
+
});
|
|
556
|
+
it('doesn’t approve all on a second click, a second Enter or a held key', async () => {
|
|
557
|
+
vi.useFakeTimers({ shouldAdvanceTime: true });
|
|
558
|
+
server.jobs = [batch({ review: { pending: 3, approved: 0, rejected: 0 } })];
|
|
559
|
+
server.items.job_417 = pending('job_417', 3);
|
|
560
|
+
server.approveAll = () => ok({ approved: 3, skipped: 0, remaining: 0 });
|
|
561
|
+
await renderJobs();
|
|
562
|
+
const dialog = await openJob('job_417');
|
|
563
|
+
const footer = within(dialog).getByRole('button', { name: 'Review 3 changes' });
|
|
564
|
+
footer.focus();
|
|
565
|
+
// A double-click: the first opens Review, the second lands on Approve all.
|
|
566
|
+
fireEvent.click(footer);
|
|
567
|
+
fireEvent.click(footer);
|
|
568
|
+
await waitFor(() => card('r01'));
|
|
569
|
+
expect(footer.textContent).toBe('Approve all 3');
|
|
570
|
+
// Enter again, quickly.
|
|
571
|
+
fireEvent.keyDown(footer, { key: 'Enter' });
|
|
572
|
+
fireEvent.click(footer);
|
|
573
|
+
expect(posts('POST', '/approve-all')).toHaveLength(0);
|
|
574
|
+
await waitOutArming();
|
|
575
|
+
// A held Enter: its repeats click too.
|
|
576
|
+
fireEvent.keyDown(footer, { key: 'Enter', repeat: true });
|
|
577
|
+
fireEvent.click(footer);
|
|
578
|
+
expect(posts('POST', '/approve-all')).toHaveLength(0);
|
|
579
|
+
fireEvent.keyUp(footer, { key: 'Enter' });
|
|
580
|
+
fireEvent.keyDown(footer, { key: 'Enter' });
|
|
581
|
+
fireEvent.click(footer);
|
|
582
|
+
await waitFor(() => expect(posts('POST', '/approve-all')).toHaveLength(1));
|
|
583
|
+
});
|
|
584
|
+
it('approves an edit still open as edited when approving all', async () => {
|
|
585
|
+
vi.useFakeTimers({ shouldAdvanceTime: true });
|
|
586
|
+
server.jobs = [batch({ review: { pending: 3, approved: 0, rejected: 0 } })];
|
|
587
|
+
server.items.job_417 = pending('job_417', 3);
|
|
588
|
+
server.approveAll = () => ok({ approved: 2, skipped: 0, remaining: 0 });
|
|
589
|
+
await renderJobs();
|
|
590
|
+
const dialog = await openJob('job_417', 'Review');
|
|
591
|
+
await waitFor(() => card('r02'));
|
|
592
|
+
fireEvent.click(within(card('r02')).getByRole('button', { name: 'Edit' }));
|
|
593
|
+
fireEvent.change(within(card('r02')).getByRole('textbox'), {
|
|
594
|
+
target: { value: 'Rewritten by hand.' },
|
|
595
|
+
});
|
|
596
|
+
await waitOutArming();
|
|
597
|
+
fireEvent.click(within(dialog).getByRole('button', { name: 'Approve all 3' }));
|
|
598
|
+
await waitFor(() => expect(toast).toHaveBeenCalledWith('3 changes approved · saved as drafts', undefined));
|
|
599
|
+
const order = server.calls.filter((call) => call.method === 'POST').map((call) => call.endpoint);
|
|
600
|
+
expect(order).toEqual(['/jobs/job_417/review/r02', '/jobs/job_417/review/approve-all']);
|
|
601
|
+
expect(posts('POST', '/review/r02')[0]?.body).toEqual({
|
|
602
|
+
action: 'approve',
|
|
603
|
+
after: 'Rewritten by hand.',
|
|
604
|
+
});
|
|
605
|
+
});
|
|
606
|
+
it.each([
|
|
607
|
+
['an emptied edit (400)', 400, 'The approved text is empty.'],
|
|
608
|
+
[
|
|
609
|
+
'changed while saving (409)',
|
|
610
|
+
409,
|
|
611
|
+
'The document changed while this was being saved, so nothing was written. Try again.',
|
|
612
|
+
],
|
|
613
|
+
['a server error', 500, 'Internal server error'],
|
|
614
|
+
])('stops approving all when an open edit is refused: %s', async (_, status, message) => {
|
|
615
|
+
vi.useFakeTimers({ shouldAdvanceTime: true });
|
|
616
|
+
server.jobs = [batch({ review: { pending: 3, approved: 0, rejected: 0 } })];
|
|
617
|
+
server.items.job_417 = pending('job_417', 3);
|
|
618
|
+
server.decide = () => ({ error: message, status });
|
|
619
|
+
server.approveAll = () => ok({ approved: 3, skipped: 0, remaining: 0 });
|
|
620
|
+
await renderJobs();
|
|
621
|
+
const dialog = await openJob('job_417', 'Review');
|
|
622
|
+
await waitFor(() => card('r02'));
|
|
623
|
+
fireEvent.click(within(card('r02')).getByRole('button', { name: 'Edit' }));
|
|
624
|
+
fireEvent.change(within(card('r02')).getByRole('textbox'), { target: { value: '' } });
|
|
625
|
+
await waitOutArming();
|
|
626
|
+
fireEvent.click(within(dialog).getByRole('button', { name: 'Approve all 3' }));
|
|
627
|
+
await waitFor(() => expect(toast.error).toHaveBeenCalledWith('Approve all stopped', {
|
|
628
|
+
description: '1 edited change couldn’t be approved, so nothing else was, and the AI’s text doesn’t replace an edit. Each change says why.',
|
|
629
|
+
}));
|
|
630
|
+
// The server's approve-all never ran, so it couldn't apply the AI's text to r02.
|
|
631
|
+
expect(posts('POST', '/approve-all')).toHaveLength(0);
|
|
632
|
+
expect(announced()).toContain(`Post r02: ${message}`);
|
|
633
|
+
expect(noteOf(card('r02'))).toContain(message);
|
|
634
|
+
expect(within(card('r02')).getByRole('textbox')).toBeTruthy();
|
|
635
|
+
expect(within(card('r02')).queryByText('Approved')).toBeNull();
|
|
636
|
+
});
|
|
637
|
+
it('locks every change while approving all', async () => {
|
|
638
|
+
vi.useFakeTimers({ shouldAdvanceTime: true });
|
|
639
|
+
server.jobs = [batch({ review: { pending: 3, approved: 0, rejected: 0 } })];
|
|
640
|
+
server.items.job_417 = pending('job_417', 3);
|
|
641
|
+
const approveEdit = server.decide;
|
|
642
|
+
let editAnswered = () => { };
|
|
643
|
+
server.decide = (jobId, itemId, body) => new Promise((resolve) => {
|
|
644
|
+
editAnswered = () => resolve(approveEdit(jobId, itemId, body));
|
|
645
|
+
});
|
|
646
|
+
let bulkAnswered = () => { };
|
|
647
|
+
server.approveAll = () => new Promise((resolve) => {
|
|
648
|
+
bulkAnswered = resolve;
|
|
649
|
+
});
|
|
650
|
+
await renderJobs();
|
|
651
|
+
const dialog = await openJob('job_417', 'Review');
|
|
652
|
+
await waitFor(() => card('r02'));
|
|
653
|
+
fireEvent.click(within(card('r02')).getByRole('button', { name: 'Edit' }));
|
|
654
|
+
const box = within(card('r02')).getByRole('textbox');
|
|
655
|
+
fireEvent.change(box, { target: { value: 'Mine.' } });
|
|
656
|
+
await waitOutArming();
|
|
657
|
+
fireEvent.click(within(dialog).getByRole('button', { name: 'Approve all 3' }));
|
|
658
|
+
const locked = () => {
|
|
659
|
+
// The open edit is read-only, so what's sent is what's shown.
|
|
660
|
+
expect(box.readOnly).toBe(true);
|
|
661
|
+
fireEvent.change(box, { target: { value: 'Mine, changed.' } });
|
|
662
|
+
expect(box.value).toBe('Mine.');
|
|
663
|
+
// Another change can't be edited or decided.
|
|
664
|
+
const edit = within(card('r01')).getByRole('button', { name: 'Edit' });
|
|
665
|
+
expect(edit.getAttribute('aria-disabled')).toBe('true');
|
|
666
|
+
fireEvent.click(edit);
|
|
667
|
+
expect(within(card('r01')).queryByRole('textbox')).toBeNull();
|
|
668
|
+
fireEvent.click(within(card('r01')).getByRole('button', { name: 'Reject' }));
|
|
669
|
+
expect(posts('POST', '/review/r01')).toHaveLength(0);
|
|
670
|
+
};
|
|
671
|
+
// While the edit is being approved…
|
|
672
|
+
await waitFor(() => expect(posts('POST', '/review/r02')).toHaveLength(1));
|
|
673
|
+
locked();
|
|
674
|
+
// …and while the server's approve-all runs.
|
|
675
|
+
act(() => editAnswered());
|
|
676
|
+
await waitFor(() => expect(posts('POST', '/approve-all')).toHaveLength(1));
|
|
677
|
+
expect(posts('POST', '/review/r02')[0]?.body).toEqual({ action: 'approve', after: 'Mine.' });
|
|
678
|
+
const edit = within(card('r01')).getByRole('button', { name: 'Edit' });
|
|
679
|
+
expect(edit.getAttribute('aria-disabled')).toBe('true');
|
|
680
|
+
fireEvent.click(edit);
|
|
681
|
+
expect(within(card('r01')).queryByRole('textbox')).toBeNull();
|
|
682
|
+
bulkAnswered(ok({ approved: 2, skipped: 0, remaining: 0 }));
|
|
683
|
+
await waitFor(() => expect(edit.getAttribute('aria-disabled')).toBeNull());
|
|
684
|
+
fireEvent.click(edit);
|
|
685
|
+
expect(within(card('r01')).getByRole('textbox')).toBeTruthy();
|
|
686
|
+
});
|
|
687
|
+
it('still approves all when an open edit stays in review on a published document', async () => {
|
|
688
|
+
vi.useFakeTimers({ shouldAdvanceTime: true });
|
|
689
|
+
server.jobs = [batch({ review: { pending: 3, approved: 0, rejected: 0 } })];
|
|
690
|
+
server.items.job_417 = pending('job_417', 3);
|
|
691
|
+
const published = 'This document is published, so the change stays in review. Approve it with Publish to apply it live.';
|
|
692
|
+
server.decide = () => ({ error: published, status: 409 });
|
|
693
|
+
// The server skips that change too: it's published and the job saves drafts.
|
|
694
|
+
server.approveAll = () => ok({ approved: 2, skipped: 1, remaining: 1 });
|
|
695
|
+
await renderJobs();
|
|
696
|
+
const dialog = await openJob('job_417', 'Review');
|
|
697
|
+
await waitFor(() => card('r02'));
|
|
698
|
+
fireEvent.click(within(card('r02')).getByRole('button', { name: 'Edit' }));
|
|
699
|
+
await waitOutArming();
|
|
700
|
+
fireEvent.click(within(dialog).getByRole('button', { name: 'Approve all 3' }));
|
|
701
|
+
await waitFor(() => expect(posts('POST', '/approve-all')).toHaveLength(1));
|
|
702
|
+
await waitFor(() => expect(announced()).toContain(`Post r02: ${published}`));
|
|
703
|
+
});
|
|
704
|
+
it('approves all, asking again while changes are left that it didn’t get to', async () => {
|
|
705
|
+
vi.useFakeTimers({ shouldAdvanceTime: true });
|
|
706
|
+
server.jobs = [
|
|
707
|
+
batch({
|
|
708
|
+
status: 'succeeded',
|
|
709
|
+
lockedAt: null,
|
|
710
|
+
review: { pending: 45, approved: 0, rejected: 0 },
|
|
711
|
+
}),
|
|
712
|
+
];
|
|
713
|
+
server.items.job_417 = pending('job_417', 45);
|
|
714
|
+
const rounds = [
|
|
715
|
+
{ approved: 20, skipped: 2, remaining: 25 },
|
|
716
|
+
{ approved: 20, skipped: 3, remaining: 5 },
|
|
717
|
+
{ approved: 2, skipped: 3, remaining: 3 },
|
|
718
|
+
];
|
|
719
|
+
server.approveAll = () => ok(rounds.shift() ?? { approved: 0, skipped: 0, remaining: 0 });
|
|
720
|
+
await renderJobs();
|
|
721
|
+
const dialog = await openJob('job_417', 'Review');
|
|
722
|
+
await waitFor(() => card('r01'));
|
|
723
|
+
await waitOutArming();
|
|
724
|
+
fireEvent.click(within(dialog).getByRole('button', { name: 'Approve all 45' }));
|
|
725
|
+
await waitFor(() => expect(toast).toHaveBeenCalledWith('42 changes approved · saved as drafts', {
|
|
726
|
+
description: '3 changes stayed in review. Changes to published documents need Publish, so approve those one at a time.',
|
|
727
|
+
}));
|
|
728
|
+
expect(posts('POST', '/approve-all')).toHaveLength(3);
|
|
729
|
+
});
|
|
730
|
+
it('hides Review from someone who can’t read the suggested changes', async () => {
|
|
731
|
+
vi.useFakeTimers({ shouldAdvanceTime: true });
|
|
732
|
+
server.jobs = [
|
|
733
|
+
batch({
|
|
734
|
+
status: 'succeeded',
|
|
735
|
+
lockedAt: null,
|
|
736
|
+
review: { pending: 6, approved: 0, rejected: 0 },
|
|
737
|
+
}),
|
|
738
|
+
];
|
|
739
|
+
server.items.job_417 = pending('job_417', 6);
|
|
740
|
+
server.reviewError = {
|
|
741
|
+
status: 403,
|
|
742
|
+
message: 'Only people who can edit content can read suggested changes',
|
|
743
|
+
};
|
|
744
|
+
await renderJobs();
|
|
745
|
+
// The list's Review button asks for the Review tab.
|
|
746
|
+
fireEvent.click(within(rowButton('job_417').closest('li')).getByRole('button', { name: /Review/ }));
|
|
747
|
+
const dialog = await screen.findByRole('dialog');
|
|
748
|
+
await waitFor(() => expect(within(dialog)
|
|
749
|
+
.getAllByRole('tab')
|
|
750
|
+
.map((tab) => tab.textContent)).toEqual(['Overview', 'Activity', 'Details']));
|
|
751
|
+
expect(within(dialog).getByRole('tab', { name: 'Overview' }).getAttribute('aria-selected')).toBe('true');
|
|
752
|
+
expect(within(dialog).queryByRole('button', { name: /Review \d/ })).toBeNull();
|
|
753
|
+
// The job changing doesn't ask again: the 403 stands while the drawer is open.
|
|
754
|
+
const asked = posts('GET', '/review').length;
|
|
755
|
+
server.jobs = [
|
|
756
|
+
batch({
|
|
757
|
+
status: 'succeeded',
|
|
758
|
+
lockedAt: null,
|
|
759
|
+
updatedAt: iso(5_000),
|
|
760
|
+
review: { pending: 7, approved: 0, rejected: 0 },
|
|
761
|
+
}),
|
|
762
|
+
];
|
|
763
|
+
await act(async () => {
|
|
764
|
+
await vi.advanceTimersByTimeAsync(6_000);
|
|
765
|
+
});
|
|
766
|
+
await waitFor(() => expect(dialog.textContent).toContain('Activity'));
|
|
767
|
+
expect(posts('GET', '/review')).toHaveLength(asked);
|
|
768
|
+
});
|
|
769
|
+
it('shows an SEO scan’s results, linking each issue to its screen', async () => {
|
|
770
|
+
const onNavigate = vi.fn();
|
|
771
|
+
const seo = batch({
|
|
772
|
+
id: 'job_414',
|
|
773
|
+
type: 'seo-scan',
|
|
774
|
+
status: 'succeeded',
|
|
775
|
+
title: 'SEO scan',
|
|
776
|
+
scope: 'All indexable pages',
|
|
777
|
+
lockedAt: null,
|
|
778
|
+
createdBy: { kind: 'cron', schedule: 'weekly-seo' },
|
|
779
|
+
payload: { trigger: 'cron' },
|
|
780
|
+
summary: 'Score is up 4 points from last week.',
|
|
781
|
+
done: 340,
|
|
782
|
+
total: 340,
|
|
783
|
+
finishedAt: iso(-60 * MIN),
|
|
784
|
+
});
|
|
785
|
+
delete seo.review;
|
|
786
|
+
server.jobs = [seo];
|
|
787
|
+
server.details.job_414 = {
|
|
788
|
+
results: {
|
|
789
|
+
score: 78,
|
|
790
|
+
prevScore: 74,
|
|
791
|
+
found: 23,
|
|
792
|
+
resolved: 11,
|
|
793
|
+
categories: [
|
|
794
|
+
{
|
|
795
|
+
key: 'missing-meta',
|
|
796
|
+
label: 'Missing meta description',
|
|
797
|
+
count: 5,
|
|
798
|
+
href: '/seo/content',
|
|
799
|
+
},
|
|
800
|
+
{ key: 'bad', label: 'Somewhere else', count: 1, href: 'javascript:alert(1)' },
|
|
801
|
+
],
|
|
802
|
+
digest: { sentAt: iso(-60 * MIN), recipients: 3 },
|
|
803
|
+
},
|
|
804
|
+
};
|
|
805
|
+
await renderJobs(onNavigate);
|
|
806
|
+
const dialog = await openJob('job_414', 'Results');
|
|
807
|
+
await within(dialog).findByText('Site score');
|
|
808
|
+
expect(within(dialog).getByText('+4')).toBeTruthy();
|
|
809
|
+
expect(dialog.textContent).toContain('Fixed since last scan11');
|
|
810
|
+
expect(within(dialog).getByText('Score is up 4 points from last week.')).toBeTruthy();
|
|
811
|
+
const link = within(dialog).getByRole('link', { name: /Missing meta description/ });
|
|
812
|
+
expect(link.getAttribute('href')).toBe('/seo/content');
|
|
813
|
+
fireEvent.click(link);
|
|
814
|
+
expect(onNavigate).toHaveBeenCalledWith('/seo/content');
|
|
815
|
+
expect(within(dialog).queryByRole('link', { name: /Somewhere else/ })).toBeNull();
|
|
816
|
+
expect(dialog.textContent).toContain('SEO digest: Sent to 3 people');
|
|
817
|
+
});
|
|
818
|
+
it('lists activity newest first, folding older finished slices', async () => {
|
|
819
|
+
server.jobs = [batch({})];
|
|
820
|
+
const slice = (minutes, n) => [
|
|
821
|
+
{
|
|
822
|
+
at: iso(-minutes * MIN),
|
|
823
|
+
kind: 'yielded',
|
|
824
|
+
message: 'Slice finished',
|
|
825
|
+
detail: `Processed ${n} of 42`,
|
|
826
|
+
durationMs: 11_800,
|
|
827
|
+
tokens: 920,
|
|
828
|
+
},
|
|
829
|
+
{ at: iso(-minutes * MIN - 12_000), kind: 'claimed', message: 'Slice started' },
|
|
830
|
+
];
|
|
831
|
+
server.events.job_417 = [
|
|
832
|
+
{ at: iso(0), kind: 'claimed', message: 'Slice started' },
|
|
833
|
+
...slice(1, 18),
|
|
834
|
+
...slice(2, 17),
|
|
835
|
+
...slice(3, 16),
|
|
836
|
+
...slice(4, 15),
|
|
837
|
+
{ at: iso(-19 * MIN), kind: 'queued', message: 'Queued' },
|
|
838
|
+
];
|
|
839
|
+
await renderJobs();
|
|
840
|
+
const dialog = await openJob('job_417', 'Activity');
|
|
841
|
+
const list = await within(dialog).findByRole('list');
|
|
842
|
+
const titles = within(list)
|
|
843
|
+
.getAllByRole('listitem')
|
|
844
|
+
.map((entry) => entry.querySelector('b, button')?.textContent);
|
|
845
|
+
expect(titles).toEqual([
|
|
846
|
+
'Slice started',
|
|
847
|
+
'Slice finished',
|
|
848
|
+
'Slice finished',
|
|
849
|
+
'2 earlier slices',
|
|
850
|
+
'Slice started',
|
|
851
|
+
'Queued',
|
|
852
|
+
]);
|
|
853
|
+
expect(list.textContent).toContain('Processed 18 of 42 · 11.8s · 920 tokens · yielded');
|
|
854
|
+
expect(list.textContent).toContain('Brandon West started it');
|
|
855
|
+
const fold = within(list).getByRole('button', { name: '2 earlier slices' });
|
|
856
|
+
fireEvent.click(fold);
|
|
857
|
+
expect(fold.getAttribute('aria-expanded')).toBe('true');
|
|
858
|
+
expect(list.textContent).toContain('Processed 15 of 42');
|
|
859
|
+
});
|
|
860
|
+
it('keeps an open fold open, with focus on it, as the job’s events grow', async () => {
|
|
861
|
+
vi.useFakeTimers({ shouldAdvanceTime: true });
|
|
862
|
+
server.jobs = [batch({})];
|
|
863
|
+
const finished = (minutes, n) => ({
|
|
864
|
+
at: iso(-minutes * MIN),
|
|
865
|
+
kind: 'yielded',
|
|
866
|
+
message: 'Slice finished',
|
|
867
|
+
detail: `Processed ${n} of 42`,
|
|
868
|
+
});
|
|
869
|
+
server.events.job_417 = [
|
|
870
|
+
{ at: iso(0), kind: 'claimed', message: 'Slice started' },
|
|
871
|
+
finished(1, 18),
|
|
872
|
+
finished(2, 17),
|
|
873
|
+
finished(3, 16),
|
|
874
|
+
finished(4, 15),
|
|
875
|
+
{ at: iso(-19 * MIN), kind: 'queued', message: 'Queued' },
|
|
876
|
+
];
|
|
877
|
+
await renderJobs();
|
|
878
|
+
const dialog = await openJob('job_417', 'Activity');
|
|
879
|
+
const fold = await within(dialog).findByRole('button', { name: '2 earlier slices' });
|
|
880
|
+
fireEvent.click(fold);
|
|
881
|
+
fold.focus();
|
|
882
|
+
// A slice finishes: the newest event moves the fold's newer edge.
|
|
883
|
+
server.events.job_417 = [
|
|
884
|
+
{ at: iso(5_000), kind: 'claimed', message: 'Slice started' },
|
|
885
|
+
finished(-0.05, 19),
|
|
886
|
+
...server.events.job_417.slice(1),
|
|
887
|
+
];
|
|
888
|
+
server.jobs = [batch({ updatedAt: iso(5_000), done: 20 })];
|
|
889
|
+
await act(async () => {
|
|
890
|
+
await vi.advanceTimersByTimeAsync(6_000);
|
|
891
|
+
});
|
|
892
|
+
await waitFor(() => expect(dialog.textContent).toContain('Processed 19 of 42'));
|
|
893
|
+
const after = within(dialog).getByRole('button', { name: '3 earlier slices' });
|
|
894
|
+
expect(after).toBe(fold);
|
|
895
|
+
expect(after.getAttribute('aria-expanded')).toBe('true');
|
|
896
|
+
expect(document.activeElement).toBe(fold);
|
|
897
|
+
});
|
|
898
|
+
it('shows the raw row, with the document ids counted', async () => {
|
|
899
|
+
server.jobs = [batch({})];
|
|
900
|
+
await renderJobs();
|
|
901
|
+
const dialog = await openJob('job_417', 'Details');
|
|
902
|
+
expect(dialog.textContent).toContain('The raw job row, for developers. Read only.');
|
|
903
|
+
const raw = within(dialog).getByLabelText('Job row as JSON').textContent ?? '';
|
|
904
|
+
expect(raw).toContain('"documentIds": "[3 ids]"');
|
|
905
|
+
expect(raw).toContain('"id": "job_417"');
|
|
906
|
+
});
|
|
907
|
+
});
|
|
908
|
+
//# sourceMappingURL=jobs-drawer.render.test.js.map
|