2022-09-08 13:01:01 +01:00
|
|
|
import { LocationDescriptor } from "history";
|
2022-09-08 11:17:52 +02:00
|
|
|
import { observer } from "mobx-react";
|
2021-08-05 18:03:55 -04:00
|
|
|
import {
|
|
|
|
TrashIcon,
|
|
|
|
ArchiveIcon,
|
|
|
|
EditIcon,
|
|
|
|
PublishIcon,
|
|
|
|
MoveIcon,
|
2022-06-09 21:16:37 +02:00
|
|
|
UnpublishIcon,
|
2025-02-19 07:44:29 -05:00
|
|
|
RestoreIcon,
|
|
|
|
UserIcon,
|
|
|
|
CrossIcon,
|
2021-08-05 18:03:55 -04:00
|
|
|
} from "outline-icons";
|
|
|
|
import * as React from "react";
|
|
|
|
import { useTranslation } from "react-i18next";
|
2022-02-17 23:42:05 -08:00
|
|
|
import { useLocation } from "react-router-dom";
|
2022-03-15 10:36:10 -07:00
|
|
|
import styled, { css } from "styled-components";
|
2024-08-16 22:15:58 -04:00
|
|
|
import EventBoundary from "@shared/components/EventBoundary";
|
2025-01-18 21:14:00 -05:00
|
|
|
import { s, hover } from "@shared/styles";
|
2025-02-19 07:44:15 -05:00
|
|
|
import { RevisionHelper } from "@shared/utils/RevisionHelper";
|
2021-11-29 06:40:55 -08:00
|
|
|
import Document from "~/models/Document";
|
2025-02-01 09:42:51 -05:00
|
|
|
import { Avatar, AvatarSize } from "~/components/Avatar";
|
2025-02-19 07:44:29 -05:00
|
|
|
import Item, { Actions } from "~/components/List/Item";
|
2021-11-29 06:40:55 -08:00
|
|
|
import Time from "~/components/Time";
|
2024-12-06 06:53:13 +05:30
|
|
|
import { useLocationSidebarContext } from "~/hooks/useLocationSidebarContext";
|
2022-09-08 11:17:52 +02:00
|
|
|
import useStores from "~/hooks/useStores";
|
2021-11-29 06:40:55 -08:00
|
|
|
import RevisionMenu from "~/menus/RevisionMenu";
|
2023-04-11 22:15:52 -04:00
|
|
|
import Logger from "~/utils/Logger";
|
2023-04-22 10:00:09 -04:00
|
|
|
import { documentHistoryPath } from "~/utils/routeHelpers";
|
2025-02-19 07:44:29 -05:00
|
|
|
import Text from "./Text";
|
2021-11-29 06:40:55 -08:00
|
|
|
|
2025-02-19 07:44:15 -05:00
|
|
|
export type RevisionEvent = {
|
|
|
|
name: "revisions.create";
|
|
|
|
latest: boolean;
|
|
|
|
};
|
|
|
|
|
|
|
|
export type DocumentEvent = {
|
|
|
|
name:
|
|
|
|
| "documents.publish"
|
|
|
|
| "documents.unpublish"
|
|
|
|
| "documents.archive"
|
|
|
|
| "documents.unarchive"
|
|
|
|
| "documents.delete"
|
|
|
|
| "documents.restore"
|
|
|
|
| "documents.add_user"
|
|
|
|
| "documents.remove_user"
|
|
|
|
| "documents.move";
|
2025-02-19 07:44:29 -05:00
|
|
|
userId: string;
|
2025-02-19 07:44:15 -05:00
|
|
|
};
|
|
|
|
|
2025-02-19 23:35:43 -05:00
|
|
|
export type Event = { id: string; actorId: string; createdAt: string } & (
|
2025-02-19 07:44:15 -05:00
|
|
|
| RevisionEvent
|
|
|
|
| DocumentEvent
|
|
|
|
);
|
|
|
|
|
2021-11-29 06:40:55 -08:00
|
|
|
type Props = {
|
|
|
|
document: Document;
|
2025-02-19 07:44:15 -05:00
|
|
|
event: Event;
|
2024-06-13 18:45:44 +05:30
|
|
|
};
|
2021-08-05 18:03:55 -04:00
|
|
|
|
2025-02-19 07:44:15 -05:00
|
|
|
const EventListItem = ({ event, document, ...rest }: Props) => {
|
2021-08-05 18:03:55 -04:00
|
|
|
const { t } = useTranslation();
|
2025-02-19 07:44:29 -05:00
|
|
|
const { revisions, users } = useStores();
|
2025-02-19 23:35:43 -05:00
|
|
|
const actor = "actorId" in event ? users.get(event.actorId) : undefined;
|
|
|
|
const user = "userId" in event ? users.get(event.userId) : undefined;
|
2022-02-17 23:42:05 -08:00
|
|
|
const location = useLocation();
|
2024-12-06 06:53:13 +05:30
|
|
|
const sidebarContext = useLocationSidebarContext();
|
2025-02-19 07:44:15 -05:00
|
|
|
const revisionLoadedRef = React.useRef(false);
|
2021-11-29 06:40:55 -08:00
|
|
|
const opts = {
|
2025-02-19 23:35:43 -05:00
|
|
|
userName: actor?.name,
|
2021-11-29 06:40:55 -08:00
|
|
|
};
|
2021-08-05 18:03:55 -04:00
|
|
|
const isRevision = event.name === "revisions.create";
|
2025-02-19 07:44:15 -05:00
|
|
|
const isDerivedFromDocument =
|
|
|
|
event.id === RevisionHelper.latestId(document.id);
|
2022-09-08 13:01:01 +01:00
|
|
|
let meta, icon, to: LocationDescriptor | undefined;
|
2021-08-05 18:03:55 -04:00
|
|
|
|
2022-03-15 10:36:10 -07:00
|
|
|
const ref = React.useRef<HTMLAnchorElement>(null);
|
|
|
|
// the time component tends to steal focus when clicked
|
|
|
|
// ...so forward the focus back to the parent item
|
2022-09-08 11:17:52 +02:00
|
|
|
const handleTimeClick = () => {
|
2022-03-15 10:36:10 -07:00
|
|
|
ref.current?.focus();
|
2022-09-08 11:17:52 +02:00
|
|
|
};
|
|
|
|
|
2023-06-28 20:18:18 -04:00
|
|
|
const prefetchRevision = async () => {
|
2025-02-19 07:44:15 -05:00
|
|
|
if (
|
|
|
|
!document.isDeleted &&
|
|
|
|
event.name === "revisions.create" &&
|
|
|
|
!isDerivedFromDocument &&
|
|
|
|
!revisionLoadedRef.current
|
|
|
|
) {
|
|
|
|
await revisions.fetch(event.id, { force: true });
|
|
|
|
revisionLoadedRef.current = true;
|
2022-09-08 11:17:52 +02:00
|
|
|
}
|
|
|
|
};
|
2022-03-15 10:36:10 -07:00
|
|
|
|
2021-08-05 18:03:55 -04:00
|
|
|
switch (event.name) {
|
|
|
|
case "revisions.create":
|
2023-04-08 11:16:05 -04:00
|
|
|
icon = <EditIcon size={16} />;
|
2025-02-19 07:44:15 -05:00
|
|
|
meta = event.latest ? (
|
2023-05-29 22:49:13 -04:00
|
|
|
<>
|
2025-02-19 23:35:43 -05:00
|
|
|
{t("Current version")} · {actor?.name}
|
2023-05-29 22:49:13 -04:00
|
|
|
</>
|
|
|
|
) : (
|
|
|
|
t("{{userName}} edited", opts)
|
|
|
|
);
|
2022-09-08 13:01:01 +01:00
|
|
|
to = {
|
2025-02-19 07:44:15 -05:00
|
|
|
pathname: documentHistoryPath(
|
|
|
|
document,
|
|
|
|
isDerivedFromDocument ? "latest" : event.id
|
|
|
|
),
|
2024-12-06 06:53:13 +05:30
|
|
|
state: {
|
|
|
|
sidebarContext,
|
|
|
|
retainScrollPosition: true,
|
|
|
|
},
|
2022-09-08 13:01:01 +01:00
|
|
|
};
|
2022-09-08 11:17:52 +02:00
|
|
|
break;
|
2021-11-29 06:40:55 -08:00
|
|
|
|
2021-08-05 18:03:55 -04:00
|
|
|
case "documents.archive":
|
2025-02-19 07:44:29 -05:00
|
|
|
icon = <ArchiveIcon />;
|
2021-08-05 18:03:55 -04:00
|
|
|
meta = t("{{userName}} archived", opts);
|
|
|
|
break;
|
2021-11-29 06:40:55 -08:00
|
|
|
|
2021-08-05 18:03:55 -04:00
|
|
|
case "documents.unarchive":
|
2025-02-19 07:44:29 -05:00
|
|
|
icon = <RestoreIcon />;
|
2021-08-05 18:03:55 -04:00
|
|
|
meta = t("{{userName}} restored", opts);
|
|
|
|
break;
|
2021-11-29 06:40:55 -08:00
|
|
|
|
2021-08-05 18:03:55 -04:00
|
|
|
case "documents.delete":
|
2025-02-19 07:44:29 -05:00
|
|
|
icon = <TrashIcon />;
|
2021-08-05 18:03:55 -04:00
|
|
|
meta = t("{{userName}} deleted", opts);
|
|
|
|
break;
|
2024-02-23 22:44:39 -05:00
|
|
|
case "documents.add_user":
|
2025-02-19 07:44:29 -05:00
|
|
|
icon = <UserIcon />;
|
2024-02-23 22:44:39 -05:00
|
|
|
meta = t("{{userName}} added {{addedUserName}}", {
|
|
|
|
...opts,
|
2025-02-19 07:44:29 -05:00
|
|
|
addedUserName: user?.name ?? t("a user"),
|
2024-02-23 22:44:39 -05:00
|
|
|
});
|
|
|
|
break;
|
|
|
|
case "documents.remove_user":
|
2025-02-19 07:44:29 -05:00
|
|
|
icon = <CrossIcon />;
|
2024-02-23 22:44:39 -05:00
|
|
|
meta = t("{{userName}} removed {{removedUserName}}", {
|
|
|
|
...opts,
|
2025-02-19 07:44:29 -05:00
|
|
|
removedUserName: user?.name ?? t("a user"),
|
2024-02-23 22:44:39 -05:00
|
|
|
});
|
|
|
|
break;
|
2021-11-29 06:40:55 -08:00
|
|
|
|
2021-08-05 18:03:55 -04:00
|
|
|
case "documents.restore":
|
2025-02-19 07:44:29 -05:00
|
|
|
icon = <RestoreIcon />;
|
2021-08-05 18:03:55 -04:00
|
|
|
meta = t("{{userName}} moved from trash", opts);
|
|
|
|
break;
|
2021-11-29 06:40:55 -08:00
|
|
|
|
2021-08-05 18:03:55 -04:00
|
|
|
case "documents.publish":
|
2025-02-19 07:44:29 -05:00
|
|
|
icon = <PublishIcon />;
|
2021-08-05 18:03:55 -04:00
|
|
|
meta = t("{{userName}} published", opts);
|
|
|
|
break;
|
2021-11-29 06:40:55 -08:00
|
|
|
|
2022-06-09 21:16:37 +02:00
|
|
|
case "documents.unpublish":
|
2025-02-19 07:44:29 -05:00
|
|
|
icon = <UnpublishIcon />;
|
2022-06-09 21:16:37 +02:00
|
|
|
meta = t("{{userName}} unpublished", opts);
|
|
|
|
break;
|
|
|
|
|
2021-08-05 18:03:55 -04:00
|
|
|
case "documents.move":
|
2025-02-19 07:44:29 -05:00
|
|
|
icon = <MoveIcon />;
|
2021-08-05 18:03:55 -04:00
|
|
|
meta = t("{{userName}} moved", opts);
|
|
|
|
break;
|
2021-11-29 06:40:55 -08:00
|
|
|
|
2021-08-05 18:03:55 -04:00
|
|
|
default:
|
2023-04-11 22:15:52 -04:00
|
|
|
Logger.warn("Unhandled event", { event });
|
2021-08-05 18:03:55 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
if (!meta) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2022-09-08 13:01:01 +01:00
|
|
|
const isActive =
|
|
|
|
typeof to === "string"
|
|
|
|
? location.pathname === to
|
|
|
|
: location.pathname === to?.pathname;
|
2022-02-17 23:42:05 -08:00
|
|
|
|
2022-03-15 10:36:10 -07:00
|
|
|
if (document.isDeleted) {
|
|
|
|
to = undefined;
|
|
|
|
}
|
|
|
|
|
2025-02-19 07:44:29 -05:00
|
|
|
return event.name === "revisions.create" ? (
|
|
|
|
<RevisionItem
|
2021-08-05 18:03:55 -04:00
|
|
|
small
|
2021-12-13 23:42:47 -08:00
|
|
|
exact
|
2022-03-15 10:36:10 -07:00
|
|
|
to={to}
|
2021-08-05 18:03:55 -04:00
|
|
|
title={
|
|
|
|
<Time
|
|
|
|
dateTime={event.createdAt}
|
2022-05-18 03:01:00 +07:00
|
|
|
format={{
|
|
|
|
en_US: "MMM do, h:mm a",
|
|
|
|
fr_FR: "'Le 'd MMMM 'à' H:mm",
|
|
|
|
}}
|
2021-08-05 18:03:55 -04:00
|
|
|
relative={false}
|
|
|
|
addSuffix
|
2022-03-15 10:36:10 -07:00
|
|
|
onClick={handleTimeClick}
|
2021-08-05 18:03:55 -04:00
|
|
|
/>
|
|
|
|
}
|
2025-02-19 23:35:43 -05:00
|
|
|
image={<Avatar model={actor} size={AvatarSize.Large} />}
|
2025-02-19 07:44:29 -05:00
|
|
|
subtitle={meta}
|
2021-08-05 18:03:55 -04:00
|
|
|
actions={
|
2025-02-19 07:44:15 -05:00
|
|
|
isRevision && isActive && !event.latest ? (
|
2024-08-16 22:15:58 -04:00
|
|
|
<StyledEventBoundary>
|
2025-02-19 07:44:15 -05:00
|
|
|
<RevisionMenu document={document} revisionId={event.id} />
|
2024-08-16 22:15:58 -04:00
|
|
|
</StyledEventBoundary>
|
2021-08-05 18:03:55 -04:00
|
|
|
) : undefined
|
|
|
|
}
|
2022-09-08 11:17:52 +02:00
|
|
|
onMouseEnter={prefetchRevision}
|
2022-03-15 10:36:10 -07:00
|
|
|
ref={ref}
|
|
|
|
{...rest}
|
2021-08-05 18:03:55 -04:00
|
|
|
/>
|
2025-02-19 07:44:29 -05:00
|
|
|
) : (
|
|
|
|
<EventItem>
|
|
|
|
<IconWrapper size="xsmall" type="secondary">
|
|
|
|
{icon}
|
|
|
|
</IconWrapper>
|
|
|
|
<Text size="xsmall" type="secondary">
|
|
|
|
{meta} ·{" "}
|
|
|
|
<Time dateTime={event.createdAt} relative shorten addSuffix />
|
|
|
|
</Text>
|
|
|
|
</EventItem>
|
2021-08-05 18:03:55 -04:00
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2025-02-19 07:44:29 -05:00
|
|
|
const lineStyle = css`
|
|
|
|
&::before {
|
|
|
|
content: "";
|
|
|
|
display: block;
|
|
|
|
position: absolute;
|
|
|
|
top: -8px;
|
|
|
|
left: 22px;
|
|
|
|
width: 1px;
|
|
|
|
height: calc(50% - 14px + 8px);
|
|
|
|
background: ${s("divider")};
|
2025-03-12 23:43:18 -04:00
|
|
|
mix-blend-mode: ${(props) => (props.theme.isDark ? "lighten" : "multiply")};
|
2025-02-19 07:44:29 -05:00
|
|
|
z-index: 1;
|
2021-08-05 18:03:55 -04:00
|
|
|
}
|
|
|
|
|
2025-02-19 07:44:29 -05:00
|
|
|
&:first-child::before {
|
|
|
|
display: none;
|
|
|
|
}
|
2021-08-05 18:03:55 -04:00
|
|
|
|
2025-02-19 07:44:29 -05:00
|
|
|
&:nth-child(2)::before {
|
|
|
|
display: none;
|
2021-08-05 18:03:55 -04:00
|
|
|
}
|
|
|
|
|
2025-02-19 07:44:29 -05:00
|
|
|
&::after {
|
2021-08-05 18:03:55 -04:00
|
|
|
content: "";
|
|
|
|
display: block;
|
|
|
|
position: absolute;
|
2025-02-19 07:44:29 -05:00
|
|
|
top: calc(50% + 14px);
|
|
|
|
left: 22px;
|
|
|
|
width: 1px;
|
|
|
|
height: calc(50% - 14px);
|
|
|
|
background: ${s("divider")};
|
2025-03-12 23:43:18 -04:00
|
|
|
mix-blend-mode: ${(props) => (props.theme.isDark ? "lighten" : "multiply")};
|
2025-02-19 07:44:29 -05:00
|
|
|
z-index: 1;
|
2021-08-05 18:03:55 -04:00
|
|
|
}
|
|
|
|
|
2025-02-19 07:44:29 -05:00
|
|
|
&:last-child::after {
|
|
|
|
display: none;
|
2021-08-05 18:03:55 -04:00
|
|
|
}
|
|
|
|
|
2025-02-19 07:44:29 -05:00
|
|
|
h3 + &::before {
|
|
|
|
display: none;
|
2021-08-05 18:03:55 -04:00
|
|
|
}
|
2025-02-19 07:44:29 -05:00
|
|
|
`;
|
2021-08-05 18:03:55 -04:00
|
|
|
|
2025-02-19 07:44:29 -05:00
|
|
|
const IconWrapper = styled(Text)`
|
|
|
|
height: 24px;
|
|
|
|
`;
|
|
|
|
|
|
|
|
const EventItem = styled.li`
|
|
|
|
display: flex;
|
|
|
|
align-items: center;
|
|
|
|
gap: 8px;
|
|
|
|
list-style: none;
|
|
|
|
margin: 8px 0;
|
|
|
|
padding: 4px 10px;
|
|
|
|
white-space: nowrap;
|
|
|
|
position: relative;
|
|
|
|
|
|
|
|
time {
|
|
|
|
white-space: nowrap;
|
2021-08-05 18:03:55 -04:00
|
|
|
}
|
|
|
|
|
2025-02-19 07:44:29 -05:00
|
|
|
svg {
|
|
|
|
flex-shrink: 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
${lineStyle}
|
|
|
|
`;
|
|
|
|
|
|
|
|
const StyledEventBoundary = styled(EventBoundary)`
|
|
|
|
height: 24px;
|
|
|
|
`;
|
|
|
|
|
|
|
|
const RevisionItem = styled(Item)`
|
|
|
|
border: 0;
|
|
|
|
position: relative;
|
|
|
|
margin: 8px 0;
|
|
|
|
padding: 8px;
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
|
|
${lineStyle}
|
|
|
|
|
2021-08-05 18:03:55 -04:00
|
|
|
${Actions} {
|
2022-02-17 23:42:05 -08:00
|
|
|
opacity: 0.5;
|
2021-08-05 18:03:55 -04:00
|
|
|
|
2023-05-20 11:32:05 -04:00
|
|
|
&: ${hover} {
|
2021-08-05 18:03:55 -04:00
|
|
|
opacity: 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
`;
|
|
|
|
|
2022-09-08 11:17:52 +02:00
|
|
|
export default observer(EventListItem);
|