UNPKG

@wordpress/editor

Version:
313 lines (312 loc) 9.53 kB
// packages/editor/src/components/collab-sidebar/hooks.js import { __ } from "@wordpress/i18n"; import { useState, useEffect, useMemo, useSyncExternalStore } from "@wordpress/element"; import { useEntityRecords, store as coreStore } from "@wordpress/core-data"; import { useDispatch, useRegistry, useSelect } from "@wordpress/data"; import { store as blockEditorStore, privateApis as blockEditorPrivateApis } from "@wordpress/block-editor"; import { store as noticesStore } from "@wordpress/notices"; import { getScrollContainer } from "@wordpress/dom"; import { decodeEntities } from "@wordpress/html-entities"; import { store as interfaceStore } from "@wordpress/interface"; import { store as editorStore } from "../../store/index.mjs"; import { FLOATING_NOTES_SIDEBAR } from "./constants.mjs"; import { unlock } from "../../lock-unlock.mjs"; import { createBoardStore } from "./board-store.mjs"; import { calculateNotePositions } from "./utils.mjs"; var { cleanEmptyObject } = unlock(blockEditorPrivateApis); function useNoteThreads(postId) { const queryArgs = { post: postId, type: "note", status: "all", per_page: -1 }; const { records: threads } = useEntityRecords( "root", "comment", queryArgs, { enabled: !!postId && typeof postId === "number" } ); const { getBlockAttributes } = useSelect(blockEditorStore); const { clientIds } = useSelect((select) => { const { getClientIdsWithDescendants } = select(blockEditorStore); return { clientIds: getClientIdsWithDescendants() }; }, []); const { notes, unresolvedNotes } = useMemo(() => { if (!threads || threads.length === 0) { return { notes: [], unresolvedNotes: [] }; } const blocksWithNotes = {}; const clientIdByNoteId = /* @__PURE__ */ new Map(); for (const clientId of clientIds) { const noteId = getBlockAttributes(clientId)?.metadata?.noteId; if (noteId) { const key = String(noteId); blocksWithNotes[clientId] = key; clientIdByNoteId.set(key, clientId); } } const threadsById = /* @__PURE__ */ new Map(); const rootThreads = []; for (const item of threads) { const thread = { ...item, reply: [], blockClientId: item.parent === 0 ? clientIdByNoteId.get(String(item.id)) ?? null : null }; threadsById.set(item.id, thread); if (item.parent === 0) { rootThreads.push(thread); } } for (const item of threads) { if (item.parent !== 0) { threadsById.get(item.parent)?.reply.unshift(threadsById.get(item.id)); } } if (rootThreads.length === 0) { return { notes: [], unresolvedNotes: [] }; } const unresolved = []; const resolved = []; for (const noteId of Object.values(blocksWithNotes)) { const thread = threadsById.get(Number(noteId)) ?? threadsById.get(noteId); if (!thread) { continue; } if (thread.status === "hold") { unresolved.push(thread); } else if (thread.status === "approved") { resolved.push(thread); } } const orphans = rootThreads.filter( (thread) => !thread.blockClientId ); return { notes: [...unresolved, ...resolved, ...orphans], unresolvedNotes: unresolved }; }, [clientIds, threads, getBlockAttributes]); return { notes, unresolvedNotes }; } function useNoteActions() { const { createNotice } = useDispatch(noticesStore); const { saveEntityRecord, deleteEntityRecord } = useDispatch(coreStore); const { getCurrentPostId } = useSelect(editorStore); const { getBlockAttributes, getSelectedBlockClientId } = useSelect(blockEditorStore); const { updateBlockAttributes } = useDispatch(blockEditorStore); const onError = (error) => { const errorMessage = error.message && error.code !== "unknown_error" ? decodeEntities(error.message) : __("An error occurred while performing an update."); createNotice("error", errorMessage, { type: "snackbar", isDismissible: true }); }; const onCreate = async ({ content, parent }) => { try { const savedRecord = await saveEntityRecord( "root", "comment", { post: getCurrentPostId(), content, status: "hold", type: "note", parent: parent || 0 }, { throwOnError: true } ); if (!parent && savedRecord?.id) { const clientId = getSelectedBlockClientId(); const metadata = getBlockAttributes(clientId)?.metadata; updateBlockAttributes(clientId, { metadata: { ...metadata, noteId: savedRecord.id } }); } createNotice( "snackbar", parent ? __("Reply added.") : __("Note added."), { type: "snackbar", isDismissible: true } ); return savedRecord; } catch (error) { onError(error); } }; const onEdit = async ({ id, content, status }) => { const messageType = status ? status : "updated"; const messages = { approved: __("Note marked as resolved."), hold: __("Note reopened."), updated: __("Note updated.") }; try { if (status === "approved" || status === "hold") { await saveEntityRecord( "root", "comment", { id, status }, { throwOnError: true } ); const newNoteData = { post: getCurrentPostId(), content: content || "", // Empty content for resolve, content for reopen. type: "note", status, parent: id, meta: { _wp_note_status: status === "approved" ? "resolved" : "reopen" } }; await saveEntityRecord("root", "comment", newNoteData, { throwOnError: true }); } else { const updateData = { id, content, status }; await saveEntityRecord("root", "comment", updateData, { throwOnError: true }); } createNotice( "snackbar", messages[messageType] ?? __("Note updated."), { type: "snackbar", isDismissible: true } ); } catch (error) { onError(error); } }; const onDelete = async (note) => { try { await deleteEntityRecord("root", "comment", note.id, void 0, { throwOnError: true }); if (!note.parent) { const clientId = getSelectedBlockClientId(); const metadata = getBlockAttributes(clientId)?.metadata; updateBlockAttributes(clientId, { metadata: cleanEmptyObject({ ...metadata, noteId: void 0 }) }); } createNotice("snackbar", __("Note deleted."), { type: "snackbar", isDismissible: true }); } catch (error) { onError(error); } }; return { onCreate, onEdit, onDelete }; } function useEnableFloatingSidebar(enabled = false) { const registry = useRegistry(); useEffect(() => { if (!enabled) { return; } const { getActiveComplementaryArea } = registry.select(interfaceStore); const { disableComplementaryArea, enableComplementaryArea } = registry.dispatch(interfaceStore); const unsubscribe = registry.subscribe(() => { if (getActiveComplementaryArea("core") === null) { enableComplementaryArea("core", FLOATING_NOTES_SIDEBAR); } }); return () => { unsubscribe(); if (getActiveComplementaryArea("core") === FLOATING_NOTES_SIDEBAR) { disableComplementaryArea("core", FLOATING_NOTES_SIDEBAR); } }; }, [enabled, registry]); } function useFloatingBoard({ threads, selectedNoteId, isFloating, sidebarRef }) { const [notePositions, setNotePositions] = useState({}); const [store] = useState(createBoardStore); const heights = useSyncExternalStore(store.subscribe, store.getSnapshot); useEffect(() => { if (!isFloating || !sidebarRef?.current) { return; } const panel = sidebarRef.current; const blockEl = store.getFirstBlockElement(); const rootEl = blockEl?.closest(".is-root-container") ?? blockEl; const canvas = rootEl ? getScrollContainer(rootEl) : null; const applyScroll = () => { panel.style.setProperty( "--canvas-scroll", `${-(canvas?.scrollTop ?? 0)}px` ); }; const rafId = window.requestAnimationFrame(() => { const result = calculateNotePositions({ threads, selectedNoteId, blockRects: store.getBlockRects(), heights, scrollTop: canvas?.scrollTop ?? 0 }); setNotePositions(result.positions); applyScroll(); }); const view = canvas?.ownerDocument?.defaultView; const listenerOptions = { passive: true, capture: true }; view?.addEventListener("scroll", applyScroll, listenerOptions); return () => { window.cancelAnimationFrame(rafId); view?.removeEventListener("scroll", applyScroll, listenerOptions); }; }, [sidebarRef, heights, isFloating, selectedNoteId, store, threads]); return { notePositions, registerThread: store.registerThread, unregisterThread: store.unregisterThread }; } export { useEnableFloatingSidebar, useFloatingBoard, useNoteActions, useNoteThreads }; //# sourceMappingURL=hooks.mjs.map