diff --git a/izanami-frontend/src/App.css b/izanami-frontend/src/App.css
index dc9896bb6..22b1f6ba9 100644
--- a/izanami-frontend/src/App.css
+++ b/izanami-frontend/src/App.css
@@ -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 {
diff --git a/izanami-frontend/src/components/ConditionInput.tsx b/izanami-frontend/src/components/ConditionInput.tsx
index 99fe22957..5264ead3e 100644
--- a/izanami-frontend/src/components/ConditionInput.tsx
+++ b/izanami-frontend/src/components/ConditionInput.tsx
@@ -1,28 +1,31 @@
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,
@@ -30,37 +33,117 @@ export function ConditionsInput() {
});
const resultType = watch("resultType");
-
+ const conditions = watch("conditions");
return (
<>
- {fields.map((condition, index) => (
-
-
- {resultType === "boolean"
- ? "Activation condition"
- : "Alternative value"}{" "}
- #{index}{" "}
- {
- remove(index);
- }}
- >
- Delete
-
-
-
-
- ))}
+ {fields.length > 0 ? (
+ resultType === "boolean" ? (
+ Activation conditions
+ ) : (
+ Alternative values
+ )
+ ) : null}
+ {fields.map(({ id }, index) => {
+ const condition = conditions?.[index];
+ const emptyCondition =
+ Object.entries(condition).filter(([key, value]) => value !== "")
+ .length === 0;
+
+ return (
+
+
+
+
+
+
+
+ {resultType === "boolean" ? (
+ <>
+
+ Activation condition #{index}
+
+ {emptyCondition ? (
+ <> {""}>
+ ) : (
+ ""
+ )}
+ >
+ ) : emptyCondition ? (
+ ""
+ ) : (
+ <>
+ Alternative value{" "}
+ {condition?.value}
+ >
+ )}
+
+
+ {condition && !emptyCondition ? (
+ <>
+
+ >
+ ) : null}
+
+
{
+ remove(index);
+ }}
+ >
+ Delete
+
+
+
+
+
+
+
+ );
+ })}
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
@@ -90,6 +173,22 @@ export function ConditionsInput() {
);
}
+function ConditionSummary(props: { condition: TClassicalCondition }) {
+ const { period, rule } = props.condition;
+
+ return (
+
+ {period &&
}
+ {rule &&
}
+
+ );
+}
+
function ConditionInput(props: { index: number }) {
const [specificPeriods, setSpecificPeriods] = React.useState(false);
const { index } = props;
@@ -129,7 +228,7 @@ function ConditionInput(props: { index: number }) {
{resultType !== "boolean" && (
<>
- Result value
+ Result value
Alternative value
@@ -157,7 +256,7 @@ function ConditionInput(props: { index: number }) {
>
)}
- Time rule
+ Time rule
Active only on specific periods
@@ -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);
@@ -195,7 +302,7 @@ function ConditionInput(props: { index: number }) {
- User rule
+ User rule
Strategy to use
diff --git a/izanami-frontend/src/components/FeatureForm.tsx b/izanami-frontend/src/components/FeatureForm.tsx
index 1e840cacf..4254e99bf 100644
--- a/izanami-frontend/src/components/FeatureForm.tsx
+++ b/izanami-frontend/src/components/FeatureForm.tsx
@@ -1251,7 +1251,7 @@ export function V2FeatureForm(props: {
}}
/>
- {type === "Classic" && }
+ {type === "Classic" && }
{type === "Existing WASM script" && }
{type === "New WASM script" && }
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[] {
@@ -242,11 +242,17 @@ function NonBooleanConditionsDetails({
const { value } = resultDetail;
return (
<>
-
Base value is
-
{value}
{conditions.map((cond, idx) => {
- return
;
+ return (
+ <>
+
+
-OR-
+ >
+ );
})}
+
Value is
+
{value}
+ otherwise
>
);
}
@@ -267,7 +273,7 @@ function NonBooleanConditionDetails({
);
}
-function ConditionDetails({
+export function ConditionDetails({
conditions,
resultDetail,
}: {
@@ -1587,16 +1593,18 @@ export function FeatureTable(props: {
if (!maybeContexts || maybeContexts.length === 0) {
return (
+ {feature.name}
- {feature.name}
);
} else {
return (
-
-
{feature.name}
+
+ {feature.name}
+
+
- {type === "Classic" &&
}
+ {type === "Classic" &&
}
{type === "Existing WASM script" &&
}
{type === "New WASM script" &&
}
diff --git a/izanami-frontend/src/components/ResultTypeIcon.tsx b/izanami-frontend/src/components/ResultTypeIcon.tsx
index 41e9ae133..4b423c9fe 100644
--- a/izanami-frontend/src/components/ResultTypeIcon.tsx
+++ b/izanami-frontend/src/components/ResultTypeIcon.tsx
@@ -6,8 +6,8 @@ const NumberTypeIcon = () => (
@@ -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 (
{rs === "boolean" ? (
) : rs === "string" ? (
diff --git a/izanami-frontend/src/index.scss b/izanami-frontend/src/index.scss
index 89e8b9746..0c348fb89 100644
--- a/izanami-frontend/src/index.scss
+++ b/izanami-frontend/src/index.scss
@@ -278,6 +278,17 @@ input[type="time"] {
z-index: 1001;
}
+.condition-accordion {
+ .accordion-body {
+ background-color: var(--bg-color_level25) !important;
+ }
+
+ .accordion-button {
+ background-color: var(--bg-color_level3) !important;
+ box-shadow: none !important;
+ }
+}
+
.log-result-count {
color: var(--color_level3);
}
diff --git a/izanami-frontend/src/utils/icons.tsx b/izanami-frontend/src/utils/icons.tsx
index a9ceb226b..fc1b51233 100644
--- a/izanami-frontend/src/utils/icons.tsx
+++ b/izanami-frontend/src/utils/icons.tsx
@@ -20,7 +20,7 @@ export function StringIcon(props: object) {
return (
diff --git a/izanami-frontend/src/utils/queries.tsx b/izanami-frontend/src/utils/queries.tsx
index e57d4a13a..66d05c241 100644
--- a/izanami-frontend/src/utils/queries.tsx
+++ b/izanami-frontend/src/utils/queries.tsx
@@ -1361,37 +1361,15 @@ export function importUsersFile(tenant: string, file: FileList): Promise {
});
}
-export function searchEntities(
- query: string,
- filter: string[] = []
-): Promise {
- let filterString = "";
- if (filter.length > 0) {
- filterString = filter.map(encodeURIComponent).join("&filter=");
- }
- return handleFetchJsonResponse(
- fetch(
- `/api/admin/search?query=${encodeURIComponent(query)}${
- filterString ? `&filter=${filterString}` : ""
- }`
- )
- );
+export function searchEntities(query: string): Promise {
+ return handleFetchJsonResponse(fetch(`/api/admin/search?query=${query}`));
}
export function searchEntitiesByTenant(
tenant: string,
- query: string,
- filter: string[] = []
+ query: string
): Promise {
- let filterString = "";
- if (filter.length > 0) {
- filterString = filter.map(encodeURIComponent).join("&filter=");
- }
return handleFetchJsonResponse(
- fetch(
- `/api/admin/tenants/${tenant}/search?query=${encodeURIComponent(query)}${
- filterString ? `&filter=${filterString}` : ""
- }`
- )
+ fetch(`/api/admin/tenants/${tenant}/search?query=${query}`)
);
}
diff --git a/izanami-frontend/src/utils/types.ts b/izanami-frontend/src/utils/types.ts
index 810f2efeb..e08fcf793 100644
--- a/izanami-frontend/src/utils/types.ts
+++ b/izanami-frontend/src/utils/types.ts
@@ -321,6 +321,7 @@ export interface TWasmConfigSource {
}
export interface TClassicalCondition {
+ id: string;
rule?: TFeatureRule;
period?: TFeaturePeriod;
}
diff --git a/izanami-frontend/tests/audit.spec.ts b/izanami-frontend/tests/audit.spec.ts
index aae92006e..566350fc4 100644
--- a/izanami-frontend/tests/audit.spec.ts
+++ b/izanami-frontend/tests/audit.spec.ts
@@ -8,7 +8,7 @@ import {
} from "./testBuilder";
test.use({
- headless: false,
+ headless: true,
});
test.describe("Audit screen should", () => {
@@ -86,6 +86,7 @@ test.describe("Audit screen should", () => {
await page.getByLabel("Bulk action").click();
await page.getByRole("option", { name: "Enable" }).click();
await page.getByRole("button", { name: "Enable 6 features" }).click();
+ await expect(page.getByRole("cell", { name: "Enabled" })).toHaveCount(6);
await page.getByRole("link", { name: "Logs" }).click();
await expect(page.getByText("30 results")).toBeVisible();
await expect(page.locator("li").filter({ hasText: "..." })).toBeVisible();
diff --git a/izanami-frontend/tests/features.spec.ts b/izanami-frontend/tests/features.spec.ts
index 2cee8728c..32ea20d1c 100644
--- a/izanami-frontend/tests/features.spec.ts
+++ b/izanami-frontend/tests/features.spec.ts
@@ -203,6 +203,7 @@ test.describe("Project screen should", () => {
await expect(page.getByRole("row", { name: "test" })).toBeVisible();
await featureAction(page, "Edit");
+ await page.getByRole("button", { name: "Activation condition #0" }).click();
await page.getByLabel("date-range-from").fill("2023-10-25T00:00");
await page.getByLabel("date-range-to").fill("2030-01-02T10:10");
await page.getByLabel("Remove TUESDAY").click();
@@ -216,7 +217,7 @@ test.describe("Project screen should", () => {
await page
.getByRole("cell", {
- name: "Active : from December 8th, 2024 at 12:00 AM to January 1st, 2030 at 10:10 AM on MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY from 10:00:00 to 14:00:00 For all users",
+ name: "Active : from October 25th, 2023 at 12:00 AM to January 2nd, 2030 at 10:10 AM on MONDAY, WEDNESDAY, THURSDAY, FRIDAY, SUNDAY from 11:01:00 to 15:15:00 for all users",
})
.click();
});