Skip to content

FileControl: View only for browser-viewable files, plus a Download icon named by x-file.fileName #99

Closed

Description

@sthanikan2000

What

FileControl's View opens a file in a new tab. That works for files a browser can display: images, PDFs, and HTML documents that are opened in a tab and printed with Ctrl+P. Anything else (xlsx, docx, csv) the browser saves instead, under the storage key, e.g. 0f8e7c1a-….xlsx.

The row label also depends on who is looking. The user who just uploaded the file sees its original name. Anyone viewing a saved form has only the key, so they see "Uploaded File".

Schemas need a way to give files a meaningful, consistent name. That naming must not take away the in-tab View for types a browser can display. This issue keeps the two apart:

  • View: look at the file. Shown only for types a browser can display. Behaviour unchanged.
  • Download (new icon, every row): save the file, named by x-file.fileName when it's set.

Proposal

Each layer below builds on the one before it and can be its own PR.

1. Pure helpers (src/utils/, no React)

  • matchesAccept(file: { name: string; type?: string }, accept: string): boolean in a new src/utils/file.ts. Move the matching code out of FileControl's processFile (FileControl.tsx ~:104-110) unchanged: wildcards, .ext entries and exact MIME types. Upload validation then calls it, with no change in behaviour.
  • isBrowserViewable(file: { name: string; type?: string }): boolean, also in file.ts, is matchesAccept(file, VIEWABLE_TYPES) against an explicit allowlist: image/png, image/jpeg, image/gif, image/webp, application/pdf, text/html, text/plain plus their extensions (.png, .jpg, .jpeg, .gif, .webp, .pdf, .html, .htm, .txt). Not image/*: browsers can't display TIFF or HEIC. A file with no type and no extension counts as viewable, so View stays visible (today's behaviour).
  • fileExtension(nameOrKey: string): string | undefined in file.ts: .xlsx from report.xlsx or 0f8e….xlsx.
  • downloadName(template: string | undefined, values: { index: number }, file: { name?: string; key: string }): string in src/utils/download.ts:
    • with a template, render it with the existing renderFileName, then add the real extension (from name, otherwise key) unless the result already ends with it;
    • without one, use the original name, otherwise the key.
  • downloadFromUrl(url: string, fileName: string): Promise<void> in src/utils/download.ts: fetch, throw on a network error or a non-OK response, then pass the Blob to downloadBlob(blob, fileName). downloadBlob is split out of the existing downloadTextFile, which then calls it.

2. Getting a file's URL (FileControl)

  • Store type (File.type) in FileEntry when a file is uploaded.
  • Add one resolveFileUrl(key): the local blobUrl if there is one, otherwise uploadContext.getDownloadUrl(key).url. View and Download both use it, replacing the lookup in today's onView (FileControl.tsx ~:186).

3. FileControl UI

  • Row label: the result of downloadName(...) when x-file.fileName is set, otherwise the original name / "Uploaded File" as today. When a template is set and the original name is known, show the original name as secondary text.
  • View: rendered only when isBrowserViewable(...). Behaviour unchanged: open the window synchronously, then set its URL. Never await before window.open, or the popup blocker stops it.
  • Download: an IconButton with DownloadIcon (@radix-ui/react-icons), aria-label="Download" and a tooltip. It's on every row, including in read-only forms, and calls downloadFromUrl(await resolveFileUrl(key), downloadName(...)).
  • Errors: a per-row error message (Unable to download file. / Unable to open file.) shown under that row and cleared on the next attempt. It's kept separate from the upload error, which shows only in the drop zone, and the drop zone is hidden when the control is read-only or full.
  • Meaning of x-file.fileName: the row label and the Download name only. It never changes what View does.

4. Docs and fixtures

  • README.md FileControl entry and the XFileOptions JSDoc: describe fileName as a base name ({index} is the file's 1-based position, which shifts when an earlier file is removed), with the real extension added. Use a neutral example (Attachment_{index}).
  • Note that Download fetches the signed storage link from the browser, so the storage bucket's CORS must allow GET from the app's origin.
  • dev/fixtures.ts file fixture: add a multi-file field with x-file.fileName whose accept covers images, PDF and xlsx, so View-hidden and View-shown rows appear side by side.

Out of scope

  • Naming through the signed link (response-content-disposition): this avoids CORS and loading the whole file into memory, but needs getDownloadUrl(key, { fileName }) and support from the storage backend. Separate issue.
  • Form values ({consignmentNo}) or {seq(padding)} in x-file.fileName, to match x-xml-export.fileName and x-template. Follow-up; this issue supports {index} only (plus the built-in {today(…)}).
  • The name of the browser tab, or of a "Save as PDF" from it: an HTML document's <title> controls that.
  • Configurable button text (Configurable text for upload/download buttons and drop zones #89 covers wording for the other controls).

Acceptance criteria

  • Layer 1: src/utils/file.test.ts covers matchesAccept (wildcard, .ext, exact MIME, no match), isBrowserViewable (allowlisted MIME, allowlisted extension on a key, xlsx → false, TIFF → false, no type and no extension → true) and fileExtension. download.test.ts covers downloadName (template + extension, template already ending in it, no template → name, no name → key) and downloadFromUrl (OK → downloadBlob called with the name; non-OK and network error → throws).
  • Layer 1: FileControl's upload validation uses matchesAccept, and the existing FileControl tests pass unchanged.
  • Layer 3: FileControl.test.tsx covers:
    • View hidden for an xlsx key and shown for png, pdf and html keys;
    • Download named from the template with the real extension, through the local blob (just uploaded) and through getDownloadUrl (a saved form);
    • Download falling back to the original name when there's no template;
    • a failed Download showing a visible row error when read-only and when the single file slot is full;
    • View still using window.open.
  • Layer 4: README and JSDoc updated, dev/fixtures.ts example added.
  • With no x-file.fileName, behaviour is unchanged apart from the new Download icon and View hidden for non-viewable types.
  • pnpm --filter @opennsw/jsonforms-renderers test, pnpm type-check, pnpm lint and pnpm format:check pass.

Line numbers are as of main (c39d889).

Related: #95.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions