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