Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion client/src/components/remixer/EditPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,13 @@ function sanitizeRemixerTitle(
trim: boolean = true,
allowColon = false,
): string {
let s = allowColon ? value : value.replace(/:/g, "");
let s = value;
if (!allowColon) {
const colonIdx = value.indexOf(":");
if (colonIdx !== -1) {
s = value.slice(colonIdx + 1);
}
}
Comment on lines +33 to +39
return trim ? s.trim() : s;
}

Expand Down
25 changes: 23 additions & 2 deletions client/src/components/remixer/PublishPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect, useMemo, useRef } from "react";
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Icon, List, Progress } from "semantic-ui-react";
import { RemixerSubPage } from "./model";
import { appendSiblingTitleSuffix } from "./services";
Expand All @@ -21,6 +21,8 @@ interface SummarySection {
items: RemixerSubPage[];
}

const NEAR_BOTTOM_PX = 24;

const PublishPanel: React.FC<PublishPanelProps> = ({
open,
dimmer,
Expand All @@ -32,10 +34,27 @@ const PublishPanel: React.FC<PublishPanelProps> = ({
publishMessages = [],
}) => {
const messagesEndRef = useRef<HTMLDivElement>(null);
const messagesContainerRef = useRef<HTMLDivElement>(null);
const [autoScroll, setAutoScroll] = useState(true);

useEffect(() => {
if (publishStatus === "idle" || publishMessages.length === 0) {
setAutoScroll(true);
}
}, [publishStatus, publishMessages.length]);

useEffect(() => {
if (!autoScroll) return;
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, [publishMessages]);
}, [publishMessages, autoScroll]);

const handleMessagesScroll = useCallback(() => {
const el = messagesContainerRef.current;
if (!el) return;
const distanceFromBottom =
el.scrollHeight - el.scrollTop - el.clientHeight;
setAutoScroll(distanceFromBottom <= NEAR_BOTTOM_PX);
}, []);

const publish = () => {
handlePublish();
Expand Down Expand Up @@ -176,6 +195,8 @@ const PublishPanel: React.FC<PublishPanelProps> = ({
)}
{publishMessages.length > 0 ? (
<div
ref={messagesContainerRef}
onScroll={handleMessagesScroll}
style={{
maxHeight: 180,
overflowY: "auto",
Expand Down
6 changes: 3 additions & 3 deletions client/src/components/remixer/RemixerDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1671,7 +1671,7 @@ const RemixerDashboard: React.FC = () => {
};

/** Discard local/server drafts and reload the book from source. Resets undo/redo and UI panels. */
const startOverMutation = useMutation({
const {mutate: startOverMutation, isPending: isStartOverPending} = useMutation({
mutationFn: async () => {
clearLocalDraft(id);
serverStateRef.current = null;
Expand Down Expand Up @@ -1725,7 +1725,7 @@ const RemixerDashboard: React.FC = () => {
cancelText="Keep Changes"
onCancel={closeAllModals}
onConfirm={() => {
startOverMutation.mutate();
startOverMutation();
closeAllModals();
}}
/>
Expand Down Expand Up @@ -2364,7 +2364,7 @@ const RemixerDashboard: React.FC = () => {
canRedo={redoStack.length > 0}
/>
</Stack>
{remixerData.currentBook ? (
{(remixerData.currentBook && !isStartOverPending) ? (
<TreeDnd
expandedNodeIds={expandedNodeIdsBook}
setExpandedNodeIds={setExpandedNodeIdsBook}
Expand Down
98 changes: 68 additions & 30 deletions server/api/services/remixer-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -507,7 +507,7 @@ const getDisplayTitle = (
autoNumbering: boolean,
): string => {
const rawTitle = page.title || page["@title"] || "Untitled";
if (page.parentID === "-1" && page.article === "topic-category") {
if (page.parentID === "-1" ) {
return rawTitle;
}
const cleanTitle = stripDefaultTitlePrefixBeforeColon(
Expand Down Expand Up @@ -545,17 +545,66 @@ const applyDefaultRemixerPageProperties = async (
await addPageProperty(subdomain, pageID, "WelcomeHidden", true);
};

/** Book root → topic-category; cover children → topic-guide; everyone else → topic. */
type RemixerArticleKind = "topic-category" | "topic-guide" | "topic";

const articleKindForPlacement = (
pageId: string | undefined,
parentId: string | undefined,
coverId?: string,
): RemixerArticleKind => {
if (coverId && (pageId === coverId || pageId === "-1")) {
return "topic-category";
}
if (coverId && parentId === coverId) {
return "topic-guide";
}
return "topic";
};

const contentTemplateForArticleKind = (kind: RemixerArticleKind): string => {
if (kind === "topic-category") {
return RemixerTemplates.POST_CreateBlankTopicCategory;
}
if (kind === "topic-guide") {
return RemixerTemplates.POST_CreateBlankTopicGuide;
}
return RemixerTemplates.POST_CreateBlankPage("topic");
};

const localArticleField = (
kind: RemixerArticleKind,
): RemixerSubPageState["article"] => (kind === "topic" ? "article" : kind);

const applyArticleKindToPage = async (
page: RemixerSubPageState,
kind: RemixerArticleKind,
subdomain: string,
coverId: string,
): Promise<void> => {
const bookService = new BookService({
bookID: `${subdomain}-${coverId}`,
});
const tag =
kind === "topic" ? "article:topic" : (`article:${kind}` as const);
await bookService.updatePageDetails(page["@id"], undefined, [tag]);
await bookService.activateShowOrg(
page["@id"],
kind === "topic-guide" || kind === "topic-category",
);
page.article = localArticleField(kind);
};

const handleNewPage = async (
page: RemixerSubPageState,
parent: RemixerSubPageState,
title: string,
subdomain: string,
coverId?: string,
): Promise<{ pageID: string; pageURI: string }> => {
const content =
page["@subpages"] === true || parent["@id"] === coverId
? RemixerTemplates.POST_CreateBlankTopicGuide
: RemixerTemplates.POST_CreateBlankPage("topic");
const kind = articleKindForPlacement(page["@id"], parent["@id"], coverId);
const content = contentTemplateForArticleKind(kind);
page.article = localArticleField(kind);
const rawTitle = page["@title"] || page.title || title;
// segment must be un-encoded here — we double-encode the full path below,
// matching CXOneFetch's encodeURIComponent(encodeURIComponent(path)) convention.
Expand Down Expand Up @@ -620,11 +669,11 @@ const remixerPagePaddedSlug = (
isBookRoot: boolean = false,
): string => {
const rawTitle = page["@title"] || page.title || displayTitle;
if (isBookRoot && page.article === "topic-category") {
if (isBookRoot ) {
return rawTitle
.toLowerCase()
.replace(/ /g, "-")
.replace(/[\:\.\-]/g, "");
.replace(/[\:\.\-]/g, "_");
}
return buildRemixerPagePathSegment(page, rawTitle, page.siblingTitleIndex);
};
Expand Down Expand Up @@ -753,22 +802,10 @@ const handleModifiedPage = async (
throwForMindTouchResponse(response, "Error moving/renaming page");
}

// Match handleNewPage article types based on placement under the cover.
// Placement: cover children are topic-guide; nested pages are topic.
if (isMoved && coverId && parent) {
const bookService = new BookService({
bookID: `${subdomain}-${coverId}`,
});
if (parent["@id"] === coverId) {
await bookService.updatePageDetails(pageId, undefined, [
"article:topic-guide",
]);
await bookService.activateShowOrg(pageId, true);
page.article = "topic-guide";
} else {
await bookService.updatePageDetails(pageId, undefined, ["article:topic"]);
await bookService.activateShowOrg(pageId, false);
page.article = "article";
}
const kind = articleKindForPlacement(page["@id"], parent["@id"], coverId);
await applyArticleKindToPage(page, kind, subdomain, coverId);
}
};

Expand Down Expand Up @@ -1051,13 +1088,8 @@ const handleImportedPage = async (
);
postComment = "Remixer fork";
}
// hasChildren is true if the page has any children
if (hasChildren) {
contentsBody = RemixerTemplates.POST_CreateBlankTopicGuide + contentsBody;
} else {
contentsBody =
RemixerTemplates.POST_CreateBlankPage("topic") + contentsBody;
}
const kind = articleKindForPlacement(page["@id"], parent["@id"], coverId);
contentsBody = contentTemplateForArticleKind(kind) + contentsBody;
const postRes = await CXOneFetch({
scope: "page",
path: parseInt(pageID, 10),
Expand Down Expand Up @@ -1472,7 +1504,7 @@ const runRemixerJob = async ({
inDeletedBranch,
autoNumbering,
);

console.log("title", title);
const status = getPageStatus(page);
const shouldSkip = shouldSkipPage(page, inMatterBranch, status);

Expand All @@ -1492,6 +1524,9 @@ const runRemixerJob = async ({

try {
if (status === "new") {
if(shouldSkip) {
return "success";
}
const parentId = page.parentID ?? "-1";
Comment on lines 1526 to 1530
const parent = parentId !== "-1" ? byId.get(parentId) : undefined;
if (parent) {
Expand All @@ -1516,6 +1551,9 @@ const runRemixerJob = async ({
await orderPageAfterPreviousSibling(pageID, page, pages, subdomain);
}
} else if (status === "imported") {
if(shouldSkip) {
return "success";
}
const parentId = page.parentID ?? "-1";
Comment on lines 1553 to 1557
const parent = parentId !== "-1" ? byId.get(parentId) : undefined;
if (parent) {
Expand Down
15 changes: 11 additions & 4 deletions server/util/remixerutils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,14 @@ export const buildRemixerPagePathSegment = (
: "";
// Prefer formattedPath so autoNumbering `start` (incl. 0 → `00%3A_…`) is honored.
const numbering =
page.formattedPath?.trim() || page.numberedPath?.trim() || "";
return numbering
? `${numbering.padStart(2, "0")}:_${titleSegment}${siblingTitleIndexPostfix}`
page.numberedPath?.trim() ||page.formattedPath?.trim() || "";
const parts = numbering.split(".");
if (parts.length > 0) {
parts[parts.length - 1] = parts[parts.length - 1]!.padStart(2, "0");
}
const paddedNumbering = numbering ? parts.join(".") : "";
Comment on lines 61 to +68
return paddedNumbering
? `${paddedNumbering}:_${titleSegment}${siblingTitleIndexPostfix}`
: titleSegment;
};
export const generatePagePath = (parent: string, title: string): string => {
Expand Down Expand Up @@ -128,7 +133,9 @@ export const getPageStatus = (page: RemixerSubPageState): RemixerPageStatus => {
export const shouldSkipPage = (page: RemixerSubPageState, inMatterBranch: boolean, status: RemixerPageStatus): boolean => {
const pathLen = page.pathNumber?.length ?? 0;
const isBookRoot = pathLen === 0;
return isBookRoot || inMatterBranch || status === "unchanged";
const pageStatus = getPageStatus(page);
const isDeleteNoExisting = page.isDeleted && (pageStatus === "imported" || pageStatus === "new") || false;
return isBookRoot || inMatterBranch || status === "unchanged" || isDeleteNoExisting;
Comment on lines 133 to +138
};

/**
Expand Down
Loading