Files
fusion/packages/dashboard/app/components/settings/sections/SourceControlSection.tsx
gsxdsm 4b7f0d2d0e fix(dashboard): theme every Settings checkbox and move all inline help behind ? icons
- One settings-scoped checkbox rule (accent, size, focus ring) so the Advanced-settings
  toggle, SettingsToggleRow, ntfy/webhook card headers, and MCP toggle stop falling
  back to the browser-default accent.
- Fix the empty/off-screen help bubble on mobile: .notification-provider-header and
  .settings-field-label-row are now positioned ancestors for SettingsHelpTip.
- Migrate every remaining inline <small> description across settings sections to the
  shared SettingsHelpTip "?" affordance (validation errors and live status stay inline).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 01:06:43 -07:00

222 lines
18 KiB
TypeScript

import { useTranslation } from "react-i18next";
import { TrackingRepoSelect, type TrackingRepoOption } from "../../TrackingRepoSelect";
import { SettingsToggleRow } from "../SettingsToggleRow";
import { SettingsHelpTip } from "../SettingsHelpTip";
import { SettingsSelectRow } from "../SettingsSelectRow";
import { SettingsTextRow } from "../SettingsTextRow";
import type { SectionBaseProps } from "./context";
export interface SourceControlSectionProps extends SectionBaseProps {
projectTrackingRepoOptions: TrackingRepoOption[];
projectTrackingRepoLoading: boolean;
projectTrackingRepoError: string | null;
}
/*
FNXC:SourceControl 2026-07-15-20:30:
Project source-control settings were split across two sections that neither named nor contained the topic: GitHub Tracking + the GitLab URL disclosure lived in "General · Project", while GitHub/GitLab authentication lived in "Merge". An operator wiring up GitLab had to visit both, and `gitlabEnabled` was writable from BOTH — two enable toggles for one key, so the last section saved won. This section is the single project-scoped home for GitHub/GitLab, which is what removes that duplicate rather than merely hiding it.
The GitLab URL block and the GitLab auth block are now ONE disclosure with ONE `gitlabEnabled` toggle in its summary. Both blocks were already governed by that same key (FN-7453), so merging them costs no behavior — the two toggles were always writing the same setting.
FNXC:SettingsScope 2026-07-15-20:30:
Rows on `gitlabEnabled`, `gitlabInstanceUrl`, `gitlabApiBaseUrl`, `gitlabAuthTokenType`, `gitlabAuthToken`, and `githubTrackingDefaultRepo` carry NO scope badge: every one of them is declared in BOTH `DEFAULT_GLOBAL_SETTINGS` and `DEFAULT_PROJECT_SETTINGS`, so a "project" badge would assert a scope the schema does not support. The section name ("Source Control · Project") carries the scope instead. The GitHub-only keys ARE project-only in the schema, so they keep their badge.
FNXC:SettingsStyling 2026-07-15-20:30:
Plain label+control+help rows render through the shared settings primitives. `githubAuthToken`/`gitlabAuthToken` use the primitive's `type: "password"` (which defaults `autocomplete="off"`) — previously these rows had to stay hand-rolled to avoid SettingsTextRow's hardcoded `type="text"` rendering a stored token in plain sight.
Rows that stay bespoke: the tracking-mode select (its help is TWO blocks, the second conditional on unrelated model settings — a descriptor `help` is one string), the tracking-repo select (custom widget), and the disclosure chrome itself.
FNXC:SettingsHelp 2026-07-15-22:40:
Staying off the primitive does NOT mean falling back to inline help. Both bespoke rows above render their copy through the same `SettingsHelpTip` as every migrated row, so the section shows one help idiom rather than a "?" on some rows and a paragraph on others. The tip hangs off the label line, so a custom control (or conditional, multi-part copy) is no obstacle — that is what its `ReactNode` children are for.
*/
export function SourceControlSection({ form, setForm, projectTrackingRepoOptions, projectTrackingRepoLoading, projectTrackingRepoError, }: SourceControlSectionProps) {
const { t } = useTranslation("app");
return (<>
<h4 className="settings-section-heading">{t("settings.general.gitHubTracking", "GitHub Tracking")}</h4>
{/*
FNXC:SettingsHelp 2026-07-15-22:40:
Both help strings ride in ONE tip rather than two inline `<small>`s. They are the same row's help — what the mode does, and how the issue title is derived — and the descriptor-based rows beside this one already carry a "?", so leaving these inline made the section render two idioms side by side.
Two strings in one bubble (not two triggers) because an operator asking "what does this control do?" wants both answers at once; the second is a caveat on the first, not a separate topic.
This is what `SettingsHelpTip`'s `ReactNode` children buy: the trailing fragment stays CONDITIONAL on the summarization settings, which a single-string descriptor `help` could not express — the reason this row is still hand-rolled.
*/}
<div className="form-group">
<div className="settings-field-label-row">
<label htmlFor="githubTrackingMode">{t("settings.general.defaultTrackingModeForNewTasks", "Default tracking mode for new tasks")}</label>
<SettingsHelpTip settingKey="githubTrackingMode">
{t("settings.general.controlsWhetherNewlyCreatedTasksHaveGitHubIssue", " Controls whether newly created tasks have GitHub issue tracking enabled by default. Individual tasks can still override this from the task detail modal. ")}
{/*
FNXC:SettingsGeneral 2026-06-22-03:20:
Tracking-issue helper copy. The FN-6771 JSX→t() extraction left a raw HTML
entity ("&apos;") in this default string. As a t() argument the string is a
plain JS value (not JSX-decoded), so the entity rendered verbatim as the
literal "&apos;" instead of an apostrophe. Use a real apostrophe so the copy
reads correctly in both modal and embedded presentations.
*/}
{t("settings.general.trackingIssuesUseThisTaskAposSTitle", " Tracking issues use this task's title. If a task has no title yet, Fusion can summarize its description using the title summarization model in Project Models. ")}
{!form.autoSummarizeTitles && !form.useAiMergeCommitSummary && !form.githubTrackingEnabledByDefault
? t("settings.general.enableSummarizationInProjectModelsToConfigureThatModel", " Enable summarization in Project Models to configure that model.")
: ""}
</SettingsHelpTip>
</div>
<select id="githubTrackingMode" className="select" value={form.githubTrackingEnabledByDefault ? "new-tasks" : "off"} onChange={(e) => setForm((f) => ({
...f,
githubTrackingEnabledByDefault: e.target.value === "new-tasks",
}))}>
<option value="off">{t("settings.general.offDefault", "Off (default)")}</option>
<option value="new-tasks">{t("settings.general.onForNewTasks", "On for new tasks")}</option>
</select>
</div>
{/*
FNXC:GithubImportTracking 2026-07-01-00:00:
This checkbox is project-scoped and import-specific: operators can link imported GitHub issues to GitHub tracking without turning tracking on for every new task.
*/}
<SettingsToggleRow
descriptor={{
key: "githubLinkImportedIssuesToTracking",
label: t("settings.general.alwaysLinkImportedGitHubIssuesToTracking", " Always link imported GitHub issues to GitHub tracking "),
help: t("settings.general.whenEnabledImportedGitHubIssuesUseTheirSource", "When enabled, GitHub issue imports become tracked tasks that adopt the source issue. This does not turn GitHub tracking on for ordinary new tasks. Default: disabled."),
scope: "project",
}}
value={form.githubLinkImportedIssuesToTracking === true}
onChange={(v) => setForm((f) => ({ ...f, githubLinkImportedIssuesToTracking: v === true }))}
/>
{/* FNXC:SettingsHelp 2026-07-15-22:40: The row keeps its bespoke `TrackingRepoSelect` widget, but its help still reads like every neighbour's — the affordance belongs to the label line, not to the control, so a custom widget is no reason to fall back to an inline paragraph. */}
<div className="form-group">
<div className="settings-field-label-row">
<label htmlFor="projectGithubTrackingDefaultRepoGeneral">{t("settings.general.projectDefaultTrackingRepo", "Project default tracking repo")}</label>
<SettingsHelpTip settingKey="projectGithubTrackingDefaultRepoGeneral">
{t("settings.general.defaultRepoUsedWhenCreatingGitHubIssuesFor", "Default repo used when creating GitHub issues for tracked tasks. Falls back to the global default if blank.")}
</SettingsHelpTip>
</div>
<TrackingRepoSelect id="projectGithubTrackingDefaultRepoGeneral" ariaLabel="Project default tracking repo" value={form.githubTrackingDefaultRepo ?? ""} options={projectTrackingRepoOptions} loading={projectTrackingRepoLoading} error={projectTrackingRepoError ?? undefined} placeholder={t("settings.general.ownerRepo", "owner/repo")} onChange={(nextValue) => setForm((f) => ({ ...f, githubTrackingDefaultRepo: nextValue || undefined }))}/>
</div>
<SettingsToggleRow
descriptor={{
key: "githubTrackingDedupEnabled",
label: t("settings.general.searchTheTrackingRepoForLikelyDuplicatesBefore", " Search the tracking repo for likely duplicates before opening a new issue "),
help: t("settings.general.whenEnabledFusionChecksOpenAndClosedIssues", " When enabled, Fusion checks open and closed issues in the target repo for likely duplicates (using File Scope paths and key symptoms) before creating a new tracking issue. Uncheck to always create a new issue. Default: enabled. "),
scope: "project",
}}
value={form.githubTrackingDedupEnabled !== false}
onChange={(v) => setForm((f) => ({ ...f, githubTrackingDedupEnabled: v === true }))}
/>
<h4 className="settings-section-heading settings-section-heading--spaced">{t("settings.merge.gitHubAuthentication", "GitHub Authentication")}</h4>
{/* FNXC:SettingsStyling 2026-07-15-17:35: No `help` — this row carried no help copy before the migration, and inventing one would be new operator-facing text rather than a restyle. */}
<SettingsSelectRow
descriptor={{
key: "githubAuthMode",
label: t("settings.merge.gitHubAuthMode", "GitHub auth mode"),
scope: "project",
options: [
{ value: "gh-cli", label: t("settings.merge.gitHubCLIGhAuth", "GitHub CLI (gh auth) (default)") },
{ value: "token", label: t("settings.merge.personalAccessToken", "Personal access token") },
],
}}
value={form.githubAuthMode ?? "gh-cli"}
onChange={(v) => setForm((f) => ({ ...f, githubAuthMode: v as "gh-cli" | "token" }))}
/>
{(form.githubAuthMode ?? "gh-cli") === "token" && (
<SettingsTextRow
descriptor={{
key: "githubAuthToken",
label: t("settings.merge.gitHubPersonalAccessToken", "GitHub personal access token"),
help: t("settings.merge.githubAuthTokenHint", "No default — unset."),
scope: "project",
type: "password",
}}
value={form.githubAuthToken ?? ""}
onChange={(v) => setForm((f) => ({ ...f, githubAuthToken: v || undefined }))}
/>
)}
<h4 className="settings-section-heading settings-section-heading--spaced">{t("settings.general.gitLabConfiguration", "GitLab Configuration")}</h4>
{/*
FNXC:GitLabEnablement 2026-07-02-00:00:
FN-7453 keeps saved GitLab URL settings separate from the active integration switch. The disclosure is collapsed by default to reduce Settings noise; the summary toggle remains reachable without expanding advanced self-managed URL fields.
FNXC:SourceControl 2026-07-15-20:30:
This is the ONLY `gitlabEnabled` control in the project scope. The Merge section rendered a second one (id `mergeGitlabEnabled`) governing the same key from a different screen; both are now this one toggle.
*/}
<details className="settings-gitlab-disclosure" data-testid="project-gitlab-configuration-disclosure">
<summary>
<span className="settings-gitlab-disclosure__title">{t("settings.general.gitLabConfiguration", "GitLab Configuration")}</span>
<label className="checkbox-label settings-gitlab-disclosure__toggle" htmlFor="gitlabEnabled" onClick={(event) => event.stopPropagation()}>
<input id="gitlabEnabled" type="checkbox" checked={form.gitlabEnabled !== false} onChange={(e) => setForm((f) => ({ ...f, gitlabEnabled: e.target.checked }))}/>
{t("settings.general.enableGitLabIntegration", "Enable GitLab integration")}
</label>
{/*
FNXC:SettingsHelp 2026-07-16-12:45:
Inline disclosure hint moved behind the shared "?" affordance beside the summary title — operator requirement: no inline description paragraphs in Settings.
The copy stays conditional on `gitlabEnabled` (the tip's ReactNode children carry it verbatim). The wrapping span stops propagation, same as the summary's checkbox label, so opening the tip never toggles the disclosure.
*/}
<span onClick={(event) => event.stopPropagation()}>
<SettingsHelpTip settingKey="project-gitlab-configuration">{form.gitlabEnabled === false ? t("settings.general.gitLabDisabledHint", "GitLab API imports, comments, close/reopen, and refresh operations are disabled. Saved URLs and tokens remain stored for re-enable.") : t("settings.general.gitLabEnabledHint", "Configure GitLab.com or self-managed GitLab URLs. Blank values inherit global fallbacks and then GitLab.com. No default — unset (unset behaves as enabled until explicitly disabled).")}</SettingsHelpTip>
</span>
</summary>
<div className="settings-gitlab-disclosure__body" aria-disabled={form.gitlabEnabled === false}>
<SettingsTextRow
descriptor={{
key: "gitlabInstanceUrl",
label: t("settings.general.gitLabInstanceUrl", "GitLab instance URL"),
help: t("settings.general.gitLabInstanceUrlHint", "Blank uses GitLab.com or the global default. Set an absolute http:// or https:// URL for self-managed GitLab, such as https://gitlab.example.com/gitlab."),
type: "url",
placeholder: "https://gitlab.com",
disabled: form.gitlabEnabled === false,
}}
value={form.gitlabInstanceUrl ?? ""}
onChange={(v) => setForm((f) => ({ ...f, gitlabInstanceUrl: v || undefined }))}
/>
<SettingsTextRow
descriptor={{
key: "gitlabApiBaseUrl",
label: t("settings.general.gitLabApiBaseUrlOptional", "GitLab API base URL (optional / advanced)"),
help: t("settings.general.gitLabApiBaseUrlHint", "Blank derives <instance>/api/v4. Override only when a self-managed GitLab API is served from a different absolute http:// or https:// URL."),
type: "url",
placeholder: "https://gitlab.com/api/v4",
disabled: form.gitlabEnabled === false,
}}
value={form.gitlabApiBaseUrl ?? ""}
onChange={(v) => setForm((f) => ({ ...f, gitlabApiBaseUrl: v || undefined }))}
/>
{/*
FNXC:GitLabEnablement 2026-07-02-00:00:
FN-7453 makes project GitLab auth controls collapsible and governed by the same project-scoped enable switch as URL settings. Disabling GitLab preserves saved tokens but blocks outbound API side effects before auth validation.
FNXC:SourceControl 2026-07-15-20:30:
The auth block keeps BOTH its own heading and its own enable/disable hint after the merge: the URL hint above describes what disabling does to imports/refresh, while this one describes the PRIVATE-TOKEN auth contract and the token's global fallback. Neither string is a paraphrase of the other, so collapsing them into one would delete operator-facing copy rather than deduplicate it.
FNXC:SettingsHelp 2026-07-16-12:45:
That hint now rides the shared "?" beside the auth heading instead of an inline paragraph — operator requirement: no inline description paragraphs in Settings. The copy stays conditional on `gitlabEnabled` inside the tip.
*/}
<div className="settings-field-label-row">
<h5 className="settings-section-heading">{t("settings.merge.gitLabAuthentication", "GitLab Authentication")}</h5>
<SettingsHelpTip settingKey="project-gitlab-authentication">{form.gitlabEnabled === false ? t("settings.merge.gitLabDisabledHint", "GitLab comments, close/reopen, import fetches, and refresh operations are disabled. Saved tokens remain stored for re-enable.") : t("settings.merge.gitLabAuthDetails", "Fusion uses GitLab REST API token authentication with the PRIVATE-TOKEN header. Leave the token blank to clear the project override and fall back to a configured global GitLab token or GITLAB_TOKEN where available. No default — unset (unset behaves as enabled until explicitly disabled).")}</SettingsHelpTip>
</div>
<SettingsSelectRow
descriptor={{
key: "gitlabAuthTokenType",
label: t("settings.merge.gitLabTokenType", "GitLab token type"),
disabled: form.gitlabEnabled === false,
options: [
{ value: "personal", label: t("settings.merge.gitLabPersonalAccessToken", "Personal access token (default)") },
{ value: "project", label: t("settings.merge.gitLabProjectAccessToken", "Project access token") },
{ value: "group", label: t("settings.merge.gitLabGroupAccessToken", "Group access token") },
],
}}
value={form.gitlabAuthTokenType ?? "personal"}
onChange={(v) => setForm((f) => ({ ...f, gitlabAuthTokenType: v as "personal" | "project" | "group" }))}
/>
<SettingsTextRow
descriptor={{
key: "gitlabAuthToken",
label: t("settings.merge.gitLabAccessToken", "GitLab access token"),
help: t("settings.merge.gitLabAuthTokenHint", "Read-only GitLab operations need read_api or api. Future write actions such as comments and auto-close need api. Project and group tokens are limited to their associated resource and role membership. No default — unset."),
type: "password",
disabled: form.gitlabEnabled === false,
}}
value={form.gitlabAuthToken ?? ""}
onChange={(v) => setForm((f) => ({ ...f, gitlabAuthToken: v || undefined }))}
/>
</div>
</details>
</>);
}
export default SourceControlSection;