v3: fixed roles propagation bug, fixed inconsistent file drop zone, fixed chat attachments wrongfully pruned, some visual tweaks
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 41s
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 41s
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Link } from "react-router";
|
||||
import { Controller } from "react-hook-form";
|
||||
import { t } from "@lingui/core/macro";
|
||||
import { Trans } from "@lingui/react/macro";
|
||||
|
||||
@@ -12,10 +11,7 @@ import type {
|
||||
RawDump,
|
||||
RawPlaylistMembership,
|
||||
} from "../model.ts";
|
||||
import {
|
||||
deserializeDump,
|
||||
deserializePlaylistMembership,
|
||||
} from "../model.ts";
|
||||
import { deserializeDump, deserializePlaylistMembership } from "../model.ts";
|
||||
import { useAuth } from "../hooks/useAuth.ts";
|
||||
import { useWS } from "../hooks/useWS.ts";
|
||||
import { dumpUrl, normalizeUrl } from "../utils/urls.ts";
|
||||
@@ -23,12 +19,12 @@ import { MAX_FILE_SIZE } from "../config/upload.ts";
|
||||
import RichContentCard from "./RichContentCard.tsx";
|
||||
import { MediaPlayer } from "./MediaPlayer.tsx";
|
||||
import type { RichContent } from "../model.ts";
|
||||
import { FileDropZone } from "./FileDropZone.tsx";
|
||||
import { Modal } from "./Modal.tsx";
|
||||
import { CategorySelect } from "./CategorySelect.tsx";
|
||||
import { PlaylistMembershipPanel } from "./PlaylistMembershipPanel.tsx";
|
||||
import {
|
||||
expectOk,
|
||||
FileField,
|
||||
FormActions,
|
||||
FormError,
|
||||
FormProvider,
|
||||
@@ -113,7 +109,7 @@ export function DumpCreateModal(
|
||||
isPublic: true,
|
||||
},
|
||||
});
|
||||
const { register, setValue, watch, clearErrors, control } = form;
|
||||
const { register, setValue, watch, clearErrors } = form;
|
||||
const submitting = form.formState.isSubmitting;
|
||||
const url = watch("url");
|
||||
const file = watch("file");
|
||||
@@ -381,16 +377,10 @@ export function DumpCreateModal(
|
||||
)
|
||||
: (
|
||||
<>
|
||||
<Controller
|
||||
<FileField<CreateValues>
|
||||
name="file"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<FileDropZone
|
||||
file={field.value}
|
||||
onChange={selectFile}
|
||||
disabled={submitting}
|
||||
/>
|
||||
)}
|
||||
onValueChange={selectFile}
|
||||
disabled={submitting}
|
||||
/>
|
||||
{file && <LocalFilePreview file={file} />}
|
||||
{file && (
|
||||
|
||||
@@ -15,7 +15,6 @@ interface FileDropZoneProps {
|
||||
file: File | null;
|
||||
onChange: (file: File | null) => void;
|
||||
disabled?: boolean;
|
||||
label?: string;
|
||||
hint?: string;
|
||||
showLimit?: boolean;
|
||||
}
|
||||
@@ -24,11 +23,9 @@ export function FileDropZone({
|
||||
file,
|
||||
onChange,
|
||||
disabled,
|
||||
label,
|
||||
hint,
|
||||
showLimit = true,
|
||||
}: FileDropZoneProps) {
|
||||
const resolvedLabel = label ?? t`File`;
|
||||
const resolvedHint = hint ?? t`Drop a file here`;
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [dragging, setDragging] = useState(false);
|
||||
@@ -72,82 +69,79 @@ export function FileDropZone({
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="fdz-wrapper">
|
||||
{resolvedLabel && <span className="fdz-label">{resolvedLabel}</span>}
|
||||
<div
|
||||
className={`fdz${dragging ? " fdz--drag" : ""}${
|
||||
disabled ? " fdz--disabled" : ""
|
||||
}${file ? " fdz--filled" : ""}`}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
onClick={file ? undefined : handleClick}
|
||||
role={file ? undefined : "button"}
|
||||
tabIndex={file || disabled ? undefined : 0}
|
||||
onKeyDown={file || disabled ? undefined : (e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
handleClick();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="file"
|
||||
onChange={(e) => onChange(e.target.files?.[0] ?? null)}
|
||||
disabled={disabled}
|
||||
style={{ display: "none" }}
|
||||
/>
|
||||
<div
|
||||
className={`fdz${dragging ? " fdz--drag" : ""}${
|
||||
disabled ? " fdz--disabled" : ""
|
||||
}${file ? " fdz--filled" : ""}`}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
onClick={file ? undefined : handleClick}
|
||||
role={file ? undefined : "button"}
|
||||
tabIndex={file || disabled ? undefined : 0}
|
||||
onKeyDown={file || disabled ? undefined : (e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
handleClick();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="file"
|
||||
onChange={(e) => onChange(e.target.files?.[0] ?? null)}
|
||||
disabled={disabled}
|
||||
style={{ display: "none" }}
|
||||
/>
|
||||
|
||||
{file
|
||||
? (
|
||||
<div className="fdz__file">
|
||||
<span className="fdz__file-icon">{fileIcon(file.type)}</span>
|
||||
<div className="fdz__file-meta">
|
||||
<span className="fdz__file-name">{file.name}</span>
|
||||
<span className="fdz__file-size">{formatBytes(file.size)}</span>
|
||||
</div>
|
||||
{!disabled && (
|
||||
<button
|
||||
type="button"
|
||||
className="fdz__clear"
|
||||
onClick={handleClear}
|
||||
aria-label={t`Remove file`}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
)}
|
||||
{file
|
||||
? (
|
||||
<div className="fdz__file">
|
||||
<span className="fdz__file-icon">{fileIcon(file.type)}</span>
|
||||
<div className="fdz__file-meta">
|
||||
<span className="fdz__file-name">{file.name}</span>
|
||||
<span className="fdz__file-size">{formatBytes(file.size)}</span>
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<div className="fdz__empty">
|
||||
<svg
|
||||
className="fdz__upload-icon"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
{!disabled && (
|
||||
<button
|
||||
type="button"
|
||||
className="fdz__clear"
|
||||
onClick={handleClear}
|
||||
aria-label={t`Remove file`}
|
||||
>
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
||||
<polyline points="17 8 12 3 7 8" />
|
||||
<line x1="12" y1="3" x2="12" y2="15" />
|
||||
</svg>
|
||||
<p className="fdz__hint">{resolvedHint}</p>
|
||||
<p className="fdz__browse">
|
||||
<Trans>
|
||||
or <span className="fdz__browse-link">browse files</span>
|
||||
</Trans>
|
||||
✕
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<div className="fdz__empty">
|
||||
<svg
|
||||
className="fdz__upload-icon"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
||||
<polyline points="17 8 12 3 7 8" />
|
||||
<line x1="12" y1="3" x2="12" y2="15" />
|
||||
</svg>
|
||||
<p className="fdz__hint">{resolvedHint}</p>
|
||||
<p className="fdz__browse">
|
||||
<Trans>
|
||||
or <span className="fdz__browse-link">browse files</span>
|
||||
</Trans>
|
||||
</p>
|
||||
{showLimit && (
|
||||
<p className="fdz__limit">
|
||||
<Trans>Max 50 MB</Trans>
|
||||
</p>
|
||||
{showLimit && (
|
||||
<p className="fdz__limit">
|
||||
<Trans>Max 50 MB</Trans>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ interface FileFieldProps<T extends FieldValues> {
|
||||
showLimit?: boolean;
|
||||
disabled?: boolean;
|
||||
rules?: RegisterOptions<T, Path<T>>;
|
||||
/** Side effect run after the field value changes (e.g. derive a title). */
|
||||
onValueChange?: (file: File | null) => void;
|
||||
}
|
||||
|
||||
/** `Controller`-wrapped {@link FileDropZone} bound to react-hook-form. */
|
||||
@@ -25,12 +27,14 @@ export function FileField<T extends FieldValues>({
|
||||
showLimit,
|
||||
disabled,
|
||||
rules,
|
||||
onValueChange,
|
||||
}: FileFieldProps<T>) {
|
||||
const { control, formState: { errors } } = useFormContext<T>();
|
||||
const error = errors[name];
|
||||
|
||||
return (
|
||||
<div className="form-field">
|
||||
{label && <span className="form-label">{label}</span>}
|
||||
<Controller
|
||||
name={name}
|
||||
control={control}
|
||||
@@ -38,8 +42,10 @@ export function FileField<T extends FieldValues>({
|
||||
render={({ field }) => (
|
||||
<FileDropZone
|
||||
file={(field.value as File | null) ?? null}
|
||||
onChange={field.onChange}
|
||||
label={label}
|
||||
onChange={(file) => {
|
||||
field.onChange(file);
|
||||
onValueChange?.(file);
|
||||
}}
|
||||
hint={hint}
|
||||
showLimit={showLimit}
|
||||
disabled={disabled}
|
||||
|
||||
Reference in New Issue
Block a user