From 876ecfc881434dc49c37199d9f112b779dcb118a Mon Sep 17 00:00:00 2001 From: somebody1234 Date: Thu, 8 Jun 2023 19:36:46 +1000 Subject: [PATCH] Remove directory create form; remove input focus ring (#6993) * Remove directory create form; remove input focus ring * Fallback modified date to created date on local backend * Fix double click to open project --- .../src/dashboard/components/dashboard.tsx | 56 ++++++++++- .../components/directoryCreateForm.tsx | 74 -------------- .../src/dashboard/components/dropdown.tsx | 59 ----------- .../components/projectCreateForm.tsx | 97 ------------------- .../src/dashboard/localBackend.ts | 2 +- .../src/dashboard/projectManager.ts | 1 + .../lib/dashboard/src/tailwind.css | 4 + 7 files changed, 57 insertions(+), 236 deletions(-) delete mode 100644 app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/directoryCreateForm.tsx delete mode 100644 app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/dropdown.tsx delete mode 100644 app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/projectCreateForm.tsx diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/dashboard.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/dashboard.tsx index 9246193accf8..535daf7b8d14 100644 --- a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/dashboard.tsx +++ b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/dashboard.tsx @@ -35,7 +35,6 @@ import ConfirmDeleteModal from './confirmDeleteModal' import RenameModal from './renameModal' import UploadFileModal from './uploadFileModal' -import DirectoryCreateForm from './directoryCreateForm' import FileCreateForm from './fileCreateForm' import SecretCreateForm from './secretCreateForm' @@ -99,6 +98,10 @@ const EXPERIMENTAL = { const IDE_ELEMENT_ID = 'root' /** The `localStorage` key under which the ID of the current directory is stored. */ const DIRECTORY_STACK_KEY = `${common.PRODUCT_NAME.toLowerCase()}-dashboard-directory-stack` +/** The {@link RegExp} matching a directory name following the default naming convention. */ +const DIRECTORY_NAME_REGEX = /^New_Directory_(?\d+)$/ +/** The default prefix of an automatically generated directory. */ +const DIRECTORY_NAME_DEFAULT_PREFIX = 'New_Directory_' /** English names for the name column. */ const ASSET_TYPE_NAME: Record = { @@ -110,12 +113,14 @@ const ASSET_TYPE_NAME: Record = { /** Forms to create each asset type. */ const ASSET_TYPE_CREATE_FORM: Record< - Exclude, + Exclude< + backendModule.AssetType, + backendModule.AssetType.directory | backendModule.AssetType.project + >, (props: CreateFormProps) => JSX.Element > = { [backendModule.AssetType.file]: FileCreateForm, [backendModule.AssetType.secret]: SecretCreateForm, - [backendModule.AssetType.directory]: DirectoryCreateForm, } /** English names for every column except for the name column. */ @@ -171,7 +176,7 @@ const PERMISSION: Record = { - [ColumnDisplayMode.release]: [Column.name, Column.lastModified, Column.sharedWith], + [ColumnDisplayMode.release]: [Column.name, Column.lastModified /*, Column.sharedWith*/], [ColumnDisplayMode.all]: [ Column.name, Column.lastModified, @@ -504,7 +509,7 @@ function Dashboard(props: DashboardProps) {
{ - if (event.detail === 2 && event.target === event.currentTarget) { + if (event.detail === 2) { // It is a double click; open the project. setProjectEvent({ type: projectActionButton.ProjectEventType.open, @@ -723,6 +728,15 @@ function Dashboard(props: DashboardProps) { error: (promiseError: Error) => `Error creating new empty project: ${promiseError.message}`, }) + } else if (assetType === backendModule.AssetType.directory) { + void toast.promise(handleCreateDirectory(), { + loading: 'Creating new directory...', + success: 'Created new directory.', + // This is UNSAFE, as the original function's parameter is of type + // `any`. + error: (promiseError: Error) => + `Error creating new directory: ${promiseError.message}`, + }) } else { // This is a React component even though it doesn't contain JSX. // eslint-disable-next-line no-restricted-syntax @@ -883,6 +897,38 @@ function Dashboard(props: DashboardProps) { doRefresh() } + const handleCreateDirectory = async () => { + if (backend.type !== backendModule.BackendType.remote) { + // This should never happen, but even if it does, it is the caller's responsibility + // to log, or display this error. + throw new Error('Folders cannot be created on the local backend.') + } else { + const directoryIndices = directoryAssets + .map(directoryAsset => DIRECTORY_NAME_REGEX.exec(directoryAsset.title)) + .map(match => match?.groups?.directoryIndex) + .map(maybeIndex => (maybeIndex != null ? parseInt(maybeIndex, 10) : 0)) + const title = `${DIRECTORY_NAME_DEFAULT_PREFIX}${Math.max(...directoryIndices) + 1}` + setDirectoryAssets([ + { + title, + type: backendModule.AssetType.directory, + id: newtype.asNewtype(Number(new Date()).toString()), + modifiedAt: dateTime.toRfc3339(new Date()), + parentId: directoryId ?? newtype.asNewtype(''), + permissions: [], + projectState: null, + }, + ...directoryAssets, + ]) + await backend.createDirectory({ + parentId: directoryId, + title, + }) + doRefresh() + return + } + } + return (
void -} - -/** A form to create a directory. */ -function DirectoryCreateForm(props: DirectoryCreateFormProps) { - const { directoryId, onSuccess, ...passThrough } = props - const { backend } = backendProvider.useBackend() - const { unsetModal } = modalProvider.useSetModal() - const [name, setName] = react.useState(null) - - if (backend.type === backendModule.BackendType.local) { - return <> - } else { - const onSubmit = async (event: react.FormEvent) => { - event.preventDefault() - if (name == null) { - toast.error('Please provide a directory name.') - } else { - unsetModal() - await toast - .promise( - backend.createDirectory({ - parentId: directoryId, - title: name, - }), - { - loading: 'Creating directory...', - success: 'Sucessfully created directory.', - error: error.unsafeIntoErrorMessage, - } - ) - .then(onSuccess) - } - } - - return ( - -
- - { - setName(event.target.value) - }} - /> -
-
- ) - } -} - -export default DirectoryCreateForm diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/dropdown.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/dropdown.tsx deleted file mode 100644 index c6c828efe310..000000000000 --- a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/dropdown.tsx +++ /dev/null @@ -1,59 +0,0 @@ -/** @file A select menu with a dropdown. */ -import * as react from 'react' - -import * as svg from '../../components/svg' - -/** Props for a {@link Dropdown}. */ -export interface DropdownProps { - items: [string, ...string[]] - onChange: (value: string) => void - className?: string - optionsClassName?: string -} - -/** A select menu with a dropdown. */ -function Dropdown(props: DropdownProps) { - const { items, onChange, className, optionsClassName } = props - const [value, setValue] = react.useState(items[0]) - // TODO: - const [isDropdownVisible, setIsDropdownVisible] = react.useState(false) - - return ( -
-
{ - setIsDropdownVisible(!isDropdownVisible) - }} - > - {value} {svg.DOWN_CARET_ICON} -
-
-
- {items.map(item => ( -
{ - setIsDropdownVisible(false) - setValue(item) - onChange(item) - }} - className="cursor-pointer bg-white first:rounded-t-lg last:rounded-b-lg hover:bg-gray-100 p-1" - > - {item} -
- ))} -
-
-
- ) -} - -export default Dropdown diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/projectCreateForm.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/projectCreateForm.tsx deleted file mode 100644 index 95ed741a07cd..000000000000 --- a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/projectCreateForm.tsx +++ /dev/null @@ -1,97 +0,0 @@ -/** @file Form to create a project. */ -import * as react from 'react' -import toast from 'react-hot-toast' - -import * as backendModule from '../backend' -import * as backendProvider from '../../providers/backend' -import * as modalProvider from '../../providers/modal' -import * as templates from './templates' - -import CreateForm, * as createForm from './createForm' -import Dropdown from './dropdown' - -// ========================= -// === ProjectCreateForm === -// ========================= - -/** Props for a {@link ProjectCreateForm}. */ -export interface ProjectCreateFormProps extends createForm.CreateFormPassthroughProps { - directoryId: backendModule.DirectoryId - getNewProjectName: (templateId: string | null) => string - onSuccess: () => void -} - -/** A form to create a project. */ -function ProjectCreateForm(props: ProjectCreateFormProps) { - const { directoryId, getNewProjectName, onSuccess, ...passThrough } = props - const { backend } = backendProvider.useBackend() - const { unsetModal } = modalProvider.useSetModal() - - const [defaultName, setDefaultName] = react.useState(() => getNewProjectName(null)) - const [name, setName] = react.useState(null) - const [templateId, setTemplateId] = react.useState(null) - - const onSubmit = async (event: react.FormEvent) => { - event.preventDefault() - unsetModal() - const finalName = name ?? defaultName - const templateText = templateId == null ? '' : `from template '${templateId}'` - await toast.promise( - backend.createProject({ - parentDirectoryId: directoryId, - projectName: name ?? defaultName, - projectTemplateName: templateId, - }), - { - loading: `Creating project '${finalName}'${templateText}...`, - success: `Sucessfully created project '${finalName}'${templateText}.`, - // This is UNSAFE, as the original function's parameter is of type `any`. - error: (promiseError: Error) => - `Error creating project '${finalName}'${templateText}: ${promiseError.message}`, - } - ) - onSuccess() - } - - return ( - -
- - { - setName(event.target.value) - }} - /> -
-
- - item.title)]} - onChange={newTemplateTitle => { - const newTemplateId = - templates.TEMPLATES.find( - template => template.title === newTemplateTitle - )?.id ?? null - setTemplateId(newTemplateId) - if (name == null) { - setDefaultName(getNewProjectName(newTemplateId)) - } - }} - /> -
-
- ) -} - -export default ProjectCreateForm diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/localBackend.ts b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/localBackend.ts index 6f8235f208b9..c3f22fd5d2de 100644 --- a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/localBackend.ts +++ b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/localBackend.ts @@ -43,7 +43,7 @@ export class LocalBackend implements Partial { type: backend.AssetType.project, id: project.id, title: project.name, - modifiedAt: project.lastOpened, + modifiedAt: project.lastOpened ?? project.created, parentId: newtype.asNewtype(''), permissions: [], projectState: { diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/projectManager.ts b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/projectManager.ts index e96cf149de6e..f911d4ed6cb8 100644 --- a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/projectManager.ts +++ b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/projectManager.ts @@ -68,6 +68,7 @@ export interface ProjectMetadata { namespace: string id: ProjectId engineVersion: string | null + created: UTCDateTime lastOpened: UTCDateTime | null } diff --git a/app/ide-desktop/lib/dashboard/src/tailwind.css b/app/ide-desktop/lib/dashboard/src/tailwind.css index e06905329093..8a0fa5851978 100644 --- a/app/ide-desktop/lib/dashboard/src/tailwind.css +++ b/app/ide-desktop/lib/dashboard/src/tailwind.css @@ -16,6 +16,10 @@ body { font-feature-settings: normal; } +.enso-dashboard *:focus { + outline: none !important; +} + /* Must be kept in sync with app/gui/view/graph-editor/src/builtin/visualization/java_script/helpers/scrollable.js. */ ::-webkit-scrollbar {