Skip to content

Commit 94f47cc

Browse files
committed
refactor(resources/scenarios): migrate scenario/iterations fetchers
1 parent 1f80d0c commit 94f47cc

11 files changed

Lines changed: 575 additions & 508 deletions

File tree

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
import { FormLabel } from '@app-builder/components/Form/Tanstack/FormLabel';
2+
import { Spinner } from '@app-builder/components/Spinner';
3+
import { useLoaderRevalidator } from '@app-builder/contexts/LoaderRevalidatorContext';
4+
import {
5+
activateIterationPayloadSchema,
6+
useActivateIterationMutation,
7+
} from '@app-builder/queries/scenarios/activate-iteration';
8+
import { useRuleSnoozesQuery } from '@app-builder/queries/scenarios/rule-snoozes';
9+
import { useCurrentScenarioIteration } from '@app-builder/routes/_builder+/scenarios+/$scenarioId+/i+/$iterationId+/_layout';
10+
import { handleSubmit } from '@app-builder/utils/form';
11+
import { useForm } from '@tanstack/react-form';
12+
import clsx from 'clsx';
13+
import * as React from 'react';
14+
import { useTranslation } from 'react-i18next';
15+
import { Button, Checkbox, CollapsibleV2, Modal, Tooltip } from 'ui-design-system';
16+
import { Icon } from 'ui-icons';
17+
18+
type ActivateScenarioVersionProps = {
19+
scenario: { id: string; isLive: boolean };
20+
iteration: { id: string; isValid: boolean };
21+
};
22+
23+
export function ActivateScenarioVersion({ scenario, iteration }: ActivateScenarioVersionProps) {
24+
const { t } = useTranslation(['common', 'scenarios']);
25+
const [open, setOpen] = React.useState(false);
26+
27+
const button = (
28+
<Button className="flex-1" variant="primary" disabled={!iteration.isValid}>
29+
<Icon icon="pushtolive" className="size-6" />
30+
{t('scenarios:deployment_modal.activate.button')}
31+
</Button>
32+
);
33+
34+
if (!iteration.isValid) {
35+
return (
36+
<Tooltip.Default
37+
className="text-xs"
38+
content={t('scenarios:deployment_modal.activate.validation_error')}
39+
>
40+
{button}
41+
</Tooltip.Default>
42+
);
43+
}
44+
45+
return (
46+
<Modal.Root open={open} onOpenChange={setOpen}>
47+
<Modal.Trigger asChild>{button}</Modal.Trigger>
48+
<Modal.Content>
49+
<ActivateScenarioVersionContent scenario={scenario} iterationId={iteration.id} />
50+
</Modal.Content>
51+
</Modal.Root>
52+
);
53+
}
54+
55+
function ActivateScenarioVersionContent({
56+
scenario,
57+
iterationId,
58+
}: {
59+
scenario: {
60+
id: string;
61+
isLive: boolean;
62+
};
63+
iterationId: string;
64+
}) {
65+
const { t } = useTranslation(['common', 'scenarios']);
66+
const activateIterationMutation = useActivateIterationMutation(scenario.id, iterationId);
67+
const revalidate = useLoaderRevalidator();
68+
69+
const form = useForm({
70+
defaultValues: {
71+
changeIsImmediate: false,
72+
willBeLive: false,
73+
},
74+
onSubmit: ({ value, formApi }) => {
75+
if (formApi.state.isValid) {
76+
const activateIterationPayload = activateIterationPayloadSchema.safeParse(value);
77+
if (activateIterationPayload.success) {
78+
activateIterationMutation.mutateAsync(activateIterationPayload.data).then(() => {
79+
revalidate();
80+
});
81+
}
82+
}
83+
},
84+
validators: {
85+
onSubmit: activateIterationPayloadSchema,
86+
},
87+
});
88+
89+
return (
90+
<form onSubmit={handleSubmit(form)}>
91+
<Modal.Title>{t('scenarios:deployment_modal.activate.title')}</Modal.Title>
92+
<div className="flex flex-col gap-6 p-6">
93+
<div className="text-s flex flex-col gap-4 font-medium">
94+
<p className="font-semibold">{t('scenarios:deployment_modal.activate.confirm')}</p>
95+
<form.Field
96+
name="willBeLive"
97+
validators={{
98+
onBlur: activateIterationPayloadSchema.shape.willBeLive,
99+
onChange: activateIterationPayloadSchema.shape.willBeLive,
100+
}}
101+
>
102+
{(field) => (
103+
<div className="group flex flex-row items-center gap-2">
104+
<Checkbox
105+
name={field.name}
106+
defaultChecked={field.state.value}
107+
onCheckedChange={(state) =>
108+
state !== 'indeterminate' && field.handleChange(state)
109+
}
110+
/>
111+
<FormLabel name={field.name}>
112+
{scenario.isLive
113+
? t('scenarios:deployment_modal.activate.replace_current_live_version')
114+
: t('scenarios:deployment_modal.activate.will_be_live')}
115+
</FormLabel>
116+
<Tooltip.Default
117+
content={
118+
<p className="max-w-60">
119+
{t('scenarios:deployment_modal.activate.live_version.tooltip')}
120+
</p>
121+
}
122+
>
123+
<Icon icon="tip" className="hover:text-purple-65 text-purple-82 size-6" />
124+
</Tooltip.Default>
125+
</div>
126+
)}
127+
</form.Field>
128+
<form.Field
129+
name="changeIsImmediate"
130+
validators={{
131+
onBlur: activateIterationPayloadSchema.shape.changeIsImmediate,
132+
onChange: activateIterationPayloadSchema.shape.changeIsImmediate,
133+
}}
134+
>
135+
{(field) => (
136+
<div className="group flex flex-row items-center gap-2">
137+
<Checkbox
138+
name={field.name}
139+
defaultChecked={field.state.value}
140+
onCheckedChange={(state) =>
141+
state !== 'indeterminate' && field.handleChange(state)
142+
}
143+
/>
144+
<FormLabel name={field.name}>
145+
{t('scenarios:deployment_modal.activate.change_is_immediate')}
146+
</FormLabel>
147+
</div>
148+
)}
149+
</form.Field>
150+
<div className="min-h-6 w-full">
151+
<RuleSnoozeDetail scenarioId={scenario.id} iterationId={iterationId} />
152+
</div>
153+
</div>
154+
<div className="flex flex-1 flex-row gap-2">
155+
<Modal.Close asChild>
156+
<Button className="flex-1" variant="secondary" name="cancel">
157+
{t('common:cancel')}
158+
</Button>
159+
</Modal.Close>
160+
<Button className="flex-1" variant="primary" type="submit">
161+
<Icon icon="pushtolive" className="size-6" />
162+
{t('scenarios:deployment_modal.activate.button')}
163+
</Button>
164+
</div>
165+
</div>
166+
</form>
167+
);
168+
}
169+
170+
function RuleSnoozeDetail({
171+
scenarioId,
172+
iterationId,
173+
}: {
174+
scenarioId: string;
175+
iterationId: string;
176+
}) {
177+
const { t } = useTranslation(['common', 'scenarios']);
178+
const iteration = useCurrentScenarioIteration();
179+
const ruleSnoozesQuery = useRuleSnoozesQuery(scenarioId, iterationId);
180+
181+
if (ruleSnoozesQuery.isPending) return <Spinner className="size-5 shrink-0" />;
182+
183+
if (ruleSnoozesQuery.isError) {
184+
return <div className="text-s text-red-47">{t('common:errors.unknown')}</div>;
185+
}
186+
187+
const ruleSnoozes = ruleSnoozesQuery.data.ruleSnoozes;
188+
const hasSnoozesActive = ruleSnoozes.some((snooze) => snooze.hasSnoozesActive);
189+
190+
if (!hasSnoozesActive) {
191+
return (
192+
<p className="text-grey-50 text-s first-letter:capitalize">
193+
{t('scenarios:deployment_modal.activate.without_rule_snooze')}
194+
</p>
195+
);
196+
}
197+
198+
return (
199+
<CollapsibleV2.Provider>
200+
<CollapsibleV2.Title className="text-grey-50 group flex flex-row items-center">
201+
<Icon
202+
icon="arrow-2-up"
203+
aria-hidden
204+
className="-ml-2 size-5 rotate-90 transition-transform duration-200 group-aria-expanded:rotate-180 group-data-initial:rotate-180"
205+
/>
206+
<span className="text-s mr-1 first-letter:capitalize">
207+
{t('scenarios:deployment_modal.activate.with_rule_snooze')}
208+
</span>
209+
</CollapsibleV2.Title>
210+
<CollapsibleV2.Content>
211+
<div className="max-h-40 overflow-y-auto p-1">
212+
<ul className="list-none">
213+
{iteration.rules.map((rule) => {
214+
const hasSnoozesActive = ruleSnoozes.find(
215+
(snooze) => snooze.ruleId === rule.id,
216+
)?.hasSnoozesActive;
217+
return (
218+
<li key={rule.id} className="flex flex-row">
219+
<Icon
220+
className={clsx(
221+
'size-5 shrink-0',
222+
hasSnoozesActive === true && 'text-green-38',
223+
hasSnoozesActive === false && 'text-red-47',
224+
)}
225+
icon={hasSnoozesActive ? 'tick' : 'cross'}
226+
/>
227+
<span className="text-s text-grey-00 font-normal">{rule.name}</span>
228+
</li>
229+
);
230+
})}
231+
</ul>
232+
</div>
233+
</CollapsibleV2.Content>
234+
</CollapsibleV2.Provider>
235+
);
236+
}

0 commit comments

Comments
 (0)