@kinetis/vector-ui 0.0.0-stage → 0.9.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 (282) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/LICENSE +18 -0
  3. package/README.md +72 -2
  4. package/app/app.config.ts +63 -0
  5. package/app/app.vue +11 -0
  6. package/app/components/Vx/Action/Dialog.vue +107 -0
  7. package/app/components/Vx/Cell/Boolean.vue +10 -0
  8. package/app/components/Vx/Cell/Color.vue +12 -0
  9. package/app/components/Vx/Cell/Count.vue +12 -0
  10. package/app/components/Vx/Cell/Date.vue +10 -0
  11. package/app/components/Vx/Cell/DateTime.vue +10 -0
  12. package/app/components/Vx/Cell/Email.vue +9 -0
  13. package/app/components/Vx/Cell/Enum.vue +14 -0
  14. package/app/components/Vx/Cell/File.vue +30 -0
  15. package/app/components/Vx/Cell/Image.vue +38 -0
  16. package/app/components/Vx/Cell/Json.vue +18 -0
  17. package/app/components/Vx/Cell/KeyValue.vue +25 -0
  18. package/app/components/Vx/Cell/Markdown.vue +18 -0
  19. package/app/components/Vx/Cell/Money.vue +16 -0
  20. package/app/components/Vx/Cell/Number.vue +16 -0
  21. package/app/components/Vx/Cell/Relation.vue +16 -0
  22. package/app/components/Vx/Cell/RichText.vue +41 -0
  23. package/app/components/Vx/Cell/Text.vue +13 -0
  24. package/app/components/Vx/Cell/Url.vue +11 -0
  25. package/app/components/Vx/Cell.vue +25 -0
  26. package/app/components/Vx/CommandPalette.vue +118 -0
  27. package/app/components/Vx/Confirm.vue +19 -0
  28. package/app/components/Vx/Dashboard.vue +27 -0
  29. package/app/components/Vx/FieldsForm.vue +46 -0
  30. package/app/components/Vx/Import/Dialog.vue +131 -0
  31. package/app/components/Vx/Inbox.vue +49 -0
  32. package/app/components/Vx/Input/Boolean.vue +10 -0
  33. package/app/components/Vx/Input/Currency.vue +25 -0
  34. package/app/components/Vx/Input/DateTime.vue +38 -0
  35. package/app/components/Vx/Input/Enum.vue +21 -0
  36. package/app/components/Vx/Input/KeyValue.vue +78 -0
  37. package/app/components/Vx/Input/Markdown.vue +120 -0
  38. package/app/components/Vx/Input/Money.vue +47 -0
  39. package/app/components/Vx/Input/Number.vue +24 -0
  40. package/app/components/Vx/Input/Relation.vue +127 -0
  41. package/app/components/Vx/Input/Repeater.vue +134 -0
  42. package/app/components/Vx/Input/RichText.vue +182 -0
  43. package/app/components/Vx/Input/Text.vue +21 -0
  44. package/app/components/Vx/Input/Textarea.vue +20 -0
  45. package/app/components/Vx/Input/Upload.vue +89 -0
  46. package/app/components/Vx/Layout.vue +111 -0
  47. package/app/components/Vx/List/BulkBar.vue +44 -0
  48. package/app/components/Vx/List/BulkEdit.vue +91 -0
  49. package/app/components/Vx/List/ColumnManager.vue +74 -0
  50. package/app/components/Vx/List/FilterBar.vue +224 -0
  51. package/app/components/Vx/List/ViewMenu.vue +115 -0
  52. package/app/components/Vx/Notifications.vue +9 -0
  53. package/app/components/Vx/Resource/Audit.vue +82 -0
  54. package/app/components/Vx/Resource/Form.vue +351 -0
  55. package/app/components/Vx/Resource/RelationPanel.vue +217 -0
  56. package/app/components/Vx/Resource/Show.vue +35 -0
  57. package/app/components/Vx/Resource/Table.vue +513 -0
  58. package/app/components/Vx/Widget/Card.vue +80 -0
  59. package/app/components/Vx/Widget/Partition.vue +21 -0
  60. package/app/components/Vx/Widget/Progress.vue +22 -0
  61. package/app/components/Vx/Widget/Stat.vue +22 -0
  62. package/app/components/Vx/Widget/Table.vue +31 -0
  63. package/app/components/Vx/Widget/Trend.vue +37 -0
  64. package/app/composables/useConfirm.ts +34 -0
  65. package/app/composables/useLeaveGuard.ts +21 -0
  66. package/app/composables/useVectorApi.ts +74 -0
  67. package/app/composables/useVectorAuthStore.ts +252 -0
  68. package/app/composables/useVectorBranding.ts +45 -0
  69. package/app/composables/useVectorDashboards.ts +15 -0
  70. package/app/composables/useVectorExtensions.ts +44 -0
  71. package/app/composables/useVectorFields.ts +68 -0
  72. package/app/composables/useVectorForm.ts +36 -0
  73. package/app/composables/useVectorIdle.ts +39 -0
  74. package/app/composables/useVectorImpersonation.ts +78 -0
  75. package/app/composables/useVectorInputs.ts +61 -0
  76. package/app/composables/useVectorJobs.ts +62 -0
  77. package/app/composables/useVectorLocale.ts +70 -0
  78. package/app/composables/useVectorNotifications.ts +24 -0
  79. package/app/composables/useVectorNotificationsInbox.ts +26 -0
  80. package/app/composables/useVectorPreferences.ts +30 -0
  81. package/app/composables/useVectorResource.ts +305 -0
  82. package/app/composables/useVectorRuntime.ts +42 -0
  83. package/app/composables/useVectorSchema.ts +56 -0
  84. package/app/composables/useVectorT.ts +9 -0
  85. package/app/composables/useVectorViews.ts +25 -0
  86. package/app/i18n/ar/actions.ts +11 -0
  87. package/app/i18n/ar/common.ts +23 -0
  88. package/app/i18n/ar/dashboard.ts +28 -0
  89. package/app/i18n/ar/fields.ts +77 -0
  90. package/app/i18n/ar/index.ts +12 -0
  91. package/app/i18n/ar/jobs.ts +36 -0
  92. package/app/i18n/ar/list.ts +160 -0
  93. package/app/i18n/ar/record.ts +62 -0
  94. package/app/i18n/ar/settings.ts +10 -0
  95. package/app/i18n/ar/shell.ts +54 -0
  96. package/app/i18n/de/actions.ts +11 -0
  97. package/app/i18n/de/common.ts +23 -0
  98. package/app/i18n/de/dashboard.ts +28 -0
  99. package/app/i18n/de/fields.ts +77 -0
  100. package/app/i18n/de/index.ts +12 -0
  101. package/app/i18n/de/jobs.ts +36 -0
  102. package/app/i18n/de/list.ts +160 -0
  103. package/app/i18n/de/record.ts +62 -0
  104. package/app/i18n/de/settings.ts +10 -0
  105. package/app/i18n/de/shell.ts +54 -0
  106. package/app/i18n/en/actions.ts +11 -0
  107. package/app/i18n/en/common.ts +23 -0
  108. package/app/i18n/en/dashboard.ts +28 -0
  109. package/app/i18n/en/fields.ts +77 -0
  110. package/app/i18n/en/index.ts +12 -0
  111. package/app/i18n/en/jobs.ts +36 -0
  112. package/app/i18n/en/list.ts +160 -0
  113. package/app/i18n/en/record.ts +62 -0
  114. package/app/i18n/en/settings.ts +10 -0
  115. package/app/i18n/en/shell.ts +54 -0
  116. package/app/i18n/es/actions.ts +11 -0
  117. package/app/i18n/es/common.ts +23 -0
  118. package/app/i18n/es/dashboard.ts +28 -0
  119. package/app/i18n/es/fields.ts +77 -0
  120. package/app/i18n/es/index.ts +12 -0
  121. package/app/i18n/es/jobs.ts +36 -0
  122. package/app/i18n/es/list.ts +160 -0
  123. package/app/i18n/es/record.ts +62 -0
  124. package/app/i18n/es/settings.ts +10 -0
  125. package/app/i18n/es/shell.ts +54 -0
  126. package/app/i18n/fr/actions.ts +11 -0
  127. package/app/i18n/fr/common.ts +23 -0
  128. package/app/i18n/fr/dashboard.ts +28 -0
  129. package/app/i18n/fr/fields.ts +77 -0
  130. package/app/i18n/fr/index.ts +12 -0
  131. package/app/i18n/fr/jobs.ts +36 -0
  132. package/app/i18n/fr/list.ts +160 -0
  133. package/app/i18n/fr/record.ts +62 -0
  134. package/app/i18n/fr/settings.ts +10 -0
  135. package/app/i18n/fr/shell.ts +54 -0
  136. package/app/i18n/he/actions.ts +11 -0
  137. package/app/i18n/he/common.ts +23 -0
  138. package/app/i18n/he/dashboard.ts +28 -0
  139. package/app/i18n/he/fields.ts +77 -0
  140. package/app/i18n/he/index.ts +12 -0
  141. package/app/i18n/he/jobs.ts +36 -0
  142. package/app/i18n/he/list.ts +160 -0
  143. package/app/i18n/he/record.ts +62 -0
  144. package/app/i18n/he/settings.ts +10 -0
  145. package/app/i18n/he/shell.ts +54 -0
  146. package/app/i18n/index.ts +111 -0
  147. package/app/i18n/it/actions.ts +11 -0
  148. package/app/i18n/it/common.ts +23 -0
  149. package/app/i18n/it/dashboard.ts +28 -0
  150. package/app/i18n/it/fields.ts +77 -0
  151. package/app/i18n/it/index.ts +12 -0
  152. package/app/i18n/it/jobs.ts +36 -0
  153. package/app/i18n/it/list.ts +160 -0
  154. package/app/i18n/it/record.ts +62 -0
  155. package/app/i18n/it/settings.ts +10 -0
  156. package/app/i18n/it/shell.ts +54 -0
  157. package/app/i18n/ja/actions.ts +11 -0
  158. package/app/i18n/ja/common.ts +23 -0
  159. package/app/i18n/ja/dashboard.ts +28 -0
  160. package/app/i18n/ja/fields.ts +77 -0
  161. package/app/i18n/ja/index.ts +12 -0
  162. package/app/i18n/ja/jobs.ts +36 -0
  163. package/app/i18n/ja/list.ts +160 -0
  164. package/app/i18n/ja/record.ts +62 -0
  165. package/app/i18n/ja/settings.ts +10 -0
  166. package/app/i18n/ja/shell.ts +54 -0
  167. package/app/i18n/ko/actions.ts +11 -0
  168. package/app/i18n/ko/common.ts +23 -0
  169. package/app/i18n/ko/dashboard.ts +28 -0
  170. package/app/i18n/ko/fields.ts +77 -0
  171. package/app/i18n/ko/index.ts +12 -0
  172. package/app/i18n/ko/jobs.ts +36 -0
  173. package/app/i18n/ko/list.ts +160 -0
  174. package/app/i18n/ko/record.ts +62 -0
  175. package/app/i18n/ko/settings.ts +10 -0
  176. package/app/i18n/ko/shell.ts +54 -0
  177. package/app/i18n/nl/actions.ts +11 -0
  178. package/app/i18n/nl/common.ts +23 -0
  179. package/app/i18n/nl/dashboard.ts +28 -0
  180. package/app/i18n/nl/fields.ts +77 -0
  181. package/app/i18n/nl/index.ts +12 -0
  182. package/app/i18n/nl/jobs.ts +36 -0
  183. package/app/i18n/nl/list.ts +160 -0
  184. package/app/i18n/nl/record.ts +62 -0
  185. package/app/i18n/nl/settings.ts +10 -0
  186. package/app/i18n/nl/shell.ts +54 -0
  187. package/app/i18n/pl/actions.ts +11 -0
  188. package/app/i18n/pl/common.ts +23 -0
  189. package/app/i18n/pl/dashboard.ts +28 -0
  190. package/app/i18n/pl/fields.ts +77 -0
  191. package/app/i18n/pl/index.ts +12 -0
  192. package/app/i18n/pl/jobs.ts +36 -0
  193. package/app/i18n/pl/list.ts +160 -0
  194. package/app/i18n/pl/record.ts +62 -0
  195. package/app/i18n/pl/settings.ts +10 -0
  196. package/app/i18n/pl/shell.ts +54 -0
  197. package/app/i18n/pt/actions.ts +11 -0
  198. package/app/i18n/pt/common.ts +23 -0
  199. package/app/i18n/pt/dashboard.ts +28 -0
  200. package/app/i18n/pt/fields.ts +77 -0
  201. package/app/i18n/pt/index.ts +12 -0
  202. package/app/i18n/pt/jobs.ts +36 -0
  203. package/app/i18n/pt/list.ts +160 -0
  204. package/app/i18n/pt/record.ts +62 -0
  205. package/app/i18n/pt/settings.ts +10 -0
  206. package/app/i18n/pt/shell.ts +54 -0
  207. package/app/i18n/pt-BR/actions.ts +11 -0
  208. package/app/i18n/pt-BR/common.ts +23 -0
  209. package/app/i18n/pt-BR/dashboard.ts +28 -0
  210. package/app/i18n/pt-BR/fields.ts +77 -0
  211. package/app/i18n/pt-BR/index.ts +12 -0
  212. package/app/i18n/pt-BR/jobs.ts +36 -0
  213. package/app/i18n/pt-BR/list.ts +160 -0
  214. package/app/i18n/pt-BR/record.ts +62 -0
  215. package/app/i18n/pt-BR/settings.ts +10 -0
  216. package/app/i18n/pt-BR/shell.ts +54 -0
  217. package/app/i18n/ru/actions.ts +11 -0
  218. package/app/i18n/ru/common.ts +23 -0
  219. package/app/i18n/ru/dashboard.ts +28 -0
  220. package/app/i18n/ru/fields.ts +77 -0
  221. package/app/i18n/ru/index.ts +12 -0
  222. package/app/i18n/ru/jobs.ts +36 -0
  223. package/app/i18n/ru/list.ts +160 -0
  224. package/app/i18n/ru/record.ts +62 -0
  225. package/app/i18n/ru/settings.ts +10 -0
  226. package/app/i18n/ru/shell.ts +54 -0
  227. package/app/i18n/tr/actions.ts +11 -0
  228. package/app/i18n/tr/common.ts +23 -0
  229. package/app/i18n/tr/dashboard.ts +28 -0
  230. package/app/i18n/tr/fields.ts +77 -0
  231. package/app/i18n/tr/index.ts +12 -0
  232. package/app/i18n/tr/jobs.ts +36 -0
  233. package/app/i18n/tr/list.ts +160 -0
  234. package/app/i18n/tr/record.ts +62 -0
  235. package/app/i18n/tr/settings.ts +10 -0
  236. package/app/i18n/tr/shell.ts +54 -0
  237. package/app/i18n/uk/actions.ts +11 -0
  238. package/app/i18n/uk/common.ts +23 -0
  239. package/app/i18n/uk/dashboard.ts +28 -0
  240. package/app/i18n/uk/fields.ts +77 -0
  241. package/app/i18n/uk/index.ts +12 -0
  242. package/app/i18n/uk/jobs.ts +36 -0
  243. package/app/i18n/uk/list.ts +160 -0
  244. package/app/i18n/uk/record.ts +62 -0
  245. package/app/i18n/uk/settings.ts +10 -0
  246. package/app/i18n/uk/shell.ts +54 -0
  247. package/app/i18n/zh-Hans/actions.ts +11 -0
  248. package/app/i18n/zh-Hans/common.ts +23 -0
  249. package/app/i18n/zh-Hans/dashboard.ts +28 -0
  250. package/app/i18n/zh-Hans/fields.ts +77 -0
  251. package/app/i18n/zh-Hans/index.ts +12 -0
  252. package/app/i18n/zh-Hans/jobs.ts +36 -0
  253. package/app/i18n/zh-Hans/list.ts +160 -0
  254. package/app/i18n/zh-Hans/record.ts +62 -0
  255. package/app/i18n/zh-Hans/settings.ts +10 -0
  256. package/app/i18n/zh-Hans/shell.ts +54 -0
  257. package/app/layouts/default.vue +8 -0
  258. package/app/layouts/vector.vue +238 -0
  259. package/app/middleware/vector-auth.ts +16 -0
  260. package/app/pages/d/[name].vue +12 -0
  261. package/app/pages/index.vue +22 -0
  262. package/app/pages/jobs/[id].vue +52 -0
  263. package/app/pages/jobs/index.vue +42 -0
  264. package/app/pages/login.vue +109 -0
  265. package/app/pages/p/[name].vue +37 -0
  266. package/app/pages/r/[resource]/[id]/edit.vue +54 -0
  267. package/app/pages/r/[resource]/[id]/index.vue +143 -0
  268. package/app/pages/r/[resource]/create.vue +53 -0
  269. package/app/pages/r/[resource]/index.vue +29 -0
  270. package/app/pages/settings.vue +104 -0
  271. package/app/plugins/vector-csp.client.ts +14 -0
  272. package/app/plugins/vector-extensions.client.ts +84 -0
  273. package/app/plugins/vector-i18n.ts +76 -0
  274. package/app/types/vector.ts +438 -0
  275. package/app/utils/files.ts +14 -0
  276. package/app/utils/format.ts +100 -0
  277. package/app/utils/richtext.ts +305 -0
  278. package/app/utils/rules.ts +113 -0
  279. package/app/utils/translate.ts +45 -0
  280. package/app/utils/widgets.ts +11 -0
  281. package/nuxt.config.ts +83 -0
  282. package/package.json +82 -5
@@ -0,0 +1,36 @@
1
+ import type { VectorField } from '#vector/app/types/vector'
2
+
3
+ /** The value a fresh input holds: the field's default, or the type's empty value. */
4
+ export function initialValue(field: VectorField): unknown {
5
+ if (field.hasDefault) return field.default
6
+ if (field.type === 'boolean') return false
7
+ if (field.type === 'manyToMany') return []
8
+ return null
9
+ }
10
+
11
+ /**
12
+ * Maps the framework's 422 document onto field names; returns the
13
+ * message that names no field, if any.
14
+ */
15
+ export function applyViolations(error: unknown, values: Record<string, unknown>, serverErrors: Record<string, string[]>): string | null {
16
+ const failure = error as { status?: number, statusCode?: number, data?: { errors?: unknown } } | null
17
+ const violations = failure?.data?.errors
18
+
19
+ if ((failure?.status ?? failure?.statusCode) !== 422 || !Array.isArray(violations)) {
20
+ return vectorErrorMessage(error)
21
+ }
22
+
23
+ let general: string | null = null
24
+
25
+ for (const violation of violations as { path: (string | number)[], message: string }[]) {
26
+ const key = String(violation.path[0] ?? '')
27
+
28
+ if (key && key in values) {
29
+ (serverErrors[key] ??= []).push(`${violation.message.charAt(0).toUpperCase()}${violation.message.slice(1)}`)
30
+ } else {
31
+ general = violation.message
32
+ }
33
+ }
34
+
35
+ return general ?? (Object.keys(serverErrors).length === 0 ? vt('common.serverRejected') : null)
36
+ }
@@ -0,0 +1,39 @@
1
+ import { useEventListener, useIdle } from '@vueuse/core'
2
+
3
+ /**
4
+ * Ends the session when nobody has used the admin for a while. The
5
+ * session otherwise never ends on its own: the store renews the token
6
+ * before it runs out and the shell polls badges and the inbox, so an open
7
+ * tab stays signed in for as long as it is open. This watches for a real
8
+ * person — a pointer, a key, a touch, a scroll — and, once app config
9
+ * `vector.idleMinutes` have passed without one, calls `onIdle` exactly
10
+ * once. A tab that comes back into view after sleeping is checked at
11
+ * that moment too, since a timer may not have fired meanwhile.
12
+ * `idleMinutes: 0` turns it off. Call it from the signed-in layout.
13
+ */
14
+ export function useVectorIdle(onIdle: () => void | Promise<void>): void {
15
+ const minutes = useAppConfig().vector.idleMinutes
16
+
17
+ if (!import.meta.client || !(minutes > 0)) return
18
+
19
+ const timeout = minutes * 60_000
20
+ const { idle, lastActive, reset } = useIdle(timeout, { listenForVisibilityChange: false })
21
+ let fired = false
22
+
23
+ const fire = () => {
24
+ if (fired) return
25
+ fired = true
26
+ void onIdle()
27
+ }
28
+
29
+ // synchronous: the session ends the moment the wait is over, not on a render tick
30
+ watch(idle, (now) => {
31
+ if (now) fire()
32
+ }, { flush: 'sync' })
33
+
34
+ useEventListener(document, 'visibilitychange', () => {
35
+ if (document.visibilityState !== 'visible') return
36
+ if (Date.now() - lastActive.value >= timeout) fire()
37
+ else reset()
38
+ })
39
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Impersonation from the UI. Bearer mode receives a new token and swaps
3
+ * it in the auth store; cookie mode is told to reload, the session
4
+ * having been swapped by the application.
5
+ */
6
+ export function useVectorImpersonation() {
7
+ const api = useVectorApi()
8
+ const auth = useVectorAuthStore()
9
+ const runtime = useVectorRuntime()
10
+ const { load } = useVectorSchema()
11
+ const impersonator = useState<{ id: number | string, name: string | null } | null>('vector.impersonator', () => null)
12
+
13
+ type Swap = { accessToken?: string, expiresIn?: number, user?: { id: number | string, name: string }, target?: { id: number | string, name: string }, reload?: boolean }
14
+
15
+ async function apply(result: Swap) {
16
+ if (result.reload && import.meta.client) {
17
+ window.location.reload()
18
+
19
+ return
20
+ }
21
+
22
+ if (!result.accessToken) {
23
+ throw new Error(vt('shell.impersonation.noToken'))
24
+ }
25
+
26
+ // who the token is for: the application's `user`, the controller's `target`, or the token's own subject
27
+ const subject = result.user ?? result.target ?? { id: subjectOf(result.accessToken) ?? '', name: auth.user?.name ?? '' }
28
+
29
+ {
30
+ const previous = { token: auth.token, expiresAt: auth.expiresAt, user: auth.user }
31
+ auth.adopt(result.accessToken, result.expiresIn ?? 3600, { id: subject.id, name: subject.name })
32
+ useVectorPreferences().clear()
33
+
34
+ try {
35
+ await load(true)
36
+ } catch (error) {
37
+ // the target cannot use the admin at all: undo the swap on both sides and say so
38
+ const status = (error as { status?: number, statusCode?: number } | null)?.status ?? (error as { statusCode?: number } | null)?.statusCode
39
+
40
+ if (status === 403) {
41
+ await api('/vector/impersonate', { method: 'DELETE' }).catch(() => undefined)
42
+ if (previous.token && previous.user) auth.adopt(previous.token, Math.max(1, Math.round((previous.expiresAt - Date.now()) / 1000)), previous.user)
43
+ await load(true)
44
+ throw new Error(vt('shell.impersonation.noAccess', { name: subject.name }), { cause: error })
45
+ }
46
+
47
+ throw error
48
+ }
49
+ }
50
+ }
51
+
52
+ /** The `sub` of a JWT, read without verifying it: the server already did, and this only labels the session. */
53
+ function subjectOf(token: string): string | null {
54
+ try {
55
+ const payload = JSON.parse(atob(token.split('.')[1]?.replace(/-/g, '+').replace(/_/g, '/') ?? '')) as { sub?: unknown }
56
+
57
+ return typeof payload.sub === 'string' || typeof payload.sub === 'number' ? String(payload.sub) : null
58
+ } catch {
59
+ return null
60
+ }
61
+ }
62
+
63
+ async function start(userId: number | string) {
64
+ await apply(await api<Swap>(`/vector/impersonate/${encodeURIComponent(String(userId))}`, { method: 'POST' }))
65
+ }
66
+
67
+ async function stop() {
68
+ await apply(await api<Swap>('/vector/impersonate', { method: 'DELETE' }))
69
+ }
70
+
71
+ /** Reads who is impersonating from /vector/me; null when nobody. */
72
+ async function refresh() {
73
+ const me = await api<{ impersonator: number | string | null }>('/vector/me')
74
+ impersonator.value = me.impersonator === null ? null : { id: me.impersonator, name: runtime.user && 'impersonatorName' in runtime.user ? String((runtime.user as { impersonatorName?: string }).impersonatorName ?? '') : null }
75
+ }
76
+
77
+ return { impersonator, start, stop, refresh }
78
+ }
@@ -0,0 +1,61 @@
1
+ import type { Component } from 'vue'
2
+ import type { VectorField } from '#vector/app/types/vector'
3
+
4
+ /**
5
+ * The input side of the field registry: the component that edits a
6
+ * field in a form. Built-ins are imported explicitly, like the cells;
7
+ * `app.config.vector.inputs` names globally registered overrides.
8
+ */
9
+ const inputs: Record<string, Component> = {
10
+ string: defineAsyncComponent(() => import('#vector/app/components/Vx/Input/Text.vue')),
11
+ email: defineAsyncComponent(() => import('#vector/app/components/Vx/Input/Text.vue')),
12
+ url: defineAsyncComponent(() => import('#vector/app/components/Vx/Input/Text.vue')),
13
+ slug: defineAsyncComponent(() => import('#vector/app/components/Vx/Input/Text.vue')),
14
+ password: defineAsyncComponent(() => import('#vector/app/components/Vx/Input/Text.vue')),
15
+ color: defineAsyncComponent(() => import('#vector/app/components/Vx/Input/Text.vue')),
16
+ file: defineAsyncComponent(() => import('#vector/app/components/Vx/Input/Upload.vue')),
17
+ image: defineAsyncComponent(() => import('#vector/app/components/Vx/Input/Upload.vue')),
18
+ text: defineAsyncComponent(() => import('#vector/app/components/Vx/Input/Textarea.vue')),
19
+ markdown: defineAsyncComponent(() => import('#vector/app/components/Vx/Input/Markdown.vue')),
20
+ richtext: defineAsyncComponent(() => import('#vector/app/components/Vx/Input/RichText.vue')),
21
+ json: defineAsyncComponent(() => import('#vector/app/components/Vx/Input/Textarea.vue')),
22
+ keyValue: defineAsyncComponent(() => import('#vector/app/components/Vx/Input/KeyValue.vue')),
23
+ number: defineAsyncComponent(() => import('#vector/app/components/Vx/Input/Number.vue')),
24
+ money: defineAsyncComponent(() => import('#vector/app/components/Vx/Input/Money.vue')),
25
+ boolean: defineAsyncComponent(() => import('#vector/app/components/Vx/Input/Boolean.vue')),
26
+ enum: defineAsyncComponent(() => import('#vector/app/components/Vx/Input/Enum.vue')),
27
+ currency: defineAsyncComponent(() => import('#vector/app/components/Vx/Input/Currency.vue')),
28
+ date: defineAsyncComponent(() => import('#vector/app/components/Vx/Input/DateTime.vue')),
29
+ datetime: defineAsyncComponent(() => import('#vector/app/components/Vx/Input/DateTime.vue')),
30
+ belongsTo: defineAsyncComponent(() => import('#vector/app/components/Vx/Input/Relation.vue')),
31
+ manyToMany: defineAsyncComponent(() => import('#vector/app/components/Vx/Input/Relation.vue')),
32
+ hasMany: defineAsyncComponent(() => import('#vector/app/components/Vx/Input/Repeater.vue')),
33
+ }
34
+
35
+ export function useVectorInputs() {
36
+ const appConfig = useAppConfig()
37
+
38
+ function inputFor(field: VectorField): Component | string {
39
+ const overrides = appConfig.vector.inputs
40
+
41
+ if (field.component && overrides[field.component]) {
42
+ return overrides[field.component]!
43
+ }
44
+
45
+ const runtime = useVectorExtensions().inputs
46
+
47
+ if (field.component && runtime.has(field.component)) {
48
+ return runtime.get(field.component)!
49
+ }
50
+
51
+ return overrides[field.type] ?? inputs[field.type] ?? inputs.string!
52
+ }
53
+
54
+ /** Whether a field is edited in this operation at all. */
55
+ function editable(field: VectorField, operation: 'create' | 'update'): boolean {
56
+ if (field.type === 'hasMany') return !!field.repeater && field.views.includes(operation)
57
+ return !field.readonly && field.views.includes(operation) && !['id', 'hasOne'].includes(field.type)
58
+ }
59
+
60
+ return { inputFor, editable }
61
+ }
@@ -0,0 +1,62 @@
1
+ import type { VectorJob } from '#vector/app/types/vector'
2
+
3
+ /** Background jobs: list, poll one until it settles, download its file. */
4
+ export function useVectorJobs() {
5
+ const api = useVectorApi()
6
+
7
+ async function list(): Promise<VectorJob[]> {
8
+ return (await api<{ data: VectorJob[] }>('/vector/jobs')).data
9
+ }
10
+
11
+ async function show(id: number): Promise<VectorJob> {
12
+ return (await api<{ data: VectorJob }>(`/vector/jobs/${id}`)).data
13
+ }
14
+
15
+ /**
16
+ * Polls every second (backing off to five) until the job is done or
17
+ * failed, reporting each state; an aborted signal ends the poll with
18
+ * the last state seen, so a page that goes away stops asking.
19
+ */
20
+ async function watch(id: number, onUpdate?: (job: VectorJob) => void, signal?: AbortSignal): Promise<VectorJob> {
21
+ let delay = 1000
22
+
23
+ for (;;) {
24
+ const job = await show(id)
25
+ onUpdate?.(job)
26
+
27
+ if (job.status === 'done' || job.status === 'failed' || job.status === 'stalled' || signal?.aborted) {
28
+ return job
29
+ }
30
+
31
+ await new Promise<void>((resolve) => {
32
+ const timer = setTimeout(resolve, delay)
33
+ signal?.addEventListener('abort', () => {
34
+ clearTimeout(timer)
35
+ resolve()
36
+ }, { once: true })
37
+ })
38
+
39
+ if (signal?.aborted) {
40
+ return job
41
+ }
42
+
43
+ delay = Math.min(5000, delay + 500)
44
+ }
45
+ }
46
+
47
+ async function download(job: VectorJob): Promise<void> {
48
+ if (!import.meta.client) return
49
+
50
+ const response = await asFile(() => api.raw(`/vector/jobs/${job.id}/download`, { responseType: 'blob' }))
51
+ const url = URL.createObjectURL(response._data as Blob)
52
+ const link = document.createElement('a')
53
+ link.href = url
54
+ link.download = job.fileName ?? `job-${job.id}`
55
+ document.body.appendChild(link)
56
+ link.click()
57
+ link.remove()
58
+ setTimeout(() => URL.revokeObjectURL(url), 1000)
59
+ }
60
+
61
+ return { list, show, watch, download }
62
+ }
@@ -0,0 +1,70 @@
1
+ import type { VectorLocale, VectorSchemaI18n } from '#vector/app/types/vector'
2
+ import { isRtl, vuetifyLocale } from '#vector/app/i18n'
3
+
4
+ // The latest language the user picked: an earlier pick still in flight
5
+ // must not apply its answer over it. The UI runs in the browser only, one
6
+ // app per page, so one counter per module is one per session.
7
+ let latestChoice = 0
8
+
9
+ /**
10
+ * The admin's language, per user. The descriptor says which language this
11
+ * user is shown and which ones the administrator lets them choose from;
12
+ * `choose()` stores the choice as the user's `locale` preference and
13
+ * reloads the descriptor, whose labels the server translates. Applying a
14
+ * language switches the UI's strings, Vuetify's, the formatting of dates,
15
+ * numbers and money, and the page's direction (right to left for Hebrew).
16
+ */
17
+ export function useVectorLocale() {
18
+ const nuxtApp = useNuxtApp()
19
+ const locales = useState<VectorLocale[]>('vector.locales', () => [])
20
+ const current = computed(() => String(nuxtApp.$vectorI18n.global.locale.value))
21
+
22
+ function apply(code: string): void {
23
+ nuxtApp.$vectorI18n.global.locale.value = code
24
+ setFormatLocale(code)
25
+
26
+ const vuetify = nuxtApp.$vuetify as { locale?: { current: { value: string }, messages: { value: Record<string, unknown> } } } | undefined
27
+ if (vuetify?.locale) {
28
+ const tag = vuetifyLocale(code)
29
+ const known = vuetify.locale.messages.value
30
+ vuetify.locale.current.value = known[tag] ? tag : (known[code.split('-')[0]!] ? code.split('-')[0]! : 'en')
31
+ }
32
+
33
+ if (import.meta.client) {
34
+ const known = locales.value.find(l => l.code === code)
35
+ document.documentElement.lang = code
36
+ document.documentElement.dir = known ? known.dir : (isRtl(code) ? 'rtl' : 'ltr')
37
+ try {
38
+ window.localStorage.setItem('vector.locale', code)
39
+ } catch {
40
+ // remembered for the next visit only when storage allows it
41
+ }
42
+ }
43
+ }
44
+
45
+ /** Takes what the descriptor says: the language to show and the ones on offer. */
46
+ function adopt(i18n: VectorSchemaI18n | undefined): void {
47
+ if (!i18n) return
48
+ locales.value = i18n.locales
49
+ if (i18n.locale !== current.value) apply(i18n.locale)
50
+ }
51
+
52
+ /**
53
+ * The user picks a language: remembered as their preference, then applied
54
+ * — in that order, since applying it makes every page fetch its labels
55
+ * again, and the server answers in the preference it has — and the
56
+ * descriptor reloaded in it.
57
+ */
58
+ async function choose(code: string): Promise<void> {
59
+ const choice = ++latestChoice
60
+ await useVectorPreferences().put({ locale: code })
61
+ if (choice !== latestChoice) return
62
+ apply(code)
63
+ const schema = await useVectorSchema().load(true)
64
+ if (choice === latestChoice) adopt(schema.i18n)
65
+ }
66
+
67
+ const direction = computed<'ltr' | 'rtl'>(() => locales.value.find(l => l.code === current.value)?.dir ?? (isRtl(current.value) ? 'rtl' : 'ltr'))
68
+
69
+ return { current, locales, direction, apply, adopt, choose }
70
+ }
@@ -0,0 +1,24 @@
1
+ export interface VectorNotification {
2
+ text: string
3
+ color?: 'success' | 'error' | 'warning' | 'info'
4
+ timeout?: number
5
+ }
6
+
7
+ /**
8
+ * The queue VxNotifications renders. Shared app state, so a page, a
9
+ * store or a fetch hook can notify from anywhere.
10
+ */
11
+ export function useVectorNotifications() {
12
+ const queue = useState<VectorNotification[]>('vector.notifications', () => [])
13
+
14
+ function notify(text: string, color: VectorNotification['color'] = 'info', timeout?: number) {
15
+ queue.value.push({ text, color, timeout })
16
+ }
17
+
18
+ return {
19
+ queue,
20
+ notify,
21
+ success: (text: string) => notify(text, 'success'),
22
+ error: (text: string) => notify(text, 'error', 6000),
23
+ }
24
+ }
@@ -0,0 +1,26 @@
1
+ import type { VectorNotification } from '#vector/app/types/vector'
2
+
3
+ /** The inbox and its unread count, polled while the admin is open. */
4
+ export function useVectorNotificationsInbox() {
5
+ const api = useVectorApi()
6
+ const unread = useState<number>('vector.inbox.unread', () => 0)
7
+ const items = useState<VectorNotification[]>('vector.inbox.items', () => [])
8
+
9
+ async function refresh(): Promise<void> {
10
+ const inbox = await api<{ data: VectorNotification[], unread: number }>('/vector/notifications')
11
+ items.value = inbox.data
12
+ unread.value = inbox.unread
13
+ }
14
+
15
+ async function markRead(id: number): Promise<void> {
16
+ await api(`/vector/notifications/${id}/read`, { method: 'POST' })
17
+ await refresh()
18
+ }
19
+
20
+ async function markAllRead(): Promise<void> {
21
+ await api('/vector/notifications/read-all', { method: 'POST' })
22
+ await refresh()
23
+ }
24
+
25
+ return { unread, items, refresh, markRead, markAllRead }
26
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Per-user UI preferences, loaded once per session and written through
3
+ * as they change. Column state lives under `columns.<resource>`.
4
+ */
5
+ export function useVectorPreferences() {
6
+ const api = useVectorApi()
7
+ const preferences = useState<Record<string, unknown> | null>('vector.preferences', () => null)
8
+
9
+ async function load(): Promise<Record<string, unknown>> {
10
+ if (preferences.value === null) {
11
+ preferences.value = (await api<{ data: Record<string, unknown> }>('/vector/preferences')).data
12
+ }
13
+
14
+ return preferences.value
15
+ }
16
+
17
+ function get<T>(key: string): T | undefined {
18
+ return preferences.value?.[key] as T | undefined
19
+ }
20
+
21
+ async function put(values: Record<string, unknown>): Promise<void> {
22
+ preferences.value = (await api<{ data: Record<string, unknown> }>('/vector/preferences', { method: 'PUT', body: values })).data
23
+ }
24
+
25
+ function clear() {
26
+ preferences.value = null
27
+ }
28
+
29
+ return { preferences, load, get, put, clear }
30
+ }