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
-
+