Skip to content

Commit

Permalink
feat 477 : select dataformat front (#556)
Browse files Browse the repository at this point in the history
  • Loading branch information
Volubyl authored Feb 2, 2023
1 parent ec9d6d8 commit 75b9414
Show file tree
Hide file tree
Showing 13 changed files with 907 additions and 139 deletions.
4 changes: 2 additions & 2 deletions client/src/definitions/datasets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ export type DatasetFormInitial = Omit<Dataset, "id">;

export type DatasetFormData = Omit<
Dataset,
"id" | "catalogRecord" | "headlines"
> & { organizationSiret: string };
"id" | "catalogRecord" | "headlines" | "formats"
> & { organizationSiret: string; formats: Partial<DataFormat>[] };

export type DatasetCreateData = Omit<DatasetFormData, "tags" | "formats"> & {
tagIds: string[];
Expand Down
28 changes: 5 additions & 23 deletions client/src/lib/components/DatasetForm/DatasetForm.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ describe("Test the dataset form", () => {
});

test('The "formats" field is present', async () => {
const { getAllByRole } = render(DatasetForm, {
const { getByLabelText } = render(DatasetForm, {
catalog,
formats: [
{
Expand All @@ -73,8 +73,10 @@ describe("Test the dataset form", () => {
},
],
});
const checkboxes = getAllByRole("checkbox");
expect(checkboxes.length).toBeGreaterThan(0);
const formatfield = getByLabelText("Format(s) des données", {
exact: false,
});
expect(formatfield).toBeInTheDocument();
});

test('The "geographicalCoverage" field is present', async () => {
Expand Down Expand Up @@ -103,26 +105,6 @@ describe("Test the dataset form", () => {
expect(tags).toBeInTheDocument();
});

test("At least one format is required", async () => {
const { getAllByRole } = render(DatasetForm, {
catalog,
formats: [
{
id: 55,
name: "fichier tabulaire",
},
],
});
const checkboxes = getAllByRole("checkbox", { checked: false });
checkboxes.forEach((checkbox) => expect(checkbox).toBeRequired());
await fireEvent.click(checkboxes[0]);
expect(checkboxes[0]).toBeChecked();
checkboxes
.slice(1)
.forEach((checkbox) => expect(checkbox).not.toBeChecked());
checkboxes.forEach((checkbox) => expect(checkbox).not.toBeRequired());
});

test('The "producerEmail" field is present', () => {
const { getByLabelText } = render(DatasetForm, { catalog, formats: [] });
const producerEmail = getByLabelText(
Expand Down
101 changes: 32 additions & 69 deletions client/src/lib/components/DatasetForm/DatasetForm.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
UPDATE_FREQUENCY_LABELS,
} from "src/constants";
import { formatHTMLDate } from "$lib/util/format";
import RequiredMarker from "../RequiredMarker/RequiredMarker.svelte";
import { account } from "src/lib/stores/auth";
import ContactEmailsField from "../ContactEmailsField/ContactEmailsField.svelte";
import GeographicalCoverageField from "./_GeographicalCoverageField.svelte";
Expand All @@ -30,6 +29,7 @@
import ExtraField from "./_ExtraField.svelte";
import Alert from "../Alert/Alert.svelte";
import type { DataFormat } from "src/definitions/dataformat";
import FormatSelector from "./_FormatSelector.svelte";
export let submitLabel = "Publier la fiche de données";
export let loadingLabel = "Publication en cours...";
Expand All @@ -42,15 +42,18 @@
export let initial: DatasetFormInitial | null = null;
const dispatch =
createEventDispatcher<{ save: DatasetFormData; touched: boolean }>();
const dispatch = createEventDispatcher<{
save: DatasetFormData;
touched: boolean;
createDataFormat: string;
}>();
type DatasetFormValues = {
organizationSiret: string;
title: string;
description: string;
service: string;
dataFormats: boolean[];
formats: Partial<DataFormat>[];
producerEmail: string | null;
contactEmails: string[];
geographicalCoverage: string;
Expand All @@ -69,9 +72,7 @@
title: initial?.title || "",
description: initial?.description || "",
service: initial?.service || "",
dataFormats: formats.map(
({ id }) => !!(initial?.formats || []).find((v) => v.id === id)
),
formats: initial ? initial.formats : [],
producerEmail: initial?.producerEmail || "",
contactEmails: initial?.contactEmails || [$account?.email || ""],
lastUpdatedAt: initial?.lastUpdatedAt
Expand All @@ -92,9 +93,6 @@
publicationRestriction: initial?.publicationRestriction || "no_restriction",
};
// Handle this value manually.
const dataFormatsValue = initialValues.dataFormats;
const { form, errors, handleChange, handleSubmit, updateValidateField } =
createForm({
initialValues,
Expand All @@ -104,7 +102,15 @@
title: yup.string().required("Ce champ est requis"),
description: yup.string().required("Ce champs est requis"),
service: yup.string().required("Ce champs est requis"),
dataFormats: yup.array(yup.boolean()).length(dataFormatsValue.length),
formats: yup
.array()
.of(
yup.object().shape({
name: yup.string(),
id: yup.string().nullable(),
})
)
.min(1, "Veuillez séléctionner au moins 1 format de donnée"),
producerEmail: yup
.string()
.email("Ce champ doit contenir une adresse e-mail valide")
Expand Down Expand Up @@ -135,10 +141,6 @@
extraFieldValues: yup.array().of(yup.string()),
}),
onSubmit: (values: DatasetFormValues) => {
const updatedFormats = values.dataFormats
.map((checked, index) => (checked ? formats[index] : null))
.filter((item) => item) as DataFormat[];
// Ensure "" becomes null.
const producerEmail = values.producerEmail
? values.producerEmail
Expand Down Expand Up @@ -167,7 +169,6 @@
const data: DatasetFormData = {
...values,
formats: updatedFormats,
producerEmail,
contactEmails,
lastUpdatedAt,
Expand All @@ -184,6 +185,7 @@
$: emailErrors = $errors.contactEmails as unknown as string[];
export const submitForm = (event: Event) => {
event.preventDefault();
handleSubmit(event);
};
Expand All @@ -192,14 +194,8 @@
dispatch("touched", true);
};
const hasError = (error: string | string[]) => {
return typeof error === "string" && Boolean(error);
};
const handleDataformatChange = (event: Event, index: number) => {
const { checked } = event.target as HTMLInputElement;
dataFormatsValue[index] = checked;
updateValidateField("dataFormats", dataFormatsValue);
const handleDataFormatChanges = async (event: CustomEvent<DataFormat[]>) => {
updateValidateField("formats", event.detail);
dispatch("touched");
};
Expand Down Expand Up @@ -232,11 +228,16 @@
updateValidateField("extraFieldValues", v);
dispatch("touched");
};
const handleAddDataFormat = (e: CustomEvent<string>) => {
dispatch("createDataFormat", e.detail);
};
</script>

<form
on:submit={submitForm}
data-bitwarden-watching="1"
novalidate
aria-label="Informations sur le jeu de données"
>
<h2 id="information-generales" class="fr-mb-5w">Informations générales</h2>
Expand Down Expand Up @@ -283,51 +284,13 @@
<h2 id="source-formats" class="fr-mt-6w fr-mb-5w">Sources et formats</h2>

<div class="form--content fr-mb-8w">
<fieldset
class="fr-fieldset fr-mb-4w {hasError($errors.dataFormats)
? 'fr-fieldset--error'
: ''}"
aria-describedby={hasError($errors.dataFormats)
? "dataformats-desc-error"
: null}
>
<legend
class="fr-fieldset__legend fr-text--regular"
id="dataformats-hint-legend"
>
Format(s) des données
<RequiredMarker />
<span class="fr-hint-text" id="select-hint-dataformats-hint">
Sélectionnez ici les différents formats de données qu'un réutilisateur
potentiel pourrait exploiter.
</span>
</legend>
<div class="fr-fieldset__content">
{#each formats as { id, name }, index}
{@const identifier = `dataformats-${id}`}
<div class="fr-checkbox-group">
<input
type="checkbox"
id={identifier}
name="dataformats"
value={id}
required={dataFormatsValue.every((checked) => !checked)}
checked={dataFormatsValue[index]}
on:change={(event) => handleDataformatChange(event, index)}
/>
<label for={identifier}>
{name}
</label>
</div>
{/each}
</div>
{#if hasError($errors.dataFormats)}
<p id="dataformats-desc-error" class="fr-error-text">
{$errors.dataFormats}
</p>
{/if}
</fieldset>

<FormatSelector
formatOptions={formats}
error={typeof $errors.formats === "string" ? $errors.formats : ""}
on:addItem={handleAddDataFormat}
on:change={handleDataFormatChanges}
bind:selectedFormatOptions={initialValues.formats}
/>
<InputField
name="technicalSource"
label="Système d'information source"
Expand Down
87 changes: 87 additions & 0 deletions client/src/lib/components/DatasetForm/_FormatSelector.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<script lang="ts">
import type { DataFormat } from "src/definitions/dataformat";
import type { SelectOption } from "src/definitions/form";
import {
transformDataFormatToSelectOption,
transoformSelectOptionToDataFormat,
} from "src/lib/transformers/form";
import { createEventDispatcher } from "svelte";
import Tag from "../Tag/Tag.svelte";
import SearcheableComboBox from "../SearchableComboBox/SearcheableComboBox.svelte";
const dispatch = createEventDispatcher<{
change: Partial<DataFormat>[];
}>();
export let formatOptions: DataFormat[];
export let error: string;
export let selectedFormatOptions: Partial<DataFormat>[] = [];
const handleSelectFormat = (e: CustomEvent<SelectOption<number>>) => {
const selectedOption = transoformSelectOptionToDataFormat(e.detail);
const itemAlreadyExists =
selectedFormatOptions.findIndex(
(item) => item.id == selectedOption.id
) !== -1;
if (!itemAlreadyExists) {
selectedFormatOptions = [...selectedFormatOptions, selectedOption];
dispatch("change", selectedFormatOptions);
}
};
const handleRemoveDataFormat = (
e: CustomEvent<{ id: string; name: string }>
) => {
const filtered = selectedFormatOptions.filter(
(item) => item.name !== e.detail.name
);
selectedFormatOptions = filtered;
dispatch("change", selectedFormatOptions);
};
const handleAddItem = (e: CustomEvent<string>) => {
selectedFormatOptions = [...selectedFormatOptions, { name: e.detail }];
dispatch("change", selectedFormatOptions);
};
</script>

<div class="fr-my-1w">
<SearcheableComboBox
label={"Format(s) des données"}
hintText={"Sélectionnez ici les différents formats de données qu'un réutilisateur potentiel pourrait exploiter."}
name="dataFormats"
on:addItem={handleAddItem}
on:addItem
options={formatOptions.map(transformDataFormatToSelectOption)}
{error}
on:selectOption={handleSelectFormat}
/>

<div role="list" aria-live="polite">
{#each selectedFormatOptions as format, index}
{#if format.name}
<Tag
id={`${format.name}-option-${index}`}
name={format.name}
role="list"
on:click={handleRemoveDataFormat}
/>
{/if}
{/each}
</div>
</div>

<style>
div[role="list"] {
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 10px;
}
</style>
Loading

0 comments on commit 75b9414

Please sign in to comment.