feat(documents): delete document from context menu
This commit is contained in:
@@ -25,6 +25,7 @@ import createCalendarEventMutation from "~~/graphql/operations/create-calendar-e
|
|||||||
import archiveCalendarEventMutation from "~~/graphql/operations/archive-calendar-event.graphql?raw";
|
import archiveCalendarEventMutation from "~~/graphql/operations/archive-calendar-event.graphql?raw";
|
||||||
import createCommunicationMutation from "~~/graphql/operations/create-communication.graphql?raw";
|
import createCommunicationMutation from "~~/graphql/operations/create-communication.graphql?raw";
|
||||||
import createWorkspaceDocumentMutation from "~~/graphql/operations/create-workspace-document.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 updateCommunicationTranscriptMutation from "~~/graphql/operations/update-communication-transcript.graphql?raw";
|
||||||
import updateFeedDecisionMutation from "~~/graphql/operations/update-feed-decision.graphql?raw";
|
import updateFeedDecisionMutation from "~~/graphql/operations/update-feed-decision.graphql?raw";
|
||||||
import chatConversationsQuery from "~~/graphql/operations/chat-conversations.graphql?raw";
|
import chatConversationsQuery from "~~/graphql/operations/chat-conversations.graphql?raw";
|
||||||
@@ -3155,6 +3156,7 @@ const selectedContactRecentMessages = computed(() => {
|
|||||||
|
|
||||||
const documentSearch = ref("");
|
const documentSearch = ref("");
|
||||||
const documentSortMode = ref<DocumentSortMode>("updatedAt");
|
const documentSortMode = ref<DocumentSortMode>("updatedAt");
|
||||||
|
const documentDeletingId = ref("");
|
||||||
const documentSortOptions: Array<{ value: DocumentSortMode; label: string }> = [
|
const documentSortOptions: Array<{ value: DocumentSortMode; label: string }> = [
|
||||||
{ value: "updatedAt", label: "Updated" },
|
{ value: "updatedAt", label: "Updated" },
|
||||||
{ value: "title", label: "Title" },
|
{ value: "title", label: "Title" },
|
||||||
@@ -3210,6 +3212,36 @@ function openDocumentsTab(push = false) {
|
|||||||
syncPathFromUi(push);
|
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 peopleListMode = ref<"contacts" | "deals">("contacts");
|
||||||
const peopleSearch = ref("");
|
const peopleSearch = ref("");
|
||||||
const peopleSortMode = ref<PeopleSortMode>("lastContact");
|
const peopleSortMode = ref<PeopleSortMode>("lastContact");
|
||||||
@@ -5454,6 +5486,7 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
|
|||||||
@update:document-sort-mode="documentSortMode = $event"
|
@update:document-sort-mode="documentSortMode = $event"
|
||||||
@select-document="selectedDocumentId = $event"
|
@select-document="selectedDocumentId = $event"
|
||||||
@update-selected-document-body="updateSelectedDocumentBody"
|
@update-selected-document-body="updateSelectedDocumentBody"
|
||||||
|
@delete-document="deleteWorkspaceDocumentById"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<CrmChangeReviewOverlay
|
<CrmChangeReviewOverlay
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { onBeforeUnmount, onMounted, ref } from "vue";
|
||||||
import ContactCollaborativeEditor from "~~/app/components/ContactCollaborativeEditor.client.vue";
|
import ContactCollaborativeEditor from "~~/app/components/ContactCollaborativeEditor.client.vue";
|
||||||
|
|
||||||
type DocumentSortOption = {
|
type DocumentSortOption = {
|
||||||
@@ -39,11 +40,74 @@ const emit = defineEmits<{
|
|||||||
(e: "update:documentSortMode", value: string): void;
|
(e: "update:documentSortMode", value: string): void;
|
||||||
(e: "select-document", documentId: string): void;
|
(e: "select-document", documentId: string): void;
|
||||||
(e: "update-selected-document-body", value: 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);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="flex h-full min-h-0 flex-col gap-0">
|
<section class="flex h-full min-h-0 flex-col gap-0" @click="closeDocumentContextMenu">
|
||||||
<div class="grid h-full min-h-0 flex-1 gap-0 md:grid-cols-[248px_minmax(0,1fr)]">
|
<div class="grid h-full min-h-0 flex-1 gap-0 md:grid-cols-[248px_minmax(0,1fr)]">
|
||||||
<aside class="h-full min-h-0 border-r border-base-300 flex flex-col">
|
<aside class="h-full min-h-0 border-r border-base-300 flex flex-col">
|
||||||
<div class="sticky top-0 z-20 border-b border-base-300 bg-base-100 p-2">
|
<div class="sticky top-0 z-20 border-b border-base-300 bg-base-100 p-2">
|
||||||
@@ -89,6 +153,7 @@ const emit = defineEmits<{
|
|||||||
class="w-full border-b border-base-300 px-3 py-2 text-left transition hover:bg-base-200/40"
|
class="w-full border-b border-base-300 px-3 py-2 text-left transition hover:bg-base-200/40"
|
||||||
:class="props.selectedDocumentId === doc.id ? 'bg-primary/10' : ''"
|
:class="props.selectedDocumentId === doc.id ? 'bg-primary/10' : ''"
|
||||||
@click="emit('select-document', doc.id)"
|
@click="emit('select-document', doc.id)"
|
||||||
|
@contextmenu="openDocumentContextMenu($event, doc)"
|
||||||
>
|
>
|
||||||
<div class="flex items-start justify-between gap-2">
|
<div class="flex items-start justify-between gap-2">
|
||||||
<p class="min-w-0 flex-1 truncate text-xs font-semibold">{{ doc.title }}</p>
|
<p class="min-w-0 flex-1 truncate text-xs font-semibold">{{ doc.title }}</p>
|
||||||
@@ -129,5 +194,19 @@ const emit = defineEmits<{
|
|||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="documentContextMenu.open"
|
||||||
|
class="fixed z-50 w-44 rounded-lg border border-base-300 bg-base-100 p-1 shadow-xl"
|
||||||
|
:style="{ left: `${documentContextMenu.x}px`, top: `${documentContextMenu.y}px` }"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="btn btn-ghost btn-sm w-full justify-start text-error"
|
||||||
|
@click="deleteDocumentFromContextMenu"
|
||||||
|
>
|
||||||
|
Delete document
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
mutation DeleteWorkspaceDocument($id: ID!) {
|
||||||
|
deleteWorkspaceDocument(id: $id) {
|
||||||
|
ok
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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(
|
async function setContactInboxHidden(
|
||||||
auth: AuthContext | null,
|
auth: AuthContext | null,
|
||||||
input: { inboxId: string; hidden: boolean },
|
input: { inboxId: string; hidden: boolean },
|
||||||
@@ -1829,6 +1864,7 @@ export const crmGraphqlSchema = buildSchema(`
|
|||||||
archiveCalendarEvent(input: ArchiveCalendarEventInput!): CalendarEvent!
|
archiveCalendarEvent(input: ArchiveCalendarEventInput!): CalendarEvent!
|
||||||
createCommunication(input: CreateCommunicationInput!): MutationWithIdResult!
|
createCommunication(input: CreateCommunicationInput!): MutationWithIdResult!
|
||||||
createWorkspaceDocument(input: CreateWorkspaceDocumentInput!): WorkspaceDocument!
|
createWorkspaceDocument(input: CreateWorkspaceDocumentInput!): WorkspaceDocument!
|
||||||
|
deleteWorkspaceDocument(id: ID!): MutationWithIdResult!
|
||||||
updateCommunicationTranscript(id: ID!, transcript: [String!]!): MutationWithIdResult!
|
updateCommunicationTranscript(id: ID!, transcript: [String!]!): MutationWithIdResult!
|
||||||
updateFeedDecision(id: ID!, decision: String!, decisionNote: String): MutationWithIdResult!
|
updateFeedDecision(id: ID!, decision: String!, decisionNote: String): MutationWithIdResult!
|
||||||
setContactInboxHidden(inboxId: ID!, hidden: Boolean!): MutationResult!
|
setContactInboxHidden(inboxId: ID!, hidden: Boolean!): MutationResult!
|
||||||
@@ -2161,6 +2197,11 @@ export const crmGraphqlRoot = {
|
|||||||
context: GraphQLContext,
|
context: GraphQLContext,
|
||||||
) => createWorkspaceDocument(context.auth, args.input),
|
) => createWorkspaceDocument(context.auth, args.input),
|
||||||
|
|
||||||
|
deleteWorkspaceDocument: async (
|
||||||
|
args: { id: string },
|
||||||
|
context: GraphQLContext,
|
||||||
|
) => deleteWorkspaceDocument(context.auth, args.id),
|
||||||
|
|
||||||
updateCommunicationTranscript: async (
|
updateCommunicationTranscript: async (
|
||||||
args: { id: string; transcript: string[] },
|
args: { id: string; transcript: string[] },
|
||||||
context: GraphQLContext,
|
context: GraphQLContext,
|
||||||
|
|||||||
Reference in New Issue
Block a user