Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/react/src/api/oauth-popup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ export const openOAuthPopup = <TAuth>(input: OpenOAuthPopupInput<TAuth>): (() =>
/** Close the popup window if it's still open. Swallows cross-origin errors. */
const closePopup = (popup: Window | null) => {
if (!popup) return;
// oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: cross-origin popup state can throw and cleanup is best-effort
try {
if (!popup.closed) popup.close();
} catch {
Expand Down Expand Up @@ -129,6 +130,7 @@ export const openOAuthPopup = <TAuth>(input: OpenOAuthPopupInput<TAuth>): (() =>
const pollMs = input.closedPollMs ?? 500;
pollHandle = setInterval(() => {
let isClosed = false;
// oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: browser popup.closed can throw while navigating cross-origin
try {
isClosed = popup.closed;
} catch {
Expand Down
4 changes: 2 additions & 2 deletions packages/react/src/api/optimistic.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ export const useConnectionsWithPendingRemovals = (scopeId: ScopeId) => {
if (!AsyncResult.isSuccess(result) || pending.length === 0) return;

const serverIds = new Set(
result.value.map((connection: { readonly id: string }) => connection.id as string),
result.value.map((connection: { readonly id: string }) => connection.id),
);
for (const entry of pending) {
if (!serverIds.has(entry.id)) remove(entry.id);
Expand All @@ -189,7 +189,7 @@ export const useConnectionsWithPendingRemovals = (scopeId: ScopeId) => {
if (pending.length === 0) return connections;
const hiddenIds = new Set(pending.map((entry) => entry.id));
return connections.filter(
(connection: { readonly id: string }) => !hiddenIds.has(connection.id as string),
(connection: { readonly id: string }) => !hiddenIds.has(connection.id),
);
}),
[result, pending],
Expand Down
1 change: 1 addition & 0 deletions packages/react/src/components/form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ function FormDescription({ className, ...props }: React.ComponentProps<"p">) {

function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
const { error, formMessageId } = useFormField();
// oxlint-disable-next-line executor/no-unknown-error-message -- boundary: react-hook-form field errors carry public validation text
const body = error ? String(error?.message ?? "") : props.children;

if (!body) {
Expand Down
18 changes: 10 additions & 8 deletions packages/react/src/pages/policies.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useState } from "react";
import { useAtomSet, useAtomValue } from "@effect/atom-react";
import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
import * as Exit from "effect/Exit";
import { generateKeyBetween } from "fractional-indexing";
import { ChevronDownIcon } from "lucide-react";
import { PolicyId, type ToolPolicyAction } from "@executor-js/sdk";
Expand Down Expand Up @@ -253,7 +254,7 @@ export function PoliciesPage() {
const scopeId = useScope();
const policies = useAtomValue(policiesOptimisticAtom(scopeId));
const doCreate = useAtomSet(createPolicyOptimistic(scopeId), {
mode: "promise",
mode: "promiseExit",
});
const doUpdate = useAtomSet(updatePolicyOptimistic(scopeId), {
mode: "promise",
Expand All @@ -265,15 +266,16 @@ export function PoliciesPage() {

const handleCreate = async (input: { pattern: string; action: ToolPolicyAction }) => {
setBusy(true);
try {
await doCreate({
params: { scopeId },
payload: { pattern: input.pattern, action: input.action },
reactivityKeys: policyWriteKeys,
});
} finally {
const exit = await doCreate({
params: { scopeId },
payload: { pattern: input.pattern, action: input.action },
reactivityKeys: policyWriteKeys,
});
if (Exit.isFailure(exit)) {
setBusy(false);
return;
}
setBusy(false);
};

const handleUpdate = async (id: string, action: ToolPolicyAction) => {
Expand Down
32 changes: 15 additions & 17 deletions packages/react/src/pages/source-detail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Suspense, useEffect, useMemo, useState } from "react";
import { useNavigate } from "@tanstack/react-router";
import { useAtomValue, useAtomSet, useAtomRefresh } from "@effect/atom-react";
import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
import * as Exit from "effect/Exit";
import { effectivePolicyFromSorted } from "@executor-js/sdk";
import {
policiesOptimisticAtom,
Expand Down Expand Up @@ -31,8 +32,8 @@ export function SourceDetailPage(props: { namespace: string }) {
const policies = useAtomValue(policiesOptimisticAtom(scopeId));
const refreshSources = useAtomRefresh(sourcesAtom(scopeId));
const refreshTools = useAtomRefresh(sourceToolsAtom(namespace, scopeId));
const doRemove = useAtomSet(removeSource, { mode: "promise" });
const doRefresh = useAtomSet(refreshSource, { mode: "promise" });
const doRemove = useAtomSet(removeSource, { mode: "promiseExit" });
const doRefresh = useAtomSet(refreshSource, { mode: "promiseExit" });
const policyActions = usePolicyActions(scopeId);
const navigate = useNavigate();

Expand Down Expand Up @@ -117,28 +118,25 @@ export function SourceDetailPage(props: { namespace: string }) {

const handleDelete = async () => {
setDeleting(true);
try {
await doRemove({
params: { scopeId, sourceId: namespace },
reactivityKeys: sourceWriteKeys,
});
void navigate({ to: "/" });
} catch {
const exit = await doRemove({
params: { scopeId, sourceId: namespace },
reactivityKeys: sourceWriteKeys,
});
if (Exit.isFailure(exit)) {
setDeleting(false);
setConfirmDelete(false);
return;
}
void navigate({ to: "/" });
};

const handleRefresh = async () => {
setRefreshing(true);
try {
await doRefresh({
params: { scopeId, sourceId: namespace },
reactivityKeys: sourceWriteKeys,
});
} finally {
setRefreshing(false);
}
await doRefresh({
params: { scopeId, sourceId: namespace },
reactivityKeys: sourceWriteKeys,
});
setRefreshing(false);
};

const handleEditSave = () => {
Expand Down
62 changes: 32 additions & 30 deletions packages/react/src/pages/sources.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Suspense, useCallback, useMemo, useState } from "react";
import { Link, useNavigate } from "@tanstack/react-router";
import { useAtomSet } from "@effect/atom-react";
import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
import * as Exit from "effect/Exit";
import { PlusIcon } from "lucide-react";
import type { SourceDetectionResult } from "@executor-js/sdk";
import {
Expand Down Expand Up @@ -138,7 +139,7 @@ const looksLikeUrl = (raw: string): boolean => {
function ConnectDialog(props: { open: boolean; onOpenChange: (open: boolean) => void }) {
const sourcePlugins = useSourcePlugins();
const scopeId = useScope();
const doDetect = useAtomSet(detectSource, { mode: "promise" });
const doDetect = useAtomSet(detectSource, { mode: "promiseExit" });
const navigate = useNavigate();

const [query, setQuery] = useState("");
Expand All @@ -160,37 +161,38 @@ function ConnectDialog(props: { open: boolean; onOpenChange: (open: boolean) =>
if (!trimmed) return;
setDetecting(true);
setError(null);
try {
const results = await doDetect({
params: { scopeId },
payload: { url: trimmed },
});
if (results.length === 0) {
setError("Could not detect a source type from this URL. Try adding manually.");
setDetecting(false);
return;
}
const detected = bestDetection(results);
if (!detected) {
setError("Could not detect a source type from this URL. Try adding manually.");
setDetecting(false);
return;
}
const pluginKey = KIND_TO_PLUGIN_KEY[detected.kind];
if (pluginKey) {
closeAndReset();
void navigate({
to: "/sources/add/$pluginKey",
params: { pluginKey },
search: { url: trimmed, namespace: detected.namespace },
});
} else {
setError(`Detected source type "${detected.kind}" but no plugin is available for it.`);
setDetecting(false);
}
} catch {
const exit = await doDetect({
params: { scopeId },
payload: { url: trimmed },
});
if (Exit.isFailure(exit)) {
setError("Detection failed. Try adding a source manually.");
setDetecting(false);
return;
}
const results = exit.value;
if (results.length === 0) {
setError("Could not detect a source type from this URL. Try adding manually.");
setDetecting(false);
return;
}
const detected = bestDetection(results);
if (!detected) {
setError("Could not detect a source type from this URL. Try adding manually.");
setDetecting(false);
return;
}
const pluginKey = KIND_TO_PLUGIN_KEY[detected.kind];
if (pluginKey) {
closeAndReset();
void navigate({
to: "/sources/add/$pluginKey",
params: { pluginKey },
search: { url: trimmed, namespace: detected.namespace },
});
} else {
setError(`Detected source type "${detected.kind}" but no plugin is available for it.`);
setDetecting(false);
}
}, [query, doDetect, navigate, scopeId, closeAndReset]);

Expand Down
Loading