Allow viewing diff before revision is written (#5399)
This commit is contained in:
@@ -6,6 +6,7 @@ import { Link } from "react-router-dom";
|
||||
import styled from "styled-components";
|
||||
import { s, ellipsis } from "@shared/styles";
|
||||
import Document from "~/models/Document";
|
||||
import Revision from "~/models/Revision";
|
||||
import DocumentBreadcrumb from "~/components/DocumentBreadcrumb";
|
||||
import DocumentTasks from "~/components/DocumentTasks";
|
||||
import Flex from "~/components/Flex";
|
||||
@@ -19,6 +20,7 @@ type Props = {
|
||||
showLastViewed?: boolean;
|
||||
showParentDocuments?: boolean;
|
||||
document: Document;
|
||||
revision?: Revision;
|
||||
replace?: boolean;
|
||||
to?: LocationDescriptor;
|
||||
};
|
||||
@@ -29,6 +31,7 @@ const DocumentMeta: React.FC<Props> = ({
|
||||
showLastViewed,
|
||||
showParentDocuments,
|
||||
document,
|
||||
revision,
|
||||
children,
|
||||
replace,
|
||||
to,
|
||||
@@ -64,7 +67,16 @@ const DocumentMeta: React.FC<Props> = ({
|
||||
const userName = updatedBy.name;
|
||||
let content;
|
||||
|
||||
if (deletedAt) {
|
||||
if (revision) {
|
||||
content = (
|
||||
<span>
|
||||
{revision.createdBy?.id === user.id
|
||||
? t("You updated")
|
||||
: t("{{ userName }} updated", { userName })}{" "}
|
||||
<Time dateTime={revision.createdAt} addSuffix />
|
||||
</span>
|
||||
);
|
||||
} else if (deletedAt) {
|
||||
content = (
|
||||
<span>
|
||||
{lastUpdatedByCurrentUser
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
PublishIcon,
|
||||
MoveIcon,
|
||||
UnpublishIcon,
|
||||
LightningIcon,
|
||||
} from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -61,18 +60,15 @@ const EventListItem = ({ event, latest, document, ...rest }: Props) => {
|
||||
switch (event.name) {
|
||||
case "revisions.create":
|
||||
icon = <EditIcon size={16} />;
|
||||
meta = t("{{userName}} edited", opts);
|
||||
meta = latest ? (
|
||||
<>
|
||||
{t("Current version")} · {event.actor.name}
|
||||
</>
|
||||
) : (
|
||||
t("{{userName}} edited", opts)
|
||||
);
|
||||
to = {
|
||||
pathname: documentHistoryPath(document, event.modelId || ""),
|
||||
state: { retainScrollPosition: true },
|
||||
};
|
||||
break;
|
||||
|
||||
case "documents.live_editing":
|
||||
icon = <LightningIcon size={16} />;
|
||||
meta = t("Latest");
|
||||
to = {
|
||||
pathname: documentHistoryPath(document),
|
||||
pathname: documentHistoryPath(document, event.modelId || "latest"),
|
||||
state: { retainScrollPosition: true },
|
||||
};
|
||||
break;
|
||||
@@ -153,7 +149,7 @@ const EventListItem = ({ event, latest, document, ...rest }: Props) => {
|
||||
</Subtitle>
|
||||
}
|
||||
actions={
|
||||
isRevision && isActive && event.modelId ? (
|
||||
isRevision && isActive && event.modelId && !latest ? (
|
||||
<RevisionMenu document={document} revisionId={event.modelId} />
|
||||
) : undefined
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { useLocation, RouteComponentProps, StaticContext } from "react-router";
|
||||
import { NavigationNode, TeamPreference } from "@shared/types";
|
||||
import { RevisionHelper } from "@shared/utils/RevisionHelper";
|
||||
import Document from "~/models/Document";
|
||||
import Revision from "~/models/Revision";
|
||||
import Error404 from "~/scenes/Error404";
|
||||
@@ -59,7 +60,14 @@ function DataLoader({ match, children }: Props) {
|
||||
documents.getByUrl(match.params.documentSlug) ??
|
||||
documents.get(match.params.documentSlug);
|
||||
|
||||
const revision = revisionId ? revisions.get(revisionId) : undefined;
|
||||
const revision = revisionId
|
||||
? revisions.get(
|
||||
revisionId === "latest"
|
||||
? RevisionHelper.latestId(document?.id)
|
||||
: revisionId
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const sharedTree = document
|
||||
? documents.getSharedTree(document.id)
|
||||
: undefined;
|
||||
@@ -94,6 +102,19 @@ function DataLoader({ match, children }: Props) {
|
||||
fetchRevision();
|
||||
}, [revisions, revisionId]);
|
||||
|
||||
React.useEffect(() => {
|
||||
async function fetchRevision() {
|
||||
if (document && revisionId === "latest") {
|
||||
try {
|
||||
await revisions.fetchLatest(document.id);
|
||||
} catch (err) {
|
||||
setError(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
fetchRevision();
|
||||
}, [document, revisionId, revisions]);
|
||||
|
||||
React.useEffect(() => {
|
||||
async function fetchSubscription() {
|
||||
if (document?.id && !revisionId) {
|
||||
|
||||
@@ -451,8 +451,8 @@ class DocumentScene extends React.Component<Props> {
|
||||
<Header
|
||||
document={document}
|
||||
documentHasHeadings={hasHeadings}
|
||||
revision={revision}
|
||||
shareId={shareId}
|
||||
isRevision={!!revision}
|
||||
isDraft={document.isDraft}
|
||||
isEditing={!readOnly && !team?.seamlessEditing}
|
||||
isSaving={this.isSaving}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Link, useRouteMatch } from "react-router-dom";
|
||||
import styled from "styled-components";
|
||||
import { TeamPreference } from "@shared/types";
|
||||
import Document from "~/models/Document";
|
||||
import Revision from "~/models/Revision";
|
||||
import DocumentMeta from "~/components/DocumentMeta";
|
||||
import Fade from "~/components/Fade";
|
||||
import useStores from "~/hooks/useStores";
|
||||
@@ -15,12 +16,19 @@ import { documentPath, documentInsightsPath } from "~/utils/routeHelpers";
|
||||
type Props = {
|
||||
/* The document to display meta data for */
|
||||
document: Document;
|
||||
revision?: Revision;
|
||||
isDraft: boolean;
|
||||
to?: LocationDescriptor;
|
||||
rtl?: boolean;
|
||||
};
|
||||
|
||||
function TitleDocumentMeta({ to, isDraft, document, ...rest }: Props) {
|
||||
function TitleDocumentMeta({
|
||||
to,
|
||||
isDraft,
|
||||
document,
|
||||
revision,
|
||||
...rest
|
||||
}: Props) {
|
||||
const { auth, views, comments, ui } = useStores();
|
||||
const { t } = useTranslation();
|
||||
const { team } = auth;
|
||||
@@ -36,7 +44,7 @@ function TitleDocumentMeta({ to, isDraft, document, ...rest }: Props) {
|
||||
const commentsCount = comments.inDocument(document.id).length;
|
||||
|
||||
return (
|
||||
<Meta document={document} to={to} replace {...rest}>
|
||||
<Meta document={document} revision={revision} to={to} replace {...rest}>
|
||||
{team?.getPreference(TeamPreference.Commenting) && (
|
||||
<>
|
||||
•
|
||||
|
||||
@@ -14,6 +14,7 @@ import styled from "styled-components";
|
||||
import { NavigationNode } from "@shared/types";
|
||||
import { Theme } from "~/stores/UiStore";
|
||||
import Document from "~/models/Document";
|
||||
import Revision from "~/models/Revision";
|
||||
import { Action, Separator } from "~/components/Actions";
|
||||
import Badge from "~/components/Badge";
|
||||
import Button from "~/components/Button";
|
||||
@@ -40,11 +41,11 @@ import ShareButton from "./ShareButton";
|
||||
type Props = {
|
||||
document: Document;
|
||||
documentHasHeadings: boolean;
|
||||
revision: Revision | undefined;
|
||||
sharedTree: NavigationNode | undefined;
|
||||
shareId: string | null | undefined;
|
||||
isDraft: boolean;
|
||||
isEditing: boolean;
|
||||
isRevision: boolean;
|
||||
isSaving: boolean;
|
||||
isPublishing: boolean;
|
||||
publishingIsDisabled: boolean;
|
||||
@@ -65,11 +66,11 @@ type Props = {
|
||||
function DocumentHeader({
|
||||
document,
|
||||
documentHasHeadings,
|
||||
revision,
|
||||
shareId,
|
||||
isEditing,
|
||||
isDraft,
|
||||
isPublishing,
|
||||
isRevision,
|
||||
isSaving,
|
||||
savingIsDisabled,
|
||||
publishingIsDisabled,
|
||||
@@ -83,6 +84,7 @@ function DocumentHeader({
|
||||
const { resolvedTheme } = ui;
|
||||
const { team } = auth;
|
||||
const isMobile = useMobile();
|
||||
const isRevision = !!revision;
|
||||
|
||||
// We cache this value for as long as the component is mounted so that if you
|
||||
// apply a template there is still the option to replace it until the user
|
||||
@@ -287,7 +289,7 @@ function DocumentHeader({
|
||||
</Button>
|
||||
</Action>
|
||||
)}
|
||||
{isRevision && (
|
||||
{revision && revision.createdAt !== document.updatedAt && (
|
||||
<Action>
|
||||
<Tooltip
|
||||
tooltip={t("Restore version")}
|
||||
|
||||
@@ -3,6 +3,7 @@ import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useHistory, useRouteMatch } from "react-router-dom";
|
||||
import styled from "styled-components";
|
||||
import { RevisionHelper } from "@shared/utils/RevisionHelper";
|
||||
import Event from "~/models/Event";
|
||||
import Empty from "~/components/Empty";
|
||||
import PaginatedEventList from "~/components/PaginatedEventList";
|
||||
@@ -41,8 +42,8 @@ function History() {
|
||||
eventsInDocument.unshift(
|
||||
new Event(
|
||||
{
|
||||
id: "live",
|
||||
name: "documents.live_editing",
|
||||
id: RevisionHelper.latestId(document.id),
|
||||
name: "revisions.create",
|
||||
documentId: document.id,
|
||||
createdAt: document.updatedAt,
|
||||
actor: document.updatedBy,
|
||||
|
||||
@@ -20,18 +20,17 @@ type Props = Omit<EditorProps, "extensions"> & {
|
||||
* Displays revision HTML pre-rendered on the server.
|
||||
*/
|
||||
function RevisionViewer(props: Props) {
|
||||
const { document, shareId, children, revision } = props;
|
||||
const { document, children, revision } = props;
|
||||
|
||||
return (
|
||||
<Flex auto column>
|
||||
<h1 dir={revision.dir}>{revision.title}</h1>
|
||||
{!shareId && (
|
||||
<DocumentMeta
|
||||
document={document}
|
||||
to={documentPath(document)}
|
||||
rtl={revision.rtl}
|
||||
/>
|
||||
)}
|
||||
<DocumentMeta
|
||||
document={document}
|
||||
revision={revision}
|
||||
to={documentPath(document)}
|
||||
rtl={revision.rtl}
|
||||
/>
|
||||
<EditorContainer
|
||||
dangerouslySetInnerHTML={{ __html: revision.html }}
|
||||
dir={revision.dir}
|
||||
|
||||
@@ -47,6 +47,16 @@ export default class RevisionsStore extends BaseStore<Revision> {
|
||||
return revisions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the latest revision for the given document.
|
||||
*
|
||||
* @returns A promise that resolves to the latest revision for the given document
|
||||
*/
|
||||
fetchLatest = async (documentId: string) => {
|
||||
const res = await client.post(`/revisions.info`, { documentId });
|
||||
return this.add(res.data);
|
||||
};
|
||||
|
||||
@action
|
||||
fetchPage = async (
|
||||
options: PaginationParams | undefined
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FindOptions, Op } from "sequelize";
|
||||
import { Op, SaveOptions } from "sequelize";
|
||||
import {
|
||||
DataType,
|
||||
BelongsTo,
|
||||
@@ -74,24 +74,26 @@ class Revision extends IdModel {
|
||||
});
|
||||
}
|
||||
|
||||
static buildFromDocument(document: Document) {
|
||||
return this.build({
|
||||
title: document.title,
|
||||
text: document.text,
|
||||
userId: document.lastModifiedById,
|
||||
editorVersion: document.editorVersion,
|
||||
version: document.version,
|
||||
documentId: document.id,
|
||||
// revision time is set to the last time document was touched as this
|
||||
// handler can be debounced in the case of an update
|
||||
createdAt: document.updatedAt,
|
||||
});
|
||||
}
|
||||
|
||||
static createFromDocument(
|
||||
document: Document,
|
||||
options?: FindOptions<Revision>
|
||||
options?: SaveOptions<Revision>
|
||||
) {
|
||||
return this.create(
|
||||
{
|
||||
title: document.title,
|
||||
text: document.text,
|
||||
userId: document.lastModifiedById,
|
||||
editorVersion: document.editorVersion,
|
||||
version: document.version,
|
||||
documentId: document.id,
|
||||
// revision time is set to the last time document was touched as this
|
||||
// handler can be debounced in the case of an update
|
||||
createdAt: document.updatedAt,
|
||||
},
|
||||
options
|
||||
);
|
||||
const revision = this.buildFromDocument(document);
|
||||
return revision.save(options);
|
||||
}
|
||||
|
||||
// instance methods
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
import Router from "koa-router";
|
||||
import { Op } from "sequelize";
|
||||
import { ValidationError } from "@server/errors";
|
||||
import auth from "@server/middlewares/authentication";
|
||||
import { Document, Revision } from "@server/models";
|
||||
import DocumentHelper from "@server/models/helpers/DocumentHelper";
|
||||
import { authorize } from "@server/policies";
|
||||
import { presentRevision } from "@server/presenters";
|
||||
import { APIContext } from "@server/types";
|
||||
import slugify from "@server/utils/slugify";
|
||||
import { assertPresent, assertSort, assertUuid } from "@server/validation";
|
||||
import pagination from "./middlewares/pagination";
|
||||
|
||||
const router = new Router();
|
||||
|
||||
router.post("revisions.info", auth(), async (ctx: APIContext) => {
|
||||
const { id } = ctx.request.body;
|
||||
assertUuid(id, "id is required");
|
||||
const { user } = ctx.state.auth;
|
||||
const revision = await Revision.findByPk(id, {
|
||||
rejectOnEmpty: true,
|
||||
});
|
||||
|
||||
const document = await Document.findByPk(revision.documentId, {
|
||||
userId: user.id,
|
||||
});
|
||||
authorize(user, "read", document);
|
||||
|
||||
const before = await revision.previous();
|
||||
|
||||
ctx.body = {
|
||||
data: await presentRevision(
|
||||
revision,
|
||||
await DocumentHelper.diff(before, revision, {
|
||||
includeTitle: false,
|
||||
includeStyles: false,
|
||||
})
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
router.post("revisions.diff", auth(), async (ctx: APIContext) => {
|
||||
const { id, compareToId } = ctx.request.body;
|
||||
assertUuid(id, "id is required");
|
||||
|
||||
const { user } = ctx.state.auth;
|
||||
const revision = await Revision.findByPk(id, {
|
||||
rejectOnEmpty: true,
|
||||
});
|
||||
const document = await Document.findByPk(revision.documentId, {
|
||||
userId: user.id,
|
||||
});
|
||||
authorize(user, "read", document);
|
||||
|
||||
let before;
|
||||
if (compareToId) {
|
||||
assertUuid(compareToId, "compareToId must be a UUID");
|
||||
before = await Revision.findOne({
|
||||
where: {
|
||||
id: compareToId,
|
||||
documentId: revision.documentId,
|
||||
createdAt: {
|
||||
[Op.lt]: revision.createdAt,
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!before) {
|
||||
throw ValidationError(
|
||||
"Revision could not be found, compareToId must be a revision of the same document before the provided revision"
|
||||
);
|
||||
}
|
||||
} else {
|
||||
before = await revision.previous();
|
||||
}
|
||||
|
||||
const accept = ctx.request.headers["accept"];
|
||||
const content = await DocumentHelper.diff(before, revision);
|
||||
|
||||
if (accept?.includes("text/html")) {
|
||||
ctx.set("Content-Type", "text/html");
|
||||
ctx.set(
|
||||
"Content-Disposition",
|
||||
`attachment; filename="${slugify(document.titleWithDefault)}-${
|
||||
revision.id
|
||||
}.html"`
|
||||
);
|
||||
ctx.body = content;
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.body = {
|
||||
data: content,
|
||||
};
|
||||
});
|
||||
|
||||
router.post("revisions.list", auth(), pagination(), async (ctx: APIContext) => {
|
||||
let { direction } = ctx.request.body;
|
||||
const { documentId, sort = "updatedAt" } = ctx.request.body;
|
||||
if (direction !== "ASC") {
|
||||
direction = "DESC";
|
||||
}
|
||||
assertSort(sort, Revision);
|
||||
assertPresent(documentId, "documentId is required");
|
||||
|
||||
const { user } = ctx.state.auth;
|
||||
const document = await Document.findByPk(documentId, {
|
||||
userId: user.id,
|
||||
});
|
||||
authorize(user, "read", document);
|
||||
|
||||
const revisions = await Revision.findAll({
|
||||
where: {
|
||||
documentId: document.id,
|
||||
},
|
||||
order: [[sort, direction]],
|
||||
offset: ctx.state.pagination.offset,
|
||||
limit: ctx.state.pagination.limit,
|
||||
});
|
||||
const data = await Promise.all(
|
||||
revisions.map((revision) => presentRevision(revision))
|
||||
);
|
||||
|
||||
ctx.body = {
|
||||
pagination: ctx.state.pagination,
|
||||
data,
|
||||
};
|
||||
});
|
||||
|
||||
export default router;
|
||||
1
server/routes/api/revisions/index.ts
Normal file
1
server/routes/api/revisions/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from "./revisions";
|
||||
154
server/routes/api/revisions/revisions.ts
Normal file
154
server/routes/api/revisions/revisions.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
import Router from "koa-router";
|
||||
import { Op } from "sequelize";
|
||||
import { RevisionHelper } from "@shared/utils/RevisionHelper";
|
||||
import { ValidationError } from "@server/errors";
|
||||
import auth from "@server/middlewares/authentication";
|
||||
import validate from "@server/middlewares/validate";
|
||||
import { Document, Revision } from "@server/models";
|
||||
import DocumentHelper from "@server/models/helpers/DocumentHelper";
|
||||
import { authorize } from "@server/policies";
|
||||
import { presentRevision } from "@server/presenters";
|
||||
import { APIContext } from "@server/types";
|
||||
import slugify from "@server/utils/slugify";
|
||||
import pagination from "../middlewares/pagination";
|
||||
import * as T from "./schema";
|
||||
|
||||
const router = new Router();
|
||||
|
||||
router.post(
|
||||
"revisions.info",
|
||||
auth(),
|
||||
validate(T.RevisionsInfoSchema),
|
||||
async (ctx: APIContext<T.RevisionsInfoReq>) => {
|
||||
const { id, documentId } = ctx.input.body;
|
||||
const { user } = ctx.state.auth;
|
||||
let before: Revision | null, after: Revision;
|
||||
|
||||
if (id) {
|
||||
const revision = await Revision.findByPk(id, {
|
||||
rejectOnEmpty: true,
|
||||
});
|
||||
|
||||
const document = await Document.findByPk(revision.documentId, {
|
||||
userId: user.id,
|
||||
});
|
||||
authorize(user, "read", document);
|
||||
after = revision;
|
||||
before = await revision.previous();
|
||||
} else if (documentId) {
|
||||
const document = await Document.findByPk(documentId, {
|
||||
userId: user.id,
|
||||
});
|
||||
authorize(user, "read", document);
|
||||
after = Revision.buildFromDocument(document);
|
||||
after.id = RevisionHelper.latestId(document.id);
|
||||
after.user = document.updatedBy;
|
||||
|
||||
before = await Revision.findLatest(documentId);
|
||||
} else {
|
||||
throw ValidationError("Either id or documentId must be provided");
|
||||
}
|
||||
|
||||
ctx.body = {
|
||||
data: await presentRevision(
|
||||
after,
|
||||
await DocumentHelper.diff(before, after, {
|
||||
includeTitle: false,
|
||||
includeStyles: false,
|
||||
})
|
||||
),
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
router.post(
|
||||
"revisions.diff",
|
||||
auth(),
|
||||
validate(T.RevisionsDiffSchema),
|
||||
async (ctx: APIContext<T.RevisionsDiffReq>) => {
|
||||
const { id, compareToId } = ctx.input.body;
|
||||
const { user } = ctx.state.auth;
|
||||
|
||||
const revision = await Revision.findByPk(id, {
|
||||
rejectOnEmpty: true,
|
||||
});
|
||||
const document = await Document.findByPk(revision.documentId, {
|
||||
userId: user.id,
|
||||
});
|
||||
authorize(user, "read", document);
|
||||
|
||||
let before;
|
||||
if (compareToId) {
|
||||
before = await Revision.findOne({
|
||||
where: {
|
||||
id: compareToId,
|
||||
documentId: revision.documentId,
|
||||
createdAt: {
|
||||
[Op.lt]: revision.createdAt,
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!before) {
|
||||
throw ValidationError(
|
||||
"Revision could not be found, compareToId must be a revision of the same document before the provided revision"
|
||||
);
|
||||
}
|
||||
} else {
|
||||
before = await revision.previous();
|
||||
}
|
||||
|
||||
const accept = ctx.request.headers["accept"];
|
||||
const content = await DocumentHelper.diff(before, revision);
|
||||
|
||||
if (accept?.includes("text/html")) {
|
||||
ctx.set("Content-Type", "text/html");
|
||||
ctx.set(
|
||||
"Content-Disposition",
|
||||
`attachment; filename="${slugify(document.titleWithDefault)}-${
|
||||
revision.id
|
||||
}.html"`
|
||||
);
|
||||
ctx.body = content;
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.body = {
|
||||
data: content,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
router.post(
|
||||
"revisions.list",
|
||||
auth(),
|
||||
pagination(),
|
||||
validate(T.RevisionsListSchema),
|
||||
async (ctx: APIContext<T.RevisionsListReq>) => {
|
||||
const { direction, documentId, sort } = ctx.input.body;
|
||||
const { user } = ctx.state.auth;
|
||||
|
||||
const document = await Document.findByPk(documentId, {
|
||||
userId: user.id,
|
||||
});
|
||||
authorize(user, "read", document);
|
||||
|
||||
const revisions = await Revision.findAll({
|
||||
where: {
|
||||
documentId: document.id,
|
||||
},
|
||||
order: [[sort, direction]],
|
||||
offset: ctx.state.pagination.offset,
|
||||
limit: ctx.state.pagination.limit,
|
||||
});
|
||||
const data = await Promise.all(
|
||||
revisions.map((revision) => presentRevision(revision))
|
||||
);
|
||||
|
||||
ctx.body = {
|
||||
pagination: ctx.state.pagination,
|
||||
data,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
export default router;
|
||||
46
server/routes/api/revisions/schema.ts
Normal file
46
server/routes/api/revisions/schema.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { isEmpty } from "lodash";
|
||||
import { z } from "zod";
|
||||
import { Revision } from "@server/models";
|
||||
import BaseSchema from "@server/routes/api/BaseSchema";
|
||||
|
||||
export const RevisionsInfoSchema = BaseSchema.extend({
|
||||
body: z
|
||||
.object({
|
||||
id: z.string().uuid().optional(),
|
||||
documentId: z.string().uuid().optional(),
|
||||
})
|
||||
.refine((req) => !(isEmpty(req.id) && isEmpty(req.documentId)), {
|
||||
message: "id or documentId is required",
|
||||
}),
|
||||
});
|
||||
|
||||
export type RevisionsInfoReq = z.infer<typeof RevisionsInfoSchema>;
|
||||
|
||||
export const RevisionsDiffSchema = BaseSchema.extend({
|
||||
body: z.object({
|
||||
id: z.string().uuid(),
|
||||
compareToId: z.string().uuid().optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
export type RevisionsDiffReq = z.infer<typeof RevisionsDiffSchema>;
|
||||
|
||||
export const RevisionsListSchema = z.object({
|
||||
body: z.object({
|
||||
direction: z
|
||||
.string()
|
||||
.optional()
|
||||
.transform((val) => (val !== "ASC" ? "DESC" : val)),
|
||||
|
||||
sort: z
|
||||
.string()
|
||||
.refine((val) => Object.keys(Revision.getAttributes()).includes(val), {
|
||||
message: "Invalid sort parameter",
|
||||
})
|
||||
.default("createdAt"),
|
||||
|
||||
documentId: z.string().uuid(),
|
||||
}),
|
||||
});
|
||||
|
||||
export type RevisionsListReq = z.infer<typeof RevisionsListSchema>;
|
||||
@@ -127,6 +127,8 @@
|
||||
"Draft": "Draft",
|
||||
"Template": "Template",
|
||||
"New doc": "New doc",
|
||||
"You updated": "You updated",
|
||||
"{{ userName }} updated": "{{ userName }} updated",
|
||||
"You deleted": "You deleted",
|
||||
"{{ userName }} deleted": "{{ userName }} deleted",
|
||||
"You archived": "You archived",
|
||||
@@ -137,8 +139,6 @@
|
||||
"{{ userName }} published": "{{ userName }} published",
|
||||
"You saved": "You saved",
|
||||
"{{ userName }} saved": "{{ userName }} saved",
|
||||
"You updated": "You updated",
|
||||
"{{ userName }} updated": "{{ userName }} updated",
|
||||
"Never viewed": "Never viewed",
|
||||
"Viewed": "Viewed",
|
||||
"in": "in",
|
||||
@@ -164,8 +164,8 @@
|
||||
"our engineers have been notified": "our engineers have been notified",
|
||||
"Report a Bug": "Report a Bug",
|
||||
"Show Detail": "Show Detail",
|
||||
"Current version": "Current version",
|
||||
"{{userName}} edited": "{{userName}} edited",
|
||||
"Latest": "Latest",
|
||||
"{{userName}} archived": "{{userName}} archived",
|
||||
"{{userName}} restored": "{{userName}} restored",
|
||||
"{{userName}} deleted": "{{userName}} deleted",
|
||||
|
||||
11
shared/utils/RevisionHelper.ts
Normal file
11
shared/utils/RevisionHelper.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export class RevisionHelper {
|
||||
/**
|
||||
* Get a static id for the latest revision of a document.
|
||||
*
|
||||
* @param documentId The document to generate an ID for.
|
||||
* @returns The ID of the latest revision of the document.
|
||||
*/
|
||||
static latestId(documentId?: string) {
|
||||
return documentId ? `latest-${documentId}` : "";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user