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:
@@ -10,9 +10,9 @@ import {
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import stores from "~/stores";
|
import stores from "~/stores";
|
||||||
import Collection from "~/models/Collection";
|
import Collection from "~/models/Collection";
|
||||||
import CollectionEdit from "~/scenes/CollectionEdit";
|
|
||||||
import CollectionNew from "~/scenes/CollectionNew";
|
|
||||||
import CollectionPermissions from "~/scenes/CollectionPermissions";
|
import CollectionPermissions from "~/scenes/CollectionPermissions";
|
||||||
|
import { CollectionEdit } from "~/components/Collection/CollectionEdit";
|
||||||
|
import { CollectionNew } from "~/components/Collection/CollectionNew";
|
||||||
import CollectionDeleteDialog from "~/components/CollectionDeleteDialog";
|
import CollectionDeleteDialog from "~/components/CollectionDeleteDialog";
|
||||||
import DynamicCollectionIcon from "~/components/Icons/CollectionIcon";
|
import DynamicCollectionIcon from "~/components/Icons/CollectionIcon";
|
||||||
import { createAction } from "~/actions";
|
import { createAction } from "~/actions";
|
||||||
@@ -103,6 +103,7 @@ export const editCollectionPermissions = createAction({
|
|||||||
|
|
||||||
stores.dialogs.openModal({
|
stores.dialogs.openModal({
|
||||||
title: t("Collection permissions"),
|
title: t("Collection permissions"),
|
||||||
|
fullscreen: true,
|
||||||
content: <CollectionPermissions collectionId={activeCollectionId} />,
|
content: <CollectionPermissions collectionId={activeCollectionId} />,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -183,7 +184,6 @@ export const deleteCollection = createAction({
|
|||||||
}
|
}
|
||||||
|
|
||||||
stores.dialogs.openModal({
|
stores.dialogs.openModal({
|
||||||
isCentered: true,
|
|
||||||
title: t("Delete collection"),
|
title: t("Delete collection"),
|
||||||
content: (
|
content: (
|
||||||
<CollectionDeleteDialog
|
<CollectionDeleteDialog
|
||||||
|
|||||||
@@ -224,7 +224,6 @@ export const publishDocument = createAction({
|
|||||||
} else if (document) {
|
} else if (document) {
|
||||||
stores.dialogs.openModal({
|
stores.dialogs.openModal({
|
||||||
title: t("Publish document"),
|
title: t("Publish document"),
|
||||||
isCentered: true,
|
|
||||||
content: <DocumentPublish document={document} />,
|
content: <DocumentPublish document={document} />,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -345,7 +344,6 @@ export const shareDocument = createAction({
|
|||||||
|
|
||||||
stores.dialogs.openModal({
|
stores.dialogs.openModal({
|
||||||
title: t("Share this document"),
|
title: t("Share this document"),
|
||||||
isCentered: true,
|
|
||||||
content: (
|
content: (
|
||||||
<SharePopover
|
<SharePopover
|
||||||
document={document}
|
document={document}
|
||||||
@@ -495,7 +493,6 @@ export const duplicateDocument = createAction({
|
|||||||
|
|
||||||
stores.dialogs.openModal({
|
stores.dialogs.openModal({
|
||||||
title: t("Copy document"),
|
title: t("Copy document"),
|
||||||
isCentered: true,
|
|
||||||
content: (
|
content: (
|
||||||
<DuplicateDialog
|
<DuplicateDialog
|
||||||
document={document}
|
document={document}
|
||||||
@@ -692,7 +689,6 @@ export const createTemplate = createAction({
|
|||||||
|
|
||||||
stores.dialogs.openModal({
|
stores.dialogs.openModal({
|
||||||
title: t("Create template"),
|
title: t("Create template"),
|
||||||
isCentered: true,
|
|
||||||
content: <DocumentTemplatizeDialog documentId={activeDocumentId} />,
|
content: <DocumentTemplatizeDialog documentId={activeDocumentId} />,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -751,7 +747,6 @@ export const moveDocument = createAction({
|
|||||||
title: t("Move {{ documentType }}", {
|
title: t("Move {{ documentType }}", {
|
||||||
documentType: document.noun,
|
documentType: document.noun,
|
||||||
}),
|
}),
|
||||||
isCentered: true,
|
|
||||||
content: <DocumentMove document={document} />,
|
content: <DocumentMove document={document} />,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -805,7 +800,6 @@ export const deleteDocument = createAction({
|
|||||||
title: t("Delete {{ documentName }}", {
|
title: t("Delete {{ documentName }}", {
|
||||||
documentName: document.noun,
|
documentName: document.noun,
|
||||||
}),
|
}),
|
||||||
isCentered: true,
|
|
||||||
content: (
|
content: (
|
||||||
<DocumentDelete
|
<DocumentDelete
|
||||||
document={document}
|
document={document}
|
||||||
@@ -840,7 +834,6 @@ export const permanentlyDeleteDocument = createAction({
|
|||||||
title: t("Permanently delete {{ documentName }}", {
|
title: t("Permanently delete {{ documentName }}", {
|
||||||
documentName: document.noun,
|
documentName: document.noun,
|
||||||
}),
|
}),
|
||||||
isCentered: true,
|
|
||||||
content: (
|
content: (
|
||||||
<DocumentPermanentDelete
|
<DocumentPermanentDelete
|
||||||
document={document}
|
document={document}
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ export const createTeam = createAction({
|
|||||||
user &&
|
user &&
|
||||||
stores.dialogs.openModal({
|
stores.dialogs.openModal({
|
||||||
title: t("Create a workspace"),
|
title: t("Create a workspace"),
|
||||||
|
fullscreen: true,
|
||||||
content: <TeamNew user={user} />,
|
content: <TeamNew user={user} />,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export const inviteUser = createAction({
|
|||||||
perform: ({ t }) => {
|
perform: ({ t }) => {
|
||||||
stores.dialogs.openModal({
|
stores.dialogs.openModal({
|
||||||
title: t("Invite people"),
|
title: t("Invite people"),
|
||||||
|
fullscreen: true,
|
||||||
content: <Invite onSubmit={stores.dialogs.closeAllModals} />,
|
content: <Invite onSubmit={stores.dialogs.closeAllModals} />,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -38,7 +39,6 @@ export const deleteUserActionFactory = (userId: string) =>
|
|||||||
|
|
||||||
stores.dialogs.openModal({
|
stores.dialogs.openModal({
|
||||||
title: t("Delete user"),
|
title: t("Delete user"),
|
||||||
isCentered: true,
|
|
||||||
content: (
|
content: (
|
||||||
<UserDeleteDialog
|
<UserDeleteDialog
|
||||||
user={user}
|
user={user}
|
||||||
|
|||||||
32
app/components/Collection/CollectionEdit.tsx
Normal file
32
app/components/Collection/CollectionEdit.tsx
Normal 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} />;
|
||||||
|
});
|
||||||
164
app/components/Collection/CollectionForm.tsx
Normal file
164
app/components/Collection/CollectionForm.tsx
Normal 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;
|
||||||
|
`;
|
||||||
32
app/components/Collection/CollectionNew.tsx
Normal file
32
app/components/Collection/CollectionNew.tsx
Normal 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} />;
|
||||||
|
});
|
||||||
@@ -22,7 +22,7 @@ function Dialogs() {
|
|||||||
<Modal
|
<Modal
|
||||||
key={id}
|
key={id}
|
||||||
isOpen={modal.isOpen}
|
isOpen={modal.isOpen}
|
||||||
isCentered={modal.isCentered}
|
fullscreen={modal.fullscreen}
|
||||||
onRequestClose={() => dialogs.closeModal(id)}
|
onRequestClose={() => dialogs.closeModal(id)}
|
||||||
title={modal.title}
|
title={modal.title}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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 * as React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
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 styled, { useTheme } from "styled-components";
|
||||||
import breakpoint from "styled-components-breakpoint";
|
|
||||||
import { s } from "@shared/styles";
|
|
||||||
import { colorPalette } from "@shared/utils/collections";
|
import { colorPalette } from "@shared/utils/collections";
|
||||||
import ContextMenu from "~/components/ContextMenu";
|
|
||||||
import Flex from "~/components/Flex";
|
import Flex from "~/components/Flex";
|
||||||
import { LabelText } from "~/components/Input";
|
|
||||||
import NudeButton from "~/components/NudeButton";
|
import NudeButton from "~/components/NudeButton";
|
||||||
import Text from "~/components/Text";
|
import Text from "~/components/Text";
|
||||||
import lazyWithRetry from "~/utils/lazyWithRetry";
|
import lazyWithRetry from "~/utils/lazyWithRetry";
|
||||||
import DelayedMount from "./DelayedMount";
|
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(
|
const TwitterPicker = lazyWithRetry(
|
||||||
() => import("react-color/lib/components/twitter/Twitter")
|
() => 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 = {
|
type Props = {
|
||||||
onOpen?: () => void;
|
onOpen?: () => void;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
@@ -284,6 +25,7 @@ type Props = {
|
|||||||
initial: string;
|
initial: string;
|
||||||
icon: string;
|
icon: string;
|
||||||
color: string;
|
color: string;
|
||||||
|
className?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
function IconPicker({
|
function IconPicker({
|
||||||
@@ -293,46 +35,70 @@ function IconPicker({
|
|||||||
initial,
|
initial,
|
||||||
color,
|
color,
|
||||||
onChange,
|
onChange,
|
||||||
|
className,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const menu = useMenuState({
|
const popover = usePopoverState({
|
||||||
modal: true,
|
gutter: 0,
|
||||||
placement: "bottom-end",
|
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 (
|
return (
|
||||||
<Wrapper>
|
<>
|
||||||
<Label>
|
<PopoverDisclosure {...popover}>
|
||||||
<LabelText>{t("Icon")}</LabelText>
|
|
||||||
</Label>
|
|
||||||
<MenuButton {...menu}>
|
|
||||||
{(props) => (
|
{(props) => (
|
||||||
<Button aria-label={t("Show menu")} {...props}>
|
<NudeButton
|
||||||
|
aria-label={t("Show menu")}
|
||||||
|
className={className}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
<Icon
|
<Icon
|
||||||
as={icons[icon || "collection"].component}
|
as={IconLibrary.getComponent(icon || "collection")}
|
||||||
color={color}
|
color={color}
|
||||||
size={30}
|
|
||||||
>
|
>
|
||||||
{initial}
|
{initial}
|
||||||
</Icon>
|
</Icon>
|
||||||
</Button>
|
</NudeButton>
|
||||||
)}
|
)}
|
||||||
</MenuButton>
|
</PopoverDisclosure>
|
||||||
<ContextMenu
|
<Popover {...popover} width={388} aria-label={t("Choose icon")}>
|
||||||
{...menu}
|
|
||||||
onOpen={onOpen}
|
|
||||||
onClose={onClose}
|
|
||||||
maxWidth={308}
|
|
||||||
aria-label={t("Choose icon")}
|
|
||||||
>
|
|
||||||
<Icons>
|
<Icons>
|
||||||
{Object.keys(icons).map((name, index) => (
|
{Object.keys(icons).map((name, index) => (
|
||||||
<MenuItem
|
<MenuItem key={name} onClick={() => onChange(color, name)}>
|
||||||
key={name}
|
|
||||||
onClick={() => onChange(color, name)}
|
|
||||||
{...menu}
|
|
||||||
>
|
|
||||||
{(props) => (
|
{(props) => (
|
||||||
<IconButton
|
<IconButton
|
||||||
style={
|
style={
|
||||||
@@ -342,7 +108,11 @@ function IconPicker({
|
|||||||
}
|
}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<Icon as={icons[name].component} color={color} size={30}>
|
<Icon
|
||||||
|
as={IconLibrary.getComponent(name)}
|
||||||
|
color={color}
|
||||||
|
size={30}
|
||||||
|
>
|
||||||
{initial}
|
{initial}
|
||||||
</Icon>
|
</Icon>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
@@ -363,28 +133,12 @@ function IconPicker({
|
|||||||
onChange={(color) => onChange(color.hex, icon)}
|
onChange={(color) => onChange(color.hex, icon)}
|
||||||
colors={colorPalette}
|
colors={colorPalette}
|
||||||
triangle="hide"
|
triangle="hide"
|
||||||
styles={{
|
styles={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",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</React.Suspense>
|
</React.Suspense>
|
||||||
</Colors>
|
</Colors>
|
||||||
</ContextMenu>
|
</Popover>
|
||||||
</Wrapper>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -397,22 +151,8 @@ const Colors = styled(Flex)`
|
|||||||
padding: 8px;
|
padding: 8px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Label = styled.label`
|
|
||||||
display: block;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Icons = styled.div`
|
const Icons = styled.div`
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
|
|
||||||
${breakpoint("tablet")`
|
|
||||||
width: 304px;
|
|
||||||
`};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Button = styled(NudeButton)`
|
|
||||||
border: 1px solid ${s("inputBorder")};
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const IconButton = styled(NudeButton)`
|
const IconButton = styled(NudeButton)`
|
||||||
@@ -429,9 +169,4 @@ const ColorPicker = styled(TwitterPicker)`
|
|||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Wrapper = styled("div")`
|
|
||||||
display: inline-block;
|
|
||||||
position: relative;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default IconPicker;
|
export default IconPicker;
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import { CollectionIcon } from "outline-icons";
|
|||||||
import { getLuminance } from "polished";
|
import { getLuminance } from "polished";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import Collection from "~/models/Collection";
|
import Collection from "~/models/Collection";
|
||||||
import { icons } from "~/components/IconPicker";
|
|
||||||
import useStores from "~/hooks/useStores";
|
import useStores from "~/hooks/useStores";
|
||||||
import Logger from "~/utils/Logger";
|
import Logger from "~/utils/Logger";
|
||||||
|
import { IconLibrary } from "./IconLibrary";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
/** The collection to show an icon for */
|
/** The collection to show an icon for */
|
||||||
@@ -38,7 +38,7 @@ function ResolvedCollectionIcon({
|
|||||||
|
|
||||||
if (collection.icon && collection.icon !== "collection") {
|
if (collection.icon && collection.icon !== "collection") {
|
||||||
try {
|
try {
|
||||||
const Component = icons[collection.icon].component;
|
const Component = IconLibrary.getComponent(collection.icon);
|
||||||
return (
|
return (
|
||||||
<Component color={color} size={size}>
|
<Component color={color} size={size}>
|
||||||
{collection.initial}
|
{collection.initial}
|
||||||
|
|||||||
300
app/components/Icons/IconLibrary.tsx
Normal file
300
app/components/Icons/IconLibrary.tsx
Normal 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",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -8,10 +8,14 @@ import Flex from "~/components/Flex";
|
|||||||
import Text from "~/components/Text";
|
import Text from "~/components/Text";
|
||||||
import { undraggableOnDesktop } from "~/styles";
|
import { undraggableOnDesktop } from "~/styles";
|
||||||
|
|
||||||
export const NativeTextarea = styled.textarea<{ hasIcon?: boolean }>`
|
export const NativeTextarea = styled.textarea<{
|
||||||
|
hasIcon?: boolean;
|
||||||
|
hasPrefix?: boolean;
|
||||||
|
}>`
|
||||||
border: 0;
|
border: 0;
|
||||||
flex: 1;
|
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;
|
outline: none;
|
||||||
background: none;
|
background: none;
|
||||||
color: ${s("text")};
|
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;
|
border: 0;
|
||||||
flex: 1;
|
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;
|
outline: none;
|
||||||
background: none;
|
background: none;
|
||||||
color: ${s("text")};
|
color: ${s("text")};
|
||||||
@@ -224,6 +232,7 @@ function Input(
|
|||||||
onFocus={handleFocus}
|
onFocus={handleFocus}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
hasIcon={!!icon}
|
hasIcon={!!icon}
|
||||||
|
hasPrefix={!!prefix}
|
||||||
{...rest}
|
{...rest}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
@@ -236,6 +245,7 @@ function Input(
|
|||||||
onFocus={handleFocus}
|
onFocus={handleFocus}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
hasIcon={!!icon}
|
hasIcon={!!icon}
|
||||||
|
hasPrefix={!!prefix}
|
||||||
type={type}
|
type={type}
|
||||||
{...rest}
|
{...rest}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -48,6 +48,12 @@ export type Props = {
|
|||||||
onChange?: (value: string | null) => void;
|
onChange?: (value: string | null) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface InputSelectRef {
|
||||||
|
value: string | null;
|
||||||
|
focus: () => void;
|
||||||
|
blur: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
interface InnerProps extends React.HTMLAttributes<HTMLDivElement> {
|
interface InnerProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
placement: Placement;
|
placement: Placement;
|
||||||
}
|
}
|
||||||
@@ -55,7 +61,7 @@ interface InnerProps extends React.HTMLAttributes<HTMLDivElement> {
|
|||||||
const getOptionFromValue = (options: Option[], value: string | null) =>
|
const getOptionFromValue = (options: Option[], value: string | null) =>
|
||||||
options.find((option) => option.value === value);
|
options.find((option) => option.value === value);
|
||||||
|
|
||||||
const InputSelect = (props: Props) => {
|
const InputSelect = (props: Props, ref: React.RefObject<InputSelectRef>) => {
|
||||||
const {
|
const {
|
||||||
value = null,
|
value = null,
|
||||||
label,
|
label,
|
||||||
@@ -122,6 +128,16 @@ const InputSelect = (props: Props) => {
|
|||||||
{ capture: true }
|
{ capture: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
React.useImperativeHandle(ref, () => ({
|
||||||
|
focus: () => {
|
||||||
|
buttonRef.current?.focus();
|
||||||
|
},
|
||||||
|
blur: () => {
|
||||||
|
buttonRef.current?.blur();
|
||||||
|
},
|
||||||
|
value: select.selectedValue,
|
||||||
|
}));
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
previousValue.current = value;
|
previousValue.current = value;
|
||||||
select.setSelectedValue(value);
|
select.setSelectedValue(value);
|
||||||
@@ -306,4 +322,4 @@ export const Positioner = styled(Position)`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default InputSelect;
|
export default React.forwardRef(InputSelect);
|
||||||
|
|||||||
@@ -3,33 +3,31 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { $Diff } from "utility-types";
|
import { $Diff } from "utility-types";
|
||||||
import { CollectionPermission } from "@shared/types";
|
import { CollectionPermission } from "@shared/types";
|
||||||
import { EmptySelectValue } from "~/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: $Diff<
|
||||||
Props,
|
Props,
|
||||||
{
|
{
|
||||||
options: Array<Option>;
|
options: Array<Option>;
|
||||||
ariaLabel: string;
|
ariaLabel: string;
|
||||||
}
|
}
|
||||||
>
|
>,
|
||||||
|
ref: React.RefObject<InputSelectRef>
|
||||||
) {
|
) {
|
||||||
const { value, onChange, ...rest } = props;
|
const { value, onChange, ...rest } = props;
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const handleChange = React.useCallback(
|
const handleChange = React.useCallback(
|
||||||
(value) => {
|
(value) => {
|
||||||
if (value === EmptySelectValue) {
|
onChange?.(value === EmptySelectValue ? null : value);
|
||||||
value = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
onChange?.(value);
|
|
||||||
},
|
},
|
||||||
[onChange]
|
[onChange]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InputSelect
|
<InputSelect
|
||||||
label={t("Default access")}
|
ref={ref}
|
||||||
|
label={t("Permission")}
|
||||||
options={[
|
options={[
|
||||||
{
|
{
|
||||||
label: t("Can edit"),
|
label: t("Can edit"),
|
||||||
@@ -51,3 +49,5 @@ export default function InputSelectPermission(
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default React.forwardRef(InputSelectPermission);
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ let openModals = 0;
|
|||||||
type Props = {
|
type Props = {
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
isCentered?: boolean;
|
fullscreen?: boolean;
|
||||||
title?: React.ReactNode;
|
title?: React.ReactNode;
|
||||||
onRequestClose: () => void;
|
onRequestClose: () => void;
|
||||||
};
|
};
|
||||||
@@ -31,7 +31,7 @@ type Props = {
|
|||||||
const Modal: React.FC<Props> = ({
|
const Modal: React.FC<Props> = ({
|
||||||
children,
|
children,
|
||||||
isOpen,
|
isOpen,
|
||||||
isCentered,
|
fullscreen,
|
||||||
title = "Untitled",
|
title = "Untitled",
|
||||||
onRequestClose,
|
onRequestClose,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
@@ -68,35 +68,17 @@ const Modal: React.FC<Props> = ({
|
|||||||
return (
|
return (
|
||||||
<DialogBackdrop {...dialog}>
|
<DialogBackdrop {...dialog}>
|
||||||
{(props) => (
|
{(props) => (
|
||||||
<Backdrop $isCentered={isCentered} {...props}>
|
<Backdrop $fullscreen={fullscreen} {...props}>
|
||||||
<Dialog
|
<Dialog
|
||||||
{...dialog}
|
{...dialog}
|
||||||
aria-label={typeof title === "string" ? title : undefined}
|
aria-label={typeof title === "string" ? title : undefined}
|
||||||
preventBodyScroll
|
preventBodyScroll
|
||||||
hideOnEsc
|
hideOnEsc
|
||||||
hideOnClickOutside={!!isCentered}
|
hideOnClickOutside={!fullscreen}
|
||||||
hide={onRequestClose}
|
hide={onRequestClose}
|
||||||
>
|
>
|
||||||
{(props) =>
|
{(props) =>
|
||||||
isCentered && !isMobile ? (
|
fullscreen || 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
|
<Fullscreen
|
||||||
$nested={!!depth}
|
$nested={!!depth}
|
||||||
style={
|
style={
|
||||||
@@ -126,6 +108,24 @@ const Modal: React.FC<Props> = ({
|
|||||||
<Text>{t("Back")} </Text>
|
<Text>{t("Back")} </Text>
|
||||||
</Back>
|
</Back>
|
||||||
</Fullscreen>
|
</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>
|
</Dialog>
|
||||||
@@ -135,16 +135,16 @@ const Modal: React.FC<Props> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const Backdrop = styled(Flex)<{ $isCentered?: boolean }>`
|
const Backdrop = styled(Flex)<{ $fullscreen?: boolean }>`
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
background-color: ${(props) =>
|
background-color: ${(props) =>
|
||||||
props.$isCentered
|
props.$fullscreen
|
||||||
? props.theme.modalBackdrop
|
? transparentize(0.25, props.theme.background)
|
||||||
: transparentize(0.25, props.theme.background)} !important;
|
: props.theme.modalBackdrop} !important;
|
||||||
z-index: ${depths.modalOverlay};
|
z-index: ${depths.modalOverlay};
|
||||||
transition: opacity 50ms ease-in-out;
|
transition: opacity 50ms ease-in-out;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ const Popover: React.FC<Props> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReakitPopover {...rest} hideOnEsc={false} hideOnClickOutside>
|
<StyledPopover {...rest} hideOnEsc={false} hideOnClickOutside>
|
||||||
<Contents
|
<Contents
|
||||||
$shrink={shrink}
|
$shrink={shrink}
|
||||||
$width={width}
|
$width={width}
|
||||||
@@ -71,7 +71,7 @@ const Popover: React.FC<Props> = ({
|
|||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</Contents>
|
</Contents>
|
||||||
</ReakitPopover>
|
</StyledPopover>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -83,6 +83,10 @@ type ContentsProps = {
|
|||||||
$mobilePosition?: "top" | "bottom";
|
$mobilePosition?: "top" | "bottom";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const StyledPopover = styled(ReakitPopover)`
|
||||||
|
z-index: ${depths.modal};
|
||||||
|
`;
|
||||||
|
|
||||||
const Contents = styled.div<ContentsProps>`
|
const Contents = styled.div<ContentsProps>`
|
||||||
display: ${(props) => (props.$flex ? "flex" : "block")};
|
display: ${(props) => (props.$flex ? "flex" : "block")};
|
||||||
animation: ${fadeAndScaleIn} 200ms ease;
|
animation: ${fadeAndScaleIn} 200ms ease;
|
||||||
|
|||||||
@@ -211,7 +211,6 @@ const Wrapper = styled.div`
|
|||||||
const DomainPrefix = styled(NativeInput)`
|
const DomainPrefix = styled(NativeInput)`
|
||||||
flex: 0 1 auto;
|
flex: 0 1 auto;
|
||||||
padding-right: 0 !important;
|
padding-right: 0 !important;
|
||||||
margin-right: -10px;
|
|
||||||
cursor: text;
|
cursor: text;
|
||||||
color: ${s("placeholder")};
|
color: ${s("placeholder")};
|
||||||
user-select: none;
|
user-select: none;
|
||||||
@@ -223,7 +222,7 @@ const ShareLinkInput = styled(Input)`
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
||||||
${NativeInput} {
|
${NativeInput} {
|
||||||
padding: 4px 8px;
|
padding: 4px 8px 4px 0;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -50,7 +50,6 @@ function TrashLink() {
|
|||||||
})}
|
})}
|
||||||
onRequestClose={() => setDocument(undefined)}
|
onRequestClose={() => setDocument(undefined)}
|
||||||
isOpen
|
isOpen
|
||||||
isCentered
|
|
||||||
>
|
>
|
||||||
<DocumentDelete
|
<DocumentDelete
|
||||||
document={document}
|
document={document}
|
||||||
|
|||||||
@@ -24,16 +24,19 @@ interface Props extends React.HTMLAttributes<HTMLInputElement> {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Switch({
|
function Switch(
|
||||||
width = 32,
|
{
|
||||||
height = 18,
|
width = 32,
|
||||||
labelPosition = "left",
|
height = 18,
|
||||||
label,
|
labelPosition = "left",
|
||||||
disabled,
|
label,
|
||||||
className,
|
disabled,
|
||||||
note,
|
className,
|
||||||
...props
|
note,
|
||||||
}: Props) {
|
...props
|
||||||
|
}: Props,
|
||||||
|
ref: React.Ref<HTMLInputElement>
|
||||||
|
) {
|
||||||
const component = (
|
const component = (
|
||||||
<Input
|
<Input
|
||||||
width={width}
|
width={width}
|
||||||
@@ -41,6 +44,7 @@ function Switch({
|
|||||||
className={label ? undefined : className}
|
className={label ? undefined : className}
|
||||||
>
|
>
|
||||||
<HiddenInput
|
<HiddenInput
|
||||||
|
ref={ref}
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
width={width}
|
width={width}
|
||||||
height={height}
|
height={height}
|
||||||
@@ -164,4 +168,4 @@ const HiddenInput = styled.input<{ width: number; height: number }>`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default Switch;
|
export default React.forwardRef(Switch);
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ function ApiKeyMenu({ apiKey }: Props) {
|
|||||||
const handleRevoke = React.useCallback(() => {
|
const handleRevoke = React.useCallback(() => {
|
||||||
dialogs.openModal({
|
dialogs.openModal({
|
||||||
title: t("Revoke token"),
|
title: t("Revoke token"),
|
||||||
isCentered: true,
|
|
||||||
content: (
|
content: (
|
||||||
<ApiKeyRevokeDialog onSubmit={dialogs.closeAllModals} apiKey={apiKey} />
|
<ApiKeyRevokeDialog onSubmit={dialogs.closeAllModals} apiKey={apiKey} />
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -63,7 +63,6 @@ function CollectionMenu({
|
|||||||
const handleExport = React.useCallback(() => {
|
const handleExport = React.useCallback(() => {
|
||||||
dialogs.openModal({
|
dialogs.openModal({
|
||||||
title: t("Export collection"),
|
title: t("Export collection"),
|
||||||
isCentered: true,
|
|
||||||
content: (
|
content: (
|
||||||
<ExportDialog
|
<ExportDialog
|
||||||
collection={collection}
|
collection={collection}
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ function CommentMenu({ comment, onEdit, onDelete, className }: Props) {
|
|||||||
const handleDelete = React.useCallback(() => {
|
const handleDelete = React.useCallback(() => {
|
||||||
dialogs.openModal({
|
dialogs.openModal({
|
||||||
title: t("Delete comment"),
|
title: t("Delete comment"),
|
||||||
isCentered: true,
|
|
||||||
content: <CommentDeleteDialog comment={comment} onSubmit={onDelete} />,
|
content: <CommentDeleteDialog comment={comment} onSubmit={onDelete} />,
|
||||||
});
|
});
|
||||||
}, [dialogs, comment, onDelete, t]);
|
}, [dialogs, comment, onDelete, t]);
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ function GroupMenu({ group, onMembers }: Props) {
|
|||||||
title={t("Delete group")}
|
title={t("Delete group")}
|
||||||
onRequestClose={() => setDeleteModalOpen(false)}
|
onRequestClose={() => setDeleteModalOpen(false)}
|
||||||
isOpen={deleteModalOpen}
|
isOpen={deleteModalOpen}
|
||||||
isCentered
|
|
||||||
>
|
>
|
||||||
<GroupDelete group={group} onSubmit={() => setDeleteModalOpen(false)} />
|
<GroupDelete group={group} onSubmit={() => setDeleteModalOpen(false)} />
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ function UserMenu({ user }: Props) {
|
|||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
dialogs.openModal({
|
dialogs.openModal({
|
||||||
title: t("Change role to admin"),
|
title: t("Change role to admin"),
|
||||||
isCentered: true,
|
|
||||||
content: (
|
content: (
|
||||||
<UserChangeToAdminDialog
|
<UserChangeToAdminDialog
|
||||||
user={user}
|
user={user}
|
||||||
@@ -57,7 +56,6 @@ function UserMenu({ user }: Props) {
|
|||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
dialogs.openModal({
|
dialogs.openModal({
|
||||||
title: t("Change role to editor"),
|
title: t("Change role to editor"),
|
||||||
isCentered: true,
|
|
||||||
content: (
|
content: (
|
||||||
<UserChangeToMemberDialog
|
<UserChangeToMemberDialog
|
||||||
user={user}
|
user={user}
|
||||||
@@ -74,7 +72,6 @@ function UserMenu({ user }: Props) {
|
|||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
dialogs.openModal({
|
dialogs.openModal({
|
||||||
title: t("Change role to viewer"),
|
title: t("Change role to viewer"),
|
||||||
isCentered: true,
|
|
||||||
content: (
|
content: (
|
||||||
<UserChangeToViewerDialog
|
<UserChangeToViewerDialog
|
||||||
user={user}
|
user={user}
|
||||||
@@ -91,7 +88,6 @@ function UserMenu({ user }: Props) {
|
|||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
dialogs.openModal({
|
dialogs.openModal({
|
||||||
title: t("Change name"),
|
title: t("Change name"),
|
||||||
isCentered: true,
|
|
||||||
content: (
|
content: (
|
||||||
<UserChangeNameDialog user={user} onSubmit={dialogs.closeAllModals} />
|
<UserChangeNameDialog user={user} onSubmit={dialogs.closeAllModals} />
|
||||||
),
|
),
|
||||||
@@ -105,7 +101,6 @@ function UserMenu({ user }: Props) {
|
|||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
dialogs.openModal({
|
dialogs.openModal({
|
||||||
title: t("Suspend user"),
|
title: t("Suspend user"),
|
||||||
isCentered: true,
|
|
||||||
content: (
|
content: (
|
||||||
<UserSuspendDialog user={user} onSubmit={dialogs.closeAllModals} />
|
<UserSuspendDialog user={user} onSubmit={dialogs.closeAllModals} />
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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);
|
|
||||||
@@ -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));
|
|
||||||
@@ -208,7 +208,6 @@ class DocumentScene extends React.Component<Props> {
|
|||||||
if (abilities.move) {
|
if (abilities.move) {
|
||||||
dialogs.openModal({
|
dialogs.openModal({
|
||||||
title: t("Move document"),
|
title: t("Move document"),
|
||||||
isCentered: true,
|
|
||||||
content: <DocumentMove document={document} />,
|
content: <DocumentMove document={document} />,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -258,7 +257,6 @@ class DocumentScene extends React.Component<Props> {
|
|||||||
} else {
|
} else {
|
||||||
dialogs.openModal({
|
dialogs.openModal({
|
||||||
title: t("Publish document"),
|
title: t("Publish document"),
|
||||||
isCentered: true,
|
|
||||||
content: <DocumentPublish document={document} />,
|
content: <DocumentPublish document={document} />,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,7 +71,6 @@ function ApiKeys() {
|
|||||||
title={t("Create a token")}
|
title={t("Create a token")}
|
||||||
onRequestClose={handleNewModalClose}
|
onRequestClose={handleNewModalClose}
|
||||||
isOpen={newModalOpen}
|
isOpen={newModalOpen}
|
||||||
isCentered
|
|
||||||
>
|
>
|
||||||
<APITokenNew onSubmit={handleNewModalClose} />
|
<APITokenNew onSubmit={handleNewModalClose} />
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -113,7 +113,6 @@ function Details() {
|
|||||||
dialogs.openModal({
|
dialogs.openModal({
|
||||||
title: t("Delete workspace"),
|
title: t("Delete workspace"),
|
||||||
content: <TeamDelete onSubmit={dialogs.closeAllModals} />,
|
content: <TeamDelete onSubmit={dialogs.closeAllModals} />,
|
||||||
isCentered: true,
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ function Export() {
|
|||||||
|
|
||||||
dialogs.openModal({
|
dialogs.openModal({
|
||||||
title: t("Export data"),
|
title: t("Export data"),
|
||||||
isCentered: true,
|
|
||||||
content: <ExportDialog onSubmit={dialogs.closeAllModals} />,
|
content: <ExportDialog onSubmit={dialogs.closeAllModals} />,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -51,7 +51,6 @@ function Import() {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
dialogs.openModal({
|
dialogs.openModal({
|
||||||
title: t("Import data"),
|
title: t("Import data"),
|
||||||
isCentered: true,
|
|
||||||
content: <ImportMarkdownDialog />,
|
content: <ImportMarkdownDialog />,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -77,7 +76,6 @@ function Import() {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
dialogs.openModal({
|
dialogs.openModal({
|
||||||
title: t("Import data"),
|
title: t("Import data"),
|
||||||
isCentered: true,
|
|
||||||
content: <ImportJSONDialog />,
|
content: <ImportJSONDialog />,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -98,7 +96,6 @@ function Import() {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
dialogs.openModal({
|
dialogs.openModal({
|
||||||
title: t("Import data"),
|
title: t("Import data"),
|
||||||
isCentered: true,
|
|
||||||
content: <ImportNotionDialog />,
|
content: <ImportNotionDialog />,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -43,7 +43,6 @@ function Preferences() {
|
|||||||
dialogs.openModal({
|
dialogs.openModal({
|
||||||
title: t("Delete account"),
|
title: t("Delete account"),
|
||||||
content: <UserDelete onSubmit={dialogs.closeAllModals} />,
|
content: <UserDelete onSubmit={dialogs.closeAllModals} />,
|
||||||
isCentered: true,
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -102,7 +102,6 @@ function Security() {
|
|||||||
|
|
||||||
if (inviteRequired) {
|
if (inviteRequired) {
|
||||||
dialogs.openModal({
|
dialogs.openModal({
|
||||||
isCentered: true,
|
|
||||||
title: t("Are you sure you want to require invites?"),
|
title: t("Are you sure you want to require invites?"),
|
||||||
content: (
|
content: (
|
||||||
<ConfirmationDialog
|
<ConfirmationDialog
|
||||||
|
|||||||
@@ -76,7 +76,6 @@ const FileOperationListItem = ({ fileOperation }: Props) => {
|
|||||||
|
|
||||||
const handleConfirmDelete = React.useCallback(async () => {
|
const handleConfirmDelete = React.useCallback(async () => {
|
||||||
dialogs.openModal({
|
dialogs.openModal({
|
||||||
isCentered: true,
|
|
||||||
title: t("Are you sure you want to delete this import?"),
|
title: t("Are you sure you want to delete this import?"),
|
||||||
content: (
|
content: (
|
||||||
<ConfirmationDialog
|
<ConfirmationDialog
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ type DialogDefinition = {
|
|||||||
title: string;
|
title: string;
|
||||||
content: React.ReactNode;
|
content: React.ReactNode;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
isCentered?: boolean;
|
fullscreen?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class DialogsStore {
|
export default class DialogsStore {
|
||||||
@@ -45,11 +45,11 @@ export default class DialogsStore {
|
|||||||
openModal = ({
|
openModal = ({
|
||||||
title,
|
title,
|
||||||
content,
|
content,
|
||||||
isCentered,
|
fullscreen,
|
||||||
replace,
|
replace,
|
||||||
}: {
|
}: {
|
||||||
title: string;
|
title: string;
|
||||||
isCentered?: boolean;
|
fullscreen?: boolean;
|
||||||
content: React.ReactNode;
|
content: React.ReactNode;
|
||||||
replace?: boolean;
|
replace?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
@@ -64,8 +64,8 @@ export default class DialogsStore {
|
|||||||
this.modalStack.set(id, {
|
this.modalStack.set(id, {
|
||||||
title,
|
title,
|
||||||
content,
|
content,
|
||||||
|
fullscreen,
|
||||||
isOpen: true,
|
isOpen: true,
|
||||||
isCentered,
|
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
0
|
0
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ const WebhookSubscriptionListItem = ({ webhook }: Props) => {
|
|||||||
const showDeletionConfirmation = React.useCallback(() => {
|
const showDeletionConfirmation = React.useCallback(() => {
|
||||||
dialogs.openModal({
|
dialogs.openModal({
|
||||||
title: t("Delete webhook"),
|
title: t("Delete webhook"),
|
||||||
isCentered: true,
|
|
||||||
content: (
|
content: (
|
||||||
<WebhookSubscriptionRevokeDialog
|
<WebhookSubscriptionRevokeDialog
|
||||||
onSubmit={dialogs.closeAllModals}
|
onSubmit={dialogs.closeAllModals}
|
||||||
|
|||||||
@@ -114,6 +114,15 @@
|
|||||||
"previously edited": "previously edited",
|
"previously edited": "previously edited",
|
||||||
"You": "You",
|
"You": "You",
|
||||||
"Viewers": "Viewers",
|
"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",
|
"Collection deleted": "Collection deleted",
|
||||||
"I’m sure – Delete": "I’m sure – Delete",
|
"I’m sure – Delete": "I’m sure – Delete",
|
||||||
"Deleting": "Deleting",
|
"Deleting": "Deleting",
|
||||||
@@ -173,7 +182,6 @@
|
|||||||
"{{ completed }} task done_plural": "{{ completed }} tasks done",
|
"{{ completed }} task done_plural": "{{ completed }} tasks done",
|
||||||
"{{ completed }} of {{ total }} tasks": "{{ completed }} of {{ total }} tasks",
|
"{{ completed }} of {{ total }} tasks": "{{ completed }} of {{ total }} tasks",
|
||||||
"Template created, go ahead and customize it": "Template created, go ahead and customize it",
|
"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.",
|
"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 editing": "Currently editing",
|
||||||
"Currently viewing": "Currently viewing",
|
"Currently viewing": "Currently viewing",
|
||||||
@@ -214,16 +222,16 @@
|
|||||||
"{{ count }} member": "{{ count }} member",
|
"{{ count }} member": "{{ count }} member",
|
||||||
"{{ count }} member_plural": "{{ count }} members",
|
"{{ count }} member_plural": "{{ count }} members",
|
||||||
"Group members": "Group members",
|
"Group members": "Group members",
|
||||||
"Icon": "Icon",
|
|
||||||
"Show menu": "Show menu",
|
"Show menu": "Show menu",
|
||||||
"Choose icon": "Choose icon",
|
"Choose icon": "Choose icon",
|
||||||
"Loading": "Loading",
|
"Loading": "Loading",
|
||||||
"Select a color": "Select a color",
|
"Select a color": "Select a color",
|
||||||
"Search": "Search",
|
"Search": "Search",
|
||||||
"Default access": "Default access",
|
"Permission": "Permission",
|
||||||
"Can edit": "Can edit",
|
"Can edit": "Can edit",
|
||||||
"View only": "View only",
|
"View only": "View only",
|
||||||
"No access": "No access",
|
"No access": "No access",
|
||||||
|
"Default access": "Default access",
|
||||||
"Role": "Role",
|
"Role": "Role",
|
||||||
"Editor": "Editor",
|
"Editor": "Editor",
|
||||||
"Viewer": "Viewer",
|
"Viewer": "Viewer",
|
||||||
@@ -296,14 +304,12 @@
|
|||||||
"No results": "No results",
|
"No results": "No results",
|
||||||
"Previous page": "Previous page",
|
"Previous page": "Previous page",
|
||||||
"Next page": "Next 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 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?",
|
"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",
|
"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 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 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.",
|
"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",
|
"New name": "New name",
|
||||||
"Name can't be empty": "Name can't be empty",
|
"Name can't be empty": "Name can't be empty",
|
||||||
"Your import completed": "Your import completed",
|
"Your import completed": "Your import completed",
|
||||||
@@ -484,15 +490,6 @@
|
|||||||
"{{ usersCount }} users with access_plural": "{{ usersCount }} users with access",
|
"{{ usersCount }} users with access_plural": "{{ usersCount }} users with access",
|
||||||
"{{ groupsCount }} groups with access": "{{ groupsCount }} group with access",
|
"{{ groupsCount }} groups with access": "{{ groupsCount }} group with access",
|
||||||
"{{ groupsCount }} groups with access_plural": "{{ groupsCount }} groups 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",
|
"{{ groupName }} was added to the collection": "{{ groupName }} was added to the collection",
|
||||||
"Could not add user": "Could not add user",
|
"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?",
|
"Can’t find the group you’re looking for?": "Can’t find the group you’re looking for?",
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ export const buildLightTheme = (input: Partial<Colors>): DefaultTheme => {
|
|||||||
|
|
||||||
commentBackground: colors.warmGrey,
|
commentBackground: colors.warmGrey,
|
||||||
|
|
||||||
modalBackdrop: colors.black10,
|
modalBackdrop: "rgba(0, 0, 0, 0.15)",
|
||||||
modalBackground: colors.white,
|
modalBackground: colors.white,
|
||||||
modalShadow:
|
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%)",
|
"0 4px 8px rgb(0 0 0 / 8%), 0 2px 4px rgb(0 0 0 / 0%), 0 30px 40px rgb(0 0 0 / 8%)",
|
||||||
|
|||||||
Reference in New Issue
Block a user