feat: Updated collection header (#4101)
* Return total results from collection membership endpoints * Display membership preview on collections * fix permissions * Revert unneccessary changes
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
CollectionIcon,
|
||||
EditIcon,
|
||||
PadlockIcon,
|
||||
PlusIcon,
|
||||
StarredIcon,
|
||||
UnstarredIcon,
|
||||
@@ -10,6 +11,7 @@ import stores from "~/stores";
|
||||
import Collection from "~/models/Collection";
|
||||
import CollectionEdit from "~/scenes/CollectionEdit";
|
||||
import CollectionNew from "~/scenes/CollectionNew";
|
||||
import CollectionPermissions from "~/scenes/CollectionPermissions";
|
||||
import DynamicCollectionIcon from "~/components/CollectionIcon";
|
||||
import { createAction } from "~/actions";
|
||||
import { CollectionSection } from "~/actions/sections";
|
||||
@@ -56,7 +58,8 @@ export const createCollection = createAction({
|
||||
});
|
||||
|
||||
export const editCollection = createAction({
|
||||
name: ({ t }) => t("Edit collection"),
|
||||
name: ({ t, isContextMenu }) =>
|
||||
isContextMenu ? `${t("Edit")}…` : t("Edit collection"),
|
||||
section: CollectionSection,
|
||||
icon: <EditIcon />,
|
||||
visible: ({ stores, activeCollectionId }) =>
|
||||
@@ -79,6 +82,26 @@ export const editCollection = createAction({
|
||||
},
|
||||
});
|
||||
|
||||
export const editCollectionPermissions = createAction({
|
||||
name: ({ t, isContextMenu }) =>
|
||||
isContextMenu ? `${t("Permissions")}…` : t("Collection permissions"),
|
||||
section: CollectionSection,
|
||||
icon: <PadlockIcon />,
|
||||
visible: ({ stores, activeCollectionId }) =>
|
||||
!!activeCollectionId &&
|
||||
stores.policies.abilities(activeCollectionId).update,
|
||||
perform: ({ t, activeCollectionId }) => {
|
||||
if (!activeCollectionId) {
|
||||
return;
|
||||
}
|
||||
|
||||
stores.dialogs.openModal({
|
||||
title: t("Collection permissions"),
|
||||
content: <CollectionPermissions collectionId={activeCollectionId} />,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const starCollection = createAction({
|
||||
name: ({ t }) => t("Star"),
|
||||
section: CollectionSection,
|
||||
|
||||
@@ -9,7 +9,7 @@ type Props = {
|
||||
users: User[];
|
||||
size?: number;
|
||||
overflow?: number;
|
||||
onClick?: React.MouseEventHandler<HTMLDivElement>;
|
||||
limit?: number;
|
||||
renderAvatar?: (user: User) => React.ReactNode;
|
||||
};
|
||||
|
||||
@@ -17,6 +17,7 @@ function Facepile({
|
||||
users,
|
||||
overflow = 0,
|
||||
size = 32,
|
||||
limit = 8,
|
||||
renderAvatar = DefaultAvatar,
|
||||
...rest
|
||||
}: Props) {
|
||||
@@ -24,10 +25,13 @@ function Facepile({
|
||||
<Avatars {...rest}>
|
||||
{overflow > 0 && (
|
||||
<More size={size}>
|
||||
<span>+{overflow}</span>
|
||||
<span>
|
||||
{users.length ? "+" : ""}
|
||||
{overflow}
|
||||
</span>
|
||||
</More>
|
||||
)}
|
||||
{users.map((user) => (
|
||||
{users.slice(0, limit).map((user) => (
|
||||
<AvatarWrapper key={user.id}>{renderAvatar(user)}</AvatarWrapper>
|
||||
))}
|
||||
</Avatars>
|
||||
|
||||
@@ -13,6 +13,7 @@ import Flex from "~/components/Flex";
|
||||
import ListItem from "~/components/List/Item";
|
||||
import Modal from "~/components/Modal";
|
||||
import withStores from "~/components/withStores";
|
||||
import NudeButton from "./NudeButton";
|
||||
|
||||
type Props = RootStore & {
|
||||
group: Group;
|
||||
@@ -63,11 +64,13 @@ class GroupListItem extends React.Component<Props> {
|
||||
actions={
|
||||
<Flex align="center" gap={8}>
|
||||
{showFacepile && (
|
||||
<Facepile
|
||||
<NudeButton
|
||||
width="auto"
|
||||
height="auto"
|
||||
onClick={this.handleMembersModalOpen}
|
||||
users={users}
|
||||
overflow={overflow}
|
||||
/>
|
||||
>
|
||||
<Facepile users={users} overflow={overflow} />
|
||||
</NudeButton>
|
||||
)}
|
||||
{renderActions({
|
||||
openMembersModal: this.handleMembersModalOpen,
|
||||
|
||||
@@ -15,19 +15,19 @@ import useStores from "~/hooks/useStores";
|
||||
import { supportsPassiveListener } from "~/utils/browser";
|
||||
|
||||
type Props = {
|
||||
breadcrumb?: React.ReactNode;
|
||||
left?: React.ReactNode;
|
||||
title: React.ReactNode;
|
||||
actions?: React.ReactNode;
|
||||
hasSidebar?: boolean;
|
||||
};
|
||||
|
||||
function Header({ breadcrumb, title, actions, hasSidebar }: Props) {
|
||||
function Header({ left, title, actions, hasSidebar }: Props) {
|
||||
const { ui } = useStores();
|
||||
const isMobile = useMobile();
|
||||
|
||||
const hasMobileSidebar = hasSidebar && isMobile;
|
||||
|
||||
const passThrough = !actions && !breadcrumb && !title;
|
||||
const passThrough = !actions && !left && !title;
|
||||
|
||||
const [isScrolled, setScrolled] = React.useState(false);
|
||||
const handleScroll = React.useMemo(
|
||||
@@ -51,7 +51,7 @@ function Header({ breadcrumb, title, actions, hasSidebar }: Props) {
|
||||
|
||||
return (
|
||||
<Wrapper align="center" shrink={false} $passThrough={passThrough}>
|
||||
{breadcrumb || hasMobileSidebar ? (
|
||||
{left || hasMobileSidebar ? (
|
||||
<Breadcrumbs>
|
||||
{hasMobileSidebar && (
|
||||
<MobileMenuButton
|
||||
@@ -61,7 +61,7 @@ function Header({ breadcrumb, title, actions, hasSidebar }: Props) {
|
||||
neutral
|
||||
/>
|
||||
)}
|
||||
{breadcrumb}
|
||||
{left}
|
||||
</Breadcrumbs>
|
||||
) : null}
|
||||
|
||||
|
||||
@@ -173,7 +173,7 @@ class Input extends React.Component<Props> {
|
||||
{type === "textarea" ? (
|
||||
<RealTextarea
|
||||
ref={this.props.innerRef}
|
||||
onBlur={this.props.onBlur}
|
||||
onBlur={this.handleBlur}
|
||||
onFocus={this.handleFocus}
|
||||
hasIcon={!!icon}
|
||||
{...rest}
|
||||
@@ -181,7 +181,7 @@ class Input extends React.Component<Props> {
|
||||
) : (
|
||||
<RealInput
|
||||
ref={this.props.innerRef}
|
||||
onBlur={this.props.onBlur}
|
||||
onBlur={this.handleBlur}
|
||||
onFocus={this.handleFocus}
|
||||
hasIcon={!!icon}
|
||||
type={type}
|
||||
|
||||
@@ -4,8 +4,8 @@ import ActionButton, {
|
||||
} from "~/components/ActionButton";
|
||||
|
||||
type Props = ActionButtonProps & {
|
||||
width?: number;
|
||||
height?: number;
|
||||
width?: number | string;
|
||||
height?: number | string;
|
||||
size?: number;
|
||||
type?: "button" | "submit" | "reset";
|
||||
};
|
||||
@@ -13,8 +13,14 @@ type Props = ActionButtonProps & {
|
||||
const NudeButton = styled(ActionButton).attrs((props: Props) => ({
|
||||
type: "type" in props ? props.type : "button",
|
||||
}))<Props>`
|
||||
width: ${(props) => props.width || props.size || 24}px;
|
||||
height: ${(props) => props.height || props.size || 24}px;
|
||||
width: ${(props) =>
|
||||
typeof props.width === "string"
|
||||
? props.width
|
||||
: `${props.width || props.size || 24}px`};
|
||||
height: ${(props) =>
|
||||
typeof props.height === "string"
|
||||
? props.height
|
||||
: `${props.width || props.size || 24}px`};
|
||||
background: none;
|
||||
border-radius: 4px;
|
||||
display: inline-block;
|
||||
|
||||
@@ -8,7 +8,7 @@ type Props = {
|
||||
icon?: React.ReactNode;
|
||||
title?: React.ReactNode;
|
||||
textTitle?: string;
|
||||
breadcrumb?: React.ReactNode;
|
||||
left?: React.ReactNode;
|
||||
actions?: React.ReactNode;
|
||||
centered?: boolean;
|
||||
};
|
||||
@@ -18,7 +18,7 @@ const Scene: React.FC<Props> = ({
|
||||
icon,
|
||||
textTitle,
|
||||
actions,
|
||||
breadcrumb,
|
||||
left,
|
||||
children,
|
||||
centered,
|
||||
}) => {
|
||||
@@ -37,7 +37,7 @@ const Scene: React.FC<Props> = ({
|
||||
)
|
||||
}
|
||||
actions={actions}
|
||||
breadcrumb={breadcrumb}
|
||||
left={left}
|
||||
/>
|
||||
{centered !== false ? (
|
||||
<CenteredContent withStickyHeader>{children}</CenteredContent>
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { observer } from "mobx-react";
|
||||
import {
|
||||
NewDocumentIcon,
|
||||
EditIcon,
|
||||
TrashIcon,
|
||||
ImportIcon,
|
||||
ExportIcon,
|
||||
PadlockIcon,
|
||||
AlphabeticalSortIcon,
|
||||
ManualSortIcon,
|
||||
UnstarredIcon,
|
||||
@@ -18,13 +16,17 @@ import { useMenuState, MenuButton, MenuButtonHTMLProps } from "reakit/Menu";
|
||||
import { VisuallyHidden } from "reakit/VisuallyHidden";
|
||||
import { getEventFiles } from "@shared/utils/files";
|
||||
import Collection from "~/models/Collection";
|
||||
import CollectionEdit from "~/scenes/CollectionEdit";
|
||||
import CollectionExport from "~/scenes/CollectionExport";
|
||||
import CollectionPermissions from "~/scenes/CollectionPermissions";
|
||||
import CollectionDeleteDialog from "~/components/CollectionDeleteDialog";
|
||||
import ContextMenu, { Placement } from "~/components/ContextMenu";
|
||||
import OverflowMenuButton from "~/components/ContextMenu/OverflowMenuButton";
|
||||
import Template from "~/components/ContextMenu/Template";
|
||||
import { actionToMenuItem } from "~/actions";
|
||||
import {
|
||||
editCollection,
|
||||
editCollectionPermissions,
|
||||
} from "~/actions/definitions/collections";
|
||||
import useActionContext from "~/hooks/useActionContext";
|
||||
import useCurrentTeam from "~/hooks/useCurrentTeam";
|
||||
import usePolicy from "~/hooks/usePolicy";
|
||||
import useStores from "~/hooks/useStores";
|
||||
@@ -60,25 +62,6 @@ function CollectionMenu({
|
||||
const history = useHistory();
|
||||
const file = React.useRef<HTMLInputElement>(null);
|
||||
|
||||
const handlePermissions = React.useCallback(() => {
|
||||
dialogs.openModal({
|
||||
title: t("Collection permissions"),
|
||||
content: <CollectionPermissions collection={collection} />,
|
||||
});
|
||||
}, [collection, dialogs, t]);
|
||||
|
||||
const handleEdit = React.useCallback(() => {
|
||||
dialogs.openModal({
|
||||
title: t("Edit collection"),
|
||||
content: (
|
||||
<CollectionEdit
|
||||
collectionId={collection.id}
|
||||
onSubmit={dialogs.closeAllModals}
|
||||
/>
|
||||
),
|
||||
});
|
||||
}, [collection.id, dialogs, t]);
|
||||
|
||||
const handleExport = React.useCallback(() => {
|
||||
dialogs.openModal({
|
||||
title: t("Export collection"),
|
||||
@@ -186,6 +169,11 @@ function CollectionMenu({
|
||||
[collection]
|
||||
);
|
||||
|
||||
const context = useActionContext({
|
||||
isContextMenu: true,
|
||||
activeCollectionId: collection.id,
|
||||
});
|
||||
|
||||
const alphabeticalSort = collection.sort.field === "title";
|
||||
const can = usePolicy(collection);
|
||||
const canUserInTeam = usePolicy(team);
|
||||
@@ -225,6 +213,8 @@ function CollectionMenu({
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
actionToMenuItem(editCollection, context),
|
||||
actionToMenuItem(editCollectionPermissions, context),
|
||||
{
|
||||
type: "submenu",
|
||||
title: t("Sort in sidebar"),
|
||||
@@ -249,20 +239,6 @@ function CollectionMenu({
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
title: `${t("Edit")}…`,
|
||||
visible: can.update,
|
||||
onClick: handleEdit,
|
||||
icon: <EditIcon />,
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
title: `${t("Permissions")}…`,
|
||||
visible: can.update,
|
||||
onClick: handlePermissions,
|
||||
icon: <PadlockIcon />,
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
title: `${t("Export")}…`,
|
||||
@@ -293,9 +269,8 @@ function CollectionMenu({
|
||||
handleStar,
|
||||
handleNewDocument,
|
||||
handleImportDocument,
|
||||
context,
|
||||
alphabeticalSort,
|
||||
handleEdit,
|
||||
handlePermissions,
|
||||
canUserInTeam.createExport,
|
||||
handleExport,
|
||||
handleDelete,
|
||||
|
||||
@@ -18,6 +18,7 @@ import CenteredContent from "~/components/CenteredContent";
|
||||
import CollectionDescription from "~/components/CollectionDescription";
|
||||
import CollectionIcon from "~/components/CollectionIcon";
|
||||
import Heading from "~/components/Heading";
|
||||
import InputSearchPage from "~/components/InputSearchPage";
|
||||
import PlaceholderList from "~/components/List/Placeholder";
|
||||
import PaginatedDocumentList from "~/components/PaginatedDocumentList";
|
||||
import PinnedDocuments from "~/components/PinnedDocuments";
|
||||
@@ -35,6 +36,7 @@ import { collectionUrl, updateCollectionUrl } from "~/utils/routeHelpers";
|
||||
import Actions from "./Collection/Actions";
|
||||
import DropToImport from "./Collection/DropToImport";
|
||||
import Empty from "./Collection/Empty";
|
||||
import MembershipPreview from "./Collection/MembershipPreview";
|
||||
|
||||
function CollectionScene() {
|
||||
const params = useParams<{ id?: string }>();
|
||||
@@ -112,13 +114,28 @@ function CollectionScene() {
|
||||
key={collection.id}
|
||||
centered={false}
|
||||
textTitle={collection.name}
|
||||
left={
|
||||
collection.isEmpty ? undefined : (
|
||||
<InputSearchPage
|
||||
source="collection"
|
||||
placeholder={`${t("Search in collection")}…`}
|
||||
label={t("Search in collection")}
|
||||
collectionId={collection.id}
|
||||
/>
|
||||
)
|
||||
}
|
||||
title={
|
||||
<>
|
||||
<CollectionIcon collection={collection} expanded />
|
||||
{collection.name}
|
||||
</>
|
||||
}
|
||||
actions={<Actions collection={collection} />}
|
||||
actions={
|
||||
<>
|
||||
<MembershipPreview collection={collection} />
|
||||
<Actions collection={collection} />
|
||||
</>
|
||||
}
|
||||
>
|
||||
<DropToImport
|
||||
accept={documents.importFileTypes.join(", ")}
|
||||
|
||||
@@ -6,7 +6,6 @@ import { Link } from "react-router-dom";
|
||||
import Collection from "~/models/Collection";
|
||||
import { Action, Separator } from "~/components/Actions";
|
||||
import Button from "~/components/Button";
|
||||
import InputSearchPage from "~/components/InputSearchPage";
|
||||
import Tooltip from "~/components/Tooltip";
|
||||
import usePolicy from "~/hooks/usePolicy";
|
||||
import CollectionMenu from "~/menus/CollectionMenu";
|
||||
@@ -22,38 +21,26 @@ function Actions({ collection }: Props) {
|
||||
|
||||
return (
|
||||
<>
|
||||
{!collection.isEmpty && (
|
||||
{can.update && (
|
||||
<>
|
||||
<Action>
|
||||
<InputSearchPage
|
||||
source="collection"
|
||||
placeholder={`${t("Search in collection")}…`}
|
||||
label={`${t("Search in collection")}…`}
|
||||
collectionId={collection.id}
|
||||
/>
|
||||
<Tooltip
|
||||
tooltip={t("New document")}
|
||||
shortcut="n"
|
||||
delay={500}
|
||||
placement="bottom"
|
||||
>
|
||||
<Button
|
||||
as={Link}
|
||||
to={collection ? newDocumentPath(collection.id) : ""}
|
||||
disabled={!collection}
|
||||
icon={<PlusIcon />}
|
||||
>
|
||||
{t("New doc")}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Action>
|
||||
{can.update && (
|
||||
<>
|
||||
<Action>
|
||||
<Tooltip
|
||||
tooltip={t("New document")}
|
||||
shortcut="n"
|
||||
delay={500}
|
||||
placement="bottom"
|
||||
>
|
||||
<Button
|
||||
as={Link}
|
||||
to={collection ? newDocumentPath(collection.id) : ""}
|
||||
disabled={!collection}
|
||||
icon={<PlusIcon />}
|
||||
>
|
||||
{t("New doc")}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Action>
|
||||
<Separator />
|
||||
</>
|
||||
)}
|
||||
<Separator />
|
||||
</>
|
||||
)}
|
||||
<Action>
|
||||
|
||||
@@ -5,12 +5,11 @@ import { Trans, useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import styled from "styled-components";
|
||||
import Collection from "~/models/Collection";
|
||||
import CollectionPermissions from "~/scenes/CollectionPermissions";
|
||||
import Button from "~/components/Button";
|
||||
import Flex from "~/components/Flex";
|
||||
import Modal from "~/components/Modal";
|
||||
import Text from "~/components/Text";
|
||||
import useBoolean from "~/hooks/useBoolean";
|
||||
import { editCollectionPermissions } from "~/actions/definitions/collections";
|
||||
import useActionContext from "~/hooks/useActionContext";
|
||||
import usePolicy from "~/hooks/usePolicy";
|
||||
import { newDocumentPath } from "~/utils/routeHelpers";
|
||||
|
||||
@@ -21,13 +20,10 @@ type Props = {
|
||||
function EmptyCollection({ collection }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const can = usePolicy(collection);
|
||||
const context = useActionContext();
|
||||
const collectionName = collection ? collection.name : "";
|
||||
|
||||
const [
|
||||
permissionsModalOpen,
|
||||
handlePermissionsModalOpen,
|
||||
handlePermissionsModalClose,
|
||||
] = useBoolean();
|
||||
console.log({ context });
|
||||
|
||||
return (
|
||||
<Centered column>
|
||||
@@ -48,23 +44,20 @@ function EmptyCollection({ collection }: Props) {
|
||||
{can.update && (
|
||||
<Empty>
|
||||
<Link to={newDocumentPath(collection.id)}>
|
||||
<Button icon={<NewDocumentIcon color="currentColor" />}>
|
||||
<Button icon={<NewDocumentIcon color="currentColor" />} neutral>
|
||||
{t("Create a document")}
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
<Button onClick={handlePermissionsModalOpen} neutral>
|
||||
<Button
|
||||
action={editCollectionPermissions}
|
||||
context={context}
|
||||
hideOnActionDisabled
|
||||
neutral
|
||||
>
|
||||
{t("Manage permissions")}…
|
||||
</Button>
|
||||
</Empty>
|
||||
)}
|
||||
<Modal
|
||||
title={t("Collection permissions")}
|
||||
onRequestClose={handlePermissionsModalClose}
|
||||
isOpen={permissionsModalOpen}
|
||||
>
|
||||
<CollectionPermissions collection={collection} />
|
||||
</Modal>
|
||||
</Centered>
|
||||
);
|
||||
}
|
||||
@@ -79,6 +72,7 @@ const Centered = styled(Flex)`
|
||||
const Empty = styled(Flex)`
|
||||
justify-content: center;
|
||||
margin: 10px 0;
|
||||
gap: 8px;
|
||||
`;
|
||||
|
||||
export default observer(EmptyCollection);
|
||||
|
||||
81
app/scenes/Collection/MembershipPreview.tsx
Normal file
81
app/scenes/Collection/MembershipPreview.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { PAGINATION_SYMBOL } from "~/stores/BaseStore";
|
||||
import Collection from "~/models/Collection";
|
||||
import Facepile from "~/components/Facepile";
|
||||
import Fade from "~/components/Fade";
|
||||
import NudeButton from "~/components/NudeButton";
|
||||
import { editCollectionPermissions } from "~/actions/definitions/collections";
|
||||
import useActionContext from "~/hooks/useActionContext";
|
||||
import useStores from "~/hooks/useStores";
|
||||
|
||||
type Props = {
|
||||
collection: Collection;
|
||||
};
|
||||
|
||||
const MembershipPreview = ({ collection }: Props) => {
|
||||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
const [totalMemberships, setTotalMemberships] = React.useState(0);
|
||||
const { t } = useTranslation();
|
||||
const { memberships, collectionGroupMemberships, users } = useStores();
|
||||
const collectionUsers = users.inCollection(collection.id);
|
||||
const context = useActionContext();
|
||||
|
||||
React.useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
if (collection.permission) {
|
||||
return;
|
||||
}
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const options = {
|
||||
id: collection.id,
|
||||
limit: 8,
|
||||
};
|
||||
const [users, groups] = await Promise.all([
|
||||
memberships.fetchPage(options),
|
||||
collectionGroupMemberships.fetchPage(options),
|
||||
]);
|
||||
setTotalMemberships(
|
||||
users[PAGINATION_SYMBOL].total + groups[PAGINATION_SYMBOL].total
|
||||
);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, [
|
||||
collection.permission,
|
||||
collection.id,
|
||||
collectionGroupMemberships,
|
||||
memberships,
|
||||
]);
|
||||
|
||||
if (isLoading || collection.permission) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const overflow = totalMemberships - collectionUsers.length;
|
||||
|
||||
return (
|
||||
<NudeButton
|
||||
context={context}
|
||||
action={editCollectionPermissions}
|
||||
tooltip={{
|
||||
tooltip: t("Users and groups with access"),
|
||||
delay: 250,
|
||||
}}
|
||||
width="auto"
|
||||
height="auto"
|
||||
>
|
||||
<Fade>
|
||||
<Facepile users={collectionUsers} overflow={overflow} />
|
||||
</Fade>
|
||||
</NudeButton>
|
||||
);
|
||||
};
|
||||
|
||||
export default observer(MembershipPreview);
|
||||
@@ -1,10 +1,10 @@
|
||||
import invariant from "invariant";
|
||||
import { observer } from "mobx-react";
|
||||
import { PlusIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { useTranslation, Trans } from "react-i18next";
|
||||
import styled from "styled-components";
|
||||
import { CollectionPermission } from "@shared/types";
|
||||
import Collection from "~/models/Collection";
|
||||
import Group from "~/models/Group";
|
||||
import User from "~/models/User";
|
||||
import Button from "~/components/Button";
|
||||
@@ -26,13 +26,14 @@ import CollectionGroupMemberListItem from "./components/CollectionGroupMemberLis
|
||||
import MemberListItem from "./components/MemberListItem";
|
||||
|
||||
type Props = {
|
||||
collection: Collection;
|
||||
collectionId: string;
|
||||
};
|
||||
|
||||
function CollectionPermissions({ collection }: Props) {
|
||||
function CollectionPermissions({ collectionId }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const user = useCurrentUser();
|
||||
const {
|
||||
collections,
|
||||
memberships,
|
||||
collectionGroupMemberships,
|
||||
users,
|
||||
@@ -40,6 +41,8 @@ function CollectionPermissions({ collection }: Props) {
|
||||
auth,
|
||||
} = useStores();
|
||||
const { showToast } = useToasts();
|
||||
const collection = collections.get(collectionId);
|
||||
invariant(collection, "Collection not found");
|
||||
|
||||
const [
|
||||
addGroupModalOpen,
|
||||
|
||||
@@ -174,7 +174,7 @@ function DocumentHeader({
|
||||
<Header
|
||||
title={document.title}
|
||||
hasSidebar={!!sharedTree}
|
||||
breadcrumb={
|
||||
left={
|
||||
isMobile ? (
|
||||
<TableOfContentsMenu headings={headings} />
|
||||
) : (
|
||||
@@ -201,7 +201,7 @@ function DocumentHeader({
|
||||
<>
|
||||
<Header
|
||||
hasSidebar
|
||||
breadcrumb={
|
||||
left={
|
||||
isMobile ? (
|
||||
<TableOfContentsMenu headings={headings} />
|
||||
) : (
|
||||
|
||||
@@ -36,15 +36,13 @@ function Home() {
|
||||
<Scene
|
||||
icon={<HomeIcon color="currentColor" />}
|
||||
title={t("Home")}
|
||||
left={
|
||||
<InputSearchPage source="dashboard" label={t("Search documents")} />
|
||||
}
|
||||
actions={
|
||||
<>
|
||||
<Action>
|
||||
<InputSearchPage source="dashboard" label={t("Search documents")} />
|
||||
</Action>
|
||||
<Action>
|
||||
<NewDocumentMenu />
|
||||
</Action>
|
||||
</>
|
||||
<Action>
|
||||
<NewDocumentMenu />
|
||||
</Action>
|
||||
}
|
||||
>
|
||||
{!ui.languagePromptDismissed && <LanguagePrompt />}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { CollectionPermission } from "@shared/types";
|
||||
import CollectionGroupMembership from "~/models/CollectionGroupMembership";
|
||||
import { PaginationParams } from "~/types";
|
||||
import { client } from "~/utils/ApiClient";
|
||||
import BaseStore, { RPCAction } from "./BaseStore";
|
||||
import BaseStore, { PAGINATION_SYMBOL, RPCAction } from "./BaseStore";
|
||||
import RootStore from "./RootStore";
|
||||
|
||||
export default class CollectionGroupMembershipsStore extends BaseStore<
|
||||
@@ -26,13 +26,15 @@ export default class CollectionGroupMembershipsStore extends BaseStore<
|
||||
const res = await client.post(`/collections.group_memberships`, params);
|
||||
invariant(res?.data, "Data not available");
|
||||
|
||||
let models: CollectionGroupMembership[] = [];
|
||||
let response: CollectionGroupMembership[] = [];
|
||||
runInAction(`CollectionGroupMembershipsStore#fetchPage`, () => {
|
||||
res.data.groups.forEach(this.rootStore.groups.add);
|
||||
models = res.data.collectionGroupMemberships.map(this.add);
|
||||
response = res.data.collectionGroupMemberships.map(this.add);
|
||||
this.isLoaded = true;
|
||||
});
|
||||
return models;
|
||||
|
||||
response[PAGINATION_SYMBOL] = res.pagination;
|
||||
return response;
|
||||
} finally {
|
||||
this.isFetching = false;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { CollectionPermission } from "@shared/types";
|
||||
import Membership from "~/models/Membership";
|
||||
import { PaginationParams } from "~/types";
|
||||
import { client } from "~/utils/ApiClient";
|
||||
import BaseStore, { RPCAction } from "./BaseStore";
|
||||
import BaseStore, { PAGINATION_SYMBOL, RPCAction } from "./BaseStore";
|
||||
import RootStore from "./RootStore";
|
||||
|
||||
export default class MembershipsStore extends BaseStore<Membership> {
|
||||
@@ -24,13 +24,14 @@ export default class MembershipsStore extends BaseStore<Membership> {
|
||||
const res = await client.post(`/collections.memberships`, params);
|
||||
invariant(res?.data, "Data not available");
|
||||
|
||||
let models: Membership[] = [];
|
||||
let response: Membership[] = [];
|
||||
runInAction(`MembershipsStore#fetchPage`, () => {
|
||||
res.data.users.forEach(this.rootStore.users.add);
|
||||
models = res.data.memberships.map(this.add);
|
||||
response = res.data.memberships.map(this.add);
|
||||
this.isLoaded = true;
|
||||
});
|
||||
return models;
|
||||
response[PAGINATION_SYMBOL] = res.pagination;
|
||||
return response;
|
||||
} finally {
|
||||
this.isFetching = false;
|
||||
}
|
||||
|
||||
@@ -316,22 +316,26 @@ router.post(
|
||||
where = { ...where, permission };
|
||||
}
|
||||
|
||||
const memberships = await CollectionGroup.findAll({
|
||||
where,
|
||||
order: [["createdAt", "DESC"]],
|
||||
offset: ctx.state.pagination.offset,
|
||||
limit: ctx.state.pagination.limit,
|
||||
include: [
|
||||
{
|
||||
model: Group,
|
||||
as: "group",
|
||||
where: groupWhere,
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
const [total, memberships] = await Promise.all([
|
||||
CollectionGroup.count({ where }),
|
||||
CollectionGroup.findAll({
|
||||
where,
|
||||
order: [["createdAt", "DESC"]],
|
||||
offset: ctx.state.pagination.offset,
|
||||
limit: ctx.state.pagination.limit,
|
||||
include: [
|
||||
{
|
||||
model: Group,
|
||||
as: "group",
|
||||
where: groupWhere,
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
}),
|
||||
]);
|
||||
|
||||
ctx.body = {
|
||||
pagination: ctx.state.pagination,
|
||||
pagination: { ...ctx.state.pagination, total },
|
||||
data: {
|
||||
collectionGroupMemberships: memberships.map(
|
||||
presentCollectionGroupMembership
|
||||
@@ -457,23 +461,26 @@ router.post("collections.memberships", auth(), pagination(), async (ctx) => {
|
||||
where = { ...where, permission };
|
||||
}
|
||||
|
||||
const memberships = await CollectionUser.findAll({
|
||||
where,
|
||||
order: [["createdAt", "DESC"]],
|
||||
offset: ctx.state.pagination.offset,
|
||||
limit: ctx.state.pagination.limit,
|
||||
include: [
|
||||
{
|
||||
model: User,
|
||||
as: "user",
|
||||
where: userWhere,
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
const [total, memberships] = await Promise.all([
|
||||
CollectionUser.count({ where }),
|
||||
CollectionUser.findAll({
|
||||
where,
|
||||
order: [["createdAt", "DESC"]],
|
||||
offset: ctx.state.pagination.offset,
|
||||
limit: ctx.state.pagination.limit,
|
||||
include: [
|
||||
{
|
||||
model: User,
|
||||
as: "user",
|
||||
where: userWhere,
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
}),
|
||||
]);
|
||||
|
||||
ctx.body = {
|
||||
pagination: ctx.state.pagination,
|
||||
pagination: { ...ctx.state.pagination, total },
|
||||
data: {
|
||||
memberships: memberships.map(presentMembership),
|
||||
users: memberships.map((membership) => presentUser(membership.user)),
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
"Open collection": "Open collection",
|
||||
"New collection": "New collection",
|
||||
"Create a collection": "Create a collection",
|
||||
"Edit": "Edit",
|
||||
"Edit collection": "Edit collection",
|
||||
"Permissions": "Permissions",
|
||||
"Collection permissions": "Collection permissions",
|
||||
"Star": "Star",
|
||||
"Unstar": "Unstar",
|
||||
"Delete IndexedDB cache": "Delete IndexedDB cache",
|
||||
@@ -284,14 +287,11 @@
|
||||
"Path to document": "Path to document",
|
||||
"Group member options": "Group member options",
|
||||
"Remove": "Remove",
|
||||
"Collection permissions": "Collection permissions",
|
||||
"Export collection": "Export collection",
|
||||
"Delete collection": "Are you sure you want to delete this collection?",
|
||||
"Sort in sidebar": "Sort in sidebar",
|
||||
"Alphabetical sort": "Alphabetical sort",
|
||||
"Manual sort": "Manual sort",
|
||||
"Edit": "Edit",
|
||||
"Permissions": "Permissions",
|
||||
"Document unpublished": "Document unpublished",
|
||||
"Document options": "Document options",
|
||||
"Restore": "Restore",
|
||||
@@ -334,19 +334,20 @@
|
||||
"API token created": "API token created",
|
||||
"Name your token something that will help you to remember it's use in the future, for example \"local development\", \"production\", or \"continuous integration\".": "Name your token something that will help you to remember it's use in the future, for example \"local development\", \"production\", or \"continuous integration\".",
|
||||
"The document archive is empty at the moment.": "The document archive is empty at the moment.",
|
||||
"Search in collection": "Search in collection",
|
||||
"This collection is only visible to those given access": "This collection is only visible to those given access",
|
||||
"Private": "Private",
|
||||
"Recently updated": "Recently updated",
|
||||
"Recently published": "Recently published",
|
||||
"Least recently updated": "Least recently updated",
|
||||
"A–Z": "A–Z",
|
||||
"Search in collection": "Search in collection",
|
||||
"Collection menu": "Collection menu",
|
||||
"Drop documents to import": "Drop documents to import",
|
||||
"<em>{{ collectionName }}</em> doesn’t contain any\n documents yet.": "<em>{{ collectionName }}</em> doesn’t contain any\n documents yet.",
|
||||
"Get started by creating a new one!": "Get started by creating a new one!",
|
||||
"Create a document": "Create a document",
|
||||
"Manage permissions": "Manage permissions",
|
||||
"Users and groups with access": "Users and groups with access",
|
||||
"The collection was updated": "The collection was updated",
|
||||
"You can edit the name and other details at any time, however doing so often might confuse your team mates.": "You can edit the name and other details at any time, however doing so often might confuse your team mates.",
|
||||
"Name": "Name",
|
||||
|
||||
Reference in New Issue
Block a user