diff --git a/Composer/packages/client/src/components/CreationFlow/FileSelector.tsx b/Composer/packages/client/src/components/CreationFlow/FileSelector.tsx index 00f218c1a6..7b92db4285 100644 --- a/Composer/packages/client/src/components/CreationFlow/FileSelector.tsx +++ b/Composer/packages/client/src/components/CreationFlow/FileSelector.tsx @@ -26,10 +26,12 @@ import { ComboBox, IComboBox, IComboBoxOption } from 'office-ui-fabric-react/lib import { TextField } from 'office-ui-fabric-react/lib/TextField'; import moment from 'moment'; import { mergeStyleSets } from 'office-ui-fabric-react/lib/Styling'; +import debounce from 'lodash/debounce'; import { FileTypes, nameRegex } from '../../constants'; import { StorageFolder, File } from '../../recoilModel/types'; import { getFileIconName, calculateTimeDiff } from '../../utils/fileUtil'; +import httpClient from '../../utils/httpUtil'; // -------------------- Styles -------------------- // @@ -173,11 +175,26 @@ export const FileSelector: React.FC = (props) => { const [folderName, setFolderName] = useState(''); const [editMode, setEditMode] = useState(EditMode.NONE); const [nameError, setNameError] = useState(''); + const [pathError, setPathError] = useState(''); + + const validate = debounce(async (path) => { + const response = await httpClient.get(`/storages/validate/${encodeURI(path)}`); + setPathError(response.data.errorMsg); + }, 300); useEffect(() => { setCurrentPath(initialPath); }, [focusedStorageFolder]); + //there is a network delay on the path validation. The error may not exist. + //the initialPath is always a valid directory + //so check and clear path error + useEffect(() => { + if (initialPath === currentPath) { + setPathError(''); + } + }, [initialPath, currentPath]); + const createOrUpdateFolder = async (index: number) => { const isValid = nameRegex.test(folderName); const isDup = storageFiles.some((file) => file.name === folderName) && storageFiles[index].name !== folderName; @@ -480,6 +497,23 @@ export const FileSelector: React.FC = (props) => { setCurrentPath(value as string); } }; + + const updatePathPending = (option?: IComboBoxOption, index?: number, value?: string) => { + if (!option && value) { + const path = value.replace(/\\/g, '/'); + validate(value); + setCurrentPath(path as string); + } + }; + + const checkPath = () => { + return pathError + ? pathError + : operationMode.write && !focusedStorageFolder.writable + ? formatMessage('You do not have permission to save bots here') + : ''; + }; + return ( @@ -489,15 +523,12 @@ export const FileSelector: React.FC = (props) => { useComboBoxAsMenuWidth autoComplete={'on'} data-testid={'FileSelectorComboBox'} - errorMessage={ - operationMode.write && !focusedStorageFolder.writable - ? formatMessage('You do not have permission to save bots here') - : '' - } + errorMessage={checkPath()} label={formatMessage('Location')} options={breadcrumbItems} selectedKey={currentPath} onChange={updatePath} + onPendingValueChanged={updatePathPending} /> {operationMode.write && ( diff --git a/Composer/packages/server/src/controllers/storage.ts b/Composer/packages/server/src/controllers/storage.ts index faf2cd4a5a..2f6886c4ed 100644 --- a/Composer/packages/server/src/controllers/storage.ts +++ b/Composer/packages/server/src/controllers/storage.ts @@ -20,6 +20,10 @@ function updateCurrentPath(req: Request, res: Response) { res.status(200).json(StorageService.updateCurrentPath(req.body.path, req.body.storageId)); } +function validatePath(req: Request, res: Response) { + res.status(200).json({ errorMsg: StorageService.validatePath(req.params.path) }); +} + async function createFolder(req: Request, res: Response) { const path = req.body.path; const folderName = req.body.name; @@ -70,4 +74,5 @@ export const StorageController = { updateFolder, getBlob, updateCurrentPath, + validatePath, }; diff --git a/Composer/packages/server/src/router/api.ts b/Composer/packages/server/src/router/api.ts index c48be054ea..46441e35d9 100644 --- a/Composer/packages/server/src/router/api.ts +++ b/Composer/packages/server/src/router/api.ts @@ -62,6 +62,7 @@ router.post('/projects/:projectId/updateBoilerplate', ProjectController.updateBo // storages router.put('/storages/currentPath', StorageController.updateCurrentPath); +router.get('/storages/validate/:path', StorageController.validatePath); router.get('/storages', StorageController.getStorageConnections); router.post('/storages', StorageController.createStorageConnection); router.get('/storages/:storageId/blobs', StorageController.getBlob); diff --git a/Composer/packages/server/src/services/storage.ts b/Composer/packages/server/src/services/storage.ts index f736c80a1e..927f462fc7 100644 --- a/Composer/packages/server/src/services/storage.ts +++ b/Composer/packages/server/src/services/storage.ts @@ -112,7 +112,7 @@ class StorageService { if (path?.endsWith(':')) { path = path + '/'; } - if (Path.isAbsolute(path) && fs.existsSync(path)) { + if (Path.isAbsolute(path) && fs.existsSync(path) && fs.statSync(path).isDirectory()) { const storage = this.storageConnections.find((s) => s.id === storageId); if (storage) { storage.path = path; @@ -122,6 +122,16 @@ class StorageService { return this.storageConnections; }; + public validatePath = (path: string) => { + if (!fs.existsSync(path)) { + return 'The path does not exist'; + } else if (!fs.statSync(path).isDirectory()) { + return 'This is not a directory'; + } else { + return ''; + } + }; + public createFolder = (path: string) => { if (!fs.existsSync(path)) { fs.mkdirSync(path);