@open-mercato/core 0.10.1-develop.7972.1.03f813eaaf → 0.10.1-develop.7974.1.c98f9304d9
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/helpers/integration/communicationChannelsFixtures.js +11 -0
- package/dist/helpers/integration/communicationChannelsFixtures.js.map +2 -2
- package/dist/modules/communication_channels/api/post/channels/[id]/set-primary/route.js +16 -1
- package/dist/modules/communication_channels/api/post/channels/[id]/set-primary/route.js.map +2 -2
- package/dist/modules/communication_channels/api/post/test-seed/route.js +31 -0
- package/dist/modules/communication_channels/api/post/test-seed/route.js.map +2 -2
- package/dist/modules/communication_channels/backend/profile/communication-channels/page.js +1 -1
- package/dist/modules/communication_channels/backend/profile/communication-channels/page.js.map +2 -2
- package/dist/modules/communication_channels/commands/set-primary-channel.js +3 -0
- package/dist/modules/communication_channels/commands/set-primary-channel.js.map +2 -2
- package/dist/modules/dictionaries/api/[dictionaryId]/route.js +9 -2
- package/dist/modules/dictionaries/api/[dictionaryId]/route.js.map +2 -2
- package/dist/modules/dictionaries/api/route.js +9 -2
- package/dist/modules/dictionaries/api/route.js.map +2 -2
- package/dist/modules/dictionaries/components/AppearanceSelector.js +4 -1
- package/dist/modules/dictionaries/components/AppearanceSelector.js.map +2 -2
- package/dist/modules/dictionaries/components/DictionariesManager.js +2 -2
- package/dist/modules/dictionaries/components/DictionariesManager.js.map +2 -2
- package/dist/modules/dictionaries/components/DictionaryEntriesEditor.js +1 -1
- package/dist/modules/dictionaries/components/DictionaryEntriesEditor.js.map +1 -1
- package/package.json +7 -7
- package/src/helpers/integration/communicationChannelsFixtures.ts +19 -0
- package/src/modules/communication_channels/api/post/channels/[id]/set-primary/route.ts +15 -0
- package/src/modules/communication_channels/api/post/test-seed/route.ts +38 -0
- package/src/modules/communication_channels/backend/profile/communication-channels/page.tsx +1 -1
- package/src/modules/communication_channels/commands/set-primary-channel.ts +7 -0
- package/src/modules/communication_channels/i18n/de.json +1 -0
- package/src/modules/communication_channels/i18n/en.json +1 -0
- package/src/modules/communication_channels/i18n/es.json +1 -0
- package/src/modules/communication_channels/i18n/ko.json +1 -0
- package/src/modules/communication_channels/i18n/pl.json +1 -0
- package/src/modules/dictionaries/api/[dictionaryId]/route.ts +9 -2
- package/src/modules/dictionaries/api/route.ts +9 -2
- package/src/modules/dictionaries/components/AppearanceSelector.tsx +4 -1
- package/src/modules/dictionaries/components/DictionariesManager.tsx +2 -2
- package/src/modules/dictionaries/components/DictionaryEntriesEditor.tsx +1 -1
- package/src/modules/dictionaries/i18n/de.json +1 -0
- package/src/modules/dictionaries/i18n/en.json +1 -0
- package/src/modules/dictionaries/i18n/es.json +1 -0
- package/src/modules/dictionaries/i18n/ko.json +1 -0
- package/src/modules/dictionaries/i18n/pl.json +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.10.1-develop.
|
|
3
|
+
"version": "0.10.1-develop.7974.1.c98f9304d9",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -266,16 +266,16 @@
|
|
|
266
266
|
"zod": "^4.6.6"
|
|
267
267
|
},
|
|
268
268
|
"peerDependencies": {
|
|
269
|
-
"@open-mercato/ai-assistant": "0.10.1-develop.
|
|
270
|
-
"@open-mercato/shared": "0.10.1-develop.
|
|
271
|
-
"@open-mercato/ui": "0.10.1-develop.
|
|
269
|
+
"@open-mercato/ai-assistant": "0.10.1-develop.7974.1.c98f9304d9",
|
|
270
|
+
"@open-mercato/shared": "0.10.1-develop.7974.1.c98f9304d9",
|
|
271
|
+
"@open-mercato/ui": "0.10.1-develop.7974.1.c98f9304d9",
|
|
272
272
|
"react": "^19.0.0",
|
|
273
273
|
"react-dom": "^19.0.0"
|
|
274
274
|
},
|
|
275
275
|
"devDependencies": {
|
|
276
|
-
"@open-mercato/ai-assistant": "0.10.1-develop.
|
|
277
|
-
"@open-mercato/shared": "0.10.1-develop.
|
|
278
|
-
"@open-mercato/ui": "0.10.1-develop.
|
|
276
|
+
"@open-mercato/ai-assistant": "0.10.1-develop.7974.1.c98f9304d9",
|
|
277
|
+
"@open-mercato/shared": "0.10.1-develop.7974.1.c98f9304d9",
|
|
278
|
+
"@open-mercato/ui": "0.10.1-develop.7974.1.c98f9304d9",
|
|
279
279
|
"@testing-library/dom": "^10.4.1",
|
|
280
280
|
"@testing-library/jest-dom": "^7.0.1",
|
|
281
281
|
"@testing-library/react": "^16.3.3",
|
|
@@ -111,6 +111,25 @@ export async function seedConnectedChannel(
|
|
|
111
111
|
return expectId(body?.channelId, 'connect-channel response should include channelId');
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Disconnect a channel the caller owns through the real disconnect command. The
|
|
116
|
+
* row stays listed with `status: 'disconnected'` (a UI disconnect deletes it).
|
|
117
|
+
*/
|
|
118
|
+
export async function disconnectSeededChannel(
|
|
119
|
+
request: APIRequestContext,
|
|
120
|
+
token: string,
|
|
121
|
+
channelId: string,
|
|
122
|
+
): Promise<void> {
|
|
123
|
+
const response = await apiRequest(request, 'POST', TEST_SEED_PATH, {
|
|
124
|
+
token,
|
|
125
|
+
data: { action: 'disconnect-channel', channelId },
|
|
126
|
+
});
|
|
127
|
+
expect(
|
|
128
|
+
response.status(),
|
|
129
|
+
'POST /api/communication_channels/test-seed (disconnect-channel) should return 200',
|
|
130
|
+
).toBe(200);
|
|
131
|
+
}
|
|
132
|
+
|
|
114
133
|
export async function seedSystemEmailChannel(
|
|
115
134
|
request: APIRequestContext,
|
|
116
135
|
token: string,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
2
|
import { z } from 'zod'
|
|
3
3
|
import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
|
|
4
|
+
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
4
5
|
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
5
6
|
import type { CommandBus } from '@open-mercato/shared/lib/commands'
|
|
6
7
|
import {
|
|
@@ -82,6 +83,19 @@ export async function POST(req: Request, context: RouteContext): Promise<Respons
|
|
|
82
83
|
if (result.status === 'not_owner') {
|
|
83
84
|
return NextResponse.json({ error: 'Channel not found' }, { status: 404 })
|
|
84
85
|
}
|
|
86
|
+
if (result.status === 'disconnected') {
|
|
87
|
+
const { translate } = await resolveTranslations()
|
|
88
|
+
return NextResponse.json(
|
|
89
|
+
{
|
|
90
|
+
error: translate(
|
|
91
|
+
'communication_channels.errors.setPrimaryDisconnected',
|
|
92
|
+
'This channel is disconnected. Reconnect it before setting it as primary.',
|
|
93
|
+
),
|
|
94
|
+
code: 'channel_disconnected',
|
|
95
|
+
},
|
|
96
|
+
{ status: 409 },
|
|
97
|
+
)
|
|
98
|
+
}
|
|
85
99
|
if (result.status === 'noop') {
|
|
86
100
|
return NextResponse.json({ channelId: id, isPrimary: true, unchanged: true }, { status: 200 })
|
|
87
101
|
}
|
|
@@ -107,6 +121,7 @@ export const openApi = {
|
|
|
107
121
|
{ status: 400, description: 'Invalid channel id' },
|
|
108
122
|
{ status: 401, description: 'Unauthorized' },
|
|
109
123
|
{ status: 404, description: 'Channel not found or not owned by current user' },
|
|
124
|
+
{ status: 409, description: 'Channel is disconnected and cannot be set as primary' },
|
|
110
125
|
],
|
|
111
126
|
},
|
|
112
127
|
},
|
|
@@ -22,6 +22,11 @@ import {
|
|
|
22
22
|
type ConnectCredentialChannelInput,
|
|
23
23
|
type ConnectCredentialChannelResult,
|
|
24
24
|
} from '../../../commands/connect-credential-channel'
|
|
25
|
+
import {
|
|
26
|
+
COMMUNICATION_CHANNELS_DISCONNECT_CHANNEL_COMMAND_ID,
|
|
27
|
+
type DisconnectChannelInput,
|
|
28
|
+
type DisconnectChannelResult,
|
|
29
|
+
} from '../../../commands/disconnect-channel'
|
|
25
30
|
import { emitCommunicationChannelsEvent } from '../../../events'
|
|
26
31
|
import {
|
|
27
32
|
TEST_SEED_CHAT_PROVIDER_KEY,
|
|
@@ -67,6 +72,9 @@ import {
|
|
|
67
72
|
* the hub accepts a message, only that downstream subscribers fire.
|
|
68
73
|
* - `purge-inbound`: hard-delete the rows one test-seeded or test-ingested
|
|
69
74
|
* inbound message created, for integration teardown.
|
|
75
|
+
* - `disconnect-channel`: run the REAL disconnect command on a channel the
|
|
76
|
+
* caller owns. No HTTP route disconnects without deleting the row, so this is
|
|
77
|
+
* the only way a test can reach a listed `disconnected` channel.
|
|
70
78
|
*/
|
|
71
79
|
type RbacServiceLike = {
|
|
72
80
|
loadAcl: (
|
|
@@ -158,6 +166,11 @@ const seedSystemChannelSchema = z.object({
|
|
|
158
166
|
externalIdentifier: z.string().min(1).max(255).optional(),
|
|
159
167
|
})
|
|
160
168
|
|
|
169
|
+
const disconnectChannelSchema = z.object({
|
|
170
|
+
action: z.literal('disconnect-channel'),
|
|
171
|
+
channelId: z.string().uuid(),
|
|
172
|
+
})
|
|
173
|
+
|
|
161
174
|
const clearCaptureSchema = z.object({
|
|
162
175
|
action: z.literal('clear-capture'),
|
|
163
176
|
systemRecipient: z.string().email().max(320).optional(),
|
|
@@ -237,6 +250,7 @@ const bodySchema = z.discriminatedUnion('action', [
|
|
|
237
250
|
emitInboundSchema,
|
|
238
251
|
purgeInboundSchema,
|
|
239
252
|
seedSystemChannelSchema,
|
|
253
|
+
disconnectChannelSchema,
|
|
240
254
|
clearCaptureSchema,
|
|
241
255
|
listCaptureSchema,
|
|
242
256
|
])
|
|
@@ -408,6 +422,30 @@ export async function POST(req: Request): Promise<Response> {
|
|
|
408
422
|
return NextResponse.json({ channelId: channel.id }, { status: 201 })
|
|
409
423
|
}
|
|
410
424
|
|
|
425
|
+
if (body.action === 'disconnect-channel') {
|
|
426
|
+
const commandBus = container.resolve('commandBus') as CommandBus
|
|
427
|
+
const { result } = await commandBus.execute<DisconnectChannelInput, DisconnectChannelResult>(
|
|
428
|
+
COMMUNICATION_CHANNELS_DISCONNECT_CHANNEL_COMMAND_ID,
|
|
429
|
+
{
|
|
430
|
+
input: { channelId: body.channelId, userId, scope: { tenantId, organizationId } },
|
|
431
|
+
ctx: {
|
|
432
|
+
container,
|
|
433
|
+
auth: auth as never,
|
|
434
|
+
organizationScope: null,
|
|
435
|
+
selectedOrganizationId: organizationId,
|
|
436
|
+
organizationIds: organizationId ? [organizationId] : null,
|
|
437
|
+
},
|
|
438
|
+
},
|
|
439
|
+
)
|
|
440
|
+
if (result.status !== 'disconnected') {
|
|
441
|
+
return NextResponse.json(
|
|
442
|
+
{ error: '[internal] test-seed disconnect failed', detail: { status: result.status } },
|
|
443
|
+
{ status: 500 },
|
|
444
|
+
)
|
|
445
|
+
}
|
|
446
|
+
return NextResponse.json({ channelId: result.channelId, status: result.status }, { status: 200 })
|
|
447
|
+
}
|
|
448
|
+
|
|
411
449
|
if (body.action === 'connect-channel') {
|
|
412
450
|
const stamp = Date.now()
|
|
413
451
|
const commandBus = container.resolve('commandBus') as CommandBus
|
|
@@ -537,7 +537,7 @@ export default function ProfileCommunicationChannelsPage() {
|
|
|
537
537
|
(channel: ChannelRow) => {
|
|
538
538
|
const items: RowActionItem[] = []
|
|
539
539
|
|
|
540
|
-
if (!channel.isPrimary) {
|
|
540
|
+
if (!channel.isPrimary && channel.status !== 'disconnected') {
|
|
541
541
|
items.push({
|
|
542
542
|
id: 'set-primary',
|
|
543
543
|
label: t('communication_channels.profile.actions.setPrimary', 'Set as primary'),
|
|
@@ -22,6 +22,7 @@ export type SetPrimaryChannelResult =
|
|
|
22
22
|
| { status: 'set'; channelId: string; previousPrimaryChannelId: string | null }
|
|
23
23
|
| { status: 'noop'; reason: string }
|
|
24
24
|
| { status: 'not_owner'; reason: string }
|
|
25
|
+
| { status: 'disconnected'; reason: string }
|
|
25
26
|
|
|
26
27
|
export const COMMUNICATION_CHANNELS_SET_PRIMARY_COMMAND_ID =
|
|
27
28
|
'communication_channels.channel.set_primary'
|
|
@@ -32,6 +33,9 @@ export const COMMUNICATION_CHANNELS_SET_PRIMARY_COMMAND_ID =
|
|
|
32
33
|
* partial unique index `communication_channels_one_primary_per_user_uq`.
|
|
33
34
|
*
|
|
34
35
|
* Ownership-checked: refuses to set a primary on someone else's channel.
|
|
36
|
+
* Refuses a disconnected channel: every path that disconnects a channel also
|
|
37
|
+
* clears its primary flag, so promoting one would hand the user a primary that
|
|
38
|
+
* cannot send.
|
|
35
39
|
*/
|
|
36
40
|
const setPrimaryChannelCommand: CommandHandler<
|
|
37
41
|
SetPrimaryChannelInput,
|
|
@@ -75,6 +79,9 @@ const setPrimaryChannelCommand: CommandHandler<
|
|
|
75
79
|
if (channel.userId !== input.userId) {
|
|
76
80
|
return { status: 'not_owner', reason: 'channel is not owned by the current user' }
|
|
77
81
|
}
|
|
82
|
+
if (channel.status === 'disconnected') {
|
|
83
|
+
return { status: 'disconnected', reason: 'channel is disconnected' }
|
|
84
|
+
}
|
|
78
85
|
if (channel.isPrimary) {
|
|
79
86
|
return { status: 'noop', reason: 'channel is already primary' }
|
|
80
87
|
}
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"communication_channels.errors.pollNowPushDriven": "Der Kanal ist Push-gesteuert — Abrufen ist nicht anwendbar. Eingehende Nachrichten kommen über die Push-Verbindung des Anbieters.",
|
|
31
31
|
"communication_channels.errors.reactionFailed": "Reaktion fehlgeschlagen",
|
|
32
32
|
"communication_channels.errors.reactionNotFound": "Reaktion nicht gefunden",
|
|
33
|
+
"communication_channels.errors.setPrimaryDisconnected": "Dieser Kanal ist getrennt. Verbinde ihn erneut, bevor du ihn als primär festlegst.",
|
|
33
34
|
"communication_channels.errors.threadNotFound": "Thread nicht gefunden",
|
|
34
35
|
"communication_channels.errors.undoBlockedPrimaryConflict": "Ein anderer Kanal ist jetzt für diesen Benutzer als primär festgelegt. Setze ihn auf nicht-primär, bevor du den getrennten Kanal wieder als primär festlegst.",
|
|
35
36
|
"communication_channels.infoPanel.aria": "Kanalinfo",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"communication_channels.errors.pollNowPushDriven": "Channel is push-driven — polling does not apply. Inbound messages arrive through the provider push connection.",
|
|
31
31
|
"communication_channels.errors.reactionFailed": "Reaction failed",
|
|
32
32
|
"communication_channels.errors.reactionNotFound": "Reaction not found",
|
|
33
|
+
"communication_channels.errors.setPrimaryDisconnected": "This channel is disconnected. Reconnect it before setting it as primary.",
|
|
33
34
|
"communication_channels.errors.threadNotFound": "Thread not found",
|
|
34
35
|
"communication_channels.errors.undoBlockedPrimaryConflict": "Another channel is now primary for this user. Set it as non-primary before restoring the disconnected channel as primary.",
|
|
35
36
|
"communication_channels.infoPanel.aria": "Channel info",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"communication_channels.errors.pollNowPushDriven": "El canal funciona con push — el sondeo no aplica. Los mensajes entrantes llegan por la conexión push del proveedor.",
|
|
31
31
|
"communication_channels.errors.reactionFailed": "La reacción falló",
|
|
32
32
|
"communication_channels.errors.reactionNotFound": "Reacción no encontrada",
|
|
33
|
+
"communication_channels.errors.setPrimaryDisconnected": "Este canal está desconectado. Vuelve a conectarlo antes de establecerlo como principal.",
|
|
33
34
|
"communication_channels.errors.threadNotFound": "Hilo no encontrado",
|
|
34
35
|
"communication_channels.errors.undoBlockedPrimaryConflict": "Otro canal es ahora el principal para este usuario. Configúralo como no principal antes de restaurar el canal desconectado como principal.",
|
|
35
36
|
"communication_channels.infoPanel.aria": "Información del canal",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"communication_channels.errors.pollNowPushDriven": "이 채널은 푸시 기반입니다 — 폴링은 적용되지 않습니다. 수신 메시지는 공급자의 푸시 연결로 전달됩니다.",
|
|
31
31
|
"communication_channels.errors.reactionFailed": "반응 처리에 실패했습니다",
|
|
32
32
|
"communication_channels.errors.reactionNotFound": "반응을 찾을 수 없습니다",
|
|
33
|
+
"communication_channels.errors.setPrimaryDisconnected": "이 채널은 연결이 끊겼습니다. 기본으로 설정하기 전에 다시 연결하세요.",
|
|
33
34
|
"communication_channels.errors.threadNotFound": "스레드를 찾을 수 없습니다",
|
|
34
35
|
"communication_channels.errors.undoBlockedPrimaryConflict": "다른 채널이 현재 이 사용자의 기본 채널로 설정되어 있습니다. 연결이 끊어진 채널을 기본으로 복원하기 전에 해당 채널을 기본이 아닌 채널로 설정하세요.",
|
|
35
36
|
"communication_channels.infoPanel.aria": "채널 정보",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"communication_channels.errors.pollNowPushDriven": "Kanał działa w trybie push — odpytywanie go nie dotyczy. Wiadomości przychodzące docierają połączeniem push dostawcy.",
|
|
31
31
|
"communication_channels.errors.reactionFailed": "Reakcja nie powiodła się",
|
|
32
32
|
"communication_channels.errors.reactionNotFound": "Nie znaleziono reakcji",
|
|
33
|
+
"communication_channels.errors.setPrimaryDisconnected": "Ten kanał jest rozłączony. Połącz go ponownie, zanim ustawisz go jako główny.",
|
|
33
34
|
"communication_channels.errors.threadNotFound": "Nie znaleziono wątku",
|
|
34
35
|
"communication_channels.errors.undoBlockedPrimaryConflict": "Inny kanał jest teraz głównym dla tego użytkownika. Ustaw go jako niegłówny, zanim przywrócisz rozłączony kanał jako główny.",
|
|
35
36
|
"communication_channels.infoPanel.aria": "Informacje o kanale",
|
|
@@ -2,7 +2,7 @@ import { NextResponse } from 'next/server'
|
|
|
2
2
|
import { z } from 'zod'
|
|
3
3
|
import { Dictionary } from '@open-mercato/core/modules/dictionaries/data/entities'
|
|
4
4
|
import { resolveDictionariesRouteContext, resolveDictionaryActorId } from '@open-mercato/core/modules/dictionaries/api/context'
|
|
5
|
-
import { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
5
|
+
import { CrudHttpError, isCrudHttpError, isUniqueViolation } from '@open-mercato/shared/lib/crud/errors'
|
|
6
6
|
import { enforceCommandOptimisticLockWithGuards } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
|
|
7
7
|
import {
|
|
8
8
|
runCrudMutationGuardAfterSuccess,
|
|
@@ -191,7 +191,14 @@ export async function PATCH(req: Request, ctx: { params?: { dictionaryId?: strin
|
|
|
191
191
|
}
|
|
192
192
|
|
|
193
193
|
dictionary.updatedAt = new Date()
|
|
194
|
-
|
|
194
|
+
try {
|
|
195
|
+
await context.em.flush()
|
|
196
|
+
} catch (err) {
|
|
197
|
+
if (isUniqueViolation(err, 'dictionaries_scope_key_unique')) {
|
|
198
|
+
throw new CrudHttpError(409, { error: context.translate('dictionaries.errors.duplicate', 'A dictionary with this key already exists') })
|
|
199
|
+
}
|
|
200
|
+
throw err
|
|
201
|
+
}
|
|
195
202
|
|
|
196
203
|
if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
|
|
197
204
|
await runCrudMutationGuardAfterSuccess(context.container, {
|
|
@@ -2,7 +2,7 @@ import { NextResponse } from 'next/server'
|
|
|
2
2
|
import { z } from 'zod'
|
|
3
3
|
import { Dictionary } from '@open-mercato/core/modules/dictionaries/data/entities'
|
|
4
4
|
import { resolveDictionariesRouteContext, resolveDictionaryActorId } from '@open-mercato/core/modules/dictionaries/api/context'
|
|
5
|
-
import { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
5
|
+
import { CrudHttpError, isCrudHttpError, isUniqueViolation } from '@open-mercato/shared/lib/crud/errors'
|
|
6
6
|
import {
|
|
7
7
|
runCrudMutationGuardAfterSuccess,
|
|
8
8
|
validateCrudMutationGuard,
|
|
@@ -126,7 +126,14 @@ export async function POST(req: Request) {
|
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
context.em.persist(dictionary)
|
|
129
|
-
|
|
129
|
+
try {
|
|
130
|
+
await context.em.flush()
|
|
131
|
+
} catch (err) {
|
|
132
|
+
if (isUniqueViolation(err, 'dictionaries_scope_key_unique')) {
|
|
133
|
+
throw new CrudHttpError(409, { error: context.translate('dictionaries.errors.duplicate', 'A dictionary with this key already exists') })
|
|
134
|
+
}
|
|
135
|
+
throw err
|
|
136
|
+
}
|
|
130
137
|
|
|
131
138
|
if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
|
|
132
139
|
await runCrudMutationGuardAfterSuccess(context.container, {
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
4
|
import { Ellipsis } from 'lucide-react'
|
|
5
|
+
import { useOptionalT } from '@open-mercato/shared/lib/i18n/context'
|
|
5
6
|
import { Button } from '@open-mercato/ui/primitives/button'
|
|
6
7
|
import { ColorPicker } from '@open-mercato/ui/primitives/color-picker'
|
|
7
8
|
import { Input } from '@open-mercato/ui/primitives/input'
|
|
@@ -52,6 +53,8 @@ export function AppearanceSelector({
|
|
|
52
53
|
className,
|
|
53
54
|
previewLabel,
|
|
54
55
|
}: AppearanceSelectorProps) {
|
|
56
|
+
const translate = useOptionalT()
|
|
57
|
+
const previewHeading = translate ? translate('dictionaries.appearance.preview', 'Preview') : 'Preview'
|
|
55
58
|
const normalizedIcon = icon ?? ''
|
|
56
59
|
const normalizedColor = color ?? '#000000'
|
|
57
60
|
const hasAppearance = Boolean(icon) || Boolean(color)
|
|
@@ -246,7 +249,7 @@ export function AppearanceSelector({
|
|
|
246
249
|
</div>
|
|
247
250
|
|
|
248
251
|
<div>
|
|
249
|
-
<label className="text-sm font-medium">
|
|
252
|
+
<label className="text-sm font-medium">{previewHeading}</label>
|
|
250
253
|
<div className="flex items-center gap-3 rounded border border-dashed border-border px-3 py-2">
|
|
251
254
|
{hasPreviewContent ? (
|
|
252
255
|
<>
|
|
@@ -392,7 +392,7 @@ export function DictionariesManager() {
|
|
|
392
392
|
|
|
393
393
|
return (
|
|
394
394
|
<div className="grid gap-6 lg:grid-cols-[320px_1fr]">
|
|
395
|
-
<div className="rounded-lg border bg-card p-4 shadow-sm">
|
|
395
|
+
<div className="min-w-0 rounded-lg border bg-card p-4 shadow-sm">
|
|
396
396
|
<div className="flex items-center justify-between">
|
|
397
397
|
<h2 className="text-base font-semibold">
|
|
398
398
|
{t('dictionaries.config.list.title', 'Dictionaries')}
|
|
@@ -477,7 +477,7 @@ export function DictionariesManager() {
|
|
|
477
477
|
)}
|
|
478
478
|
</div>
|
|
479
479
|
</div>
|
|
480
|
-
<div>
|
|
480
|
+
<div className="min-w-0">
|
|
481
481
|
{selectedDictionary ? (
|
|
482
482
|
<DictionaryEntriesEditor
|
|
483
483
|
dictionaryId={selectedDictionary.id}
|
|
@@ -409,7 +409,7 @@ export function DictionaryEntriesEditor({ dictionaryId, dictionaryName, readOnly
|
|
|
409
409
|
</div>
|
|
410
410
|
</div>
|
|
411
411
|
|
|
412
|
-
<div className="overflow-
|
|
412
|
+
<div className="overflow-x-auto rounded-md border">
|
|
413
413
|
<Table>
|
|
414
414
|
<TableHeader>
|
|
415
415
|
<TableRow>
|