diff --git a/CHANGELOG.md b/CHANGELOG.md index c69c5218ba..14e68ea8ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ and this project adheres to ### Added +- Add missing adaptor and credential tooltips to the collab editor + [#3919](https://github.com/OpenFn/lightning/issues/3919) - Show server validation errors in the collab editor forms [#3783](https://github.com/OpenFn/lightning/issues/3783) - Enforce readonly state in collaborative editor forms for viewers and old diff --git a/assets/js/collaborative-editor/components/ConfigureAdaptorModal.tsx b/assets/js/collaborative-editor/components/ConfigureAdaptorModal.tsx index 390ceee9c9..e2db8cc051 100644 --- a/assets/js/collaborative-editor/components/ConfigureAdaptorModal.tsx +++ b/assets/js/collaborative-editor/components/ConfigureAdaptorModal.tsx @@ -3,25 +3,26 @@ import { DialogBackdrop, DialogPanel, DialogTitle, -} from "@headlessui/react"; -import { useEffect, useMemo, useState, useRef } from "react"; -import { useHotkeysContext } from "react-hotkeys-hook"; +} from '@headlessui/react'; +import { useEffect, useMemo, useState, useRef } from 'react'; +import { useHotkeysContext } from 'react-hotkeys-hook'; -import { HOTKEY_SCOPES } from "#/collaborative-editor/constants/hotkeys"; +import { HOTKEY_SCOPES } from '#/collaborative-editor/constants/hotkeys'; import { useCredentials, useCredentialQueries, -} from "#/collaborative-editor/hooks/useCredentials"; -import type { Adaptor } from "#/collaborative-editor/types/adaptor"; -import type { CredentialWithType } from "#/collaborative-editor/types/credential"; +} from '#/collaborative-editor/hooks/useCredentials'; +import type { Adaptor } from '#/collaborative-editor/types/adaptor'; +import type { CredentialWithType } from '#/collaborative-editor/types/credential'; import { extractAdaptorName, extractAdaptorDisplayName, extractPackageName, -} from "#/collaborative-editor/utils/adaptorUtils"; +} from '#/collaborative-editor/utils/adaptorUtils'; -import { AdaptorIcon } from "./AdaptorIcon"; -import { VersionPicker } from "./VersionPicker"; +import { AdaptorIcon } from './AdaptorIcon'; +import { Tooltip } from './Tooltip'; +import { VersionPicker } from './VersionPicker'; interface ConfigureAdaptorModalProps { isOpen: boolean; @@ -98,10 +99,10 @@ export function ConfigureAdaptorModal({ if (adaptor) { const sortedVersions = adaptor.versions .map(v => v.version) - .filter(v => v !== "latest") + .filter(v => v !== 'latest') .sort((a, b) => { - const aParts = a.split(".").map(Number); - const bParts = b.split(".").map(Number); + const aParts = a.split('.').map(Number); + const bParts = b.split('.').map(Number); for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) { const aNum = aParts[i] || 0; const bNum = bParts[i] || 0; @@ -138,7 +139,7 @@ export function ConfigureAdaptorModal({ const adaptorNeedsCredentials = useMemo(() => { const adaptorName = extractAdaptorName(currentAdaptor); // Common adaptor doesn't require credentials - return adaptorName !== "common"; + return adaptorName !== 'common'; }, [currentAdaptor]); // Filter credentials into sections @@ -159,10 +160,10 @@ export function ConfigureAdaptorModal({ if (c.schema === adaptorName) return true; // Smart OAuth matching: if credential is OAuth, check oauth_client_name - if (c.schema === "oauth" && c.oauth_client_name) { + if (c.schema === 'oauth' && c.oauth_client_name) { // Normalize both strings: lowercase, remove spaces/hyphens/underscores const normalizeString = (str: string) => - str.toLowerCase().replace(/[\s\-_]/g, ""); + str.toLowerCase().replace(/[\s\-_]/g, ''); const normalizedClientName = normalizeString(c.oauth_client_name); const normalizedAdaptorName = normalizeString(adaptorName); @@ -177,22 +178,22 @@ export function ConfigureAdaptorModal({ return false; }) - .map(c => ({ ...c, type: "project" as const })); + .map(c => ({ ...c, type: 'project' as const })); // Universal project credentials (http and raw work with all adaptors) // Only show if not already in schemaMatched (avoid duplicates) const universal: CredentialWithType[] = projectCredentials .filter(c => { - const isUniversal = c.schema === "http" || c.schema === "raw"; - const alreadyMatched = adaptorName === "http" || adaptorName === "raw"; + const isUniversal = c.schema === 'http' || c.schema === 'raw'; + const alreadyMatched = adaptorName === 'http' || adaptorName === 'raw'; return isUniversal && !alreadyMatched; }) - .map(c => ({ ...c, type: "project" as const })); + .map(c => ({ ...c, type: 'project' as const })); // All keychain credentials (can't reliably filter by schema) const keychain: CredentialWithType[] = keychainCredentials.map(c => ({ ...c, - type: "keychain" as const, + type: 'keychain' as const, })); return { schemaMatched, universal, keychain }; @@ -253,11 +254,11 @@ export function ConfigureAdaptorModal({ // Sort versions using semantic versioning (newest first) const sortedVersions = adaptor.versions .map(v => v.version) - .filter(v => v !== "latest") + .filter(v => v !== 'latest') .sort((a, b) => { // Split version strings into parts [major, minor, patch] - const aParts = a.split(".").map(Number); - const bParts = b.split(".").map(Number); + const aParts = a.split('.').map(Number); + const bParts = b.split('.').map(Number); // Compare major, minor, patch in order for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) { @@ -271,7 +272,7 @@ export function ConfigureAdaptorModal({ }); // Add "latest" as the first option - return ["latest", ...sortedVersions]; + return ['latest', ...sortedVersions]; }, [currentAdaptor, allAdaptors]); // Extract adaptor display name @@ -352,12 +353,17 @@ export function ConfigureAdaptorModal({ font-medium text-gray-700 mb-2" > Adaptor - + + +
Credentials - + + + {adaptorNeedsCredentials && (
- {cred.type === "project" && cred.owner && ( + {cred.type === 'project' && cred.owner && (
- {cred.type === "project" && + {cred.type === 'project' && cred.owner && (