Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ability to modify document titles #342

Merged
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions backend/src/workspace-documents/dto/update-document-title.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsString, IsNotEmpty } from "class-validator";

export class UpdateDocumentTitleDto {
@ApiProperty({
description: "The new title of the document",
example: "Updated Document Title",
type: String,
})
@IsString()
@IsNotEmpty()
title: string;
}
38 changes: 38 additions & 0 deletions backend/src/workspace-documents/workspace-documents.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
Param,
ParseIntPipe,
Post,
Put,
Query,
Req,
} from "@nestjs/common";
Expand All @@ -24,6 +25,7 @@ import {
import { AuthroizedRequest } from "src/utils/types/req.type";
import { CreateWorkspaceDocumentDto } from "./dto/create-workspace-document.dto";
import { CreateWorkspaceDocumentResponse } from "./types/create-workspace-document-response.type";
import { UpdateDocumentTitleDto } from "./dto/update-document-title.dto";
import { HttpExceptionResponse } from "src/utils/types/http-exception-response.type";
import { FindWorkspaceDocumentsResponse } from "./types/find-workspace-documents-response.type";
import { CreateWorkspaceDocumentShareTokenResponse } from "./types/create-workspace-document-share-token-response.type";
Expand All @@ -36,6 +38,39 @@ import { FindWorkspaceDocumentResponse } from "./types/find-workspace-document-r
export class WorkspaceDocumentsController {
constructor(private workspaceDocumentsService: WorkspaceDocumentsService) {}

// PUT endpoint for updating document title
devleejb marked this conversation as resolved.
Show resolved Hide resolved
@Put(":document_id/title")
devleejb marked this conversation as resolved.
Show resolved Hide resolved
@ApiOperation({
summary: "Update the title of a document in the workspace",
description: "If the user has the access permissions, update the document's title.",
})
@ApiOkResponse({
description: "Document title updated successfully",
})
@ApiNotFoundResponse({
type: HttpExceptionResponse,
description:
"The workspace or document does not exist, or the user lacks the appropriate permissions.",
})
@ApiBody({
description: "The new title of the document",
type: UpdateDocumentTitleDto,
})
async updateTitle(
@Param("workspace_id") workspaceId: string,
@Param("document_id") documentId: string,
@Body() updateDocumentTitleDto: UpdateDocumentTitleDto,
@Req() req: AuthroizedRequest
): Promise<void> {
await this.workspaceDocumentsService.updateTitle(
req.user.id,
workspaceId,
documentId,
updateDocumentTitleDto.title
);
}

// Get the list of documents in the workspace
@Get("")
@ApiOperation({
summary: "Retrieve the Documents in Workspace",
Expand Down Expand Up @@ -68,6 +103,7 @@ export class WorkspaceDocumentsController {
return this.workspaceDocumentsService.findMany(req.user.id, workspaceId, pageSize, cursor);
}

// Get a specific document by ID
@Get(":document_id")
@ApiOperation({
summary: "Retrieve a Document in the Workspace",
Expand All @@ -87,6 +123,7 @@ export class WorkspaceDocumentsController {
return this.workspaceDocumentsService.findOne(req.user.id, workspaceId, documentId);
}

// Create a new document in the workspace
@Post()
@ApiOperation({
summary: "Create a Document in a Workspace",
Expand All @@ -110,6 +147,7 @@ export class WorkspaceDocumentsController {
);
}

// Generate a share token for a document
@Post(":document_id/share-token")
@ApiOperation({
summary: "Retrieve a Share Token for the Document",
Expand Down
24 changes: 24 additions & 0 deletions backend/src/workspace-documents/workspace-documents.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,30 @@ export class WorkspaceDocumentsService {
private configService: ConfigService
) {}

async updateTitle(
userId: string,
workspaceId: string,
documentId: string,
title: string
): Promise<void> {
const document = await this.prismaService.document.findFirst({
devleejb marked this conversation as resolved.
Show resolved Hide resolved
where: {
id: documentId,
workspaceId: workspaceId,
},
});

if (!document) {
throw new NotFoundException("Document not found");
}
hugosandsjo marked this conversation as resolved.
Show resolved Hide resolved

// Update the document's title
await this.prismaService.document.update({
where: { id: documentId },
data: { title: title },
});
}

async create(userId: string, workspaceId: string, title: string) {
try {
await this.prismaService.userWorkspace.findFirstOrThrow({
Expand Down
39 changes: 22 additions & 17 deletions frontend/src/components/editor/Editor.tsx
devleejb marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -170,26 +170,31 @@ function Editor() {
]);

return (
<ScrollSyncPane>
<div
style={{
height: "100%",
overflow: "auto",
}}
>
<>
<ScrollSyncPane>
<div
ref={ref}
style={{
display: "flex",
alignItems: "stretch",
minHeight: "100%",
height: "100%",
overflow: "auto",
}}
/>
{Boolean(toolBarState.show) && (
<ToolBar toolBarState={toolBarState} onChangeToolBarState={setToolBarState} />
)}
</div>
</ScrollSyncPane>
>
<div
ref={ref}
style={{
display: "flex",
alignItems: "stretch",
minHeight: "100%",
}}
/>
{Boolean(toolBarState.show) && (
<ToolBar
toolBarState={toolBarState}
onChangeToolBarState={setToolBarState}
/>
)}
</div>
</ScrollSyncPane>
</>
);
}

Expand Down
78 changes: 76 additions & 2 deletions frontend/src/components/headers/DocumentHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,51 @@ import {
ToggleButtonGroup,
Toolbar,
Tooltip,
Button,
FormControl,
} from "@mui/material";
import { useEffect } from "react";
import { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useNavigate } from "react-router-dom";
import { useNavigate, useParams } from "react-router-dom";
import { useUserPresence } from "../../hooks/useUserPresence";
import { EditorModeType, selectEditor, setMode } from "../../store/editorSlice";
import { selectWorkspace } from "../../store/workspaceSlice";
import DownloadMenu from "../common/DownloadMenu";
import ShareButton from "../common/ShareButton";
import ThemeButton from "../common/ThemeButton";
import UserPresenceList from "./UserPresenceList";
import { FormContainer, TextFieldElement } from "react-hook-form-mui";
import { useGetWorkspaceQuery } from "../../hooks/api/workspace";

import {
useUpdateDocumentTitleMutation,
useGetDocumentQuery,
} from "../../hooks/api/workspaceDocument";

function DocumentHeader() {
const dispatch = useDispatch();
const navigate = useNavigate();
const editorState = useSelector(selectEditor);
const workspaceState = useSelector(selectWorkspace);
const { presenceList } = useUserPresence(editorState.doc);
const [focused, setFocused] = useState(false);
const [documentTitle, setDocumentTitle] = useState("");
devleejb marked this conversation as resolved.
Show resolved Hide resolved
const params = useParams();
const { data: workspace } = useGetWorkspaceQuery(params.workspaceSlug);

const { data: documentData } = useGetDocumentQuery(
workspace?.id || "",
params.documentId || ""
);
devleejb marked this conversation as resolved.
Show resolved Hide resolved

const { mutateAsync: updateDocumentTitle } = useUpdateDocumentTitleMutation(
workspace?.id || "",
params.documentId || ""
);

const handleFocus = () => {
setFocused(true);
};

useEffect(() => {
if (editorState.shareRole === "READ") {
Expand All @@ -45,6 +72,23 @@ function DocumentHeader() {
navigate(`/${workspaceState.data?.slug}`);
};

const handleDocumentTitleChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) => {
setDocumentTitle(e.target.value);
devleejb marked this conversation as resolved.
Show resolved Hide resolved
};

const handleUpdateDocumentTitle = async (data: { title: string }) => {
devleejb marked this conversation as resolved.
Show resolved Hide resolved
await updateDocumentTitle(data);
setFocused(false);
};

useEffect(() => {
if (documentData && documentData.title) {
devleejb marked this conversation as resolved.
Show resolved Hide resolved
setDocumentTitle(documentData.title);
}
}, [documentData]);

return (
<AppBar position="static" sx={{ zIndex: 100 }}>
<Toolbar>
Expand Down Expand Up @@ -84,7 +128,37 @@ function DocumentHeader() {
)}
</Paper>
<DownloadMenu />

<Stack alignItems="center">
<FormControl>
<FormContainer
defaultValues={{ title: documentTitle }}
onSuccess={handleUpdateDocumentTitle}
>
<Stack gap={4} alignItems="flex-end" flexDirection="row">
<TextFieldElement
variant="standard"
name="title"
label={documentTitle}
required
fullWidth
inputProps={{
maxLength: 255,
}}
onChange={handleDocumentTitleChange}
onFocus={handleFocus}
/>
{focused && (
<Button type="submit" variant="contained" size="large">
Update
</Button>
)}
</Stack>
</FormContainer>
</FormControl>
</Stack>
devleejb marked this conversation as resolved.
Show resolved Hide resolved
</Stack>

<Stack direction="row" alignItems="center" gap={1}>
<UserPresenceList presenceList={presenceList} />
{!editorState.shareRole && <ShareButton />}
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/hooks/api/types/document.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,7 @@ export class Document {
export class GetDocumentBySharingTokenResponse extends Document {
role: ShareRole;
}

export class UpdateDocumentRequest {
title: string;
}
24 changes: 23 additions & 1 deletion frontend/src/hooks/api/workspaceDocument.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ import {
GetWorkspaceDocumentResponse,
GetWorkspaceDocumentListResponse,
} from "./types/workspaceDocument";
import { useDispatch } from "react-redux";

import { useEffect } from "react";
import { setDocumentData } from "../../store/documentSlice";
import { UpdateDocumentRequest } from "./types/document";
import { useDispatch } from "react-redux";

export const generateGetWorkspaceDocumentListQueryKey = (workspaceId: string) => {
return ["workspaces", workspaceId, "documents"];
Expand Down Expand Up @@ -105,3 +107,23 @@ export const useCreateWorkspaceSharingTokenMutation = (workspaceId: string, docu
},
});
};

export const useUpdateDocumentTitleMutation = (workspaceId: string, documentId: string) => {
const queryClient = useQueryClient();

return useMutation({
mutationFn: async (data: UpdateDocumentRequest) => {
const res = await axios.put<void>(
`/workspaces/${workspaceId}/documents/${documentId}/title`,
data
);

return res.data;
},
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: generateGetDocumentQueryKey(workspaceId, documentId),
});
},
});
};