Skip to content

feat(project-create): add the project creation page - #142

Open
huyanxius wants to merge 15 commits into
1024XEngineer:mainfrom
huyanxius:feat/141-project-create-page
Open

feat(project-create): add the project creation page#142
huyanxius wants to merge 15 commits into
1024XEngineer:mainfrom
huyanxius:feat/141-project-create-page

Conversation

@huyanxius

@huyanxius huyanxius commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

新增 /projects/new,补上 #99 里唯一还没落地的那一段:项目创建。

Closes #141

Why

main 上没有任何地方能创建项目。首页「工作流画布」卡片的两条动作都落在 /projectspages/home/index.tsx:88 的注释写明这是占位;项目中心右上角的「+ 新建项目」从 #119 加进来那天起就是 disabledtitle 写着「新建项目流程不在本模块中」。用户看得到项目列表、能删项目,唯独建不出项目——这条链的第一步是断的。

#99 把列表、创建、详情放在一起,其中列表与详情已由 #118 落地,剩下创建这一段没人接。

Change Description

  • 新增 pages/project-create/,源码 2 个文件、测试 1 个文件;app.tsx 加一条路由,shared/api 加一个当前用户读取边界,index.css 加装饰点阵的 keyframes。
  • 首页画布卡片的「创建新项目」与项目中心的「+ 新建项目」都指向 /projects/new,一份实现两个入口。「继续已有项目」本次不动,仍落项目中心。
  • 项目中心那个按钮从 disabled 换成链接;空状态里「完成新建项目流程后,项目会显示在这里」改成指向右上角的实话。
  • 11 条原子提交:用户边界、页面、入口、测试各一条,独立验收后又补了 7 条(详见下)。

一轮独立验收(只看 diff 与 issue,不看本文)后修掉的:

  • 顶栏遮挡。 这页不套 PageContainer,自己写的顶部留白在 lg 断点以下只有 48px,而 fixed 顶栏下沿在 72px,标题被压在底下。已按 PageContainer 同源的 pt-24 让位。
  • 重复提交的防护名不副实。 submit 里读的是本次 render 闭包中的 submitting,同一批事件里它还是旧值,真正拦住第二次的是按钮的 disabled。已改成 ref latch,handler 声称有的那道闸真的会拦。
  • 错误横幅不随输入撤销;禁用原因与按钮之间没有 aria-describedby;三处常量注释把出处指向未合入 PR 里的代码(grep 不到);API_CONTRACT.md 仍写着「新建项目流程:只保留禁用入口」。均已修。
  • 补了 4 个用例:名称留空、宽高越界、传输失败文案、错误随输入撤销——原先只有重名那一半有测试。

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 请求校验一致),三档退成快捷填充。
  • 名称限 20 字,取两个后端 PR 里更严的那个。 feat(backend): 后端全栈实现——基础设施 + 领域模块 + 用户认证 + API 契约 #75ProjectCreate 写的是 max_length=20,与 mainproject/model.pyString(20) 一致;feat: integrate verified Windup source snapshot #126 放宽到 64 并同时改了 model。按 20 实现在两种契约下都能提交成功。后端定稿后放宽是加法,反过来是破坏。
  • 拿不到当前用户就不让写。 后端 POST /projectsuser_id 放在请求体里,不从 token 推导,而 main 上没有任何「当前用户是谁」的来源。新增 registerCurrentUserIdProvider / getCurrentUserId,照 getApiAccessToken 那套惰性 provider 的写法;没有人注册时返回 null,页面禁用提交并在页面上写明原因,不塞常量假 ID 让它看起来能用。
  • 错误分两类。 assertSuccessfulEnvelope 把后端 message 原样带进 ApiErrorshared/api/index.ts:128),所以重名显示的是后端那句「项目名称已存在」,只有传输失败才收敛成「项目暂时无法创建」。两种情况都不清空已填内容。
  • 重复提交靠 ref latch,不只靠 disabled。 按钮变灰之前的那一帧仍可能收到第二次事件,而 submitting 在同一批事件里读到的是上一次 render 的值,挡不住;inFlight ref 在 submit 入口同步判断并置位,请求失败时释放。
  • 点阵是纯 CSS。 17×17 共 289 个空元素,动画延迟由每格到中心的环号与 (x+y) % 4 相位算出,四组 keyframes 错峰循环。没有 canvas、没有 pointer 监听、不引任何依赖,prefers-reduced-motion 下整体停掉。它画的是画布方框、四角把手和一支光标形状;那支光标是画出来的图形,不跟随真实鼠标。
  • 不套 PageContainer 这页左右满幅,全局顶栏保留:路由挂在 AppShellRoute 下,与 /projects 平级。

Screenshots

Beforemain 上的项目中心,「+ 新建项目」是灰的,title 写着流程不在本模块中:

before-projects

Before — 首页画布卡片展开后的两条动作,此时都指向 /projects

before-home

After/projects/new,左侧点阵、右侧表单,顶栏保留:

after-form

After — 重名时显示后端原话,已填内容不丢:

after-duplicate

After — 创建成功后项目进入列表,「+ 新建项目」已是可点的入口:

after-projects

截图取自本地 dev server,配一个只存在于本机、按 PR #75ProjectCreate / 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

Related

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.
@vercel

vercel Bot commented Aug 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
windup Ready Ready Preview Aug 6, 2026 2:13pm

@fennoai fennoai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new create flow depends on a current-user provider, but this branch never registers one in production. As shipped, /projects/new is reachable from Home/Projects yet the submit button stays disabled forever outside tests.

/** 创建真实项目;首页画布入口与项目中心的新建按钮共用这一页。 */
export function ProjectCreatePage() {
const navigate = useNavigate()
const ownerId = getCurrentUserId()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

确实,运行时确实没人注册 provider。

但是是因为登录还没做,后端 /auth 在 PR #75,前端还没有实现。这个 provider 就是给登录留的位置,登录接上后本页不用改就能用。

现在不随便塞一个,是因为 POST /projects 也不在 main 上,塞了只会把「按钮灰着」变成「按下去报错」。

这个功能我们会在后续单独 PR 实现。

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fennoai /review

@fennoai fennoai Bot Aug 6, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.0 engine 要求有警告)
  • 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(通过)

View job run

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fennoai 查询你的长期记忆有没有写着在当前repo 对我 comment 使用英文

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fennoai 纠正:查询你的长期记忆有没有写着在当前repo 对我 comment 使用中文,非英文

@fennoai fennoai Bot Aug 6, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

查询结果

是的,长期记忆里确实写着:在 1024XEngineer/Windup 仓库里,FennoAI 发给你的 GitHub comment 使用中文。

没有看到“当前 repo 使用英文 comment”的记忆。

View job run

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.0 engine 要求有警告)
  • 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(通过)

View job run

该问题属实,我已修改,请你继续 review 该PR

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fennoai 该问题属实,我已修改,请你继续 review 该PR

@fennoai fennoai Bot Aug 6, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

复审结果

这版没有发现新的问题。前一个关于尺寸预设按钮不会清错误横幅的问题已经修掉了;我也复跑了相关测试和 typecheck,都通过了。

验证:

  • npm run test -- src/pages/project-create/index.test.tsx src/entities/project/index.test.ts src/shared/api/index.test.ts
  • npm run typecheck

View job run

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: 实现项目创建页 /projects/new(Refs #99)

1 participant