mirror of
https://github.com/enso-org/enso.git
synced 2024-12-23 11:41:56 +03:00
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:
parent
cfb2f2916e
commit
f09d922a41
@ -264,6 +264,13 @@ export const DEFAULT_USER_ICON = (
|
|||||||
</svg>
|
</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}. */
|
/** Props for a {@link Spinner}. */
|
||||||
export interface SpinnerProps {
|
export interface SpinnerProps {
|
||||||
size: number
|
size: number
|
||||||
|
@ -39,7 +39,7 @@ function CreateForm(props: CreateFormProps) {
|
|||||||
<Modal className="absolute overflow-hidden bg-opacity-25 w-full h-full top-0 left-0">
|
<Modal className="absolute overflow-hidden bg-opacity-25 w-full h-full top-0 left-0">
|
||||||
<form
|
<form
|
||||||
style={{ left, top }}
|
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}
|
onSubmit={onSubmit}
|
||||||
onClick={event => {
|
onClick={event => {
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
/** @file Main dashboard component, responsible for listing user's projects as well as other
|
/** @file Main dashboard component, responsible for listing user's projects as well as other
|
||||||
* interactive components. */
|
* interactive components. */
|
||||||
import * as react from 'react'
|
import * as react from 'react'
|
||||||
|
import toast from 'react-hot-toast'
|
||||||
|
|
||||||
import * as common from 'enso-common'
|
import * as common from 'enso-common'
|
||||||
|
|
||||||
@ -80,6 +81,7 @@ export interface CreateFormProps {
|
|||||||
left: number
|
left: number
|
||||||
top: number
|
top: number
|
||||||
directoryId: backendModule.DirectoryId
|
directoryId: backendModule.DirectoryId
|
||||||
|
getNewProjectName: (templateId: string | null) => string
|
||||||
onSuccess: () => void
|
onSuccess: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -585,19 +587,26 @@ function Dashboard(props: DashboardProps) {
|
|||||||
/** Heading element for every column. */
|
/** Heading element for every column. */
|
||||||
const ColumnHeading = (column: Column, assetType: backendModule.AssetType) =>
|
const ColumnHeading = (column: Column, assetType: backendModule.AssetType) =>
|
||||||
column === Column.name ? (
|
column === Column.name ? (
|
||||||
assetType === backendModule.AssetType.project ? (
|
<div className="inline-flex">
|
||||||
<>{ASSET_TYPE_NAME[assetType]}</>
|
{ASSET_TYPE_NAME[assetType]}
|
||||||
) : (
|
<button
|
||||||
<div className="inline-flex">
|
className="mx-1"
|
||||||
{ASSET_TYPE_NAME[assetType]}
|
onClick={event => {
|
||||||
<button
|
event.stopPropagation()
|
||||||
className="mx-1"
|
const buttonPosition =
|
||||||
onClick={event => {
|
// This type assertion is safe as this event handler is on a button.
|
||||||
event.stopPropagation()
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
const buttonPosition =
|
(event.target as HTMLButtonElement).getBoundingClientRect()
|
||||||
// This type assertion is safe as this event handler is on a button.
|
if (assetType === backendModule.AssetType.project) {
|
||||||
// eslint-disable-next-line no-restricted-syntax
|
void toast.promise(handleCreateProject(null), {
|
||||||
(event.target as HTMLButtonElement).getBoundingClientRect()
|
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.
|
// This is a React component even though it doesn't contain JSX.
|
||||||
// eslint-disable-next-line no-restricted-syntax
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
const CreateForm = ASSET_TYPE_CREATE_FORM[assetType]
|
const CreateForm = ASSET_TYPE_CREATE_FORM[assetType]
|
||||||
@ -605,6 +614,7 @@ function Dashboard(props: DashboardProps) {
|
|||||||
<CreateForm
|
<CreateForm
|
||||||
left={buttonPosition.left + window.scrollX}
|
left={buttonPosition.left + window.scrollX}
|
||||||
top={buttonPosition.top + window.scrollY}
|
top={buttonPosition.top + window.scrollY}
|
||||||
|
getNewProjectName={getNewProjectName}
|
||||||
// This is safe; headings are not rendered when there is no
|
// This is safe; headings are not rendered when there is no
|
||||||
// internet connection.
|
// internet connection.
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
@ -612,12 +622,12 @@ function Dashboard(props: DashboardProps) {
|
|||||||
onSuccess={doRefresh}
|
onSuccess={doRefresh}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
}}
|
}
|
||||||
>
|
}}
|
||||||
{svg.ADD_ICON}
|
>
|
||||||
</button>
|
{svg.ADD_ICON}
|
||||||
</div>
|
</button>
|
||||||
)
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>{COLUMN_NAME[column]}</>
|
<>{COLUMN_NAME[column]}</>
|
||||||
)
|
)
|
||||||
|
@ -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
|
@ -4,9 +4,11 @@ import toast from 'react-hot-toast'
|
|||||||
|
|
||||||
import * as backendModule from '../backend'
|
import * as backendModule from '../backend'
|
||||||
import * as backendProvider from '../../providers/backend'
|
import * as backendProvider from '../../providers/backend'
|
||||||
import * as error from '../../error'
|
|
||||||
import * as modalProvider from '../../providers/modal'
|
import * as modalProvider from '../../providers/modal'
|
||||||
|
import * as templates from './templates'
|
||||||
|
|
||||||
import CreateForm, * as createForm from './createForm'
|
import CreateForm, * as createForm from './createForm'
|
||||||
|
import Dropdown from './dropdown'
|
||||||
|
|
||||||
// =========================
|
// =========================
|
||||||
// === ProjectCreateForm ===
|
// === ProjectCreateForm ===
|
||||||
@ -15,78 +17,81 @@ import CreateForm, * as createForm from './createForm'
|
|||||||
/** Props for a {@link ProjectCreateForm}. */
|
/** Props for a {@link ProjectCreateForm}. */
|
||||||
export interface ProjectCreateFormProps extends createForm.CreateFormPassthroughProps {
|
export interface ProjectCreateFormProps extends createForm.CreateFormPassthroughProps {
|
||||||
directoryId: backendModule.DirectoryId
|
directoryId: backendModule.DirectoryId
|
||||||
|
getNewProjectName: (templateId: string | null) => string
|
||||||
onSuccess: () => void
|
onSuccess: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A form to create a project. */
|
/** A form to create a project. */
|
||||||
function ProjectCreateForm(props: ProjectCreateFormProps) {
|
function ProjectCreateForm(props: ProjectCreateFormProps) {
|
||||||
const { directoryId, onSuccess, ...passThrough } = props
|
const { directoryId, getNewProjectName, onSuccess, ...passThrough } = props
|
||||||
const { backend } = backendProvider.useBackend()
|
const { backend } = backendProvider.useBackend()
|
||||||
const { unsetModal } = modalProvider.useSetModal()
|
const { unsetModal } = modalProvider.useSetModal()
|
||||||
|
|
||||||
|
const [defaultName, setDefaultName] = react.useState(() => getNewProjectName(null))
|
||||||
const [name, setName] = react.useState<string | null>(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) {
|
const onSubmit = async (event: react.FormEvent) => {
|
||||||
return <></>
|
event.preventDefault()
|
||||||
} else {
|
unsetModal()
|
||||||
const onSubmit = async (event: react.FormEvent) => {
|
const finalName = name ?? defaultName
|
||||||
event.preventDefault()
|
const templateText = templateId == null ? '' : `from template '${templateId}'`
|
||||||
if (name == null) {
|
await toast.promise(
|
||||||
toast.error('Please provide a project name.')
|
backend.createProject({
|
||||||
} else {
|
parentDirectoryId: directoryId,
|
||||||
unsetModal()
|
projectName: name ?? defaultName,
|
||||||
await toast
|
projectTemplateName: templateId,
|
||||||
.promise(
|
}),
|
||||||
backend.createProject({
|
{
|
||||||
parentDirectoryId: directoryId,
|
loading: `Creating project '${finalName}'${templateText}...`,
|
||||||
projectName: name,
|
success: `Sucessfully created project '${finalName}'${templateText}.`,
|
||||||
projectTemplateName: template,
|
// This is UNSAFE, as the original function's parameter is of type `any`.
|
||||||
}),
|
error: (promiseError: Error) =>
|
||||||
{
|
`Error creating project '${finalName}'${templateText}: ${promiseError.message}`,
|
||||||
loading: 'Creating project...',
|
|
||||||
success: 'Sucessfully created project.',
|
|
||||||
error: error.unsafeIntoErrorMessage,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.then(onSuccess)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CreateForm title="New Project" onSubmit={onSubmit} {...passThrough}>
|
|
||||||
<div className="flex flex-row flex-nowrap m-1">
|
|
||||||
<label className="inline-block flex-1 grow m-1" htmlFor="project_name">
|
|
||||||
Name
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="project_name"
|
|
||||||
type="text"
|
|
||||||
size={1}
|
|
||||||
className="bg-gray-200 rounded-full flex-1 grow-2 px-2 m-1"
|
|
||||||
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)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</CreateForm>
|
|
||||||
)
|
)
|
||||||
|
onSuccess()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CreateForm title="New Project" onSubmit={onSubmit} {...passThrough}>
|
||||||
|
<div className="flex flex-row flex-nowrap m-1">
|
||||||
|
<label className="inline-block flex-1 grow m-1" htmlFor="project_name">
|
||||||
|
Name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="project_name"
|
||||||
|
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">
|
||||||
|
<label className="inline-block flex-1 grow m-1" htmlFor="project_template_name">
|
||||||
|
Template
|
||||||
|
</label>
|
||||||
|
<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
|
export default ProjectCreateForm
|
||||||
|
@ -6,7 +6,7 @@ import * as svg from '../../components/svg'
|
|||||||
// =================
|
// =================
|
||||||
|
|
||||||
/** Template metadata. */
|
/** Template metadata. */
|
||||||
interface Template {
|
export interface Template {
|
||||||
title: string
|
title: string
|
||||||
description: string
|
description: string
|
||||||
id: string
|
id: string
|
||||||
@ -14,7 +14,7 @@ interface Template {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** The full list of templates. */
|
/** The full list of templates. */
|
||||||
const TEMPLATES: Template[] = [
|
export const TEMPLATES: [Template, ...Template[]] = [
|
||||||
{
|
{
|
||||||
title: 'Colorado COVID',
|
title: 'Colorado COVID',
|
||||||
id: 'Colorado_COVID',
|
id: 'Colorado_COVID',
|
||||||
|
Loading…
Reference in New Issue
Block a user