mirror of
https://github.com/outline/outline.git
synced 2025-12-19 17:50:12 -06:00
InputSelectPermission component (#9585)
This commit is contained in:
@@ -14,7 +14,7 @@ import Collection from "~/models/Collection";
|
||||
import Button from "~/components/Button";
|
||||
import Flex from "~/components/Flex";
|
||||
import Input from "~/components/Input";
|
||||
import InputSelectPermission from "~/components/InputSelectPermission";
|
||||
import { InputSelectPermission } from "~/components/InputSelectPermission";
|
||||
import { createLazyComponent } from "~/components/LazyLoad";
|
||||
import Switch from "~/components/Switch";
|
||||
import Text from "~/components/Text";
|
||||
@@ -172,7 +172,7 @@ export const CollectionForm = observer(function CollectionForm_({
|
||||
) => {
|
||||
field.onChange(value === EmptySelectValue ? null : value);
|
||||
}}
|
||||
note={t(
|
||||
help={t(
|
||||
"The default access for workspace members, you can share with more users or groups later."
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import * as VisuallyHidden from "@radix-ui/react-visually-hidden";
|
||||
import { QuestionMarkIcon } from "outline-icons";
|
||||
import { transparentize } from "polished";
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import { s } from "@shared/styles";
|
||||
import Text from "~/components/Text";
|
||||
import useMobile from "~/hooks/useMobile";
|
||||
import Separator from "./ContextMenu/Separator";
|
||||
import Flex from "./Flex";
|
||||
import { LabelText } from "./Input";
|
||||
import NudeButton from "./NudeButton";
|
||||
import Scrollable from "./Scrollable";
|
||||
import { IconWrapper } from "./Sidebar/components/SidebarLink";
|
||||
import Tooltip from "./Tooltip";
|
||||
import {
|
||||
Drawer,
|
||||
DrawerContent,
|
||||
@@ -53,7 +57,7 @@ type Props = {
|
||||
/* Options to display in the select menu. */
|
||||
options: Option[];
|
||||
/* Current chosen value. */
|
||||
value?: string;
|
||||
value?: string | null;
|
||||
/* Callback when an option is selected. */
|
||||
onChange: (value: string) => void;
|
||||
/* ARIA label for accessibility. */
|
||||
@@ -66,231 +70,259 @@ type Props = {
|
||||
disabled?: boolean;
|
||||
/* When true, width of the menu trigger is restricted. Otherwise, takes up the full width of parent. */
|
||||
short?: boolean;
|
||||
/** Display a tooltip with the descriptive help text about the select menu. */
|
||||
help?: string;
|
||||
} & TriggerButtonProps;
|
||||
|
||||
export function InputSelectNew(props: Props) {
|
||||
const {
|
||||
options,
|
||||
value,
|
||||
onChange,
|
||||
ariaLabel,
|
||||
label,
|
||||
hideLabel,
|
||||
disabled,
|
||||
short,
|
||||
...triggerProps
|
||||
} = props;
|
||||
export const InputSelectNew = React.forwardRef<HTMLButtonElement, Props>(
|
||||
(props, ref) => {
|
||||
const {
|
||||
options,
|
||||
value,
|
||||
onChange,
|
||||
ariaLabel,
|
||||
label,
|
||||
hideLabel,
|
||||
short,
|
||||
help,
|
||||
...triggerProps
|
||||
} = props;
|
||||
|
||||
const [localValue, setLocalValue] = React.useState(value);
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [localValue, setLocalValue] = React.useState(value);
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
||||
const triggerRef =
|
||||
React.useRef<React.ElementRef<typeof InputSelectTrigger>>(null);
|
||||
const contentRef =
|
||||
React.useRef<React.ElementRef<typeof InputSelectContent>>(null);
|
||||
const contentRef =
|
||||
React.useRef<React.ElementRef<typeof InputSelectContent>>(null);
|
||||
|
||||
const isMobile = useMobile();
|
||||
const isMobile = useMobile();
|
||||
|
||||
const placeholder = `Select a ${ariaLabel.toLowerCase()}`;
|
||||
const optionsHaveIcon = options.some(
|
||||
(opt) => opt.type === "item" && !!opt.icon
|
||||
);
|
||||
const placeholder = `Select a ${ariaLabel.toLowerCase()}`;
|
||||
const optionsHaveIcon = options.some(
|
||||
(opt) => opt.type === "item" && !!opt.icon
|
||||
);
|
||||
|
||||
const renderOption = React.useCallback(
|
||||
(option: Option) => {
|
||||
if (option.type === "separator") {
|
||||
return <InputSelectSeparator />;
|
||||
const renderOption = React.useCallback(
|
||||
(option: Option) => {
|
||||
if (option.type === "separator") {
|
||||
return <InputSelectSeparator />;
|
||||
}
|
||||
|
||||
return (
|
||||
<InputSelectItem key={option.value} value={option.value}>
|
||||
<Option option={option} optionsHaveIcon={optionsHaveIcon} />
|
||||
</InputSelectItem>
|
||||
);
|
||||
},
|
||||
[optionsHaveIcon]
|
||||
);
|
||||
|
||||
const onValueChange = React.useCallback(
|
||||
async (val: string) => {
|
||||
setLocalValue(val);
|
||||
onChange(val);
|
||||
},
|
||||
[onChange, setLocalValue]
|
||||
);
|
||||
|
||||
const enablePointerEvents = React.useCallback(() => {
|
||||
if (contentRef.current) {
|
||||
contentRef.current.style.pointerEvents = "auto";
|
||||
}
|
||||
}, []);
|
||||
|
||||
const disablePointerEvents = React.useCallback(() => {
|
||||
if (contentRef.current) {
|
||||
contentRef.current.style.pointerEvents = "none";
|
||||
}
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
setLocalValue(value);
|
||||
}, [value]);
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<InputSelectItem key={option.value} value={option.value}>
|
||||
<Option option={option} optionsHaveIcon={optionsHaveIcon} />
|
||||
</InputSelectItem>
|
||||
<MobileSelect
|
||||
ref={ref}
|
||||
{...props}
|
||||
value={localValue}
|
||||
onChange={onValueChange}
|
||||
placeholder={placeholder}
|
||||
optionsHaveIcon={optionsHaveIcon}
|
||||
/>
|
||||
);
|
||||
},
|
||||
[optionsHaveIcon]
|
||||
);
|
||||
|
||||
const onValueChange = React.useCallback(
|
||||
async (val: string) => {
|
||||
setLocalValue(val);
|
||||
onChange(val);
|
||||
},
|
||||
[onChange, setLocalValue]
|
||||
);
|
||||
|
||||
const enablePointerEvents = React.useCallback(() => {
|
||||
if (contentRef.current) {
|
||||
contentRef.current.style.pointerEvents = "auto";
|
||||
}
|
||||
}, []);
|
||||
|
||||
const disablePointerEvents = React.useCallback(() => {
|
||||
if (contentRef.current) {
|
||||
contentRef.current.style.pointerEvents = "none";
|
||||
}
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
setLocalValue(value);
|
||||
}, [value]);
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<MobileSelect
|
||||
{...props}
|
||||
value={localValue}
|
||||
onChange={onValueChange}
|
||||
placeholder={placeholder}
|
||||
optionsHaveIcon={optionsHaveIcon}
|
||||
/>
|
||||
<Wrapper short={short}>
|
||||
<Label text={label} hidden={hideLabel ?? false} help={help} />
|
||||
<InputSelectRoot
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
value={localValue ?? undefined}
|
||||
onValueChange={onValueChange}
|
||||
>
|
||||
<InputSelectTrigger
|
||||
ref={ref}
|
||||
placeholder={placeholder}
|
||||
{...triggerProps}
|
||||
/>
|
||||
<InputSelectContent
|
||||
ref={contentRef}
|
||||
aria-label={ariaLabel}
|
||||
onAnimationStart={disablePointerEvents}
|
||||
onAnimationEnd={enablePointerEvents}
|
||||
>
|
||||
{options.map(renderOption)}
|
||||
</InputSelectContent>
|
||||
</InputSelectRoot>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Wrapper short={short}>
|
||||
<Label text={label} hidden={hideLabel ?? false} />
|
||||
<InputSelectRoot
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
value={localValue}
|
||||
onValueChange={onValueChange}
|
||||
>
|
||||
<InputSelectTrigger
|
||||
ref={triggerRef}
|
||||
placeholder={placeholder}
|
||||
{...triggerProps}
|
||||
/>
|
||||
<InputSelectContent
|
||||
ref={contentRef}
|
||||
aria-label={ariaLabel}
|
||||
onAnimationStart={disablePointerEvents}
|
||||
onAnimationEnd={enablePointerEvents}
|
||||
>
|
||||
{options.map(renderOption)}
|
||||
</InputSelectContent>
|
||||
</InputSelectRoot>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
);
|
||||
InputSelectNew.displayName = "InputSelect";
|
||||
|
||||
type MobileSelectProps = Props & {
|
||||
placeholder: string;
|
||||
optionsHaveIcon: boolean;
|
||||
};
|
||||
|
||||
function MobileSelect(props: MobileSelectProps) {
|
||||
const {
|
||||
options,
|
||||
value,
|
||||
onChange,
|
||||
ariaLabel,
|
||||
label,
|
||||
hideLabel,
|
||||
disabled,
|
||||
short,
|
||||
placeholder,
|
||||
optionsHaveIcon,
|
||||
...triggerProps
|
||||
} = props;
|
||||
const MobileSelect = React.forwardRef<HTMLButtonElement, MobileSelectProps>(
|
||||
(props, ref) => {
|
||||
const {
|
||||
options,
|
||||
value,
|
||||
onChange,
|
||||
ariaLabel,
|
||||
label,
|
||||
hideLabel,
|
||||
disabled,
|
||||
short,
|
||||
placeholder,
|
||||
optionsHaveIcon,
|
||||
...triggerProps
|
||||
} = props;
|
||||
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const contentRef = React.useRef<React.ElementRef<typeof DrawerContent>>(null);
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const contentRef =
|
||||
React.useRef<React.ElementRef<typeof DrawerContent>>(null);
|
||||
|
||||
const selectedOption = React.useMemo(
|
||||
() =>
|
||||
value
|
||||
? options.find((opt) => opt.type === "item" && opt.value === value)
|
||||
: undefined,
|
||||
[value, options]
|
||||
);
|
||||
const selectedOption = React.useMemo(
|
||||
() =>
|
||||
value
|
||||
? options.find((opt) => opt.type === "item" && opt.value === value)
|
||||
: undefined,
|
||||
[value, options]
|
||||
);
|
||||
|
||||
const handleSelect = React.useCallback(
|
||||
async (val: string) => {
|
||||
setOpen(false);
|
||||
onChange(val);
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
const handleSelect = React.useCallback(
|
||||
async (val: string) => {
|
||||
setOpen(false);
|
||||
onChange(val);
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
|
||||
const renderOption = React.useCallback(
|
||||
(option: Option) => {
|
||||
if (option.type === "separator") {
|
||||
return <Separator />;
|
||||
}
|
||||
const renderOption = React.useCallback(
|
||||
(option: Option) => {
|
||||
if (option.type === "separator") {
|
||||
return <Separator />;
|
||||
}
|
||||
|
||||
const isSelected = option === selectedOption;
|
||||
const isSelected = option === selectedOption;
|
||||
|
||||
return (
|
||||
<SelectItemWrapper
|
||||
key={option.value}
|
||||
onClick={() => handleSelect(option.value)}
|
||||
data-state={isSelected ? "checked" : "unchecked"}
|
||||
>
|
||||
<Option option={option} optionsHaveIcon={optionsHaveIcon} />
|
||||
{isSelected && <SelectItemIndicator />}
|
||||
</SelectItemWrapper>
|
||||
);
|
||||
},
|
||||
[handleSelect, selectedOption, optionsHaveIcon]
|
||||
);
|
||||
|
||||
const enablePointerEvents = React.useCallback(() => {
|
||||
if (contentRef.current) {
|
||||
contentRef.current.style.pointerEvents = "auto";
|
||||
}
|
||||
}, []);
|
||||
|
||||
const disablePointerEvents = React.useCallback(() => {
|
||||
if (contentRef.current) {
|
||||
contentRef.current.style.pointerEvents = "none";
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<Label text={label} hidden={hideLabel ?? false} />
|
||||
<Drawer open={open} onOpenChange={setOpen}>
|
||||
<DrawerTrigger asChild>
|
||||
<SelectButton
|
||||
{...triggerProps}
|
||||
neutral
|
||||
disclosure
|
||||
data-placeholder={selectedOption ? false : ""}
|
||||
return (
|
||||
<SelectItemWrapper
|
||||
key={option.value}
|
||||
onClick={() => handleSelect(option.value)}
|
||||
data-state={isSelected ? "checked" : "unchecked"}
|
||||
>
|
||||
{selectedOption ? (
|
||||
<Option
|
||||
option={selectedOption as Item}
|
||||
optionsHaveIcon={optionsHaveIcon}
|
||||
/>
|
||||
) : (
|
||||
<>{placeholder}</>
|
||||
)}
|
||||
</SelectButton>
|
||||
</DrawerTrigger>
|
||||
<DrawerContent
|
||||
ref={contentRef}
|
||||
aria-label={ariaLabel}
|
||||
onAnimationStart={disablePointerEvents}
|
||||
onAnimationEnd={enablePointerEvents}
|
||||
>
|
||||
<DrawerTitle hidden={!label}>{label ?? ariaLabel}</DrawerTitle>
|
||||
<StyledScrollable hiddenScrollbars>
|
||||
{options.map(renderOption)}
|
||||
</StyledScrollable>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
<Option option={option} optionsHaveIcon={optionsHaveIcon} />
|
||||
{isSelected && <SelectItemIndicator />}
|
||||
</SelectItemWrapper>
|
||||
);
|
||||
},
|
||||
[handleSelect, selectedOption, optionsHaveIcon]
|
||||
);
|
||||
|
||||
function Label({ text, hidden }: { text: string; hidden: boolean }) {
|
||||
const labelText = <LabelText>{text}</LabelText>;
|
||||
const enablePointerEvents = React.useCallback(() => {
|
||||
if (contentRef.current) {
|
||||
contentRef.current.style.pointerEvents = "auto";
|
||||
}
|
||||
}, []);
|
||||
|
||||
const disablePointerEvents = React.useCallback(() => {
|
||||
if (contentRef.current) {
|
||||
contentRef.current.style.pointerEvents = "none";
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<Label text={label} hidden={hideLabel ?? false} />
|
||||
<Drawer open={open} onOpenChange={setOpen}>
|
||||
<DrawerTrigger asChild>
|
||||
<SelectButton
|
||||
ref={ref}
|
||||
{...triggerProps}
|
||||
neutral
|
||||
disclosure
|
||||
data-placeholder={selectedOption ? false : ""}
|
||||
>
|
||||
{selectedOption ? (
|
||||
<Option
|
||||
option={selectedOption as Item}
|
||||
optionsHaveIcon={optionsHaveIcon}
|
||||
/>
|
||||
) : (
|
||||
<>{placeholder}</>
|
||||
)}
|
||||
</SelectButton>
|
||||
</DrawerTrigger>
|
||||
<DrawerContent
|
||||
ref={contentRef}
|
||||
aria-label={ariaLabel}
|
||||
onAnimationStart={disablePointerEvents}
|
||||
onAnimationEnd={enablePointerEvents}
|
||||
>
|
||||
<DrawerTitle hidden={!label}>{label ?? ariaLabel}</DrawerTitle>
|
||||
<StyledScrollable hiddenScrollbars>
|
||||
{options.map(renderOption)}
|
||||
</StyledScrollable>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
);
|
||||
MobileSelect.displayName = "InputSelect";
|
||||
|
||||
function Label({
|
||||
text,
|
||||
hidden,
|
||||
help,
|
||||
}: {
|
||||
text: string;
|
||||
hidden: boolean;
|
||||
help?: string;
|
||||
}) {
|
||||
const content = (
|
||||
<Flex align="center" gap={2} style={{ marginBottom: "4px" }}>
|
||||
<LabelText style={{ paddingBottom: 0 }}>{text}</LabelText>
|
||||
{help ? (
|
||||
<Tooltip content={help}>
|
||||
<TooltipButton size={18}>
|
||||
<QuestionMarkIcon size={18} />
|
||||
</TooltipButton>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</Flex>
|
||||
);
|
||||
|
||||
return hidden ? (
|
||||
<VisuallyHidden.Root>{labelText}</VisuallyHidden.Root>
|
||||
<VisuallyHidden.Root>{content}</VisuallyHidden.Root>
|
||||
) : (
|
||||
labelText
|
||||
content
|
||||
);
|
||||
}
|
||||
|
||||
@@ -352,3 +384,12 @@ const IconSpacer = styled.div`
|
||||
const StyledScrollable = styled(Scrollable)`
|
||||
max-height: 75vh;
|
||||
`;
|
||||
|
||||
const TooltipButton = styled(NudeButton)`
|
||||
color: ${s("textSecondary")};
|
||||
|
||||
&:hover,
|
||||
&[aria-expanded="true"] {
|
||||
background: none !important;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -1,54 +1,67 @@
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import styled from "styled-components";
|
||||
import { $Diff } from "utility-types";
|
||||
import { s } from "@shared/styles";
|
||||
import { CollectionPermission } from "@shared/types";
|
||||
import {
|
||||
InputSelectNew,
|
||||
Option as OptionNew,
|
||||
} from "~/components/InputSelectNew";
|
||||
import { EmptySelectValue } from "~/types";
|
||||
import InputSelect, { Props, Option, InputSelectRef } from "./InputSelect";
|
||||
|
||||
function InputSelectPermission(
|
||||
props: $Diff<
|
||||
Props,
|
||||
{
|
||||
options: Array<Option>;
|
||||
ariaLabel: string;
|
||||
}
|
||||
>,
|
||||
ref: React.RefObject<InputSelectRef>
|
||||
) {
|
||||
const { value, onChange, ...rest } = props;
|
||||
const { t } = useTranslation();
|
||||
type Props = {
|
||||
shrink?: boolean;
|
||||
} & Pick<
|
||||
React.ComponentProps<typeof InputSelectNew>,
|
||||
"value" | "onChange" | "disabled" | "hideLabel" | "nude" | "help"
|
||||
>;
|
||||
|
||||
return (
|
||||
<Select
|
||||
ref={ref}
|
||||
label={t("Permission")}
|
||||
options={[
|
||||
export const InputSelectPermission = React.forwardRef<HTMLButtonElement, Props>(
|
||||
(props, ref) => {
|
||||
const { value, onChange, shrink, ...rest } = props;
|
||||
const { t } = useTranslation();
|
||||
|
||||
const options = React.useMemo<OptionNew[]>(
|
||||
() => [
|
||||
{
|
||||
type: "item",
|
||||
label: t("View only"),
|
||||
value: CollectionPermission.Read,
|
||||
},
|
||||
{
|
||||
type: "item",
|
||||
label: t("Can edit"),
|
||||
value: CollectionPermission.ReadWrite,
|
||||
},
|
||||
{
|
||||
divider: true,
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
type: "item",
|
||||
label: t("No access"),
|
||||
value: EmptySelectValue,
|
||||
},
|
||||
]}
|
||||
ariaLabel={t("Default access")}
|
||||
value={value || EmptySelectValue}
|
||||
onChange={onChange}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}
|
||||
],
|
||||
[t]
|
||||
);
|
||||
|
||||
const Select = styled(InputSelect)`
|
||||
return (
|
||||
<Select
|
||||
ref={ref}
|
||||
options={options}
|
||||
value={value || EmptySelectValue}
|
||||
onChange={onChange}
|
||||
ariaLabel={t("Default access")}
|
||||
label={t("Permission")}
|
||||
$shrink={shrink}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
InputSelectPermission.displayName = "InputSelectPermission";
|
||||
|
||||
const Select = styled(InputSelectNew)<{ $shrink?: boolean }>`
|
||||
color: ${s("textSecondary")};
|
||||
${({ $shrink }) => !$shrink && "margin-bottom: 16px;"}
|
||||
`;
|
||||
|
||||
export default React.forwardRef(InputSelectPermission);
|
||||
|
||||
@@ -9,7 +9,7 @@ import { CollectionPermission } from "@shared/types";
|
||||
import Collection from "~/models/Collection";
|
||||
import { Avatar, GroupAvatar, AvatarSize } from "~/components/Avatar";
|
||||
import InputMemberPermissionSelect from "~/components/InputMemberPermissionSelect";
|
||||
import InputSelectPermission from "~/components/InputSelectPermission";
|
||||
import { InputSelectPermission } from "~/components/InputSelectPermission";
|
||||
import Scrollable from "~/components/Scrollable";
|
||||
import useMaxHeight from "~/hooks/useMaxHeight";
|
||||
import usePolicy from "~/hooks/usePolicy";
|
||||
@@ -121,7 +121,6 @@ export const AccessControlList = observer(
|
||||
actions={
|
||||
<div style={{ marginRight: -8 }}>
|
||||
<InputSelectPermission
|
||||
style={{ margin: 0 }}
|
||||
onChange={(
|
||||
value: CollectionPermission | typeof EmptySelectValue
|
||||
) => {
|
||||
@@ -131,8 +130,9 @@ export const AccessControlList = observer(
|
||||
}}
|
||||
disabled={!can.update}
|
||||
value={collection?.permission}
|
||||
labelHidden
|
||||
hideLabel
|
||||
nude
|
||||
shrink
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -30,11 +30,11 @@ const InputSelectTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof InputSelectPrimitive.Trigger>,
|
||||
InputSelectTriggerProps
|
||||
>((props, ref) => {
|
||||
const { placeholder, children, ...buttonProps } = props;
|
||||
const { placeholder, children, nude, ...buttonProps } = props;
|
||||
|
||||
return (
|
||||
<InputSelectPrimitive.Trigger ref={ref} asChild>
|
||||
<SelectButton neutral disclosure {...buttonProps}>
|
||||
<SelectButton neutral disclosure $nude={nude} {...buttonProps}>
|
||||
<InputSelectPrimitive.Value placeholder={placeholder} />
|
||||
</SelectButton>
|
||||
</InputSelectPrimitive.Trigger>
|
||||
|
||||
@@ -10,7 +10,7 @@ import { AttachmentPreset, CollectionPermission } from "@shared/types";
|
||||
import { bytesToHumanReadable } from "@shared/utils/files";
|
||||
import Button from "~/components/Button";
|
||||
import Flex from "~/components/Flex";
|
||||
import InputSelectPermission from "~/components/InputSelectPermission";
|
||||
import { InputSelectPermission } from "~/components/InputSelectPermission";
|
||||
import LoadingIndicator from "~/components/LoadingIndicator";
|
||||
import Text from "~/components/Text";
|
||||
import useStores from "~/hooks/useStores";
|
||||
|
||||
@@ -5,7 +5,7 @@ import { ImportInput } from "@shared/schema";
|
||||
import { CollectionPermission, IntegrationService } from "@shared/types";
|
||||
import Button from "~/components/Button";
|
||||
import Flex from "~/components/Flex";
|
||||
import InputSelectPermission from "~/components/InputSelectPermission";
|
||||
import { InputSelectPermission } from "~/components/InputSelectPermission";
|
||||
import Text from "~/components/Text";
|
||||
import useBoolean from "~/hooks/useBoolean";
|
||||
import useStores from "~/hooks/useStores";
|
||||
|
||||
@@ -289,11 +289,11 @@
|
||||
"Flags": "Flags",
|
||||
"Select a color": "Select a color",
|
||||
"Loading": "Loading",
|
||||
"Permission": "Permission",
|
||||
"View only": "View only",
|
||||
"Can edit": "Can edit",
|
||||
"No access": "No access",
|
||||
"Default access": "Default access",
|
||||
"Permission": "Permission",
|
||||
"Change Language": "Change Language",
|
||||
"Dismiss": "Dismiss",
|
||||
"You’re offline.": "You’re offline.",
|
||||
|
||||
Reference in New Issue
Block a user