Skip to content

Commit

Permalink
feat: hide complete conditions by default on feature update
Browse files Browse the repository at this point in the history
This also reworks feature type display
  • Loading branch information
helakaraa authored and ptitFicus committed Dec 9, 2024
1 parent bec4938 commit ec01482
Show file tree
Hide file tree
Showing 12 changed files with 195 additions and 88 deletions.
6 changes: 3 additions & 3 deletions izanami-frontend/src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,10 @@ header .nav-link {
}

.feature-separator {
text-align: center;
margin-left: 24px;
font-weight: bold;
margin-top: 8px;
margin-bottom: 8px;
margin-top: 4px;
margin-bottom: 4px;
}

.error-message {
Expand Down
183 changes: 145 additions & 38 deletions izanami-frontend/src/components/ConditionInput.tsx
Original file line number Diff line number Diff line change
@@ -1,66 +1,149 @@
import * as React from "react";
import {
DAYS,
TClassicalCondition,
TClassicalContextOverload,
THourPeriod,
TValuedCondition,
ValuedFeature,
} from "../utils/types";
import Select from "react-select";
import CreatableSelect from "react-select/creatable";
import { customStyles } from "../styles/reactSelect";
import { Strategy } from "./FeatureTable";
import { Rule, Strategy, Period as PeriodDetails } from "./FeatureTable";
import { useEffect } from "react";
import {
useFieldArray,
Controller,
useFormContext,
FieldErrors,
useWatch,
} from "react-hook-form";
import { format, parse, startOfDay } from "date-fns";
import { DEFAULT_TIMEZONE, TimeZoneSelect } from "./TimeZoneSelect";
import { ErrorDisplay } from "./FeatureForm";
import { Tooltip } from "./Tooltip";

export function ConditionsInput() {
const { control, watch } = useFormContext();
export function ConditionsInput(props: { folded: boolean }) {
const { control, watch, getValues } = useFormContext();

const { fields, append, remove } = useFieldArray({
control,
name: "conditions",
});

const resultType = watch("resultType");

const conditions = watch("conditions");
return (
<>
{fields.map((condition, index) => (
<fieldset
className="mt-2 sub_container sub_container-bglighter anim__popUp"
key={`condition-${index}`}
>
<h5>
{resultType === "boolean"
? "Activation condition"
: "Alternative value"}{" "}
#{index}{" "}
<button
className="btn btn-danger btn-sm m-2"
type="button"
onClick={() => {
remove(index);
}}
>
Delete
</button>
</h5>
<ConditionInput index={index} />
</fieldset>
))}
{fields.length > 0 ? (
resultType === "boolean" ? (
<h5 className="mt-4">Activation conditions</h5>
) : (
<h5 className="mt-4">Alternative values</h5>
)
) : null}
{fields.map(({ id }, index) => {
const condition = conditions?.[index];
const emptyCondition =
Object.entries(condition).filter(([key, value]) => value !== "")
.length === 0;

return (
<div
className="accordion condition-accordion"
id={`${id}-accordion`}
key={id}
>
<div className="accordion-item mt-3">
<h3 className="accordion-header">
<button
className={`accordion-button ${
props.folded ? "collapsed" : ""
}`}
type="button"
data-bs-toggle="collapse"
data-bs-target={`#${id}-accordion-collapse`}
aria-expanded={!props.folded}
aria-controls={`${id}-accordion-collapse`}
>
<div className="d-flex align-items-center justify-content-between w-100">
<div className="d-flex flex-row align-items-center">
<div>
{resultType === "boolean" ? (
<>
<span className="fw-bold">
Activation condition #{index}
</span>
{emptyCondition ? (
<>&nbsp;{"<Empty condition>"}</>
) : (
""
)}
</>
) : emptyCondition ? (
"<No value specified>"
) : (
<>
Alternative value{" "}
<span className="fw-bold">{condition?.value}</span>
</>
)}
&nbsp;
</div>
{condition && !emptyCondition ? (
<>
<ConditionSummary condition={condition} />
</>
) : null}
</div>
<button
className="btn btn-danger btn-sm"
type="button"
onClick={(e) => {
remove(index);
}}
>
Delete
</button>
</div>
</button>
</h3>
<div
className={`accordion-collapse collapse ${
props.folded ? "" : "show"
}`}
aria-labelledby="headingOne"
data-bs-parent={`#${id}-accordion`}
id={`${id}-accordion-collapse`}
>
<div className="accordion-body">
<ConditionInput index={index} />
</div>
</div>
</div>
</div>
);
})}
<div className="d-flex align-items-center justify-content-end mb-2 ms-3 mt-3">
<button
className="btn btn-secondary btn-sm"
type="button"
onClick={() => append({})}
onClick={() => {
append({});
// This is super dirty, however bootstrap accordion state is really hard to control programatically
// the need is to have existing accordions closed by default (on edition) but to have new one opened

if (props.folded) {
setTimeout(() => {
const buttons = document.querySelectorAll(
"button.accordion-button"
);
const last = buttons?.[buttons.length - 1] as HTMLButtonElement;
last.click();
}, 0);
}
}}
>
{resultType === "boolean"
? fields.length > 0
Expand Down Expand Up @@ -90,6 +173,22 @@ export function ConditionsInput() {
);
}

function ConditionSummary(props: { condition: TClassicalCondition }) {
const { period, rule } = props.condition;

return (
<div
className="d-flex flex-column border-start border-2"
style={{
paddingLeft: "8px",
}}
>
{period && <PeriodDetails period={period as any} />}
{rule && <Rule rule={rule} />}
</div>
);
}

function ConditionInput(props: { index: number }) {
const [specificPeriods, setSpecificPeriods] = React.useState(false);
const { index } = props;
Expand Down Expand Up @@ -129,7 +228,7 @@ function ConditionInput(props: { index: number }) {
{resultType !== "boolean" && (
<>
<legend>
<h6>Result value</h6>
<h5>Result value</h5>
</legend>
<label className="mb-4">
Alternative value
Expand Down Expand Up @@ -157,7 +256,7 @@ function ConditionInput(props: { index: number }) {
</>
)}
<legend>
<h6>Time rule</h6>
<h5>Time rule</h5>
</legend>
<label>
Active only on specific periods
Expand All @@ -175,14 +274,22 @@ function ConditionInput(props: { index: number }) {
if (e.target.checked) {
const period = getValues(`conditions.${index}.period`);
if (!period || JSON.stringify(period) === "{}") {
setValue(`conditions.${index}.period`, {
begin: startOfDay(new Date()),
activationDays: {
days: DAYS.slice(),
setValue(
`conditions.${index}.period`,
{
begin: startOfDay(new Date()),
activationDays: {
days: DAYS.slice(),
},
hourPeriods: [],
timezone:
Intl.DateTimeFormat().resolvedOptions().timeZone,
end: null as any,
},
hourPeriods: [],
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
});
{
shouldValidate: true,
}
);
}
} else {
setValue(`conditions.${index}.period`, undefined);
Expand All @@ -195,7 +302,7 @@ function ConditionInput(props: { index: number }) {
</fieldset>
<fieldset style={{ border: "none" }} className="mt-3">
<legend>
<h6>User rule</h6>
<h5>User rule</h5>
</legend>
<label>
Strategy to use
Expand Down
2 changes: 1 addition & 1 deletion izanami-frontend/src/components/FeatureForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1251,7 +1251,7 @@ export function V2FeatureForm(props: {
}}
/>
</label>
{type === "Classic" && <ConditionsInput />}
{type === "Classic" && <ConditionsInput folded={!!defaultValue} />}
{type === "Existing WASM script" && <ExistingScript />}
{type === "New WASM script" && <WasmInput />}
<div
Expand Down
32 changes: 20 additions & 12 deletions izanami-frontend/src/components/FeatureTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ function PeriodDetails(props: { period: TFeaturePeriod }): JSX.Element {
}
}

function Period({ period }: { period: TFeaturePeriod }): JSX.Element {
export function Period({ period }: { period: TFeaturePeriod }): JSX.Element {
let display = "";
if (period.begin && period.end) {
display = `from ${format(period.begin, "PPPp")} to ${format(
Expand All @@ -173,14 +173,14 @@ function Period({ period }: { period: TFeaturePeriod }): JSX.Element {
);
}

function Rule(props: { rule: TFeatureRule }): JSX.Element {
export function Rule(props: { rule: TFeatureRule }): JSX.Element {
const { rule } = props;
if (isPercentageRule(rule)) {
return <>For {`${rule.percentage}% of users`}</>;
return <>for {`${rule.percentage}% of users`}</>;
} else if (isUserListRule(rule)) {
return <>{`Only for : ${rule.users.join(", ")}`}</>;
return <>{`only for : ${rule.users.join(", ")}`}</>;
} else {
return <>For all users</>;
return <>for all users</>;
}
}
export function possiblePaths(contexts: TContext[], path = ""): string[] {
Expand Down Expand Up @@ -242,11 +242,17 @@ function NonBooleanConditionsDetails({
const { value } = resultDetail;
return (
<>
<span className="fw-semibold">Base value is</span>&nbsp;
<span className="fst-italic">{value}</span>
{conditions.map((cond, idx) => {
return <NonBooleanConditionDetails key={idx} condition={cond} />;
return (
<>
<NonBooleanConditionDetails key={idx} condition={cond} />
<div className="feature-separator">-OR-</div>
</>
);
})}
<span className="fw-semibold">Value is</span>&nbsp;
<span className="fst-italic">{value}</span>
&nbsp;otherwise
</>
);
}
Expand All @@ -267,7 +273,7 @@ function NonBooleanConditionDetails({
);
}

function ConditionDetails({
export function ConditionDetails({
conditions,
resultDetail,
}: {
Expand Down Expand Up @@ -1587,16 +1593,18 @@ export function FeatureTable(props: {
if (!maybeContexts || maybeContexts.length === 0) {
return (
<div className="d-flex justify-start align-items-center">
<span style={{ paddingRight: "0.25rem" }}>{feature.name}</span>
<ResultTypeIcon resultType={feature.resultType} />
<span style={{ paddingLeft: "0.75rem" }}>{feature.name}</span>
</div>
);
} else {
return (
<div className="d-flex align-items-center">
<ResultTypeIcon resultType={feature.resultType} />
<div className="d-flex py-2">
<span style={{ paddingLeft: "0.75rem" }}>{feature.name}</span>
<span style={{ paddingRight: "0.25rem" }}>
{feature.name}
</span>
<ResultTypeIcon resultType={feature.resultType} />
<button
className="top-10 align-self-start badge rounded-pill bg-primary-outline"
role="button"
Expand Down
2 changes: 1 addition & 1 deletion izanami-frontend/src/components/OverloadCreationForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ export function OverloadCreationForm(props: {
}}
/>
</label>
{type === "Classic" && <ConditionsInput />}
{type === "Classic" && <ConditionsInput folded={true} />}
{type === "Existing WASM script" && <ExistingScript />}
{type === "New WASM script" && <WasmInput />}
<div className="d-flex justify-content-end">
Expand Down
8 changes: 4 additions & 4 deletions izanami-frontend/src/components/ResultTypeIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ const NumberTypeIcon = () => (
<svg
xmlns=" http://www.w3.org/2000/svg"
viewBox="0 0 301.95 240.38"
width="18"
height="18"
width="12"
height="12"
style={{ marginTop: "-2px" }}
>
<g id="Calque_2" data-name="Calque 2">
Expand Down Expand Up @@ -79,12 +79,12 @@ export const ResultTypeIcon = (props: {
color?: string;
}) => {
const rs = props.resultType;
const color = props.color ?? "var(--color_level3)";
const color = props.color ?? "var(--color_level1)";
return (
<div className="custom-badge" style={{ fill: color }} aria-hidden>
{rs === "boolean" ? (
<i
style={{ fontSize: "18px", color: color }}
style={{ fontSize: "14px", color: color }}
className="fa-solid fa-toggle-off"
></i>
) : rs === "string" ? (
Expand Down
Loading

0 comments on commit ec01482

Please sign in to comment.