@kinetis/vector-ui 0.0.0-stage → 0.9.1
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 +34 -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 +83 -5
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import type { VectorActionResponse, VectorAuditEntry, VectorExportFormat, VectorFilterQuery, VectorImportMode, VectorImportPreview, VectorPage, VectorRecord, VectorRecordPermissions, VectorRelationValue, VectorSelection, VectorSort, VectorUploaded, VectorWidgetData } from '#vector/app/types/vector'
|
|
2
|
+
|
|
3
|
+
export interface VectorListOptions {
|
|
4
|
+
page?: number
|
|
5
|
+
perPage?: number
|
|
6
|
+
sort?: VectorSort[]
|
|
7
|
+
filter?: VectorFilterQuery
|
|
8
|
+
q?: string
|
|
9
|
+
group?: string
|
|
10
|
+
summaries?: boolean
|
|
11
|
+
columns?: string[]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface VectorRecordResponse {
|
|
15
|
+
data: VectorRecord
|
|
16
|
+
permissions: VectorRecordPermissions
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** `filter[status][eq]=active`: the bracketed keys PHP reads back as a nested array. */
|
|
20
|
+
export function filterQuery(filter: VectorFilterQuery | undefined, prefix = 'filter'): Record<string, string> {
|
|
21
|
+
const query: Record<string, string> = {}
|
|
22
|
+
|
|
23
|
+
for (const [key, spec] of Object.entries(filter ?? {})) {
|
|
24
|
+
if (spec === undefined) continue
|
|
25
|
+
|
|
26
|
+
if (Array.isArray(spec)) {
|
|
27
|
+
// or / and: filter objects of their own, by position
|
|
28
|
+
spec.forEach((child, index) => Object.assign(query, filterQuery(child, `${prefix}[${key}][${index}]`)))
|
|
29
|
+
} else if (typeof spec === 'string') {
|
|
30
|
+
query[`${prefix}[${key}]`] = spec
|
|
31
|
+
} else {
|
|
32
|
+
for (const [op, value] of Object.entries(spec)) {
|
|
33
|
+
if (value !== undefined) query[`${prefix}[${key}][${op}]`] = value
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return query
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The record endpoints for one resource. */
|
|
42
|
+
export function useVectorResource(slug: MaybeRefOrGetter<string>) {
|
|
43
|
+
const api = useVectorApi()
|
|
44
|
+
|
|
45
|
+
function base() {
|
|
46
|
+
return `/vector/r/${encodeURIComponent(toValue(slug))}`
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function listQuery(options: VectorListOptions): Record<string, string | number> {
|
|
50
|
+
const query: Record<string, string | number> = {}
|
|
51
|
+
|
|
52
|
+
if (options.page) {
|
|
53
|
+
query.page = options.page
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (options.perPage) {
|
|
57
|
+
query.perPage = options.perPage
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (options.sort && options.sort.length > 0) {
|
|
61
|
+
query.sort = options.sort.map(s => (s.order === 'desc' ? '-' : '') + s.field).join(',')
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (options.q) {
|
|
65
|
+
query.q = options.q
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (options.group) {
|
|
69
|
+
query.group = options.group
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (options.summaries) {
|
|
73
|
+
query.summaries = '1'
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (options.columns && options.columns.length > 0) {
|
|
77
|
+
query.columns = options.columns.join(',')
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return { ...query, ...filterQuery(options.filter) }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function list(options: VectorListOptions = {}): Promise<VectorPage> {
|
|
84
|
+
return api<VectorPage>(base(), { query: listQuery(options) })
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function related(id: string | number, relation: string, options: VectorListOptions = {}): Promise<VectorPage> {
|
|
88
|
+
return api<VectorPage>(`${base()}/${encodeURIComponent(String(id))}/${encodeURIComponent(relation)}`, { query: listQuery(options) })
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function show(id: string | number): Promise<VectorRecordResponse> {
|
|
92
|
+
return api<VectorRecordResponse>(`${base()}/${encodeURIComponent(String(id))}`)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function create(values: Record<string, unknown>): Promise<VectorRecordResponse> {
|
|
96
|
+
return api<VectorRecordResponse>(base(), { method: 'POST', body: values })
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function update(id: string | number, values: Record<string, unknown>, version?: string | number | null): Promise<VectorRecordResponse> {
|
|
100
|
+
return api<VectorRecordResponse>(`${base()}/${encodeURIComponent(String(id))}`, {
|
|
101
|
+
method: 'PATCH',
|
|
102
|
+
body: values,
|
|
103
|
+
headers: version === undefined || version === null ? {} : { 'If-Match': `"${version}"` },
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function remove(id: string | number): Promise<void> {
|
|
108
|
+
await api(`${base()}/${encodeURIComponent(String(id))}`, { method: 'DELETE' })
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function removeMany(ids: (string | number)[]): Promise<{ deleted: number }> {
|
|
112
|
+
return api<{ deleted: number }>(`${base()}/batch`, { method: 'POST', body: { action: 'delete', ids } })
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function batch(action: 'delete' | 'restore' | 'forceDelete' | 'update', selection: VectorSelection, fields?: Record<string, unknown>): Promise<{ affected: number }> {
|
|
116
|
+
return api<{ affected: number }>(`${base()}/batch`, { method: 'POST', body: { action, ...selection, ...(fields ? { fields } : {}) } })
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** The list's changeset: different values for different records, in one transaction. */
|
|
120
|
+
function changes(list: { id: number | string, version?: number, values: Record<string, unknown> }[]): Promise<{ data: VectorRecord[] }> {
|
|
121
|
+
return api<{ data: VectorRecord[] }>(`${base()}/changes`, { method: 'POST', body: { changes: list } })
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Stores one file for a file or image field; the answer's key is the value to write. */
|
|
125
|
+
function upload(field: string, file: File): Promise<VectorUploaded> {
|
|
126
|
+
const body = new FormData()
|
|
127
|
+
body.append('file', file)
|
|
128
|
+
return api<VectorUploaded>(`${base()}/fields/${encodeURIComponent(field)}/upload`, { method: 'POST', body })
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* A record's stored file, fetched with the API's credentials (an <img src> could not send a bearer token);
|
|
133
|
+
* with `key`, an image one of its rich-text documents shows from that field's uploads.
|
|
134
|
+
*/
|
|
135
|
+
function fileBlob(id: string | number, field: string, key?: string): Promise<Blob> {
|
|
136
|
+
return api<Blob>(`${base()}/${encodeURIComponent(String(id))}/files/${encodeURIComponent(field)}`, { responseType: 'blob', query: key ? { key } : undefined })
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function downloadFile(id: string | number, field: string, fallbackName: string): Promise<void> {
|
|
140
|
+
const response = await asFile(() => api.raw(`${base()}/${encodeURIComponent(String(id))}/files/${encodeURIComponent(field)}`, { responseType: 'blob' }))
|
|
141
|
+
saveBlob(response._data as Blob, filenameOf(response.headers.get('content-disposition')) ?? fallbackName)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function attach(id: string | number, relation: string, ids: (string | number)[]): Promise<VectorRecordResponse> {
|
|
145
|
+
return api<VectorRecordResponse>(`${base()}/${encodeURIComponent(String(id))}/${encodeURIComponent(relation)}/attach`, { method: 'POST', body: { ids } })
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function detach(id: string | number, relation: string, ids: (string | number)[]): Promise<VectorRecordResponse> {
|
|
149
|
+
return api<VectorRecordResponse>(`${base()}/${encodeURIComponent(String(id))}/${encodeURIComponent(relation)}/detach`, { method: 'POST', body: { ids } })
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function forceRemove(id: string | number): Promise<void> {
|
|
153
|
+
await api(`${base()}/${encodeURIComponent(String(id))}`, { method: 'DELETE', query: { force: '1' } })
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function restore(id: string | number): Promise<VectorRecordResponse> {
|
|
157
|
+
return api<VectorRecordResponse>(`${base()}/${encodeURIComponent(String(id))}/restore`, { method: 'POST' })
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Runs an action. A download answers with a file, which lands in the
|
|
162
|
+
* browser's download list; anything else is the JSON envelope. An
|
|
163
|
+
* action that declines answers 422 with `type: 'danger'`, which is an
|
|
164
|
+
* outcome for the dialog to show, not a failure.
|
|
165
|
+
*/
|
|
166
|
+
async function runAction(name: string, selection: VectorSelection | null, fields: Record<string, unknown>): Promise<VectorActionResponse> {
|
|
167
|
+
let response
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
response = await asFile(() => api.raw(`${base()}/actions/${encodeURIComponent(name)}`, {
|
|
171
|
+
method: 'POST',
|
|
172
|
+
body: { ...(selection ?? {}), fields },
|
|
173
|
+
responseType: 'blob',
|
|
174
|
+
}))
|
|
175
|
+
} catch (error) {
|
|
176
|
+
const declined = (error as { status?: number, data?: { type?: string } } | null)
|
|
177
|
+
|
|
178
|
+
if (declined?.status === 422 && declined.data?.type === 'danger') {
|
|
179
|
+
return declined.data as VectorActionResponse
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
throw error
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const type = response.headers.get('content-type') ?? ''
|
|
186
|
+
|
|
187
|
+
if (!type.includes('json')) {
|
|
188
|
+
saveBlob(response._data as Blob, filenameOf(response.headers.get('content-disposition')) ?? `${name}.csv`)
|
|
189
|
+
|
|
190
|
+
return { type: 'download', message: null, url: null, affected: 0 }
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return JSON.parse(await (response._data as Blob).text()) as VectorActionResponse
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Downloads the CSV, or answers the job the server queued for a large one. */
|
|
197
|
+
/** A CSV above the inline limit, and every XLSX, comes back as a queued job instead of a file. */
|
|
198
|
+
async function exportFile(options: VectorListOptions = {}, format: VectorExportFormat = 'csv'): Promise<{ jobId: number } | null> {
|
|
199
|
+
const response = await asFile(() => api.raw(`${base()}/export`, { query: { ...listQuery({ ...options, page: undefined, perPage: undefined }), format }, responseType: 'blob' }))
|
|
200
|
+
|
|
201
|
+
if (response.status === 202) {
|
|
202
|
+
const queued = JSON.parse(await (response._data as Blob).text()) as { jobId: number }
|
|
203
|
+
|
|
204
|
+
return { jobId: queued.jobId }
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
saveBlob(response._data as Blob, filenameOf(response.headers.get('content-disposition')) ?? `${toValue(slug)}.${format}`)
|
|
208
|
+
|
|
209
|
+
return null
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const exportCsv = (options: VectorListOptions = {}) => exportFile(options, 'csv')
|
|
213
|
+
|
|
214
|
+
function audit(id: string | number): Promise<{ data: VectorAuditEntry[] }> {
|
|
215
|
+
return api<{ data: VectorAuditEntry[] }>(`${base()}/${encodeURIComponent(String(id))}/audit`)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** The rows a repeater edits: every child of the record, as the child resource shows one. */
|
|
219
|
+
function rows(id: string | number, field: string): Promise<{ data: VectorRecord[] }> {
|
|
220
|
+
return api<{ data: VectorRecord[] }>(`${base()}/${encodeURIComponent(String(id))}/${encodeURIComponent(field)}/rows`)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Puts back what one history entry changed; 409 when a field changed since. */
|
|
224
|
+
function revert(id: string | number, entryId: number): Promise<VectorRecordResponse> {
|
|
225
|
+
return api<VectorRecordResponse>(`${base()}/${encodeURIComponent(String(id))}/audit/${entryId}/revert`, { method: 'POST' })
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function widget(name: string, range?: string): Promise<{ data: VectorWidgetData }> {
|
|
229
|
+
return api<{ data: VectorWidgetData }>(`${base()}/widgets/${encodeURIComponent(name)}`, { query: range ? { range } : {} })
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function importPreview(file: File): Promise<VectorImportPreview> {
|
|
233
|
+
const body = new FormData()
|
|
234
|
+
body.append('file', file)
|
|
235
|
+
|
|
236
|
+
return api<VectorImportPreview>(`${base()}/import/preview`, { method: 'POST', body })
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** `mode` update or upsert matches each row to a record by `key`, a mapped key or unique field. */
|
|
240
|
+
function importCsv(file: File, mapping: Record<string, string>, mode: VectorImportMode = 'create', key = ''): Promise<{ jobId: number, rows: number }> {
|
|
241
|
+
const body = new FormData()
|
|
242
|
+
body.append('file', file)
|
|
243
|
+
body.append('mapping', JSON.stringify(mapping))
|
|
244
|
+
body.append('mode', mode)
|
|
245
|
+
if (mode !== 'create') body.append('key', key)
|
|
246
|
+
|
|
247
|
+
return api<{ jobId: number, rows: number }>(`${base()}/import`, { method: 'POST', body })
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** `depends` carries the form's values a dependent choice list narrows by. */
|
|
251
|
+
function options(field: string, search: string, ids: (string | number)[] = [], depends: Record<string, string | number> = {}): Promise<{ data: VectorRelationValue[] }> {
|
|
252
|
+
const narrowing = Object.fromEntries(Object.entries(depends).map(([name, value]) => [`depends[${name}]`, String(value)]))
|
|
253
|
+
|
|
254
|
+
return api<{ data: VectorRelationValue[] }>(`${base()}/fields/${encodeURIComponent(field)}/options`, {
|
|
255
|
+
query: ids.length > 0 ? { ids: ids.join(',') } : { q: search, ...narrowing },
|
|
256
|
+
})
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return { list, related, show, create, update, remove, removeMany, batch, changes, attach, detach, upload, fileBlob, downloadFile, forceRemove, restore, runAction, exportCsv, exportFile, audit, revert, rows, widget, importPreview, importCsv, options }
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Runs a request made with `responseType: 'blob'`. ofetch reads the body
|
|
264
|
+
* as a Blob before it looks at the status, so a 4xx arrives as a Blob of
|
|
265
|
+
* JSON; this reads that JSON back onto the error, so `status` and `data`
|
|
266
|
+
* mean what they mean on every other request (422 violations included).
|
|
267
|
+
*/
|
|
268
|
+
export async function asFile<T>(request: () => Promise<T>): Promise<T> {
|
|
269
|
+
try {
|
|
270
|
+
return await request()
|
|
271
|
+
} catch (error) {
|
|
272
|
+
const failure = error as { data?: unknown } | null
|
|
273
|
+
|
|
274
|
+
if (failure?.data instanceof Blob) {
|
|
275
|
+
const text = await failure.data.text()
|
|
276
|
+
|
|
277
|
+
try {
|
|
278
|
+
failure.data = JSON.parse(text)
|
|
279
|
+
} catch {
|
|
280
|
+
failure.data = { error: text }
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
throw error
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function filenameOf(disposition: string | null): string | undefined {
|
|
289
|
+
const match = disposition?.match(/filename="([^"]+)"/)
|
|
290
|
+
|
|
291
|
+
return match?.[1]
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function saveBlob(blob: Blob, filename: string) {
|
|
295
|
+
if (!import.meta.client) return
|
|
296
|
+
|
|
297
|
+
const url = URL.createObjectURL(blob)
|
|
298
|
+
const link = document.createElement('a')
|
|
299
|
+
link.href = url
|
|
300
|
+
link.download = filename
|
|
301
|
+
document.body.appendChild(link)
|
|
302
|
+
link.click()
|
|
303
|
+
link.remove()
|
|
304
|
+
setTimeout(() => URL.revokeObjectURL(url), 1000)
|
|
305
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How this UI was deployed. In hosted mode the PHP application serves the
|
|
3
|
+
* shell page and injects `window.__VECTOR__` before the bundle loads:
|
|
4
|
+
* same-origin API, the application's session cookie for authentication,
|
|
5
|
+
* a CSRF token for writes, and where its login page lives. Standalone
|
|
6
|
+
* mode has no such object and authenticates with a bearer token against
|
|
7
|
+
* the API origin runtime config names.
|
|
8
|
+
*/
|
|
9
|
+
export interface VectorHostedConfig {
|
|
10
|
+
apiBase: string
|
|
11
|
+
basePath: string
|
|
12
|
+
authMode: 'cookie'
|
|
13
|
+
loginUrl: string
|
|
14
|
+
logoutUrl: string
|
|
15
|
+
csrfToken: string | null
|
|
16
|
+
cspNonce: string | null
|
|
17
|
+
user: { id: number | string, name: string | null } | null
|
|
18
|
+
branding: import('#vector/app/types/vector').VectorBranding | null
|
|
19
|
+
extensions: string[]
|
|
20
|
+
/** The language the server resolved for this user: the shell starts in it. */
|
|
21
|
+
locale?: string | null
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
declare global {
|
|
25
|
+
interface Window {
|
|
26
|
+
__VECTOR__?: VectorHostedConfig
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function useVectorRuntime() {
|
|
31
|
+
const hosted = import.meta.client ? window.__VECTOR__ ?? null : null
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
hosted: hosted !== null,
|
|
35
|
+
authMode: hosted ? 'cookie' as const : 'bearer' as const,
|
|
36
|
+
apiBase: hosted?.apiBase ?? null,
|
|
37
|
+
loginUrl: hosted?.loginUrl ?? null,
|
|
38
|
+
logoutUrl: hosted?.logoutUrl ?? null,
|
|
39
|
+
csrfToken: hosted?.csrfToken ?? null,
|
|
40
|
+
user: hosted?.user ?? null,
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { VectorResource, VectorSchema } from '#vector/app/types/vector'
|
|
2
|
+
|
|
3
|
+
// The latest descriptor request: an older answer arriving after it (the
|
|
4
|
+
// language switched twice, say) must not replace it. One app per page.
|
|
5
|
+
let latestLoad = 0
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The descriptor, loaded once per session and shared by every page. The
|
|
9
|
+
* server's ETag makes a reload cheap; the layer reloads it after login
|
|
10
|
+
* because the permission overlay is per identity.
|
|
11
|
+
*/
|
|
12
|
+
export function useVectorSchema() {
|
|
13
|
+
const schema = useState<VectorSchema | null>('vector.schema', () => null)
|
|
14
|
+
const loading = useState<boolean>('vector.schema.loading', () => false)
|
|
15
|
+
const api = useVectorApi()
|
|
16
|
+
|
|
17
|
+
async function load(force = false): Promise<VectorSchema> {
|
|
18
|
+
if (schema.value && !force) {
|
|
19
|
+
adoptMinorUnits(schema.value.minorUnits ?? {})
|
|
20
|
+
return schema.value
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
loading.value = true
|
|
24
|
+
const request = ++latestLoad
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const answer = await api<VectorSchema>('/vector/schema')
|
|
28
|
+
|
|
29
|
+
if (request === latestLoad) {
|
|
30
|
+
schema.value = answer
|
|
31
|
+
adoptMinorUnits(answer.minorUnits ?? {})
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return answer
|
|
35
|
+
} finally {
|
|
36
|
+
if (request === latestLoad) loading.value = false
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function clear() {
|
|
41
|
+
schema.value = null
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function resource(slug: string): VectorResource | undefined {
|
|
45
|
+
return schema.value?.resources[slug]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const menu = computed(() => (schema.value?.menu ?? []).map(group => ({
|
|
49
|
+
label: group.label,
|
|
50
|
+
items: group.resources
|
|
51
|
+
.map(slug => schema.value!.resources[slug])
|
|
52
|
+
.filter((resource): resource is VectorResource => resource !== undefined),
|
|
53
|
+
})))
|
|
54
|
+
|
|
55
|
+
return { schema, loading, load, clear, resource, menu }
|
|
56
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The admin's translation function for a component or composable:
|
|
3
|
+
* `const { t } = useVectorT()`, then `t('list.selected', { count }, count)`.
|
|
4
|
+
* `noun(label)` writes a resource label into a sentence in the current
|
|
5
|
+
* language's case. The same functions are `vt()` and `vnoun()` anywhere else.
|
|
6
|
+
*/
|
|
7
|
+
export function useVectorT() {
|
|
8
|
+
return { t: vt, noun: vnoun, locale: useNuxtApp().$vectorI18n.global.locale }
|
|
9
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { VectorListState, VectorSavedView } from '#vector/app/types/vector'
|
|
2
|
+
|
|
3
|
+
/** Saved views of one resource's list. */
|
|
4
|
+
export function useVectorViews(slug: MaybeRefOrGetter<string>) {
|
|
5
|
+
const api = useVectorApi()
|
|
6
|
+
const base = () => `/vector/views/${encodeURIComponent(toValue(slug))}`
|
|
7
|
+
|
|
8
|
+
async function list(): Promise<VectorSavedView[]> {
|
|
9
|
+
return (await api<{ data: VectorSavedView[] }>(base())).data
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function create(name: string, state: VectorListState, shared: boolean, isDefault: boolean, resourceDefault = false): Promise<VectorSavedView> {
|
|
13
|
+
return (await api<{ data: VectorSavedView }>(base(), { method: 'POST', body: { name, state, shared, isDefault, resourceDefault } })).data
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function update(id: number, name: string, state: VectorListState, shared: boolean, isDefault: boolean, resourceDefault = false): Promise<VectorSavedView> {
|
|
17
|
+
return (await api<{ data: VectorSavedView }>(`${base()}/${id}`, { method: 'PUT', body: { name, state, shared, isDefault, resourceDefault } })).data
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function remove(id: number): Promise<void> {
|
|
21
|
+
await api(`${base()}/${id}`, { method: 'DELETE' })
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return { list, create, update, remove }
|
|
25
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// The actions area's strings; see common.ts for words every area shares.
|
|
2
|
+
export default {
|
|
3
|
+
// "Run {action} on {subject}?", with the action's label in bold.
|
|
4
|
+
runOn: 'تنفيذ {action} على {subject}؟',
|
|
5
|
+
subjectAll: 'جميع {label} المطابقة ({count})',
|
|
6
|
+
subjectCount: '{count} {label}',
|
|
7
|
+
done: 'تم تنفيذ {label}.',
|
|
8
|
+
declined: 'رُفض الإجراء.',
|
|
9
|
+
downloadStarted: 'بدأ التنزيل.',
|
|
10
|
+
queued: 'أُضيف {label} إلى قائمة الانتظار.',
|
|
11
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Words every area shares. Keep a key here only when two areas or more use it.
|
|
2
|
+
export default {
|
|
3
|
+
save: 'حفظ',
|
|
4
|
+
cancel: 'إلغاء',
|
|
5
|
+
create: 'إنشاء',
|
|
6
|
+
edit: 'تعديل',
|
|
7
|
+
delete: 'حذف',
|
|
8
|
+
deleteForever: 'حذف نهائي',
|
|
9
|
+
restore: 'استعادة',
|
|
10
|
+
close: 'إغلاق',
|
|
11
|
+
apply: 'تطبيق',
|
|
12
|
+
remove: 'إزالة',
|
|
13
|
+
search: 'بحث',
|
|
14
|
+
loading: 'جارٍ التحميل…',
|
|
15
|
+
required: 'مطلوب',
|
|
16
|
+
none: '—',
|
|
17
|
+
yes: 'نعم',
|
|
18
|
+
no: 'لا',
|
|
19
|
+
download: 'تنزيل',
|
|
20
|
+
actions: 'الإجراءات',
|
|
21
|
+
confirm: 'تأكيد',
|
|
22
|
+
serverRejected: 'رفض الخادم الطلب.',
|
|
23
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// The dashboard area's strings; see common.ts for words every area shares.
|
|
2
|
+
export default {
|
|
3
|
+
title: 'لوحة المعلومات',
|
|
4
|
+
range: 'النطاق',
|
|
5
|
+
loading: 'جارٍ التحميل',
|
|
6
|
+
percent: '{value}%',
|
|
7
|
+
duration: {
|
|
8
|
+
minutes: '{value} دقيقة',
|
|
9
|
+
hours: '{value} ساعة',
|
|
10
|
+
},
|
|
11
|
+
partition: {
|
|
12
|
+
empty: 'لا شيء في هذا النطاق.',
|
|
13
|
+
},
|
|
14
|
+
progress: {
|
|
15
|
+
of: 'من {target}',
|
|
16
|
+
label: 'التقدم',
|
|
17
|
+
},
|
|
18
|
+
stat: {
|
|
19
|
+
previous: 'مقابل {value} سابقًا',
|
|
20
|
+
},
|
|
21
|
+
table: {
|
|
22
|
+
empty: 'لا شيء لعرضه.',
|
|
23
|
+
seeAll: 'عرض الكل',
|
|
24
|
+
},
|
|
25
|
+
trend: {
|
|
26
|
+
total: 'الإجمالي {value}',
|
|
27
|
+
},
|
|
28
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// The fields area's strings; see common.ts for words every area shares.
|
|
2
|
+
export default {
|
|
3
|
+
cell: {
|
|
4
|
+
loading: 'جارٍ تحميل {label}',
|
|
5
|
+
},
|
|
6
|
+
keyValue: {
|
|
7
|
+
key: 'المفتاح',
|
|
8
|
+
value: 'القيمة',
|
|
9
|
+
entry: 'الإدخال {n}',
|
|
10
|
+
add: 'إضافة إدخال',
|
|
11
|
+
remove: 'إزالة الإدخال {n}',
|
|
12
|
+
more: '+{count} أخرى',
|
|
13
|
+
repeated: 'مستخدم أكثر من مرة: {keys}. يُحتفظ بالأخير.',
|
|
14
|
+
},
|
|
15
|
+
repeater: {
|
|
16
|
+
add: 'إضافة {label}',
|
|
17
|
+
remove: 'إزالة {label} {n}',
|
|
18
|
+
row: '{label} {n}',
|
|
19
|
+
count: '{count} من {max} كحد أقصى',
|
|
20
|
+
unavailable: 'لا يمكنك رؤية هذه الصفوف.',
|
|
21
|
+
},
|
|
22
|
+
relation: {
|
|
23
|
+
placeholder: 'اكتب للبحث',
|
|
24
|
+
dependsFirst: 'يُرجى اختيار {label} أولًا',
|
|
25
|
+
},
|
|
26
|
+
upload: {
|
|
27
|
+
storageKey: 'مفتاح تخزين الملف.',
|
|
28
|
+
tooLarge: 'حجم الملف أكبر من {size} ميغابايت.',
|
|
29
|
+
rejected: 'الملف {message}',
|
|
30
|
+
uploaded: 'تم رفع {name} ({size} كيلوبايت)',
|
|
31
|
+
theFile: 'الملف',
|
|
32
|
+
upTo: 'حتى {size} ميغابايت',
|
|
33
|
+
preview: 'معاينة {label}',
|
|
34
|
+
remove: 'إزالة {label}',
|
|
35
|
+
},
|
|
36
|
+
richtext: {
|
|
37
|
+
toolbar: 'تنسيق {label}',
|
|
38
|
+
bold: 'غامق',
|
|
39
|
+
italic: 'مائل',
|
|
40
|
+
underline: 'تسطير',
|
|
41
|
+
strike: 'يتوسطه خط',
|
|
42
|
+
code: 'تعليمات برمجية',
|
|
43
|
+
heading: 'عنوان {level}',
|
|
44
|
+
bulletList: 'قائمة نقطية',
|
|
45
|
+
orderedList: 'قائمة مرقمة',
|
|
46
|
+
blockquote: 'اقتباس',
|
|
47
|
+
codeBlock: 'كتلة تعليمات برمجية',
|
|
48
|
+
horizontalRule: 'فاصل',
|
|
49
|
+
link: 'رابط',
|
|
50
|
+
linkUrl: 'عنوان الرابط',
|
|
51
|
+
linkRefused: 'يُرجى استخدام رابط http(s) أو mailto أو رابط نسبي',
|
|
52
|
+
insertTable: 'إدراج جدول',
|
|
53
|
+
addRow: 'إضافة صف',
|
|
54
|
+
addColumn: 'إضافة عمود',
|
|
55
|
+
deleteRow: 'حذف الصف',
|
|
56
|
+
deleteColumn: 'حذف العمود',
|
|
57
|
+
deleteTable: 'حذف الجدول',
|
|
58
|
+
image: 'إدراج صورة',
|
|
59
|
+
undo: 'تراجع',
|
|
60
|
+
redo: 'إعادة',
|
|
61
|
+
source: 'المصدر',
|
|
62
|
+
visual: 'مرئي',
|
|
63
|
+
sourceOf: 'مصدر {label}',
|
|
64
|
+
},
|
|
65
|
+
rules: {
|
|
66
|
+
maxLength: 'بحد أقصى {max} من الأحرف',
|
|
67
|
+
minLength: 'بحد أدنى {min} من الأحرف',
|
|
68
|
+
email: 'يُرجى إدخال عنوان بريد إلكتروني صالح',
|
|
69
|
+
url: 'يُرجى إدخال عنوان URL صالح',
|
|
70
|
+
greaterThan: 'يجب أن تكون القيمة أكبر من {limit}',
|
|
71
|
+
greaterThanOrEqual: 'يجب ألا تقل القيمة عن {limit}',
|
|
72
|
+
lessThan: 'يجب أن تكون القيمة أقل من {limit}',
|
|
73
|
+
lessThanOrEqual: 'يجب ألا تزيد القيمة على {limit}',
|
|
74
|
+
invalidFormat: 'تنسيق غير صالح',
|
|
75
|
+
notAllowed: 'قيمة غير مسموح بها',
|
|
76
|
+
},
|
|
77
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import actions from './actions'
|
|
2
|
+
import common from './common'
|
|
3
|
+
import dashboard from './dashboard'
|
|
4
|
+
import fields from './fields'
|
|
5
|
+
import jobs from './jobs'
|
|
6
|
+
import list from './list'
|
|
7
|
+
import record from './record'
|
|
8
|
+
import settings from './settings'
|
|
9
|
+
import shell from './shell'
|
|
10
|
+
|
|
11
|
+
/** Arabic (machine translation): one object per area of the UI, mirroring en. */
|
|
12
|
+
export default { common, shell, list, record, actions, dashboard, jobs, settings, fields }
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// The jobs area's strings; see common.ts for words every area shares.
|
|
2
|
+
export default {
|
|
3
|
+
title: 'المهام',
|
|
4
|
+
heading: 'مهام الخلفية',
|
|
5
|
+
columns: {
|
|
6
|
+
job: 'المهمة',
|
|
7
|
+
resource: 'المورد',
|
|
8
|
+
status: 'الحالة',
|
|
9
|
+
progress: 'التقدم',
|
|
10
|
+
started: 'وقت البدء',
|
|
11
|
+
},
|
|
12
|
+
empty: 'لا توجد مهام بعد. تظهر هنا عمليات التصدير والاستيراد الكبيرة والإجراءات الموضوعة في قائمة الانتظار.',
|
|
13
|
+
kind: {
|
|
14
|
+
action: 'إجراء',
|
|
15
|
+
export: 'تصدير',
|
|
16
|
+
import: 'استيراد',
|
|
17
|
+
},
|
|
18
|
+
status: {
|
|
19
|
+
queued: 'في قائمة الانتظار',
|
|
20
|
+
running: 'قيد التشغيل',
|
|
21
|
+
done: 'مكتملة',
|
|
22
|
+
failed: 'فاشلة',
|
|
23
|
+
stalled: 'متوقفة',
|
|
24
|
+
},
|
|
25
|
+
detail: {
|
|
26
|
+
pageTitle: 'المهمة #{id}',
|
|
27
|
+
fallbackTitle: 'مهمة',
|
|
28
|
+
started: 'بدأت {date}',
|
|
29
|
+
finished: 'انتهت {date}',
|
|
30
|
+
progress: 'تقدم المهمة',
|
|
31
|
+
processed: 'تمت معالجة {processed}',
|
|
32
|
+
processedOfTotal: 'تمت معالجة {processed} من {total}',
|
|
33
|
+
download: 'تنزيل {file}',
|
|
34
|
+
file: 'الملف',
|
|
35
|
+
},
|
|
36
|
+
}
|