Compare commits

...

8 Commits

Author SHA1 Message Date
Philipinho a4ec2dac6c v0.2.2 2024-07-03 13:01:00 +01:00
Philip Okugbe 681d7c789c merge: fix/media-in-firefox
fix media visibility in firefox
2024-07-03 11:57:03 +01:00
Philipinho 9f583174a9 fix media visibility in firefox 2024-07-03 11:53:09 +01:00
Philip Okugbe 05633082c5 feat/fuzzy-match-menu
added fuzzy matching logic for menu search
2024-07-03 11:25:21 +01:00
Philip Okugbe 491fbad4ac feat/full-width
add full page width user preference
2024-07-03 11:24:32 +01:00
Philipinho e824aeced7 Add width option to page menu 2024-07-03 11:23:42 +01:00
Philipinho 8f056d1071 add full page width preference 2024-07-03 11:00:42 +01:00
SurajJadhav7 99cf6dab62 added fuzzy matching logic for menu search 2024-07-01 16:49:36 +05:30
18 changed files with 197 additions and 67 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "client", "name": "client",
"private": true, "private": true,
"version": "0.2.1", "version": "0.2.2",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "tsc && vite build", "build": "tsc && vite build",
@@ -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,32 +2,28 @@ import { NodeViewProps, NodeViewWrapper } from "@tiptap/react";
import { useMemo } from "react"; import { useMemo } from "react";
import { Image } from "@mantine/core"; import { Image } from "@mantine/core";
import { getFileUrl } from "@/lib/config.ts"; import { getFileUrl } from "@/lib/config.ts";
import clsx from "clsx";
export default function ImageView(props: NodeViewProps) { export default function ImageView(props: NodeViewProps) {
const { node, selected } = props; const { node, selected } = props;
const { src, width, align, title } = node.attrs; const { src, width, align, title } = node.attrs;
const flexJustifyContent = useMemo(() => { const alignClass = useMemo(() => {
if (align === "center") return "center"; if (align === "left") return "alignLeft";
if (align === "right") return "flex-end"; if (align === "right") return "alignRight";
return "flex-start"; if (align === "center") return "alignCenter";
return "alignCenter";
}, [align]); }, [align]);
return ( return (
<NodeViewWrapper <NodeViewWrapper>
style={{
position: "relative",
display: "flex",
justifyContent: flexJustifyContent,
}}
>
<Image <Image
radius="md" radius="md"
fit="contain" fit="contain"
w={width} w={width}
src={getFileUrl(src)} src={getFileUrl(src)}
alt={title} alt={title}
className={selected ? "ProseMirror-selectednode" : ""} className={clsx(selected ? "ProseMirror-selectednode" : "", alignClass)}
/> />
</NodeViewWrapper> </NodeViewWrapper>
); );
@@ -264,10 +264,20 @@ export const getSuggestionItems = ({
const search = query.toLowerCase(); const search = query.toLowerCase();
const filteredGroups: SlashMenuGroupedItemsType = {}; const filteredGroups: SlashMenuGroupedItemsType = {};
const fuzzyMatch = (query, target) => {
let queryIndex = 0;
target = target.toLowerCase();
for (let char of target) {
if (query[queryIndex] === char) queryIndex++;
if (queryIndex === query.length) return true;
}
return false;
};
for (const [group, items] of Object.entries(CommandGroups)) { for (const [group, items] of Object.entries(CommandGroups)) {
const filteredItems = items.filter((item) => { const filteredItems = items.filter((item) => {
return ( return (
item.title.toLowerCase().includes(search) || fuzzyMatch(search, item.title) ||
item.description.toLowerCase().includes(search) || item.description.toLowerCase().includes(search) ||
(item.searchTerms && (item.searchTerms &&
item.searchTerms.some((term: string) => term.includes(search))) item.searchTerms.some((term: string) => term.includes(search)))
@@ -1,31 +1,27 @@
import { NodeViewProps, NodeViewWrapper } from "@tiptap/react"; import { NodeViewProps, NodeViewWrapper } from "@tiptap/react";
import { useMemo } from "react"; import { useMemo } from "react";
import { getFileUrl } from "@/lib/config.ts"; import { getFileUrl } from "@/lib/config.ts";
import clsx from "clsx";
export default function VideoView(props: NodeViewProps) { export default function VideoView(props: NodeViewProps) {
const { node, selected } = props; const { node, selected } = props;
const { src, width, align } = node.attrs; const { src, width, align } = node.attrs;
const flexJustifyContent = useMemo(() => { const alignClass = useMemo(() => {
if (align === "center") return "center"; if (align === "left") return "alignLeft";
if (align === "right") return "flex-end"; if (align === "right") return "alignRight";
return "flex-start"; if (align === "center") return "alignCenter";
return "alignCenter";
}, [align]); }, [align]);
return ( return (
<NodeViewWrapper <NodeViewWrapper>
style={{
position: "relative",
display: "flex",
justifyContent: flexJustifyContent,
}}
>
<video <video
preload="metadata" preload="metadata"
width={width} width={width}
controls controls
src={getFileUrl(src)} src={getFileUrl(src)}
className={selected ? "ProseMirror-selectednode" : ""} className={clsx(selected ? "ProseMirror-selectednode" : "", alignClass)}
/> />
</NodeViewWrapper> </NodeViewWrapper>
); );
@@ -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>
); );
} }
@@ -125,6 +125,21 @@
cursor: ew-resize; cursor: ew-resize;
cursor: col-resize; cursor: col-resize;
} }
.alignLeft {
margin-left: 0;
margin-right: auto;
}
.alignRight {
margin-right: 0;
margin-left: auto;
}
.alignCenter {
margin-left: auto;
margin-right: auto;
}
} }
.ProseMirror-icon { .ProseMirror-icon {
@@ -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,6 @@
import { ActionIcon, Menu, Tooltip } from "@mantine/core"; import { ActionIcon, Group, Menu, Tooltip } from "@mantine/core";
import { import {
IconArrowsHorizontal,
IconDots, IconDots,
IconHistory, IconHistory,
IconLink, IconLink,
@@ -19,6 +20,7 @@ import { getAppUrl } from "@/lib/config.ts";
import { extractPageSlugId } from "@/lib"; import { extractPageSlugId } from "@/lib";
import { treeApiAtom } from "@/features/page/tree/atoms/tree-api-atom.ts"; import { treeApiAtom } from "@/features/page/tree/atoms/tree-api-atom.ts";
import { useDeletePageModal } from "@/features/page/hooks/use-delete-page-modal.tsx"; import { useDeletePageModal } from "@/features/page/hooks/use-delete-page-modal.tsx";
import { PageWidthToggle } from "@/features/user/components/page-width-pref.tsx";
interface PageHeaderMenuProps { interface PageHeaderMenuProps {
readOnly?: boolean; readOnly?: boolean;
@@ -95,6 +97,13 @@ function PageActionMenu({ readOnly }: PageActionMenuProps) {
Copy link Copy link
</Menu.Item> </Menu.Item>
<Menu.Divider /> <Menu.Divider />
<Menu.Item leftSection={<IconArrowsHorizontal size={16} stroke={2} />}>
<Group wrap="nowrap">
<PageWidthToggle label="Full width" />
</Group>
</Menu.Item>
<Menu.Item <Menu.Item
leftSection={<IconHistory size={16} stroke={2} />} leftSection={<IconHistory size={16} stroke={2} />}
onClick={openHistoryModal} onClick={openHistoryModal}
@@ -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"),
);
@@ -0,0 +1,49 @@
import { Group, Text, Switch, MantineSize } 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>
<PageWidthToggle />
</Group>
);
}
interface PageWidthToggleProps {
size?: MantineSize;
label?: string;
}
export function PageWidthToggle({ size, label }: PageWidthToggleProps) {
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
size={size}
label={label}
labelPosition="left"
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 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "server", "name": "server",
"version": "0.2.1", "version": "0.2.2",
"description": "", "description": "",
"author": "", "author": "",
"private": true, "private": true,
@@ -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;
} }
+17
View File
@@ -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,
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "docmost", "name": "docmost",
"homepage": "https://docmost.com", "homepage": "https://docmost.com",
"version": "0.2.1", "version": "0.2.2",
"private": true, "private": true,
"scripts": { "scripts": {
"build": "nx run-many -t build", "build": "nx run-many -t build",