From 76ecb52995425ac799951d34e1b77994f889fdae Mon Sep 17 00:00:00 2001 From: CasVT Date: Fri, 1 Aug 2025 15:54:51 +0200 Subject: [PATCH] Refactored notification service UI --- frontend/static/css/notification.css | 272 ++++++++++----------- frontend/static/js/general.js | 20 +- frontend/static/js/notification.js | 342 ++++++++++++++++++--------- frontend/static/js/reminders.js | 16 +- frontend/templates/reminders.html | 119 ++++++---- 5 files changed, 468 insertions(+), 301 deletions(-) diff --git a/frontend/static/css/notification.css b/frontend/static/css/notification.css index c257e21..81eef93 100644 --- a/frontend/static/css/notification.css +++ b/frontend/static/css/notification.css @@ -1,15 +1,7 @@ -.ns-table-container { - margin-top: 2rem; - - display: flex; - flex-direction: column; - align-items: center; - gap: 1rem; -} - #add-service-button { width: min(100%, 17rem); height: 2rem; + margin-inline: auto; display: flex; justify-content: center; @@ -25,129 +17,139 @@ transition: transform .125s linear; } -.ns-table-container:has(#service-list-toggle:checked) #add-service-button > svg { +#notification:has(#service-list-toggle:checked) #add-service-button > svg { transform: rotate(45deg); } -.overflow-container { +#notification .entries-table { margin-inline: auto; - width: min(100%, 50rem); - overflow-x: auto; + --inline-padding: .5rem; } -.overflow-container > table { - border-spacing: 0px; +#services-list .empty-row td { + text-align: center; } -.overflow-container > table:not(:has(tbody > tr)) { +#services-list:has(tr:not(.empty-row)) .empty-row { display: none; } -.overflow-container > table th, -.overflow-container > table td { - text-align: left; -} - -.overflow-container > table th { - padding: .5rem; -} - -.overflow-container td { - border-top: 1px solid var(--color-gray); - padding: .25rem; -} - .title-column { min-width: 9.5rem; width: 25%; - - padding-left: 1.5rem; - padding-right: 1rem; } .url-column { - min-width: 26rem; - width: 65%; + width: 26rem; + text-wrap-mode: nowrap; } -.overflow-container table input { - width: 100%; - border-radius: 4px; - padding: .25rem; - box-shadow: none; -} - -.overflow-container input:read-only { - border-color: transparent; -} - -.overflow-container .action-column { +.action-column { min-width: 4rem; - width: 20%; - - display: flex; - gap: .5rem; - - padding: calc(.5rem + 2px); - padding-right: 1.5rem; } -.action-column > button { +/* */ +/* Edit and delete windows */ +/* */ +#edit-ns-form { + width: 100%; + height: 10rem; + display: flex; - justify-content: center; + flex-direction: column; align-items: center; + gap: 1rem; + + padding: 1rem; + + & > * { + max-width: 24rem; + + &.checked-input-container > input { + max-width: inherit; + } + } } -.overflow-container .action-column svg { - width: 1.25rem; - height: 1.25rem; +#delete-ns-dialog p { + padding-inline: 1rem; + text-align: center; } -tr:has(input:not(:read-only)) button[data-type="save"], -tr:has(input:read-only) button[data-type="edit"] { - display: flex; -} +#confirm-delete-ns { + min-width: 6rem; + width: unset; -tr:has(input:not(:read-only)) button[data-type="edit"], -tr:has(input:read-only) button[data-type="save"] { - display: none; + padding-inline: 1rem; } /* */ /* Add service */ /* */ #add-service-container { + margin-top: 1rem; display: none; + flex-direction: column; + align-items: center; } -.overflow-container:has(#service-list-toggle:checked) table { - display: none; +#notification:has(#service-list-toggle:checked) { + & table { + display: none; + } + + & #add-service-container { + display: flex; + } } -.overflow-container:has(#service-list-toggle:checked) #add-service-container { - display: block; +#notification:has(#add-service-toggle:checked) { + & #service-list, + & #ns-search-input { + display: none; + } + + & #add-service-window { + display: flex; + } } -.overflow-container:has(#add-service-toggle:checked) #service-list { - display: none; -} +#ns-search-input { + margin-bottom: 1rem; -.overflow-container:has(#add-service-toggle:checked) #add-service-window { - display: flex; + max-width: 26rem; + height: 2rem; + + padding: .25rem .5rem; } #service-list { + width: min(100%, 48rem); + margin-inline: auto; + display: flex; gap: 1rem; flex-wrap: wrap; justify-content: center; } +#service-list p { + width: 100%; + text-align: center; +} + +#service-list:not(:has(button)) > p, +#service-list:has(button:not(.hidden)) > p { + display: none; +} + #service-list button { - width: max(30%, 10rem); + width: 12rem; + max-width: 15rem; height: 6rem; - + flex-grow: 1; + display: flex; justify-content: center; align-items: center; @@ -155,15 +157,25 @@ tr:has(input:read-only) button[data-type="save"] { padding: .75rem; border-radius: 4px; border: 2px solid var(--color-gray); - + background-color: var(--color-dark); + color: var(--color-light); + text-align: center; font-size: 1.1rem; + + box-shadow: var(--default-shadow); + transition: background-color 150ms ease-in-out; + + &:hover { + background-color: var(--color-gray); + } } /* */ /* Add service form */ /* */ #add-service-window { + width: 100%; max-width: 30rem; margin: auto; @@ -171,129 +183,121 @@ tr:has(input:read-only) button[data-type="save"] { flex-direction: column; justify-content: center; gap: 1rem; - + text-align: center; } +#add-service-window .input-style { + max-width: 100%; + + &::placeholder { + color: var(--color-mid-gray); + } +} + #add-service-window > h3 { font-size: 1.75rem; } #add-service-window > p { + height: 2.7rem; margin-bottom: calc((1rem + 2px) * -1); - + + display: flex; + align-items: center; + border: 2px solid var(--color-gray); border-top-left-radius: 4px; border-top-right-radius: 4px; - padding: .75rem 1rem; - color: var(--color-gray); + padding-inline: .5rem; + color: var(--color-mid-gray); - text-align: left; - box-shadow: var(--default-shadow); } -#add-service-window > button { - border-radius: 4px; - border: 2px solid var(--color-gray); - padding: .75rem; -} - -#add-service-window > a, -#add-service-window > p > a { +#add-service-window > a { color: var(--color-light); } -#add-service-window > div[data-map], -#add-service-window > div[data-map] > .entries-list { +#add-service-window > div[data-map] { display: flex; flex-direction: column; - gap: inherit; -} + gap: .6rem; -#add-service-window > div[data-map] { padding: .5rem; border: 2px solid var(--color-gray); border-radius: 4px; box-shadow: var(--default-shadow); -} -#add-service-window > div[data-map] > p { - color: var(--color-gray); - font-size: 1.1rem; + & > p { + color: var(--color-mid-gray); + font-size: 1.1rem; + } } .entries-list { - min-height: 5rem; - max-height: 15rem; - overflow-y: auto; - + display: flex; + flex-direction: column; align-items: center; + gap: .6rem; background-color: var(--color-dark); color: var(--color-light); border: 2px solid var(--color-gray); border-radius: 4px; - padding: .75rem; + padding: .5rem; box-shadow: var(--default-shadow); font-size: 1rem; } .entries-list > p:first-child { - color: var(--color-gray); + color: var(--color-mid-gray); font-size: 1.1rem; } +.input-entries { + width: 100%; + max-height: 12rem; + width: 100%; + + overflow-y: auto; +} + .input-entries:not(:has(div)) { display: none; } .add-row { - width: min(100%, 21rem); + width: 100%; display: flex; justify-content: center; - flex-wrap: wrap; gap: 1rem; } .add-row input { - flex-grow: 1; height: 2rem; - min-width: 0rem; font-size: .8rem; } .add-row button { + width: 6rem; height: 2rem; - padding: .35rem .75rem; - background-color: var(--color-gray); - border-radius: 4px; + + padding: 0rem; } .entries-list > button { - height: 1.5rem; - width: min(100%, 21rem); - - display: flex; - justify-content: center; - align-items: center; - - background-color: var(--color-gray); -} + height: 1.9rem; -.entries-list > button svg { - height: 60%; -} + & > svg { + height: 1rem; -.entries-list > button path { - height: inherit; - fill: var(--color-dark); -} - -@media (max-width: 543px) { - #service-list button { - flex-grow: 1; + transition: transform 125ms linear; } } + +.entries-list:has(.add-row:not(.hidden)) > button > svg { + transform: rotate(45deg); +} diff --git a/frontend/static/js/general.js b/frontend/static/js/general.js index 5c3f000..7b18115 100644 --- a/frontend/static/js/general.js +++ b/frontend/static/js/general.js @@ -11,16 +11,22 @@ const constants = { * The amount of time to wait after the user stops typing to automatically * trigger the search */ - autoSearchTimeout: 500 + autoSearchTimeout: 500, + + /** + * The amount of time to wait after the user stops typing to automatically + * trigger the search for notification services + */ + autoSearchTimeoutNs: 250 } const icons = { - save: '', - edit: '', - delete: '', - add: '', - download: '', - upload: '', + save: '', + edit: '', + delete: '', + add: '', + download: '', + upload: '', loading: '' } diff --git a/frontend/static/js/notification.js b/frontend/static/js/notification.js index c51c275..9818a4e 100644 --- a/frontend/static/js/notification.js +++ b/frontend/static/js/notification.js @@ -1,5 +1,6 @@ const NotiEls = { services_list: document.querySelector('#services-list'), + search_input: document.querySelector('#ns-search-input'), service_list: document.querySelector('#service-list'), default_service_input: document.querySelector('#default-service-input'), service_selection: document.querySelector('.notification-service-selection'), @@ -8,6 +9,27 @@ const NotiEls = { triggers: { add_service: document.querySelector('#add-service-toggle'), service_list: document.querySelector('#service-list-toggle') + }, + windows: { + editService: { + dialog: document.querySelector("#edit-ns-dialog"), + form: document.querySelector("#edit-ns-form"), + close: document.querySelector("#close-edit-ns"), + inputContainers: { + url: document.querySelector("#edit-ns-form .checked-input-container:has(#edit-ns-url-input)") + }, + inputs: { + title: document.querySelector("#edit-ns-title-input"), + url: document.querySelector("#edit-ns-url-input") + }, + error: document.querySelector("#edit-ns-url-error") + }, + deleteService: { + dialog: document.querySelector("#delete-ns-dialog"), + error: document.querySelector("#delete-ns-error"), + close: document.querySelector("#close-delete-ns"), + confirm: document.querySelector("#confirm-delete-ns") + } } }; @@ -15,33 +37,24 @@ const NotiEls = { // Fill lists and tables // function fillNotificationTable(json) { - NotiEls.services_list.innerHTML = ''; + NotiEls.services_list.querySelectorAll("tr[data-id]").forEach( + e => e.remove() + ) json.result.forEach(service => { - const entry = NotiEls.notification_service_row.cloneNode(true); - entry.dataset.id = service.id; + const entry = NotiEls.notification_service_row.cloneNode(true) + entry.dataset.id = service.id - entry.querySelector('.title-column input').value = service.title; + entry.querySelector('.title-column').innerText = service.title + entry.querySelector('.url-column').innerText = service.url - const url_input = entry.querySelector('.url-column input'); - url_input.value = service.url; - url_input.onkeydown = e => { - if (e.key === 'Enter') - saveService(service.id); - }; + entry.querySelector('button[data-type="edit"]').onclick = + e => openEditNotificationService(service.id) - entry.querySelector('button[data-type="edit"]').onclick = e => - document.querySelectorAll(`tr[data-id="${service.id}"] input`).forEach( - e => e.removeAttribute('readonly') - ); + entry.querySelector('button[data-type="delete"]').onclick = + e => openDeleteNotificationService(service.id) - entry.querySelector('button[data-type="save"]').onclick = e => - saveService(service.id); - - entry.querySelector('button[data-type="delete"]').onclick = e => - deleteService(service.id); - - NotiEls.services_list.appendChild(entry); - }); + NotiEls.services_list.appendChild(entry) + }) }; function fillNotificationSelection(json) { @@ -58,7 +71,7 @@ function fillNotificationSelection(json) { }); if (!NotiEls.default_service_input.querySelector(`option[value="${default_service}"]`)) - setLocalStorage({'default_service': + setLocalStorage({'default_service': parseInt(NotiEls.default_service_input.querySelector('option')?.value) || null }); @@ -122,75 +135,83 @@ function fillNotificationServices() { }); }; -// +// // Actions for table // -function saveService(id) { - const row = document.querySelector(`tr[data-id="${id}"]`); - const save_button = row.querySelector('button[data-type="save"]'); +function openEditNotificationService(serviceId) { + NotiEls.windows.editService.dialog.dataset.id = serviceId; + const row = NotiEls.services_list.querySelector(`tr[data-id="${serviceId}"]`); + NotiEls.windows.editService.inputs.title.value = row.querySelector('.title-column').innerText; + NotiEls.windows.editService.inputs.url.value = row.querySelector('.url-column').innerText; + NotiEls.windows.editService.inputContainers.url.classList.remove('error-input-container'); + NotiEls.windows.editService.dialog.showModal(); +}; + +function editNotificationService() { + NotiEls.windows.editService.inputContainers.url.classList.remove('error-input-container') const data = { - 'title': row.querySelector(`td.title-column > input`).value, - 'url': row.querySelector(`td.url-column > input`).value - }; - fetch(`${urlPrefix}/api/notificationservices/${id}?api_key=${apiKey}`, { - 'method': 'PUT', - 'headers': {'Content-Type': 'application/json'}, - 'body': JSON.stringify(data) - }) - .then(response => { - if (!response.ok) return Promise.reject(response.status); + title: NotiEls.windows.editService.inputs.title.value, + url: NotiEls.windows.editService.inputs.url.value + } - fillNotificationServices(); - }) - .catch(e => { - if (e === 401) - window.location.href = `${urlPrefix}/`; - else if (e === 400) { - save_button.classList.add('error-icon'); - save_button.title = 'Invalid Apprise URL'; - } else - console.log(e); - }); -}; - -function deleteService(id, delete_reminders_using=false) { - const row = document.querySelector(`tr[data-id="${id}"]`); - fetch(`${urlPrefix}/api/notificationservices/${id}?api_key=${apiKey}&delete_reminders_using=${delete_reminders_using}`, { - 'method': 'DELETE' - }) - .then(response => response.json()) + const id = parseInt(NotiEls.windows.editService.dialog.dataset.id) + sendAPI("PUT", `/notificationservices/${id}`, {}, data) .then(json => { - if (json.error !== null) return Promise.reject(json); - - row.remove(); - fillNotificationServices(); - if (delete_reminders_using) { - fillLibrary(reminderTypes.reminder); - fillLibrary(reminderTypes.static_reminder); - fillLibrary(reminderTypes.template); - }; + fillNotificationServices() + NotiEls.windows.editService.dialog.close() }) .catch(e => { - if (e.error === 'ApiKeyExpired' || e.error === 'ApiKeyInvalid') - window.location.href = `${urlPrefix}/`; + e.json().then(json => { + if (json.error === "URLInvalid" || json.error === "InvalidKeyValue") { + NotiEls.windows.editService.error.innerText = json.result.reason || "Syntax of URL invalid" + hide([], [NotiEls.windows.editService.error]) + NotiEls.windows.editService.inputContainers.url.classList.add('error-input-container') + } + else + console.log(json) + }) + }) +} - else if (e.error === 'NotificationServiceInUse') { - const delete_reminders = confirm( - `The notification service is still in use by a ${e.result.reminder_type.toLowerCase()}. Do you want to delete all ${e.result.reminder_type.toLowerCase()}s that are using the notification service?` - ); - - if (delete_reminders) - deleteService(id, delete_reminders_using=true); - return; +function openDeleteNotificationService(serviceId) { + NotiEls.windows.deleteService.dialog.dataset.id = serviceId + NotiEls.windows.deleteService.error.classList.add("hidden") + NotiEls.windows.deleteService.confirm.innerText = "Delete" + NotiEls.windows.deleteService.confirm.onclick = e => deleteService() + NotiEls.windows.deleteService.dialog.showModal() +} - } else - console.log(e); - }); -}; +function deleteService(delete_reminders_using=false) { + const id = parseInt(NotiEls.windows.deleteService.dialog.dataset.id) + sendAPI("DELETE", `/notificationservices/${id}`, {delete_reminders_using: delete_reminders_using}) + .then(json => { + NotiEls.windows.deleteService.error.classList.add('hidden') + fillNotificationServices() + if (delete_reminders_using) { + fillLibrary(reminderTypes.reminder) + fillLibrary(reminderTypes.static_reminder) + fillLibrary(reminderTypes.template) + } + NotiEls.windows.deleteService.dialog.close() + }) + .catch(e => { + e.json().then(json => { + if (json.error === 'NotificationServiceInUse') { + NotiEls.windows.deleteService.error.innerText = + `The notification service is still in use by a ${json.result.reminder_type.toLowerCase()}. Do you want to delete all ${json.result.reminder_type.toLowerCase()}s that are using the notification service?` + NotiEls.windows.deleteService.error.classList.remove('hidden') + NotiEls.windows.deleteService.confirm.innerText = "Delete Anyway" + NotiEls.windows.deleteService.confirm.onclick = e => deleteService(delete_reminders_using=true) + } + else + console.log(json) + }) + }) +} -// +// // Adding a service -// +// function showServiceList(e) { if (!e.target.checked) return; @@ -208,13 +229,51 @@ function showServiceList(e) { const entry = document.createElement('button'); entry.innerText = result.name; entry.onclick = e => showAddServiceWindow(index); + entry.style.viewTransitionName = `ns-${index}`; NotiEls.service_list.appendChild(entry); }); }); }; +function searchServiceList() { + if (autoSearchTimerNs !== null) + clearTimeout(autoSearchTimerNs) + + const f = () => { + const query = NotiEls.search_input.value + .toLowerCase() + .replace('-', '') + .replace('_', '') + .replace(' ', ''); + + if (query === '') + NotiEls.service_list.querySelectorAll('button').forEach( + e => e.classList.remove('hidden') + ); + + else + NotiEls.service_list.querySelectorAll('button').forEach( + e => e.classList.toggle( + 'hidden', + !e.innerText + .toLowerCase() + .replace('-', '') + .replace('_', '') + .replace(' ', '') + .includes(query) + ) + ); + }; + + if (!document.startViewTransition) + f(); + else + document.startViewTransition(f); +}; + function createTitle() { const service_title = document.createElement('input'); + service_title.classList.add('input-style'); service_title.id = 'service-title'; service_title.type = 'text'; service_title.placeholder = 'Service Title'; @@ -224,9 +283,13 @@ function createTitle() { function createChoice(token) { const choice = document.createElement('select'); + choice.classList.add('input-style'); choice.dataset.map = token.map_to || ''; choice.dataset.prefix = ''; - choice.dataset.default = token.default || ''; + if (![null, undefined, ''].includes(token.default)) + choice.dataset.default = token.default + else + choice.dataset.default = '' choice.placeholder = token.name; choice.required = token.required; token.options.forEach(option => { @@ -235,7 +298,7 @@ function createChoice(token) { entry.innerText = option; choice.appendChild(entry); }); - if (token.default) + if (![null, undefined, ''].includes(token.default)) choice.querySelector(`option[value="${token.default}"]`).setAttribute('selected', ''); return choice; @@ -243,6 +306,7 @@ function createChoice(token) { function createString(token) { const str_input = document.createElement('input'); + str_input.classList.add('input-style'); str_input.dataset.map = token.map_to || ''; str_input.dataset.prefix = token.prefix || ''; str_input.dataset.regex = token.regex || ''; @@ -255,9 +319,13 @@ function createString(token) { function createInt(token) { const int_input = document.createElement('input'); + int_input.classList.add('input-style'); int_input.dataset.map = token.map_to || ''; int_input.dataset.prefix = token.prefix || ''; - int_input.dataset.default = token.default || ''; + if (![null, undefined, ''].includes(token.default)) + int_input.dataset.default = token.default + else + int_input.dataset.default = '' int_input.type = 'number'; int_input.placeholder = `${token.name}${!token.required ? ' (Optional)' : ''}`; int_input.required = token.required; @@ -268,11 +336,35 @@ function createInt(token) { return int_input; }; +function createFloat(token) { + const float_input = document.createElement('input'); + float_input.classList.add('input-style'); + float_input.dataset.map = token.map_to || ''; + float_input.dataset.prefix = token.prefix || ''; + if (![null, undefined, ''].includes(token.default)) + float_input.dataset.default = token.default + else + float_input.dataset.default = '' + float_input.type = 'number'; + float_input.step = 0.1; + float_input.placeholder = `${token.name}${!token.required ? ' (Optional)' : ''}`; + float_input.required = token.required; + if (token.min !== null) + float_input.min = token.min; + if (token.max !== null) + float_input.max = token.max; + return float_input; +}; + function createBool(token) { const bool_input = document.createElement('select'); + bool_input.classList.add('input-style'); bool_input.dataset.map = token.map_to || ''; bool_input.dataset.prefix = ''; - bool_input.dataset.default = token.default || ''; + if (![null, undefined, ''].includes(token.default)) + bool_input.dataset.default = token.default + else + bool_input.dataset.default = '' bool_input.placeholder = token.name; bool_input.required = token.required; [['Yes', 'true'], ['No', 'false']].forEach(option => { @@ -282,7 +374,7 @@ function createBool(token) { bool_input.appendChild(entry); }); bool_input.querySelector(`option[value="${token.default}"]`).setAttribute('selected', ''); - + return bool_input; }; @@ -304,6 +396,7 @@ function createEntriesList(token) { const add_row = document.createElement('div'); add_row.classList.add('add-row', 'hidden'); const add_input = document.createElement('input'); + add_input.classList.add('input-style'); add_input.type = 'text'; add_input.onkeydown = e => { if (e.key === "Enter") { @@ -314,6 +407,7 @@ function createEntriesList(token) { }; add_row.appendChild(add_input); const add_entry_button = document.createElement('button'); + add_entry_button.classList.add('input-style'); add_entry_button.type = 'button'; add_entry_button.innerText = 'Add'; add_entry_button.onclick = e => addEntry(entries_list); @@ -321,19 +415,22 @@ function createEntriesList(token) { entries_list.appendChild(add_row); const add_button = document.createElement('button'); + add_button.classList.add('input-style'); add_button.type = 'button'; add_button.innerHTML = icons.add; add_button.onclick = e => toggleAddRow(add_row); entries_list.appendChild(add_button); - + return entries_list; }; function toggleAddRow(row) { if (row.classList.contains('hidden')) { // Show row - row.querySelector('input').value = ''; + const add_input = row.querySelector('input'); + add_input.value = ''; row.classList.remove('hidden'); + add_input.focus(); } else { // Hide row row.classList.add('hidden'); @@ -355,7 +452,7 @@ function showAddServiceWindow(index) { const data = notification_services[index]; console.log(data); - + const title = document.createElement('h3'); title.innerText = data.name; window.appendChild(title); @@ -366,12 +463,13 @@ function showAddServiceWindow(index) { docs.innerText = 'Documentation'; window.appendChild(docs); - window.appendChild(createTitle()); - + window.appendChild(createTitle()); + [[data.details.tokens, 'tokens'], [data.details.args, 'args']].forEach(vars => { if (vars[1] === 'args' && vars[0].length > 0) { // The args are hidden behind a "Show Advanced Settings" button const show_args = document.createElement('button'); + show_args.classList.add('input-style'); show_args.type = 'button'; show_args.innerText = 'Show Advanced Settings'; show_args.onclick = e => { @@ -389,29 +487,31 @@ function showAddServiceWindow(index) { desc.dataset.is_arg = vars[1] === 'args'; window.appendChild(desc); result = createChoice(token); - + } else if (token.type === 'list') { const joint_list = document.createElement('div'); joint_list.dataset.map = token.map_to; joint_list.dataset.delim = token.delim; - + const desc = document.createElement('p'); desc.innerText = `${token.name}${!token.required ? ' (Optional)' : ''}`; joint_list.appendChild(desc); - + if (token.content.length === 0) joint_list.appendChild(createEntriesList(token)); else token.content.forEach(content => joint_list.appendChild(createEntriesList(content)) ); - + result = joint_list; - + } else if (token.type === 'string') result = createString(token); else if (token.type === 'int') result = createInt(token); + else if (token.type === 'float') + result = createFloat(token); else if (token.type === 'bool') { const desc = document.createElement('p'); desc.innerText = `${token.name}${!token.required ? ' (Optional)' : ''}`; @@ -423,24 +523,26 @@ function showAddServiceWindow(index) { result.dataset.is_arg = vars[1] === 'args'; window.appendChild(result); }); - + if (vars[1] === 'args' && vars[0].length > 0) window.querySelectorAll('[data-is_arg="true"]').forEach( el => el.classList.toggle('hidden') ); }) - + // Bottom options const options = document.createElement('div'); options.classList.add('options'); const cancel = document.createElement('button'); + cancel.classList.add('input-style'); cancel.type = 'button'; cancel.innerText = 'Cancel'; cancel.onclick = e => NotiEls.triggers.add_service.checked = false; options.appendChild(cancel); const test = document.createElement('button'); + test.classList.add('input-style'); test.id = 'test-service'; test.type = 'button'; test.onclick = e => testService(); @@ -453,6 +555,7 @@ function showAddServiceWindow(index) { test.appendChild(test_sent_text); const add = document.createElement('button'); + add.classList.add('input-style'); add.type = 'submit'; add.innerText = 'Add'; options.appendChild(add); @@ -476,7 +579,7 @@ function buildAppriseURL() { } else if (i.nodeName === 'DIV') { let value = [...i.querySelectorAll('.entries-list')] - .map(l => + .map(l => [...l.querySelectorAll('.input-entries > div')] .map(e => `${l.dataset.prefix || ''}${e.innerText}`) ) @@ -493,7 +596,7 @@ function buildAppriseURL() { const matching_templates = data.details.templates.filter(template => input_keys === template.replaceAll('}', '{').split('{').filter((e, i) => i % 2).sort().join() ); - + if (!matching_templates.length) return null; @@ -509,9 +612,9 @@ function buildAppriseURL() { if (['INPUT', 'SELECT'].includes(el.nodeName) && el.value && el.value !== el.dataset.default) return `${el.dataset.map}=${el.value}`; else if (el.nodeName == 'DIV') { - let value = + let value = [...el.querySelectorAll('.entries-list')] - .map(l => + .map(l => [...l.querySelectorAll('.input-entries > div')] .map(e => `${l.dataset.prefix || ''}${e.innerText}`) ) @@ -575,7 +678,7 @@ function testService() { }) .then(response => { if (!response.ok) return Promise.reject(response.status); - + test_button.classList.remove('error-input'); test_button.title = ''; test_button.classList.add('show-sent'); @@ -593,14 +696,14 @@ function testService() { function addService() { const add_button = NotiEls.add_service_window.querySelector('.options > button[type="submit"]'); - + // Check regexes for input's [...NotiEls.add_service_window.querySelectorAll('input:not([data-regex=""])[data-regex]')] .forEach(el => el.classList.remove('error-input')); const faulty_inputs = [...NotiEls.add_service_window.querySelectorAll('input:not([data-regex=""])[data-regex]')] - .filter(el => + .filter(el => !( (!el.required && el.value === '') || @@ -631,7 +734,7 @@ function addService() { }) .then(response => { if (!response.ok) return Promise.reject(response.status); - + add_button.classList.remove('error-input'); add_button.title = ''; @@ -657,4 +760,29 @@ fillNotificationServices(); let notification_services = null; NotiEls.triggers.service_list.onchange = showServiceList; -NotiEls.add_service_window.action = 'javascript:addService();'; \ No newline at end of file +NotiEls.add_service_window.action = 'javascript:addService();'; + +NotiEls.windows.editService.dialog.onclick = e => { + if (e.target === e.currentTarget) { + e.stopPropagation() + NotiEls.windows.editService.dialog.close(); + } +} +NotiEls.windows.editService.form.action = 'javascript:editNotificationService()' +NotiEls.windows.editService.close.onclick = e => NotiEls.windows.editService.dialog.close(); + +NotiEls.windows.deleteService.dialog.onclick = e => { + if (e.target === e.currentTarget) { + e.stopPropagation() + NotiEls.windows.deleteService.dialog.close() + } +} +NotiEls.windows.deleteService.close.onclick = e => NotiEls.windows.deleteService.dialog.close() + +var autoSearchTimerNs = null +NotiEls.search_input.oninput = e => { + if (autoSearchTimerNs !== null) + clearTimeout(autoSearchTimerNs) + + autoSearchTimerNs = setTimeout(searchServiceList, constants.autoSearchTimeoutNs) +} diff --git a/frontend/static/js/reminders.js b/frontend/static/js/reminders.js index a1a7361..f5eebb4 100644 --- a/frontend/static/js/reminders.js +++ b/frontend/static/js/reminders.js @@ -49,7 +49,21 @@ function showWindow(id) { constants.windowAnimationDuration ) - document.body.onkeydown = null + if (id === "notification") { + document.body.onkeydown = e => { + if ( + e.key === '/' + && NotiEls.triggers.service_list.checked + && !NotiEls.triggers.add_service.checked + && document.activeElement !== NotiEls.search_input + ) { + NotiEls.search_input.focus() + e.preventDefault() + } + } + } + else + document.body.onkeydown = null } } diff --git a/frontend/templates/reminders.html b/frontend/templates/reminders.html index 4cfd459..3de3029 100644 --- a/frontend/templates/reminders.html +++ b/frontend/templates/reminders.html @@ -38,12 +38,8 @@ - - + +
- - - - - + + + + + + +
+
+

Are you sure you want to permanently delete this notification service?

+ +
+ + +
+
+
+
+