mirror of
https://github.com/docmost/docmost.git
synced 2025-11-13 17:12:38 +10:00
add full page width preference
This commit is contained in:
@ -1,5 +1,6 @@
|
|||||||
import { Avatar, Group, Menu, rem, UnstyledButton, Text } from "@mantine/core";
|
import { Group, Menu, UnstyledButton, Text } from "@mantine/core";
|
||||||
import {
|
import {
|
||||||
|
IconBrush,
|
||||||
IconChevronDown,
|
IconChevronDown,
|
||||||
IconLogout,
|
IconLogout,
|
||||||
IconSettings,
|
IconSettings,
|
||||||
@ -38,10 +39,7 @@ export default function TopMenu() {
|
|||||||
<Text fw={500} size="sm" lh={1} mr={3}>
|
<Text fw={500} size="sm" lh={1} mr={3}>
|
||||||
{workspace.name}
|
{workspace.name}
|
||||||
</Text>
|
</Text>
|
||||||
<IconChevronDown
|
<IconChevronDown size={16} />
|
||||||
style={{ width: rem(12), height: rem(12) }}
|
|
||||||
stroke={1.5}
|
|
||||||
/>
|
|
||||||
</Group>
|
</Group>
|
||||||
</UnstyledButton>
|
</UnstyledButton>
|
||||||
</Menu.Target>
|
</Menu.Target>
|
||||||
@ -51,12 +49,7 @@ export default function TopMenu() {
|
|||||||
<Menu.Item
|
<Menu.Item
|
||||||
component={Link}
|
component={Link}
|
||||||
to={APP_ROUTE.SETTINGS.WORKSPACE.GENERAL}
|
to={APP_ROUTE.SETTINGS.WORKSPACE.GENERAL}
|
||||||
leftSection={
|
leftSection={<IconSettings size={16} />}
|
||||||
<IconSettings
|
|
||||||
style={{ width: rem(16), height: rem(16) }}
|
|
||||||
stroke={1.5}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
Workspace settings
|
Workspace settings
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
@ -64,12 +57,7 @@ export default function TopMenu() {
|
|||||||
<Menu.Item
|
<Menu.Item
|
||||||
component={Link}
|
component={Link}
|
||||||
to={APP_ROUTE.SETTINGS.WORKSPACE.MEMBERS}
|
to={APP_ROUTE.SETTINGS.WORKSPACE.MEMBERS}
|
||||||
leftSection={
|
leftSection={<IconUsers size={16} />}
|
||||||
<IconUsers
|
|
||||||
style={{ width: rem(16), height: rem(16) }}
|
|
||||||
stroke={1.5}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
Manage members
|
Manage members
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
@ -98,27 +86,22 @@ export default function TopMenu() {
|
|||||||
<Menu.Item
|
<Menu.Item
|
||||||
component={Link}
|
component={Link}
|
||||||
to={APP_ROUTE.SETTINGS.ACCOUNT.PROFILE}
|
to={APP_ROUTE.SETTINGS.ACCOUNT.PROFILE}
|
||||||
leftSection={
|
leftSection={<IconUserCircle size={16} />}
|
||||||
<IconUserCircle
|
|
||||||
style={{ width: rem(16), height: rem(16) }}
|
|
||||||
stroke={1.5}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
My profile
|
My profile
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
|
||||||
|
<Menu.Item
|
||||||
|
component={Link}
|
||||||
|
to={APP_ROUTE.SETTINGS.ACCOUNT.PREFERENCES}
|
||||||
|
leftSection={<IconBrush size={16} />}
|
||||||
|
>
|
||||||
|
My preferences
|
||||||
|
</Menu.Item>
|
||||||
|
|
||||||
<Menu.Divider />
|
<Menu.Divider />
|
||||||
|
|
||||||
<Menu.Item
|
<Menu.Item onClick={logout} leftSection={<IconLogout size={16} />}>
|
||||||
onClick={logout}
|
|
||||||
leftSection={
|
|
||||||
<IconLogout
|
|
||||||
style={{ width: rem(16), height: rem(16) }}
|
|
||||||
stroke={1.5}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Logout
|
Logout
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
</Menu.Dropdown>
|
</Menu.Dropdown>
|
||||||
|
|||||||
@ -2,6 +2,9 @@ import classes from "@/features/editor/styles/editor.module.css";
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { TitleEditor } from "@/features/editor/title-editor";
|
import { TitleEditor } from "@/features/editor/title-editor";
|
||||||
import PageEditor from "@/features/editor/page-editor";
|
import PageEditor from "@/features/editor/page-editor";
|
||||||
|
import { Container } from "@mantine/core";
|
||||||
|
import { useAtom } from "jotai/index";
|
||||||
|
import { userAtom } from "@/features/user/atoms/current-user-atom.ts";
|
||||||
|
|
||||||
const MemoizedTitleEditor = React.memo(TitleEditor);
|
const MemoizedTitleEditor = React.memo(TitleEditor);
|
||||||
const MemoizedPageEditor = React.memo(PageEditor);
|
const MemoizedPageEditor = React.memo(PageEditor);
|
||||||
@ -21,8 +24,16 @@ export function FullEditor({
|
|||||||
spaceSlug,
|
spaceSlug,
|
||||||
editable,
|
editable,
|
||||||
}: FullEditorProps) {
|
}: FullEditorProps) {
|
||||||
|
const [user] = useAtom(userAtom);
|
||||||
|
const fullPageWidth = user.settings?.preferences?.fullPageWidth;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes.editor}>
|
<Container
|
||||||
|
fluid={fullPageWidth}
|
||||||
|
{...(fullPageWidth && { mx: 80 })}
|
||||||
|
size={850}
|
||||||
|
className={classes.editor}
|
||||||
|
>
|
||||||
<MemoizedTitleEditor
|
<MemoizedTitleEditor
|
||||||
pageId={pageId}
|
pageId={pageId}
|
||||||
slugId={slugId}
|
slugId={slugId}
|
||||||
@ -31,6 +42,6 @@ export function FullEditor({
|
|||||||
editable={editable}
|
editable={editable}
|
||||||
/>
|
/>
|
||||||
<MemoizedPageEditor pageId={pageId} editable={editable} />
|
<MemoizedPageEditor pageId={pageId} editable={editable} />
|
||||||
</div>
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
.editor {
|
.editor {
|
||||||
max-width: 800px;
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 8px 20px;
|
padding: 8px 20px;
|
||||||
margin: 64px auto;
|
margin: 64px auto;
|
||||||
|
|||||||
@ -1,5 +1,16 @@
|
|||||||
import { atomWithStorage } from "jotai/utils";
|
import { atomWithStorage } from "jotai/utils";
|
||||||
|
|
||||||
import { ICurrentUser } from "@/features/user/types/user.types";
|
import { ICurrentUser } from "@/features/user/types/user.types";
|
||||||
|
import { focusAtom } from "jotai-optics";
|
||||||
|
|
||||||
export const currentUserAtom = atomWithStorage<ICurrentUser | null>("currentUser", null);
|
export const currentUserAtom = atomWithStorage<ICurrentUser | null>(
|
||||||
|
"currentUser",
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
|
export const userAtom = focusAtom(currentUserAtom, (optic) =>
|
||||||
|
optic.prop("user"),
|
||||||
|
);
|
||||||
|
export const workspaceAtom = focusAtom(currentUserAtom, (optic) =>
|
||||||
|
optic.prop("workspace"),
|
||||||
|
);
|
||||||
|
|||||||
42
apps/client/src/features/user/components/page-width-pref.tsx
Normal file
42
apps/client/src/features/user/components/page-width-pref.tsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { Group, Text, Switch } from "@mantine/core";
|
||||||
|
import { useAtom } from "jotai/index";
|
||||||
|
import { userAtom } from "@/features/user/atoms/current-user-atom.ts";
|
||||||
|
import { updateUser } from "@/features/user/services/user-service.ts";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
|
||||||
|
export default function PageWidthPref() {
|
||||||
|
return (
|
||||||
|
<Group justify="space-between" wrap="nowrap" gap="xl">
|
||||||
|
<div>
|
||||||
|
<Text size="md">Full page width</Text>
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
Choose your preferred page width.
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<WidthToggle />
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function WidthToggle() {
|
||||||
|
const [user, setUser] = useAtom(userAtom);
|
||||||
|
const [checked, setChecked] = useState(
|
||||||
|
user.settings?.preferences?.fullPageWidth,
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const value = event.currentTarget.checked;
|
||||||
|
const updatedUser = await updateUser({ fullPageWidth: value });
|
||||||
|
setChecked(value);
|
||||||
|
setUser(updatedUser);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Switch
|
||||||
|
defaultChecked={checked}
|
||||||
|
onChange={handleChange}
|
||||||
|
aria-label="Toggle full page width"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -7,7 +7,7 @@ export interface IUser {
|
|||||||
emailVerifiedAt: Date;
|
emailVerifiedAt: Date;
|
||||||
avatarUrl: string;
|
avatarUrl: string;
|
||||||
timezone: string;
|
timezone: string;
|
||||||
settings: any;
|
settings: IUserSettings;
|
||||||
invitedById: string;
|
invitedById: string;
|
||||||
lastLoginAt: string;
|
lastLoginAt: string;
|
||||||
lastActiveAt: Date;
|
lastActiveAt: Date;
|
||||||
@ -17,9 +17,16 @@ export interface IUser {
|
|||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
deactivatedAt: Date;
|
deactivatedAt: Date;
|
||||||
deletedAt: Date;
|
deletedAt: Date;
|
||||||
|
fullPageWidth: boolean; // used for update
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ICurrentUser {
|
export interface ICurrentUser {
|
||||||
user: IUser;
|
user: IUser;
|
||||||
workspace: IWorkspace;
|
workspace: IWorkspace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IUserSettings {
|
||||||
|
preferences: {
|
||||||
|
fullPageWidth: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@ -1,11 +1,15 @@
|
|||||||
import SettingsTitle from "@/components/settings/settings-title.tsx";
|
import SettingsTitle from "@/components/settings/settings-title.tsx";
|
||||||
import AccountTheme from "@/features/user/components/account-theme.tsx";
|
import AccountTheme from "@/features/user/components/account-theme.tsx";
|
||||||
|
import PageWidthPref from "@/features/user/components/page-width-pref.tsx";
|
||||||
|
import { Divider } from "@mantine/core";
|
||||||
|
|
||||||
export default function AccountPreferences() {
|
export default function AccountPreferences() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SettingsTitle title="Preferences" />
|
<SettingsTitle title="Preferences" />
|
||||||
<AccountTheme />
|
<AccountTheme />
|
||||||
|
<Divider my={"md"} />
|
||||||
|
<PageWidthPref />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { OmitType, PartialType } from '@nestjs/mapped-types';
|
import { OmitType, PartialType } from '@nestjs/mapped-types';
|
||||||
import { CreateUserDto } from '../../auth/dto/create-user.dto';
|
import { CreateUserDto } from '../../auth/dto/create-user.dto';
|
||||||
import { IsOptional, IsString } from 'class-validator';
|
import { IsBoolean, IsOptional, IsString } from 'class-validator';
|
||||||
|
|
||||||
export class UpdateUserDto extends PartialType(
|
export class UpdateUserDto extends PartialType(
|
||||||
OmitType(CreateUserDto, ['password'] as const),
|
OmitType(CreateUserDto, ['password'] as const),
|
||||||
@ -8,4 +8,8 @@ export class UpdateUserDto extends PartialType(
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
avatarUrl: string;
|
avatarUrl: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
fullPageWidth: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,10 +20,19 @@ export class UserService {
|
|||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
) {
|
) {
|
||||||
const user = await this.userRepo.findById(userId, workspaceId);
|
const user = await this.userRepo.findById(userId, workspaceId);
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new NotFoundException('User not found');
|
throw new NotFoundException('User not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// preference update
|
||||||
|
if (typeof updateUserDto.fullPageWidth !== 'undefined') {
|
||||||
|
return this.updateUserPageWidthPreference(
|
||||||
|
userId,
|
||||||
|
updateUserDto.fullPageWidth,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (updateUserDto.name) {
|
if (updateUserDto.name) {
|
||||||
user.name = updateUserDto.name;
|
user.name = updateUserDto.name;
|
||||||
}
|
}
|
||||||
@ -42,4 +51,12 @@ export class UserService {
|
|||||||
await this.userRepo.updateUser(updateUserDto, userId, workspaceId);
|
await this.userRepo.updateUser(updateUserDto, userId, workspaceId);
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updateUserPageWidthPreference(userId: string, fullPageWidth: boolean) {
|
||||||
|
return this.userRepo.updatePreference(
|
||||||
|
userId,
|
||||||
|
'fullPageWidth',
|
||||||
|
fullPageWidth,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import {
|
|||||||
executeWithPagination,
|
executeWithPagination,
|
||||||
PaginationResult,
|
PaginationResult,
|
||||||
} from '@docmost/db/pagination/pagination';
|
} from '@docmost/db/pagination/pagination';
|
||||||
|
import { sql } from 'kysely';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UserRepo {
|
export class UserRepo {
|
||||||
@ -157,6 +158,24 @@ export class UserRepo {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updatePreference(
|
||||||
|
userId: string,
|
||||||
|
prefKey: string,
|
||||||
|
prefValue: string | boolean,
|
||||||
|
) {
|
||||||
|
return await this.db
|
||||||
|
.updateTable('users')
|
||||||
|
.set({
|
||||||
|
settings: sql`COALESCE(settings, '{}'::jsonb)
|
||||||
|
|| jsonb_build_object('preferences', COALESCE(settings->'preferences', '{}'::jsonb)
|
||||||
|
|| jsonb_build_object('${sql.raw(prefKey)}', ${sql.lit(prefValue)}))`,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
})
|
||||||
|
.where('id', '=', userId)
|
||||||
|
.returning(this.baseFields)
|
||||||
|
.executeTakeFirst();
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
async getSpaceIds(
|
async getSpaceIds(
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
|
|||||||
Reference in New Issue
Block a user