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:
Tom Moor
2022-09-11 14:54:57 +02:00
committed by GitHub
parent 3aa7f34a73
commit 0fd576cdd5
19 changed files with 267 additions and 165 deletions

View File

@@ -1,6 +1,7 @@
import { import {
CollectionIcon, CollectionIcon,
EditIcon, EditIcon,
PadlockIcon,
PlusIcon, PlusIcon,
StarredIcon, StarredIcon,
UnstarredIcon, UnstarredIcon,
@@ -10,6 +11,7 @@ import stores from "~/stores";
import Collection from "~/models/Collection"; import Collection from "~/models/Collection";
import CollectionEdit from "~/scenes/CollectionEdit"; import CollectionEdit from "~/scenes/CollectionEdit";
import CollectionNew from "~/scenes/CollectionNew"; import CollectionNew from "~/scenes/CollectionNew";
import CollectionPermissions from "~/scenes/CollectionPermissions";
import DynamicCollectionIcon from "~/components/CollectionIcon"; import DynamicCollectionIcon from "~/components/CollectionIcon";
import { createAction } from "~/actions"; import { createAction } from "~/actions";
import { CollectionSection } from "~/actions/sections"; import { CollectionSection } from "~/actions/sections";
@@ -56,7 +58,8 @@ export const createCollection = createAction({
}); });
export const editCollection = createAction({ export const editCollection = createAction({
name: ({ t }) => t("Edit collection"), name: ({ t, isContextMenu }) =>
isContextMenu ? `${t("Edit")}` : t("Edit collection"),
section: CollectionSection, section: CollectionSection,
icon: <EditIcon />, icon: <EditIcon />,
visible: ({ stores, activeCollectionId }) => 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({ export const starCollection = createAction({
name: ({ t }) => t("Star"), name: ({ t }) => t("Star"),
section: CollectionSection, section: CollectionSection,

View File

@@ -9,7 +9,7 @@ type Props = {
users: User[]; users: User[];
size?: number; size?: number;
overflow?: number; overflow?: number;
onClick?: React.MouseEventHandler<HTMLDivElement>; limit?: number;
renderAvatar?: (user: User) => React.ReactNode; renderAvatar?: (user: User) => React.ReactNode;
}; };
@@ -17,6 +17,7 @@ function Facepile({
users, users,
overflow = 0, overflow = 0,
size = 32, size = 32,
limit = 8,
renderAvatar = DefaultAvatar, renderAvatar = DefaultAvatar,
...rest ...rest
}: Props) { }: Props) {
@@ -24,10 +25,13 @@ function Facepile({
<Avatars {...rest}> <Avatars {...rest}>
{overflow > 0 && ( {overflow > 0 && (
<More size={size}> <More size={size}>
<span>+{overflow}</span> <span>
{users.length ? "+" : ""}
{overflow}
</span>
</More> </More>
)} )}
{users.map((user) => ( {users.slice(0, limit).map((user) => (
<AvatarWrapper key={user.id}>{renderAvatar(user)}</AvatarWrapper> <AvatarWrapper key={user.id}>{renderAvatar(user)}</AvatarWrapper>
))} ))}
</Avatars> </Avatars>

View File

@@ -13,6 +13,7 @@ import Flex from "~/components/Flex";
import ListItem from "~/components/List/Item"; import ListItem from "~/components/List/Item";
import Modal from "~/components/Modal"; import Modal from "~/components/Modal";
import withStores from "~/components/withStores"; import withStores from "~/components/withStores";
import NudeButton from "./NudeButton";
type Props = RootStore & { type Props = RootStore & {
group: Group; group: Group;
@@ -63,11 +64,13 @@ class GroupListItem extends React.Component<Props> {
actions={ actions={
<Flex align="center" gap={8}> <Flex align="center" gap={8}>
{showFacepile && ( {showFacepile && (
<Facepile <NudeButton
width="auto"
height="auto"
onClick={this.handleMembersModalOpen} onClick={this.handleMembersModalOpen}
users={users} >
overflow={overflow} <Facepile users={users} overflow={overflow} />
/> </NudeButton>
)} )}
{renderActions({ {renderActions({
openMembersModal: this.handleMembersModalOpen, openMembersModal: this.handleMembersModalOpen,

View File

@@ -15,19 +15,19 @@ import useStores from "~/hooks/useStores";
import { supportsPassiveListener } from "~/utils/browser"; import { supportsPassiveListener } from "~/utils/browser";
type Props = { type Props = {
breadcrumb?: React.ReactNode; left?: React.ReactNode;
title: React.ReactNode; title: React.ReactNode;
actions?: React.ReactNode; actions?: React.ReactNode;
hasSidebar?: boolean; hasSidebar?: boolean;
}; };
function Header({ breadcrumb, title, actions, hasSidebar }: Props) { function Header({ left, title, actions, hasSidebar }: Props) {
const { ui } = useStores(); const { ui } = useStores();
const isMobile = useMobile(); const isMobile = useMobile();
const hasMobileSidebar = hasSidebar && isMobile; const hasMobileSidebar = hasSidebar && isMobile;
const passThrough = !actions && !breadcrumb && !title; const passThrough = !actions && !left && !title;
const [isScrolled, setScrolled] = React.useState(false); const [isScrolled, setScrolled] = React.useState(false);
const handleScroll = React.useMemo( const handleScroll = React.useMemo(
@@ -51,7 +51,7 @@ function Header({ breadcrumb, title, actions, hasSidebar }: Props) {
return ( return (
<Wrapper align="center" shrink={false} $passThrough={passThrough}> <Wrapper align="center" shrink={false} $passThrough={passThrough}>
{breadcrumb || hasMobileSidebar ? ( {left || hasMobileSidebar ? (
<Breadcrumbs> <Breadcrumbs>
{hasMobileSidebar && ( {hasMobileSidebar && (
<MobileMenuButton <MobileMenuButton
@@ -61,7 +61,7 @@ function Header({ breadcrumb, title, actions, hasSidebar }: Props) {
neutral neutral
/> />
)} )}
{breadcrumb} {left}
</Breadcrumbs> </Breadcrumbs>
) : null} ) : null}

View File

@@ -173,7 +173,7 @@ class Input extends React.Component<Props> {
{type === "textarea" ? ( {type === "textarea" ? (
<RealTextarea <RealTextarea
ref={this.props.innerRef} ref={this.props.innerRef}
onBlur={this.props.onBlur} onBlur={this.handleBlur}
onFocus={this.handleFocus} onFocus={this.handleFocus}
hasIcon={!!icon} hasIcon={!!icon}
{...rest} {...rest}
@@ -181,7 +181,7 @@ class Input extends React.Component<Props> {
) : ( ) : (
<RealInput <RealInput
ref={this.props.innerRef} ref={this.props.innerRef}
onBlur={this.props.onBlur} onBlur={this.handleBlur}
onFocus={this.handleFocus} onFocus={this.handleFocus}
hasIcon={!!icon} hasIcon={!!icon}
type={type} type={type}

View File

@@ -4,8 +4,8 @@ import ActionButton, {
} from "~/components/ActionButton"; } from "~/components/ActionButton";
type Props = ActionButtonProps & { type Props = ActionButtonProps & {
width?: number; width?: number | string;
height?: number; height?: number | string;
size?: number; size?: number;
type?: "button" | "submit" | "reset"; type?: "button" | "submit" | "reset";
}; };
@@ -13,8 +13,14 @@ type Props = ActionButtonProps & {
const NudeButton = styled(ActionButton).attrs((props: Props) => ({ const NudeButton = styled(ActionButton).attrs((props: Props) => ({
type: "type" in props ? props.type : "button", type: "type" in props ? props.type : "button",
}))<Props>` }))<Props>`
width: ${(props) => props.width || props.size || 24}px; width: ${(props) =>
height: ${(props) => props.height || props.size || 24}px; 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; background: none;
border-radius: 4px; border-radius: 4px;
display: inline-block; display: inline-block;

View File

@@ -8,7 +8,7 @@ type Props = {
icon?: React.ReactNode; icon?: React.ReactNode;
title?: React.ReactNode; title?: React.ReactNode;
textTitle?: string; textTitle?: string;
breadcrumb?: React.ReactNode; left?: React.ReactNode;
actions?: React.ReactNode; actions?: React.ReactNode;
centered?: boolean; centered?: boolean;
}; };
@@ -18,7 +18,7 @@ const Scene: React.FC<Props> = ({
icon, icon,
textTitle, textTitle,
actions, actions,
breadcrumb, left,
children, children,
centered, centered,
}) => { }) => {
@@ -37,7 +37,7 @@ const Scene: React.FC<Props> = ({
) )
} }
actions={actions} actions={actions}
breadcrumb={breadcrumb} left={left}
/> />
{centered !== false ? ( {centered !== false ? (
<CenteredContent withStickyHeader>{children}</CenteredContent> <CenteredContent withStickyHeader>{children}</CenteredContent>

View File

@@ -1,11 +1,9 @@
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { import {
NewDocumentIcon, NewDocumentIcon,
EditIcon,
TrashIcon, TrashIcon,
ImportIcon, ImportIcon,
ExportIcon, ExportIcon,
PadlockIcon,
AlphabeticalSortIcon, AlphabeticalSortIcon,
ManualSortIcon, ManualSortIcon,
UnstarredIcon, UnstarredIcon,
@@ -18,13 +16,17 @@ import { useMenuState, MenuButton, MenuButtonHTMLProps } from "reakit/Menu";
import { VisuallyHidden } from "reakit/VisuallyHidden"; import { VisuallyHidden } from "reakit/VisuallyHidden";
import { getEventFiles } from "@shared/utils/files"; import { getEventFiles } from "@shared/utils/files";
import Collection from "~/models/Collection"; import Collection from "~/models/Collection";
import CollectionEdit from "~/scenes/CollectionEdit";
import CollectionExport from "~/scenes/CollectionExport"; import CollectionExport from "~/scenes/CollectionExport";
import CollectionPermissions from "~/scenes/CollectionPermissions";
import CollectionDeleteDialog from "~/components/CollectionDeleteDialog"; import CollectionDeleteDialog from "~/components/CollectionDeleteDialog";
import ContextMenu, { Placement } from "~/components/ContextMenu"; import ContextMenu, { Placement } from "~/components/ContextMenu";
import OverflowMenuButton from "~/components/ContextMenu/OverflowMenuButton"; import OverflowMenuButton from "~/components/ContextMenu/OverflowMenuButton";
import Template from "~/components/ContextMenu/Template"; 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 useCurrentTeam from "~/hooks/useCurrentTeam";
import usePolicy from "~/hooks/usePolicy"; import usePolicy from "~/hooks/usePolicy";
import useStores from "~/hooks/useStores"; import useStores from "~/hooks/useStores";
@@ -60,25 +62,6 @@ function CollectionMenu({
const history = useHistory(); const history = useHistory();
const file = React.useRef<HTMLInputElement>(null); 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(() => { const handleExport = React.useCallback(() => {
dialogs.openModal({ dialogs.openModal({
title: t("Export collection"), title: t("Export collection"),
@@ -186,6 +169,11 @@ function CollectionMenu({
[collection] [collection]
); );
const context = useActionContext({
isContextMenu: true,
activeCollectionId: collection.id,
});
const alphabeticalSort = collection.sort.field === "title"; const alphabeticalSort = collection.sort.field === "title";
const can = usePolicy(collection); const can = usePolicy(collection);
const canUserInTeam = usePolicy(team); const canUserInTeam = usePolicy(team);
@@ -225,6 +213,8 @@ function CollectionMenu({
{ {
type: "separator", type: "separator",
}, },
actionToMenuItem(editCollection, context),
actionToMenuItem(editCollectionPermissions, context),
{ {
type: "submenu", type: "submenu",
title: t("Sort in sidebar"), 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", type: "button",
title: `${t("Export")}`, title: `${t("Export")}`,
@@ -293,9 +269,8 @@ function CollectionMenu({
handleStar, handleStar,
handleNewDocument, handleNewDocument,
handleImportDocument, handleImportDocument,
context,
alphabeticalSort, alphabeticalSort,
handleEdit,
handlePermissions,
canUserInTeam.createExport, canUserInTeam.createExport,
handleExport, handleExport,
handleDelete, handleDelete,

View File

@@ -18,6 +18,7 @@ import CenteredContent from "~/components/CenteredContent";
import CollectionDescription from "~/components/CollectionDescription"; import CollectionDescription from "~/components/CollectionDescription";
import CollectionIcon from "~/components/CollectionIcon"; import CollectionIcon from "~/components/CollectionIcon";
import Heading from "~/components/Heading"; import Heading from "~/components/Heading";
import InputSearchPage from "~/components/InputSearchPage";
import PlaceholderList from "~/components/List/Placeholder"; import PlaceholderList from "~/components/List/Placeholder";
import PaginatedDocumentList from "~/components/PaginatedDocumentList"; import PaginatedDocumentList from "~/components/PaginatedDocumentList";
import PinnedDocuments from "~/components/PinnedDocuments"; import PinnedDocuments from "~/components/PinnedDocuments";
@@ -35,6 +36,7 @@ import { collectionUrl, updateCollectionUrl } from "~/utils/routeHelpers";
import Actions from "./Collection/Actions"; import Actions from "./Collection/Actions";
import DropToImport from "./Collection/DropToImport"; import DropToImport from "./Collection/DropToImport";
import Empty from "./Collection/Empty"; import Empty from "./Collection/Empty";
import MembershipPreview from "./Collection/MembershipPreview";
function CollectionScene() { function CollectionScene() {
const params = useParams<{ id?: string }>(); const params = useParams<{ id?: string }>();
@@ -112,13 +114,28 @@ function CollectionScene() {
key={collection.id} key={collection.id}
centered={false} centered={false}
textTitle={collection.name} textTitle={collection.name}
left={
collection.isEmpty ? undefined : (
<InputSearchPage
source="collection"
placeholder={`${t("Search in collection")}`}
label={t("Search in collection")}
collectionId={collection.id}
/>
)
}
title={ title={
<> <>
<CollectionIcon collection={collection} expanded /> <CollectionIcon collection={collection} expanded />
&nbsp;{collection.name} &nbsp;{collection.name}
</> </>
} }
actions={<Actions collection={collection} />} actions={
<>
<MembershipPreview collection={collection} />
<Actions collection={collection} />
</>
}
> >
<DropToImport <DropToImport
accept={documents.importFileTypes.join(", ")} accept={documents.importFileTypes.join(", ")}

View File

@@ -6,7 +6,6 @@ import { Link } from "react-router-dom";
import Collection from "~/models/Collection"; import Collection from "~/models/Collection";
import { Action, Separator } from "~/components/Actions"; import { Action, Separator } from "~/components/Actions";
import Button from "~/components/Button"; import Button from "~/components/Button";
import InputSearchPage from "~/components/InputSearchPage";
import Tooltip from "~/components/Tooltip"; import Tooltip from "~/components/Tooltip";
import usePolicy from "~/hooks/usePolicy"; import usePolicy from "~/hooks/usePolicy";
import CollectionMenu from "~/menus/CollectionMenu"; import CollectionMenu from "~/menus/CollectionMenu";
@@ -22,38 +21,26 @@ function Actions({ collection }: Props) {
return ( return (
<> <>
{!collection.isEmpty && ( {can.update && (
<> <>
<Action> <Action>
<InputSearchPage <Tooltip
source="collection" tooltip={t("New document")}
placeholder={`${t("Search in collection")}`} shortcut="n"
label={`${t("Search in collection")}`} delay={500}
collectionId={collection.id} placement="bottom"
/> >
<Button
as={Link}
to={collection ? newDocumentPath(collection.id) : ""}
disabled={!collection}
icon={<PlusIcon />}
>
{t("New doc")}
</Button>
</Tooltip>
</Action> </Action>
{can.update && ( <Separator />
<>
<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 />
</>
)}
</> </>
)} )}
<Action> <Action>

View File

@@ -5,12 +5,11 @@ import { Trans, useTranslation } from "react-i18next";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import styled from "styled-components"; import styled from "styled-components";
import Collection from "~/models/Collection"; import Collection from "~/models/Collection";
import CollectionPermissions from "~/scenes/CollectionPermissions";
import Button from "~/components/Button"; import Button from "~/components/Button";
import Flex from "~/components/Flex"; import Flex from "~/components/Flex";
import Modal from "~/components/Modal";
import Text from "~/components/Text"; 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 usePolicy from "~/hooks/usePolicy";
import { newDocumentPath } from "~/utils/routeHelpers"; import { newDocumentPath } from "~/utils/routeHelpers";
@@ -21,13 +20,10 @@ type Props = {
function EmptyCollection({ collection }: Props) { function EmptyCollection({ collection }: Props) {
const { t } = useTranslation(); const { t } = useTranslation();
const can = usePolicy(collection); const can = usePolicy(collection);
const context = useActionContext();
const collectionName = collection ? collection.name : ""; const collectionName = collection ? collection.name : "";
const [ console.log({ context });
permissionsModalOpen,
handlePermissionsModalOpen,
handlePermissionsModalClose,
] = useBoolean();
return ( return (
<Centered column> <Centered column>
@@ -48,23 +44,20 @@ function EmptyCollection({ collection }: Props) {
{can.update && ( {can.update && (
<Empty> <Empty>
<Link to={newDocumentPath(collection.id)}> <Link to={newDocumentPath(collection.id)}>
<Button icon={<NewDocumentIcon color="currentColor" />}> <Button icon={<NewDocumentIcon color="currentColor" />} neutral>
{t("Create a document")} {t("Create a document")}
</Button> </Button>
</Link> </Link>
&nbsp;&nbsp; <Button
<Button onClick={handlePermissionsModalOpen} neutral> action={editCollectionPermissions}
context={context}
hideOnActionDisabled
neutral
>
{t("Manage permissions")} {t("Manage permissions")}
</Button> </Button>
</Empty> </Empty>
)} )}
<Modal
title={t("Collection permissions")}
onRequestClose={handlePermissionsModalClose}
isOpen={permissionsModalOpen}
>
<CollectionPermissions collection={collection} />
</Modal>
</Centered> </Centered>
); );
} }
@@ -79,6 +72,7 @@ const Centered = styled(Flex)`
const Empty = styled(Flex)` const Empty = styled(Flex)`
justify-content: center; justify-content: center;
margin: 10px 0; margin: 10px 0;
gap: 8px;
`; `;
export default observer(EmptyCollection); export default observer(EmptyCollection);

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

View File

@@ -1,10 +1,10 @@
import invariant from "invariant";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { PlusIcon } from "outline-icons"; import { PlusIcon } from "outline-icons";
import * as React from "react"; import * as React from "react";
import { useTranslation, Trans } from "react-i18next"; import { useTranslation, Trans } from "react-i18next";
import styled from "styled-components"; import styled from "styled-components";
import { CollectionPermission } from "@shared/types"; import { CollectionPermission } from "@shared/types";
import Collection from "~/models/Collection";
import Group from "~/models/Group"; import Group from "~/models/Group";
import User from "~/models/User"; import User from "~/models/User";
import Button from "~/components/Button"; import Button from "~/components/Button";
@@ -26,13 +26,14 @@ import CollectionGroupMemberListItem from "./components/CollectionGroupMemberLis
import MemberListItem from "./components/MemberListItem"; import MemberListItem from "./components/MemberListItem";
type Props = { type Props = {
collection: Collection; collectionId: string;
}; };
function CollectionPermissions({ collection }: Props) { function CollectionPermissions({ collectionId }: Props) {
const { t } = useTranslation(); const { t } = useTranslation();
const user = useCurrentUser(); const user = useCurrentUser();
const { const {
collections,
memberships, memberships,
collectionGroupMemberships, collectionGroupMemberships,
users, users,
@@ -40,6 +41,8 @@ function CollectionPermissions({ collection }: Props) {
auth, auth,
} = useStores(); } = useStores();
const { showToast } = useToasts(); const { showToast } = useToasts();
const collection = collections.get(collectionId);
invariant(collection, "Collection not found");
const [ const [
addGroupModalOpen, addGroupModalOpen,

View File

@@ -174,7 +174,7 @@ function DocumentHeader({
<Header <Header
title={document.title} title={document.title}
hasSidebar={!!sharedTree} hasSidebar={!!sharedTree}
breadcrumb={ left={
isMobile ? ( isMobile ? (
<TableOfContentsMenu headings={headings} /> <TableOfContentsMenu headings={headings} />
) : ( ) : (
@@ -201,7 +201,7 @@ function DocumentHeader({
<> <>
<Header <Header
hasSidebar hasSidebar
breadcrumb={ left={
isMobile ? ( isMobile ? (
<TableOfContentsMenu headings={headings} /> <TableOfContentsMenu headings={headings} />
) : ( ) : (

View File

@@ -36,15 +36,13 @@ function Home() {
<Scene <Scene
icon={<HomeIcon color="currentColor" />} icon={<HomeIcon color="currentColor" />}
title={t("Home")} title={t("Home")}
left={
<InputSearchPage source="dashboard" label={t("Search documents")} />
}
actions={ actions={
<> <Action>
<Action> <NewDocumentMenu />
<InputSearchPage source="dashboard" label={t("Search documents")} /> </Action>
</Action>
<Action>
<NewDocumentMenu />
</Action>
</>
} }
> >
{!ui.languagePromptDismissed && <LanguagePrompt />} {!ui.languagePromptDismissed && <LanguagePrompt />}

View File

@@ -4,7 +4,7 @@ import { CollectionPermission } from "@shared/types";
import CollectionGroupMembership from "~/models/CollectionGroupMembership"; import CollectionGroupMembership from "~/models/CollectionGroupMembership";
import { PaginationParams } from "~/types"; import { PaginationParams } from "~/types";
import { client } from "~/utils/ApiClient"; import { client } from "~/utils/ApiClient";
import BaseStore, { RPCAction } from "./BaseStore"; import BaseStore, { PAGINATION_SYMBOL, RPCAction } from "./BaseStore";
import RootStore from "./RootStore"; import RootStore from "./RootStore";
export default class CollectionGroupMembershipsStore extends BaseStore< 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); const res = await client.post(`/collections.group_memberships`, params);
invariant(res?.data, "Data not available"); invariant(res?.data, "Data not available");
let models: CollectionGroupMembership[] = []; let response: CollectionGroupMembership[] = [];
runInAction(`CollectionGroupMembershipsStore#fetchPage`, () => { runInAction(`CollectionGroupMembershipsStore#fetchPage`, () => {
res.data.groups.forEach(this.rootStore.groups.add); 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; this.isLoaded = true;
}); });
return models;
response[PAGINATION_SYMBOL] = res.pagination;
return response;
} finally { } finally {
this.isFetching = false; this.isFetching = false;
} }

View File

@@ -4,7 +4,7 @@ import { CollectionPermission } from "@shared/types";
import Membership from "~/models/Membership"; import Membership from "~/models/Membership";
import { PaginationParams } from "~/types"; import { PaginationParams } from "~/types";
import { client } from "~/utils/ApiClient"; import { client } from "~/utils/ApiClient";
import BaseStore, { RPCAction } from "./BaseStore"; import BaseStore, { PAGINATION_SYMBOL, RPCAction } from "./BaseStore";
import RootStore from "./RootStore"; import RootStore from "./RootStore";
export default class MembershipsStore extends BaseStore<Membership> { 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); const res = await client.post(`/collections.memberships`, params);
invariant(res?.data, "Data not available"); invariant(res?.data, "Data not available");
let models: Membership[] = []; let response: Membership[] = [];
runInAction(`MembershipsStore#fetchPage`, () => { runInAction(`MembershipsStore#fetchPage`, () => {
res.data.users.forEach(this.rootStore.users.add); 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; this.isLoaded = true;
}); });
return models; response[PAGINATION_SYMBOL] = res.pagination;
return response;
} finally { } finally {
this.isFetching = false; this.isFetching = false;
} }

View File

@@ -316,22 +316,26 @@ router.post(
where = { ...where, permission }; where = { ...where, permission };
} }
const memberships = await CollectionGroup.findAll({ const [total, memberships] = await Promise.all([
where, CollectionGroup.count({ where }),
order: [["createdAt", "DESC"]], CollectionGroup.findAll({
offset: ctx.state.pagination.offset, where,
limit: ctx.state.pagination.limit, order: [["createdAt", "DESC"]],
include: [ offset: ctx.state.pagination.offset,
{ limit: ctx.state.pagination.limit,
model: Group, include: [
as: "group", {
where: groupWhere, model: Group,
required: true, as: "group",
}, where: groupWhere,
], required: true,
}); },
],
}),
]);
ctx.body = { ctx.body = {
pagination: ctx.state.pagination, pagination: { ...ctx.state.pagination, total },
data: { data: {
collectionGroupMemberships: memberships.map( collectionGroupMemberships: memberships.map(
presentCollectionGroupMembership presentCollectionGroupMembership
@@ -457,23 +461,26 @@ router.post("collections.memberships", auth(), pagination(), async (ctx) => {
where = { ...where, permission }; where = { ...where, permission };
} }
const memberships = await CollectionUser.findAll({ const [total, memberships] = await Promise.all([
where, CollectionUser.count({ where }),
order: [["createdAt", "DESC"]], CollectionUser.findAll({
offset: ctx.state.pagination.offset, where,
limit: ctx.state.pagination.limit, order: [["createdAt", "DESC"]],
include: [ offset: ctx.state.pagination.offset,
{ limit: ctx.state.pagination.limit,
model: User, include: [
as: "user", {
where: userWhere, model: User,
required: true, as: "user",
}, where: userWhere,
], required: true,
}); },
],
}),
]);
ctx.body = { ctx.body = {
pagination: ctx.state.pagination, pagination: { ...ctx.state.pagination, total },
data: { data: {
memberships: memberships.map(presentMembership), memberships: memberships.map(presentMembership),
users: memberships.map((membership) => presentUser(membership.user)), users: memberships.map((membership) => presentUser(membership.user)),

View File

@@ -2,7 +2,10 @@
"Open collection": "Open collection", "Open collection": "Open collection",
"New collection": "New collection", "New collection": "New collection",
"Create a collection": "Create a collection", "Create a collection": "Create a collection",
"Edit": "Edit",
"Edit collection": "Edit collection", "Edit collection": "Edit collection",
"Permissions": "Permissions",
"Collection permissions": "Collection permissions",
"Star": "Star", "Star": "Star",
"Unstar": "Unstar", "Unstar": "Unstar",
"Delete IndexedDB cache": "Delete IndexedDB cache", "Delete IndexedDB cache": "Delete IndexedDB cache",
@@ -284,14 +287,11 @@
"Path to document": "Path to document", "Path to document": "Path to document",
"Group member options": "Group member options", "Group member options": "Group member options",
"Remove": "Remove", "Remove": "Remove",
"Collection permissions": "Collection permissions",
"Export collection": "Export collection", "Export collection": "Export collection",
"Delete collection": "Are you sure you want to delete this collection?", "Delete collection": "Are you sure you want to delete this collection?",
"Sort in sidebar": "Sort in sidebar", "Sort in sidebar": "Sort in sidebar",
"Alphabetical sort": "Alphabetical sort", "Alphabetical sort": "Alphabetical sort",
"Manual sort": "Manual sort", "Manual sort": "Manual sort",
"Edit": "Edit",
"Permissions": "Permissions",
"Document unpublished": "Document unpublished", "Document unpublished": "Document unpublished",
"Document options": "Document options", "Document options": "Document options",
"Restore": "Restore", "Restore": "Restore",
@@ -334,19 +334,20 @@
"API token created": "API token created", "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\".", "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.", "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", "This collection is only visible to those given access": "This collection is only visible to those given access",
"Private": "Private", "Private": "Private",
"Recently updated": "Recently updated", "Recently updated": "Recently updated",
"Recently published": "Recently published", "Recently published": "Recently published",
"Least recently updated": "Least recently updated", "Least recently updated": "Least recently updated",
"AZ": "AZ", "AZ": "AZ",
"Search in collection": "Search in collection",
"Collection menu": "Collection menu", "Collection menu": "Collection menu",
"Drop documents to import": "Drop documents to import", "Drop documents to import": "Drop documents to import",
"<em>{{ collectionName }}</em> doesnt contain any\n documents yet.": "<em>{{ collectionName }}</em> doesnt contain any\n documents yet.", "<em>{{ collectionName }}</em> doesnt contain any\n documents yet.": "<em>{{ collectionName }}</em> doesnt contain any\n documents yet.",
"Get started by creating a new one!": "Get started by creating a new one!", "Get started by creating a new one!": "Get started by creating a new one!",
"Create a document": "Create a document", "Create a document": "Create a document",
"Manage permissions": "Manage permissions", "Manage permissions": "Manage permissions",
"Users and groups with access": "Users and groups with access",
"The collection was updated": "The collection was updated", "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.", "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", "Name": "Name",