Refine documents UX and extract document scope helpers
This commit is contained in:
269
frontend/app.vue
269
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<DocumentSortMode>("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")
|
||||
</div>
|
||||
<div
|
||||
class="min-h-0 flex-1"
|
||||
:class="selectedTab === 'communications' && peopleLeftMode === 'contacts' ? 'px-0 pt-0 pb-0' : 'px-3 pt-3 pb-0 md:px-4 md:pt-4 md:pb-0'"
|
||||
:class="selectedTab === 'documents' || (selectedTab === 'communications' && peopleLeftMode === 'contacts') ? 'px-0 pt-0 pb-0' : 'px-3 pt-3 pb-0 md:px-4 md:pt-4 md:pb-0'"
|
||||
>
|
||||
<section
|
||||
v-if="selectedTab === 'communications' && peopleLeftMode === 'calendar'"
|
||||
@@ -5570,7 +5550,66 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
|
||||
|
||||
<aside class="h-full min-h-0">
|
||||
<div class="flex h-full min-h-0 flex-col p-3">
|
||||
<div class="min-h-0 flex-1 space-y-3 overflow-y-auto pr-1">
|
||||
<div
|
||||
v-if="selectedWorkspaceContactDocuments.length"
|
||||
class="mb-2 flex flex-wrap items-center gap-1.5 rounded-xl border border-base-300 bg-base-200/35 px-2 py-1.5"
|
||||
>
|
||||
<button
|
||||
class="badge badge-sm badge-outline"
|
||||
@click="contactRightPanelMode = 'documents'"
|
||||
>
|
||||
{{ selectedWorkspaceContactDocuments.length }} documents
|
||||
</button>
|
||||
<button
|
||||
v-for="doc in selectedWorkspaceContactDocuments.slice(0, 15)"
|
||||
:key="`contact-doc-chip-${doc.id}`"
|
||||
class="rounded-full border border-base-300 bg-base-100 px-2 py-0.5 text-[10px] text-base-content/80 hover:bg-base-200/70"
|
||||
@click="contactRightPanelMode = 'documents'; selectedDocumentId = doc.id"
|
||||
>
|
||||
{{ doc.title }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="contactRightPanelMode === 'documents'" class="min-h-0 flex-1 overflow-y-auto pr-1">
|
||||
<div class="sticky top-0 z-10 border-b border-base-300 bg-base-100 pb-2">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<p class="text-xs font-semibold uppercase tracking-wide text-base-content/60">
|
||||
Contact documents
|
||||
</p>
|
||||
<button class="btn btn-ghost btn-xs" @click="contactRightPanelMode = 'summary'">Summary</button>
|
||||
</div>
|
||||
<input
|
||||
v-model="contactDocumentsSearch"
|
||||
type="text"
|
||||
class="input input-bordered input-xs mt-2 w-full"
|
||||
placeholder="Search documents..."
|
||||
>
|
||||
</div>
|
||||
<div class="mt-2 space-y-1.5">
|
||||
<article
|
||||
v-for="doc in filteredSelectedWorkspaceContactDocuments"
|
||||
:key="`contact-doc-right-${doc.id}`"
|
||||
class="w-full rounded-xl border border-base-300 px-2.5 py-2 text-left transition hover:bg-base-200/50"
|
||||
:class="selectedDocumentId === doc.id ? 'border-primary bg-primary/10' : ''"
|
||||
@click="selectedDocumentId = doc.id"
|
||||
>
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<p class="min-w-0 flex-1 truncate text-xs font-semibold">{{ doc.title }}</p>
|
||||
<span class="shrink-0 text-[10px] text-base-content/55">{{ doc.type }}</span>
|
||||
</div>
|
||||
<p class="mt-0.5 line-clamp-2 text-[11px] text-base-content/70">{{ doc.summary }}</p>
|
||||
<div class="mt-1 flex items-center justify-between gap-2">
|
||||
<p class="text-[10px] text-base-content/55">Updated {{ formatStamp(doc.updatedAt) }}</p>
|
||||
<button class="btn btn-ghost btn-xs px-1" @click.stop="selectedDocumentId = doc.id; openDocumentsTab(true)">Open</button>
|
||||
</div>
|
||||
</article>
|
||||
<p v-if="filteredSelectedWorkspaceContactDocuments.length === 0" class="px-1 py-2 text-xs text-base-content/55">
|
||||
No linked documents.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="min-h-0 flex-1 space-y-3 overflow-y-auto pr-1">
|
||||
<div
|
||||
v-if="selectedWorkspaceDeal"
|
||||
class="rounded-xl border border-base-300 bg-base-200/30 p-2.5"
|
||||
@@ -5653,72 +5692,92 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-else-if="selectedTab === 'documents'" class="flex h-full min-h-0 flex-col gap-3">
|
||||
<div class="rounded-xl border border-base-300 p-3">
|
||||
<div class="grid gap-2 md:grid-cols-[1fr_220px]">
|
||||
<label class="form-control">
|
||||
<span class="label-text text-xs uppercase tracking-wide text-base-content/60">Search docs</span>
|
||||
<input
|
||||
v-model="documentSearch"
|
||||
type="text"
|
||||
class="input input-bordered input-sm"
|
||||
placeholder="Title, owner, scope, content"
|
||||
>
|
||||
</label>
|
||||
<section v-else-if="selectedTab === 'documents'" class="flex h-full min-h-0 flex-col gap-0">
|
||||
<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">
|
||||
<div class="sticky top-0 z-20 border-b border-base-300 bg-base-100 p-2">
|
||||
<div class="flex items-center gap-1">
|
||||
<input
|
||||
v-model="documentSearch"
|
||||
type="text"
|
||||
class="input input-bordered input-sm w-full"
|
||||
placeholder="Search documents"
|
||||
>
|
||||
|
||||
<label class="form-control">
|
||||
<span class="label-text text-xs uppercase tracking-wide text-base-content/60">Type</span>
|
||||
<select v-model="selectedDocumentType" class="select select-bordered select-sm">
|
||||
<option v-for="item in documentTypes" :key="`doc-type-${item}`" :value="item">{{ item }}</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dropdown dropdown-end">
|
||||
<button
|
||||
tabindex="0"
|
||||
class="btn btn-ghost btn-sm btn-square"
|
||||
title="Sort documents"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" class="h-4 w-4 fill-current">
|
||||
<path d="M3 5h18v2H3zm3 6h12v2H6zm4 6h4v2h-4z" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div class="grid min-h-0 flex-1 gap-0 md:grid-cols-12">
|
||||
<aside class="min-h-0 border-r border-base-300 md:col-span-4 flex flex-col">
|
||||
<div class="min-h-0 flex-1 space-y-2 overflow-y-auto p-2">
|
||||
<button
|
||||
v-for="doc in filteredDocuments"
|
||||
:key="doc.id"
|
||||
class="w-full rounded-xl border border-base-300 p-3 text-left transition hover:bg-base-200/60"
|
||||
:class="selectedDocumentId === doc.id ? 'border-primary bg-primary/5' : ''"
|
||||
@click="selectedDocumentId = doc.id"
|
||||
>
|
||||
<p class="font-medium">{{ doc.title }}</p>
|
||||
<p class="mt-1 text-xs text-base-content/60">{{ doc.type }} · {{ doc.owner }}</p>
|
||||
<p class="mt-1 line-clamp-2 text-xs text-base-content/75">{{ doc.summary }}</p>
|
||||
<p class="mt-1 text-xs text-base-content/55">Updated {{ formatStamp(doc.updatedAt) }}</p>
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<article class="min-h-0 md:col-span-8 flex flex-col">
|
||||
<div v-if="selectedDocument" class="flex h-full min-h-0 flex-col p-3 md:p-4">
|
||||
<div class="border-b border-base-300 pb-2">
|
||||
<p class="font-medium">{{ selectedDocument.title }}</p>
|
||||
<p class="text-xs text-base-content/60">
|
||||
{{ selectedDocument.type }} · {{ selectedDocument.scope }} · {{ selectedDocument.owner }}
|
||||
<div tabindex="0" class="dropdown-content z-20 mt-2 w-44 rounded-xl border border-base-300 bg-base-100 p-2 shadow-lg">
|
||||
<p class="px-2 pb-1 text-[11px] font-semibold uppercase tracking-wide text-base-content/55">Sort docs</p>
|
||||
<button
|
||||
v-for="option in documentSortOptions"
|
||||
:key="`document-sort-${option.value}`"
|
||||
class="btn btn-ghost btn-sm w-full justify-between"
|
||||
@click="documentSortMode = option.value"
|
||||
>
|
||||
<span>{{ option.label }}</span>
|
||||
<span v-if="documentSortMode === option.value">✓</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="min-h-0 flex-1 overflow-y-auto p-0">
|
||||
<button
|
||||
v-for="doc in filteredDocuments"
|
||||
:key="doc.id"
|
||||
class="w-full border-b border-base-300 px-3 py-2 text-left transition hover:bg-base-200/40"
|
||||
:class="selectedDocumentId === doc.id ? 'bg-primary/10' : ''"
|
||||
@click="selectedDocumentId = doc.id"
|
||||
>
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<p class="min-w-0 flex-1 truncate text-xs font-semibold">{{ doc.title }}</p>
|
||||
<span class="shrink-0 text-[10px] text-base-content/55">{{ doc.type }}</span>
|
||||
</div>
|
||||
<p class="mt-0.5 truncate text-[11px] text-base-content/75">{{ formatDocumentScope(doc.scope) }}</p>
|
||||
<p class="mt-0.5 line-clamp-2 text-[11px] text-base-content/70">{{ doc.summary }}</p>
|
||||
<p class="mt-1 text-[10px] text-base-content/55">Updated {{ formatStamp(doc.updatedAt) }}</p>
|
||||
</button>
|
||||
<p v-if="filteredDocuments.length === 0" class="px-2 py-2 text-xs text-base-content/55">
|
||||
No documents found.
|
||||
</p>
|
||||
<p class="mt-1 text-sm text-base-content/80">{{ selectedDocument.summary }}</p>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<article class="h-full min-h-0 flex flex-col">
|
||||
<div v-if="selectedDocument" class="flex h-full min-h-0 flex-col p-3 md:p-4">
|
||||
<div class="border-b border-base-300 pb-2">
|
||||
<p class="font-medium">{{ selectedDocument.title }}</p>
|
||||
<p class="text-xs text-base-content/60">
|
||||
{{ selectedDocument.type }} · {{ formatDocumentScope(selectedDocument.scope) }} · {{ selectedDocument.owner }}
|
||||
</p>
|
||||
<p class="mt-1 text-sm text-base-content/80">{{ selectedDocument.summary }}</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 min-h-0 flex-1 overflow-y-auto">
|
||||
<ContactCollaborativeEditor
|
||||
:key="`doc-editor-${selectedDocument.id}`"
|
||||
v-model="selectedDocument.body"
|
||||
:room="`crm-doc-${selectedDocument.id}`"
|
||||
placeholder="Describe policy, steps, rules, and exceptions..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 min-h-0 flex-1 overflow-y-auto">
|
||||
<ContactCollaborativeEditor
|
||||
:key="`doc-editor-${selectedDocument.id}`"
|
||||
v-model="selectedDocument.body"
|
||||
:room="`crm-doc-${selectedDocument.id}`"
|
||||
placeholder="Describe policy, steps, rules, and exceptions..."
|
||||
/>
|
||||
<div v-else class="flex h-full items-center justify-center text-sm text-base-content/60">
|
||||
No document selected.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="flex h-full items-center justify-center text-sm text-base-content/60">
|
||||
No document selected.
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div
|
||||
v-if="reviewActive && selectedTab === 'communications'"
|
||||
|
||||
36
frontend/composables/useWorkspaceDocuments.ts
Normal file
36
frontend/composables/useWorkspaceDocuments.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
export const CONTACT_DOCUMENT_SCOPE_PREFIX = "contact:";
|
||||
|
||||
export function buildContactDocumentScope(contactId: string, contactName: string) {
|
||||
return `${CONTACT_DOCUMENT_SCOPE_PREFIX}${encodeURIComponent(contactId)}:${encodeURIComponent(contactName)}`;
|
||||
}
|
||||
|
||||
export 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,
|
||||
};
|
||||
}
|
||||
|
||||
export function formatDocumentScope(scope: string) {
|
||||
const linked = parseContactDocumentScope(scope);
|
||||
if (!linked) return scope;
|
||||
return linked.contactName ? `Contact · ${linked.contactName}` : "Contact document";
|
||||
}
|
||||
|
||||
export function isDocumentLinkedToContact(
|
||||
scope: string,
|
||||
contact: { id: string; name: string } | null | undefined,
|
||||
) {
|
||||
if (!contact) return false;
|
||||
const linked = parseContactDocumentScope(scope);
|
||||
if (!linked) return false;
|
||||
if (linked.contactId) return linked.contactId === contact.id;
|
||||
return Boolean(linked.contactName && linked.contactName === contact.name);
|
||||
}
|
||||
@@ -600,6 +600,52 @@ async function createCommunication(auth: AuthContext | null, input: {
|
||||
return { ok: true, id: created.id };
|
||||
}
|
||||
|
||||
async function createWorkspaceDocument(auth: AuthContext | null, input: {
|
||||
title: string;
|
||||
type?: string;
|
||||
owner?: string;
|
||||
scope: string;
|
||||
summary: string;
|
||||
body?: string;
|
||||
}) {
|
||||
const ctx = requireAuth(auth);
|
||||
const title = String(input?.title ?? "").trim();
|
||||
const scope = String(input?.scope ?? "").trim();
|
||||
const summary = String(input?.summary ?? "").trim();
|
||||
const body = String(input?.body ?? "").trim();
|
||||
const owner = String(input?.owner ?? "").trim() || "Workspace";
|
||||
const typeRaw = String(input?.type ?? "Template").trim();
|
||||
const allowedTypes = new Set(["Regulation", "Playbook", "Policy", "Template"]);
|
||||
const type = allowedTypes.has(typeRaw) ? typeRaw : "Template";
|
||||
|
||||
if (!title) throw new Error("title is required");
|
||||
if (!scope) throw new Error("scope is required");
|
||||
if (!summary) throw new Error("summary is required");
|
||||
|
||||
const created = await prisma.workspaceDocument.create({
|
||||
data: {
|
||||
teamId: ctx.teamId,
|
||||
title,
|
||||
type: type as any,
|
||||
owner,
|
||||
scope,
|
||||
summary,
|
||||
body: body || summary,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
id: created.id,
|
||||
title: created.title,
|
||||
type: created.type,
|
||||
owner: created.owner,
|
||||
scope: created.scope,
|
||||
updatedAt: created.updatedAt.toISOString(),
|
||||
summary: created.summary,
|
||||
body: created.body,
|
||||
};
|
||||
}
|
||||
|
||||
async function updateCommunicationTranscript(auth: AuthContext | null, id: string, transcript: string[]) {
|
||||
const ctx = requireAuth(auth);
|
||||
const messageId = String(id ?? "").trim();
|
||||
@@ -951,6 +997,7 @@ export const crmGraphqlSchema = buildSchema(`
|
||||
createCalendarEvent(input: CreateCalendarEventInput!): CalendarEvent!
|
||||
archiveCalendarEvent(input: ArchiveCalendarEventInput!): CalendarEvent!
|
||||
createCommunication(input: CreateCommunicationInput!): MutationWithIdResult!
|
||||
createWorkspaceDocument(input: CreateWorkspaceDocumentInput!): WorkspaceDocument!
|
||||
updateCommunicationTranscript(id: ID!, transcript: [String!]!): MutationWithIdResult!
|
||||
updateFeedDecision(id: ID!, decision: String!, decisionNote: String): MutationWithIdResult!
|
||||
}
|
||||
@@ -996,6 +1043,15 @@ export const crmGraphqlSchema = buildSchema(`
|
||||
transcript: [String!]
|
||||
}
|
||||
|
||||
input CreateWorkspaceDocumentInput {
|
||||
title: String!
|
||||
type: String
|
||||
owner: String
|
||||
scope: String!
|
||||
summary: String!
|
||||
body: String
|
||||
}
|
||||
|
||||
type MePayload {
|
||||
user: MeUser!
|
||||
team: MeTeam!
|
||||
@@ -1232,6 +1288,20 @@ export const crmGraphqlRoot = {
|
||||
context: GraphQLContext,
|
||||
) => 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,
|
||||
|
||||
Reference in New Issue
Block a user