Project create form (#6923)

* Re-add project create form

* Add dropdown component and use in projct create form

* Fix "project create" button behavior
This commit is contained in:
somebody1234 2023-06-05 23:14:59 +10:00 committed by GitHub
parent cfb2f2916e
commit f09d922a41
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 163 additions and 82 deletions

View File

@ -264,6 +264,13 @@ export const DEFAULT_USER_ICON = (
</svg>
)
/** An icon representing a menu that can be expanded downwards. */
export const DOWN_CARET_ICON = (
<svg height={16} width={16} viewBox="-1-1 12 12" xmlns="http://www.w3.org/2000/svg">
<path d="M1 3l4 4 4-4" fill="transparent" stroke="currentColor" strokeLinecap="round" />
</svg>
)
/** Props for a {@link Spinner}. */
export interface SpinnerProps {
size: number

View File

@ -39,7 +39,7 @@ function CreateForm(props: CreateFormProps) {
<Modal className="absolute overflow-hidden bg-opacity-25 w-full h-full top-0 left-0">
<form
style={{ left, top }}
className="sticky bg-white shadow-soft rounded-lg w-60"
className="sticky bg-white shadow-soft rounded-lg w-64"
onSubmit={onSubmit}
onClick={event => {
event.stopPropagation()

View File

@ -1,6 +1,7 @@
/** @file Main dashboard component, responsible for listing user's projects as well as other
* interactive components. */
import * as react from 'react'
import toast from 'react-hot-toast'
import * as common from 'enso-common'
@ -80,6 +81,7 @@ export interface CreateFormProps {
left: number
top: number
directoryId: backendModule.DirectoryId
getNewProjectName: (templateId: string | null) => string
onSuccess: () => void
}
@ -585,9 +587,6 @@ function Dashboard(props: DashboardProps) {
/** Heading element for every column. */
const ColumnHeading = (column: Column, assetType: backendModule.AssetType) =>
column === Column.name ? (
assetType === backendModule.AssetType.project ? (
<>{ASSET_TYPE_NAME[assetType]}</>
) : (
<div className="inline-flex">
{ASSET_TYPE_NAME[assetType]}
<button
@ -598,6 +597,16 @@ function Dashboard(props: DashboardProps) {
// This type assertion is safe as this event handler is on a button.
// eslint-disable-next-line no-restricted-syntax
(event.target as HTMLButtonElement).getBoundingClientRect()
if (assetType === backendModule.AssetType.project) {
void toast.promise(handleCreateProject(null), {
loading: 'Creating new empty project...',
success: 'Created new empty project.',
// This is UNSAFE, as the original function's parameter is of type
// `any`.
error: (promiseError: Error) =>
`Error creating new empty project: ${promiseError.message}`,
})
} else {
// This is a React component even though it doesn't contain JSX.
// eslint-disable-next-line no-restricted-syntax
const CreateForm = ASSET_TYPE_CREATE_FORM[assetType]
@ -605,6 +614,7 @@ function Dashboard(props: DashboardProps) {
<CreateForm
left={buttonPosition.left + window.scrollX}
top={buttonPosition.top + window.scrollY}
getNewProjectName={getNewProjectName}
// This is safe; headings are not rendered when there is no
// internet connection.
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
@ -612,12 +622,12 @@ function Dashboard(props: DashboardProps) {
onSuccess={doRefresh}
/>
))
}
}}
>
{svg.ADD_ICON}
</button>
</div>
)
) : (
<>{COLUMN_NAME[column]}</>
)

View File

@ -0,0 +1,59 @@
/** @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 (
<div
className={`whitespace-nowrap bg-gray-200 rounded-full cursor-pointer ${
className ?? ''
}`}
>
<div
className="flex flex-1"
onClick={() => {
setIsDropdownVisible(!isDropdownVisible)
}}
>
<span className="grow">{value}</span> {svg.DOWN_CARET_ICON}
</div>
<div className={`relative h-0 ${optionsClassName ?? ''}`}>
<div
className={`rounded-lg h-0 ${
isDropdownVisible ? 'overflow-visible' : 'overflow-hidden'
}`}
>
{items.map(item => (
<div
onClick={() => {
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}
</div>
))}
</div>
</div>
</div>
)
}
export default Dropdown

View File

@ -4,9 +4,11 @@ import toast from 'react-hot-toast'
import * as backendModule from '../backend'
import * as backendProvider from '../../providers/backend'
import * as error from '../../error'
import * as modalProvider from '../../providers/modal'
import * as templates from './templates'
import CreateForm, * as createForm from './createForm'
import Dropdown from './dropdown'
// =========================
// === ProjectCreateForm ===
@ -15,42 +17,40 @@ import CreateForm, * as createForm from './createForm'
/** 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, onSuccess, ...passThrough } = props
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<string | null>(null)
const [template, setTemplate] = react.useState<string | null>(null)
const [templateId, setTemplateId] = react.useState<string | null>(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 project name.')
} else {
unsetModal()
await toast
.promise(
const finalName = name ?? defaultName
const templateText = templateId == null ? '' : `from template '${templateId}'`
await toast.promise(
backend.createProject({
parentDirectoryId: directoryId,
projectName: name,
projectTemplateName: template,
projectName: name ?? defaultName,
projectTemplateName: templateId,
}),
{
loading: 'Creating project...',
success: 'Sucessfully created project.',
error: error.unsafeIntoErrorMessage,
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}`,
}
)
.then(onSuccess)
}
onSuccess()
}
return (
@ -64,29 +64,34 @@ function ProjectCreateForm(props: ProjectCreateFormProps) {
type="text"
size={1}
className="bg-gray-200 rounded-full flex-1 grow-2 px-2 m-1"
value={name ?? defaultName}
onChange={event => {
setName(event.target.value)
}}
/>
</div>
<div className="flex flex-row flex-nowrap m-1">
{/* FIXME[sb]: Use the array of templates in a dropdown when it becomes available. */}
<label className="inline-block flex-1 grow m-1" htmlFor="project_template_name">
Template
</label>
<input
id="project_template_name"
type="text"
size={1}
className="bg-gray-200 rounded-full flex-1 grow-2 px-2 m-1"
onChange={event => {
setTemplate(event.target.value)
<Dropdown
className="flex-1 grow-2 px-2 m-1"
optionsClassName="-mx-2"
items={['None', ...templates.TEMPLATES.map(item => item.title)]}
onChange={newTemplateTitle => {
const newTemplateId =
templates.TEMPLATES.find(
template => template.title === newTemplateTitle
)?.id ?? null
setTemplateId(newTemplateId)
if (name == null) {
setDefaultName(getNewProjectName(newTemplateId))
}
}}
/>
</div>
</CreateForm>
)
}
}
export default ProjectCreateForm

View File

@ -6,7 +6,7 @@ import * as svg from '../../components/svg'
// =================
/** Template metadata. */
interface Template {
export interface Template {
title: string
description: string
id: string
@ -14,7 +14,7 @@ interface Template {
}
/** The full list of templates. */
const TEMPLATES: Template[] = [
export const TEMPLATES: [Template, ...Template[]] = [
{
title: 'Colorado COVID',
id: 'Colorado_COVID',