mirror of
https://github.com/docmost/docmost.git
synced 2026-06-22 09:01:37 +10:00
feat(ee): personal spaces (#2298)
* feat(ee): personal spaces * pref * feat: on-demand only * error notification
This commit is contained in:
@@ -1087,5 +1087,11 @@
|
||||
"Added {{name}} to favorites": "Added {{name}} to favorites",
|
||||
"Removed {{name}} from favorites": "Removed {{name}} from favorites",
|
||||
"Page menu for {{name}}": "Page menu for {{name}}",
|
||||
"Create subpage of {{name}}": "Create subpage of {{name}}"
|
||||
"Create subpage of {{name}}": "Create subpage of {{name}}",
|
||||
"Allow personal spaces": "Allow personal spaces",
|
||||
"Members can create their own personal space.": "Members can create their own personal space.",
|
||||
"Toggle allow personal spaces": "Toggle allow personal spaces",
|
||||
"Create personal space": "Create personal space",
|
||||
"Personal space": "Personal space",
|
||||
"{{name}}'s space": "{{name}}'s space"
|
||||
}
|
||||
|
||||
@@ -15,9 +15,16 @@ import {
|
||||
IconMoon,
|
||||
IconSettings,
|
||||
IconSun,
|
||||
IconUser,
|
||||
IconUserCircle,
|
||||
IconUsers,
|
||||
} from "@tabler/icons-react";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
import { getSpaceUrl } from "@/lib/config.ts";
|
||||
import { useHasFeature } from "@/ee/hooks/use-feature";
|
||||
import { Feature } from "@/ee/features";
|
||||
import { usePersonalSpaceQuery } from "@/ee/personal-space/queries/personal-space-query";
|
||||
import CreatePersonalSpaceModal from "@/ee/personal-space/components/create-personal-space-modal";
|
||||
import { useAtom } from "jotai";
|
||||
import { currentUserAtom } from "@/features/user/atoms/current-user-atom.ts";
|
||||
import { Link } from "react-router-dom";
|
||||
@@ -36,11 +43,20 @@ export default function TopMenu() {
|
||||
const user = currentUser?.user;
|
||||
const workspace = currentUser?.workspace;
|
||||
|
||||
const hasPersonalSpaces = useHasFeature(Feature.PERSONAL_SPACES);
|
||||
const settingEnabled = workspace?.settings?.spaces?.allowPersonal === true;
|
||||
const { data: personalSpace } = usePersonalSpaceQuery(hasPersonalSpaces);
|
||||
const [
|
||||
createOpened,
|
||||
{ open: openCreate, close: closeCreate },
|
||||
] = useDisclosure(false);
|
||||
|
||||
if (!user || !workspace) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Menu width={250} position="bottom-end" withArrow shadow={"lg"}>
|
||||
<Menu.Target>
|
||||
<UnstyledButton>
|
||||
@@ -115,6 +131,26 @@ export default function TopMenu() {
|
||||
{t("My preferences")}
|
||||
</Menu.Item>
|
||||
|
||||
{personalSpace ? (
|
||||
<Menu.Item
|
||||
component={Link}
|
||||
to={getSpaceUrl(personalSpace.slug)}
|
||||
leftSection={<IconUser size={16} />}
|
||||
>
|
||||
{t("Personal space")}
|
||||
</Menu.Item>
|
||||
) : (
|
||||
hasPersonalSpaces &&
|
||||
settingEnabled && (
|
||||
<Menu.Item
|
||||
onClick={openCreate}
|
||||
leftSection={<IconUser size={16} />}
|
||||
>
|
||||
{t("Create personal space")}
|
||||
</Menu.Item>
|
||||
)
|
||||
)}
|
||||
|
||||
<Menu.Sub>
|
||||
<Menu.Sub.Target>
|
||||
<Menu.Sub.Item leftSection={<IconBrightnessFilled size={16} />}>
|
||||
@@ -160,5 +196,8 @@ export default function TopMenu() {
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
|
||||
<CreatePersonalSpaceModal opened={createOpened} onClose={closeCreate} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -17,8 +17,8 @@ const formSchema = z.object({
|
||||
.min(2)
|
||||
.max(100)
|
||||
.regex(
|
||||
/^[a-zA-Z0-9]+$/,
|
||||
"Space slug must be alphanumeric. No special characters",
|
||||
/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/,
|
||||
"Space slug must start with a letter or number and may contain hyphens and underscores",
|
||||
),
|
||||
description: z.string().max(500),
|
||||
});
|
||||
|
||||
@@ -15,8 +15,8 @@ const formSchema = z.object({
|
||||
.min(2)
|
||||
.max(100)
|
||||
.regex(
|
||||
/^[a-zA-Z0-9]+$/,
|
||||
"Space slug must be alphanumeric. No special characters",
|
||||
/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/,
|
||||
"Space slug must start with a letter or number and may contain hyphens and underscores",
|
||||
),
|
||||
});
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ export interface ISpace {
|
||||
description: string;
|
||||
logo?: string;
|
||||
slug: string;
|
||||
isPersonal?: boolean;
|
||||
hostname: string;
|
||||
creatorId: string;
|
||||
createdAt: Date;
|
||||
|
||||
@@ -28,6 +28,7 @@ export interface IWorkspace {
|
||||
trashRetentionDays?: number;
|
||||
restrictApiToAdmins?: boolean;
|
||||
allowMemberTemplates?: boolean;
|
||||
allowPersonalSpaces?: boolean;
|
||||
isScimEnabled?: boolean;
|
||||
}
|
||||
|
||||
@@ -36,6 +37,7 @@ export interface IWorkspaceSettings {
|
||||
sharing?: IWorkspaceSharingSettings;
|
||||
api?: IWorkspaceApiSettings;
|
||||
templates?: IWorkspaceTemplateSettings;
|
||||
spaces?: IWorkspaceSpaceSettings;
|
||||
}
|
||||
|
||||
export interface IWorkspaceApiSettings {
|
||||
@@ -57,6 +59,10 @@ export interface IWorkspaceTemplateSettings {
|
||||
allowMemberTemplates?: boolean;
|
||||
}
|
||||
|
||||
export interface IWorkspaceSpaceSettings {
|
||||
allowPersonal?: boolean;
|
||||
}
|
||||
|
||||
export interface ICreateInvite {
|
||||
role: string;
|
||||
emails: string[];
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Helmet } from "react-helmet-async";
|
||||
import ManageHostname from "@/ee/components/manage-hostname.tsx";
|
||||
import { Divider } from "@mantine/core";
|
||||
import AllowMemberTemplates from "@/ee/security/components/allow-member-templates.tsx";
|
||||
import PersonalSpacesSetting from "@/ee/personal-space/components/personal-spaces-setting.tsx";
|
||||
|
||||
export default function WorkspaceSettings() {
|
||||
const { t } = useTranslation();
|
||||
@@ -22,6 +23,9 @@ export default function WorkspaceSettings() {
|
||||
<Divider my="md" />
|
||||
<AllowMemberTemplates />
|
||||
|
||||
<Divider my="md" />
|
||||
<PersonalSpacesSetting />
|
||||
|
||||
{isCloud() && (
|
||||
<>
|
||||
<Divider my="md" />
|
||||
|
||||
@@ -25,3 +25,11 @@
|
||||
.mantine-Input-input[data-variant="default"] {
|
||||
border-color: var(--mantine-color-gray-6);
|
||||
}
|
||||
|
||||
/* better contrast for disabled inputs */
|
||||
.mantine-Input-input:disabled,
|
||||
.mantine-Input-input[data-disabled] {
|
||||
opacity: 0.7;
|
||||
color: var(--mantine-color-dimmed);
|
||||
-webkit-text-fill-color: var(--mantine-color-dimmed);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ export const Feature = {
|
||||
VIEWER_COMMENTS: 'comment:viewer',
|
||||
TEMPLATES: 'templates',
|
||||
PDF_EXPORT: 'export:pdf',
|
||||
PERSONAL_SPACES: 'spaces:personal',
|
||||
DOCX_EXPORT: 'export:docx',
|
||||
} as const;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
IsAlphanumeric,
|
||||
IsOptional,
|
||||
IsString,
|
||||
Matches,
|
||||
MaxLength,
|
||||
MinLength,
|
||||
} from 'class-validator';
|
||||
@@ -20,6 +20,9 @@ export class CreateSpaceDto {
|
||||
|
||||
@MinLength(2)
|
||||
@MaxLength(100)
|
||||
@IsAlphanumeric()
|
||||
@Matches(/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/, {
|
||||
message:
|
||||
'Space slug must start with a letter or number and may contain hyphens and underscores',
|
||||
})
|
||||
slug: string;
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ export class SpaceService {
|
||||
workspaceId: string,
|
||||
createSpaceDto: CreateSpaceDto,
|
||||
trx?: KyselyTransaction,
|
||||
options?: { isPersonal?: boolean },
|
||||
): Promise<Space> {
|
||||
let space = null;
|
||||
|
||||
@@ -59,6 +60,7 @@ export class SpaceService {
|
||||
workspaceId,
|
||||
createSpaceDto,
|
||||
trx,
|
||||
options,
|
||||
);
|
||||
|
||||
await this.spaceMemberService.addUserToSpace(
|
||||
@@ -81,6 +83,7 @@ export class SpaceService {
|
||||
after: {
|
||||
name: space.name,
|
||||
slug: space.slug,
|
||||
...(space.isPersonal ? { isPersonal: true } : {}),
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -93,6 +96,7 @@ export class SpaceService {
|
||||
workspaceId: string,
|
||||
createSpaceDto: CreateSpaceDto,
|
||||
trx?: KyselyTransaction,
|
||||
options?: { isPersonal?: boolean },
|
||||
): Promise<Space> {
|
||||
const slugExists = await this.spaceRepo.slugExists(
|
||||
createSpaceDto.slug,
|
||||
@@ -112,6 +116,7 @@ export class SpaceService {
|
||||
creatorId: userId,
|
||||
workspaceId: workspaceId,
|
||||
slug: createSpaceDto.slug,
|
||||
isPersonal: options?.isPersonal ?? false,
|
||||
},
|
||||
trx,
|
||||
);
|
||||
|
||||
@@ -57,4 +57,8 @@ export class UpdateWorkspaceDto extends PartialType(CreateWorkspaceDto) {
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
allowMemberTemplates: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
allowPersonalSpaces: boolean;
|
||||
}
|
||||
|
||||
@@ -333,7 +333,8 @@ export class WorkspaceService {
|
||||
typeof updateWorkspaceDto.mcpEnabled !== 'undefined' ||
|
||||
typeof updateWorkspaceDto.restrictApiToAdmins !== 'undefined' ||
|
||||
typeof updateWorkspaceDto.allowMemberTemplates !== 'undefined' ||
|
||||
typeof updateWorkspaceDto.isScimEnabled !== 'undefined'
|
||||
typeof updateWorkspaceDto.isScimEnabled !== 'undefined' ||
|
||||
typeof updateWorkspaceDto.allowPersonalSpaces !== 'undefined'
|
||||
) {
|
||||
const ws = await this.db
|
||||
.selectFrom('workspaces')
|
||||
@@ -361,6 +362,18 @@ export class WorkspaceService {
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof updateWorkspaceDto.allowPersonalSpaces !== 'undefined') {
|
||||
if (
|
||||
!this.licenseCheckService.hasFeature(
|
||||
ws.licenseKey,
|
||||
Feature.PERSONAL_SPACES,
|
||||
ws.plan,
|
||||
)
|
||||
) {
|
||||
throw new ForbiddenException('This feature requires a valid license');
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
typeof updateWorkspaceDto.disablePublicSharing !== 'undefined' ||
|
||||
typeof updateWorkspaceDto.trashRetentionDays !== 'undefined' ||
|
||||
@@ -500,6 +513,20 @@ export class WorkspaceService {
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof updateWorkspaceDto.allowPersonalSpaces !== 'undefined') {
|
||||
const prev = settingsBefore?.spaces?.allowPersonal ?? false;
|
||||
if (prev !== updateWorkspaceDto.allowPersonalSpaces) {
|
||||
before.allowPersonalSpaces = prev;
|
||||
after.allowPersonalSpaces = updateWorkspaceDto.allowPersonalSpaces;
|
||||
}
|
||||
await this.workspaceRepo.updateSpaceSettings(
|
||||
workspaceId,
|
||||
'allowPersonal',
|
||||
updateWorkspaceDto.allowPersonalSpaces,
|
||||
trx,
|
||||
);
|
||||
}
|
||||
|
||||
delete updateWorkspaceDto.restrictApiToAdmins;
|
||||
delete updateWorkspaceDto.aiSearch;
|
||||
delete updateWorkspaceDto.generativeAi;
|
||||
@@ -507,6 +534,7 @@ export class WorkspaceService {
|
||||
delete updateWorkspaceDto.mcpEnabled;
|
||||
delete updateWorkspaceDto.allowMemberTemplates;
|
||||
delete updateWorkspaceDto.aiChat;
|
||||
delete updateWorkspaceDto.allowPersonalSpaces;
|
||||
|
||||
await this.workspaceRepo.updateWorkspace(
|
||||
updateWorkspaceDto,
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Kysely, sql } from 'kysely';
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await db.schema
|
||||
.alterTable('spaces')
|
||||
.addColumn('is_personal', 'boolean', (col) =>
|
||||
col.notNull().defaultTo(false),
|
||||
)
|
||||
.execute();
|
||||
|
||||
await sql`
|
||||
CREATE UNIQUE INDEX spaces_personal_creator_unique
|
||||
ON spaces (creator_id)
|
||||
WHERE is_personal = true AND deleted_at IS NULL
|
||||
`.execute(db);
|
||||
}
|
||||
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await db.schema
|
||||
.dropIndex('spaces_personal_creator_unique')
|
||||
.ifExists()
|
||||
.execute();
|
||||
await db.schema.alterTable('spaces').dropColumn('is_personal').execute();
|
||||
}
|
||||
@@ -57,6 +57,22 @@ export class SpaceRepo {
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
async findPersonalSpace(
|
||||
userId: string,
|
||||
workspaceId: string,
|
||||
trx?: KyselyTransaction,
|
||||
): Promise<Space | undefined> {
|
||||
const db = dbOrTx(this.db, trx);
|
||||
return db
|
||||
.selectFrom('spaces')
|
||||
.selectAll('spaces')
|
||||
.where('workspaceId', '=', workspaceId)
|
||||
.where('creatorId', '=', userId)
|
||||
.where('isPersonal', '=', true)
|
||||
.where('deletedAt', 'is', null)
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
async slugExists(
|
||||
slug: string,
|
||||
workspaceId: string,
|
||||
|
||||
@@ -251,4 +251,24 @@ export class WorkspaceRepo {
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
async updateSpaceSettings(
|
||||
workspaceId: string,
|
||||
prefKey: string,
|
||||
prefValue: string | boolean,
|
||||
trx?: KyselyTransaction,
|
||||
) {
|
||||
const db = dbOrTx(this.db, trx);
|
||||
return db
|
||||
.updateTable('workspaces')
|
||||
.set({
|
||||
settings: sql`COALESCE(settings, '{}'::jsonb)
|
||||
|| jsonb_build_object('spaces', COALESCE(settings->'spaces', '{}'::jsonb)
|
||||
|| jsonb_build_object('${sql.raw(prefKey)}', ${sql.lit(prefValue)}))`,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where('id', '=', workspaceId)
|
||||
.returning(this.baseFields)
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+1
@@ -322,6 +322,7 @@ export interface Spaces {
|
||||
deletedAt: Timestamp | null;
|
||||
description: string | null;
|
||||
id: Generated<string>;
|
||||
isPersonal: Generated<boolean>;
|
||||
logo: string | null;
|
||||
name: string | null;
|
||||
settings: Json | null;
|
||||
|
||||
+1
-1
Submodule apps/server/src/ee updated: 7afa4e9f2b...efd4d10dfd
Reference in New Issue
Block a user