fix: UX changes regarding showing dates on table and actions pannel
This commit is contained in:
parent
9d9aa4e9c9
commit
e401889b04
|
@ -1,8 +1,8 @@
|
|||
import { useMemo} from "react";
|
||||
import { BaseRecord } from "@refinedev/core";
|
||||
import type { BaseRecord } from "@refinedev/core";
|
||||
import { useTable } from "@refinedev/react-table";
|
||||
import {
|
||||
ColumnDef,
|
||||
type ColumnDef,
|
||||
flexRender,
|
||||
} from "@tanstack/react-table";
|
||||
|
||||
|
|
|
@ -29,10 +29,10 @@ import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuGr
|
|||
import { Avatar } from "@radix-ui/react-avatar";
|
||||
import { cn } from "~/utils";
|
||||
import { useGetIdentity, useLogout } from "@refinedev/core";
|
||||
import { Identity } from "~/data/auth-provider";
|
||||
import type { Identity } from "~/data/auth-provider";
|
||||
|
||||
|
||||
export const GeneralLayout = ({ children }: React.PropsWithChildren<{}>) => {
|
||||
export const GeneralLayout = ({ children }: React.PropsWithChildren) => {
|
||||
const location = useLocation();
|
||||
const { data: identity } = useGetIdentity<Identity>();
|
||||
const{ mutate: logout } = useLogout()
|
||||
|
|
|
@ -1,14 +1,21 @@
|
|||
import Uppy, {debugLogger, type State, UppyFile} from "@uppy/core"
|
||||
import Uppy, { debugLogger, type State, type UppyFile } from "@uppy/core";
|
||||
|
||||
import Tus from "@uppy/tus"
|
||||
import toArray from "@uppy/utils/lib/toArray"
|
||||
import Tus from "@uppy/tus";
|
||||
import toArray from "@uppy/utils/lib/toArray";
|
||||
|
||||
import {type ChangeEvent, useCallback, useEffect, useMemo, useRef, useState} from "react"
|
||||
import DropTarget, {type DropTargetOptions} from "./uppy-dropzone"
|
||||
import {useSdk} from "~/components/lib/sdk-context.js";
|
||||
import UppyFileUpload from "~/components/lib/uppy-file-upload.js";
|
||||
import {PROTOCOL_S5, Sdk} from "@lumeweb/portal-sdk";
|
||||
import {S5Client, HashProgressEvent} from "@lumeweb/s5-js";
|
||||
import {
|
||||
type ChangeEvent,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import DropTarget, { type DropTargetOptions } from "./uppy-dropzone";
|
||||
import { useSdk } from "~/components/lib/sdk-context";
|
||||
import UppyFileUpload from "~/components/lib/uppy-file-upload";
|
||||
import { PROTOCOL_S5, type Sdk } from "@lumeweb/portal-sdk";
|
||||
import type { S5Client, HashProgressEvent } from "@lumeweb/s5-js";
|
||||
|
||||
const LISTENING_EVENTS = [
|
||||
"upload",
|
||||
|
@ -16,13 +23,13 @@ const LISTENING_EVENTS = [
|
|||
"upload-error",
|
||||
"file-added",
|
||||
"file-removed",
|
||||
"files-added"
|
||||
] as const
|
||||
"files-added",
|
||||
] as const;
|
||||
|
||||
export function useUppy() {
|
||||
const sdk = useSdk()
|
||||
const sdk = useSdk();
|
||||
|
||||
const [uploadLimit, setUploadLimit] = useState<number>(0)
|
||||
const [uploadLimit, setUploadLimit] = useState<number>(0);
|
||||
|
||||
useEffect(() => {
|
||||
async function getUploadLimit() {
|
||||
|
@ -30,32 +37,32 @@ export function useUppy() {
|
|||
const limit = await sdk.account!().uploadLimit();
|
||||
setUploadLimit(limit);
|
||||
} catch (err) {
|
||||
console.log('Error occured while fetching upload limit', err);
|
||||
console.log("Error occured while fetching upload limit", err);
|
||||
}
|
||||
}
|
||||
getUploadLimit();
|
||||
}, []);
|
||||
}, [sdk.account]);
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
const [targetRef, _setTargetRef] = useState<HTMLElement | null>(null)
|
||||
const uppyInstance = useRef<Uppy>()
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [targetRef, _setTargetRef] = useState<HTMLElement | null>(null);
|
||||
const uppyInstance = useRef<Uppy>();
|
||||
const setRef = useCallback(
|
||||
(element: HTMLElement | null) => _setTargetRef(element),
|
||||
[]
|
||||
)
|
||||
const [, setUppyState] = useState<State>()
|
||||
[],
|
||||
);
|
||||
const [, setUppyState] = useState<State>();
|
||||
const [state, setState] = useState<
|
||||
"completed" | "idle" | "initializing" | "error" | "uploading"
|
||||
>("initializing")
|
||||
>("initializing");
|
||||
|
||||
const [inputProps, setInputProps] = useState<
|
||||
| {
|
||||
ref: typeof inputRef
|
||||
type: "file"
|
||||
onChange: (event: ChangeEvent<HTMLInputElement>) => void
|
||||
ref: typeof inputRef;
|
||||
type: "file";
|
||||
onChange: (event: ChangeEvent<HTMLInputElement>) => void;
|
||||
}
|
||||
| object
|
||||
>({})
|
||||
>({});
|
||||
const getRootProps = useMemo(
|
||||
() => () => {
|
||||
return {
|
||||
|
@ -63,35 +70,35 @@ export function useUppy() {
|
|||
onClick: () => {
|
||||
if (inputRef.current) {
|
||||
//@ts-expect-error -- dumb html
|
||||
inputRef.current.value = null
|
||||
inputRef.current.click()
|
||||
console.log("clicked", { input: inputRef.current })
|
||||
inputRef.current.value = null;
|
||||
inputRef.current.click();
|
||||
console.log("clicked", { input: inputRef.current });
|
||||
}
|
||||
},
|
||||
role: "presentation"
|
||||
}
|
||||
role: "presentation",
|
||||
};
|
||||
},
|
||||
[setRef]
|
||||
)
|
||||
[setRef],
|
||||
);
|
||||
const removeFile = useCallback(
|
||||
(id: string) => {
|
||||
uppyInstance.current?.removeFile(id)
|
||||
uppyInstance.current?.removeFile(id);
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[targetRef, uppyInstance]
|
||||
)
|
||||
[targetRef, uppyInstance],
|
||||
);
|
||||
const cancelAll = useCallback(
|
||||
() => uppyInstance.current?.cancelAll({ reason: "user" }),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[targetRef, uppyInstance]
|
||||
)
|
||||
[targetRef, uppyInstance],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!targetRef) return
|
||||
if (!targetRef) return;
|
||||
|
||||
const tusPreprocessor = async (fileIDs: string[]) => {
|
||||
for(const fileID of fileIDs) {
|
||||
const file = uppyInstance.current?.getFile(fileID) as UppyFile
|
||||
for (const fileID of fileIDs) {
|
||||
const file = uppyInstance.current?.getFile(fileID) as UppyFile;
|
||||
// @ts-ignore
|
||||
if (file.uploader === "tus") {
|
||||
const hashProgressCb = (event: HashProgressEvent) => {
|
||||
|
@ -101,21 +108,28 @@ export function useUppy() {
|
|||
preprocess: {
|
||||
mode: "determinate",
|
||||
message: "Hashing file...",
|
||||
value: Math.round((event.total / event.total) * 100)
|
||||
}
|
||||
})
|
||||
}
|
||||
const options = await sdk.protocols!().get<S5Client>(PROTOCOL_S5).getSdk().getTusOptions(file.data as File, {}, {onHashProgress: hashProgressCb})
|
||||
value: Math.round((event.total / event.total) * 100),
|
||||
},
|
||||
});
|
||||
};
|
||||
const options = await sdk.protocols!()
|
||||
.get<S5Client>(PROTOCOL_S5)
|
||||
.getSdk()
|
||||
.getTusOptions(
|
||||
file.data as File,
|
||||
{},
|
||||
{ onHashProgress: hashProgressCb },
|
||||
);
|
||||
uppyInstance.current?.setFileState(fileID, {
|
||||
tus: options,
|
||||
meta: {
|
||||
...options.metadata,
|
||||
...file.meta,
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const uppy = new Uppy({
|
||||
logger: debugLogger,
|
||||
|
@ -128,25 +142,25 @@ export function useUppy() {
|
|||
return true;
|
||||
},
|
||||
}).use(DropTarget, {
|
||||
target: targetRef
|
||||
} as DropTargetOptions)
|
||||
target: targetRef,
|
||||
} as DropTargetOptions);
|
||||
|
||||
uppyInstance.current = uppy
|
||||
uppyInstance.current = uppy;
|
||||
setInputProps({
|
||||
ref: inputRef,
|
||||
type: "file",
|
||||
onChange: (event) => {
|
||||
const files = toArray(event.target.files)
|
||||
const files = toArray(event.target.files);
|
||||
if (files.length > 0) {
|
||||
uppyInstance.current?.log("[DragDrop] Files selected through input")
|
||||
uppyInstance.current?.addFiles(files)
|
||||
uppyInstance.current?.log("[DragDrop] Files selected through input");
|
||||
uppyInstance.current?.addFiles(files);
|
||||
}
|
||||
|
||||
uppy.iteratePlugins((plugin) => {
|
||||
uppy.removePlugin(plugin);
|
||||
});
|
||||
|
||||
uppy.use(UppyFileUpload, { sdk: sdk as Sdk })
|
||||
uppy.use(UppyFileUpload, { sdk: sdk as Sdk });
|
||||
|
||||
let useTus = false;
|
||||
|
||||
|
@ -154,11 +168,11 @@ export function useUppy() {
|
|||
if (file.size > uploadLimit) {
|
||||
useTus = true;
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
if (useTus) {
|
||||
uppy.use(Tus, { limit: 6, parallelUploads: 10 })
|
||||
uppy.addPreProcessor(tusPreprocessor)
|
||||
uppy.use(Tus, { limit: 6, parallelUploads: 10 });
|
||||
uppy.addPreProcessor(tusPreprocessor);
|
||||
}
|
||||
|
||||
// We clear the input after a file is selected, because otherwise
|
||||
|
@ -169,54 +183,52 @@ export function useUppy() {
|
|||
// Chrome will not trigger change if we drop the same file twice (Issue #768).
|
||||
// @ts-expect-error TS freaks out, but this is fine
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
event.target.value = null
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
event.target.value = null;
|
||||
},
|
||||
});
|
||||
|
||||
uppy.on("complete", (result) => {
|
||||
if (result.failed.length === 0) {
|
||||
console.log("Upload successful üòÄ")
|
||||
setState("completed")
|
||||
console.log("Upload successful üòÄ");
|
||||
setState("completed");
|
||||
} else {
|
||||
console.warn("Upload failed üòû")
|
||||
setState("error")
|
||||
console.warn("Upload failed üòû");
|
||||
setState("error");
|
||||
}
|
||||
console.log("successful files:", result.successful)
|
||||
console.log("failed files:", result.failed)
|
||||
})
|
||||
console.log("successful files:", result.successful);
|
||||
console.log("failed files:", result.failed);
|
||||
});
|
||||
|
||||
const setStateCb = (event: (typeof LISTENING_EVENTS)[number]) => {
|
||||
switch (event) {
|
||||
case "upload":
|
||||
setState("uploading")
|
||||
break
|
||||
setState("uploading");
|
||||
break;
|
||||
case "upload-error":
|
||||
setState("error")
|
||||
break
|
||||
setState("error");
|
||||
break;
|
||||
default:
|
||||
break
|
||||
}
|
||||
setUppyState(uppy.getState())
|
||||
break;
|
||||
}
|
||||
setUppyState(uppy.getState());
|
||||
};
|
||||
|
||||
for (const event of LISTENING_EVENTS) {
|
||||
uppy.on(event, function cb() {
|
||||
setStateCb(event)
|
||||
})
|
||||
setStateCb(event);
|
||||
});
|
||||
}
|
||||
setState("idle")
|
||||
}, [targetRef, uploadLimit])
|
||||
setState("idle");
|
||||
}, [targetRef, uploadLimit]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
uppyInstance.current?.cancelAll({ reason: "unmount" })
|
||||
uppyInstance.current?.logout()
|
||||
uppyInstance.current?.close()
|
||||
uppyInstance.current = undefined
|
||||
}
|
||||
}, [])
|
||||
uppyInstance.current?.cancelAll({ reason: "unmount" });
|
||||
uppyInstance.current?.logout();
|
||||
uppyInstance.current?.close();
|
||||
uppyInstance.current = undefined;
|
||||
};
|
||||
}, []);
|
||||
return {
|
||||
getFiles: () => uppyInstance.current?.getFiles() ?? [],
|
||||
error: uppyInstance.current?.getState,
|
||||
|
@ -227,6 +239,6 @@ export function useUppy() {
|
|||
getInputProps: () => inputProps,
|
||||
getRootProps,
|
||||
removeFile,
|
||||
cancelAll
|
||||
}
|
||||
cancelAll,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -60,7 +60,7 @@ const ToastAction = React.forwardRef<
|
|||
<ToastPrimitives.Action
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium transition-colors hover:bg-secondary focus:outline-none focus:ring-1 focus:ring-ring disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
|
||||
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-background px-3 text-sm font-medium transition-colors hover:bg-secondary focus:outline-none focus:ring-1 focus:ring-ring disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
|
@ -14,7 +14,7 @@ export function Toaster() {
|
|||
|
||||
return (
|
||||
<ToastProvider>
|
||||
{toasts.map(function ({ id, title, description, action, cancelMutation, ...props }) {
|
||||
{toasts.map(({ id, title, description, action, cancelMutation, ...props }) => {
|
||||
const undoButton = cancelMutation ? <ToastAction altText="Undo" onClick={cancelMutation}>Undo</ToastAction> : undefined
|
||||
return (
|
||||
<Toast key={id} {...props}>
|
||||
|
|
|
@ -28,7 +28,7 @@ export function Layout({children}: { children: React.ReactNode }) {
|
|||
<Meta/>
|
||||
<Links/>
|
||||
</head>
|
||||
<body>
|
||||
<body className="overflow-hidden">
|
||||
{children}
|
||||
<Toaster/>
|
||||
<ScrollRestoration/>
|
||||
|
|
|
@ -2,14 +2,20 @@ import { DrawingPinIcon, TrashIcon } from "@radix-ui/react-icons";
|
|||
import type { ColumnDef, RowData } from "@tanstack/react-table";
|
||||
import { FileIcon, MoreIcon } from "~/components/icons";
|
||||
import { Checkbox } from "~/components/ui/checkbox";
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "~/components/ui/dropdown-menu";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "~/components/ui/dropdown-menu";
|
||||
import { cn } from "~/utils";
|
||||
import {FileItem} from "~/data/file-provider.js";
|
||||
import {format} from "date-fns/fp";
|
||||
import type { FileItem } from "~/data/file-provider";
|
||||
|
||||
declare module '@tanstack/table-core' {
|
||||
declare module "@tanstack/table-core" {
|
||||
interface TableMeta<TData extends RowData> {
|
||||
hoveredRowId: string,
|
||||
hoveredRowId: string;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -19,7 +25,6 @@ export const columns: ColumnDef<FileItem>[] = [
|
|||
size: 20,
|
||||
header: ({ table }) => (
|
||||
<Checkbox
|
||||
|
||||
checked={
|
||||
table.getIsAllPageRowsSelected() ||
|
||||
(table.getIsSomePageRowsSelected() && "indeterminate")
|
||||
|
@ -46,7 +51,7 @@ export const columns: ColumnDef<FileItem>[] = [
|
|||
<FileIcon />
|
||||
{row.getValue("name")}
|
||||
</div>
|
||||
)
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "cid",
|
||||
|
@ -58,24 +63,25 @@ export const columns: ColumnDef<FileItem>[] = [
|
|||
},
|
||||
{
|
||||
accessorKey: "pinned",
|
||||
size: 200,
|
||||
header: "Pinned On",
|
||||
cell: ({ row }) => new Date(row.getValue("pinned")).toLocaleString(),
|
||||
},
|
||||
{
|
||||
accessorKey: "actions",
|
||||
header: () => null,
|
||||
size: 20,
|
||||
cell: ({ row }) => (
|
||||
<div className="flex items-center justify-between">
|
||||
{format(row.getValue("pinned")) as unknown as string}
|
||||
<div className="flex w-5 items-center justify-between">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger className={
|
||||
cn("hidden group-hover:block data-[state=open]:block", row.getIsSelected() && "block")
|
||||
}>
|
||||
<DropdownMenuTrigger
|
||||
className={cn(
|
||||
"hidden group-hover:block data-[state=open]:block",
|
||||
row.getIsSelected() && "block",
|
||||
)}>
|
||||
<MoreIcon />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<DrawingPinIcon className="mr-2" />
|
||||
Ping CID
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem variant="destructive">
|
||||
<TrashIcon className="mr-2" />
|
||||
Delete
|
||||
|
@ -84,6 +90,6 @@ export const columns: ColumnDef<FileItem>[] = [
|
|||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
),
|
||||
},
|
||||
];
|
||||
|
|
|
@ -18,8 +18,8 @@ import { Field } from "~/components/forms";
|
|||
export default function FileManager() {
|
||||
return (
|
||||
<Authenticated key="dashboard" v3LegacyAuthProviderCompatible>
|
||||
<Dialog>
|
||||
<GeneralLayout>
|
||||
<Dialog>
|
||||
<h1 className="font-bold mb-4 text-lg">File Manager</h1>
|
||||
<FileCardList>
|
||||
<FileCard
|
||||
|
@ -67,14 +67,16 @@ export default function FileManager() {
|
|||
</DialogTrigger>
|
||||
</div>
|
||||
<DataTable columns={columns} />
|
||||
</GeneralLayout>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Pinning Contnet</DialogTitle>
|
||||
</DialogHeader>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Pin Content</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form action="" className="w-full flex flex-col gap-y-4">
|
||||
<Field
|
||||
inputProps={{ name: "cids", placeholder: "Comma separated CIDs" }}
|
||||
inputProps={{
|
||||
name: "cids",
|
||||
placeholder: "Comma separated CIDs",
|
||||
}}
|
||||
labelProps={{ htmlFor: "cids", children: "Content to Pin" }}
|
||||
/>
|
||||
|
||||
|
@ -84,6 +86,7 @@ export default function FileManager() {
|
|||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</GeneralLayout>
|
||||
</Authenticated>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -47,7 +47,7 @@ export default function Login() {
|
|||
go({ to, type: "push" });
|
||||
}
|
||||
}
|
||||
}, [isAuthLoading, authData]);
|
||||
}, [isAuthLoading, authData, parsed, go]);
|
||||
|
||||
return (
|
||||
<div className="p-10 h-screen relative">
|
||||
|
@ -109,16 +109,18 @@ const LoginForm = () => {
|
|||
return parseWithZod(formData, { schema: LoginSchema });
|
||||
},
|
||||
shouldValidate: "onSubmit",
|
||||
onSubmit(e) {
|
||||
onSubmit(e, { submission }) {
|
||||
e.preventDefault();
|
||||
|
||||
const data = Object.fromEntries(new FormData(e.currentTarget).entries());
|
||||
if (submission?.status === "success") {
|
||||
const data = submission.value;
|
||||
login.mutate({
|
||||
email: data.email.toString(),
|
||||
password: data.password.toString(),
|
||||
rememberMe: data.rememberMe.toString() === "on",
|
||||
email: data.email,
|
||||
password: data.password,
|
||||
rememberMe: data.rememberMe ?? false,
|
||||
redirectTo: parsed.params?.to,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -4,8 +4,8 @@
|
|||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 240, 33%, 3%;
|
||||
--foreground: 0, 0%, 88%;
|
||||
--background: 0 0% 3.9%;
|
||||
--foreground: 0 0% 98%;
|
||||
|
||||
--card: 0 0% 0%;
|
||||
--card-foreground: 0 0% 3.9%;
|
||||
|
|
Loading…
Reference in New Issue