@open-mercato/core 0.6.6-develop.6286.1.d985c5c374 → 0.6.6-develop.6290.1.4bb5a8ba3f

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.
@@ -1,4 +1,4 @@
1
- [build:core] found 3423 entry points
1
+ [build:core] found 3424 entry points
2
2
  [build:core] built successfully
3
3
  [build:core:generated] found 186 entry points
4
4
  [build:core:generated] built successfully
@@ -1,6 +1,6 @@
1
1
  import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
2
2
  import { SyncSchedule } from "../data/entities.js";
3
- import { getSyncQueue } from "../lib/queue.js";
3
+ import { startDataSyncRun } from "../lib/start-run.js";
4
4
  const metadata = {
5
5
  queue: "data-sync-scheduled",
6
6
  id: "data-sync:scheduled",
@@ -9,6 +9,7 @@ const metadata = {
9
9
  async function handle(job, ctx) {
10
10
  const em = ctx.resolve("em");
11
11
  const syncRunService = ctx.resolve("dataSyncRunService");
12
+ const progressService = ctx.resolve("progressService");
12
13
  const integrationStateService = ctx.resolve("integrationStateService");
13
14
  const schedule = await findOneWithDecryption(
14
15
  em,
@@ -44,21 +45,19 @@ async function handle(job, ctx) {
44
45
  schedule.direction,
45
46
  job.payload.scope
46
47
  );
47
- const run = await syncRunService.createRun({
48
- integrationId: schedule.integrationId,
49
- entityType: schedule.entityType,
50
- direction: schedule.direction,
51
- cursor,
52
- triggeredBy: "scheduler"
53
- }, job.payload.scope);
54
48
  schedule.lastRunAt = /* @__PURE__ */ new Date();
55
49
  await em.flush();
56
- const queueName = schedule.direction === "import" ? "data-sync-import" : "data-sync-export";
57
- const queue = getSyncQueue(queueName);
58
- await queue.enqueue({
59
- runId: run.id,
60
- batchSize: 100,
61
- scope: job.payload.scope
50
+ await startDataSyncRun({
51
+ syncRunService,
52
+ progressService,
53
+ scope: job.payload.scope,
54
+ input: {
55
+ integrationId: schedule.integrationId,
56
+ entityType: schedule.entityType,
57
+ direction: schedule.direction,
58
+ cursor,
59
+ triggeredBy: "scheduler"
60
+ }
62
61
  });
63
62
  }
64
63
  export {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/data_sync/workers/sync-scheduled.ts"],
4
- "sourcesContent": ["import type { JobContext, QueuedJob, WorkerMeta } from '@open-mercato/queue'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport type { IntegrationStateService } from '../../integrations/lib/state-service'\nimport type { SyncRunService } from '../lib/sync-run-service'\nimport { SyncSchedule } from '../data/entities'\nimport { getSyncQueue } from '../lib/queue'\n\ntype ScheduledSyncPayload = {\n scheduleId: string\n scope: {\n organizationId: string\n tenantId: string\n }\n}\n\nexport const metadata: WorkerMeta = {\n queue: 'data-sync-scheduled',\n id: 'data-sync:scheduled',\n concurrency: 3,\n}\n\ntype HandlerContext = JobContext & {\n resolve: <T = unknown>(name: string) => T\n}\n\nexport default async function handle(job: QueuedJob<ScheduledSyncPayload>, ctx: HandlerContext): Promise<void> {\n const em = ctx.resolve<EntityManager>('em')\n const syncRunService = ctx.resolve<SyncRunService>('dataSyncRunService')\n const integrationStateService = ctx.resolve<IntegrationStateService>('integrationStateService')\n\n const schedule = await findOneWithDecryption(\n em,\n SyncSchedule,\n {\n id: job.payload.scheduleId,\n organizationId: job.payload.scope.organizationId,\n tenantId: job.payload.scope.tenantId,\n deletedAt: null,\n },\n undefined,\n job.payload.scope,\n )\n\n if (!schedule || !schedule.isEnabled) {\n return\n }\n\n const integrationEnabled = await integrationStateService.isEnabled(schedule.integrationId, job.payload.scope)\n if (!integrationEnabled) {\n return\n }\n\n const overlap = await syncRunService.findRunningOverlap(\n schedule.integrationId,\n schedule.entityType,\n schedule.direction,\n job.payload.scope,\n )\n if (overlap) {\n return\n }\n\n const cursor = schedule.fullSync\n ? null\n : await syncRunService.resolveCursor(\n schedule.integrationId,\n schedule.entityType,\n schedule.direction,\n job.payload.scope,\n )\n\n const run = await syncRunService.createRun({\n integrationId: schedule.integrationId,\n entityType: schedule.entityType,\n direction: schedule.direction,\n cursor,\n triggeredBy: 'scheduler',\n }, job.payload.scope)\n\n schedule.lastRunAt = new Date()\n await em.flush()\n\n const queueName = schedule.direction === 'import' ? 'data-sync-import' : 'data-sync-export'\n const queue = getSyncQueue(queueName)\n await queue.enqueue({\n runId: run.id,\n batchSize: 100,\n scope: job.payload.scope,\n })\n}\n"],
5
- "mappings": "AAEA,SAAS,6BAA6B;AAGtC,SAAS,oBAAoB;AAC7B,SAAS,oBAAoB;AAUtB,MAAM,WAAuB;AAAA,EAClC,OAAO;AAAA,EACP,IAAI;AAAA,EACJ,aAAa;AACf;AAMA,eAAO,OAA8B,KAAsC,KAAoC;AAC7G,QAAM,KAAK,IAAI,QAAuB,IAAI;AAC1C,QAAM,iBAAiB,IAAI,QAAwB,oBAAoB;AACvE,QAAM,0BAA0B,IAAI,QAAiC,yBAAyB;AAE9F,QAAM,WAAW,MAAM;AAAA,IACrB;AAAA,IACA;AAAA,IACA;AAAA,MACE,IAAI,IAAI,QAAQ;AAAA,MAChB,gBAAgB,IAAI,QAAQ,MAAM;AAAA,MAClC,UAAU,IAAI,QAAQ,MAAM;AAAA,MAC5B,WAAW;AAAA,IACb;AAAA,IACA;AAAA,IACA,IAAI,QAAQ;AAAA,EACd;AAEA,MAAI,CAAC,YAAY,CAAC,SAAS,WAAW;AACpC;AAAA,EACF;AAEA,QAAM,qBAAqB,MAAM,wBAAwB,UAAU,SAAS,eAAe,IAAI,QAAQ,KAAK;AAC5G,MAAI,CAAC,oBAAoB;AACvB;AAAA,EACF;AAEA,QAAM,UAAU,MAAM,eAAe;AAAA,IACnC,SAAS;AAAA,IACT,SAAS;AAAA,IACT,SAAS;AAAA,IACT,IAAI,QAAQ;AAAA,EACd;AACA,MAAI,SAAS;AACX;AAAA,EACF;AAEA,QAAM,SAAS,SAAS,WACpB,OACA,MAAM,eAAe;AAAA,IACnB,SAAS;AAAA,IACT,SAAS;AAAA,IACT,SAAS;AAAA,IACT,IAAI,QAAQ;AAAA,EACd;AAEJ,QAAM,MAAM,MAAM,eAAe,UAAU;AAAA,IACzC,eAAe,SAAS;AAAA,IACxB,YAAY,SAAS;AAAA,IACrB,WAAW,SAAS;AAAA,IACpB;AAAA,IACA,aAAa;AAAA,EACf,GAAG,IAAI,QAAQ,KAAK;AAEpB,WAAS,YAAY,oBAAI,KAAK;AAC9B,QAAM,GAAG,MAAM;AAEf,QAAM,YAAY,SAAS,cAAc,WAAW,qBAAqB;AACzE,QAAM,QAAQ,aAAa,SAAS;AACpC,QAAM,MAAM,QAAQ;AAAA,IAClB,OAAO,IAAI;AAAA,IACX,WAAW;AAAA,IACX,OAAO,IAAI,QAAQ;AAAA,EACrB,CAAC;AACH;",
4
+ "sourcesContent": ["import type { JobContext, QueuedJob, WorkerMeta } from '@open-mercato/queue'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport type { IntegrationStateService } from '../../integrations/lib/state-service'\nimport type { ProgressService } from '../../progress/lib/progressService'\nimport type { SyncRunService } from '../lib/sync-run-service'\nimport { SyncSchedule } from '../data/entities'\nimport { startDataSyncRun } from '../lib/start-run'\n\ntype ScheduledSyncPayload = {\n scheduleId: string\n scope: {\n organizationId: string\n tenantId: string\n }\n}\n\nexport const metadata: WorkerMeta = {\n queue: 'data-sync-scheduled',\n id: 'data-sync:scheduled',\n concurrency: 3,\n}\n\ntype HandlerContext = JobContext & {\n resolve: <T = unknown>(name: string) => T\n}\n\nexport default async function handle(job: QueuedJob<ScheduledSyncPayload>, ctx: HandlerContext): Promise<void> {\n const em = ctx.resolve<EntityManager>('em')\n const syncRunService = ctx.resolve<SyncRunService>('dataSyncRunService')\n const progressService = ctx.resolve<ProgressService>('progressService')\n const integrationStateService = ctx.resolve<IntegrationStateService>('integrationStateService')\n\n const schedule = await findOneWithDecryption(\n em,\n SyncSchedule,\n {\n id: job.payload.scheduleId,\n organizationId: job.payload.scope.organizationId,\n tenantId: job.payload.scope.tenantId,\n deletedAt: null,\n },\n undefined,\n job.payload.scope,\n )\n\n if (!schedule || !schedule.isEnabled) {\n return\n }\n\n const integrationEnabled = await integrationStateService.isEnabled(schedule.integrationId, job.payload.scope)\n if (!integrationEnabled) {\n return\n }\n\n const overlap = await syncRunService.findRunningOverlap(\n schedule.integrationId,\n schedule.entityType,\n schedule.direction,\n job.payload.scope,\n )\n if (overlap) {\n return\n }\n\n const cursor = schedule.fullSync\n ? null\n : await syncRunService.resolveCursor(\n schedule.integrationId,\n schedule.entityType,\n schedule.direction,\n job.payload.scope,\n )\n\n schedule.lastRunAt = new Date()\n await em.flush()\n\n await startDataSyncRun({\n syncRunService,\n progressService,\n scope: job.payload.scope,\n input: {\n integrationId: schedule.integrationId,\n entityType: schedule.entityType,\n direction: schedule.direction,\n cursor,\n triggeredBy: 'scheduler',\n },\n })\n}\n"],
5
+ "mappings": "AAEA,SAAS,6BAA6B;AAItC,SAAS,oBAAoB;AAC7B,SAAS,wBAAwB;AAU1B,MAAM,WAAuB;AAAA,EAClC,OAAO;AAAA,EACP,IAAI;AAAA,EACJ,aAAa;AACf;AAMA,eAAO,OAA8B,KAAsC,KAAoC;AAC7G,QAAM,KAAK,IAAI,QAAuB,IAAI;AAC1C,QAAM,iBAAiB,IAAI,QAAwB,oBAAoB;AACvE,QAAM,kBAAkB,IAAI,QAAyB,iBAAiB;AACtE,QAAM,0BAA0B,IAAI,QAAiC,yBAAyB;AAE9F,QAAM,WAAW,MAAM;AAAA,IACrB;AAAA,IACA;AAAA,IACA;AAAA,MACE,IAAI,IAAI,QAAQ;AAAA,MAChB,gBAAgB,IAAI,QAAQ,MAAM;AAAA,MAClC,UAAU,IAAI,QAAQ,MAAM;AAAA,MAC5B,WAAW;AAAA,IACb;AAAA,IACA;AAAA,IACA,IAAI,QAAQ;AAAA,EACd;AAEA,MAAI,CAAC,YAAY,CAAC,SAAS,WAAW;AACpC;AAAA,EACF;AAEA,QAAM,qBAAqB,MAAM,wBAAwB,UAAU,SAAS,eAAe,IAAI,QAAQ,KAAK;AAC5G,MAAI,CAAC,oBAAoB;AACvB;AAAA,EACF;AAEA,QAAM,UAAU,MAAM,eAAe;AAAA,IACnC,SAAS;AAAA,IACT,SAAS;AAAA,IACT,SAAS;AAAA,IACT,IAAI,QAAQ;AAAA,EACd;AACA,MAAI,SAAS;AACX;AAAA,EACF;AAEA,QAAM,SAAS,SAAS,WACpB,OACA,MAAM,eAAe;AAAA,IACnB,SAAS;AAAA,IACT,SAAS;AAAA,IACT,SAAS;AAAA,IACT,IAAI,QAAQ;AAAA,EACd;AAEJ,WAAS,YAAY,oBAAI,KAAK;AAC9B,QAAM,GAAG,MAAM;AAEf,QAAM,iBAAiB;AAAA,IACrB;AAAA,IACA;AAAA,IACA,OAAO,IAAI,QAAQ;AAAA,IACnB,OAAO;AAAA,MACL,eAAe,SAAS;AAAA,MACxB,YAAY,SAAS;AAAA,MACrB,WAAW,SAAS;AAAA,MACpB;AAAA,MACA,aAAa;AAAA,IACf;AAAA,EACF,CAAC;AACH;",
6
6
  "names": []
7
7
  }
@@ -47,21 +47,10 @@ function filterTenantsByStatus(list, status) {
47
47
  if (status === "inactive") return list.filter((tenant) => !tenant.isActive);
48
48
  return list;
49
49
  }
50
- async function fetchDirectoryTenants(status, errorMessage) {
51
- const search = new URLSearchParams();
52
- search.set("page", "1");
53
- search.set("pageSize", "100");
54
- search.set("sortField", "name");
55
- search.set("sortDir", "asc");
56
- if (status === "active") search.set("isActive", "true");
57
- if (status === "inactive") search.set("isActive", "false");
58
- const json = await readApiResultOrThrow(
59
- `/api/directory/tenants?${search.toString()}`,
60
- void 0,
61
- { errorMessage, allowNullResult: true }
62
- );
63
- const items = Array.isArray(json?.items) ? json.items : [];
64
- const normalized = items.map((item) => {
50
+ const DIRECTORY_TENANTS_PAGE_SIZE = 100;
51
+ const DIRECTORY_TENANTS_MAX_PAGES = 200;
52
+ function normalizeTenantItems(items) {
53
+ return items.map((item) => {
65
54
  if (!item || typeof item !== "object") return null;
66
55
  const entry = item;
67
56
  const rawId = entry.id;
@@ -72,7 +61,31 @@ async function fetchDirectoryTenants(status, errorMessage) {
72
61
  const isActive = entry.isActive !== false;
73
62
  return { id, name, isActive };
74
63
  }).filter((tenant) => tenant !== null);
75
- return mergeTenantLists(filterTenantsByStatus(normalized, status));
64
+ }
65
+ async function fetchDirectoryTenants(status, errorMessage) {
66
+ const collected = [];
67
+ let page = 1;
68
+ let totalPages = 1;
69
+ do {
70
+ const search = new URLSearchParams();
71
+ search.set("page", String(page));
72
+ search.set("pageSize", String(DIRECTORY_TENANTS_PAGE_SIZE));
73
+ search.set("sortField", "name");
74
+ search.set("sortDir", "asc");
75
+ if (status === "active") search.set("isActive", "true");
76
+ if (status === "inactive") search.set("isActive", "false");
77
+ const json = await readApiResultOrThrow(
78
+ `/api/directory/tenants?${search.toString()}`,
79
+ void 0,
80
+ { errorMessage, allowNullResult: true }
81
+ );
82
+ const items = Array.isArray(json?.items) ? json.items : [];
83
+ collected.push(...normalizeTenantItems(items));
84
+ totalPages = typeof json?.totalPages === "number" && Number.isFinite(json.totalPages) ? Math.max(1, Math.floor(json.totalPages)) : 1;
85
+ if (items.length < DIRECTORY_TENANTS_PAGE_SIZE) break;
86
+ page += 1;
87
+ } while (page <= totalPages && page <= DIRECTORY_TENANTS_MAX_PAGES);
88
+ return mergeTenantLists(filterTenantsByStatus(collected, status));
76
89
  }
77
90
  async function fetchTenantsFromOrganizationSwitcher(status, errorMessage) {
78
91
  const payload = await readApiResultOrThrow(
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/directory/components/TenantSelect.tsx"],
4
- "sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\n\nexport type TenantRecord = {\n id: string\n name: string\n isActive: boolean\n}\n\nfunction normalizeTenant(tenant: TenantRecord | null | undefined): TenantRecord | null {\n if (!tenant || typeof tenant.id !== 'string' || tenant.id.trim().length === 0) return null\n const id = tenant.id.trim()\n const name = typeof tenant.name === 'string' && tenant.name.trim().length > 0 ? tenant.name.trim() : id\n const isActive = tenant.isActive !== false\n return { id, name, isActive }\n}\n\nfunction sanitizeTenantList(list?: TenantRecord[] | null): TenantRecord[] {\n if (!Array.isArray(list)) return []\n const seen = new Map<string, TenantRecord>()\n for (const tenant of list) {\n const normalized = normalizeTenant(tenant)\n if (!normalized) continue\n seen.set(normalized.id, normalized)\n }\n return Array.from(seen.values())\n}\n\nfunction mergeTenantLists(...lists: TenantRecord[][]): TenantRecord[] {\n const map = new Map<string, TenantRecord>()\n for (const list of lists) {\n for (const tenant of list) {\n if (!tenant || typeof tenant.id !== 'string' || tenant.id.trim().length === 0) continue\n const id = tenant.id.trim()\n const existing = map.get(id)\n if (!existing) {\n map.set(id, {\n id,\n name: typeof tenant.name === 'string' && tenant.name.length > 0 ? tenant.name : id,\n isActive: tenant.isActive !== false,\n })\n } else {\n const name = typeof tenant.name === 'string' && tenant.name.length > 0 ? tenant.name : existing.name\n const isActive = tenant.isActive !== undefined ? tenant.isActive !== false : existing.isActive\n map.set(id, { id, name, isActive })\n }\n }\n }\n return Array.from(map.values()).sort((a, b) => a.name.localeCompare(b.name))\n}\n\nfunction filterTenantsByStatus(list: TenantRecord[], status: 'all' | 'active' | 'inactive'): TenantRecord[] {\n if (status === 'active') return list.filter((tenant: TenantRecord) => tenant.isActive)\n if (status === 'inactive') return list.filter((tenant: TenantRecord) => !tenant.isActive)\n return list\n}\n\nasync function fetchDirectoryTenants(status: 'all' | 'active' | 'inactive', errorMessage: string): Promise<TenantRecord[]> {\n const search = new URLSearchParams()\n search.set('page', '1')\n search.set('pageSize', '100')\n search.set('sortField', 'name')\n search.set('sortDir', 'asc')\n if (status === 'active') search.set('isActive', 'true')\n if (status === 'inactive') search.set('isActive', 'false')\n const json = await readApiResultOrThrow<{ items?: unknown[] }>(\n `/api/directory/tenants?${search.toString()}`,\n undefined,\n { errorMessage, allowNullResult: true },\n )\n const items = Array.isArray(json?.items) ? json.items : []\n const normalized = items\n .map((item: unknown): TenantRecord | null => {\n if (!item || typeof item !== 'object') return null\n const entry = item as Record<string, unknown>\n const rawId = entry.id\n const id = typeof rawId === 'string' ? rawId : null\n if (!id || !id.length) return null\n const rawName = entry.name\n const name = typeof rawName === 'string' && rawName.length > 0 ? rawName : id\n const isActive = entry.isActive !== false\n return { id, name, isActive }\n })\n .filter((tenant: TenantRecord | null): tenant is TenantRecord => tenant !== null)\n return mergeTenantLists(filterTenantsByStatus(normalized, status))\n}\n\nasync function fetchTenantsFromOrganizationSwitcher(status: 'all' | 'active' | 'inactive', errorMessage: string): Promise<TenantRecord[]> {\n const payload = await readApiResultOrThrow<Record<string, unknown>>(\n '/api/directory/organization-switcher',\n undefined,\n { errorMessage, allowNullResult: true },\n )\n const json = payload ?? {}\n const rawTenants = Array.isArray(json.tenants) ? json.tenants : []\n const sanitized = sanitizeTenantList(rawTenants as TenantRecord[] | null)\n return mergeTenantLists(filterTenantsByStatus(sanitized, status))\n}\n\nexport type TenantSelectProps = {\n value?: string | null\n onChange?: (value: string | null) => void\n disabled?: boolean\n required?: boolean\n className?: string\n id?: string\n name?: string\n includeEmptyOption?: boolean\n emptyOptionLabel?: string\n fetchOnMount?: boolean\n status?: 'all' | 'active' | 'inactive'\n tenants?: TenantRecord[] | null\n}\n\nexport const TenantSelect = React.forwardRef<HTMLSelectElement, TenantSelectProps>(function TenantSelect(\n {\n value,\n onChange,\n disabled = false,\n required = false,\n className,\n id,\n name,\n includeEmptyOption = false,\n emptyOptionLabel,\n fetchOnMount = true,\n status = 'all',\n tenants: providedTenantsInput = null,\n },\n ref,\n) {\n const t = useT()\n const providedTenants = React.useMemo(() => sanitizeTenantList(providedTenantsInput), [providedTenantsInput])\n const [remoteTenants, setRemoteTenants] = React.useState<TenantRecord[]>([])\n const [fetchStatus, setFetchStatus] = React.useState<'idle' | 'loading' | 'success' | 'error'>(fetchOnMount ? 'loading' : 'idle')\n\n React.useEffect(() => {\n const fetchErrorMessage = t('tenantSelect.error', 'Failed to load tenants')\n if (!fetchOnMount) {\n setFetchStatus((prev) => (prev === 'loading' ? 'idle' : prev))\n return\n }\n let cancelled = false\n const loadTenants = async () => {\n setFetchStatus('loading')\n const autoSelect = (tenants: TenantRecord[]) => {\n if (!value && !includeEmptyOption && tenants.length > 0) {\n onChange?.(tenants[0].id)\n }\n }\n try {\n const tenants = await fetchDirectoryTenants(status, fetchErrorMessage)\n if (cancelled) return\n setRemoteTenants(tenants)\n autoSelect(tenants)\n setFetchStatus('success')\n return\n } catch {\n try {\n const fallbackTenants = await fetchTenantsFromOrganizationSwitcher(status, fetchErrorMessage)\n if (cancelled) return\n setRemoteTenants(fallbackTenants)\n autoSelect(fallbackTenants)\n setFetchStatus('success')\n return\n } catch {\n if (cancelled) return\n setFetchStatus('error')\n }\n }\n }\n void loadTenants()\n return () => { cancelled = true }\n // eslint-disable-next-line react-hooks/exhaustive-deps -- value/onChange/includeEmptyOption are read once at fetch time for auto-select\n }, [fetchOnMount, status, t])\n\n const mergedTenants = React.useMemo(\n () => mergeTenantLists(providedTenants, remoteTenants),\n [providedTenants, remoteTenants],\n )\n\n const fallbackTenant = React.useMemo(() => {\n const tenantId = typeof value === 'string' && value.trim().length > 0 ? value.trim() : null\n if (!tenantId) return null\n if (mergedTenants.some((tenant) => tenant.id === tenantId)) return null\n return { id: tenantId, name: tenantId, isActive: true }\n }, [mergedTenants, value])\n\n const tenantsForRender = React.useMemo(\n () => (fallbackTenant ? mergeTenantLists(mergedTenants, [fallbackTenant]) : mergedTenants),\n [mergedTenants, fallbackTenant],\n )\n\n const showLoading = fetchStatus === 'loading' && tenantsForRender.length === 0\n const showError = fetchStatus === 'error' && tenantsForRender.length === 0\n\n const handleChange = React.useCallback((event: React.ChangeEvent<HTMLSelectElement>) => {\n if (!onChange) return\n const next = event.target.value\n onChange(next ? next : null)\n }, [onChange])\n\n const selectValue = value ?? ''\n const resolvedEmptyOptionLabel = emptyOptionLabel ?? t('tenantSelect.empty')\n const loadingLabel = t('tenantSelect.loading')\n const errorLabel = t('tenantSelect.error')\n const inactiveSuffix = ` (${t('tenantSelect.inactive')})`\n\n return (\n <select\n ref={ref}\n id={id}\n name={name}\n value={selectValue}\n onChange={handleChange}\n disabled={disabled || (showError && tenantsForRender.length === 0)}\n required={required}\n className={className}\n >\n {includeEmptyOption ? (\n <option value=\"\">\n {resolvedEmptyOptionLabel}\n </option>\n ) : null}\n {showLoading ? (\n <option value=\"\" disabled>{loadingLabel}</option>\n ) : null}\n {showError ? (\n <option value=\"\" disabled>{errorLabel}</option>\n ) : null}\n {!showLoading && tenantsForRender.length > 0\n ? tenantsForRender.map((tenant) => (\n <option key={tenant.id} value={tenant.id}>\n {tenant.name}{tenant.isActive ? '' : inactiveSuffix}\n </option>\n ))\n : null}\n </select>\n )\n})\n"],
5
- "mappings": ";AA8NQ,cAYE,YAZF;AA7NR,YAAY,WAAW;AACvB,SAAS,4BAA4B;AACrC,SAAS,YAAY;AAQrB,SAAS,gBAAgB,QAA8D;AACrF,MAAI,CAAC,UAAU,OAAO,OAAO,OAAO,YAAY,OAAO,GAAG,KAAK,EAAE,WAAW,EAAG,QAAO;AACtF,QAAM,KAAK,OAAO,GAAG,KAAK;AAC1B,QAAM,OAAO,OAAO,OAAO,SAAS,YAAY,OAAO,KAAK,KAAK,EAAE,SAAS,IAAI,OAAO,KAAK,KAAK,IAAI;AACrG,QAAM,WAAW,OAAO,aAAa;AACrC,SAAO,EAAE,IAAI,MAAM,SAAS;AAC9B;AAEA,SAAS,mBAAmB,MAA8C;AACxE,MAAI,CAAC,MAAM,QAAQ,IAAI,EAAG,QAAO,CAAC;AAClC,QAAM,OAAO,oBAAI,IAA0B;AAC3C,aAAW,UAAU,MAAM;AACzB,UAAM,aAAa,gBAAgB,MAAM;AACzC,QAAI,CAAC,WAAY;AACjB,SAAK,IAAI,WAAW,IAAI,UAAU;AAAA,EACpC;AACA,SAAO,MAAM,KAAK,KAAK,OAAO,CAAC;AACjC;AAEA,SAAS,oBAAoB,OAAyC;AACpE,QAAM,MAAM,oBAAI,IAA0B;AAC1C,aAAW,QAAQ,OAAO;AACxB,eAAW,UAAU,MAAM;AACzB,UAAI,CAAC,UAAU,OAAO,OAAO,OAAO,YAAY,OAAO,GAAG,KAAK,EAAE,WAAW,EAAG;AAC/E,YAAM,KAAK,OAAO,GAAG,KAAK;AAC1B,YAAM,WAAW,IAAI,IAAI,EAAE;AAC3B,UAAI,CAAC,UAAU;AACb,YAAI,IAAI,IAAI;AAAA,UACV;AAAA,UACA,MAAM,OAAO,OAAO,SAAS,YAAY,OAAO,KAAK,SAAS,IAAI,OAAO,OAAO;AAAA,UAChF,UAAU,OAAO,aAAa;AAAA,QAChC,CAAC;AAAA,MACH,OAAO;AACL,cAAM,OAAO,OAAO,OAAO,SAAS,YAAY,OAAO,KAAK,SAAS,IAAI,OAAO,OAAO,SAAS;AAChG,cAAM,WAAW,OAAO,aAAa,SAAY,OAAO,aAAa,QAAQ,SAAS;AACtF,YAAI,IAAI,IAAI,EAAE,IAAI,MAAM,SAAS,CAAC;AAAA,MACpC;AAAA,IACF;AAAA,EACF;AACA,SAAO,MAAM,KAAK,IAAI,OAAO,CAAC,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,KAAK,cAAc,EAAE,IAAI,CAAC;AAC7E;AAEA,SAAS,sBAAsB,MAAsB,QAAuD;AAC1G,MAAI,WAAW,SAAU,QAAO,KAAK,OAAO,CAAC,WAAyB,OAAO,QAAQ;AACrF,MAAI,WAAW,WAAY,QAAO,KAAK,OAAO,CAAC,WAAyB,CAAC,OAAO,QAAQ;AACxF,SAAO;AACT;AAEA,eAAe,sBAAsB,QAAuC,cAA+C;AACzH,QAAM,SAAS,IAAI,gBAAgB;AACnC,SAAO,IAAI,QAAQ,GAAG;AACtB,SAAO,IAAI,YAAY,KAAK;AAC5B,SAAO,IAAI,aAAa,MAAM;AAC9B,SAAO,IAAI,WAAW,KAAK;AAC3B,MAAI,WAAW,SAAU,QAAO,IAAI,YAAY,MAAM;AACtD,MAAI,WAAW,WAAY,QAAO,IAAI,YAAY,OAAO;AACzD,QAAM,OAAO,MAAM;AAAA,IACjB,0BAA0B,OAAO,SAAS,CAAC;AAAA,IAC3C;AAAA,IACA,EAAE,cAAc,iBAAiB,KAAK;AAAA,EACxC;AACA,QAAM,QAAQ,MAAM,QAAQ,MAAM,KAAK,IAAI,KAAK,QAAQ,CAAC;AACzD,QAAM,aAAa,MAChB,IAAI,CAAC,SAAuC;AAC3C,QAAI,CAAC,QAAQ,OAAO,SAAS,SAAU,QAAO;AAC9C,UAAM,QAAQ;AACd,UAAM,QAAQ,MAAM;AACpB,UAAM,KAAK,OAAO,UAAU,WAAW,QAAQ;AAC/C,QAAI,CAAC,MAAM,CAAC,GAAG,OAAQ,QAAO;AAC9B,UAAM,UAAU,MAAM;AACtB,UAAM,OAAO,OAAO,YAAY,YAAY,QAAQ,SAAS,IAAI,UAAU;AAC3E,UAAM,WAAW,MAAM,aAAa;AACpC,WAAO,EAAE,IAAI,MAAM,SAAS;AAAA,EAC9B,CAAC,EACA,OAAO,CAAC,WAAwD,WAAW,IAAI;AAClF,SAAO,iBAAiB,sBAAsB,YAAY,MAAM,CAAC;AACnE;AAEA,eAAe,qCAAqC,QAAuC,cAA+C;AACxI,QAAM,UAAU,MAAM;AAAA,IACpB;AAAA,IACA;AAAA,IACA,EAAE,cAAc,iBAAiB,KAAK;AAAA,EACxC;AACA,QAAM,OAAO,WAAW,CAAC;AACzB,QAAM,aAAa,MAAM,QAAQ,KAAK,OAAO,IAAI,KAAK,UAAU,CAAC;AACjE,QAAM,YAAY,mBAAmB,UAAmC;AACxE,SAAO,iBAAiB,sBAAsB,WAAW,MAAM,CAAC;AAClE;AAiBO,MAAM,eAAe,MAAM,WAAiD,SAASA,cAC1F;AAAA,EACE;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX,WAAW;AAAA,EACX;AAAA,EACA;AAAA,EACA;AAAA,EACA,qBAAqB;AAAA,EACrB;AAAA,EACA,eAAe;AAAA,EACf,SAAS;AAAA,EACT,SAAS,uBAAuB;AAClC,GACA,KACA;AACA,QAAM,IAAI,KAAK;AACf,QAAM,kBAAkB,MAAM,QAAQ,MAAM,mBAAmB,oBAAoB,GAAG,CAAC,oBAAoB,CAAC;AAC5G,QAAM,CAAC,eAAe,gBAAgB,IAAI,MAAM,SAAyB,CAAC,CAAC;AAC3E,QAAM,CAAC,aAAa,cAAc,IAAI,MAAM,SAAmD,eAAe,YAAY,MAAM;AAEhI,QAAM,UAAU,MAAM;AACpB,UAAM,oBAAoB,EAAE,sBAAsB,wBAAwB;AAC1E,QAAI,CAAC,cAAc;AACjB,qBAAe,CAAC,SAAU,SAAS,YAAY,SAAS,IAAK;AAC7D;AAAA,IACF;AACA,QAAI,YAAY;AAChB,UAAM,cAAc,YAAY;AAC9B,qBAAe,SAAS;AACxB,YAAM,aAAa,CAAC,YAA4B;AAC9C,YAAI,CAAC,SAAS,CAAC,sBAAsB,QAAQ,SAAS,GAAG;AACvD,qBAAW,QAAQ,CAAC,EAAE,EAAE;AAAA,QAC1B;AAAA,MACF;AACA,UAAI;AACF,cAAM,UAAU,MAAM,sBAAsB,QAAQ,iBAAiB;AACrE,YAAI,UAAW;AACf,yBAAiB,OAAO;AACxB,mBAAW,OAAO;AAClB,uBAAe,SAAS;AACxB;AAAA,MACF,QAAQ;AACN,YAAI;AACF,gBAAM,kBAAkB,MAAM,qCAAqC,QAAQ,iBAAiB;AAC5F,cAAI,UAAW;AACf,2BAAiB,eAAe;AAChC,qBAAW,eAAe;AAC1B,yBAAe,SAAS;AACxB;AAAA,QACF,QAAQ;AACN,cAAI,UAAW;AACf,yBAAe,OAAO;AAAA,QACxB;AAAA,MACF;AAAA,IACF;AACA,SAAK,YAAY;AACjB,WAAO,MAAM;AAAE,kBAAY;AAAA,IAAK;AAAA,EAElC,GAAG,CAAC,cAAc,QAAQ,CAAC,CAAC;AAE5B,QAAM,gBAAgB,MAAM;AAAA,IAC1B,MAAM,iBAAiB,iBAAiB,aAAa;AAAA,IACrD,CAAC,iBAAiB,aAAa;AAAA,EACjC;AAEA,QAAM,iBAAiB,MAAM,QAAQ,MAAM;AACzC,UAAM,WAAW,OAAO,UAAU,YAAY,MAAM,KAAK,EAAE,SAAS,IAAI,MAAM,KAAK,IAAI;AACvF,QAAI,CAAC,SAAU,QAAO;AACtB,QAAI,cAAc,KAAK,CAAC,WAAW,OAAO,OAAO,QAAQ,EAAG,QAAO;AACnE,WAAO,EAAE,IAAI,UAAU,MAAM,UAAU,UAAU,KAAK;AAAA,EACxD,GAAG,CAAC,eAAe,KAAK,CAAC;AAEzB,QAAM,mBAAmB,MAAM;AAAA,IAC7B,MAAO,iBAAiB,iBAAiB,eAAe,CAAC,cAAc,CAAC,IAAI;AAAA,IAC5E,CAAC,eAAe,cAAc;AAAA,EAChC;AAEA,QAAM,cAAc,gBAAgB,aAAa,iBAAiB,WAAW;AAC7E,QAAM,YAAY,gBAAgB,WAAW,iBAAiB,WAAW;AAEzE,QAAM,eAAe,MAAM,YAAY,CAAC,UAAgD;AACtF,QAAI,CAAC,SAAU;AACf,UAAM,OAAO,MAAM,OAAO;AAC1B,aAAS,OAAO,OAAO,IAAI;AAAA,EAC7B,GAAG,CAAC,QAAQ,CAAC;AAEb,QAAM,cAAc,SAAS;AAC7B,QAAM,2BAA2B,oBAAoB,EAAE,oBAAoB;AAC3E,QAAM,eAAe,EAAE,sBAAsB;AAC7C,QAAM,aAAa,EAAE,oBAAoB;AACzC,QAAM,iBAAiB,KAAK,EAAE,uBAAuB,CAAC;AAEtD,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA;AAAA,MACA;AAAA,MACA,OAAO;AAAA,MACP,UAAU;AAAA,MACV,UAAU,YAAa,aAAa,iBAAiB,WAAW;AAAA,MAChE;AAAA,MACA;AAAA,MAEC;AAAA,6BACC,oBAAC,YAAO,OAAM,IACX,oCACH,IACE;AAAA,QACH,cACC,oBAAC,YAAO,OAAM,IAAG,UAAQ,MAAE,wBAAa,IACtC;AAAA,QACH,YACC,oBAAC,YAAO,OAAM,IAAG,UAAQ,MAAE,sBAAW,IACpC;AAAA,QACH,CAAC,eAAe,iBAAiB,SAAS,IACvC,iBAAiB,IAAI,CAAC,WACtB,qBAAC,YAAuB,OAAO,OAAO,IACnC;AAAA,iBAAO;AAAA,UAAM,OAAO,WAAW,KAAK;AAAA,aAD1B,OAAO,EAEpB,CACD,IACC;AAAA;AAAA;AAAA,EACN;AAEJ,CAAC;",
4
+ "sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\n\nexport type TenantRecord = {\n id: string\n name: string\n isActive: boolean\n}\n\nfunction normalizeTenant(tenant: TenantRecord | null | undefined): TenantRecord | null {\n if (!tenant || typeof tenant.id !== 'string' || tenant.id.trim().length === 0) return null\n const id = tenant.id.trim()\n const name = typeof tenant.name === 'string' && tenant.name.trim().length > 0 ? tenant.name.trim() : id\n const isActive = tenant.isActive !== false\n return { id, name, isActive }\n}\n\nfunction sanitizeTenantList(list?: TenantRecord[] | null): TenantRecord[] {\n if (!Array.isArray(list)) return []\n const seen = new Map<string, TenantRecord>()\n for (const tenant of list) {\n const normalized = normalizeTenant(tenant)\n if (!normalized) continue\n seen.set(normalized.id, normalized)\n }\n return Array.from(seen.values())\n}\n\nfunction mergeTenantLists(...lists: TenantRecord[][]): TenantRecord[] {\n const map = new Map<string, TenantRecord>()\n for (const list of lists) {\n for (const tenant of list) {\n if (!tenant || typeof tenant.id !== 'string' || tenant.id.trim().length === 0) continue\n const id = tenant.id.trim()\n const existing = map.get(id)\n if (!existing) {\n map.set(id, {\n id,\n name: typeof tenant.name === 'string' && tenant.name.length > 0 ? tenant.name : id,\n isActive: tenant.isActive !== false,\n })\n } else {\n const name = typeof tenant.name === 'string' && tenant.name.length > 0 ? tenant.name : existing.name\n const isActive = tenant.isActive !== undefined ? tenant.isActive !== false : existing.isActive\n map.set(id, { id, name, isActive })\n }\n }\n }\n return Array.from(map.values()).sort((a, b) => a.name.localeCompare(b.name))\n}\n\nfunction filterTenantsByStatus(list: TenantRecord[], status: 'all' | 'active' | 'inactive'): TenantRecord[] {\n if (status === 'active') return list.filter((tenant: TenantRecord) => tenant.isActive)\n if (status === 'inactive') return list.filter((tenant: TenantRecord) => !tenant.isActive)\n return list\n}\n\nconst DIRECTORY_TENANTS_PAGE_SIZE = 100\nconst DIRECTORY_TENANTS_MAX_PAGES = 200\n\nfunction normalizeTenantItems(items: unknown[]): TenantRecord[] {\n return items\n .map((item: unknown): TenantRecord | null => {\n if (!item || typeof item !== 'object') return null\n const entry = item as Record<string, unknown>\n const rawId = entry.id\n const id = typeof rawId === 'string' ? rawId : null\n if (!id || !id.length) return null\n const rawName = entry.name\n const name = typeof rawName === 'string' && rawName.length > 0 ? rawName : id\n const isActive = entry.isActive !== false\n return { id, name, isActive }\n })\n .filter((tenant: TenantRecord | null): tenant is TenantRecord => tenant !== null)\n}\n\nasync function fetchDirectoryTenants(status: 'all' | 'active' | 'inactive', errorMessage: string): Promise<TenantRecord[]> {\n const collected: TenantRecord[] = []\n let page = 1\n let totalPages = 1\n do {\n const search = new URLSearchParams()\n search.set('page', String(page))\n search.set('pageSize', String(DIRECTORY_TENANTS_PAGE_SIZE))\n search.set('sortField', 'name')\n search.set('sortDir', 'asc')\n if (status === 'active') search.set('isActive', 'true')\n if (status === 'inactive') search.set('isActive', 'false')\n const json = await readApiResultOrThrow<{ items?: unknown[]; totalPages?: unknown }>(\n `/api/directory/tenants?${search.toString()}`,\n undefined,\n { errorMessage, allowNullResult: true },\n )\n const items = Array.isArray(json?.items) ? json.items : []\n collected.push(...normalizeTenantItems(items))\n totalPages = typeof json?.totalPages === 'number' && Number.isFinite(json.totalPages)\n ? Math.max(1, Math.floor(json.totalPages))\n : 1\n if (items.length < DIRECTORY_TENANTS_PAGE_SIZE) break\n page += 1\n } while (page <= totalPages && page <= DIRECTORY_TENANTS_MAX_PAGES)\n return mergeTenantLists(filterTenantsByStatus(collected, status))\n}\n\nasync function fetchTenantsFromOrganizationSwitcher(status: 'all' | 'active' | 'inactive', errorMessage: string): Promise<TenantRecord[]> {\n const payload = await readApiResultOrThrow<Record<string, unknown>>(\n '/api/directory/organization-switcher',\n undefined,\n { errorMessage, allowNullResult: true },\n )\n const json = payload ?? {}\n const rawTenants = Array.isArray(json.tenants) ? json.tenants : []\n const sanitized = sanitizeTenantList(rawTenants as TenantRecord[] | null)\n return mergeTenantLists(filterTenantsByStatus(sanitized, status))\n}\n\nexport type TenantSelectProps = {\n value?: string | null\n onChange?: (value: string | null) => void\n disabled?: boolean\n required?: boolean\n className?: string\n id?: string\n name?: string\n includeEmptyOption?: boolean\n emptyOptionLabel?: string\n fetchOnMount?: boolean\n status?: 'all' | 'active' | 'inactive'\n tenants?: TenantRecord[] | null\n}\n\nexport const TenantSelect = React.forwardRef<HTMLSelectElement, TenantSelectProps>(function TenantSelect(\n {\n value,\n onChange,\n disabled = false,\n required = false,\n className,\n id,\n name,\n includeEmptyOption = false,\n emptyOptionLabel,\n fetchOnMount = true,\n status = 'all',\n tenants: providedTenantsInput = null,\n },\n ref,\n) {\n const t = useT()\n const providedTenants = React.useMemo(() => sanitizeTenantList(providedTenantsInput), [providedTenantsInput])\n const [remoteTenants, setRemoteTenants] = React.useState<TenantRecord[]>([])\n const [fetchStatus, setFetchStatus] = React.useState<'idle' | 'loading' | 'success' | 'error'>(fetchOnMount ? 'loading' : 'idle')\n\n React.useEffect(() => {\n const fetchErrorMessage = t('tenantSelect.error', 'Failed to load tenants')\n if (!fetchOnMount) {\n setFetchStatus((prev) => (prev === 'loading' ? 'idle' : prev))\n return\n }\n let cancelled = false\n const loadTenants = async () => {\n setFetchStatus('loading')\n const autoSelect = (tenants: TenantRecord[]) => {\n if (!value && !includeEmptyOption && tenants.length > 0) {\n onChange?.(tenants[0].id)\n }\n }\n try {\n const tenants = await fetchDirectoryTenants(status, fetchErrorMessage)\n if (cancelled) return\n setRemoteTenants(tenants)\n autoSelect(tenants)\n setFetchStatus('success')\n return\n } catch {\n try {\n const fallbackTenants = await fetchTenantsFromOrganizationSwitcher(status, fetchErrorMessage)\n if (cancelled) return\n setRemoteTenants(fallbackTenants)\n autoSelect(fallbackTenants)\n setFetchStatus('success')\n return\n } catch {\n if (cancelled) return\n setFetchStatus('error')\n }\n }\n }\n void loadTenants()\n return () => { cancelled = true }\n // eslint-disable-next-line react-hooks/exhaustive-deps -- value/onChange/includeEmptyOption are read once at fetch time for auto-select\n }, [fetchOnMount, status, t])\n\n const mergedTenants = React.useMemo(\n () => mergeTenantLists(providedTenants, remoteTenants),\n [providedTenants, remoteTenants],\n )\n\n const fallbackTenant = React.useMemo(() => {\n const tenantId = typeof value === 'string' && value.trim().length > 0 ? value.trim() : null\n if (!tenantId) return null\n if (mergedTenants.some((tenant) => tenant.id === tenantId)) return null\n return { id: tenantId, name: tenantId, isActive: true }\n }, [mergedTenants, value])\n\n const tenantsForRender = React.useMemo(\n () => (fallbackTenant ? mergeTenantLists(mergedTenants, [fallbackTenant]) : mergedTenants),\n [mergedTenants, fallbackTenant],\n )\n\n const showLoading = fetchStatus === 'loading' && tenantsForRender.length === 0\n const showError = fetchStatus === 'error' && tenantsForRender.length === 0\n\n const handleChange = React.useCallback((event: React.ChangeEvent<HTMLSelectElement>) => {\n if (!onChange) return\n const next = event.target.value\n onChange(next ? next : null)\n }, [onChange])\n\n const selectValue = value ?? ''\n const resolvedEmptyOptionLabel = emptyOptionLabel ?? t('tenantSelect.empty')\n const loadingLabel = t('tenantSelect.loading')\n const errorLabel = t('tenantSelect.error')\n const inactiveSuffix = ` (${t('tenantSelect.inactive')})`\n\n return (\n <select\n ref={ref}\n id={id}\n name={name}\n value={selectValue}\n onChange={handleChange}\n disabled={disabled || (showError && tenantsForRender.length === 0)}\n required={required}\n className={className}\n >\n {includeEmptyOption ? (\n <option value=\"\">\n {resolvedEmptyOptionLabel}\n </option>\n ) : null}\n {showLoading ? (\n <option value=\"\" disabled>{loadingLabel}</option>\n ) : null}\n {showError ? (\n <option value=\"\" disabled>{errorLabel}</option>\n ) : null}\n {!showLoading && tenantsForRender.length > 0\n ? tenantsForRender.map((tenant) => (\n <option key={tenant.id} value={tenant.id}>\n {tenant.name}{tenant.isActive ? '' : inactiveSuffix}\n </option>\n ))\n : null}\n </select>\n )\n})\n"],
5
+ "mappings": "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",
6
6
  "names": ["TenantSelect"]
7
7
  }
@@ -0,0 +1,35 @@
1
+ import {
2
+ readVersionedPreference,
3
+ writeVersionedPreference,
4
+ clearVersionedPreference
5
+ } from "@open-mercato/shared/lib/browser/versionedPreference";
6
+ const LEGACY_HIDDEN_WIDGETS_KEY = "om:portal:dashboard:hidden";
7
+ const HIDDEN_WIDGETS_KEY_PREFIX = "om:portal:dashboard:hidden:v1:";
8
+ const HIDDEN_WIDGETS_VERSION = 1;
9
+ function buildHiddenWidgetsKey(orgSlug, userId) {
10
+ return `${HIDDEN_WIDGETS_KEY_PREFIX}${orgSlug}:${userId}`;
11
+ }
12
+ function isStringArray(value) {
13
+ return Array.isArray(value) && value.every((id) => typeof id === "string");
14
+ }
15
+ function loadHiddenWidgets(orgSlug, userId) {
16
+ const hidden = readVersionedPreference(
17
+ buildHiddenWidgetsKey(orgSlug, userId),
18
+ HIDDEN_WIDGETS_VERSION,
19
+ isStringArray,
20
+ []
21
+ );
22
+ return new Set(hidden);
23
+ }
24
+ function saveHiddenWidgets(orgSlug, userId, hidden) {
25
+ writeVersionedPreference(buildHiddenWidgetsKey(orgSlug, userId), HIDDEN_WIDGETS_VERSION, Array.from(hidden));
26
+ }
27
+ function clearLegacyHiddenWidgetsKey() {
28
+ clearVersionedPreference(LEGACY_HIDDEN_WIDGETS_KEY);
29
+ }
30
+ export {
31
+ clearLegacyHiddenWidgetsKey,
32
+ loadHiddenWidgets,
33
+ saveHiddenWidgets
34
+ };
35
+ //# sourceMappingURL=hiddenWidgetsStorage.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../../../src/modules/portal/frontend/%5BorgSlug%5D/portal/dashboard/hiddenWidgetsStorage.ts"],
4
+ "sourcesContent": ["import {\n readVersionedPreference,\n writeVersionedPreference,\n clearVersionedPreference,\n} from '@open-mercato/shared/lib/browser/versionedPreference'\n\nconst LEGACY_HIDDEN_WIDGETS_KEY = 'om:portal:dashboard:hidden'\nconst HIDDEN_WIDGETS_KEY_PREFIX = 'om:portal:dashboard:hidden:v1:'\nconst HIDDEN_WIDGETS_VERSION = 1\n\nfunction buildHiddenWidgetsKey(orgSlug: string, userId: string): string {\n return `${HIDDEN_WIDGETS_KEY_PREFIX}${orgSlug}:${userId}`\n}\n\nfunction isStringArray(value: unknown): value is string[] {\n return Array.isArray(value) && value.every((id) => typeof id === 'string')\n}\n\nexport function loadHiddenWidgets(orgSlug: string, userId: string): Set<string> {\n const hidden = readVersionedPreference<string[]>(\n buildHiddenWidgetsKey(orgSlug, userId),\n HIDDEN_WIDGETS_VERSION,\n isStringArray,\n [],\n )\n return new Set(hidden)\n}\n\nexport function saveHiddenWidgets(orgSlug: string, userId: string, hidden: Set<string>): void {\n writeVersionedPreference(buildHiddenWidgetsKey(orgSlug, userId), HIDDEN_WIDGETS_VERSION, Array.from(hidden))\n}\n\n/** Drops the pre-scoping global key so stale preferences stop leaking across org/user contexts. */\nexport function clearLegacyHiddenWidgetsKey(): void {\n clearVersionedPreference(LEGACY_HIDDEN_WIDGETS_KEY)\n}\n"],
5
+ "mappings": "AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,MAAM,4BAA4B;AAClC,MAAM,4BAA4B;AAClC,MAAM,yBAAyB;AAE/B,SAAS,sBAAsB,SAAiB,QAAwB;AACtE,SAAO,GAAG,yBAAyB,GAAG,OAAO,IAAI,MAAM;AACzD;AAEA,SAAS,cAAc,OAAmC;AACxD,SAAO,MAAM,QAAQ,KAAK,KAAK,MAAM,MAAM,CAAC,OAAO,OAAO,OAAO,QAAQ;AAC3E;AAEO,SAAS,kBAAkB,SAAiB,QAA6B;AAC9E,QAAM,SAAS;AAAA,IACb,sBAAsB,SAAS,MAAM;AAAA,IACrC;AAAA,IACA;AAAA,IACA,CAAC;AAAA,EACH;AACA,SAAO,IAAI,IAAI,MAAM;AACvB;AAEO,SAAS,kBAAkB,SAAiB,QAAgB,QAA2B;AAC5F,2BAAyB,sBAAsB,SAAS,MAAM,GAAG,wBAAwB,MAAM,KAAK,MAAM,CAAC;AAC7G;AAGO,SAAS,8BAAoC;AAClD,2BAAyB,yBAAyB;AACpD;",
6
+ "names": []
7
+ }
@@ -12,22 +12,11 @@ import { PortalEmptyState } from "@open-mercato/ui/portal/components/PortalEmpty
12
12
  import { usePortalDashboardWidgets } from "@open-mercato/ui/portal/hooks/usePortalDashboardWidgets";
13
13
  import { InjectionSpot } from "@open-mercato/ui/backend/injection/InjectionSpot";
14
14
  import { PortalInjectionSpots } from "@open-mercato/ui/backend/injection/spotIds";
15
- const HIDDEN_WIDGETS_KEY = "om:portal:dashboard:hidden";
16
- function loadHiddenWidgets() {
17
- try {
18
- const raw = localStorage.getItem(HIDDEN_WIDGETS_KEY);
19
- if (!raw) return /* @__PURE__ */ new Set();
20
- return new Set(JSON.parse(raw));
21
- } catch {
22
- return /* @__PURE__ */ new Set();
23
- }
24
- }
25
- function saveHiddenWidgets(hidden) {
26
- try {
27
- localStorage.setItem(HIDDEN_WIDGETS_KEY, JSON.stringify(Array.from(hidden)));
28
- } catch {
29
- }
30
- }
15
+ import {
16
+ loadHiddenWidgets,
17
+ saveHiddenWidgets,
18
+ clearLegacyHiddenWidgetsKey
19
+ } from "./hiddenWidgetsStorage.js";
31
20
  function WidgetIcon({ className }) {
32
21
  return /* @__PURE__ */ jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", className, children: [
33
22
  /* @__PURE__ */ jsx("rect", { width: "7", height: "9", x: "3", y: "3", rx: "1" }),
@@ -42,14 +31,24 @@ function PortalDashboardPage({ params }) {
42
31
  const { auth } = usePortalContext();
43
32
  const { user, loading } = auth;
44
33
  const [editing, setEditing] = useState(false);
45
- const [hiddenWidgets, setHiddenWidgets] = useState(() => loadHiddenWidgets());
34
+ const [hiddenWidgetsScopeKey, setHiddenWidgetsScopeKey] = useState(null);
35
+ const [hiddenWidgets, setHiddenWidgets] = useState(() => /* @__PURE__ */ new Set());
46
36
  const { widgets: dashboardWidgets, isLoading: widgetsLoading } = usePortalDashboardWidgets("portal:dashboard:sections");
37
+ const activeHiddenWidgetsScopeKey = user ? `${params.orgSlug}:${user.id}` : null;
38
+ if (activeHiddenWidgetsScopeKey !== hiddenWidgetsScopeKey) {
39
+ setHiddenWidgetsScopeKey(activeHiddenWidgetsScopeKey);
40
+ setHiddenWidgets(user ? loadHiddenWidgets(params.orgSlug, user.id) : /* @__PURE__ */ new Set());
41
+ }
47
42
  useEffect(() => {
48
43
  if (!loading && !user) {
49
44
  router.replace(`/${params.orgSlug}/portal/login`);
50
45
  }
51
46
  }, [loading, user, router, params.orgSlug]);
47
+ useEffect(() => {
48
+ clearLegacyHiddenWidgetsKey();
49
+ }, []);
52
50
  const toggleWidget = useCallback((widgetId) => {
51
+ if (!user) return;
53
52
  setHiddenWidgets((prev) => {
54
53
  const next = new Set(prev);
55
54
  if (next.has(widgetId)) {
@@ -57,10 +56,10 @@ function PortalDashboardPage({ params }) {
57
56
  } else {
58
57
  next.add(widgetId);
59
58
  }
60
- saveHiddenWidgets(next);
59
+ saveHiddenWidgets(params.orgSlug, user.id, next);
61
60
  return next;
62
61
  });
63
- }, []);
62
+ }, [params.orgSlug, user?.id]);
64
63
  const visibleWidgets = useMemo(
65
64
  () => dashboardWidgets.filter((w) => !hiddenWidgets.has(w.metadata.id)),
66
65
  [dashboardWidgets, hiddenWidgets]
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../../src/modules/portal/frontend/%5BorgSlug%5D/portal/dashboard/page.tsx"],
4
- "sourcesContent": ["\"use client\"\nimport React, { useEffect, useMemo, useState, useCallback } from 'react'\nimport { useRouter } from 'next/navigation'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { usePortalContext } from '@open-mercato/ui/portal/PortalContext'\nimport { PortalPageHeader } from '@open-mercato/ui/portal/components/PortalPageHeader'\nimport { PortalCard, PortalCardHeader } from '@open-mercato/ui/portal/components/PortalCard'\nimport { PortalEmptyState } from '@open-mercato/ui/portal/components/PortalEmptyState'\nimport { usePortalDashboardWidgets } from '@open-mercato/ui/portal/hooks/usePortalDashboardWidgets'\nimport { InjectionSpot } from '@open-mercato/ui/backend/injection/InjectionSpot'\nimport { PortalInjectionSpots } from '@open-mercato/ui/backend/injection/spotIds'\n\ntype Props = { params: { orgSlug: string } }\n\nconst HIDDEN_WIDGETS_KEY = 'om:portal:dashboard:hidden'\n\nfunction loadHiddenWidgets(): Set<string> {\n try {\n const raw = localStorage.getItem(HIDDEN_WIDGETS_KEY)\n if (!raw) return new Set()\n return new Set(JSON.parse(raw))\n } catch {\n return new Set()\n }\n}\n\nfunction saveHiddenWidgets(hidden: Set<string>) {\n try {\n localStorage.setItem(HIDDEN_WIDGETS_KEY, JSON.stringify(Array.from(hidden)))\n } catch {\n // best effort\n }\n}\n\nfunction WidgetIcon({ className }: { className?: string }) {\n return (\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" className={className}>\n <rect width=\"7\" height=\"9\" x=\"3\" y=\"3\" rx=\"1\" /><rect width=\"7\" height=\"5\" x=\"14\" y=\"3\" rx=\"1\" /><rect width=\"7\" height=\"5\" x=\"14\" y=\"12\" rx=\"1\" /><rect width=\"7\" height=\"9\" x=\"3\" y=\"16\" rx=\"1\" />\n </svg>\n )\n}\n\nexport default function PortalDashboardPage({ params }: Props) {\n const t = useT()\n const router = useRouter()\n const { auth } = usePortalContext()\n const { user, loading } = auth\n\n const [editing, setEditing] = useState(false)\n const [hiddenWidgets, setHiddenWidgets] = useState<Set<string>>(() => loadHiddenWidgets())\n\n const { widgets: dashboardWidgets, isLoading: widgetsLoading } = usePortalDashboardWidgets('portal:dashboard:sections' as any)\n\n useEffect(() => {\n if (!loading && !user) {\n router.replace(`/${params.orgSlug}/portal/login`)\n }\n }, [loading, user, router, params.orgSlug])\n\n const toggleWidget = useCallback((widgetId: string) => {\n setHiddenWidgets((prev) => {\n const next = new Set(prev)\n if (next.has(widgetId)) {\n next.delete(widgetId)\n } else {\n next.add(widgetId)\n }\n saveHiddenWidgets(next)\n return next\n })\n }, [])\n\n const visibleWidgets = useMemo(\n () => dashboardWidgets.filter((w) => !hiddenWidgets.has(w.metadata.id)),\n [dashboardWidgets, hiddenWidgets],\n )\n\n const injectionContext = useMemo(\n () => ({ orgSlug: params.orgSlug, user, roles: auth.roles, resolvedFeatures: auth.resolvedFeatures }),\n [params.orgSlug, user, auth.roles, auth.resolvedFeatures],\n )\n\n if (loading) {\n return <div className=\"flex items-center justify-center py-20\"><Spinner /></div>\n }\n\n if (!user) return null\n\n return (\n <div className=\"flex flex-col gap-8\">\n <PortalPageHeader\n label={t('portal.dashboard.title', 'Dashboard')}\n title={t('portal.dashboard.welcome', { name: user.displayName })}\n action={\n dashboardWidgets.length > 0 ? (\n <Button\n type=\"button\"\n variant={editing ? 'default' : 'outline'}\n size=\"sm\"\n className=\"rounded-lg text-sm\"\n onClick={() => setEditing((prev) => !prev)}\n >\n {editing ? t('portal.dashboard.done', 'Done') : t('portal.dashboard.customize', 'Customize')}\n </Button>\n ) : null\n }\n />\n\n <InjectionSpot spotId={PortalInjectionSpots.pageBefore('dashboard')} context={injectionContext} />\n\n {editing && dashboardWidgets.length > 0 ? (\n <PortalCard>\n <PortalCardHeader\n label={t('portal.dashboard.customize', 'Customize')}\n title={t('portal.dashboard.widgets', 'Dashboard Widgets')}\n />\n <div className=\"flex flex-wrap gap-2\">\n {dashboardWidgets.map((widget) => {\n const isHidden = hiddenWidgets.has(widget.metadata.id)\n return (\n <Button key={widget.metadata.id} type=\"button\" variant={isHidden ? 'outline' : 'default'} size=\"sm\" className=\"rounded-lg text-sm\" onClick={() => toggleWidget(widget.metadata.id)}>\n {widget.metadata.title || widget.metadata.id}\n </Button>\n )\n })}\n </div>\n </PortalCard>\n ) : null}\n\n {visibleWidgets.length > 0 ? (\n <div className=\"grid gap-5 md:grid-cols-2 xl:grid-cols-3\">\n {visibleWidgets.map((widget) => {\n const WidgetComponent = widget.Widget\n if (!WidgetComponent) return null\n return (\n <PortalCard key={widget.metadata.id}>\n <PortalCardHeader title={widget.metadata.title || widget.metadata.id} />\n <WidgetComponent context={{ orgSlug: params.orgSlug, user, roles: auth.roles, resolvedFeatures: auth.resolvedFeatures }} />\n </PortalCard>\n )\n })}\n </div>\n ) : null}\n\n {dashboardWidgets.length === 0 && !widgetsLoading ? (\n <PortalEmptyState\n icon={<WidgetIcon className=\"size-5\" />}\n title={t('portal.dashboard.emptyWidgets', 'No dashboard widgets yet')}\n description=\"Modules can inject widgets into this dashboard via the portal:dashboard:sections injection spot.\"\n />\n ) : null}\n\n <InjectionSpot spotId={PortalInjectionSpots.pageAfter('dashboard')} context={injectionContext} />\n </div>\n )\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\nimport React, { useEffect, useMemo, useState, useCallback } from 'react'\nimport { useRouter } from 'next/navigation'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { usePortalContext } from '@open-mercato/ui/portal/PortalContext'\nimport { PortalPageHeader } from '@open-mercato/ui/portal/components/PortalPageHeader'\nimport { PortalCard, PortalCardHeader } from '@open-mercato/ui/portal/components/PortalCard'\nimport { PortalEmptyState } from '@open-mercato/ui/portal/components/PortalEmptyState'\nimport { usePortalDashboardWidgets } from '@open-mercato/ui/portal/hooks/usePortalDashboardWidgets'\nimport { InjectionSpot } from '@open-mercato/ui/backend/injection/InjectionSpot'\nimport { PortalInjectionSpots } from '@open-mercato/ui/backend/injection/spotIds'\nimport {\n loadHiddenWidgets,\n saveHiddenWidgets,\n clearLegacyHiddenWidgetsKey,\n} from './hiddenWidgetsStorage'\n\ntype Props = { params: { orgSlug: string } }\n\nfunction WidgetIcon({ className }: { className?: string }) {\n return (\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" className={className}>\n <rect width=\"7\" height=\"9\" x=\"3\" y=\"3\" rx=\"1\" /><rect width=\"7\" height=\"5\" x=\"14\" y=\"3\" rx=\"1\" /><rect width=\"7\" height=\"5\" x=\"14\" y=\"12\" rx=\"1\" /><rect width=\"7\" height=\"9\" x=\"3\" y=\"16\" rx=\"1\" />\n </svg>\n )\n}\n\nexport default function PortalDashboardPage({ params }: Props) {\n const t = useT()\n const router = useRouter()\n const { auth } = usePortalContext()\n const { user, loading } = auth\n\n const [editing, setEditing] = useState(false)\n const [hiddenWidgetsScopeKey, setHiddenWidgetsScopeKey] = useState<string | null>(null)\n const [hiddenWidgets, setHiddenWidgets] = useState<Set<string>>(() => new Set())\n\n const { widgets: dashboardWidgets, isLoading: widgetsLoading } = usePortalDashboardWidgets('portal:dashboard:sections' as any)\n\n // Recompute synchronously during render (not in an effect) so a freshly\n // resolved `user` never paints a frame with the previous scope's hidden set.\n const activeHiddenWidgetsScopeKey = user ? `${params.orgSlug}:${user.id}` : null\n if (activeHiddenWidgetsScopeKey !== hiddenWidgetsScopeKey) {\n setHiddenWidgetsScopeKey(activeHiddenWidgetsScopeKey)\n setHiddenWidgets(user ? loadHiddenWidgets(params.orgSlug, user.id) : new Set())\n }\n\n useEffect(() => {\n if (!loading && !user) {\n router.replace(`/${params.orgSlug}/portal/login`)\n }\n }, [loading, user, router, params.orgSlug])\n\n useEffect(() => {\n clearLegacyHiddenWidgetsKey()\n }, [])\n\n const toggleWidget = useCallback((widgetId: string) => {\n if (!user) return\n setHiddenWidgets((prev) => {\n const next = new Set(prev)\n if (next.has(widgetId)) {\n next.delete(widgetId)\n } else {\n next.add(widgetId)\n }\n saveHiddenWidgets(params.orgSlug, user.id, next)\n return next\n })\n }, [params.orgSlug, user?.id])\n\n const visibleWidgets = useMemo(\n () => dashboardWidgets.filter((w) => !hiddenWidgets.has(w.metadata.id)),\n [dashboardWidgets, hiddenWidgets],\n )\n\n const injectionContext = useMemo(\n () => ({ orgSlug: params.orgSlug, user, roles: auth.roles, resolvedFeatures: auth.resolvedFeatures }),\n [params.orgSlug, user, auth.roles, auth.resolvedFeatures],\n )\n\n if (loading) {\n return <div className=\"flex items-center justify-center py-20\"><Spinner /></div>\n }\n\n if (!user) return null\n\n return (\n <div className=\"flex flex-col gap-8\">\n <PortalPageHeader\n label={t('portal.dashboard.title', 'Dashboard')}\n title={t('portal.dashboard.welcome', { name: user.displayName })}\n action={\n dashboardWidgets.length > 0 ? (\n <Button\n type=\"button\"\n variant={editing ? 'default' : 'outline'}\n size=\"sm\"\n className=\"rounded-lg text-sm\"\n onClick={() => setEditing((prev) => !prev)}\n >\n {editing ? t('portal.dashboard.done', 'Done') : t('portal.dashboard.customize', 'Customize')}\n </Button>\n ) : null\n }\n />\n\n <InjectionSpot spotId={PortalInjectionSpots.pageBefore('dashboard')} context={injectionContext} />\n\n {editing && dashboardWidgets.length > 0 ? (\n <PortalCard>\n <PortalCardHeader\n label={t('portal.dashboard.customize', 'Customize')}\n title={t('portal.dashboard.widgets', 'Dashboard Widgets')}\n />\n <div className=\"flex flex-wrap gap-2\">\n {dashboardWidgets.map((widget) => {\n const isHidden = hiddenWidgets.has(widget.metadata.id)\n return (\n <Button key={widget.metadata.id} type=\"button\" variant={isHidden ? 'outline' : 'default'} size=\"sm\" className=\"rounded-lg text-sm\" onClick={() => toggleWidget(widget.metadata.id)}>\n {widget.metadata.title || widget.metadata.id}\n </Button>\n )\n })}\n </div>\n </PortalCard>\n ) : null}\n\n {visibleWidgets.length > 0 ? (\n <div className=\"grid gap-5 md:grid-cols-2 xl:grid-cols-3\">\n {visibleWidgets.map((widget) => {\n const WidgetComponent = widget.Widget\n if (!WidgetComponent) return null\n return (\n <PortalCard key={widget.metadata.id}>\n <PortalCardHeader title={widget.metadata.title || widget.metadata.id} />\n <WidgetComponent context={{ orgSlug: params.orgSlug, user, roles: auth.roles, resolvedFeatures: auth.resolvedFeatures }} />\n </PortalCard>\n )\n })}\n </div>\n ) : null}\n\n {dashboardWidgets.length === 0 && !widgetsLoading ? (\n <PortalEmptyState\n icon={<WidgetIcon className=\"size-5\" />}\n title={t('portal.dashboard.emptyWidgets', 'No dashboard widgets yet')}\n description=\"Modules can inject widgets into this dashboard via the portal:dashboard:sections injection spot.\"\n />\n ) : null}\n\n <InjectionSpot spotId={PortalInjectionSpots.pageAfter('dashboard')} context={injectionContext} />\n </div>\n )\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/core",
3
- "version": "0.6.6-develop.6286.1.d985c5c374",
3
+ "version": "0.6.6-develop.6290.1.4bb5a8ba3f",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -246,16 +246,16 @@
246
246
  "zod": "^4.4.3"
247
247
  },
248
248
  "peerDependencies": {
249
- "@open-mercato/ai-assistant": "0.6.6-develop.6286.1.d985c5c374",
250
- "@open-mercato/shared": "0.6.6-develop.6286.1.d985c5c374",
251
- "@open-mercato/ui": "0.6.6-develop.6286.1.d985c5c374",
249
+ "@open-mercato/ai-assistant": "0.6.6-develop.6290.1.4bb5a8ba3f",
250
+ "@open-mercato/shared": "0.6.6-develop.6290.1.4bb5a8ba3f",
251
+ "@open-mercato/ui": "0.6.6-develop.6290.1.4bb5a8ba3f",
252
252
  "react": "^19.0.0",
253
253
  "react-dom": "^19.0.0"
254
254
  },
255
255
  "devDependencies": {
256
- "@open-mercato/ai-assistant": "0.6.6-develop.6286.1.d985c5c374",
257
- "@open-mercato/shared": "0.6.6-develop.6286.1.d985c5c374",
258
- "@open-mercato/ui": "0.6.6-develop.6286.1.d985c5c374",
256
+ "@open-mercato/ai-assistant": "0.6.6-develop.6290.1.4bb5a8ba3f",
257
+ "@open-mercato/shared": "0.6.6-develop.6290.1.4bb5a8ba3f",
258
+ "@open-mercato/ui": "0.6.6-develop.6290.1.4bb5a8ba3f",
259
259
  "@testing-library/dom": "^10.4.1",
260
260
  "@testing-library/jest-dom": "^6.9.1",
261
261
  "@testing-library/react": "^16.3.1",
@@ -2,9 +2,10 @@ import type { JobContext, QueuedJob, WorkerMeta } from '@open-mercato/queue'
2
2
  import type { EntityManager } from '@mikro-orm/postgresql'
3
3
  import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
4
4
  import type { IntegrationStateService } from '../../integrations/lib/state-service'
5
+ import type { ProgressService } from '../../progress/lib/progressService'
5
6
  import type { SyncRunService } from '../lib/sync-run-service'
6
7
  import { SyncSchedule } from '../data/entities'
7
- import { getSyncQueue } from '../lib/queue'
8
+ import { startDataSyncRun } from '../lib/start-run'
8
9
 
9
10
  type ScheduledSyncPayload = {
10
11
  scheduleId: string
@@ -27,6 +28,7 @@ type HandlerContext = JobContext & {
27
28
  export default async function handle(job: QueuedJob<ScheduledSyncPayload>, ctx: HandlerContext): Promise<void> {
28
29
  const em = ctx.resolve<EntityManager>('em')
29
30
  const syncRunService = ctx.resolve<SyncRunService>('dataSyncRunService')
31
+ const progressService = ctx.resolve<ProgressService>('progressService')
30
32
  const integrationStateService = ctx.resolve<IntegrationStateService>('integrationStateService')
31
33
 
32
34
  const schedule = await findOneWithDecryption(
@@ -70,22 +72,19 @@ export default async function handle(job: QueuedJob<ScheduledSyncPayload>, ctx:
70
72
  job.payload.scope,
71
73
  )
72
74
 
73
- const run = await syncRunService.createRun({
74
- integrationId: schedule.integrationId,
75
- entityType: schedule.entityType,
76
- direction: schedule.direction,
77
- cursor,
78
- triggeredBy: 'scheduler',
79
- }, job.payload.scope)
80
-
81
75
  schedule.lastRunAt = new Date()
82
76
  await em.flush()
83
77
 
84
- const queueName = schedule.direction === 'import' ? 'data-sync-import' : 'data-sync-export'
85
- const queue = getSyncQueue(queueName)
86
- await queue.enqueue({
87
- runId: run.id,
88
- batchSize: 100,
78
+ await startDataSyncRun({
79
+ syncRunService,
80
+ progressService,
89
81
  scope: job.payload.scope,
82
+ input: {
83
+ integrationId: schedule.integrationId,
84
+ entityType: schedule.entityType,
85
+ direction: schedule.direction,
86
+ cursor,
87
+ triggeredBy: 'scheduler',
88
+ },
90
89
  })
91
90
  }
@@ -57,21 +57,11 @@ function filterTenantsByStatus(list: TenantRecord[], status: 'all' | 'active' |
57
57
  return list
58
58
  }
59
59
 
60
- async function fetchDirectoryTenants(status: 'all' | 'active' | 'inactive', errorMessage: string): Promise<TenantRecord[]> {
61
- const search = new URLSearchParams()
62
- search.set('page', '1')
63
- search.set('pageSize', '100')
64
- search.set('sortField', 'name')
65
- search.set('sortDir', 'asc')
66
- if (status === 'active') search.set('isActive', 'true')
67
- if (status === 'inactive') search.set('isActive', 'false')
68
- const json = await readApiResultOrThrow<{ items?: unknown[] }>(
69
- `/api/directory/tenants?${search.toString()}`,
70
- undefined,
71
- { errorMessage, allowNullResult: true },
72
- )
73
- const items = Array.isArray(json?.items) ? json.items : []
74
- const normalized = items
60
+ const DIRECTORY_TENANTS_PAGE_SIZE = 100
61
+ const DIRECTORY_TENANTS_MAX_PAGES = 200
62
+
63
+ function normalizeTenantItems(items: unknown[]): TenantRecord[] {
64
+ return items
75
65
  .map((item: unknown): TenantRecord | null => {
76
66
  if (!item || typeof item !== 'object') return null
77
67
  const entry = item as Record<string, unknown>
@@ -84,7 +74,34 @@ async function fetchDirectoryTenants(status: 'all' | 'active' | 'inactive', erro
84
74
  return { id, name, isActive }
85
75
  })
86
76
  .filter((tenant: TenantRecord | null): tenant is TenantRecord => tenant !== null)
87
- return mergeTenantLists(filterTenantsByStatus(normalized, status))
77
+ }
78
+
79
+ async function fetchDirectoryTenants(status: 'all' | 'active' | 'inactive', errorMessage: string): Promise<TenantRecord[]> {
80
+ const collected: TenantRecord[] = []
81
+ let page = 1
82
+ let totalPages = 1
83
+ do {
84
+ const search = new URLSearchParams()
85
+ search.set('page', String(page))
86
+ search.set('pageSize', String(DIRECTORY_TENANTS_PAGE_SIZE))
87
+ search.set('sortField', 'name')
88
+ search.set('sortDir', 'asc')
89
+ if (status === 'active') search.set('isActive', 'true')
90
+ if (status === 'inactive') search.set('isActive', 'false')
91
+ const json = await readApiResultOrThrow<{ items?: unknown[]; totalPages?: unknown }>(
92
+ `/api/directory/tenants?${search.toString()}`,
93
+ undefined,
94
+ { errorMessage, allowNullResult: true },
95
+ )
96
+ const items = Array.isArray(json?.items) ? json.items : []
97
+ collected.push(...normalizeTenantItems(items))
98
+ totalPages = typeof json?.totalPages === 'number' && Number.isFinite(json.totalPages)
99
+ ? Math.max(1, Math.floor(json.totalPages))
100
+ : 1
101
+ if (items.length < DIRECTORY_TENANTS_PAGE_SIZE) break
102
+ page += 1
103
+ } while (page <= totalPages && page <= DIRECTORY_TENANTS_MAX_PAGES)
104
+ return mergeTenantLists(filterTenantsByStatus(collected, status))
88
105
  }
89
106
 
90
107
  async function fetchTenantsFromOrganizationSwitcher(status: 'all' | 'active' | 'inactive', errorMessage: string): Promise<TenantRecord[]> {
@@ -0,0 +1,36 @@
1
+ import {
2
+ readVersionedPreference,
3
+ writeVersionedPreference,
4
+ clearVersionedPreference,
5
+ } from '@open-mercato/shared/lib/browser/versionedPreference'
6
+
7
+ const LEGACY_HIDDEN_WIDGETS_KEY = 'om:portal:dashboard:hidden'
8
+ const HIDDEN_WIDGETS_KEY_PREFIX = 'om:portal:dashboard:hidden:v1:'
9
+ const HIDDEN_WIDGETS_VERSION = 1
10
+
11
+ function buildHiddenWidgetsKey(orgSlug: string, userId: string): string {
12
+ return `${HIDDEN_WIDGETS_KEY_PREFIX}${orgSlug}:${userId}`
13
+ }
14
+
15
+ function isStringArray(value: unknown): value is string[] {
16
+ return Array.isArray(value) && value.every((id) => typeof id === 'string')
17
+ }
18
+
19
+ export function loadHiddenWidgets(orgSlug: string, userId: string): Set<string> {
20
+ const hidden = readVersionedPreference<string[]>(
21
+ buildHiddenWidgetsKey(orgSlug, userId),
22
+ HIDDEN_WIDGETS_VERSION,
23
+ isStringArray,
24
+ [],
25
+ )
26
+ return new Set(hidden)
27
+ }
28
+
29
+ export function saveHiddenWidgets(orgSlug: string, userId: string, hidden: Set<string>): void {
30
+ writeVersionedPreference(buildHiddenWidgetsKey(orgSlug, userId), HIDDEN_WIDGETS_VERSION, Array.from(hidden))
31
+ }
32
+
33
+ /** Drops the pre-scoping global key so stale preferences stop leaking across org/user contexts. */
34
+ export function clearLegacyHiddenWidgetsKey(): void {
35
+ clearVersionedPreference(LEGACY_HIDDEN_WIDGETS_KEY)
36
+ }
@@ -11,29 +11,14 @@ import { PortalEmptyState } from '@open-mercato/ui/portal/components/PortalEmpty
11
11
  import { usePortalDashboardWidgets } from '@open-mercato/ui/portal/hooks/usePortalDashboardWidgets'
12
12
  import { InjectionSpot } from '@open-mercato/ui/backend/injection/InjectionSpot'
13
13
  import { PortalInjectionSpots } from '@open-mercato/ui/backend/injection/spotIds'
14
+ import {
15
+ loadHiddenWidgets,
16
+ saveHiddenWidgets,
17
+ clearLegacyHiddenWidgetsKey,
18
+ } from './hiddenWidgetsStorage'
14
19
 
15
20
  type Props = { params: { orgSlug: string } }
16
21
 
17
- const HIDDEN_WIDGETS_KEY = 'om:portal:dashboard:hidden'
18
-
19
- function loadHiddenWidgets(): Set<string> {
20
- try {
21
- const raw = localStorage.getItem(HIDDEN_WIDGETS_KEY)
22
- if (!raw) return new Set()
23
- return new Set(JSON.parse(raw))
24
- } catch {
25
- return new Set()
26
- }
27
- }
28
-
29
- function saveHiddenWidgets(hidden: Set<string>) {
30
- try {
31
- localStorage.setItem(HIDDEN_WIDGETS_KEY, JSON.stringify(Array.from(hidden)))
32
- } catch {
33
- // best effort
34
- }
35
- }
36
-
37
22
  function WidgetIcon({ className }: { className?: string }) {
38
23
  return (
39
24
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" className={className}>
@@ -49,17 +34,31 @@ export default function PortalDashboardPage({ params }: Props) {
49
34
  const { user, loading } = auth
50
35
 
51
36
  const [editing, setEditing] = useState(false)
52
- const [hiddenWidgets, setHiddenWidgets] = useState<Set<string>>(() => loadHiddenWidgets())
37
+ const [hiddenWidgetsScopeKey, setHiddenWidgetsScopeKey] = useState<string | null>(null)
38
+ const [hiddenWidgets, setHiddenWidgets] = useState<Set<string>>(() => new Set())
53
39
 
54
40
  const { widgets: dashboardWidgets, isLoading: widgetsLoading } = usePortalDashboardWidgets('portal:dashboard:sections' as any)
55
41
 
42
+ // Recompute synchronously during render (not in an effect) so a freshly
43
+ // resolved `user` never paints a frame with the previous scope's hidden set.
44
+ const activeHiddenWidgetsScopeKey = user ? `${params.orgSlug}:${user.id}` : null
45
+ if (activeHiddenWidgetsScopeKey !== hiddenWidgetsScopeKey) {
46
+ setHiddenWidgetsScopeKey(activeHiddenWidgetsScopeKey)
47
+ setHiddenWidgets(user ? loadHiddenWidgets(params.orgSlug, user.id) : new Set())
48
+ }
49
+
56
50
  useEffect(() => {
57
51
  if (!loading && !user) {
58
52
  router.replace(`/${params.orgSlug}/portal/login`)
59
53
  }
60
54
  }, [loading, user, router, params.orgSlug])
61
55
 
56
+ useEffect(() => {
57
+ clearLegacyHiddenWidgetsKey()
58
+ }, [])
59
+
62
60
  const toggleWidget = useCallback((widgetId: string) => {
61
+ if (!user) return
63
62
  setHiddenWidgets((prev) => {
64
63
  const next = new Set(prev)
65
64
  if (next.has(widgetId)) {
@@ -67,10 +66,10 @@ export default function PortalDashboardPage({ params }: Props) {
67
66
  } else {
68
67
  next.add(widgetId)
69
68
  }
70
- saveHiddenWidgets(next)
69
+ saveHiddenWidgets(params.orgSlug, user.id, next)
71
70
  return next
72
71
  })
73
- }, [])
72
+ }, [params.orgSlug, user?.id])
74
73
 
75
74
  const visibleWidgets = useMemo(
76
75
  () => dashboardWidgets.filter((w) => !hiddenWidgets.has(w.metadata.id)),