From 38133be15d7df232fd9d4551522f21beb4072403 Mon Sep 17 00:00:00 2001 From: jude Date: Sat, 12 Aug 2023 12:27:15 +0100 Subject: [PATCH 01/10] In progress --- .../20230812111348_orphan_reminders.sql | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 migrations/20230812111348_orphan_reminders.sql diff --git a/migrations/20230812111348_orphan_reminders.sql b/migrations/20230812111348_orphan_reminders.sql new file mode 100644 index 0000000..a2f7c19 --- /dev/null +++ b/migrations/20230812111348_orphan_reminders.sql @@ -0,0 +1,21 @@ +-- Drop existing constraint +-- TODO +ALTER TABLE `reminders` DROP CONSTRAINT `channel_id_` + +ALTER TABLE `reminders` MODIFY COLUMN `channel_id` BIGINT ; +ALTER TABLE `reminders` ADD COLUMN `guild_id` BIGINT; + +ALTER TABLE `reminders` + ADD CONSTRAINT `guild_id_fk` + FOREIGN KEY (`guild_id`) + REFERENCES `guilds`(`id`) + ON DELETE CASCADE; + +ALTER TABLE `reminders` + ADD CONSTRAINT `channel_id_fk` + FOREIGN KEY (`channel_id`) + REFERENCES `channels`(`id`) + ON DELETE SET NULL; + +-- TODO +UPDATE `reminders` SET `guild_id` = (SELECT guilds.`id` FROM `channels` INNER JOIN `guilds` ON channels.guild_id = guilds.id WHERE ) From 6726ca0c2d277994ac4811376faba5b24c5fb1e5 Mon Sep 17 00:00:00 2001 From: jude Date: Sat, 19 Aug 2023 19:24:06 +0100 Subject: [PATCH 02/10] Correct migration script. Stub code for routes. Update existing routes to set the reminder's guild ID. --- web/static/js/reminder_errors.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 web/static/js/reminder_errors.js diff --git a/web/static/js/reminder_errors.js b/web/static/js/reminder_errors.js new file mode 100644 index 0000000..6fa4798 --- /dev/null +++ b/web/static/js/reminder_errors.js @@ -0,0 +1,19 @@ +let _reminderErrors = []; + +const reminderErrors = () => { + return _reminderErrors; +} + +const guildId = () => { + let selected: HTMLElement = document.querySelector(".guildList a.is-active"); + return selected.dataset["guild"]; +} + + +function loadErrors() { + fetch(`/dashboard/api/guild/${guildId()}/errors`).then(response => response.json()) +} + +document.addEventListener('DOMContentLoaded', () => { + +}) From 109cf16dbbd9a7fdc9f6fb7ef676d1dea94c33b2 Mon Sep 17 00:00:00 2001 From: jude Date: Sat, 19 Aug 2023 21:21:56 +0100 Subject: [PATCH 03/10] Store guild when creating reminders --- .../20230812111348_orphan_reminders.sql | 10 +- src/models/channel_data.rs | 5 +- src/models/reminder/builder.rs | 15 +- web/src/lib.rs | 1 + web/src/routes/dashboard/guild.rs | 135 ++++++++++-------- web/src/routes/dashboard/mod.rs | 16 ++- web/static/js/reminder_errors.js | 2 +- 7 files changed, 108 insertions(+), 76 deletions(-) diff --git a/migrations/20230812111348_orphan_reminders.sql b/migrations/20230812111348_orphan_reminders.sql index a2f7c19..00bf1a7 100644 --- a/migrations/20230812111348_orphan_reminders.sql +++ b/migrations/20230812111348_orphan_reminders.sql @@ -1,9 +1,8 @@ -- Drop existing constraint --- TODO -ALTER TABLE `reminders` DROP CONSTRAINT `channel_id_` +ALTER TABLE `reminders` DROP CONSTRAINT `reminders_ibfk_1`; -ALTER TABLE `reminders` MODIFY COLUMN `channel_id` BIGINT ; -ALTER TABLE `reminders` ADD COLUMN `guild_id` BIGINT; +ALTER TABLE `reminders` MODIFY COLUMN `channel_id` INT UNSIGNED; +ALTER TABLE `reminders` ADD COLUMN `guild_id` INT UNSIGNED; ALTER TABLE `reminders` ADD CONSTRAINT `guild_id_fk` @@ -17,5 +16,4 @@ ALTER TABLE `reminders` REFERENCES `channels`(`id`) ON DELETE SET NULL; --- TODO -UPDATE `reminders` SET `guild_id` = (SELECT guilds.`id` FROM `channels` INNER JOIN `guilds` ON channels.guild_id = guilds.id WHERE ) +UPDATE `reminders` SET `guild_id` = (SELECT guilds.`id` FROM `channels` INNER JOIN `guilds` ON channels.guild_id = guilds.id WHERE reminders.channel_id = channels.id); diff --git a/src/models/channel_data.rs b/src/models/channel_data.rs index 9983056..e36f31c 100644 --- a/src/models/channel_data.rs +++ b/src/models/channel_data.rs @@ -10,6 +10,7 @@ pub struct ChannelData { pub webhook_id: Option, pub webhook_token: Option, pub paused: bool, + pub db_guild_id: Option, pub paused_until: Option, } @@ -22,7 +23,7 @@ impl ChannelData { if let Ok(c) = sqlx::query_as_unchecked!( Self, - "SELECT id, name, nudge, blacklisted, webhook_id, webhook_token, paused, paused_until FROM channels WHERE channel = ?", + "SELECT id, name, nudge, blacklisted, webhook_id, webhook_token, paused, paused_until, guild_id AS db_guild_id FROM channels WHERE channel = ?", channel_id ) .fetch_one(pool) @@ -46,7 +47,7 @@ impl ChannelData { Ok(sqlx::query_as_unchecked!( Self, " -SELECT id, name, nudge, blacklisted, webhook_id, webhook_token, paused, paused_until FROM channels WHERE channel = ? +SELECT id, name, nudge, blacklisted, webhook_id, webhook_token, paused, paused_until, guild_id AS db_guild_id FROM channels WHERE channel = ? ", channel_id ) diff --git a/src/models/reminder/builder.rs b/src/models/reminder/builder.rs index 0d2e6d8..8e3cf42 100644 --- a/src/models/reminder/builder.rs +++ b/src/models/reminder/builder.rs @@ -51,6 +51,7 @@ pub struct ReminderBuilder { pool: MySqlPool, uid: String, channel: u32, + guild: Option, thread_id: Option, utc_time: NaiveDateTime, timezone: String, @@ -86,6 +87,7 @@ impl ReminderBuilder { INSERT INTO reminders ( `uid`, `channel_id`, + `guild_id`, `utc_time`, `timezone`, `interval_seconds`, @@ -110,11 +112,13 @@ INSERT INTO reminders ( ?, ?, ?, + ?, ? ) ", self.uid, self.channel, + self.guild, utc_time, self.timezone, self.interval_seconds, @@ -247,10 +251,10 @@ impl<'a> MultiReminderBuilder<'a> { { Err(ReminderError::UserBlockedDm) } else { - Ok(user_data.dm_channel) + Ok((user_data.dm_channel, None)) } } else { - Ok(user_data.dm_channel) + Ok((user_data.dm_channel, None)) } } else { Err(ReminderError::InvalidTag) @@ -297,13 +301,13 @@ impl<'a> MultiReminderBuilder<'a> { .commit_changes(&self.ctx.data().database) .await; - Ok(channel_data.id) + Ok((channel_data.id, channel_data.db_guild_id)) } Err(e) => Err(ReminderError::DiscordError(e.to_string())), } } else { - Ok(channel_data.id) + Ok((channel_data.id, channel_data.db_guild_id)) } } } else { @@ -317,7 +321,8 @@ impl<'a> MultiReminderBuilder<'a> { let builder = ReminderBuilder { pool: self.ctx.data().database.clone(), uid: generate_uid(), - channel: c, + channel: c.0, + guild: c.1, thread_id, utc_time: self.utc_time, timezone: self.timezone.to_string(), diff --git a/web/src/lib.rs b/web/src/lib.rs index 54631cd..7e9fb03 100644 --- a/web/src/lib.rs +++ b/web/src/lib.rs @@ -165,6 +165,7 @@ pub async fn initialize( routes::dashboard::guild::get_reminders, routes::dashboard::guild::edit_reminder, routes::dashboard::guild::delete_reminder, + routes::dashboard::guild::get_reminder_errors, routes::dashboard::export::export_reminders, routes::dashboard::export::export_reminder_templates, routes::dashboard::export::export_todos, diff --git a/web/src/routes/dashboard/guild.rs b/web/src/routes/dashboard/guild.rs index 1f89aca..019e99d 100644 --- a/web/src/routes/dashboard/guild.rs +++ b/web/src/routes/dashboard/guild.rs @@ -26,7 +26,7 @@ use crate::{ routes::{ dashboard::{ create_database_channel, create_reminder, template_name_default, DeleteReminder, - DeleteReminderTemplate, PatchReminder, Reminder, ReminderTemplate, + DeleteReminderTemplate, PatchReminder, Reminder, ReminderError, ReminderTemplate, }, JsonResult, }, @@ -322,72 +322,53 @@ pub async fn create_guild_reminder( pub async fn get_reminders( id: u64, cookies: &CookieJar<'_>, - ctx: &State, serenity_context: &State, pool: &State>, ) -> JsonResult { check_authorization!(cookies, serenity_context.inner(), id); - let channels_res = GuildId(id).channels(&ctx.inner()).await; + sqlx::query_as_unchecked!( + Reminder, + "SELECT + reminders.attachment, + reminders.attachment_name, + reminders.avatar, + channels.channel, + reminders.content, + reminders.embed_author, + reminders.embed_author_url, + reminders.embed_color, + reminders.embed_description, + reminders.embed_footer, + reminders.embed_footer_url, + reminders.embed_image_url, + reminders.embed_thumbnail_url, + reminders.embed_title, + IFNULL(reminders.embed_fields, '[]') AS embed_fields, + reminders.enabled, + reminders.expires, + reminders.interval_seconds, + reminders.interval_days, + reminders.interval_months, + reminders.name, + reminders.restartable, + reminders.tts, + reminders.uid, + reminders.username, + reminders.utc_time + FROM reminders + LEFT JOIN channels ON channels.id = reminders.channel_id + WHERE `status` = 'pending' AND reminders.guild_id = (SELECT id FROM guilds WHERE guild = ?)", + id + ) + .fetch_all(pool.inner()) + .await + .map(|r| Ok(json!(r))) + .unwrap_or_else(|e| { + warn!("Failed to complete SQL query: {:?}", e); - match channels_res { - Ok(channels) => { - let channels = channels - .keys() - .into_iter() - .map(|k| k.as_u64().to_string()) - .collect::>() - .join(","); - - sqlx::query_as_unchecked!( - Reminder, - "SELECT - reminders.attachment, - reminders.attachment_name, - reminders.avatar, - channels.channel, - reminders.content, - reminders.embed_author, - reminders.embed_author_url, - reminders.embed_color, - reminders.embed_description, - reminders.embed_footer, - reminders.embed_footer_url, - reminders.embed_image_url, - reminders.embed_thumbnail_url, - reminders.embed_title, - IFNULL(reminders.embed_fields, '[]') AS embed_fields, - reminders.enabled, - reminders.expires, - reminders.interval_seconds, - reminders.interval_days, - reminders.interval_months, - reminders.name, - reminders.restartable, - reminders.tts, - reminders.uid, - reminders.username, - reminders.utc_time - FROM reminders - LEFT JOIN channels ON channels.id = reminders.channel_id - WHERE `status` = 'pending' AND FIND_IN_SET(channels.channel, ?)", - channels - ) - .fetch_all(pool.inner()) - .await - .map(|r| Ok(json!(r))) - .unwrap_or_else(|e| { - warn!("Failed to complete SQL query: {:?}", e); - - json_err!("Could not load reminders") - }) - } - Err(e) => { - warn!("Could not fetch channels from {}: {:?}", id, e); - - Ok(json!([])) - } - } + json_err!("Could not load reminders") + }) } #[patch("/api/guild//reminders", data = "")] @@ -623,3 +604,35 @@ pub async fn delete_reminder( } } } + +#[get("/api/guild//errors")] +pub async fn get_reminder_errors( + id: u64, + cookies: &CookieJar<'_>, + serenity_context: &State, + pool: &State>, +) -> JsonResult { + check_authorization!(cookies, serenity_context.inner(), id); + + sqlx::query_as_unchecked!( + ReminderError, + "SELECT + reminders.status, + reminders.utc_time, + reminders.name, + reminders.uid, + reminders.channel_id AS channel + FROM reminders + LEFT JOIN channels ON channels.id = reminders.channel_id + WHERE (`status` != 'pending' OR reminders.channel_id IS NULL) AND reminders.guild_id = (SELECT id FROM guilds WHERE guild = ?)", + id + ) + .fetch_all(pool.inner()) + .await + .map(|r| Ok(json!(r))) + .unwrap_or_else(|e| { + warn!("Failed to complete SQL query: {:?}", e); + + json_err!("Could not load reminders") + }) +} diff --git a/web/src/routes/dashboard/mod.rs b/web/src/routes/dashboard/mod.rs index 4168a4a..37f2f89 100644 --- a/web/src/routes/dashboard/mod.rs +++ b/web/src/routes/dashboard/mod.rs @@ -152,6 +152,18 @@ pub struct Reminder { utc_time: NaiveDateTime, } +#[derive(Serialize)] +pub struct ReminderError { + #[serde(with = "string")] + channel: u64, + status: String, + #[serde(default = "name_default")] + name: String, + #[serde(default)] + uid: String, + utc_time: NaiveDateTime, +} + #[derive(Serialize, Deserialize)] pub struct ReminderCsv { #[serde(with = "base64s")] @@ -479,6 +491,7 @@ pub async fn create_reminder( attachment, attachment_name, channel_id, + guild_id, avatar, content, embed_author, @@ -501,11 +514,12 @@ pub async fn create_reminder( tts, username, `utc_time` - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", new_uid, attachment_data, reminder.attachment_name, channel, + guild_id.0, reminder.avatar, reminder.content, reminder.embed_author, diff --git a/web/static/js/reminder_errors.js b/web/static/js/reminder_errors.js index 6fa4798..42246bc 100644 --- a/web/static/js/reminder_errors.js +++ b/web/static/js/reminder_errors.js @@ -5,7 +5,7 @@ const reminderErrors = () => { } const guildId = () => { - let selected: HTMLElement = document.querySelector(".guildList a.is-active"); + let selected = document.querySelector(".guildList a.is-active"); return selected.dataset["guild"]; } From ea3fe3f5436f97494fbb81978286fb9afde0df38 Mon Sep 17 00:00:00 2001 From: jude Date: Sat, 19 Aug 2023 21:28:05 +0100 Subject: [PATCH 04/10] Ensure postman doesn't try to send reminders with no channel --- postman/src/sender.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/postman/src/sender.rs b/postman/src/sender.rs index 08e83e9..096dad9 100644 --- a/postman/src/sender.rs +++ b/postman/src/sender.rs @@ -310,6 +310,7 @@ WHERE reminders WHERE reminders.`utc_time` <= NOW() AND + reminders.`channel_id` NOT NULL AND `status` = 'pending' AND ( reminders.`interval_seconds` IS NOT NULL From adf29dca5d474f233b997675bf949ab963433d99 Mon Sep 17 00:00:00 2001 From: jude Date: Sat, 19 Aug 2023 22:37:48 +0100 Subject: [PATCH 05/10] Start to think about how to display errors --- postman/src/sender.rs | 2 +- web/src/routes/dashboard/guild.rs | 2 +- web/static/js/main.js | 10 ++++++++++ web/static/js/reminder_errors.js | 19 ------------------- web/static/js/reminder_errors.ts | 19 ------------------- web/templates/dashboard.html.tera | 4 ++++ .../reminder_dashboard.html.tera | 4 ++++ 7 files changed, 20 insertions(+), 40 deletions(-) delete mode 100644 web/static/js/reminder_errors.js delete mode 100644 web/static/js/reminder_errors.ts diff --git a/postman/src/sender.rs b/postman/src/sender.rs index 096dad9..da99555 100644 --- a/postman/src/sender.rs +++ b/postman/src/sender.rs @@ -310,7 +310,7 @@ WHERE reminders WHERE reminders.`utc_time` <= NOW() AND - reminders.`channel_id` NOT NULL AND + reminders.`channel_id` IS NOT NULL AND `status` = 'pending' AND ( reminders.`interval_seconds` IS NOT NULL diff --git a/web/src/routes/dashboard/guild.rs b/web/src/routes/dashboard/guild.rs index 019e99d..0d34365 100644 --- a/web/src/routes/dashboard/guild.rs +++ b/web/src/routes/dashboard/guild.rs @@ -624,7 +624,7 @@ pub async fn get_reminder_errors( reminders.channel_id AS channel FROM reminders LEFT JOIN channels ON channels.id = reminders.channel_id - WHERE (`status` != 'pending' OR reminders.channel_id IS NULL) AND reminders.guild_id = (SELECT id FROM guilds WHERE guild = ?)", + WHERE (`status` = 'failed' OR reminders.channel_id IS NULL) AND reminders.guild_id = (SELECT id FROM guilds WHERE guild = ?)", id ) .fetch_all(pool.inner()) diff --git a/web/static/js/main.js b/web/static/js/main.js index b79ce7d..2199295 100644 --- a/web/static/js/main.js +++ b/web/static/js/main.js @@ -18,6 +18,7 @@ const $downloader = document.querySelector("a#downloader"); const $uploader = document.querySelector("input#uploader"); let channels = []; +let reminderErrors = []; let guildNames = {}; let roles = []; let templates = {}; @@ -36,6 +37,12 @@ function guildId() { return document.querySelector(".guildList a.is-active").dataset["guild"]; } +function loadErrors() { + return fetch(`/dashboard/api/guild/${guildId()}/errors`).then((response) => + response.json() + ); +} + function colorToInt(r, g, b) { return (r << 16) + (g << 8) + b; } @@ -476,6 +483,9 @@ document.addEventListener("guildSwitched", async (e) => { fetch_roles(e.detail.guild_id); fetch_templates(e.detail.guild_id); fetch_reminders(e.detail.guild_id); + loadErrors().then((res) => { + console.log(res); + }); document.querySelectorAll("p.pageTitle").forEach((el) => { el.textContent = `${e.detail.guild_name} Reminders`; diff --git a/web/static/js/reminder_errors.js b/web/static/js/reminder_errors.js deleted file mode 100644 index 42246bc..0000000 --- a/web/static/js/reminder_errors.js +++ /dev/null @@ -1,19 +0,0 @@ -let _reminderErrors = []; - -const reminderErrors = () => { - return _reminderErrors; -} - -const guildId = () => { - let selected = document.querySelector(".guildList a.is-active"); - return selected.dataset["guild"]; -} - - -function loadErrors() { - fetch(`/dashboard/api/guild/${guildId()}/errors`).then(response => response.json()) -} - -document.addEventListener('DOMContentLoaded', () => { - -}) diff --git a/web/static/js/reminder_errors.ts b/web/static/js/reminder_errors.ts deleted file mode 100644 index 6fa4798..0000000 --- a/web/static/js/reminder_errors.ts +++ /dev/null @@ -1,19 +0,0 @@ -let _reminderErrors = []; - -const reminderErrors = () => { - return _reminderErrors; -} - -const guildId = () => { - let selected: HTMLElement = document.querySelector(".guildList a.is-active"); - return selected.dataset["guild"]; -} - - -function loadErrors() { - fetch(`/dashboard/api/guild/${guildId()}/errors`).then(response => response.json()) -} - -document.addEventListener('DOMContentLoaded', () => { - -}) diff --git a/web/templates/dashboard.html.tera b/web/templates/dashboard.html.tera index c84b5dc..4731427 100644 --- a/web/templates/dashboard.html.tera +++ b/web/templates/dashboard.html.tera @@ -385,6 +385,10 @@ {% include "reminder_dashboard/guild_reminder" %} + + diff --git a/web/templates/reminder_dashboard/reminder_dashboard.html.tera b/web/templates/reminder_dashboard/reminder_dashboard.html.tera index 8849767..2ef2dd2 100644 --- a/web/templates/reminder_dashboard/reminder_dashboard.html.tera +++ b/web/templates/reminder_dashboard/reminder_dashboard.html.tera @@ -46,6 +46,10 @@
+ +
+ +
From b6b5e6d2b25a8efc0cc3e4c049770c198536c7ca Mon Sep 17 00:00:00 2001 From: jude Date: Sun, 27 Aug 2023 17:41:23 +0100 Subject: [PATCH 06/10] Add error pane --- web/src/lib.rs | 3 +- web/src/routes/dashboard/mod.rs | 12 ++++- web/static/css/style.css | 27 ++++++++-- web/static/js/main.js | 89 +++++++++++++++++++------------ web/static/js/reminder_errors.js | 16 ++++++ web/templates/dashboard.html.tera | 12 ++++- 6 files changed, 119 insertions(+), 40 deletions(-) create mode 100644 web/static/js/reminder_errors.js diff --git a/web/src/lib.rs b/web/src/lib.rs index 7e9fb03..ab77794 100644 --- a/web/src/lib.rs +++ b/web/src/lib.rs @@ -150,7 +150,8 @@ pub async fn initialize( .mount( "/dashboard", routes![ - routes::dashboard::dashboard, + routes::dashboard::dashboard_1, + routes::dashboard::dashboard_2, routes::dashboard::dashboard_home, routes::dashboard::user::get_user_info, routes::dashboard::user::update_user_info, diff --git a/web/src/routes/dashboard/mod.rs b/web/src/routes/dashboard/mod.rs index 37f2f89..c56089a 100644 --- a/web/src/routes/dashboard/mod.rs +++ b/web/src/routes/dashboard/mod.rs @@ -676,7 +676,17 @@ pub async fn dashboard_home(cookies: &CookieJar<'_>) -> Result")] -pub async fn dashboard(cookies: &CookieJar<'_>) -> Result { +pub async fn dashboard_1(cookies: &CookieJar<'_>) -> Result { + if cookies.get_private("userid").is_some() { + let map: HashMap<&str, String> = HashMap::new(); + Ok(Template::render("dashboard", &map)) + } else { + Err(Redirect::to("/login/discord")) + } +} + +#[get("/<_>/<_>")] +pub async fn dashboard_2(cookies: &CookieJar<'_>) -> Result { if cookies.get_private("userid").is_some() { let map: HashMap<&str, String> = HashMap::new(); Ok(Template::render("dashboard", &map)) diff --git a/web/static/css/style.css b/web/static/css/style.css index 6d4ea76..fb42aa9 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -291,10 +291,19 @@ div.dashboard-sidebar:not(.mobile-sidebar) { flex-direction: column; } +ul.guildList { + flex-grow: 1; + flex-shrink: 1; + overflow: scroll; +} + div.dashboard-sidebar:not(.mobile-sidebar) .aside-footer { - position: fixed; - bottom: 0; - width: 226px; + flex-shrink: 0; + flex-grow: 0; +} + +div.dashboard-sidebar svg { + flex-shrink: 0; } div.mobile-sidebar { @@ -716,6 +725,18 @@ a.switch-pane { text-overflow: ellipsis; } +.guild-submenu { + display: none; +} + +.guild-submenu li { + font-size: 0.8rem; +} + +a.switch-pane.is-active ~ .guild-submenu { + display: block; +} + .feedback { background-color: #5865F2; } diff --git a/web/static/js/main.js b/web/static/js/main.js index 2199295..cd164c7 100644 --- a/web/static/js/main.js +++ b/web/static/js/main.js @@ -34,13 +34,11 @@ let globalPatreon = false; let guildPatreon = false; function guildId() { - return document.querySelector(".guildList a.is-active").dataset["guild"]; + return document.querySelector("li > a.is-active").parentElement.dataset["guild"]; } -function loadErrors() { - return fetch(`/dashboard/api/guild/${guildId()}/errors`).then((response) => - response.json() - ); +function guildName() { + return guildNames[guildId()]; } function colorToInt(r, g, b) { @@ -456,21 +454,19 @@ document.addEventListener("guildSwitched", async (e) => { .querySelectorAll(".patreon-only") .forEach((el) => el.classList.add("is-locked")); - let $anchor = document.querySelector( - `.switch-pane[data-guild="${e.detail.guild_id}"]` - ); + let $li = document.querySelector(`li[data-guild="${e.detail.guild_id}"]`); - let hasError = false; - - if ($anchor === null) { + if ($li === null) { switch_pane("user-error"); - hasError = true; return; } - switch_pane($anchor.dataset["pane"]); + switch_pane(e.detail.pane); reset_guild_pane(); - $anchor.classList.add("is-active"); + $li.querySelector("li > a").classList.add("is-active"); + $li.querySelectorAll(`*[data-pane="${e.detail.pane}"]`).forEach((el) => { + el.classList.add("is-active"); + }); if (globalPatreon || (await fetch_patreon(e.detail.guild_id))) { document @@ -478,18 +474,26 @@ document.addEventListener("guildSwitched", async (e) => { .forEach((el) => el.classList.remove("is-locked")); } - hasError = await fetch_channels(e.detail.guild_id); + const event = new CustomEvent("paneLoad", { + detail: { + guild_id: e.detail.guild_id, + pane: e.detail.pane, + }, + }); + document.dispatchEvent(event); +}); + +document.addEventListener("paneLoad", async (ev) => { + const hasError = await fetch_channels(ev.detail.guild_id); if (!hasError) { - fetch_roles(e.detail.guild_id); - fetch_templates(e.detail.guild_id); - fetch_reminders(e.detail.guild_id); - loadErrors().then((res) => { - console.log(res); - }); + fetch_roles(ev.detail.guild_id); + fetch_templates(ev.detail.guild_id); + fetch_reminders(ev.detail.guild_id); document.querySelectorAll("p.pageTitle").forEach((el) => { - el.textContent = `${e.detail.guild_name} Reminders`; + el.textContent = `${guildName()} Reminders`; }); + document.querySelectorAll("select.channel-selector").forEach((el) => { el.addEventListener("change", (e) => { update_select(e.target); @@ -694,21 +698,37 @@ document.addEventListener("DOMContentLoaded", async () => { "%guildname%", guild.name ); - $anchor.dataset["guild"] = guild.id; $anchor.dataset["name"] = guild.name; - $anchor.href = `/dashboard/${guild.id}?name=${guild.name}`; + $anchor.href = `/dashboard/${guild.id}/reminders`; - $anchor.addEventListener("click", async (e) => { - e.preventDefault(); - window.history.pushState({}, "", `/dashboard/${guild.id}`); - const event = new CustomEvent("guildSwitched", { - detail: { - guild_name: guild.name, - guild_id: guild.id, - }, + const $li = $anchor.parentElement; + $li.dataset["guild"] = guild.id; + + $li.querySelectorAll("a").forEach((el) => { + el.addEventListener("click", (e) => { + const pane = el.dataset["pane"]; + const slug = el.dataset["slug"]; + + if (pane !== undefined && slug !== undefined) { + e.preventDefault(); + + switch_pane(pane); + + window.history.pushState( + {}, + "", + `/dashboard/${guild.id}/${slug}` + ); + const event = new CustomEvent("guildSwitched", { + detail: { + guild_id: guild.id, + pane, + }, + }); + + document.dispatchEvent(event); + } }); - - document.dispatchEvent(event); }); element.append($clone); @@ -724,6 +744,7 @@ document.addEventListener("DOMContentLoaded", async () => { detail: { guild_name: name, guild_id: id, + pane: "guild", }, }); diff --git a/web/static/js/reminder_errors.js b/web/static/js/reminder_errors.js new file mode 100644 index 0000000..ff12367 --- /dev/null +++ b/web/static/js/reminder_errors.js @@ -0,0 +1,16 @@ +function loadErrors() { + return fetch(`/dashboard/api/guild/${guildId()}/errors`).then((response) => + response.json() + ); +} + +document.addEventListener("paneLoad", (ev) => { + if (ev.detail.pane !== "reminder-errors") { + return; + } + + // Load errors + loadErrors().then((res) => {}); + + $loader.classList.add("is-hidden"); +}); diff --git a/web/templates/dashboard.html.tera b/web/templates/dashboard.html.tera index 4731427..18585c8 100644 --- a/web/templates/dashboard.html.tera +++ b/web/templates/dashboard.html.tera @@ -375,9 +375,19 @@ From ee89cb40c50ebd17e086cb0933e1f71391c3d3de Mon Sep 17 00:00:00 2001 From: jude Date: Sun, 3 Sep 2023 15:01:42 +0100 Subject: [PATCH 07/10] Move errors route into get_reminders route. Add database migration. --- .../20230903131153_reminder_status_timing.sql | 4 ++ src/component_models/mod.rs | 22 ++++--- src/models/reminder/mod.rs | 11 ++-- web/src/lib.rs | 1 - web/src/routes/dashboard/export.rs | 2 + web/src/routes/dashboard/guild.rs | 59 ++++++------------- web/src/routes/dashboard/mod.rs | 16 ++--- web/static/css/style.css | 38 ++++++++++++ web/static/js/main.js | 9 ++- web/static/js/reminder_errors.js | 12 ++-- web/templates/dashboard.html.tera | 22 +++---- .../reminder_dashboard.html.tera | 2 +- .../reminder_errors.html.tera | 46 +++++++++++++++ .../{ => templates}/guild_reminder.html.tera | 0 .../templates/reminder_error.html.tera | 13 ++++ 15 files changed, 172 insertions(+), 85 deletions(-) create mode 100644 migrations/20230903131153_reminder_status_timing.sql rename web/templates/reminder_dashboard/{ => templates}/guild_reminder.html.tera (100%) create mode 100644 web/templates/reminder_dashboard/templates/reminder_error.html.tera diff --git a/migrations/20230903131153_reminder_status_timing.sql b/migrations/20230903131153_reminder_status_timing.sql new file mode 100644 index 0000000..742b1a9 --- /dev/null +++ b/migrations/20230903131153_reminder_status_timing.sql @@ -0,0 +1,4 @@ +ALTER TABLE reminders ADD COLUMN `status_change_time` DATETIME; + +-- This is a best-guess as to the status change time. +UPDATE reminders SET `status_change_time` = `utc_time`; diff --git a/src/component_models/mod.rs b/src/component_models/mod.rs index 9251796..ff1832a 100644 --- a/src/component_models/mod.rs +++ b/src/component_models/mod.rs @@ -166,15 +166,21 @@ impl ComponentDataModel { .await; } ComponentDataModel::DelSelector(selector) => { - let selected_id = component.data.values.join(","); + for id in &component.data.values { + match id.parse::() { + Ok(id) => { + if let Some(reminder) = Reminder::from_id(&data.database, id).await { + reminder.delete(&data.database).await.unwrap(); + } else { + warn!("Attempt to delete non-existent reminder"); + } + } - sqlx::query!( - "UPDATE reminders SET `status` = 'deleted' WHERE FIND_IN_SET(id, ?)", - selected_id - ) - .execute(&data.database) - .await - .unwrap(); + Err(e) => { + warn!("Error casting ID to integer: {:?}.", e); + } + } + } let reminders = Reminder::from_guild( &ctx, diff --git a/src/models/reminder/mod.rs b/src/models/reminder/mod.rs index 5e69971..edf9d24 100644 --- a/src/models/reminder/mod.rs +++ b/src/models/reminder/mod.rs @@ -304,10 +304,13 @@ WHERE &self, db: impl Executor<'_, Database = Database>, ) -> Result<(), sqlx::Error> { - sqlx::query!("UPDATE reminders SET `status` = 'deleted' WHERE uid = ?", self.uid) - .execute(db) - .await - .map(|_| ()) + sqlx::query!( + "UPDATE reminders SET `status` = 'deleted', `status_change_time` = NOW() WHERE uid = ?", + self.uid + ) + .execute(db) + .await + .map(|_| ()) } pub fn display_content(&self) -> &str { diff --git a/web/src/lib.rs b/web/src/lib.rs index ab77794..c45e2d5 100644 --- a/web/src/lib.rs +++ b/web/src/lib.rs @@ -166,7 +166,6 @@ pub async fn initialize( routes::dashboard::guild::get_reminders, routes::dashboard::guild::edit_reminder, routes::dashboard::guild::delete_reminder, - routes::dashboard::guild::get_reminder_errors, routes::dashboard::export::export_reminders, routes::dashboard::export::export_reminder_templates, routes::dashboard::export::export_todos, diff --git a/web/src/routes/dashboard/export.rs b/web/src/routes/dashboard/export.rs index d1f33c1..86cf10e 100644 --- a/web/src/routes/dashboard/export.rs +++ b/web/src/routes/dashboard/export.rs @@ -171,6 +171,8 @@ pub async fn import_reminders( uid: generate_uid(), username: record.username, utc_time: record.utc_time, + status: "pending".to_string(), + status_change_time: None, }; create_reminder( diff --git a/web/src/routes/dashboard/guild.rs b/web/src/routes/dashboard/guild.rs index 0d34365..4c33798 100644 --- a/web/src/routes/dashboard/guild.rs +++ b/web/src/routes/dashboard/guild.rs @@ -26,7 +26,7 @@ use crate::{ routes::{ dashboard::{ create_database_channel, create_reminder, template_name_default, DeleteReminder, - DeleteReminderTemplate, PatchReminder, Reminder, ReminderError, ReminderTemplate, + DeleteReminderTemplate, PatchReminder, Reminder, ReminderTemplate, }, JsonResult, }, @@ -318,15 +318,18 @@ pub async fn create_guild_reminder( .await } -#[get("/api/guild//reminders")] +#[get("/api/guild//reminders?")] pub async fn get_reminders( id: u64, cookies: &CookieJar<'_>, serenity_context: &State, pool: &State>, + status: Option, ) -> JsonResult { check_authorization!(cookies, serenity_context.inner(), id); + let status = status.unwrap_or("pending".to_string()); + sqlx::query_as_unchecked!( Reminder, "SELECT @@ -355,10 +358,13 @@ pub async fn get_reminders( reminders.tts, reminders.uid, reminders.username, - reminders.utc_time + reminders.utc_time, + reminders.status, + reminders.status_change_time FROM reminders LEFT JOIN channels ON channels.id = reminders.channel_id - WHERE `status` = 'pending' AND reminders.guild_id = (SELECT id FROM guilds WHERE guild = ?)", + WHERE FIND_IN_SET(`status`, ?) AND reminders.guild_id = (SELECT id FROM guilds WHERE guild = ?)", + status, id ) .fetch_all(pool.inner()) @@ -567,7 +573,9 @@ pub async fn edit_reminder( reminders.tts, reminders.uid, reminders.username, - reminders.utc_time + reminders.utc_time, + reminders.status, + reminders.status_change_time FROM reminders LEFT JOIN channels ON channels.id = reminders.channel_id WHERE uid = ?", @@ -591,9 +599,12 @@ pub async fn delete_reminder( reminder: Json, pool: &State>, ) -> JsonResult { - match sqlx::query!("UPDATE reminders SET `status` = 'deleted' WHERE uid = ?", reminder.uid) - .execute(pool.inner()) - .await + match sqlx::query!( + "UPDATE reminders SET `status` = 'deleted', `status_change_time` = NOW() WHERE uid = ?", + reminder.uid + ) + .execute(pool.inner()) + .await { Ok(_) => Ok(json!({})), @@ -604,35 +615,3 @@ pub async fn delete_reminder( } } } - -#[get("/api/guild//errors")] -pub async fn get_reminder_errors( - id: u64, - cookies: &CookieJar<'_>, - serenity_context: &State, - pool: &State>, -) -> JsonResult { - check_authorization!(cookies, serenity_context.inner(), id); - - sqlx::query_as_unchecked!( - ReminderError, - "SELECT - reminders.status, - reminders.utc_time, - reminders.name, - reminders.uid, - reminders.channel_id AS channel - FROM reminders - LEFT JOIN channels ON channels.id = reminders.channel_id - WHERE (`status` = 'failed' OR reminders.channel_id IS NULL) AND reminders.guild_id = (SELECT id FROM guilds WHERE guild = ?)", - id - ) - .fetch_all(pool.inner()) - .await - .map(|r| Ok(json!(r))) - .unwrap_or_else(|e| { - warn!("Failed to complete SQL query: {:?}", e); - - json_err!("Could not load reminders") - }) -} diff --git a/web/src/routes/dashboard/mod.rs b/web/src/routes/dashboard/mod.rs index c56089a..65ff743 100644 --- a/web/src/routes/dashboard/mod.rs +++ b/web/src/routes/dashboard/mod.rs @@ -150,18 +150,8 @@ pub struct Reminder { uid: String, username: Option, utc_time: NaiveDateTime, -} - -#[derive(Serialize)] -pub struct ReminderError { - #[serde(with = "string")] - channel: u64, status: String, - #[serde(default = "name_default")] - name: String, - #[serde(default)] - uid: String, - utc_time: NaiveDateTime, + status_change_time: Option, } #[derive(Serialize, Deserialize)] @@ -574,7 +564,9 @@ pub async fn create_reminder( reminders.tts, reminders.uid, reminders.username, - reminders.utc_time + reminders.utc_time, + reminders.status, + reminders.status_change_time FROM reminders LEFT JOIN channels ON channels.id = reminders.channel_id WHERE uid = ?", diff --git a/web/static/css/style.css b/web/static/css/style.css index fb42aa9..e254b9c 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -688,6 +688,44 @@ li.highlight { /* END */ +div.reminderError { + margin: 10px; + padding: 14px; + background-color: #f5f5f5; + border-radius: 8px; +} + +div.reminderError .errorHead { + display: flex; + flex-direction: row; +} + +div.reminderError .errorIcon { + padding: 8px; + border-radius: 4px; + margin-right: 12px; +} + +div.reminderError .errorIcon.deleted { + background-color: #e7e5e4; +} + +div.reminderError .errorIcon.success { + background-color: #d9f99d; +} + +div.reminderError .errorIcon.errored { + background-color: #fecaca; +} + +div.reminderError .errorHead .reminderName { + font-size: 1rem; + display: flex; + flex-direction: column; + justify-content: center; + color: rgb(54, 54, 54); +} + /* other stuff */ .half-rem { diff --git a/web/static/js/main.js b/web/static/js/main.js index cd164c7..9aec630 100644 --- a/web/static/js/main.js +++ b/web/static/js/main.js @@ -57,7 +57,7 @@ function switch_pane(selector) { el.classList.add("is-hidden"); }); - document.getElementById(selector).classList.remove("is-hidden"); + document.querySelector(`*[data-name=${selector}]`).classList.remove("is-hidden"); } function update_select(sel) { @@ -735,16 +735,19 @@ document.addEventListener("DOMContentLoaded", async () => { }); } - const matches = window.location.href.match(/dashboard\/(\d+)/); + const matches = window.location.href.match( + /dashboard\/(\d+)(\/)?([a-zA-Z\-]+)?/ + ); if (matches) { let id = matches[1]; + let kind = matches[3]; let name = guildNames[id]; const event = new CustomEvent("guildSwitched", { detail: { guild_name: name, guild_id: id, - pane: "guild", + pane: kind, }, }); diff --git a/web/static/js/reminder_errors.js b/web/static/js/reminder_errors.js index ff12367..4affb99 100644 --- a/web/static/js/reminder_errors.js +++ b/web/static/js/reminder_errors.js @@ -1,16 +1,18 @@ function loadErrors() { - return fetch(`/dashboard/api/guild/${guildId()}/errors`).then((response) => - response.json() - ); + return fetch( + `/dashboard/api/guild/${guildId()}/reminders?status=deleted,sent,error` + ).then((response) => response.json()); } document.addEventListener("paneLoad", (ev) => { - if (ev.detail.pane !== "reminder-errors") { + if (ev.detail.pane !== "errors") { return; } // Load errors - loadErrors().then((res) => {}); + loadErrors().then((res) => { + console.log(res); + }); $loader.classList.add("is-hidden"); }); diff --git a/web/templates/dashboard.html.tera b/web/templates/dashboard.html.tera index 18585c8..1e12718 100644 --- a/web/templates/dashboard.html.tera +++ b/web/templates/dashboard.html.tera @@ -332,16 +332,16 @@

Press the to get started

-