mirror of
https://github.com/docmost/docmost.git
synced 2026-08-23 02:32:12 +10:00
feat(ee): personal spaces (#2298)
* feat(ee): personal spaces * pref * feat: on-demand only * error notification
This commit is contained in:
@@ -19,5 +19,6 @@ export const Feature = {
|
||||
SHARING_CONTROLS: 'sharing:controls',
|
||||
TEMPLATES: 'templates',
|
||||
VIEWER_COMMENTS: 'comment:viewer',
|
||||
PERSONAL_SPACES: 'spaces:personal',
|
||||
DOCX_EXPORT: 'export:docx',
|
||||
} as const;
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { Modal, TextInput, Button, Group, Divider } from "@mantine/core";
|
||||
import { useForm } from "@mantine/form";
|
||||
import { zod4Resolver } from "mantine-form-zod-resolver";
|
||||
import { z } from "zod/v4";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { currentUserAtom } from "@/features/user/atoms/current-user-atom.ts";
|
||||
import { useCreatePersonalSpaceMutation } from "@/ee/personal-space/queries/personal-space-query";
|
||||
import { getSpaceUrl } from "@/lib/config.ts";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z.string().trim().min(2).max(100),
|
||||
});
|
||||
type FormValues = z.infer<typeof formSchema>;
|
||||
|
||||
type Props = {
|
||||
opened: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export default function CreatePersonalSpaceModal({ opened, onClose }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const currentUser = useAtomValue(currentUserAtom);
|
||||
const createMutation = useCreatePersonalSpaceMutation();
|
||||
|
||||
const firstName = (currentUser?.user?.name ?? "").trim().split(/\s+/)[0] || "";
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
validate: zod4Resolver(formSchema),
|
||||
initialValues: {
|
||||
name: firstName ? t("{{name}}'s space", { name: firstName }) : "",
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = async (values: FormValues) => {
|
||||
try {
|
||||
const createdSpace = await createMutation.mutateAsync({
|
||||
name: values.name,
|
||||
});
|
||||
onClose();
|
||||
navigate(getSpaceUrl(createdSpace.slug));
|
||||
} catch (err) {
|
||||
notifications.show({
|
||||
message: err?.response?.data?.message,
|
||||
color: "red",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
opened={opened}
|
||||
onClose={onClose}
|
||||
title={t("Create personal space")}
|
||||
closeButtonProps={{ "aria-label": t("Close") }}
|
||||
>
|
||||
<Divider size="xs" mb="md" />
|
||||
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||
<TextInput
|
||||
withAsterisk
|
||||
data-autofocus
|
||||
label={t("Space name")}
|
||||
variant="filled"
|
||||
errorProps={{ role: "alert" }}
|
||||
{...form.getInputProps("name")}
|
||||
/>
|
||||
<Group justify="flex-end" mt="md">
|
||||
<Button type="submit" loading={createMutation.isPending}>
|
||||
{t("Create")}
|
||||
</Button>
|
||||
</Group>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { Group, Text, Switch, Tooltip } from "@mantine/core";
|
||||
import { useAtom } from "jotai";
|
||||
import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { updateWorkspace } from "@/features/workspace/services/workspace-service.ts";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import { useHasFeature } from "@/ee/hooks/use-feature";
|
||||
import { Feature } from "@/ee/features";
|
||||
import { useUpgradeLabel } from "@/ee/hooks/use-upgrade-label.ts";
|
||||
|
||||
export default function PersonalSpacesSetting() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Group justify="space-between" wrap="nowrap" gap="xl">
|
||||
<div>
|
||||
<Text size="md">{t("Allow personal spaces")}</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
{t("Members can create their own personal space.")}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<PersonalSpacesToggle />
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
function PersonalSpacesToggle() {
|
||||
const { t } = useTranslation();
|
||||
const [workspace, setWorkspace] = useAtom(workspaceAtom);
|
||||
const [checked, setChecked] = useState(
|
||||
workspace?.settings?.spaces?.allowPersonal === true,
|
||||
);
|
||||
const hasPersonalSpaces = useHasFeature(Feature.PERSONAL_SPACES);
|
||||
const upgradeLabel = useUpgradeLabel();
|
||||
|
||||
const handleChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = event.currentTarget.checked;
|
||||
try {
|
||||
const updatedWorkspace = await updateWorkspace({
|
||||
allowPersonalSpaces: value,
|
||||
});
|
||||
setChecked(value);
|
||||
setWorkspace(updatedWorkspace);
|
||||
} catch (err) {
|
||||
notifications.show({
|
||||
message: err?.response?.data?.message,
|
||||
color: "red",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Tooltip label={upgradeLabel} disabled={hasPersonalSpaces} refProp="rootRef">
|
||||
<Switch
|
||||
checked={checked}
|
||||
onChange={handleChange}
|
||||
disabled={!hasPersonalSpaces}
|
||||
aria-label={t("Toggle allow personal spaces")}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import {
|
||||
useMutation,
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
UseQueryResult,
|
||||
} from "@tanstack/react-query";
|
||||
import { ISpace } from "@/features/space/types/space.types";
|
||||
import {
|
||||
createPersonalSpace,
|
||||
getPersonalSpace,
|
||||
} from "@/ee/personal-space/services/personal-space-service";
|
||||
|
||||
export function usePersonalSpaceQuery(
|
||||
enabled: boolean,
|
||||
): UseQueryResult<ISpace | null, Error> {
|
||||
return useQuery({
|
||||
queryKey: ["personal-space"],
|
||||
queryFn: () => getPersonalSpace(),
|
||||
enabled,
|
||||
staleTime: 5 * 60 * 1000,
|
||||
});
|
||||
}
|
||||
|
||||
export function useCreatePersonalSpaceMutation() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<ISpace, Error, { name?: string }>({
|
||||
mutationFn: (data) => createPersonalSpace(data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["personal-space"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["spaces"] });
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import api from "@/lib/api-client";
|
||||
import { ISpace } from "@/features/space/types/space.types";
|
||||
|
||||
export async function getPersonalSpace(): Promise<ISpace | null> {
|
||||
const req = await api.post<ISpace | null>("/personal-space/info", {});
|
||||
return req.data;
|
||||
}
|
||||
|
||||
export async function createPersonalSpace(data: {
|
||||
name?: string;
|
||||
}): Promise<ISpace> {
|
||||
const req = await api.post<ISpace>("/personal-space/create", data);
|
||||
return req.data;
|
||||
}
|
||||
Reference in New Issue
Block a user