feat: Implement document permission handling and sharing features
This commit is contained in:
@@ -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();
|
||||
},
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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' : ''}`}
|
||||
|
||||
@@ -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 };
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user