@open-mercato/core 0.6.3-develop.3901.1.ddad60693a → 0.6.3
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/global.d.js +1 -0
- package/dist/global.d.js.map +7 -0
- package/dist/modules/catalog/commands/variants.js +11 -5
- package/dist/modules/catalog/commands/variants.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/create/page.js +3 -61
- package/dist/modules/customers/backend/customers/deals/create/page.js.map +2 -2
- package/dist/modules/customers/components/detail/DealForm.js +2 -0
- package/dist/modules/customers/components/detail/DealForm.js.map +2 -2
- package/dist/modules/customers/components/detail/create/CreateDealForm.js +233 -0
- package/dist/modules/customers/components/detail/create/CreateDealForm.js.map +7 -0
- package/dist/modules/customers/components/detail/create/DealAssociationsField.js +209 -0
- package/dist/modules/customers/components/detail/create/DealAssociationsField.js.map +7 -0
- package/dist/modules/customers/components/detail/create/DealAssociationsSection.js +67 -0
- package/dist/modules/customers/components/detail/create/DealAssociationsSection.js.map +7 -0
- package/dist/modules/customers/components/detail/create/DealCreateSidebar.js +73 -0
- package/dist/modules/customers/components/detail/create/DealCreateSidebar.js.map +7 -0
- package/dist/modules/customers/components/detail/create/DealCurrencyField.js +92 -0
- package/dist/modules/customers/components/detail/create/DealCurrencyField.js.map +7 -0
- package/dist/modules/customers/components/detail/create/DealCustomAttributes.js +81 -0
- package/dist/modules/customers/components/detail/create/DealCustomAttributes.js.map +7 -0
- package/dist/modules/customers/components/detail/create/DealDetailsFields.js +171 -0
- package/dist/modules/customers/components/detail/create/DealDetailsFields.js.map +7 -0
- package/dist/modules/customers/components/detail/create/DealFormField.js +24 -0
- package/dist/modules/customers/components/detail/create/DealFormField.js.map +7 -0
- package/dist/modules/customers/components/detail/create/DealSectionCard.js +29 -0
- package/dist/modules/customers/components/detail/create/DealSectionCard.js.map +7 -0
- package/dist/modules/customers/components/detail/create/DealTipsCard.js +19 -0
- package/dist/modules/customers/components/detail/create/DealTipsCard.js.map +7 -0
- package/dist/modules/customers/components/detail/create/PipelineSelect.js +41 -0
- package/dist/modules/customers/components/detail/create/PipelineSelect.js.map +7 -0
- package/dist/modules/customers/components/detail/create/PipelineStageSelect.js +49 -0
- package/dist/modules/customers/components/detail/create/PipelineStageSelect.js.map +7 -0
- package/dist/modules/customers/components/detail/create/SuffixInput.js +21 -0
- package/dist/modules/customers/components/detail/create/SuffixInput.js.map +7 -0
- package/dist/modules/customers/components/detail/create/dealCustomFieldControl.js +270 -0
- package/dist/modules/customers/components/detail/create/dealCustomFieldControl.js.map +7 -0
- package/dist/modules/customers/components/detail/create/dealFormTypes.js +17 -0
- package/dist/modules/customers/components/detail/create/dealFormTypes.js.map +7 -0
- package/dist/modules/customers/components/detail/create/dealNumericInput.js +16 -0
- package/dist/modules/customers/components/detail/create/dealNumericInput.js.map +7 -0
- package/dist/modules/customers/components/detail/create/useDealCustomFields.js +93 -0
- package/dist/modules/customers/components/detail/create/useDealCustomFields.js.map +7 -0
- package/dist/modules/customers/components/detail/create/useDealPipelines.js +59 -0
- package/dist/modules/customers/components/detail/create/useDealPipelines.js.map +7 -0
- package/dist/modules/customers/components/formConfig.js +4 -2
- package/dist/modules/customers/components/formConfig.js.map +2 -2
- package/dist/modules/dictionaries/components/DictionaryEntrySelect.js +5 -2
- package/dist/modules/dictionaries/components/DictionaryEntrySelect.js.map +2 -2
- package/dist/modules/feature_toggles/lib/feature-flag-check.js +13 -5
- package/dist/modules/feature_toggles/lib/feature-flag-check.js.map +2 -2
- package/dist/modules/query_index/subscribers/coverage_refresh.js +6 -1
- package/dist/modules/query_index/subscribers/coverage_refresh.js.map +2 -2
- package/dist/modules/workflows/components/WorkflowGraph.js +29 -186
- package/dist/modules/workflows/components/WorkflowGraph.js.map +2 -2
- package/dist/modules/workflows/components/WorkflowGraphImpl.js +196 -0
- package/dist/modules/workflows/components/WorkflowGraphImpl.js.map +7 -0
- package/package.json +8 -9
- package/src/global.d.ts +9 -0
- package/src/modules/catalog/commands/variants.ts +14 -5
- package/src/modules/customers/backend/customers/deals/create/page.tsx +3 -64
- package/src/modules/customers/components/detail/DealForm.tsx +2 -0
- package/src/modules/customers/components/detail/create/CreateDealForm.tsx +254 -0
- package/src/modules/customers/components/detail/create/DealAssociationsField.tsx +253 -0
- package/src/modules/customers/components/detail/create/DealAssociationsSection.tsx +72 -0
- package/src/modules/customers/components/detail/create/DealCreateSidebar.tsx +79 -0
- package/src/modules/customers/components/detail/create/DealCurrencyField.tsx +108 -0
- package/src/modules/customers/components/detail/create/DealCustomAttributes.tsx +118 -0
- package/src/modules/customers/components/detail/create/DealDetailsFields.tsx +171 -0
- package/src/modules/customers/components/detail/create/DealFormField.tsx +39 -0
- package/src/modules/customers/components/detail/create/DealSectionCard.tsx +40 -0
- package/src/modules/customers/components/detail/create/DealTipsCard.tsx +26 -0
- package/src/modules/customers/components/detail/create/PipelineSelect.tsx +55 -0
- package/src/modules/customers/components/detail/create/PipelineStageSelect.tsx +70 -0
- package/src/modules/customers/components/detail/create/SuffixInput.tsx +20 -0
- package/src/modules/customers/components/detail/create/dealCustomFieldControl.tsx +310 -0
- package/src/modules/customers/components/detail/create/dealFormTypes.ts +29 -0
- package/src/modules/customers/components/detail/create/dealNumericInput.ts +20 -0
- package/src/modules/customers/components/detail/create/useDealCustomFields.ts +118 -0
- package/src/modules/customers/components/detail/create/useDealPipelines.ts +80 -0
- package/src/modules/customers/components/formConfig.tsx +3 -0
- package/src/modules/customers/i18n/de.json +26 -0
- package/src/modules/customers/i18n/en.json +26 -0
- package/src/modules/customers/i18n/es.json +26 -0
- package/src/modules/customers/i18n/pl.json +26 -0
- package/src/modules/dictionaries/components/DictionaryEntrySelect.tsx +12 -1
- package/src/modules/feature_toggles/lib/feature-flag-check.ts +14 -4
- package/src/modules/query_index/subscribers/coverage_refresh.ts +7 -1
- package/src/modules/resources/i18n/de.json +1 -0
- package/src/modules/resources/i18n/en.json +1 -0
- package/src/modules/resources/i18n/es.json +1 -0
- package/src/modules/resources/i18n/pl.json +1 -0
- package/src/modules/sales/i18n/de.json +2 -0
- package/src/modules/sales/i18n/en.json +2 -0
- package/src/modules/sales/i18n/es.json +2 -0
- package/src/modules/sales/i18n/pl.json +2 -0
- package/src/modules/workflows/components/WorkflowGraph.tsx +39 -235
- package/src/modules/workflows/components/WorkflowGraphImpl.tsx +233 -0
|
@@ -665,8 +665,34 @@
|
|
|
665
665
|
"customers.deal_analyzer.sheet.dock": "Anclar al lado",
|
|
666
666
|
"customers.deal_analyzer.sheet.title": "Analizador de negocios",
|
|
667
667
|
"customers.deal_analyzer.sheet.welcomeTitle": "Analizador de negocios",
|
|
668
|
+
"customers.deals.create.associations.companiesPlaceholder": "Buscar empresas por nombre o dominio…",
|
|
669
|
+
"customers.deals.create.associations.peoplePlaceholder": "Buscar personas por nombre o correo…",
|
|
670
|
+
"customers.deals.create.back": "Volver a oportunidades",
|
|
671
|
+
"customers.deals.create.cancel": "Cancelar",
|
|
668
672
|
"customers.deals.create.error": "No se pudo crear la oportunidad.",
|
|
673
|
+
"customers.deals.create.fields.datePlaceholder": "Elegir una fecha",
|
|
674
|
+
"customers.deals.create.fields.expectedCloseAt": "Fecha de cierre prevista",
|
|
675
|
+
"customers.deals.create.fields.probability": "Probabilidad",
|
|
676
|
+
"customers.deals.create.fields.stageOf": "· etapa {position} de {total}",
|
|
677
|
+
"customers.deals.create.fields.title": "Título de la oportunidad",
|
|
678
|
+
"customers.deals.create.fields.valueAmount": "Valor de la oportunidad",
|
|
679
|
+
"customers.deals.create.hints.pipelineStage": "Las etapas dependen del pipeline seleccionado",
|
|
680
|
+
"customers.deals.create.hints.probability": "0 – 100 %, usada para el valor ponderado del pipeline",
|
|
681
|
+
"customers.deals.create.hints.title": "Nombre corto y descriptivo que se muestra en las tarjetas del pipeline",
|
|
682
|
+
"customers.deals.create.hints.valueAmount": "Ingresos potenciales de esta oportunidad",
|
|
683
|
+
"customers.deals.create.sections.associations.subtitle": "Vincula personas y empresas a esta oportunidad",
|
|
684
|
+
"customers.deals.create.sections.associations.title": "Asociaciones",
|
|
685
|
+
"customers.deals.create.sections.custom.empty": "Aún no hay campos personalizados definidos para oportunidades.",
|
|
686
|
+
"customers.deals.create.sections.custom.loading": "Cargando campos personalizados…",
|
|
687
|
+
"customers.deals.create.sections.custom.manage": "Gestionar campos",
|
|
688
|
+
"customers.deals.create.sections.custom.subtitle": "{count} campos definidos para este tenant",
|
|
689
|
+
"customers.deals.create.sections.custom.title": "Atributos personalizados",
|
|
690
|
+
"customers.deals.create.sections.details.subtitle": "Información principal de la oportunidad",
|
|
669
691
|
"customers.deals.create.submit": "Crear oportunidad",
|
|
692
|
+
"customers.deals.create.tips.item1": "Usa el formato nombre de la empresa + entregable corto en el título (p. ej. \"Copperleaf — Q3 Renewal\")",
|
|
693
|
+
"customers.deals.create.tips.item2": "Define la probabilidad según la etapa del pipeline: Calificación 10-25 %, Propuesta 30-50 %, Negociación 50-75 %, Contrato 75-90 %",
|
|
694
|
+
"customers.deals.create.tips.item3": "Vincula al principal responsable de la decisión como primera persona — recibe la copia (CC) por defecto en las actividades",
|
|
695
|
+
"customers.deals.create.tips.title": "Consejos para mejores oportunidades",
|
|
670
696
|
"customers.deals.create.title": "Crear oportunidad",
|
|
671
697
|
"customers.deals.detail.actions.apply": "Apply",
|
|
672
698
|
"customers.deals.detail.actions.backToList": "Volver a los deals",
|
|
@@ -665,8 +665,34 @@
|
|
|
665
665
|
"customers.deal_analyzer.sheet.dock": "Zadokuj z boku",
|
|
666
666
|
"customers.deal_analyzer.sheet.title": "Analizator transakcji",
|
|
667
667
|
"customers.deal_analyzer.sheet.welcomeTitle": "Analizator transakcji",
|
|
668
|
+
"customers.deals.create.associations.companiesPlaceholder": "Szukaj firm po nazwie lub domenie…",
|
|
669
|
+
"customers.deals.create.associations.peoplePlaceholder": "Szukaj osób po nazwie lub e-mailu…",
|
|
670
|
+
"customers.deals.create.back": "Powrót do szans sprzedaży",
|
|
671
|
+
"customers.deals.create.cancel": "Anuluj",
|
|
668
672
|
"customers.deals.create.error": "Nie udało się utworzyć szansy.",
|
|
673
|
+
"customers.deals.create.fields.datePlaceholder": "Wybierz datę",
|
|
674
|
+
"customers.deals.create.fields.expectedCloseAt": "Przewidywana data zamknięcia",
|
|
675
|
+
"customers.deals.create.fields.probability": "Prawdopodobieństwo",
|
|
676
|
+
"customers.deals.create.fields.stageOf": "· etap {position} z {total}",
|
|
677
|
+
"customers.deals.create.fields.title": "Tytuł szansy",
|
|
678
|
+
"customers.deals.create.fields.valueAmount": "Wartość szansy",
|
|
679
|
+
"customers.deals.create.hints.pipelineStage": "Etapy zależą od wybranego lejka",
|
|
680
|
+
"customers.deals.create.hints.probability": "0 – 100%, używane do ważonej wartości lejka",
|
|
681
|
+
"customers.deals.create.hints.title": "Krótka, opisowa nazwa wyświetlana na kartach lejka",
|
|
682
|
+
"customers.deals.create.hints.valueAmount": "Potencjalny przychód z tej szansy",
|
|
683
|
+
"customers.deals.create.sections.associations.subtitle": "Powiąż osoby i firmy z tą szansą",
|
|
684
|
+
"customers.deals.create.sections.associations.title": "Powiązania",
|
|
685
|
+
"customers.deals.create.sections.custom.empty": "Nie zdefiniowano jeszcze pól niestandardowych dla szans sprzedaży.",
|
|
686
|
+
"customers.deals.create.sections.custom.loading": "Ładowanie pól niestandardowych…",
|
|
687
|
+
"customers.deals.create.sections.custom.manage": "Zarządzaj polami",
|
|
688
|
+
"customers.deals.create.sections.custom.subtitle": "{count} pól zdefiniowanych dla tego najemcy",
|
|
689
|
+
"customers.deals.create.sections.custom.title": "Atrybuty niestandardowe",
|
|
690
|
+
"customers.deals.create.sections.details.subtitle": "Podstawowe informacje o szansie",
|
|
669
691
|
"customers.deals.create.submit": "Utwórz szansę",
|
|
692
|
+
"customers.deals.create.tips.item1": "Użyj w tytule formatu nazwa firmy + krótki rezultat (np. \"Copperleaf — Q3 Renewal\")",
|
|
693
|
+
"customers.deals.create.tips.item2": "Ustaw prawdopodobieństwo na podstawie etapu lejka: Kwalifikacja 10-25%, Oferta 30-50%, Negocjacje 50-75%, Umowa 75-90%",
|
|
694
|
+
"customers.deals.create.tips.item3": "Powiąż głównego decydenta jako pierwszą osobę — domyślnie otrzyma kopię (CC) e-maili przy aktywnościach",
|
|
695
|
+
"customers.deals.create.tips.title": "Wskazówki dla lepszych szans sprzedaży",
|
|
670
696
|
"customers.deals.create.title": "Utwórz szansę",
|
|
671
697
|
"customers.deals.detail.actions.apply": "Apply",
|
|
672
698
|
"customers.deals.detail.actions.backToList": "Powrót do listy szans",
|
|
@@ -70,6 +70,7 @@ export type DictionarySelectLabels = {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
export type DictionaryEntrySelectProps = {
|
|
73
|
+
id?: string
|
|
73
74
|
value?: string
|
|
74
75
|
onChange: (value: string | undefined) => void
|
|
75
76
|
fetchOptions: () => Promise<DictionaryOption[]>
|
|
@@ -83,9 +84,17 @@ export type DictionaryEntrySelectProps = {
|
|
|
83
84
|
disabled?: boolean
|
|
84
85
|
showLabelInput?: boolean
|
|
85
86
|
showManage?: boolean
|
|
87
|
+
/**
|
|
88
|
+
* When false, hides the read-only appearance preview (color swatch + icon + hex)
|
|
89
|
+
* rendered below the trigger for the currently-selected entry. Defaults to true to
|
|
90
|
+
* preserve existing behavior; set false where the host only wants a plain select
|
|
91
|
+
* (e.g. a create form that shouldn't surface dictionary styling).
|
|
92
|
+
*/
|
|
93
|
+
showActiveAppearance?: boolean
|
|
86
94
|
}
|
|
87
95
|
|
|
88
96
|
export function DictionaryEntrySelect({
|
|
97
|
+
id,
|
|
89
98
|
value,
|
|
90
99
|
onChange,
|
|
91
100
|
fetchOptions,
|
|
@@ -99,6 +108,7 @@ export function DictionaryEntrySelect({
|
|
|
99
108
|
disabled: disabledProp = false,
|
|
100
109
|
showLabelInput = true,
|
|
101
110
|
showManage = true,
|
|
111
|
+
showActiveAppearance = true,
|
|
102
112
|
}: DictionaryEntrySelectProps) {
|
|
103
113
|
const pathname = usePathname()
|
|
104
114
|
const searchParams = useSearchParams()
|
|
@@ -247,6 +257,7 @@ export function DictionaryEntrySelect({
|
|
|
247
257
|
disabled={disabled}
|
|
248
258
|
>
|
|
249
259
|
<SelectTrigger
|
|
260
|
+
id={id}
|
|
250
261
|
className={selectClassName}
|
|
251
262
|
title={activeOption?.label ?? undefined}
|
|
252
263
|
>
|
|
@@ -345,7 +356,7 @@ export function DictionaryEntrySelect({
|
|
|
345
356
|
) : null}
|
|
346
357
|
</div>
|
|
347
358
|
</div>
|
|
348
|
-
{activeOption && (activeOption.icon || activeOption.color) ? (
|
|
359
|
+
{showActiveAppearance && activeOption && (activeOption.icon || activeOption.color) ? (
|
|
349
360
|
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
|
350
361
|
<span className="inline-flex items-center gap-2 rounded border border-dashed px-2 py-1">
|
|
351
362
|
{activeOption.icon ? renderDictionaryIcon(activeOption.icon, 'h-4 w-4') : null}
|
|
@@ -62,6 +62,13 @@ const getCacheTags = (identifier: string, tenantId: string) => {
|
|
|
62
62
|
|
|
63
63
|
export class FeatureTogglesService {
|
|
64
64
|
private cacheTtlMs: number = 1 * 60 * 1000 // 1 minute
|
|
65
|
+
// Resolution cache can be disabled via env (e.g. integration tests that flip
|
|
66
|
+
// overrides rapidly between cases). The 1-minute TTL is a production
|
|
67
|
+
// optimization; under fast flag churn it can serve a stale value across
|
|
68
|
+
// override set/clear despite invalidation, so tests opt out for determinism.
|
|
69
|
+
private readonly cacheDisabled: boolean =
|
|
70
|
+
process.env.OM_FEATURE_TOGGLES_CACHE_DISABLED === '1' ||
|
|
71
|
+
process.env.OM_FEATURE_TOGGLES_CACHE_DISABLED === 'true'
|
|
65
72
|
constructor(
|
|
66
73
|
private readonly cache: CacheService,
|
|
67
74
|
private readonly em: EntityManager
|
|
@@ -72,6 +79,7 @@ export class FeatureTogglesService {
|
|
|
72
79
|
tenantId: string,
|
|
73
80
|
result: ToggleResolutionResult,
|
|
74
81
|
) {
|
|
82
|
+
if (this.cacheDisabled) return
|
|
75
83
|
const key = getIsEnabledCacheKey(identifier, tenantId)
|
|
76
84
|
await runWithCacheTenant(
|
|
77
85
|
tenantId,
|
|
@@ -82,10 +90,12 @@ export class FeatureTogglesService {
|
|
|
82
90
|
private async resolveToggle(identifier: string, tenantId: string): Promise<ToggleResolutionResult> {
|
|
83
91
|
const key = getIsEnabledCacheKey(identifier, tenantId)
|
|
84
92
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
93
|
+
if (!this.cacheDisabled) {
|
|
94
|
+
const cached = await runWithCacheTenant(tenantId, () => this.cache.get(key))
|
|
95
|
+
if (cached) {
|
|
96
|
+
const parsed = toCachedResolution(cached)
|
|
97
|
+
if (parsed) return parsed
|
|
98
|
+
}
|
|
89
99
|
}
|
|
90
100
|
|
|
91
101
|
let toggle: FeatureToggle | null = null
|
|
@@ -15,6 +15,12 @@ type Payload = {
|
|
|
15
15
|
const DEFAULT_DELAY_MS = 0
|
|
16
16
|
const pending = new Map<string, NodeJS.Timeout>()
|
|
17
17
|
|
|
18
|
+
function forkRefreshEntityManager(em: EntityManager): EntityManager {
|
|
19
|
+
const fork = (em as unknown as { fork?: (options?: Record<string, unknown>) => EntityManager }).fork
|
|
20
|
+
if (typeof fork !== 'function') return em
|
|
21
|
+
return fork.call(em, { clear: true, freshEventManager: true, useContext: false })
|
|
22
|
+
}
|
|
23
|
+
|
|
18
24
|
function scopeKey(input: Payload): string {
|
|
19
25
|
const entity = String(input.entityType || '')
|
|
20
26
|
const tenant = input.tenantId ?? '__null__'
|
|
@@ -35,10 +41,10 @@ export default async function handle(payload: Payload, ctx: { resolve: <T = any>
|
|
|
35
41
|
const withDeleted = payload?.withDeleted === true
|
|
36
42
|
const delayMs = typeof payload?.delayMs === 'number' && payload.delayMs >= 0 ? payload.delayMs : DEFAULT_DELAY_MS
|
|
37
43
|
|
|
38
|
-
const em = ctx.resolve<EntityManager>('em')
|
|
39
44
|
const key = scopeKey({ entityType, tenantId, organizationId, withDeleted })
|
|
40
45
|
|
|
41
46
|
const handleRefresh = async () => {
|
|
47
|
+
const em = forkRefreshEntityManager(ctx.resolve<EntityManager>('em'))
|
|
42
48
|
try {
|
|
43
49
|
await refreshCoverageSnapshot(em, { entityType, tenantId, organizationId, withDeleted })
|
|
44
50
|
} catch (err) {
|
|
@@ -199,6 +199,7 @@
|
|
|
199
199
|
"resources.resourceTypes.errors.delete": "Ressourcentyp konnte nicht gelöscht werden.",
|
|
200
200
|
"resources.resourceTypes.errors.deleteAssigned": "Dem Ressourcentyp sind Ressourcen zugewiesen.",
|
|
201
201
|
"resources.resourceTypes.errors.load": "Ressourcentypen konnten nicht geladen werden.",
|
|
202
|
+
"resources.resourceTypes.errors.notFound": "Ressourcentyp nicht gefunden.",
|
|
202
203
|
"resources.resourceTypes.errors.save": "Ressourcentyp konnte nicht gespeichert werden.",
|
|
203
204
|
"resources.resourceTypes.form.appearance.colorClear": "Farbe entfernen",
|
|
204
205
|
"resources.resourceTypes.form.appearance.colorHelp": "Wähle eine Farbe für diesen Ressourcentyp.",
|
|
@@ -199,6 +199,7 @@
|
|
|
199
199
|
"resources.resourceTypes.errors.delete": "Failed to delete resource type.",
|
|
200
200
|
"resources.resourceTypes.errors.deleteAssigned": "Resource type has assigned resources.",
|
|
201
201
|
"resources.resourceTypes.errors.load": "Failed to load resource types.",
|
|
202
|
+
"resources.resourceTypes.errors.notFound": "Resource type not found.",
|
|
202
203
|
"resources.resourceTypes.errors.save": "Failed to save resource type.",
|
|
203
204
|
"resources.resourceTypes.form.appearance.colorClear": "Clear color",
|
|
204
205
|
"resources.resourceTypes.form.appearance.colorHelp": "Pick a color for this resource type.",
|
|
@@ -199,6 +199,7 @@
|
|
|
199
199
|
"resources.resourceTypes.errors.delete": "No se pudo eliminar el tipo de recurso.",
|
|
200
200
|
"resources.resourceTypes.errors.deleteAssigned": "El tipo de recurso tiene recursos asignados.",
|
|
201
201
|
"resources.resourceTypes.errors.load": "No se pudieron cargar los tipos de recurso.",
|
|
202
|
+
"resources.resourceTypes.errors.notFound": "Tipo de recurso no encontrado.",
|
|
202
203
|
"resources.resourceTypes.errors.save": "No se pudo guardar el tipo de recurso.",
|
|
203
204
|
"resources.resourceTypes.form.appearance.colorClear": "Quitar color",
|
|
204
205
|
"resources.resourceTypes.form.appearance.colorHelp": "Elige un color para este tipo de recurso.",
|
|
@@ -199,6 +199,7 @@
|
|
|
199
199
|
"resources.resourceTypes.errors.delete": "Nie udało się usunąć typu zasobu.",
|
|
200
200
|
"resources.resourceTypes.errors.deleteAssigned": "Ten typ zasobu ma przypisane zasoby.",
|
|
201
201
|
"resources.resourceTypes.errors.load": "Nie udało się wczytać typów zasobów.",
|
|
202
|
+
"resources.resourceTypes.errors.notFound": "Nie znaleziono typu zasobu.",
|
|
202
203
|
"resources.resourceTypes.errors.save": "Nie udało się zapisać typu zasobu.",
|
|
203
204
|
"resources.resourceTypes.form.appearance.colorClear": "Wyczyść kolor",
|
|
204
205
|
"resources.resourceTypes.form.appearance.colorHelp": "Wybierz kolor dla tego typu zasobu.",
|
|
@@ -69,6 +69,7 @@
|
|
|
69
69
|
"sales.channels.form.errors.create": "Kanal konnte nicht erstellt werden.",
|
|
70
70
|
"sales.channels.form.errors.delete": "Kanal konnte nicht gelöscht werden.",
|
|
71
71
|
"sales.channels.form.errors.load": "Kanal konnte nicht geladen werden.",
|
|
72
|
+
"sales.channels.form.errors.notFound": "Kanal nicht gefunden.",
|
|
72
73
|
"sales.channels.form.errors.update": "Kanal konnte nicht gespeichert werden.",
|
|
73
74
|
"sales.channels.form.groups.address": "Standort",
|
|
74
75
|
"sales.channels.form.groups.contact": "Kontakt",
|
|
@@ -96,6 +97,7 @@
|
|
|
96
97
|
"sales.channels.offers.errors.duplicateProduct": "Dieses Produkt hat bereits ein Angebot in diesem Kanal.",
|
|
97
98
|
"sales.channels.offers.errors.load": "Angebote konnten nicht geladen werden.",
|
|
98
99
|
"sales.channels.offers.errors.loadOffer": "Angebot konnte nicht geladen werden.",
|
|
100
|
+
"sales.channels.offers.errors.notFound": "Angebot nicht gefunden.",
|
|
99
101
|
"sales.channels.offers.errors.priceKindDuplicate": "Jede Preisart kann nur einmal überschrieben werden.",
|
|
100
102
|
"sales.channels.offers.errors.priceKindRequired": "Wähle für jede Preisüberschreibung eine Preisart.",
|
|
101
103
|
"sales.channels.offers.errors.removePrice": "Preisüberschreibung konnte nicht entfernt werden.",
|
|
@@ -69,6 +69,7 @@
|
|
|
69
69
|
"sales.channels.form.errors.create": "Failed to create channel.",
|
|
70
70
|
"sales.channels.form.errors.delete": "Failed to delete channel.",
|
|
71
71
|
"sales.channels.form.errors.load": "Failed to load channel.",
|
|
72
|
+
"sales.channels.form.errors.notFound": "Channel not found.",
|
|
72
73
|
"sales.channels.form.errors.update": "Failed to save channel.",
|
|
73
74
|
"sales.channels.form.groups.address": "Location",
|
|
74
75
|
"sales.channels.form.groups.contact": "Contact",
|
|
@@ -96,6 +97,7 @@
|
|
|
96
97
|
"sales.channels.offers.errors.duplicateProduct": "This product already has an offer in this channel.",
|
|
97
98
|
"sales.channels.offers.errors.load": "Failed to load offers.",
|
|
98
99
|
"sales.channels.offers.errors.loadOffer": "Failed to load offer.",
|
|
100
|
+
"sales.channels.offers.errors.notFound": "Offer not found.",
|
|
99
101
|
"sales.channels.offers.errors.priceKindDuplicate": "Each price kind can only be overridden once.",
|
|
100
102
|
"sales.channels.offers.errors.priceKindRequired": "Select a price kind for each price override.",
|
|
101
103
|
"sales.channels.offers.errors.removePrice": "Failed to remove price override.",
|
|
@@ -69,6 +69,7 @@
|
|
|
69
69
|
"sales.channels.form.errors.create": "No se pudo crear el canal.",
|
|
70
70
|
"sales.channels.form.errors.delete": "No se pudo eliminar el canal.",
|
|
71
71
|
"sales.channels.form.errors.load": "No se pudo cargar el canal.",
|
|
72
|
+
"sales.channels.form.errors.notFound": "Canal no encontrado.",
|
|
72
73
|
"sales.channels.form.errors.update": "No se pudo guardar el canal.",
|
|
73
74
|
"sales.channels.form.groups.address": "Ubicación",
|
|
74
75
|
"sales.channels.form.groups.contact": "Contacto",
|
|
@@ -96,6 +97,7 @@
|
|
|
96
97
|
"sales.channels.offers.errors.duplicateProduct": "Este producto ya tiene una oferta en este canal.",
|
|
97
98
|
"sales.channels.offers.errors.load": "No se pudieron cargar las ofertas.",
|
|
98
99
|
"sales.channels.offers.errors.loadOffer": "No se pudo cargar la oferta.",
|
|
100
|
+
"sales.channels.offers.errors.notFound": "Oferta no encontrada.",
|
|
99
101
|
"sales.channels.offers.errors.priceKindDuplicate": "Cada tipo de precio solo se puede anular una vez.",
|
|
100
102
|
"sales.channels.offers.errors.priceKindRequired": "Selecciona un tipo de precio para cada precio personalizado.",
|
|
101
103
|
"sales.channels.offers.errors.removePrice": "No se pudo eliminar el precio personalizado.",
|
|
@@ -69,6 +69,7 @@
|
|
|
69
69
|
"sales.channels.form.errors.create": "Nie udało się utworzyć kanału.",
|
|
70
70
|
"sales.channels.form.errors.delete": "Nie udało się usunąć kanału.",
|
|
71
71
|
"sales.channels.form.errors.load": "Nie udało się wczytać kanału.",
|
|
72
|
+
"sales.channels.form.errors.notFound": "Nie znaleziono kanału.",
|
|
72
73
|
"sales.channels.form.errors.update": "Nie udało się zapisać kanału.",
|
|
73
74
|
"sales.channels.form.groups.address": "Lokalizacja",
|
|
74
75
|
"sales.channels.form.groups.contact": "Kontakt",
|
|
@@ -96,6 +97,7 @@
|
|
|
96
97
|
"sales.channels.offers.errors.duplicateProduct": "Ten produkt ma już ofertę w tym kanale.",
|
|
97
98
|
"sales.channels.offers.errors.load": "Nie udało się wczytać ofert.",
|
|
98
99
|
"sales.channels.offers.errors.loadOffer": "Nie udało się wczytać oferty.",
|
|
100
|
+
"sales.channels.offers.errors.notFound": "Nie znaleziono oferty.",
|
|
99
101
|
"sales.channels.offers.errors.priceKindDuplicate": "Każdy typ ceny można nadpisać tylko raz.",
|
|
100
102
|
"sales.channels.offers.errors.priceKindRequired": "Wybierz rodzaj ceny dla każdego nadpisania.",
|
|
101
103
|
"sales.channels.offers.errors.removePrice": "Nie udało się usunąć nadpisania ceny.",
|
|
@@ -1,32 +1,9 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
Edge,
|
|
8
|
-
Controls,
|
|
9
|
-
Background,
|
|
10
|
-
BackgroundVariant,
|
|
11
|
-
MiniMap,
|
|
12
|
-
Panel,
|
|
13
|
-
useNodesState,
|
|
14
|
-
useEdgesState,
|
|
15
|
-
addEdge,
|
|
16
|
-
Connection,
|
|
17
|
-
ConnectionMode,
|
|
18
|
-
MarkerType,
|
|
19
|
-
} from '@xyflow/react'
|
|
20
|
-
import {StartNode, EndNode, UserTaskNode, AutomatedNode, SubWorkflowNode, WaitForSignalNode, WaitForTimerNode} from './nodes'
|
|
21
|
-
import { WorkflowTransitionEdge } from './WorkflowTransitionEdge'
|
|
22
|
-
import { STATUS_COLORS } from '../lib/status-colors'
|
|
23
|
-
import { Alert, AlertDescription } from '@open-mercato/ui/primitives/alert'
|
|
24
|
-
import { Edit3 } from 'lucide-react'
|
|
25
|
-
import { useTheme } from '@open-mercato/ui/theme'
|
|
26
|
-
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
27
|
-
|
|
28
|
-
// NOTE: ReactFlow styles should be imported in the page that uses this component
|
|
29
|
-
// or in a global CSS file. Import: '@xyflow/react/dist/style.css'
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import dynamic from 'next/dynamic'
|
|
5
|
+
import type { Node, Edge, Connection } from '@xyflow/react'
|
|
6
|
+
import { Spinner } from '@open-mercato/ui/primitives/spinner'
|
|
30
7
|
|
|
31
8
|
export interface WorkflowGraphProps {
|
|
32
9
|
initialNodes?: Node[]
|
|
@@ -41,224 +18,51 @@ export interface WorkflowGraphProps {
|
|
|
41
18
|
height?: string
|
|
42
19
|
}
|
|
43
20
|
|
|
21
|
+
const WorkflowGraphImpl = dynamic(() => import('./WorkflowGraphImpl'), {
|
|
22
|
+
ssr: false,
|
|
23
|
+
loading: () => null,
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
function WorkflowGraphPlaceholder({ height }: { height: string }) {
|
|
27
|
+
return (
|
|
28
|
+
<div
|
|
29
|
+
className="workflow-graph-container flex items-center justify-center rounded-lg border border-border bg-muted/30"
|
|
30
|
+
style={{ height }}
|
|
31
|
+
>
|
|
32
|
+
<Spinner className="h-6 w-6 text-muted-foreground" />
|
|
33
|
+
</div>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
44
37
|
/**
|
|
45
|
-
* WorkflowGraph - ReactFlow wrapper
|
|
38
|
+
* WorkflowGraph — lazy-loaded ReactFlow wrapper.
|
|
46
39
|
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* - Mini-map for navigation
|
|
51
|
-
* - Optional editing capabilities
|
|
40
|
+
* @xyflow/react is loaded via next/dynamic({ ssr: false }) so the ~12 MB
|
|
41
|
+
* package only enters the Turbopack module graph when this component
|
|
42
|
+
* actually renders.
|
|
52
43
|
*/
|
|
53
|
-
export function WorkflowGraph({
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}: WorkflowGraphProps) {
|
|
65
|
-
const t = useT()
|
|
66
|
-
// Use ReactFlow hooks for node and edge state management
|
|
67
|
-
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes)
|
|
68
|
-
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges)
|
|
69
|
-
|
|
70
|
-
// Get theme for dark mode support
|
|
71
|
-
const { resolvedTheme } = useTheme()
|
|
72
|
-
const isDark = resolvedTheme === 'dark'
|
|
73
|
-
const backgroundDotColor = isDark ? '#374151' : '#e5e7eb'
|
|
74
|
-
const [isCompactViewport, setIsCompactViewport] = useState(false)
|
|
75
|
-
|
|
76
|
-
useEffect(() => {
|
|
77
|
-
if (typeof window === 'undefined') return
|
|
78
|
-
const mediaQuery = window.matchMedia('(max-width: 1279px)')
|
|
79
|
-
const updateViewportMode = () => setIsCompactViewport(mediaQuery.matches)
|
|
80
|
-
|
|
81
|
-
updateViewportMode()
|
|
82
|
-
mediaQuery.addEventListener('change', updateViewportMode)
|
|
83
|
-
|
|
44
|
+
export function WorkflowGraph(props: WorkflowGraphProps) {
|
|
45
|
+
const { height = '600px' } = props
|
|
46
|
+
// Track impl-chunk readiness so the loading placeholder respects the
|
|
47
|
+
// caller's `height` prop (next/dynamic's `loading` cannot access props).
|
|
48
|
+
// The browser caches the module, so the duplicate `import()` is free.
|
|
49
|
+
const [isImplReady, setIsImplReady] = React.useState(false)
|
|
50
|
+
React.useEffect(() => {
|
|
51
|
+
let cancelled = false
|
|
52
|
+
void import('./WorkflowGraphImpl').then(() => {
|
|
53
|
+
if (!cancelled) setIsImplReady(true)
|
|
54
|
+
})
|
|
84
55
|
return () => {
|
|
85
|
-
|
|
56
|
+
cancelled = true
|
|
86
57
|
}
|
|
87
58
|
}, [])
|
|
88
59
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
setNodes(initialNodes)
|
|
92
|
-
}, [initialNodes, setNodes])
|
|
93
|
-
|
|
94
|
-
useEffect(() => {
|
|
95
|
-
setEdges(initialEdges)
|
|
96
|
-
}, [initialEdges, setEdges])
|
|
97
|
-
|
|
98
|
-
// Handle connection between nodes (when user drags from one node to another)
|
|
99
|
-
const onConnect = useCallback(
|
|
100
|
-
(connection: Connection) => {
|
|
101
|
-
if (onConnectProp) {
|
|
102
|
-
// Let parent handle the connection
|
|
103
|
-
onConnectProp(connection)
|
|
104
|
-
} else {
|
|
105
|
-
// Fallback: handle internally if no parent callback
|
|
106
|
-
const newEdge = {
|
|
107
|
-
...connection,
|
|
108
|
-
type: 'workflowTransition',
|
|
109
|
-
animated: false,
|
|
110
|
-
markerEnd: {
|
|
111
|
-
type: MarkerType.ArrowClosed,
|
|
112
|
-
width: 16,
|
|
113
|
-
height: 16,
|
|
114
|
-
color: '#9ca3af',
|
|
115
|
-
},
|
|
116
|
-
}
|
|
117
|
-
setEdges((eds) => addEdge(newEdge, eds))
|
|
118
|
-
}
|
|
119
|
-
},
|
|
120
|
-
[setEdges, onConnectProp]
|
|
121
|
-
)
|
|
122
|
-
|
|
123
|
-
// Notify parent when nodes change
|
|
124
|
-
const handleNodesChange = useCallback(
|
|
125
|
-
(changes: any) => {
|
|
126
|
-
onNodesChange(changes)
|
|
127
|
-
if (onNodesChangeProp) {
|
|
128
|
-
onNodesChangeProp(changes)
|
|
129
|
-
}
|
|
130
|
-
},
|
|
131
|
-
[onNodesChange, onNodesChangeProp]
|
|
132
|
-
)
|
|
133
|
-
|
|
134
|
-
// Notify parent when edges change
|
|
135
|
-
const handleEdgesChange = useCallback(
|
|
136
|
-
(changes: any) => {
|
|
137
|
-
onEdgesChange(changes)
|
|
138
|
-
if (onEdgesChangeProp) {
|
|
139
|
-
onEdgesChangeProp(changes)
|
|
140
|
-
}
|
|
141
|
-
},
|
|
142
|
-
[onEdgesChange, onEdgesChangeProp]
|
|
143
|
-
)
|
|
144
|
-
|
|
145
|
-
// Register custom node types
|
|
146
|
-
const nodeTypes = useMemo(
|
|
147
|
-
() => ({
|
|
148
|
-
start: StartNode,
|
|
149
|
-
end: EndNode,
|
|
150
|
-
userTask: UserTaskNode,
|
|
151
|
-
automated: AutomatedNode,
|
|
152
|
-
subWorkflow: SubWorkflowNode,
|
|
153
|
-
waitForSignal: WaitForSignalNode,
|
|
154
|
-
waitForTimer: WaitForTimerNode,
|
|
155
|
-
}),
|
|
156
|
-
[]
|
|
157
|
-
)
|
|
158
|
-
|
|
159
|
-
// Register custom edge types
|
|
160
|
-
const edgeTypes = useMemo(
|
|
161
|
-
() => ({
|
|
162
|
-
workflowTransition: WorkflowTransitionEdge,
|
|
163
|
-
}),
|
|
164
|
-
[]
|
|
165
|
-
)
|
|
166
|
-
|
|
167
|
-
return (
|
|
168
|
-
<div className={`workflow-graph-container ${className}`} style={{ height }}>
|
|
169
|
-
<ReactFlow
|
|
170
|
-
nodes={nodes}
|
|
171
|
-
edges={edges}
|
|
172
|
-
nodeTypes={nodeTypes}
|
|
173
|
-
edgeTypes={edgeTypes}
|
|
174
|
-
onNodesChange={handleNodesChange}
|
|
175
|
-
onEdgesChange={handleEdgesChange}
|
|
176
|
-
onConnect={editable ? onConnect : undefined}
|
|
177
|
-
onNodeClick={onNodeClickProp}
|
|
178
|
-
onEdgeClick={onEdgeClickProp}
|
|
179
|
-
connectionMode={ConnectionMode.Loose}
|
|
180
|
-
fitView
|
|
181
|
-
fitViewOptions={{
|
|
182
|
-
padding: 0.2,
|
|
183
|
-
maxZoom: isCompactViewport ? 0.9 : 1,
|
|
184
|
-
}}
|
|
185
|
-
minZoom={0.1}
|
|
186
|
-
maxZoom={2}
|
|
187
|
-
defaultEdgeOptions={{
|
|
188
|
-
type: 'workflowTransition',
|
|
189
|
-
animated: false,
|
|
190
|
-
markerEnd: {
|
|
191
|
-
type: MarkerType.ArrowClosed,
|
|
192
|
-
width: 16,
|
|
193
|
-
height: 16,
|
|
194
|
-
color: '#9ca3af',
|
|
195
|
-
},
|
|
196
|
-
}}
|
|
197
|
-
nodesDraggable={editable}
|
|
198
|
-
nodesConnectable={editable}
|
|
199
|
-
elementsSelectable={editable}
|
|
200
|
-
proOptions={{ hideAttribution: true }}
|
|
201
|
-
>
|
|
202
|
-
{/* Background grid for visual reference */}
|
|
203
|
-
<Background
|
|
204
|
-
variant={BackgroundVariant.Dots}
|
|
205
|
-
gap={16}
|
|
206
|
-
size={1}
|
|
207
|
-
color={backgroundDotColor}
|
|
208
|
-
/>
|
|
209
|
-
|
|
210
|
-
{/* Zoom and pan controls */}
|
|
211
|
-
<Controls
|
|
212
|
-
showZoom={true}
|
|
213
|
-
showFitView={true}
|
|
214
|
-
showInteractive={false}
|
|
215
|
-
position={isCompactViewport ? 'bottom-right' : 'top-right'}
|
|
216
|
-
className={`!bg-card !border-border !shadow-md [&>button]:!bg-card [&>button]:!border-border [&>button]:!fill-foreground [&>button:hover]:!bg-muted ${isCompactViewport ? 'scale-90 origin-bottom-right' : ''}`}
|
|
217
|
-
/>
|
|
218
|
-
|
|
219
|
-
{/* Mini-map for navigation in large workflows */}
|
|
220
|
-
{!isCompactViewport && (
|
|
221
|
-
<MiniMap
|
|
222
|
-
nodeStrokeWidth={3}
|
|
223
|
-
nodeColor={(node) => {
|
|
224
|
-
// Color nodes by status - using status-based colors
|
|
225
|
-
const status = (node.data?.status || 'not_started') as keyof typeof STATUS_COLORS
|
|
226
|
-
return STATUS_COLORS[status]?.hex || STATUS_COLORS.not_started.hex
|
|
227
|
-
}}
|
|
228
|
-
maskColor="rgba(0, 0, 0, 0.1)"
|
|
229
|
-
position="bottom-left"
|
|
230
|
-
className="!bg-card !border !border-border !rounded-lg"
|
|
231
|
-
/>
|
|
232
|
-
)}
|
|
233
|
-
|
|
234
|
-
{/* Info panel */}
|
|
235
|
-
{!editable && !isCompactViewport && (
|
|
236
|
-
<Panel position="top-left" style={{ margin: 10 }}>
|
|
237
|
-
<div className="bg-card rounded-lg shadow-sm border border-border px-4 py-2">
|
|
238
|
-
<p className="text-sm text-muted-foreground font-medium">
|
|
239
|
-
{t('workflows.graph.visualization')}
|
|
240
|
-
</p>
|
|
241
|
-
</div>
|
|
242
|
-
</Panel>
|
|
243
|
-
)}
|
|
244
|
-
|
|
245
|
-
{editable && !isCompactViewport && (
|
|
246
|
-
<Panel position="top-left" style={{ margin: 10 }}>
|
|
247
|
-
<Alert variant="info" className="max-w-sm">
|
|
248
|
-
<Edit3 className="size-4" />
|
|
249
|
-
<AlertDescription className="font-medium">
|
|
250
|
-
{t('workflows.graph.editModeInfo')}
|
|
251
|
-
</AlertDescription>
|
|
252
|
-
</Alert>
|
|
253
|
-
</Panel>
|
|
254
|
-
)}
|
|
255
|
-
</ReactFlow>
|
|
256
|
-
</div>
|
|
257
|
-
)
|
|
60
|
+
if (!isImplReady) return <WorkflowGraphPlaceholder height={height} />
|
|
61
|
+
return <WorkflowGraphImpl {...props} />
|
|
258
62
|
}
|
|
259
63
|
|
|
260
64
|
/**
|
|
261
|
-
* WorkflowGraphReadOnly
|
|
65
|
+
* WorkflowGraphReadOnly — read-only viewer that reuses WorkflowGraph.
|
|
262
66
|
*/
|
|
263
67
|
export function WorkflowGraphReadOnly({
|
|
264
68
|
nodes,
|