@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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/modules/data_sync/workers/sync-scheduled.js +13 -14
- package/dist/modules/data_sync/workers/sync-scheduled.js.map +2 -2
- package/dist/modules/directory/components/TenantSelect.js +29 -16
- package/dist/modules/directory/components/TenantSelect.js.map +2 -2
- package/dist/modules/portal/frontend/[orgSlug]/portal/dashboard/hiddenWidgetsStorage.js +35 -0
- package/dist/modules/portal/frontend/[orgSlug]/portal/dashboard/hiddenWidgetsStorage.js.map +7 -0
- package/dist/modules/portal/frontend/[orgSlug]/portal/dashboard/page.js +18 -19
- package/dist/modules/portal/frontend/[orgSlug]/portal/dashboard/page.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/data_sync/workers/sync-scheduled.ts +13 -14
- package/src/modules/directory/components/TenantSelect.tsx +33 -16
- package/src/modules/portal/frontend/[orgSlug]/portal/dashboard/hiddenWidgetsStorage.ts +36 -0
- package/src/modules/portal/frontend/[orgSlug]/portal/dashboard/page.tsx +22 -23
package/.turbo/turbo-build.log
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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 {
|
|
5
|
-
"mappings": "AAEA,SAAS,6BAA6B;
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
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
|
|
5
|
-
"mappings": ";
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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 [
|
|
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'\
|
|
5
|
-
"mappings": ";
|
|
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.
|
|
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.
|
|
250
|
-
"@open-mercato/shared": "0.6.6-develop.
|
|
251
|
-
"@open-mercato/ui": "0.6.6-develop.
|
|
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.
|
|
257
|
-
"@open-mercato/shared": "0.6.6-develop.
|
|
258
|
-
"@open-mercato/ui": "0.6.6-develop.
|
|
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 {
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
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 [
|
|
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)),
|