| 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 * as stylex from '@stylexjs/stylex'; |
| 9 | import {ErrorNotice} from 'isl-components/ErrorNotice'; |
| 10 | import {Icon} from 'isl-components/Icon'; |
| 11 | import {Panels} from 'isl-components/Panels'; |
| 12 | import {useAtom, useAtomValue} from 'jotai'; |
| 13 | import {useState} from 'react'; |
| 14 | import {Center, FlexSpacer, Row, ScrollY} from '../../ComponentUtils'; |
| 15 | import {Modal} from '../../Modal'; |
| 16 | import {tracker} from '../../analytics'; |
| 17 | import {t} from '../../i18n'; |
| 18 | import {AbsorbStackEditPanel} from './AbsorbStackEditPanel'; |
| 19 | import {SplitStackEditPanel, SplitStackToolbar} from './SplitStackEditPanel'; |
| 20 | import {StackEditConfirmButtons} from './StackEditConfirmButtons'; |
| 21 | import {StackEditSubTree} from './StackEditSubTree'; |
| 22 | import {editingStackIntentionHashes, loadingStackState} from './stackEditState'; |
| 23 | |
| 24 | const styles = stylex.create({ |
| 25 | container: { |
| 26 | minWidth: '500px', |
| 27 | minHeight: '300px', |
| 28 | }, |
| 29 | loading: { |
| 30 | paddingBottom: 'calc(24px + 2 * var(--pad))', |
| 31 | }, |
| 32 | tab: { |
| 33 | fontSize: '110%', |
| 34 | padding: 'var(--halfpad) calc(2 * var(--pad))', |
| 35 | }, |
| 36 | }); |
| 37 | |
| 38 | /// Show a <Modal /> when editing a stack. |
| 39 | export function MaybeEditStackModal() { |
| 40 | const loadingState = useAtomValue(loadingStackState); |
| 41 | const [[stackIntention, stackHashes], setStackIntention] = useAtom(editingStackIntentionHashes); |
| 42 | |
| 43 | const isEditing = stackHashes.size > 0; |
| 44 | const isLoaded = isEditing && loadingState.state === 'hasValue'; |
| 45 | |
| 46 | return isLoaded ? ( |
| 47 | { |
| 48 | split: () => <LoadedSplitModal />, |
| 49 | general: () => <LoadedEditStackModal />, |
| 50 | // TODO: implement absorb model. |
| 51 | absorb: () => <LoadedAbsorbModal />, |
| 52 | }[stackIntention]() |
| 53 | ) : isEditing ? ( |
| 54 | <Modal |
| 55 | dataTestId="edit-stack-loading" |
| 56 | dismiss={() => { |
| 57 | // allow dismissing in loading state in case it gets stuck |
| 58 | setStackIntention(['general', new Set()]); |
| 59 | }}> |
| 60 | <Center |
| 61 | xstyle={[ |
| 62 | (stackIntention === 'general' || stackIntention === 'absorb') && styles.container, |
| 63 | styles.loading, |
| 64 | ]} |
| 65 | className={stackIntention === 'split' ? 'interactive-split' : undefined}> |
| 66 | {loadingState.state === 'hasError' ? ( |
| 67 | <ErrorNotice error={new Error(loadingState.error)} title={t('Loading stack failed')} /> |
| 68 | ) : ( |
| 69 | <Row> |
| 70 | <Icon icon="loading" size="M" /> |
| 71 | {(loadingState.state === 'loading' && loadingState.message) ?? null} |
| 72 | </Row> |
| 73 | )} |
| 74 | </Center> |
| 75 | </Modal> |
| 76 | ) : null; |
| 77 | } |
| 78 | |
| 79 | /** A Modal for dedicated split UI. Subset of `LoadedEditStackModal`. */ |
| 80 | function LoadedSplitModal() { |
| 81 | return ( |
| 82 | <Modal dataTestId="interactive-split-modal" className="split-single-commit-modal-contents"> |
| 83 | <SplitStackEditPanel /> |
| 84 | <Row style={{padding: 'var(--pad) 0', justifyContent: 'flex-end', zIndex: 1}}> |
| 85 | <StackEditConfirmButtons /> |
| 86 | </Row> |
| 87 | </Modal> |
| 88 | ); |
| 89 | } |
| 90 | |
| 91 | /** |
| 92 | * A Modal for dedicated absorb UI. |
| 93 | * While absorbing, the other edit stacks features are unavailable. |
| 94 | * See `StackStateWithOperationProps.absorbChunks` for details. |
| 95 | */ |
| 96 | function LoadedAbsorbModal() { |
| 97 | return ( |
| 98 | <Modal dataTestId="interactive-absorb-modal" className="absorb-modal-contents"> |
| 99 | <AbsorbStackEditPanel /> |
| 100 | <Row style={{padding: 'var(--pad) 0', justifyContent: 'flex-end', zIndex: 1}}> |
| 101 | <StackEditConfirmButtons /> |
| 102 | </Row> |
| 103 | </Modal> |
| 104 | ); |
| 105 | } |
| 106 | |
| 107 | /** A Modal for general stack editing UI. */ |
| 108 | function LoadedEditStackModal() { |
| 109 | const panels = { |
| 110 | commits: { |
| 111 | label: t('Commits'), |
| 112 | render: () => ( |
| 113 | <ScrollY maxSize="calc((100vh / var(--zoom)) - 200px)"> |
| 114 | <StackEditSubTree |
| 115 | activateSplitTab={() => { |
| 116 | setActiveTab('split'); |
| 117 | tracker.track('StackEditInlineSplitButton'); |
| 118 | }} |
| 119 | /> |
| 120 | </ScrollY> |
| 121 | ), |
| 122 | }, |
| 123 | split: { |
| 124 | label: t('Split'), |
| 125 | render: () => <SplitStackEditPanel />, |
| 126 | }, |
| 127 | // TODO: re-enable the "files" tab |
| 128 | // files: {label: t('Files'), render: () => <FileStackEditPanel />}, |
| 129 | } as const; |
| 130 | type Tab = keyof typeof panels; |
| 131 | const [activeTab, setActiveTab] = useState<Tab>('commits'); |
| 132 | |
| 133 | return ( |
| 134 | <Modal className="edit-stack-modal-contents"> |
| 135 | <Panels |
| 136 | active={activeTab} |
| 137 | panels={panels} |
| 138 | onSelect={tab => { |
| 139 | setActiveTab(tab); |
| 140 | tracker.track('StackEditChangeTab', {extras: {tab}}); |
| 141 | }} |
| 142 | xstyle={styles.container} |
| 143 | tabXstyle={styles.tab} |
| 144 | /> |
| 145 | <Row style={{padding: 'var(--pad) 0', justifyContent: 'flex-end'}}> |
| 146 | {activeTab === 'split' && <SplitStackToolbar />} |
| 147 | <FlexSpacer /> |
| 148 | <StackEditConfirmButtons /> |
| 149 | </Row> |
| 150 | </Modal> |
| 151 | ); |
| 152 | } |
| 153 | |