feat(dashboard-v2): add sharing features, pagination and mobile view for /files page
This commit is contained in:
parent
c325865faa
commit
ea11c3de48
|
@ -1,29 +1,40 @@
|
||||||
import * as React from "react";
|
import { useState } from "react";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { useMedia } from "react-use";
|
import { useMedia } from "react-use";
|
||||||
|
|
||||||
import theme from "../../lib/theme";
|
import theme from "../../lib/theme";
|
||||||
|
|
||||||
import { ContainerLoadingIndicator } from "../LoadingIndicator";
|
import { ContainerLoadingIndicator } from "../LoadingIndicator";
|
||||||
import { Select, SelectOption } from "../Select";
|
|
||||||
import { Switch } from "../Switch";
|
|
||||||
import { TextInputIcon } from "../TextInputIcon/TextInputIcon";
|
|
||||||
import { SearchIcon } from "../Icons";
|
|
||||||
|
|
||||||
import FileTable from "./FileTable";
|
import FileTable from "./FileTable";
|
||||||
import useFormattedFilesData from "./useFormattedFilesData";
|
import useFormattedFilesData from "./useFormattedFilesData";
|
||||||
|
import { MobileFileList } from "./MobileFileList";
|
||||||
|
import { Pagination } from "./Pagination";
|
||||||
|
|
||||||
|
const PAGE_SIZE = 10;
|
||||||
|
|
||||||
const FileList = ({ type }) => {
|
const FileList = ({ type }) => {
|
||||||
const isMediumScreenOrLarger = useMedia(`(min-width: ${theme.screens.md})`);
|
const isMediumScreenOrLarger = useMedia(`(min-width: ${theme.screens.md})`);
|
||||||
const { data, error } = useSWR(`user/${type}?pageSize=10`);
|
const [offset, setOffset] = useState(0);
|
||||||
|
const baseUrl = `user/${type}?pageSize=${PAGE_SIZE}`;
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
error,
|
||||||
|
mutate: refreshList,
|
||||||
|
} = useSWR(`${baseUrl}&offset=${offset}`, {
|
||||||
|
revalidateOnMount: true,
|
||||||
|
});
|
||||||
const items = useFormattedFilesData(data?.items || []);
|
const items = useFormattedFilesData(data?.items || []);
|
||||||
|
const count = data?.count || 0;
|
||||||
|
|
||||||
const setFilter = (name, value) => console.log("filter", name, "set to", value);
|
// Next page preloading
|
||||||
|
const hasMoreRecords = data ? data.offset + data.pageSize < data.count : false;
|
||||||
|
const nextPageOffset = hasMoreRecords ? data.offset + data.pageSize : offset;
|
||||||
|
useSWR(`${baseUrl}&offset=${nextPageOffset}`);
|
||||||
|
|
||||||
if (!items.length) {
|
if (!items.length) {
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full h-full justify-center items-center text-palette-400">
|
<div className="flex w-full h-full justify-center items-center text-palette-400">
|
||||||
{/* TODO: proper error message */}
|
|
||||||
{!data && !error && <ContainerLoadingIndicator />}
|
{!data && !error && <ContainerLoadingIndicator />}
|
||||||
{!data && error && <p>An error occurred while loading this data.</p>}
|
{!data && error && <p>An error occurred while loading this data.</p>}
|
||||||
{data && <p>No {type} found.</p>}
|
{data && <p>No {type} found.</p>}
|
||||||
|
@ -32,42 +43,14 @@ const FileList = ({ type }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4 pt-4">
|
<>
|
||||||
<div className="flex flex-col gap-4 lg:flex-row justify-between items-center">
|
{isMediumScreenOrLarger ? (
|
||||||
<TextInputIcon
|
<FileTable onUpdated={refreshList} items={items} />
|
||||||
className="w-full lg:w-[280px] xl:w-[420px]"
|
) : (
|
||||||
placeholder="Search"
|
<MobileFileList items={items} onUpdated={refreshList} />
|
||||||
icon={<SearchIcon size={20} />}
|
)}
|
||||||
onChange={console.log.bind(console)}
|
<Pagination count={count} offset={offset} setOffset={setOffset} pageSize={PAGE_SIZE} />
|
||||||
/>
|
</>
|
||||||
<div className="flex flex-row items-center uppercase font-light text-sm gap-4">
|
|
||||||
<Switch onChange={(value) => setFilter("showSmallFiles", value)} className="mr-8">
|
|
||||||
<span className="underline decoration-dashed underline-offset-2 decoration-2 decoration-gray-300">
|
|
||||||
Show small files
|
|
||||||
</span>
|
|
||||||
</Switch>
|
|
||||||
<div>
|
|
||||||
<span className="pr-2">File type:</span>
|
|
||||||
<Select onChange={(value) => setFilter("type", value)}>
|
|
||||||
<SelectOption value="all" label="All" />
|
|
||||||
<SelectOption value="mp4" label=".mp4" />
|
|
||||||
<SelectOption value="pdf" label=".pdf" />
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span className="pr-2">Sort:</span>
|
|
||||||
<Select onChange={(value) => setFilter("type", value)}>
|
|
||||||
<SelectOption value="size-desc" label="Biggest size" />
|
|
||||||
<SelectOption value="size-asc" label="Smallest size" />
|
|
||||||
<SelectOption value="date-desc" label="Latest" />
|
|
||||||
<SelectOption value="date-asc" label="Oldest" />
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* TODO: mobile view (it's not tabular) */}
|
|
||||||
{isMediumScreenOrLarger ? <FileTable items={items} /> : "Mobile view"}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -2,72 +2,47 @@ import { CogIcon, ShareIcon } from "../Icons";
|
||||||
import { PopoverMenu } from "../PopoverMenu/PopoverMenu";
|
import { PopoverMenu } from "../PopoverMenu/PopoverMenu";
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeadCell, TableRow } from "../Table";
|
import { Table, TableBody, TableCell, TableHead, TableHeadCell, TableRow } from "../Table";
|
||||||
import { CopyButton } from "../CopyButton";
|
import { CopyButton } from "../CopyButton";
|
||||||
|
import { useSkylinkOptions } from "./useSkylinkOptions";
|
||||||
|
import { useSkylinkSharing } from "./useSkylinkSharing";
|
||||||
|
|
||||||
const buildShareMenu = (item) => {
|
const SkylinkOptionsMenu = ({ skylink, onUpdated }) => {
|
||||||
return [
|
const { inProgress, options } = useSkylinkOptions({ skylink, onUpdated });
|
||||||
{
|
|
||||||
label: "Facebook",
|
|
||||||
callback: () => {
|
|
||||||
console.info("share to Facebook", item);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Twitter",
|
|
||||||
callback: () => {
|
|
||||||
console.info("share to Twitter", item);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Discord",
|
|
||||||
callback: () => {
|
|
||||||
console.info("share to Discord", item);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
const buildOptionsMenu = (item) => {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
label: "Preview",
|
|
||||||
callback: () => {
|
|
||||||
console.info("preview", item);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Download",
|
|
||||||
callback: () => {
|
|
||||||
console.info("download", item);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Unpin",
|
|
||||||
callback: () => {
|
|
||||||
console.info("unpin", item);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Report",
|
|
||||||
callback: () => {
|
|
||||||
console.info("report", item);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function FileTable({ items }) {
|
|
||||||
return (
|
return (
|
||||||
|
<PopoverMenu inProgress={inProgress} options={options} openClassName="text-primary">
|
||||||
|
<button aria-label="Manage this skylink">
|
||||||
|
<CogIcon />
|
||||||
|
</button>
|
||||||
|
</PopoverMenu>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const SkylinkSharingMenu = ({ skylink }) => {
|
||||||
|
const { options } = useSkylinkSharing(skylink);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PopoverMenu options={options} openClassName="text-primary">
|
||||||
|
<button aria-label="Share this skylink">
|
||||||
|
<ShareIcon size={22} />
|
||||||
|
</button>
|
||||||
|
</PopoverMenu>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function FileTable({ items, onUpdated }) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
<Table style={{ tableLayout: "fixed" }}>
|
<Table style={{ tableLayout: "fixed" }}>
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<TableRow noHoverEffect>
|
<TableRow noHoverEffect>
|
||||||
<TableHeadCell className="w-[240px] xl:w-[360px]">Name</TableHeadCell>
|
<TableHeadCell className="w-[180px] xl:w-[360px]">Name</TableHeadCell>
|
||||||
<TableHeadCell className="w-[80px]">Type</TableHeadCell>
|
<TableHeadCell className="w-[80px]">Type</TableHeadCell>
|
||||||
<TableHeadCell className="w-[80px]" align="right">
|
<TableHeadCell className="w-[100px]" align="right">
|
||||||
Size
|
Size
|
||||||
</TableHeadCell>
|
</TableHeadCell>
|
||||||
<TableHeadCell className="w-[180px]">Uploaded</TableHeadCell>
|
<TableHeadCell className="w-[160px]">Uploaded</TableHeadCell>
|
||||||
<TableHeadCell className="hidden lg:table-cell">Skylink</TableHeadCell>
|
<TableHeadCell className="hidden lg:table-cell">Skylink</TableHeadCell>
|
||||||
<TableHeadCell className="w-[100px]">Activity</TableHeadCell>
|
<TableHeadCell className="w-[90px]">Activity</TableHeadCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
|
@ -76,30 +51,22 @@ export default function FileTable({ items }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRow key={id}>
|
<TableRow key={id}>
|
||||||
<TableCell className="w-[240px] xl:w-[360px]">{name}</TableCell>
|
<TableCell className="w-[180px] xl:w-[360px]">{name}</TableCell>
|
||||||
<TableCell className="w-[80px]">{type}</TableCell>
|
<TableCell className="w-[80px]">{type}</TableCell>
|
||||||
<TableCell className="w-[80px]" align="right">
|
<TableCell className="w-[100px]" align="right">
|
||||||
{size}
|
{size}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="w-[180px]">{date}</TableCell>
|
<TableCell className="w-[160px]">{date}</TableCell>
|
||||||
<TableCell className="hidden lg:table-cell pr-6 !overflow-visible">
|
<TableCell className="hidden lg:table-cell pr-6 !overflow-visible">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<CopyButton value={skylink} className="mr-2" aria-label="Copy skylink" />
|
<CopyButton value={skylink} className="mr-2" aria-label="Copy skylink" />
|
||||||
<span className="w-full inline-block truncate">{skylink}</span>
|
<span className="w-full inline-block truncate">{skylink}</span>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="w-[100px] !overflow-visible">
|
<TableCell className="w-[90px] !overflow-visible">
|
||||||
<div className="flex text-palette-600 gap-4">
|
<div className="flex text-palette-600 gap-4">
|
||||||
<PopoverMenu options={buildShareMenu(item)} openClassName="text-primary">
|
<SkylinkOptionsMenu skylink={skylink} onUpdated={onUpdated} />
|
||||||
<button aria-label="Share this skylink">
|
<SkylinkSharingMenu skylink={skylink} />
|
||||||
<ShareIcon size={22} />
|
|
||||||
</button>
|
|
||||||
</PopoverMenu>
|
|
||||||
<PopoverMenu options={buildOptionsMenu(item)} openClassName="text-primary">
|
|
||||||
<button aria-label="Manage this skylink">
|
|
||||||
<CogIcon />
|
|
||||||
</button>
|
|
||||||
</PopoverMenu>
|
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
@ -107,5 +74,6 @@ export default function FileTable({ items }) {
|
||||||
})}
|
})}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,84 @@
|
||||||
|
import { useState } from "react";
|
||||||
|
import cn from "classnames";
|
||||||
|
|
||||||
|
import { ChevronDownIcon } from "../Icons";
|
||||||
|
import { useSkylinkSharing } from "./useSkylinkSharing";
|
||||||
|
import { ContainerLoadingIndicator } from "../LoadingIndicator";
|
||||||
|
import { useSkylinkOptions } from "./useSkylinkOptions";
|
||||||
|
|
||||||
|
const SharingMenu = ({ skylink }) => {
|
||||||
|
const { options } = useSkylinkSharing(skylink);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-4 bg-white px-4 py-6 w-1/2">
|
||||||
|
{options.map(({ label, callback }, index) => (
|
||||||
|
<button key={index} className="uppercase text-left" onClick={callback}>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const OptionsMenu = ({ skylink, onUpdated }) => {
|
||||||
|
const { inProgress, options } = useSkylinkOptions({ skylink, onUpdated });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn("relative px-4 py-6 w-1/2", { "bg-primary/10": !inProgress })}>
|
||||||
|
<div className={cn("flex flex-col gap-4", { "opacity-0": inProgress })}>
|
||||||
|
{options.map(({ label, callback }, index) => (
|
||||||
|
<button key={index} className="uppercase text-left" onClick={callback}>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{inProgress && (
|
||||||
|
<ContainerLoadingIndicator className="absolute inset-0 !p-0 z-50 bg-primary/10 !text-palette-200" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ListItem = ({ item, onUpdated }) => {
|
||||||
|
const { name, type, size, date, skylink } = item;
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
const toggle = () => setOpen((open) => !open);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn("p-4 flex flex-col bg-palette-100", { "bg-opacity-50": !open })}>
|
||||||
|
<div className="flex items-center gap-4 justify-between">
|
||||||
|
<div className="info flex flex-col gap-2 truncate">
|
||||||
|
<div className="truncate">{name}</div>
|
||||||
|
<div className="flex divide-x divide-palette-200 text-xs">
|
||||||
|
<span className="px-1">{type}</span>
|
||||||
|
<span className="px-1">{size}</span>
|
||||||
|
<span className="px-1">{date}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button onClick={toggle}>
|
||||||
|
<ChevronDownIcon className={cn("transition-[transform]", { "-scale-100": open })} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex transition-[max-height_padding] overflow-hidden text-xs text-left font-sans tracking-normal",
|
||||||
|
{ "pt-4 max-h-[150px]": open, "pt-0 max-h-0": !open }
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<SharingMenu skylink={skylink} />
|
||||||
|
<OptionsMenu skylink={skylink} onUpdated={onUpdated} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MobileFileList = ({ items, onUpdated }) => {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
{items.map((item) => (
|
||||||
|
<ListItem key={item.id} item={item} onUpdated={onUpdated} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { Button } from "../Button";
|
||||||
|
|
||||||
|
export const Pagination = ({ count, offset, setOffset, pageSize }) => {
|
||||||
|
const start = count ? offset + 1 : 0;
|
||||||
|
const end = offset + pageSize > count ? count : offset + pageSize;
|
||||||
|
|
||||||
|
const showPaginationButtons = offset > 0 || count > end;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav className="px-4 py-3 flex items-center justify-between" aria-label="Pagination">
|
||||||
|
<div className="hidden sm:block">
|
||||||
|
<p className="text-sm text-gray-700">
|
||||||
|
Showing {start} to {end} of {count} results
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{showPaginationButtons && (
|
||||||
|
<div className="flex-1 flex justify-between sm:justify-end space-x-3">
|
||||||
|
<Button disabled={offset - pageSize < 0} onClick={() => setOffset(offset - pageSize)} className="!border-0">
|
||||||
|
Previous page
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
disabled={offset + pageSize >= count}
|
||||||
|
onClick={() => setOffset(offset + pageSize)}
|
||||||
|
className="!border-0"
|
||||||
|
>
|
||||||
|
Next page
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { useMemo, useState } from "react";
|
||||||
|
|
||||||
|
import accountsService from "../../services/accountsService";
|
||||||
|
import skynetClient from "../../services/skynetClient";
|
||||||
|
|
||||||
|
export const useSkylinkOptions = ({ skylink, onUpdated }) => {
|
||||||
|
const [inProgress, setInProgress] = useState(false);
|
||||||
|
|
||||||
|
const options = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
label: "Preview",
|
||||||
|
callback: async () => window.open(await skynetClient.getSkylinkUrl(skylink)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Download",
|
||||||
|
callback: () => skynetClient.downloadFile(skylink),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Unpin",
|
||||||
|
callback: async () => {
|
||||||
|
setInProgress(true);
|
||||||
|
await accountsService.delete(`user/uploads/${skylink}`);
|
||||||
|
await onUpdated(); // No need to setInProgress(false), since at this point this hook should already be unmounted
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[skylink, onUpdated]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
inProgress,
|
||||||
|
options,
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import copy from "copy-text-to-clipboard";
|
||||||
|
|
||||||
|
import skynetClient from "../../services/skynetClient";
|
||||||
|
|
||||||
|
const COPY_LINK_LABEL = "Copy link";
|
||||||
|
|
||||||
|
export const useSkylinkSharing = (skylink) => {
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
const [copyLabel, setCopyLabel] = useState(COPY_LINK_LABEL);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (copied) {
|
||||||
|
setCopyLabel("Copied!");
|
||||||
|
|
||||||
|
const timeout = setTimeout(() => setCopied(false), 1500);
|
||||||
|
|
||||||
|
return () => clearTimeout(timeout);
|
||||||
|
} else {
|
||||||
|
setCopyLabel(COPY_LINK_LABEL);
|
||||||
|
}
|
||||||
|
}, [copied]);
|
||||||
|
|
||||||
|
const options = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
label: copyLabel,
|
||||||
|
callback: async () => {
|
||||||
|
setCopied(true);
|
||||||
|
copy(await skynetClient.getSkylinkUrl(skylink));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[skylink, copyLabel]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
options,
|
||||||
|
};
|
||||||
|
};
|
|
@ -2,6 +2,9 @@ import { Children, cloneElement, useRef, useState } from "react";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import { useClickAway } from "react-use";
|
import { useClickAway } from "react-use";
|
||||||
import styled, { css, keyframes } from "styled-components";
|
import styled, { css, keyframes } from "styled-components";
|
||||||
|
import cn from "classnames";
|
||||||
|
|
||||||
|
import { ContainerLoadingIndicator } from "../LoadingIndicator";
|
||||||
|
|
||||||
const dropDown = keyframes`
|
const dropDown = keyframes`
|
||||||
0% {
|
0% {
|
||||||
|
@ -41,15 +44,15 @@ const Option = styled.li.attrs({
|
||||||
hover:before:content-['']`,
|
hover:before:content-['']`,
|
||||||
})``;
|
})``;
|
||||||
|
|
||||||
export const PopoverMenu = ({ options, children, openClassName, ...props }) => {
|
export const PopoverMenu = ({ options, children, openClassName, inProgress, ...props }) => {
|
||||||
const containerRef = useRef();
|
const containerRef = useRef();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
useClickAway(containerRef, () => setOpen(false));
|
useClickAway(containerRef, () => setOpen(false));
|
||||||
|
|
||||||
const handleChoice = (callback) => () => {
|
const handleChoice = (callback) => async () => {
|
||||||
|
await callback();
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
callback();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -62,11 +65,16 @@ export const PopoverMenu = ({ options, children, openClassName, ...props }) => {
|
||||||
)}
|
)}
|
||||||
{open && (
|
{open && (
|
||||||
<Flyout>
|
<Flyout>
|
||||||
|
<div className={cn("relative", { "opacity-50": inProgress })}>
|
||||||
{options.map(({ label, callback }) => (
|
{options.map(({ label, callback }) => (
|
||||||
<Option key={label} onClick={handleChoice(callback)}>
|
<Option key={label} onClick={handleChoice(callback)}>
|
||||||
{label}
|
{label}
|
||||||
</Option>
|
</Option>
|
||||||
))}
|
))}
|
||||||
|
{inProgress && (
|
||||||
|
<ContainerLoadingIndicator className="absolute inset-0 !p-0 z-50 bg-white !text-palette-300" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</Flyout>
|
</Flyout>
|
||||||
)}
|
)}
|
||||||
</Container>
|
</Container>
|
||||||
|
@ -87,4 +95,9 @@ PopoverMenu.propTypes = {
|
||||||
callback: PropTypes.func.isRequired,
|
callback: PropTypes.func.isRequired,
|
||||||
})
|
})
|
||||||
).isRequired,
|
).isRequired,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If true, a loading icon will be displayed to signal an async action is taking place.
|
||||||
|
*/
|
||||||
|
inProgress: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
|
@ -12,7 +12,7 @@ const FilesPage = () => {
|
||||||
<Metadata>
|
<Metadata>
|
||||||
<title>Files</title>
|
<title>Files</title>
|
||||||
</Metadata>
|
</Metadata>
|
||||||
<Panel title="Files">
|
<Panel title="Uploads">
|
||||||
<FileList type="uploads" />
|
<FileList type="uploads" />
|
||||||
</Panel>
|
</Panel>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -29,6 +29,7 @@ module.exports = {
|
||||||
textColor: (theme) => ({ ...theme("colors"), ...colors }),
|
textColor: (theme) => ({ ...theme("colors"), ...colors }),
|
||||||
placeholderColor: (theme) => ({ ...theme("colors"), ...colors }),
|
placeholderColor: (theme) => ({ ...theme("colors"), ...colors }),
|
||||||
outlineColor: (theme) => ({ ...theme("colors"), ...colors }),
|
outlineColor: (theme) => ({ ...theme("colors"), ...colors }),
|
||||||
|
divideColor: (theme) => ({ ...theme("colors"), ...colors }),
|
||||||
extend: {
|
extend: {
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
sans: ["Sora", ...defaultTheme.fontFamily.sans],
|
sans: ["Sora", ...defaultTheme.fontFamily.sans],
|
||||||
|
|
Reference in New Issue