Skip to content

Commit ebb4316

Browse files
committed
IBX-9060: Notification list filter
1 parent 5dff9a0 commit ebb4316

17 files changed

+664
-73
lines changed

src/bundle/Resources/config/ezplatform_default_settings.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ parameters:
2222
ibexa.site_access.config.admin_group.pagination.content_role_limit: 5
2323
ibexa.site_access.config.admin_group.pagination.content_policy_limit: 5
2424
ibexa.site_access.config.admin_group.pagination.bookmark_limit: 10
25-
ibexa.site_access.config.admin_group.pagination.notification_limit: 5
25+
ibexa.site_access.config.admin_group.pagination.notification_limit: 10
2626
ibexa.site_access.config.admin_group.pagination.user_settings_limit: 10
2727
ibexa.site_access.config.admin_group.pagination.content_draft_limit: 10
2828
ibexa.site_access.config.admin_group.pagination.location_limit: 10

src/bundle/Resources/encore/ibexa.js.config.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,5 +254,8 @@ module.exports = (Encore) => {
254254
path.resolve(__dirname, '../public/js/scripts/admin.location.adaptive.tabs.js'),
255255
])
256256
.addEntry('ibexa-admin-ui-edit-base-js', [path.resolve(__dirname, '../public/js/scripts/edit.header.js')])
257-
.addEntry('ibexa-admin-notifications-list-js', [path.resolve(__dirname, '../public/js/scripts/admin.notifications.list.js')]);
257+
.addEntry('ibexa-admin-notifications-list-js', [
258+
path.resolve(__dirname, '../public/js/scripts/admin.notifications.list.js'),
259+
path.resolve(__dirname, '../public/js/scripts/admin.notifications.filters.js'),
260+
])
258261
};
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
(function (global, doc) {
2+
const searchForm = doc.querySelector('.ibexa-al-list-search-form');
3+
const filtersContainerNode = doc.querySelector('.ibexa-list-filters');
4+
const applyFiltersBtn = filtersContainerNode.querySelector('.ibexa-btn--apply');
5+
const clearFiltersBtn = filtersContainerNode.querySelector('.ibexa-btn--clear');
6+
const statusFilterNode = filtersContainerNode.querySelector('.ibexa-list-filters__item--statuses');
7+
const typeFilterNode = filtersContainerNode.querySelector('.ibexa-list-filters__item--type');
8+
const datetimeFilterNodes = filtersContainerNode.querySelectorAll('.ibexa-list-filters__item--date-time .ibexa-picker');
9+
10+
const clearFilter = (filterNode) => {
11+
if (!filterNode) {
12+
return;
13+
}
14+
15+
const sourceSelect = filterNode.querySelector('.ibexa-list-filters__item-content .ibexa-dropdown__source .ibexa-input--select');
16+
const sourceSelectOptions = sourceSelect?.querySelectorAll('option');
17+
const checkboxes = filterNode.querySelectorAll(
18+
'.ibexa-list-filters__item-content .ibexa-input--checkbox:not([name="dropdown-checkbox"])',
19+
);
20+
const timePicker = filterNode.querySelector('.ibexa-date-time-picker__input');
21+
22+
if (sourceSelect) {
23+
sourceSelectOptions.forEach((option) => (option.selected = false));
24+
25+
if (isNodeTimeFilter(filterNode)) {
26+
sourceSelectOptions[0].selected = true;
27+
}
28+
} else if (checkboxes.length) {
29+
checkboxes.forEach((checkbox) => (checkbox.checked = false));
30+
} else if (timePicker.value.length) {
31+
const timePicker = filterNode.querySelector('.ibexa-date-time-picker__input');
32+
const formInput = filterNode.querySelector('.ibexa-picker__form-input');
33+
34+
timePicker.value = '';
35+
formInput.value = '';
36+
37+
timePicker.dispatchEvent(new Event('input'));
38+
formInput.dispatchEvent(new Event('input'));
39+
}
40+
41+
searchForm.submit();
42+
};
43+
const attachFilterEvent = (filterNode) => {
44+
if (!filterNode) {
45+
return;
46+
}
47+
48+
const sourceSelect = filterNode.querySelector('.ibexa-list-filters__item-content .ibexa-dropdown__source .ibexa-input--select');
49+
const checkboxes = filterNode.querySelectorAll(
50+
'.ibexa-list-filters__item-content .ibexa-input--checkbox:not([name="dropdown-checkbox"])',
51+
);
52+
const picker = filterNode.querySelector(
53+
'.ibexa-input--date',
54+
);
55+
picker?.addEventListener('change', filterChange, false);
56+
sourceSelect?.addEventListener('change', filterChange, false);
57+
checkboxes.forEach((checkbox) => {
58+
checkbox.addEventListener('change', filterChange, false);
59+
});
60+
};
61+
const isNodeTimeFilter = (filterNode) => {
62+
return filterNode.classList.contains('ibexa-picker');
63+
};
64+
const hasFilterValue = (filterNode) => {
65+
if (!filterNode) {
66+
return;
67+
}
68+
69+
const select = filterNode.querySelector('.ibexa-dropdown__source .ibexa-input--select');
70+
const checkedCheckboxes = filterNode.querySelectorAll('.ibexa-input--checkbox:checked');
71+
72+
if (isNodeTimeFilter(filterNode)) {
73+
const timePicker = filterNode.querySelector('.ibexa-date-time-picker__input');
74+
75+
return !!timePicker.dataset.timestamp;
76+
}
77+
78+
return !!(select?.value || checkedCheckboxes?.length);
79+
};
80+
const isSomeFilterSet = () => {
81+
const hasStatusFilterValue = hasFilterValue(statusFilterNode);
82+
const hasTypeFilterValue = hasFilterValue(typeFilterNode);
83+
const hasDatetimeFilterValue = [...datetimeFilterNodes].some((input) => hasFilterValue(input));
84+
85+
return hasStatusFilterValue || hasTypeFilterValue || hasDatetimeFilterValue;
86+
};
87+
const attachInitEvents = () => {
88+
attachFilterEvent(statusFilterNode);
89+
attachFilterEvent(typeFilterNode);
90+
datetimeFilterNodes.forEach((input) => attachFilterEvent(input));
91+
};
92+
const filterChange = () => {
93+
const hasFiltersSetValue = isSomeFilterSet();
94+
95+
applyFiltersBtn.disabled = false;
96+
clearFiltersBtn.disabled = !hasFiltersSetValue;
97+
};
98+
const clearAllFilters = () => {
99+
clearFilter(statusFilterNode);
100+
clearFilter(typeFilterNode);
101+
datetimeFilterNodes.forEach((input) => clearFilter(input));
102+
};
103+
104+
attachInitEvents();
105+
106+
clearFiltersBtn.addEventListener('click', clearAllFilters, false);
107+
})(window, window.document);

src/bundle/Resources/public/js/scripts/admin.notifications.list.js

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
const { showErrorNotification } = ibexa.helpers.notification;
66
const { getJsonFromResponse } = ibexa.helpers.request;
77
const markAllAsReadBtn = doc.querySelector('.ibexa-notification-list__mark-all-read');
8+
const markAsReadBtn = doc.querySelector('.ibexa-notification-list__btn--mark-as-read');
9+
const deleteBtn = doc.querySelector('.ibexa-notification-list__btn--delete');
10+
const checkboxes = [...doc.querySelectorAll('.ibexa-notification-list .ibexa-table__cell--has-checkbox .ibexa-input--checkbox')];
811
const markAllAsRead = () => {
912
const markAllAsReadLink = Routing.generate('ibexa.notifications.mark_all_as_read');
1013

@@ -25,6 +28,53 @@
2528
showErrorNotification(message);
2629
});
2730
};
31+
32+
//TODO:
33+
const markSelectedAsRead = () => {
34+
const checkedElements = doc.querySelectorAll('.ibexa-notification-list__mark-row-checkbox:checked');
35+
const notificationIds = [...checkedElements].map((element) => element.dataset.notificationId);
36+
const bulkOperations = getBulkOperations(notificationIds);
37+
const request = new Request('/api/ibexa/v2/bulk', {
38+
method: 'POST',
39+
headers: {
40+
Accept: 'application/vnd.ibexa.api.BulkOperationResponse+json',
41+
'Content-Type': 'application/vnd.ibexa.api.BulkOperation+json',
42+
},
43+
body: JSON.stringify({
44+
bulkOperations: {
45+
operations: bulkOperations,
46+
},
47+
}),
48+
mode: 'same-origin',
49+
credentials: 'include',
50+
});
51+
const errorMessage = Translator.trans(
52+
/*@Desc("Cannot mark selected notifications as read")*/ 'notifications.modal.message.error.mark_selected_as_read',
53+
{},
54+
'ibexa_notifications',
55+
);
56+
57+
fetch(request)
58+
.then(getJsonFromResponse)
59+
.catch(() => ibexa.helpers.notification.showErrorNotification(errorMessage));
60+
};
61+
const getBulkOperations = (notificationIds) => notificationIds.reduce((total, notificationId) => {
62+
const markAsReadLink = Routing.generate('ibexa.notifications.mark_as_read', { notificationId });
63+
64+
total[markAsReadLink] = {
65+
uri: markAsReadLink,
66+
method: 'GET',
67+
mode: 'same-origin',
68+
headers: {
69+
Accept: 'application/vnd.ibexa.api.ContentType+json',
70+
'X-Requested-With': 'XMLHttpRequest',
71+
},
72+
credentials: 'same-origin'
73+
};
74+
75+
return total;
76+
}, {});
77+
2878
const handleNotificationClick = (notification, isToggle = false) => {
2979
const notificationRow = notification.closest('.ibexa-table__row');
3080
const isRead = notification.classList.contains('ibexa-notifications-modal__item--read');
@@ -99,4 +149,26 @@
99149
link.addEventListener('click', (event) => handleNotificationActionClick(event, true), false),
100150
);
101151
markAllAsReadBtn.addEventListener('click', markAllAsRead, false);
152+
markAsReadBtn.addEventListener('click', markSelectedAsRead, false);
153+
154+
155+
const toggleActionButtonState = () => {
156+
const checkedNotifications = checkboxes.filter((el) => el.checked);
157+
const isAnythingSelected = checkedNotifications.length > 0;
158+
159+
deleteBtn.disabled = !isAnythingSelected;
160+
markAsReadBtn.disabled = !isAnythingSelected || !checkedNotifications.every((checkbox) =>
161+
checkbox.closest('.ibexa-table__row').querySelector('.ibexa-notification-view-all__read').innerText === 'Unread'
162+
);
163+
};
164+
const handleCheckboxChange = (checkbox) => {
165+
const checkboxFormId = checkbox.dataset?.formRemoveId;
166+
const formRemoveCheckbox = doc.getElementById(checkboxFormId);
167+
if (formRemoveCheckbox) {
168+
formRemoveCheckbox.checked = checkbox.checked;
169+
}
170+
toggleActionButtonState();
171+
};
172+
173+
checkboxes.forEach((checkbox) => checkbox.addEventListener('change', () => handleCheckboxChange(checkbox), false));
102174
})(window, window.document, window.ibexa, window.Translator, window.Routing);

src/bundle/Resources/public/js/scripts/admin.notifications.modal.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,14 @@
8989
};
9090
const updateModalTitleTotalInfo = (notificationsCount) => {
9191
const modalTitle = panel.querySelector(SELECTOR_MODAL_TITLE);
92-
92+
const modalFooter = panel.querySelector('.ibexa-notifications__view-all-btn--count');
93+
modalFooter.textContent = ` (${notificationsCount})`;
9394
modalTitle.dataset.notificationsTotal = `(${notificationsCount})`;
95+
96+
if (notificationsCount < 10) {
97+
panel.querySelector('.ibexa-notifications-modal__count').textContent = `(${notificationsCount})`;
98+
}
99+
94100
};
95101
const updatePendingNotificationsView = (notificationsInfo) => {
96102
const noticeDot = doc.querySelector('.ibexa-header-user-menu__notice-dot');
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
.ibexa-list-filters {
2+
$self: &;
3+
4+
overflow: hidden;
5+
6+
&__header {
7+
display: flex;
8+
align-items: center;
9+
justify-content: space-between;
10+
padding: calculateRem(16px);
11+
}
12+
13+
&__title {
14+
margin: 0;
15+
padding: 0;
16+
font-weight: 600;
17+
}
18+
19+
.accordion-item {
20+
background: transparent;
21+
22+
#{$self} {
23+
&__item-header-btn {
24+
justify-content: space-between;
25+
font-size: $ibexa-text-font-size-medium;
26+
font-weight: 600;
27+
transition: all $ibexa-admin-transition-duration $ibexa-admin-transition;
28+
border-top-color: $ibexa-color-light;
29+
border-bottom-color: transparent;
30+
border-style: solid;
31+
border-width: calculateRem(1px) 0;
32+
background: transparent;
33+
34+
.ibexa-icon--toggle {
35+
transition: var(--bs-accordion-btn-icon-transition);
36+
}
37+
38+
&:not(.collapsed) {
39+
border-bottom-color: $ibexa-color-light;
40+
41+
.ibexa-icon--toggle {
42+
transform: var(--bs-accordion-btn-icon-transform);
43+
}
44+
}
45+
46+
&::after {
47+
display: none;
48+
}
49+
}
50+
}
51+
52+
&:last-of-type {
53+
#{$self} {
54+
&__item-header-btn {
55+
border-bottom-color: $ibexa-color-light;
56+
57+
&.collapsed {
58+
border-bottom-color: transparent;
59+
}
60+
}
61+
}
62+
}
63+
}
64+
65+
&__item {
66+
.ibexa-label {
67+
margin: 0;
68+
padding: 0;
69+
}
70+
71+
&--date-time {
72+
#{$self} {
73+
&__item-content {
74+
padding-bottom: calculateRem(48px);
75+
}
76+
}
77+
78+
.form-group:first-child {
79+
padding-bottom: calculateRem(16px);
80+
}
81+
.ibexa-date-time-picker {
82+
width: 100%;
83+
}
84+
}
85+
86+
&--hidden {
87+
display: none;
88+
}
89+
}
90+
91+
&__item-content {
92+
padding: calculateRem(24px) calculateRem(16px);
93+
}
94+
}

0 commit comments

Comments
 (0)