feat(project-create): add the project creation page - #142
Conversation
Most backend write endpoints carry user_id in the request body instead of deriving it from the token, so pages need the signed-in user id on its own. Register and read it through the same lazy provider pattern already used for the access token, returning null when no login module is mounted. Callers can now require a real user id before writing, without falling back to a constant placeholder.
Nothing in the app could create a project: the entry existed only as a disabled button and a link to the project list. Add /projects/new with the fields the Project contract defines, submitting through ProjectApis and reporting business failures with the reason the backend gives. Users reach a real creation form, and the page keeps itself closed while no signed-in user is available.
The home canvas card and the project center both advertised project creation while leading somewhere that could not create anything. Send the home canvas action and the project center button to /projects/new, and rewrite the empty state copy that referred to a flow living elsewhere. Creation now has one destination reachable from either place.
Creation crosses the page, the entity adapter and the HTTP envelope, so the mapped request body and the failure paths need to be pinned down. Teach the shared test backend to answer POST /projects including the duplicate name rejection, then assert the payload, the guards and both entry links. Regressions in the field mapping, the submit guard or the entry points now fail the suite.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
| /** 创建真实项目;首页画布入口与项目中心的新建按钮共用这一页。 */ | ||
| export function ProjectCreatePage() { | ||
| const navigate = useNavigate() | ||
| const ownerId = getCurrentUserId() |
There was a problem hiding this comment.
High: getCurrentUserId() is only backed by test code in this branch. I can't find any runtime registration of registerCurrentUserIdProvider, so ownerId is always null in the app and the form cannot submit for real users.
There was a problem hiding this comment.
确实,运行时确实没人注册 provider。
但是是因为登录还没做,后端 /auth 在 PR #75,前端还没有实现。这个 provider 就是给登录留的位置,登录接上后本页不用改就能用。
现在不随便塞一个,是因为 POST /projects 也不在 main 上,塞了只会把「按钮灰着」变成「按下去报错」。
这个功能我们会在后续单独 PR 实现。
There was a problem hiding this comment.
Review Result
发现 1 个需要处理的问题:
- P2
frontend/src/pages/project-create/index.tsx:181— 尺寸快捷按钮修正了宽高,但不会撤掉已有错误横幅。表单依赖<form onChange={() => setError(null)}>清理错误,普通输入和 select 会触发这个路径;这里的预设按钮是type="button"并只在onClick里调用setSpriteWidth/setSpriteHeight,不会触发表单change。复现路径:把宽度改成16提交,出现“精灵宽高需要是 32 到 2048 之间的整数”,再点256 × 256,当前值已经合法但错误仍留在页面上。建议在预设按钮的onClick里同步setError(null),并补一个覆盖这个路径的用例。
验证:
npm ci(通过;sandbox 的 Node 20 对react-router@8.3.0的>=22.22.0engine 要求有警告)npm run test -- src/pages/project-create/index.test.tsx src/entities/project/index.test.ts src/shared/api/index.test.ts(通过,27 tests)npm run typecheck(通过)
There was a problem hiding this comment.
@fennoai 查询你的长期记忆有没有写着在当前repo 对我 comment 使用英文
There was a problem hiding this comment.
@fennoai 纠正:查询你的长期记忆有没有写着在当前repo 对我 comment 使用中文,非英文
There was a problem hiding this comment.
查询结果
是的,长期记忆里确实写着:在 1024XEngineer/Windup 仓库里,FennoAI 发给你的 GitHub comment 使用中文。
没有看到“当前 repo 使用英文 comment”的记忆。
There was a problem hiding this comment.
Review Result
发现 1 个需要处理的问题:
- P2
frontend/src/pages/project-create/index.tsx:181— 尺寸快捷按钮修正了宽高,但不会撤掉已有错误横幅。表单依赖<form onChange={() => setError(null)}>清理错误,普通输入和 select 会触发这个路径;这里的预设按钮是type="button"并只在onClick里调用setSpriteWidth/setSpriteHeight,不会触发表单change。复现路径:把宽度改成16提交,出现“精灵宽高需要是 32 到 2048 之间的整数”,再点256 × 256,当前值已经合法但错误仍留在页面上。建议在预设按钮的onClick里同步setError(null),并补一个覆盖这个路径的用例。验证:
npm ci(通过;sandbox 的 Node 20 对react-router@8.3.0的>=22.22.0engine 要求有警告)npm run test -- src/pages/project-create/index.test.tsx src/entities/project/index.test.ts src/shared/api/index.test.ts(通过,27 tests)npm run typecheck(通过)
该问题属实,我已修改,请你继续 review 该PR
There was a problem hiding this comment.
复审结果
这版没有发现新的问题。前一个关于尺寸预设按钮不会清错误横幅的问题已经修掉了;我也复跑了相关测试和 typecheck,都通过了。
验证:
npm run test -- src/pages/project-create/index.test.tsx src/entities/project/index.test.ts src/shared/api/index.test.tsnpm run typecheck
The page sets its own padding instead of using PageContainer, and below the lg breakpoint that padding was shorter than the fixed header, so the eyebrow and heading sat underneath it. Raise the top padding to the same value PageContainer reserves for the header, and note where the number comes from. The heading clears the header at every width instead of only on wide screens.
The submit handler read the submitting flag out of its own render closure, so within one batch of events the second submit still saw the stale value and only the disabled attribute stopped it. Hold the in-flight state in a ref and check that before doing any work, releasing it when the request fails. The guard the handler claims to have is the one that actually runs.
The error banner stayed on screen while the user corrected the field it complained about, so the page kept accusing them of something they had already fixed. Clear it from a single change handler on the form, which every control inside already bubbles to. The banner describes the current state of the form rather than the last attempt.
A disabled button takes no focus, so the sentence explaining why creation is closed sat next to it with nothing linking the two. Give the sentence an id and point the button at it with aria-describedby. Assistive technology reaches the reason from the control it applies to.
The comments on the field limits pointed at a ProjectCreate schema and a relaxed name length that exist only in unmerged backend PRs, so a reader grepping the repository finds nothing. Name what main actually carries, mark the rest as pending, and record that the style cap is a frontend decision rather than a contract. Each limit can be traced to something a reader can open.
Only the duplicate-name failure had a test, leaving the empty name, the out-of-range sprite size, the transport error message and the error dismissal unproven. Add four cases asserting each guard blocks the request or reports the expected sentence. The validation branches the page carries are all exercised.
The contract listed the creation flow among the things this round would not implement, which stopped being true, and said nothing about where user_id comes from. Move the note into the Project section and state that a missing current-user source keeps the entry disabled rather than substituting a placeholder. The document matches what the frontend now does.
The banner is dismissed from a change handler on the form, but the size presets are buttons that write the width and height in code, so clicking one fixed the value and left the complaint on screen. Clear the error inside the preset handler as well, and cover the path with a test. The fastest way to correct a size no longer leaves a stale message behind.
The backend moved project ownership out of the request body: ProjectCreate no longer declares user_id and every /projects route reads request.state.current_user.id, which closed a hole where a caller could create a project for someone else. Drop ownerId from CreateProjectInput, stop sending user_id, and gate the create page on getApiAccessToken() instead of a current user id. The entry now opens on the condition the backend actually enforces, and its disabled hint says a sign-in is required.
The provider existed only because POST /projects used to take user_id in its body; with ownership read from the token nothing consumes it. Remove the CurrentUserIdProvider type, its registration function and getCurrentUserId. The module keeps one boundary for the auth module to fill instead of two, and no caller can mistake a user id for proof of a session.
The Project section still described user_id travelling in the create body and pointed at a helper that no longer exists. Rewrite that paragraph around the token-derived ownership and the sign-in gate. The document matches the contract the code now targets.
新增
/projects/new,补上 #99 里唯一还没落地的那一段:项目创建。Closes #141
Why
main上没有任何地方能创建项目。首页「工作流画布」卡片的两条动作都落在/projects,pages/home/index.tsx:88的注释写明这是占位;项目中心右上角的「+ 新建项目」从 #119 加进来那天起就是disabled,title写着「新建项目流程不在本模块中」。用户看得到项目列表、能删项目,唯独建不出项目——这条链的第一步是断的。#99 把列表、创建、详情放在一起,其中列表与详情已由 #118 落地,剩下创建这一段没人接。
Change Description
pages/project-create/,源码 2 个文件、测试 1 个文件;app.tsx加一条路由,shared/api加一个当前用户读取边界,index.css加装饰点阵的 keyframes。/projects/new,一份实现两个入口。「继续已有项目」本次不动,仍落项目中心。disabled换成链接;空状态里「完成新建项目流程后,项目会显示在这里」改成指向右上角的实话。一轮独立验收(只看 diff 与 issue,不看本文)后修掉的:
PageContainer,自己写的顶部留白在 lg 断点以下只有 48px,而 fixed 顶栏下沿在 72px,标题被压在底下。已按PageContainer同源的pt-24让位。submit里读的是本次 render 闭包中的submitting,同一批事件里它还是旧值,真正拦住第二次的是按钮的disabled。已改成 ref latch,handler 声称有的那道闸真的会拦。aria-describedby;三处常量注释把出处指向未合入 PR 里的代码(grep不到);API_CONTRACT.md仍写着「新建项目流程:只保留禁用入口」。均已修。Implementation Approach
CreateProjectInput,不照 live demo。 视觉参考了 asset-lab 那版「新建角色项目」,但它是 mock:「启动方式」在架构里没有对应物(feat: 实现单角色 Workflow Editor 卡片画布 #120 已把 Workflow 模板复用划到范围外),名称限 48 字,尺寸只有三档正方形的单选。这里只保留契约里真有的六个字段,尺寸拆成宽高两个数字输入(32–2048,取自 issue feat: 实现项目创建页 /projects/new(Refs #99) #141 的产品规则,与未合入的 feat(backend): 后端全栈实现——基础设施 + 领域模块 + 用户认证 + API 契约 #75 / feat: integrate verified Windup source snapshot #126 请求校验一致),三档退成快捷填充。ProjectCreate写的是max_length=20,与main上project/model.py的String(20)一致;feat: integrate verified Windup source snapshot #126 放宽到 64 并同时改了 model。按 20 实现在两种契约下都能提交成功。后端定稿后放宽是加法,反过来是破坏。POST /projects把user_id放在请求体里,不从 token 推导,而main上没有任何「当前用户是谁」的来源。新增registerCurrentUserIdProvider/getCurrentUserId,照getApiAccessToken那套惰性 provider 的写法;没有人注册时返回null,页面禁用提交并在页面上写明原因,不塞常量假 ID 让它看起来能用。assertSuccessfulEnvelope把后端 message 原样带进ApiError(shared/api/index.ts:128),所以重名显示的是后端那句「项目名称已存在」,只有传输失败才收敛成「项目暂时无法创建」。两种情况都不清空已填内容。submitting在同一批事件里读到的是上一次 render 的值,挡不住;inFlightref 在submit入口同步判断并置位,请求失败时释放。(x+y) % 4相位算出,四组 keyframes 错峰循环。没有 canvas、没有 pointer 监听、不引任何依赖,prefers-reduced-motion下整体停掉。它画的是画布方框、四角把手和一支光标形状;那支光标是画出来的图形,不跟随真实鼠标。PageContainer。 这页左右满幅,全局顶栏保留:路由挂在AppShellRoute下,与/projects平级。Screenshots
Before —
main上的项目中心,「+ 新建项目」是灰的,title写着流程不在本模块中:Before — 首页画布卡片展开后的两条动作,此时都指向
/projects:After —
/projects/new,左侧点阵、右侧表单,顶栏保留:After — 重名时显示后端原话,已填内容不丢:
After — 创建成功后项目进入列表,「+ 新建项目」已是可点的入口:
截图取自本地 dev server,配一个只存在于本机、按 PR #75 的
ProjectCreate/ProjectOut形状回应的桩服务(业务错误照后端习惯走 HTTP 200 +code != 200)。为让创建入口可用,本机临时注册了一个返回固定user_id的 provider。桩服务与那行临时注册都不进仓库。Testing
本地执行了 CI 工作流中的全部步骤:
npm run format:check— 通过(58 个文件)npm run lint— 通过,无告警npm run typecheck— 通过npm run test— 通过(11 个文件 / 48 个用例,其中 project-create 9 个用例)npm run build— 通过(280.25 kB / gzip 87.98 kB)浏览器手工核对:两个入口都进
/projects/new;重名报错后表单内容保留;改名后提交成功并跳进该项目的资产工作区,侧栏规格与表单填的一致;未注册用户 provider 时提交按钮为灰并给出原因。Follow-ups
sprite_sample_url后端已有,但前端还没有文件上传能力(feat: 实现前端媒体上传适配与错误边界(Refs #70) #109 / feat(media): add validated upload adapter #111)。严格照契约做出来会是一个「请粘贴图片链接」的输入框,产品上是负分,因此本 PR 有意不放这个字段,上传适配落地后单独提 PR 补。/projects/:projectId/history。/workflow-editor/:runId需要一个 runId,而WorkflowController在main上只有接口没有实现(PR feat(workflow-controller): add resumable workflow coordination #107),编辑器本身也还是占位页(feat: 实现单角色 Workflow Editor 卡片画布 #120)。现在创建成功后落该项目的资产工作区。/projects与/auth都还没进main(feat(backend): 后端全栈实现——基础设施 + 领域模块 + 用户认证 + API 契约 #75 / feat: integrate verified Windup source snapshot #126)。接口挂载前,部署环境里这页的创建入口是禁用状态。project_name限长不一致(feat(backend): 后端全栈实现——基础设施 + 领域模块 + 用户认证 + API 契约 #75 是 20,feat: integrate verified Windup source snapshot #126 是 64),需要定一个。另外POST /projects在请求体里收user_id而不是从登录态取,在已有 JWT 与/auth/me的前提下意味着可以替别人建项目,这条也值得后端确认。Related