import { useCallback, useMemo, useState } from "react";import { useAtomSet } from "@effect/atom-react";import * as Exit from "effect/Exit"; import { integrationWriteKeys } from "@executor-js/react/api/reactivity-keys";import { slugifyNamespace, useIntegrationIdentity,} from "@executor-js/react/plugins/integration-identity";import { Button } from "@executor-js/react/components/button";import { FloatActions } from "@executor-js/react/components/float-actions";import { addIntegrationErrorMessage, FormErrorAlert, SlugCollisionAlert, useSlugAlreadyExists,} from "@executor-js/react/lib/integration-add";import { OpenApiSourceDetailsFields } from "@executor-js/plugin-openapi/react"; import { addGoogleBundle } from "./atoms";import { GoogleProductPicker } from "./GoogleProductPicker";import { GOOGLE_PHOTOS_PRESET_ID, googleOpenApiPresets, googlePhotosPresetIds, type GoogleOpenApiPreset,} from "../sdk/presets"; const GOOGLE_BUNDLE_FAVICON = "https://fonts.gstatic.com/s/i/productlogos/googleg/v6/192px.svg"; const googleBundleDefaultPresetIds: ReadonlySet<string> = new Set( googleOpenApiPresets .filter((preset: GoogleOpenApiPreset) => preset.featured) .map((preset: GoogleOpenApiPreset) => preset.id),); const googleBundleUrls = ( selectedPresetIds: ReadonlySet<string>, customUrls: readonly string[],): readonly string[] => { const fromPresets = googleOpenApiPresets.flatMap((preset: GoogleOpenApiPreset) => preset.url && selectedPresetIds.has(preset.id) ? [preset.url] : [], ); return [...new Set([...fromPresets, ...customUrls])];}; export default function AddGoogleSource(props: { onComplete: (slug?: string) => void; onCancel: () => void; initialPreset?: string; initialNamespace?: string;}) { const isGooglePhotosPreset = props.initialPreset === GOOGLE_PHOTOS_PRESET_ID; const [selectedPresetIds, setSelectedPresetIds] = useState<ReadonlySet<string>>( isGooglePhotosPreset ? new Set(googlePhotosPresetIds) : googleBundleDefaultPresetIds, ); const [customDiscoveryUrls, setCustomDiscoveryUrls] = useState<readonly string[]>([]); const [baseUrl, setBaseUrl] = useState(""); const [descriptionDraft, setDescriptionDraft] = useState<string | null>(null); const [adding, setAdding] = useState(false); const [addError, setAddError] = useState<string | null>(null); const identity = useIntegrationIdentity({ fallbackName: isGooglePhotosPreset ? "Google Photos" : "Google", fallbackNamespace: props.initialNamespace ?? (isGooglePhotosPreset ? "google_photos" : "google"), }); const bundleDiscoveryUrls = useMemo( () => googleBundleUrls(selectedPresetIds, customDiscoveryUrls), [selectedPresetIds, customDiscoveryUrls], ); const toggleBundlePreset = useCallback((presetId: string, checked: boolean) => { setSelectedPresetIds((current: ReadonlySet<string>) => { const next = new Set(current); if (checked) next.add(presetId); else next.delete(presetId); return next; }); }, []); const addCustomDiscoveryUrl = useCallback((url: string) => { setCustomDiscoveryUrls((current: readonly string[]) => current.includes(url) ? current : [...current, url], ); }, []); const removeCustomDiscoveryUrl = useCallback((url: string) => { setCustomDiscoveryUrls((current: readonly string[]) => current.filter((entry: string) => entry !== url), ); }, []); const doAdd = useAtomSet(addGoogleBundle, { mode: "promiseExit" }); const resolvedSourceId = slugifyNamespace(identity.namespace) || (isGooglePhotosPreset ? "google_photos" : "google"); const resolvedDisplayName = identity.name.trim() || (isGooglePhotosPreset ? "Google Photos" : "Google"); const resolvedDescription = descriptionDraft ?? (isGooglePhotosPreset ? "Google Photos albums, uploads, app-created media, and selected picker media." : "Google APIs"); const slugAlreadyExists = useSlugAlreadyExists(resolvedSourceId); const canAdd = bundleDiscoveryUrls.length > 0 && !slugAlreadyExists; const handleAdd = async () => { setAdding(true); setAddError(null); const exit = await doAdd({ payload: { urls: [...bundleDiscoveryUrls], slug: resolvedSourceId, name: resolvedDisplayName, ...(resolvedDescription.trim().length > 0 ? { description: resolvedDescription.trim() } : {}), ...(baseUrl.trim().length > 0 ? { baseUrl: baseUrl.trim() } : {}), }, reactivityKeys: integrationWriteKeys, }); if (Exit.isFailure(exit)) { setAddError(addIntegrationErrorMessage(exit, resolvedSourceId, "Failed to add Google")); setAdding(false); return; } props.onComplete(String(exit.value.slug)); }; return ( <div className="flex flex-1 flex-col gap-6"> <div> <h1 className="text-xl font-semibold text-foreground">Add Google integration</h1> <p className="mt-1 text-[13px] text-muted-foreground"> Bundle Google APIs into one integration with a shared OAuth consent. </p> </div> <GoogleProductPicker selectedPresetIds={selectedPresetIds} onToggle={toggleBundlePreset} customUrls={customDiscoveryUrls} onAddCustomUrl={addCustomDiscoveryUrl} onRemoveCustomUrl={removeCustomDiscoveryUrl} /> <OpenApiSourceDetailsFields title="Google" subtitle={`${bundleDiscoveryUrls.length} Google API${ bundleDiscoveryUrls.length !== 1 ? "s" : "" } · one shared OAuth consent`} identity={identity} description={resolvedDescription} onDescriptionChange={setDescriptionDraft} baseUrl={baseUrl} onBaseUrlChange={setBaseUrl} baseUrlLabel="Base URL override (optional)" faviconIcon={GOOGLE_BUNDLE_FAVICON} faviconUrl={baseUrl} /> {slugAlreadyExists && !adding && <SlugCollisionAlert slug={resolvedSourceId} />} {addError && <FormErrorAlert message={addError} />} <FloatActions> <Button variant="ghost" onClick={() => props.onCancel()} disabled={adding}> Cancel </Button> <Button onClick={() => void handleAdd()} disabled={!canAdd} loading={adding}> Connect Google </Button> </FloatActions> </div> );}
import { useCallback, useMemo, useState } from "react";
import { useAtomSet } from "@effect/atom-react";
import * as Exit from "effect/Exit";
import { integrationWriteKeys } from "@executor-js/react/api/reactivity-keys";
import {
slugifyNamespace,
useIntegrationIdentity,
} from "@executor-js/react/plugins/integration-identity";
import { Button } from "@executor-js/react/components/button";
import { FloatActions } from "@executor-js/react/components/float-actions";
import {
addIntegrationErrorMessage,
FormErrorAlert,
SlugCollisionAlert,
useSlugAlreadyExists,
} from "@executor-js/react/lib/integration-add";
import { OpenApiSourceDetailsFields } from "@executor-js/plugin-openapi/react";
import { addGoogleBundle } from "./atoms";
import { GoogleProductPicker } from "./GoogleProductPicker";
import {
GOOGLE_PHOTOS_PRESET_ID,
googleOpenApiPresets,
googlePhotosPresetIds,
type GoogleOpenApiPreset,
} from "../sdk/presets";
const GOOGLE_BUNDLE_FAVICON = "https://fonts.gstatic.com/s/i/productlogos/googleg/v6/192px.svg";
const googleBundleDefaultPresetIds: ReadonlySet<string> = new Set(
googleOpenApiPresets
.filter((preset: GoogleOpenApiPreset) => preset.featured)
.map((preset: GoogleOpenApiPreset) => preset.id),
);
const googleBundleUrls = (
selectedPresetIds: ReadonlySet<string>,
customUrls: readonly string[],
): readonly string[] => {
const fromPresets = googleOpenApiPresets.flatMap((preset: GoogleOpenApiPreset) =>
preset.url && selectedPresetIds.has(preset.id) ? [preset.url] : [],
);
return [...new Set([...fromPresets, ...customUrls])];
};
export default function AddGoogleSource(props: {
onComplete: (slug?: string) => void;
onCancel: () => void;
initialPreset?: string;
initialNamespace?: string;
}) {
const isGooglePhotosPreset = props.initialPreset === GOOGLE_PHOTOS_PRESET_ID;
const [selectedPresetIds, setSelectedPresetIds] = useState<ReadonlySet<string>>(
isGooglePhotosPreset ? new Set(googlePhotosPresetIds) : googleBundleDefaultPresetIds,
);
const [customDiscoveryUrls, setCustomDiscoveryUrls] = useState<readonly string[]>([]);
const [baseUrl, setBaseUrl] = useState("");
const [descriptionDraft, setDescriptionDraft] = useState<string | null>(null);
const [adding, setAdding] = useState(false);
const [addError, setAddError] = useState<string | null>(null);
const identity = useIntegrationIdentity({
fallbackName: isGooglePhotosPreset ? "Google Photos" : "Google",
fallbackNamespace:
props.initialNamespace ?? (isGooglePhotosPreset ? "google_photos" : "google"),
});
const bundleDiscoveryUrls = useMemo(
() => googleBundleUrls(selectedPresetIds, customDiscoveryUrls),
[selectedPresetIds, customDiscoveryUrls],
);
const toggleBundlePreset = useCallback((presetId: string, checked: boolean) => {
setSelectedPresetIds((current: ReadonlySet<string>) => {
const next = new Set(current);
if (checked) next.add(presetId);
else next.delete(presetId);
return next;
});
}, []);
const addCustomDiscoveryUrl = useCallback((url: string) => {
setCustomDiscoveryUrls((current: readonly string[]) =>
current.includes(url) ? current : [...current, url],
);
}, []);
const removeCustomDiscoveryUrl = useCallback((url: string) => {
setCustomDiscoveryUrls((current: readonly string[]) =>
current.filter((entry: string) => entry !== url),
);
}, []);
const doAdd = useAtomSet(addGoogleBundle, { mode: "promiseExit" });
const resolvedSourceId =
slugifyNamespace(identity.namespace) || (isGooglePhotosPreset ? "google_photos" : "google");
const resolvedDisplayName =
identity.name.trim() || (isGooglePhotosPreset ? "Google Photos" : "Google");
const resolvedDescription =
descriptionDraft ??
(isGooglePhotosPreset
? "Google Photos albums, uploads, app-created media, and selected picker media."
: "Google APIs");
const slugAlreadyExists = useSlugAlreadyExists(resolvedSourceId);
const canAdd = bundleDiscoveryUrls.length > 0 && !slugAlreadyExists;
const handleAdd = async () => {
setAdding(true);
setAddError(null);
const exit = await doAdd({
payload: {
urls: [...bundleDiscoveryUrls],
slug: resolvedSourceId,
name: resolvedDisplayName,
...(resolvedDescription.trim().length > 0
? { description: resolvedDescription.trim() }
: {}),
...(baseUrl.trim().length > 0 ? { baseUrl: baseUrl.trim() } : {}),
},
reactivityKeys: integrationWriteKeys,
});
if (Exit.isFailure(exit)) {
setAddError(addIntegrationErrorMessage(exit, resolvedSourceId, "Failed to add Google"));
setAdding(false);
return;
}
props.onComplete(String(exit.value.slug));
};
return (
<div className="flex flex-1 flex-col gap-6">
<div>
<h1 className="text-xl font-semibold text-foreground">Add Google integration</h1>
<p className="mt-1 text-[13px] text-muted-foreground">
Bundle Google APIs into one integration with a shared OAuth consent.
</p>
</div>
<GoogleProductPicker
selectedPresetIds={selectedPresetIds}
onToggle={toggleBundlePreset}
customUrls={customDiscoveryUrls}
onAddCustomUrl={addCustomDiscoveryUrl}
onRemoveCustomUrl={removeCustomDiscoveryUrl}
/>
<OpenApiSourceDetailsFields
title="Google"
subtitle={`${bundleDiscoveryUrls.length} Google API${
bundleDiscoveryUrls.length !== 1 ? "s" : ""
} · one shared OAuth consent`}
identity={identity}
description={resolvedDescription}
onDescriptionChange={setDescriptionDraft}
baseUrl={baseUrl}
onBaseUrlChange={setBaseUrl}
baseUrlLabel="Base URL override (optional)"
faviconIcon={GOOGLE_BUNDLE_FAVICON}
faviconUrl={baseUrl}
/>
{slugAlreadyExists && !adding && <SlugCollisionAlert slug={resolvedSourceId} />}
{addError && <FormErrorAlert message={addError} />}
<FloatActions>
<Button variant="ghost" onClick={() => props.onCancel()} disabled={adding}>
Cancel
</Button>
<Button onClick={() => void handleAdd()} disabled={!canAdd} loading={adding}>
Connect Google
</Button>
</FloatActions>
</div>
);
}