qdadm 0.13.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.
Files changed (82) hide show
  1. package/CHANGELOG.md +270 -0
  2. package/LICENSE +21 -0
  3. package/README.md +166 -0
  4. package/package.json +48 -0
  5. package/src/assets/logo.svg +6 -0
  6. package/src/components/BoolCell.vue +28 -0
  7. package/src/components/dialogs/BulkStatusDialog.vue +43 -0
  8. package/src/components/dialogs/MultiStepDialog.vue +321 -0
  9. package/src/components/dialogs/SimpleDialog.vue +108 -0
  10. package/src/components/dialogs/UnsavedChangesDialog.vue +87 -0
  11. package/src/components/display/CardsGrid.vue +155 -0
  12. package/src/components/display/CopyableId.vue +92 -0
  13. package/src/components/display/EmptyState.vue +114 -0
  14. package/src/components/display/IntensityBar.vue +171 -0
  15. package/src/components/display/RichCardsGrid.vue +220 -0
  16. package/src/components/editors/JsonEditorFoldable.vue +467 -0
  17. package/src/components/editors/JsonStructuredField.vue +218 -0
  18. package/src/components/editors/JsonViewer.vue +91 -0
  19. package/src/components/editors/KeyValueEditor.vue +314 -0
  20. package/src/components/editors/LanguageEditor.vue +245 -0
  21. package/src/components/editors/ScopeEditor.vue +341 -0
  22. package/src/components/editors/VanillaJsonEditor.vue +185 -0
  23. package/src/components/forms/FormActions.vue +104 -0
  24. package/src/components/forms/FormField.vue +64 -0
  25. package/src/components/forms/FormTab.vue +217 -0
  26. package/src/components/forms/FormTabs.vue +108 -0
  27. package/src/components/index.js +44 -0
  28. package/src/components/layout/AppLayout.vue +430 -0
  29. package/src/components/layout/Breadcrumb.vue +106 -0
  30. package/src/components/layout/PageHeader.vue +75 -0
  31. package/src/components/layout/PageLayout.vue +93 -0
  32. package/src/components/lists/ActionButtons.vue +41 -0
  33. package/src/components/lists/ActionColumn.vue +37 -0
  34. package/src/components/lists/FilterBar.vue +53 -0
  35. package/src/components/lists/ListPage.vue +319 -0
  36. package/src/composables/index.js +19 -0
  37. package/src/composables/useApp.js +43 -0
  38. package/src/composables/useAuth.js +49 -0
  39. package/src/composables/useBareForm.js +143 -0
  40. package/src/composables/useBreadcrumb.js +221 -0
  41. package/src/composables/useDirtyState.js +103 -0
  42. package/src/composables/useEntityTitle.js +121 -0
  43. package/src/composables/useForm.js +254 -0
  44. package/src/composables/useGuardStore.js +37 -0
  45. package/src/composables/useJsonSyntax.js +101 -0
  46. package/src/composables/useListPageBuilder.js +1176 -0
  47. package/src/composables/useNavigation.js +89 -0
  48. package/src/composables/usePageBuilder.js +334 -0
  49. package/src/composables/useStatus.js +146 -0
  50. package/src/composables/useSubEditor.js +165 -0
  51. package/src/composables/useTabSync.js +110 -0
  52. package/src/composables/useUnsavedChangesGuard.js +122 -0
  53. package/src/entity/EntityManager.js +540 -0
  54. package/src/entity/index.js +11 -0
  55. package/src/entity/storage/ApiStorage.js +146 -0
  56. package/src/entity/storage/LocalStorage.js +220 -0
  57. package/src/entity/storage/MemoryStorage.js +201 -0
  58. package/src/entity/storage/index.js +10 -0
  59. package/src/index.js +29 -0
  60. package/src/kernel/Kernel.js +234 -0
  61. package/src/kernel/index.js +7 -0
  62. package/src/module/index.js +16 -0
  63. package/src/module/moduleRegistry.js +222 -0
  64. package/src/orchestrator/Orchestrator.js +141 -0
  65. package/src/orchestrator/index.js +8 -0
  66. package/src/orchestrator/useOrchestrator.js +61 -0
  67. package/src/plugin.js +142 -0
  68. package/src/styles/_alerts.css +48 -0
  69. package/src/styles/_code.css +33 -0
  70. package/src/styles/_dialogs.css +17 -0
  71. package/src/styles/_markdown.css +82 -0
  72. package/src/styles/_show-pages.css +84 -0
  73. package/src/styles/index.css +16 -0
  74. package/src/styles/main.css +845 -0
  75. package/src/styles/theme/components.css +286 -0
  76. package/src/styles/theme/index.css +10 -0
  77. package/src/styles/theme/tokens.css +125 -0
  78. package/src/styles/theme/utilities.css +172 -0
  79. package/src/utils/debugInjector.js +261 -0
  80. package/src/utils/formatters.js +165 -0
  81. package/src/utils/index.js +35 -0
  82. package/src/utils/transformers.js +105 -0
@@ -0,0 +1,143 @@
1
+ import { ref, computed, provide, onUnmounted } from 'vue'
2
+ import { useRoute, useRouter } from 'vue-router'
3
+ import { useToast } from 'primevue/usetoast'
4
+ import { useDirtyState } from './useDirtyState'
5
+ import { useUnsavedChangesGuard } from './useUnsavedChangesGuard'
6
+ import { useBreadcrumb } from './useBreadcrumb'
7
+ import { registerGuardDialog, unregisterGuardDialog } from './useGuardStore'
8
+
9
+ /**
10
+ * Base form composable providing common form functionality.
11
+ *
12
+ * Usage:
13
+ * const {
14
+ * // State
15
+ * loading, saving, dirty, isEdit, entityId,
16
+ * // Dirty tracking
17
+ * isFieldDirty, takeSnapshot, checkDirty,
18
+ * // Helpers
19
+ * router, route, toast, cancel
20
+ * } = useBareForm({
21
+ * getState: () => ({ form: form.value }),
22
+ * routePrefix: 'agents', // for cancel() navigation
23
+ * guard: true, // enable unsaved changes modal
24
+ * onGuardSave: () => save() // optional save callback for guard modal
25
+ * })
26
+ *
27
+ * This composable provides:
28
+ * - Dirty state tracking (form-level and field-level)
29
+ * - isFieldDirty for child FormField components via inject
30
+ * - Unsaved changes guard modal
31
+ * - Common state refs (loading, saving)
32
+ * - Common computed (isEdit, entityId)
33
+ * - Navigation helpers (cancel)
34
+ * - Access to router, route, toast
35
+ *
36
+ * @param {Object} options
37
+ * @param {Function} options.getState - Function returning current form state for comparison
38
+ * @param {string} options.routePrefix - Route name for cancel navigation (default: '')
39
+ * @param {boolean} options.guard - Enable unsaved changes guard (default: true)
40
+ * @param {Function} options.onGuardSave - Callback for save button in guard modal
41
+ * @param {Function} options.getId - Custom function to extract entity ID from route (optional)
42
+ * @param {Ref|Object} options.entity - Entity data for dynamic breadcrumb labels (optional)
43
+ * @param {Function} options.breadcrumbLabel - Callback (entity) => string for custom breadcrumb label (optional)
44
+ */
45
+ export function useBareForm(options = {}) {
46
+ const {
47
+ getState,
48
+ routePrefix = '',
49
+ guard = true,
50
+ onGuardSave = null,
51
+ getId = null,
52
+ entity = null,
53
+ breadcrumbLabel = null
54
+ } = options
55
+
56
+ if (!getState || typeof getState !== 'function') {
57
+ throw new Error('useBareForm requires a getState function')
58
+ }
59
+
60
+ // Router, route, toast - common dependencies
61
+ const router = useRouter()
62
+ const route = useRoute()
63
+ const toast = useToast()
64
+
65
+ // Common state
66
+ const loading = ref(false)
67
+ const saving = ref(false)
68
+
69
+ // Common computed
70
+ const entityId = computed(() => {
71
+ if (getId) return getId()
72
+ return route.params.id || route.params.key || null
73
+ })
74
+
75
+ const isEdit = computed(() => !!entityId.value)
76
+
77
+ // Dirty state tracking
78
+ const {
79
+ dirty,
80
+ dirtyFields,
81
+ isFieldDirty,
82
+ takeSnapshot,
83
+ checkDirty,
84
+ reset
85
+ } = useDirtyState(getState)
86
+
87
+ // Provide isFieldDirty and dirtyFields for child components (FormField)
88
+ provide('isFieldDirty', isFieldDirty)
89
+ provide('dirtyFields', dirtyFields)
90
+
91
+ // Breadcrumb (auto-generated from route path, with optional entity for dynamic labels)
92
+ const { breadcrumbItems } = useBreadcrumb({ entity, getEntityLabel: breadcrumbLabel })
93
+
94
+ // Unsaved changes guard
95
+ let guardDialog = null
96
+ if (guard) {
97
+ const guardOptions = onGuardSave ? { onSave: onGuardSave } : {}
98
+ const guardResult = useUnsavedChangesGuard(dirty, guardOptions)
99
+ guardDialog = guardResult.guardDialog
100
+ // Register guardDialog in shared store so AppLayout can render it
101
+ registerGuardDialog(guardDialog)
102
+ onUnmounted(() => unregisterGuardDialog(guardDialog))
103
+ }
104
+
105
+ // Navigation helper
106
+ function cancel() {
107
+ if (routePrefix) {
108
+ router.push({ name: routePrefix })
109
+ } else {
110
+ router.back()
111
+ }
112
+ }
113
+
114
+ return {
115
+ // Dependencies (for custom logic in form)
116
+ router,
117
+ route,
118
+ toast,
119
+
120
+ // State
121
+ loading,
122
+ saving,
123
+ dirty,
124
+ dirtyFields,
125
+ isEdit,
126
+ entityId,
127
+
128
+ // Dirty tracking
129
+ isFieldDirty,
130
+ takeSnapshot,
131
+ checkDirty,
132
+ reset,
133
+
134
+ // Helpers
135
+ cancel,
136
+
137
+ // Breadcrumb
138
+ breadcrumb: breadcrumbItems,
139
+
140
+ // Guard dialog (for UnsavedChangesDialog component)
141
+ guardDialog
142
+ }
143
+ }
@@ -0,0 +1,221 @@
1
+ import { computed } from 'vue'
2
+ import { useRoute, useRouter } from 'vue-router'
3
+
4
+ /**
5
+ * useBreadcrumb - Auto-generate breadcrumb from route hierarchy
6
+ *
7
+ * Generates breadcrumb items automatically from:
8
+ * 1. route.meta.breadcrumb if defined (manual override)
9
+ * 2. Route path segments with smart label generation
10
+ *
11
+ * Usage:
12
+ * const { breadcrumbItems } = useBreadcrumb()
13
+ *
14
+ * // Or with entity data for dynamic labels
15
+ * const { breadcrumbItems } = useBreadcrumb({ entity: agentData })
16
+ *
17
+ * // Or with custom label callback
18
+ * const { breadcrumbItems } = useBreadcrumb({
19
+ * entity: newsroomData,
20
+ * getEntityLabel: (entity) => entity.name || entity.slug
21
+ * })
22
+ *
23
+ * Route meta example:
24
+ * {
25
+ * path: 'agents/:id/edit',
26
+ * name: 'agent-edit',
27
+ * meta: {
28
+ * breadcrumb: [
29
+ * { label: 'Agents', to: { name: 'agents' } },
30
+ * { label: ':name', dynamic: true } // Resolved from entity.name
31
+ * ]
32
+ * }
33
+ * }
34
+ */
35
+ export function useBreadcrumb(options = {}) {
36
+ const route = useRoute()
37
+ const router = useRouter()
38
+
39
+ // Label mapping for common route names
40
+ const labelMap = {
41
+ dashboard: 'Dashboard',
42
+ users: 'Users',
43
+ roles: 'Roles',
44
+ apikeys: 'API Keys',
45
+ newsrooms: 'Newsrooms',
46
+ agents: 'Agents',
47
+ events: 'Events',
48
+ taxonomy: 'Taxonomy',
49
+ domains: 'Domains',
50
+ nexus: 'Nexus',
51
+ queue: 'Queue',
52
+ create: 'Create',
53
+ edit: 'Edit',
54
+ show: 'View'
55
+ }
56
+
57
+ // Icon mapping for root sections
58
+ const iconMap = {
59
+ users: 'pi pi-users',
60
+ roles: 'pi pi-shield',
61
+ apikeys: 'pi pi-key',
62
+ newsrooms: 'pi pi-building',
63
+ agents: 'pi pi-user',
64
+ events: 'pi pi-calendar',
65
+ taxonomy: 'pi pi-tags',
66
+ domains: 'pi pi-globe',
67
+ nexus: 'pi pi-sitemap',
68
+ queue: 'pi pi-server'
69
+ }
70
+
71
+ /**
72
+ * Capitalize first letter
73
+ */
74
+ function capitalize(str) {
75
+ return str.charAt(0).toUpperCase() + str.slice(1)
76
+ }
77
+
78
+ /**
79
+ * Get human-readable label from path segment
80
+ */
81
+ function getLabel(segment) {
82
+ // Check labelMap first
83
+ if (labelMap[segment]) return labelMap[segment]
84
+ // Capitalize and replace hyphens
85
+ return capitalize(segment.replace(/-/g, ' '))
86
+ }
87
+
88
+ /**
89
+ * Resolve dynamic label from entity data
90
+ */
91
+ function resolveDynamicLabel(label, entity) {
92
+ if (!label.startsWith(':')) return label
93
+ const field = label.slice(1) // Remove ':'
94
+ return entity?.[field] || label
95
+ }
96
+
97
+ /**
98
+ * Check if a route exists
99
+ */
100
+ function routeExists(name) {
101
+ return router.getRoutes().some(r => r.name === name)
102
+ }
103
+
104
+ /**
105
+ * Get home breadcrumb item (dashboard if exists, otherwise null)
106
+ */
107
+ function getHomeItem() {
108
+ if (routeExists('dashboard')) {
109
+ return { label: 'Dashboard', to: { name: 'dashboard' }, icon: 'pi pi-home' }
110
+ }
111
+ return null
112
+ }
113
+
114
+ /**
115
+ * Build breadcrumb from route.meta.breadcrumb (manual)
116
+ */
117
+ function buildFromMeta(metaBreadcrumb, entity) {
118
+ const items = []
119
+ const home = getHomeItem()
120
+ if (home) items.push(home)
121
+
122
+ for (const item of metaBreadcrumb) {
123
+ const resolved = {
124
+ label: item.dynamic ? resolveDynamicLabel(item.label, entity) : item.label,
125
+ to: item.to || null,
126
+ icon: item.icon || null
127
+ }
128
+ items.push(resolved)
129
+ }
130
+
131
+ return items
132
+ }
133
+
134
+ /**
135
+ * Build breadcrumb automatically from route path
136
+ */
137
+ function buildFromPath(entity, getEntityLabel) {
138
+ const items = []
139
+ const home = getHomeItem()
140
+ if (home) items.push(home)
141
+
142
+ // Get path segments, filter empty and params
143
+ const segments = route.path.split('/').filter(s => s && !s.startsWith(':'))
144
+
145
+ let currentPath = ''
146
+ for (let i = 0; i < segments.length; i++) {
147
+ const segment = segments[i]
148
+ currentPath += `/${segment}`
149
+
150
+ // Handle IDs: numeric, UUID, ULID, or any alphanumeric string > 10 chars - use entity label instead
151
+ const isId = /^\d+$/.test(segment) || // numeric
152
+ segment.match(/^[0-9a-f-]{36}$/i) || // UUID
153
+ segment.match(/^[0-7][0-9a-hjkmnp-tv-z]{25}$/i) || // ULID
154
+ (segment.match(/^[a-z0-9]+$/i) && segment.length > 10) // Generated ID (like LocalStorage)
155
+ if (isId) {
156
+ // If we have entity data, show its label instead of the ID
157
+ if (entity && getEntityLabel) {
158
+ const entityLabel = getEntityLabel(entity)
159
+ if (entityLabel) {
160
+ items.push({ label: entityLabel, to: null, icon: null })
161
+ }
162
+ }
163
+ continue
164
+ }
165
+
166
+ // Get label for this segment
167
+ const label = getLabel(segment)
168
+
169
+ // Find matching route for this path
170
+ const matchedRoute = router.getRoutes().find(r => {
171
+ const routePath = r.path.replace(/:\w+/g, '[^/]+')
172
+ const regex = new RegExp(`^${routePath}$`)
173
+ return regex.test(currentPath)
174
+ })
175
+
176
+ const item = {
177
+ label,
178
+ to: matchedRoute ? { name: matchedRoute.name } : null,
179
+ icon: i === 0 ? iconMap[segment] : null
180
+ }
181
+
182
+ // Last item has no link
183
+ if (i === segments.length - 1) {
184
+ item.to = null
185
+ }
186
+
187
+ items.push(item)
188
+ }
189
+
190
+ return items
191
+ }
192
+
193
+ /**
194
+ * Computed breadcrumb items
195
+ */
196
+ const breadcrumbItems = computed(() => {
197
+ const entity = options.entity?.value || options.entity
198
+ const getEntityLabel = options.getEntityLabel || null
199
+
200
+ // Use meta.breadcrumb if defined
201
+ if (route.meta?.breadcrumb) {
202
+ return buildFromMeta(route.meta.breadcrumb, entity)
203
+ }
204
+
205
+ // Auto-generate from path
206
+ return buildFromPath(entity, getEntityLabel)
207
+ })
208
+
209
+ /**
210
+ * Manual override - set custom breadcrumb items
211
+ */
212
+ function setBreadcrumb(items) {
213
+ const home = getHomeItem()
214
+ return home ? [home, ...items] : items
215
+ }
216
+
217
+ return {
218
+ breadcrumbItems,
219
+ setBreadcrumb
220
+ }
221
+ }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Composable for tracking form dirty state
3
+ *
4
+ * Usage:
5
+ * const { dirty, dirtyFields, isFieldDirty, takeSnapshot, checkDirty } = useDirtyState(() => ({
6
+ * form: formData.value
7
+ * }))
8
+ *
9
+ * // Take snapshot after loading data
10
+ * takeSnapshot()
11
+ *
12
+ * // Watch for changes
13
+ * watch(formData, checkDirty, { deep: true })
14
+ *
15
+ * // Check if specific field is dirty
16
+ * isFieldDirty('username') // true/false
17
+ *
18
+ * // Get all dirty field names
19
+ * dirtyFields.value // ['username', 'email']
20
+ */
21
+
22
+ import { ref, nextTick } from 'vue'
23
+
24
+ export function useDirtyState(getState) {
25
+ const dirty = ref(false)
26
+ const dirtyFields = ref([])
27
+ const initialSnapshot = ref(null)
28
+ const initialState = ref(null)
29
+ const ready = ref(false)
30
+
31
+ function getFormSnapshot() {
32
+ return JSON.stringify(getState())
33
+ }
34
+
35
+ function takeSnapshot() {
36
+ // Disable dirty checking temporarily to prevent watchers from re-setting dirty
37
+ ready.value = false
38
+ dirty.value = false
39
+ dirtyFields.value = []
40
+
41
+ // Use nextTick to ensure all reactive updates have settled for snapshot
42
+ nextTick(() => {
43
+ const state = getState()
44
+ initialSnapshot.value = JSON.stringify(state)
45
+ // Store raw state for field-level comparison
46
+ initialState.value = JSON.parse(JSON.stringify(state))
47
+ ready.value = true
48
+ })
49
+ }
50
+
51
+ function checkDirty() {
52
+ // Don't check until snapshot is taken and ready
53
+ if (!ready.value || !initialSnapshot.value) return
54
+
55
+ const currentSnapshot = getFormSnapshot()
56
+ dirty.value = currentSnapshot !== initialSnapshot.value
57
+
58
+ // Calculate dirty fields
59
+ if (dirty.value && initialState.value) {
60
+ const current = getState()
61
+ const changed = []
62
+
63
+ // Compare form fields if state has a 'form' or 'formData' key (common patterns)
64
+ const initial = initialState.value.form || initialState.value.formData || initialState.value
65
+ const currentForm = current.form || current.formData || current
66
+
67
+ for (const key in currentForm) {
68
+ const initialVal = JSON.stringify(initial[key])
69
+ const currentVal = JSON.stringify(currentForm[key])
70
+ if (initialVal !== currentVal) {
71
+ changed.push(key)
72
+ }
73
+ }
74
+ dirtyFields.value = changed
75
+ } else {
76
+ dirtyFields.value = []
77
+ }
78
+ }
79
+
80
+ function isFieldDirty(fieldName) {
81
+ return dirtyFields.value.includes(fieldName)
82
+ }
83
+
84
+ function reset() {
85
+ dirty.value = false
86
+ dirtyFields.value = []
87
+ initialSnapshot.value = null
88
+ initialState.value = null
89
+ ready.value = false
90
+ }
91
+
92
+ return {
93
+ dirty,
94
+ dirtyFields,
95
+ ready,
96
+ takeSnapshot,
97
+ checkDirty,
98
+ isFieldDirty,
99
+ reset
100
+ }
101
+ }
102
+
103
+ export default useDirtyState
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Composable for generating display titles for entities
3
+ *
4
+ * Usage:
5
+ * const { displayTitle, pageTitle } = useEntityTitle(entity, {
6
+ * type: 'Agent',
7
+ * nameField: 'name', // default
8
+ * fallbackField: 'id' // default
9
+ * })
10
+ *
11
+ * // In template:
12
+ * <h1>{{ pageTitle }}</h1> // "Edit Agent John Doe" or "Create Agent"
13
+ */
14
+
15
+ import { computed } from 'vue'
16
+
17
+ /**
18
+ * Field priority for display title (first match wins)
19
+ */
20
+ const DEFAULT_NAME_FIELDS = ['name', 'title', 'label', 'username', 'slug']
21
+
22
+ /**
23
+ * Get display title from an entity object
24
+ *
25
+ * @param {Object} entity - Entity object
26
+ * @param {Object} options - Options
27
+ * @param {string|string[]} options.nameField - Field(s) to use for display name
28
+ * @param {string} options.fallbackField - Fallback field (usually 'id')
29
+ * @param {number} options.maxLength - Max length before truncation (default: 50)
30
+ * @returns {string} Display title
31
+ */
32
+ export function getEntityDisplayTitle(entity, options = {}) {
33
+ if (!entity) return ''
34
+
35
+ const {
36
+ nameField = DEFAULT_NAME_FIELDS,
37
+ fallbackField = 'id',
38
+ maxLength = 50
39
+ } = options
40
+
41
+ // Try name fields in order
42
+ const fields = Array.isArray(nameField) ? nameField : [nameField]
43
+ for (const field of fields) {
44
+ const value = entity[field]
45
+ if (value && typeof value === 'string' && value.trim()) {
46
+ const title = value.trim()
47
+ if (title.length > maxLength) {
48
+ return title.slice(0, maxLength - 3) + '...'
49
+ }
50
+ return title
51
+ }
52
+ }
53
+
54
+ // Fallback to ID (truncated)
55
+ const fallback = entity[fallbackField]
56
+ if (fallback) {
57
+ const id = String(fallback)
58
+ if (id.length > 12) {
59
+ return id.slice(0, 8) + '...'
60
+ }
61
+ return id
62
+ }
63
+
64
+ return ''
65
+ }
66
+
67
+ /**
68
+ * Composable for entity display titles
69
+ *
70
+ * @param {Ref<Object>} entityRef - Reactive entity reference
71
+ * @param {Object} options - Options
72
+ * @param {string} options.type - Entity type name (e.g., 'Agent', 'Newsroom')
73
+ * @param {string|string[]} options.nameField - Field(s) to use for display name
74
+ * @param {string} options.fallbackField - Fallback field (usually 'id')
75
+ * @param {Ref<boolean>} options.isEdit - Reactive boolean for edit mode
76
+ */
77
+ export function useEntityTitle(entityRef, options = {}) {
78
+ const {
79
+ type = 'Entity',
80
+ nameField = DEFAULT_NAME_FIELDS,
81
+ fallbackField = 'id',
82
+ isEdit = null
83
+ } = options
84
+
85
+ /**
86
+ * Display title for the entity (just the name/id part)
87
+ */
88
+ const displayTitle = computed(() => {
89
+ return getEntityDisplayTitle(entityRef.value, { nameField, fallbackField })
90
+ })
91
+
92
+ /**
93
+ * Full page title (e.g., "Edit Agent John Doe" or "Create Agent")
94
+ */
95
+ const pageTitle = computed(() => {
96
+ if (isEdit?.value) {
97
+ const title = displayTitle.value
98
+ return title ? `Edit ${type}: ${title}` : `Edit ${type}`
99
+ }
100
+ return `Create ${type}`
101
+ })
102
+
103
+ /**
104
+ * Browser document title
105
+ */
106
+ const documentTitle = computed(() => {
107
+ if (isEdit?.value) {
108
+ const title = displayTitle.value
109
+ return title ? `${title} - ${type}` : `Edit ${type}`
110
+ }
111
+ return `New ${type}`
112
+ })
113
+
114
+ return {
115
+ displayTitle,
116
+ pageTitle,
117
+ documentTitle
118
+ }
119
+ }
120
+
121
+ export default useEntityTitle