@wordpress/editor
Version:
Enhanced block editor for WordPress posts.
313 lines (312 loc) • 9.53 kB
JavaScript
// 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