addons/isl/src/UnsavedFiles.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 {ContextMenuItem} from 'shared/ContextMenu';
b69ab319import type {RepoRelativePath} from './types';
b69ab3110
b69ab3111import {Button} from 'isl-components/Button';
b69ab3112import {Icon} from 'isl-components/Icon';
b69ab3113import {Subtle} from 'isl-components/Subtle';
b69ab3114import {atom, useAtomValue} from 'jotai';
b69ab3115import {useContextMenu} from 'shared/ContextMenu';
b69ab3116import {minimalDisambiguousPaths} from 'shared/minimalDisambiguousPaths';
b69ab3117import serverAPI from './ClientToServerAPI';
b69ab3118import {Column, Row} from './ComponentUtils';
b69ab3119import {availableCwds} from './CwdSelector';
b69ab3120import {T, t} from './i18n';
b69ab3121import {readAtom, writeAtom} from './jotaiUtils';
b69ab3122import platform from './platform';
b69ab3123import {showModal} from './useModal';
b69ab3124import {registerCleanup, registerDisposable} from './utils';
b69ab3125
b69ab3126/**
b69ab3127 * A list of files for this repo that are unsaved in the IDE.
b69ab3128 * This is always `[]` for unsupported platforms like browser.
b69ab3129 */
b69ab3130export const unsavedFiles = atom<Array<{path: RepoRelativePath; uri: string}>>([]);
b69ab3131registerCleanup(
b69ab3132 availableCwds,
b69ab3133 serverAPI.onConnectOrReconnect(() => {
b69ab3134 serverAPI.postMessage({
b69ab3135 type: 'platform/subscribeToUnsavedFiles',
b69ab3136 });
b69ab3137 }),
b69ab3138 import.meta.hot,
b69ab3139);
b69ab3140registerDisposable(
b69ab3141 availableCwds,
b69ab3142 serverAPI.onMessageOfType('platform/unsavedFiles', event =>
b69ab3143 writeAtom(unsavedFiles, event.unsaved),
b69ab3144 ),
b69ab3145 import.meta.hot,
b69ab3146);
b69ab3147
b69ab3148export function UnsavedFilesCount() {
b69ab3149 const unsaved = useAtomValue(unsavedFiles);
b69ab3150
b69ab3151 const menu = useContextMenu(() => {
b69ab3152 const fullPaths = unsaved.map(({path}) => path);
b69ab3153 const disambiguated = minimalDisambiguousPaths(fullPaths);
b69ab3154 const options: Array<ContextMenuItem> = disambiguated.map((name, i) => ({
b69ab3155 label: t('Open $name', {replace: {$name: name}}),
b69ab3156 onClick: () => {
b69ab3157 platform.openFile(fullPaths[i]);
b69ab3158 },
b69ab3159 }));
b69ab3160 options.push({type: 'divider'});
b69ab3161 options.push({
b69ab3162 label: t('Save All'),
b69ab3163 onClick: () => serverAPI.postMessage({type: 'platform/saveAllUnsavedFiles'}),
b69ab3164 });
b69ab3165 return options;
b69ab3166 });
b69ab3167
b69ab3168 if (unsaved.length === 0) {
b69ab3169 return null;
b69ab3170 }
b69ab3171 return (
b69ab3172 <Subtle>
b69ab3173 <Row>
b69ab3174 <T count={unsaved.length}>unsavedFileCount</T>
b69ab3175 <Button icon>
b69ab3176 <Icon icon="ellipsis" onClick={menu} />
b69ab3177 </Button>
b69ab3178 </Row>
b69ab3179 </Subtle>
b69ab3180 );
b69ab3181}
b69ab3182
b69ab3183/**
b69ab3184 * If there are unsaved files, ask the user if they want to save them.
b69ab3185 * Returns true if the user wants to continue with the operation (after possibly having saved the files),
b69ab3186 * false if they cancelled.
b69ab3187 */
b69ab3188export async function confirmUnsavedFiles(): Promise<boolean> {
b69ab3189 const unsaved = readAtom(unsavedFiles);
b69ab3190 if (unsaved.length === 0) {
b69ab3191 return true;
b69ab3192 }
b69ab3193
b69ab3194 const buttons = [
b69ab3195 t('Cancel'),
b69ab3196 t('Continue Without Saving'),
b69ab3197 {label: t('Save All and Continue'), primary: true},
b69ab3198 ];
b69ab3199 const answer = await showModal({
b69ab31100 type: 'confirm',
b69ab31101 buttons,
b69ab31102 title: <T count={unsaved.length}>confirmUnsavedFileCount</T>,
b69ab31103 message: (
b69ab31104 <Column alignStart>
b69ab31105 <Column alignStart>
b69ab31106 {unsaved.map(({path}) => (
b69ab31107 <Row key={path}>{path}</Row>
b69ab31108 ))}
b69ab31109 </Column>
b69ab31110 <Row>
b69ab31111 <T count={unsaved.length}>doYouWantToSaveThem</T>
b69ab31112 </Row>
b69ab31113 </Column>
b69ab31114 ),
b69ab31115 });
b69ab31116
b69ab31117 if (answer === buttons[2]) {
b69ab31118 serverAPI.postMessage({type: 'platform/saveAllUnsavedFiles'});
b69ab31119 const message = await serverAPI.nextMessageMatching(
b69ab31120 'platform/savedAllUnsavedFiles',
b69ab31121 () => true,
b69ab31122 );
b69ab31123 return message.success;
b69ab31124 }
b69ab31125
b69ab31126 return answer === buttons[1];
b69ab31127}