Refactor collection creation UI (#6485)

* Iteration, before functional component

* Use react-hook-form, shared form for new and edit

* Avoid negative margin on input prefix

* Centered now default for modals
This commit is contained in:
Tom Moor
2024-02-03 11:23:25 -08:00
committed by GitHub
parent abaa56c8f1
commit 0a54227d97
38 changed files with 705 additions and 744 deletions

View File

@@ -10,9 +10,9 @@ import {
import * as React from "react";
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 { CollectionEdit } from "~/components/Collection/CollectionEdit";
import { CollectionNew } from "~/components/Collection/CollectionNew";
import CollectionDeleteDialog from "~/components/CollectionDeleteDialog";
import DynamicCollectionIcon from "~/components/Icons/CollectionIcon";
import { createAction } from "~/actions";
@@ -103,6 +103,7 @@ export const editCollectionPermissions = createAction({
stores.dialogs.openModal({
title: t("Collection permissions"),
fullscreen: true,
content: <CollectionPermissions collectionId={activeCollectionId} />,
});
},
@@ -183,7 +184,6 @@ export const deleteCollection = createAction({
}
stores.dialogs.openModal({
isCentered: true,
title: t("Delete collection"),
content: (
<CollectionDeleteDialog

View File

@@ -224,7 +224,6 @@ export const publishDocument = createAction({
} else if (document) {
stores.dialogs.openModal({
title: t("Publish document"),
isCentered: true,
content: <DocumentPublish document={document} />,
});
}
@@ -345,7 +344,6 @@ export const shareDocument = createAction({
stores.dialogs.openModal({
title: t("Share this document"),
isCentered: true,
content: (
<SharePopover
document={document}
@@ -495,7 +493,6 @@ export const duplicateDocument = createAction({
stores.dialogs.openModal({
title: t("Copy document"),
isCentered: true,
content: (
<DuplicateDialog
document={document}
@@ -692,7 +689,6 @@ export const createTemplate = createAction({
stores.dialogs.openModal({
title: t("Create template"),
isCentered: true,
content: <DocumentTemplatizeDialog documentId={activeDocumentId} />,
});
},
@@ -751,7 +747,6 @@ export const moveDocument = createAction({
title: t("Move {{ documentType }}", {
documentType: document.noun,
}),
isCentered: true,
content: <DocumentMove document={document} />,
});
}
@@ -805,7 +800,6 @@ export const deleteDocument = createAction({
title: t("Delete {{ documentName }}", {
documentName: document.noun,
}),
isCentered: true,
content: (
<DocumentDelete
document={document}
@@ -840,7 +834,6 @@ export const permanentlyDeleteDocument = createAction({
title: t("Permanently delete {{ documentName }}", {
documentName: document.noun,
}),
isCentered: true,
content: (
<DocumentPermanentDelete
document={document}

View File

@@ -60,6 +60,7 @@ export const createTeam = createAction({
user &&
stores.dialogs.openModal({
title: t("Create a workspace"),
fullscreen: true,
content: <TeamNew user={user} />,
});
},

View File

@@ -17,6 +17,7 @@ export const inviteUser = createAction({
perform: ({ t }) => {
stores.dialogs.openModal({
title: t("Invite people"),
fullscreen: true,
content: <Invite onSubmit={stores.dialogs.closeAllModals} />,
});
},
@@ -38,7 +39,6 @@ export const deleteUserActionFactory = (userId: string) =>
stores.dialogs.openModal({
title: t("Delete user"),
isCentered: true,
content: (
<UserDeleteDialog
user={user}

View File

@@ -0,0 +1,32 @@
import { observer } from "mobx-react";
import * as React from "react";
import { toast } from "sonner";
import useStores from "~/hooks/useStores";
import { CollectionForm, FormData } from "./CollectionForm";
type Props = {
collectionId: string;
onSubmit: () => void;
};
export const CollectionEdit = observer(function CollectionEdit_({
collectionId,
onSubmit,
}: Props) {
const { collections } = useStores();
const collection = collections.get(collectionId);
const handleSubmit = React.useCallback(
async (data: FormData) => {
try {
await collection?.save(data);
onSubmit?.();
} catch (error) {
toast.error(error.message);
}
},
[collection, onSubmit]
);
return <CollectionForm collection={collection} handleSubmit={handleSubmit} />;
});

View File

@@ -0,0 +1,164 @@
import { observer } from "mobx-react";
import * as React from "react";
import { Controller, useForm } from "react-hook-form";
import { Trans, useTranslation } from "react-i18next";
import styled from "styled-components";
import { randomElement } from "@shared/random";
import { CollectionPermission } from "@shared/types";
import { colorPalette } from "@shared/utils/collections";
import { CollectionValidation } from "@shared/validations";
import Collection from "~/models/Collection";
import Button from "~/components/Button";
import Flex from "~/components/Flex";
import IconPicker from "~/components/IconPicker";
import { IconLibrary } from "~/components/Icons/IconLibrary";
import Input from "~/components/Input";
import InputSelectPermission from "~/components/InputSelectPermission";
import Switch from "~/components/Switch";
import Text from "~/components/Text";
import useBoolean from "~/hooks/useBoolean";
import useCurrentTeam from "~/hooks/useCurrentTeam";
export interface FormData {
name: string;
icon: string;
color: string;
sharing: boolean;
permission: CollectionPermission | undefined;
}
export const CollectionForm = observer(function CollectionForm_({
handleSubmit,
collection,
}: {
handleSubmit: (data: FormData) => void;
collection?: Collection;
}) {
const team = useCurrentTeam();
const { t } = useTranslation();
const [hasOpenedIconPicker, setHasOpenedIconPicker] = useBoolean(false);
const {
register,
handleSubmit: formHandleSubmit,
formState,
watch,
control,
setValue,
setFocus,
} = useForm<FormData>({
mode: "all",
defaultValues: {
name: collection?.name ?? "",
icon: collection?.icon,
sharing: collection?.sharing ?? true,
permission: collection?.permission,
color: collection?.color ?? randomElement(colorPalette),
},
});
const values = watch();
React.useEffect(() => {
// If the user hasn't picked an icon yet, go ahead and suggest one based on
// the name of the collection. It's the little things sometimes.
if (!hasOpenedIconPicker) {
setValue(
"icon",
IconLibrary.findIconByKeyword(values.name) ??
values.icon ??
"collection"
);
}
}, [values.name]);
const handleIconPickerChange = React.useCallback(
(color: string, icon: string) => {
if (icon !== values.icon) {
setFocus("name");
}
setValue("color", color);
setValue("icon", icon);
},
[setFocus, setValue, values.icon]
);
return (
<form onSubmit={formHandleSubmit(handleSubmit)}>
<Text as="p">
<Trans>
Collections are used to group documents and choose permissions
</Trans>
.
</Text>
<Flex gap={8}>
<Input
type="text"
placeholder={t("Name")}
{...register("name", {
required: true,
maxLength: CollectionValidation.maxNameLength,
})}
prefix={
<StyledIconPicker
onOpen={setHasOpenedIconPicker}
onChange={handleIconPickerChange}
initial={values.name[0]}
color={values.color}
icon={values.icon}
/>
}
autoFocus
flex
/>
</Flex>
<Controller
control={control}
name="permission"
render={({ field }) => (
<InputSelectPermission
ref={field.ref}
value={field.value}
onChange={(value: CollectionPermission) => {
field.onChange(value);
}}
note={t(
"The default access for workspace members, you can share with more users or groups later."
)}
/>
)}
/>
{team.sharing && (
<Switch
id="sharing"
label={t("Public document sharing")}
note={t(
"Allow documents within this collection to be shared publicly on the internet."
)}
{...register("sharing")}
/>
)}
<Flex justify="flex-end">
<Button
type="submit"
disabled={formState.isSubmitting || !formState.isValid}
>
{collection
? formState.isSubmitting
? `${t("Saving")}`
: t("Save")
: formState.isSubmitting
? `${t("Creating")}`
: t("Create")}
</Button>
</Flex>
</form>
);
});
const StyledIconPicker = styled(IconPicker)`
margin-left: 4px;
margin-right: 4px;
`;

View File

@@ -0,0 +1,32 @@
import { observer } from "mobx-react";
import * as React from "react";
import { toast } from "sonner";
import Collection from "~/models/Collection";
import useStores from "~/hooks/useStores";
import history from "~/utils/history";
import { CollectionForm, FormData } from "./CollectionForm";
type Props = {
onSubmit: () => void;
};
export const CollectionNew = observer(function CollectionNew_({
onSubmit,
}: Props) {
const { collections } = useStores();
const handleSubmit = React.useCallback(
async (data: FormData) => {
try {
const collection = new Collection(data, collections);
await collection.save();
onSubmit?.();
history.push(collection.path);
} catch (error) {
toast.error(error.message);
}
},
[collections, onSubmit]
);
return <CollectionForm handleSubmit={handleSubmit} />;
});

View File

@@ -22,7 +22,7 @@ function Dialogs() {
<Modal
key={id}
isOpen={modal.isOpen}
isCentered={modal.isCentered}
fullscreen={modal.fullscreen}
onRequestClose={() => dialogs.closeModal(id)}
title={modal.title}
>

View File

@@ -1,282 +1,23 @@
import {
BookmarkedIcon,
BicycleIcon,
AcademicCapIcon,
BeakerIcon,
BuildingBlocksIcon,
BrowserIcon,
CollectionIcon,
CoinsIcon,
CameraIcon,
CarrotIcon,
FlameIcon,
HashtagIcon,
GraphIcon,
InternetIcon,
LibraryIcon,
PlaneIcon,
RamenIcon,
CloudIcon,
CodeIcon,
EditIcon,
EmailIcon,
EyeIcon,
GlobeIcon,
InfoIcon,
ImageIcon,
LeafIcon,
LightBulbIcon,
MathIcon,
MoonIcon,
NotepadIcon,
PadlockIcon,
PaletteIcon,
PromoteIcon,
QuestionMarkIcon,
SportIcon,
SunIcon,
ShapesIcon,
TargetIcon,
TerminalIcon,
ToolsIcon,
VehicleIcon,
WarningIcon,
DatabaseIcon,
SmileyIcon,
LightningIcon,
ClockIcon,
DoneIcon,
FeedbackIcon,
ServerRackIcon,
ThumbsUpIcon,
} from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { useMenuState, MenuButton, MenuItem } from "reakit/Menu";
import { PopoverDisclosure, usePopoverState } from "reakit";
import { MenuItem } from "reakit/Menu";
import styled, { useTheme } from "styled-components";
import breakpoint from "styled-components-breakpoint";
import { s } from "@shared/styles";
import { colorPalette } from "@shared/utils/collections";
import ContextMenu from "~/components/ContextMenu";
import Flex from "~/components/Flex";
import { LabelText } from "~/components/Input";
import NudeButton from "~/components/NudeButton";
import Text from "~/components/Text";
import lazyWithRetry from "~/utils/lazyWithRetry";
import DelayedMount from "./DelayedMount";
import LetterIcon from "./Icons/LetterIcon";
import { IconLibrary } from "./Icons/IconLibrary";
import Popover from "./Popover";
const icons = IconLibrary.mapping;
const TwitterPicker = lazyWithRetry(
() => import("react-color/lib/components/twitter/Twitter")
);
export const icons = {
academicCap: {
component: AcademicCapIcon,
keywords: "learn teach lesson guide tutorial onboarding training",
},
bicycle: {
component: BicycleIcon,
keywords: "bicycle bike cycle",
},
beaker: {
component: BeakerIcon,
keywords: "lab research experiment test",
},
buildingBlocks: {
component: BuildingBlocksIcon,
keywords: "app blocks product prototype",
},
bookmark: {
component: BookmarkedIcon,
keywords: "bookmark",
},
browser: {
component: BrowserIcon,
keywords: "browser web app",
},
collection: {
component: CollectionIcon,
keywords: "collection",
},
coins: {
component: CoinsIcon,
keywords: "coins money finance sales income revenue cash",
},
camera: {
component: CameraIcon,
keywords: "photo picture",
},
carrot: {
component: CarrotIcon,
keywords: "food vegetable produce",
},
clock: {
component: ClockIcon,
keywords: "time",
},
cloud: {
component: CloudIcon,
keywords: "cloud service aws infrastructure",
},
code: {
component: CodeIcon,
keywords: "developer api code development engineering programming",
},
database: {
component: DatabaseIcon,
keywords: "server ops database",
},
done: {
component: DoneIcon,
keywords: "checkmark success complete finished",
},
email: {
component: EmailIcon,
keywords: "email at",
},
eye: {
component: EyeIcon,
keywords: "eye view",
},
feedback: {
component: FeedbackIcon,
keywords: "faq help support",
},
flame: {
component: FlameIcon,
keywords: "fire hot",
},
graph: {
component: GraphIcon,
keywords: "chart analytics data",
},
globe: {
component: GlobeIcon,
keywords: "world translate",
},
hashtag: {
component: HashtagIcon,
keywords: "social media tag",
},
info: {
component: InfoIcon,
keywords: "info information",
},
image: {
component: ImageIcon,
keywords: "image photo picture",
},
internet: {
component: InternetIcon,
keywords: "network global globe world",
},
leaf: {
component: LeafIcon,
keywords: "leaf plant outdoors nature ecosystem climate",
},
library: {
component: LibraryIcon,
keywords: "library collection archive",
},
lightbulb: {
component: LightBulbIcon,
keywords: "lightbulb idea",
},
lightning: {
component: LightningIcon,
keywords: "lightning fast zap",
},
letter: {
component: LetterIcon,
keywords: "letter",
},
math: {
component: MathIcon,
keywords: "math formula",
},
moon: {
component: MoonIcon,
keywords: "night moon dark",
},
notepad: {
component: NotepadIcon,
keywords: "journal notepad write notes",
},
padlock: {
component: PadlockIcon,
keywords: "padlock private security authentication authorization auth",
},
palette: {
component: PaletteIcon,
keywords: "design palette art brand",
},
pencil: {
component: EditIcon,
keywords: "copy writing post blog",
},
plane: {
component: PlaneIcon,
keywords: "airplane travel flight trip vacation",
},
promote: {
component: PromoteIcon,
keywords: "marketing promotion",
},
ramen: {
component: RamenIcon,
keywords: "soup food noodle bowl meal",
},
question: {
component: QuestionMarkIcon,
keywords: "question help support faq",
},
server: {
component: ServerRackIcon,
keywords: "ops infra server",
},
sun: {
component: SunIcon,
keywords: "day sun weather",
},
shapes: {
component: ShapesIcon,
keywords: "blocks toy",
},
sport: {
component: SportIcon,
keywords: "sport outdoor racket game",
},
smiley: {
component: SmileyIcon,
keywords: "emoji smiley happy",
},
target: {
component: TargetIcon,
keywords: "target goal sales",
},
terminal: {
component: TerminalIcon,
keywords: "terminal code",
},
thumbsup: {
component: ThumbsUpIcon,
keywords: "like social favorite upvote",
},
tools: {
component: ToolsIcon,
keywords: "tool settings",
},
vehicle: {
component: VehicleIcon,
keywords: "truck car travel transport",
},
warning: {
component: WarningIcon,
keywords: "warning alert error",
},
};
type Props = {
onOpen?: () => void;
onClose?: () => void;
@@ -284,6 +25,7 @@ type Props = {
initial: string;
icon: string;
color: string;
className?: string;
};
function IconPicker({
@@ -293,46 +35,70 @@ function IconPicker({
initial,
color,
onChange,
className,
}: Props) {
const { t } = useTranslation();
const theme = useTheme();
const menu = useMenuState({
modal: true,
const popover = usePopoverState({
gutter: 0,
placement: "bottom-end",
modal: true,
});
React.useEffect(() => {
if (popover.visible) {
onOpen?.();
} else {
onClose?.();
}
}, [onOpen, onClose, popover.visible]);
const styles = React.useMemo(
() => ({
default: {
body: {
padding: 0,
marginRight: -8,
},
hash: {
color: theme.text,
background: theme.inputBorder,
},
swatch: {
cursor: "var(--cursor-pointer)",
},
input: {
color: theme.text,
boxShadow: `inset 0 0 0 1px ${theme.inputBorder}`,
background: "transparent",
},
},
}),
[theme]
);
return (
<Wrapper>
<Label>
<LabelText>{t("Icon")}</LabelText>
</Label>
<MenuButton {...menu}>
<>
<PopoverDisclosure {...popover}>
{(props) => (
<Button aria-label={t("Show menu")} {...props}>
<NudeButton
aria-label={t("Show menu")}
className={className}
{...props}
>
<Icon
as={icons[icon || "collection"].component}
as={IconLibrary.getComponent(icon || "collection")}
color={color}
size={30}
>
{initial}
</Icon>
</Button>
</NudeButton>
)}
</MenuButton>
<ContextMenu
{...menu}
onOpen={onOpen}
onClose={onClose}
maxWidth={308}
aria-label={t("Choose icon")}
>
</PopoverDisclosure>
<Popover {...popover} width={388} aria-label={t("Choose icon")}>
<Icons>
{Object.keys(icons).map((name, index) => (
<MenuItem
key={name}
onClick={() => onChange(color, name)}
{...menu}
>
<MenuItem key={name} onClick={() => onChange(color, name)}>
{(props) => (
<IconButton
style={
@@ -342,7 +108,11 @@ function IconPicker({
}
{...props}
>
<Icon as={icons[name].component} color={color} size={30}>
<Icon
as={IconLibrary.getComponent(name)}
color={color}
size={30}
>
{initial}
</Icon>
</IconButton>
@@ -363,28 +133,12 @@ function IconPicker({
onChange={(color) => onChange(color.hex, icon)}
colors={colorPalette}
triangle="hide"
styles={{
default: {
body: {
padding: 0,
marginRight: -8,
},
hash: {
color: theme.text,
background: theme.inputBorder,
},
input: {
color: theme.text,
boxShadow: `inset 0 0 0 1px ${theme.inputBorder}`,
background: "transparent",
},
},
}}
styles={styles}
/>
</React.Suspense>
</Colors>
</ContextMenu>
</Wrapper>
</Popover>
</>
);
}
@@ -397,22 +151,8 @@ const Colors = styled(Flex)`
padding: 8px;
`;
const Label = styled.label`
display: block;
`;
const Icons = styled.div`
padding: 8px;
${breakpoint("tablet")`
width: 304px;
`};
`;
const Button = styled(NudeButton)`
border: 1px solid ${s("inputBorder")};
width: 32px;
height: 32px;
`;
const IconButton = styled(NudeButton)`
@@ -429,9 +169,4 @@ const ColorPicker = styled(TwitterPicker)`
width: 100% !important;
`;
const Wrapper = styled("div")`
display: inline-block;
position: relative;
`;
export default IconPicker;

View File

@@ -3,9 +3,9 @@ import { CollectionIcon } from "outline-icons";
import { getLuminance } from "polished";
import * as React from "react";
import Collection from "~/models/Collection";
import { icons } from "~/components/IconPicker";
import useStores from "~/hooks/useStores";
import Logger from "~/utils/Logger";
import { IconLibrary } from "./IconLibrary";
type Props = {
/** The collection to show an icon for */
@@ -38,7 +38,7 @@ function ResolvedCollectionIcon({
if (collection.icon && collection.icon !== "collection") {
try {
const Component = icons[collection.icon].component;
const Component = IconLibrary.getComponent(collection.icon);
return (
<Component color={color} size={size}>
{collection.initial}

View File

@@ -0,0 +1,300 @@
import { intersection } from "lodash";
import {
BookmarkedIcon,
BicycleIcon,
AcademicCapIcon,
BeakerIcon,
BuildingBlocksIcon,
BrowserIcon,
CollectionIcon,
CoinsIcon,
CameraIcon,
CarrotIcon,
FlameIcon,
HashtagIcon,
GraphIcon,
InternetIcon,
LibraryIcon,
PlaneIcon,
RamenIcon,
CloudIcon,
CodeIcon,
EditIcon,
EmailIcon,
EyeIcon,
GlobeIcon,
InfoIcon,
ImageIcon,
LeafIcon,
LightBulbIcon,
MathIcon,
MoonIcon,
NotepadIcon,
PadlockIcon,
PaletteIcon,
PromoteIcon,
QuestionMarkIcon,
SportIcon,
SunIcon,
ShapesIcon,
TargetIcon,
TerminalIcon,
ToolsIcon,
VehicleIcon,
WarningIcon,
DatabaseIcon,
SmileyIcon,
LightningIcon,
ClockIcon,
DoneIcon,
FeedbackIcon,
ServerRackIcon,
ThumbsUpIcon,
} from "outline-icons";
import LetterIcon from "./LetterIcon";
export class IconLibrary {
/**
* Get the component for a given icon name
*
* @param icon The name of the icon
* @returns The component for the icon
*/
public static getComponent(icon: string) {
return this.mapping[icon].component;
}
/**
* Find an icon by keyword. This is useful for searching for an icon based on a user's input.
*
* @param keyword The keyword to search for
* @returns The name of the icon that matches the keyword, or undefined if no match is found
*/
public static findIconByKeyword(keyword: string) {
const keys = Object.keys(this.mapping);
for (const key of keys) {
const icon = this.mapping[key];
const keywords = icon.keywords.split(" ");
const namewords = keyword.toLocaleLowerCase().split(" ");
const matches = intersection(namewords, keywords);
if (matches.length > 0) {
return key;
}
}
return undefined;
}
/**
* A map of all icons available to end users in the app. This does not include icons that are used
* internally only, which can be imported from `outline-icons` directly.
*/
public static mapping = {
academicCap: {
component: AcademicCapIcon,
keywords: "learn teach lesson guide tutorial onboarding training",
},
bicycle: {
component: BicycleIcon,
keywords: "bicycle bike cycle",
},
beaker: {
component: BeakerIcon,
keywords: "lab research experiment test",
},
buildingBlocks: {
component: BuildingBlocksIcon,
keywords: "app blocks product prototype",
},
bookmark: {
component: BookmarkedIcon,
keywords: "bookmark",
},
browser: {
component: BrowserIcon,
keywords: "browser web app",
},
collection: {
component: CollectionIcon,
keywords: "collection",
},
coins: {
component: CoinsIcon,
keywords: "coins money finance sales income revenue cash",
},
camera: {
component: CameraIcon,
keywords: "photo picture",
},
carrot: {
component: CarrotIcon,
keywords: "food vegetable produce",
},
clock: {
component: ClockIcon,
keywords: "time",
},
cloud: {
component: CloudIcon,
keywords: "cloud service aws infrastructure",
},
code: {
component: CodeIcon,
keywords: "developer api code development engineering programming",
},
database: {
component: DatabaseIcon,
keywords: "server ops database",
},
done: {
component: DoneIcon,
keywords: "checkmark success complete finished",
},
email: {
component: EmailIcon,
keywords: "email at",
},
eye: {
component: EyeIcon,
keywords: "eye view",
},
feedback: {
component: FeedbackIcon,
keywords: "faq help support",
},
flame: {
component: FlameIcon,
keywords: "fire hot",
},
graph: {
component: GraphIcon,
keywords: "chart analytics data",
},
globe: {
component: GlobeIcon,
keywords: "world translate",
},
hashtag: {
component: HashtagIcon,
keywords: "social media tag",
},
info: {
component: InfoIcon,
keywords: "info information",
},
image: {
component: ImageIcon,
keywords: "image photo picture",
},
internet: {
component: InternetIcon,
keywords: "network global globe world",
},
leaf: {
component: LeafIcon,
keywords: "leaf plant outdoors nature ecosystem climate",
},
library: {
component: LibraryIcon,
keywords: "library collection archive",
},
lightbulb: {
component: LightBulbIcon,
keywords: "lightbulb idea",
},
lightning: {
component: LightningIcon,
keywords: "lightning fast zap",
},
letter: {
component: LetterIcon,
keywords: "letter",
},
math: {
component: MathIcon,
keywords: "math formula",
},
moon: {
component: MoonIcon,
keywords: "night moon dark",
},
notepad: {
component: NotepadIcon,
keywords: "journal notepad write notes",
},
padlock: {
component: PadlockIcon,
keywords: "padlock private security authentication authorization auth",
},
palette: {
component: PaletteIcon,
keywords: "design palette art brand",
},
pencil: {
component: EditIcon,
keywords: "copy writing post blog",
},
plane: {
component: PlaneIcon,
keywords: "airplane travel flight trip vacation",
},
promote: {
component: PromoteIcon,
keywords: "marketing promotion",
},
ramen: {
component: RamenIcon,
keywords: "soup food noodle bowl meal",
},
question: {
component: QuestionMarkIcon,
keywords: "question help support faq",
},
server: {
component: ServerRackIcon,
keywords: "ops infra server",
},
sun: {
component: SunIcon,
keywords: "day sun weather",
},
shapes: {
component: ShapesIcon,
keywords: "blocks toy",
},
sport: {
component: SportIcon,
keywords: "sport outdoor racket game",
},
smiley: {
component: SmileyIcon,
keywords: "emoji smiley happy",
},
target: {
component: TargetIcon,
keywords: "target goal sales",
},
terminal: {
component: TerminalIcon,
keywords: "terminal code",
},
thumbsup: {
component: ThumbsUpIcon,
keywords: "like social favorite upvote",
},
tools: {
component: ToolsIcon,
keywords: "tool settings",
},
vehicle: {
component: VehicleIcon,
keywords: "truck car travel transport",
},
warning: {
component: WarningIcon,
keywords: "warning alert error",
},
};
}

View File

@@ -8,10 +8,14 @@ import Flex from "~/components/Flex";
import Text from "~/components/Text";
import { undraggableOnDesktop } from "~/styles";
export const NativeTextarea = styled.textarea<{ hasIcon?: boolean }>`
export const NativeTextarea = styled.textarea<{
hasIcon?: boolean;
hasPrefix?: boolean;
}>`
border: 0;
flex: 1;
padding: 8px 12px 8px ${(props) => (props.hasIcon ? "8px" : "12px")};
padding: 8px 12px 8px
${(props) => (props.hasPrefix ? 0 : props.hasIcon ? "8px" : "12px")};
outline: none;
background: none;
color: ${s("text")};
@@ -23,10 +27,14 @@ export const NativeTextarea = styled.textarea<{ hasIcon?: boolean }>`
}
`;
export const NativeInput = styled.input<{ hasIcon?: boolean }>`
export const NativeInput = styled.input<{
hasIcon?: boolean;
hasPrefix?: boolean;
}>`
border: 0;
flex: 1;
padding: 8px 12px 8px ${(props) => (props.hasIcon ? "8px" : "12px")};
padding: 8px 12px 8px
${(props) => (props.hasPrefix ? 0 : props.hasIcon ? "8px" : "12px")};
outline: none;
background: none;
color: ${s("text")};
@@ -224,6 +232,7 @@ function Input(
onFocus={handleFocus}
onKeyDown={handleKeyDown}
hasIcon={!!icon}
hasPrefix={!!prefix}
{...rest}
/>
) : (
@@ -236,6 +245,7 @@ function Input(
onFocus={handleFocus}
onKeyDown={handleKeyDown}
hasIcon={!!icon}
hasPrefix={!!prefix}
type={type}
{...rest}
/>

View File

@@ -48,6 +48,12 @@ export type Props = {
onChange?: (value: string | null) => void;
};
export interface InputSelectRef {
value: string | null;
focus: () => void;
blur: () => void;
}
interface InnerProps extends React.HTMLAttributes<HTMLDivElement> {
placement: Placement;
}
@@ -55,7 +61,7 @@ interface InnerProps extends React.HTMLAttributes<HTMLDivElement> {
const getOptionFromValue = (options: Option[], value: string | null) =>
options.find((option) => option.value === value);
const InputSelect = (props: Props) => {
const InputSelect = (props: Props, ref: React.RefObject<InputSelectRef>) => {
const {
value = null,
label,
@@ -122,6 +128,16 @@ const InputSelect = (props: Props) => {
{ capture: true }
);
React.useImperativeHandle(ref, () => ({
focus: () => {
buttonRef.current?.focus();
},
blur: () => {
buttonRef.current?.blur();
},
value: select.selectedValue,
}));
React.useEffect(() => {
previousValue.current = value;
select.setSelectedValue(value);
@@ -306,4 +322,4 @@ export const Positioner = styled(Position)`
}
`;
export default InputSelect;
export default React.forwardRef(InputSelect);

View File

@@ -3,33 +3,31 @@ import { useTranslation } from "react-i18next";
import { $Diff } from "utility-types";
import { CollectionPermission } from "@shared/types";
import { EmptySelectValue } from "~/types";
import InputSelect, { Props, Option } from "./InputSelect";
import InputSelect, { Props, Option, InputSelectRef } from "./InputSelect";
export default function InputSelectPermission(
function InputSelectPermission(
props: $Diff<
Props,
{
options: Array<Option>;
ariaLabel: string;
}
>
>,
ref: React.RefObject<InputSelectRef>
) {
const { value, onChange, ...rest } = props;
const { t } = useTranslation();
const handleChange = React.useCallback(
(value) => {
if (value === EmptySelectValue) {
value = null;
}
onChange?.(value);
onChange?.(value === EmptySelectValue ? null : value);
},
[onChange]
);
return (
<InputSelect
label={t("Default access")}
ref={ref}
label={t("Permission")}
options={[
{
label: t("Can edit"),
@@ -51,3 +49,5 @@ export default function InputSelectPermission(
/>
);
}
export default React.forwardRef(InputSelectPermission);

View File

@@ -23,7 +23,7 @@ let openModals = 0;
type Props = {
children?: React.ReactNode;
isOpen: boolean;
isCentered?: boolean;
fullscreen?: boolean;
title?: React.ReactNode;
onRequestClose: () => void;
};
@@ -31,7 +31,7 @@ type Props = {
const Modal: React.FC<Props> = ({
children,
isOpen,
isCentered,
fullscreen,
title = "Untitled",
onRequestClose,
}: Props) => {
@@ -68,35 +68,17 @@ const Modal: React.FC<Props> = ({
return (
<DialogBackdrop {...dialog}>
{(props) => (
<Backdrop $isCentered={isCentered} {...props}>
<Backdrop $fullscreen={fullscreen} {...props}>
<Dialog
{...dialog}
aria-label={typeof title === "string" ? title : undefined}
preventBodyScroll
hideOnEsc
hideOnClickOutside={!!isCentered}
hideOnClickOutside={!fullscreen}
hide={onRequestClose}
>
{(props) =>
isCentered && !isMobile ? (
<Small {...props}>
<Centered
onClick={(ev) => ev.stopPropagation()}
column
reverse
>
<SmallContent shadow>
<ErrorBoundary component="div">{children}</ErrorBoundary>
</SmallContent>
<Header>
{title && <Text size="large">{title}</Text>}
<NudeButton onClick={onRequestClose}>
<CloseIcon />
</NudeButton>
</Header>
</Centered>
</Small>
) : (
fullscreen || isMobile ? (
<Fullscreen
$nested={!!depth}
style={
@@ -126,6 +108,24 @@ const Modal: React.FC<Props> = ({
<Text>{t("Back")} </Text>
</Back>
</Fullscreen>
) : (
<Small {...props}>
<Centered
onClick={(ev) => ev.stopPropagation()}
column
reverse
>
<SmallContent shadow>
<ErrorBoundary component="div">{children}</ErrorBoundary>
</SmallContent>
<Header>
{title && <Text size="large">{title}</Text>}
<NudeButton onClick={onRequestClose}>
<CloseIcon />
</NudeButton>
</Header>
</Centered>
</Small>
)
}
</Dialog>
@@ -135,16 +135,16 @@ const Modal: React.FC<Props> = ({
);
};
const Backdrop = styled(Flex)<{ $isCentered?: boolean }>`
const Backdrop = styled(Flex)<{ $fullscreen?: boolean }>`
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: ${(props) =>
props.$isCentered
? props.theme.modalBackdrop
: transparentize(0.25, props.theme.background)} !important;
props.$fullscreen
? transparentize(0.25, props.theme.background)
: props.theme.modalBackdrop} !important;
z-index: ${depths.modalOverlay};
transition: opacity 50ms ease-in-out;
opacity: 0;

View File

@@ -62,7 +62,7 @@ const Popover: React.FC<Props> = ({
}
return (
<ReakitPopover {...rest} hideOnEsc={false} hideOnClickOutside>
<StyledPopover {...rest} hideOnEsc={false} hideOnClickOutside>
<Contents
$shrink={shrink}
$width={width}
@@ -71,7 +71,7 @@ const Popover: React.FC<Props> = ({
>
{children}
</Contents>
</ReakitPopover>
</StyledPopover>
);
};
@@ -83,6 +83,10 @@ type ContentsProps = {
$mobilePosition?: "top" | "bottom";
};
const StyledPopover = styled(ReakitPopover)`
z-index: ${depths.modal};
`;
const Contents = styled.div<ContentsProps>`
display: ${(props) => (props.$flex ? "flex" : "block")};
animation: ${fadeAndScaleIn} 200ms ease;

View File

@@ -211,7 +211,6 @@ const Wrapper = styled.div`
const DomainPrefix = styled(NativeInput)`
flex: 0 1 auto;
padding-right: 0 !important;
margin-right: -10px;
cursor: text;
color: ${s("placeholder")};
user-select: none;
@@ -223,7 +222,7 @@ const ShareLinkInput = styled(Input)`
flex: 1;
${NativeInput} {
padding: 4px 8px;
padding: 4px 8px 4px 0;
flex: 1;
}
`;

View File

@@ -50,7 +50,6 @@ function TrashLink() {
})}
onRequestClose={() => setDocument(undefined)}
isOpen
isCentered
>
<DocumentDelete
document={document}

View File

@@ -24,16 +24,19 @@ interface Props extends React.HTMLAttributes<HTMLInputElement> {
disabled?: boolean;
}
function Switch({
width = 32,
height = 18,
labelPosition = "left",
label,
disabled,
className,
note,
...props
}: Props) {
function Switch(
{
width = 32,
height = 18,
labelPosition = "left",
label,
disabled,
className,
note,
...props
}: Props,
ref: React.Ref<HTMLInputElement>
) {
const component = (
<Input
width={width}
@@ -41,6 +44,7 @@ function Switch({
className={label ? undefined : className}
>
<HiddenInput
ref={ref}
type="checkbox"
width={width}
height={height}
@@ -164,4 +168,4 @@ const HiddenInput = styled.input<{ width: number; height: number }>`
}
`;
export default Switch;
export default React.forwardRef(Switch);

View File

@@ -24,7 +24,6 @@ function ApiKeyMenu({ apiKey }: Props) {
const handleRevoke = React.useCallback(() => {
dialogs.openModal({
title: t("Revoke token"),
isCentered: true,
content: (
<ApiKeyRevokeDialog onSubmit={dialogs.closeAllModals} apiKey={apiKey} />
),

View File

@@ -63,7 +63,6 @@ function CollectionMenu({
const handleExport = React.useCallback(() => {
dialogs.openModal({
title: t("Export collection"),
isCentered: true,
content: (
<ExportDialog
collection={collection}

View File

@@ -38,7 +38,6 @@ function CommentMenu({ comment, onEdit, onDelete, className }: Props) {
const handleDelete = React.useCallback(() => {
dialogs.openModal({
title: t("Delete comment"),
isCentered: true,
content: <CommentDeleteDialog comment={comment} onSubmit={onDelete} />,
});
}, [dialogs, comment, onDelete, t]);

View File

@@ -39,7 +39,6 @@ function GroupMenu({ group, onMembers }: Props) {
title={t("Delete group")}
onRequestClose={() => setDeleteModalOpen(false)}
isOpen={deleteModalOpen}
isCentered
>
<GroupDelete group={group} onSubmit={() => setDeleteModalOpen(false)} />
</Modal>

View File

@@ -40,7 +40,6 @@ function UserMenu({ user }: Props) {
ev.preventDefault();
dialogs.openModal({
title: t("Change role to admin"),
isCentered: true,
content: (
<UserChangeToAdminDialog
user={user}
@@ -57,7 +56,6 @@ function UserMenu({ user }: Props) {
ev.preventDefault();
dialogs.openModal({
title: t("Change role to editor"),
isCentered: true,
content: (
<UserChangeToMemberDialog
user={user}
@@ -74,7 +72,6 @@ function UserMenu({ user }: Props) {
ev.preventDefault();
dialogs.openModal({
title: t("Change role to viewer"),
isCentered: true,
content: (
<UserChangeToViewerDialog
user={user}
@@ -91,7 +88,6 @@ function UserMenu({ user }: Props) {
ev.preventDefault();
dialogs.openModal({
title: t("Change name"),
isCentered: true,
content: (
<UserChangeNameDialog user={user} onSubmit={dialogs.closeAllModals} />
),
@@ -105,7 +101,6 @@ function UserMenu({ user }: Props) {
ev.preventDefault();
dialogs.openModal({
title: t("Suspend user"),
isCentered: true,
content: (
<UserSuspendDialog user={user} onSubmit={dialogs.closeAllModals} />
),

View File

@@ -1,129 +0,0 @@
import invariant from "invariant";
import { observer } from "mobx-react";
import { useState } from "react";
import * as React from "react";
import { Trans, useTranslation } from "react-i18next";
import { toast } from "sonner";
import { CollectionValidation } from "@shared/validations";
import Button from "~/components/Button";
import Flex from "~/components/Flex";
import IconPicker from "~/components/IconPicker";
import Input from "~/components/Input";
import InputSelect from "~/components/InputSelect";
import Text from "~/components/Text";
import useStores from "~/hooks/useStores";
type Props = {
collectionId: string;
onSubmit: () => void;
};
const CollectionEdit = ({ collectionId, onSubmit }: Props) => {
const { collections } = useStores();
const collection = collections.get(collectionId);
invariant(collection, "Collection not found");
const [name, setName] = useState(collection.name);
const [icon, setIcon] = useState(collection.icon);
const [color, setColor] = useState(collection.color || "#4E5C6E");
const [sort, setSort] = useState<{
field: string;
direction: "asc" | "desc";
}>(collection.sort);
const [isSaving, setIsSaving] = useState(false);
const { t } = useTranslation();
const handleSubmit = React.useCallback(
async (ev: React.SyntheticEvent<HTMLFormElement>) => {
ev.preventDefault();
setIsSaving(true);
try {
await collection.save({
name,
icon,
color,
sort,
});
onSubmit();
toast.success(t("The collection was updated"));
} catch (err) {
toast.error(err.message);
} finally {
setIsSaving(false);
}
},
[collection, color, icon, name, onSubmit, sort, t]
);
const handleSortChange = (value: string) => {
const [field, direction] = value.split(".");
if (direction === "asc" || direction === "desc") {
setSort({
field,
direction,
});
}
};
const handleNameChange = (ev: React.ChangeEvent<HTMLInputElement>) => {
setName(ev.target.value);
};
const handleChange = (color: string, icon: string) => {
setColor(color);
setIcon(icon);
};
return (
<Flex column>
<form onSubmit={handleSubmit}>
<Text as="p" type="secondary">
<Trans>
You can edit the name and other details at any time, however doing
so often might confuse your team mates.
</Trans>
</Text>
<Flex gap={8}>
<Input
type="text"
label={t("Name")}
onChange={handleNameChange}
maxLength={CollectionValidation.maxNameLength}
value={name}
required
autoFocus
flex
/>
<IconPicker
onChange={handleChange}
color={color}
initial={name[0]}
icon={icon}
/>
</Flex>
<InputSelect
label={t("Sort in sidebar")}
options={[
{
label: t("Alphabetical sort"),
value: "title.asc",
},
{
label: t("Manual sort"),
value: "index.asc",
},
]}
value={`${sort.field}.${sort.direction}`}
onChange={handleSortChange}
ariaLabel={t("Sort")}
/>
<Button type="submit" disabled={isSaving || !collection.name}>
{isSaving ? `${t("Saving")}` : t("Save")}
</Button>
</form>
</Flex>
);
};
export default observer(CollectionEdit);

View File

@@ -1,175 +0,0 @@
import intersection from "lodash/intersection";
import { observable } from "mobx";
import { observer } from "mobx-react";
import * as React from "react";
import { withTranslation, Trans, WithTranslation } from "react-i18next";
import { toast } from "sonner";
import { randomElement } from "@shared/random";
import { CollectionPermission } from "@shared/types";
import { colorPalette } from "@shared/utils/collections";
import { CollectionValidation } from "@shared/validations";
import RootStore from "~/stores/RootStore";
import Collection from "~/models/Collection";
import Button from "~/components/Button";
import Flex from "~/components/Flex";
import IconPicker, { icons } from "~/components/IconPicker";
import Input from "~/components/Input";
import InputSelectPermission from "~/components/InputSelectPermission";
import Switch from "~/components/Switch";
import Text from "~/components/Text";
import withStores from "~/components/withStores";
import history from "~/utils/history";
type Props = RootStore &
WithTranslation & {
onSubmit: () => void;
};
@observer
class CollectionNew extends React.Component<Props> {
@observable
name = "";
@observable
icon = "";
@observable
color = randomElement(colorPalette);
@observable
sharing = true;
@observable
permission = CollectionPermission.ReadWrite;
@observable
isSaving: boolean;
hasOpenedIconPicker = false;
handleSubmit = async (ev: React.SyntheticEvent) => {
ev.preventDefault();
this.isSaving = true;
const collection = new Collection(
{
name: this.name,
sharing: this.sharing,
icon: this.icon,
color: this.color,
permission: this.permission,
documents: [],
},
this.props.collections
);
try {
await collection.save();
this.props.onSubmit();
history.push(collection.path);
} catch (err) {
toast.error(err.message);
} finally {
this.isSaving = false;
}
};
handleNameChange = (ev: React.ChangeEvent<HTMLInputElement>) => {
this.name = ev.target.value;
// If the user hasn't picked an icon yet, go ahead and suggest one based on
// the name of the collection. It's the little things sometimes.
if (!this.hasOpenedIconPicker) {
const keys = Object.keys(icons);
for (const key of keys) {
const icon = icons[key];
const keywords = icon.keywords.split(" ");
const namewords = this.name.toLowerCase().split(" ");
const matches = intersection(namewords, keywords);
if (matches.length > 0) {
this.icon = key;
return;
}
}
this.icon = "collection";
}
};
handleIconPickerOpen = () => {
this.hasOpenedIconPicker = true;
};
handlePermissionChange = (permission: CollectionPermission) => {
this.permission = permission;
};
handleSharingChange = (ev: React.ChangeEvent<HTMLInputElement>) => {
this.sharing = ev.target.checked;
};
handleChange = (color: string, icon: string) => {
this.color = color;
this.icon = icon;
};
render() {
const { t, auth } = this.props;
const teamSharingEnabled = !!auth.team && auth.team.sharing;
return (
<form onSubmit={this.handleSubmit}>
<Text as="p" type="secondary">
<Trans>
Collections are for grouping your documents. They work best when
organized around a topic or internal team Product or Engineering
for example.
</Trans>
</Text>
<Flex gap={8}>
<Input
type="text"
label={t("Name")}
onChange={this.handleNameChange}
maxLength={CollectionValidation.maxNameLength}
value={this.name}
required
autoFocus
flex
/>
<IconPicker
onOpen={this.handleIconPickerOpen}
onChange={this.handleChange}
initial={this.name[0]}
color={this.color}
icon={this.icon}
/>
</Flex>
<InputSelectPermission
value={this.permission}
onChange={this.handlePermissionChange}
note={t(
"This is the default level of access, you can give individual users or groups more access once the collection is created."
)}
/>
{teamSharingEnabled && (
<Switch
id="sharing"
label={t("Public document sharing")}
onChange={this.handleSharingChange}
checked={this.sharing}
note={t(
"When enabled any documents within this collection can be shared publicly on the internet."
)}
/>
)}
<Button type="submit" disabled={this.isSaving || !this.name}>
{this.isSaving ? `${t("Creating")}` : t("Create")}
</Button>
</form>
);
}
}
export default withTranslation()(withStores(CollectionNew));

View File

@@ -208,7 +208,6 @@ class DocumentScene extends React.Component<Props> {
if (abilities.move) {
dialogs.openModal({
title: t("Move document"),
isCentered: true,
content: <DocumentMove document={document} />,
});
}
@@ -258,7 +257,6 @@ class DocumentScene extends React.Component<Props> {
} else {
dialogs.openModal({
title: t("Publish document"),
isCentered: true,
content: <DocumentPublish document={document} />,
});
}

View File

@@ -71,7 +71,6 @@ function ApiKeys() {
title={t("Create a token")}
onRequestClose={handleNewModalClose}
isOpen={newModalOpen}
isCentered
>
<APITokenNew onSubmit={handleNewModalClose} />
</Modal>

View File

@@ -113,7 +113,6 @@ function Details() {
dialogs.openModal({
title: t("Delete workspace"),
content: <TeamDelete onSubmit={dialogs.closeAllModals} />,
isCentered: true,
});
};

View File

@@ -24,7 +24,6 @@ function Export() {
dialogs.openModal({
title: t("Export data"),
isCentered: true,
content: <ExportDialog onSubmit={dialogs.closeAllModals} />,
});
},

View File

@@ -51,7 +51,6 @@ function Import() {
onClick={() => {
dialogs.openModal({
title: t("Import data"),
isCentered: true,
content: <ImportMarkdownDialog />,
});
}}
@@ -77,7 +76,6 @@ function Import() {
onClick={() => {
dialogs.openModal({
title: t("Import data"),
isCentered: true,
content: <ImportJSONDialog />,
});
}}
@@ -98,7 +96,6 @@ function Import() {
onClick={() => {
dialogs.openModal({
title: t("Import data"),
isCentered: true,
content: <ImportNotionDialog />,
});
}}

View File

@@ -43,7 +43,6 @@ function Preferences() {
dialogs.openModal({
title: t("Delete account"),
content: <UserDelete onSubmit={dialogs.closeAllModals} />,
isCentered: true,
});
};

View File

@@ -102,7 +102,6 @@ function Security() {
if (inviteRequired) {
dialogs.openModal({
isCentered: true,
title: t("Are you sure you want to require invites?"),
content: (
<ConfirmationDialog

View File

@@ -76,7 +76,6 @@ const FileOperationListItem = ({ fileOperation }: Props) => {
const handleConfirmDelete = React.useCallback(async () => {
dialogs.openModal({
isCentered: true,
title: t("Are you sure you want to delete this import?"),
content: (
<ConfirmationDialog

View File

@@ -6,7 +6,7 @@ type DialogDefinition = {
title: string;
content: React.ReactNode;
isOpen: boolean;
isCentered?: boolean;
fullscreen?: boolean;
};
export default class DialogsStore {
@@ -45,11 +45,11 @@ export default class DialogsStore {
openModal = ({
title,
content,
isCentered,
fullscreen,
replace,
}: {
title: string;
isCentered?: boolean;
fullscreen?: boolean;
content: React.ReactNode;
replace?: boolean;
}) => {
@@ -64,8 +64,8 @@ export default class DialogsStore {
this.modalStack.set(id, {
title,
content,
fullscreen,
isOpen: true,
isCentered,
});
}),
0

View File

@@ -25,7 +25,6 @@ const WebhookSubscriptionListItem = ({ webhook }: Props) => {
const showDeletionConfirmation = React.useCallback(() => {
dialogs.openModal({
title: t("Delete webhook"),
isCentered: true,
content: (
<WebhookSubscriptionRevokeDialog
onSubmit={dialogs.closeAllModals}

View File

@@ -114,6 +114,15 @@
"previously edited": "previously edited",
"You": "You",
"Viewers": "Viewers",
"Collections are used to group documents and choose permissions": "Collections are used to group documents and choose permissions",
"Name": "Name",
"The default access for workspace members, you can share with more users or groups later.": "The default access for workspace members, you can share with more users or groups later.",
"Public document sharing": "Public document sharing",
"Allow documents within this collection to be shared publicly on the internet.": "Allow documents within this collection to be shared publicly on the internet.",
"Saving": "Saving",
"Save": "Save",
"Creating": "Creating",
"Create": "Create",
"Collection deleted": "Collection deleted",
"Im sure Delete": "Im sure Delete",
"Deleting": "Deleting",
@@ -173,7 +182,6 @@
"{{ completed }} task done_plural": "{{ completed }} tasks done",
"{{ completed }} of {{ total }} tasks": "{{ completed }} of {{ total }} tasks",
"Template created, go ahead and customize it": "Template created, go ahead and customize it",
"Creating": "Creating",
"Creating a template from <em>{{titleWithDefault}}</em> is a non-destructive action we'll make a copy of the document and turn it into a template that can be used as a starting point for new documents.": "Creating a template from <em>{{titleWithDefault}}</em> is a non-destructive action we'll make a copy of the document and turn it into a template that can be used as a starting point for new documents.",
"Currently editing": "Currently editing",
"Currently viewing": "Currently viewing",
@@ -214,16 +222,16 @@
"{{ count }} member": "{{ count }} member",
"{{ count }} member_plural": "{{ count }} members",
"Group members": "Group members",
"Icon": "Icon",
"Show menu": "Show menu",
"Choose icon": "Choose icon",
"Loading": "Loading",
"Select a color": "Select a color",
"Search": "Search",
"Default access": "Default access",
"Permission": "Permission",
"Can edit": "Can edit",
"View only": "View only",
"No access": "No access",
"Default access": "Default access",
"Role": "Role",
"Editor": "Editor",
"Viewer": "Viewer",
@@ -296,14 +304,12 @@
"No results": "No results",
"Previous page": "Previous page",
"Next page": "Next page",
"Saving": "Saving",
"Are you sure you want to make {{ userName }} a read-only viewer? They will not be able to edit any content": "Are you sure you want to make {{ userName }} a read-only viewer? They will not be able to edit any content",
"Are you sure you want to make {{ userName }} a member?": "Are you sure you want to make {{ userName }} a member?",
"I understand, delete": "I understand, delete",
"Are you sure you want to permanently delete {{ userName }}? This operation is unrecoverable, consider suspending the user instead.": "Are you sure you want to permanently delete {{ userName }}? This operation is unrecoverable, consider suspending the user instead.",
"Are you sure you want to make {{ userName }} an admin? Admins can modify team and billing information.": "Are you sure you want to make {{ userName }} an admin? Admins can modify team and billing information.",
"Are you sure you want to suspend {{ userName }}? Suspended users will be prevented from logging in.": "Are you sure you want to suspend {{ userName }}? Suspended users will be prevented from logging in.",
"Save": "Save",
"New name": "New name",
"Name can't be empty": "Name can't be empty",
"Your import completed": "Your import completed",
@@ -484,15 +490,6 @@
"{{ usersCount }} users with access_plural": "{{ usersCount }} users with access",
"{{ groupsCount }} groups with access": "{{ groupsCount }} group with access",
"{{ groupsCount }} groups with access_plural": "{{ groupsCount }} 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",
"Sort": "Sort",
"Collections are for grouping your documents. They work best when organized around a topic or internal team — Product or Engineering for example.": "Collections are for grouping your documents. They work best when organized around a topic or internal team — Product or Engineering for example.",
"This is the default level of access, you can give individual users or groups more access once the collection is created.": "This is the default level of access, you can give individual users or groups more access once the collection is created.",
"Public document sharing": "Public document sharing",
"When enabled any documents within this collection can be shared publicly on the internet.": "When enabled any documents within this collection can be shared publicly on the internet.",
"Create": "Create",
"{{ groupName }} was added to the collection": "{{ groupName }} was added to the collection",
"Could not add user": "Could not add user",
"Cant find the group youre looking for?": "Cant find the group youre looking for?",

View File

@@ -130,7 +130,7 @@ export const buildLightTheme = (input: Partial<Colors>): DefaultTheme => {
commentBackground: colors.warmGrey,
modalBackdrop: colors.black10,
modalBackdrop: "rgba(0, 0, 0, 0.15)",
modalBackground: colors.white,
modalShadow:
"0 4px 8px rgb(0 0 0 / 8%), 0 2px 4px rgb(0 0 0 / 0%), 0 30px 40px rgb(0 0 0 / 8%)",