@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.
- package/CHANGELOG.md +26 -0
- package/LICENSE +18 -0
- package/README.md +72 -2
- package/app/app.config.ts +63 -0
- package/app/app.vue +11 -0
- package/app/components/Vx/Action/Dialog.vue +107 -0
- package/app/components/Vx/Cell/Boolean.vue +10 -0
- package/app/components/Vx/Cell/Color.vue +12 -0
- package/app/components/Vx/Cell/Count.vue +12 -0
- package/app/components/Vx/Cell/Date.vue +10 -0
- package/app/components/Vx/Cell/DateTime.vue +10 -0
- package/app/components/Vx/Cell/Email.vue +9 -0
- package/app/components/Vx/Cell/Enum.vue +14 -0
- package/app/components/Vx/Cell/File.vue +30 -0
- package/app/components/Vx/Cell/Image.vue +38 -0
- package/app/components/Vx/Cell/Json.vue +18 -0
- package/app/components/Vx/Cell/KeyValue.vue +25 -0
- package/app/components/Vx/Cell/Markdown.vue +18 -0
- package/app/components/Vx/Cell/Money.vue +16 -0
- package/app/components/Vx/Cell/Number.vue +16 -0
- package/app/components/Vx/Cell/Relation.vue +16 -0
- package/app/components/Vx/Cell/RichText.vue +41 -0
- package/app/components/Vx/Cell/Text.vue +13 -0
- package/app/components/Vx/Cell/Url.vue +11 -0
- package/app/components/Vx/Cell.vue +25 -0
- package/app/components/Vx/CommandPalette.vue +118 -0
- package/app/components/Vx/Confirm.vue +19 -0
- package/app/components/Vx/Dashboard.vue +27 -0
- package/app/components/Vx/FieldsForm.vue +46 -0
- package/app/components/Vx/Import/Dialog.vue +131 -0
- package/app/components/Vx/Inbox.vue +49 -0
- package/app/components/Vx/Input/Boolean.vue +10 -0
- package/app/components/Vx/Input/Currency.vue +25 -0
- package/app/components/Vx/Input/DateTime.vue +38 -0
- package/app/components/Vx/Input/Enum.vue +21 -0
- package/app/components/Vx/Input/KeyValue.vue +78 -0
- package/app/components/Vx/Input/Markdown.vue +120 -0
- package/app/components/Vx/Input/Money.vue +47 -0
- package/app/components/Vx/Input/Number.vue +24 -0
- package/app/components/Vx/Input/Relation.vue +127 -0
- package/app/components/Vx/Input/Repeater.vue +134 -0
- package/app/components/Vx/Input/RichText.vue +182 -0
- package/app/components/Vx/Input/Text.vue +21 -0
- package/app/components/Vx/Input/Textarea.vue +20 -0
- package/app/components/Vx/Input/Upload.vue +89 -0
- package/app/components/Vx/Layout.vue +111 -0
- package/app/components/Vx/List/BulkBar.vue +44 -0
- package/app/components/Vx/List/BulkEdit.vue +91 -0
- package/app/components/Vx/List/ColumnManager.vue +74 -0
- package/app/components/Vx/List/FilterBar.vue +224 -0
- package/app/components/Vx/List/ViewMenu.vue +115 -0
- package/app/components/Vx/Notifications.vue +9 -0
- package/app/components/Vx/Resource/Audit.vue +82 -0
- package/app/components/Vx/Resource/Form.vue +351 -0
- package/app/components/Vx/Resource/RelationPanel.vue +217 -0
- package/app/components/Vx/Resource/Show.vue +35 -0
- package/app/components/Vx/Resource/Table.vue +513 -0
- package/app/components/Vx/Widget/Card.vue +80 -0
- package/app/components/Vx/Widget/Partition.vue +21 -0
- package/app/components/Vx/Widget/Progress.vue +22 -0
- package/app/components/Vx/Widget/Stat.vue +22 -0
- package/app/components/Vx/Widget/Table.vue +31 -0
- package/app/components/Vx/Widget/Trend.vue +37 -0
- package/app/composables/useConfirm.ts +34 -0
- package/app/composables/useLeaveGuard.ts +21 -0
- package/app/composables/useVectorApi.ts +74 -0
- package/app/composables/useVectorAuthStore.ts +252 -0
- package/app/composables/useVectorBranding.ts +45 -0
- package/app/composables/useVectorDashboards.ts +15 -0
- package/app/composables/useVectorExtensions.ts +44 -0
- package/app/composables/useVectorFields.ts +68 -0
- package/app/composables/useVectorForm.ts +36 -0
- package/app/composables/useVectorIdle.ts +39 -0
- package/app/composables/useVectorImpersonation.ts +78 -0
- package/app/composables/useVectorInputs.ts +61 -0
- package/app/composables/useVectorJobs.ts +62 -0
- package/app/composables/useVectorLocale.ts +70 -0
- package/app/composables/useVectorNotifications.ts +24 -0
- package/app/composables/useVectorNotificationsInbox.ts +26 -0
- package/app/composables/useVectorPreferences.ts +30 -0
- package/app/composables/useVectorResource.ts +305 -0
- package/app/composables/useVectorRuntime.ts +42 -0
- package/app/composables/useVectorSchema.ts +56 -0
- package/app/composables/useVectorT.ts +9 -0
- package/app/composables/useVectorViews.ts +25 -0
- package/app/i18n/ar/actions.ts +11 -0
- package/app/i18n/ar/common.ts +23 -0
- package/app/i18n/ar/dashboard.ts +28 -0
- package/app/i18n/ar/fields.ts +77 -0
- package/app/i18n/ar/index.ts +12 -0
- package/app/i18n/ar/jobs.ts +36 -0
- package/app/i18n/ar/list.ts +160 -0
- package/app/i18n/ar/record.ts +62 -0
- package/app/i18n/ar/settings.ts +10 -0
- package/app/i18n/ar/shell.ts +54 -0
- package/app/i18n/de/actions.ts +11 -0
- package/app/i18n/de/common.ts +23 -0
- package/app/i18n/de/dashboard.ts +28 -0
- package/app/i18n/de/fields.ts +77 -0
- package/app/i18n/de/index.ts +12 -0
- package/app/i18n/de/jobs.ts +36 -0
- package/app/i18n/de/list.ts +160 -0
- package/app/i18n/de/record.ts +62 -0
- package/app/i18n/de/settings.ts +10 -0
- package/app/i18n/de/shell.ts +54 -0
- package/app/i18n/en/actions.ts +11 -0
- package/app/i18n/en/common.ts +23 -0
- package/app/i18n/en/dashboard.ts +28 -0
- package/app/i18n/en/fields.ts +77 -0
- package/app/i18n/en/index.ts +12 -0
- package/app/i18n/en/jobs.ts +36 -0
- package/app/i18n/en/list.ts +160 -0
- package/app/i18n/en/record.ts +62 -0
- package/app/i18n/en/settings.ts +10 -0
- package/app/i18n/en/shell.ts +54 -0
- package/app/i18n/es/actions.ts +11 -0
- package/app/i18n/es/common.ts +23 -0
- package/app/i18n/es/dashboard.ts +28 -0
- package/app/i18n/es/fields.ts +77 -0
- package/app/i18n/es/index.ts +12 -0
- package/app/i18n/es/jobs.ts +36 -0
- package/app/i18n/es/list.ts +160 -0
- package/app/i18n/es/record.ts +62 -0
- package/app/i18n/es/settings.ts +10 -0
- package/app/i18n/es/shell.ts +54 -0
- package/app/i18n/fr/actions.ts +11 -0
- package/app/i18n/fr/common.ts +23 -0
- package/app/i18n/fr/dashboard.ts +28 -0
- package/app/i18n/fr/fields.ts +77 -0
- package/app/i18n/fr/index.ts +12 -0
- package/app/i18n/fr/jobs.ts +36 -0
- package/app/i18n/fr/list.ts +160 -0
- package/app/i18n/fr/record.ts +62 -0
- package/app/i18n/fr/settings.ts +10 -0
- package/app/i18n/fr/shell.ts +54 -0
- package/app/i18n/he/actions.ts +11 -0
- package/app/i18n/he/common.ts +23 -0
- package/app/i18n/he/dashboard.ts +28 -0
- package/app/i18n/he/fields.ts +77 -0
- package/app/i18n/he/index.ts +12 -0
- package/app/i18n/he/jobs.ts +36 -0
- package/app/i18n/he/list.ts +160 -0
- package/app/i18n/he/record.ts +62 -0
- package/app/i18n/he/settings.ts +10 -0
- package/app/i18n/he/shell.ts +54 -0
- package/app/i18n/index.ts +111 -0
- package/app/i18n/it/actions.ts +11 -0
- package/app/i18n/it/common.ts +23 -0
- package/app/i18n/it/dashboard.ts +28 -0
- package/app/i18n/it/fields.ts +77 -0
- package/app/i18n/it/index.ts +12 -0
- package/app/i18n/it/jobs.ts +36 -0
- package/app/i18n/it/list.ts +160 -0
- package/app/i18n/it/record.ts +62 -0
- package/app/i18n/it/settings.ts +10 -0
- package/app/i18n/it/shell.ts +54 -0
- package/app/i18n/ja/actions.ts +11 -0
- package/app/i18n/ja/common.ts +23 -0
- package/app/i18n/ja/dashboard.ts +28 -0
- package/app/i18n/ja/fields.ts +77 -0
- package/app/i18n/ja/index.ts +12 -0
- package/app/i18n/ja/jobs.ts +36 -0
- package/app/i18n/ja/list.ts +160 -0
- package/app/i18n/ja/record.ts +62 -0
- package/app/i18n/ja/settings.ts +10 -0
- package/app/i18n/ja/shell.ts +54 -0
- package/app/i18n/ko/actions.ts +11 -0
- package/app/i18n/ko/common.ts +23 -0
- package/app/i18n/ko/dashboard.ts +28 -0
- package/app/i18n/ko/fields.ts +77 -0
- package/app/i18n/ko/index.ts +12 -0
- package/app/i18n/ko/jobs.ts +36 -0
- package/app/i18n/ko/list.ts +160 -0
- package/app/i18n/ko/record.ts +62 -0
- package/app/i18n/ko/settings.ts +10 -0
- package/app/i18n/ko/shell.ts +54 -0
- package/app/i18n/nl/actions.ts +11 -0
- package/app/i18n/nl/common.ts +23 -0
- package/app/i18n/nl/dashboard.ts +28 -0
- package/app/i18n/nl/fields.ts +77 -0
- package/app/i18n/nl/index.ts +12 -0
- package/app/i18n/nl/jobs.ts +36 -0
- package/app/i18n/nl/list.ts +160 -0
- package/app/i18n/nl/record.ts +62 -0
- package/app/i18n/nl/settings.ts +10 -0
- package/app/i18n/nl/shell.ts +54 -0
- package/app/i18n/pl/actions.ts +11 -0
- package/app/i18n/pl/common.ts +23 -0
- package/app/i18n/pl/dashboard.ts +28 -0
- package/app/i18n/pl/fields.ts +77 -0
- package/app/i18n/pl/index.ts +12 -0
- package/app/i18n/pl/jobs.ts +36 -0
- package/app/i18n/pl/list.ts +160 -0
- package/app/i18n/pl/record.ts +62 -0
- package/app/i18n/pl/settings.ts +10 -0
- package/app/i18n/pl/shell.ts +54 -0
- package/app/i18n/pt/actions.ts +11 -0
- package/app/i18n/pt/common.ts +23 -0
- package/app/i18n/pt/dashboard.ts +28 -0
- package/app/i18n/pt/fields.ts +77 -0
- package/app/i18n/pt/index.ts +12 -0
- package/app/i18n/pt/jobs.ts +36 -0
- package/app/i18n/pt/list.ts +160 -0
- package/app/i18n/pt/record.ts +62 -0
- package/app/i18n/pt/settings.ts +10 -0
- package/app/i18n/pt/shell.ts +54 -0
- package/app/i18n/pt-BR/actions.ts +11 -0
- package/app/i18n/pt-BR/common.ts +23 -0
- package/app/i18n/pt-BR/dashboard.ts +28 -0
- package/app/i18n/pt-BR/fields.ts +77 -0
- package/app/i18n/pt-BR/index.ts +12 -0
- package/app/i18n/pt-BR/jobs.ts +36 -0
- package/app/i18n/pt-BR/list.ts +160 -0
- package/app/i18n/pt-BR/record.ts +62 -0
- package/app/i18n/pt-BR/settings.ts +10 -0
- package/app/i18n/pt-BR/shell.ts +54 -0
- package/app/i18n/ru/actions.ts +11 -0
- package/app/i18n/ru/common.ts +23 -0
- package/app/i18n/ru/dashboard.ts +28 -0
- package/app/i18n/ru/fields.ts +77 -0
- package/app/i18n/ru/index.ts +12 -0
- package/app/i18n/ru/jobs.ts +36 -0
- package/app/i18n/ru/list.ts +160 -0
- package/app/i18n/ru/record.ts +62 -0
- package/app/i18n/ru/settings.ts +10 -0
- package/app/i18n/ru/shell.ts +54 -0
- package/app/i18n/tr/actions.ts +11 -0
- package/app/i18n/tr/common.ts +23 -0
- package/app/i18n/tr/dashboard.ts +28 -0
- package/app/i18n/tr/fields.ts +77 -0
- package/app/i18n/tr/index.ts +12 -0
- package/app/i18n/tr/jobs.ts +36 -0
- package/app/i18n/tr/list.ts +160 -0
- package/app/i18n/tr/record.ts +62 -0
- package/app/i18n/tr/settings.ts +10 -0
- package/app/i18n/tr/shell.ts +54 -0
- package/app/i18n/uk/actions.ts +11 -0
- package/app/i18n/uk/common.ts +23 -0
- package/app/i18n/uk/dashboard.ts +28 -0
- package/app/i18n/uk/fields.ts +77 -0
- package/app/i18n/uk/index.ts +12 -0
- package/app/i18n/uk/jobs.ts +36 -0
- package/app/i18n/uk/list.ts +160 -0
- package/app/i18n/uk/record.ts +62 -0
- package/app/i18n/uk/settings.ts +10 -0
- package/app/i18n/uk/shell.ts +54 -0
- package/app/i18n/zh-Hans/actions.ts +11 -0
- package/app/i18n/zh-Hans/common.ts +23 -0
- package/app/i18n/zh-Hans/dashboard.ts +28 -0
- package/app/i18n/zh-Hans/fields.ts +77 -0
- package/app/i18n/zh-Hans/index.ts +12 -0
- package/app/i18n/zh-Hans/jobs.ts +36 -0
- package/app/i18n/zh-Hans/list.ts +160 -0
- package/app/i18n/zh-Hans/record.ts +62 -0
- package/app/i18n/zh-Hans/settings.ts +10 -0
- package/app/i18n/zh-Hans/shell.ts +54 -0
- package/app/layouts/default.vue +8 -0
- package/app/layouts/vector.vue +238 -0
- package/app/middleware/vector-auth.ts +16 -0
- package/app/pages/d/[name].vue +12 -0
- package/app/pages/index.vue +22 -0
- package/app/pages/jobs/[id].vue +52 -0
- package/app/pages/jobs/index.vue +42 -0
- package/app/pages/login.vue +109 -0
- package/app/pages/p/[name].vue +37 -0
- package/app/pages/r/[resource]/[id]/edit.vue +54 -0
- package/app/pages/r/[resource]/[id]/index.vue +143 -0
- package/app/pages/r/[resource]/create.vue +53 -0
- package/app/pages/r/[resource]/index.vue +29 -0
- package/app/pages/settings.vue +104 -0
- package/app/plugins/vector-csp.client.ts +14 -0
- package/app/plugins/vector-extensions.client.ts +84 -0
- package/app/plugins/vector-i18n.ts +76 -0
- package/app/types/vector.ts +438 -0
- package/app/utils/files.ts +14 -0
- package/app/utils/format.ts +100 -0
- package/app/utils/richtext.ts +305 -0
- package/app/utils/rules.ts +113 -0
- package/app/utils/translate.ts +45 -0
- package/app/utils/widgets.ts +11 -0
- package/nuxt.config.ts +83 -0
- package/package.json +82 -5
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@kinetis/vector-ui` are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.9.0] - 2026-10-18
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- A Nuxt 4 layer on Vuetify 4 that renders the `kinetis/vector`
|
|
15
|
+
descriptor: navigation with badges, lists, record pages, generated
|
|
16
|
+
forms, relation panels, actions, dashboards, jobs, notifications,
|
|
17
|
+
settings and global search.
|
|
18
|
+
- Standalone mode with a bearer token and refresh, and hosted mode on
|
|
19
|
+
the application's session cookie with CSRF, with an idle sign-out in
|
|
20
|
+
both.
|
|
21
|
+
- A rich-text editor on Tiptap and a Markdown editor, rendered through
|
|
22
|
+
DOMPurify.
|
|
23
|
+
- Overrides for cells, inputs and widget renderers in `app.config`, and
|
|
24
|
+
runtime extensions through `window.Vector`, custom pages included.
|
|
25
|
+
- English and sixteen machine-translated languages, right to left for
|
|
26
|
+
Hebrew and Arabic, with per-locale message overrides.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alon Noy
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
|
6
|
+
associated documentation files (the "Software"), to deal in the Software without restriction, including
|
|
7
|
+
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
|
|
9
|
+
following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial
|
|
12
|
+
portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
|
15
|
+
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
|
|
16
|
+
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
17
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
|
18
|
+
USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,73 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://kinetis.dev/vector/docs/_static/logo.svg" alt="Kinetis" width="420">
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
<p align="center">
|
|
6
|
+
<strong>@kinetis/vector-ui</strong>
|
|
7
|
+
<br>
|
|
8
|
+
<strong>The Kinetis Vector admin UI, as a Nuxt layer on Vuetify</strong>
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<p align="center">
|
|
12
|
+
<a href="https://www.npmjs.com/package/@kinetis/vector-ui"><img src="https://img.shields.io/npm/v/@kinetis/vector-ui?label=version" alt="npm Version"></a>
|
|
13
|
+
<a href="https://www.npmjs.com/package/@kinetis/vector-ui"><img src="https://img.shields.io/npm/dm/@kinetis/vector-ui" alt="npm Downloads"></a>
|
|
14
|
+
<a href="https://www.npmjs.com/package/@kinetis/vector-ui"><img src="https://img.shields.io/npm/l/@kinetis/vector-ui" alt="License"></a>
|
|
15
|
+
<a href="https://github.com/kinetis-dev/vector-dev/actions/workflows/ci.yml"><img src="https://github.com/kinetis-dev/vector-dev/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
|
|
16
|
+
</p>
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
Part of [Kinetis](https://kinetis.dev/), a non-blocking PHP framework for
|
|
21
|
+
API-first applications. Vector is developed in the
|
|
22
|
+
[kinetis-dev/vector-dev](https://github.com/kinetis-dev/vector-dev)
|
|
23
|
+
monorepo, beside its PHP package and a demo application.
|
|
24
|
+
|
|
25
|
+
The admin UI for [`kinetis/vector`](https://packagist.org/packages/kinetis/vector):
|
|
26
|
+
a Nuxt 4 layer on Vuetify 4 that renders the descriptor the API
|
|
27
|
+
publishes — navigation, lists, forms, relation panels, actions,
|
|
28
|
+
dashboards, jobs, settings — and contains no application knowledge. An
|
|
29
|
+
application is a Nuxt app that extends the layer, served as a static
|
|
30
|
+
site on its own origin with a bearer token (standalone), or published
|
|
31
|
+
into the PHP application and served at `/vector/ui` behind its session
|
|
32
|
+
(hosted).
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
pnpm add @kinetis/vector-ui nuxt vuetify vuetify-nuxt-module
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
// nuxt.config.ts
|
|
42
|
+
export default defineNuxtConfig({
|
|
43
|
+
extends: ['@kinetis/vector-ui'],
|
|
44
|
+
runtimeConfig: { public: { vector: { apiBase: 'https://api.example.com' } } },
|
|
45
|
+
})
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`pnpm nuxt dev` for development, `pnpm nuxt generate` for a static site
|
|
49
|
+
any web server hosts. The API allows the UI's origin through
|
|
50
|
+
`CorsMiddleware` and offers the `POST /auth/login` endpoint the sign-in
|
|
51
|
+
page calls; see
|
|
52
|
+
[Deployment](https://kinetis.dev/vector/docs/deployment.html).
|
|
53
|
+
|
|
54
|
+
## Configuration
|
|
55
|
+
|
|
56
|
+
| Where | Key | Default | Purpose |
|
|
57
|
+
|---|---|---|---|
|
|
58
|
+
| `runtimeConfig.public.vector` | `apiBase` | `http://localhost:8080` | The API origin in standalone mode (`NUXT_PUBLIC_VECTOR_API_BASE`). |
|
|
59
|
+
| | `extensions` | `[]` | ES-module URLs to import after boot, in standalone mode. |
|
|
60
|
+
| `app.config.vector` | `brand.title` | `Vector` | The title before the Branding settings load. |
|
|
61
|
+
| | `usersResource` | `null` | The resource whose records are users, for **Impersonate**. |
|
|
62
|
+
| | `idleMinutes` | `30` | Minutes without use after which the admin signs the user out; `0` never. |
|
|
63
|
+
| | `locale` | `en` | The language before the descriptor names the user's. |
|
|
64
|
+
|
|
65
|
+
Every setting, including component overrides, custom pages and
|
|
66
|
+
messages:
|
|
67
|
+
[kinetis.dev/vector/docs/configuration.html](https://kinetis.dev/vector/docs/configuration.html).
|
|
68
|
+
Cells, inputs, widgets and runtime extensions:
|
|
69
|
+
[Customizing the UI](https://kinetis.dev/vector/docs/customizing-the-ui.html).
|
|
70
|
+
|
|
71
|
+
## License
|
|
72
|
+
|
|
73
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export default defineAppConfig({
|
|
2
|
+
vector: {
|
|
3
|
+
brand: {
|
|
4
|
+
title: 'Vector',
|
|
5
|
+
},
|
|
6
|
+
// A path prefix inside the router; the deployment's URL prefix is
|
|
7
|
+
// Nuxt's app.baseURL ('/' standalone, '/vector/ui/' hosted).
|
|
8
|
+
basePath: '',
|
|
9
|
+
// Field components a consuming app adds or replaces, keyed by the
|
|
10
|
+
// descriptor's field type or explicit component name (Phase 2).
|
|
11
|
+
fields: {} as Record<string, string>,
|
|
12
|
+
// Input components, the same way (Phase 3).
|
|
13
|
+
inputs: {} as Record<string, string>,
|
|
14
|
+
// The Ctrl/Cmd+K global search palette (Phase 4).
|
|
15
|
+
commandPalette: true,
|
|
16
|
+
// Widget renderers by type, the same way (Phase 5).
|
|
17
|
+
widgets: {} as Record<string, string>,
|
|
18
|
+
// The resource whose records are users, for the Impersonate button (Phase 5).
|
|
19
|
+
usersResource: null as string | null,
|
|
20
|
+
// The UI's language before the descriptor names the user's, when neither the hosted shell nor this browser does.
|
|
21
|
+
locale: 'en',
|
|
22
|
+
// Minutes without a pointer, key, touch or scroll after which the admin signs the user out; 0 never does.
|
|
23
|
+
idleMinutes: 30,
|
|
24
|
+
// Messages an application adds or overrides, by locale and area: { de: { common: { save: 'Speichern' } } }.
|
|
25
|
+
messages: {} as Record<string, Record<string, unknown>>,
|
|
26
|
+
// Custom pages at /p/{name}: { stock: { title: 'Stock', icon: 'mdi-warehouse', component: 'StockPage' } },
|
|
27
|
+
// the component registered globally by the application.
|
|
28
|
+
pages: {} as Record<string, { title: string, icon?: string, group?: string, component: string }>,
|
|
29
|
+
},
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
declare module 'nuxt/schema' {
|
|
33
|
+
interface AppConfigInput {
|
|
34
|
+
vector?: {
|
|
35
|
+
brand?: { title?: string }
|
|
36
|
+
basePath?: string
|
|
37
|
+
fields?: Record<string, string>
|
|
38
|
+
inputs?: Record<string, string>
|
|
39
|
+
commandPalette?: boolean
|
|
40
|
+
widgets?: Record<string, string>
|
|
41
|
+
usersResource?: string | null
|
|
42
|
+
locale?: string
|
|
43
|
+
idleMinutes?: number
|
|
44
|
+
messages?: Record<string, Record<string, unknown>>
|
|
45
|
+
pages?: Record<string, { title: string, icon?: string, group?: string, component: string }>
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
interface AppConfig {
|
|
49
|
+
vector: {
|
|
50
|
+
brand: { title: string }
|
|
51
|
+
basePath: string
|
|
52
|
+
fields: Record<string, string>
|
|
53
|
+
inputs: Record<string, string>
|
|
54
|
+
commandPalette: boolean
|
|
55
|
+
widgets: Record<string, string>
|
|
56
|
+
usersResource: string | null
|
|
57
|
+
locale: string
|
|
58
|
+
idleMinutes: number
|
|
59
|
+
messages: Record<string, Record<string, unknown>>
|
|
60
|
+
pages: Record<string, { title: string, icon?: string, group?: string, component: string }>
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
package/app/app.vue
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// The document's language follows the admin's locale, for screen readers and hyphenation.
|
|
3
|
+
const { locale } = useVectorT()
|
|
4
|
+
useHead({ htmlAttrs: { lang: () => locale.value } })
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<template>
|
|
8
|
+
<NuxtLayout>
|
|
9
|
+
<NuxtPage />
|
|
10
|
+
</NuxtLayout>
|
|
11
|
+
</template>
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { SubmitEventPromise } from 'vuetify'
|
|
3
|
+
import type { VectorAction, VectorResource, VectorSelection } from '#vector/app/types/vector'
|
|
4
|
+
|
|
5
|
+
// Runs one action: the confirmation, the dialog's fields, the request,
|
|
6
|
+
// and what the response asks the client to do.
|
|
7
|
+
const props = defineProps<{
|
|
8
|
+
resource: VectorResource
|
|
9
|
+
action: VectorAction
|
|
10
|
+
selection: VectorSelection | null
|
|
11
|
+
count: number
|
|
12
|
+
}>()
|
|
13
|
+
|
|
14
|
+
const emit = defineEmits<{ done: [affected: number], cancel: [] }>()
|
|
15
|
+
|
|
16
|
+
const { t, noun } = useVectorT()
|
|
17
|
+
const appConfig = useAppConfig()
|
|
18
|
+
const notifications = useVectorNotifications()
|
|
19
|
+
const client = useVectorResource(() => props.resource.slug)
|
|
20
|
+
|
|
21
|
+
const values = reactive<Record<string, unknown>>({})
|
|
22
|
+
const serverErrors = reactive<Record<string, string[]>>({})
|
|
23
|
+
const formError = ref<string | null>(null)
|
|
24
|
+
const running = ref(false)
|
|
25
|
+
|
|
26
|
+
for (const field of props.action.fields) {
|
|
27
|
+
values[field.name] = initialValue(field)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const subject = computed(() => props.action.standalone
|
|
31
|
+
? ''
|
|
32
|
+
: props.selection?.all
|
|
33
|
+
? t('actions.subjectAll', { label: noun(props.resource.labelSingular), count: props.count })
|
|
34
|
+
: t('actions.subjectCount', { count: props.count, label: noun(props.count === 1 ? props.resource.labelSingular : props.resource.label) }))
|
|
35
|
+
|
|
36
|
+
async function run(event: SubmitEventPromise) {
|
|
37
|
+
const { valid } = await event
|
|
38
|
+
if (!valid) return
|
|
39
|
+
|
|
40
|
+
running.value = true
|
|
41
|
+
formError.value = null
|
|
42
|
+
for (const key of Object.keys(serverErrors)) delete serverErrors[key]
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const response = await client.runAction(props.action.name, props.action.standalone ? null : props.selection, { ...values })
|
|
46
|
+
|
|
47
|
+
switch (response.type) {
|
|
48
|
+
case 'message':
|
|
49
|
+
notifications.success(response.message ?? t('actions.done', { label: props.action.label }))
|
|
50
|
+
break
|
|
51
|
+
case 'danger':
|
|
52
|
+
formError.value = response.message ?? t('actions.declined')
|
|
53
|
+
return
|
|
54
|
+
case 'redirect':
|
|
55
|
+
if (response.message) notifications.success(response.message)
|
|
56
|
+
emit('done', response.affected)
|
|
57
|
+
await navigateTo(`${appConfig.vector.basePath}${response.url ?? ''}`)
|
|
58
|
+
return
|
|
59
|
+
case 'url':
|
|
60
|
+
if (response.message) notifications.success(response.message)
|
|
61
|
+
if (response.url && import.meta.client) window.open(response.url, '_blank', 'noopener')
|
|
62
|
+
break
|
|
63
|
+
case 'download':
|
|
64
|
+
notifications.success(t('actions.downloadStarted'))
|
|
65
|
+
break
|
|
66
|
+
case 'job':
|
|
67
|
+
notifications.success(response.message ?? t('actions.queued', { label: props.action.label }))
|
|
68
|
+
emit('done', 0)
|
|
69
|
+
await navigateTo(`${appConfig.vector.basePath}/jobs/${response.jobId}`)
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
emit('done', response.affected)
|
|
74
|
+
} catch (error) {
|
|
75
|
+
formError.value = applyViolations(error, values, serverErrors)
|
|
76
|
+
} finally {
|
|
77
|
+
running.value = false
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
</script>
|
|
81
|
+
|
|
82
|
+
<template>
|
|
83
|
+
<v-dialog :model-value="true" max-width="560" persistent @update:model-value="emit('cancel')">
|
|
84
|
+
<v-form validate-on="blur lazy" @submit.prevent="run">
|
|
85
|
+
<v-card>
|
|
86
|
+
<v-card-title class="d-flex align-center ga-2">
|
|
87
|
+
<v-icon v-if="action.icon" :icon="action.icon" />
|
|
88
|
+
{{ action.label }}
|
|
89
|
+
</v-card-title>
|
|
90
|
+
<v-card-text>
|
|
91
|
+
<p v-if="action.confirmation" class="mb-4">{{ action.confirmation }}</p>
|
|
92
|
+
<i18n-t v-else-if="!action.standalone" keypath="actions.runOn" tag="p" class="mb-4" scope="global">
|
|
93
|
+
<template #action><strong>{{ action.label }}</strong></template>
|
|
94
|
+
<template #subject>{{ subject }}</template>
|
|
95
|
+
</i18n-t>
|
|
96
|
+
<v-alert v-if="formError" type="error" variant="tonal" class="mb-4" :text="formError" />
|
|
97
|
+
<VxFieldsForm v-if="action.fields.length" :fields="action.fields" :values="values" :server-errors="serverErrors" :resource="resource.slug" @clear="(name: string) => delete serverErrors[name]" />
|
|
98
|
+
</v-card-text>
|
|
99
|
+
<v-card-actions>
|
|
100
|
+
<v-spacer />
|
|
101
|
+
<v-btn variant="text" :disabled="running" @click="emit('cancel')">{{ t('common.cancel') }}</v-btn>
|
|
102
|
+
<v-btn type="submit" :color="action.destructive ? 'error' : 'primary'" :loading="running">{{ action.label }}</v-btn>
|
|
103
|
+
</v-card-actions>
|
|
104
|
+
</v-card>
|
|
105
|
+
</v-form>
|
|
106
|
+
</v-dialog>
|
|
107
|
+
</template>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
defineProps<{ value: unknown }>()
|
|
3
|
+
const { t } = useVectorT()
|
|
4
|
+
</script>
|
|
5
|
+
|
|
6
|
+
<template>
|
|
7
|
+
<v-icon v-if="value === true" icon="mdi-check-circle" color="success" size="small" />
|
|
8
|
+
<v-icon v-else-if="value === false" icon="mdi-close-circle-outline" color="grey" size="small" />
|
|
9
|
+
<span v-else class="text-disabled">{{ t('common.none') }}</span>
|
|
10
|
+
</template>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
defineProps<{ value: unknown }>()
|
|
3
|
+
const { t } = useVectorT()
|
|
4
|
+
</script>
|
|
5
|
+
|
|
6
|
+
<template>
|
|
7
|
+
<span v-if="typeof value === 'string' && value" class="d-inline-flex align-center ga-2">
|
|
8
|
+
<v-avatar :color="value" size="16" />
|
|
9
|
+
<code>{{ value }}</code>
|
|
10
|
+
</span>
|
|
11
|
+
<span v-else class="text-disabled">{{ t('common.none') }}</span>
|
|
12
|
+
</template>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { VectorCountValue } from '#vector/app/types/vector'
|
|
3
|
+
|
|
4
|
+
const props = defineProps<{ value: unknown }>()
|
|
5
|
+
const count = computed(() => (props.value && typeof props.value === 'object' && 'count' in props.value ? (props.value as VectorCountValue).count : null))
|
|
6
|
+
const { t } = useVectorT()
|
|
7
|
+
</script>
|
|
8
|
+
|
|
9
|
+
<template>
|
|
10
|
+
<v-chip v-if="count !== null" size="small" variant="tonal" :color="count > 0 ? 'primary' : undefined">{{ count }}</v-chip>
|
|
11
|
+
<span v-else class="text-disabled">{{ t('common.none') }}</span>
|
|
12
|
+
</template>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
const props = defineProps<{ value: unknown }>()
|
|
3
|
+
const text = computed(() => typeof props.value === 'string' ? formatDate(props.value) : '')
|
|
4
|
+
const { t } = useVectorT()
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<template>
|
|
8
|
+
<span v-if="text === ''" class="text-disabled">{{ t('common.none') }}</span>
|
|
9
|
+
<span v-else class="text-no-wrap">{{ text }}</span>
|
|
10
|
+
</template>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
const props = defineProps<{ value: unknown }>()
|
|
3
|
+
const text = computed(() => typeof props.value === 'string' ? formatDateTime(props.value) : '')
|
|
4
|
+
const { t } = useVectorT()
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<template>
|
|
8
|
+
<span v-if="text === ''" class="text-disabled">{{ t('common.none') }}</span>
|
|
9
|
+
<span v-else class="text-no-wrap" :title="String(value)">{{ text }}</span>
|
|
10
|
+
</template>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
defineProps<{ value: unknown }>()
|
|
3
|
+
const { t } = useVectorT()
|
|
4
|
+
</script>
|
|
5
|
+
|
|
6
|
+
<template>
|
|
7
|
+
<a v-if="typeof value === 'string' && value" :href="`mailto:${value}`" @click.stop>{{ value }}</a>
|
|
8
|
+
<span v-else class="text-disabled">{{ t('common.none') }}</span>
|
|
9
|
+
</template>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { VectorField } from '#vector/app/types/vector'
|
|
3
|
+
|
|
4
|
+
const props = defineProps<{ field: VectorField, value: unknown }>()
|
|
5
|
+
const option = computed(() => props.field.options?.find(o => o.value === props.value))
|
|
6
|
+
const { t } = useVectorT()
|
|
7
|
+
</script>
|
|
8
|
+
|
|
9
|
+
<template>
|
|
10
|
+
<span v-if="value === null || value === undefined" class="text-disabled">{{ t('common.none') }}</span>
|
|
11
|
+
<v-chip v-else size="small" :color="option?.color ?? undefined" variant="tonal" label>
|
|
12
|
+
{{ option?.label ?? String(value) }}
|
|
13
|
+
</v-chip>
|
|
14
|
+
</template>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { VectorField, VectorResource } from '#vector/app/types/vector'
|
|
3
|
+
|
|
4
|
+
// A stored document as a download button, fetched with the API's credentials.
|
|
5
|
+
const props = defineProps<{ field: VectorField, value: unknown, record: Record<string, unknown>, resource: VectorResource, context: 'index' | 'show' }>()
|
|
6
|
+
|
|
7
|
+
const client = useVectorResource(() => props.resource.slug)
|
|
8
|
+
const notifications = useVectorNotifications()
|
|
9
|
+
const key = computed(() => (typeof props.value === 'string' && props.value !== '' ? props.value : null))
|
|
10
|
+
const name = computed(() => key.value?.split('/').pop() ?? '')
|
|
11
|
+
const busy = ref(false)
|
|
12
|
+
|
|
13
|
+
async function download() {
|
|
14
|
+
if (!key.value) return
|
|
15
|
+
busy.value = true
|
|
16
|
+
try {
|
|
17
|
+
await client.downloadFile(props.record.id as string | number, props.field.name, name.value)
|
|
18
|
+
} catch (error) {
|
|
19
|
+
notifications.error(vectorErrorMessage(error))
|
|
20
|
+
} finally {
|
|
21
|
+
busy.value = false
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const { t } = useVectorT()
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
<template>
|
|
28
|
+
<span v-if="!key" class="text-disabled">{{ t('common.none') }}</span>
|
|
29
|
+
<v-btn v-else size="small" variant="tonal" prepend-icon="mdi-download" :loading="busy" @click.stop="download">{{ context === 'show' ? name : t('common.download') }}</v-btn>
|
|
30
|
+
</template>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { VectorField, VectorResource } from '#vector/app/types/vector'
|
|
3
|
+
|
|
4
|
+
// A stored image as a thumbnail. The bytes come through the API client,
|
|
5
|
+
// so a bearer-authenticated admin sees them too; a key with no stored
|
|
6
|
+
// file (or no upload support) shows as its name.
|
|
7
|
+
const props = defineProps<{ field: VectorField, value: unknown, record: Record<string, unknown>, resource: VectorResource, context: 'index' | 'show' }>()
|
|
8
|
+
|
|
9
|
+
const client = useVectorResource(() => props.resource.slug)
|
|
10
|
+
const url = ref<string | null>(null)
|
|
11
|
+
const failed = ref(false)
|
|
12
|
+
const key = computed(() => (typeof props.value === 'string' && props.value !== '' ? props.value : null))
|
|
13
|
+
const size = computed(() => (props.context === 'show' ? 200 : 40))
|
|
14
|
+
|
|
15
|
+
watch([key, () => props.record.id], async () => {
|
|
16
|
+
url.value = null
|
|
17
|
+
failed.value = false
|
|
18
|
+
if (!key.value || props.record.id === undefined || props.record.id === null) return
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
url.value = await dataUrl(await client.fileBlob(props.record.id as string | number, props.field.name))
|
|
22
|
+
} catch {
|
|
23
|
+
failed.value = true
|
|
24
|
+
}
|
|
25
|
+
}, { immediate: true })
|
|
26
|
+
|
|
27
|
+
const { t } = useVectorT()
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<template>
|
|
31
|
+
<span v-if="!key" class="text-disabled">{{ t('common.none') }}</span>
|
|
32
|
+
<v-img v-else-if="url" :src="url" :width="size" :height="size" :alt="field.label" cover class="rounded" />
|
|
33
|
+
<span v-else-if="failed" class="d-inline-flex align-center ga-1 text-medium-emphasis" :title="key">
|
|
34
|
+
<v-icon icon="mdi-image-off-outline" size="small" />
|
|
35
|
+
<span class="text-caption">{{ key.split('/').pop() }}</span>
|
|
36
|
+
</span>
|
|
37
|
+
<v-progress-circular v-else indeterminate size="16" width="2" :aria-label="t('fields.cell.loading', { label: field.label })" />
|
|
38
|
+
</template>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
const props = defineProps<{ value: unknown, context: 'index' | 'show' }>()
|
|
3
|
+
const pretty = computed(() => {
|
|
4
|
+
if (typeof props.value !== 'string' || props.value === '') return ''
|
|
5
|
+
try {
|
|
6
|
+
return JSON.stringify(JSON.parse(props.value), null, props.context === 'show' ? 2 : 0)
|
|
7
|
+
} catch {
|
|
8
|
+
return props.value
|
|
9
|
+
}
|
|
10
|
+
})
|
|
11
|
+
const { t } = useVectorT()
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<template>
|
|
15
|
+
<span v-if="pretty === ''" class="text-disabled">{{ t('common.none') }}</span>
|
|
16
|
+
<pre v-else-if="context === 'show'" class="text-body-2 pa-3 rounded bg-surface-light overflow-auto">{{ pretty }}</pre>
|
|
17
|
+
<code v-else class="text-truncate d-inline-block" style="max-width: 24rem">{{ pretty }}</code>
|
|
18
|
+
</template>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// A key-value field: its first entries as chips in a list, every entry
|
|
3
|
+
// as a two-column table on a record.
|
|
4
|
+
const props = defineProps<{ value: unknown, context: 'index' | 'show' }>()
|
|
5
|
+
const entries = computed(() => (props.value && typeof props.value === 'object' && !Array.isArray(props.value)
|
|
6
|
+
? Object.entries(props.value as Record<string, unknown>).map(([key, value]) => [key, String(value ?? '')] as const)
|
|
7
|
+
: []))
|
|
8
|
+
const { t } = useVectorT()
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<template>
|
|
12
|
+
<span v-if="!entries.length" class="text-disabled">{{ t('common.none') }}</span>
|
|
13
|
+
<v-table v-else-if="context === 'show'" density="compact">
|
|
14
|
+
<tbody>
|
|
15
|
+
<tr v-for="[key, text] in entries" :key="key">
|
|
16
|
+
<th scope="row" class="text-medium-emphasis font-weight-regular">{{ key }}</th>
|
|
17
|
+
<td>{{ text }}</td>
|
|
18
|
+
</tr>
|
|
19
|
+
</tbody>
|
|
20
|
+
</v-table>
|
|
21
|
+
<span v-else class="d-inline-flex flex-wrap align-center ga-1">
|
|
22
|
+
<v-chip v-for="[key, text] in entries.slice(0, 3)" :key="key" size="x-small" label>{{ key }}: {{ text }}</v-chip>
|
|
23
|
+
<span v-if="entries.length > 3" class="text-caption text-medium-emphasis">{{ t('fields.keyValue.more', { count: entries.length - 3 }) }}</span>
|
|
24
|
+
</span>
|
|
25
|
+
</template>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Markdown source. On the record page it is rendered: markdown-it with HTML
|
|
3
|
+
// in the source turned off (raw HTML shows as text), then DOMPurify with the
|
|
4
|
+
// same allow-list as rich text — the only other `v-html` in the layer. In a
|
|
5
|
+
// list, the plain text, shortened.
|
|
6
|
+
const props = defineProps<{ value: unknown, context: 'index' | 'show' }>()
|
|
7
|
+
const text = computed(() => typeof props.value === 'string' ? props.value : '')
|
|
8
|
+
const html = computed(() => props.context === 'show' && text.value !== '' ? markdownHtml(text.value) : '')
|
|
9
|
+
const plain = computed(() => props.context === 'index' && text.value !== '' ? markdownPlain(text.value) : '')
|
|
10
|
+
const { t } = useVectorT()
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<template>
|
|
14
|
+
<span v-if="text === ''" class="text-disabled">{{ t('common.none') }}</span>
|
|
15
|
+
<!-- eslint-disable-next-line vue/no-v-html -- rendered with HTML off, then sanitized (see above) -->
|
|
16
|
+
<div v-else-if="context === 'show'" class="vx-richtext-view text-body-1" v-html="html" />
|
|
17
|
+
<span v-else class="text-truncate d-inline-block" style="max-width: 24rem">{{ plain }}</span>
|
|
18
|
+
</template>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { VectorField } from '#vector/app/types/vector'
|
|
3
|
+
|
|
4
|
+
const props = defineProps<{ field: VectorField, value: unknown, record: Record<string, unknown> }>()
|
|
5
|
+
const text = computed(() => {
|
|
6
|
+
if (typeof props.value !== 'number') return ''
|
|
7
|
+
const currency = currencyOf(props.field, props.record)
|
|
8
|
+
return currency ? formatMoney(props.value, currency) : formatNumber(props.value / 100, 2)
|
|
9
|
+
})
|
|
10
|
+
const { t } = useVectorT()
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<template>
|
|
14
|
+
<span v-if="text === ''" class="text-disabled">{{ t('common.none') }}</span>
|
|
15
|
+
<span v-else class="text-no-wrap font-weight-medium">{{ text }}</span>
|
|
16
|
+
</template>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { fieldProps, type VectorField } from '#vector/app/types/vector'
|
|
3
|
+
|
|
4
|
+
const props = defineProps<{ field: VectorField, value: unknown }>()
|
|
5
|
+
const text = computed(() => {
|
|
6
|
+
if (typeof props.value !== 'number') return ''
|
|
7
|
+
const decimals = fieldProps(props.field).decimals
|
|
8
|
+
return formatNumber(props.value, typeof decimals === 'number' ? decimals : undefined)
|
|
9
|
+
})
|
|
10
|
+
const { t } = useVectorT()
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<template>
|
|
14
|
+
<span v-if="text === ''" class="text-disabled">{{ t('common.none') }}</span>
|
|
15
|
+
<span v-else class="text-no-wrap">{{ text }}</span>
|
|
16
|
+
</template>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { VectorField, VectorRelationValue } from '#vector/app/types/vector'
|
|
3
|
+
|
|
4
|
+
const props = defineProps<{ field: VectorField, value: unknown }>()
|
|
5
|
+
const appConfig = useAppConfig()
|
|
6
|
+
const related = computed(() => (props.value && typeof props.value === 'object' && 'id' in props.value ? props.value as VectorRelationValue : null))
|
|
7
|
+
const to = computed(() => related.value && props.field.relation
|
|
8
|
+
? `${appConfig.vector.basePath}/r/${props.field.relation.resource}/${encodeURIComponent(String(related.value.id))}`
|
|
9
|
+
: undefined)
|
|
10
|
+
const { t } = useVectorT()
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<template>
|
|
14
|
+
<v-chip v-if="related" size="small" variant="outlined" :to="to" link @click.stop>{{ related.label }}</v-chip>
|
|
15
|
+
<span v-else class="text-disabled">{{ t('common.none') }}</span>
|
|
16
|
+
</template>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { VectorField, VectorResource } from '#vector/app/types/vector'
|
|
3
|
+
|
|
4
|
+
// A rich-text value. On the record page it is shown as HTML — and this is
|
|
5
|
+
// the one `v-html` in the layer. It is safe because the HTML is never the
|
|
6
|
+
// stored value: it is generated here from the document through the
|
|
7
|
+
// field's fixed schema (anything outside the schema cannot be expressed),
|
|
8
|
+
// then passed through DOMPurify with an allow-list matching that schema
|
|
9
|
+
// (no style, no event handlers, links only to http(s), mailto or relative
|
|
10
|
+
// places). Images carry their storage key and are fetched with the API's
|
|
11
|
+
// credentials. In a list, the plain text, shortened.
|
|
12
|
+
const props = defineProps<{ field: VectorField, value: unknown, record: Record<string, unknown>, resource: VectorResource, context: 'index' | 'show' }>()
|
|
13
|
+
|
|
14
|
+
const { t } = useVectorT()
|
|
15
|
+
const client = useVectorResource(() => props.resource.slug)
|
|
16
|
+
const { richText: custom } = useVectorExtensions()
|
|
17
|
+
const extensions = richTextExtensions({ features: props.field.richtext?.features ?? [], custom })
|
|
18
|
+
const text = computed(() => typeof props.value === 'string' ? props.value : '')
|
|
19
|
+
const html = computed(() => props.context === 'show' && text.value !== '' ? richTextHtml(text.value, extensions) : '')
|
|
20
|
+
const plain = computed(() => props.context === 'index' && text.value !== '' ? richTextPlain(text.value, extensions) : '')
|
|
21
|
+
|
|
22
|
+
const root = ref<HTMLElement | null>(null)
|
|
23
|
+
watch([html, root], async () => {
|
|
24
|
+
await nextTick()
|
|
25
|
+
const imageField = props.field.richtext?.imageField
|
|
26
|
+
const id = props.record.id
|
|
27
|
+
if (!root.value || !imageField || id === undefined || id === null) return
|
|
28
|
+
|
|
29
|
+
for (const img of root.value.querySelectorAll<HTMLImageElement>('img[data-vector-key]')) {
|
|
30
|
+
const key = img.getAttribute('data-vector-key')!
|
|
31
|
+
client.fileBlob(id as string | number, imageField, key).then(dataUrl).then((url) => { img.src = url }).catch(() => undefined)
|
|
32
|
+
}
|
|
33
|
+
}, { immediate: true })
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<template>
|
|
37
|
+
<span v-if="text === ''" class="text-disabled">{{ t('common.none') }}</span>
|
|
38
|
+
<!-- eslint-disable-next-line vue/no-v-html -- generated through the schema, then sanitized (see above) -->
|
|
39
|
+
<div v-else-if="context === 'show'" ref="root" class="vx-richtext-view text-body-1" v-html="html" />
|
|
40
|
+
<span v-else class="text-truncate d-inline-block" style="max-width: 24rem">{{ plain }}</span>
|
|
41
|
+
</template>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { VectorField } from '#vector/app/types/vector'
|
|
3
|
+
|
|
4
|
+
const props = defineProps<{ field: VectorField, value: unknown, context: 'index' | 'show' }>()
|
|
5
|
+
const text = computed(() => props.value === null || props.value === undefined ? '' : String(props.value))
|
|
6
|
+
const truncated = computed(() => props.context === 'index' && text.value.length > 80 ? text.value.slice(0, 77) + '…' : text.value)
|
|
7
|
+
const { t } = useVectorT()
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
<template>
|
|
11
|
+
<span v-if="text === ''" class="text-disabled">{{ t('common.none') }}</span>
|
|
12
|
+
<span v-else :title="truncated !== text ? text : undefined">{{ truncated }}</span>
|
|
13
|
+
</template>
|