diff --git a/core/static/bundled/core/dynamic-formset-index.ts b/core/static/bundled/core/dynamic-formset-index.ts new file mode 100644 index 00000000..6b71e25f --- /dev/null +++ b/core/static/bundled/core/dynamic-formset-index.ts @@ -0,0 +1,77 @@ +interface Config { + /** + * The prefix of the formset, in case it has been changed. + * See https://docs.djangoproject.com/fr/stable/topics/forms/formsets/#customizing-a-formset-s-prefix + */ + prefix?: string; +} + +// biome-ignore lint/style/useNamingConvention: It's the DOM API naming +type HTMLFormInputElement = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement; + +document.addEventListener("alpine:init", () => { + /** + * Alpine data element to allow the dynamic addition of forms to a formset. + * + * To use this, you need : + * - an HTML element containing the existing forms, noted by `x-ref="formContainer"` + * - a template containing the empty form + * (that you can obtain jinja-side with `{{ formset.empty_form }}`), + * noted by `x-ref="formTemplate"` + * - a button with `@click="addForm"` + * - you may also have one or more buttons with `@click="removeForm(element)"`, + * where `element` is the HTML element containing the form. + * + * For an example of how this is used, you can have a look to + * `counter/templates/counter/product_form.jinja` + */ + Alpine.data("dynamicFormSet", (config?: Config) => ({ + init() { + this.formContainer = this.$refs.formContainer as HTMLElement; + this.nbForms = this.formContainer.children.length as number; + this.template = this.$refs.formTemplate as HTMLTemplateElement; + const prefix = config?.prefix ?? "form"; + this.$root + .querySelector(`#id_${prefix}-TOTAL_FORMS`) + .setAttribute(":value", "nbForms"); + }, + + addForm() { + this.formContainer.appendChild(document.importNode(this.template.content, true)); + const newForm = this.formContainer.lastElementChild; + const inputs: NodeListOf = newForm.querySelectorAll( + "input, select, textarea", + ); + for (const el of inputs) { + el.name = el.name.replace("__prefix__", this.nbForms.toString()); + el.id = el.id.replace("__prefix__", this.nbForms.toString()); + } + const labels: NodeListOf = newForm.querySelectorAll("label"); + for (const el of labels) { + el.htmlFor = el.htmlFor.replace("__prefix__", this.nbForms.toString()); + } + inputs[0].focus(); + this.nbForms += 1; + }, + + removeForm(container: HTMLDivElement) { + container.remove(); + this.nbForms -= 1; + // adjust the id of remaining forms + for (let i = 0; i < this.nbForms; i++) { + const form: HTMLDivElement = this.formContainer.children[i]; + const inputs: NodeListOf = form.querySelectorAll( + "input, select, textarea", + ); + for (const el of inputs) { + el.name = el.name.replace(/\d+/, i.toString()); + el.id = el.id.replace(/\d+/, i.toString()); + } + const labels: NodeListOf = form.querySelectorAll("label"); + for (const el of labels) { + el.htmlFor = el.htmlFor.replace(/\d+/, i.toString()); + } + } + }, + })); +}); diff --git a/core/templates/core/base.jinja b/core/templates/core/base.jinja index 34a8040b..025dacdf 100644 --- a/core/templates/core/base.jinja +++ b/core/templates/core/base.jinja @@ -35,8 +35,8 @@ - - + + diff --git a/counter/forms.py b/counter/forms.py index 1d9fa8a0..33bfd51a 100644 --- a/counter/forms.py +++ b/counter/forms.py @@ -291,7 +291,8 @@ ScheduledProductActionFormSet = forms.modelformset_factory( absolute_max=None, can_delete=True, can_delete_extra=False, - extra=2, + extra=0, + min_num=1, ) diff --git a/counter/templates/counter/product_form.jinja b/counter/templates/counter/product_form.jinja index f3d3728c..ffd751c0 100644 --- a/counter/templates/counter/product_form.jinja +++ b/counter/templates/counter/product_form.jinja @@ -1,5 +1,44 @@ {% extends "core/base.jinja" %} +{% block additional_js %} + +{% endblock %} + + +{% macro action_form(form) %} +
+ {{ form.non_field_errors() }} +
+
+ {{ form.task.errors }} + {{ form.task.label_tag() }} + {{ form.task|add_attr("x-model=action") }} +
+
{{ form.trigger_at.as_field_group() }}
+
+
+ {{ form.counters.as_field_group() }} +
+ {%- if form.DELETE -%} +
+ {{ form.DELETE.as_field_group() }} +
+ {%- else -%} + + {%- endif -%} + {%- for field in form.hidden_fields() -%} + {{ field }} + {%- endfor -%} +
+
+{% endmacro %} + + {% block content %} {% if object %}

{% trans name=object %}Edit product {{ name }}{% endtrans %}

@@ -25,34 +64,20 @@

- {{ form.action_formset.management_form }} - {%- for action_form in form.action_formset.forms -%} -
- {{ action_form.non_field_errors() }} -
-
- {{ action_form.task.errors }} - {{ action_form.task.label_tag() }} - {{ action_form.task|add_attr("x-model=action") }} -
-
{{ action_form.trigger_at.as_field_group() }}
-
-
- {{ action_form.counters.as_field_group() }} -
- {%- if action_form.DELETE -%} -
- {{ action_form.DELETE.as_field_group() }} -
- {%- endif -%} - {%- for field in action_form.hidden_fields() -%} - {{ field }} +
+ {{ form.action_formset.management_form }} +
+ {%- for f in form.action_formset.forms -%} + {{ action_form(f) }} {%- endfor -%} -
- {%- if not loop.last -%} -
- {%- endif -%} - {%- endfor -%} -

+ + + + +

{% endblock %} \ No newline at end of file diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index 8600040b..9275ce76 100644 --- a/locale/fr/LC_MESSAGES/django.po +++ b/locale/fr/LC_MESSAGES/django.po @@ -3757,6 +3757,10 @@ msgstr "" "votre cotisation. Si vous ne renouvelez pas votre cotisation, il n'y aura " "aucune conséquence autre que le retrait de l'argent de votre compte." +#: counter/templates/counter/product_form.jinja +msgid "Remove this action" +msgstr "Retirer cette action" + #: counter/templates/counter/product_form.jinja #, python-format msgid "Edit product %(name)s" @@ -3784,6 +3788,10 @@ msgstr "" "Les actions automatiques vous permettent de planifier des modifications du " "produit à l'avance." +#: counter/templates/counter/product_form.jinja +msgid "Add action" +msgstr "Ajouter une action" + #: counter/templates/counter/product_list.jinja msgid "Product list" msgstr "Liste des produits"