diff --git a/.env.sample b/.env.sample index 51046501da..8f2e4910e8 100644 --- a/.env.sample +++ b/.env.sample @@ -147,6 +147,10 @@ DISCORD_SERVER_ID= # DISCORD_SERVER_ID and DISCORD_SERVER_ROLES must be set together. DISCORD_SERVER_ROLES= +# –––––––––––––– IMPORTS –––––––––––––– +NOTION_CLIENT_ID= +NOTION_CLIENT_SECRET= + # –––––––––––––––– OPTIONAL –––––––––––––––– # Base64 encoded private key and certificate for HTTPS termination. This is only diff --git a/app/components/Dialogs.tsx b/app/components/Dialogs.tsx index e7f4d05589..d1af42168c 100644 --- a/app/components/Dialogs.tsx +++ b/app/components/Dialogs.tsx @@ -23,7 +23,10 @@ function Dialogs() { key={id} isOpen={modal.isOpen} fullscreen={modal.fullscreen ?? false} - onRequestClose={() => dialogs.closeModal(id)} + onRequestClose={() => { + modal.onClose?.(); + dialogs.closeModal(id); + }} title={modal.title} style={modal.style} > diff --git a/app/components/WebsocketProvider.tsx b/app/components/WebsocketProvider.tsx index 03d007e6bb..4a4b0405da 100644 --- a/app/components/WebsocketProvider.tsx +++ b/app/components/WebsocketProvider.tsx @@ -6,7 +6,11 @@ import * as React from "react"; import { withTranslation, WithTranslation } from "react-i18next"; import { io, Socket } from "socket.io-client"; import { toast } from "sonner"; -import { FileOperationState, FileOperationType } from "@shared/types"; +import { + FileOperationState, + FileOperationType, + ImportState, +} from "@shared/types"; import RootStore from "~/stores/RootStore"; import Collection from "~/models/Collection"; import Comment from "~/models/Comment"; @@ -15,6 +19,7 @@ import FileOperation from "~/models/FileOperation"; import Group from "~/models/Group"; import GroupMembership from "~/models/GroupMembership"; import GroupUser from "~/models/GroupUser"; +import Import from "~/models/Import"; import Membership from "~/models/Membership"; import Notification from "~/models/Notification"; import Pin from "~/models/Pin"; @@ -100,6 +105,7 @@ class WebsocketProvider extends React.Component { subscriptions, fileOperations, notifications, + imports, } = this.props; const currentUserId = auth?.user?.id; @@ -620,6 +626,23 @@ class WebsocketProvider extends React.Component { } ); + this.socket.on("imports.create", (event: PartialExcept) => { + imports.add(event); + }); + + this.socket.on("imports.update", (event: PartialExcept) => { + imports.add(event); + + if ( + event.state === ImportState.Completed && + event.createdBy?.id === auth.user?.id + ) { + toast.success(event.name, { + description: this.props.t("Your import completed"), + }); + } + }); + this.socket.on( "subscriptions.create", (event: PartialExcept) => { diff --git a/app/hooks/usePolicy.ts b/app/hooks/usePolicy.ts index 86052f7c3d..4bb9e8b86b 100644 --- a/app/hooks/usePolicy.ts +++ b/app/hooks/usePolicy.ts @@ -18,6 +18,7 @@ export default function usePolicy(entity?: string | Model | null) { ? entity : entity.id : ""; + const policy = policies.get(entityId); React.useEffect(() => { if ( @@ -28,11 +29,11 @@ export default function usePolicy(entity?: string | Model | null) { ) { // The policy for this model is missing and we have an authenticated session, attempt to // reload relationships for this model. - if (!policies.get(entity.id) && user) { + if (!policy && user) { void entity.loadRelations(); } } - }, [policies, user, entity]); + }, [policies, policy, user, entity]); return policies.abilities(entityId); } diff --git a/app/menus/ImportMenu.tsx b/app/menus/ImportMenu.tsx new file mode 100644 index 0000000000..3b55e6fe40 --- /dev/null +++ b/app/menus/ImportMenu.tsx @@ -0,0 +1,62 @@ +import { observer } from "mobx-react"; +import { CrossIcon, TrashIcon } from "outline-icons"; +import * as React from "react"; +import { useTranslation } from "react-i18next"; +import { useMenuState } from "reakit/Menu"; +import Import from "~/models/Import"; +import ContextMenu from "~/components/ContextMenu"; +import OverflowMenuButton from "~/components/ContextMenu/OverflowMenuButton"; +import Template from "~/components/ContextMenu/Template"; +import usePolicy from "~/hooks/usePolicy"; +import { MenuItem } from "~/types"; + +type Props = { + /** Import to which actions will be applied. */ + importModel: Import; + /** Callback to handle import cancellation. */ + onCancel: () => Promise; + /** Callback to handle import deletion. */ + onDelete: () => Promise; +}; + +export const ImportMenu = observer( + ({ importModel, onCancel, onDelete }: Props) => { + const { t } = useTranslation(); + const can = usePolicy(importModel); + const menu = useMenuState({ + modal: true, + }); + + const items = React.useMemo( + () => + [ + { + type: "button", + title: t("Cancel"), + visible: can.cancel, + icon: , + dangerous: true, + onClick: onCancel, + }, + { + type: "button", + title: t("Delete"), + visible: can.delete, + icon: , + dangerous: true, + onClick: onDelete, + }, + ] satisfies MenuItem[], + [t, can.delete, can.cancel, onCancel, onDelete] + ); + + return ( + <> + + +