Square off the rail header and add a back button
The rail header now sits in the standard 64px row with a hairline under it, matching the pane headers, and shows the full logo off the chat pages instead of the bare stone. A back button joins the conversation and settings headers (workspace pages only for the conversation). The rail's own back link and the dividers around it are switched off for now. Also renames the bot and workspace Delete group to Danger Zone, lifts the step budget to 50, and lets Logo render without its home link.
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
import Button from "@moduletrace/twui/layout/Button";
|
||||
import { ChevronLeft, type LucideIcon } from "lucide-react";
|
||||
|
||||
/**
|
||||
* Back Button
|
||||
*/
|
||||
export default function BackButton() {
|
||||
return (
|
||||
<Button
|
||||
title="Back Button"
|
||||
// variant="ghost"
|
||||
color="gray"
|
||||
onClick={() => {
|
||||
window.history.back();
|
||||
}}
|
||||
className="p-2! w-9 min-w-9 h-9"
|
||||
>
|
||||
<ChevronLeft size={20} />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -13,6 +13,7 @@ type Props = {
|
||||
* Only `className` applies: it sets the wordmark colour.
|
||||
*/
|
||||
text_props?: ComponentProps<typeof Span>;
|
||||
no_link?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -22,21 +23,30 @@ export default function Logo({
|
||||
icon_size = 36,
|
||||
text_props,
|
||||
text_wrapper_props,
|
||||
no_link,
|
||||
}: Props) {
|
||||
const content = (
|
||||
<Row className="gap-2.5! flex-nowrap items-center">
|
||||
<LogoIcon icon_size={icon_size} />
|
||||
<Stack
|
||||
{...text_wrapper_props}
|
||||
className={twMerge("gap-0!", text_wrapper_props?.className)}
|
||||
>
|
||||
<Wordmark
|
||||
height={Math.round(icon_size * 0.5)}
|
||||
className={text_props?.className}
|
||||
/>
|
||||
</Stack>
|
||||
</Row>
|
||||
);
|
||||
|
||||
if (no_link) {
|
||||
return content;
|
||||
}
|
||||
|
||||
return (
|
||||
<a href="/" className="logo-link">
|
||||
<Row className="gap-2.5! flex-nowrap items-center">
|
||||
<LogoIcon icon_size={icon_size} />
|
||||
<Stack
|
||||
{...text_wrapper_props}
|
||||
className={twMerge("gap-0!", text_wrapper_props?.className)}
|
||||
>
|
||||
<Wordmark
|
||||
height={Math.round(icon_size * 0.5)}
|
||||
className={text_props?.className}
|
||||
/>
|
||||
</Stack>
|
||||
</Row>
|
||||
{content}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -240,7 +240,7 @@ export const AppData = {
|
||||
/**
|
||||
* Model turns in one text run before it must stop and report.
|
||||
*/
|
||||
HarnessMaxToolSteps: 30,
|
||||
HarnessMaxToolSteps: 50,
|
||||
/**
|
||||
* Longest tool result handed back to the model.
|
||||
*/
|
||||
@@ -462,7 +462,8 @@ export const AppData = {
|
||||
/**
|
||||
* What a speech model reads when a voice is tried in the model settings.
|
||||
*/
|
||||
VoicePreviewText: "Hi there. This is how I'll sound when I read my replies aloud.",
|
||||
VoicePreviewText:
|
||||
"Hi there. This is how I'll sound when I read my replies aloud.",
|
||||
/**
|
||||
* Largest picture a bot or a workspace can have.
|
||||
*/
|
||||
|
||||
@@ -41,7 +41,7 @@ export const SettingsRecordSections: Record<
|
||||
{ title: "Models", url: "#models", Icon: CpuAi },
|
||||
{ title: "Workspaces", url: "#workspaces", Icon: Layers },
|
||||
{ title: "Computers", url: "#computers", Icon: Monitor },
|
||||
{ title: "Delete", url: "#delete", Icon: Trash2 },
|
||||
{ title: "Danger Zone", url: "#danger-zone", Icon: CircleAlert },
|
||||
],
|
||||
workspaces: [
|
||||
{ title: "Picture", url: "#picture", Icon: Image },
|
||||
@@ -49,7 +49,7 @@ export const SettingsRecordSections: Record<
|
||||
{ title: "Bots", url: "#bots", Icon: Bot },
|
||||
{ title: "Computers", url: "#computers", Icon: Monitor },
|
||||
{ title: "Models", url: "#models", Icon: CpuAi },
|
||||
{ title: "Delete", url: "#delete", Icon: Trash2 },
|
||||
{ title: "Danger Zone", url: "#danger-zone", Icon: CircleAlert },
|
||||
],
|
||||
users: [
|
||||
{ title: "Overview", url: "#overview", Icon: Info },
|
||||
|
||||
@@ -3,6 +3,8 @@ import path from "node:path";
|
||||
import { AppData } from "@/src/data/app-data";
|
||||
import AppError from "@/src/utils/app-error";
|
||||
|
||||
const empty_folder_text = `Empty Folder`;
|
||||
|
||||
type Params = {
|
||||
/**
|
||||
* The volume's absolute root (`resolveStorageSpec`'s `root`), already
|
||||
@@ -48,7 +50,10 @@ export type ResolvedStoragePath = {
|
||||
* the bot's tools); a caller that needs an existing file checks it
|
||||
* afterwards.
|
||||
*/
|
||||
export default function resolveStoragePath({ root, path: requested }: Params): ResolvedStoragePath {
|
||||
export default function resolveStoragePath({
|
||||
root,
|
||||
path: requested,
|
||||
}: Params): ResolvedStoragePath {
|
||||
const resolved_root = path.resolve(root);
|
||||
const raw = (requested || "").trim().replace(/\\/g, "/");
|
||||
|
||||
@@ -57,19 +62,26 @@ export default function resolveStoragePath({ root, path: requested }: Params): R
|
||||
}
|
||||
|
||||
if (raw.startsWith("/") || /^[a-zA-Z]:/.test(raw)) {
|
||||
throw new AppError(`Use a path inside the folder, not an absolute one.`);
|
||||
throw new AppError(
|
||||
`Use a path inside the folder, not an absolute one.`,
|
||||
);
|
||||
}
|
||||
|
||||
const segments = raw.split("/").map((segment) => segment.trim()).filter(Boolean);
|
||||
const segments = raw
|
||||
.split("/")
|
||||
.map((segment) => segment.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
for (const segment of segments) {
|
||||
if (segment == "." || segment == "..") {
|
||||
throw new AppError(`That path isn't valid: it must stay inside the folder.`);
|
||||
throw new AppError(empty_folder_text);
|
||||
}
|
||||
}
|
||||
|
||||
if (segments.length > AppData.BotFilePathMaxDepth) {
|
||||
throw new AppError(`That path is too deep: at most ${AppData.BotFilePathMaxDepth} folders.`);
|
||||
throw new AppError(
|
||||
`That path is too deep: at most ${AppData.BotFilePathMaxDepth} folders.`,
|
||||
);
|
||||
}
|
||||
|
||||
const relative = segments.join("/");
|
||||
@@ -81,7 +93,7 @@ export default function resolveStoragePath({ root, path: requested }: Params): R
|
||||
const absolute = path.join(resolved_root, ...segments);
|
||||
|
||||
if (!isInsideRoot({ root: resolved_root, absolute })) {
|
||||
throw new AppError(`That path isn't valid: it must stay inside the folder.`);
|
||||
throw new AppError(empty_folder_text);
|
||||
}
|
||||
|
||||
guardRealPath({ root: resolved_root, absolute });
|
||||
@@ -99,7 +111,9 @@ export default function resolveStoragePath({ root, path: requested }: Params): R
|
||||
* symlink (or any other reason).
|
||||
*/
|
||||
function guardRealPath({ root, absolute }: { root: string; absolute: string }) {
|
||||
const real_root = existsSync(root) ? realpathSync(root) : path.resolve(root);
|
||||
const real_root = existsSync(root)
|
||||
? realpathSync(root)
|
||||
: path.resolve(root);
|
||||
let probe = absolute;
|
||||
|
||||
while (!existsSync(probe)) {
|
||||
@@ -113,16 +127,25 @@ function guardRealPath({ root, absolute }: { root: string; absolute: string }) {
|
||||
const real_probe = realpathSync(probe);
|
||||
|
||||
if (!isInsideRoot({ root: real_root, absolute: real_probe })) {
|
||||
throw new AppError(`That path isn't valid: it must stay inside the folder.`);
|
||||
throw new AppError(empty_folder_text);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a path is inside a root, before it exists.
|
||||
*/
|
||||
export function isInsideRoot({ root, absolute }: { root: string; absolute: string }) {
|
||||
export function isInsideRoot({
|
||||
root,
|
||||
absolute,
|
||||
}: {
|
||||
root: string;
|
||||
absolute: string;
|
||||
}) {
|
||||
const resolved_root = path.resolve(root);
|
||||
const resolved = path.resolve(absolute);
|
||||
|
||||
return resolved == resolved_root || resolved.startsWith(`${resolved_root}${path.sep}`);
|
||||
return (
|
||||
resolved == resolved_root ||
|
||||
resolved.startsWith(`${resolved_root}${path.sep}`)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@ export default function RailBackLink({
|
||||
title = `Back to ${SiteData["SiteSlug"]}`,
|
||||
href = AdminHomePath,
|
||||
}: Props) {
|
||||
return null;
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={href}
|
||||
|
||||
@@ -3,6 +3,10 @@ import Link from "@moduletrace/twui/layout/Link";
|
||||
import Wordmark from "@/src/components/general/wordmark";
|
||||
import LogoIcon from "@/src/components/general/logo-icon";
|
||||
import RailAddButton from "./rail-add-button";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { useContext } from "react";
|
||||
import { AppContext } from "@/src/pages/__root";
|
||||
import Logo from "@/src/components/general/logo";
|
||||
|
||||
type Props = {
|
||||
/**
|
||||
@@ -12,14 +16,36 @@ type Props = {
|
||||
};
|
||||
|
||||
export default function RailHeader({ show_add = true }: Props) {
|
||||
const { pageProps } = useContext(AppContext);
|
||||
|
||||
return (
|
||||
<Row className="h-[72px] shrink-0 gap-0! flex-nowrap items-start justify-center @rail:justify-between px-0 @rail:pl-5 @rail:pr-6 pt-5">
|
||||
<Row
|
||||
className={twMerge(
|
||||
"h-16 shrink-0 gap-0! flex-nowrap justify-center @rail:justify-between",
|
||||
"px-0 @rail:pl-5 @rail:pr-6 py-1 border-0 border-b border-solid border-hairline",
|
||||
"mb-6 w-full",
|
||||
)}
|
||||
>
|
||||
<Link
|
||||
href="/admin/bots"
|
||||
className="border-0 mt-1 rounded-default-xs hover:opacity-100 focus-visible:ring-2 focus-visible:ring-ink focus-visible:ring-offset-2 focus-visible:ring-offset-paper"
|
||||
className={twMerge(
|
||||
"border-0 mt-1 rounded-default-xs hover:opacity-100 focus-visible:ring-2",
|
||||
"focus-visible:ring-ink focus-visible:ring-offset-2 focus-visible:ring-offset-paper",
|
||||
)}
|
||||
>
|
||||
<Wordmark height={15} className="hidden @rail:block" />
|
||||
<LogoIcon icon_size={28} img_props={{ className: "@rail:hidden" }} />
|
||||
{pageProps?.url?.pathname.startsWith(`/admin/bots/`) ? (
|
||||
<>
|
||||
<Wordmark height={15} className="hidden @rail:block" />
|
||||
<LogoIcon
|
||||
icon_size={28}
|
||||
img_props={{ className: "@rail:hidden" }}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Logo icon_size={28} no_link />
|
||||
</>
|
||||
)}
|
||||
</Link>
|
||||
{show_add ? <RailAddButton /> : null}
|
||||
</Row>
|
||||
|
||||
@@ -17,17 +17,24 @@ type Props = {
|
||||
export default function RailPagesList({ pathname }: Props) {
|
||||
const { pageProps } = useContext(AppContext);
|
||||
const current = pathname?.replace(/\/$/, "");
|
||||
const is_super_admin = isPassThroughUser({ user_types: pageProps?.user_types });
|
||||
const is_super_admin = isPassThroughUser({
|
||||
user_types: pageProps?.user_types,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<RailBackLink />
|
||||
<Divider className="shrink-0 mx-1 @rail:mx-3 my-2 w-auto border-hairline dark:border-hairline" />
|
||||
{RailPages.filter((page) => is_super_admin || !page.super_admin_only).map((page) => (
|
||||
{/* <Divider className="shrink-0 mx-1 @rail:mx-3 my-2 w-auto border-hairline dark:border-hairline" /> */}
|
||||
{RailPages.filter(
|
||||
(page) => is_super_admin || !page.super_admin_only,
|
||||
).map((page) => (
|
||||
<RailSettingsLink
|
||||
key={page.url}
|
||||
{...page}
|
||||
selected={current == page.url || Boolean(current?.startsWith(`${page.url}/`))}
|
||||
selected={
|
||||
current == page.url ||
|
||||
Boolean(current?.startsWith(`${page.url}/`))
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
|
||||
@@ -64,7 +64,8 @@ export default function RailSettingsAside({ pathname }: Props) {
|
||||
? Boolean(pageProps?.bot_harness_overview?.harnesses?.length)
|
||||
: record.kind == "workspaces"
|
||||
? Boolean(
|
||||
pageProps?.workspace_harness_overview?.harnesses?.length,
|
||||
pageProps?.workspace_harness_overview?.harnesses
|
||||
?.length,
|
||||
)
|
||||
: true;
|
||||
}
|
||||
@@ -101,9 +102,7 @@ export default function RailSettingsAside({ pathname }: Props) {
|
||||
|
||||
title = workspace.name || `Workspace ${workspace.id}`;
|
||||
subtitle = workspace.description || "A workspace";
|
||||
avatar = (
|
||||
<WorkspaceAvatar image_media_id={workspace.image_media_id} />
|
||||
);
|
||||
avatar = <WorkspaceAvatar image_media_id={workspace.image_media_id} />;
|
||||
} else if (record.kind == "users") {
|
||||
const person = pageProps?.target_person;
|
||||
|
||||
@@ -139,7 +138,7 @@ export default function RailSettingsAside({ pathname }: Props) {
|
||||
return (
|
||||
<>
|
||||
<RailBackLink title={back.title} href={back.href} />
|
||||
<Divider className="shrink-0 mx-1 @rail:mx-3 my-2 w-auto border-hairline dark:border-hairline" />
|
||||
{/* <Divider className="shrink-0 mx-1 @rail:mx-3 my-2 w-auto border-hairline dark:border-hairline" /> */}
|
||||
<RailSettingsRecordRow
|
||||
avatar={avatar}
|
||||
title={title}
|
||||
|
||||
@@ -25,7 +25,7 @@ export default function RailSettingsList({ pathname }: Props) {
|
||||
return (
|
||||
<>
|
||||
<RailBackLink />
|
||||
<Divider className="shrink-0 mx-1 @rail:mx-3 my-2 w-auto border-hairline dark:border-hairline" />
|
||||
{/* <Divider className="shrink-0 mx-1 @rail:mx-3 my-2 w-auto border-hairline dark:border-hairline" /> */}
|
||||
{SettingsPages.filter((page) =>
|
||||
page.can_view
|
||||
? page.can_view({ user_types: pageProps?.user_types })
|
||||
|
||||
@@ -29,7 +29,12 @@ type Props = {
|
||||
* page), the coordinator working in it, then the workspace's bots. Every
|
||||
* conversation here runs with the workspace's context.
|
||||
*/
|
||||
export default function RailWorkspaceList({ workspace, bots, selected_id, on_workspace_page }: Props) {
|
||||
export default function RailWorkspaceList({
|
||||
workspace,
|
||||
bots,
|
||||
selected_id,
|
||||
on_workspace_page,
|
||||
}: Props) {
|
||||
const { openLinkBots } = useContext(AddFlowContext);
|
||||
const coordinator = bots.find((bot) => bot.kind === "coordinator");
|
||||
const workers = bots.filter((bot) => bot.kind !== "coordinator");
|
||||
@@ -38,11 +43,16 @@ export default function RailWorkspaceList({ workspace, bots, selected_id, on_wor
|
||||
return (
|
||||
<>
|
||||
<RailBackLink
|
||||
title={on_workspace_page ? "All bots" : `Back to ${workspace.name}`}
|
||||
title={
|
||||
on_workspace_page ? "All bots" : `Back to ${workspace.name}`
|
||||
}
|
||||
href={on_workspace_page ? undefined : base}
|
||||
/>
|
||||
<Divider className="shrink-0 mx-1 @rail:mx-3 my-2 w-auto border-hairline dark:border-hairline" />
|
||||
<WorkspaceRow workspace={{ ...workspace, unread: 0 }} selected={on_workspace_page} />
|
||||
{/* <Divider className="shrink-0 mx-1 @rail:mx-3 my-2 w-auto border-hairline dark:border-hairline" /> */}
|
||||
<WorkspaceRow
|
||||
workspace={{ ...workspace, unread: 0 }}
|
||||
selected={on_workspace_page}
|
||||
/>
|
||||
{coordinator ? (
|
||||
<RailBotCard selected={coordinator.id === selected_id}>
|
||||
<CoordinatorRow
|
||||
@@ -61,7 +71,12 @@ export default function RailWorkspaceList({ workspace, bots, selected_id, on_wor
|
||||
</RailBotCard>
|
||||
) : null}
|
||||
<RailBotsSection
|
||||
action={<RailSectionAddButton title="Add bots to this workspace" onClick={openLinkBots} />}
|
||||
action={
|
||||
<RailSectionAddButton
|
||||
title="Add bots to this workspace"
|
||||
onClick={openLinkBots}
|
||||
/>
|
||||
}
|
||||
title="Workspace bots"
|
||||
workers={workers}
|
||||
selected_id={on_workspace_page ? undefined : selected_id}
|
||||
|
||||
@@ -53,4 +53,4 @@ export default function Header() {
|
||||
</Section>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,9 +79,6 @@ export default function App({
|
||||
return (
|
||||
<AppContext.Provider value={appContextObject}>
|
||||
<Layout
|
||||
// no_header_divider={
|
||||
// url?.pathname ? ["/"].includes(url.pathname) : undefined
|
||||
// }
|
||||
blank={
|
||||
url?.pathname
|
||||
? ["/auth/logout"].includes(url.pathname)
|
||||
|
||||
@@ -16,6 +16,7 @@ import { BotComputersContext } from "../(data)/bot-computers-context";
|
||||
import BotProfileModal from "./bot-profile-modal";
|
||||
import ConversationHeaderMenu from "./conversation-header-menu";
|
||||
import HeaderMetaItem from "./header-meta-item";
|
||||
import BackButton from "@/src/components/general/back-button";
|
||||
|
||||
type Props = {
|
||||
bot: BotWithLastMessage;
|
||||
@@ -48,9 +49,12 @@ export default function ConversationHeader({
|
||||
? computer.name || `Computer ${computer.id}`
|
||||
: null;
|
||||
|
||||
const is_workspace = pageProps?.url?.pathname.includes(`/workspaces/`);
|
||||
|
||||
return (
|
||||
<Row className="h-16 shrink-0 gap-2! md:gap-3! flex-nowrap items-center px-3 md:px-6 border-0 border-b border-solid border-hairline">
|
||||
<RailDrawerButton />
|
||||
{is_workspace ? <BackButton /> : null}
|
||||
<BotProfileModal bot={bot} />
|
||||
<Stack className="min-w-0 gap-0!">
|
||||
<H1 className="text-lg! leading-6! tracking-normal! font-bold text-ink truncate w-full line-clamp-1">
|
||||
|
||||
@@ -4,6 +4,8 @@ import Stack from "@moduletrace/twui/layout/Stack";
|
||||
import H1 from "@moduletrace/twui/layout/H1";
|
||||
import Span from "@moduletrace/twui/layout/Span";
|
||||
import RailDrawerButton from "@/src/layouts/bots/(partials)/rail-drawer-button";
|
||||
import BackButton from "@/src/components/general/back-button";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
@@ -25,13 +27,29 @@ export default function SettingsPaneHeader({
|
||||
action,
|
||||
}: Props) {
|
||||
return (
|
||||
<Row className="h-16 shrink-0 gap-2! md:gap-3! flex-nowrap items-center px-3 md:px-6 border-0 border-b border-solid border-hairline">
|
||||
<Row
|
||||
className={twMerge(
|
||||
"h-16 shrink-0 gap-2! md:gap-3! flex-nowrap items-center px-3",
|
||||
"md:px-6 border-0 border-b border-solid border-hairline",
|
||||
)}
|
||||
>
|
||||
<RailDrawerButton />
|
||||
<BackButton />
|
||||
<Stack className="min-w-0 gap-0!">
|
||||
<H1 className="text-lg! leading-6! tracking-normal! font-bold text-ink truncate">
|
||||
<H1
|
||||
className={twMerge(
|
||||
"text-lg! leading-6! tracking-normal! font-bold text-ink truncate line-clamp-1",
|
||||
"whitespace-nowrap w-full",
|
||||
)}
|
||||
>
|
||||
{title}
|
||||
</H1>
|
||||
<Span className="text-[13px] leading-[18px] text-ink-secondary truncate">
|
||||
<Span
|
||||
className={twMerge(
|
||||
"text-[13px] leading-[18px] text-ink-secondary truncate",
|
||||
"line-clamp-1 whitespace-nowrap w-full",
|
||||
)}
|
||||
>
|
||||
{description}
|
||||
</Span>
|
||||
</Stack>
|
||||
|
||||
@@ -45,9 +45,9 @@ export default function BotDeleteGroup({ bot }: Props) {
|
||||
|
||||
return (
|
||||
<SettingsGroup
|
||||
id="delete"
|
||||
title="Delete"
|
||||
description="Deletes the bot with its conversations, tasks and models."
|
||||
id="danger-zone"
|
||||
title="Danger Zone"
|
||||
description="Dangerous section. Actions here are irreversible. Tread carefully"
|
||||
>
|
||||
<Button
|
||||
title={`Delete ${name}`}
|
||||
|
||||
+16
-6
@@ -19,12 +19,20 @@ export default function WorkspaceDeleteGroup({ workspace }: Props) {
|
||||
const name = workspace.name || "this workspace";
|
||||
|
||||
const remove = async () => {
|
||||
if (!window.confirm(`Delete ${name} and every conversation in it? Its bots and computers stay.`)) return;
|
||||
if (
|
||||
!window.confirm(
|
||||
`Delete ${name} and every conversation in it? Its bots and computers stay.`,
|
||||
)
|
||||
)
|
||||
return;
|
||||
|
||||
setLoading(true);
|
||||
setError(undefined);
|
||||
|
||||
const res = await postAdminApi({ path: "workspaces/delete-workspace", body: { workspace_id: workspace.id } });
|
||||
const res = await postAdminApi({
|
||||
path: "workspaces/delete-workspace",
|
||||
body: { workspace_id: workspace.id },
|
||||
});
|
||||
|
||||
if (res.success) {
|
||||
window.location.href = "/admin/settings/workspaces";
|
||||
@@ -37,9 +45,9 @@ export default function WorkspaceDeleteGroup({ workspace }: Props) {
|
||||
|
||||
return (
|
||||
<SettingsGroup
|
||||
id="delete"
|
||||
title="Delete"
|
||||
description="Deletes the workspace and its conversations. Its bots and computers stay yours."
|
||||
id="danger-zone"
|
||||
title="Danger Zone"
|
||||
description="Irreversible actions live here. Be careful."
|
||||
>
|
||||
<Button
|
||||
title={`Delete ${name}`}
|
||||
@@ -54,7 +62,9 @@ export default function WorkspaceDeleteGroup({ workspace }: Props) {
|
||||
>
|
||||
Delete workspace
|
||||
</Button>
|
||||
<SettingsStatus status={error ? { error: true, msg: error } : undefined} />
|
||||
<SettingsStatus
|
||||
status={error ? { error: true, msg: error } : undefined}
|
||||
/>
|
||||
</SettingsGroup>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user