Podminka se muze ptat na vic veci naraz
Podminka byla prave jedna otazka. Slozitejsi vetveni se muselo skladat
z vnorenych podminek, takze "vysledek dorazil" a v nem "vysledek je X" byly dve
urovne stromu misto jedne vety. U tri hodnot, ktere maji dopadnout stejne, to
byly tri urovne.
Model: rules (otazky) a match ('all' = a zaroven, 'any' = nebo). Stara podoba
fieldId/operator/value primo na kroku se dal cte, prevadi ji rulesOf - jedine
misto, kde se to deje. Kdyby se fieldId cetlo primo, krok ulozeny driv by po
zmene modelu prisel o svou otazku a vetvil by vzdycky stejne, tise a bez chyby.
Zapisuje se uz vzdycky rules.
fieldId je v typu nepovinne schvalne: prekladac tim ukazal vsech pet mist,
ktera podminku ctou (vyhodnoceni, obe hlasky do logu, kontrola pri ukladani,
kontrola nedodelku).
Proc jedna uroven a ne vyrazy se zavorkami: dve treti podminek jsou "vsechny
tohle" nebo "cokoliv z tohohle". Zavorky by v rozhrani znamenaly editor vyrazu,
ktery uz nikdo neuklika, a textovy zapis by navic zahodil odkaz na parametr pres
ID - diky nemu prejmenovani parametru podminku nerozbije a builder umi nabidnout
jen operatory, ktere na dany typ sedi. Az se ukaze, ze jedna uroven nestaci,
da se textovy zapis pridat nad tentyz vyhodnocovac. Opacne to nejde.
V builderu radek na otazku, tlacitko "Přidat otázku" a od druhe otazky prepinac
"sedí všechny" / "sedí aspoň jedna". Posledni otazka nejde smazat: podminka bez
otazky by tise nevetvila, runtime ji povazuje za nesplnenou a zaloguje to.
Radek podminky v logu nese vsechny otazky i s tim, ktera rozhodla - bez rozpadu
by u spojene podminky bylo videt jen "nesplneno".
Overeno na bezici instanci, strom se dvema spojenymi podminkami a peti volanimi:
vysledek nedorazil (nic), Chybějící informace (prirazeno), Přesměrování
a Mimo téma (zavreno pres any), Něco jiného (nic). Strom ulozeny ve stare
podobe se nacte a ulozi beze zmeny.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
1532ea0af7
commit
9f4b8c0596
@@ -222,7 +222,24 @@ result neq "Chybějící informace"
|
||||
znamena "vsechno ostatni **vcetne toho, co jeste nevime**". Prvni zprava hovoru
|
||||
vysledek nenese, podminka sedne a strom udela to, co mel udelat az na konci.
|
||||
|
||||
**Ptat se kladne a napred na existenci:** `isNotEmpty`, a teprve uvnitr `eq`.
|
||||
**Ptat se kladne a napred na existenci:** `isNotEmpty`, a teprve pak `eq`.
|
||||
|
||||
Od zari 2026 to nemusi byt dve vnorene podminky. Jedna podminka se muze ptat
|
||||
na **vic veci naraz**:
|
||||
|
||||
```ts
|
||||
match: 'all' // a zaroven
|
||||
rules: [
|
||||
{ fieldId: 'f_result', operator: 'isNotEmpty' },
|
||||
{ fieldId: 'f_result', operator: 'eq', value: 'Chybějící informace' },
|
||||
]
|
||||
```
|
||||
|
||||
`any` je "nebo", tedy tri hodnoty, ktere maji dopadnout stejne, na jednom radku
|
||||
misto tri urovni stromu. Je to **jedna uroven**, zavorky ne - viz zaznam zmen
|
||||
k 7. 9. 2026.
|
||||
|
||||
Stara podoba (`fieldId` primo na kroku) se dal cte, prevadi ji `rulesOf`.
|
||||
|
||||
Radek podminky v logu proto nese i to, s cim se porovnavalo, a rozlisuje
|
||||
`nedorazilo` od `prázdné`:
|
||||
|
||||
@@ -2,6 +2,75 @@
|
||||
|
||||
Nejnovejsi nahore.
|
||||
|
||||
## 2026-09-07 - Podminka se muze ptat na vic veci naraz
|
||||
|
||||
Podminka byla **prave jedna otazka**. Slozitejsi vetveni se muselo skladat
|
||||
z vnorenych podminek, takze "vysledek dorazil" a v nem "vysledek je X" byly dve
|
||||
urovne stromu misto jedne vety. U tri hodnot, ktere maji dopadnout stejne, to
|
||||
byly tri urovne, ve kterych se nikdo nevyzna.
|
||||
|
||||
### Model
|
||||
|
||||
```ts
|
||||
rules: ConditionRule[] // otazky
|
||||
match: 'all' | 'any' // a zaroven / nebo
|
||||
```
|
||||
|
||||
Stara podoba (`fieldId`, `operator`, `value` primo na kroku) se dal cte, prevadi
|
||||
ji `rulesOf` - **jedine misto, kde se to deje**. Kdyby se `fieldId` cetlo primo,
|
||||
krok ulozeny driv by po zmene modelu prisel o svou otazku a vetvil by vzdycky
|
||||
stejne, tise a bez chyby. Zapisuje se uz vzdycky `rules`.
|
||||
|
||||
Aby se na zadne cteni nezapomnelo, je `fieldId` v typu **nepovinne**. Prekladac
|
||||
tim ukazal vsech pet mist, ktera podminku ctou: vyhodnoceni, obe hlasky do logu,
|
||||
kontrola pri ukladani a kontrola nedodelku.
|
||||
|
||||
### Proc jedna uroven a ne vyrazy se zavorkami
|
||||
|
||||
Dve treti podminek jsou "vsechny tohle" nebo "cokoliv z tohohle". Zavorky by
|
||||
v rozhrani znamenaly editor vyrazu, ktery uz nikdo neuklika, a textovy zapis by
|
||||
navic zahodil to podstatne: **odkaz na parametr pres ID**. Diky nemu prejmenovani
|
||||
parametru podminku nerozbije a builder umi nabidnout jen operatory, ktere na dany
|
||||
typ sedi, a rovnou rict, ze se odkazuje na parametr, ktery vznika az pozdeji.
|
||||
|
||||
Az se ukaze, ze jedna uroven nestaci, da se textovy zapis pridat nad tentyz
|
||||
vyhodnocovac. Opacne to nejde.
|
||||
|
||||
### V builderu
|
||||
|
||||
Radek na otazku, k tomu tlacitko "Přidat otázku" a od druhe otazky prepinac
|
||||
**sedí všechny** nebo **sedí aspoň jedna**. U jedne otazky se prepinac nenabizi,
|
||||
nema co spojovat. Posledni otazka nejde smazat: podminka bez otazky by tise
|
||||
nevetvila, proto ji runtime rovnou povazuje za nesplnenou a zaloguje to.
|
||||
|
||||
### V logu
|
||||
|
||||
Radek podminky nese vsechny otazky i s tim, ktera rozhodla:
|
||||
|
||||
```text
|
||||
Podmínka: result isNotEmpty a zároveň result eq Chybějící informace: nesplněno
|
||||
result = "Přesměrování": sedí
|
||||
result = "Přesměrování", porovnáno s "Chybějící informace": nesedí
|
||||
```
|
||||
|
||||
Bez rozpadu by u spojene podminky bylo videt jen "nesplneno" a ne to, ktera
|
||||
otazka to zpusobila.
|
||||
|
||||
### Overeno na bezici instanci
|
||||
|
||||
Strom se dvema spojenymi podminkami, pet prichozich volani:
|
||||
|
||||
| Co prislo | Prirazeno | Vyrizeny |
|
||||
| --------------------------- | ------------- | -------- |
|
||||
| vysledek nedorazil | ne | ne |
|
||||
| Chybějící informace | ano | ne |
|
||||
| Přesměrování | ne | ano |
|
||||
| Mimo téma | ne | ano |
|
||||
| Něco jiného | ne | ne |
|
||||
|
||||
Prvni podminka je `all` (dorazil a zaroven je to Chybejici informace), druha
|
||||
`any` (Přesměrování nebo Vyřešeno nebo Mimo téma). Strom ulozeny ve stare podobe
|
||||
se nacte a ulozi beze zmeny.
|
||||
## 2026-09-07 - Automatizace zavirala tickety uz pri zvoneni
|
||||
|
||||
Na instanci nebyl ani jeden nevyrizeny ticket: **vsech 131 melo `closed: true`**,
|
||||
|
||||
+63
-19
@@ -11,7 +11,13 @@ import { config } from '../config.js';
|
||||
import { publish } from '../events/bus.js';
|
||||
import { defineStore } from './store/index.js';
|
||||
import { withMirror } from './store/mirror.js';
|
||||
import { isUnary, type ConditionOperator, type FieldType } from './conditions.js';
|
||||
import {
|
||||
isUnary,
|
||||
type ConditionMatch,
|
||||
type ConditionOperator,
|
||||
type ConditionRule,
|
||||
type FieldType,
|
||||
} from './conditions.js';
|
||||
import { actionInputsFor, findService } from './services.js';
|
||||
import { collectScopes, duplicateNames, scopeFor } from './flowScope.js';
|
||||
import { referencedFields, rootOf } from './templates.js';
|
||||
@@ -97,10 +103,24 @@ export type FlowStep =
|
||||
| {
|
||||
id: string;
|
||||
kind: 'condition';
|
||||
/** id parametru z trigger.fields */
|
||||
fieldId: string;
|
||||
operator: ConditionOperator;
|
||||
/** Chybi u operatoru, ktere hodnotu nepotrebuji (isEmpty, isTrue apod.). */
|
||||
/**
|
||||
* Otazky, ktere se ptaji naraz. Cte se **vzdycky pres `rulesOf`**.
|
||||
*
|
||||
* Driv byla podminka prave jedna otazka a slozitejsi vetveni se skladalo
|
||||
* z vnorenych podminek: "vysledek dorazil" a v nem "vysledek je X" byly
|
||||
* dve urovne stromu misto jedne vety.
|
||||
*/
|
||||
rules?: ConditionRule[];
|
||||
/** `all` = a zaroven, `any` = nebo. Chybi u jedne otazky, tam nema co spojovat. */
|
||||
match?: ConditionMatch;
|
||||
/**
|
||||
* Stara podoba: jedna otazka primo na kroku.
|
||||
*
|
||||
* Zaznamy ulozene driv ji maji misto `rules`. Prevadi ji `rulesOf`,
|
||||
* zapisuje se uz vzdycky `rules`.
|
||||
*/
|
||||
fieldId?: string;
|
||||
operator?: ConditionOperator;
|
||||
value?: string;
|
||||
yes: FlowStep[];
|
||||
no: FlowStep[];
|
||||
@@ -130,6 +150,24 @@ export interface AutomationFlow {
|
||||
steps: FlowStep[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Otazky podminky, at uz je krok v jakekoliv podobe.
|
||||
*
|
||||
* **Jedine misto, kde se stara podoba prevadi.** Kdyby se `fieldId` cetlo
|
||||
* primo, krok ulozeny driv by po zmene modelu prisel o svou otazku a vetvil
|
||||
* by vzdycky stejne - tise a bez chyby.
|
||||
*/
|
||||
export function rulesOf(step: Extract<FlowStep, { kind: 'condition' }>): ConditionRule[] {
|
||||
if (step.rules && step.rules.length > 0) return step.rules;
|
||||
if (!step.fieldId || !step.operator) return [];
|
||||
return [{ fieldId: step.fieldId, operator: step.operator, value: step.value }];
|
||||
}
|
||||
|
||||
/** Jak se otazky spoji. Jedna otazka nema co spojovat, bere se `all`. */
|
||||
export function matchOf(step: Extract<FlowStep, { kind: 'condition' }>): ConditionMatch {
|
||||
return step.match ?? 'all';
|
||||
}
|
||||
|
||||
/** Neodhadnutelny token do adresy webhooku (32 znaku, base64url). */
|
||||
export function generateWebhookToken(): string {
|
||||
return randomBytes(24).toString('base64url');
|
||||
@@ -463,20 +501,26 @@ export function collectFlowIssues(flow: AutomationFlow): string[] {
|
||||
continue;
|
||||
}
|
||||
|
||||
const field = available.find((candidate) => candidate.id === step.fieldId);
|
||||
if (!field) {
|
||||
// Rozlisujeme "neexistuje" od "vznikne az pozdeji". Druhy pripad nastane
|
||||
// po presunuti kroku a chce jinou radu nez smazat podminku.
|
||||
issues.push(
|
||||
scopes.all.has(step.fieldId)
|
||||
? 'Podmínka se ptá na parametr, který vzniká až v pozdějším kroku. Posuňte ji níž.'
|
||||
: 'Podmínka se odkazuje na parametr, který už neexistuje.',
|
||||
);
|
||||
} else if (!isUnary(step.operator) && (step.value ?? '').trim().length === 0) {
|
||||
issues.push(`Podmínka nad parametrem "${field.name}" nemá vyplněnou hodnotu.`);
|
||||
} else if (field.type === 'number' && !isUnary(step.operator)) {
|
||||
if (Number.isNaN(Number(step.value))) {
|
||||
issues.push(`Podmínka nad parametrem "${field.name}" má nečíselnou hodnotu.`);
|
||||
// Kazda otazka podminky zvlast. Jedna spatna nesmi schovat ostatni.
|
||||
const rules = rulesOf(step);
|
||||
if (rules.length === 0) issues.push('Podmínka nemá žádnou otázku.');
|
||||
|
||||
for (const rule of rules) {
|
||||
const field = available.find((candidate) => candidate.id === rule.fieldId);
|
||||
if (!field) {
|
||||
// Rozlisujeme "neexistuje" od "vznikne az pozdeji". Druhy pripad nastane
|
||||
// po presunuti kroku a chce jinou radu nez smazat podminku.
|
||||
issues.push(
|
||||
scopes.all.has(rule.fieldId)
|
||||
? 'Podmínka se ptá na parametr, který vzniká až v pozdějším kroku. Posuňte ji níž.'
|
||||
: 'Podmínka se odkazuje na parametr, který už neexistuje.',
|
||||
);
|
||||
} else if (!isUnary(rule.operator) && (rule.value ?? '').trim().length === 0) {
|
||||
issues.push(`Podmínka nad parametrem "${field.name}" nemá vyplněnou hodnotu.`);
|
||||
} else if (field.type === 'number' && !isUnary(rule.operator)) {
|
||||
if (Number.isNaN(Number(rule.value))) {
|
||||
issues.push(`Podmínka nad parametrem "${field.name}" má nečíselnou hodnotu.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -70,6 +70,30 @@ export const operatorsByType: Record<FieldType, ConditionOperator[]> = {
|
||||
list: ['contains', 'isEmpty', 'isNotEmpty'],
|
||||
};
|
||||
|
||||
/**
|
||||
* Jedna otazka v podmince.
|
||||
*
|
||||
* Driv byla podminka **prave jedna otazka** a slozitejsi vetveni se muselo
|
||||
* skladat z vnorenych podminek. U tri hodnot, ktere maji dopadnout stejne, to
|
||||
* znamenalo tri urovne stromu, ve kterych se nikdo nevyzna.
|
||||
*/
|
||||
export interface ConditionRule {
|
||||
/** ID parametru spoustece, nebo `krok.vystup`. */
|
||||
fieldId: string;
|
||||
operator: ConditionOperator;
|
||||
/** Chybi u operatoru, ktere hodnotu nepotrebuji (isEmpty, isTrue apod.). */
|
||||
value?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Jak se otazky spoji.
|
||||
*
|
||||
* `all` = a zaroven, `any` = nebo. Zamerne **jedna uroven**, ne strom se
|
||||
* zavorkami: dve treti podminek jsou "vsechny tohle" nebo "cokoliv z tohohle"
|
||||
* a zavorky by v rozhrani znamenaly editor vyrazu, ktery uz nikdo neuklika.
|
||||
*/
|
||||
export type ConditionMatch = 'all' | 'any';
|
||||
|
||||
/** Operatory, ktere nepotrebuji hodnotu k porovnani. */
|
||||
export const unaryOperators: ConditionOperator[] = [
|
||||
'isEmpty',
|
||||
|
||||
+42
-22
@@ -14,6 +14,7 @@ import {
|
||||
getAutomation,
|
||||
listAutomations,
|
||||
regenerateWebhookToken,
|
||||
rulesOf,
|
||||
updateAutomation,
|
||||
type FlowStep,
|
||||
} from '../data/automationStore.js';
|
||||
@@ -1083,6 +1084,29 @@ dashboardRouter.get('/services', async (req, res) => {
|
||||
|
||||
// ------------------------------------------------------------- automatizace
|
||||
|
||||
/** Operator podminky. Vyctem, ne volnym retezcem - neznamy by tise nevetvil. */
|
||||
const conditionOperatorSchema = z.enum([
|
||||
'eq',
|
||||
'neq',
|
||||
'gt',
|
||||
'gte',
|
||||
'lt',
|
||||
'lte',
|
||||
'contains',
|
||||
'startsWith',
|
||||
'isEmpty',
|
||||
'isNotEmpty',
|
||||
'isTrue',
|
||||
'isFalse',
|
||||
]);
|
||||
|
||||
/** Jedna otazka podminky. Podminka jich muze mit vic, spojene pres `match`. */
|
||||
const conditionRuleSchema = z.object({
|
||||
fieldId: z.string().min(1, 'Podmínka musí mít vybraný parametr.'),
|
||||
operator: conditionOperatorSchema,
|
||||
value: z.string().optional(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Rekurzivni schema kroku. z.lazy je nutne, protoze podminka obsahuje
|
||||
* dalsi kroky - bez toho by se typ odkazoval sam na sebe drive, nez existuje.
|
||||
@@ -1109,21 +1133,14 @@ const stepSchema: z.ZodType<FlowStep> = z.lazy(() =>
|
||||
z.object({
|
||||
id: z.string().min(1),
|
||||
kind: z.literal('condition'),
|
||||
fieldId: z.string().min(1, 'Podmínka musí mít vybraný parametr.'),
|
||||
operator: z.enum([
|
||||
'eq',
|
||||
'neq',
|
||||
'gt',
|
||||
'gte',
|
||||
'lt',
|
||||
'lte',
|
||||
'contains',
|
||||
'startsWith',
|
||||
'isEmpty',
|
||||
'isNotEmpty',
|
||||
'isTrue',
|
||||
'isFalse',
|
||||
]),
|
||||
/*
|
||||
* Otazky podminky. `rules` je dnesni podoba, `fieldId` a spol. stara -
|
||||
* strom ulozeny driv musi jit ulozit znovu, aniz by se prepisoval.
|
||||
*/
|
||||
rules: z.array(conditionRuleSchema).max(10).optional(),
|
||||
match: z.enum(['all', 'any']).optional(),
|
||||
fieldId: z.string().min(1).optional(),
|
||||
operator: conditionOperatorSchema.optional(),
|
||||
value: z.string().optional(),
|
||||
yes: z.array(stepSchema),
|
||||
no: z.array(stepSchema),
|
||||
@@ -1243,13 +1260,16 @@ async function validateFlowReferences(
|
||||
const walk = async (steps: FlowStep[]): Promise<void> => {
|
||||
for (const step of steps) {
|
||||
if (step.kind === 'condition') {
|
||||
const field = scopes.all.get(step.fieldId);
|
||||
if (!field) {
|
||||
problems.push(`Podmínka odkazuje na neexistující parametr (${step.fieldId}).`);
|
||||
} else if (!operatorAllowedForType(step.operator, field.type)) {
|
||||
problems.push(
|
||||
`Operátor "${step.operator}" nelze použít na parametr "${field.name}" typu ${field.type}.`,
|
||||
);
|
||||
// Kazda otazka zvlast, jinak by prvni spatna schovala ostatni.
|
||||
for (const rule of rulesOf(step)) {
|
||||
const field = scopes.all.get(rule.fieldId);
|
||||
if (!field) {
|
||||
problems.push(`Podmínka odkazuje na neexistující parametr (${rule.fieldId}).`);
|
||||
} else if (!operatorAllowedForType(rule.operator, field.type)) {
|
||||
problems.push(
|
||||
`Operátor "${rule.operator}" nelze použít na parametr "${field.name}" typu ${field.type}.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
await walk(step.yes);
|
||||
await walk(step.no);
|
||||
|
||||
+53
-25
@@ -22,8 +22,8 @@
|
||||
*/
|
||||
|
||||
import { defaultConnectorFor, getConnector, type Connector } from '../data/connectorStore.js';
|
||||
import type { FlowStep } from '../data/automationStore.js';
|
||||
import { isUnary, type ConditionOperator } from '../data/conditions.js';
|
||||
import { matchOf, rulesOf, type FlowStep } from '../data/automationStore.js';
|
||||
import { isUnary, type ConditionOperator, type ConditionRule } from '../data/conditions.js';
|
||||
import { escapeHtml, renderTemplate } from '../data/templates.js';
|
||||
import { actionInputsFor, type OperationField } from '../data/services.js';
|
||||
import { getPath } from '../scripts/mapping.js';
|
||||
@@ -543,33 +543,56 @@ function conditionDetail(
|
||||
context: RunContext,
|
||||
options: RunOptions,
|
||||
): string {
|
||||
const raw = conditionValue(step.fieldId, context, options);
|
||||
const name = options.fieldNames?.[step.fieldId] ?? step.fieldId;
|
||||
// Kazda otazka na svem radku, i s tim, jak dopadla. U spojenych podminek
|
||||
// je jinak videt jen vysledek a ne to, ktera z nich rozhodla.
|
||||
return rulesOf(step)
|
||||
.map((rule) => {
|
||||
const raw = conditionValue(rule.fieldId, context, options);
|
||||
const name = options.fieldNames?.[rule.fieldId] ?? rule.fieldId;
|
||||
|
||||
const got =
|
||||
raw === undefined
|
||||
? 'nedorazilo'
|
||||
: raw === null
|
||||
? 'null'
|
||||
: stringify(raw) === ''
|
||||
? 'prázdné'
|
||||
: JSON.stringify(stringify(raw));
|
||||
const got =
|
||||
raw === undefined
|
||||
? 'nedorazilo'
|
||||
: raw === null
|
||||
? 'null'
|
||||
: stringify(raw) === ''
|
||||
? 'prázdné'
|
||||
: JSON.stringify(stringify(raw));
|
||||
|
||||
if (isUnary(step.operator)) return `${name} = ${got}`;
|
||||
return `${name} = ${got}, porovnáno s ${JSON.stringify(step.value ?? '')}`;
|
||||
const sedi = matchRule(rule, context, options) ? 'sedí' : 'nesedí';
|
||||
if (isUnary(rule.operator)) return `${name} = ${got}: ${sedi}`;
|
||||
return `${name} = ${got}, porovnáno s ${JSON.stringify(rule.value ?? '')}: ${sedi}`;
|
||||
})
|
||||
.join(String.fromCharCode(10));
|
||||
}
|
||||
|
||||
/** Vyhodnoti podminku nad kontextem. */
|
||||
/** Jedna otazka podminky. */
|
||||
function matchRule(rule: ConditionRule, context: RunContext, options: RunOptions): boolean {
|
||||
const raw = conditionValue(rule.fieldId, context, options);
|
||||
const value = raw === undefined || raw === null ? '' : stringify(raw);
|
||||
return compare(rule.operator, value, rule.value ?? '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Vyhodnoti podminku nad kontextem.
|
||||
*
|
||||
* `all` = musi sedet vsechny, `any` = staci jedna. Podminka **bez otazky**
|
||||
* neplati: tise vetvit vzdycky stejne je horsi nez neudelat nic.
|
||||
*/
|
||||
function evaluate(
|
||||
step: Extract<FlowStep, { kind: 'condition' }>,
|
||||
context: RunContext,
|
||||
options: RunOptions,
|
||||
): boolean {
|
||||
const raw = conditionValue(step.fieldId, context, options);
|
||||
const value = raw === undefined || raw === null ? '' : stringify(raw);
|
||||
const expected = step.value ?? '';
|
||||
const rules = rulesOf(step);
|
||||
if (rules.length === 0) {
|
||||
console.warn(`[runtime] podminka ${step.id} nema zadnou otazku, nesplnena`);
|
||||
return false;
|
||||
}
|
||||
|
||||
return compare(step.operator, value, expected);
|
||||
return matchOf(step) === 'any'
|
||||
? rules.some((rule) => matchRule(rule, context, options))
|
||||
: rules.every((rule) => matchRule(rule, context, options));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -622,13 +645,18 @@ function describeCondition(
|
||||
context: RunContext,
|
||||
options: RunOptions,
|
||||
): string {
|
||||
const raw = conditionValue(step.fieldId, context, options);
|
||||
const value = raw === undefined || raw === null ? '(prázdné)' : stringify(raw);
|
||||
// V logu ma stat jmeno, ktere clovek zna z builderu, ne vnitrni ID.
|
||||
const name = options.fieldNames?.[step.fieldId] ?? nameOf(step.fieldId);
|
||||
const rules = rulesOf(step);
|
||||
if (rules.length === 0) return 'Podmínka bez otázky';
|
||||
|
||||
if (isUnary(step.operator)) return `Podmínka: ${name} ${step.operator} (${value})`;
|
||||
return `Podmínka: ${name} ${step.operator} ${step.value ?? ''} (${value})`;
|
||||
const parts = rules.map((rule) => {
|
||||
// V logu ma stat jmeno, ktere clovek zna z builderu, ne vnitrni ID.
|
||||
const name = options.fieldNames?.[rule.fieldId] ?? nameOf(rule.fieldId);
|
||||
if (isUnary(rule.operator)) return `${name} ${rule.operator}`;
|
||||
return `${name} ${rule.operator} ${rule.value ?? ''}`;
|
||||
});
|
||||
|
||||
const spojka = matchOf(step) === 'any' ? ' nebo ' : ' a zároveň ';
|
||||
return `Podmínka: ${parts.join(spojka)}`;
|
||||
}
|
||||
|
||||
/** Co krok vratil, do logu. Delsi vystup se orizne, cely je ve vysledku. */
|
||||
|
||||
@@ -17,16 +17,20 @@ import { serviceIcon } from '@/lib/serviceIcons';
|
||||
import {
|
||||
collectScopes,
|
||||
defaultOperatorFor,
|
||||
describeCondition,
|
||||
describeRules,
|
||||
isUnaryOperator,
|
||||
matchOf,
|
||||
operatorLabel,
|
||||
operatorsByType,
|
||||
resolveOperation,
|
||||
rulesOf,
|
||||
type FlowPath,
|
||||
} from '@/lib/flow';
|
||||
import type {
|
||||
AutomationFlow,
|
||||
ConditionMatch,
|
||||
ConditionOperator,
|
||||
ConditionRule,
|
||||
ModelNode,
|
||||
Service,
|
||||
FlowStep,
|
||||
@@ -58,7 +62,13 @@ interface CanvasCallbacks {
|
||||
onRemoveStep: (stepId: string) => void;
|
||||
onUpdateCondition: (
|
||||
stepId: string,
|
||||
patch: { fieldId?: string; operator?: ConditionOperator; value?: string },
|
||||
patch: {
|
||||
rules?: ConditionRule[];
|
||||
match?: ConditionMatch;
|
||||
fieldId?: string;
|
||||
operator?: ConditionOperator;
|
||||
value?: string;
|
||||
},
|
||||
) => void;
|
||||
onUpdateInputs: (stepId: string, inputs: Record<string, string>) => void;
|
||||
/** Zmena cesty k seznamu u smycky. */
|
||||
@@ -579,26 +589,59 @@ function ConditionCard({
|
||||
}) {
|
||||
const { onRemoveStep, onMoveStep, onUpdateCondition } = callbacks;
|
||||
|
||||
const field = fields.find((f) => f.id === step.fieldId);
|
||||
const allowedOperators = field ? operatorsByType[field.type] : [];
|
||||
const needsValue = !isUnaryOperator(step.operator);
|
||||
const valueMissing = needsValue && (step.value ?? '').trim().length === 0;
|
||||
/*
|
||||
* Podminka se muze ptat na vic veci naraz.
|
||||
*
|
||||
* Zapisuje se vzdycky `rules`, i kdyz je otazka jedna - stara podoba se tim
|
||||
* pri prvni uprave prevede a dal uz existuje jen jeden tvar.
|
||||
*/
|
||||
const rules = rulesOf(step);
|
||||
const match = matchOf(step);
|
||||
const missing = rules.some((rule) => {
|
||||
const field = fields.find((f) => f.id === rule.fieldId);
|
||||
return field && !isUnaryOperator(rule.operator) && (rule.value ?? '').trim().length === 0;
|
||||
});
|
||||
|
||||
function writeRules(next: ConditionRule[], nextMatch: ConditionMatch = match) {
|
||||
onUpdateCondition(step.id, { rules: next, match: nextMatch });
|
||||
}
|
||||
|
||||
function changeRule(index: number, patch: Partial<ConditionRule>) {
|
||||
writeRules(rules.map((rule, i) => (i === index ? { ...rule, ...patch } : rule)));
|
||||
}
|
||||
|
||||
/** Zmena parametru muze zneplatnit operator - v tom pripade ho prepneme. */
|
||||
function changeField(fieldId: string) {
|
||||
function changeField(index: number, fieldId: string) {
|
||||
const next = fields.find((f) => f.id === fieldId);
|
||||
if (!next) {
|
||||
console.warn(`[flow] podminka: neznamy parametr ${fieldId}`);
|
||||
return;
|
||||
}
|
||||
const operatorStillValid = operatorsByType[next.type].includes(step.operator);
|
||||
onUpdateCondition(step.id, {
|
||||
const rule = rules[index];
|
||||
const stillValid = operatorsByType[next.type].includes(rule.operator);
|
||||
changeRule(index, {
|
||||
fieldId,
|
||||
operator: operatorStillValid ? step.operator : defaultOperatorFor(next.type),
|
||||
value: operatorStillValid ? step.value : '',
|
||||
operator: stillValid ? rule.operator : defaultOperatorFor(next.type),
|
||||
value: stillValid ? rule.value : '',
|
||||
});
|
||||
}
|
||||
|
||||
/** Nova otazka se pta na tentyz parametr jako ta prvni, aby nebyla prazdna. */
|
||||
function addRule() {
|
||||
const first = fields[0];
|
||||
if (!first) return;
|
||||
const base = fields.find((f) => f.id === rules[0]?.fieldId) ?? first;
|
||||
writeRules([
|
||||
...rules,
|
||||
{ fieldId: base.id, operator: defaultOperatorFor(base.type), value: '' },
|
||||
]);
|
||||
}
|
||||
|
||||
function removeRule(index: number) {
|
||||
// Posledni otazka se nemaze: podminka bez otazky by tise nevetvila.
|
||||
if (rules.length <= 1) return;
|
||||
writeRules(rules.filter((_, i) => i !== index));
|
||||
}
|
||||
return (
|
||||
<div className="@container rounded-card border border-accent-400/30 bg-accent-500/6 p-3 sm:p-4">
|
||||
<div className="flex items-start gap-2.5 sm:gap-3">
|
||||
@@ -608,73 +651,132 @@ function ConditionCard({
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<p className="text-xs text-white/40">Podmínka</p>
|
||||
{valueMissing && <Badge tone="warn">Doplňte hodnotu</Badge>}
|
||||
{missing && <Badge tone="warn">Doplňte hodnotu</Badge>}
|
||||
</div>
|
||||
|
||||
{!field ? (
|
||||
<p className="mt-1 text-sm text-danger-400">
|
||||
Parametr, na který se podmínka odkazovala, už neexistuje. Vyberte jiný, nebo
|
||||
podmínku odeberte.
|
||||
</p>
|
||||
) : (
|
||||
<p className="mt-0.5 font-semibold break-words text-white">
|
||||
{describeCondition(field, step.operator, step.value)}
|
||||
</p>
|
||||
<p className="mt-0.5 font-semibold break-words text-white">
|
||||
{describeRules(step, fields)}
|
||||
</p>
|
||||
|
||||
{/*
|
||||
Spojka se nabizi az od druhe otazky. U jedne nema co spojovat
|
||||
a prepinac, ktery nic nedela, jen mate.
|
||||
*/}
|
||||
{rules.length > 1 && (
|
||||
<div className="mt-2.5 flex flex-wrap items-center gap-2">
|
||||
<span className="text-xs text-white/40">Platí, když</span>
|
||||
{(['all', 'any'] as const).map((option) => (
|
||||
<button
|
||||
key={option}
|
||||
type="button"
|
||||
onClick={() => writeRules(rules, option)}
|
||||
className={cn(
|
||||
'rounded-lg px-2.5 py-1 text-xs font-medium transition-colors',
|
||||
match === option
|
||||
? 'bg-accent-500/20 text-accent-200'
|
||||
: 'text-white/45 hover:text-white',
|
||||
)}
|
||||
>
|
||||
{option === 'all' ? 'sedí všechny' : 'sedí aspoň jedna'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Vlastni editor: parametr - operator - hodnota */}
|
||||
<div className="mt-2.5 flex flex-wrap items-center gap-2">
|
||||
<select
|
||||
value={field ? step.fieldId : ''}
|
||||
onChange={(event) => changeField(event.target.value)}
|
||||
aria-label="Parametr"
|
||||
className="rounded-lg border border-ink-600/70 bg-ink-900/70 px-3 py-1.5 font-mono text-sm text-white focus:border-accent-400/70 focus:outline-none"
|
||||
>
|
||||
{!field && (
|
||||
<option value="" className="bg-ink-850">
|
||||
- vyberte parametr -
|
||||
</option>
|
||||
)}
|
||||
{fields.map((option) => (
|
||||
<option key={option.id} value={option.id} className="bg-ink-850">
|
||||
{option.name || '(bez názvu)'}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{/* Radek na otazku: parametr - operator - hodnota */}
|
||||
<div className="mt-2.5 space-y-2">
|
||||
{rules.map((rule, index) => {
|
||||
const field = fields.find((f) => f.id === rule.fieldId);
|
||||
const allowed = field ? operatorsByType[field.type] : [];
|
||||
const needsValue = !isUnaryOperator(rule.operator);
|
||||
const valueMissing = needsValue && (rule.value ?? '').trim().length === 0;
|
||||
|
||||
<select
|
||||
value={step.operator}
|
||||
onChange={(event) =>
|
||||
onUpdateCondition(step.id, {
|
||||
operator: event.target.value as ConditionOperator,
|
||||
})
|
||||
}
|
||||
disabled={!field}
|
||||
aria-label="Operátor"
|
||||
className="rounded-lg border border-ink-600/70 bg-ink-900/70 px-3 py-1.5 text-sm text-white focus:border-accent-400/70 focus:outline-none disabled:opacity-50"
|
||||
>
|
||||
{allowedOperators.map((operator) => (
|
||||
<option key={operator} value={operator} className="bg-ink-850">
|
||||
{field ? operatorLabel(operator, field.type) : operator}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
return (
|
||||
<div key={index} className="flex flex-wrap items-center gap-2">
|
||||
{index > 0 && (
|
||||
<span className="w-16 shrink-0 text-xs text-white/35">
|
||||
{match === 'any' ? 'nebo' : 'a zároveň'}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{needsValue && field && (
|
||||
<input
|
||||
value={step.value ?? ''}
|
||||
onChange={(event) => onUpdateCondition(step.id, { value: event.target.value })}
|
||||
type={field.type === 'number' ? 'number' : field.type === 'date' ? 'date' : 'text'}
|
||||
placeholder={field.type === 'number' ? '15' : 'hodnota'}
|
||||
aria-label="Hodnota"
|
||||
className={cn(
|
||||
'w-32 rounded-lg border bg-ink-900/70 px-3 py-1.5 text-sm text-white placeholder:text-white/25 focus:outline-none',
|
||||
valueMissing
|
||||
? 'border-warn-400/60'
|
||||
: 'border-ink-600/70 focus:border-accent-400/70',
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<select
|
||||
value={field ? rule.fieldId : ''}
|
||||
onChange={(event) => changeField(index, event.target.value)}
|
||||
aria-label="Parametr"
|
||||
className="rounded-lg border border-ink-600/70 bg-ink-900/70 px-3 py-1.5 font-mono text-sm text-white focus:border-accent-400/70 focus:outline-none"
|
||||
>
|
||||
{!field && (
|
||||
<option value="" className="bg-ink-850">
|
||||
- vyberte parametr -
|
||||
</option>
|
||||
)}
|
||||
{fields.map((option) => (
|
||||
<option key={option.id} value={option.id} className="bg-ink-850">
|
||||
{option.name || '(bez názvu)'}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
<select
|
||||
value={rule.operator}
|
||||
onChange={(event) =>
|
||||
changeRule(index, { operator: event.target.value as ConditionOperator })
|
||||
}
|
||||
disabled={!field}
|
||||
aria-label="Operátor"
|
||||
className="rounded-lg border border-ink-600/70 bg-ink-900/70 px-3 py-1.5 text-sm text-white focus:border-accent-400/70 focus:outline-none disabled:opacity-50"
|
||||
>
|
||||
{allowed.map((operator) => (
|
||||
<option key={operator} value={operator} className="bg-ink-850">
|
||||
{field ? operatorLabel(operator, field.type) : operator}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
{needsValue && field && (
|
||||
<input
|
||||
value={rule.value ?? ''}
|
||||
onChange={(event) => changeRule(index, { value: event.target.value })}
|
||||
type={
|
||||
field.type === 'number' ? 'number' : field.type === 'date' ? 'date' : 'text'
|
||||
}
|
||||
placeholder={field.type === 'number' ? '15' : 'hodnota'}
|
||||
aria-label="Hodnota"
|
||||
className={cn(
|
||||
'w-32 rounded-lg border bg-ink-900/70 px-3 py-1.5 text-sm text-white placeholder:text-white/25 focus:outline-none',
|
||||
valueMissing
|
||||
? 'border-warn-400/60'
|
||||
: 'border-ink-600/70 focus:border-accent-400/70',
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!field && (
|
||||
<span className="text-xs text-danger-400">parametr už neexistuje</span>
|
||||
)}
|
||||
|
||||
{rules.length > 1 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeRule(index)}
|
||||
aria-label="Odebrat otázku"
|
||||
className="rounded-lg px-2 py-1.5 text-white/35 transition-colors hover:text-danger-400"
|
||||
>
|
||||
<Trash2 className="size-3.5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={addRule}
|
||||
className="inline-flex items-center gap-1.5 text-xs font-medium text-accent-300 transition-colors hover:text-accent-200"
|
||||
>
|
||||
<Plus className="size-3.5" />
|
||||
Přidat otázku
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
+47
-2
@@ -1,6 +1,8 @@
|
||||
import type {
|
||||
AutomationFlow,
|
||||
ConditionMatch,
|
||||
ConditionOperator,
|
||||
ConditionRule,
|
||||
Service,
|
||||
ServiceOperation,
|
||||
FieldType,
|
||||
@@ -125,7 +127,44 @@ export const fieldTypeLabels: Record<FieldType, string> = {
|
||||
list: 'seznam',
|
||||
};
|
||||
|
||||
/** Lidsky citelny zapis podminky, napr. "score je větší nebo rovno 15". */
|
||||
/**
|
||||
* Otazky podminky, at uz je krok v jakekoliv podobe.
|
||||
*
|
||||
* Jedine misto, kde se stara podoba prevadi. Zrcadli `rulesOf` na serveru,
|
||||
* server je autorita.
|
||||
*/
|
||||
export function rulesOf(step: Extract<FlowStep, { kind: 'condition' }>): ConditionRule[] {
|
||||
if (step.rules && step.rules.length > 0) return step.rules;
|
||||
if (!step.fieldId || !step.operator) return [];
|
||||
return [{ fieldId: step.fieldId, operator: step.operator, value: step.value }];
|
||||
}
|
||||
|
||||
/** Jak se otazky spoji. Jedna otazka nema co spojovat. */
|
||||
export function matchOf(step: Extract<FlowStep, { kind: 'condition' }>): ConditionMatch {
|
||||
return step.match ?? 'all';
|
||||
}
|
||||
|
||||
/** Lidsky citelny zapis cele podminky, vcetne spojek. */
|
||||
export function describeRules(
|
||||
step: Extract<FlowStep, { kind: 'condition' }>,
|
||||
fields: TriggerField[],
|
||||
): string {
|
||||
const rules = rulesOf(step);
|
||||
if (rules.length === 0) return 'Bez otázky';
|
||||
|
||||
const spojka = matchOf(step) === 'any' ? ' nebo ' : ' a zároveň ';
|
||||
return rules
|
||||
.map((rule) =>
|
||||
describeCondition(
|
||||
fields.find((field) => field.id === rule.fieldId),
|
||||
rule.operator,
|
||||
rule.value,
|
||||
),
|
||||
)
|
||||
.join(spojka);
|
||||
}
|
||||
|
||||
/** Lidsky citelny zapis jedne otazky, napr. "score je větší nebo rovno 15". */
|
||||
export function describeCondition(
|
||||
field: TriggerField | undefined,
|
||||
operator: ConditionOperator,
|
||||
@@ -282,7 +321,13 @@ export function updateStepInputs(
|
||||
export function updateCondition(
|
||||
steps: FlowStep[],
|
||||
stepId: string,
|
||||
patch: { fieldId?: string; operator?: ConditionOperator; value?: string },
|
||||
patch: {
|
||||
rules?: ConditionRule[];
|
||||
match?: ConditionMatch;
|
||||
fieldId?: string;
|
||||
operator?: ConditionOperator;
|
||||
value?: string;
|
||||
},
|
||||
): FlowStep[] {
|
||||
return steps.map((step) => {
|
||||
if (step.kind === 'foreach') {
|
||||
|
||||
@@ -29,6 +29,16 @@ export interface Tenant {
|
||||
name: string;
|
||||
}
|
||||
|
||||
/** Jedna otazka podminky. Zrcadli src/data/conditions.ts. */
|
||||
export interface ConditionRule {
|
||||
fieldId: string;
|
||||
operator: ConditionOperator;
|
||||
value?: string;
|
||||
}
|
||||
|
||||
/** Jak se otazky podminky spoji. `all` = a zaroven, `any` = nebo. */
|
||||
export type ConditionMatch = 'all' | 'any';
|
||||
|
||||
/** Pohled na tickety. Server rozhoduje, ktery smi kdo pouzit. */
|
||||
export type TicketScope = 'all' | 'tenant' | 'mine';
|
||||
|
||||
@@ -597,9 +607,13 @@ export type FlowStep =
|
||||
| {
|
||||
id: string;
|
||||
kind: 'condition';
|
||||
/** id parametru z trigger.fields */
|
||||
fieldId: string;
|
||||
operator: ConditionOperator;
|
||||
/** Otazky, ktere se ptaji naraz. Cte se vzdycky pres `rulesOf`. */
|
||||
rules?: ConditionRule[];
|
||||
/** `all` = a zaroven, `any` = nebo. */
|
||||
match?: ConditionMatch;
|
||||
/** Stara podoba: jedna otazka primo na kroku. Prevadi ji `rulesOf`. */
|
||||
fieldId?: string;
|
||||
operator?: ConditionOperator;
|
||||
value?: string;
|
||||
yes: FlowStep[];
|
||||
no: FlowStep[];
|
||||
|
||||
Reference in New Issue
Block a user