shutro-erp 0.2.0 → 0.3.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/README.md +17 -10
- package/dist/account_portal/.kosh/bindings.d.ts +22 -0
- package/dist/account_portal/.kosh/bindings.js +15 -0
- package/dist/account_portal/.kosh/schema.d.ts +1350 -0
- package/dist/account_portal/index.d.ts +3 -0
- package/dist/account_portal/index.js +73 -0
- package/dist/account_portal/module.d.ts +5983 -0
- package/dist/account_portal/module.js +18 -0
- package/dist/accounting/.kosh/bindings.js +1 -1
- package/dist/accounting/.kosh/schema.d.ts +142 -9
- package/dist/accounting/chart.d.ts +40 -8
- package/dist/accounting/chart.js +124 -30
- package/dist/accounting/closing.d.ts +6 -1
- package/dist/accounting/closing.js +5 -1
- package/dist/accounting/demo.js +6 -5
- package/dist/accounting/index.d.ts +3 -3
- package/dist/accounting/index.js +200 -23
- package/dist/accounting/module.d.ts +3326 -1086
- package/dist/accounting/module.js +408 -66
- package/dist/accounting/payments.d.ts +25 -0
- package/dist/accounting/payments.js +96 -4
- package/dist/accounting/web/RegisterPayment.d.ts +3 -2
- package/dist/accounting/web/RegisterPayment.js +39 -6
- package/dist/accounting/web/SettingsPage.js +5 -1
- package/dist/accounting/web/messages.js +10 -0
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/cli/init.d.ts +5 -0
- package/dist/cli/init.js +195 -53
- package/dist/cli/launch.d.ts +16 -0
- package/dist/cli/launch.js +138 -0
- package/dist/cli/prompt.d.ts +35 -0
- package/dist/cli/prompt.js +46 -0
- package/dist/cli/templates.d.ts +11 -3
- package/dist/cli/templates.js +32 -7
- package/dist/cli/tui.d.ts +7 -0
- package/dist/cli/tui.js +254 -0
- package/dist/cli/ui.d.ts +25 -5
- package/dist/cli/ui.js +49 -9
- package/dist/contacts/.kosh/bindings.js +1 -1
- package/dist/contacts/.kosh/schema.d.ts +3 -1
- package/dist/contacts/index.js +24 -1
- package/dist/contacts/module.d.ts +42 -0
- package/dist/contacts/module.js +3 -1
- package/dist/import/convert.d.ts +55 -0
- package/dist/import/convert.js +224 -0
- package/dist/import/csv.d.ts +28 -0
- package/dist/import/csv.js +115 -0
- package/dist/import/index.d.ts +33 -0
- package/dist/import/index.js +60 -0
- package/dist/import/mapping.d.ts +15 -0
- package/dist/import/mapping.js +33 -0
- package/dist/import/match.d.ts +24 -0
- package/dist/import/match.js +60 -0
- package/dist/import/models.d.ts +2 -0
- package/dist/import/models.js +102 -0
- package/dist/import/routes.d.ts +13 -0
- package/dist/import/routes.js +392 -0
- package/dist/import/run.d.ts +131 -0
- package/dist/import/run.js +568 -0
- package/dist/import/service.d.ts +153 -0
- package/dist/import/service.js +367 -0
- package/dist/import/web/ImportButton.d.ts +7 -0
- package/dist/import/web/ImportButton.js +31 -0
- package/dist/import/web/ImportWizard.d.ts +75 -0
- package/dist/import/web/ImportWizard.js +217 -0
- package/dist/import/web/index.d.ts +13 -0
- package/dist/import/web/index.js +18 -0
- package/dist/import/web/messages.d.ts +2 -0
- package/dist/import/web/messages.js +139 -0
- package/dist/import/worker.d.ts +3 -0
- package/dist/import/worker.js +52 -0
- package/dist/import/xlsx.d.ts +20 -0
- package/dist/import/xlsx.js +192 -0
- package/dist/l10n_bd/.kosh/bindings.d.ts +22 -0
- package/dist/l10n_bd/.kosh/bindings.js +15 -0
- package/dist/l10n_bd/.kosh/schema.d.ts +1281 -0
- package/dist/l10n_bd/documents.d.ts +8 -0
- package/dist/l10n_bd/documents.js +183 -0
- package/dist/l10n_bd/index.d.ts +8 -0
- package/dist/l10n_bd/index.js +156 -0
- package/dist/l10n_bd/module.d.ts +5705 -0
- package/dist/l10n_bd/module.js +163 -0
- package/dist/l10n_bd/mushak.d.ts +67 -0
- package/dist/l10n_bd/mushak.js +139 -0
- package/dist/l10n_bd/routes.d.ts +3 -0
- package/dist/l10n_bd/routes.js +205 -0
- package/dist/l10n_bd/web/MushakPage.d.ts +7 -0
- package/dist/l10n_bd/web/MushakPage.js +53 -0
- package/dist/l10n_bd/web/index.d.ts +13 -0
- package/dist/l10n_bd/web/index.js +18 -0
- package/dist/l10n_bd/web/messages.d.ts +2 -0
- package/dist/l10n_bd/web/messages.js +101 -0
- package/dist/l10n_in/.kosh/bindings.d.ts +22 -0
- package/dist/l10n_in/.kosh/bindings.js +15 -0
- package/dist/l10n_in/.kosh/schema.d.ts +1343 -0
- package/dist/l10n_in/documents.d.ts +8 -0
- package/dist/l10n_in/documents.js +114 -0
- package/dist/l10n_in/einvoice.d.ts +253 -0
- package/dist/l10n_in/einvoice.js +485 -0
- package/dist/l10n_in/gstr.d.ts +109 -0
- package/dist/l10n_in/gstr.js +430 -0
- package/dist/l10n_in/index.d.ts +20 -0
- package/dist/l10n_in/index.js +232 -0
- package/dist/l10n_in/module.d.ts +7823 -0
- package/dist/l10n_in/module.js +427 -0
- package/dist/l10n_in/routes-einvoice.d.ts +15 -0
- package/dist/l10n_in/routes-einvoice.js +124 -0
- package/dist/l10n_in/routes.d.ts +17 -0
- package/dist/l10n_in/routes.js +282 -0
- package/dist/l10n_in/web/EInvoicePanel.d.ts +8 -0
- package/dist/l10n_in/web/EInvoicePanel.js +63 -0
- package/dist/l10n_in/web/GstPage.d.ts +7 -0
- package/dist/l10n_in/web/GstPage.js +57 -0
- package/dist/l10n_in/web/GstSettings.d.ts +10 -0
- package/dist/l10n_in/web/GstSettings.js +36 -0
- package/dist/l10n_in/web/index.d.ts +15 -0
- package/dist/l10n_in/web/index.js +22 -0
- package/dist/l10n_in/web/messages.d.ts +2 -0
- package/dist/l10n_in/web/messages.js +166 -0
- package/dist/modules-sdk/demo.d.ts +2 -0
- package/dist/modules-sdk/demo.js +7 -3
- package/dist/modules-sdk/index.d.ts +2 -2
- package/dist/modules-sdk/index.js +1 -1
- package/dist/modules-sdk/manifest.d.ts +132 -0
- package/dist/modules-sdk/manifest.js +22 -0
- package/dist/notify/inbox.js +21 -0
- package/dist/notify/messages.js +4 -0
- package/dist/notify/models.d.ts +1 -1
- package/dist/notify/models.js +2 -0
- package/dist/notify/web/messages.js +4 -0
- package/dist/portal/.kosh/bindings.d.ts +22 -0
- package/dist/portal/.kosh/bindings.js +15 -0
- package/dist/portal/.kosh/schema.d.ts +346 -0
- package/dist/portal/announce.d.ts +16 -0
- package/dist/portal/announce.js +117 -0
- package/dist/portal/documents.d.ts +17 -0
- package/dist/portal/documents.js +61 -0
- package/dist/portal/index.d.ts +20 -0
- package/dist/portal/index.js +65 -0
- package/dist/portal/module.d.ts +1405 -0
- package/dist/portal/module.js +84 -0
- package/dist/portal/routes-documents.d.ts +2 -0
- package/dist/portal/routes-documents.js +391 -0
- package/dist/portal/routes.d.ts +13 -0
- package/dist/portal/routes.js +313 -0
- package/dist/portal/rules.d.ts +11 -0
- package/dist/portal/rules.js +25 -0
- package/dist/portal/web/PortalAccess.d.ts +8 -0
- package/dist/portal/web/PortalAccess.js +66 -0
- package/dist/portal/web/index.d.ts +12 -0
- package/dist/portal/web/index.js +17 -0
- package/dist/portal/web/messages.d.ts +2 -0
- package/dist/portal/web/messages.js +33 -0
- package/dist/product/.kosh/bindings.js +1 -1
- package/dist/product/.kosh/schema.d.ts +12 -3
- package/dist/product/demo.js +6 -5
- package/dist/product/index.d.ts +1 -0
- package/dist/product/index.js +17 -4
- package/dist/product/module.d.ts +346 -14
- package/dist/product/module.js +48 -18
- package/dist/product/taxes.d.ts +59 -0
- package/dist/product/taxes.js +61 -0
- package/dist/project/.kosh/bindings.js +1 -1
- package/dist/project/.kosh/schema.d.ts +3 -1
- package/dist/project/module.d.ts +20 -0
- package/dist/project_portal/.kosh/bindings.d.ts +22 -0
- package/dist/project_portal/.kosh/bindings.js +15 -0
- package/dist/project_portal/.kosh/schema.d.ts +671 -0
- package/dist/project_portal/index.d.ts +9 -0
- package/dist/project_portal/index.js +42 -0
- package/dist/project_portal/module.d.ts +2142 -0
- package/dist/project_portal/module.js +22 -0
- package/dist/purchase/.kosh/bindings.js +1 -1
- package/dist/purchase/.kosh/schema.d.ts +15 -4
- package/dist/purchase/demo.js +3 -3
- package/dist/purchase/index.js +1 -1
- package/dist/purchase/module.d.ts +232 -8
- package/dist/purchase/module.js +51 -9
- package/dist/purchase_account/.kosh/bindings.js +1 -1
- package/dist/purchase_account/.kosh/schema.d.ts +144 -10
- package/dist/purchase_account/module.d.ts +2008 -641
- package/dist/purchase_account/module.js +2 -2
- package/dist/purchase_portal/.kosh/bindings.d.ts +22 -0
- package/dist/purchase_portal/.kosh/bindings.js +15 -0
- package/dist/purchase_portal/.kosh/schema.d.ts +822 -0
- package/dist/purchase_portal/index.d.ts +9 -0
- package/dist/purchase_portal/index.js +53 -0
- package/dist/purchase_portal/module.d.ts +2758 -0
- package/dist/purchase_portal/module.js +18 -0
- package/dist/reporting/documents.js +20 -3
- package/dist/reporting/index.d.ts +1 -0
- package/dist/reporting/index.js +1 -0
- package/dist/reporting/routes-documents.js +9 -2
- package/dist/reporting/words.d.ts +11 -0
- package/dist/reporting/words.js +229 -0
- package/dist/sale_account/.kosh/bindings.js +1 -1
- package/dist/sale_account/.kosh/schema.d.ts +144 -10
- package/dist/sale_account/module.d.ts +2008 -641
- package/dist/sale_account/module.js +3 -3
- package/dist/sale_portal/.kosh/bindings.d.ts +22 -0
- package/dist/sale_portal/.kosh/bindings.js +15 -0
- package/dist/sale_portal/.kosh/schema.d.ts +736 -0
- package/dist/sale_portal/index.d.ts +9 -0
- package/dist/sale_portal/index.js +71 -0
- package/dist/sale_portal/module.d.ts +2718 -0
- package/dist/sale_portal/module.js +19 -0
- package/dist/sale_project/.kosh/bindings.js +1 -1
- package/dist/sale_project/.kosh/schema.d.ts +15 -4
- package/dist/sale_project/module.d.ts +390 -10
- package/dist/sales/.kosh/bindings.js +1 -1
- package/dist/sales/.kosh/schema.d.ts +15 -4
- package/dist/sales/demo.js +3 -3
- package/dist/sales/index.js +67 -1
- package/dist/sales/module.d.ts +232 -8
- package/dist/sales/module.js +51 -12
- package/dist/server/auth/base.js +2 -0
- package/dist/server/auth/service.d.ts +2 -0
- package/dist/server/auth/service.js +7 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/server.js +26 -1
- package/dist/ui/api.d.ts +2 -0
- package/dist/ui/app.js +1 -1
- package/dist/ui/context.d.ts +3 -1
- package/dist/ui/context.js +2 -2
- package/dist/ui/index.d.ts +1 -1
- package/dist/ui/index.js +1 -1
- package/dist/ui/lib/format.d.ts +2 -0
- package/dist/ui/lib/format.js +3 -1
- package/dist/ui/views/FormView.js +19 -6
- package/package.json +50 -2
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the import routes and the background worker share (RFC 0014): reading sessions as their user, the fields of
|
|
3
|
+
* a model, options, saved mappings and module templates, running an import and recording its outcome.
|
|
4
|
+
*/
|
|
5
|
+
import { type ApiModel, type KoshRegistry } from '../server/index.js';
|
|
6
|
+
import type { Driver, Env, Shutro } from '../server/orm.js';
|
|
7
|
+
import type { Cell } from './convert.js';
|
|
8
|
+
import { type ColumnMapping } from './mapping.js';
|
|
9
|
+
import { type ImportField, type ImportModels, type ImportOptions, type ImportResult, type RowError, type UndoData } from './run.js';
|
|
10
|
+
type Row = Record<string, unknown>;
|
|
11
|
+
export interface Rows {
|
|
12
|
+
readonly ids: readonly number[];
|
|
13
|
+
readonly length: number;
|
|
14
|
+
load(keys: readonly string[]): Promise<{
|
|
15
|
+
read(keys: readonly string[]): Row[];
|
|
16
|
+
}>;
|
|
17
|
+
write(values: unknown): Promise<void>;
|
|
18
|
+
unlink(): Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
interface Ref {
|
|
21
|
+
search(q: unknown): Promise<Rows>;
|
|
22
|
+
create(rows: readonly unknown[]): Promise<Rows>;
|
|
23
|
+
}
|
|
24
|
+
export declare const ref: (env: Env, name: string) => Ref;
|
|
25
|
+
/** Rows a file may have (`KOSH_IMPORT_MAX_ROWS`). */
|
|
26
|
+
export declare const IMPORT_ROWS = 1000000;
|
|
27
|
+
/** Rows run within the request; larger files run as a background job (RFC 0014 §5.3). */
|
|
28
|
+
export declare const INLINE_ROWS = 2000;
|
|
29
|
+
/** The default file size limit (`KOSH_IMPORT_MAX_BYTES`). */
|
|
30
|
+
export declare const IMPORT_MAX_BYTES = 50000000;
|
|
31
|
+
/** How long an import can be undone (RFC 0014 §5.5). */
|
|
32
|
+
export declare const UNDO_DAYS = 7;
|
|
33
|
+
/** The background job that runs an import or its dry run: `{ session, dryRun }`. */
|
|
34
|
+
export declare const RUN_JOB = "import.run";
|
|
35
|
+
/** The event a background run emits when it ends (a notification for its user). */
|
|
36
|
+
export declare const FINISHED_TOPIC = "import.finished";
|
|
37
|
+
/** The options of a mapping request (or a template), checked. */
|
|
38
|
+
export declare function parseOptions(input: unknown): ImportOptions;
|
|
39
|
+
/** A result as the API shows it: undo data stays on the server. */
|
|
40
|
+
export declare const shown: ({ undo: _, ...result }: ImportResult) => Omit<ImportResult, 'undo'>;
|
|
41
|
+
export declare const isXlsx: (bytes: Uint8Array) => boolean;
|
|
42
|
+
/** The cells of a file. @throws {HttpProblem} 422 when it cannot be read */
|
|
43
|
+
export declare function readTable(bytes: Uint8Array, type: 'csv' | 'xlsx', sheet: string | null): Cell[][];
|
|
44
|
+
export declare const dataRows: (table: readonly (readonly Cell[])[], options: ImportOptions) => number;
|
|
45
|
+
export interface Session {
|
|
46
|
+
readonly id: number;
|
|
47
|
+
readonly rows: Rows;
|
|
48
|
+
readonly model: ApiModel;
|
|
49
|
+
readonly fileName: string;
|
|
50
|
+
readonly fileType: 'csv' | 'xlsx';
|
|
51
|
+
readonly file: Uint8Array;
|
|
52
|
+
readonly sheet: string | null;
|
|
53
|
+
readonly columns: readonly ColumnMapping[];
|
|
54
|
+
readonly options: ImportOptions;
|
|
55
|
+
readonly state: string;
|
|
56
|
+
readonly rowCount: number;
|
|
57
|
+
readonly undo: UndoData | null;
|
|
58
|
+
/** The errors and the file rows of the records left out. */
|
|
59
|
+
readonly rejected: {
|
|
60
|
+
readonly errors: readonly RowError[];
|
|
61
|
+
readonly rows: readonly number[];
|
|
62
|
+
};
|
|
63
|
+
readonly importedAt: Date | null;
|
|
64
|
+
readonly result: unknown;
|
|
65
|
+
readonly resultDry: boolean;
|
|
66
|
+
readonly failure: string | null;
|
|
67
|
+
}
|
|
68
|
+
/** A saved mapping or a module's template, as the screen lists them. */
|
|
69
|
+
export interface SavedMapping {
|
|
70
|
+
/** A record id, or `module.key` for a template. */
|
|
71
|
+
readonly id: string;
|
|
72
|
+
readonly name: string;
|
|
73
|
+
readonly model: string;
|
|
74
|
+
readonly template: boolean;
|
|
75
|
+
readonly shared: boolean;
|
|
76
|
+
readonly own: boolean;
|
|
77
|
+
readonly columns: readonly {
|
|
78
|
+
readonly header: string;
|
|
79
|
+
readonly field: string;
|
|
80
|
+
}[];
|
|
81
|
+
readonly options: ImportOptions;
|
|
82
|
+
}
|
|
83
|
+
export interface ImportContext {
|
|
84
|
+
readonly shutro: Shutro;
|
|
85
|
+
readonly driver: Driver;
|
|
86
|
+
readonly registry: KoshRegistry;
|
|
87
|
+
}
|
|
88
|
+
/** A session as the API shows it. */
|
|
89
|
+
export interface SessionView {
|
|
90
|
+
readonly id: number;
|
|
91
|
+
readonly model: string;
|
|
92
|
+
readonly fileName: string;
|
|
93
|
+
readonly fileType: 'csv' | 'xlsx';
|
|
94
|
+
readonly sheet: string | null;
|
|
95
|
+
readonly sheets: readonly string[];
|
|
96
|
+
readonly state: string;
|
|
97
|
+
readonly rowCount: number;
|
|
98
|
+
/** Runs as a background job. */
|
|
99
|
+
readonly background: boolean;
|
|
100
|
+
readonly headers: readonly string[];
|
|
101
|
+
readonly sample: readonly (readonly unknown[])[];
|
|
102
|
+
readonly columns: readonly ColumnMapping[];
|
|
103
|
+
readonly options: ImportOptions;
|
|
104
|
+
readonly fields: readonly {
|
|
105
|
+
readonly key: string;
|
|
106
|
+
readonly kind: string;
|
|
107
|
+
readonly label: string;
|
|
108
|
+
readonly required: boolean;
|
|
109
|
+
readonly keyable: boolean;
|
|
110
|
+
readonly line?: string;
|
|
111
|
+
readonly target?: string;
|
|
112
|
+
}[];
|
|
113
|
+
readonly progress: {
|
|
114
|
+
readonly done: number;
|
|
115
|
+
readonly total: number;
|
|
116
|
+
} | null;
|
|
117
|
+
readonly result: unknown;
|
|
118
|
+
readonly resultDry: boolean;
|
|
119
|
+
readonly failure: string | null;
|
|
120
|
+
readonly importedAt: string | null;
|
|
121
|
+
readonly undoUntil: string | null;
|
|
122
|
+
/** Rows left out by the last import (the rejected-rows file). */
|
|
123
|
+
readonly rejected: number;
|
|
124
|
+
}
|
|
125
|
+
/** What the routes and the worker share. */
|
|
126
|
+
export interface ImportService {
|
|
127
|
+
readonly inlineRows: number;
|
|
128
|
+
readonly catalog: ImportModels;
|
|
129
|
+
/** @throws {HttpProblem} 403 without the Import data access level */
|
|
130
|
+
guard(env: Env): void;
|
|
131
|
+
/** @throws {HttpProblem} 404 for an unknown model, 403 without its permissions */
|
|
132
|
+
modelFor(env: Env, name: string): ApiModel;
|
|
133
|
+
fieldsOf(env: Env, model: ApiModel): Map<string, ImportField>;
|
|
134
|
+
columnFields(fields: ReadonlyMap<string, ImportField>): ImportField[];
|
|
135
|
+
/** @throws {HttpProblem} 404 when the session is not the user's */
|
|
136
|
+
sessionOf(env: Env, id: number): Promise<Session>;
|
|
137
|
+
progressOf(env: Env, session: number): Promise<{
|
|
138
|
+
done: number;
|
|
139
|
+
total: number;
|
|
140
|
+
} | null>;
|
|
141
|
+
startProgress(env: Env, session: number, total: number): Promise<void>;
|
|
142
|
+
reportProgress(company: number, session: number, done: number, total: number): Promise<void>;
|
|
143
|
+
describe(env: Env, session: Session, table: readonly (readonly Cell[])[]): Promise<SessionView>;
|
|
144
|
+
mappingsFor(env: Env, model: string): Promise<SavedMapping[]>;
|
|
145
|
+
fitting(mappings: readonly SavedMapping[], headers: readonly string[]): SavedMapping | null;
|
|
146
|
+
applyMapping(mapping: SavedMapping, headers: readonly string[], fields: ReadonlyMap<string, ImportField>): ColumnMapping[];
|
|
147
|
+
execute(env: Env, session: Session, dryRun: boolean, onProgress?: (done: number, total: number) => Promise<void>): Promise<ImportResult>;
|
|
148
|
+
record(env: Env, session: Session, result: ImportResult, dryRun: boolean, ip?: string): Promise<void>;
|
|
149
|
+
}
|
|
150
|
+
export declare function createImportService(context: ImportContext, limits?: {
|
|
151
|
+
readonly inlineRows?: number;
|
|
152
|
+
}): ImportService;
|
|
153
|
+
export {};
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the import routes and the background worker share (RFC 0014): reading sessions as their user, the fields of
|
|
3
|
+
* a model, options, saved mappings and module templates, running an import and recording its outcome.
|
|
4
|
+
*/
|
|
5
|
+
import { announceChange, audit, HttpProblem, publicApiModels, } from '../server/index.js';
|
|
6
|
+
import { readCsv } from './csv.js';
|
|
7
|
+
import { normalize } from './mapping.js';
|
|
8
|
+
import { importFields, runImport, } from './run.js';
|
|
9
|
+
import { readXlsx, XlsxError, xlsxSheetNames } from './xlsx.js';
|
|
10
|
+
export const ref = (env, name) => env.model(name);
|
|
11
|
+
/** Rows a file may have (`KOSH_IMPORT_MAX_ROWS`). */
|
|
12
|
+
export const IMPORT_ROWS = 1_000_000;
|
|
13
|
+
/** Rows run within the request; larger files run as a background job (RFC 0014 §5.3). */
|
|
14
|
+
export const INLINE_ROWS = 2_000;
|
|
15
|
+
/** The default file size limit (`KOSH_IMPORT_MAX_BYTES`). */
|
|
16
|
+
export const IMPORT_MAX_BYTES = 50_000_000;
|
|
17
|
+
/** How long an import can be undone (RFC 0014 §5.5). */
|
|
18
|
+
export const UNDO_DAYS = 7;
|
|
19
|
+
/** The background job that runs an import or its dry run: `{ session, dryRun }`. */
|
|
20
|
+
export const RUN_JOB = 'import.run';
|
|
21
|
+
/** The event a background run emits when it ends (a notification for its user). */
|
|
22
|
+
export const FINISHED_TOPIC = 'import.finished';
|
|
23
|
+
/** Rows of the file shown with the mapping. */
|
|
24
|
+
const SAMPLE_ROWS = 5;
|
|
25
|
+
const DATE_ORDERS = ['dmy', 'mdy', 'ymd'];
|
|
26
|
+
const MODES = ['create', 'update', 'upsert'];
|
|
27
|
+
const invalid = (detail) => new HttpProblem(400, 'invalid-options', 'Invalid options', detail);
|
|
28
|
+
/** The options of a mapping request (or a template), checked. */
|
|
29
|
+
export function parseOptions(input) {
|
|
30
|
+
if (input === undefined || input === null)
|
|
31
|
+
return {};
|
|
32
|
+
if (typeof input !== 'object' || Array.isArray(input))
|
|
33
|
+
throw invalid('options is an object.');
|
|
34
|
+
const o = input;
|
|
35
|
+
const out = {};
|
|
36
|
+
if (o['dateOrder'] !== undefined) {
|
|
37
|
+
if (!DATE_ORDERS.includes(String(o['dateOrder'])))
|
|
38
|
+
throw invalid('dateOrder is dmy, mdy or ymd.');
|
|
39
|
+
out.dateOrder = o['dateOrder'];
|
|
40
|
+
}
|
|
41
|
+
if (o['decimal'] !== undefined) {
|
|
42
|
+
if (o['decimal'] !== '.' && o['decimal'] !== ',')
|
|
43
|
+
throw invalid('decimal is "." or ",".');
|
|
44
|
+
out.decimal = o['decimal'];
|
|
45
|
+
}
|
|
46
|
+
if (o['timeZone'] !== undefined) {
|
|
47
|
+
const zone = String(o['timeZone']);
|
|
48
|
+
try {
|
|
49
|
+
new Intl.DateTimeFormat('en', { timeZone: zone });
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
throw invalid(`No time zone '${zone}'.`);
|
|
53
|
+
}
|
|
54
|
+
out.timeZone = zone;
|
|
55
|
+
}
|
|
56
|
+
if (o['headerRow'] !== undefined)
|
|
57
|
+
out.headerRow = o['headerRow'] === true;
|
|
58
|
+
if (o['mode'] !== undefined) {
|
|
59
|
+
if (!MODES.includes(String(o['mode'])))
|
|
60
|
+
throw invalid('mode is create, update or upsert.');
|
|
61
|
+
out.mode = o['mode'];
|
|
62
|
+
}
|
|
63
|
+
if (o['onError'] !== undefined) {
|
|
64
|
+
if (o['onError'] !== 'stop' && o['onError'] !== 'skip')
|
|
65
|
+
throw invalid('onError is stop or skip.');
|
|
66
|
+
out.onError = o['onError'];
|
|
67
|
+
}
|
|
68
|
+
if (o['key'] !== undefined && o['key'] !== null)
|
|
69
|
+
out.key = String(o['key']);
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
/** A result as the API shows it: undo data stays on the server. */
|
|
73
|
+
export const shown = ({ undo: _, ...result }) => result;
|
|
74
|
+
export const isXlsx = (bytes) => bytes[0] === 0x50 && bytes[1] === 0x4b && bytes[2] === 0x03 && bytes[3] === 0x04;
|
|
75
|
+
/** The cells of a file. @throws {HttpProblem} 422 when it cannot be read */
|
|
76
|
+
export function readTable(bytes, type, sheet) {
|
|
77
|
+
try {
|
|
78
|
+
return type === 'xlsx' ? readXlsx(bytes, sheet ?? undefined).rows : readCsv(bytes).rows;
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
if (!(error instanceof XlsxError))
|
|
82
|
+
throw error;
|
|
83
|
+
throw new HttpProblem(422, 'unreadable-file', 'The file cannot be read', error.message);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
export const dataRows = (table, options) => Math.max(0, table.length - (options.headerRow === false ? 0 : 1));
|
|
87
|
+
const SESSION_FIELDS = [
|
|
88
|
+
'resModel',
|
|
89
|
+
'fileName',
|
|
90
|
+
'fileType',
|
|
91
|
+
'file',
|
|
92
|
+
'sheet',
|
|
93
|
+
'columns',
|
|
94
|
+
'options',
|
|
95
|
+
'state',
|
|
96
|
+
'rowCount',
|
|
97
|
+
'undo',
|
|
98
|
+
'rejected',
|
|
99
|
+
'importedAt',
|
|
100
|
+
'result',
|
|
101
|
+
'resultDry',
|
|
102
|
+
'failure',
|
|
103
|
+
];
|
|
104
|
+
export function createImportService(context, limits = {}) {
|
|
105
|
+
const inlineRows = limits.inlineRows ?? INLINE_ROWS;
|
|
106
|
+
const { driver, shutro } = context;
|
|
107
|
+
const registry = context.registry.registry;
|
|
108
|
+
const models = publicApiModels(context.registry);
|
|
109
|
+
/** How records of `name` are found from text: its name field and its own unique char fields. */
|
|
110
|
+
const targets = (name) => {
|
|
111
|
+
if (!registry.models.has(name))
|
|
112
|
+
return null;
|
|
113
|
+
const description = registry.describe(name);
|
|
114
|
+
const keys = new Set();
|
|
115
|
+
const unique = [
|
|
116
|
+
...description.sqlConstraints.filter((c) => c.kind === 'unique').map((c) => c.fields),
|
|
117
|
+
...description.indexes.filter((i) => i.unique && !i.partial).map((i) => i.fields),
|
|
118
|
+
];
|
|
119
|
+
for (const fields of unique) {
|
|
120
|
+
const own = fields.filter((f) => f !== 'company');
|
|
121
|
+
const [only] = own;
|
|
122
|
+
if (own.length === 1 && only !== undefined && description.fields[only]?.kind === 'char')
|
|
123
|
+
keys.add(only);
|
|
124
|
+
}
|
|
125
|
+
return { model: name, recName: description.recName, keys: [...keys] };
|
|
126
|
+
};
|
|
127
|
+
const catalog = {
|
|
128
|
+
models,
|
|
129
|
+
targets,
|
|
130
|
+
inverseOf: (model, field) => {
|
|
131
|
+
const raw = registry.get(model).model.fields;
|
|
132
|
+
const inverse = raw[field]?.inverseField;
|
|
133
|
+
return typeof inverse === 'string' ? inverse : null;
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
const guard = (env) => {
|
|
137
|
+
if (!env.administrator && !env.groups.has('import.user'))
|
|
138
|
+
throw new HttpProblem(403, 'access', 'Import users only', 'Importing data needs the Import data access level.');
|
|
139
|
+
};
|
|
140
|
+
/** The model a user may import into. */
|
|
141
|
+
const modelFor = (env, name) => {
|
|
142
|
+
const model = models.get(name);
|
|
143
|
+
if (model === undefined)
|
|
144
|
+
throw new HttpProblem(404, 'unknown-model', 'Unknown model', `No model is named '${name}'.`);
|
|
145
|
+
env.access.check(name, 'import');
|
|
146
|
+
// Creating or updating: one of them, checked again for the mode of the run.
|
|
147
|
+
if (!env.access.can(name, 'create'))
|
|
148
|
+
env.access.check(name, 'write');
|
|
149
|
+
return model;
|
|
150
|
+
};
|
|
151
|
+
const fieldsOf = (env, model) => importFields(env, model, catalog);
|
|
152
|
+
/** The fields a column may fill (not the one2many fields themselves: their lines' fields). */
|
|
153
|
+
const columnFields = (fields) => [...fields.values()].filter((f) => f.kind !== 'one2many');
|
|
154
|
+
/** The caller's session `id` (a record rule hides the others: 404). */
|
|
155
|
+
const sessionOf = async (env, id) => {
|
|
156
|
+
guard(env);
|
|
157
|
+
const rows = await ref(env, 'import.session').search({ where: { id }, limit: 1 });
|
|
158
|
+
const [row] = (await rows.load(SESSION_FIELDS)).read(SESSION_FIELDS);
|
|
159
|
+
if (rows.length === 0 || row === undefined)
|
|
160
|
+
throw new HttpProblem(404, 'not-found', 'No such import', `No import #${id} of yours.`);
|
|
161
|
+
return {
|
|
162
|
+
id,
|
|
163
|
+
rows,
|
|
164
|
+
model: modelFor(env, String(row['resModel'])),
|
|
165
|
+
fileName: String(row['fileName']),
|
|
166
|
+
fileType: row['fileType'] === 'xlsx' ? 'xlsx' : 'csv',
|
|
167
|
+
file: row['file'],
|
|
168
|
+
sheet: typeof row['sheet'] === 'string' ? row['sheet'] : null,
|
|
169
|
+
columns: row['columns'] ?? [],
|
|
170
|
+
options: row['options'] ?? {},
|
|
171
|
+
state: String(row['state']),
|
|
172
|
+
rowCount: Number(row['rowCount']),
|
|
173
|
+
undo: row['undo'] ?? null,
|
|
174
|
+
rejected: row['rejected'] ?? { errors: [], rows: [] },
|
|
175
|
+
importedAt: typeof row['importedAt'] === 'string' ? new Date(row['importedAt']) : null,
|
|
176
|
+
result: row['result'] ?? null,
|
|
177
|
+
resultDry: row['resultDry'] === true,
|
|
178
|
+
failure: typeof row['failure'] === 'string' ? row['failure'] : null,
|
|
179
|
+
};
|
|
180
|
+
};
|
|
181
|
+
/** How far a background run is (the progress row is the system's). */
|
|
182
|
+
const progressOf = (env, session) => env.asSuperuser({ reason: 'import progress' }, async (su) => {
|
|
183
|
+
const found = await ref(su, 'import.progress').search({ where: { session }, limit: 1 });
|
|
184
|
+
const [row] = (await found.load(['done', 'total'])).read(['done', 'total']);
|
|
185
|
+
return row === undefined ? null : { done: Number(row['done']), total: Number(row['total']) };
|
|
186
|
+
});
|
|
187
|
+
const writeProgress = async (su, session, done, total) => {
|
|
188
|
+
const found = await ref(su, 'import.progress').search({ where: { session }, limit: 1 });
|
|
189
|
+
if (found.length === 0)
|
|
190
|
+
await ref(su, 'import.progress').create([{ session, done, total }]);
|
|
191
|
+
else
|
|
192
|
+
await found.write({ done, total });
|
|
193
|
+
};
|
|
194
|
+
/** Starts a run's progress in the caller's transaction (it commits with the job it announces). */
|
|
195
|
+
const startProgress = (env, session, total) => env.asSuperuser({ reason: 'import progress' }, (su) => writeProgress(su, session, 0, total));
|
|
196
|
+
/** Writes progress in a short transaction of its own, so others see it while the run's goes on. */
|
|
197
|
+
const reportProgress = (company, session, done, total) => shutro.bootstrap({ reason: 'import progress', company }, (su) => writeProgress(su, session, done, total));
|
|
198
|
+
/** What the screen shows of a session. */
|
|
199
|
+
const describe = async (env, session, table) => {
|
|
200
|
+
const fields = fieldsOf(env, session.model);
|
|
201
|
+
const header = session.options.headerRow === false ? [] : (table[0] ?? []);
|
|
202
|
+
return {
|
|
203
|
+
id: session.id,
|
|
204
|
+
model: session.model.name,
|
|
205
|
+
fileName: session.fileName,
|
|
206
|
+
fileType: session.fileType,
|
|
207
|
+
sheet: session.sheet,
|
|
208
|
+
sheets: session.fileType === 'xlsx' ? xlsxSheetNames(session.file) : [],
|
|
209
|
+
state: session.state,
|
|
210
|
+
rowCount: session.rowCount,
|
|
211
|
+
background: session.rowCount > inlineRows,
|
|
212
|
+
headers: header.map((c) => (c === null ? '' : String(c))),
|
|
213
|
+
sample: table
|
|
214
|
+
.slice(session.options.headerRow === false ? 0 : 1, SAMPLE_ROWS + 1)
|
|
215
|
+
.map((r) => r.map((c) => (c instanceof Date ? c.toISOString() : c))),
|
|
216
|
+
columns: session.columns,
|
|
217
|
+
options: session.options,
|
|
218
|
+
fields: columnFields(fields).map((f) => ({
|
|
219
|
+
key: f.key,
|
|
220
|
+
kind: f.kind,
|
|
221
|
+
label: f.label,
|
|
222
|
+
required: f.required,
|
|
223
|
+
keyable: f.keyable,
|
|
224
|
+
...(f.line === null ? {} : { line: f.line }),
|
|
225
|
+
...(f.target === null ? {} : { target: f.target.model }),
|
|
226
|
+
})),
|
|
227
|
+
progress: session.state === 'running' ? await progressOf(env, session.id) : null,
|
|
228
|
+
result: session.result,
|
|
229
|
+
resultDry: session.resultDry,
|
|
230
|
+
failure: session.failure,
|
|
231
|
+
importedAt: session.importedAt?.toISOString() ?? null,
|
|
232
|
+
undoUntil: session.state === 'done' && session.importedAt !== null && session.undo !== null
|
|
233
|
+
? new Date(session.importedAt.getTime() + UNDO_DAYS * 86_400_000).toISOString()
|
|
234
|
+
: null,
|
|
235
|
+
rejected: session.rejected.rows.length,
|
|
236
|
+
};
|
|
237
|
+
};
|
|
238
|
+
/** The saved mappings the user sees for `model`, and the templates installed modules ship for it. */
|
|
239
|
+
const mappingsFor = async (env, model) => {
|
|
240
|
+
const templates = context.registry.loaded.modules.flatMap((m) => (m.importTemplates ?? [])
|
|
241
|
+
.filter((t) => t.model === model)
|
|
242
|
+
.map((t) => ({
|
|
243
|
+
id: `${m.name}.${t.key}`,
|
|
244
|
+
name: t.label,
|
|
245
|
+
model,
|
|
246
|
+
template: true,
|
|
247
|
+
shared: true,
|
|
248
|
+
own: false,
|
|
249
|
+
columns: t.columns,
|
|
250
|
+
options: parseOptions(t.options),
|
|
251
|
+
})));
|
|
252
|
+
const keys = ['name', 'user', 'shared', 'columns', 'options'];
|
|
253
|
+
const found = await ref(env, 'import.mapping').search({ where: { resModel: model } });
|
|
254
|
+
const saved = (await found.load(keys)).read(keys).map((row) => ({
|
|
255
|
+
id: String(row['id']),
|
|
256
|
+
name: String(row['name']),
|
|
257
|
+
model,
|
|
258
|
+
template: false,
|
|
259
|
+
shared: row['shared'] === true,
|
|
260
|
+
own: row['user'] === env.uid,
|
|
261
|
+
columns: row['columns'] ?? [],
|
|
262
|
+
options: row['options'] ?? {},
|
|
263
|
+
}));
|
|
264
|
+
return [...templates, ...saved];
|
|
265
|
+
};
|
|
266
|
+
/** The mapping whose every header the file has, the one with the most columns; null when none fits. */
|
|
267
|
+
const fitting = (mappings, headers) => {
|
|
268
|
+
const present = new Set(headers.map(normalize));
|
|
269
|
+
let best = null;
|
|
270
|
+
for (const m of mappings)
|
|
271
|
+
if (m.columns.length > 0 &&
|
|
272
|
+
m.columns.every((c) => present.has(normalize(c.header))) &&
|
|
273
|
+
(best === null || m.columns.length > best.columns.length))
|
|
274
|
+
best = m;
|
|
275
|
+
return best;
|
|
276
|
+
};
|
|
277
|
+
/** The columns of a file under a saved mapping: by header; fields the user may not import are left out. */
|
|
278
|
+
const applyMapping = (mapping, headers, fields) => {
|
|
279
|
+
const byHeader = new Map(mapping.columns.map((c) => [normalize(c.header), c.field]));
|
|
280
|
+
const taken = new Set();
|
|
281
|
+
return headers.map((header, index) => {
|
|
282
|
+
const field = byHeader.get(normalize(header)) ?? null;
|
|
283
|
+
if (field === null ||
|
|
284
|
+
taken.has(field) ||
|
|
285
|
+
!fields.has(field) ||
|
|
286
|
+
fields.get(field)?.kind === 'one2many')
|
|
287
|
+
return { index, header, field: null };
|
|
288
|
+
taken.add(field);
|
|
289
|
+
return { index, header, field };
|
|
290
|
+
});
|
|
291
|
+
};
|
|
292
|
+
/** Runs the session's import (or its dry run) as `env`'s user, with the checks of its mode. */
|
|
293
|
+
const execute = async (env, session, dryRun, onProgress) => {
|
|
294
|
+
const mode = session.options.mode ?? 'create';
|
|
295
|
+
if (mode !== 'update')
|
|
296
|
+
env.access.check(session.model.name, 'create');
|
|
297
|
+
if (mode !== 'create')
|
|
298
|
+
env.access.check(session.model.name, 'write');
|
|
299
|
+
const table = readTable(session.file, session.fileType, session.sheet);
|
|
300
|
+
return runImport(env, session.model, fieldsOf(env, session.model), table, session.columns, {
|
|
301
|
+
...session.options,
|
|
302
|
+
dryRun,
|
|
303
|
+
...(onProgress === undefined ? {} : { onProgress }),
|
|
304
|
+
});
|
|
305
|
+
};
|
|
306
|
+
/** Records the outcome of a run on the session; a kept import is announced and audited. */
|
|
307
|
+
const record = async (env, session, result, dryRun, ip) => {
|
|
308
|
+
const view = shown(result);
|
|
309
|
+
if (dryRun || result.undo === null) {
|
|
310
|
+
await session.rows.write({ state: 'draft', result: view, resultDry: dryRun, failure: null });
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
await session.rows.write({
|
|
314
|
+
state: 'done',
|
|
315
|
+
createdIds: [...result.created],
|
|
316
|
+
undo: result.undo,
|
|
317
|
+
rejected: { errors: [...result.errors], rows: [...result.rejectedRows] },
|
|
318
|
+
importedAt: new Date(),
|
|
319
|
+
result: view,
|
|
320
|
+
resultDry: false,
|
|
321
|
+
failure: null,
|
|
322
|
+
});
|
|
323
|
+
if (result.created.length > 0)
|
|
324
|
+
announceChange(env, driver, {
|
|
325
|
+
model: session.model.name,
|
|
326
|
+
ids: [...result.created],
|
|
327
|
+
op: 'create',
|
|
328
|
+
});
|
|
329
|
+
if (result.updated.length > 0)
|
|
330
|
+
announceChange(env, driver, {
|
|
331
|
+
model: session.model.name,
|
|
332
|
+
ids: [...result.updated],
|
|
333
|
+
op: 'update',
|
|
334
|
+
});
|
|
335
|
+
await audit(env, {
|
|
336
|
+
kind: 'data.import',
|
|
337
|
+
summary: `Imported ${session.fileName} into ${session.model.name}: ${result.created.length} created, ${result.updated.length} updated, ${result.skipped} left out.`,
|
|
338
|
+
targetModel: session.model.name,
|
|
339
|
+
details: {
|
|
340
|
+
created: result.created.length,
|
|
341
|
+
updated: result.updated.length,
|
|
342
|
+
skipped: result.skipped,
|
|
343
|
+
file: session.fileName,
|
|
344
|
+
session: session.id,
|
|
345
|
+
},
|
|
346
|
+
...(ip === undefined ? {} : { ip }),
|
|
347
|
+
});
|
|
348
|
+
};
|
|
349
|
+
return {
|
|
350
|
+
inlineRows,
|
|
351
|
+
catalog,
|
|
352
|
+
guard,
|
|
353
|
+
modelFor,
|
|
354
|
+
fieldsOf,
|
|
355
|
+
columnFields,
|
|
356
|
+
sessionOf,
|
|
357
|
+
progressOf,
|
|
358
|
+
startProgress,
|
|
359
|
+
reportProgress,
|
|
360
|
+
describe,
|
|
361
|
+
mappingsFor,
|
|
362
|
+
fitting,
|
|
363
|
+
applyMapping,
|
|
364
|
+
execute,
|
|
365
|
+
record,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "Import" next to every list's "New" (RFC 0014 §5.8): shown when the user may import into the model and the import
|
|
3
|
+
* module answers; it opens the import screen.
|
|
4
|
+
*/
|
|
5
|
+
import { type CreateAssistProps } from '../../ui/index.js';
|
|
6
|
+
import { type ReactElement } from 'react';
|
|
7
|
+
export declare function ImportButton({ api, model }: CreateAssistProps): ReactElement | null;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* "Import" next to every list's "New" (RFC 0014 §5.8): shown when the user may import into the model and the import
|
|
4
|
+
* module answers; it opens the import screen.
|
|
5
|
+
*/
|
|
6
|
+
import { Button } from '../../ui/index.js';
|
|
7
|
+
import { useQuery } from '@tanstack/react-query';
|
|
8
|
+
import { Upload } from 'lucide-react';
|
|
9
|
+
import { useState } from 'react';
|
|
10
|
+
import { useTranslation } from 'react-i18next';
|
|
11
|
+
import { ImportWizard } from './ImportWizard.js';
|
|
12
|
+
export function ImportButton({ api, model }) {
|
|
13
|
+
const { t } = useTranslation('import');
|
|
14
|
+
const [open, setOpen] = useState(false);
|
|
15
|
+
const meta = useQuery({
|
|
16
|
+
queryKey: ['meta', model],
|
|
17
|
+
queryFn: () => api.meta(model),
|
|
18
|
+
staleTime: 60_000,
|
|
19
|
+
});
|
|
20
|
+
// The import module may not be installed, or the user may lack Import data: then the button stays hidden.
|
|
21
|
+
const available = useQuery({
|
|
22
|
+
queryKey: ['import', 'mappings', model],
|
|
23
|
+
queryFn: () => api.call('GET', `/api/v1/import-mappings?model=${encodeURIComponent(model)}`),
|
|
24
|
+
enabled: meta.data?.operations.import === true,
|
|
25
|
+
retry: false,
|
|
26
|
+
staleTime: 60_000,
|
|
27
|
+
});
|
|
28
|
+
if (meta.data?.operations.import !== true || available.data === undefined)
|
|
29
|
+
return null;
|
|
30
|
+
return (_jsxs(_Fragment, { children: [_jsxs(Button, { size: "sm", variant: "secondary", onClick: () => setOpen(true), children: [_jsx(Upload, { "aria-hidden": true }), t('import')] }), _jsx(ImportWizard, { api: api, model: model, open: open, onOpenChange: setOpen })] }));
|
|
31
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The import screen (RFC 0014 §5.8): three steps in a wide dialog. The file (with a sample workbook to start from);
|
|
3
|
+
* the columns (each with its first values and the field it fills, the mode and key, what to do with errors, saved
|
|
4
|
+
* mappings); the result (counts, the errors with their row and column, the rejected rows, undo). Large files run
|
|
5
|
+
* in the background and the dialog follows their progress. The server reads, converts and checks everything; the
|
|
6
|
+
* screen only shows what it says.
|
|
7
|
+
*/
|
|
8
|
+
import { type KoshApi } from '../../ui/index.js';
|
|
9
|
+
import { type ReactElement } from 'react';
|
|
10
|
+
interface Field {
|
|
11
|
+
readonly key: string;
|
|
12
|
+
readonly kind: string;
|
|
13
|
+
readonly label: string;
|
|
14
|
+
readonly required: boolean;
|
|
15
|
+
readonly keyable: boolean;
|
|
16
|
+
readonly line?: string;
|
|
17
|
+
}
|
|
18
|
+
interface Options {
|
|
19
|
+
readonly mode?: 'create' | 'update' | 'upsert';
|
|
20
|
+
readonly key?: string;
|
|
21
|
+
readonly onError?: 'stop' | 'skip';
|
|
22
|
+
readonly dateOrder?: 'dmy' | 'mdy' | 'ymd';
|
|
23
|
+
readonly decimal?: '.' | ',';
|
|
24
|
+
}
|
|
25
|
+
interface RowError {
|
|
26
|
+
readonly row: number;
|
|
27
|
+
readonly column: number | null;
|
|
28
|
+
readonly code: string;
|
|
29
|
+
readonly message: string;
|
|
30
|
+
}
|
|
31
|
+
interface Result {
|
|
32
|
+
readonly rows: number;
|
|
33
|
+
readonly created: readonly number[];
|
|
34
|
+
readonly updated: readonly number[];
|
|
35
|
+
readonly wouldCreate: number;
|
|
36
|
+
readonly wouldUpdate: number;
|
|
37
|
+
readonly skipped: number;
|
|
38
|
+
readonly errors: readonly RowError[];
|
|
39
|
+
}
|
|
40
|
+
export interface ImportSession {
|
|
41
|
+
readonly id: number;
|
|
42
|
+
readonly model: string;
|
|
43
|
+
readonly fileName: string;
|
|
44
|
+
readonly state: 'draft' | 'running' | 'done' | 'undone';
|
|
45
|
+
readonly rowCount: number;
|
|
46
|
+
readonly background: boolean;
|
|
47
|
+
readonly headers: readonly string[];
|
|
48
|
+
readonly sample: readonly (readonly unknown[])[];
|
|
49
|
+
readonly columns: readonly {
|
|
50
|
+
readonly index: number;
|
|
51
|
+
readonly field: string | null;
|
|
52
|
+
}[];
|
|
53
|
+
readonly options: Options;
|
|
54
|
+
readonly fields: readonly Field[];
|
|
55
|
+
readonly progress: {
|
|
56
|
+
readonly done: number;
|
|
57
|
+
readonly total: number;
|
|
58
|
+
} | null;
|
|
59
|
+
readonly result: Result | null;
|
|
60
|
+
readonly resultDry: boolean;
|
|
61
|
+
readonly failure: string | null;
|
|
62
|
+
readonly undoUntil: string | null;
|
|
63
|
+
readonly rejected: number;
|
|
64
|
+
readonly applied?: {
|
|
65
|
+
readonly id: string;
|
|
66
|
+
readonly name: string;
|
|
67
|
+
} | null;
|
|
68
|
+
}
|
|
69
|
+
export declare function ImportWizard({ api, model, open, onOpenChange, }: {
|
|
70
|
+
readonly api: KoshApi;
|
|
71
|
+
readonly model: string;
|
|
72
|
+
readonly open: boolean;
|
|
73
|
+
readonly onOpenChange: (open: boolean) => void;
|
|
74
|
+
}): ReactElement;
|
|
75
|
+
export {};
|