mirror of
https://github.com/Nutomic/ibis.git
synced 2024-11-25 23:01:08 +00:00
wip2
This commit is contained in:
parent
8033a00b4e
commit
68f7f31b8e
10 changed files with 152 additions and 94 deletions
|
@ -3,20 +3,34 @@ use crate::{
|
||||||
frontend::{
|
frontend::{
|
||||||
app::GlobalState,
|
app::GlobalState,
|
||||||
article_link,
|
article_link,
|
||||||
|
article_title,
|
||||||
components::instance_follow_button::InstanceFollowButton,
|
components::instance_follow_button::InstanceFollowButton,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use leptos::*;
|
use leptos::*;
|
||||||
use leptos_router::*;
|
use leptos_router::*;
|
||||||
|
|
||||||
|
pub enum ActiveTab {
|
||||||
|
Read,
|
||||||
|
History,
|
||||||
|
Edit,
|
||||||
|
Actions,
|
||||||
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn ArticleNav(article: Resource<Option<String>, ArticleView>) -> impl IntoView {
|
pub fn ArticleNav(
|
||||||
|
article: Resource<Option<String>, ArticleView>,
|
||||||
|
active_tab: ActiveTab,
|
||||||
|
) -> impl IntoView {
|
||||||
|
let tab_classes = tab_classes(&active_tab);
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<Suspense>
|
<Suspense>
|
||||||
{move || {
|
{move || {
|
||||||
article
|
article
|
||||||
.get()
|
.get()
|
||||||
.map(|article_| {
|
.map(|article_| {
|
||||||
|
let title = article_title(&article_.article);
|
||||||
let instance = create_local_resource(
|
let instance = create_local_resource(
|
||||||
move || article_.article.instance_id,
|
move || article_.article.instance_id,
|
||||||
move |instance_id| async move {
|
move |instance_id| async move {
|
||||||
|
@ -32,8 +46,12 @@ pub fn ArticleNav(article: Resource<Option<String>, ArticleView>) -> impl IntoVi
|
||||||
let protected = article_.article.protected;
|
let protected = article_.article.protected;
|
||||||
view! {
|
view! {
|
||||||
<div role="tablist" class="tabs tabs-lifted">
|
<div role="tablist" class="tabs tabs-lifted">
|
||||||
<A class="tab tab-active" href=article_link.clone()>"Read"</A>
|
<A class=tab_classes.read href=article_link.clone()>
|
||||||
<A class="tab" href=format!("{article_link}/history")>"History"</A>
|
"Read"
|
||||||
|
</A>
|
||||||
|
<A class=tab_classes.history href=format!("{article_link}/history")>
|
||||||
|
"History"
|
||||||
|
</A>
|
||||||
<Show when=move || {
|
<Show when=move || {
|
||||||
global_state
|
global_state
|
||||||
.with(|state| {
|
.with(|state| {
|
||||||
|
@ -46,12 +64,19 @@ pub fn ArticleNav(article: Resource<Option<String>, ArticleView>) -> impl IntoVi
|
||||||
&& can_edit_article(&article_.article, is_admin).is_ok()
|
&& can_edit_article(&article_.article, is_admin).is_ok()
|
||||||
})
|
})
|
||||||
}>
|
}>
|
||||||
<A class="tab" href=format!("{article_link}/edit")>"Edit"</A>
|
<A class=tab_classes.edit href=format!("{article_link}/edit")>
|
||||||
|
"Edit"
|
||||||
|
</A>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when=move || {
|
<Show when=move || {
|
||||||
global_state.with(|state| state.my_profile.is_some())
|
global_state.with(|state| state.my_profile.is_some())
|
||||||
}>
|
}>
|
||||||
<A class="tab" href=format!("{article_link_}/actions")>"Actions"</A>
|
<A
|
||||||
|
class=tab_classes.actions
|
||||||
|
href=format!("{article_link_}/actions")
|
||||||
|
>
|
||||||
|
"Actions"
|
||||||
|
</A>
|
||||||
{instance
|
{instance
|
||||||
.get()
|
.get()
|
||||||
.map(|i| {
|
.map(|i| {
|
||||||
|
@ -61,6 +86,9 @@ pub fn ArticleNav(article: Resource<Option<String>, ArticleView>) -> impl IntoVi
|
||||||
})}
|
})}
|
||||||
|
|
||||||
</Show>
|
</Show>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row">
|
||||||
|
<h1 class="text-4xl font-bold font-serif my-4 grow max-w-fit">{title}</h1>
|
||||||
<Show when=move || protected>
|
<Show when=move || protected>
|
||||||
<span title="Article can only be edited by local admins">
|
<span title="Article can only be edited by local admins">
|
||||||
"Protected"
|
"Protected"
|
||||||
|
@ -74,3 +102,28 @@ pub fn ArticleNav(article: Resource<Option<String>, ArticleView>) -> impl IntoVi
|
||||||
</Suspense>
|
</Suspense>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct ActiveTabClasses {
|
||||||
|
read: &'static str,
|
||||||
|
history: &'static str,
|
||||||
|
edit: &'static str,
|
||||||
|
actions: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tab_classes(active_tab: &ActiveTab) -> ActiveTabClasses {
|
||||||
|
const TAB_INACTIVE: &str = "tab";
|
||||||
|
const TAB_ACTIVE: &str = "tab tab-active";
|
||||||
|
let mut classes = ActiveTabClasses {
|
||||||
|
read: TAB_INACTIVE,
|
||||||
|
history: TAB_INACTIVE,
|
||||||
|
edit: TAB_INACTIVE,
|
||||||
|
actions: TAB_INACTIVE,
|
||||||
|
};
|
||||||
|
match active_tab {
|
||||||
|
ActiveTab::Read => classes.read = TAB_ACTIVE,
|
||||||
|
ActiveTab::History => classes.history = TAB_ACTIVE,
|
||||||
|
ActiveTab::Edit => classes.edit = TAB_ACTIVE,
|
||||||
|
ActiveTab::Actions => classes.actions = TAB_ACTIVE,
|
||||||
|
}
|
||||||
|
classes
|
||||||
|
}
|
||||||
|
|
|
@ -24,12 +24,13 @@ pub fn CredentialsForm(
|
||||||
error
|
error
|
||||||
.get()
|
.get()
|
||||||
.map(|err| {
|
.map(|err| {
|
||||||
view! { <p style="color:red;">{err}</p> }
|
view! { <p class="alert alert-error">{err}</p> }
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
class="input"
|
||||||
required
|
required
|
||||||
placeholder="Username"
|
placeholder="Username"
|
||||||
prop:disabled=move || disabled.get()
|
prop:disabled=move || disabled.get()
|
||||||
|
@ -46,6 +47,7 @@ pub fn CredentialsForm(
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
|
class="input"
|
||||||
required
|
required
|
||||||
placeholder="Password"
|
placeholder="Password"
|
||||||
prop:disabled=move || disabled.get()
|
prop:disabled=move || disabled.get()
|
||||||
|
@ -69,6 +71,7 @@ pub fn CredentialsForm(
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
|
class="btn"
|
||||||
prop:disabled=move || button_is_disabled.get()
|
prop:disabled=move || button_is_disabled.get()
|
||||||
on:click=move |_| dispatch_action()
|
on:click=move |_| dispatch_action()
|
||||||
>
|
>
|
||||||
|
|
|
@ -47,7 +47,8 @@ pub fn Nav() -> impl IntoView {
|
||||||
if !query.is_empty() {
|
if !query.is_empty() {
|
||||||
navigate(&format!("/search?query={query}"), Default::default());
|
navigate(&format!("/search?query={query}"), Default::default());
|
||||||
}
|
}
|
||||||
}>
|
}
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="input input-secondary input-bordered input-xs w-full rounded"
|
class="input input-secondary input-bordered input-xs w-full rounded"
|
||||||
|
@ -84,14 +85,13 @@ pub fn Nav() -> impl IntoView {
|
||||||
let profile_link = format!("/user/{}", my_profile.person.username);
|
let profile_link = format!("/user/{}", my_profile.person.username);
|
||||||
view! {
|
view! {
|
||||||
<p class="self-center pb-2">
|
<p class="self-center pb-2">
|
||||||
"Logged in as "
|
"Logged in as " <a class="link" href=profile_link>
|
||||||
<a class="link"
|
|
||||||
href=profile_link
|
|
||||||
>
|
|
||||||
{my_profile.person.username}
|
{my_profile.person.username}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
<button class="btn" on:click=move |_| logout_action.dispatch(())>Logout</button>
|
<button class="btn" on:click=move |_| logout_action.dispatch(())>
|
||||||
|
Logout
|
||||||
|
</button>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -52,7 +52,11 @@ fn user_link(person: &DbPerson) -> impl IntoView {
|
||||||
extract_domain(&person.ap_id)
|
extract_domain(&person.ap_id)
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
view! { <a href=creator_path>{user_title(person)}</a> }
|
view! {
|
||||||
|
<a class="link" href=creator_path>
|
||||||
|
{user_title(person)}
|
||||||
|
</a>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_date_time(date_time: DateTime<Utc>) -> String {
|
fn render_date_time(date_time: DateTime<Utc>) -> String {
|
||||||
|
|
|
@ -3,8 +3,7 @@ use crate::{
|
||||||
frontend::{
|
frontend::{
|
||||||
app::GlobalState,
|
app::GlobalState,
|
||||||
article_link,
|
article_link,
|
||||||
article_title,
|
components::article_nav::{ActiveTab, ArticleNav},
|
||||||
components::article_nav::ArticleNav,
|
|
||||||
pages::article_resource,
|
pages::article_resource,
|
||||||
DbArticle,
|
DbArticle,
|
||||||
},
|
},
|
||||||
|
@ -52,7 +51,7 @@ pub fn ArticleActions() -> impl IntoView {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
view! {
|
view! {
|
||||||
<ArticleNav article=article />
|
<ArticleNav article=article active_tab=ActiveTab::Actions />
|
||||||
<Suspense fallback=|| {
|
<Suspense fallback=|| {
|
||||||
view! { "Loading..." }
|
view! { "Loading..." }
|
||||||
}>
|
}>
|
||||||
|
@ -61,16 +60,14 @@ pub fn ArticleActions() -> impl IntoView {
|
||||||
.get()
|
.get()
|
||||||
.map(|article| {
|
.map(|article| {
|
||||||
view! {
|
view! {
|
||||||
<div class="item-view">
|
<div>
|
||||||
<h1>{article_title(&article.article)}</h1>
|
|
||||||
{move || {
|
{move || {
|
||||||
error
|
error
|
||||||
.get()
|
.get()
|
||||||
.map(|err| {
|
.map(|err| {
|
||||||
view! { <p style="color:red;">{err}</p> }
|
view! { <p class="alert">{err}</p> }
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
|
|
||||||
<Show when=move || {
|
<Show when=move || {
|
||||||
global_state
|
global_state
|
||||||
.with(|state| {
|
.with(|state| {
|
||||||
|
@ -81,14 +78,19 @@ pub fn ArticleActions() -> impl IntoView {
|
||||||
.unwrap_or_default() && article.article.local
|
.unwrap_or_default() && article.article.local
|
||||||
})
|
})
|
||||||
}>
|
}>
|
||||||
<button on:click=move |_| {
|
<button
|
||||||
|
class="btn btn-secondary"
|
||||||
|
on:click=move |_| {
|
||||||
protect_action
|
protect_action
|
||||||
.dispatch((article.article.id, article.article.protected))
|
.dispatch((article.article.id, article.article.protected))
|
||||||
}>Toggle Article Protection</button>
|
}
|
||||||
|
>
|
||||||
|
Toggle Article Protection
|
||||||
|
</button>
|
||||||
<p>"Protect a local article so that only admins can edit it"</p>
|
<p>"Protect a local article so that only admins can edit it"</p>
|
||||||
</Show>
|
</Show> <Show when=move || !article.article.local>
|
||||||
<Show when=move || !article.article.local>
|
|
||||||
<input
|
<input
|
||||||
|
class="input"
|
||||||
placeholder="New Title"
|
placeholder="New Title"
|
||||||
on:keyup=move |ev: ev::KeyboardEvent| {
|
on:keyup=move |ev: ev::KeyboardEvent| {
|
||||||
let val = event_target_value(&ev);
|
let val = event_target_value(&ev);
|
||||||
|
@ -97,6 +99,7 @@ pub fn ArticleActions() -> impl IntoView {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
class="btn"
|
||||||
disabled=move || new_title.get().is_empty()
|
disabled=move || new_title.get().is_empty()
|
||||||
on:click=move |_| {
|
on:click=move |_| {
|
||||||
fork_action.dispatch((article.article.id, new_title.get()))
|
fork_action.dispatch((article.article.id, new_title.get()))
|
||||||
|
|
|
@ -2,8 +2,7 @@ use crate::{
|
||||||
common::{ApiConflict, ArticleView, EditArticleForm},
|
common::{ApiConflict, ArticleView, EditArticleForm},
|
||||||
frontend::{
|
frontend::{
|
||||||
app::GlobalState,
|
app::GlobalState,
|
||||||
article_title,
|
components::article_nav::{ActiveTab, ArticleNav},
|
||||||
components::article_nav::ArticleNav,
|
|
||||||
markdown::render_markdown,
|
markdown::render_markdown,
|
||||||
pages::article_resource,
|
pages::article_resource,
|
||||||
},
|
},
|
||||||
|
@ -103,7 +102,7 @@ pub fn EditArticle() -> impl IntoView {
|
||||||
);
|
);
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<ArticleNav article=article />
|
<ArticleNav article=article active_tab=ActiveTab::Edit />
|
||||||
<Show
|
<Show
|
||||||
when=move || edit_response.get() == EditResponse::Success
|
when=move || edit_response.get() == EditResponse::Success
|
||||||
fallback=move || {
|
fallback=move || {
|
||||||
|
@ -126,7 +125,6 @@ pub fn EditArticle() -> impl IntoView {
|
||||||
view! {
|
view! {
|
||||||
// set initial text, otherwise submit with no changes results in empty text
|
// set initial text, otherwise submit with no changes results in empty text
|
||||||
<div>
|
<div>
|
||||||
<h1>{article_title(&article.article)}</h1>
|
|
||||||
{move || {
|
{move || {
|
||||||
edit_error
|
edit_error
|
||||||
.get()
|
.get()
|
||||||
|
@ -134,7 +132,6 @@ pub fn EditArticle() -> impl IntoView {
|
||||||
view! { <p style="color:red;">{err}</p> }
|
view! { <p style="color:red;">{err}</p> }
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
|
|
||||||
<textarea
|
<textarea
|
||||||
id="edit-article-textarea"
|
id="edit-article-textarea"
|
||||||
class="textarea textarea-bordered textarea-primary min-w-full"
|
class="textarea textarea-bordered textarea-primary min-w-full"
|
||||||
|
@ -147,19 +144,19 @@ pub fn EditArticle() -> impl IntoView {
|
||||||
>
|
>
|
||||||
{article.article.text.clone()}
|
{article.article.text.clone()}
|
||||||
</textarea>
|
</textarea>
|
||||||
<button class="btn" on:click=move |_| {
|
<button
|
||||||
set_show_preview.update(|s| *s = !*s)
|
class="btn"
|
||||||
}>Preview</button>
|
on:click=move |_| { set_show_preview.update(|s| *s = !*s) }
|
||||||
<Show when=move || { show_preview.get() }>
|
>
|
||||||
|
Preview
|
||||||
|
</button> <Show when=move || { show_preview.get() }>
|
||||||
<div class="preview" inner_html=move || preview.get()></div>
|
<div class="preview" inner_html=move || preview.get()></div>
|
||||||
</Show>
|
</Show> <div>
|
||||||
<div>
|
|
||||||
<a href="https://commonmark.org/help/" target="blank_">
|
<a href="https://commonmark.org/help/" target="blank_">
|
||||||
Markdown
|
Markdown
|
||||||
</a>
|
</a>
|
||||||
" formatting is supported"
|
" formatting is supported"
|
||||||
</div>
|
</div> <div class="inputs">
|
||||||
<div class="inputs">
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="input input-secondary"
|
class="input input-secondary"
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
use crate::frontend::{
|
use crate::frontend::{
|
||||||
article_title,
|
components::article_nav::{ActiveTab, ArticleNav},
|
||||||
components::article_nav::ArticleNav,
|
|
||||||
extract_domain,
|
extract_domain,
|
||||||
pages::article_resource,
|
pages::article_resource,
|
||||||
render_date_time,
|
render_date_time,
|
||||||
|
@ -13,7 +12,7 @@ pub fn ArticleHistory() -> impl IntoView {
|
||||||
let article = article_resource();
|
let article = article_resource();
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<ArticleNav article=article />
|
<ArticleNav article=article active_tab=ActiveTab::History />
|
||||||
<Suspense fallback=|| {
|
<Suspense fallback=|| {
|
||||||
view! { "Loading..." }
|
view! { "Loading..." }
|
||||||
}>
|
}>
|
||||||
|
@ -22,9 +21,8 @@ pub fn ArticleHistory() -> impl IntoView {
|
||||||
.get()
|
.get()
|
||||||
.map(|article| {
|
.map(|article| {
|
||||||
view! {
|
view! {
|
||||||
<div class="item-view">
|
<div>
|
||||||
<h1>{article_title(&article.article)}</h1>
|
<ul class="list-disc">
|
||||||
|
|
||||||
{article
|
{article
|
||||||
.edits
|
.edits
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
@ -39,13 +37,14 @@ pub fn ArticleHistory() -> impl IntoView {
|
||||||
view! {
|
view! {
|
||||||
<li>
|
<li>
|
||||||
{render_date_time(edit.edit.created)}": "
|
{render_date_time(edit.edit.created)}": "
|
||||||
<a href=path>{edit.edit.summary}</a> " by "
|
<a class="link link-primary" href=path>
|
||||||
{user_link(&edit.creator)}
|
{edit.edit.summary}
|
||||||
|
</a> " by " {user_link(&edit.creator)}
|
||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>()}
|
.collect::<Vec<_>>()}
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -21,19 +21,19 @@ pub fn ListArticles() -> impl IntoView {
|
||||||
);
|
);
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<h1>Most recently edited Articles</h1>
|
<h1 class="text-4xl font-bold font-serif my-4">Most recently edited Articles</h1>
|
||||||
<Suspense fallback=|| view! { "Loading..." }>
|
<Suspense fallback=|| view! { "Loading..." }>
|
||||||
<fieldset on:input=move |ev| {
|
<fieldset on:input=move |ev| {
|
||||||
let val = ev.target().unwrap().unchecked_into::<web_sys::HtmlInputElement>().id();
|
let val = ev.target().unwrap().unchecked_into::<web_sys::HtmlInputElement>().id();
|
||||||
let is_local_only = val == "only-local";
|
let is_local_only = val == "only-local";
|
||||||
set_only_local.update(|p| *p = is_local_only);
|
set_only_local.update(|p| *p = is_local_only);
|
||||||
}>
|
}>
|
||||||
<input type="radio" name="listing-type" id="only-local" />
|
<input type="radio" name="listing-type" class="radio radio-primary" />
|
||||||
<label for="only-local">Only Local</label>
|
<label for="only-local">Only Local</label>
|
||||||
<input type="radio" name="listing-type" id="all" checked />
|
<input type="radio" name="listing-type" class="radio radio-primary" checked />
|
||||||
<label for="all">All</label>
|
<label for="all">All</label>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<ul>
|
<ul class="list-disc">
|
||||||
{move || {
|
{move || {
|
||||||
articles
|
articles
|
||||||
.get()
|
.get()
|
||||||
|
@ -42,7 +42,7 @@ pub fn ListArticles() -> impl IntoView {
|
||||||
.map(|a| {
|
.map(|a| {
|
||||||
view! {
|
view! {
|
||||||
<li>
|
<li>
|
||||||
<a href=article_link(&a)>{article_title(&a)}</a>
|
<a class="link link-secondary" href=article_link(&a)>{article_title(&a)}</a>
|
||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
use crate::frontend::{
|
use crate::frontend::{
|
||||||
article_title,
|
components::article_nav::{ActiveTab, ArticleNav},
|
||||||
components::article_nav::ArticleNav,
|
|
||||||
markdown::render_markdown,
|
markdown::render_markdown,
|
||||||
pages::article_resource,
|
pages::article_resource,
|
||||||
};
|
};
|
||||||
|
@ -11,7 +10,7 @@ pub fn ReadArticle() -> impl IntoView {
|
||||||
let article = article_resource();
|
let article = article_resource();
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<ArticleNav article=article />
|
<ArticleNav article=article active_tab=ActiveTab::Read />
|
||||||
<Suspense fallback=|| {
|
<Suspense fallback=|| {
|
||||||
view! { "Loading..." }
|
view! { "Loading..." }
|
||||||
}>
|
}>
|
||||||
|
@ -21,10 +20,10 @@ pub fn ReadArticle() -> impl IntoView {
|
||||||
.get()
|
.get()
|
||||||
.map(|article| {
|
.map(|article| {
|
||||||
view! {
|
view! {
|
||||||
<div class="prose prose-slate">
|
<div
|
||||||
<h1 class="slate">{article_title(&article.article)}</h1>
|
class="prose prose-slate"
|
||||||
<div inner_html=render_markdown(&article.article.text)></div>
|
inner_html=render_markdown(&article.article.text)
|
||||||
</div>
|
></div>
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::frontend::{
|
use crate::frontend::{
|
||||||
components::article_nav::ArticleNav,
|
components::article_nav::{ActiveTab, ArticleNav},
|
||||||
pages::article_resource,
|
pages::article_resource,
|
||||||
render_date_time,
|
render_date_time,
|
||||||
user_link,
|
user_link,
|
||||||
|
@ -13,7 +13,7 @@ pub fn EditDiff() -> impl IntoView {
|
||||||
let article = article_resource();
|
let article = article_resource();
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<ArticleNav article=article />
|
<ArticleNav article=article active_tab=ActiveTab::Edit />
|
||||||
<Suspense fallback=|| {
|
<Suspense fallback=|| {
|
||||||
view! { "Loading..." }
|
view! { "Loading..." }
|
||||||
}>
|
}>
|
||||||
|
|
Loading…
Reference in a new issue