diff --git a/apps/forms/app/(api)/private/editor/connect/[form_id]/supabase/table/[table_id]/query/route.ts b/apps/forms/app/(api)/private/editor/connect/[form_id]/supabase/table/[table_id]/query/route.ts index cc4c63fc3..76fe1e1c0 100644 --- a/apps/forms/app/(api)/private/editor/connect/[form_id]/supabase/table/[table_id]/query/route.ts +++ b/apps/forms/app/(api)/private/editor/connect/[form_id]/supabase/table/[table_id]/query/route.ts @@ -52,7 +52,7 @@ export async function GET(req: NextRequest, context: Context) { pooler.queue(data, fields); const files = await pooler.resolve(); - console.log("files", files); + // console.log("files", files); const datawithstorage = data.map((row: Record) => { // TODO: get pk based on table schema (read comment in GridaXSupabaseStorageTaskPooler class) @@ -180,14 +180,18 @@ class GridaXSupabaseStorageTaskPooler { rows: ReadonlyArray>, fields: FormFieldDefinition[] ) { - const file_fields = fields.filter((f) => FieldSupports.file_alias(f.type)); + const x_supabase_storage_file_fields = fields.filter( + (f) => + FieldSupports.file_alias(f.type) && + (f.storage as XSupabaseStorageSchema)?.type === "x-supabase" + ); for (const row of rows) { // TODO: get pk based on table schema (alternatively, we can use index as well - doesnt have to be a data from a fetched row) const pk = row.id; const task = this.storage.createSignedUrls( row, - file_fields.map((ff) => ({ + x_supabase_storage_file_fields.map((ff) => ({ ...(ff.storage as XSupabaseStorageSchema), id: ff.id, })) diff --git a/apps/forms/components/form-field-type-icon.tsx b/apps/forms/components/form-field-type-icon.tsx index 9e658fb24..1f15357c8 100644 --- a/apps/forms/components/form-field-type-icon.tsx +++ b/apps/forms/components/form-field-type-icon.tsx @@ -76,13 +76,10 @@ export function FormFieldTypeIcon({ case "range": return ; case "image": - return ; case "audio": - return ; case "video": - return ; case "file": - return ; + return ; case "signature": // TODO: replace icon return ; @@ -99,3 +96,27 @@ export function FormFieldTypeIcon({ return ; } } + +export function FileTypeIcon({ + type, + className, +}: { + type: "file" | "image" | "audio" | "video"; + className?: string; +}) { + const props = { + className: className, + }; + + switch (type) { + case "image": + return ; + case "audio": + return ; + case "video": + return ; + case "file": + default: + return ; + } +} diff --git a/apps/forms/scaffolds/editor/feed.tsx b/apps/forms/scaffolds/editor/feed.tsx index f6f2abb57..6f75748ac 100644 --- a/apps/forms/scaffolds/editor/feed.tsx +++ b/apps/forms/scaffolds/editor/feed.tsx @@ -437,7 +437,6 @@ function rowdiff( continue; } if (!equal(newRow[key], prevRow[key])) { - console.log("changed", key, newRow[key], prevRow[key]); changedFields[key] = newRow[key]; } } @@ -452,7 +451,10 @@ export function XSupabaseMainTableFeedProvider({ const { datagrid_rows_per_page } = state; const request = state.connections.supabase?.main_supabase_table_id - ? `/private/editor/connect/${state.form_id}/supabase/table/${state.connections.supabase.main_supabase_table_id}/query?limit=${datagrid_rows_per_page}` + ? `/private/editor/connect/${state.form_id}/supabase/table/${state.connections.supabase.main_supabase_table_id}/query?limit=${datagrid_rows_per_page}` + + // refresh when fields are updated + "&r=" + + state.fields.length : null; const res = useSWR>( diff --git a/apps/forms/scaffolds/grid/file-cell.tsx b/apps/forms/scaffolds/grid/file-cell.tsx index fed97284b..b7f3c7037 100644 --- a/apps/forms/scaffolds/grid/file-cell.tsx +++ b/apps/forms/scaffolds/grid/file-cell.tsx @@ -40,6 +40,7 @@ import { import { useState } from "react"; import { ScrollArea } from "@/components/ui/scroll-area"; import { MediaPicker } from "../mediapicker"; +import { FileTypeIcon } from "@/components/form-field-type-icon"; export function FileEditCell({ type, @@ -47,7 +48,7 @@ export function FileEditCell({ multiple, files, }: { - type: "image" | "file"; + type: "image" | "file" | "audio" | "video"; accept?: string; multiple?: boolean; files: { @@ -64,7 +65,19 @@ export function FileEditCell({ const { open: openMediaViewer } = useMediaViewer(); const onEnterFullScreen = (f: GFFile) => { - openMediaViewer(f, "image/*"); + switch (type) { + case "audio": + openMediaViewer(f, "audio/*"); + break; + case "video": + openMediaViewer(f, "video/*"); + break; + case "image": + openMediaViewer(f, "image/*"); + default: + openMediaViewer(f); + break; + } }; const canAddNewFile = multiple || files?.length === 0; @@ -94,51 +107,11 @@ export function FileEditCell({ */}
- {type === "image" ? ( - <> -
onEnterFullScreen(f)} - > - {/* eslint-disable-next-line @next/next/no-img-element */} - {f.name} -
- - - {f.name} - -
-
- - ) : ( - - )} + onEnterFullScreen(f)} + {...f} + /> @@ -249,3 +222,70 @@ export function FileEditCell({ ); } + +function FileCard(props: { + type: "image" | "file" | "audio" | "video"; + src: string; + srcset: { + thumbnail: string; + original: string; + }; + download: string; + name: string; + onEnterFullScreen?: () => void; +}) { + // + + const { type, onEnterFullScreen, ...f } = props; + + switch (type) { + case "image": + return ( + <> +
+ {/* eslint-disable-next-line @next/next/no-img-element */} + {f.name} +
+ + + {f.name} + +
+
+ + ); + default: + return ( + + ); + } +} diff --git a/apps/forms/scaffolds/grid/response-grid.tsx b/apps/forms/scaffolds/grid/response-grid.tsx index 38ff8b56b..1c27915e2 100644 --- a/apps/forms/scaffolds/grid/response-grid.tsx +++ b/apps/forms/scaffolds/grid/response-grid.tsx @@ -41,7 +41,10 @@ import { SelectColumn } from "./select-column"; import "./grid.css"; import { unwrapFeildValue } from "@/lib/forms/unwrap"; import { Button } from "@/components/ui/button"; -import { FormFieldTypeIcon } from "@/components/form-field-type-icon"; +import { + FileTypeIcon, + FormFieldTypeIcon, +} from "@/components/form-field-type-icon"; import { toZonedTime } from "date-fns-tz"; import { tztostr } from "../editor/symbols"; import { mask } from "./mask"; @@ -455,12 +458,17 @@ function FieldCell({ column, row }: RenderCellProps) {
); } + case "video": + case "audio": case "file": { return (
{files?.map((f, i) => ( - + {f.name} ))} @@ -477,6 +485,7 @@ function FieldCell({ column, row }: RenderCellProps) { src={file.src} alt={file.name} className="h-full min-w-10 aspect-square rounded overflow-hidden object-cover bg-neutral-500" + loading="lazy" /> {/*
{file.name}
*/} @@ -642,10 +651,12 @@ function FieldEditCell(props: RenderEditCellProps) { /> ); case "file": + case "audio": + case "video": case "image": { return ( diff --git a/apps/forms/scaffolds/mediaviewer/index.tsx b/apps/forms/scaffolds/mediaviewer/index.tsx index f5d865574..957378c6c 100644 --- a/apps/forms/scaffolds/mediaviewer/index.tsx +++ b/apps/forms/scaffolds/mediaviewer/index.tsx @@ -22,8 +22,10 @@ import { import { Button } from "@/components/ui/button"; import { Spinner } from "@/components/spinner"; import { Menubar } from "@/components/ui/menubar"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { FileTypeIcon } from "@/components/form-field-type-icon"; -type MediaViewerAcceptedMimeTypes = "image/*" | "video/*" | "audio/*"; +type MediaViewerAcceptedMimeTypes = "image/*" | "video/*" | "audio/*" | "*/*"; type Src = { src: string; @@ -35,7 +37,7 @@ type Src = { }; interface MediaViewerContextType { - open: (src: Src, type: MediaViewerAcceptedMimeTypes) => void; + open: (src: Src, type?: MediaViewerAcceptedMimeTypes) => void; close: () => void; } @@ -55,11 +57,20 @@ export function MediaViewerProvider({ children }: React.PropsWithChildren<{}>) { const [isOpen, setIsOpen] = useState(false); const [mediaSrc, setMediaSrc] = useState(undefined); const [mediaType, setMediaType] = - useState("image/*"); + useState("*/*"); + const [contentType, setContentType] = useState< + "unknwon" | (string & {}) | undefined + >(undefined); - const open = (src: Src, type: MediaViewerAcceptedMimeTypes) => { + const open = (src: Src, type?: MediaViewerAcceptedMimeTypes) => { setMediaSrc(src); - setMediaType(type); + if (type && type !== "*/*") { + setMediaType(type); + setContentType(type); + } else { + setMediaType("*/*"); + setContentType(undefined); + } setIsOpen(true); }; @@ -67,6 +78,19 @@ export function MediaViewerProvider({ children }: React.PropsWithChildren<{}>) { setIsOpen(false); }; + useEffect(() => { + // Fetch content type if not provided + if ((!mediaType || mediaType === "*/*") && mediaSrc?.src) { + head(mediaSrc.src) + .then((res) => { + setContentType(res["content-type"]); + }) + .catch(() => { + setContentType("unknwon"); + }); + } + }, [mediaSrc?.src, mediaType]); + return ( @@ -79,35 +103,7 @@ export function MediaViewerProvider({ children }: React.PropsWithChildren<{}>) {
- {mediaType.startsWith("image/") && - (mediaSrc?.srcset ? ( - - ) : ( - // eslint-disable-next-line @next/next/no-img-element - Media - ))} - {mediaType.startsWith("video/") && ( - - )} - {mediaType.startsWith("audio/") && ( - - )} +