From 48605418ebbe3cd621679568709ebff8ff59d8e2 Mon Sep 17 00:00:00 2001 From: Raunak Bhagat Date: Tue, 13 Oct 2026 19:59:24 +0000 Subject: [PATCH] feat(opal): update `InputUpload` and remove previous, legacy component (#15965) --- web/.type-coverage-baseline.yaml | 17 +- web/lib/opal/src/components/README.md | 2 +- web/lib/opal/src/components/index.ts | 10 +- .../components/inputs/input-file/README.md | 24 -- .../inputs/input-file/components.tsx | 181 ---------- .../InputUpload.stories.tsx} | 46 ++- .../components/inputs/input-upload/README.md | 62 ++++ .../inputs/input-upload/components.tsx | 330 ++++++++++++++++++ .../components/inputs/input-upload/styles.css | 53 +++ .../opal/src/form/InputUploadField.test.tsx | 173 +++++++++ web/lib/opal/src/form/InputUploadField.tsx | 72 ++++ web/lib/opal/src/form/index.ts | 4 + web/lib/opal/src/hooks/index.ts | 1 + web/lib/opal/src/hooks/useWindowFileDrag.ts | 55 +++ web/lib/opal/src/icons/import-icon.tsx | 22 -- web/lib/opal/src/icons/import.tsx | 20 ++ web/lib/opal/src/icons/index.ts | 2 +- web/lib/opal/src/strings.tsx | 17 +- .../admin/billing/LicenseActivationCard.tsx | 6 +- web/src/components/Field.tsx | 165 +-------- .../admin/connectors/FileUpload.tsx | 96 ----- web/src/i18n/OpalStringsBridge.tsx | 6 +- web/src/i18n/messages/ar.json | 10 +- web/src/i18n/messages/de.json | 10 +- web/src/i18n/messages/en.json | 10 +- web/src/i18n/messages/es.json | 10 +- web/src/i18n/messages/fr.json | 10 +- web/src/i18n/messages/ja.json | 10 +- web/src/i18n/messages/ko.json | 10 +- web/src/i18n/messages/pt.json | 10 +- web/src/i18n/messages/zh.json | 10 +- web/src/lib/connectors/fileTypes.ts | 4 - web/src/lib/connectors/types/fileTypes.ts | 1 - .../components/AccountNameField.tsx | 35 ++ .../components/CreateCredential.test.tsx | 59 ++++ .../components/CreateCredential.tsx | 24 +- .../components/CredentialFieldsRenderer.tsx | 105 +++++- .../credentials/components/EditCredential.tsx | 7 +- web/src/lib/credentials/constants.ts | 7 +- .../form/FileDropzoneField.tsx | 2 +- .../modals/languageModels/VertexAIModal.tsx | 8 +- .../forms/VertexImageGenForm.tsx | 21 +- .../views/admin/IndexSettingsPage/shared.tsx | 21 +- .../form/gdrive/Credential.tsx | 10 +- .../form/gmail/Credential.tsx | 10 +- .../sections/AuthenticationAccountSection.tsx | 38 +- 46 files changed, 1169 insertions(+), 637 deletions(-) delete mode 100644 web/lib/opal/src/components/inputs/input-file/README.md delete mode 100644 web/lib/opal/src/components/inputs/input-file/components.tsx rename web/lib/opal/src/components/inputs/{input-file/InputFile.stories.tsx => input-upload/InputUpload.stories.tsx} (59%) create mode 100644 web/lib/opal/src/components/inputs/input-upload/README.md create mode 100644 web/lib/opal/src/components/inputs/input-upload/components.tsx create mode 100644 web/lib/opal/src/components/inputs/input-upload/styles.css create mode 100644 web/lib/opal/src/form/InputUploadField.test.tsx create mode 100644 web/lib/opal/src/form/InputUploadField.tsx create mode 100644 web/lib/opal/src/hooks/useWindowFileDrag.ts delete mode 100644 web/lib/opal/src/icons/import-icon.tsx create mode 100644 web/lib/opal/src/icons/import.tsx delete mode 100644 web/src/components/admin/connectors/FileUpload.tsx create mode 100644 web/src/lib/credentials/components/AccountNameField.tsx create mode 100644 web/src/lib/credentials/components/CreateCredential.test.tsx diff --git a/web/.type-coverage-baseline.yaml b/web/.type-coverage-baseline.yaml index a62a9742d4..af2221be7c 100644 --- a/web/.type-coverage-baseline.yaml +++ b/web/.type-coverage-baseline.yaml @@ -8,7 +8,7 @@ # `ods type-coverage typescript --update`. # # Generated file: do not edit by hand. -total: 98.8 +total: 98.9 packages: .: 100 src: 99.4 @@ -21,15 +21,15 @@ packages: src/app/components: 100 src/app/config: 100 src/app/connector: 100 - src/app/craft: 99.3 + src/app/craft: 99.4 src/app/ee: 95.6 src/app/federated: 97 src/app/mcp: 99.5 src/app/nrf: 99.2 src/app/oauth-config: 100 src/app/oauth-provider: 100 - src/components: 97.7 - src/components/admin: 96.1 + src/components: 97.6 + src/components/admin: 95.8 src/components/auth: 89.3 src/components/chat: 97.2 src/components/context: 100 @@ -42,7 +42,7 @@ packages: src/components/oauth: 89.5 src/components/search: 99.5 src/components/standardAnswers: 100 - src/components/tools: 99.7 + src/components/tools: 100 src/components/tooltip: 100 src/components/ui: 99.9 src/components/voice: 100 @@ -69,7 +69,7 @@ packages: src/lib/chat: 98.6 src/lib/connectors: 99 src/lib/constants: 100 - src/lib/credentials: 98.3 + src/lib/credentials: 98.4 src/lib/extension: 100 src/lib/externalApps: 100 src/lib/hierarchy: 100 @@ -91,10 +91,11 @@ packages: src/lib/sidebar: 100 src/lib/skills: 97.4 src/lib/sso: 99.3 + src/lib/tiers: 100 src/lib/tools: 98.1 src/lib/tracing: 96.9 src/lib/usage: 97.9 - src/lib/users: 99 + src/lib/users: 99.1 src/lib/voice: 99.3 src/lib/webSearch: 97.4 src/providers: 99.5 @@ -130,5 +131,5 @@ packages: src/sections/skills: 99.8 src/sections/usage: 99.6 src/views: 99.4 - src/views/admin: 98.4 + src/views/admin: 98.5 types: 100 diff --git a/web/lib/opal/src/components/README.md b/web/lib/opal/src/components/README.md index 24e15cdb95..2e6c6a3173 100644 --- a/web/lib/opal/src/components/README.md +++ b/web/lib/opal/src/components/README.md @@ -22,7 +22,7 @@ The barrel file at `index.ts` re-exports each component and its prop types. Each | [OpenButton](./buttons/open-button/) | Trigger button with rotating chevron for popovers | [README](./buttons/open-button/README.md) | | [Tag](./tag/) | Small colored label for status/category metadata | [README](./tag/README.md) | | [InputDateRangePicker](./inputs/chrono/input-date-range-picker/) | Preset date-range strip with a custom-range calendar popover | [README](./inputs/chrono/input-date-range-picker/README.md) | -| [InputFile](./inputs/input-file/) | Text field that doubles as a file picker | [README](./inputs/input-file/README.md) | +| [InputUpload](./inputs/input-upload/) | Upload control in the input chrome: pick, drop or paste | [README](./inputs/input-upload/README.md) | | [InputAvatar](./inputs/input-avatar/) | Avatar frame with primary input chrome | [README](./inputs/input-avatar/README.md) | | [InputImage](./inputs/input-image/) | Circular image dropzone with edit overlay | [README](./inputs/input-image/README.md) | | [InputKeyValue](./inputs/input-key-value/) | Key/value pair editor with validation | [README](./inputs/input-key-value/README.md) | diff --git a/web/lib/opal/src/components/index.ts b/web/lib/opal/src/components/index.ts index 42125f159c..dae6f6a54b 100644 --- a/web/lib/opal/src/components/index.ts +++ b/web/lib/opal/src/components/index.ts @@ -91,11 +91,13 @@ export { type InputNumberProps, } from "@opal/components/inputs/input-number/components"; -/* InputFile */ +/* InputUpload */ export { - default as InputFile, - type InputFileProps, -} from "@opal/components/inputs/input-file/components"; + default as InputUpload, + type InputUploadProps, + type InputUploadTextProps, + type InputUploadFileProps, +} from "@opal/components/inputs/input-upload/components"; /* InputList */ export { diff --git a/web/lib/opal/src/components/inputs/input-file/README.md b/web/lib/opal/src/components/inputs/input-file/README.md deleted file mode 100644 index 60783c3efa..0000000000 --- a/web/lib/opal/src/components/inputs/input-file/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# InputFile - -**Import:** `import { InputFile, type InputFileProps } from "@opal/components";` - -A text field that doubles as a file picker: type a value directly, or attach a file via the -paperclip action and the field displays the file's name and carries its text content. Clearing -returns to typed mode. Built on `InputTypeIn`; the attach/clear actions are `Button`s in the -right slot. - -## Props - -Extends `InputTypeInProps` minus the props the component owns itself — -`type`, `rightChildren`, `value`, `onChange`, `readOnly`, `clearButton` — -plus: - -| Prop | Type | Description | -| -------------------- | ------------------------------- | ------------------------------------------------------ | -| `setValue` | `(value: string) => void` | Receives the typed text or the selected file's content | -| `onValueSet` | callback | Fires after a value lands | -| `accept` | `string` | File-picker accept filter | -| `maxSizeKb` | `number` | Reject files above this size | -| `onFileSizeExceeded` | `({ file, maxSizeKb }) => void` | Called when a file is rejected for size | - -The attach/clear aria labels come from `OpalStrings` (`attachFile` / `clearFile`). diff --git a/web/lib/opal/src/components/inputs/input-file/components.tsx b/web/lib/opal/src/components/inputs/input-file/components.tsx deleted file mode 100644 index be1ec83566..0000000000 --- a/web/lib/opal/src/components/inputs/input-file/components.tsx +++ /dev/null @@ -1,181 +0,0 @@ -"use client"; - -import React, { useRef, useState } from "react"; -import InputTypeIn, { - type InputTypeInProps, -} from "@opal/components/inputs/texts/input-type-in/components"; -import { Button } from "@opal/components/buttons/button/components"; -import { SvgPaperclip, SvgX } from "@opal/icons"; -import { noProp } from "@opal/utils"; -import { useOpalStrings } from "@opal/strings"; - -export interface InputFileProps extends Omit< - InputTypeInProps, - "type" | "rightChildren" | "value" | "onChange" | "readOnly" | "clearButton" -> { - /** - * Whether the input is disabled. - */ - disabled?: boolean; - /** - * Whether the input has an error. - */ - error?: boolean; - // Receives the extracted file content (text) or pasted value - setValue: (value: string) => void; - // Called when a value is committed via file selection or paste (not on each keystroke) - onValueSet?: (value: string, source: "file" | "paste") => void; - // HTML accept attribute e.g. "application/json" or ".txt,.md" - accept?: string; - // Maximum allowed file size in kilobytes. If exceeded, file is rejected. - maxSizeKb?: number; - // Optional callback when the selected file exceeds max size - onFileSizeExceeded?: (args: { file: File; maxSizeKb: number }) => void; -} - -export default function InputFile({ - setValue, - onValueSet, - accept, - maxSizeKb, - onFileSizeExceeded, - disabled, - error, - variant, - placeholder, - ...rest -}: InputFileProps) { - const strings = useOpalStrings(); - const [displayValue, setDisplayValue] = useState(""); - const [selectedFileName, setSelectedFileName] = useState(null); - const [isFileMode, setIsFileMode] = useState(false); - const fileInputRef = useRef(null); - - // Derive disabled/readOnly state from either the boolean props or the variant - const isDisabled = disabled || variant === "disabled"; - const isReadOnly = variant === "readOnly"; - const isNonEditable = isDisabled || isReadOnly; - - function openFilePicker() { - if (isNonEditable) return; - fileInputRef.current?.click(); - } - - function handleFileChange(e: React.ChangeEvent) { - const file = e.target.files?.[0]; - if (!file) return; - - // Enforce file size limit if provided - if (typeof maxSizeKb === "number" && maxSizeKb >= 0) { - const maxBytes = maxSizeKb * 1024; - if (file.size > maxBytes) { - onFileSizeExceeded?.({ file, maxSizeKb }); - // Reset file input to allow re-selecting the same file - e.target.value = ""; - return; - } - } - - const reader = new FileReader(); - reader.onload = () => { - const textContent = - typeof reader.result === "string" ? reader.result : ""; - setValue(textContent); - setSelectedFileName(file.name); - setDisplayValue(file.name); - setIsFileMode(true); - onValueSet?.(textContent, "file"); - }; - reader.onerror = () => { - // Reset state on error - setSelectedFileName(null); - setDisplayValue(""); - setIsFileMode(false); - setValue(""); - }; - reader.readAsText(file); - // clear the input value to allow re-selecting the same file if needed - e.target.value = ""; - } - - function handleClear() { - setSelectedFileName(null); - setDisplayValue(""); - setIsFileMode(false); - setValue(""); - } - - function handleChangeWhenTyping(e: React.ChangeEvent) { - if (isFileMode) return; // ignore typing when file-mode is active - const next = e.target.value; - setDisplayValue(next); - setValue(next); - } - - function handlePaste(e: React.ClipboardEvent) { - // Don't allow paste when non-editable - if (isNonEditable) return; - // Switch to editable mode and use pasted text as the value - const pastedText = e.clipboardData.getData("text"); - if (!pastedText) return; - e.preventDefault(); - setIsFileMode(false); - setSelectedFileName(null); - setDisplayValue(pastedText); - setValue(pastedText); - onValueSet?.(pastedText, "paste"); - } - - const rightChildren = isFileMode ? ( - + {hasValue && !isNonEditable && drag === null && ( +