From f81a0fde5594e728a47b67104ac7cf0b2509d6de Mon Sep 17 00:00:00 2001 From: Ruslan Bakiev <572431+veikab@users.noreply.github.com> Date: Mon, 23 Feb 2026 08:04:58 +0700 Subject: [PATCH] Refine documents UX and extract document scope helpers --- frontend/app.vue | 269 +++++++++++------- frontend/composables/useWorkspaceDocuments.ts | 36 +++ frontend/server/graphql/schema.ts | 70 +++++ 3 files changed, 270 insertions(+), 105 deletions(-) create mode 100644 frontend/composables/useWorkspaceDocuments.ts diff --git a/frontend/app.vue b/frontend/app.vue index fb71d60..9aea6a3 100644 --- a/frontend/app.vue +++ b/frontend/app.vue @@ -20,6 +20,11 @@ import toggleContactPinMutation from "./graphql/operations/toggle-contact-pin.gr import confirmLatestChangeSetMutation from "./graphql/operations/confirm-latest-change-set.graphql?raw"; import rollbackLatestChangeSetMutation from "./graphql/operations/rollback-latest-change-set.graphql?raw"; import rollbackChangeSetItemsMutation from "./graphql/operations/rollback-change-set-items.graphql?raw"; +import { + buildContactDocumentScope, + formatDocumentScope, + isDocumentLinkedToContact, +} from "./composables/useWorkspaceDocuments"; import { Chat as AiChat } from "@ai-sdk/vue"; import { DefaultChatTransport, isTextUIPart, type UIMessage } from "ai"; type TabId = "communications" | "documents"; @@ -27,6 +32,7 @@ type CalendarView = "day" | "week" | "month" | "year" | "agenda"; type SortMode = "name" | "lastContact"; type PeopleLeftMode = "contacts" | "calendar"; type PeopleSortMode = "name" | "lastContact" | "company" | "country"; +type DocumentSortMode = "updatedAt" | "title" | "owner" | "type"; type FeedCard = { id: string; @@ -2803,58 +2809,32 @@ const selectedContactRecentMessages = computed(() => { .slice(0, 8); }); -const CONTACT_DOCUMENT_SCOPE_PREFIX = "contact:"; - -function buildContactDocumentScope(contactId: string, contactName: string) { - return `${CONTACT_DOCUMENT_SCOPE_PREFIX}${encodeURIComponent(contactId)}:${encodeURIComponent(contactName)}`; -} - -function parseContactDocumentScope(scope: string) { - const raw = String(scope ?? "").trim(); - if (!raw.startsWith(CONTACT_DOCUMENT_SCOPE_PREFIX)) return null; - const payload = raw.slice(CONTACT_DOCUMENT_SCOPE_PREFIX.length); - const [idRaw, ...nameParts] = payload.split(":"); - const contactId = decodeURIComponent(idRaw ?? "").trim(); - const contactName = decodeURIComponent(nameParts.join(":") ?? "").trim(); - if (!contactId) return null; - return { - contactId, - contactName, - }; -} - -function formatDocumentScope(scope: string) { - const linked = parseContactDocumentScope(scope); - if (!linked) return scope; - return linked.contactName ? `Contact · ${linked.contactName}` : "Contact document"; -} - -function isDocumentLinkedToContact(doc: WorkspaceDocument, contact: Contact | null | undefined) { - if (!contact) return false; - const linked = parseContactDocumentScope(doc.scope); - if (!linked) return false; - if (linked.contactId) return linked.contactId === contact.id; - return Boolean(linked.contactName && linked.contactName === contact.name); -} - const documentSearch = ref(""); -const selectedDocumentType = ref<"All" | WorkspaceDocument["type"]>("All"); - -const documentTypes = computed(() => - ["All", ...new Set(documents.value.map((item) => item.type))] as ("All" | WorkspaceDocument["type"])[], -); +const documentSortMode = ref("updatedAt"); +const documentSortOptions: Array<{ value: DocumentSortMode; label: string }> = [ + { value: "updatedAt", label: "Updated" }, + { value: "title", label: "Title" }, + { value: "owner", label: "Owner" }, + { value: "type", label: "Type" }, +]; const filteredDocuments = computed(() => { const query = documentSearch.value.trim().toLowerCase(); - return documents.value + const list = documents.value .filter((item) => { - if (selectedDocumentType.value !== "All" && item.type !== selectedDocumentType.value) return false; if (!query) return true; const haystack = [item.title, item.summary, item.owner, formatDocumentScope(item.scope), item.body].join(" ").toLowerCase(); return haystack.includes(query); }) - .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)); + .sort((a, b) => { + if (documentSortMode.value === "title") return a.title.localeCompare(b.title); + if (documentSortMode.value === "owner") return a.owner.localeCompare(b.owner); + if (documentSortMode.value === "type") return a.type.localeCompare(b.type); + return b.updatedAt.localeCompare(a.updatedAt); + }); + + return list; }); const selectedDocumentId = ref(documents.value[0]?.id ?? ""); @@ -3387,7 +3367,7 @@ const selectedWorkspaceContactDocuments = computed(() => { const contact = selectedWorkspaceContact.value; if (!contact) return []; return documents.value - .filter((doc) => isDocumentLinkedToContact(doc, contact)) + .filter((doc) => isDocumentLinkedToContact(doc.scope, contact)) .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)); }); @@ -4497,7 +4477,7 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
-
+
+ + +
+ +
+
+
+

+ Contact documents +

+ +
+ +
+
+
+
+

{{ doc.title }}

+ {{ doc.type }} +
+

{{ doc.summary }}

+
+

Updated {{ formatStamp(doc.updatedAt) }}

+ +
+
+

+ No linked documents. +

+
+
+ +
-
-
-
- +
+
+ + +
+
+
+

{{ selectedDocument.title }}

+

+ {{ selectedDocument.type }} · {{ formatDocumentScope(selectedDocument.scope) }} · {{ selectedDocument.owner }} +

+

{{ selectedDocument.summary }}

+
+ +
+ +
-
- +
+ No document selected.
-
- -
- No document selected. -
-
-
-
+ +
+
createCommunication(context.auth, args.input), + createWorkspaceDocument: async ( + args: { + input: { + title: string; + type?: string; + owner?: string; + scope: string; + summary: string; + body?: string; + }; + }, + context: GraphQLContext, + ) => createWorkspaceDocument(context.auth, args.input), + updateCommunicationTranscript: async ( args: { id: string; transcript: string[] }, context: GraphQLContext,