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 [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);

View File

@@ -49,8 +49,8 @@ function DocumentListItem(
ref: React.RefObject<HTMLAnchorElement>
) {
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 && (
<Badge yellow>{t("New")}</Badge>
)}
{canStar && (

View File

@@ -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,
};

View File

@@ -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) {

View File

@@ -44,7 +44,7 @@ const CollectionLink: React.FC<Props> = ({
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();

View File

@@ -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();

View File

@@ -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

View File

@@ -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<Props> {
this.socket.on(
"documents.update",
(event: PartialWithId<Document> & { 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<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) => {
groups.remove(event.modelId);
});
@@ -299,7 +307,7 @@ class SocketProvider extends React.Component<Props> {
});
this.socket.on("pins.update", (event: PartialWithId<Pin>) => {
pins.add(event);
pins.patch(event);
});
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>) => {
stars.add(event);
stars.patch(event);
});
this.socket.on("stars.delete", (event: WebsocketEntityDeletedEvent) => {

View File

@@ -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(

View File

@@ -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);
}

View File

@@ -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(
() => [
{

View File

@@ -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(
() => [

View File

@@ -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 (
<>

View File

@@ -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<MenuItem[]>((filtered, collection) => {

View File

@@ -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(
() =>

View File

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

View File

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

View File

@@ -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 [

View File

@@ -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<LocationState>();
React.useEffect(() => {

View File

@@ -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 = (

View File

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

View File

@@ -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);

View File

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

View File

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

View File

@@ -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,

View File

@@ -40,7 +40,7 @@ function Members() {
const [data, setData] = React.useState<User[]>([]);
const [totalPages, setTotalPages] = React.useState(0);
const [userIds, setUserIds] = React.useState<string[]>([]);
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";

View File

@@ -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<Share[]>([]);
const [totalPages, setTotalPages] = React.useState(0);

View File

@@ -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 (
<Scene

View File

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

View File

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

View File

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

View File

@@ -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": {