From 6ad53e64c55a179acd44c4cf4354b782a1b0f753 Mon Sep 17 00:00:00 2001 From: Ruslan Bakiev <572431+veikab@users.noreply.github.com> Date: Mon, 23 Feb 2026 14:27:00 +0700 Subject: [PATCH] feat(documents): delete document from context menu --- .../components/workspace/CrmWorkspaceApp.vue | 33 ++++++++ .../workspace/documents/CrmDocumentsPanel.vue | 81 ++++++++++++++++++- .../delete-workspace-document.graphql | 6 ++ frontend/server/graphql/schema.ts | 41 ++++++++++ 4 files changed, 160 insertions(+), 1 deletion(-) create mode 100644 frontend/graphql/operations/delete-workspace-document.graphql diff --git a/frontend/app/components/workspace/CrmWorkspaceApp.vue b/frontend/app/components/workspace/CrmWorkspaceApp.vue index f6f3d18..c2a638a 100644 --- a/frontend/app/components/workspace/CrmWorkspaceApp.vue +++ b/frontend/app/components/workspace/CrmWorkspaceApp.vue @@ -25,6 +25,7 @@ import createCalendarEventMutation from "~~/graphql/operations/create-calendar-e import archiveCalendarEventMutation from "~~/graphql/operations/archive-calendar-event.graphql?raw"; import createCommunicationMutation from "~~/graphql/operations/create-communication.graphql?raw"; import createWorkspaceDocumentMutation from "~~/graphql/operations/create-workspace-document.graphql?raw"; +import deleteWorkspaceDocumentMutation from "~~/graphql/operations/delete-workspace-document.graphql?raw"; import updateCommunicationTranscriptMutation from "~~/graphql/operations/update-communication-transcript.graphql?raw"; import updateFeedDecisionMutation from "~~/graphql/operations/update-feed-decision.graphql?raw"; import chatConversationsQuery from "~~/graphql/operations/chat-conversations.graphql?raw"; @@ -3155,6 +3156,7 @@ const selectedContactRecentMessages = computed(() => { const documentSearch = ref(""); const documentSortMode = ref("updatedAt"); +const documentDeletingId = ref(""); const documentSortOptions: Array<{ value: DocumentSortMode; label: string }> = [ { value: "updatedAt", label: "Updated" }, { value: "title", label: "Title" }, @@ -3210,6 +3212,36 @@ function openDocumentsTab(push = false) { syncPathFromUi(push); } +async function deleteWorkspaceDocumentById(documentIdInput: string) { + const documentId = safeTrim(documentIdInput); + if (!documentId) return; + if (documentDeletingId.value === documentId) return; + + const target = documents.value.find((doc) => doc.id === documentId); + const targetLabel = safeTrim(target?.title) || "this document"; + if (process.client && !window.confirm(`Delete ${targetLabel}?`)) return; + + documentDeletingId.value = documentId; + try { + await gqlFetch<{ deleteWorkspaceDocument: { ok: boolean; id: string } }>(deleteWorkspaceDocumentMutation, { + id: documentId, + }); + documents.value = documents.value.filter((doc) => doc.id !== documentId); + clientTimelineItems.value = clientTimelineItems.value.filter((item) => { + const isDocumentEntry = String(item.contentType).toLowerCase() === "document"; + if (!isDocumentEntry) return true; + return item.contentId !== documentId && item.document?.id !== documentId; + }); + if (selectedDocumentId.value === documentId) { + selectedDocumentId.value = ""; + } + } finally { + if (documentDeletingId.value === documentId) { + documentDeletingId.value = ""; + } + } +} + const peopleListMode = ref<"contacts" | "deals">("contacts"); const peopleSearch = ref(""); const peopleSortMode = ref("lastContact"); @@ -5454,6 +5486,7 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected") @update:document-sort-mode="documentSortMode = $event" @select-document="selectedDocumentId = $event" @update-selected-document-body="updateSelectedDocumentBody" + @delete-document="deleteWorkspaceDocumentById" /> +import { onBeforeUnmount, onMounted, ref } from "vue"; import ContactCollaborativeEditor from "~~/app/components/ContactCollaborativeEditor.client.vue"; type DocumentSortOption = { @@ -39,11 +40,74 @@ const emit = defineEmits<{ (e: "update:documentSortMode", value: string): void; (e: "select-document", documentId: string): void; (e: "update-selected-document-body", value: string): void; + (e: "delete-document", documentId: string): void; }>(); + +const documentContextMenu = ref<{ + open: boolean; + x: number; + y: number; + documentId: string; +}>({ + open: false, + x: 0, + y: 0, + documentId: "", +}); + +function closeDocumentContextMenu() { + if (!documentContextMenu.value.open) return; + documentContextMenu.value = { + open: false, + x: 0, + y: 0, + documentId: "", + }; +} + +function openDocumentContextMenu(event: MouseEvent, doc: DocumentListItem) { + event.preventDefault(); + event.stopPropagation(); + emit("select-document", doc.id); + const padding = 8; + const menuWidth = 176; + const menuHeight = 44; + const maxX = Math.max(padding, window.innerWidth - menuWidth - padding); + const maxY = Math.max(padding, window.innerHeight - menuHeight - padding); + documentContextMenu.value = { + open: true, + x: Math.min(maxX, Math.max(padding, event.clientX)), + y: Math.min(maxY, Math.max(padding, event.clientY)), + documentId: doc.id, + }; +} + +function deleteDocumentFromContextMenu() { + const documentId = documentContextMenu.value.documentId; + if (!documentId) return; + emit("delete-document", documentId); + closeDocumentContextMenu(); +} + +function onWindowKeydown(event: KeyboardEvent) { + if (event.key === "Escape") { + closeDocumentContextMenu(); + } +} + +onMounted(() => { + window.addEventListener("keydown", onWindowKeydown); + window.addEventListener("scroll", closeDocumentContextMenu, true); +}); + +onBeforeUnmount(() => { + window.removeEventListener("keydown", onWindowKeydown); + window.removeEventListener("scroll", closeDocumentContextMenu, true); +}); diff --git a/frontend/graphql/operations/delete-workspace-document.graphql b/frontend/graphql/operations/delete-workspace-document.graphql new file mode 100644 index 0000000..aeee4dd --- /dev/null +++ b/frontend/graphql/operations/delete-workspace-document.graphql @@ -0,0 +1,6 @@ +mutation DeleteWorkspaceDocument($id: ID!) { + deleteWorkspaceDocument(id: $id) { + ok + id + } +} diff --git a/frontend/server/graphql/schema.ts b/frontend/server/graphql/schema.ts index 0fbb479..08cfe36 100644 --- a/frontend/server/graphql/schema.ts +++ b/frontend/server/graphql/schema.ts @@ -1425,6 +1425,41 @@ async function createWorkspaceDocument(auth: AuthContext | null, input: { }; } +async function deleteWorkspaceDocument(auth: AuthContext | null, documentIdInput: string) { + const ctx = requireAuth(auth); + const documentId = String(documentIdInput ?? "").trim(); + if (!documentId) throw new Error("id is required"); + + const existing = await prisma.workspaceDocument.findFirst({ + where: { + id: documentId, + teamId: ctx.teamId, + }, + select: { id: true }, + }); + if (!existing) throw new Error("document not found"); + + await prisma.$transaction([ + prisma.workspaceDocument.delete({ + where: { id: existing.id }, + }), + prisma.clientTimelineEntry.deleteMany({ + where: { + teamId: ctx.teamId, + contentType: "DOCUMENT", + contentId: existing.id, + }, + }), + ]); + + await fs.rm(datasetRoot({ teamId: ctx.teamId, userId: ctx.userId }), { + recursive: true, + force: true, + }).catch(() => undefined); + + return { ok: true, id: existing.id }; +} + async function setContactInboxHidden( auth: AuthContext | null, input: { inboxId: string; hidden: boolean }, @@ -1829,6 +1864,7 @@ export const crmGraphqlSchema = buildSchema(` archiveCalendarEvent(input: ArchiveCalendarEventInput!): CalendarEvent! createCommunication(input: CreateCommunicationInput!): MutationWithIdResult! createWorkspaceDocument(input: CreateWorkspaceDocumentInput!): WorkspaceDocument! + deleteWorkspaceDocument(id: ID!): MutationWithIdResult! updateCommunicationTranscript(id: ID!, transcript: [String!]!): MutationWithIdResult! updateFeedDecision(id: ID!, decision: String!, decisionNote: String): MutationWithIdResult! setContactInboxHidden(inboxId: ID!, hidden: Boolean!): MutationResult! @@ -2161,6 +2197,11 @@ export const crmGraphqlRoot = { context: GraphQLContext, ) => createWorkspaceDocument(context.auth, args.input), + deleteWorkspaceDocument: async ( + args: { id: string }, + context: GraphQLContext, + ) => deleteWorkspaceDocument(context.auth, args.id), + updateCommunicationTranscript: async ( args: { id: string; transcript: string[] }, context: GraphQLContext,