feat: Implement document permission handling and sharing features

This commit is contained in:
M1ngdaXie
2026-01-10 21:19:12 -08:00
parent 6ba18854bf
commit 6b1ed8d11c
13 changed files with 340 additions and 31 deletions

View File

@@ -13,6 +13,11 @@ export type CreateDocumentRequest = {
type: "editor" | "kanban";
}
export type PermissionResponse = {
permission: "view" | "edit";
role: "owner" | "editor" | "viewer";
}
export const documentsApi = {
// List all documents
list: async (): Promise<{ documents: DocumentType[]; total: number }> => {
@@ -67,4 +72,15 @@ export const documentsApi = {
});
if (!response.ok) throw new Error("Failed to update document state");
},
// Get user's permission for a document
getPermission: async (id: string, shareToken?: string): Promise<PermissionResponse> => {
const url = shareToken
? `${API_BASE_URL}/documents/${id}/permission?share=${shareToken}`
: `${API_BASE_URL}/documents/${id}/permission`;
const response = await authFetch(url);
if (!response.ok) throw new Error("Failed to fetch document permission");
return response.json();
},
};

View File

@@ -8,9 +8,12 @@ import Toolbar from "./Toolbar.tsx";
interface EditorProps {
providers: YjsProviders;
permission: string | null;
}
const Editor = ({ providers }: EditorProps) => {
const Editor = ({ providers, permission }: EditorProps) => {
const isEditable = permission !== "view";
const editor = useEditor({
extensions: [
StarterKit.configure({
@@ -28,6 +31,7 @@ const Editor = ({ providers }: EditorProps) => {
}),
],
content: "",
editable: isEditable,
});
useEffect(() => {
@@ -49,8 +53,11 @@ const Editor = ({ providers }: EditorProps) => {
return (
<div className="editor-container">
<Toolbar editor={editor} />
<EditorContent editor={editor} className="editor-content" />
{isEditable && <Toolbar editor={editor} />}
<EditorContent
editor={editor}
className={`editor-content ${!isEditable ? 'view-only' : ''}`}
/>
</div>
);
};

View File

@@ -6,9 +6,11 @@ import './ShareModal.css';
interface ShareModalProps {
documentId: string;
onClose: () => void;
currentPermission?: string;
currentRole?: string;
}
function ShareModal({ documentId, onClose }: ShareModalProps) {
function ShareModal({ documentId, onClose, currentPermission, currentRole }: ShareModalProps) {
const [activeTab, setActiveTab] = useState<'users' | 'link'>('users');
const [shares, setShares] = useState<DocumentShareWithUser[]>([]);
const [shareLink, setShareLink] = useState<ShareLink | null>(null);
@@ -149,6 +151,31 @@ function ShareModal({ documentId, onClose }: ShareModalProps) {
<button className="close-button" onClick={onClose}>×</button>
</div>
{currentRole && (
<div
style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
padding: '12px 16px',
backgroundColor: currentRole === 'owner' ? '#e8f5e9' : currentPermission === 'edit' ? '#fff3e0' : '#e3f2fd',
borderRadius: '8px',
margin: '16px 0',
fontSize: '14px',
fontWeight: '500',
}}
>
<span style={{ fontSize: '18px' }}>
{currentRole === 'owner' ? '👑' : currentPermission === 'edit' ? '✏️' : '👁️'}
</span>
<span>
{currentRole === 'owner' && 'You are the owner'}
{currentRole === 'editor' && 'You have edit access'}
{currentRole === 'viewer' && 'You have view-only access'}
</span>
</div>
)}
<div className="tabs">
<button
className={`tab ${activeTab === 'users' ? 'active' : ''}`}

View File

@@ -7,15 +7,36 @@ import {
type YjsProviders,
} from "../lib/yjs";
import { useAutoSave } from "./useAutoSave";
import { documentsApi } from "../api/document";
export const useYjsDocument = (documentId: string, shareToken?: string) => {
const { user, token } = useAuth();
const [providers, setProviders] = useState<YjsProviders | null>(null);
const [synced, setSynced] = useState(false);
const [permission, setPermission] = useState<string | null>(null);
const [role, setRole] = useState<string | null>(null);
// Enable auto-save when providers are ready
useAutoSave(documentId, providers?.ydoc || null);
// Fetch permission when component mounts
useEffect(() => {
if (!documentId) return;
const fetchPermission = async () => {
try {
const permData = await documentsApi.getPermission(documentId, shareToken);
setPermission(permData.permission);
setRole(permData.role);
console.log(`📋 Permission loaded: ${permData.role} (${permData.permission})`);
} catch (error) {
console.error("Failed to fetch permission:", error);
}
};
fetchPermission();
}, [documentId, shareToken]);
useEffect(() => {
// Wait for auth (unless we have a share token for public access)
if (!shareToken && (!user || !token)) {
@@ -73,6 +94,7 @@ export const useYjsDocument = (documentId: string, shareToken?: string) => {
color: getColorFromUserId(currentId),
avatar: currentAvatar,
});
console.log(currentAvatar)
// NEW: Add awareness event logging
const handleAwarenessChange = ({
@@ -154,5 +176,5 @@ export const useYjsDocument = (documentId: string, shareToken?: string) => {
}, [documentId, user, token, shareToken]);
return { providers, synced };
return { providers, synced, permission, role };
};

View File

@@ -11,7 +11,7 @@ const EditorPage = () => {
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const shareToken = searchParams.get('share') || undefined;
const { providers, synced } = useYjsDocument(id!, shareToken);
const { providers, synced, permission, role } = useYjsDocument(id!, shareToken);
const [showShareModal, setShowShareModal] = useState(false);
if (!providers) {
@@ -24,6 +24,12 @@ const EditorPage = () => {
<div className="page-header">
<button onClick={() => navigate("/")}> Back to Home</button>
<div className="header-actions">
{permission === "view" && (
<div className="view-only-badge" style={{ display: 'flex', alignItems: 'center', gap: '4px', padding: '4px 12px', backgroundColor: '#f0f0f0', borderRadius: '4px', fontSize: '14px' }}>
<span>👁</span>
<span>View only</span>
</div>
)}
<div className="sync-status">
{synced ? "✓ Synced" : "⟳ Syncing..."}
</div>
@@ -37,7 +43,7 @@ const EditorPage = () => {
<div className="page-content">
<div className="main-area">
<Editor providers={providers} />
<Editor providers={providers} permission={permission} />
</div>
<div className="sidebar">
<UserList awareness={providers.awareness} />
@@ -45,7 +51,12 @@ const EditorPage = () => {
</div>
{showShareModal && (
<ShareModal documentId={id!} onClose={() => setShowShareModal(false)} />
<ShareModal
documentId={id!}
onClose={() => setShowShareModal(false)}
currentPermission={permission || undefined}
currentRole={role || undefined}
/>
)}
</div>
);