diff --git a/app/actions/definitions/collections.tsx b/app/actions/definitions/collections.tsx
index 693ecbdc91..700c84a2c9 100644
--- a/app/actions/definitions/collections.tsx
+++ b/app/actions/definitions/collections.tsx
@@ -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: ,
});
},
@@ -183,7 +184,6 @@ export const deleteCollection = createAction({
}
stores.dialogs.openModal({
- isCentered: true,
title: t("Delete collection"),
content: (
,
});
}
@@ -345,7 +344,6 @@ export const shareDocument = createAction({
stores.dialogs.openModal({
title: t("Share this document"),
- isCentered: true,
content: (
,
});
},
@@ -751,7 +747,6 @@ export const moveDocument = createAction({
title: t("Move {{ documentType }}", {
documentType: document.noun,
}),
- isCentered: true,
content: ,
});
}
@@ -805,7 +800,6 @@ export const deleteDocument = createAction({
title: t("Delete {{ documentName }}", {
documentName: document.noun,
}),
- isCentered: true,
content: (
,
});
},
diff --git a/app/actions/definitions/users.tsx b/app/actions/definitions/users.tsx
index c2ba446118..df6082d6b4 100644
--- a/app/actions/definitions/users.tsx
+++ b/app/actions/definitions/users.tsx
@@ -17,6 +17,7 @@ export const inviteUser = createAction({
perform: ({ t }) => {
stores.dialogs.openModal({
title: t("Invite people"),
+ fullscreen: true,
content: ,
});
},
@@ -38,7 +39,6 @@ export const deleteUserActionFactory = (userId: string) =>
stores.dialogs.openModal({
title: t("Delete user"),
- isCentered: true,
content: (
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 ;
+});
diff --git a/app/components/Collection/CollectionForm.tsx b/app/components/Collection/CollectionForm.tsx
new file mode 100644
index 0000000000..aa0a8e4c2e
--- /dev/null
+++ b/app/components/Collection/CollectionForm.tsx
@@ -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({
+ 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 (
+
+ );
+});
+
+const StyledIconPicker = styled(IconPicker)`
+ margin-left: 4px;
+ margin-right: 4px;
+`;
diff --git a/app/components/Collection/CollectionNew.tsx b/app/components/Collection/CollectionNew.tsx
new file mode 100644
index 0000000000..c00586120a
--- /dev/null
+++ b/app/components/Collection/CollectionNew.tsx
@@ -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 ;
+});
diff --git a/app/components/Dialogs.tsx b/app/components/Dialogs.tsx
index afd6b611b5..12d56294e4 100644
--- a/app/components/Dialogs.tsx
+++ b/app/components/Dialogs.tsx
@@ -22,7 +22,7 @@ function Dialogs() {
dialogs.closeModal(id)}
title={modal.title}
>
diff --git a/app/components/IconPicker.tsx b/app/components/IconPicker.tsx
index 7a17eea75e..c642399696 100644
--- a/app/components/IconPicker.tsx
+++ b/app/components/IconPicker.tsx
@@ -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 (
-
-
-
+ <>
+
{(props) => (
-
+
)}
-
-
+
+
{Object.keys(icons).map((name, index) => (
-
-
+
+ >
);
}
@@ -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;
diff --git a/app/components/Icons/CollectionIcon.tsx b/app/components/Icons/CollectionIcon.tsx
index 52cddac24b..89582f4e25 100644
--- a/app/components/Icons/CollectionIcon.tsx
+++ b/app/components/Icons/CollectionIcon.tsx
@@ -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 (
{collection.initial}
diff --git a/app/components/Icons/IconLibrary.tsx b/app/components/Icons/IconLibrary.tsx
new file mode 100644
index 0000000000..831c97a70f
--- /dev/null
+++ b/app/components/Icons/IconLibrary.tsx
@@ -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",
+ },
+ };
+}
diff --git a/app/components/Input.tsx b/app/components/Input.tsx
index 5e13ab264b..3def4e5ac3 100644
--- a/app/components/Input.tsx
+++ b/app/components/Input.tsx
@@ -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}
/>
diff --git a/app/components/InputSelect.tsx b/app/components/InputSelect.tsx
index da2933876d..49456f71c2 100644
--- a/app/components/InputSelect.tsx
+++ b/app/components/InputSelect.tsx
@@ -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 {
placement: Placement;
}
@@ -55,7 +61,7 @@ interface InnerProps extends React.HTMLAttributes {
const getOptionFromValue = (options: Option[], value: string | null) =>
options.find((option) => option.value === value);
-const InputSelect = (props: Props) => {
+const InputSelect = (props: Props, ref: React.RefObject) => {
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);
diff --git a/app/components/InputSelectPermission.tsx b/app/components/InputSelectPermission.tsx
index 1ecff92705..c1f69c4b62 100644
--- a/app/components/InputSelectPermission.tsx
+++ b/app/components/InputSelectPermission.tsx
@@ -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
diff --git a/app/menus/UserMenu.tsx b/app/menus/UserMenu.tsx
index c4e10a6bcc..717c5c795a 100644
--- a/app/menus/UserMenu.tsx
+++ b/app/menus/UserMenu.tsx
@@ -40,7 +40,6 @@ function UserMenu({ user }: Props) {
ev.preventDefault();
dialogs.openModal({
title: t("Change role to admin"),
- isCentered: true,
content: (
),
@@ -105,7 +101,6 @@ function UserMenu({ user }: Props) {
ev.preventDefault();
dialogs.openModal({
title: t("Suspend user"),
- isCentered: true,
content: (
),
diff --git a/app/scenes/CollectionEdit.tsx b/app/scenes/CollectionEdit.tsx
deleted file mode 100644
index 48a0efe626..0000000000
--- a/app/scenes/CollectionEdit.tsx
+++ /dev/null
@@ -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) => {
- 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) => {
- setName(ev.target.value);
- };
-
- const handleChange = (color: string, icon: string) => {
- setColor(color);
- setIcon(icon);
- };
-
- return (
-
-
-
- );
-};
-
-export default observer(CollectionEdit);
diff --git a/app/scenes/CollectionNew.tsx b/app/scenes/CollectionNew.tsx
deleted file mode 100644
index 55cb571c47..0000000000
--- a/app/scenes/CollectionNew.tsx
+++ /dev/null
@@ -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 {
- @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) => {
- 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) => {
- 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 (
-
- );
- }
-}
-
-export default withTranslation()(withStores(CollectionNew));
diff --git a/app/scenes/Document/components/Document.tsx b/app/scenes/Document/components/Document.tsx
index ce5bcbe371..a231a90ee0 100644
--- a/app/scenes/Document/components/Document.tsx
+++ b/app/scenes/Document/components/Document.tsx
@@ -208,7 +208,6 @@ class DocumentScene extends React.Component {
if (abilities.move) {
dialogs.openModal({
title: t("Move document"),
- isCentered: true,
content: ,
});
}
@@ -258,7 +257,6 @@ class DocumentScene extends React.Component {
} else {
dialogs.openModal({
title: t("Publish document"),
- isCentered: true,
content: ,
});
}
diff --git a/app/scenes/Settings/ApiKeys.tsx b/app/scenes/Settings/ApiKeys.tsx
index 60e453d7d4..52e30694fe 100644
--- a/app/scenes/Settings/ApiKeys.tsx
+++ b/app/scenes/Settings/ApiKeys.tsx
@@ -71,7 +71,6 @@ function ApiKeys() {
title={t("Create a token")}
onRequestClose={handleNewModalClose}
isOpen={newModalOpen}
- isCentered
>
diff --git a/app/scenes/Settings/Details.tsx b/app/scenes/Settings/Details.tsx
index 37a072c831..5b2ad2e46b 100644
--- a/app/scenes/Settings/Details.tsx
+++ b/app/scenes/Settings/Details.tsx
@@ -113,7 +113,6 @@ function Details() {
dialogs.openModal({
title: t("Delete workspace"),
content: ,
- isCentered: true,
});
};
diff --git a/app/scenes/Settings/Export.tsx b/app/scenes/Settings/Export.tsx
index e25d4a638a..112b2b12f7 100644
--- a/app/scenes/Settings/Export.tsx
+++ b/app/scenes/Settings/Export.tsx
@@ -24,7 +24,6 @@ function Export() {
dialogs.openModal({
title: t("Export data"),
- isCentered: true,
content: ,
});
},
diff --git a/app/scenes/Settings/Import.tsx b/app/scenes/Settings/Import.tsx
index 334df921d1..ae85e37279 100644
--- a/app/scenes/Settings/Import.tsx
+++ b/app/scenes/Settings/Import.tsx
@@ -51,7 +51,6 @@ function Import() {
onClick={() => {
dialogs.openModal({
title: t("Import data"),
- isCentered: true,
content: ,
});
}}
@@ -77,7 +76,6 @@ function Import() {
onClick={() => {
dialogs.openModal({
title: t("Import data"),
- isCentered: true,
content: ,
});
}}
@@ -98,7 +96,6 @@ function Import() {
onClick={() => {
dialogs.openModal({
title: t("Import data"),
- isCentered: true,
content: ,
});
}}
diff --git a/app/scenes/Settings/Preferences.tsx b/app/scenes/Settings/Preferences.tsx
index 6f62f2efe7..0755a6adb0 100644
--- a/app/scenes/Settings/Preferences.tsx
+++ b/app/scenes/Settings/Preferences.tsx
@@ -43,7 +43,6 @@ function Preferences() {
dialogs.openModal({
title: t("Delete account"),
content: ,
- isCentered: true,
});
};
diff --git a/app/scenes/Settings/Security.tsx b/app/scenes/Settings/Security.tsx
index f2a4f1a47d..d933c7847c 100644
--- a/app/scenes/Settings/Security.tsx
+++ b/app/scenes/Settings/Security.tsx
@@ -102,7 +102,6 @@ function Security() {
if (inviteRequired) {
dialogs.openModal({
- isCentered: true,
title: t("Are you sure you want to require invites?"),
content: (
{
const handleConfirmDelete = React.useCallback(async () => {
dialogs.openModal({
- isCentered: true,
title: t("Are you sure you want to delete this import?"),
content: (
{
@@ -64,8 +64,8 @@ export default class DialogsStore {
this.modalStack.set(id, {
title,
content,
+ fullscreen,
isOpen: true,
- isCentered,
});
}),
0
diff --git a/plugins/webhooks/client/components/WebhookSubscriptionListItem.tsx b/plugins/webhooks/client/components/WebhookSubscriptionListItem.tsx
index 2e785682f0..19a04690db 100644
--- a/plugins/webhooks/client/components/WebhookSubscriptionListItem.tsx
+++ b/plugins/webhooks/client/components/WebhookSubscriptionListItem.tsx
@@ -25,7 +25,6 @@ const WebhookSubscriptionListItem = ({ webhook }: Props) => {
const showDeletionConfirmation = React.useCallback(() => {
dialogs.openModal({
title: t("Delete webhook"),
- isCentered: true,
content: (
{{titleWithDefault}} 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 {{titleWithDefault}} 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",
"Can’t find the group you’re looking for?": "Can’t find the group you’re looking for?",
diff --git a/shared/styles/theme.ts b/shared/styles/theme.ts
index 8514c20842..5f10aa263b 100644
--- a/shared/styles/theme.ts
+++ b/shared/styles/theme.ts
@@ -130,7 +130,7 @@ export const buildLightTheme = (input: Partial): 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%)",