| 1 | /** |
| 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. |
| 3 | * |
| 4 | * This source code is licensed under the MIT license found in the |
| 5 | * LICENSE file in the root directory of this source tree. |
| 6 | */ |
| 7 | |
| 8 | import type {ThemeColor} from './theme'; |
| 9 | import type {PreferredSubmitCommand} from './types'; |
| 10 | |
| 11 | import {Button} from 'isl-components/Button'; |
| 12 | import {Checkbox} from 'isl-components/Checkbox'; |
| 13 | import {Dropdown} from 'isl-components/Dropdown'; |
| 14 | import {Icon} from 'isl-components/Icon'; |
| 15 | import {Kbd} from 'isl-components/Kbd'; |
| 16 | import {KeyCode, Modifier} from 'isl-components/KeyboardShortcuts'; |
| 17 | import {Subtle} from 'isl-components/Subtle'; |
| 18 | import {Tooltip} from 'isl-components/Tooltip'; |
| 19 | import {useAtom, useAtomValue} from 'jotai'; |
| 20 | import {Suspense} from 'react'; |
| 21 | import {nullthrows, tryJsonParse} from 'shared/utils'; |
| 22 | import { |
| 23 | distantRebaseWarningEnabled, |
| 24 | rebaseOffWarmWarningEnabled, |
| 25 | rebaseOntoMasterWarningEnabled, |
| 26 | } from './Commit'; |
| 27 | import {condenseObsoleteStacks} from './CommitTreeList'; |
| 28 | import {Column, Row} from './ComponentUtils'; |
| 29 | import {confirmShouldSubmitEnabledAtom} from './ConfirmSubmitStack'; |
| 30 | import {DropdownField, DropdownFields} from './DropdownFields'; |
| 31 | import {useShowKeyboardShortcutsHelp} from './ISLShortcuts'; |
| 32 | import {Link} from './Link'; |
| 33 | import {RestackBehaviorSetting} from './RestackBehavior'; |
| 34 | import {Setting} from './Setting'; |
| 35 | import { |
| 36 | currentExperimentalFeaturesList, |
| 37 | hasExperimentalFeatures, |
| 38 | } from './atoms/experimentalFeatureAtoms'; |
| 39 | import {codeReviewProvider} from './codeReview/CodeReviewInfo'; |
| 40 | import {showDiffNumberConfig} from './codeReview/DiffBadge'; |
| 41 | import {SubmitAsDraftCheckbox} from './codeReview/DraftCheckbox'; |
| 42 | import { |
| 43 | branchPRsSupported, |
| 44 | experimentalBranchPRsEnabled, |
| 45 | overrideDisabledSubmitModes, |
| 46 | } from './codeReview/github/branchPrState'; |
| 47 | import {debugToolsEnabledState} from './debug/DebugToolsState'; |
| 48 | import {externalMergeToolAtom} from './externalMergeTool'; |
| 49 | import {t, T} from './i18n'; |
| 50 | import {configBackedAtom, readAtom} from './jotaiUtils'; |
| 51 | import {AutoResolveSettingCheckbox} from './mergeConflicts/state'; |
| 52 | import {SetConfigOperation} from './operations/SetConfigOperation'; |
| 53 | import {useRunOperation} from './operationsState'; |
| 54 | import platform from './platform'; |
| 55 | import {irrelevantCwdDisplayModeAtom} from './repositoryData'; |
| 56 | import {renderCompactAtom, useZoomShortcut, zoomUISettingAtom} from './responsive'; |
| 57 | import {mainCommandName, repositoryInfo} from './serverAPIState'; |
| 58 | import {themeState, useThemeShortcut} from './theme'; |
| 59 | |
| 60 | import './SettingsTooltip.css'; |
| 61 | import {enableSaplingDebugFlag, enableSaplingVerboseFlag} from './atoms/debugToolAtoms'; |
| 62 | |
| 63 | export function SettingsGearButton() { |
| 64 | useThemeShortcut(); |
| 65 | useZoomShortcut(); |
| 66 | const showShortcutsHelp = useShowKeyboardShortcutsHelp(); |
| 67 | return ( |
| 68 | <Tooltip |
| 69 | trigger="click" |
| 70 | component={dismiss => ( |
| 71 | <SettingsDropdown dismiss={dismiss} showShortcutsHelp={showShortcutsHelp} /> |
| 72 | )} |
| 73 | group="topbar" |
| 74 | placement="bottom"> |
| 75 | <Button icon data-testid="settings-gear-button"> |
| 76 | <Icon icon="gear" /> |
| 77 | </Button> |
| 78 | </Tooltip> |
| 79 | ); |
| 80 | } |
| 81 | |
| 82 | function SettingsDropdown({ |
| 83 | dismiss, |
| 84 | showShortcutsHelp, |
| 85 | }: { |
| 86 | dismiss: () => unknown; |
| 87 | showShortcutsHelp: () => unknown; |
| 88 | }) { |
| 89 | const [theme, setTheme] = useAtom(themeState); |
| 90 | const [repoInfo, setRepoInfo] = useAtom(repositoryInfo); |
| 91 | const runOperation = useRunOperation(); |
| 92 | const [showDiffNumber, setShowDiffNumber] = useAtom(showDiffNumberConfig); |
| 93 | return ( |
| 94 | <DropdownFields title={<T>Settings</T>} icon="gear" data-testid="settings-dropdown"> |
| 95 | <Button |
| 96 | style={{justifyContent: 'center', gap: 0}} |
| 97 | icon |
| 98 | onClick={() => { |
| 99 | dismiss(); |
| 100 | showShortcutsHelp(); |
| 101 | }}> |
| 102 | <T |
| 103 | replace={{ |
| 104 | $shortcut: <Kbd keycode={KeyCode.QuestionMark} modifiers={[Modifier.SHIFT]} />, |
| 105 | }}> |
| 106 | View Keyboard Shortcuts - $shortcut |
| 107 | </T> |
| 108 | </Button> |
| 109 | {platform.theme != null ? null : ( |
| 110 | <Setting title={<T>Theme</T>}> |
| 111 | <Dropdown |
| 112 | options={ |
| 113 | [ |
| 114 | {value: 'light', name: 'Light'}, |
| 115 | {value: 'dark', name: 'Dark'}, |
| 116 | ] as Array<{value: ThemeColor; name: string}> |
| 117 | } |
| 118 | value={theme} |
| 119 | onChange={event => setTheme(event.currentTarget.value as ThemeColor)} |
| 120 | /> |
| 121 | <div style={{marginTop: 'var(--pad)'}}> |
| 122 | <Subtle> |
| 123 | <T>Toggle: </T> |
| 124 | <Kbd keycode={KeyCode.T} modifiers={[Modifier.ALT]} /> |
| 125 | </Subtle> |
| 126 | </div> |
| 127 | </Setting> |
| 128 | )} |
| 129 | |
| 130 | <Setting title={<T>UI Scale</T>}> |
| 131 | <ZoomUISetting /> |
| 132 | </Setting> |
| 133 | <Setting title={<T>Commits</T>}> |
| 134 | <Column alignStart> |
| 135 | <RenderCompactSetting /> |
| 136 | <CondenseObsoleteSetting /> |
| 137 | <RebaseOffWarmWarningSetting /> |
| 138 | <DistantRebaseWarningSetting /> |
| 139 | <RebaseOntoMasterWarningSetting /> |
| 140 | <DeemphasizeIrrelevantCommitsSetting /> |
| 141 | </Column> |
| 142 | </Setting> |
| 143 | <Setting title={<T>Conflicts</T>}> |
| 144 | <AutoResolveSettingCheckbox /> |
| 145 | <RestackBehaviorSetting /> |
| 146 | </Setting> |
| 147 | {/* TODO: enable this setting when there is actually a chocie to be made here. */} |
| 148 | {/* <Setting |
| 149 | title={<T>Language</T>} |
| 150 | description={<T>Locale for translations used in the UI. Currently only en supported.</T>}> |
| 151 | <Dropdown value="en" options=['en'] /> |
| 152 | </Setting> */} |
| 153 | {repoInfo == null ? ( |
| 154 | <Icon icon="loading" /> |
| 155 | ) : repoInfo.codeReviewSystem.type === 'github' ? ( |
| 156 | <Setting |
| 157 | title={<T>Preferred Code Review Submit Method</T>} |
| 158 | description={ |
| 159 | <> |
| 160 | <T>How to submit code for code review on GitHub.</T>{' '} |
| 161 | {/* TODO: update this to document branchign PRs */} |
| 162 | <Link href="https://sapling-scm.com/docs/git/intro#pull-requests"> |
| 163 | <T>Learn More</T> |
| 164 | </Link> |
| 165 | </> |
| 166 | }> |
| 167 | <Dropdown |
| 168 | value={repoInfo.preferredSubmitCommand ?? 'not set'} |
| 169 | options={(repoInfo.preferredSubmitCommand == null |
| 170 | ? [{value: 'not set', name: '(not set)'}] |
| 171 | : [] |
| 172 | ).concat([ |
| 173 | {value: 'ghstack', name: 'sl ghstack (stacked PRs)'}, |
| 174 | {value: 'pr', name: 'sl pr (stacked PRs)'}, |
| 175 | ...(readAtom(branchPRsSupported) |
| 176 | ? [{value: 'push', name: 'sl push (branching PR)'}] |
| 177 | : []), |
| 178 | ])} |
| 179 | onChange={event => { |
| 180 | const value = (event as React.FormEvent<HTMLSelectElement>).currentTarget.value as |
| 181 | | PreferredSubmitCommand |
| 182 | | 'not set'; |
| 183 | if (value === 'not set') { |
| 184 | return; |
| 185 | } |
| 186 | |
| 187 | runOperation( |
| 188 | new SetConfigOperation('local', 'github.preferred_submit_command', value), |
| 189 | ); |
| 190 | setRepoInfo(info => ({...nullthrows(info), preferredSubmitCommand: value})); |
| 191 | }} |
| 192 | /> |
| 193 | </Setting> |
| 194 | ) : null} |
| 195 | <Setting title={<T>Code Review</T>}> |
| 196 | <div className="multiple-settings"> |
| 197 | <Checkbox |
| 198 | checked={showDiffNumber} |
| 199 | onChange={checked => { |
| 200 | setShowDiffNumber(checked); |
| 201 | }}> |
| 202 | <T>Show copyable Diff / Pull Request numbers inline for each commit</T> |
| 203 | </Checkbox> |
| 204 | <ConfirmSubmitStackSetting /> |
| 205 | <SubmitAsDraftCheckbox forceShow /> |
| 206 | </div> |
| 207 | </Setting> |
| 208 | {platform.canCustomizeFileOpener && ( |
| 209 | <Setting title={<T>Environment</T>}> |
| 210 | <Column alignStart> |
| 211 | <OpenFilesCmdSetting /> |
| 212 | <ExternalMergeToolSetting /> |
| 213 | </Column> |
| 214 | </Setting> |
| 215 | )} |
| 216 | <Suspense>{platform.Settings == null ? null : <platform.Settings />}</Suspense> |
| 217 | <DebugToolsField /> |
| 218 | </DropdownFields> |
| 219 | ); |
| 220 | } |
| 221 | |
| 222 | function ConfirmSubmitStackSetting() { |
| 223 | const [value, setValue] = useAtom(confirmShouldSubmitEnabledAtom); |
| 224 | const provider = useAtomValue(codeReviewProvider); |
| 225 | if (provider == null || !provider.supportSubmittingAsDraft) { |
| 226 | return null; |
| 227 | } |
| 228 | return ( |
| 229 | <Tooltip |
| 230 | title={t( |
| 231 | 'This lets you choose to submit as draft and provide an update message. ' + |
| 232 | 'If false, no confirmation is shown and it will submit as draft if you previously ' + |
| 233 | 'checked the submit as draft checkbox.', |
| 234 | )}> |
| 235 | <Checkbox |
| 236 | checked={value} |
| 237 | onChange={checked => { |
| 238 | setValue(checked); |
| 239 | }}> |
| 240 | <T>Show confirmation when submitting a stack</T> |
| 241 | </Checkbox> |
| 242 | </Tooltip> |
| 243 | ); |
| 244 | } |
| 245 | |
| 246 | function RenderCompactSetting() { |
| 247 | const [value, setValue] = useAtom(renderCompactAtom); |
| 248 | return ( |
| 249 | <Tooltip |
| 250 | title={t( |
| 251 | 'Render commits in the tree more compactly, by reducing spacing and not wrapping Diff info to multiple lines. ' + |
| 252 | 'May require more horizontal scrolling.', |
| 253 | )}> |
| 254 | <Checkbox |
| 255 | checked={value} |
| 256 | onChange={checked => { |
| 257 | setValue(checked); |
| 258 | }}> |
| 259 | <T>Compact Mode</T> |
| 260 | </Checkbox> |
| 261 | </Tooltip> |
| 262 | ); |
| 263 | } |
| 264 | |
| 265 | function CondenseObsoleteSetting() { |
| 266 | const [value, setValue] = useAtom(condenseObsoleteStacks); |
| 267 | return ( |
| 268 | <Tooltip |
| 269 | title={t( |
| 270 | 'Visually condense a continuous stack of obsolete commits into just the top and bottom commits.', |
| 271 | )}> |
| 272 | <Checkbox |
| 273 | data-testid="condense-obsolete-stacks" |
| 274 | checked={value !== false} |
| 275 | onChange={checked => { |
| 276 | setValue(checked); |
| 277 | }}> |
| 278 | <T>Condense Obsolete Stacks</T> |
| 279 | </Checkbox> |
| 280 | </Tooltip> |
| 281 | ); |
| 282 | } |
| 283 | |
| 284 | function DeemphasizeIrrelevantCommitsSetting() { |
| 285 | const [value, setValue] = useAtom(irrelevantCwdDisplayModeAtom); |
| 286 | return ( |
| 287 | <Tooltip |
| 288 | title={t( |
| 289 | 'How to display commits which only change files in an unrelated directory to your current working directory.\n', |
| 290 | )}> |
| 291 | <div className="dropdown-container setting-inline-dropdown"> |
| 292 | <T>Cwd-Irrelevant Commits</T> |
| 293 | <Dropdown<{value: typeof value; name: string}> |
| 294 | data-testid="cwd-irrelevant-commits" |
| 295 | options={[ |
| 296 | {value: 'show', name: t('Show')}, |
| 297 | {value: 'deemphasize', name: t('Deemphasize')}, |
| 298 | {value: 'hide', name: t('Hide')}, |
| 299 | ]} |
| 300 | value={value} |
| 301 | onChange={event => { |
| 302 | setValue(event.currentTarget.value as typeof value); |
| 303 | }} |
| 304 | /> |
| 305 | </div> |
| 306 | </Tooltip> |
| 307 | ); |
| 308 | } |
| 309 | |
| 310 | function RebaseOffWarmWarningSetting() { |
| 311 | const [value, setValue] = useAtom(rebaseOffWarmWarningEnabled); |
| 312 | return ( |
| 313 | <Tooltip |
| 314 | title={t( |
| 315 | 'Show a warning when rebasing off a commit that is not warm (i.e. not in the current stack).', |
| 316 | )}> |
| 317 | <Checkbox |
| 318 | data-testid="rebase-off-warm-warning-enabled" |
| 319 | checked={value} |
| 320 | onChange={checked => { |
| 321 | setValue(checked); |
| 322 | }}> |
| 323 | <T>Show Warning on Rebase Off Warm</T> |
| 324 | </Checkbox> |
| 325 | </Tooltip> |
| 326 | ); |
| 327 | } |
| 328 | |
| 329 | function DistantRebaseWarningSetting() { |
| 330 | const [value, setValue] = useAtom(distantRebaseWarningEnabled); |
| 331 | return ( |
| 332 | <Tooltip |
| 333 | title={t( |
| 334 | 'Show a warning when rebasing onto a commit that is significantly older than the current commit.', |
| 335 | )}> |
| 336 | <Checkbox |
| 337 | data-testid="distant-rebase-warning-enabled" |
| 338 | checked={value} |
| 339 | onChange={checked => { |
| 340 | setValue(checked); |
| 341 | }}> |
| 342 | <T>Show Warning on Distant Rebase</T> |
| 343 | </Checkbox> |
| 344 | </Tooltip> |
| 345 | ); |
| 346 | } |
| 347 | |
| 348 | function RebaseOntoMasterWarningSetting() { |
| 349 | const [value, setValue] = useAtom(rebaseOntoMasterWarningEnabled); |
| 350 | return ( |
| 351 | <Tooltip |
| 352 | title={t( |
| 353 | 'Show a warning when rebasing directly onto master branch, which can cause unexpected failures and slower builds.', |
| 354 | )}> |
| 355 | <Checkbox |
| 356 | data-testid="rebase-master-warning-enabled" |
| 357 | checked={value} |
| 358 | onChange={checked => { |
| 359 | setValue(checked); |
| 360 | }}> |
| 361 | <T>Show Warning on Rebase onto Master</T> |
| 362 | </Checkbox> |
| 363 | </Tooltip> |
| 364 | ); |
| 365 | } |
| 366 | |
| 367 | export const openFileCmdAtom = configBackedAtom<string | null>( |
| 368 | 'isl.open-file-cmd', |
| 369 | null, |
| 370 | /* readonly */ true, |
| 371 | /* use raw value */ true, |
| 372 | ); |
| 373 | |
| 374 | function OpenFilesCmdSetting() { |
| 375 | const cmdRaw = useAtomValue(openFileCmdAtom); |
| 376 | const cmd = cmdRaw == null ? null : ((tryJsonParse(cmdRaw) as string | Array<string>) ?? cmdRaw); |
| 377 | const cmdEl = |
| 378 | cmd == null ? ( |
| 379 | <T>OS Default Program</T> |
| 380 | ) : ( |
| 381 | <code>{Array.isArray(cmd) ? cmd.join(' ') : cmd}</code> |
| 382 | ); |
| 383 | return ( |
| 384 | <Tooltip |
| 385 | component={() => ( |
| 386 | <div> |
| 387 | <div> |
| 388 | <T>You can configure how to open files from ISL via</T> |
| 389 | </div> |
| 390 | <pre>sl config --user isl.open-file-cmd "/path/to/command"</pre> |
| 391 | <div> |
| 392 | <T>or</T> |
| 393 | </div> |
| 394 | <pre>sl config --user isl.open-file-cmd '["cmd", "with", "args"]'</pre> |
| 395 | </div> |
| 396 | )}> |
| 397 | <Row> |
| 398 | <T replace={{$cmd: cmdEl}}>Open files in: $cmd</T> |
| 399 | <Subtle> |
| 400 | <T>How to configure?</T> |
| 401 | </Subtle> |
| 402 | <Icon icon="question" /> |
| 403 | </Row> |
| 404 | </Tooltip> |
| 405 | ); |
| 406 | } |
| 407 | |
| 408 | function ExternalMergeToolSetting() { |
| 409 | const mergeTool = useAtomValue(externalMergeToolAtom); |
| 410 | const cmdEl = mergeTool == null ? <T>None</T> : <code>{mergeTool}</code>; |
| 411 | return ( |
| 412 | <Tooltip |
| 413 | component={() => ( |
| 414 | <div> |
| 415 | <div style={{alignItems: 'flex-start', maxWidth: 400}}> |
| 416 | <T |
| 417 | replace={{ |
| 418 | $help: <code>sl help config.merge-tools</code>, |
| 419 | $configedit: <code>sl config --edit</code>, |
| 420 | $mymergetool: <code>merge-tools.mymergetool</code>, |
| 421 | $uimerge: <code>ui.merge = mymergetool</code>, |
| 422 | $gui: <code>merge-tools.mymergetool.gui</code>, |
| 423 | $local: <code>--local</code>, |
| 424 | $br: ( |
| 425 | <> |
| 426 | <br /> |
| 427 | <br /> |
| 428 | </> |
| 429 | ), |
| 430 | }}> |
| 431 | You can configure Sapling and ISL to use a custom external merge tool, which is used |
| 432 | when a merge conflict is detected.$br Define your tool with $configedit (or with |
| 433 | $local to configure only for the current repository), by setting $mymergetool and |
| 434 | $uimerge$brCLI merge tools like vimdiff won't be used from ISL. Ensure $gui is set to |
| 435 | True.$br For more information, see: $help |
| 436 | </T> |
| 437 | </div> |
| 438 | </div> |
| 439 | )}> |
| 440 | <Row> |
| 441 | <T replace={{$cmd: cmdEl}}>External Merge Tool: $cmd</T> |
| 442 | <Subtle> |
| 443 | <T>How to configure?</T> |
| 444 | </Subtle> |
| 445 | <Icon icon="question" /> |
| 446 | </Row> |
| 447 | </Tooltip> |
| 448 | ); |
| 449 | } |
| 450 | |
| 451 | function ZoomUISetting() { |
| 452 | const [zoom, setZoom] = useAtom(zoomUISettingAtom); |
| 453 | function roundToPercent(n: number): number { |
| 454 | return Math.round(n * 100) / 100; |
| 455 | } |
| 456 | return ( |
| 457 | <div className="zoom-setting"> |
| 458 | <Tooltip title={t('Decrease UI Zoom')}> |
| 459 | <Button |
| 460 | icon |
| 461 | onClick={() => { |
| 462 | setZoom(roundToPercent(zoom - 0.1)); |
| 463 | }}> |
| 464 | <Icon icon="zoom-out" /> |
| 465 | </Button> |
| 466 | </Tooltip> |
| 467 | <span>{`${Math.round(100 * zoom)}%`}</span> |
| 468 | <Tooltip title={t('Increase UI Zoom')}> |
| 469 | <Button |
| 470 | icon |
| 471 | onClick={() => { |
| 472 | setZoom(roundToPercent(zoom + 0.1)); |
| 473 | }}> |
| 474 | <Icon icon="zoom-in" /> |
| 475 | </Button> |
| 476 | </Tooltip> |
| 477 | <div style={{width: '20px'}} /> |
| 478 | <label> |
| 479 | <T>Presets:</T> |
| 480 | </label> |
| 481 | <Button |
| 482 | style={{fontSize: '80%'}} |
| 483 | icon |
| 484 | onClick={() => { |
| 485 | setZoom(0.8); |
| 486 | }}> |
| 487 | <T>Small</T> |
| 488 | </Button> |
| 489 | <Button |
| 490 | icon |
| 491 | onClick={() => { |
| 492 | setZoom(1.0); |
| 493 | }}> |
| 494 | <T>Normal</T> |
| 495 | </Button> |
| 496 | <Button |
| 497 | style={{fontSize: '120%'}} |
| 498 | icon |
| 499 | onClick={() => { |
| 500 | setZoom(1.2); |
| 501 | }}> |
| 502 | <T>Large</T> |
| 503 | </Button> |
| 504 | </div> |
| 505 | ); |
| 506 | } |
| 507 | |
| 508 | function DebugToolsField() { |
| 509 | const [isDebug, setIsDebug] = useAtom(debugToolsEnabledState); |
| 510 | const [overrideDisabledSubmit, setOverrideDisabledSubmit] = useAtom(overrideDisabledSubmitModes); |
| 511 | const [debugFlag, setDebugFlag] = useAtom(enableSaplingDebugFlag); |
| 512 | const [verboseFlag, setVerboseFlag] = useAtom(enableSaplingVerboseFlag); |
| 513 | const provider = useAtomValue(codeReviewProvider); |
| 514 | const commandName = useAtomValue(mainCommandName); |
| 515 | |
| 516 | const [branchPrsEnabled, setBranchPrsEnabled] = useAtom(experimentalBranchPRsEnabled); |
| 517 | |
| 518 | return ( |
| 519 | <DropdownField title={t('Debug Tools & Experimental')}> |
| 520 | <Column alignStart> |
| 521 | <Checkbox |
| 522 | checked={isDebug} |
| 523 | onChange={checked => { |
| 524 | setIsDebug(checked); |
| 525 | }}> |
| 526 | <T>Enable Debug Tools</T> |
| 527 | </Checkbox> |
| 528 | <ExperimentalFeaturesCheckbox /> |
| 529 | {provider?.submitDisabledReason?.() != null && ( |
| 530 | <Checkbox |
| 531 | checked={overrideDisabledSubmit} |
| 532 | onChange={setOverrideDisabledSubmit} |
| 533 | data-testid="force-enable-github-submit"> |
| 534 | <T>Force enable `sl pr submit` and `sl ghstack submit`</T> |
| 535 | </Checkbox> |
| 536 | )} |
| 537 | {provider?.supportBranchingPrs === true && ( |
| 538 | <Checkbox |
| 539 | checked={branchPrsEnabled} |
| 540 | onChange={checked => { |
| 541 | setBranchPrsEnabled(checked); |
| 542 | }}> |
| 543 | <T>Enable Experimental Branching PRs for GitHub</T> |
| 544 | </Checkbox> |
| 545 | )} |
| 546 | <Row> |
| 547 | <T |
| 548 | replace={{ |
| 549 | $sl: <code>{commandName}</code>, |
| 550 | $verbose: ( |
| 551 | <Checkbox checked={verboseFlag} onChange={setVerboseFlag}> |
| 552 | <code>--verbose</code> |
| 553 | </Checkbox> |
| 554 | ), |
| 555 | $debug: ( |
| 556 | <Checkbox checked={debugFlag} onChange={setDebugFlag}> |
| 557 | <code>--debug</code> |
| 558 | </Checkbox> |
| 559 | ), |
| 560 | }}> |
| 561 | Pass extra flags to $sl: $verbose $debug |
| 562 | </T> |
| 563 | </Row> |
| 564 | </Column> |
| 565 | </DropdownField> |
| 566 | ); |
| 567 | } |
| 568 | |
| 569 | function ExperimentalFeaturesCheckbox() { |
| 570 | const [experimentalFeaturesEnabled, setExperimentalFeaturesEnabled] = |
| 571 | useAtom(hasExperimentalFeatures); |
| 572 | |
| 573 | if (currentExperimentalFeaturesList.length === 0) { |
| 574 | return null; |
| 575 | } |
| 576 | |
| 577 | return ( |
| 578 | <Tooltip |
| 579 | title={t( |
| 580 | `Enable experimental features that are still being developed and may not work as expected. |
| 581 | |
| 582 | Current experimental features: ${currentExperimentalFeaturesList.join(', ')}`, |
| 583 | )}> |
| 584 | <Checkbox |
| 585 | checked={experimentalFeaturesEnabled} |
| 586 | onChange={checked => { |
| 587 | setExperimentalFeaturesEnabled(checked); |
| 588 | }}> |
| 589 | <T>Enable Experimental Features</T> |
| 590 | </Checkbox> |
| 591 | </Tooltip> |
| 592 | ); |
| 593 | } |
| 594 | |