Skip to content

Commit 34a3cf0

Browse files
committed
[#3081] New checkbox and modal design for plan-previews screen
1 parent 788a90a commit 34a3cf0

File tree

27 files changed

+630
-240
lines changed

27 files changed

+630
-240
lines changed

src/open_inwoner/cms/collaborate/urls.py

+14-2
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,32 @@
66
PlanActionEditStatusTagView,
77
PlanActionEditView,
88
PlanActionHistoryView,
9-
PlanCreateView,
9+
PlanCreateNoTemplateView,
10+
PlanCreateWithTemplateView,
1011
PlanDetailView,
1112
PlanEditView,
1213
PlanExportView,
1314
PlanFileUploadView,
1415
PlanGoalEditView,
1516
PlanListView,
17+
PlanTemplateChooseView,
1618
)
1719

1820
app_name = "collaborate"
1921

2022
urlpatterns = [
2123
path("", PlanListView.as_view(), name="plan_list"),
22-
path("create/", PlanCreateView.as_view(), name="plan_create"),
24+
path(
25+
"template-choice/",
26+
PlanTemplateChooseView.as_view(),
27+
name="plan_choose_template",
28+
),
29+
path("create/", PlanCreateNoTemplateView.as_view(), name="plan_create_no_template"),
30+
path(
31+
"create-from-template/",
32+
PlanCreateWithTemplateView.as_view(),
33+
name="plan_create_with_template",
34+
),
2335
path("<uuid:uuid>/", PlanDetailView.as_view(), name="plan_detail"),
2436
path("<uuid:uuid>/edit/", PlanEditView.as_view(), name="plan_edit"),
2537
path("<uuid:uuid>/edit/goal/", PlanGoalEditView.as_view(), name="plan_edit_goal"),

src/open_inwoner/components/templates/components/File/File.html

+3-3
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@
1818
<span class="file__name">
1919
{{ name }}
2020
{% if extension and size %}
21-
({{ extension }}, {{ size|readable_size }})
21+
<span class="file__extension">({{ extension }}</span>, <span class="file__size">{{ size|readable_size }})</span>
2222
{% elif extension %}
23-
({{ extension }})
23+
<span class="file__extension">({{ extension }})</span>
2424
{% elif size %}
25-
({{ size|readable_size }})
25+
<span class="file__size">({{ size|readable_size }})</span>
2626
{% endif %}
2727
</span>
2828
<span class="file__date">{{ created|date:'j F Y' }}</span>

src/open_inwoner/js/components/plan-preview/index.js

+109-4
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,13 @@ export class PlanPreview {
1313
event.stopPropagation()
1414
event.preventDefault()
1515

16-
const modalId = document.getElementById(this.node.dataset.id)
17-
const modal = new Modal(modalId)
16+
const modalId = this.node.dataset.id || 'modal'
17+
const modalElement = document.getElementById(modalId)
18+
if (!modalElement) {
19+
return
20+
}
21+
22+
const modal = new Modal(modalElement)
1823
modal.setModalIcons(false)
1924
modal.setConfirmButtonVisibility(false)
2025
modal.setCancelButtonVisibility(true)
@@ -23,9 +28,109 @@ export class PlanPreview {
2328
// Track the element that opened the modal
2429
modal.openedBy = this.node
2530

26-
// Set the modal-closed callback to focus on the element that opened it
31+
// Find the corresponding radio input
32+
let radioInput = null
33+
let radioLabel = null
34+
35+
// First structure (original page)
36+
const templateRow = this.node.closest('.plan-template__row')
37+
if (templateRow) {
38+
radioInput = templateRow.querySelector('.radio__input')
39+
radioLabel = templateRow.querySelector('.radio__label')
40+
}
41+
42+
// Second structure (choice-list page)
43+
if (!radioInput) {
44+
const choiceListItem = this.node.closest('.choice-list__item')
45+
if (choiceListItem) {
46+
radioInput = choiceListItem.querySelector('.choice-list__radio')
47+
radioLabel = choiceListItem.querySelector('.choice-list__label')
48+
}
49+
}
50+
51+
// Get the template ID from the modalId
52+
const templateId = modalId.split('-')[1]
53+
54+
// As a fallback, try to find the radio by its ID
55+
if (!radioInput && templateId) {
56+
radioInput = document.getElementById(`id_template_${templateId}`)
57+
if (radioInput) {
58+
// Try to find the associated label
59+
radioLabel = document.querySelector(
60+
`label[for="id_template_${templateId}"]`
61+
)
62+
if (!radioLabel) {
63+
// If no explicit label found, look for parent or ancestor label
64+
radioLabel =
65+
radioInput.closest('label') ||
66+
radioInput.parentElement.querySelector('label')
67+
}
68+
}
69+
}
70+
71+
// Find all close buttons in the modal
72+
const closeButtons = modalElement.querySelectorAll(
73+
'.modal__close, .modal__close-title'
74+
)
75+
76+
// Add event listener to all close buttons to select the radio input
77+
if (radioInput) {
78+
closeButtons.forEach((closeButton) => {
79+
closeButton.addEventListener(
80+
'click',
81+
() => {
82+
// Select the radio input
83+
radioInput.checked = true
84+
85+
// Set focus to the radio input to trigger CSS focus styles
86+
radioInput.focus()
87+
88+
// If using the choice-list structure, add the 'selected' class to the parent list item
89+
const choiceListItem = radioInput.closest('.choice-list__item')
90+
if (choiceListItem) {
91+
// Remove 'selected' class from all items
92+
const allItems = document.querySelectorAll('.choice-list__item')
93+
allItems.forEach((item) => item.classList.remove('selected'))
94+
95+
// Add 'selected' class to the clicked item
96+
choiceListItem.classList.add('selected')
97+
}
98+
99+
// Trigger change event to ensure any listeners know the radio was changed
100+
const changeEvent = new Event('change', { bubbles: true })
101+
radioInput.dispatchEvent(changeEvent)
102+
103+
// Add a small delay before setting focus to ensure DOM updates are processed
104+
setTimeout(() => {
105+
// For the first HTML variant, ensure focus affects the label for visual feedback
106+
if (radioInput && radioLabel && templateRow) {
107+
// First try to focus the label if possible (better for accessibility)
108+
if (radioLabel.getAttribute('for') === radioInput.id) {
109+
radioLabel.focus()
110+
} else {
111+
// Otherwise focus the input itself
112+
radioInput.focus()
113+
}
114+
}
115+
}, 50)
116+
},
117+
{ once: true }
118+
)
119+
})
120+
}
121+
122+
// Set the modal-closed callback to focus on the selected radio or label
27123
modal.setModalClosedCallback(() => {
28-
if (modal.openedBy) {
124+
if (radioInput && radioInput.checked) {
125+
// Try to focus the label first (if it exists and is properly linked)
126+
if (radioLabel && radioLabel.getAttribute('for') === radioInput.id) {
127+
radioLabel.focus()
128+
} else {
129+
// Fall back to focusing the input itself
130+
radioInput.focus()
131+
}
132+
} else if (modal.openedBy) {
133+
// If no radio was selected, return focus to the element that opened the modal
29134
modal.openedBy.focus()
30135
}
31136
})
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,28 @@
1-
{% load i18n link_tags %}
1+
{% load i18n link_tags file_tags %}
22

3-
<div class='preview'>
3+
<div class='plan-preview'>
4+
<p class='plan-preview__key'>{% trans "Titel" %}</p>
5+
<h2 class="utrecht-heading-4">{{ plan_template.name }}</h2>
46
{% if plan_template.goal %}
5-
<div class='preview__title'>{% trans "Doel:" %}</div>
6-
<div>{{ plan_template.goal }}</div>
7+
<p class='plan-preview__key'>{% trans "Doel" %}</p>
8+
<div>{{ plan_template.goal|truncatewords:20 }}</div>
79
{% endif %}
810

911
{% if plan_template.description %}
10-
<div class='preview__title'>{% trans "Description:" %}</div>
11-
<div>{{ plan_template.description }}</div>
12+
<div class='plan-preview__key'>{% trans "Description:" %}</div>
13+
<div>{{ plan_template.description|truncatewords:10 }}</div>
1214
{% endif %}
1315

1416
{% if plan_template.file %}
15-
<div class='preview__title'>{% trans 'File:' %}</div>
16-
<div>{% link href=plan_template.file.url text=plan_template.file %}</div>
17+
<div class='plan-preview__key'>{% trans 'Bestand' %}</div>
18+
<div>{% file file=plan_template.file extension=plan_template.file.extension size=plan_template.file.size show_download=False %}</div>
1719
{% endif %}
1820

1921
{% if plan_template.actiontemplates.exists %}
20-
<div class='preview__title'>{% trans 'Actions:' %}</div>
22+
<div class='plan-preview__key'>{% trans 'Actions:' %}</div>
2123

2224
{% for action_template in plan_template.actiontemplates.all %}
23-
<div class='preview__span'>
24-
<div class='preview__title'>{{ forloop.counter }}</div>
25-
<div class='preview__title'>{% trans 'Name:' %}</div>
26-
<div>{{ action_template.name }}</div>
27-
<div></div>
28-
<div class='preview__title'>{% trans 'Description:' %}</div>
29-
<div>{{ action_template.description }}</div>
30-
<div></div>
31-
<div class='preview__title'>{% trans 'Type:' %}</div>
32-
<div>{{ action_template.get_type_display }}</div>
33-
<div></div>
34-
<div class='preview__title'>{% trans 'Eindigt' %}</div>
35-
<div>{% trans "Over" %}{{ over }} {{ action_template.end_in_days }} {% trans "dagen" %} {{ days }}</div>
36-
</div>
25+
<div class='plan-preview__span'>{{ forloop.counter }}. {{ action_template.name }}</div>
3726
{% endfor %}
3827
{% endif %}
3928
</div>

src/open_inwoner/plans/tests/test_logging.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,9 @@ def setUp(self):
3232

3333
def test_created_plan_is_logged(self):
3434
plan = PlanFactory.build(created_by=self.user)
35-
form = self.app.get(reverse("collaborate:plan_create"), user=self.user).forms[
36-
"plan-form"
37-
]
35+
form = self.app.get(
36+
reverse("collaborate:plan_create_no_template"), user=self.user
37+
).forms["plan-form"]
3838
form["title"] = plan.title
3939
form["goal"] = plan.goal
4040
form["end_date"] = plan.end_date

src/open_inwoner/plans/tests/test_views.py

+22-14
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,11 @@ def setUp(self) -> None:
4444

4545
self.login_url = reverse("login")
4646
self.list_url = reverse("collaborate:plan_list")
47-
self.create_url = reverse("collaborate:plan_create")
47+
self.choose_template_url = reverse("collaborate:plan_choose_template")
48+
# TODO: new tests for choosing template form
49+
self.create_url = reverse("collaborate:plan_create_no_template")
50+
self.create_with_template_url = reverse("collaborate:plan_create_with_template")
51+
# TODO: new tests for creating plan from template where Title/Goal are prefilled, but Contact/Enddate are not.
4852
self.detail_url = reverse(
4953
"collaborate:plan_detail", kwargs={"uuid": self.plan.uuid}
5054
)
@@ -315,11 +319,11 @@ def test_plan_action_create_not_your_action(self):
315319
other_user = UserFactory()
316320
self.app.get(self.action_add_url, user=other_user, status=404)
317321

318-
def test_plan_create_login_required(self):
322+
def test_plan_create_no_template_login_required(self):
319323
response = self.app.get(self.create_url)
320324
self.assertRedirects(response, f"{self.login_url}?next={self.create_url}")
321325

322-
def test_plan_create_fields_required(self):
326+
def test_plan_create_no_template_fields_required(self):
323327
response = self.app.get(self.create_url, user=self.user)
324328
form = response.forms["plan-form"]
325329
response = form.submit()
@@ -333,7 +337,7 @@ def test_plan_create_fields_required(self):
333337
},
334338
)
335339

336-
def test_plan_create_fails_with_no_collaborators(self):
340+
def test_plan_create_no_template_fails_with_no_collaborators(self):
337341
response = self.app.get(self.create_url, user=self.user)
338342
form = response.forms["plan-form"]
339343
form["title"] = "Plan"
@@ -348,20 +352,20 @@ def test_plan_create_fails_with_no_collaborators(self):
348352
{"__all__": [_("At least one collaborator is required for a plan.")]},
349353
)
350354

351-
def test_plan_create_contains_expected_contacts(self):
355+
def test_plan_create_no_template_contains_expected_contacts(self):
352356
another_contact = UserFactory()
353357
self.user.user_contacts.add(another_contact)
354358
response = self.app.get(self.create_url, user=self.user)
355359

356-
rendered_contacts = response.pyquery("#plan-form .grid .form__grid-box")[
360+
rendered_contacts = response.pyquery("#plan-form .plan__contacts")[
357361
0
358362
].text_content()
359363

360364
self.assertNotIn(self.user.get_full_name(), rendered_contacts)
361365
self.assertIn(self.contact.get_full_name(), rendered_contacts)
362366
self.assertIn(another_contact.get_full_name(), rendered_contacts)
363367

364-
def test_plan_create_plan(self):
368+
def test_plan_create_no_template_plan(self):
365369
self.assertEqual(Plan.objects.count(), 1)
366370
response = self.app.get(self.create_url, user=self.user)
367371
form = response.forms["plan-form"]
@@ -378,7 +382,7 @@ def test_plan_create_plan(self):
378382
self.assertEqual(plan.goal, "Goal")
379383
self.assertEqual(plan.description, "Description")
380384

381-
def test_plan_create_plan_with_template(self):
385+
def test_plan_create_no_template_plan_with_template(self):
382386
plan_template = PlanTemplateFactory(file=None)
383387
self.assertEqual(Plan.objects.count(), 1)
384388
response = self.app.get(self.create_url, user=self.user)
@@ -397,7 +401,7 @@ def test_plan_create_plan_with_template(self):
397401
self.assertEqual(plan.documents.count(), 0)
398402
self.assertEqual(plan.actions.count(), 0)
399403

400-
def test_plan_create_plan_with_template_and_field_overrides(self):
404+
def test_plan_create_no_template_plan_with_template_and_field_overrides(self):
401405
plan_template = PlanTemplateFactory(file=None)
402406
self.assertEqual(Plan.objects.count(), 1)
403407
response = self.app.get(self.create_url, user=self.user)
@@ -418,7 +422,7 @@ def test_plan_create_plan_with_template_and_field_overrides(self):
418422
self.assertEqual(plan.documents.count(), 0)
419423
self.assertEqual(plan.actions.count(), 0)
420424

421-
def test_plan_create_plan_with_template_and_file(self):
425+
def test_plan_create_no_template_plan_with_template_and_file(self):
422426
plan_template = PlanTemplateFactory()
423427
self.assertEqual(Plan.objects.count(), 1)
424428
response = self.app.get(self.create_url, user=self.user)
@@ -437,7 +441,7 @@ def test_plan_create_plan_with_template_and_file(self):
437441
self.assertEqual(plan.documents.count(), 1)
438442
self.assertEqual(plan.actions.count(), 0)
439443

440-
def test_plan_create_plan_with_template_and_actions(self):
444+
def test_plan_create_no_template_plan_with_template_and_actions(self):
441445
plan_template = PlanTemplateFactory(file=None)
442446
ActionTemplateFactory(plan_template=plan_template)
443447
self.assertEqual(Plan.objects.count(), 1)
@@ -457,7 +461,9 @@ def test_plan_create_plan_with_template_and_actions(self):
457461
self.assertEqual(plan.documents.count(), 0)
458462
self.assertEqual(plan.actions.count(), 1)
459463

460-
def test_plan_create_plan_validation_error_reselects_template_and_contact(self):
464+
def test_plan_create_no_template_plan_validation_error_reselects_template_and_contact(
465+
self,
466+
):
461467
plan_template = PlanTemplateFactory(file=None)
462468
ActionTemplateFactory(plan_template=plan_template)
463469
# make sure we have only one plan
@@ -483,12 +489,14 @@ def test_plan_create_plan_validation_error_reselects_template_and_contact(self):
483489
elem = response.pyquery("#id_plan_contacts_1")[0]
484490
self.assertEqual(elem.attrib.get("checked"), "checked")
485491

486-
def test_plan_create_contains_contact_create_link_when_no_contacts_exist(self):
492+
def test_plan_create_no_template_contains_contact_create_link_when_no_contacts_exist(
493+
self,
494+
):
487495
self.user.user_contacts.remove(self.contact)
488496
response = self.app.get(self.create_url, user=self.user)
489497
self.assertContains(response, reverse("profile:contact_create"))
490498

491-
def test_plan_create_does_not_contain_contact_create_link_when_contacts_exist(
499+
def test_plan_create_no_template_does_not_contain_contact_create_link_when_contacts_exist(
492500
self,
493501
):
494502
response = self.app.get(self.create_url, user=self.user)

0 commit comments

Comments
 (0)