feat(canvas): add shared workspace access and controls
This commit is contained in:
@@ -18,6 +18,10 @@ import type {
|
||||
NormalizedBox,
|
||||
Project,
|
||||
RecognizeObjectResponse,
|
||||
ResolveShareResponse,
|
||||
SharePermission,
|
||||
ShareSettings,
|
||||
ShareVisitor,
|
||||
TextExtractionSource,
|
||||
ProjectListResponse
|
||||
} from "@/domain/design";
|
||||
@@ -148,6 +152,7 @@ type CandaChatHistoryResponse = {
|
||||
|
||||
const canvasDataPrefix = "SHAKKERDATA://";
|
||||
const canvasSaveSessionStorageKey = "img-infinite-canvas.canvas.saveSessionId";
|
||||
const shareVisitorStorageKey = "img-infinite-canvas.share.visitor";
|
||||
|
||||
function isVersionConflictError(error: unknown) {
|
||||
return isUserMessageKey(error, USER_MESSAGE_KEYS.conflict) || httpStatusOf(error) === 409;
|
||||
@@ -157,6 +162,8 @@ async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
const headers = new Headers(init?.headers);
|
||||
headers.set("Content-Type", "application/json");
|
||||
Object.entries(authHeaders()).forEach(([key, value]) => headers.set(key, value));
|
||||
const shareId = activeShareId();
|
||||
if (shareId) headers.set("X-Share-Id", shareId);
|
||||
|
||||
let response: Response;
|
||||
try {
|
||||
@@ -205,6 +212,7 @@ export const designGateway = {
|
||||
},
|
||||
|
||||
async getProject(id: string) {
|
||||
if (activeShareId()) return getProjectDocument(id);
|
||||
try {
|
||||
const response = await request<QueryProjectResponse>("/api/canva/project/queryProject", {
|
||||
method: "POST",
|
||||
@@ -246,6 +254,7 @@ export const designGateway = {
|
||||
},
|
||||
|
||||
async getProjectHistory(id: string, threadId?: string) {
|
||||
if (activeShareId()) return getLegacyProjectHistory(id, threadId);
|
||||
try {
|
||||
const resolvedThreadId = threadId || (await designGateway.queryAgentLastThread(id));
|
||||
if (!resolvedThreadId) return { projectId: id, threadId: "", messages: [] };
|
||||
@@ -262,6 +271,9 @@ export const designGateway = {
|
||||
},
|
||||
|
||||
async listAgentThreads(id: string) {
|
||||
if (activeShareId()) {
|
||||
return request<AgentThreadListResponse>(`/api/projects/${id}/agent/threads`);
|
||||
}
|
||||
try {
|
||||
const response = await request<LovartAgentThreadListResponse>("/api/canva/agent/agentThreadList", {
|
||||
method: "POST",
|
||||
@@ -364,6 +376,7 @@ export const designGateway = {
|
||||
submitGeneratorTask(projectId: string, generatorName: string, inputArgs: Record<string, unknown>) {
|
||||
return request<GeneratorTaskSubmitResponse>("/api/generator/tasks", {
|
||||
method: "POST",
|
||||
headers: { "X-Share-Project-Id": projectId },
|
||||
body: JSON.stringify({
|
||||
cid: createClientId(),
|
||||
project_id: projectId,
|
||||
@@ -408,6 +421,53 @@ export const designGateway = {
|
||||
});
|
||||
},
|
||||
|
||||
resolveShare(shareId: string) {
|
||||
return request<ResolveShareResponse>(`/api/shares/${encodeURIComponent(shareId)}`, {
|
||||
headers: { "X-Share-Visitor": getShareVisitorId() }
|
||||
});
|
||||
},
|
||||
|
||||
getShareSettings(projectId: string) {
|
||||
return request<{ settings: ShareSettings }>(`/api/projects/${projectId}/sharing`);
|
||||
},
|
||||
|
||||
updateShareLink(projectId: string, permission: Exclude<SharePermission, "owner">) {
|
||||
return request<{ settings: ShareSettings }>(`/api/projects/${projectId}/sharing/link`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({ permission })
|
||||
});
|
||||
},
|
||||
|
||||
inviteShareMember(projectId: string, identifier: string, permission: Exclude<SharePermission, "private" | "owner">) {
|
||||
return request<{ settings: ShareSettings }>(`/api/projects/${projectId}/sharing/members`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ identifier, permission })
|
||||
});
|
||||
},
|
||||
|
||||
updateShareMember(projectId: string, memberId: string, permission: Exclude<SharePermission, "private" | "owner">) {
|
||||
return request<{ settings: ShareSettings }>(`/api/projects/${projectId}/sharing/members/${memberId}`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ permission })
|
||||
});
|
||||
},
|
||||
|
||||
deleteShareMember(projectId: string, memberId: string) {
|
||||
return request<{ settings: ShareSettings }>(`/api/projects/${projectId}/sharing/members/${memberId}`, {
|
||||
method: "DELETE"
|
||||
});
|
||||
},
|
||||
|
||||
listShareVisitors(projectId: string) {
|
||||
return request<{ visitors: ShareVisitor[] }>(`/api/projects/${projectId}/sharing/visitors`);
|
||||
},
|
||||
|
||||
revokeShareAccess(projectId: string) {
|
||||
return request<{ settings: ShareSettings }>(`/api/projects/${projectId}/sharing/access`, {
|
||||
method: "DELETE"
|
||||
});
|
||||
},
|
||||
|
||||
projectEvents(projectId: string, handlers: ProjectEventHandlers, options: ProjectEventsOptions = {}) {
|
||||
const controller = new AbortController();
|
||||
let settled = false;
|
||||
@@ -561,6 +621,12 @@ async function getLegacyProjectHistory(id: string, threadId?: string) {
|
||||
}
|
||||
|
||||
async function saveProjectCanvas(projectId: string, payload: object) {
|
||||
if (activeShareId()) {
|
||||
return request<SaveProjectResponse>(`/api/projects/${projectId}/canvas`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
}
|
||||
try {
|
||||
return await request<SaveProjectResponse>("/api/canva/project/saveProject", {
|
||||
method: "POST",
|
||||
@@ -575,6 +641,26 @@ async function saveProjectCanvas(projectId: string, payload: object) {
|
||||
}
|
||||
}
|
||||
|
||||
function activeShareId() {
|
||||
if (typeof window === "undefined") return "";
|
||||
const hashPath = window.location.hash.replace(/^#/, "");
|
||||
const path = hashPath || window.location.pathname;
|
||||
const [pathOnly] = path.split(/[?#]/);
|
||||
const match = pathOnly.replace(/\/+$/, "").match(/^\/share\/([^/]+)$/);
|
||||
return match ? decodeURIComponent(match[1]) : "";
|
||||
}
|
||||
|
||||
function getShareVisitorId() {
|
||||
if (typeof window === "undefined") return "";
|
||||
const stored = window.localStorage.getItem(shareVisitorStorageKey)?.trim();
|
||||
if (stored) return stored;
|
||||
const bytes = new Uint8Array(18);
|
||||
window.crypto.getRandomValues(bytes);
|
||||
const visitorId = Array.from(bytes, (value) => value.toString(16).padStart(2, "0")).join("");
|
||||
window.localStorage.setItem(shareVisitorStorageKey, visitorId);
|
||||
return visitorId;
|
||||
}
|
||||
|
||||
function messageFromCandaHistoryItem(item: CandaChatHistoryResponse["data"][number], fallbackThreadId: string): AgentMessage {
|
||||
const type = item.type || "assistant";
|
||||
const role: AgentMessage["role"] = type === "user" ? "user" : type === "tool_use" || item.tool_call_id ? "tool" : "assistant";
|
||||
|
||||
Reference in New Issue
Block a user