sync: update from private repo (91d8d79c)
CI / build-and-test (push) Failing after 7m0s

This commit is contained in:
oss-sync
2026-07-09 23:57:26 +00:00
parent 63d34d7cf6
commit 2044f0a2c4
81 changed files with 3456 additions and 372 deletions
+52 -3
View File
@@ -52,6 +52,7 @@ function PreferencesFormWrapper() {
user={{
defaultVisibility: auth.user.defaultVisibility ?? 'private',
defaultVisibilityOrgId: auth.user.defaultVisibilityOrgId ?? null,
hasLocalCredential: auth.user.hasLocalCredential,
}}
/>
);
@@ -161,14 +162,39 @@ function ConfigFormInner({ section }: ConfigFormProps) {
const [saving, setSaving] = useState(false);
const [toast, setToast] = useState<string | null>(null);
const [toastIsError, setToastIsError] = useState(false);
// Field keys currently flagged invalid via SectionFormProps.onValidityChange.
// While non-empty, Save & Apply is disabled: an invalid draft never reaches
// the config draft (the field withholds onChange), so allowing a save from
// another dirty field would silently discard the invalid field's edit while
// looking saved. Reporting components clear their key on unmount (see the
// contract on SectionFormProps), so section switches / row removals cannot
// leave Save bricked by a stale key.
const [invalidKeys, setInvalidKeys] = useState<ReadonlySet<string>>(new Set());
// Bumped whenever the draft is replaced wholesale out from under any
// in-progress local field state (Discard Changes, or a fresh `data`
// load/refetch below). Section forms with local "in-progress draft"
// state that doesn't purely derive from the `value` prop (see
// ExtraBodyField in LlmWorkersForm) fold this into their row keys to
// force a remount — see SectionFormProps.resetToken for the contract.
const [resetToken, setResetToken] = useState(0);
// Sync fetched config into draft
// Sync fetched config into draft. This effect only re-runs when the
// `data` object identity changes — i.e. the initial load and any
// subsequent refetch (e.g. after a save-conflict reload) — never on
// every render, so bumping resetToken here only fires on a genuine
// external replacement of the draft, matching the Discard Changes case
// below. Also clears invalidKeys: any field-local invalid draft that
// hasn't reached this fresh `data` (and therefore never reached the
// server) is being discarded along with the rest of the draft, so a
// phantom invalid key must not survive to permanently block Save.
useEffect(() => {
if (data) {
setDraft(data.config);
setEtag(data.etag);
setOverriddenByEnv(data.overriddenByEnv);
setIsDirty(false);
setInvalidKeys(new Set());
setResetToken(t => t + 1);
}
}, [data]);
@@ -177,10 +203,27 @@ function ConfigFormInner({ section }: ConfigFormProps) {
setIsDirty(true);
}, []);
const handleValidityChange = useCallback((fieldKey: string, valid: boolean) => {
setInvalidKeys(prev => {
if (valid ? !prev.has(fieldKey) : prev.has(fieldKey)) return prev; // no-op → keep identity
const next = new Set(prev);
if (valid) next.delete(fieldKey);
else next.add(fieldKey);
return next;
});
}, []);
const handleDiscard = () => {
if (data) {
setDraft(data.config);
setIsDirty(false);
// Discard doesn't change `data`, so the [data] sync effect above
// won't fire — clear any phantom invalid key and bump resetToken
// here directly so field-local draft state (e.g. ExtraBodyField's
// in-progress textarea + JSON error) remounts from the reverted
// value instead of getting stuck showing a stale error forever.
setInvalidKeys(new Set());
setResetToken(t => t + 1);
}
};
@@ -218,7 +261,7 @@ function ConfigFormInner({ section }: ConfigFormProps) {
if (error) return <div className="text-sm text-red-500">{t('configForm.loadError')}</div>;
if (!draft) return null;
const formProps = { config: draft, onChange: handleChange, overriddenByEnv };
const formProps = { config: draft, onChange: handleChange, overriddenByEnv, onValidityChange: handleValidityChange, resetToken };
const sectionForm = (() => {
switch (section) {
@@ -290,6 +333,7 @@ function ConfigFormInner({ section }: ConfigFormProps) {
})();
const dirty = dirtyCount > 0;
const blockedByInvalid = invalidKeys.size > 0;
return (
<div className="max-w-2xl pb-20">
@@ -309,6 +353,11 @@ function ConfigFormInner({ section }: ConfigFormProps) {
<span className={`text-2xs mr-auto ${toastIsError ? 'text-red-600' : 'text-emerald-700 dark:text-emerald-300'}`}>
{toast}
</span>
) : blockedByInvalid ? (
<span className="text-xs mr-auto text-red-600 dark:text-red-400 flex items-center gap-1.5 font-medium min-w-0">
<span className="inline-block w-1.5 h-1.5 rounded-full bg-red-500 flex-shrink-0" aria-hidden />
<span className="truncate">{t('configForm.invalidBlocked')}</span>
</span>
) : dirty ? (
<span className="text-xs mr-auto text-amber-800 dark:text-amber-300 flex items-center gap-1.5 font-medium min-w-0">
<span className="inline-block w-1.5 h-1.5 rounded-full bg-amber-500 animate-pulse flex-shrink-0" aria-hidden />
@@ -328,7 +377,7 @@ function ConfigFormInner({ section }: ConfigFormProps) {
</button>
<button
onClick={handleSave}
disabled={!dirty || saving}
disabled={!dirty || saving || blockedByInvalid}
className="px-3 h-8 text-xs font-semibold bg-accent text-accent-fg rounded-md hover:bg-accent-deep disabled:opacity-50 transition-colors whitespace-nowrap flex-shrink-0"
>
{saving ? 'Saving...' : (