@endora-commerce/mod-import-export 0.100.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +53 -0
  3. package/dist/admin/index.d.ts +30 -0
  4. package/dist/admin/index.d.ts.map +1 -0
  5. package/dist/admin/index.js +36 -0
  6. package/dist/admin/index.js.map +1 -0
  7. package/dist/admin/pages/ImportExportPage.d.ts +3 -0
  8. package/dist/admin/pages/ImportExportPage.d.ts.map +1 -0
  9. package/dist/admin/pages/ImportExportPage.js +71 -0
  10. package/dist/admin/pages/ImportExportPage.js.map +1 -0
  11. package/dist/backend/index.d.ts +43 -0
  12. package/dist/backend/index.d.ts.map +1 -0
  13. package/dist/backend/index.js +54 -0
  14. package/dist/backend/index.js.map +1 -0
  15. package/dist/backend/routes.d.ts +9 -0
  16. package/dist/backend/routes.d.ts.map +1 -0
  17. package/dist/backend/routes.js +44 -0
  18. package/dist/backend/routes.js.map +1 -0
  19. package/dist/backend/services/adapter.d.ts +59 -0
  20. package/dist/backend/services/adapter.d.ts.map +1 -0
  21. package/dist/backend/services/adapter.js +2 -0
  22. package/dist/backend/services/adapter.js.map +1 -0
  23. package/dist/backend/services/adapters/categories.adapter.d.ts +19 -0
  24. package/dist/backend/services/adapters/categories.adapter.d.ts.map +1 -0
  25. package/dist/backend/services/adapters/categories.adapter.js +93 -0
  26. package/dist/backend/services/adapters/categories.adapter.js.map +1 -0
  27. package/dist/backend/services/adapters/customers.adapter.d.ts +16 -0
  28. package/dist/backend/services/adapters/customers.adapter.d.ts.map +1 -0
  29. package/dist/backend/services/adapters/customers.adapter.js +48 -0
  30. package/dist/backend/services/adapters/customers.adapter.js.map +1 -0
  31. package/dist/backend/services/adapters/orders.adapter.d.ts +13 -0
  32. package/dist/backend/services/adapters/orders.adapter.d.ts.map +1 -0
  33. package/dist/backend/services/adapters/orders.adapter.js +48 -0
  34. package/dist/backend/services/adapters/orders.adapter.js.map +1 -0
  35. package/dist/backend/services/adapters/products.adapter.d.ts +16 -0
  36. package/dist/backend/services/adapters/products.adapter.d.ts.map +1 -0
  37. package/dist/backend/services/adapters/products.adapter.js +88 -0
  38. package/dist/backend/services/adapters/products.adapter.js.map +1 -0
  39. package/dist/backend/services/adapters/stock.adapter.d.ts +15 -0
  40. package/dist/backend/services/adapters/stock.adapter.d.ts.map +1 -0
  41. package/dist/backend/services/adapters/stock.adapter.js +62 -0
  42. package/dist/backend/services/adapters/stock.adapter.js.map +1 -0
  43. package/dist/backend/services/csv-codec.d.ts +17 -0
  44. package/dist/backend/services/csv-codec.d.ts.map +1 -0
  45. package/dist/backend/services/csv-codec.js +95 -0
  46. package/dist/backend/services/csv-codec.js.map +1 -0
  47. package/dist/backend/services/import-export-service.d.ts +52 -0
  48. package/dist/backend/services/import-export-service.d.ts.map +1 -0
  49. package/dist/backend/services/import-export-service.js +100 -0
  50. package/dist/backend/services/import-export-service.js.map +1 -0
  51. package/dist/manifest.d.ts +169 -0
  52. package/dist/manifest.d.ts.map +1 -0
  53. package/dist/manifest.js +138 -0
  54. package/dist/manifest.js.map +1 -0
  55. package/docs/import_export.md +79 -0
  56. package/i18n/en.json +27 -0
  57. package/i18n/pl.json +27 -0
  58. package/package.json +82 -0
  59. package/tailwind.css +14 -0
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Import / Export module — bulk product import/export wizards.
3
+ *
4
+ * Manifest backfilled alongside feature 020 so the module can declare
5
+ * command-palette actions. Same legacy-module note as catalog: the
6
+ * underlying behaviour is foundational; hard-uninstall is intentionally
7
+ * unsupported in this iteration.
8
+ */
9
+ export declare const manifest: {
10
+ id: string;
11
+ name: string;
12
+ version: string;
13
+ dependencies: string[];
14
+ description?: string | undefined;
15
+ acknowledgedDependencies?: {
16
+ moduleId: string;
17
+ port: string;
18
+ reason: string;
19
+ }[] | undefined;
20
+ nonBindingDependencies?: {
21
+ moduleId: string;
22
+ name: string;
23
+ kind: "contributes-to" | "degrades-without" | "refuses-without";
24
+ reason: string;
25
+ whenAbsent?: string | undefined;
26
+ }[] | undefined;
27
+ activation?: {
28
+ settingCode: string;
29
+ default: boolean;
30
+ } | {
31
+ nonDeactivatable: true;
32
+ reason: string;
33
+ } | undefined;
34
+ settings?: {
35
+ moduleCode: string;
36
+ groups: {
37
+ code: string;
38
+ name: string;
39
+ salesChannelCodes?: string[] | undefined;
40
+ isSystemProtected?: boolean | undefined;
41
+ }[];
42
+ settings: {
43
+ code: string;
44
+ name: string;
45
+ valueType: "string" | "number" | "boolean" | "json" | "string_list" | "secret" | "credential_ref";
46
+ defaultValue: unknown;
47
+ description?: string | undefined;
48
+ groupCode?: string | undefined;
49
+ previousDefaultValues?: unknown[] | undefined;
50
+ salesChannelCodes?: string[] | undefined;
51
+ enumOptions?: string[] | undefined;
52
+ configurationType?: string | undefined;
53
+ hidden?: boolean | undefined;
54
+ }[];
55
+ } | undefined;
56
+ i18n?: {
57
+ bundlesDir: string;
58
+ } | undefined;
59
+ docs?: false | {
60
+ dir: string;
61
+ } | undefined;
62
+ demo?: false | {
63
+ summary: string;
64
+ seed: (context: import("@endora-commerce/contracts").ModuleDemoContext<never>) => Promise<import("@endora-commerce/contracts").DemoSeedResult>;
65
+ reset: (context: import("@endora-commerce/contracts").ModuleDemoContext<never>) => Promise<import("@endora-commerce/contracts").DemoResetResult>;
66
+ after?: readonly string[] | undefined;
67
+ package?: string | undefined;
68
+ } | undefined;
69
+ actions?: {
70
+ id: string;
71
+ labelKey: string;
72
+ icon: "Plus" | "Sparkles" | "Settings" | "Search" | "Boxes" | "Layers" | "Menu" | "PlusCircle" | "PlusSquare" | "FilePlus" | "FolderPlus" | "Upload" | "FileUp" | "CloudUpload" | "Download" | "FileDown" | "FileText" | "BookOpen" | "Rss" | "Package" | "Tag" | "ShoppingCart" | "Receipt" | "CreditCard" | "Users" | "UserPlus" | "Inbox" | "ListChecks" | "ClipboardList" | "Image" | "Video" | "LayoutDashboard" | "PanelLeft" | "KeyRound" | "ShieldCheck" | "Edit" | "Archive" | "Box" | "Truck" | "CircleDollarSign" | "Activity" | "LineChart" | "Smartphone" | "Webhook" | "Scale" | "PlugZap" | "PercentDiamond" | "Newspaper" | "Languages" | "Eraser" | "Warehouse" | "TrendingDown" | "Bell" | "PackageOpen" | "Building2" | "Store" | "ClipboardCheck";
73
+ targetRoute: string;
74
+ keywords: string[];
75
+ weight: number;
76
+ descriptionKey?: string | undefined;
77
+ requiredPermission?: string | undefined;
78
+ }[] | undefined;
79
+ permissions?: {
80
+ code: string;
81
+ label: string;
82
+ module?: string | undefined;
83
+ description?: string | undefined;
84
+ requires?: string[] | undefined;
85
+ }[] | undefined;
86
+ transactionalEmails?: {
87
+ code: string;
88
+ name: string;
89
+ variables: {
90
+ key: string;
91
+ label: string;
92
+ sampleValue?: string | undefined;
93
+ description?: string | undefined;
94
+ }[];
95
+ description?: string | undefined;
96
+ group?: string | undefined;
97
+ }[] | undefined;
98
+ capabilities?: string[] | undefined;
99
+ exclusiveCapabilities?: {
100
+ key: string;
101
+ errorCode: string;
102
+ }[] | undefined;
103
+ errorCodes?: {
104
+ code: string;
105
+ tokens?: string[] | undefined;
106
+ }[] | undefined;
107
+ blocks?: {
108
+ name: string;
109
+ labelKey: string;
110
+ category: string;
111
+ contexts: ("email" | "invoice" | "cms" | "newsletter")[];
112
+ fields: Record<string, {
113
+ type: "number" | "object" | "array" | "text" | "textarea" | "select" | "radio" | "external" | "uuid" | "richtext";
114
+ label?: string | undefined;
115
+ required?: boolean | undefined;
116
+ options?: {
117
+ label: string;
118
+ value: string | number;
119
+ }[] | undefined;
120
+ refKind?: string | undefined;
121
+ }>;
122
+ descriptionKey?: string | undefined;
123
+ defaultProps?: Record<string, unknown> | undefined;
124
+ responsiveFields?: string[] | undefined;
125
+ previewIcon?: string | undefined;
126
+ weight?: number | undefined;
127
+ }[] | undefined;
128
+ blockCategories?: {
129
+ key: string;
130
+ titleKey: string;
131
+ contexts: ("email" | "invoice" | "cms" | "newsletter")[];
132
+ weight?: number | undefined;
133
+ visible?: boolean | undefined;
134
+ }[] | undefined;
135
+ env?: {
136
+ name: string;
137
+ describes: {
138
+ en: string;
139
+ pl: string;
140
+ };
141
+ requirement: {
142
+ kind: "required";
143
+ } | {
144
+ kind: "requiredWhen";
145
+ input: string;
146
+ equals: string;
147
+ } | {
148
+ kind: "optional";
149
+ without: {
150
+ en: string;
151
+ pl: string;
152
+ };
153
+ };
154
+ secret: boolean;
155
+ generable: boolean;
156
+ owner: {
157
+ kind: "platform";
158
+ } | {
159
+ kind: "application";
160
+ application: "admin" | "backend" | "storefront";
161
+ } | {
162
+ kind: "module";
163
+ moduleId: string;
164
+ };
165
+ consumers: ("admin" | "backend" | "storefront")[];
166
+ addressOf: "admin" | "backend" | "storefront" | null;
167
+ }[] | undefined;
168
+ };
169
+ //# sourceMappingURL=manifest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AACH,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgJnB,CAAC"}
@@ -0,0 +1,138 @@
1
+ import { defineModuleManifest } from '@endora-commerce/contracts';
2
+ /**
3
+ * Import / Export module — bulk product import/export wizards.
4
+ *
5
+ * Manifest backfilled alongside feature 020 so the module can declare
6
+ * command-palette actions. Same legacy-module note as catalog: the
7
+ * underlying behaviour is foundational; hard-uninstall is intentionally
8
+ * unsupported in this iteration.
9
+ */
10
+ export const manifest = defineModuleManifest({
11
+ id: 'import_export',
12
+ name: 'Import / Export',
13
+ description: 'Bulk product import and export wizard.',
14
+ version: '1.0.0',
15
+ // `auth` owns the `requireAdmin` port the routes are gated by; feature 072
16
+ // made it a container resolution rather than a constructor argument. It is
17
+ // the only **binding** dependency: every route is admin-gated, so an
18
+ // import/export centre without `auth` is a centre with no door.
19
+ //
20
+ // `catalog` left this list in D-74, and the direction is the point. It used
21
+ // to be here while `inventory` — whose `stock_levels` this module wrote —
22
+ // was not, an edge no check in the tree could see. Both are declared now,
23
+ // and neither binds: an operator must be able to switch `orders` off without
24
+ // being told an import module forbids it.
25
+ dependencies: ['auth'],
26
+ /**
27
+ * D-74 — the five owners whose rows this module reads and writes, declared as
28
+ * real container edges that bind no operator.
29
+ *
30
+ * `degrades-without` obliges the module to **check presence before it reads**,
31
+ * and it does: the adapter table carries the owning module ids per entity and
32
+ * the supported-entity list is filtered by `effectiveState.isPresent`, so a
33
+ * switched-off owner removes one entity from
34
+ * `GET /api/v1/admin/import-export/entities` and makes a POST naming it the
35
+ * same 404 an unknown slug gets. That is what makes these declarations true
36
+ * rather than decorative — before it, the five slugs were a literal array in
37
+ * the admin SPA.
38
+ */
39
+ nonBindingDependencies: [
40
+ {
41
+ moduleId: 'catalog',
42
+ name: 'catalogProductReadPort',
43
+ kind: 'degrades-without',
44
+ whenAbsent: 'The import/export centre stops offering the Products and Stock entities; every other entity is unaffected.',
45
+ reason: 'The products export reads the catalogue through `catalog`\'s published read port. An operator switching the catalogue off should lose the products sheet, not the orders and customers sheets beside it.',
46
+ },
47
+ {
48
+ moduleId: 'catalog',
49
+ name: 'catalogCategoryReadPort',
50
+ kind: 'degrades-without',
51
+ whenAbsent: 'The import/export centre stops offering the Categories entity; every other entity is unaffected.',
52
+ reason: 'The categories export reads the tree through `catalog`\'s published read port; the entity is withdrawn from the offered list rather than answering 503.',
53
+ },
54
+ {
55
+ moduleId: 'catalog',
56
+ name: 'catalogBulkImportPort',
57
+ kind: 'degrades-without',
58
+ whenAbsent: 'The import/export centre stops offering the Products and Categories imports; every other entity is unaffected.',
59
+ reason: 'A catalogue import is `catalog`\'s transaction and `catalog`\'s audit row (D-74). With the module off the entity is not offered, so the operator never reaches the gate.',
60
+ },
61
+ {
62
+ moduleId: 'inventory',
63
+ name: 'inventoryStockReadPort',
64
+ kind: 'degrades-without',
65
+ whenAbsent: 'The import/export centre stops offering the Stock entity; every other entity is unaffected.',
66
+ reason: 'The stock export joins `inventory`\'s levels onto `catalog`\'s SKUs. This module wrote `stock_levels` for a year without declaring the edge at all; declaring it non-bindingly is what keeps the centre usable while stock management is off.',
67
+ },
68
+ {
69
+ moduleId: 'inventory',
70
+ name: 'inventoryStockImportPort',
71
+ kind: 'degrades-without',
72
+ whenAbsent: 'The import/export centre stops offering the Stock import; every other entity is unaffected.',
73
+ reason: 'A stock import is `inventory`\'s transaction and `inventory`\'s audit row (D-74), including the seeded warehouse this module used to name by a copied UUID.',
74
+ },
75
+ {
76
+ moduleId: 'orders',
77
+ name: 'orderReadPort',
78
+ kind: 'degrades-without',
79
+ whenAbsent: 'The import/export centre stops offering the Orders export; every other entity is unaffected.',
80
+ reason: 'Export only — orders are produced by the checkout flow. `OrderReadPort.listAll` was published by Phase P with "for the bulk export adapter" in its own doc comment; this is that adapter.',
81
+ },
82
+ {
83
+ moduleId: 'customer_accounts',
84
+ name: 'customerAccountReadPort',
85
+ kind: 'degrades-without',
86
+ whenAbsent: 'The import/export centre stops offering the Customers export; every other entity is unaffected.',
87
+ reason: 'Export only — provisioning accounts needs a password story a CSV upload has no place for. `CustomerAccountReadPort.listAll` was published for this adapter by name.',
88
+ },
89
+ {
90
+ moduleId: 'organizations',
91
+ name: 'organizationDetailsPort',
92
+ kind: 'degrades-without',
93
+ whenAbsent: 'The import/export centre stops offering the Customers export; every other entity is unaffected.',
94
+ reason: 'The customers sheet names each account\'s organisation, so that one entity needs both owners present. No other entity reads this port.',
95
+ },
96
+ ],
97
+ settings: {
98
+ moduleCode: 'import_export',
99
+ groups: [{ code: 'import_export', name: 'Import / export' }],
100
+ settings: [
101
+ {
102
+ // Feature 073 — the operator's activation control. Platform-wide.
103
+ code: 'import_export.enabled',
104
+ name: 'Import / export enabled',
105
+ description: 'Switches the CSV import and export endpoints on or off. The module holds no data of its own, so nothing is dropped — the catalog it reads and writes is untouched either way.',
106
+ groupCode: 'import_export',
107
+ valueType: 'boolean',
108
+ defaultValue: true,
109
+ },
110
+ ],
111
+ },
112
+ i18n: { bundlesDir: 'i18n' },
113
+ docs: { dir: 'docs' },
114
+ actions: [
115
+ {
116
+ id: 'import-products',
117
+ labelKey: 'actions.importProducts.label',
118
+ descriptionKey: 'actions.importProducts.description',
119
+ icon: 'Upload',
120
+ targetRoute: '/import-export',
121
+ requiredPermission: 'catalog:write',
122
+ keywords: ['import', 'csv', 'excel', 'upload', 'wgraj'],
123
+ weight: 110,
124
+ },
125
+ {
126
+ id: 'open-import-export-center',
127
+ labelKey: 'actions.openCenter.label',
128
+ descriptionKey: 'actions.openCenter.description',
129
+ icon: 'FileUp',
130
+ targetRoute: '/import-export',
131
+ requiredPermission: 'catalog:write',
132
+ keywords: ['import', 'export', 'center', 'centrum'],
133
+ weight: 220,
134
+ },
135
+ ],
136
+ activation: { settingCode: 'import_export.enabled', default: true },
137
+ });
138
+ //# sourceMappingURL=manifest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.js","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,79 @@
1
+ ---
2
+ title: import_export
3
+ description: CSV import / export for bulk-edit entities
4
+ ---
5
+
6
+ # `import_export`
7
+
8
+ CSV import and export for the bulk-edit cases. Owns a small RFC-4180 codec
9
+ (no new dependency) and a per-entity adapter registry.
10
+
11
+ ## Public surface
12
+
13
+ | Verb + Path | Audience | Purpose |
14
+ | --- | --- | --- |
15
+ | `GET /api/v1/admin/export/:entity.csv` | admin (`catalog:write`) | Streaming CSV download with attachment headers |
16
+ | `POST /api/v1/admin/import/:entity` | admin (`catalog:write`) | Apply a `text/csv` body inside a single transaction |
17
+
18
+ ## Supported entities
19
+
20
+ | Entity | Export | Import | Notes |
21
+ | --- | --- | --- | --- |
22
+ | `products` | ✓ | ✓ | Multilingual `name` / `description` collapse to a single locale (`en-US`); product `type` is immutable post-create |
23
+ | `categories` | ✓ | ✓ | Parents resolved by `parent_slug`; rows must list parents before children |
24
+ | `stock` | ✓ | ✓ | Operators only edit `on_hand`; reservations are read-only |
25
+ | `customers` | ✓ | — | Import is intentionally unsupported — bulk account provisioning needs a password story that doesn't fit a CSV upload |
26
+ | `orders` | ✓ | — | Orders are produced by the checkout flow; importing historical orders would bypass stock, payment, and credit-limit lifecycles |
27
+
28
+ ## Import semantics
29
+
30
+ Every import runs inside one MikroORM transaction. If any single row
31
+ fails validation, the entire batch rolls back and the response surfaces a
32
+ row-numbered error report (1-based, excluding the header line, matching
33
+ what spreadsheet apps display). This makes re-uploads after a fix
34
+ deterministic.
35
+
36
+ ```http
37
+ POST /api/v1/admin/import/products
38
+ Content-Type: text/csv
39
+
40
+ sku,status,visibility,name_en,description_en
41
+ SKU-001,active,public,Updated name,Updated description.
42
+ ```
43
+
44
+ Response:
45
+
46
+ ```json
47
+ {
48
+ "data": {
49
+ "imported": 1,
50
+ "errors": []
51
+ }
52
+ }
53
+ ```
54
+
55
+ If a row fails (e.g. unknown SKU):
56
+
57
+ ```json
58
+ {
59
+ "data": {
60
+ "imported": 0,
61
+ "errors": [
62
+ { "rowNumber": 3, "reason": "unknown sku: SKU-XYZ" }
63
+ ]
64
+ }
65
+ }
66
+ ```
67
+
68
+ ## Extension points
69
+
70
+ - **New entity** — implement `ImportExportAdapter` (`name`,
71
+ `exportHeader`, `exportRows`, optional `importHeader` + `importRow`)
72
+ and register in `import-export-service.ts`.
73
+ - **Different delimiter / Excel quirks** — extend `csv-codec.ts`. RFC
74
+ 4180 covers the common cases; if a real-world file fails, the codec is
75
+ the single place to evolve.
76
+ - **Streaming export** — the in-memory loader fits hundreds of thousands
77
+ of rows comfortably; for sustained million-row exports, swap
78
+ `serializeCsv` for an async iterator that pipes into Fastify's
79
+ `reply.raw`.
package/i18n/en.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "actions.importProducts.description": "Upload products from a CSV or Excel file",
3
+ "actions.importProducts.label": "Import products",
4
+ "actions.openCenter.description": "Browse all import and export operations",
5
+ "actions.openCenter.label": "Open import/export center",
6
+ "column.reason": "Reason",
7
+ "column.rowNumber": "Row #",
8
+ "empty": "No entity is available right now — every module that owns one is switched off.",
9
+ "entity.categories": "Categories",
10
+ "entity.customers": "Customers",
11
+ "entity.orders": "Orders",
12
+ "entity.products": "Products",
13
+ "entity.stock": "Stock levels",
14
+ "error.import": "Import failed.",
15
+ "error.load": "Could not load the available entities.",
16
+ "exportCsv": "Export CSV",
17
+ "importCsv": "Import CSV",
18
+ "importNotSupported": "Import not supported — see the docs site for the rationale.",
19
+ "imported": "Imported {count} row(s).",
20
+ "loading": "Loading available entities…",
21
+ "nav.importExport.label": "Import / Export",
22
+ "page.description": "CSV-only round-trips for the bulk-edit cases. Per-row errors roll the entire upload back, so re-uploads are deterministic.",
23
+ "page.title": "Import / Export",
24
+ "requiredHeader": "Required header:",
25
+ "rolledBack": "Rolled back: {count} row(s) failed. No changes applied.",
26
+ "uploading": "Uploading…"
27
+ }
package/i18n/pl.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "actions.importProducts.description": "Wgraj produkty z pliku CSV lub Excel",
3
+ "actions.importProducts.label": "Importuj produkty",
4
+ "actions.openCenter.description": "Przeglądaj wszystkie operacje importu i eksportu",
5
+ "actions.openCenter.label": "Otwórz centrum importu/eksportu",
6
+ "column.reason": "Powód",
7
+ "column.rowNumber": "Nr wiersza",
8
+ "empty": "Brak dostępnych encji — każdy moduł, który jakąś posiada, jest wyłączony.",
9
+ "entity.categories": "Kategorie",
10
+ "entity.customers": "Klienci",
11
+ "entity.orders": "Zamówienia",
12
+ "entity.products": "Produkty",
13
+ "entity.stock": "Stany magazynowe",
14
+ "error.import": "Import nie powiódł się.",
15
+ "error.load": "Nie udało się wczytać dostępnych encji.",
16
+ "exportCsv": "Eksportuj CSV",
17
+ "importCsv": "Importuj CSV",
18
+ "importNotSupported": "Import nieobsługiwany — rationale znajdziesz w dokumentacji.",
19
+ "imported": "Zaimportowano {count} wierszy.",
20
+ "loading": "Wczytywanie dostępnych encji…",
21
+ "nav.importExport.label": "Import / Eksport",
22
+ "page.description": "Wymiana danych w obie strony tylko przez CSV dla operacji masowej edycji. Błędy per-wiersz wycofują cały upload, więc ponowne wgrania są deterministyczne.",
23
+ "page.title": "Import / Eksport",
24
+ "requiredHeader": "Wymagany nagłówek:",
25
+ "rolledBack": "Wycofano: {count} wierszy nie powiodło się. Brak zmian.",
26
+ "uploading": "Wgrywanie…"
27
+ }
package/package.json ADDED
@@ -0,0 +1,82 @@
1
+ {
2
+ "name": "@endora-commerce/mod-import-export",
3
+ "version": "0.100.0",
4
+ "type": "module",
5
+ "sideEffects": false,
6
+ "description": "Bulk product import and export wizard.",
7
+ "license": "MIT",
8
+ "endora": {
9
+ "type": "module",
10
+ "id": "import_export"
11
+ },
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/endora-commerce/endora-commerce.git",
15
+ "directory": "packages/modules/import_export"
16
+ },
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/manifest.d.ts",
23
+ "default": "./dist/manifest.js"
24
+ },
25
+ "./backend": {
26
+ "types": "./dist/backend/index.d.ts",
27
+ "default": "./dist/backend/index.js"
28
+ },
29
+ "./admin": {
30
+ "types": "./dist/admin/index.d.ts",
31
+ "default": "./dist/admin/index.js"
32
+ },
33
+ "./tailwind.css": "./tailwind.css",
34
+ "./package.json": "./package.json"
35
+ },
36
+ "files": [
37
+ "dist",
38
+ "i18n",
39
+ "docs",
40
+ "tailwind.css"
41
+ ],
42
+ "engines": {
43
+ "node": ">=22.18.0"
44
+ },
45
+ "peerDependencies": {
46
+ "fastify": "^5",
47
+ "lucide-react": "^1",
48
+ "react": "^19",
49
+ "@endora-commerce/admin-kit": "0.100.0",
50
+ "@endora-commerce/platform": "0.100.0",
51
+ "@endora-commerce/contracts": "0.100.0"
52
+ },
53
+ "peerDependenciesMeta": {
54
+ "@endora-commerce/admin-kit": {
55
+ "optional": true
56
+ },
57
+ "lucide-react": {
58
+ "optional": true
59
+ },
60
+ "react": {
61
+ "optional": true
62
+ }
63
+ },
64
+ "devDependencies": {
65
+ "@types/node": "^22.9.0",
66
+ "@types/react": "^19.2.14",
67
+ "fastify": "^5.12.5",
68
+ "lucide-react": "^1.11.0",
69
+ "react": "^19.2.5",
70
+ "typescript": "^5.9.3",
71
+ "vitest": "^4.1.11",
72
+ "@endora-commerce/contracts": "0.100.0",
73
+ "@endora-commerce/admin-kit": "0.100.0",
74
+ "@endora-commerce/platform": "0.100.0"
75
+ },
76
+ "scripts": {
77
+ "build": "tsc -p tsconfig.build.json && tsc -p tsconfig.ui.json",
78
+ "typecheck": "tsc -p tsconfig.json && tsc -p tsconfig.ui.json --noEmit",
79
+ "lint": "eslint src",
80
+ "test": "vitest run"
81
+ }
82
+ }
package/tailwind.css ADDED
@@ -0,0 +1,14 @@
1
+ /* @endora-commerce/mod-import-export — AUTO-GENERATED by `pnpm --filter backend run manifests:generate`.
2
+ *
3
+ * The `@source` directives this package asks its host to scan
4
+ * (`specs/110-instance-repository/contracts/admin-stylesheet-composition.md` R1).
5
+ * They resolve relative to **this file**, so they hold wherever the package is
6
+ * installed — a workspace link here, `node_modules` in a client's instance.
7
+ *
8
+ * The `dist` line is what a published tarball ships and is what an instance
9
+ * scans; the `src` line is inert there and is what keeps `pnpm --filter admin
10
+ * run dev` reading source in this repository. Do not edit: run
11
+ * `pnpm --filter backend run manifests:generate`.
12
+ */
13
+ @source "./dist/admin";
14
+ @source "./src/admin";