@peanut-admin/admin 0.1.0-alpha.11

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 (73) hide show
  1. package/LICENSE +202 -0
  2. package/admin-core/src/access/access.ts +27 -0
  3. package/admin-core/src/access/permission-policy.ts +37 -0
  4. package/admin-core/src/api/client.ts +200 -0
  5. package/admin-core/src/api/problem.ts +67 -0
  6. package/admin-core/src/api/refresh.ts +122 -0
  7. package/admin-core/src/auth/stores.ts +121 -0
  8. package/admin-core/src/auth/tenant-session.ts +32 -0
  9. package/admin-core/src/generated/api.d.ts +22106 -0
  10. package/admin-core/src/governance/audit.ts +56 -0
  11. package/admin-core/src/governance/catalog.ts +86 -0
  12. package/admin-core/src/governance/index.ts +26 -0
  13. package/admin-core/src/governance/menu.ts +63 -0
  14. package/admin-core/src/governance/roles.ts +144 -0
  15. package/admin-core/src/governance/types.ts +64 -0
  16. package/admin-core/src/index.ts +122 -0
  17. package/admin-core/src/lifecycle/tenant.ts +59 -0
  18. package/admin-core/src/module/contribution.ts +124 -0
  19. package/admin-core/src/module/plugin-contribution-policy.ts +51 -0
  20. package/admin-core/src/module/tenant-modules.ts +20 -0
  21. package/admin-core/src/runtime/config.ts +55 -0
  22. package/admin-core/src/runtime/errors.ts +81 -0
  23. package/admin-core/src/runtime/guard.ts +40 -0
  24. package/admin-core/src/runtime/navigation.ts +105 -0
  25. package/admin-core/src/runtime/overrides.ts +214 -0
  26. package/admin-core/src/targets/store.ts +153 -0
  27. package/admin-shell/src/config.ts +84 -0
  28. package/admin-shell/src/deployment-mode.ts +36 -0
  29. package/admin-shell/src/index.ts +44 -0
  30. package/admin-shell/src/layout.ts +332 -0
  31. package/admin-shell/src/overrides.ts +53 -0
  32. package/admin-shell/src/states.ts +93 -0
  33. package/admin-shell/src/tabs.ts +31 -0
  34. package/admin-shell/src/targets.ts +128 -0
  35. package/admin-shell/src/theme.ts +15 -0
  36. package/client-core/src/index.ts +415 -0
  37. package/client-nuxt/src/index.ts +48 -0
  38. package/client-uniapp/src/index.ts +50 -0
  39. package/file-media/src/FileAssetSelector.vue +117 -0
  40. package/file-media/src/FileMediaPage.vue +158 -0
  41. package/file-media/src/contracts.ts +220 -0
  42. package/file-media/src/index.ts +19 -0
  43. package/file-media/src/runtime.ts +210 -0
  44. package/import-export/src/ImportExportPage.vue +155 -0
  45. package/import-export/src/contracts.ts +96 -0
  46. package/import-export/src/index.ts +3 -0
  47. package/import-export/src/runtime.ts +128 -0
  48. package/integration-security/src/IntegrationSecurityPage.vue +402 -0
  49. package/integration-security/src/contracts.ts +171 -0
  50. package/integration-security/src/index.ts +3 -0
  51. package/integration-security/src/runtime.ts +180 -0
  52. package/notification-sms/src/NotificationInboxPage.vue +266 -0
  53. package/notification-sms/src/contracts.ts +195 -0
  54. package/notification-sms/src/index.ts +4 -0
  55. package/notification-sms/src/runtime.ts +143 -0
  56. package/ops-console/src/OpsConsolePage.vue +337 -0
  57. package/ops-console/src/contracts.ts +169 -0
  58. package/ops-console/src/index.ts +3 -0
  59. package/ops-console/src/runtime.ts +199 -0
  60. package/package.json +139 -0
  61. package/reference-codes/src/ReferenceCodesPage.vue +942 -0
  62. package/reference-codes/src/contracts.ts +484 -0
  63. package/reference-codes/src/index.ts +53 -0
  64. package/reference-codes/src/runtime.ts +855 -0
  65. package/settings/src/SettingsPage.vue +536 -0
  66. package/settings/src/contracts.ts +331 -0
  67. package/settings/src/index.ts +45 -0
  68. package/settings/src/runtime.ts +545 -0
  69. package/task-job/src/TaskJobPage.vue +120 -0
  70. package/task-job/src/contracts.ts +117 -0
  71. package/task-job/src/index.ts +2 -0
  72. package/task-job/src/runtime.ts +105 -0
  73. package/testing/src/index.ts +141 -0
@@ -0,0 +1,155 @@
1
+ <script setup lang="ts">
2
+ import { EmptyState, ForbiddenState, ModuleUnavailableState, PageContent, PageHeader, PageToolbar, SessionExpiredState } from '@peanut-admin/admin/shell'
3
+ import { ElButton, ElInput } from 'element-plus'
4
+ import { computed, onMounted, ref } from 'vue'
5
+ import { useImportExportRuntime } from './runtime'
6
+
7
+ const runtime = useImportExportRuntime(); const state = runtime.state
8
+ const providerKey = ref(''); const inputFileKey = ref(''); const mappingJson = ref('{}')
9
+ const canCreate = computed(runtime.canCreate); const canCancel = computed(runtime.canCancel)
10
+ const statuses = ['queued', 'running', 'cancel_requested', 'succeeded', 'failed', 'cancelled', 'expired'] as const
11
+ const submitImport = async (): Promise<void> => { try { const mapping = JSON.parse(mappingJson.value) as unknown; if (typeof mapping !== 'object' || mapping === null || Array.isArray(mapping)) throw new Error(); await runtime.submitImport(providerKey.value, inputFileKey.value, mapping as Record<string, string>) } catch { /* Runtime validation reports request errors; malformed JSON stays local. */ } }
12
+ onMounted(runtime.load)
13
+ </script>
14
+
15
+ <template>
16
+ <PageContent class="import-export-page">
17
+ <PageHeader>
18
+ Import / Export<template #actions>
19
+ <ElButton
20
+ :loading="state.loading"
21
+ :disabled="state.mutating"
22
+ @click="runtime.load"
23
+ >
24
+ Reload
25
+ </ElButton>
26
+ </template>
27
+ </PageHeader>
28
+ <section
29
+ v-if="canCreate"
30
+ class="submission"
31
+ aria-label="Create import or export"
32
+ >
33
+ <ElInput
34
+ v-model="providerKey"
35
+ placeholder="Registered provider key"
36
+ aria-label="Provider key"
37
+ />
38
+ <ElInput
39
+ v-model="inputFileKey"
40
+ placeholder="Private CSV file key"
41
+ aria-label="Input file key"
42
+ />
43
+ <ElInput
44
+ v-model="mappingJson"
45
+ type="textarea"
46
+ placeholder="{&quot;CSV heading&quot;:&quot;column_key&quot;}"
47
+ aria-label="Column mapping JSON"
48
+ />
49
+ <div>
50
+ <ElButton
51
+ type="primary"
52
+ :disabled="state.mutating"
53
+ @click="submitImport"
54
+ >
55
+ Import CSV
56
+ </ElButton><ElButton
57
+ :disabled="state.mutating"
58
+ @click="runtime.submitExport(providerKey)"
59
+ >
60
+ Export CSV
61
+ </ElButton>
62
+ </div>
63
+ </section>
64
+ <PageToolbar label="Operation status">
65
+ <ElButton
66
+ v-for="status in statuses"
67
+ :key="status"
68
+ :type="state.status === status ? 'primary' : 'default'"
69
+ @click="runtime.setStatus(status)"
70
+ >
71
+ {{ status }}
72
+ </ElButton>
73
+ </PageToolbar>
74
+ <SessionExpiredState
75
+ v-if="state.error?.status === 401"
76
+ :message="state.error.message"
77
+ />
78
+ <ForbiddenState
79
+ v-else-if="state.error?.status === 403"
80
+ :message="state.error.message"
81
+ />
82
+ <ModuleUnavailableState
83
+ v-else-if="state.error?.status === 503"
84
+ :message="state.error.message"
85
+ @action="runtime.load"
86
+ />
87
+ <section
88
+ v-else-if="state.error"
89
+ role="alert"
90
+ >
91
+ <h2>Unable to complete the import/export request</h2><p>{{ state.error.message }}</p><p v-if="state.error.requestId">
92
+ Request ID: {{ state.error.requestId }}
93
+ </p>
94
+ </section>
95
+ <div
96
+ v-else-if="state.loading"
97
+ role="status"
98
+ >
99
+ Loading import/export operations...
100
+ </div>
101
+ <EmptyState
102
+ v-else-if="state.items.length === 0"
103
+ title="No operations"
104
+ message="No import/export operations match this status."
105
+ />
106
+ <div
107
+ v-else
108
+ class="table-wrap"
109
+ >
110
+ <table>
111
+ <thead><tr><th>Provider</th><th>Direction</th><th>Status</th><th>Progress</th><th>Rows</th><th>Expires</th><th>Actions</th></tr></thead><tbody>
112
+ <tr
113
+ v-for="operation in state.items"
114
+ :key="operation.operationKey"
115
+ >
116
+ <td>{{ operation.providerKey }}</td><td>{{ operation.direction }}</td><td>{{ operation.status }}</td><td>
117
+ <progress
118
+ :value="operation.processedRows"
119
+ :max="Math.max(operation.totalRows, operation.processedRows, 1)"
120
+ />
121
+ </td><td>{{ operation.acceptedRows }} accepted / {{ operation.rejectedRows }} rejected</td><td>{{ operation.retentionUntil }}</td><td>
122
+ <ElButton
123
+ v-if="['queued','running'].includes(operation.status)"
124
+ text
125
+ :disabled="!canCancel || state.mutating"
126
+ @click="runtime.cancel(operation)"
127
+ >
128
+ Cancel
129
+ </ElButton><ElButton
130
+ v-if="operation.resultFileKey"
131
+ text
132
+ :disabled="state.mutating"
133
+ @click="runtime.download(operation.resultFileKey)"
134
+ >
135
+ Result
136
+ </ElButton><ElButton
137
+ v-if="operation.errorFileKey"
138
+ text
139
+ :disabled="state.mutating"
140
+ @click="runtime.download(operation.errorFileKey)"
141
+ >
142
+ Errors
143
+ </ElButton>
144
+ </td>
145
+ </tr>
146
+ </tbody>
147
+ </table>
148
+ </div>
149
+ </PageContent>
150
+ </template>
151
+
152
+ <style scoped>
153
+ .submission { display: grid; gap: 8px; max-width: 720px; margin-bottom: 16px; }
154
+ .table-wrap { overflow-x: auto; } table { width: 100%; border-collapse: collapse; } th, td { padding: 10px 8px; border-bottom: 1px solid var(--el-border-color); text-align: left; } progress { min-width: 120px; }
155
+ </style>
@@ -0,0 +1,96 @@
1
+ export type ImportExportDirection = 'import' | 'export'
2
+ export type ImportExportStatus = 'queued' | 'running' | 'cancel_requested' | 'succeeded' | 'failed' | 'cancelled' | 'expired'
3
+
4
+ export interface ImportExportOperation {
5
+ operationKey: string
6
+ providerKey: string
7
+ direction: ImportExportDirection
8
+ status: ImportExportStatus
9
+ inputFileKey: string | null
10
+ resultFileKey: string | null
11
+ errorFileKey: string | null
12
+ taskJobKey: string | null
13
+ schemaRevision: string
14
+ processedRows: number
15
+ acceptedRows: number
16
+ rejectedRows: number
17
+ totalRows: number
18
+ revision: number
19
+ lastErrorCode: string | null
20
+ retentionUntil: string
21
+ createdAt: string
22
+ updatedAt: string
23
+ completedAt: string | null
24
+ }
25
+
26
+ export interface ImportExportList { items: ImportExportOperation[]; page: number; pageSize: number; total: number }
27
+ export interface ImportExportTransportResult { status: number; body: unknown; headers: Headers }
28
+ export interface ImportExportTransport {
29
+ list(status: ImportExportStatus, page: number, pageSize: number, signal: AbortSignal): Promise<ImportExportTransportResult>
30
+ submitImport(providerKey: string, fileKey: string, mapping: Record<string, string>, idempotencyKey: string, signal: AbortSignal): Promise<ImportExportTransportResult>
31
+ submitExport(providerKey: string, idempotencyKey: string, signal: AbortSignal): Promise<ImportExportTransportResult>
32
+ cancel(operationKey: string, revision: number, signal: AbortSignal): Promise<ImportExportTransportResult>
33
+ download(fileKey: string, signal: AbortSignal): Promise<Response>
34
+ }
35
+
36
+ const operationPattern = /^iox_[0-9a-f]{32}$/
37
+ const filePattern = /^file_[0-9a-f]{32}$/
38
+ const jobPattern = /^job_[0-9a-f]{32}$/
39
+ const providerPattern = /^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*$/
40
+ const revisionPattern = /^[a-z0-9][a-z0-9._-]{0,63}$/
41
+ const errorPattern = /^[A-Z][A-Z0-9_]{2,63}$/
42
+ const datePattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/
43
+
44
+ const record = (value: unknown): Record<string, unknown> => {
45
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) throw new Error('IMPORT_EXPORT_RESPONSE_INVALID')
46
+ return value as Record<string, unknown>
47
+ }
48
+ const exact = (value: Record<string, unknown>, keys: readonly string[]): void => {
49
+ if (Object.keys(value).sort().join('|') !== [...keys].sort().join('|')) throw new Error('IMPORT_EXPORT_RESPONSE_INVALID')
50
+ }
51
+ const nullablePattern = (value: unknown, pattern: RegExp): value is string | null => value === null || (typeof value === 'string' && pattern.test(value))
52
+ const integer = (value: unknown, minimum = 0): value is number => typeof value === 'number' && Number.isSafeInteger(value) && value >= minimum
53
+
54
+ export const parseOperation = (value: unknown): ImportExportOperation => {
55
+ const item = record(value)
56
+ exact(item, ['operation_key', 'provider_key', 'direction', 'format', 'status', 'input_file_key', 'result_file_key', 'error_file_key', 'task_job_key', 'schema_revision', 'mapping', 'processed_rows', 'accepted_rows', 'rejected_rows', 'total_rows', 'revision', 'last_error_code', 'retention_until', 'created_at', 'updated_at', 'completed_at'])
57
+ if (!operationPattern.test(String(item.operation_key)) || !providerPattern.test(String(item.provider_key))
58
+ || (item.direction !== 'import' && item.direction !== 'export') || item.format !== 'csv'
59
+ || !['queued', 'running', 'cancel_requested', 'succeeded', 'failed', 'cancelled', 'expired'].includes(String(item.status))
60
+ || !nullablePattern(item.input_file_key, filePattern) || !nullablePattern(item.result_file_key, filePattern)
61
+ || !nullablePattern(item.error_file_key, filePattern) || !nullablePattern(item.task_job_key, jobPattern)
62
+ || !revisionPattern.test(String(item.schema_revision)) || !integer(item.processed_rows) || !integer(item.accepted_rows)
63
+ || !integer(item.rejected_rows) || !integer(item.total_rows) || !integer(item.revision, 1)
64
+ || (item.accepted_rows as number) + (item.rejected_rows as number) > (item.processed_rows as number)
65
+ || (item.last_error_code !== null && (typeof item.last_error_code !== 'string' || !errorPattern.test(item.last_error_code)))
66
+ || !datePattern.test(String(item.retention_until)) || !datePattern.test(String(item.created_at)) || !datePattern.test(String(item.updated_at))
67
+ || (item.completed_at !== null && (typeof item.completed_at !== 'string' || !datePattern.test(item.completed_at)))) {
68
+ throw new Error('IMPORT_EXPORT_RESPONSE_INVALID')
69
+ }
70
+ const terminal = ['succeeded', 'failed', 'cancelled', 'expired'].includes(String(item.status))
71
+ if ((item.direction === 'import') !== (item.input_file_key !== null) || terminal !== (item.completed_at !== null)) throw new Error('IMPORT_EXPORT_RESPONSE_INVALID')
72
+ const mapping = record(item.mapping)
73
+ for (const [source, target] of Object.entries(mapping)) if (source === '' || source.length > 120 || typeof target !== 'string' || !/^[a-z][a-z0-9_]{0,63}$/.test(target)) throw new Error('IMPORT_EXPORT_RESPONSE_INVALID')
74
+ return {
75
+ operationKey: item.operation_key as string, providerKey: item.provider_key as string,
76
+ direction: item.direction as ImportExportDirection, status: item.status as ImportExportStatus,
77
+ inputFileKey: item.input_file_key as string | null, resultFileKey: item.result_file_key as string | null,
78
+ errorFileKey: item.error_file_key as string | null, taskJobKey: item.task_job_key as string | null,
79
+ schemaRevision: item.schema_revision as string, processedRows: item.processed_rows as number,
80
+ acceptedRows: item.accepted_rows as number, rejectedRows: item.rejected_rows as number,
81
+ totalRows: item.total_rows as number, revision: item.revision as number,
82
+ lastErrorCode: item.last_error_code as string | null, retentionUntil: item.retention_until as string,
83
+ createdAt: item.created_at as string, updatedAt: item.updated_at as string, completedAt: item.completed_at as string | null,
84
+ }
85
+ }
86
+
87
+ export const parseOperationResponse = (value: unknown): ImportExportOperation => {
88
+ const body = record(value); exact(body, ['data', 'meta']); record(body.meta); return parseOperation(body.data)
89
+ }
90
+ export const parseOperationList = (value: unknown): ImportExportList => {
91
+ const body = record(value); exact(body, ['data', 'meta'])
92
+ const data = record(body.data); exact(data, ['items']); if (!Array.isArray(data.items)) throw new Error('IMPORT_EXPORT_RESPONSE_INVALID')
93
+ const meta = record(body.meta); exact(meta, ['request_id', 'page', 'page_size', 'total'])
94
+ if (typeof meta.request_id !== 'string' || meta.request_id === '' || !integer(meta.page, 1) || !integer(meta.page_size, 1) || !integer(meta.total)) throw new Error('IMPORT_EXPORT_RESPONSE_INVALID')
95
+ return { items: data.items.map(parseOperation), page: meta.page, pageSize: meta.page_size, total: meta.total }
96
+ }
@@ -0,0 +1,3 @@
1
+ export * from './contracts'
2
+ export * from './runtime'
3
+ export { default as ImportExportPage } from './ImportExportPage.vue'
@@ -0,0 +1,128 @@
1
+ import { defineAdminModule } from '@peanut-admin/admin/core'
2
+ import type { AdminModuleContribution } from '@peanut-admin/admin/core'
3
+ import { inject, reactive } from 'vue'
4
+ import type { InjectionKey } from 'vue'
5
+ import { parseOperationList, parseOperationResponse } from './contracts'
6
+ import type { ImportExportOperation, ImportExportStatus, ImportExportTransport, ImportExportTransportResult } from './contracts'
7
+
8
+ export const IMPORT_EXPORT_MODULE_KEY = 'peanut.import-export' as const
9
+ export const IMPORT_EXPORT_ROUTE_NAME = 'peanut.import-export.list' as const
10
+ export const IMPORT_EXPORT_ROUTE_PATH = '/app/import-export' as const
11
+ export const IMPORT_EXPORT_READ_PERMISSION = 'peanut.import-export.read' as const
12
+ export const IMPORT_EXPORT_CREATE_PERMISSION = 'peanut.import-export.create' as const
13
+ export const IMPORT_EXPORT_CANCEL_PERMISSION = 'peanut.import-export.cancel' as const
14
+ export const IMPORT_EXPORT_STORE_KEY = 'peanut.import-export.runtime' as const
15
+
16
+ export interface ImportExportError { message: string; requestId: string | null; status: number | null }
17
+ export interface ImportExportState {
18
+ items: ImportExportOperation[]; status: ImportExportStatus; page: number; pageSize: number; total: number
19
+ loading: boolean; mutating: boolean; error: ImportExportError | null
20
+ }
21
+ export interface ImportExportRuntime {
22
+ state: ImportExportState
23
+ canCreate(): boolean
24
+ canCancel(): boolean
25
+ load(): Promise<void>
26
+ setStatus(status: ImportExportStatus): Promise<void>
27
+ submitImport(providerKey: string, fileKey: string, mapping: Record<string, string>): Promise<void>
28
+ submitExport(providerKey: string): Promise<void>
29
+ cancel(operation: ImportExportOperation): Promise<void>
30
+ download(fileKey: string): Promise<void>
31
+ dispose(): void
32
+ }
33
+ export interface ImportExportRuntimeOptions {
34
+ transport: ImportExportTransport
35
+ canRead(): boolean
36
+ canCreate(): boolean
37
+ canCancel(): boolean
38
+ saveDownload?: (response: Response, fileKey: string, signal: AbortSignal) => Promise<void>
39
+ idempotencyKey?: () => string
40
+ }
41
+
42
+ const requestId = (result: ImportExportTransportResult): string | null => {
43
+ const body = typeof result.body === 'object' && result.body !== null && !Array.isArray(result.body) ? result.body as Record<string, unknown> : {}
44
+ const value = body.request_id ?? result.headers.get('X-Request-Id')
45
+ return typeof value === 'string' && value !== '' ? value : null
46
+ }
47
+ const failure = (result: ImportExportTransportResult): ImportExportError => {
48
+ const body = typeof result.body === 'object' && result.body !== null && !Array.isArray(result.body) ? result.body as Record<string, unknown> : {}
49
+ return { message: typeof body.detail === 'string' && body.detail !== '' ? body.detail : `Import/export request failed (${result.status}).`, requestId: requestId(result), status: result.status }
50
+ }
51
+ const key = (): string => `web-${crypto.randomUUID()}`
52
+ const save = async (response: Response, fileKey: string, signal: AbortSignal): Promise<void> => {
53
+ const blob = await response.blob(); if (signal.aborted) throw new DOMException('Aborted', 'AbortError')
54
+ const url = URL.createObjectURL(blob); const anchor = document.createElement('a')
55
+ anchor.href = url; anchor.download = `${fileKey}.csv`; anchor.click(); URL.revokeObjectURL(url)
56
+ }
57
+
58
+ export const createImportExportRuntime = (options: ImportExportRuntimeOptions): ImportExportRuntime => {
59
+ const state = reactive<ImportExportState>({ items: [], status: 'queued', page: 1, pageSize: 20, total: 0, loading: false, mutating: false, error: null })
60
+ let disposeEpoch = 0; let readEpoch = 0; let downloadEpoch = 0
61
+ let readController: AbortController | null = null; let commandController: AbortController | null = null; let downloadController: AbortController | null = null
62
+ const aborted = (error: unknown): boolean => error instanceof DOMException && error.name === 'AbortError'
63
+ const load = async (): Promise<void> => {
64
+ const currentDispose = disposeEpoch; const currentRead = ++readEpoch
65
+ readController?.abort(); const controller = new AbortController(); readController = controller
66
+ state.loading = true; state.error = null
67
+ try {
68
+ if (!options.canRead()) throw new Error('IMPORT_EXPORT_PERMISSION_DENIED')
69
+ const status = state.status; const page = state.page; const pageSize = state.pageSize
70
+ const result = await options.transport.list(status, page, pageSize, controller.signal)
71
+ if (currentDispose !== disposeEpoch || currentRead !== readEpoch) return
72
+ if (result.status !== 200) { state.error = failure(result); return }
73
+ const list = parseOperationList(result.body); state.items = [...list.items]; state.page = list.page; state.pageSize = list.pageSize; state.total = list.total
74
+ } catch (error) { if (currentDispose === disposeEpoch && currentRead === readEpoch && !aborted(error)) state.error = { message: 'The import/export service could not be reached.', requestId: null, status: null } }
75
+ finally {
76
+ if (readController === controller) readController = null
77
+ if (currentDispose === disposeEpoch && currentRead === readEpoch) state.loading = false
78
+ }
79
+ }
80
+ const mutate = async (operation: (signal: AbortSignal) => Promise<ImportExportTransportResult>): Promise<void> => {
81
+ if (state.mutating || commandController !== null) return
82
+ const currentDispose = disposeEpoch; const controller = new AbortController(); commandController = controller
83
+ state.mutating = true; state.error = null
84
+ try {
85
+ const result = await operation(controller.signal); if (currentDispose !== disposeEpoch) return
86
+ if (result.status !== 201 && result.status !== 200) { state.error = failure(result); return }
87
+ parseOperationResponse(result.body); await load()
88
+ } catch (error) { if (currentDispose === disposeEpoch && !aborted(error)) state.error = { message: 'The import/export request could not be completed.', requestId: null, status: null } }
89
+ finally {
90
+ if (commandController === controller) commandController = null
91
+ if (currentDispose === disposeEpoch) state.mutating = false
92
+ }
93
+ }
94
+ return {
95
+ state, canCreate: options.canCreate, canCancel: options.canCancel, load,
96
+ async setStatus(status) { state.status = status; state.page = 1; await load() },
97
+ async submitImport(providerKey, fileKey, mapping) { if (!options.canCreate()) return; await mutate(signal => options.transport.submitImport(providerKey, fileKey, mapping, (options.idempotencyKey ?? key)(), signal)) },
98
+ async submitExport(providerKey) { if (!options.canCreate()) return; await mutate(signal => options.transport.submitExport(providerKey, (options.idempotencyKey ?? key)(), signal)) },
99
+ async cancel(operation) { if (!options.canCancel() || !['queued', 'running'].includes(operation.status)) return; await mutate(signal => options.transport.cancel(operation.operationKey, operation.revision, signal)) },
100
+ async download(fileKey) {
101
+ const currentDispose = disposeEpoch; const currentDownload = ++downloadEpoch
102
+ downloadController?.abort(); const controller = new AbortController(); downloadController = controller; state.error = null
103
+ try {
104
+ const response = await options.transport.download(fileKey, controller.signal)
105
+ if (currentDispose !== disposeEpoch || currentDownload !== downloadEpoch) return
106
+ if (!response.ok) { let body: unknown = null; try { body = await response.json() } catch { body = null }; if (currentDispose === disposeEpoch && currentDownload === downloadEpoch) state.error = failure({ status: response.status, body, headers: response.headers }); return }
107
+ await (options.saveDownload ?? save)(response, fileKey, controller.signal)
108
+ } catch (error) { if (currentDispose === disposeEpoch && currentDownload === downloadEpoch && !aborted(error)) state.error = { message: 'The CSV file could not be downloaded.', requestId: null, status: null } }
109
+ finally { if (downloadController === controller) downloadController = null }
110
+ },
111
+ dispose() {
112
+ disposeEpoch += 1; readEpoch += 1; downloadEpoch += 1
113
+ readController?.abort(); commandController?.abort(); downloadController?.abort()
114
+ readController = null; commandController = null; downloadController = null
115
+ state.items = []; state.status = 'queued'; state.page = 1; state.pageSize = 20; state.total = 0
116
+ state.loading = false; state.mutating = false; state.error = null
117
+ },
118
+ }
119
+ }
120
+
121
+ export const importExportRuntimeKey: InjectionKey<ImportExportRuntime> = Symbol(IMPORT_EXPORT_STORE_KEY)
122
+ export const useImportExportRuntime = (): ImportExportRuntime => { const runtime = inject(importExportRuntimeKey); if (runtime === undefined) throw new Error('IMPORT_EXPORT_RUNTIME_MISSING'); return runtime }
123
+ export const createImportExportModuleContribution = (runtime: ImportExportRuntime): AdminModuleContribution => defineAdminModule({
124
+ key: IMPORT_EXPORT_MODULE_KEY,
125
+ routes: [{ name: IMPORT_EXPORT_ROUTE_NAME, path: IMPORT_EXPORT_ROUTE_PATH, component: async () => ({ default: (await import('./ImportExportPage.vue')).default }), access: { moduleKey: IMPORT_EXPORT_MODULE_KEY, permissionKeys: [IMPORT_EXPORT_READ_PERMISSION] } }],
126
+ disposeOnTenantChange: true,
127
+ stores: [{ key: IMPORT_EXPORT_STORE_KEY, dispose: runtime.dispose }],
128
+ })