mirror of
https://github.com/outline/outline.git
synced 2026-06-13 03:14:59 +03:00
235 lines
6.8 KiB
TypeScript
235 lines
6.8 KiB
TypeScript
import isEqual from "fast-deep-equal";
|
|
import orderBy from "lodash/orderBy";
|
|
import { observer } from "mobx-react";
|
|
import * as React from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { useHistory, useRouteMatch } from "react-router-dom";
|
|
import styled from "styled-components";
|
|
import { Pagination } from "@shared/constants";
|
|
import { RevisionHelper } from "@shared/utils/RevisionHelper";
|
|
import Revision from "~/models/Revision";
|
|
import Empty from "~/components/Empty";
|
|
import PaginatedEventList from "~/components/PaginatedEventList";
|
|
import useKeyDown from "~/hooks/useKeyDown";
|
|
import { useLocationSidebarContext } from "~/hooks/useLocationSidebarContext";
|
|
import useStores from "~/hooks/useStores";
|
|
import { documentPath } from "~/utils/routeHelpers";
|
|
import Sidebar from "./SidebarLayout";
|
|
import useMobile from "~/hooks/useMobile";
|
|
import Switch from "~/components/Switch";
|
|
import Text from "@shared/components/Text";
|
|
import usePersistedState from "~/hooks/usePersistedState";
|
|
import Scrollable from "~/components/Scrollable";
|
|
|
|
const DocumentEvents = [
|
|
"documents.publish",
|
|
"documents.unpublish",
|
|
"documents.archive",
|
|
"documents.unarchive",
|
|
"documents.delete",
|
|
"documents.restore",
|
|
"documents.add_user",
|
|
"documents.remove_user",
|
|
"documents.move",
|
|
];
|
|
|
|
function History() {
|
|
const { events, documents, revisions } = useStores();
|
|
const { t } = useTranslation();
|
|
const match = useRouteMatch<{ documentSlug: string }>();
|
|
const history = useHistory();
|
|
const sidebarContext = useLocationSidebarContext();
|
|
const document = documents.get(match.params.documentSlug);
|
|
const [revisionsOffset, setRevisionsOffset] = React.useState(0);
|
|
const [eventsOffset, setEventsOffset] = React.useState(0);
|
|
const isMobile = useMobile();
|
|
|
|
const [defaultShowChanges, setDefaultShowChanges] =
|
|
usePersistedState<boolean>("history-show-changes", true);
|
|
|
|
const searchParams = new URLSearchParams(history.location.search);
|
|
const [showChanges, setShowChanges] = React.useState(
|
|
searchParams.get("changes") === "true" || defaultShowChanges
|
|
);
|
|
|
|
const updateLocation = React.useCallback(
|
|
(changes: Record<string, string | null>) => {
|
|
const params = new URLSearchParams(history.location.search);
|
|
|
|
Object.entries(changes).forEach(([key, value]) => {
|
|
if (value === null) {
|
|
params.delete(key);
|
|
} else {
|
|
params.set(key, value);
|
|
}
|
|
});
|
|
|
|
const search = params.toString();
|
|
history.replace({
|
|
pathname: history.location.pathname,
|
|
search: search ? `?${search}` : "",
|
|
state: history.location.state,
|
|
});
|
|
},
|
|
[history]
|
|
);
|
|
|
|
// Handler for toggling the "Show Changes" switch, updating state and URL parameter
|
|
const handleShowChangesToggle = React.useCallback(
|
|
(checked: boolean) => {
|
|
setShowChanges(checked);
|
|
setDefaultShowChanges(checked);
|
|
updateLocation({ changes: checked ? "true" : null });
|
|
},
|
|
[history]
|
|
);
|
|
|
|
// Ensure that the URL parameter is in sync with the persisted state on mount
|
|
React.useEffect(() => {
|
|
if (defaultShowChanges) {
|
|
updateLocation({ changes: "true" });
|
|
}
|
|
}, [defaultShowChanges]);
|
|
|
|
const fetchHistory = React.useCallback(async () => {
|
|
if (!document) {
|
|
return [];
|
|
}
|
|
|
|
const limit = Pagination.defaultLimit;
|
|
const [revisionsPage, eventsPage] = await Promise.all([
|
|
revisions.fetchPage({
|
|
documentId: document.id,
|
|
offset: revisionsOffset,
|
|
limit,
|
|
}),
|
|
events.fetchPage({
|
|
events: DocumentEvents,
|
|
documentId: document.id,
|
|
offset: eventsOffset,
|
|
limit,
|
|
}),
|
|
]);
|
|
|
|
const pageEvents = orderBy(
|
|
[...revisionsPage, ...eventsPage],
|
|
"createdAt",
|
|
"desc"
|
|
).slice(0, limit);
|
|
|
|
setRevisionsOffset(revisionsOffset + revisionsPage.length);
|
|
setEventsOffset(eventsOffset + pageEvents.length - revisionsPage.length);
|
|
return pageEvents;
|
|
}, [document, revisions, events, revisionsOffset, eventsOffset]);
|
|
|
|
const revisionEvents = React.useMemo(() => {
|
|
if (!document) {
|
|
return [];
|
|
}
|
|
|
|
const latestRevisionId = RevisionHelper.latestId(document.id);
|
|
return revisions
|
|
.getByDocumentId(document.id)
|
|
.filter((revision: Revision) => revision.id !== latestRevisionId)
|
|
.slice(0, revisionsOffset);
|
|
}, [document, revisions.orderedData, revisionsOffset]);
|
|
|
|
const nonRevisionEvents = React.useMemo(
|
|
() =>
|
|
document
|
|
? events.getByDocumentId(document.id).slice(0, eventsOffset)
|
|
: [],
|
|
[document, events.orderedData, eventsOffset]
|
|
);
|
|
|
|
const items = React.useMemo(() => {
|
|
const merged = orderBy(
|
|
[...revisionEvents, ...nonRevisionEvents],
|
|
"createdAt",
|
|
"desc"
|
|
);
|
|
|
|
const latestRevisionEvent = revisionEvents[0];
|
|
|
|
if (latestRevisionEvent && document) {
|
|
const latestRevision = revisions.get(latestRevisionEvent.id);
|
|
|
|
const isDocUpdated =
|
|
latestRevision?.title !== document.title ||
|
|
!isEqual(latestRevision.data, document.data);
|
|
|
|
if (isDocUpdated) {
|
|
const createdById = document.updatedBy?.id ?? "";
|
|
merged.unshift(
|
|
new Revision(
|
|
{
|
|
id: RevisionHelper.latestId(document.id),
|
|
createdAt: document.updatedAt,
|
|
createdById,
|
|
collaboratorIds: [createdById],
|
|
},
|
|
revisions
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
return merged;
|
|
}, [revisions, document, revisionEvents, nonRevisionEvents]);
|
|
|
|
const onCloseHistory = React.useCallback(() => {
|
|
if (isMobile) {
|
|
// Allow closing the history drawer on mobile to view revision content
|
|
return;
|
|
}
|
|
if (document) {
|
|
history.push({
|
|
pathname: documentPath(document),
|
|
state: { sidebarContext },
|
|
});
|
|
} else {
|
|
history.goBack();
|
|
}
|
|
}, [history, document, sidebarContext]);
|
|
|
|
useKeyDown("Escape", onCloseHistory);
|
|
|
|
return (
|
|
<Sidebar title={t("History")} onClose={onCloseHistory} scrollable={false}>
|
|
<Content>
|
|
<Text type="secondary" size="small" as="span">
|
|
<Switch
|
|
label={t("Highlight changes")}
|
|
checked={showChanges}
|
|
onChange={handleShowChangesToggle}
|
|
/>
|
|
</Text>
|
|
</Content>
|
|
<Scrollable hiddenScrollbars topShadow>
|
|
{document ? (
|
|
<PaginatedEventList
|
|
aria-label={t("History")}
|
|
fetch={fetchHistory}
|
|
items={items}
|
|
document={document}
|
|
empty={
|
|
<Content>
|
|
<Empty>{t("No history yet")}</Empty>
|
|
</Content>
|
|
}
|
|
/>
|
|
) : null}
|
|
</Scrollable>
|
|
</Sidebar>
|
|
);
|
|
}
|
|
|
|
const Content = styled.div`
|
|
margin: 0 16px 8px;
|
|
border: 1px solid ${(props) => props.theme.inputBorder};
|
|
border-radius: 8px;
|
|
padding: 8px 8px 0;
|
|
`;
|
|
|
|
export default observer(History);
|