Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: select dataformat front #556

Merged
merged 16 commits into from
Feb 2, 2023
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