More work on todo list support
This commit is contained in:
parent
9989ab3b35
commit
e128b9848f
@ -1,5 +1,4 @@
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { DateTime } from "luxon";
|
|
||||||
|
|
||||||
type UserInfo = {
|
type UserInfo = {
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -20,6 +20,7 @@ export function App() {
|
|||||||
<div class="columns is-gapless dashboard-frame">
|
<div class="columns is-gapless dashboard-frame">
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
<div class="column is-main-content">
|
<div class="column is-main-content">
|
||||||
|
<div style={{ margin: "0 12px 12px 12px" }}>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path={"/@me/reminders"} component={User}></Route>
|
<Route path={"/@me/reminders"} component={User}></Route>
|
||||||
<Route
|
<Route
|
||||||
@ -32,11 +33,11 @@ export function App() {
|
|||||||
></Route>
|
></Route>
|
||||||
<Route
|
<Route
|
||||||
path={"/:guild/todos"}
|
path={"/:guild/todos"}
|
||||||
component={
|
component={() => (
|
||||||
<Guild>
|
<Guild>
|
||||||
<GuildTodos />
|
<GuildTodos />
|
||||||
</Guild>
|
</Guild>
|
||||||
}
|
)}
|
||||||
></Route>
|
></Route>
|
||||||
<Route>
|
<Route>
|
||||||
<Welcome />
|
<Welcome />
|
||||||
@ -44,6 +45,7 @@ export function App() {
|
|||||||
</Switch>
|
</Switch>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</Router>
|
</Router>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
</FlashProvider>
|
</FlashProvider>
|
||||||
|
@ -37,12 +37,12 @@ export const GuildReminders = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!isFetched && <Loader />}
|
{!isFetched && <Loader />}
|
||||||
<div style={{ margin: "0 12px 12px 12px" }}>
|
|
||||||
<strong>Create Reminder</strong>
|
<strong>Create Reminder</strong>
|
||||||
<div id={"reminderCreator"}>
|
<div id={"reminderCreator"}>
|
||||||
<CreateReminder />
|
<CreateReminder />
|
||||||
</div>
|
</div>
|
||||||
<br></br>
|
<br />
|
||||||
<div class={"field"}>
|
<div class={"field"}>
|
||||||
<div class={"columns is-mobile"}>
|
<div class={"columns is-mobile"}>
|
||||||
<div class={"column"}>
|
<div class={"column"}>
|
||||||
@ -63,10 +63,7 @@ export const GuildReminders = () => {
|
|||||||
<option value={Sort.Name} selected={sort == Sort.Name}>
|
<option value={Sort.Name} selected={sort == Sort.Name}>
|
||||||
Name
|
Name
|
||||||
</option>
|
</option>
|
||||||
<option
|
<option value={Sort.Channel} selected={sort == Sort.Channel}>
|
||||||
value={Sort.Channel}
|
|
||||||
selected={sort == Sort.Channel}
|
|
||||||
>
|
|
||||||
Channel
|
Channel
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
@ -142,7 +139,6 @@ export const GuildReminders = () => {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,21 +1,52 @@
|
|||||||
import { useQuery, useQueryClient } from "react-query";
|
import { useQuery } from "react-query";
|
||||||
import { fetchGuildChannels, fetchGuildTodos } from "../../api";
|
import { fetchGuildChannels, fetchGuildTodos } from "../../api";
|
||||||
import { useState } from "preact/hooks";
|
|
||||||
import { useGuild } from "../App/useGuild";
|
import { useGuild } from "../App/useGuild";
|
||||||
|
import { Todo } from "../Todo";
|
||||||
|
import { Loader } from "../Loader";
|
||||||
|
import { CreateTodo } from "../Todo/CreateTodo";
|
||||||
|
|
||||||
export const GuildTodos = () => {
|
export const GuildTodos = () => {
|
||||||
const guild = useGuild();
|
const guild = useGuild();
|
||||||
|
|
||||||
const {
|
const { isFetched, data: guildTodos } = useQuery(fetchGuildTodos(guild));
|
||||||
isSuccess,
|
|
||||||
isFetching,
|
|
||||||
isFetched,
|
|
||||||
data: guildReminders,
|
|
||||||
} = useQuery(fetchGuildTodos(guild));
|
|
||||||
const { data: channels } = useQuery(fetchGuildChannels(guild));
|
const { data: channels } = useQuery(fetchGuildChannels(guild));
|
||||||
|
|
||||||
const [collapsed, setCollapsed] = useState(false);
|
if (!isFetched || !channels) {
|
||||||
const queryClient = useQueryClient();
|
return <Loader />;
|
||||||
|
}
|
||||||
|
|
||||||
return <></>;
|
const sortedTodos = guildTodos.sort((a, b) => (a.channel_id < b.channel_id ? 0 : 1));
|
||||||
|
let prevChannel;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<strong>Create Todo</strong>
|
||||||
|
<CreateTodo />
|
||||||
|
<br />
|
||||||
|
<strong>Todo list</strong>
|
||||||
|
{sortedTodos.map((todo) => {
|
||||||
|
if (prevChannel !== todo.channel_id) {
|
||||||
|
prevChannel = todo.channel_id;
|
||||||
|
if (todo.channel_id === null) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h2>Server Todos</h2>
|
||||||
|
<Todo todo={todo} key={todo.id} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const channel = channels.find((ch) => ch.id === todo.channel_id);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h2>#{channel.name} Todos</h2>
|
||||||
|
<Todo todo={todo} key={todo.id} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Todo todo={todo} key={todo.id} />;
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
5
reminder-dashboard/src/components/Guild/index.scss
Normal file
5
reminder-dashboard/src/components/Guild/index.scss
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
.page-links {
|
||||||
|
> * {
|
||||||
|
margin: 2px;
|
||||||
|
}
|
||||||
|
}
|
@ -4,6 +4,9 @@ import { GuildError } from "./GuildError";
|
|||||||
import { createPortal, PropsWithChildren } from "preact/compat";
|
import { createPortal, PropsWithChildren } from "preact/compat";
|
||||||
import { Import } from "../Import";
|
import { Import } from "../Import";
|
||||||
import { useGuild } from "../App/useGuild";
|
import { useGuild } from "../App/useGuild";
|
||||||
|
import { Link } from "wouter";
|
||||||
|
|
||||||
|
import "./index.scss";
|
||||||
|
|
||||||
export const Guild = ({ children }: PropsWithChildren) => {
|
export const Guild = ({ children }: PropsWithChildren) => {
|
||||||
const guild = useGuild();
|
const guild = useGuild();
|
||||||
@ -19,6 +22,23 @@ export const Guild = ({ children }: PropsWithChildren) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{importModal}
|
{importModal}
|
||||||
|
<div class="page-links">
|
||||||
|
<Link
|
||||||
|
class="button is-outlined is-success is-small"
|
||||||
|
href={`/${guild}/reminders`}
|
||||||
|
>
|
||||||
|
<span>Reminders</span>
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fa fa-chevron-right"></i>
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
<Link class="button is-outlined is-success is-small" href={`/${guild}/todos`}>
|
||||||
|
<span>Todo lists</span>
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fa fa-chevron-right"></i>
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
{children}
|
{children}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { useQuery } from "react-query";
|
import { useQuery } from "react-query";
|
||||||
import { useParams } from "wouter";
|
|
||||||
import { fetchGuildChannels } from "../../api";
|
import { fetchGuildChannels } from "../../api";
|
||||||
|
import { useGuild } from "../App/useGuild";
|
||||||
|
|
||||||
export const ChannelSelector = ({ channel, setChannel }) => {
|
export const ChannelSelector = ({ channel, setChannel }) => {
|
||||||
const { guild } = useParams();
|
const guild = useGuild();
|
||||||
const { isSuccess, data } = useQuery(fetchGuildChannels(guild));
|
const { isSuccess, data } = useQuery(fetchGuildChannels(guild));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -61,6 +61,7 @@ export const CreateReminder = () => {
|
|||||||
<ReminderContext.Provider value={[reminder, setReminder]}>
|
<ReminderContext.Provider value={[reminder, setReminder]}>
|
||||||
<div class={collapsed ? "reminderContent is-collapsed" : "reminderContent"}>
|
<div class={collapsed ? "reminderContent is-collapsed" : "reminderContent"}>
|
||||||
<TopBar
|
<TopBar
|
||||||
|
isCreating={true}
|
||||||
toggleCollapsed={() => {
|
toggleCollapsed={() => {
|
||||||
setCollapsed(!collapsed);
|
setCollapsed(!collapsed);
|
||||||
}}
|
}}
|
||||||
|
@ -34,6 +34,7 @@ export const EditReminder = ({ reminder: initialReminder, globalCollapse }: Prop
|
|||||||
id={`reminder-${reminder.uid.slice(0, 12)}`}
|
id={`reminder-${reminder.uid.slice(0, 12)}`}
|
||||||
>
|
>
|
||||||
<TopBar
|
<TopBar
|
||||||
|
isCreating={false}
|
||||||
toggleCollapsed={() => {
|
toggleCollapsed={() => {
|
||||||
setCollapsed(!collapsed);
|
setCollapsed(!collapsed);
|
||||||
}}
|
}}
|
||||||
|
@ -6,7 +6,7 @@ import { useCallback } from "preact/hooks";
|
|||||||
import { DateTime } from "luxon";
|
import { DateTime } from "luxon";
|
||||||
import { Name } from "../Name";
|
import { Name } from "../Name";
|
||||||
|
|
||||||
export const Guild = ({ toggleCollapsed }) => {
|
export const Guild = ({ toggleCollapsed, isCreating }) => {
|
||||||
const guild = useGuild();
|
const guild = useGuild();
|
||||||
const [reminder] = useReminder();
|
const [reminder] = useReminder();
|
||||||
|
|
||||||
@ -55,7 +55,7 @@ export const Guild = ({ toggleCollapsed }) => {
|
|||||||
<div class="columns is-mobile column reminder-topbar">
|
<div class="columns is-mobile column reminder-topbar">
|
||||||
{isSuccess && <div class="invert-collapses channel-bar">#{channelName(reminder)}</div>}
|
{isSuccess && <div class="invert-collapses channel-bar">#{channelName(reminder)}</div>}
|
||||||
<Name />
|
<Name />
|
||||||
<div class="time-bar">in {string}</div>
|
{!isCreating && <div class="time-bar">in {string}</div>}
|
||||||
<div class="hide-button-bar">
|
<div class="hide-button-bar">
|
||||||
<button class="button hide-box" onClick={toggleCollapsed}>
|
<button class="button hide-box" onClick={toggleCollapsed}>
|
||||||
<span class="is-sr-only">Hide reminder</span>
|
<span class="is-sr-only">Hide reminder</span>
|
||||||
|
@ -2,11 +2,11 @@ import { useGuild } from "../../App/useGuild";
|
|||||||
import { Guild } from "./Guild";
|
import { Guild } from "./Guild";
|
||||||
import { User } from "./User";
|
import { User } from "./User";
|
||||||
|
|
||||||
export const TopBar = ({ toggleCollapsed }) => {
|
export const TopBar = ({ toggleCollapsed, isCreating }) => {
|
||||||
const guild = useGuild();
|
const guild = useGuild();
|
||||||
|
|
||||||
if (guild) {
|
if (guild) {
|
||||||
return <Guild toggleCollapsed={toggleCollapsed} />;
|
return <Guild toggleCollapsed={toggleCollapsed} isCreating={isCreating} />;
|
||||||
} else {
|
} else {
|
||||||
return <User toggleCollapsed={toggleCollapsed} />;
|
return <User toggleCollapsed={toggleCollapsed} />;
|
||||||
}
|
}
|
||||||
|
@ -17,9 +17,6 @@ export const GuildEntry = ({ guild }: Props) => {
|
|||||||
? "is-active switch-pane"
|
? "is-active switch-pane"
|
||||||
: "switch-pane"
|
: "switch-pane"
|
||||||
}
|
}
|
||||||
data-pane="guild"
|
|
||||||
data-guild={guild.id}
|
|
||||||
data-name={guild.name}
|
|
||||||
href={`/${guild.id}/reminders`}
|
href={`/${guild.id}/reminders`}
|
||||||
>
|
>
|
||||||
<>
|
<>
|
||||||
|
@ -29,7 +29,6 @@ const SidebarContent = ({ guilds }: ContentProps) => {
|
|||||||
<li>
|
<li>
|
||||||
<Link
|
<Link
|
||||||
class={loc.startsWith("/@me") ? "is-active switch-pane" : "switch-pane"}
|
class={loc.startsWith("/@me") ? "is-active switch-pane" : "switch-pane"}
|
||||||
data-pane="guild"
|
|
||||||
href={"/@me/reminders"}
|
href={"/@me/reminders"}
|
||||||
>
|
>
|
||||||
<>
|
<>
|
||||||
|
31
reminder-dashboard/src/components/Todo/CreateTodo.tsx
Normal file
31
reminder-dashboard/src/components/Todo/CreateTodo.tsx
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { useQuery } from "react-query";
|
||||||
|
import { fetchGuildChannels } from "../../api";
|
||||||
|
import { useGuild } from "../App/useGuild";
|
||||||
|
|
||||||
|
export const CreateTodo = () => {
|
||||||
|
const guild = useGuild();
|
||||||
|
|
||||||
|
const { isSuccess, data: channels } = useQuery(fetchGuildChannels(guild));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="todo">
|
||||||
|
<textarea class="input todo-input" onInput={() => null} />
|
||||||
|
<div class="control has-icons-left">
|
||||||
|
<div class="select">
|
||||||
|
<select name="channel" class="channel-selector" onInput={() => {}}>
|
||||||
|
<option value="">(None)</option>
|
||||||
|
{isSuccess && channels.map((c) => <option value={c.id}>{c.name}</option>)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="icon is-small is-left">
|
||||||
|
<i class="fas fa-hashtag"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button onClick={() => null} class="button is-success save-btn">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fa fa-sparkles"></i>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
10
reminder-dashboard/src/components/Todo/index.scss
Normal file
10
reminder-dashboard/src/components/Todo/index.scss
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
.todo {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
margin: 6px 0;
|
||||||
|
|
||||||
|
> * {
|
||||||
|
margin: 0 3px;
|
||||||
|
}
|
||||||
|
}
|
@ -1,19 +1,21 @@
|
|||||||
import { Todo as TodoT } from "../../api";
|
import { Todo as TodoT } from "../../api";
|
||||||
|
|
||||||
|
import "./index.scss";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
todo: TodoT;
|
todo: TodoT;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Todo = ({ todo }: Props) => {
|
export const Todo = ({ todo }: Props) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div class="todo">
|
||||||
<textarea value={todo.value} onInput={() => null} />
|
<textarea class="input todo-input" value={todo.value} onInput={() => null} />
|
||||||
<button onClick={() => null} class="btn save-btn">
|
<button onClick={() => null} class="button is-success save-btn">
|
||||||
<span class="icon">
|
<span class="icon">
|
||||||
<i class="fa fa-save"></i>
|
<i class="fa fa-save"></i>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<button onClick={() => null} class="btn delete-btn">
|
<button onClick={() => null} class="button is-danger">
|
||||||
<span class="icon">
|
<span class="icon">
|
||||||
<i class="fa fa-trash"></i>
|
<i class="fa fa-trash"></i>
|
||||||
</span>
|
</span>
|
||||||
|
@ -45,4 +45,5 @@ lazy_static! {
|
|||||||
.map(|inner| inner.parse::<u32>().ok())
|
.map(|inner| inner.parse::<u32>().ok())
|
||||||
.flatten()
|
.flatten()
|
||||||
.unwrap_or(600);
|
.unwrap_or(600);
|
||||||
|
pub static ref SALT: String = env::var("SALT").unwrap();
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,46 @@ mod catchers;
|
|||||||
mod guards;
|
mod guards;
|
||||||
mod metrics;
|
mod metrics;
|
||||||
mod routes;
|
mod routes;
|
||||||
|
pub mod string {
|
||||||
|
use std::{fmt::Display, str::FromStr};
|
||||||
|
|
||||||
|
use serde::{de, Deserialize, Deserializer, Serializer};
|
||||||
|
|
||||||
|
pub fn serialize<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
T: Display,
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
serializer.collect_str(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
|
||||||
|
where
|
||||||
|
T: FromStr,
|
||||||
|
T::Err: Display,
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
String::deserialize(deserializer)?.parse().map_err(de::Error::custom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod string_opt {
|
||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
|
use serde::{Deserializer, Serializer};
|
||||||
|
|
||||||
|
pub fn serialize<T, S>(value: &Option<T>, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
T: Display,
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
if let Some(v) = value {
|
||||||
|
serializer.collect_str(v)
|
||||||
|
} else {
|
||||||
|
serializer.serialize_none()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
use std::{env, path::Path};
|
use std::{env, path::Path};
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
use log::warn;
|
||||||
use rocket::{
|
use rocket::{
|
||||||
delete, get,
|
delete, get,
|
||||||
http::CookieJar,
|
http::CookieJar,
|
||||||
@ -5,13 +6,23 @@ use rocket::{
|
|||||||
serde::json::{json, Json},
|
serde::json::{json, Json},
|
||||||
State,
|
State,
|
||||||
};
|
};
|
||||||
use serde::Deserialize;
|
use serde::{Deserialize, Serialize};
|
||||||
use serenity::prelude::Context;
|
use serenity::prelude::Context;
|
||||||
|
|
||||||
use crate::web::{check_authorization, guards::transaction::Transaction, routes::JsonResult};
|
use crate::web::{
|
||||||
|
check_authorization, guards::transaction::Transaction, routes::JsonResult, string_opt,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct CreateTodo {
|
pub struct CreateTodo {
|
||||||
|
channel_id: Option<u64>,
|
||||||
|
value: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct GetTodo {
|
||||||
|
id: u32,
|
||||||
|
#[serde(with = "string_opt")]
|
||||||
channel_id: Option<u64>,
|
channel_id: Option<u64>,
|
||||||
value: String,
|
value: String,
|
||||||
}
|
}
|
||||||
@ -38,7 +49,30 @@ pub async fn get_todo(
|
|||||||
) -> JsonResult {
|
) -> JsonResult {
|
||||||
check_authorization(cookies, ctx.inner(), id).await?;
|
check_authorization(cookies, ctx.inner(), id).await?;
|
||||||
|
|
||||||
Ok(json!([]))
|
let todos = sqlx::query_as!(
|
||||||
|
GetTodo,
|
||||||
|
"
|
||||||
|
SELECT
|
||||||
|
todos.id,
|
||||||
|
channels.channel AS channel_id,
|
||||||
|
value
|
||||||
|
FROM todos
|
||||||
|
INNER JOIN guilds
|
||||||
|
ON guilds.id = todos.guild_id
|
||||||
|
LEFT JOIN channels
|
||||||
|
ON channels.id = todos.channel_id
|
||||||
|
WHERE guilds.guild = ?
|
||||||
|
",
|
||||||
|
id
|
||||||
|
)
|
||||||
|
.fetch_all(transaction.executor())
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
warn!("Error fetching todos: {:?}", e);
|
||||||
|
json!({ "errors": vec!["Unknown error"] })
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(json!(todos))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[patch("/api/guild/<id>/todos")]
|
#[patch("/api/guild/<id>/todos")]
|
||||||
|
@ -29,7 +29,7 @@ use crate::web::{
|
|||||||
},
|
},
|
||||||
guards::transaction::Transaction,
|
guards::transaction::Transaction,
|
||||||
routes::JsonResult,
|
routes::JsonResult,
|
||||||
Error,
|
string, Error,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub mod api;
|
pub mod api;
|
||||||
@ -348,30 +348,6 @@ where
|
|||||||
Ok(Some(Option::deserialize(deserializer)?))
|
Ok(Some(Option::deserialize(deserializer)?))
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://github.com/serde-rs/json/issues/329#issuecomment-305608405
|
|
||||||
mod string {
|
|
||||||
use std::{fmt::Display, str::FromStr};
|
|
||||||
|
|
||||||
use serde::{de, Deserialize, Deserializer, Serializer};
|
|
||||||
|
|
||||||
pub fn serialize<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
|
|
||||||
where
|
|
||||||
T: Display,
|
|
||||||
S: Serializer,
|
|
||||||
{
|
|
||||||
serializer.collect_str(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
|
|
||||||
where
|
|
||||||
T: FromStr,
|
|
||||||
T::Err: Display,
|
|
||||||
D: Deserializer<'de>,
|
|
||||||
{
|
|
||||||
String::deserialize(deserializer)?.parse().map_err(de::Error::custom)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct DeleteReminder {
|
pub struct DeleteReminder {
|
||||||
uid: String,
|
uid: String,
|
||||||
|
Loading…
Reference in New Issue
Block a user