addons/isl/src/App.tsxblame
View source
b69ab311/**
b69ab312 * Copyright (c) Meta Platforms, Inc. and affiliates.
b69ab313 *
b69ab314 * This source code is licensed under the MIT license found in the
b69ab315 * LICENSE file in the root directory of this source tree.
b69ab316 */
b69ab317
b69ab318import type {AppMode, RepositoryError} from './types';
b69ab319
b69ab3110import {Button} from 'isl-components/Button';
b69ab3111import {ErrorBoundary, ErrorNotice} from 'isl-components/ErrorNotice';
b69ab3112import {Icon} from 'isl-components/Icon';
b69ab3113import {atom, useAtomValue, useSetAtom} from 'jotai';
b69ab3114import {Suspense, useEffect, useMemo} from 'react';
b69ab3115import {useThrottledEffect} from 'shared/hooks';
b69ab3116import {AllProviders} from './AppWrapper';
b69ab3117import {CommandHistoryAndProgress} from './CommandHistoryAndProgress';
b69ab3118import {CommitInfoSidebar} from './CommitInfoView/CommitInfoView';
b69ab3119import {CommitTreeList} from './CommitTreeList';
b69ab3120import {ComparisonViewApp, ComparisonViewModal} from './ComparisonView/ComparisonViewModal';
b69ab3121import {availableCwds, CwdSelections} from './CwdSelector';
b69ab3122import {Drawers} from './Drawers';
b69ab3123import {EmptyState} from './EmptyState';
b69ab3124import {useCommand} from './ISLShortcuts';
b69ab3125import {Internal} from './Internal';
b69ab3126import {TopBar} from './TopBar';
b69ab3127import {TopLevelAlerts} from './TopLevelAlert';
6c9fcae28import {CreateGroveRepoBanner} from './codeReview/grove/CreateGroveRepoBanner';
b69ab3129import {TopLevelErrors} from './TopLevelErrors';
b69ab3130import {tracker} from './analytics';
b69ab3131import {islDrawerState} from './drawerState';
b69ab3132import {t, T} from './i18n';
b69ab3133import platform from './platform';
b69ab3134import {useMainContentWidth} from './responsive';
b69ab3135import {repositoryInfoOrError} from './serverAPIState';
b69ab3136
b69ab3137import clientToServerAPI from './ClientToServerAPI';
b69ab3138import './index.css';
b69ab3139import './stackEdit/ui/AISplitMessageHandlers';
b69ab3140
b69ab3141declare global {
b69ab3142 interface Window {
b69ab3143 /**
b69ab3144 * AppMode that determines what feature the App is rendering.
b69ab3145 * This is set at creation time (e.g. in HTML), and is not dynamic.
b69ab3146 */
b69ab3147 islAppMode?: AppMode;
b69ab3148 }
b69ab3149}
b69ab3150let hasInitialized = false;
b69ab3151export default function App() {
b69ab3152 const mode = window.islAppMode ?? {mode: 'isl'};
b69ab3153
b69ab3154 useEffect(() => {
b69ab3155 if (!hasInitialized) {
b69ab3156 clientToServerAPI.postMessage({type: 'clientReady'});
b69ab3157 hasInitialized = true;
b69ab3158 }
b69ab3159 }, []);
b69ab3160
b69ab3161 return (
b69ab3162 <AllProviders>
b69ab3163 {mode.mode === 'isl' ? (
b69ab3164 <>
b69ab3165 <NullStateOrDrawers />
b69ab3166 <ComparisonViewModal />
b69ab3167 </>
b69ab3168 ) : (
b69ab3169 <ComparisonApp />
b69ab3170 )}
b69ab3171 </AllProviders>
b69ab3172 );
b69ab3173}
b69ab3174
b69ab3175function ComparisonApp() {
b69ab3176 return (
b69ab3177 <Suspense fallback={<Icon icon="loading" />}>
b69ab3178 <ComparisonViewApp />
b69ab3179 </Suspense>
b69ab3180 );
b69ab3181}
b69ab3182
b69ab3183function NullStateOrDrawers() {
b69ab3184 const repoInfo = useAtomValue(repositoryInfoOrError);
b69ab3185 if (repoInfo != null && repoInfo.type !== 'success') {
b69ab3186 return <ISLNullState repoError={repoInfo} />;
b69ab3187 }
b69ab3188 return <ISLDrawers />;
b69ab3189}
b69ab3190
b69ab3191function ISLDrawers() {
b69ab3192 const setDrawerState = useSetAtom(islDrawerState);
b69ab3193 useCommand('ToggleSidebar', () => {
b69ab3194 setDrawerState(state => ({
b69ab3195 ...state,
b69ab3196 right: {...state.right, collapsed: !state.right.collapsed},
b69ab3197 }));
b69ab3198 });
b69ab3199
b69ab31100 return (
b69ab31101 <Drawers
b69ab31102 rightLabel={
b69ab31103 <>
b69ab31104 <Icon icon="edit" />
b69ab31105 <T>Commit Info</T>
b69ab31106 </>
b69ab31107 }
b69ab31108 right={<CommitInfoSidebar />}
b69ab31109 errorBoundary={ErrorBoundary}>
b69ab31110 <MainContent />
b69ab31111 <CommandHistoryAndProgress />
b69ab31112 </Drawers>
b69ab31113 );
b69ab31114}
b69ab31115
b69ab31116function MainContent() {
b69ab31117 const ref = useMainContentWidth();
b69ab31118 return (
b69ab31119 <div className="main-content-area" ref={ref}>
b69ab31120 <TopBar />
b69ab31121 <TopLevelErrors />
b69ab31122 <TopLevelAlerts />
6c9fcae123 <CreateGroveRepoBanner />
b69ab31124 <CommitTreeList />
b69ab31125 </div>
b69ab31126 );
b69ab31127}
b69ab31128
b69ab31129function ISLNullState({repoError}: {repoError: RepositoryError}) {
b69ab31130 const emptyCwds = useAtomValue(useMemo(() => atom(get => get(availableCwds).length === 0), []));
b69ab31131 useThrottledEffect(
b69ab31132 () => {
b69ab31133 if (repoError != null) {
b69ab31134 switch (repoError.type) {
b69ab31135 case 'cwdNotARepository':
b69ab31136 tracker.track('UIEmptyState', {extras: {cwd: repoError.cwd}, errorName: 'InvalidCwd'});
b69ab31137 break;
b69ab31138 case 'edenFsUnhealthy':
b69ab31139 tracker.track('UIEmptyState', {
b69ab31140 extras: {cwd: repoError.cwd},
b69ab31141 errorName: 'EdenFsUnhealthy',
b69ab31142 });
b69ab31143 break;
b69ab31144 case 'invalidCommand':
b69ab31145 tracker.track('UIEmptyState', {
b69ab31146 extras: {command: repoError.command},
b69ab31147 errorName: 'InvalidCommand',
b69ab31148 });
b69ab31149 break;
b69ab31150 case 'unknownError':
b69ab31151 tracker.error('UIEmptyState', 'RepositoryError', repoError.error);
b69ab31152 break;
b69ab31153 }
b69ab31154 }
b69ab31155 },
b69ab31156 1_000,
b69ab31157 [repoError],
b69ab31158 );
b69ab31159 let content;
b69ab31160 if (repoError != null) {
b69ab31161 if (repoError.type === 'cwdNotARepository') {
b69ab31162 if (platform.platformName === 'vscode' && emptyCwds) {
b69ab31163 content = (
b69ab31164 <>
b69ab31165 <EmptyState>
b69ab31166 <div>
b69ab31167 <T>No folder opened</T>
b69ab31168 </div>
b69ab31169 <p>
b69ab31170 <T>Open a folder to get started.</T>
b69ab31171 </p>
b69ab31172 </EmptyState>
b69ab31173 </>
b69ab31174 );
b69ab31175 } else {
b69ab31176 content = (
b69ab31177 <>
b69ab31178 <EmptyState>
b69ab31179 <div>
b69ab31180 <T>Not a valid repository</T>
b69ab31181 </div>
b69ab31182 <p>
b69ab31183 <T replace={{$cwd: <code>{repoError.cwd}</code>}}>
b69ab31184 $cwd is not a valid Sapling repository. Clone or init a repository to use ISL.
b69ab31185 </T>
b69ab31186 </p>
b69ab31187 </EmptyState>
b69ab31188 <CwdSelections dismiss={() => null} />
b69ab31189 </>
b69ab31190 );
b69ab31191 }
b69ab31192 } else if (repoError.type === 'cwdDoesNotExist') {
b69ab31193 content = (
b69ab31194 <>
b69ab31195 {Internal.InternalInstallationDocs ? (
b69ab31196 <Internal.InternalInstallationDocs repoError={repoError} />
b69ab31197 ) : (
b69ab31198 <ErrorNotice
b69ab31199 title={
b69ab31200 <T replace={{$cwd: repoError.cwd}}>
b69ab31201 cwd $cwd does not exist. Make sure the folder exists.
b69ab31202 </T>
b69ab31203 }
b69ab31204 error={
b69ab31205 new Error(
b69ab31206 t('$cwd not found', {
b69ab31207 replace: {$cwd: repoError.cwd},
b69ab31208 }),
b69ab31209 )
b69ab31210 }
b69ab31211 buttons={[
b69ab31212 <Button
b69ab31213 key="help-button"
b69ab31214 onClick={e => {
b69ab31215 platform.openExternalLink(
b69ab31216 'https://sapling-scm.com/docs/introduction/installation',
b69ab31217 );
b69ab31218 e.preventDefault();
b69ab31219 e.stopPropagation();
b69ab31220 }}>
b69ab31221 <T>See installation docs</T>
b69ab31222 </Button>,
b69ab31223 ]}
b69ab31224 />
b69ab31225 )}
b69ab31226 <CwdSelections dismiss={() => null} />
b69ab31227 </>
b69ab31228 );
b69ab31229 } else if (repoError.type === 'edenFsUnhealthy') {
b69ab31230 content = (
b69ab31231 <>
b69ab31232 <ErrorNotice
b69ab31233 title={<T replace={{$cwd: repoError.cwd}}>EdenFS is not running properly in $cwd</T>}
b69ab31234 description={
b69ab31235 <T replace={{$edenDoctor: <code>eden doctor</code>}}>
b69ab31236 Try running $edenDoctor and reloading the ISL window
b69ab31237 </T>
b69ab31238 }
b69ab31239 error={
b69ab31240 new Error(
b69ab31241 t('README_EDEN.txt found in $cwd', {
b69ab31242 replace: {$cwd: repoError.cwd},
b69ab31243 }),
b69ab31244 )
b69ab31245 }
b69ab31246 />
b69ab31247 <CwdSelections dismiss={() => null} />
b69ab31248 </>
b69ab31249 );
b69ab31250 } else if (repoError.type === 'invalidCommand') {
b69ab31251 if (Internal.InvalidSlCommand) {
b69ab31252 content = <Internal.InvalidSlCommand repoError={repoError} />;
b69ab31253 } else {
b69ab31254 content = (
b69ab31255 <ErrorNotice
b69ab31256 startExpanded
b69ab31257 title={<T>Invalid Sapling command. Is Sapling installed correctly?</T>}
b69ab31258 description={
b69ab31259 <T replace={{$cmd: repoError.command}}>Command "$cmd" was not found in PATH</T>
b69ab31260 }
b69ab31261 details={<T replace={{$path: repoError.path ?? '(no path found)'}}>PATH: $path'</T>}
b69ab31262 buttons={[
b69ab31263 <Button
b69ab31264 key="help-button"
b69ab31265 onClick={e => {
b69ab31266 platform.openExternalLink(
b69ab31267 'https://sapling-scm.com/docs/introduction/installation',
b69ab31268 );
b69ab31269 e.preventDefault();
b69ab31270 e.stopPropagation();
b69ab31271 }}>
b69ab31272 <T>See installation docs</T>
b69ab31273 </Button>,
b69ab31274 ]}
b69ab31275 />
b69ab31276 );
b69ab31277 }
b69ab31278 } else {
b69ab31279 content = <ErrorNotice title={<T>Something went wrong</T>} error={repoError.error} />;
b69ab31280 }
b69ab31281 }
b69ab31282
b69ab31283 return <div className="empty-app-state">{content}</div>;
b69ab31284}