From 60309975e058c30fe4966be8a826273b18c97291 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Thu, 25 Aug 2022 10:06:44 +0200 Subject: [PATCH] Allow usePolicy to fetch missing policies --- app/components/CollectionDescription.tsx | 2 +- app/components/DocumentListItem.tsx | 8 ++--- app/components/EventListItem.tsx | 2 +- app/components/Sidebar/App.tsx | 2 +- .../Sidebar/components/CollectionLink.tsx | 2 +- .../components/CollectionLinkChildren.tsx | 2 +- .../components/DraggableCollectionLink.tsx | 2 +- app/components/SocketProvider.tsx | 16 +++++++--- app/hooks/useAuthorizedSettingsConfig.ts | 2 +- app/hooks/usePolicy.ts | 30 ++++++++++++++++--- app/menus/CollectionMenu.tsx | 4 +-- app/menus/DocumentMenu.tsx | 2 +- app/menus/GroupMenu.tsx | 2 +- app/menus/NewDocumentMenu.tsx | 2 +- app/menus/NewTemplateMenu.tsx | 2 +- app/routes/authenticated.tsx | 2 +- app/scenes/Collection/Actions.tsx | 2 +- app/scenes/Collection/Empty.tsx | 2 +- app/scenes/Document/components/DataLoader.tsx | 2 +- app/scenes/Document/components/Header.tsx | 2 +- .../Document/components/SharePopover.tsx | 2 +- app/scenes/GroupMembers/GroupMembers.tsx | 2 +- app/scenes/Home.tsx | 2 +- app/scenes/Invite.tsx | 2 +- app/scenes/Settings/Groups.tsx | 2 +- app/scenes/Settings/Members.tsx | 2 +- app/scenes/Settings/Shares.tsx | 2 +- app/scenes/Settings/Tokens.tsx | 2 +- app/scenes/Settings/Webhooks.tsx | 2 +- app/scenes/Templates.tsx | 2 +- app/stores/BaseStore.ts | 16 ++++++++-- .../queues/processors/WebsocketsProcessor.ts | 18 +++++------ 32 files changed, 91 insertions(+), 53 deletions(-) diff --git a/app/components/CollectionDescription.tsx b/app/components/CollectionDescription.tsx index 81e34b02e..968e8a72c 100644 --- a/app/components/CollectionDescription.tsx +++ b/app/components/CollectionDescription.tsx @@ -25,7 +25,7 @@ function CollectionDescription({ collection }: Props) { const [isExpanded, setExpanded] = React.useState(false); const [isEditing, setEditing] = React.useState(false); const [isDirty, setDirty] = React.useState(false); - const can = usePolicy(collection.id); + const can = usePolicy(collection); const handleStartEditing = React.useCallback(() => { setEditing(true); diff --git a/app/components/DocumentListItem.tsx b/app/components/DocumentListItem.tsx index c5a9e3960..b043e1213 100644 --- a/app/components/DocumentListItem.tsx +++ b/app/components/DocumentListItem.tsx @@ -49,8 +49,8 @@ function DocumentListItem( ref: React.RefObject ) { const { t } = useTranslation(); - const currentUser = useCurrentUser(); - const currentTeam = useCurrentTeam(); + const user = useCurrentUser(); + const team = useCurrentTeam(); const [menuOpen, handleMenuOpen, handleMenuClose] = useBoolean(); const { @@ -70,7 +70,7 @@ function DocumentListItem( !!document.title.toLowerCase().includes(highlight.toLowerCase()); const canStar = !document.isDraft && !document.isArchived && !document.isTemplate; - const can = usePolicy(currentTeam.id); + const can = usePolicy(team); const canCollection = usePolicy(document.collectionId); return ( @@ -96,7 +96,7 @@ function DocumentListItem( highlight={highlight} dir={document.dir} /> - {document.isBadgedNew && document.createdBy.id !== currentUser.id && ( + {document.isBadgedNew && document.createdBy.id !== user.id && ( {t("New")} )} {canStar && ( diff --git a/app/components/EventListItem.tsx b/app/components/EventListItem.tsx index 47e43716b..0627439c7 100644 --- a/app/components/EventListItem.tsx +++ b/app/components/EventListItem.tsx @@ -33,7 +33,7 @@ type Props = { const EventListItem = ({ event, latest, document, ...rest }: Props) => { const { t } = useTranslation(); const location = useLocation(); - const can = usePolicy(document.id); + const can = usePolicy(document); const opts = { userName: event.actor.name, }; diff --git a/app/components/Sidebar/App.tsx b/app/components/Sidebar/App.tsx index b92be2181..fad3164a3 100644 --- a/app/components/Sidebar/App.tsx +++ b/app/components/Sidebar/App.tsx @@ -36,7 +36,7 @@ function AppSidebar() { const { documents } = useStores(); const team = useCurrentTeam(); const user = useCurrentUser(); - const can = usePolicy(team.id); + const can = usePolicy(team); React.useEffect(() => { if (!user.isViewer) { diff --git a/app/components/Sidebar/components/CollectionLink.tsx b/app/components/Sidebar/components/CollectionLink.tsx index aecabab02..0cdcb04eb 100644 --- a/app/components/Sidebar/components/CollectionLink.tsx +++ b/app/components/Sidebar/components/CollectionLink.tsx @@ -44,7 +44,7 @@ const CollectionLink: React.FC = ({ const { dialogs, documents, collections } = useStores(); const [menuOpen, handleMenuOpen, handleMenuClose] = useBoolean(); const [isEditing, setIsEditing] = React.useState(false); - const canUpdate = usePolicy(collection.id).update; + const canUpdate = usePolicy(collection).update; const { t } = useTranslation(); const history = useHistory(); const inStarredSection = useStarredContext(); diff --git a/app/components/Sidebar/components/CollectionLinkChildren.tsx b/app/components/Sidebar/components/CollectionLinkChildren.tsx index 9357a1d6c..8495a628a 100644 --- a/app/components/Sidebar/components/CollectionLinkChildren.tsx +++ b/app/components/Sidebar/components/CollectionLinkChildren.tsx @@ -25,7 +25,7 @@ function CollectionLinkChildren({ expanded, prefetchDocument, }: Props) { - const can = usePolicy(collection.id); + const can = usePolicy(collection); const { showToast } = useToasts(); const manualSort = collection.sort.field === "index"; const { documents } = useStores(); diff --git a/app/components/Sidebar/components/DraggableCollectionLink.tsx b/app/components/Sidebar/components/DraggableCollectionLink.tsx index 3815129e8..b2f17ab4c 100644 --- a/app/components/Sidebar/components/DraggableCollectionLink.tsx +++ b/app/components/Sidebar/components/DraggableCollectionLink.tsx @@ -39,7 +39,7 @@ function DraggableCollectionLink({ const [expanded, setExpanded] = React.useState( collection.id === ui.activeCollectionId && !locationStateStarred ); - const can = usePolicy(collection.id); + const can = usePolicy(collection); const belowCollectionIndex = belowCollection ? belowCollection.index : null; // Drop to reorder collection diff --git a/app/components/SocketProvider.tsx b/app/components/SocketProvider.tsx index 98d561ddd..d95ad4647 100644 --- a/app/components/SocketProvider.tsx +++ b/app/components/SocketProvider.tsx @@ -8,6 +8,7 @@ import RootStore from "~/stores/RootStore"; import Collection from "~/models/Collection"; import Document from "~/models/Document"; import FileOperation from "~/models/FileOperation"; +import Group from "~/models/Group"; import Pin from "~/models/Pin"; import Star from "~/models/Star"; import Team from "~/models/Team"; @@ -237,8 +238,7 @@ class SocketProvider extends React.Component { this.socket.on( "documents.update", (event: PartialWithId & { title: string; url: string }) => { - const document = documents.get(event.id); - document?.updateFromJson(event); + documents.patch(event); if (event.collectionId) { const collection = collections.get(event.collectionId); @@ -264,6 +264,14 @@ class SocketProvider extends React.Component { } ); + this.socket.on("groups.create", (event: PartialWithId) => { + groups.add(event); + }); + + this.socket.on("groups.update", (event: PartialWithId) => { + groups.patch(event); + }); + this.socket.on("groups.delete", (event: WebsocketEntityDeletedEvent) => { groups.remove(event.modelId); }); @@ -299,7 +307,7 @@ class SocketProvider extends React.Component { }); this.socket.on("pins.update", (event: PartialWithId) => { - pins.add(event); + pins.patch(event); }); this.socket.on("pins.delete", (event: WebsocketEntityDeletedEvent) => { @@ -311,7 +319,7 @@ class SocketProvider extends React.Component { }); this.socket.on("stars.update", (event: PartialWithId) => { - stars.add(event); + stars.patch(event); }); this.socket.on("stars.delete", (event: WebsocketEntityDeletedEvent) => { diff --git a/app/hooks/useAuthorizedSettingsConfig.ts b/app/hooks/useAuthorizedSettingsConfig.ts index 47aafc572..5ab3c6d14 100644 --- a/app/hooks/useAuthorizedSettingsConfig.ts +++ b/app/hooks/useAuthorizedSettingsConfig.ts @@ -67,7 +67,7 @@ type ConfigType = { const useAuthorizedSettingsConfig = () => { const team = useCurrentTeam(); - const can = usePolicy(team.id); + const can = usePolicy(team); const { t } = useTranslation(); const config: ConfigType = React.useMemo( diff --git a/app/hooks/usePolicy.ts b/app/hooks/usePolicy.ts index 762884cc6..5e455ab57 100644 --- a/app/hooks/usePolicy.ts +++ b/app/hooks/usePolicy.ts @@ -1,12 +1,34 @@ +import * as React from "react"; +import BaseModel from "~/models/BaseModel"; import useStores from "./useStores"; /** - * Quick access to retrieve the abilities of a policy for a given entity + * Retrieve the abilities of a policy for a given entity, if the policy is not + * located in the store, it will be fetched from the server. * - * @param entityId The entity id - * @returns The available abilities + * @param entity The model or model id + * @returns The policy for the model */ -export default function usePolicy(entityId: string) { +export default function usePolicy(entity: string | BaseModel | undefined) { const { policies } = useStores(); + const triggered = React.useRef(false); + const entityId = entity + ? typeof entity === "string" + ? entity + : entity.id + : ""; + + React.useEffect(() => { + if (entity && typeof entity !== "string") { + // The policy for this model is missing and we haven't tried to fetch it + // yet, go ahead and do that now. The force flag is needed otherwise the + // network request will be skipped due to the model existing in the store + if (!policies.get(entity.id) && !triggered.current) { + triggered.current = true; + void entity.store.fetch(entity.id, { force: true }); + } + } + }, [policies, entity]); + return policies.abilities(entityId); } diff --git a/app/menus/CollectionMenu.tsx b/app/menus/CollectionMenu.tsx index 8591c033e..faca20f54 100644 --- a/app/menus/CollectionMenu.tsx +++ b/app/menus/CollectionMenu.tsx @@ -187,8 +187,8 @@ function CollectionMenu({ ); const alphabeticalSort = collection.sort.field === "title"; - const can = usePolicy(collection.id); - const canUserInTeam = usePolicy(team.id); + const can = usePolicy(collection); + const canUserInTeam = usePolicy(team); const items: MenuItem[] = React.useMemo( () => [ { diff --git a/app/menus/DocumentMenu.tsx b/app/menus/DocumentMenu.tsx index 2b84faa74..638f98667 100644 --- a/app/menus/DocumentMenu.tsx +++ b/app/menus/DocumentMenu.tsx @@ -123,7 +123,7 @@ function DocumentMenu({ }, [menu]); const collection = collections.get(document.collectionId); - const can = usePolicy(document.id); + const can = usePolicy(document); const canViewHistory = can.read && !can.restore; const restoreItems = React.useMemo( () => [ diff --git a/app/menus/GroupMenu.tsx b/app/menus/GroupMenu.tsx index 711bd0841..86e19e0e6 100644 --- a/app/menus/GroupMenu.tsx +++ b/app/menus/GroupMenu.tsx @@ -24,7 +24,7 @@ function GroupMenu({ group, onMembers }: Props) { }); const [editModalOpen, setEditModalOpen] = React.useState(false); const [deleteModalOpen, setDeleteModalOpen] = React.useState(false); - const can = usePolicy(group.id); + const can = usePolicy(group); return ( <> diff --git a/app/menus/NewDocumentMenu.tsx b/app/menus/NewDocumentMenu.tsx index 901531280..1e9aa7acb 100644 --- a/app/menus/NewDocumentMenu.tsx +++ b/app/menus/NewDocumentMenu.tsx @@ -27,7 +27,7 @@ function NewDocumentMenu() { const { t } = useTranslation(); const team = useCurrentTeam(); const { collections, policies } = useStores(); - const can = usePolicy(team.id); + const can = usePolicy(team); const items = React.useMemo( () => collections.orderedData.reduce((filtered, collection) => { diff --git a/app/menus/NewTemplateMenu.tsx b/app/menus/NewTemplateMenu.tsx index 40ea0f916..e812bc575 100644 --- a/app/menus/NewTemplateMenu.tsx +++ b/app/menus/NewTemplateMenu.tsx @@ -22,7 +22,7 @@ function NewTemplateMenu() { const { t } = useTranslation(); const team = useCurrentTeam(); const { collections, policies } = useStores(); - const can = usePolicy(team.id); + const can = usePolicy(team); const items = React.useMemo( () => diff --git a/app/routes/authenticated.tsx b/app/routes/authenticated.tsx index 8bce9bb62..53e68e81a 100644 --- a/app/routes/authenticated.tsx +++ b/app/routes/authenticated.tsx @@ -64,7 +64,7 @@ const RedirectDocument = ({ function AuthenticatedRoutes() { const team = useCurrentTeam(); - const can = usePolicy(team.id); + const can = usePolicy(team); return ( diff --git a/app/scenes/Collection/Actions.tsx b/app/scenes/Collection/Actions.tsx index fe22705b7..ef98abe59 100644 --- a/app/scenes/Collection/Actions.tsx +++ b/app/scenes/Collection/Actions.tsx @@ -18,7 +18,7 @@ type Props = { function Actions({ collection }: Props) { const { t } = useTranslation(); - const can = usePolicy(collection.id); + const can = usePolicy(collection); return ( <> diff --git a/app/scenes/Collection/Empty.tsx b/app/scenes/Collection/Empty.tsx index 7d7136364..883c4b5fb 100644 --- a/app/scenes/Collection/Empty.tsx +++ b/app/scenes/Collection/Empty.tsx @@ -20,7 +20,7 @@ type Props = { function EmptyCollection({ collection }: Props) { const { t } = useTranslation(); - const can = usePolicy(collection.id); + const can = usePolicy(collection); const collectionName = collection ? collection.name : ""; const [ diff --git a/app/scenes/Document/components/DataLoader.tsx b/app/scenes/Document/components/DataLoader.tsx index 3924274bd..0c8251316 100644 --- a/app/scenes/Document/components/DataLoader.tsx +++ b/app/scenes/Document/components/DataLoader.tsx @@ -57,7 +57,7 @@ function DataLoader({ match, children }: Props) { : undefined; const isEditRoute = match.path === matchDocumentEdit; const isEditing = isEditRoute || !!auth.team?.collaborativeEditing; - const can = usePolicy(document ? document.id : ""); + const can = usePolicy(document); const location = useLocation(); React.useEffect(() => { diff --git a/app/scenes/Document/components/Header.tsx b/app/scenes/Document/components/Header.tsx index d17eeb9c4..cad4b769e 100644 --- a/app/scenes/Document/components/Header.tsx +++ b/app/scenes/Document/components/Header.tsx @@ -100,7 +100,7 @@ function DocumentHeader({ }, [onSave]); const { isDeleted, isTemplate } = document; - const can = usePolicy(document.id); + const can = usePolicy(document); const canToggleEmbeds = team?.documentEmbeds; const canEdit = can.update && !isEditing; const toc = ( diff --git a/app/scenes/Document/components/SharePopover.tsx b/app/scenes/Document/components/SharePopover.tsx index 6490d0fd5..35bff4f1e 100644 --- a/app/scenes/Document/components/SharePopover.tsx +++ b/app/scenes/Document/components/SharePopover.tsx @@ -45,7 +45,7 @@ function SharePopover({ const timeout = React.useRef>(); const buttonRef = React.useRef(null); const can = usePolicy(share ? share.id : ""); - const documentAbilities = usePolicy(document.id); + const documentAbilities = usePolicy(document); const canPublish = can.update && !document.isTemplate && diff --git a/app/scenes/GroupMembers/GroupMembers.tsx b/app/scenes/GroupMembers/GroupMembers.tsx index 305a20ded..33a9b23ed 100644 --- a/app/scenes/GroupMembers/GroupMembers.tsx +++ b/app/scenes/GroupMembers/GroupMembers.tsx @@ -26,7 +26,7 @@ function GroupMembers({ group }: Props) { const { users, groupMemberships } = useStores(); const { showToast } = useToasts(); const { t } = useTranslation(); - const can = usePolicy(group.id); + const can = usePolicy(group); const handleAddModal = (state: boolean) => { setAddModalOpen(state); diff --git a/app/scenes/Home.tsx b/app/scenes/Home.tsx index 2e1841c6f..cf6445e64 100644 --- a/app/scenes/Home.tsx +++ b/app/scenes/Home.tsx @@ -30,7 +30,7 @@ function Home() { pins.fetchPage(); }, [pins]); - const canManageTeam = usePolicy(team.id).manage; + const canManageTeam = usePolicy(team).manage; return ( { diff --git a/app/scenes/Settings/Groups.tsx b/app/scenes/Settings/Groups.tsx index 4971db55d..14b83aacc 100644 --- a/app/scenes/Settings/Groups.tsx +++ b/app/scenes/Settings/Groups.tsx @@ -24,7 +24,7 @@ function Groups() { const { t } = useTranslation(); const { groups } = useStores(); const team = useCurrentTeam(); - const can = usePolicy(team.id); + const can = usePolicy(team); const [ newGroupModalOpen, handleNewGroupModalOpen, diff --git a/app/scenes/Settings/Members.tsx b/app/scenes/Settings/Members.tsx index 291d3909a..d4262b56b 100644 --- a/app/scenes/Settings/Members.tsx +++ b/app/scenes/Settings/Members.tsx @@ -40,7 +40,7 @@ function Members() { const [data, setData] = React.useState([]); const [totalPages, setTotalPages] = React.useState(0); const [userIds, setUserIds] = React.useState([]); - const can = usePolicy(team.id); + const can = usePolicy(team); const query = params.get("query") || ""; const filter = params.get("filter") || ""; const sort = params.get("sort") || "name"; diff --git a/app/scenes/Settings/Shares.tsx b/app/scenes/Settings/Shares.tsx index c9bae0811..f3be922de 100644 --- a/app/scenes/Settings/Shares.tsx +++ b/app/scenes/Settings/Shares.tsx @@ -21,7 +21,7 @@ function Shares() { const { t } = useTranslation(); const { shares, auth } = useStores(); const canShareDocuments = auth.team && auth.team.sharing; - const can = usePolicy(team.id); + const can = usePolicy(team); const [isLoading, setIsLoading] = React.useState(false); const [data, setData] = React.useState([]); const [totalPages, setTotalPages] = React.useState(0); diff --git a/app/scenes/Settings/Tokens.tsx b/app/scenes/Settings/Tokens.tsx index 5379b1fc1..d2a4878e5 100644 --- a/app/scenes/Settings/Tokens.tsx +++ b/app/scenes/Settings/Tokens.tsx @@ -23,7 +23,7 @@ function Tokens() { const { t } = useTranslation(); const { apiKeys } = useStores(); const [newModalOpen, handleNewModalOpen, handleNewModalClose] = useBoolean(); - const can = usePolicy(team.id); + const can = usePolicy(team); return ( ) { const team = useCurrentTeam(); const { fetchTemplates, templates, templatesAlphabetical } = documents; const { sort } = props.match.params; - const can = usePolicy(team.id); + const can = usePolicy(team); return ( { }; @action - remove(id: string): void { - this.data.delete(id); + patch = (item: PartialWithId | T): T | undefined => { + const existingModel = this.data.get(item.id); + + if (existingModel) { + existingModel.updateFromJson(item); + return existingModel; + } + + return; + }; + + @action + remove(id: string): boolean { + return this.data.delete(id); } save( diff --git a/server/queues/processors/WebsocketsProcessor.ts b/server/queues/processors/WebsocketsProcessor.ts index 5f1255563..96ac954ba 100644 --- a/server/queues/processors/WebsocketsProcessor.ts +++ b/server/queues/processors/WebsocketsProcessor.ts @@ -16,6 +16,7 @@ import { presentCollection, presentDocument, presentFileOperation, + presentGroup, presentPin, presentStar, presentTeam, @@ -356,8 +357,9 @@ export default class WebsocketsProcessor { if (!fileOperation) { return; } - const data = await presentFileOperation(fileOperation); - return socketio.to(`user-${event.actorId}`).emit(event.name, data); + return socketio + .to(`user-${event.actorId}`) + .emit(event.name, presentFileOperation(fileOperation)); } case "pins.create": @@ -412,15 +414,9 @@ export default class WebsocketsProcessor { if (!group) { return; } - return socketio.to(`team-${group.teamId}`).emit("entities", { - event: event.name, - groupIds: [ - { - id: group.id, - updatedAt: group.updatedAt, - }, - ], - }); + return socketio + .to(`team-${group.teamId}`) + .emit(event.name, presentGroup(group)); } case "groups.add_user": {