Merge pull request #10 from WIT-LAB-LLC/codex/complete-admin-panel-for-funnels-jbwsju
Restore preview and improve funnel builder reordering
This commit is contained in:
commit
da92fe28c8
@ -5,6 +5,7 @@ import { useCallback, useState } from "react";
|
|||||||
import { BuilderLayout } from "@/components/admin/builder/BuilderLayout";
|
import { BuilderLayout } from "@/components/admin/builder/BuilderLayout";
|
||||||
import { BuilderSidebar } from "@/components/admin/builder/BuilderSidebar";
|
import { BuilderSidebar } from "@/components/admin/builder/BuilderSidebar";
|
||||||
import { BuilderCanvas } from "@/components/admin/builder/BuilderCanvas";
|
import { BuilderCanvas } from "@/components/admin/builder/BuilderCanvas";
|
||||||
|
import { BuilderPreview } from "@/components/admin/builder/BuilderPreview";
|
||||||
import { BuilderTopBar } from "@/components/admin/builder/BuilderTopBar";
|
import { BuilderTopBar } from "@/components/admin/builder/BuilderTopBar";
|
||||||
import {
|
import {
|
||||||
BuilderProvider,
|
BuilderProvider,
|
||||||
@ -41,6 +42,7 @@ function BuilderView() {
|
|||||||
}
|
}
|
||||||
sidebar={<BuilderSidebar />}
|
sidebar={<BuilderSidebar />}
|
||||||
canvas={<BuilderCanvas />}
|
canvas={<BuilderCanvas />}
|
||||||
|
preview={<BuilderPreview />}
|
||||||
showPreview={showPreview}
|
showPreview={showPreview}
|
||||||
onTogglePreview={handleTogglePreview}
|
onTogglePreview={handleTogglePreview}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1,10 +1,14 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useCallback, useMemo, useRef, useState } from "react";
|
import React, { useCallback, useMemo, useRef, useState } from "react";
|
||||||
|
import { ArrowDown, ArrowRight, CircleSlash2, GitBranch } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { useBuilderDispatch, useBuilderState } from "@/lib/admin/builder/context";
|
import { useBuilderDispatch, useBuilderState } from "@/lib/admin/builder/context";
|
||||||
import type { ListOptionDefinition, ScreenDefinition } from "@/lib/funnel/types";
|
import type {
|
||||||
|
ListOptionDefinition,
|
||||||
|
NavigationConditionDefinition,
|
||||||
|
ScreenDefinition,
|
||||||
|
} from "@/lib/funnel/types";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function DropIndicator({ isActive }: { isActive: boolean }) {
|
function DropIndicator({ isActive }: { isActive: boolean }) {
|
||||||
@ -18,6 +22,106 @@ function DropIndicator({ isActive }: { isActive: boolean }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const TEMPLATE_TITLES: Record<ScreenDefinition["template"], string> = {
|
||||||
|
list: "Список",
|
||||||
|
form: "Форма",
|
||||||
|
info: "Инфо",
|
||||||
|
date: "Дата",
|
||||||
|
coupon: "Купон",
|
||||||
|
};
|
||||||
|
|
||||||
|
const OPERATOR_LABELS: Record<Exclude<NavigationConditionDefinition["operator"], undefined>, string> = {
|
||||||
|
includesAny: "любой из",
|
||||||
|
includesAll: "все из",
|
||||||
|
includesExactly: "точное совпадение",
|
||||||
|
};
|
||||||
|
|
||||||
|
interface TransitionRowProps {
|
||||||
|
type: "default" | "branch" | "end";
|
||||||
|
label: string;
|
||||||
|
targetLabel?: string;
|
||||||
|
targetIndex?: number | null;
|
||||||
|
optionSummaries?: { id: string; label: string }[];
|
||||||
|
operator?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TransitionRow({
|
||||||
|
type,
|
||||||
|
label,
|
||||||
|
targetLabel,
|
||||||
|
targetIndex,
|
||||||
|
optionSummaries = [],
|
||||||
|
operator,
|
||||||
|
}: TransitionRowProps) {
|
||||||
|
const Icon = type === "branch" ? GitBranch : type === "end" ? CircleSlash2 : ArrowDown;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"relative flex items-start gap-3 rounded-xl border p-3 text-xs transition-colors",
|
||||||
|
type === "branch"
|
||||||
|
? "border-primary/40 bg-primary/5"
|
||||||
|
: "border-border/60 bg-background/90"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"mt-0.5 flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full",
|
||||||
|
type === "branch" ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon className="h-4 w-4" />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-1 flex-col gap-2">
|
||||||
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"text-[11px] font-semibold uppercase tracking-wide",
|
||||||
|
type === "branch" ? "text-primary" : "text-muted-foreground"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
{operator && (
|
||||||
|
<span className="rounded-full bg-primary/10 px-2 py-0.5 text-[10px] uppercase tracking-wide text-primary">
|
||||||
|
{operator}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{optionSummaries.length > 0 && (
|
||||||
|
<div className="flex flex-wrap gap-1.5">
|
||||||
|
{optionSummaries.map((option) => (
|
||||||
|
<span
|
||||||
|
key={option.id}
|
||||||
|
className="rounded-full bg-primary/10 px-2 py-1 text-[11px] font-medium text-primary"
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex items-center gap-2 text-sm text-foreground">
|
||||||
|
{type === "end" ? (
|
||||||
|
<span className="text-muted-foreground">Завершение воронки</span>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<ArrowRight className="h-4 w-4 text-muted-foreground" />
|
||||||
|
{typeof targetIndex === "number" && (
|
||||||
|
<span className="inline-flex items-center rounded-md bg-muted px-2 py-0.5 text-[10px] font-semibold uppercase text-muted-foreground">
|
||||||
|
#{targetIndex + 1}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span className="font-semibold">
|
||||||
|
{targetLabel ?? "Не выбрано"}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function TemplateSummary({ screen }: { screen: ScreenDefinition }) {
|
function TemplateSummary({ screen }: { screen: ScreenDefinition }) {
|
||||||
switch (screen.template) {
|
switch (screen.template) {
|
||||||
case "list": {
|
case "list": {
|
||||||
@ -253,16 +357,27 @@ export function BuilderCanvas() {
|
|||||||
const isDropAfter = dropIndex === screens.length && index === screens.length - 1;
|
const isDropAfter = dropIndex === screens.length && index === screens.length - 1;
|
||||||
const rules = screen.navigation?.rules ?? [];
|
const rules = screen.navigation?.rules ?? [];
|
||||||
const defaultNext = screen.navigation?.defaultNextScreenId;
|
const defaultNext = screen.navigation?.defaultNextScreenId;
|
||||||
|
const isLast = index === screens.length - 1;
|
||||||
|
const defaultTargetIndex = defaultNext
|
||||||
|
? screens.findIndex((candidate) => candidate.id === defaultNext)
|
||||||
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={screen.id} className="relative">
|
<div key={screen.id} className="relative">
|
||||||
<div className="absolute left-0 top-6 hidden h-[calc(100%-1.5rem)] w-px bg-border md:block" aria-hidden />
|
|
||||||
{isDropBefore && <DropIndicator isActive={isDropBefore} />}
|
{isDropBefore && <DropIndicator isActive={isDropBefore} />}
|
||||||
<div className="flex items-start gap-4 md:gap-6">
|
<div className="flex items-start gap-4 md:gap-6">
|
||||||
<div className="relative mt-1 hidden h-3 w-3 flex-shrink-0 rounded-full border-2 border-background bg-primary shadow md:block" />
|
<div className="relative hidden w-8 flex-shrink-0 md:flex md:flex-col md:items-center">
|
||||||
|
<span className="mt-1 h-3 w-3 rounded-full border-2 border-background bg-primary shadow" />
|
||||||
|
{!isLast && (
|
||||||
|
<div className="mt-2 flex h-full flex-col items-center">
|
||||||
|
<div className="flex-1 w-px bg-gradient-to-b from-primary/40 via-border/40 to-transparent" />
|
||||||
|
<ArrowDown className="mt-1 h-4 w-4 text-border/70" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex-1 cursor-grab rounded-2xl border border-border/70 bg-background/95 p-5 shadow-sm transition-all hover:border-primary/40 hover:shadow-md",
|
"relative flex-1 cursor-grab rounded-2xl border border-border/70 bg-background/95 p-5 shadow-sm transition-all hover:border-primary/40 hover:shadow-md",
|
||||||
isSelected && "border-primary/50 ring-2 ring-primary",
|
isSelected && "border-primary/50 ring-2 ring-primary",
|
||||||
dragStateRef.current?.screenId === screen.id && "cursor-grabbing opacity-90"
|
dragStateRef.current?.screenId === screen.id && "cursor-grabbing opacity-90"
|
||||||
)}
|
)}
|
||||||
@ -272,45 +387,47 @@ export function BuilderCanvas() {
|
|||||||
onDragEnd={handleDragEnd}
|
onDragEnd={handleDragEnd}
|
||||||
onClick={() => handleSelectScreen(screen.id)}
|
onClick={() => handleSelectScreen(screen.id)}
|
||||||
>
|
>
|
||||||
<div className="flex flex-wrap items-start justify-between gap-4">
|
<span className="absolute right-5 top-5 inline-flex items-center rounded-full bg-muted px-3 py-1 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
||||||
<div className="flex items-center gap-3">
|
{TEMPLATE_TITLES[screen.template] ?? screen.template}
|
||||||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-primary/10 text-sm font-semibold text-primary">
|
</span>
|
||||||
|
<div className="pr-28">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<div className="flex h-9 w-9 items-center justify-center rounded-full bg-primary/10 text-sm font-semibold text-primary">
|
||||||
{index + 1}
|
{index + 1}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col gap-1">
|
||||||
<span className="text-xs font-semibold uppercase text-muted-foreground">#{screen.id}</span>
|
<span className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
||||||
<span className="text-lg font-semibold text-foreground">
|
#{screen.id}
|
||||||
|
</span>
|
||||||
|
<span className="max-h-14 overflow-hidden break-words text-lg font-semibold leading-tight text-foreground">
|
||||||
{screen.title.text || "Без названия"}
|
{screen.title.text || "Без названия"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span className="inline-flex items-center rounded-full bg-muted px-3 py-1 text-xs font-medium uppercase text-muted-foreground">
|
|
||||||
{screen.template}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{"subtitle" in screen && screen.subtitle?.text && (
|
{("subtitle" in screen && screen.subtitle?.text) && (
|
||||||
<p className="mt-3 text-sm text-muted-foreground">{screen.subtitle.text}</p>
|
<p className="mt-3 max-h-12 overflow-hidden text-sm leading-snug text-muted-foreground">
|
||||||
|
{screen.subtitle.text}
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="mt-4 space-y-4">
|
<div className="mt-4 space-y-5">
|
||||||
<TemplateSummary screen={screen} />
|
<TemplateSummary screen={screen} />
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-xs font-semibold uppercase text-muted-foreground">Переходы</span>
|
<span className="text-xs font-semibold uppercase text-muted-foreground">Переходы</span>
|
||||||
<div className="h-px flex-1 bg-border/60" />
|
<div className="h-px flex-1 bg-border/60" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-3">
|
||||||
<div className="flex flex-col gap-1 rounded-xl border border-border/60 bg-muted/20 p-3 text-xs text-muted-foreground">
|
<TransitionRow
|
||||||
<span className="inline-flex w-fit items-center rounded-full bg-primary/10 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-primary">
|
type={defaultNext ? "default" : "end"}
|
||||||
По умолчанию
|
label={defaultNext ? "По умолчанию" : "Завершение"}
|
||||||
</span>
|
targetLabel={defaultNext ? screenTitleMap[defaultNext] ?? defaultNext : undefined}
|
||||||
<span className="text-sm text-foreground">
|
targetIndex={defaultNext && defaultTargetIndex !== -1 ? defaultTargetIndex : null}
|
||||||
{defaultNext ? screenTitleMap[defaultNext] ?? defaultNext : "Воронка завершится"}
|
/>
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{rules.map((rule, ruleIndex) => {
|
{rules.map((rule, ruleIndex) => {
|
||||||
const condition = rule.conditions[0];
|
const condition = rule.conditions[0];
|
||||||
@ -322,37 +439,28 @@ export function BuilderCanvas() {
|
|||||||
}))
|
}))
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
|
const operatorKey = condition?.operator as
|
||||||
|
| Exclude<NavigationConditionDefinition["operator"], undefined>
|
||||||
|
| undefined;
|
||||||
|
const operatorLabel = operatorKey
|
||||||
|
? OPERATOR_LABELS[operatorKey] ?? operatorKey
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const ruleTargetIndex = screens.findIndex(
|
||||||
|
(candidate) => candidate.id === rule.nextScreenId
|
||||||
|
);
|
||||||
|
const ruleTargetLabel = screenTitleMap[rule.nextScreenId] ?? rule.nextScreenId;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<TransitionRow
|
||||||
key={`${ruleIndex}-${rule.nextScreenId}`}
|
key={`${ruleIndex}-${rule.nextScreenId}`}
|
||||||
className="flex flex-col gap-2 rounded-xl border border-primary/30 bg-primary/5 p-3 text-xs text-muted-foreground"
|
type="branch"
|
||||||
>
|
label="Вариативность"
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
targetLabel={ruleTargetLabel}
|
||||||
<span className="inline-flex items-center rounded-full bg-primary px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-primary-foreground">
|
targetIndex={ruleTargetIndex !== -1 ? ruleTargetIndex : null}
|
||||||
Вариативность
|
optionSummaries={optionSummaries}
|
||||||
</span>
|
operator={operatorLabel}
|
||||||
{condition?.operator && (
|
/>
|
||||||
<span className="rounded-full bg-primary/10 px-2 py-0.5 text-[10px] uppercase tracking-wide text-primary">
|
|
||||||
{condition.operator}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{optionSummaries.length > 0 && (
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{optionSummaries.map((option) => (
|
|
||||||
<span
|
|
||||||
key={option.id}
|
|
||||||
className="inline-flex items-center rounded-lg bg-background px-2 py-1 text-[11px] text-foreground"
|
|
||||||
>
|
|
||||||
{option.label}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="text-sm text-foreground">
|
|
||||||
→ {screenTitleMap[rule.nextScreenId] ?? rule.nextScreenId}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -478,30 +478,28 @@ export function BuilderSidebar() {
|
|||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
{selectedScreen.template === "list" && (
|
<div className="flex flex-col gap-2">
|
||||||
<div className="flex flex-col gap-2">
|
<span className="text-sm font-medium text-muted-foreground">Варианты ответа</span>
|
||||||
<span className="text-sm font-medium text-muted-foreground">Варианты ответа</span>
|
<div className="flex flex-col gap-2 rounded-lg border border-border/60 p-3">
|
||||||
<div className="flex flex-col gap-2 rounded-lg border border-border/60 p-3">
|
{selectedScreen.list.options.map((option) => {
|
||||||
{selectedScreen.list.options.map((option) => {
|
const condition = rule.conditions[0];
|
||||||
const condition = rule.conditions[0];
|
const isChecked = condition.optionIds?.includes(option.id) ?? false;
|
||||||
const isChecked = condition.optionIds?.includes(option.id) ?? false;
|
return (
|
||||||
return (
|
<label key={option.id} className="flex items-center gap-2 text-sm">
|
||||||
<label key={option.id} className="flex items-center gap-2 text-sm">
|
<input
|
||||||
<input
|
type="checkbox"
|
||||||
type="checkbox"
|
checked={isChecked}
|
||||||
checked={isChecked}
|
onChange={() => handleRuleOptionToggle(selectedScreen.id, ruleIndex, option.id)}
|
||||||
onChange={() => handleRuleOptionToggle(selectedScreen.id, ruleIndex, option.id)}
|
/>
|
||||||
/>
|
<span>
|
||||||
<span>
|
{option.label}
|
||||||
{option.label}
|
<span className="text-muted-foreground"> ({option.id})</span>
|
||||||
<span className="text-muted-foreground"> ({option.id})</span>
|
</span>
|
||||||
</span>
|
</label>
|
||||||
</label>
|
);
|
||||||
);
|
})}
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
|
|
||||||
<label className="flex flex-col gap-2">
|
<label className="flex flex-col gap-2">
|
||||||
<span className="text-sm font-medium text-muted-foreground">Следующий экран</span>
|
<span className="text-sm font-medium text-muted-foreground">Следующий экран</span>
|
||||||
|
|||||||
@ -65,7 +65,8 @@ export function FormScreenConfig({ screen, onUpdate }: FormScreenConfigProps) {
|
|||||||
</span>
|
</span>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="h-8 px-3 text-xs text-destructive"
|
size="sm"
|
||||||
|
className="text-destructive"
|
||||||
onClick={() => removeField(index)}
|
onClick={() => removeField(index)}
|
||||||
>
|
>
|
||||||
Удалить
|
Удалить
|
||||||
|
|||||||
@ -76,9 +76,7 @@ export function InfoScreenConfig({ screen, onUpdate }: InfoScreenConfigProps) {
|
|||||||
<select
|
<select
|
||||||
className="rounded-lg border border-border bg-background px-2 py-1"
|
className="rounded-lg border border-border bg-background px-2 py-1"
|
||||||
value={infoScreen.icon?.size ?? "lg"}
|
value={infoScreen.icon?.size ?? "lg"}
|
||||||
onChange={(event) =>
|
onChange={(event) => handleIconChange("size", event.target.value)}
|
||||||
handleIconChange("size", event.target.value as "sm" | "md" | "lg" | "xl")
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<option value="sm">Маленький</option>
|
<option value="sm">Маленький</option>
|
||||||
<option value="md">Средний</option>
|
<option value="md">Средний</option>
|
||||||
|
|||||||
@ -221,13 +221,103 @@ function builderReducer(state: BuilderState, action: BuilderAction): BuilderStat
|
|||||||
}
|
}
|
||||||
case "reorder-screens": {
|
case "reorder-screens": {
|
||||||
const { fromIndex, toIndex } = action.payload;
|
const { fromIndex, toIndex } = action.payload;
|
||||||
const newScreens = [...state.screens];
|
const previousScreens = state.screens;
|
||||||
const [removed] = newScreens.splice(fromIndex, 1);
|
const newScreens = [...previousScreens];
|
||||||
newScreens.splice(toIndex, 0, removed);
|
const [movedScreen] = newScreens.splice(fromIndex, 1);
|
||||||
|
newScreens.splice(toIndex, 0, movedScreen);
|
||||||
|
|
||||||
|
const previousSequentialNext = new Map<string, string | undefined>();
|
||||||
|
const previousIndexMap = new Map<string, number>();
|
||||||
|
|
||||||
|
previousScreens.forEach((screen, index) => {
|
||||||
|
previousSequentialNext.set(screen.id, previousScreens[index + 1]?.id);
|
||||||
|
previousIndexMap.set(screen.id, index);
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalScreens = newScreens.length;
|
||||||
|
|
||||||
|
const rewiredScreens = newScreens.map((screen, index) => {
|
||||||
|
const prevIndex = previousIndexMap.get(screen.id);
|
||||||
|
const prevSequential = previousSequentialNext.get(screen.id);
|
||||||
|
const nextSequential = newScreens[index + 1]?.id;
|
||||||
|
const navigation = screen.navigation;
|
||||||
|
const hasRules = Boolean(navigation?.rules && navigation.rules.length > 0);
|
||||||
|
|
||||||
|
let defaultNext = navigation?.defaultNextScreenId;
|
||||||
|
if (!hasRules) {
|
||||||
|
if (!defaultNext || defaultNext === prevSequential) {
|
||||||
|
defaultNext = nextSequential;
|
||||||
|
}
|
||||||
|
} else if (defaultNext === prevSequential) {
|
||||||
|
defaultNext = nextSequential;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedNavigation = (() => {
|
||||||
|
if ((navigation?.rules && navigation.rules.length > 0) || defaultNext) {
|
||||||
|
return {
|
||||||
|
...(navigation?.rules ? { rules: navigation.rules } : {}),
|
||||||
|
...(defaultNext ? { defaultNextScreenId: defaultNext } : {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
})();
|
||||||
|
|
||||||
|
let updatedHeader = screen.header;
|
||||||
|
if (screen.header?.progress) {
|
||||||
|
const progress = { ...screen.header.progress };
|
||||||
|
const previousProgress = prevIndex !== undefined ? previousScreens[prevIndex]?.header?.progress : undefined;
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof progress.current === "number" &&
|
||||||
|
prevIndex !== undefined &&
|
||||||
|
(progress.current === prevIndex + 1 || previousProgress?.current === prevIndex + 1)
|
||||||
|
) {
|
||||||
|
progress.current = index + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof progress.total === "number") {
|
||||||
|
const previousTotal = previousProgress?.total ?? progress.total;
|
||||||
|
if (previousTotal === previousScreens.length) {
|
||||||
|
progress.total = totalScreens;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedHeader = {
|
||||||
|
...screen.header,
|
||||||
|
progress,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextScreen: BuilderScreen = {
|
||||||
|
...screen,
|
||||||
|
...(updatedHeader ? { header: updatedHeader } : {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (updatedNavigation) {
|
||||||
|
nextScreen.navigation = updatedNavigation;
|
||||||
|
} else if ("navigation" in nextScreen) {
|
||||||
|
delete nextScreen.navigation;
|
||||||
|
}
|
||||||
|
|
||||||
|
return nextScreen;
|
||||||
|
});
|
||||||
|
|
||||||
|
const nextMeta = {
|
||||||
|
...state.meta,
|
||||||
|
firstScreenId: rewiredScreens[0]?.id,
|
||||||
|
};
|
||||||
|
|
||||||
|
const nextSelectedScreenId =
|
||||||
|
movedScreen && state.selectedScreenId === movedScreen.id
|
||||||
|
? movedScreen.id
|
||||||
|
: state.selectedScreenId;
|
||||||
|
|
||||||
return withDirty(state, {
|
return withDirty(state, {
|
||||||
...state,
|
...state,
|
||||||
screens: newScreens,
|
screens: rewiredScreens,
|
||||||
|
meta: nextMeta,
|
||||||
|
selectedScreenId: nextSelectedScreenId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
case "set-selected-screen": {
|
case "set-selected-screen": {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user