import { EnterpriseBadge, Select, Typography } from "@humansignal/ui";
|
import React from "react";
|
import { useHistory } from "react-router";
|
import { ToggleItems } from "../../components";
|
import { Button } from "@humansignal/ui";
|
import { Modal } from "../../components/Modal/Modal";
|
import { Space } from "../../components/Space/Space";
|
import { HeidiTips } from "../../components/HeidiTips/HeidiTips";
|
import { useAPI } from "../../providers/ApiProvider";
|
import { cn } from "../../utils/bem";
|
import { ConfigPage } from "./Config/Config";
|
import "./CreateProject.scss";
|
import { ImportPage } from "./Import/Import";
|
import { useImportPage } from "./Import/useImportPage";
|
import { useDraftProject } from "./utils/useDraftProject";
|
import { Input, TextArea } from "../../components/Form";
|
import { FF_LSDV_E_297, isFF } from "../../utils/feature-flags";
|
import { createURL } from "../../components/HeidiTips/utils";
|
|
const ProjectName = ({ name, setName, onSaveName, onSubmit, error, description, setDescription, show = true }) =>
|
!show ? null : (
|
<form
|
className={cn("project-name")}
|
onSubmit={(e) => {
|
e.preventDefault();
|
onSubmit();
|
}}
|
>
|
<div className="w-full flex flex-col gap-2">
|
<label className="w-full" htmlFor="project_name">
|
项目名称
|
</label>
|
<Input
|
name="name"
|
id="project_name"
|
value={name}
|
onChange={(e) => setName(e.target.value)}
|
onBlur={onSaveName}
|
className="project-title w-full"
|
/>
|
{error && <span className="-mt-1 text-negative-content">{error}</span>}
|
</div>
|
<div className="w-full flex flex-col gap-2">
|
<label className="w-full" htmlFor="project_description">
|
描述
|
</label>
|
<TextArea
|
name="description"
|
id="project_description"
|
placeholder="项目的可选描述"
|
rows="4"
|
style={{ minHeight: 100 }}
|
value={description}
|
onChange={(e) => setDescription(e.target.value)}
|
className="project-description w-full"
|
/>
|
</div>
|
{isFF(FF_LSDV_E_297) && (
|
<div className="w-full flex flex-col gap-2">
|
<label>
|
工作空间
|
<EnterpriseBadge className="ml-2" />
|
</label>
|
<Select placeholder="选择一个选项" disabled options={[]} triggerClassName="!flex-1" />
|
<Typography size="small" className="mt-tight mb-wider">
|
通过将项目组织到工作空间中来简化项目管理。{" "}
|
<a
|
href={createURL(
|
"https://docs.humansignal.com/guide/manage_projects#Create-workspaces-to-organize-projects",
|
{
|
experiment: "project_creation_dropdown",
|
treatment: "simplify_project_management",
|
},
|
)}
|
target="_blank"
|
rel="noreferrer"
|
className="underline hover:no-underline"
|
>
|
了解更多
|
</a>
|
</Typography>
|
<HeidiTips collection="projectCreation" />
|
</div>
|
)}
|
</form>
|
);
|
|
export const CreateProject = ({ onClose }) => {
|
const [step, _setStep] = React.useState("name"); // name | import | config
|
const [waiting, setWaitingStatus] = React.useState(false);
|
|
const { project, setProject: updateProject } = useDraftProject();
|
const history = useHistory();
|
const api = useAPI();
|
|
const [name, setName] = React.useState("");
|
const [error, setError] = React.useState();
|
const [description, setDescription] = React.useState("");
|
const [sample, setSample] = React.useState(null);
|
|
const setStep = React.useCallback((step) => {
|
_setStep(step);
|
const eventNameMap = {
|
name: "project_name",
|
import: "data_import",
|
config: "labeling_setup",
|
};
|
__lsa(`create_project.tab.${eventNameMap[step]}`);
|
}, []);
|
|
React.useEffect(() => {
|
setError(null);
|
}, [name]);
|
|
const { columns, uploading, uploadDisabled, finishUpload, pageProps, uploadSample } = useImportPage(project, sample);
|
|
const rootClass = cn("create-project");
|
const tabClass = rootClass.elem("tab");
|
const steps = {
|
name: <span className={tabClass.mod({ disabled: !!error })}>项目名称</span>,
|
import: <span className={tabClass.mod({ disabled: uploadDisabled })}>数据导入</span>,
|
config: "标注设置",
|
};
|
|
// name intentionally skipped from deps:
|
// this should trigger only once when we got project loaded
|
React.useEffect(() => {
|
project && !name && setName(project.title);
|
}, [project]);
|
|
const projectBody = React.useMemo(
|
() => ({
|
title: name,
|
description,
|
label_config: project?.label_config ?? "<View></View>",
|
}),
|
[name, description, project?.label_config],
|
);
|
|
const onCreate = React.useCallback(async () => {
|
// First, persist project with label_config so import/reimport validates against it
|
const response = await api.callApi("updateProject", {
|
params: {
|
pk: project.id,
|
},
|
body: { ...projectBody, is_draft: false },
|
});
|
|
if (response === null) return;
|
|
const imported = await finishUpload();
|
|
if (!imported) return;
|
|
setWaitingStatus(true);
|
|
if (sample) await uploadSample(sample);
|
|
__lsa("create_project.create", { sample: sample?.url });
|
|
setWaitingStatus(false);
|
|
history.push(`/projects/${response.id}/data`);
|
}, [project, projectBody, finishUpload]);
|
|
const onSaveName = async () => {
|
if (error) return;
|
const res = await api.callApi("updateProjectRaw", {
|
params: {
|
pk: project.id,
|
},
|
body: {
|
title: name,
|
},
|
});
|
|
if (res.ok) return;
|
const err = await res.json();
|
|
setError(err.validation_errors?.title);
|
};
|
|
const onDelete = React.useCallback(() => {
|
const performClose = async () => {
|
setWaitingStatus(true);
|
if (project)
|
await api.callApi("deleteProject", {
|
params: {
|
pk: project.id,
|
},
|
});
|
setWaitingStatus(false);
|
updateProject(null);
|
onClose?.();
|
};
|
performClose();
|
}, [project]);
|
|
return (
|
<Modal onHide={onDelete} closeOnClickOutside={false} allowToInterceptEscape fullscreen visible bare>
|
<div className={rootClass}>
|
<Modal.Header>
|
<h1>创建项目</h1>
|
<ToggleItems items={steps} active={step} onSelect={setStep} />
|
|
<Space>
|
<Button
|
variant="negative"
|
look="outlined"
|
onClick={onDelete}
|
waiting={waiting}
|
aria-label="取消创建项目"
|
>
|
取消
|
</Button>
|
<Button
|
look="primary"
|
onClick={onCreate}
|
waiting={waiting || uploading}
|
waitingClickable={false}
|
disabled={!project || uploadDisabled || error}
|
>
|
保存
|
</Button>
|
</Space>
|
</Modal.Header>
|
<ProjectName
|
name={name}
|
setName={setName}
|
error={error}
|
onSaveName={onSaveName}
|
onSubmit={onCreate}
|
description={description}
|
setDescription={setDescription}
|
show={step === "name"}
|
/>
|
<ImportPage
|
project={project}
|
show={step === "import"}
|
sample={sample}
|
onSampleDatasetSelect={setSample}
|
openLabelingConfig={() => setStep("config")}
|
{...pageProps}
|
/>
|
<ConfigPage
|
project={project}
|
onUpdate={(config) => {
|
updateProject({ ...project, label_config: config });
|
}}
|
show={step === "config"}
|
columns={columns}
|
disableSaveButton={true}
|
/>
|
</div>
|
</Modal>
|
);
|
};
|