@open-mercato/core 0.4.6-develop-be2da141e3 → 0.4.6-develop-1f4478d60c

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.
@@ -0,0 +1,8 @@
1
+ const features = [
2
+ { id: "integrations.view", title: "View integrations and external ID mappings", module: "integrations" },
3
+ { id: "integrations.manage", title: "Manage integration configurations", module: "integrations" }
4
+ ];
5
+ export {
6
+ features
7
+ };
8
+ //# sourceMappingURL=acl.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/modules/integrations/acl.ts"],
4
+ "sourcesContent": ["export const features = [\n { id: 'integrations.view', title: 'View integrations and external ID mappings', module: 'integrations' },\n { id: 'integrations.manage', title: 'Manage integration configurations', module: 'integrations' },\n]\n"],
5
+ "mappings": "AAAO,MAAM,WAAW;AAAA,EACtB,EAAE,IAAI,qBAAqB,OAAO,8CAA8C,QAAQ,eAAe;AAAA,EACvG,EAAE,IAAI,uBAAuB,OAAO,qCAAqC,QAAQ,eAAe;AAClG;",
6
+ "names": []
7
+ }
@@ -0,0 +1,72 @@
1
+ import { getIntegration } from "@open-mercato/shared/modules/integrations/types";
2
+ import { SyncExternalIdMapping } from "./entities.js";
3
+ function buildIntegrationData(mappings) {
4
+ const data = {};
5
+ for (const mapping of mappings) {
6
+ const definition = getIntegration(mapping.integrationId);
7
+ data[mapping.integrationId] = {
8
+ externalId: mapping.externalId,
9
+ externalUrl: definition?.buildExternalUrl?.(mapping.externalId),
10
+ lastSyncedAt: mapping.lastSyncedAt?.toISOString(),
11
+ syncStatus: mapping.syncStatus
12
+ };
13
+ }
14
+ return data;
15
+ }
16
+ const externalIdMappingEnricher = {
17
+ id: "integrations.external-id-mapping",
18
+ targetEntity: "*",
19
+ features: ["integrations.view"],
20
+ priority: 10,
21
+ timeout: 500,
22
+ critical: false,
23
+ fallback: {},
24
+ async enrichOne(record, context) {
25
+ const em = context.em.fork();
26
+ const targetEntity = context.targetEntity;
27
+ if (!targetEntity) return { ...record, _integrations: {} };
28
+ const mappings = await em.find(SyncExternalIdMapping, {
29
+ internalEntityType: targetEntity,
30
+ internalEntityId: record.id,
31
+ organizationId: context.organizationId,
32
+ deletedAt: null
33
+ });
34
+ if (mappings.length === 0) return { ...record, _integrations: {} };
35
+ return {
36
+ ...record,
37
+ _integrations: buildIntegrationData(mappings)
38
+ };
39
+ },
40
+ async enrichMany(records, context) {
41
+ const em = context.em.fork();
42
+ const targetEntity = context.targetEntity;
43
+ if (!targetEntity || records.length === 0) return records.map((r) => ({ ...r, _integrations: {} }));
44
+ const recordIds = records.map((r) => r.id);
45
+ const allMappings = await em.find(SyncExternalIdMapping, {
46
+ internalEntityType: targetEntity,
47
+ internalEntityId: { $in: recordIds },
48
+ organizationId: context.organizationId,
49
+ deletedAt: null
50
+ });
51
+ if (allMappings.length === 0) return records.map((r) => ({ ...r, _integrations: {} }));
52
+ const mappingsByRecord = /* @__PURE__ */ new Map();
53
+ for (const mapping of allMappings) {
54
+ const list = mappingsByRecord.get(mapping.internalEntityId) ?? [];
55
+ list.push(mapping);
56
+ mappingsByRecord.set(mapping.internalEntityId, list);
57
+ }
58
+ return records.map((record) => {
59
+ const mappings = mappingsByRecord.get(record.id);
60
+ if (!mappings || mappings.length === 0) return { ...record, _integrations: {} };
61
+ return {
62
+ ...record,
63
+ _integrations: buildIntegrationData(mappings)
64
+ };
65
+ });
66
+ }
67
+ };
68
+ const enrichers = [externalIdMappingEnricher];
69
+ export {
70
+ enrichers
71
+ };
72
+ //# sourceMappingURL=enrichers.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/integrations/data/enrichers.ts"],
4
+ "sourcesContent": ["/**\n * External ID Mapping Enricher\n *\n * Enriches any entity's API response with external integration ID mappings.\n * Adds `_integrations` namespace containing all external IDs for the record.\n *\n * Uses batch queries via `enrichMany` to prevent N+1.\n */\n\nimport type { ResponseEnricher, EnricherContext } from '@open-mercato/shared/lib/crud/response-enricher'\nimport type { ExternalIdEnrichment } from '@open-mercato/shared/modules/integrations/types'\nimport { getIntegration } from '@open-mercato/shared/modules/integrations/types'\nimport { SyncExternalIdMapping } from './entities'\n\ntype EntityRecord = Record<string, unknown> & { id: string }\n\nfunction buildIntegrationData(\n mappings: SyncExternalIdMapping[],\n): ExternalIdEnrichment['_integrations'] {\n const data: ExternalIdEnrichment['_integrations'] = {}\n\n for (const mapping of mappings) {\n const definition = getIntegration(mapping.integrationId)\n data[mapping.integrationId] = {\n externalId: mapping.externalId,\n externalUrl: definition?.buildExternalUrl?.(mapping.externalId),\n lastSyncedAt: mapping.lastSyncedAt?.toISOString(),\n syncStatus: mapping.syncStatus,\n }\n }\n\n return data\n}\n\nconst externalIdMappingEnricher: ResponseEnricher<EntityRecord, ExternalIdEnrichment> = {\n id: 'integrations.external-id-mapping',\n targetEntity: '*',\n features: ['integrations.view'],\n priority: 10,\n timeout: 500,\n critical: false,\n fallback: {},\n\n async enrichOne(record, context: EnricherContext & { targetEntity?: string }) {\n const em = (context.em as any).fork()\n const targetEntity = (context as any).targetEntity as string | undefined\n if (!targetEntity) return { ...record, _integrations: {} }\n\n const mappings: SyncExternalIdMapping[] = await em.find(SyncExternalIdMapping, {\n internalEntityType: targetEntity,\n internalEntityId: record.id,\n organizationId: context.organizationId,\n deletedAt: null,\n })\n\n if (mappings.length === 0) return { ...record, _integrations: {} }\n\n return {\n ...record,\n _integrations: buildIntegrationData(mappings),\n }\n },\n\n async enrichMany(records, context: EnricherContext & { targetEntity?: string }) {\n const em = (context.em as any).fork()\n const targetEntity = (context as any).targetEntity as string | undefined\n if (!targetEntity || records.length === 0) return records.map((r) => ({ ...r, _integrations: {} }))\n\n const recordIds = records.map((r) => r.id)\n const allMappings: SyncExternalIdMapping[] = await em.find(SyncExternalIdMapping, {\n internalEntityType: targetEntity,\n internalEntityId: { $in: recordIds },\n organizationId: context.organizationId,\n deletedAt: null,\n })\n\n if (allMappings.length === 0) return records.map((r) => ({ ...r, _integrations: {} }))\n\n const mappingsByRecord = new Map<string, SyncExternalIdMapping[]>()\n for (const mapping of allMappings) {\n const list = mappingsByRecord.get(mapping.internalEntityId) ?? []\n list.push(mapping)\n mappingsByRecord.set(mapping.internalEntityId, list)\n }\n\n return records.map((record) => {\n const mappings = mappingsByRecord.get(record.id)\n if (!mappings || mappings.length === 0) return { ...record, _integrations: {} }\n\n return {\n ...record,\n _integrations: buildIntegrationData(mappings),\n }\n })\n },\n}\n\nexport const enrichers: ResponseEnricher[] = [externalIdMappingEnricher]\n"],
5
+ "mappings": "AAWA,SAAS,sBAAsB;AAC/B,SAAS,6BAA6B;AAItC,SAAS,qBACP,UACuC;AACvC,QAAM,OAA8C,CAAC;AAErD,aAAW,WAAW,UAAU;AAC9B,UAAM,aAAa,eAAe,QAAQ,aAAa;AACvD,SAAK,QAAQ,aAAa,IAAI;AAAA,MAC5B,YAAY,QAAQ;AAAA,MACpB,aAAa,YAAY,mBAAmB,QAAQ,UAAU;AAAA,MAC9D,cAAc,QAAQ,cAAc,YAAY;AAAA,MAChD,YAAY,QAAQ;AAAA,IACtB;AAAA,EACF;AAEA,SAAO;AACT;AAEA,MAAM,4BAAkF;AAAA,EACtF,IAAI;AAAA,EACJ,cAAc;AAAA,EACd,UAAU,CAAC,mBAAmB;AAAA,EAC9B,UAAU;AAAA,EACV,SAAS;AAAA,EACT,UAAU;AAAA,EACV,UAAU,CAAC;AAAA,EAEX,MAAM,UAAU,QAAQ,SAAsD;AAC5E,UAAM,KAAM,QAAQ,GAAW,KAAK;AACpC,UAAM,eAAgB,QAAgB;AACtC,QAAI,CAAC,aAAc,QAAO,EAAE,GAAG,QAAQ,eAAe,CAAC,EAAE;AAEzD,UAAM,WAAoC,MAAM,GAAG,KAAK,uBAAuB;AAAA,MAC7E,oBAAoB;AAAA,MACpB,kBAAkB,OAAO;AAAA,MACzB,gBAAgB,QAAQ;AAAA,MACxB,WAAW;AAAA,IACb,CAAC;AAED,QAAI,SAAS,WAAW,EAAG,QAAO,EAAE,GAAG,QAAQ,eAAe,CAAC,EAAE;AAEjE,WAAO;AAAA,MACL,GAAG;AAAA,MACH,eAAe,qBAAqB,QAAQ;AAAA,IAC9C;AAAA,EACF;AAAA,EAEA,MAAM,WAAW,SAAS,SAAsD;AAC9E,UAAM,KAAM,QAAQ,GAAW,KAAK;AACpC,UAAM,eAAgB,QAAgB;AACtC,QAAI,CAAC,gBAAgB,QAAQ,WAAW,EAAG,QAAO,QAAQ,IAAI,CAAC,OAAO,EAAE,GAAG,GAAG,eAAe,CAAC,EAAE,EAAE;AAElG,UAAM,YAAY,QAAQ,IAAI,CAAC,MAAM,EAAE,EAAE;AACzC,UAAM,cAAuC,MAAM,GAAG,KAAK,uBAAuB;AAAA,MAChF,oBAAoB;AAAA,MACpB,kBAAkB,EAAE,KAAK,UAAU;AAAA,MACnC,gBAAgB,QAAQ;AAAA,MACxB,WAAW;AAAA,IACb,CAAC;AAED,QAAI,YAAY,WAAW,EAAG,QAAO,QAAQ,IAAI,CAAC,OAAO,EAAE,GAAG,GAAG,eAAe,CAAC,EAAE,EAAE;AAErF,UAAM,mBAAmB,oBAAI,IAAqC;AAClE,eAAW,WAAW,aAAa;AACjC,YAAM,OAAO,iBAAiB,IAAI,QAAQ,gBAAgB,KAAK,CAAC;AAChE,WAAK,KAAK,OAAO;AACjB,uBAAiB,IAAI,QAAQ,kBAAkB,IAAI;AAAA,IACrD;AAEA,WAAO,QAAQ,IAAI,CAAC,WAAW;AAC7B,YAAM,WAAW,iBAAiB,IAAI,OAAO,EAAE;AAC/C,UAAI,CAAC,YAAY,SAAS,WAAW,EAAG,QAAO,EAAE,GAAG,QAAQ,eAAe,CAAC,EAAE;AAE9E,aAAO;AAAA,QACL,GAAG;AAAA,QACH,eAAe,qBAAqB,QAAQ;AAAA,MAC9C;AAAA,IACF,CAAC;AAAA,EACH;AACF;AAEO,MAAM,YAAgC,CAAC,yBAAyB;",
6
+ "names": []
7
+ }
@@ -0,0 +1,63 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
+ var __decorateClass = (decorators, target, key, kind) => {
4
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
5
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
6
+ if (decorator = decorators[i])
7
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
8
+ if (kind && result) __defProp(target, key, result);
9
+ return result;
10
+ };
11
+ import { Entity, PrimaryKey, Property, Index } from "@mikro-orm/core";
12
+ let SyncExternalIdMapping = class {
13
+ constructor() {
14
+ this.syncStatus = "not_synced";
15
+ this.createdAt = /* @__PURE__ */ new Date();
16
+ this.updatedAt = /* @__PURE__ */ new Date();
17
+ }
18
+ };
19
+ __decorateClass([
20
+ PrimaryKey({ type: "uuid", defaultRaw: "gen_random_uuid()" })
21
+ ], SyncExternalIdMapping.prototype, "id", 2);
22
+ __decorateClass([
23
+ Property({ name: "integration_id", type: "text" })
24
+ ], SyncExternalIdMapping.prototype, "integrationId", 2);
25
+ __decorateClass([
26
+ Property({ name: "internal_entity_type", type: "text" })
27
+ ], SyncExternalIdMapping.prototype, "internalEntityType", 2);
28
+ __decorateClass([
29
+ Property({ name: "internal_entity_id", type: "uuid" })
30
+ ], SyncExternalIdMapping.prototype, "internalEntityId", 2);
31
+ __decorateClass([
32
+ Property({ name: "external_id", type: "text" })
33
+ ], SyncExternalIdMapping.prototype, "externalId", 2);
34
+ __decorateClass([
35
+ Property({ name: "sync_status", type: "text", default: "not_synced" })
36
+ ], SyncExternalIdMapping.prototype, "syncStatus", 2);
37
+ __decorateClass([
38
+ Property({ name: "last_synced_at", type: Date, nullable: true })
39
+ ], SyncExternalIdMapping.prototype, "lastSyncedAt", 2);
40
+ __decorateClass([
41
+ Property({ name: "organization_id", type: "uuid" })
42
+ ], SyncExternalIdMapping.prototype, "organizationId", 2);
43
+ __decorateClass([
44
+ Property({ name: "tenant_id", type: "uuid" })
45
+ ], SyncExternalIdMapping.prototype, "tenantId", 2);
46
+ __decorateClass([
47
+ Property({ name: "created_at", type: Date, onCreate: () => /* @__PURE__ */ new Date() })
48
+ ], SyncExternalIdMapping.prototype, "createdAt", 2);
49
+ __decorateClass([
50
+ Property({ name: "updated_at", type: Date, onUpdate: () => /* @__PURE__ */ new Date() })
51
+ ], SyncExternalIdMapping.prototype, "updatedAt", 2);
52
+ __decorateClass([
53
+ Property({ name: "deleted_at", type: Date, nullable: true })
54
+ ], SyncExternalIdMapping.prototype, "deletedAt", 2);
55
+ SyncExternalIdMapping = __decorateClass([
56
+ Entity({ tableName: "sync_external_id_mappings" }),
57
+ Index({ properties: ["internalEntityType", "internalEntityId", "organizationId"] }),
58
+ Index({ properties: ["integrationId", "externalId", "organizationId"] })
59
+ ], SyncExternalIdMapping);
60
+ export {
61
+ SyncExternalIdMapping
62
+ };
63
+ //# sourceMappingURL=entities.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/integrations/data/entities.ts"],
4
+ "sourcesContent": ["import { Entity, PrimaryKey, Property, Index } from '@mikro-orm/core'\n\n/**\n * Stores mappings between internal entity IDs and external system IDs.\n * Used by integration modules to track synced records across platforms.\n */\n@Entity({ tableName: 'sync_external_id_mappings' })\n@Index({ properties: ['internalEntityType', 'internalEntityId', 'organizationId'] })\n@Index({ properties: ['integrationId', 'externalId', 'organizationId'] })\nexport class SyncExternalIdMapping {\n @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })\n id!: string\n\n @Property({ name: 'integration_id', type: 'text' })\n integrationId!: string\n\n @Property({ name: 'internal_entity_type', type: 'text' })\n internalEntityType!: string\n\n @Property({ name: 'internal_entity_id', type: 'uuid' })\n internalEntityId!: string\n\n @Property({ name: 'external_id', type: 'text' })\n externalId!: string\n\n @Property({ name: 'sync_status', type: 'text', default: 'not_synced' })\n syncStatus: 'synced' | 'pending' | 'error' | 'not_synced' = 'not_synced'\n\n @Property({ name: 'last_synced_at', type: Date, nullable: true })\n lastSyncedAt?: Date | null\n\n @Property({ name: 'organization_id', type: 'uuid' })\n organizationId!: string\n\n @Property({ name: 'tenant_id', type: 'uuid' })\n tenantId!: string\n\n @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })\n createdAt: Date = new Date()\n\n @Property({ name: 'updated_at', type: Date, onUpdate: () => new Date() })\n updatedAt: Date = new Date()\n\n @Property({ name: 'deleted_at', type: Date, nullable: true })\n deletedAt?: Date | null\n}\n"],
5
+ "mappings": ";;;;;;;;;;AAAA,SAAS,QAAQ,YAAY,UAAU,aAAa;AAS7C,IAAM,wBAAN,MAA4B;AAAA,EAA5B;AAiBL,sBAA4D;AAY5D,qBAAkB,oBAAI,KAAK;AAG3B,qBAAkB,oBAAI,KAAK;AAAA;AAI7B;AAlCE;AAAA,EADC,WAAW,EAAE,MAAM,QAAQ,YAAY,oBAAoB,CAAC;AAAA,GADlD,sBAEX;AAGA;AAAA,EADC,SAAS,EAAE,MAAM,kBAAkB,MAAM,OAAO,CAAC;AAAA,GAJvC,sBAKX;AAGA;AAAA,EADC,SAAS,EAAE,MAAM,wBAAwB,MAAM,OAAO,CAAC;AAAA,GAP7C,sBAQX;AAGA;AAAA,EADC,SAAS,EAAE,MAAM,sBAAsB,MAAM,OAAO,CAAC;AAAA,GAV3C,sBAWX;AAGA;AAAA,EADC,SAAS,EAAE,MAAM,eAAe,MAAM,OAAO,CAAC;AAAA,GAbpC,sBAcX;AAGA;AAAA,EADC,SAAS,EAAE,MAAM,eAAe,MAAM,QAAQ,SAAS,aAAa,CAAC;AAAA,GAhB3D,sBAiBX;AAGA;AAAA,EADC,SAAS,EAAE,MAAM,kBAAkB,MAAM,MAAM,UAAU,KAAK,CAAC;AAAA,GAnBrD,sBAoBX;AAGA;AAAA,EADC,SAAS,EAAE,MAAM,mBAAmB,MAAM,OAAO,CAAC;AAAA,GAtBxC,sBAuBX;AAGA;AAAA,EADC,SAAS,EAAE,MAAM,aAAa,MAAM,OAAO,CAAC;AAAA,GAzBlC,sBA0BX;AAGA;AAAA,EADC,SAAS,EAAE,MAAM,cAAc,MAAM,MAAM,UAAU,MAAM,oBAAI,KAAK,EAAE,CAAC;AAAA,GA5B7D,sBA6BX;AAGA;AAAA,EADC,SAAS,EAAE,MAAM,cAAc,MAAM,MAAM,UAAU,MAAM,oBAAI,KAAK,EAAE,CAAC;AAAA,GA/B7D,sBAgCX;AAGA;AAAA,EADC,SAAS,EAAE,MAAM,cAAc,MAAM,MAAM,UAAU,KAAK,CAAC;AAAA,GAlCjD,sBAmCX;AAnCW,wBAAN;AAAA,EAHN,OAAO,EAAE,WAAW,4BAA4B,CAAC;AAAA,EACjD,MAAM,EAAE,YAAY,CAAC,sBAAsB,oBAAoB,gBAAgB,EAAE,CAAC;AAAA,EAClF,MAAM,EAAE,YAAY,CAAC,iBAAiB,cAAc,gBAAgB,EAAE,CAAC;AAAA,GAC3D;",
6
+ "names": []
7
+ }
@@ -0,0 +1,9 @@
1
+ const metadata = {
2
+ id: "integrations",
3
+ title: "Integrations",
4
+ description: "Core integration framework \u2014 external ID mapping, status badges, and integration registry."
5
+ };
6
+ export {
7
+ metadata
8
+ };
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/modules/integrations/index.ts"],
4
+ "sourcesContent": ["export const metadata = {\n id: 'integrations',\n title: 'Integrations',\n description: 'Core integration framework \u2014 external ID mapping, status badges, and integration registry.',\n}\n"],
5
+ "mappings": "AAAO,MAAM,WAAW;AAAA,EACtB,IAAI;AAAA,EACJ,OAAO;AAAA,EACP,aAAa;AACf;",
6
+ "names": []
7
+ }
@@ -0,0 +1,13 @@
1
+ const setup = {
2
+ defaultRoleFeatures: {
3
+ superadmin: ["integrations.view", "integrations.manage"],
4
+ admin: ["integrations.view", "integrations.manage"],
5
+ employee: ["integrations.view"]
6
+ }
7
+ };
8
+ var setup_default = setup;
9
+ export {
10
+ setup_default as default,
11
+ setup
12
+ };
13
+ //# sourceMappingURL=setup.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/modules/integrations/setup.ts"],
4
+ "sourcesContent": ["import type { ModuleSetupConfig } from '@open-mercato/shared/modules/setup'\n\nexport const setup: ModuleSetupConfig = {\n defaultRoleFeatures: {\n superadmin: ['integrations.view', 'integrations.manage'],\n admin: ['integrations.view', 'integrations.manage'],\n employee: ['integrations.view'],\n },\n}\n\nexport default setup\n"],
5
+ "mappings": "AAEO,MAAM,QAA2B;AAAA,EACtC,qBAAqB;AAAA,IACnB,YAAY,CAAC,qBAAqB,qBAAqB;AAAA,IACvD,OAAO,CAAC,qBAAqB,qBAAqB;AAAA,IAClD,UAAU,CAAC,mBAAmB;AAAA,EAChC;AACF;AAEA,IAAO,gBAAQ;",
6
+ "names": []
7
+ }
@@ -0,0 +1,69 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { getIntegrationTitle } from "@open-mercato/shared/modules/integrations/types";
4
+ import { useT } from "@open-mercato/shared/lib/i18n/context";
5
+ import { cn } from "@open-mercato/shared/lib/utils";
6
+ const SYNC_STATUS_STYLES = {
7
+ synced: { dot: "bg-green-500", label: "integrations.syncStatus.synced" },
8
+ pending: { dot: "bg-yellow-500", label: "integrations.syncStatus.pending" },
9
+ error: { dot: "bg-red-500", label: "integrations.syncStatus.error" },
10
+ not_synced: { dot: "bg-gray-400", label: "integrations.syncStatus.notSynced" }
11
+ };
12
+ function SyncStatusBadge({ status, lastSynced }) {
13
+ const t = useT();
14
+ const config = SYNC_STATUS_STYLES[status];
15
+ const label = t(config.label, status);
16
+ return /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1 text-xs text-muted-foreground", children: [
17
+ /* @__PURE__ */ jsx("span", { className: cn("inline-block size-1.5 rounded-full", config.dot), "aria-hidden": "true" }),
18
+ /* @__PURE__ */ jsx("span", { children: label }),
19
+ lastSynced && /* @__PURE__ */ jsx("span", { title: lastSynced, children: new Date(lastSynced).toLocaleDateString() })
20
+ ] });
21
+ }
22
+ function ExternalLinkIcon() {
23
+ return /* @__PURE__ */ jsxs("svg", { className: "size-3.5", viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: "1.5", children: [
24
+ /* @__PURE__ */ jsx("path", { d: "M6.5 3.5H3a1 1 0 0 0-1 1V13a1 1 0 0 0 1 1h8.5a1 1 0 0 0 1-1V9.5" }),
25
+ /* @__PURE__ */ jsx("path", { d: "M9.5 2h4.5v4.5M14 2 7.5 8.5" })
26
+ ] });
27
+ }
28
+ function ExternalIdsWidget({ data }) {
29
+ const t = useT();
30
+ const integrations = data?._integrations;
31
+ if (!integrations || Object.keys(integrations).length === 0) return null;
32
+ return /* @__PURE__ */ jsxs("div", { className: "rounded-lg border bg-card p-4", children: [
33
+ /* @__PURE__ */ jsx("h3", { className: "mb-3 text-sm font-medium", children: t("integrations.externalIds.title", "External IDs") }),
34
+ /* @__PURE__ */ jsx("div", { className: "space-y-2", children: Object.entries(integrations).map(([integrationId, mapping]) => /* @__PURE__ */ jsxs(
35
+ "div",
36
+ {
37
+ className: "flex items-center justify-between gap-2 rounded-md bg-muted/50 px-3 py-2",
38
+ children: [
39
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 min-w-0", children: [
40
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-medium truncate", children: getIntegrationTitle(integrationId) }),
41
+ /* @__PURE__ */ jsx("code", { className: "rounded bg-muted px-1.5 py-0.5 text-xs font-mono text-muted-foreground", children: mapping.externalId }),
42
+ mapping.externalUrl && /* @__PURE__ */ jsx(
43
+ "a",
44
+ {
45
+ href: mapping.externalUrl,
46
+ target: "_blank",
47
+ rel: "noopener noreferrer",
48
+ className: "text-muted-foreground hover:text-foreground transition-colors",
49
+ "aria-label": t("integrations.externalIds.openExternal", "Open in external system"),
50
+ children: /* @__PURE__ */ jsx(ExternalLinkIcon, {})
51
+ }
52
+ )
53
+ ] }),
54
+ /* @__PURE__ */ jsx(SyncStatusBadge, { status: mapping.syncStatus, lastSynced: mapping.lastSyncedAt })
55
+ ]
56
+ },
57
+ integrationId
58
+ )) })
59
+ ] });
60
+ }
61
+ ExternalIdsWidget.metadata = {
62
+ id: "integrations.injection.external-ids",
63
+ title: "External IDs",
64
+ features: ["integrations.view"]
65
+ };
66
+ export {
67
+ ExternalIdsWidget as default
68
+ };
69
+ //# sourceMappingURL=widget.client.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../../src/modules/integrations/widgets/injection/external-ids/widget.client.tsx"],
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport type { InjectionWidgetComponentProps } from '@open-mercato/shared/modules/widgets/injection'\nimport type { ExternalIdMapping } from '@open-mercato/shared/modules/integrations/types'\nimport { getIntegrationTitle } from '@open-mercato/shared/modules/integrations/types'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { cn } from '@open-mercato/shared/lib/utils'\n\ntype IntegrationsData = Record<string, ExternalIdMapping>\n\nconst SYNC_STATUS_STYLES: Record<ExternalIdMapping['syncStatus'], { dot: string; label: string }> = {\n synced: { dot: 'bg-green-500', label: 'integrations.syncStatus.synced' },\n pending: { dot: 'bg-yellow-500', label: 'integrations.syncStatus.pending' },\n error: { dot: 'bg-red-500', label: 'integrations.syncStatus.error' },\n not_synced: { dot: 'bg-gray-400', label: 'integrations.syncStatus.notSynced' },\n}\n\nfunction SyncStatusBadge({ status, lastSynced }: { status: ExternalIdMapping['syncStatus']; lastSynced?: string }) {\n const t = useT()\n const config = SYNC_STATUS_STYLES[status]\n const label = t(config.label, status)\n\n return (\n <span className=\"inline-flex items-center gap-1 text-xs text-muted-foreground\">\n <span className={cn('inline-block size-1.5 rounded-full', config.dot)} aria-hidden=\"true\" />\n <span>{label}</span>\n {lastSynced && (\n <span title={lastSynced}>\n {new Date(lastSynced).toLocaleDateString()}\n </span>\n )}\n </span>\n )\n}\n\nfunction ExternalLinkIcon() {\n return (\n <svg className=\"size-3.5\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.5\">\n <path d=\"M6.5 3.5H3a1 1 0 0 0-1 1V13a1 1 0 0 0 1 1h8.5a1 1 0 0 0 1-1V9.5\" />\n <path d=\"M9.5 2h4.5v4.5M14 2 7.5 8.5\" />\n </svg>\n )\n}\n\nexport default function ExternalIdsWidget({ data }: InjectionWidgetComponentProps) {\n const t = useT()\n const integrations = (data as Record<string, unknown>)?._integrations as IntegrationsData | undefined\n\n if (!integrations || Object.keys(integrations).length === 0) return null\n\n return (\n <div className=\"rounded-lg border bg-card p-4\">\n <h3 className=\"mb-3 text-sm font-medium\">\n {t('integrations.externalIds.title', 'External IDs')}\n </h3>\n <div className=\"space-y-2\">\n {Object.entries(integrations).map(([integrationId, mapping]) => (\n <div\n key={integrationId}\n className=\"flex items-center justify-between gap-2 rounded-md bg-muted/50 px-3 py-2\"\n >\n <div className=\"flex items-center gap-2 min-w-0\">\n <span className=\"text-sm font-medium truncate\">\n {getIntegrationTitle(integrationId)}\n </span>\n <code className=\"rounded bg-muted px-1.5 py-0.5 text-xs font-mono text-muted-foreground\">\n {mapping.externalId}\n </code>\n {mapping.externalUrl && (\n <a\n href={mapping.externalUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-muted-foreground hover:text-foreground transition-colors\"\n aria-label={t('integrations.externalIds.openExternal', 'Open in external system')}\n >\n <ExternalLinkIcon />\n </a>\n )}\n </div>\n <SyncStatusBadge status={mapping.syncStatus} lastSynced={mapping.lastSyncedAt} />\n </div>\n ))}\n </div>\n </div>\n )\n}\n\nExternalIdsWidget.metadata = {\n id: 'integrations.injection.external-ids',\n title: 'External IDs',\n features: ['integrations.view'],\n}\n"],
5
+ "mappings": ";AAwBI,SACE,KADF;AAnBJ,SAAS,2BAA2B;AACpC,SAAS,YAAY;AACrB,SAAS,UAAU;AAInB,MAAM,qBAA8F;AAAA,EAClG,QAAQ,EAAE,KAAK,gBAAgB,OAAO,iCAAiC;AAAA,EACvE,SAAS,EAAE,KAAK,iBAAiB,OAAO,kCAAkC;AAAA,EAC1E,OAAO,EAAE,KAAK,cAAc,OAAO,gCAAgC;AAAA,EACnE,YAAY,EAAE,KAAK,eAAe,OAAO,oCAAoC;AAC/E;AAEA,SAAS,gBAAgB,EAAE,QAAQ,WAAW,GAAqE;AACjH,QAAM,IAAI,KAAK;AACf,QAAM,SAAS,mBAAmB,MAAM;AACxC,QAAM,QAAQ,EAAE,OAAO,OAAO,MAAM;AAEpC,SACE,qBAAC,UAAK,WAAU,gEACd;AAAA,wBAAC,UAAK,WAAW,GAAG,sCAAsC,OAAO,GAAG,GAAG,eAAY,QAAO;AAAA,IAC1F,oBAAC,UAAM,iBAAM;AAAA,IACZ,cACC,oBAAC,UAAK,OAAO,YACV,cAAI,KAAK,UAAU,EAAE,mBAAmB,GAC3C;AAAA,KAEJ;AAEJ;AAEA,SAAS,mBAAmB;AAC1B,SACE,qBAAC,SAAI,WAAU,YAAW,SAAQ,aAAY,MAAK,QAAO,QAAO,gBAAe,aAAY,OAC1F;AAAA,wBAAC,UAAK,GAAE,mEAAkE;AAAA,IAC1E,oBAAC,UAAK,GAAE,+BAA8B;AAAA,KACxC;AAEJ;AAEe,SAAR,kBAAmC,EAAE,KAAK,GAAkC;AACjF,QAAM,IAAI,KAAK;AACf,QAAM,eAAgB,MAAkC;AAExD,MAAI,CAAC,gBAAgB,OAAO,KAAK,YAAY,EAAE,WAAW,EAAG,QAAO;AAEpE,SACE,qBAAC,SAAI,WAAU,iCACb;AAAA,wBAAC,QAAG,WAAU,4BACX,YAAE,kCAAkC,cAAc,GACrD;AAAA,IACA,oBAAC,SAAI,WAAU,aACZ,iBAAO,QAAQ,YAAY,EAAE,IAAI,CAAC,CAAC,eAAe,OAAO,MACxD;AAAA,MAAC;AAAA;AAAA,QAEC,WAAU;AAAA,QAEV;AAAA,+BAAC,SAAI,WAAU,mCACb;AAAA,gCAAC,UAAK,WAAU,gCACb,8BAAoB,aAAa,GACpC;AAAA,YACA,oBAAC,UAAK,WAAU,0EACb,kBAAQ,YACX;AAAA,YACC,QAAQ,eACP;AAAA,cAAC;AAAA;AAAA,gBACC,MAAM,QAAQ;AAAA,gBACd,QAAO;AAAA,gBACP,KAAI;AAAA,gBACJ,WAAU;AAAA,gBACV,cAAY,EAAE,yCAAyC,yBAAyB;AAAA,gBAEhF,8BAAC,oBAAiB;AAAA;AAAA,YACpB;AAAA,aAEJ;AAAA,UACA,oBAAC,mBAAgB,QAAQ,QAAQ,YAAY,YAAY,QAAQ,cAAc;AAAA;AAAA;AAAA,MAtB1E;AAAA,IAuBP,CACD,GACH;AAAA,KACF;AAEJ;AAEA,kBAAkB,WAAW;AAAA,EAC3B,IAAI;AAAA,EACJ,OAAO;AAAA,EACP,UAAU,CAAC,mBAAmB;AAChC;",
6
+ "names": []
7
+ }
@@ -0,0 +1,13 @@
1
+ const injectionTable = {
2
+ // External IDs widget appears in detail page sidebars for all entities
3
+ "detail:*:sidebar": {
4
+ widgetId: "integrations.injection.external-ids",
5
+ priority: -10
6
+ }
7
+ };
8
+ var injection_table_default = injectionTable;
9
+ export {
10
+ injection_table_default as default,
11
+ injectionTable
12
+ };
13
+ //# sourceMappingURL=injection-table.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/integrations/widgets/injection-table.ts"],
4
+ "sourcesContent": ["import type { ModuleInjectionTable } from '@open-mercato/shared/modules/widgets/injection'\n\n/**\n * Integrations module injection table.\n *\n * The external-ids widget auto-appears on entity detail pages via wildcard spot matching.\n * Status badge widgets are registered by individual integration modules (e.g. sync_medusa).\n */\nexport const injectionTable: ModuleInjectionTable = {\n // External IDs widget appears in detail page sidebars for all entities\n 'detail:*:sidebar': {\n widgetId: 'integrations.injection.external-ids',\n priority: -10,\n },\n}\n\nexport default injectionTable\n"],
5
+ "mappings": "AAQO,MAAM,iBAAuC;AAAA;AAAA,EAElD,oBAAoB;AAAA,IAClB,UAAU;AAAA,IACV,UAAU;AAAA,EACZ;AACF;AAEA,IAAO,0BAAQ;",
6
+ "names": []
7
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/core",
3
- "version": "0.4.6-develop-be2da141e3",
3
+ "version": "0.4.6-develop-1f4478d60c",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {
@@ -207,7 +207,7 @@
207
207
  }
208
208
  },
209
209
  "dependencies": {
210
- "@open-mercato/shared": "0.4.6-develop-be2da141e3",
210
+ "@open-mercato/shared": "0.4.6-develop-1f4478d60c",
211
211
  "@types/html-to-text": "^9.0.4",
212
212
  "@types/semver": "^7.5.8",
213
213
  "@xyflow/react": "^12.6.0",
@@ -0,0 +1,4 @@
1
+ export const features = [
2
+ { id: 'integrations.view', title: 'View integrations and external ID mappings', module: 'integrations' },
3
+ { id: 'integrations.manage', title: 'Manage integration configurations', module: 'integrations' },
4
+ ]
@@ -0,0 +1,98 @@
1
+ /**
2
+ * External ID Mapping Enricher
3
+ *
4
+ * Enriches any entity's API response with external integration ID mappings.
5
+ * Adds `_integrations` namespace containing all external IDs for the record.
6
+ *
7
+ * Uses batch queries via `enrichMany` to prevent N+1.
8
+ */
9
+
10
+ import type { ResponseEnricher, EnricherContext } from '@open-mercato/shared/lib/crud/response-enricher'
11
+ import type { ExternalIdEnrichment } from '@open-mercato/shared/modules/integrations/types'
12
+ import { getIntegration } from '@open-mercato/shared/modules/integrations/types'
13
+ import { SyncExternalIdMapping } from './entities'
14
+
15
+ type EntityRecord = Record<string, unknown> & { id: string }
16
+
17
+ function buildIntegrationData(
18
+ mappings: SyncExternalIdMapping[],
19
+ ): ExternalIdEnrichment['_integrations'] {
20
+ const data: ExternalIdEnrichment['_integrations'] = {}
21
+
22
+ for (const mapping of mappings) {
23
+ const definition = getIntegration(mapping.integrationId)
24
+ data[mapping.integrationId] = {
25
+ externalId: mapping.externalId,
26
+ externalUrl: definition?.buildExternalUrl?.(mapping.externalId),
27
+ lastSyncedAt: mapping.lastSyncedAt?.toISOString(),
28
+ syncStatus: mapping.syncStatus,
29
+ }
30
+ }
31
+
32
+ return data
33
+ }
34
+
35
+ const externalIdMappingEnricher: ResponseEnricher<EntityRecord, ExternalIdEnrichment> = {
36
+ id: 'integrations.external-id-mapping',
37
+ targetEntity: '*',
38
+ features: ['integrations.view'],
39
+ priority: 10,
40
+ timeout: 500,
41
+ critical: false,
42
+ fallback: {},
43
+
44
+ async enrichOne(record, context: EnricherContext & { targetEntity?: string }) {
45
+ const em = (context.em as any).fork()
46
+ const targetEntity = (context as any).targetEntity as string | undefined
47
+ if (!targetEntity) return { ...record, _integrations: {} }
48
+
49
+ const mappings: SyncExternalIdMapping[] = await em.find(SyncExternalIdMapping, {
50
+ internalEntityType: targetEntity,
51
+ internalEntityId: record.id,
52
+ organizationId: context.organizationId,
53
+ deletedAt: null,
54
+ })
55
+
56
+ if (mappings.length === 0) return { ...record, _integrations: {} }
57
+
58
+ return {
59
+ ...record,
60
+ _integrations: buildIntegrationData(mappings),
61
+ }
62
+ },
63
+
64
+ async enrichMany(records, context: EnricherContext & { targetEntity?: string }) {
65
+ const em = (context.em as any).fork()
66
+ const targetEntity = (context as any).targetEntity as string | undefined
67
+ if (!targetEntity || records.length === 0) return records.map((r) => ({ ...r, _integrations: {} }))
68
+
69
+ const recordIds = records.map((r) => r.id)
70
+ const allMappings: SyncExternalIdMapping[] = await em.find(SyncExternalIdMapping, {
71
+ internalEntityType: targetEntity,
72
+ internalEntityId: { $in: recordIds },
73
+ organizationId: context.organizationId,
74
+ deletedAt: null,
75
+ })
76
+
77
+ if (allMappings.length === 0) return records.map((r) => ({ ...r, _integrations: {} }))
78
+
79
+ const mappingsByRecord = new Map<string, SyncExternalIdMapping[]>()
80
+ for (const mapping of allMappings) {
81
+ const list = mappingsByRecord.get(mapping.internalEntityId) ?? []
82
+ list.push(mapping)
83
+ mappingsByRecord.set(mapping.internalEntityId, list)
84
+ }
85
+
86
+ return records.map((record) => {
87
+ const mappings = mappingsByRecord.get(record.id)
88
+ if (!mappings || mappings.length === 0) return { ...record, _integrations: {} }
89
+
90
+ return {
91
+ ...record,
92
+ _integrations: buildIntegrationData(mappings),
93
+ }
94
+ })
95
+ },
96
+ }
97
+
98
+ export const enrichers: ResponseEnricher[] = [externalIdMappingEnricher]
@@ -0,0 +1,46 @@
1
+ import { Entity, PrimaryKey, Property, Index } from '@mikro-orm/core'
2
+
3
+ /**
4
+ * Stores mappings between internal entity IDs and external system IDs.
5
+ * Used by integration modules to track synced records across platforms.
6
+ */
7
+ @Entity({ tableName: 'sync_external_id_mappings' })
8
+ @Index({ properties: ['internalEntityType', 'internalEntityId', 'organizationId'] })
9
+ @Index({ properties: ['integrationId', 'externalId', 'organizationId'] })
10
+ export class SyncExternalIdMapping {
11
+ @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })
12
+ id!: string
13
+
14
+ @Property({ name: 'integration_id', type: 'text' })
15
+ integrationId!: string
16
+
17
+ @Property({ name: 'internal_entity_type', type: 'text' })
18
+ internalEntityType!: string
19
+
20
+ @Property({ name: 'internal_entity_id', type: 'uuid' })
21
+ internalEntityId!: string
22
+
23
+ @Property({ name: 'external_id', type: 'text' })
24
+ externalId!: string
25
+
26
+ @Property({ name: 'sync_status', type: 'text', default: 'not_synced' })
27
+ syncStatus: 'synced' | 'pending' | 'error' | 'not_synced' = 'not_synced'
28
+
29
+ @Property({ name: 'last_synced_at', type: Date, nullable: true })
30
+ lastSyncedAt?: Date | null
31
+
32
+ @Property({ name: 'organization_id', type: 'uuid' })
33
+ organizationId!: string
34
+
35
+ @Property({ name: 'tenant_id', type: 'uuid' })
36
+ tenantId!: string
37
+
38
+ @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })
39
+ createdAt: Date = new Date()
40
+
41
+ @Property({ name: 'updated_at', type: Date, onUpdate: () => new Date() })
42
+ updatedAt: Date = new Date()
43
+
44
+ @Property({ name: 'deleted_at', type: Date, nullable: true })
45
+ deletedAt?: Date | null
46
+ }
@@ -0,0 +1,5 @@
1
+ export const metadata = {
2
+ id: 'integrations',
3
+ title: 'Integrations',
4
+ description: 'Core integration framework — external ID mapping, status badges, and integration registry.',
5
+ }
@@ -0,0 +1,11 @@
1
+ import type { ModuleSetupConfig } from '@open-mercato/shared/modules/setup'
2
+
3
+ export const setup: ModuleSetupConfig = {
4
+ defaultRoleFeatures: {
5
+ superadmin: ['integrations.view', 'integrations.manage'],
6
+ admin: ['integrations.view', 'integrations.manage'],
7
+ employee: ['integrations.view'],
8
+ },
9
+ }
10
+
11
+ export default setup
@@ -0,0 +1,94 @@
1
+ "use client"
2
+
3
+ import * as React from 'react'
4
+ import type { InjectionWidgetComponentProps } from '@open-mercato/shared/modules/widgets/injection'
5
+ import type { ExternalIdMapping } from '@open-mercato/shared/modules/integrations/types'
6
+ import { getIntegrationTitle } from '@open-mercato/shared/modules/integrations/types'
7
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
8
+ import { cn } from '@open-mercato/shared/lib/utils'
9
+
10
+ type IntegrationsData = Record<string, ExternalIdMapping>
11
+
12
+ const SYNC_STATUS_STYLES: Record<ExternalIdMapping['syncStatus'], { dot: string; label: string }> = {
13
+ synced: { dot: 'bg-green-500', label: 'integrations.syncStatus.synced' },
14
+ pending: { dot: 'bg-yellow-500', label: 'integrations.syncStatus.pending' },
15
+ error: { dot: 'bg-red-500', label: 'integrations.syncStatus.error' },
16
+ not_synced: { dot: 'bg-gray-400', label: 'integrations.syncStatus.notSynced' },
17
+ }
18
+
19
+ function SyncStatusBadge({ status, lastSynced }: { status: ExternalIdMapping['syncStatus']; lastSynced?: string }) {
20
+ const t = useT()
21
+ const config = SYNC_STATUS_STYLES[status]
22
+ const label = t(config.label, status)
23
+
24
+ return (
25
+ <span className="inline-flex items-center gap-1 text-xs text-muted-foreground">
26
+ <span className={cn('inline-block size-1.5 rounded-full', config.dot)} aria-hidden="true" />
27
+ <span>{label}</span>
28
+ {lastSynced && (
29
+ <span title={lastSynced}>
30
+ {new Date(lastSynced).toLocaleDateString()}
31
+ </span>
32
+ )}
33
+ </span>
34
+ )
35
+ }
36
+
37
+ function ExternalLinkIcon() {
38
+ return (
39
+ <svg className="size-3.5" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5">
40
+ <path d="M6.5 3.5H3a1 1 0 0 0-1 1V13a1 1 0 0 0 1 1h8.5a1 1 0 0 0 1-1V9.5" />
41
+ <path d="M9.5 2h4.5v4.5M14 2 7.5 8.5" />
42
+ </svg>
43
+ )
44
+ }
45
+
46
+ export default function ExternalIdsWidget({ data }: InjectionWidgetComponentProps) {
47
+ const t = useT()
48
+ const integrations = (data as Record<string, unknown>)?._integrations as IntegrationsData | undefined
49
+
50
+ if (!integrations || Object.keys(integrations).length === 0) return null
51
+
52
+ return (
53
+ <div className="rounded-lg border bg-card p-4">
54
+ <h3 className="mb-3 text-sm font-medium">
55
+ {t('integrations.externalIds.title', 'External IDs')}
56
+ </h3>
57
+ <div className="space-y-2">
58
+ {Object.entries(integrations).map(([integrationId, mapping]) => (
59
+ <div
60
+ key={integrationId}
61
+ className="flex items-center justify-between gap-2 rounded-md bg-muted/50 px-3 py-2"
62
+ >
63
+ <div className="flex items-center gap-2 min-w-0">
64
+ <span className="text-sm font-medium truncate">
65
+ {getIntegrationTitle(integrationId)}
66
+ </span>
67
+ <code className="rounded bg-muted px-1.5 py-0.5 text-xs font-mono text-muted-foreground">
68
+ {mapping.externalId}
69
+ </code>
70
+ {mapping.externalUrl && (
71
+ <a
72
+ href={mapping.externalUrl}
73
+ target="_blank"
74
+ rel="noopener noreferrer"
75
+ className="text-muted-foreground hover:text-foreground transition-colors"
76
+ aria-label={t('integrations.externalIds.openExternal', 'Open in external system')}
77
+ >
78
+ <ExternalLinkIcon />
79
+ </a>
80
+ )}
81
+ </div>
82
+ <SyncStatusBadge status={mapping.syncStatus} lastSynced={mapping.lastSyncedAt} />
83
+ </div>
84
+ ))}
85
+ </div>
86
+ </div>
87
+ )
88
+ }
89
+
90
+ ExternalIdsWidget.metadata = {
91
+ id: 'integrations.injection.external-ids',
92
+ title: 'External IDs',
93
+ features: ['integrations.view'],
94
+ }
@@ -0,0 +1,17 @@
1
+ import type { ModuleInjectionTable } from '@open-mercato/shared/modules/widgets/injection'
2
+
3
+ /**
4
+ * Integrations module injection table.
5
+ *
6
+ * The external-ids widget auto-appears on entity detail pages via wildcard spot matching.
7
+ * Status badge widgets are registered by individual integration modules (e.g. sync_medusa).
8
+ */
9
+ export const injectionTable: ModuleInjectionTable = {
10
+ // External IDs widget appears in detail page sidebars for all entities
11
+ 'detail:*:sidebar': {
12
+ widgetId: 'integrations.injection.external-ids',
13
+ priority: -10,
14
+ },
15
+ }
16
+
17
+ export default injectionTable