Allow usePolicy to fetch missing policies

This commit is contained in:
Tom Moor
2022-08-25 10:06:44 +02:00
parent 983010b5d8
commit 60309975e0
32 changed files with 91 additions and 53 deletions

View File

@@ -25,7 +25,7 @@ function CollectionDescription({ collection }: Props) {
const [isExpanded, setExpanded] = React.useState(false); const [isExpanded, setExpanded] = React.useState(false);
const [isEditing, setEditing] = React.useState(false); const [isEditing, setEditing] = React.useState(false);
const [isDirty, setDirty] = React.useState(false); const [isDirty, setDirty] = React.useState(false);
const can = usePolicy(collection.id); const can = usePolicy(collection);
const handleStartEditing = React.useCallback(() => { const handleStartEditing = React.useCallback(() => {
setEditing(true); setEditing(true);

View File

@@ -49,8 +49,8 @@ function DocumentListItem(
ref: React.RefObject<HTMLAnchorElement> ref: React.RefObject<HTMLAnchorElement>
) { ) {
const { t } = useTranslation(); const { t } = useTranslation();
const currentUser = useCurrentUser(); const user = useCurrentUser();
const currentTeam = useCurrentTeam(); const team = useCurrentTeam();
const [menuOpen, handleMenuOpen, handleMenuClose] = useBoolean(); const [menuOpen, handleMenuOpen, handleMenuClose] = useBoolean();
const { const {
@@ -70,7 +70,7 @@ function DocumentListItem(
!!document.title.toLowerCase().includes(highlight.toLowerCase()); !!document.title.toLowerCase().includes(highlight.toLowerCase());
const canStar = const canStar =
!document.isDraft && !document.isArchived && !document.isTemplate; !document.isDraft && !document.isArchived && !document.isTemplate;
const can = usePolicy(currentTeam.id); const can = usePolicy(team);
const canCollection = usePolicy(document.collectionId); const canCollection = usePolicy(document.collectionId);
return ( return (
@@ -96,7 +96,7 @@ function DocumentListItem(
highlight={highlight} highlight={highlight}
dir={document.dir} dir={document.dir}
/> />
{document.isBadgedNew && document.createdBy.id !== currentUser.id && ( {document.isBadgedNew && document.createdBy.id !== user.id && (
<Badge yellow>{t("New")}</Badge> <Badge yellow>{t("New")}</Badge>
)} )}
{canStar && ( {canStar && (

View File

@@ -33,7 +33,7 @@ type Props = {
const EventListItem = ({ event, latest, document, ...rest }: Props) => { const EventListItem = ({ event, latest, document, ...rest }: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const location = useLocation(); const location = useLocation();
const can = usePolicy(document.id); const can = usePolicy(document);
const opts = { const opts = {
userName: event.actor.name, userName: event.actor.name,
}; };

View File

@@ -36,7 +36,7 @@ function AppSidebar() {
const { documents } = useStores(); const { documents } = useStores();
const team = useCurrentTeam(); const team = useCurrentTeam();
const user = useCurrentUser(); const user = useCurrentUser();
const can = usePolicy(team.id); const can = usePolicy(team);
React.useEffect(() => { React.useEffect(() => {
if (!user.isViewer) { if (!user.isViewer) {

View File

@@ -44,7 +44,7 @@ const CollectionLink: React.FC<Props> = ({
const { dialogs, documents, collections } = useStores(); const { dialogs, documents, collections } = useStores();
const [menuOpen, handleMenuOpen, handleMenuClose] = useBoolean(); const [menuOpen, handleMenuOpen, handleMenuClose] = useBoolean();
const [isEditing, setIsEditing] = React.useState(false); const [isEditing, setIsEditing] = React.useState(false);
const canUpdate = usePolicy(collection.id).update; const canUpdate = usePolicy(collection).update;
const { t } = useTranslation(); const { t } = useTranslation();
const history = useHistory(); const history = useHistory();
const inStarredSection = useStarredContext(); const inStarredSection = useStarredContext();

View File

@@ -25,7 +25,7 @@ function CollectionLinkChildren({
expanded, expanded,
prefetchDocument, prefetchDocument,
}: Props) { }: Props) {
const can = usePolicy(collection.id); const can = usePolicy(collection);
const { showToast } = useToasts(); const { showToast } = useToasts();
const manualSort = collection.sort.field === "index"; const manualSort = collection.sort.field === "index";
const { documents } = useStores(); const { documents } = useStores();

View File

@@ -39,7 +39,7 @@ function DraggableCollectionLink({
const [expanded, setExpanded] = React.useState( const [expanded, setExpanded] = React.useState(
collection.id === ui.activeCollectionId && !locationStateStarred collection.id === ui.activeCollectionId && !locationStateStarred
); );
const can = usePolicy(collection.id); const can = usePolicy(collection);
const belowCollectionIndex = belowCollection ? belowCollection.index : null; const belowCollectionIndex = belowCollection ? belowCollection.index : null;
// Drop to reorder collection // Drop to reorder collection

View File

@@ -8,6 +8,7 @@ import RootStore from "~/stores/RootStore";
import Collection from "~/models/Collection"; import Collection from "~/models/Collection";
import Document from "~/models/Document"; import Document from "~/models/Document";
import FileOperation from "~/models/FileOperation"; import FileOperation from "~/models/FileOperation";
import Group from "~/models/Group";
import Pin from "~/models/Pin"; import Pin from "~/models/Pin";
import Star from "~/models/Star"; import Star from "~/models/Star";
import Team from "~/models/Team"; import Team from "~/models/Team";
@@ -237,8 +238,7 @@ class SocketProvider extends React.Component<Props> {
this.socket.on( this.socket.on(
"documents.update", "documents.update",
(event: PartialWithId<Document> & { title: string; url: string }) => { (event: PartialWithId<Document> & { title: string; url: string }) => {
const document = documents.get(event.id); documents.patch(event);
document?.updateFromJson(event);
if (event.collectionId) { if (event.collectionId) {
const collection = collections.get(event.collectionId); const collection = collections.get(event.collectionId);
@@ -264,6 +264,14 @@ class SocketProvider extends React.Component<Props> {
} }
); );
this.socket.on("groups.create", (event: PartialWithId<Group>) => {
groups.add(event);
});
this.socket.on("groups.update", (event: PartialWithId<Group>) => {
groups.patch(event);
});
this.socket.on("groups.delete", (event: WebsocketEntityDeletedEvent) => { this.socket.on("groups.delete", (event: WebsocketEntityDeletedEvent) => {
groups.remove(event.modelId); groups.remove(event.modelId);
}); });
@@ -299,7 +307,7 @@ class SocketProvider extends React.Component<Props> {
}); });
this.socket.on("pins.update", (event: PartialWithId<Pin>) => { this.socket.on("pins.update", (event: PartialWithId<Pin>) => {
pins.add(event); pins.patch(event);
}); });
this.socket.on("pins.delete", (event: WebsocketEntityDeletedEvent) => { this.socket.on("pins.delete", (event: WebsocketEntityDeletedEvent) => {
@@ -311,7 +319,7 @@ class SocketProvider extends React.Component<Props> {
}); });
this.socket.on("stars.update", (event: PartialWithId<Star>) => { this.socket.on("stars.update", (event: PartialWithId<Star>) => {
stars.add(event); stars.patch(event);
}); });
this.socket.on("stars.delete", (event: WebsocketEntityDeletedEvent) => { this.socket.on("stars.delete", (event: WebsocketEntityDeletedEvent) => {

View File

@@ -67,7 +67,7 @@ type ConfigType = {
const useAuthorizedSettingsConfig = () => { const useAuthorizedSettingsConfig = () => {
const team = useCurrentTeam(); const team = useCurrentTeam();
const can = usePolicy(team.id); const can = usePolicy(team);
const { t } = useTranslation(); const { t } = useTranslation();
const config: ConfigType = React.useMemo( const config: ConfigType = React.useMemo(

View File

@@ -1,12 +1,34 @@
import * as React from "react";
import BaseModel from "~/models/BaseModel";
import useStores from "./useStores"; 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 * @param entity The model or model id
* @returns The available abilities * @returns The policy for the model
*/ */
export default function usePolicy(entityId: string) { export default function usePolicy(entity: string | BaseModel | undefined) {
const { policies } = useStores(); 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); return policies.abilities(entityId);
} }

View File

@@ -187,8 +187,8 @@ function CollectionMenu({
); );
const alphabeticalSort = collection.sort.field === "title"; const alphabeticalSort = collection.sort.field === "title";
const can = usePolicy(collection.id); const can = usePolicy(collection);
const canUserInTeam = usePolicy(team.id); const canUserInTeam = usePolicy(team);
const items: MenuItem[] = React.useMemo( const items: MenuItem[] = React.useMemo(
() => [ () => [
{ {

View File

@@ -123,7 +123,7 @@ function DocumentMenu({
}, [menu]); }, [menu]);
const collection = collections.get(document.collectionId); const collection = collections.get(document.collectionId);
const can = usePolicy(document.id); const can = usePolicy(document);
const canViewHistory = can.read && !can.restore; const canViewHistory = can.read && !can.restore;
const restoreItems = React.useMemo( const restoreItems = React.useMemo(
() => [ () => [

View File

@@ -24,7 +24,7 @@ function GroupMenu({ group, onMembers }: Props) {
}); });
const [editModalOpen, setEditModalOpen] = React.useState(false); const [editModalOpen, setEditModalOpen] = React.useState(false);
const [deleteModalOpen, setDeleteModalOpen] = React.useState(false); const [deleteModalOpen, setDeleteModalOpen] = React.useState(false);
const can = usePolicy(group.id); const can = usePolicy(group);
return ( return (
<> <>

View File

@@ -27,7 +27,7 @@ function NewDocumentMenu() {
const { t } = useTranslation(); const { t } = useTranslation();
const team = useCurrentTeam(); const team = useCurrentTeam();
const { collections, policies } = useStores(); const { collections, policies } = useStores();
const can = usePolicy(team.id); const can = usePolicy(team);
const items = React.useMemo( const items = React.useMemo(
() => () =>
collections.orderedData.reduce<MenuItem[]>((filtered, collection) => { collections.orderedData.reduce<MenuItem[]>((filtered, collection) => {

View File

@@ -22,7 +22,7 @@ function NewTemplateMenu() {
const { t } = useTranslation(); const { t } = useTranslation();
const team = useCurrentTeam(); const team = useCurrentTeam();
const { collections, policies } = useStores(); const { collections, policies } = useStores();
const can = usePolicy(team.id); const can = usePolicy(team);
const items = React.useMemo( const items = React.useMemo(
() => () =>

View File

@@ -64,7 +64,7 @@ const RedirectDocument = ({
function AuthenticatedRoutes() { function AuthenticatedRoutes() {
const team = useCurrentTeam(); const team = useCurrentTeam();
const can = usePolicy(team.id); const can = usePolicy(team);
return ( return (
<SocketProvider> <SocketProvider>

View File

@@ -18,7 +18,7 @@ type Props = {
function Actions({ collection }: Props) { function Actions({ collection }: Props) {
const { t } = useTranslation(); const { t } = useTranslation();
const can = usePolicy(collection.id); const can = usePolicy(collection);
return ( return (
<> <>

View File

@@ -20,7 +20,7 @@ type Props = {
function EmptyCollection({ collection }: Props) { function EmptyCollection({ collection }: Props) {
const { t } = useTranslation(); const { t } = useTranslation();
const can = usePolicy(collection.id); const can = usePolicy(collection);
const collectionName = collection ? collection.name : ""; const collectionName = collection ? collection.name : "";
const [ const [

View File

@@ -57,7 +57,7 @@ function DataLoader({ match, children }: Props) {
: undefined; : undefined;
const isEditRoute = match.path === matchDocumentEdit; const isEditRoute = match.path === matchDocumentEdit;
const isEditing = isEditRoute || !!auth.team?.collaborativeEditing; const isEditing = isEditRoute || !!auth.team?.collaborativeEditing;
const can = usePolicy(document ? document.id : ""); const can = usePolicy(document);
const location = useLocation<LocationState>(); const location = useLocation<LocationState>();
React.useEffect(() => { React.useEffect(() => {

View File

@@ -100,7 +100,7 @@ function DocumentHeader({
}, [onSave]); }, [onSave]);
const { isDeleted, isTemplate } = document; const { isDeleted, isTemplate } = document;
const can = usePolicy(document.id); const can = usePolicy(document);
const canToggleEmbeds = team?.documentEmbeds; const canToggleEmbeds = team?.documentEmbeds;
const canEdit = can.update && !isEditing; const canEdit = can.update && !isEditing;
const toc = ( const toc = (

View File

@@ -45,7 +45,7 @@ function SharePopover({
const timeout = React.useRef<ReturnType<typeof setTimeout>>(); const timeout = React.useRef<ReturnType<typeof setTimeout>>();
const buttonRef = React.useRef<HTMLButtonElement>(null); const buttonRef = React.useRef<HTMLButtonElement>(null);
const can = usePolicy(share ? share.id : ""); const can = usePolicy(share ? share.id : "");
const documentAbilities = usePolicy(document.id); const documentAbilities = usePolicy(document);
const canPublish = const canPublish =
can.update && can.update &&
!document.isTemplate && !document.isTemplate &&

View File

@@ -26,7 +26,7 @@ function GroupMembers({ group }: Props) {
const { users, groupMemberships } = useStores(); const { users, groupMemberships } = useStores();
const { showToast } = useToasts(); const { showToast } = useToasts();
const { t } = useTranslation(); const { t } = useTranslation();
const can = usePolicy(group.id); const can = usePolicy(group);
const handleAddModal = (state: boolean) => { const handleAddModal = (state: boolean) => {
setAddModalOpen(state); setAddModalOpen(state);

View File

@@ -30,7 +30,7 @@ function Home() {
pins.fetchPage(); pins.fetchPage();
}, [pins]); }, [pins]);
const canManageTeam = usePolicy(team.id).manage; const canManageTeam = usePolicy(team).manage;
return ( return (
<Scene <Scene

View File

@@ -56,7 +56,7 @@ function Invite({ onSubmit }: Props) {
const team = useCurrentTeam(); const team = useCurrentTeam();
const { t } = useTranslation(); const { t } = useTranslation();
const predictedDomain = user.email.split("@")[1]; const predictedDomain = user.email.split("@")[1];
const can = usePolicy(team.id); const can = usePolicy(team);
const handleSubmit = React.useCallback( const handleSubmit = React.useCallback(
async (ev: React.SyntheticEvent) => { async (ev: React.SyntheticEvent) => {

View File

@@ -24,7 +24,7 @@ function Groups() {
const { t } = useTranslation(); const { t } = useTranslation();
const { groups } = useStores(); const { groups } = useStores();
const team = useCurrentTeam(); const team = useCurrentTeam();
const can = usePolicy(team.id); const can = usePolicy(team);
const [ const [
newGroupModalOpen, newGroupModalOpen,
handleNewGroupModalOpen, handleNewGroupModalOpen,

View File

@@ -40,7 +40,7 @@ function Members() {
const [data, setData] = React.useState<User[]>([]); const [data, setData] = React.useState<User[]>([]);
const [totalPages, setTotalPages] = React.useState(0); const [totalPages, setTotalPages] = React.useState(0);
const [userIds, setUserIds] = React.useState<string[]>([]); const [userIds, setUserIds] = React.useState<string[]>([]);
const can = usePolicy(team.id); const can = usePolicy(team);
const query = params.get("query") || ""; const query = params.get("query") || "";
const filter = params.get("filter") || ""; const filter = params.get("filter") || "";
const sort = params.get("sort") || "name"; const sort = params.get("sort") || "name";

View File

@@ -21,7 +21,7 @@ function Shares() {
const { t } = useTranslation(); const { t } = useTranslation();
const { shares, auth } = useStores(); const { shares, auth } = useStores();
const canShareDocuments = auth.team && auth.team.sharing; const canShareDocuments = auth.team && auth.team.sharing;
const can = usePolicy(team.id); const can = usePolicy(team);
const [isLoading, setIsLoading] = React.useState(false); const [isLoading, setIsLoading] = React.useState(false);
const [data, setData] = React.useState<Share[]>([]); const [data, setData] = React.useState<Share[]>([]);
const [totalPages, setTotalPages] = React.useState(0); const [totalPages, setTotalPages] = React.useState(0);

View File

@@ -23,7 +23,7 @@ function Tokens() {
const { t } = useTranslation(); const { t } = useTranslation();
const { apiKeys } = useStores(); const { apiKeys } = useStores();
const [newModalOpen, handleNewModalOpen, handleNewModalClose] = useBoolean(); const [newModalOpen, handleNewModalOpen, handleNewModalClose] = useBoolean();
const can = usePolicy(team.id); const can = usePolicy(team);
return ( return (
<Scene <Scene

View File

@@ -23,7 +23,7 @@ function Webhooks() {
const { t } = useTranslation(); const { t } = useTranslation();
const { webhookSubscriptions } = useStores(); const { webhookSubscriptions } = useStores();
const [newModalOpen, handleNewModalOpen, handleNewModalClose] = useBoolean(); const [newModalOpen, handleNewModalOpen, handleNewModalClose] = useBoolean();
const can = usePolicy(team.id); const can = usePolicy(team);
return ( return (
<Scene <Scene

View File

@@ -21,7 +21,7 @@ function Templates(props: RouteComponentProps<{ sort: string }>) {
const team = useCurrentTeam(); const team = useCurrentTeam();
const { fetchTemplates, templates, templatesAlphabetical } = documents; const { fetchTemplates, templates, templatesAlphabetical } = documents;
const { sort } = props.match.params; const { sort } = props.match.params;
const can = usePolicy(team.id); const can = usePolicy(team);
return ( return (
<Scene <Scene

View File

@@ -101,8 +101,20 @@ export default abstract class BaseStore<T extends BaseModel> {
}; };
@action @action
remove(id: string): void { patch = (item: PartialWithId<T> | T): T | undefined => {
this.data.delete(id); 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( save(

View File

@@ -16,6 +16,7 @@ import {
presentCollection, presentCollection,
presentDocument, presentDocument,
presentFileOperation, presentFileOperation,
presentGroup,
presentPin, presentPin,
presentStar, presentStar,
presentTeam, presentTeam,
@@ -356,8 +357,9 @@ export default class WebsocketsProcessor {
if (!fileOperation) { if (!fileOperation) {
return; return;
} }
const data = await presentFileOperation(fileOperation); return socketio
return socketio.to(`user-${event.actorId}`).emit(event.name, data); .to(`user-${event.actorId}`)
.emit(event.name, presentFileOperation(fileOperation));
} }
case "pins.create": case "pins.create":
@@ -412,15 +414,9 @@ export default class WebsocketsProcessor {
if (!group) { if (!group) {
return; return;
} }
return socketio.to(`team-${group.teamId}`).emit("entities", { return socketio
event: event.name, .to(`team-${group.teamId}`)
groupIds: [ .emit(event.name, presentGroup(group));
{
id: group.id,
updatedAt: group.updatedAt,
},
],
});
} }
case "groups.add_user": { case "groups.add_user": {