feat: Add team deletion flow for cloud-hosted (#5717)
This commit is contained in:
@@ -32,7 +32,7 @@ function Home() {
|
||||
void pins.fetchPage();
|
||||
}, [pins]);
|
||||
|
||||
const canManageTeam = usePolicy(team).manage;
|
||||
const can = usePolicy(team);
|
||||
|
||||
return (
|
||||
<Scene
|
||||
@@ -49,7 +49,7 @@ function Home() {
|
||||
>
|
||||
{!ui.languagePromptDismissed && <LanguagePrompt />}
|
||||
<Heading>{t("Home")}</Heading>
|
||||
<PinnedDocuments pins={pins.home} canUpdate={canManageTeam} />
|
||||
<PinnedDocuments pins={pins.home} canUpdate={can.update} />
|
||||
<Documents>
|
||||
<Tabs>
|
||||
<Tab to="/home" exact>
|
||||
|
||||
@@ -37,7 +37,13 @@ export default function Notices() {
|
||||
Please use a Google Workspaces account instead.
|
||||
</Trans>
|
||||
)}
|
||||
{notice === "maximum-teams" && (
|
||||
{notice === "pending-deletion" && (
|
||||
<Trans>
|
||||
The workspace associated with your user is scheduled for deletion and
|
||||
cannot at accessed at this time.
|
||||
</Trans>
|
||||
)}
|
||||
{notice === "maximum-reached" && (
|
||||
<Trans>
|
||||
The workspace you authenticated with is not authorized on this
|
||||
installation. Try another?
|
||||
|
||||
@@ -20,18 +20,22 @@ import Switch from "~/components/Switch";
|
||||
import Text from "~/components/Text";
|
||||
import env from "~/env";
|
||||
import useCurrentTeam from "~/hooks/useCurrentTeam";
|
||||
import usePolicy from "~/hooks/usePolicy";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import useToasts from "~/hooks/useToasts";
|
||||
import isCloudHosted from "~/utils/isCloudHosted";
|
||||
import TeamDelete from "../TeamDelete";
|
||||
import ImageInput from "./components/ImageInput";
|
||||
import SettingRow from "./components/SettingRow";
|
||||
|
||||
function Details() {
|
||||
const { auth, ui } = useStores();
|
||||
const { auth, dialogs, ui } = useStores();
|
||||
const { showToast } = useToasts();
|
||||
const { t } = useTranslation();
|
||||
const team = useCurrentTeam();
|
||||
const theme = useTheme();
|
||||
const can = usePolicy(team);
|
||||
|
||||
const form = useRef<HTMLFormElement>(null);
|
||||
const [accent, setAccent] = useState<null | undefined | string>(
|
||||
team.preferences?.customTheme?.accent
|
||||
@@ -125,6 +129,14 @@ function Details() {
|
||||
[showToast, t]
|
||||
);
|
||||
|
||||
const showDeleteWorkspace = () => {
|
||||
dialogs.openModal({
|
||||
title: t("Delete workspace"),
|
||||
content: <TeamDelete onSubmit={dialogs.closeAllModals} />,
|
||||
isCentered: true,
|
||||
});
|
||||
};
|
||||
|
||||
const onSelectCollection = React.useCallback(async (value: string) => {
|
||||
const defaultCollectionId = value === "home" ? null : value;
|
||||
setDefaultCollectionId(defaultCollectionId);
|
||||
@@ -222,6 +234,7 @@ function Details() {
|
||||
</SettingRow>
|
||||
{team.avatarUrl && (
|
||||
<SettingRow
|
||||
border={false}
|
||||
name={TeamPreference.PublicBranding}
|
||||
label={t("Public branding")}
|
||||
description={t(
|
||||
@@ -287,6 +300,28 @@ function Details() {
|
||||
<Button type="submit" disabled={auth.isSaving || !isValid}>
|
||||
{auth.isSaving ? `${t("Saving")}…` : t("Save")}
|
||||
</Button>
|
||||
|
||||
{can.delete && (
|
||||
<>
|
||||
<p> </p>
|
||||
|
||||
<Heading as="h2">{t("Danger")}</Heading>
|
||||
<SettingRow
|
||||
name="delete"
|
||||
border={false}
|
||||
label={t("Delete workspace")}
|
||||
description={t(
|
||||
"You can delete this entire workspace including collections, documents, and users."
|
||||
)}
|
||||
>
|
||||
<span>
|
||||
<Button onClick={showDeleteWorkspace} neutral>
|
||||
{t("Delete workspace")}…
|
||||
</Button>
|
||||
</span>
|
||||
</SettingRow>
|
||||
</>
|
||||
)}
|
||||
</form>
|
||||
</Scene>
|
||||
</ThemeProvider>
|
||||
|
||||
@@ -184,7 +184,7 @@ function Members() {
|
||||
</Flex>
|
||||
<PeopleTable
|
||||
data={data}
|
||||
canManage={can.manage}
|
||||
canManage={can.update}
|
||||
isLoading={isLoading}
|
||||
page={page}
|
||||
pageSize={limit}
|
||||
|
||||
@@ -47,6 +47,7 @@ function Preferences() {
|
||||
dialogs.openModal({
|
||||
title: t("Delete account"),
|
||||
content: <UserDelete />,
|
||||
isCentered: true,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -131,8 +132,7 @@ function Preferences() {
|
||||
/>
|
||||
</SettingRow>
|
||||
|
||||
<p> </p>
|
||||
|
||||
<Heading as="h2">{t("Danger")}</Heading>
|
||||
<SettingRow
|
||||
name="delete"
|
||||
label={t("Delete account")}
|
||||
|
||||
@@ -70,7 +70,7 @@ function Shares() {
|
||||
<Scene title={t("Shared Links")} icon={<LinkIcon />}>
|
||||
<Heading>{t("Shared Links")}</Heading>
|
||||
|
||||
{can.manage && !canShareDocuments && (
|
||||
{can.update && !canShareDocuments && (
|
||||
<>
|
||||
<Notice icon={<WarningIcon />}>
|
||||
{t("Sharing is currently disabled.")}{" "}
|
||||
@@ -95,7 +95,7 @@ function Shares() {
|
||||
|
||||
<SharesTable
|
||||
data={data}
|
||||
canManage={can.manage}
|
||||
canManage={can.update}
|
||||
isLoading={isLoading}
|
||||
page={page}
|
||||
pageSize={limit}
|
||||
|
||||
122
app/scenes/TeamDelete.tsx
Normal file
122
app/scenes/TeamDelete.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useTranslation, Trans } from "react-i18next";
|
||||
import Button from "~/components/Button";
|
||||
import Flex from "~/components/Flex";
|
||||
import Input from "~/components/Input";
|
||||
import Text from "~/components/Text";
|
||||
import env from "~/env";
|
||||
import useCurrentTeam from "~/hooks/useCurrentTeam";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import useToasts from "~/hooks/useToasts";
|
||||
|
||||
type FormData = {
|
||||
code: string;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
onSubmit: () => void;
|
||||
};
|
||||
|
||||
function TeamDelete({ onSubmit }: Props) {
|
||||
const [isWaitingCode, setWaitingCode] = React.useState(false);
|
||||
const { auth } = useStores();
|
||||
const { showToast } = useToasts();
|
||||
const team = useCurrentTeam();
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
register,
|
||||
handleSubmit: formHandleSubmit,
|
||||
formState,
|
||||
} = useForm<FormData>();
|
||||
|
||||
const handleRequestDelete = React.useCallback(
|
||||
async (ev: React.SyntheticEvent) => {
|
||||
ev.preventDefault();
|
||||
|
||||
try {
|
||||
await auth.requestDeleteTeam();
|
||||
setWaitingCode(true);
|
||||
} catch (error) {
|
||||
showToast(error.message, {
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
},
|
||||
[auth, showToast]
|
||||
);
|
||||
|
||||
const handleSubmit = React.useCallback(
|
||||
async (data: FormData) => {
|
||||
try {
|
||||
await auth.deleteTeam(data);
|
||||
await auth.logout();
|
||||
onSubmit();
|
||||
} catch (error) {
|
||||
showToast(error.message, {
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
},
|
||||
[auth, onSubmit, showToast]
|
||||
);
|
||||
|
||||
const inputProps = register("code", {
|
||||
required: true,
|
||||
});
|
||||
const appName = env.APP_NAME;
|
||||
const workspaceName = team.name;
|
||||
|
||||
return (
|
||||
<Flex column>
|
||||
<form onSubmit={formHandleSubmit(handleSubmit)}>
|
||||
{isWaitingCode ? (
|
||||
<>
|
||||
<Text type="secondary">
|
||||
<Trans>
|
||||
A confirmation code has been sent to your email address, please
|
||||
enter the code below to permanantly destroy this workspace.
|
||||
</Trans>
|
||||
</Text>
|
||||
<Input
|
||||
placeholder={t("Confirmation code")}
|
||||
autoComplete="off"
|
||||
autoFocus
|
||||
maxLength={8}
|
||||
required
|
||||
{...inputProps}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Text type="secondary">
|
||||
<Trans>
|
||||
Deleting the <strong>{{ workspaceName }}</strong> workspace will
|
||||
destroy all collections, documents, users, and associated data.
|
||||
You will be immediately logged out of {{ appName }}.
|
||||
</Trans>
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
{env.EMAIL_ENABLED && !isWaitingCode ? (
|
||||
<Button type="submit" onClick={handleRequestDelete} neutral>
|
||||
{t("Continue")}…
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={formState.isSubmitting || !formState.isValid}
|
||||
danger
|
||||
>
|
||||
{formState.isSubmitting
|
||||
? `${t("Deleting")}…`
|
||||
: t("Delete workspace")}
|
||||
</Button>
|
||||
)}
|
||||
</form>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(TeamDelete);
|
||||
@@ -30,7 +30,7 @@ function UserDelete() {
|
||||
ev.preventDefault();
|
||||
|
||||
try {
|
||||
await auth.requestDelete();
|
||||
await auth.requestDeleteUser();
|
||||
setWaitingCode(true);
|
||||
} catch (error) {
|
||||
showToast(error.message, {
|
||||
@@ -71,16 +71,8 @@ function UserDelete() {
|
||||
enter the code below to permanantly destroy your account.
|
||||
</Trans>
|
||||
</Text>
|
||||
<Text type="secondary">
|
||||
<Trans
|
||||
defaults="<em>Note:</em> Signing back in will cause a new account to be automatically reprovisioned."
|
||||
components={{
|
||||
em: <strong />,
|
||||
}}
|
||||
/>
|
||||
</Text>
|
||||
<Input
|
||||
placeholder="CODE"
|
||||
placeholder={t("Confirmation code")}
|
||||
autoComplete="off"
|
||||
autoFocus
|
||||
maxLength={8}
|
||||
@@ -105,10 +97,14 @@ function UserDelete() {
|
||||
{t("Continue")}…
|
||||
</Button>
|
||||
) : (
|
||||
<Button type="submit" disabled={formState.isSubmitting} danger>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={formState.isSubmitting || !formState.isValid}
|
||||
danger
|
||||
>
|
||||
{formState.isSubmitting
|
||||
? `${t("Deleting")}…`
|
||||
: t("Delete My Account")}
|
||||
: t("Delete my account")}
|
||||
</Button>
|
||||
)}
|
||||
</form>
|
||||
|
||||
Reference in New Issue
Block a user