@open-mercato/core 0.5.1-develop.2975.ccbadc8198 → 0.5.1-develop.2996.ce62fd491c
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 +2 -2
- package/dist/generated/entities/sidebar_variant/index.js +25 -0
- package/dist/generated/entities/sidebar_variant/index.js.map +7 -0
- package/dist/generated/entities.ids.generated.js +1 -0
- package/dist/generated/entities.ids.generated.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +13 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/helpers/integration/authUi.js +1 -1
- package/dist/helpers/integration/authUi.js.map +2 -2
- package/dist/modules/audit_logs/services/actionLogService.js +4 -5
- package/dist/modules/audit_logs/services/actionLogService.js.map +2 -2
- package/dist/modules/auth/api/sidebar/preferences/route.js +224 -35
- package/dist/modules/auth/api/sidebar/preferences/route.js.map +3 -3
- package/dist/modules/auth/api/sidebar/variants/[id]/route.js +161 -0
- package/dist/modules/auth/api/sidebar/variants/[id]/route.js.map +7 -0
- package/dist/modules/auth/api/sidebar/variants/route.js +142 -0
- package/dist/modules/auth/api/sidebar/variants/route.js.map +7 -0
- package/dist/modules/auth/backend/sidebar-customization/page.js +16 -0
- package/dist/modules/auth/backend/sidebar-customization/page.js.map +7 -0
- package/dist/modules/auth/backend/sidebar-customization/page.meta.js +28 -0
- package/dist/modules/auth/backend/sidebar-customization/page.meta.js.map +7 -0
- package/dist/modules/auth/data/entities.js +45 -4
- package/dist/modules/auth/data/entities.js.map +2 -2
- package/dist/modules/auth/data/validators.js +63 -1
- package/dist/modules/auth/data/validators.js.map +2 -2
- package/dist/modules/auth/migrations/Migration20260427081815.js +15 -0
- package/dist/modules/auth/migrations/Migration20260427081815.js.map +7 -0
- package/dist/modules/auth/migrations/Migration20260427124900.js +15 -0
- package/dist/modules/auth/migrations/Migration20260427124900.js.map +7 -0
- package/dist/modules/auth/migrations/Migration20260427143311.js +72 -0
- package/dist/modules/auth/migrations/Migration20260427143311.js.map +7 -0
- package/dist/modules/auth/services/sidebarPreferencesService.js +176 -16
- package/dist/modules/auth/services/sidebarPreferencesService.js.map +2 -2
- package/dist/modules/customers/backend/customers/companies/[id]/page.js +3 -1
- package/dist/modules/customers/backend/customers/companies/[id]/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js +4 -2
- package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/people-v2/[id]/page.js +8 -3
- package/dist/modules/customers/backend/customers/people-v2/[id]/page.js.map +2 -2
- package/dist/modules/customers/components/detail/CompanyPeopleSection.js +3 -2
- package/dist/modules/customers/components/detail/CompanyPeopleSection.js.map +2 -2
- package/dist/modules/customers/components/formConfig.js +3 -3
- package/dist/modules/customers/components/formConfig.js.map +2 -2
- package/dist/modules/customers/lib/displayName.js +12 -0
- package/dist/modules/customers/lib/displayName.js.map +2 -2
- package/dist/modules/entities/cli.js +5 -6
- package/dist/modules/entities/cli.js.map +2 -2
- package/dist/modules/portal/frontend/[orgSlug]/portal/reset-password/page.js +124 -0
- package/dist/modules/portal/frontend/[orgSlug]/portal/reset-password/page.js.map +7 -0
- package/dist/modules/portal/frontend/[orgSlug]/portal/reset-password/page.meta.js +11 -0
- package/dist/modules/portal/frontend/[orgSlug]/portal/reset-password/page.meta.js.map +7 -0
- package/generated/entities/sidebar_variant/index.ts +11 -0
- package/generated/entities.ids.generated.ts +1 -0
- package/generated/entity-fields-registry.ts +13 -0
- package/package.json +6 -6
- package/src/helpers/integration/authUi.ts +1 -1
- package/src/modules/audit_logs/services/actionLogService.ts +5 -6
- package/src/modules/auth/api/sidebar/preferences/route.ts +266 -34
- package/src/modules/auth/api/sidebar/variants/[id]/route.ts +183 -0
- package/src/modules/auth/api/sidebar/variants/route.ts +157 -0
- package/src/modules/auth/backend/sidebar-customization/page.meta.ts +34 -0
- package/src/modules/auth/backend/sidebar-customization/page.tsx +17 -0
- package/src/modules/auth/data/entities.ts +48 -2
- package/src/modules/auth/data/validators.ts +70 -0
- package/src/modules/auth/migrations/.snapshot-open-mercato.json +790 -71
- package/src/modules/auth/migrations/Migration20260427081815.ts +16 -0
- package/src/modules/auth/migrations/Migration20260427124900.ts +19 -0
- package/src/modules/auth/migrations/Migration20260427143311.ts +83 -0
- package/src/modules/auth/services/sidebarPreferencesService.ts +243 -18
- package/src/modules/customers/backend/customers/companies/[id]/page.tsx +5 -4
- package/src/modules/customers/backend/customers/companies-v2/[id]/page.tsx +6 -5
- package/src/modules/customers/backend/customers/people-v2/[id]/page.tsx +13 -9
- package/src/modules/customers/components/detail/CompanyPeopleSection.tsx +3 -2
- package/src/modules/customers/components/formConfig.tsx +3 -3
- package/src/modules/customers/lib/displayName.ts +21 -0
- package/src/modules/entities/cli.ts +5 -6
- package/src/modules/portal/frontend/[orgSlug]/portal/reset-password/page.meta.ts +9 -0
- package/src/modules/portal/frontend/[orgSlug]/portal/reset-password/page.tsx +168 -0
- package/src/modules/portal/i18n/de.json +20 -0
- package/src/modules/portal/i18n/en.json +20 -0
- package/src/modules/portal/i18n/es.json +20 -0
- package/src/modules/portal/i18n/pl.json +20 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Migration } from '@mikro-orm/migrations';
|
|
2
|
+
|
|
3
|
+
export class Migration20260427081815 extends Migration {
|
|
4
|
+
|
|
5
|
+
override up(): void | Promise<void> {
|
|
6
|
+
this.addSql(`create table "sidebar_variants" ("id" uuid not null default gen_random_uuid(), "user_id" uuid not null, "tenant_id" uuid null, "organization_id" uuid null, "locale" text not null, "name" text not null, "settings_json" jsonb null, "is_active" boolean not null default false, "created_at" timestamptz not null, "updated_at" timestamptz null, "deleted_at" timestamptz null, primary key ("id"));`);
|
|
7
|
+
this.addSql(`alter table "sidebar_variants" add constraint "sidebar_variants_user_id_tenant_id_locale_name_unique" unique ("user_id", "tenant_id", "locale", "name");`);
|
|
8
|
+
|
|
9
|
+
this.addSql(`alter table "sidebar_variants" add constraint "sidebar_variants_user_id_foreign" foreign key ("user_id") references "users" ("id");`);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
override down(): void | Promise<void> {
|
|
13
|
+
this.addSql(`drop table if exists "sidebar_variants" cascade;`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Migration } from '@mikro-orm/migrations';
|
|
2
|
+
|
|
3
|
+
// Replace the full unique constraint on sidebar_variants with a partial unique index
|
|
4
|
+
// scoped to live rows (deleted_at IS NULL). Without this, soft-deleting a variant and
|
|
5
|
+
// then trying to recreate one with the same name throws a duplicate-key error because
|
|
6
|
+
// the regular unique constraint considers tombstoned rows.
|
|
7
|
+
export class Migration20260427124900 extends Migration {
|
|
8
|
+
|
|
9
|
+
override up(): void | Promise<void> {
|
|
10
|
+
this.addSql(`alter table "sidebar_variants" drop constraint if exists "sidebar_variants_user_id_tenant_id_locale_name_unique";`);
|
|
11
|
+
this.addSql(`create unique index if not exists "sidebar_variants_active_name_unique_idx" on "sidebar_variants" ("user_id", "tenant_id", "locale", "name") where "deleted_at" is null;`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
override down(): void | Promise<void> {
|
|
15
|
+
this.addSql(`drop index if exists "sidebar_variants_active_name_unique_idx";`);
|
|
16
|
+
this.addSql(`alter table "sidebar_variants" add constraint "sidebar_variants_user_id_tenant_id_locale_name_unique" unique ("user_id", "tenant_id", "locale", "name");`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { Migration } from '@mikro-orm/migrations';
|
|
2
|
+
|
|
3
|
+
// Cross-locale sidebar customization. Strip `locale` from uniqueness scope so the same
|
|
4
|
+
// variant/preference applies regardless of the user's active language. Soft-delete any
|
|
5
|
+
// historical duplicates first (keep the most-recently-updated row per scope) so the new
|
|
6
|
+
// constraints can be created without violations.
|
|
7
|
+
export class Migration20260427143311 extends Migration {
|
|
8
|
+
|
|
9
|
+
override up(): void | Promise<void> {
|
|
10
|
+
// --- sidebar_variants: collapse (user_id, tenant_id, name) across locales ---------
|
|
11
|
+
this.addSql(`
|
|
12
|
+
with ranked as (
|
|
13
|
+
select id,
|
|
14
|
+
row_number() over (
|
|
15
|
+
partition by user_id, tenant_id, name
|
|
16
|
+
order by coalesce(updated_at, created_at) desc, created_at desc, id desc
|
|
17
|
+
) as rn
|
|
18
|
+
from sidebar_variants
|
|
19
|
+
where deleted_at is null
|
|
20
|
+
)
|
|
21
|
+
update sidebar_variants
|
|
22
|
+
set deleted_at = now()
|
|
23
|
+
from ranked
|
|
24
|
+
where sidebar_variants.id = ranked.id and ranked.rn > 1;
|
|
25
|
+
`);
|
|
26
|
+
this.addSql(`drop index if exists "sidebar_variants_active_name_unique_idx";`);
|
|
27
|
+
this.addSql(`alter table "sidebar_variants" drop constraint if exists "sidebar_variants_user_id_tenant_id_locale_name_unique";`);
|
|
28
|
+
this.addSql(`create unique index if not exists "sidebar_variants_active_name_unique_idx" on "sidebar_variants" ("user_id", "tenant_id", "name") where "deleted_at" is null;`);
|
|
29
|
+
|
|
30
|
+
// --- user_sidebar_preferences: collapse (user_id, tenant_id, organization_id) -----
|
|
31
|
+
this.addSql(`
|
|
32
|
+
with ranked as (
|
|
33
|
+
select id,
|
|
34
|
+
row_number() over (
|
|
35
|
+
partition by user_id, tenant_id, organization_id
|
|
36
|
+
order by coalesce(updated_at, created_at) desc, created_at desc, id desc
|
|
37
|
+
) as rn
|
|
38
|
+
from user_sidebar_preferences
|
|
39
|
+
where deleted_at is null
|
|
40
|
+
)
|
|
41
|
+
update user_sidebar_preferences
|
|
42
|
+
set deleted_at = now()
|
|
43
|
+
from ranked
|
|
44
|
+
where user_sidebar_preferences.id = ranked.id and ranked.rn > 1;
|
|
45
|
+
`);
|
|
46
|
+
this.addSql(`alter table "user_sidebar_preferences" drop constraint if exists "user_sidebar_preferences_user_id_tenant_id_organi_35248_unique";`);
|
|
47
|
+
this.addSql(`alter table "user_sidebar_preferences" drop constraint if exists "user_sidebar_preferences_user_id_tenant_id_organi_f3f2f_unique";`);
|
|
48
|
+
this.addSql(`drop index if exists "user_sidebar_preferences_active_unique_idx";`);
|
|
49
|
+
this.addSql(`create unique index if not exists "user_sidebar_preferences_active_unique_idx" on "user_sidebar_preferences" ("user_id", "tenant_id", "organization_id") where "deleted_at" is null;`);
|
|
50
|
+
|
|
51
|
+
// --- role_sidebar_preferences: collapse (role_id, tenant_id) ----------------------
|
|
52
|
+
this.addSql(`
|
|
53
|
+
with ranked as (
|
|
54
|
+
select id,
|
|
55
|
+
row_number() over (
|
|
56
|
+
partition by role_id, tenant_id
|
|
57
|
+
order by coalesce(updated_at, created_at) desc, created_at desc, id desc
|
|
58
|
+
) as rn
|
|
59
|
+
from role_sidebar_preferences
|
|
60
|
+
where deleted_at is null
|
|
61
|
+
)
|
|
62
|
+
update role_sidebar_preferences
|
|
63
|
+
set deleted_at = now()
|
|
64
|
+
from ranked
|
|
65
|
+
where role_sidebar_preferences.id = ranked.id and ranked.rn > 1;
|
|
66
|
+
`);
|
|
67
|
+
this.addSql(`alter table "role_sidebar_preferences" drop constraint if exists "role_sidebar_preferences_role_id_tenant_id_locale_unique";`);
|
|
68
|
+
this.addSql(`drop index if exists "role_sidebar_preferences_active_unique_idx";`);
|
|
69
|
+
this.addSql(`create unique index if not exists "role_sidebar_preferences_active_unique_idx" on "role_sidebar_preferences" ("role_id", "tenant_id") where "deleted_at" is null;`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
override down(): void | Promise<void> {
|
|
73
|
+
this.addSql(`drop index if exists "sidebar_variants_active_name_unique_idx";`);
|
|
74
|
+
this.addSql(`alter table "sidebar_variants" add constraint "sidebar_variants_user_id_tenant_id_locale_name_unique" unique ("user_id", "tenant_id", "locale", "name");`);
|
|
75
|
+
|
|
76
|
+
this.addSql(`drop index if exists "user_sidebar_preferences_active_unique_idx";`);
|
|
77
|
+
this.addSql(`alter table "user_sidebar_preferences" add constraint "user_sidebar_preferences_user_id_tenant_id_organi_35248_unique" unique ("user_id", "tenant_id", "organization_id", "locale");`);
|
|
78
|
+
|
|
79
|
+
this.addSql(`drop index if exists "role_sidebar_preferences_active_unique_idx";`);
|
|
80
|
+
this.addSql(`alter table "role_sidebar_preferences" add constraint "role_sidebar_preferences_role_id_tenant_id_locale_unique" unique ("role_id", "tenant_id", "locale");`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { EntityManager } from '@mikro-orm/postgresql'
|
|
2
|
-
import {
|
|
1
|
+
import { EntityManager, type FilterQuery } from '@mikro-orm/postgresql'
|
|
2
|
+
import { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
3
|
+
import { Role, RoleSidebarPreference, SidebarVariant, User, UserSidebarPreference } from '../data/entities'
|
|
3
4
|
import {
|
|
4
5
|
SIDEBAR_PREFERENCES_VERSION,
|
|
5
6
|
SidebarPreferencesSettings,
|
|
@@ -39,8 +40,16 @@ export async function loadSidebarPreference(
|
|
|
39
40
|
em: EntityManager,
|
|
40
41
|
scope: SidebarPreferenceScope,
|
|
41
42
|
): Promise<SidebarPreferencesSettings> {
|
|
42
|
-
|
|
43
|
-
|
|
43
|
+
// Cross-locale: variants & preferences are scoped per (user, tenant, org) only.
|
|
44
|
+
// The `locale` field on the row is kept for audit / when the row was created.
|
|
45
|
+
const { userId, tenantId, organizationId } = normalizeScope(scope)
|
|
46
|
+
const existing = await findOneWithDecryption(
|
|
47
|
+
em,
|
|
48
|
+
UserSidebarPreference,
|
|
49
|
+
{ user: userId, tenantId, organizationId },
|
|
50
|
+
undefined,
|
|
51
|
+
{ tenantId, organizationId },
|
|
52
|
+
)
|
|
44
53
|
return normalizeSidebarSettings(existing?.settingsJson as SidebarPreferencesSettings | undefined)
|
|
45
54
|
}
|
|
46
55
|
|
|
@@ -54,7 +63,13 @@ export async function saveSidebarPreference(
|
|
|
54
63
|
version: input?.version ?? SIDEBAR_PREFERENCES_VERSION,
|
|
55
64
|
})
|
|
56
65
|
const { userId, tenantId, organizationId, locale } = normalizeScope(scope)
|
|
57
|
-
let pref = await
|
|
66
|
+
let pref = await findOneWithDecryption(
|
|
67
|
+
em,
|
|
68
|
+
UserSidebarPreference,
|
|
69
|
+
{ user: userId, tenantId, organizationId },
|
|
70
|
+
undefined,
|
|
71
|
+
{ tenantId, organizationId },
|
|
72
|
+
)
|
|
58
73
|
if (!pref) {
|
|
59
74
|
pref = em.create(UserSidebarPreference, {
|
|
60
75
|
user: em.getReference(User, userId),
|
|
@@ -73,16 +88,18 @@ export async function saveSidebarPreference(
|
|
|
73
88
|
|
|
74
89
|
export async function loadRoleSidebarPreferences(
|
|
75
90
|
em: EntityManager,
|
|
76
|
-
options: { roleIds: string[]; tenantId?: string | null; locale
|
|
91
|
+
options: { roleIds: string[]; tenantId?: string | null; locale?: string },
|
|
77
92
|
): Promise<Map<string, SidebarPreferencesSettings>> {
|
|
78
93
|
if (!options.roleIds.length) return new Map()
|
|
79
94
|
const tenantId = options.tenantId ?? null
|
|
80
95
|
const tenantFilter = tenantId === null ? null : { $in: [tenantId, null] }
|
|
81
|
-
const prefs = await
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
96
|
+
const prefs = await findWithDecryption(
|
|
97
|
+
em,
|
|
98
|
+
RoleSidebarPreference,
|
|
99
|
+
{ role: { $in: options.roleIds }, tenantId: tenantFilter } as FilterQuery<RoleSidebarPreference>,
|
|
100
|
+
undefined,
|
|
101
|
+
{ tenantId, organizationId: null },
|
|
102
|
+
)
|
|
86
103
|
const map = new Map<string, SidebarPreferencesSettings>()
|
|
87
104
|
for (const pref of prefs) {
|
|
88
105
|
const key = pref.role.id
|
|
@@ -101,16 +118,18 @@ export async function loadRoleSidebarPreferences(
|
|
|
101
118
|
|
|
102
119
|
export async function loadFirstRoleSidebarPreference(
|
|
103
120
|
em: EntityManager,
|
|
104
|
-
options: { roleIds: string[]; tenantId?: string | null; locale
|
|
121
|
+
options: { roleIds: string[]; tenantId?: string | null; locale?: string },
|
|
105
122
|
): Promise<SidebarPreferencesSettings | null> {
|
|
106
123
|
if (!options.roleIds.length) return null
|
|
107
124
|
const tenantId = options.tenantId ?? null
|
|
108
125
|
const tenantFilter = tenantId === null ? null : { $in: [tenantId, null] }
|
|
109
|
-
const prefs = await
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
126
|
+
const prefs = await findWithDecryption(
|
|
127
|
+
em,
|
|
128
|
+
RoleSidebarPreference,
|
|
129
|
+
{ role: { $in: options.roleIds }, tenantId: tenantFilter } as FilterQuery<RoleSidebarPreference>,
|
|
130
|
+
undefined,
|
|
131
|
+
{ tenantId, organizationId: null },
|
|
132
|
+
)
|
|
114
133
|
if (!prefs.length) return null
|
|
115
134
|
const ordered = options.roleIds
|
|
116
135
|
.map((id) => {
|
|
@@ -135,7 +154,13 @@ export async function saveRoleSidebarPreference(
|
|
|
135
154
|
version: input?.version ?? SIDEBAR_PREFERENCES_VERSION,
|
|
136
155
|
})
|
|
137
156
|
const { roleId, tenantId, locale } = normalizeRoleScope(scope)
|
|
138
|
-
let pref = await
|
|
157
|
+
let pref = await findOneWithDecryption(
|
|
158
|
+
em,
|
|
159
|
+
RoleSidebarPreference,
|
|
160
|
+
{ role: roleId, tenantId },
|
|
161
|
+
undefined,
|
|
162
|
+
{ tenantId, organizationId: null },
|
|
163
|
+
)
|
|
139
164
|
if (!pref) {
|
|
140
165
|
pref = em.create(RoleSidebarPreference, {
|
|
141
166
|
role: em.getReference(Role, roleId),
|
|
@@ -217,3 +242,203 @@ function normalizeRoleScope(scope: RoleSidebarPreferenceScope) {
|
|
|
217
242
|
locale: scope.locale,
|
|
218
243
|
}
|
|
219
244
|
}
|
|
245
|
+
|
|
246
|
+
// --- Named variants (per-user library of saved sidebar layouts) ----------------
|
|
247
|
+
|
|
248
|
+
export type VariantScope = {
|
|
249
|
+
userId: string
|
|
250
|
+
tenantId?: string | null
|
|
251
|
+
organizationId?: string | null
|
|
252
|
+
locale: string
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export type SidebarVariantRecord = {
|
|
256
|
+
id: string
|
|
257
|
+
name: string
|
|
258
|
+
isActive: boolean
|
|
259
|
+
settings: SidebarPreferencesSettings
|
|
260
|
+
createdAt: Date
|
|
261
|
+
updatedAt?: Date | null
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function toVariantRecord(variant: SidebarVariant): SidebarVariantRecord {
|
|
265
|
+
return {
|
|
266
|
+
id: variant.id,
|
|
267
|
+
name: variant.name,
|
|
268
|
+
isActive: variant.isActive === true,
|
|
269
|
+
settings: normalizeSidebarSettings(variant.settingsJson as SidebarPreferencesSettings | undefined),
|
|
270
|
+
createdAt: variant.createdAt,
|
|
271
|
+
updatedAt: variant.updatedAt ?? null,
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export async function listSidebarVariants(
|
|
276
|
+
em: EntityManager,
|
|
277
|
+
scope: VariantScope,
|
|
278
|
+
): Promise<SidebarVariantRecord[]> {
|
|
279
|
+
// Cross-locale: variants are scoped per (user, tenant) only.
|
|
280
|
+
const { userId, tenantId, organizationId } = normalizeVariantScope(scope)
|
|
281
|
+
const variants = await findWithDecryption(
|
|
282
|
+
em,
|
|
283
|
+
SidebarVariant,
|
|
284
|
+
{ user: userId, tenantId, deletedAt: null },
|
|
285
|
+
{ orderBy: { createdAt: 'asc' } },
|
|
286
|
+
{ tenantId, organizationId },
|
|
287
|
+
)
|
|
288
|
+
return variants.map(toVariantRecord)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export async function loadSidebarVariant(
|
|
292
|
+
em: EntityManager,
|
|
293
|
+
scope: VariantScope,
|
|
294
|
+
variantId: string,
|
|
295
|
+
): Promise<SidebarVariantRecord | null> {
|
|
296
|
+
const { userId, tenantId, organizationId } = normalizeVariantScope(scope)
|
|
297
|
+
const variant = await findOneWithDecryption(
|
|
298
|
+
em,
|
|
299
|
+
SidebarVariant,
|
|
300
|
+
{ id: variantId, user: userId, tenantId, deletedAt: null },
|
|
301
|
+
undefined,
|
|
302
|
+
{ tenantId, organizationId },
|
|
303
|
+
)
|
|
304
|
+
return variant ? toVariantRecord(variant) : null
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export async function nextVariantAutoName(
|
|
308
|
+
em: EntityManager,
|
|
309
|
+
scope: VariantScope,
|
|
310
|
+
prefix = 'My preferences',
|
|
311
|
+
): Promise<string> {
|
|
312
|
+
const variants = await listSidebarVariants(em, scope)
|
|
313
|
+
// Match names like "My preferences", "My preferences 2", "My preferences 17"
|
|
314
|
+
const usedNumbers = new Set<number>()
|
|
315
|
+
for (const variant of variants) {
|
|
316
|
+
if (variant.name === prefix) {
|
|
317
|
+
usedNumbers.add(1)
|
|
318
|
+
continue
|
|
319
|
+
}
|
|
320
|
+
const escaped = prefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
321
|
+
const match = variant.name.match(new RegExp(`^${escaped}\\s+(\\d+)$`))
|
|
322
|
+
if (match) {
|
|
323
|
+
const n = Number.parseInt(match[1], 10)
|
|
324
|
+
if (!Number.isNaN(n)) usedNumbers.add(n)
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
if (!usedNumbers.has(1)) return prefix
|
|
328
|
+
let next = 2
|
|
329
|
+
while (usedNumbers.has(next)) next += 1
|
|
330
|
+
return `${prefix} ${next}`
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export async function createSidebarVariant(
|
|
334
|
+
em: EntityManager,
|
|
335
|
+
scope: VariantScope,
|
|
336
|
+
input: {
|
|
337
|
+
name?: string | null
|
|
338
|
+
settings?: Partial<SidebarPreferencesSettings> | null
|
|
339
|
+
isActive?: boolean
|
|
340
|
+
},
|
|
341
|
+
): Promise<SidebarVariantRecord> {
|
|
342
|
+
const { userId, tenantId, organizationId, locale } = normalizeVariantScope(scope)
|
|
343
|
+
const finalName = (input.name ?? '').trim() || (await nextVariantAutoName(em, scope))
|
|
344
|
+
const settings = normalizeSidebarSettings({
|
|
345
|
+
...(input.settings ?? {}),
|
|
346
|
+
version: input.settings?.version ?? SIDEBAR_PREFERENCES_VERSION,
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
if (input.isActive === true) {
|
|
350
|
+
await deactivateAllVariants(em, scope)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const variant = em.create(SidebarVariant, {
|
|
354
|
+
user: em.getReference(User, userId),
|
|
355
|
+
tenantId,
|
|
356
|
+
organizationId,
|
|
357
|
+
locale,
|
|
358
|
+
name: finalName,
|
|
359
|
+
settingsJson: settings,
|
|
360
|
+
isActive: input.isActive === true,
|
|
361
|
+
createdAt: new Date(),
|
|
362
|
+
})
|
|
363
|
+
await em.flush()
|
|
364
|
+
return toVariantRecord(variant)
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export async function updateSidebarVariant(
|
|
368
|
+
em: EntityManager,
|
|
369
|
+
scope: VariantScope,
|
|
370
|
+
variantId: string,
|
|
371
|
+
input: {
|
|
372
|
+
name?: string
|
|
373
|
+
settings?: Partial<SidebarPreferencesSettings> | null
|
|
374
|
+
isActive?: boolean
|
|
375
|
+
},
|
|
376
|
+
): Promise<SidebarVariantRecord | null> {
|
|
377
|
+
const { userId, tenantId, organizationId } = normalizeVariantScope(scope)
|
|
378
|
+
const variant = await findOneWithDecryption(
|
|
379
|
+
em,
|
|
380
|
+
SidebarVariant,
|
|
381
|
+
{ id: variantId, user: userId, tenantId, deletedAt: null },
|
|
382
|
+
undefined,
|
|
383
|
+
{ tenantId, organizationId },
|
|
384
|
+
)
|
|
385
|
+
if (!variant) return null
|
|
386
|
+
if (typeof input.name === 'string' && input.name.trim().length > 0) {
|
|
387
|
+
variant.name = input.name.trim()
|
|
388
|
+
}
|
|
389
|
+
if (input.settings) {
|
|
390
|
+
variant.settingsJson = normalizeSidebarSettings({
|
|
391
|
+
...input.settings,
|
|
392
|
+
version: input.settings.version ?? SIDEBAR_PREFERENCES_VERSION,
|
|
393
|
+
})
|
|
394
|
+
}
|
|
395
|
+
if (typeof input.isActive === 'boolean') {
|
|
396
|
+
if (input.isActive) {
|
|
397
|
+
await deactivateAllVariants(em, scope, variantId)
|
|
398
|
+
}
|
|
399
|
+
variant.isActive = input.isActive
|
|
400
|
+
}
|
|
401
|
+
await em.flush()
|
|
402
|
+
return toVariantRecord(variant)
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
export async function deleteSidebarVariant(
|
|
406
|
+
em: EntityManager,
|
|
407
|
+
scope: VariantScope,
|
|
408
|
+
variantId: string,
|
|
409
|
+
): Promise<boolean> {
|
|
410
|
+
const { userId, tenantId, organizationId } = normalizeVariantScope(scope)
|
|
411
|
+
const variant = await findOneWithDecryption(
|
|
412
|
+
em,
|
|
413
|
+
SidebarVariant,
|
|
414
|
+
{ id: variantId, user: userId, tenantId, deletedAt: null },
|
|
415
|
+
undefined,
|
|
416
|
+
{ tenantId, organizationId },
|
|
417
|
+
)
|
|
418
|
+
if (!variant) return false
|
|
419
|
+
variant.deletedAt = new Date()
|
|
420
|
+
variant.isActive = false
|
|
421
|
+
await em.flush()
|
|
422
|
+
return true
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
async function deactivateAllVariants(
|
|
426
|
+
em: EntityManager,
|
|
427
|
+
scope: VariantScope,
|
|
428
|
+
exceptId?: string,
|
|
429
|
+
): Promise<void> {
|
|
430
|
+
const { userId, tenantId } = normalizeVariantScope(scope)
|
|
431
|
+
const where: FilterQuery<SidebarVariant> = exceptId
|
|
432
|
+
? { user: userId, tenantId, isActive: true, deletedAt: null, id: { $ne: exceptId } }
|
|
433
|
+
: { user: userId, tenantId, isActive: true, deletedAt: null }
|
|
434
|
+
await em.nativeUpdate(SidebarVariant, where, { isActive: false })
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function normalizeVariantScope(scope: VariantScope) {
|
|
438
|
+
return {
|
|
439
|
+
userId: scope.userId,
|
|
440
|
+
tenantId: scope.tenantId ?? null,
|
|
441
|
+
organizationId: scope.organizationId ?? null,
|
|
442
|
+
locale: scope.locale,
|
|
443
|
+
}
|
|
444
|
+
}
|
|
@@ -35,6 +35,7 @@ import { CompanyHighlights } from '../../../../components/detail/CompanyHighligh
|
|
|
35
35
|
import { normalizeCustomFieldSubmitValue } from '../../../../components/detail/customFieldUtils'
|
|
36
36
|
import { InlineDictionaryEditor, renderMultilineMarkdownDisplay } from '../../../../components/detail/InlineEditors'
|
|
37
37
|
import { formatTemplate } from '../../../../components/detail/utils'
|
|
38
|
+
import { coerceDisplayName } from '../../../../lib/displayName'
|
|
38
39
|
import { createTranslatorWithFallback } from '@open-mercato/shared/lib/i18n/translate'
|
|
39
40
|
import {
|
|
40
41
|
CompanyPeopleSection,
|
|
@@ -117,10 +118,10 @@ export default function CustomerCompanyDetailPage({ params }: { params?: { id?:
|
|
|
117
118
|
const [sectionAction, setSectionAction] = React.useState<SectionAction | null>(null)
|
|
118
119
|
const [isDeleting, setIsDeleting] = React.useState(false)
|
|
119
120
|
const currentCompanyId = data?.company?.id ?? null
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
121
|
+
const companyDisplayName = coerceDisplayName(data?.company?.displayName)
|
|
122
|
+
const companyName = companyDisplayName.trim().length
|
|
123
|
+
? companyDisplayName
|
|
124
|
+
: t('customers.companies.list.deleteFallbackName', 'this company')
|
|
124
125
|
const translateCompanyDetail = React.useCallback(
|
|
125
126
|
(key: string, fallback?: string, params?: Record<string, string | number>) => {
|
|
126
127
|
const mappedKey = key.startsWith('customers.people.detail.')
|
|
@@ -25,6 +25,7 @@ import { ActivityLogTab } from '../../../../components/detail/ActivityLogTab'
|
|
|
25
25
|
import { CompanyPeopleSection, type CompanyPersonSummary } from '../../../../components/detail/CompanyPeopleSection'
|
|
26
26
|
import type { TagSummary } from '../../../../components/detail/types'
|
|
27
27
|
import type { TagsSectionController } from '@open-mercato/ui/backend/detail'
|
|
28
|
+
import { coerceDisplayName } from '../../../../lib/displayName'
|
|
28
29
|
import { CompanyDetailHeader } from '../../../../components/detail/CompanyDetailHeader'
|
|
29
30
|
import { CompanyDetailTabs, resolveLegacyTab, type CompanyTabId } from '../../../../components/detail/CompanyDetailTabs'
|
|
30
31
|
import { CompanyKpiBar } from '../../../../components/detail/CompanyKpiBar'
|
|
@@ -103,10 +104,10 @@ export default function CompanyDetailV2Page({ params }: { params?: { id?: string
|
|
|
103
104
|
blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),
|
|
104
105
|
})
|
|
105
106
|
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
107
|
+
const companyDisplayName = coerceDisplayName(data?.company?.displayName)
|
|
108
|
+
const companyName = companyDisplayName.trim().length
|
|
109
|
+
? companyDisplayName
|
|
110
|
+
: t('customers.companies.list.deleteFallbackName', 'this company')
|
|
110
111
|
|
|
111
112
|
// Data loading
|
|
112
113
|
const initialLoadDoneRef = React.useRef(false)
|
|
@@ -425,7 +426,7 @@ export default function CompanyDetailV2Page({ params }: { params?: { id?: string
|
|
|
425
426
|
{activeTab === 'people' && (
|
|
426
427
|
<CompanyPeopleSection
|
|
427
428
|
companyId={companyId}
|
|
428
|
-
companyName={
|
|
429
|
+
companyName={companyDisplayName}
|
|
429
430
|
initialPeople={[]}
|
|
430
431
|
addActionLabel={t('customers.companies.detail.people.add', 'Add person')}
|
|
431
432
|
emptyLabel={t('customers.companies.detail.people.empty', 'No people linked to this company yet.')}
|
|
@@ -45,6 +45,7 @@ import {
|
|
|
45
45
|
type PersonEditFormValues,
|
|
46
46
|
type PersonOverview,
|
|
47
47
|
} from '../../../../components/formConfig'
|
|
48
|
+
import { coerceDisplayName, coerceDisplayNameOrNull } from '../../../../lib/displayName'
|
|
48
49
|
|
|
49
50
|
export default function PersonDetailV2Page({ params }: { params?: { id?: string } }) {
|
|
50
51
|
const id = params?.id
|
|
@@ -95,15 +96,18 @@ export default function PersonDetailV2Page({ params }: { params?: { id?: string
|
|
|
95
96
|
contextId: mutationContextId,
|
|
96
97
|
blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),
|
|
97
98
|
})
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
99
|
+
const personDisplayName = coerceDisplayName(data?.person?.displayName)
|
|
100
|
+
const personName = personDisplayName.trim().length
|
|
101
|
+
? personDisplayName
|
|
102
|
+
: t('customers.people.list.deleteFallbackName', 'this person')
|
|
102
103
|
|
|
103
|
-
const personDisplayNameForGroups =
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
104
|
+
const personDisplayNameForGroups = personDisplayName.trim().length
|
|
105
|
+
? personDisplayName.trim()
|
|
106
|
+
: null
|
|
107
|
+
|
|
108
|
+
const scheduleDialogCompanyName = coerceDisplayNameOrNull(
|
|
109
|
+
data?.company?.displayName ?? data?.companies?.[0]?.displayName ?? null,
|
|
110
|
+
)
|
|
107
111
|
|
|
108
112
|
const groups = React.useMemo(
|
|
109
113
|
() => createPersonPersonalDataGroups(t, { entityName: personDisplayNameForGroups }),
|
|
@@ -579,7 +583,7 @@ export default function PersonDetailV2Page({ params }: { params?: { id?: string
|
|
|
579
583
|
onClose={() => { setScheduleDialogOpen(false); setScheduleEditData(null) }}
|
|
580
584
|
entityId={personId}
|
|
581
585
|
entityName={personName}
|
|
582
|
-
companyName={
|
|
586
|
+
companyName={scheduleDialogCompanyName}
|
|
583
587
|
entityType="person"
|
|
584
588
|
onActivityCreated={handleActivityCreated}
|
|
585
589
|
editData={scheduleEditData}
|
|
@@ -17,6 +17,7 @@ import { useAppEvent } from '@open-mercato/ui/backend/injection/useAppEvent'
|
|
|
17
17
|
import type { SectionAction, TabEmptyStateConfig, Translator } from './types'
|
|
18
18
|
import { CreatePersonDialog } from './CreatePersonDialog'
|
|
19
19
|
import { PersonCard } from './PersonCard'
|
|
20
|
+
import { coerceDisplayName } from '../../lib/displayName'
|
|
20
21
|
import { DecisionMakersFooter } from './DecisionMakersFooter'
|
|
21
22
|
import { RolesSection } from './RolesSection'
|
|
22
23
|
import { LinkEntityDialog, type LinkEntityOption } from '../linking/LinkEntityDialog'
|
|
@@ -165,8 +166,8 @@ function sortCompanyPeople(
|
|
|
165
166
|
const rightTimestamp = Date.parse(right.linkedAt ?? right.createdAt ?? '') || 0
|
|
166
167
|
return rightTimestamp - leftTimestamp
|
|
167
168
|
}
|
|
168
|
-
const leftLabel = left.displayName.trim().toLowerCase()
|
|
169
|
-
const rightLabel = right.displayName.trim().toLowerCase()
|
|
169
|
+
const leftLabel = coerceDisplayName(left.displayName).trim().toLowerCase()
|
|
170
|
+
const rightLabel = coerceDisplayName(right.displayName).trim().toLowerCase()
|
|
170
171
|
if (sortMode === 'name-desc') return rightLabel.localeCompare(leftLabel)
|
|
171
172
|
return leftLabel.localeCompare(rightLabel)
|
|
172
173
|
})
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
SelectTrigger,
|
|
16
16
|
SelectValue,
|
|
17
17
|
} from '@open-mercato/ui/primitives/select'
|
|
18
|
-
import { deriveDisplayName, isDerivedDisplayName } from '../lib/displayName'
|
|
18
|
+
import { coerceDisplayName, deriveDisplayName, isDerivedDisplayName } from '../lib/displayName'
|
|
19
19
|
import {
|
|
20
20
|
Dialog,
|
|
21
21
|
DialogContent,
|
|
@@ -1952,7 +1952,7 @@ export function mapCompanyOverviewToFormValues(overview: CompanyOverview): Parti
|
|
|
1952
1952
|
const phoneValue = rawPhone == null ? '' : String(rawPhone)
|
|
1953
1953
|
return {
|
|
1954
1954
|
id: overview.company.id,
|
|
1955
|
-
displayName: overview.company.displayName,
|
|
1955
|
+
displayName: coerceDisplayName(overview.company.displayName),
|
|
1956
1956
|
primaryEmail: overview.company.primaryEmail ?? '',
|
|
1957
1957
|
primaryPhone: phoneValue,
|
|
1958
1958
|
status: overview.company.status ?? '',
|
|
@@ -1975,7 +1975,7 @@ export function mapPersonOverviewToFormValues(overview: PersonOverview): Partial
|
|
|
1975
1975
|
const phoneValue = rawPhone == null ? '' : String(rawPhone)
|
|
1976
1976
|
return {
|
|
1977
1977
|
id: overview.person.id,
|
|
1978
|
-
displayName: overview.person.displayName,
|
|
1978
|
+
displayName: coerceDisplayName(overview.person.displayName),
|
|
1979
1979
|
firstName: overview.profile?.firstName ?? '',
|
|
1980
1980
|
lastName: overview.profile?.lastName ?? '',
|
|
1981
1981
|
primaryEmail: overview.person.primaryEmail ?? '',
|
|
@@ -1,3 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coerce an arbitrary `displayName` payload to a string for safe UI consumption.
|
|
3
|
+
*
|
|
4
|
+
* The encryption pipeline used to coerce numeric-looking string display names
|
|
5
|
+
* back into numbers (issue #1734). The root cause is fixed in
|
|
6
|
+
* `parseDecryptedFieldValue`, but this helper remains as belt-and-suspenders
|
|
7
|
+
* for any persisted data that was already corrupted on read paths that bypass
|
|
8
|
+
* the new heuristic.
|
|
9
|
+
*/
|
|
10
|
+
export function coerceDisplayName(value: unknown): string {
|
|
11
|
+
if (typeof value === 'string') return value
|
|
12
|
+
if (value == null) return ''
|
|
13
|
+
return String(value)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function coerceDisplayNameOrNull(value: unknown): string | null {
|
|
17
|
+
if (value == null) return null
|
|
18
|
+
if (typeof value === 'string') return value
|
|
19
|
+
return String(value)
|
|
20
|
+
}
|
|
21
|
+
|
|
1
22
|
export function deriveDisplayName(
|
|
2
23
|
firstName: string | null | undefined,
|
|
3
24
|
lastName: string | null | undefined,
|
|
@@ -17,7 +17,10 @@ import {
|
|
|
17
17
|
TenantDataEncryptionError,
|
|
18
18
|
TenantDataEncryptionErrorCode,
|
|
19
19
|
} from '@open-mercato/shared/lib/encryption/aes'
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
TenantDataEncryptionService,
|
|
22
|
+
parseDecryptedFieldValue,
|
|
23
|
+
} from '@open-mercato/shared/lib/encryption/tenantDataEncryptionService'
|
|
21
24
|
import { resolveEntityIdFromMetadata } from '@open-mercato/shared/lib/encryption/entityIds'
|
|
22
25
|
import { Organization } from '../directory/data/entities'
|
|
23
26
|
import crypto from 'node:crypto'
|
|
@@ -563,11 +566,7 @@ const rotateEncryptionKey: ModuleCli = {
|
|
|
563
566
|
if (typeof value !== 'string' || !isEncryptedPayload(value)) continue
|
|
564
567
|
const decrypted = decryptWithOldKey(value, oldDek)
|
|
565
568
|
if (decrypted === null) continue
|
|
566
|
-
|
|
567
|
-
payload[rule.field] = JSON.parse(decrypted)
|
|
568
|
-
} catch {
|
|
569
|
-
payload[rule.field] = decrypted
|
|
570
|
-
}
|
|
569
|
+
payload[rule.field] = parseDecryptedFieldValue(decrypted)
|
|
571
570
|
}
|
|
572
571
|
}
|
|
573
572
|
const encrypted = await encryptionService.encryptEntityPayload(
|