Nastaveni prezije nasazeni, ukazkova data uz se nevraci
Bez databaze lezi data uvnitr containeru, takze redeploy je smaze a seed je nasype znovu. Dokud nebude Postgres, resi se to trema vecmi: Ukazkova data jen se SEED_DEMO=1. Automatizace, tickety a incidenty se uz po kazdem nasazeni nevraci. Konfigurace se nasypava dal, bez ni je portal nepouzitelny. Token webhooku z WEBHOOK_TOKEN_TEST. Driv se pri kazdem nasazeni vygeneroval novy, takze odesilatel musel prepisovat adresu ve svem kodu. Ted je token v promenne aplikace: neni v gitu a adresa se nemeni. Automatizace, ktera na instanci opravdu bezi, je v seedu. Je to provizorium, ne cil - az data prezijou nasazeni, patri zpatky do dat. Dal: - `ticket/upsert` umi vsechna pole ticketu: zakaznik, kanal, odkaz na zdroj, priorita, stitky, resitel, skupina a vlastni pole typu jako JSON. Zakaznik a kanal se vyplnuji jen pri zalozeni, aby pozdejsi udalost s prazdnym jmenem neprepsala, co uz tam je. - Kroky ve strome jdou sbalit, vychozi je sbaleno. Sbaleny krok ukazuje, co ma vyplneno, ne popis operace. - Seskupovani widgetu podle faze a dva nove widgety: tickety podle stavu a podle faze za tento mesic, obojí s proklikem na vyfiltrovany seznam. - Faze se ukazuje jako stav. Driv byl videt jen nas ctyrprvkovy ciselnik, coz u ticketu z cizi aplikace nedava smysl. Zivotni cyklus zustava vedle jako drobny text, protoze se z nej pocitaji statistiky. Overeno 18 kontrolami proti bezicimu serveru. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
6abac82d5e
commit
ef3080d3f6
@@ -66,9 +66,39 @@ const incidentStatusMap: Record<IncidentStatus, { label: string; tone: BadgeTone
|
||||
resolved: { label: 'Vyřešeno', tone: 'ok' },
|
||||
};
|
||||
|
||||
export function TicketStatusBadge({ status }: { status: TicketStatus }) {
|
||||
/**
|
||||
* Stav ticketu.
|
||||
*
|
||||
* Kdyz ma ticket **fazi** z workflow sveho typu, ukaze se **ona**, protoze to
|
||||
* je hodnota, kterou tam dala automatizace nebo clovek a kterou lidi ve firme
|
||||
* pouzivaji. Nas zivotni cyklus (novy, v reseni, ceka, vyreseno) zustava
|
||||
* vedle jako mensi znacka - pocitaji se z nej statistiky a fronta, takze
|
||||
* schovat ho uplne by znamenalo, ze nikdo nepozna, proc je ticket ve fronte.
|
||||
*/
|
||||
export function TicketStatusBadge({
|
||||
status,
|
||||
stage,
|
||||
}: {
|
||||
status: TicketStatus;
|
||||
stage?: string | null;
|
||||
}) {
|
||||
const { label, tone } = ticketStatusMap[status];
|
||||
const Icon = status === 'resolved' ? CheckCircle2 : status === 'waiting' ? Clock : CircleDot;
|
||||
|
||||
if (stage) {
|
||||
return (
|
||||
<span className="inline-flex flex-wrap items-center gap-1.5">
|
||||
<Badge tone={tone}>
|
||||
<Icon className="size-3.5" />
|
||||
{stage}
|
||||
</Badge>
|
||||
<span className="text-xs text-white/30" title="Stav v životním cyklu, podle něj se počítají statistiky">
|
||||
{label}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Badge tone={tone}>
|
||||
<Icon className="size-3.5" />
|
||||
|
||||
@@ -101,7 +101,7 @@ export function TicketTable({
|
||||
)}
|
||||
|
||||
<td className="px-3 py-3">
|
||||
<TicketStatusBadge status={ticket.status} />
|
||||
<TicketStatusBadge status={ticket.status} stage={ticket.stage} />
|
||||
</td>
|
||||
<td className="px-3 py-3">
|
||||
<TicketPriorityBadge priority={ticket.priority} />
|
||||
@@ -143,7 +143,7 @@ export function TicketTable({
|
||||
<Link to={`/dashboard/tickety/${ticket.id}`} className="block">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<span className="font-mono text-xs text-white/35">{ticket.id}</span>
|
||||
<TicketStatusBadge status={ticket.status} />
|
||||
<TicketStatusBadge status={ticket.status} stage={ticket.stage} />
|
||||
</div>
|
||||
|
||||
<p className="mt-1 text-sm text-white/85">{ticket.subject}</p>
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
Trash2,
|
||||
Zap,
|
||||
} from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { StepInputs } from '@/components/dashboard/flow/StepInputs';
|
||||
import { TriggerConfig } from '@/components/dashboard/flow/TriggerConfig';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
@@ -361,6 +362,7 @@ function ActionCard({
|
||||
onMove: (offset: number) => void;
|
||||
onUpdateInputs: (inputs: Record<string, string>) => void;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const resolved = resolveOperation(connectors, step.serviceId, step.operationId, 'action');
|
||||
|
||||
if (!resolved) {
|
||||
@@ -376,20 +378,76 @@ function ActionCard({
|
||||
const Icon = serviceIcon(resolved.connector.icon);
|
||||
const inputs = resolved.operation.inputs;
|
||||
|
||||
/*
|
||||
* Sbalene kroky.
|
||||
*
|
||||
* Strom o peti krocich s vyplnenymi poli je pres celou obrazovku a nejde
|
||||
* v nem najit, co kde je. Sbaleny krok ukazuje jen to podstatne: co dela
|
||||
* a co ma vyplneno. Vychozi je **sbaleno**, protoze vetsinou clovek strom
|
||||
* cte, ne prenastavuje.
|
||||
*/
|
||||
const filled = Object.entries(step.inputs ?? {}).filter(([, value]) => value.trim() !== '');
|
||||
|
||||
return (
|
||||
<div className="@container rounded-card border border-ink-600/60 bg-ink-800/60 p-3 transition-colors hover:border-ink-600 sm:p-4">
|
||||
<div className="flex items-start gap-2.5 sm:gap-3">
|
||||
<span className="grid size-8 shrink-0 place-items-center rounded-xl bg-ink-700/70 text-brand-300 @xs:size-10">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen(!open)}
|
||||
aria-expanded={open}
|
||||
title={open ? 'Sbalit krok' : 'Rozbalit krok'}
|
||||
className="grid size-8 shrink-0 place-items-center rounded-xl bg-ink-700/70 text-brand-300 transition-colors hover:bg-ink-700 @xs:size-10"
|
||||
>
|
||||
<Icon className="size-4 @xs:size-4.5" />
|
||||
</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate text-xs text-white/40">{resolved.connector.name}</p>
|
||||
<p className="mt-0.5 font-semibold break-words text-white">{resolved.operation.name}</p>
|
||||
{/* Popis je jen doplnek. V uzkem sloupci by se lamal po slovech. */}
|
||||
<p className="mt-0.5 hidden text-sm text-white/50 @xs:block">
|
||||
{resolved.operation.description}
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen(!open)}
|
||||
className="min-w-0 flex-1 text-left"
|
||||
>
|
||||
<span className="block truncate text-xs text-white/40">{resolved.connector.name}</span>
|
||||
<span className="mt-0.5 flex flex-wrap items-center gap-2 font-semibold break-words text-white">
|
||||
{resolved.operation.name}
|
||||
<ChevronDown
|
||||
className={cn(
|
||||
'size-4 shrink-0 text-white/30 transition-transform',
|
||||
open && 'rotate-180',
|
||||
)}
|
||||
/>
|
||||
</span>
|
||||
|
||||
{open ? (
|
||||
/* Popis je jen doplnek. V uzkem sloupci by se lamal po slovech. */
|
||||
<span className="mt-0.5 hidden text-sm text-white/50 @xs:block">
|
||||
{resolved.operation.description}
|
||||
</span>
|
||||
) : (
|
||||
/*
|
||||
* Sbalene: co ma krok vyplneno. Tohle clovek hleda, kdyz cte strom -
|
||||
* ne popis operace, ktery zna.
|
||||
*/
|
||||
<span className="mt-1 flex flex-wrap gap-1.5">
|
||||
{filled.length === 0 ? (
|
||||
<span className="text-xs text-white/30">nic nevyplněno</span>
|
||||
) : (
|
||||
filled.slice(0, 4).map(([key, value]) => (
|
||||
<span
|
||||
key={key}
|
||||
className="max-w-56 truncate rounded bg-ink-950/60 px-1.5 py-0.5 font-mono text-xs text-white/50"
|
||||
title={`${key}: ${value}`}
|
||||
>
|
||||
{key}: {value}
|
||||
</span>
|
||||
))
|
||||
)}
|
||||
{filled.length > 4 && (
|
||||
<span className="text-xs text-white/30">a {filled.length - 4} dalších</span>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<StepControls
|
||||
canMoveUp={canMoveUp}
|
||||
canMoveDown={canMoveDown}
|
||||
@@ -398,23 +456,24 @@ function ActionCard({
|
||||
/>
|
||||
</div>
|
||||
|
||||
{inputs && inputs.length > 0 ? (
|
||||
<StepInputs
|
||||
fields={inputs}
|
||||
values={step.inputs ?? {}}
|
||||
available={available}
|
||||
onChange={onUpdateInputs}
|
||||
/>
|
||||
) : (
|
||||
// Akce, ktere zatim nemaji `inputs`. Radeji to rekneme nahlas,
|
||||
// nez aby uzivatel hledal nastaveni, ktere neexistuje.
|
||||
<p className="mt-3 border-t border-ink-600/50 pt-3 text-xs text-white/35">
|
||||
Tuhle akci zatím nejde nastavit.
|
||||
{resolved.operation.fields && resolved.operation.fields.length > 0 && (
|
||||
<> Až půjde, budou to pole: {resolved.operation.fields.join(', ')}.</>
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
{open &&
|
||||
(inputs && inputs.length > 0 ? (
|
||||
<StepInputs
|
||||
fields={inputs}
|
||||
values={step.inputs ?? {}}
|
||||
available={available}
|
||||
onChange={onUpdateInputs}
|
||||
/>
|
||||
) : (
|
||||
// Akce, ktere zatim nemaji `inputs`. Radeji to rekneme nahlas,
|
||||
// nez aby uzivatel hledal nastaveni, ktere neexistuje.
|
||||
<p className="mt-3 border-t border-ink-600/50 pt-3 text-xs text-white/35">
|
||||
Tuhle akci zatím nejde nastavit.
|
||||
{resolved.operation.fields && resolved.operation.fields.length > 0 && (
|
||||
<> Až půjde, budou to pole: {resolved.operation.fields.join(', ')}.</>
|
||||
)}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -202,7 +202,7 @@ function TicketCards({
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<span className="font-mono text-xs text-white/35">{ticket.id}</span>
|
||||
<TicketStatusBadge status={ticket.status} />
|
||||
<TicketStatusBadge status={ticket.status} stage={ticket.stage} />
|
||||
</div>
|
||||
<p className="mt-1.5 line-clamp-2 text-sm text-white/85 group-hover:text-brand-200">
|
||||
{ticket.subject}
|
||||
|
||||
@@ -117,7 +117,7 @@ export default function TicketDetail() {
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="font-mono text-xs text-white/35">{ticket.data.id}</span>
|
||||
<TicketChannelBadge channel={ticket.data.channel} />
|
||||
<TicketStatusBadge status={ticket.data.status} />
|
||||
<TicketStatusBadge status={ticket.data.status} stage={ticket.data.stage} />
|
||||
<TicketPriorityBadge priority={ticket.data.priority} />
|
||||
{ticket.data.externalId && (
|
||||
<Badge tone="neutral">
|
||||
|
||||
@@ -63,6 +63,7 @@ export default function Tickets() {
|
||||
channel: params.get('channel') as TicketChannel | null,
|
||||
typeId: params.get('typeId'),
|
||||
tag: params.get('tag'),
|
||||
stage: params.get('stage'),
|
||||
groupId: params.get('groupId'),
|
||||
}).current;
|
||||
|
||||
@@ -75,6 +76,7 @@ export default function Tickets() {
|
||||
/** Filtry, ktere se daji nastavit jen odkazem. V UI k nim tlacitko neni. */
|
||||
const [typeId, setTypeId] = useState<string | null>(initial.typeId);
|
||||
const [tag, setTag] = useState<string | null>(initial.tag);
|
||||
const [stage, setStage] = useState<string | null>(initial.stage);
|
||||
const [groupId, setGroupId] = useState<string | null>(initial.groupId);
|
||||
const [query, setQuery] = useState('');
|
||||
/** Tabulka na porovnavani, dlazdice na rychly prehled. */
|
||||
@@ -90,6 +92,7 @@ export default function Tickets() {
|
||||
if (assignee) params.set('assignee', assignee);
|
||||
if (typeId) params.set('typeId', typeId);
|
||||
if (tag) params.set('tag', tag);
|
||||
if (stage) params.set('stage', stage);
|
||||
if (groupId) params.set('groupId', groupId);
|
||||
if (status) params.set('status', status);
|
||||
if (channel) params.set('channel', channel);
|
||||
@@ -239,6 +242,11 @@ export default function Tickets() {
|
||||
tag: {tag === 'none' ? 'bez tagu' : tag} x
|
||||
</Chip>
|
||||
)}
|
||||
{stage && (
|
||||
<Chip active onClick={() => setStage(null)} title="Zrušit filtr na fázi">
|
||||
fáze: {stage === 'none' ? 'bez fáze' : stage} x
|
||||
</Chip>
|
||||
)}
|
||||
{groupId && (
|
||||
<Chip active onClick={() => setGroupId(null)} title="Zrušit filtr na skupinu">
|
||||
skupina: {groupId === 'none' ? 'bez skupiny' : groupId} x
|
||||
@@ -293,7 +301,7 @@ export default function Tickets() {
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<span className="font-mono text-xs text-white/35">{ticket.id}</span>
|
||||
<TicketStatusBadge status={ticket.status} />
|
||||
<TicketStatusBadge status={ticket.status} stage={ticket.stage} />
|
||||
</div>
|
||||
|
||||
<p className="mt-2 line-clamp-2 text-sm text-white/85 group-hover:text-brand-200">
|
||||
|
||||
Reference in New Issue
Block a user