feat: Collection admins (#5273
* Split permissions for reading documents from updating collection * fix: Admins should have collection read permission, tests * tsc * Add admin option to permission selector * Combine publish and create permissions, update -> createDocuments where appropriate * Plural -> singular * wip * Quick version of collection structure loading, will revisit * Remove documentIds method * stash * fixing tests to account for admin creation * Add self-hosted migration * fix: Allow groups to have admin permission * Prefetch collection documents * fix: Document explorer (move/publish) not working with async documents * fix: Cannot re-parent document to collection by drag and drop * fix: Cannot drag to import into collection item without admin permission * Remove unused isEditor getter
This commit is contained in:
@@ -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).update;
|
||||
const can = usePolicy(collection);
|
||||
const { t } = useTranslation();
|
||||
const history = useHistory();
|
||||
const inStarredSection = useStarredContext();
|
||||
@@ -105,7 +105,7 @@ const CollectionLink: React.FC<Props> = ({
|
||||
}
|
||||
}
|
||||
},
|
||||
canDrop: () => canUpdate,
|
||||
canDrop: () => can.createDocument,
|
||||
collect: (monitor) => ({
|
||||
isOver: !!monitor.isOver({
|
||||
shallow: true,
|
||||
@@ -118,6 +118,10 @@ const CollectionLink: React.FC<Props> = ({
|
||||
setIsEditing(isEditing);
|
||||
}, []);
|
||||
|
||||
const handleMouseEnter = React.useCallback(() => {
|
||||
void collection.fetchDocuments();
|
||||
}, [collection]);
|
||||
|
||||
const context = useActionContext({
|
||||
activeCollectionId: collection.id,
|
||||
inStarredSection,
|
||||
@@ -134,6 +138,7 @@ const CollectionLink: React.FC<Props> = ({
|
||||
}}
|
||||
expanded={expanded}
|
||||
onDisclosureClick={onDisclosureClick}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
icon={
|
||||
<CollectionIcon collection={collection} expanded={expanded} />
|
||||
}
|
||||
@@ -147,7 +152,7 @@ const CollectionLink: React.FC<Props> = ({
|
||||
title={collection.name}
|
||||
onSubmit={handleTitleChange}
|
||||
onEditing={handleTitleEditing}
|
||||
canUpdate={canUpdate}
|
||||
canUpdate={can.update}
|
||||
/>
|
||||
}
|
||||
exact={false}
|
||||
|
||||
@@ -2,8 +2,12 @@ import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { useDrop } from "react-dnd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import styled from "styled-components";
|
||||
import Collection from "~/models/Collection";
|
||||
import Document from "~/models/Document";
|
||||
import DelayedMount from "~/components/DelayedMount";
|
||||
import DocumentsLoader from "~/components/DocumentsLoader";
|
||||
import { ResizingHeightContainer } from "~/components/ResizingHeightContainer";
|
||||
import usePolicy from "~/hooks/usePolicy";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import useToasts from "~/hooks/useToasts";
|
||||
@@ -11,6 +15,7 @@ import DocumentLink from "./DocumentLink";
|
||||
import DropCursor from "./DropCursor";
|
||||
import EmptyCollectionPlaceholder from "./EmptyCollectionPlaceholder";
|
||||
import Folder from "./Folder";
|
||||
import PlaceholderCollections from "./PlaceholderCollections";
|
||||
import { DragObject } from "./SidebarLink";
|
||||
import useCollectionDocuments from "./useCollectionDocuments";
|
||||
|
||||
@@ -63,28 +68,42 @@ function CollectionLinkChildren({
|
||||
|
||||
return (
|
||||
<Folder expanded={expanded}>
|
||||
{isDraggingAnyDocument && can.update && manualSort && (
|
||||
{isDraggingAnyDocument && can.createDocument && manualSort && (
|
||||
<DropCursor
|
||||
isActiveDrop={isOverReorder}
|
||||
innerRef={dropToReorder}
|
||||
position="top"
|
||||
/>
|
||||
)}
|
||||
{childDocuments.map((node, index) => (
|
||||
<DocumentLink
|
||||
key={node.id}
|
||||
node={node}
|
||||
collection={collection}
|
||||
activeDocument={documents.active}
|
||||
prefetchDocument={prefetchDocument}
|
||||
isDraft={node.isDraft}
|
||||
depth={2}
|
||||
index={index}
|
||||
/>
|
||||
))}
|
||||
{childDocuments.length === 0 && <EmptyCollectionPlaceholder />}
|
||||
<DocumentsLoader collection={collection} enabled={expanded}>
|
||||
{!childDocuments && (
|
||||
<ResizingHeightContainer hideOverflow>
|
||||
<DelayedMount>
|
||||
<Loading />
|
||||
</DelayedMount>
|
||||
</ResizingHeightContainer>
|
||||
)}
|
||||
{childDocuments?.map((node, index) => (
|
||||
<DocumentLink
|
||||
key={node.id}
|
||||
node={node}
|
||||
collection={collection}
|
||||
activeDocument={documents.active}
|
||||
prefetchDocument={prefetchDocument}
|
||||
isDraft={node.isDraft}
|
||||
depth={2}
|
||||
index={index}
|
||||
/>
|
||||
))}
|
||||
{childDocuments?.length === 0 && <EmptyCollectionPlaceholder />}
|
||||
</DocumentsLoader>
|
||||
</Folder>
|
||||
);
|
||||
}
|
||||
|
||||
const Loading = styled(PlaceholderCollections)`
|
||||
margin-left: 44px;
|
||||
min-height: 90px;
|
||||
`;
|
||||
|
||||
export default observer(CollectionLinkChildren);
|
||||
|
||||
@@ -26,10 +26,14 @@ function DropToImport({ disabled, children, collectionId, documentId }: Props) {
|
||||
collectionId,
|
||||
documentId
|
||||
);
|
||||
const targetId = collectionId || documentId;
|
||||
invariant(targetId, "Must provide either collectionId or documentId");
|
||||
invariant(
|
||||
collectionId || documentId,
|
||||
"Must provide either collectionId or documentId"
|
||||
);
|
||||
|
||||
const canCollection = usePolicy(collectionId);
|
||||
const canDocument = usePolicy(documentId);
|
||||
|
||||
const can = usePolicy(targetId);
|
||||
const handleRejection = React.useCallback(() => {
|
||||
showToast(
|
||||
t("Document not supported – try Markdown, Plain text, HTML, or Word"),
|
||||
@@ -39,7 +43,11 @@ function DropToImport({ disabled, children, collectionId, documentId }: Props) {
|
||||
);
|
||||
}, [t, showToast]);
|
||||
|
||||
if (disabled || !can.update) {
|
||||
if (
|
||||
disabled ||
|
||||
(collectionId && !canCollection.createDocument) ||
|
||||
(documentId && !canDocument.createChildDocument)
|
||||
) {
|
||||
return children;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@ import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import PlaceholderText from "~/components/PlaceholderText";
|
||||
|
||||
function PlaceholderCollections() {
|
||||
function PlaceholderCollections(props: React.HTMLAttributes<HTMLDivElement>) {
|
||||
return (
|
||||
<Wrapper>
|
||||
<Wrapper {...props}>
|
||||
<PlaceholderText />
|
||||
<PlaceholderText delay={0.2} />
|
||||
<PlaceholderText delay={0.4} />
|
||||
|
||||
@@ -8,8 +8,8 @@ export default function useCollectionDocuments(
|
||||
activeDocument: Document | undefined
|
||||
) {
|
||||
return React.useMemo(() => {
|
||||
if (!collection) {
|
||||
return [];
|
||||
if (!collection?.sortedDocuments) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const insertDraftDocument =
|
||||
|
||||
Reference in New Issue
Block a user