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 {
|
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,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 />
|
||||||
{collection.name}
|
{collection.name}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
actions={<Actions collection={collection} />}
|
actions={
|
||||||
|
<>
|
||||||
|
<MembershipPreview collection={collection} />
|
||||||
|
<Actions collection={collection} />
|
||||||
|
</>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<DropToImport
|
<DropToImport
|
||||||
accept={documents.importFileTypes.join(", ")}
|
accept={documents.importFileTypes.join(", ")}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
<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);
|
||||||
|
|||||||
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 { 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,
|
||||||
|
|||||||
@@ -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} />
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -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 />}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)),
|
||||||
|
|||||||
@@ -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",
|
||||||
"A–Z": "A–Z",
|
"A–Z": "A–Z",
|
||||||
"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> doesn’t contain any\n documents yet.": "<em>{{ collectionName }}</em> doesn’t contain any\n documents yet.",
|
"<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!",
|
"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",
|
||||||
|
|||||||
Reference in New Issue
Block a user