mirror of
https://github.com/iflytek/skillhub.git
synced 2026-11-14 09:01:16 +00:00
feat(web): unify console layout and refine skill card presentation
Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
This commit is contained in:
parent
897880d5df
commit
f0cd9a555d
20 changed files with 387 additions and 194 deletions
|
|
@ -18,7 +18,7 @@
|
|||
{"name":"joinRoute","path":"join","declaration":{"file":"web/src/app/router.tsx","line":357},"implementation":{"file":"web/src/pages/join.tsx","line":12,"export":"JoinPage"},"guarded":true,"roleProtected":false},
|
||||
{"name":"skillDetailRoute","path":"/space/$namespace/$slug","declaration":{"file":"web/src/app/router.tsx","line":364},"implementation":{"file":"web/src/pages/skill-detail.tsx","line":133,"export":"SkillDetailPage"},"guarded":false,"roleProtected":false},
|
||||
{"name":"skillVersionCompareRoute","path":"/space/$namespace/$slug/compare","declaration":{"file":"web/src/app/router.tsx","line":374},"implementation":{"file":"web/src/pages/skill-version-compare.tsx","line":5,"export":"SkillVersionComparePage"},"guarded":false,"roleProtected":false},
|
||||
{"name":"dashboardRoute","path":"dashboard","declaration":{"file":"web/src/app/router.tsx","line":384},"implementation":{"file":"web/src/pages/dashboard.tsx","line":98,"export":"DashboardPage"},"guarded":true,"roleProtected":false},
|
||||
{"name":"dashboardRoute","path":"dashboard","declaration":{"file":"web/src/app/router.tsx","line":384},"implementation":{"file":"web/src/pages/dashboard.tsx","line":97,"export":"DashboardPage"},"guarded":true,"roleProtected":false},
|
||||
{"name":"dashboardSkillsRoute","path":"dashboard/skills","declaration":{"file":"web/src/app/router.tsx","line":391},"implementation":{"file":"web/src/pages/dashboard/my-skills.tsx","line":54,"export":"MySkillsPage"},"guarded":true,"roleProtected":false},
|
||||
{"name":"dashboardPublishRoute","path":"dashboard/publish","declaration":{"file":"web/src/app/router.tsx","line":404},"implementation":{"file":"web/src/pages/dashboard/publish.tsx","line":41,"export":"PublishPage"},"guarded":true,"roleProtected":false},
|
||||
{"name":"dashboardSuiteCreateRoute","path":"dashboard/suites/new","declaration":{"file":"web/src/app/router.tsx","line":430},"implementation":{"file":"web/src/pages/dashboard/suite-editor.tsx","line":716,"export":"SuiteCreatePage"},"guarded":true,"roleProtected":false},
|
||||
|
|
@ -59,55 +59,55 @@
|
|||
{"name":"adminNamespacesRoute","path":"admin/namespaces","declaration":{"file":"web/src/app/router.tsx","line":770},"implementation":{"file":"web/src/pages/admin/namespaces.tsx","line":104,"export":"AdminNamespacesPage"},"guarded":true,"roleProtected":true}
|
||||
],
|
||||
"definitions": [
|
||||
{"file":"web/src/app/layout.tsx","line":89,"ids":["navigation.home.open"]},
|
||||
{"file":"web/src/app/layout.tsx","line":90,"ids":["skill.publish.open"]},
|
||||
{"file":"web/src/app/layout.tsx","line":91,"ids":["navigation.skills.open"]},
|
||||
{"file":"web/src/app/layout.tsx","line":92,"ids":["navigation.github.open"]},
|
||||
{"file":"web/src/app/layout.tsx","line":93,"ids":["navigation.suites.open"]},
|
||||
{"file":"web/src/app/layout.tsx","line":94,"ids":["navigation.dashboard.open"]},
|
||||
{"file":"web/src/app/layout.tsx","line":88,"ids":["navigation.home.open"]},
|
||||
{"file":"web/src/app/layout.tsx","line":89,"ids":["skill.publish.open"]},
|
||||
{"file":"web/src/app/layout.tsx","line":90,"ids":["navigation.skills.open"]},
|
||||
{"file":"web/src/app/layout.tsx","line":91,"ids":["navigation.github.open"]},
|
||||
{"file":"web/src/app/layout.tsx","line":92,"ids":["navigation.suites.open"]},
|
||||
{"file":"web/src/app/layout.tsx","line":93,"ids":["navigation.dashboard.open"]},
|
||||
{"file":"web/src/features/namespace/namespace-management-nav.tsx","line":26,"ids":["namespace.skills.open"]},
|
||||
{"file":"web/src/features/namespace/namespace-management-nav.tsx","line":27,"ids":["namespace.members.open"]},
|
||||
{"file":"web/src/features/namespace/namespace-management-nav.tsx","line":28,"ids":["namespace.reviews.open"]},
|
||||
{"file":"web/src/features/namespace/namespace-management-nav.tsx","line":29,"ids":["namespace.settings.open"]},
|
||||
{"file":"web/src/pages/dashboard.tsx","line":41,"ids":["account.profile.open"]},
|
||||
{"file":"web/src/pages/dashboard.tsx","line":42,"ids":["account.security.open"]},
|
||||
{"file":"web/src/pages/dashboard.tsx","line":43,"ids":["notification.settings.open"]},
|
||||
{"file":"web/src/pages/dashboard.tsx","line":49,"ids":["navigation.owned.skills"]},
|
||||
{"file":"web/src/pages/dashboard.tsx","line":50,"ids":["navigation.owned.suites"]},
|
||||
{"file":"web/src/pages/dashboard.tsx","line":51,"ids":["namespace.list.open"]},
|
||||
{"file":"web/src/pages/dashboard.tsx","line":52,"ids":["navigation.starred.open"]},
|
||||
{"file":"web/src/pages/dashboard.tsx","line":53,"ids":["navigation.subscriptions.open"]},
|
||||
{"file":"web/src/pages/dashboard.tsx","line":54,"ids":["account.tokens.open"]},
|
||||
{"file":"web/src/pages/dashboard.tsx","line":55,"ids":["review.progress.open"]},
|
||||
{"file":"web/src/pages/dashboard.tsx","line":61,"ids":["governance.center.open"]},
|
||||
{"file":"web/src/pages/dashboard.tsx","line":62,"ids":["report.queue.open"]},
|
||||
{"file":"web/src/pages/dashboard.tsx","line":63,"ids":["github.sync.open"]},
|
||||
{"file":"web/src/pages/dashboard.tsx","line":87,"ids":["navigation.owned.skills"]},
|
||||
{"file":"web/src/pages/dashboard.tsx","line":88,"ids":["navigation.owned.suites"]},
|
||||
{"file":"web/src/pages/dashboard.tsx","line":89,"ids":["account.tokens.open"]},
|
||||
{"file":"web/src/pages/dashboard.tsx","line":90,"ids":["navigation.starred.open"]},
|
||||
{"file":"web/src/pages/dashboard.tsx","line":91,"ids":["account.profile.open"]},
|
||||
{"file":"web/src/pages/dashboard.tsx","line":92,"ids":["account.security.open"]}
|
||||
{"file":"web/src/pages/dashboard.tsx","line":40,"ids":["account.profile.open"]},
|
||||
{"file":"web/src/pages/dashboard.tsx","line":41,"ids":["account.security.open"]},
|
||||
{"file":"web/src/pages/dashboard.tsx","line":42,"ids":["notification.settings.open"]},
|
||||
{"file":"web/src/pages/dashboard.tsx","line":48,"ids":["navigation.owned.skills"]},
|
||||
{"file":"web/src/pages/dashboard.tsx","line":49,"ids":["navigation.owned.suites"]},
|
||||
{"file":"web/src/pages/dashboard.tsx","line":50,"ids":["namespace.list.open"]},
|
||||
{"file":"web/src/pages/dashboard.tsx","line":51,"ids":["navigation.starred.open"]},
|
||||
{"file":"web/src/pages/dashboard.tsx","line":52,"ids":["navigation.subscriptions.open"]},
|
||||
{"file":"web/src/pages/dashboard.tsx","line":53,"ids":["account.tokens.open"]},
|
||||
{"file":"web/src/pages/dashboard.tsx","line":54,"ids":["review.progress.open"]},
|
||||
{"file":"web/src/pages/dashboard.tsx","line":60,"ids":["governance.center.open"]},
|
||||
{"file":"web/src/pages/dashboard.tsx","line":61,"ids":["report.queue.open"]},
|
||||
{"file":"web/src/pages/dashboard.tsx","line":62,"ids":["github.sync.open"]},
|
||||
{"file":"web/src/pages/dashboard.tsx","line":86,"ids":["navigation.owned.skills"]},
|
||||
{"file":"web/src/pages/dashboard.tsx","line":87,"ids":["navigation.owned.suites"]},
|
||||
{"file":"web/src/pages/dashboard.tsx","line":88,"ids":["account.tokens.open"]},
|
||||
{"file":"web/src/pages/dashboard.tsx","line":89,"ids":["navigation.starred.open"]},
|
||||
{"file":"web/src/pages/dashboard.tsx","line":90,"ids":["account.profile.open"]},
|
||||
{"file":"web/src/pages/dashboard.tsx","line":91,"ids":["account.security.open"]}
|
||||
],
|
||||
"sites": [
|
||||
{"file":"web/src/app/layout.tsx","line":126,"host":"Link","prop":"data-analytics-id","event":"click","ids":["navigation.home.open"]},
|
||||
{"file":"web/src/app/layout.tsx","line":139,"host":"Link","prop":"data-analytics-id","event":"click","forward":"item.analyticsId"},
|
||||
{"file":"web/src/app/layout.tsx","line":159,"host":"button","prop":"data-analytics-id","event":"click","ids":["navigation.menu.toggle"]},
|
||||
{"file":"web/src/app/layout.tsx","line":175,"host":"Link","prop":"data-analytics-id","event":"click","ids":["auth.login.open"]},
|
||||
{"file":"web/src/app/layout.tsx","line":193,"host":"Link","prop":"data-analytics-id","event":"click","forward":"item.analyticsId"},
|
||||
{"file":"web/src/app/layout.tsx","line":251,"host":"Link","prop":"data-analytics-id","event":"click","ids":["navigation.skills.open"]},
|
||||
{"file":"web/src/app/layout.tsx","line":252,"host":"Link","prop":"data-analytics-id","event":"click","ids":["skill.publish.open"]},
|
||||
{"file":"web/src/app/layout.tsx","line":253,"host":"Link","prop":"data-analytics-id","event":"click","ids":["navigation.dashboard.open"]},
|
||||
{"file":"web/src/app/layout.tsx","line":260,"host":"a","prop":"data-analytics-id","event":"click","ids":["navigation.documentation.open"]},
|
||||
{"file":"web/src/app/layout.tsx","line":261,"host":"a","prop":"data-analytics-id","event":"click","ids":["navigation.cli.open"]},
|
||||
{"file":"web/src/app/layout.tsx","line":262,"host":"a","prop":"data-analytics-id","event":"click","ids":["navigation.repository.open"]},
|
||||
{"file":"web/src/app/layout.tsx","line":269,"host":"a","prop":"data-analytics-id","event":"click","ids":["navigation.license.open"]},
|
||||
{"file":"web/src/app/layout.tsx","line":270,"host":"a","prop":"data-analytics-id","event":"click","ids":["navigation.contributing.open"]},
|
||||
{"file":"web/src/app/layout.tsx","line":271,"host":"a","prop":"data-analytics-id","event":"click","ids":["navigation.changelog.open"]},
|
||||
{"file":"web/src/app/layout.tsx","line":272,"host":"a","prop":"data-analytics-id","event":"click","ids":["navigation.conduct.open"]},
|
||||
{"file":"web/src/app/layout.tsx","line":279,"host":"a","prop":"data-analytics-id","event":"click","ids":["navigation.community.open"]},
|
||||
{"file":"web/src/app/layout.tsx","line":280,"host":"Link","prop":"data-analytics-id","event":"click","ids":["navigation.privacy.open"]},
|
||||
{"file":"web/src/app/layout.tsx","line":281,"host":"Link","prop":"data-analytics-id","event":"click","ids":["navigation.terms.open"]},
|
||||
{"file":"web/src/app/layout.tsx","line":125,"host":"Link","prop":"data-analytics-id","event":"click","ids":["navigation.home.open"]},
|
||||
{"file":"web/src/app/layout.tsx","line":138,"host":"Link","prop":"data-analytics-id","event":"click","forward":"item.analyticsId"},
|
||||
{"file":"web/src/app/layout.tsx","line":158,"host":"button","prop":"data-analytics-id","event":"click","ids":["navigation.menu.toggle"]},
|
||||
{"file":"web/src/app/layout.tsx","line":174,"host":"Link","prop":"data-analytics-id","event":"click","ids":["auth.login.open"]},
|
||||
{"file":"web/src/app/layout.tsx","line":192,"host":"Link","prop":"data-analytics-id","event":"click","forward":"item.analyticsId"},
|
||||
{"file":"web/src/app/layout.tsx","line":250,"host":"Link","prop":"data-analytics-id","event":"click","ids":["navigation.skills.open"]},
|
||||
{"file":"web/src/app/layout.tsx","line":251,"host":"Link","prop":"data-analytics-id","event":"click","ids":["skill.publish.open"]},
|
||||
{"file":"web/src/app/layout.tsx","line":252,"host":"Link","prop":"data-analytics-id","event":"click","ids":["navigation.dashboard.open"]},
|
||||
{"file":"web/src/app/layout.tsx","line":259,"host":"a","prop":"data-analytics-id","event":"click","ids":["navigation.documentation.open"]},
|
||||
{"file":"web/src/app/layout.tsx","line":260,"host":"a","prop":"data-analytics-id","event":"click","ids":["navigation.cli.open"]},
|
||||
{"file":"web/src/app/layout.tsx","line":261,"host":"a","prop":"data-analytics-id","event":"click","ids":["navigation.repository.open"]},
|
||||
{"file":"web/src/app/layout.tsx","line":268,"host":"a","prop":"data-analytics-id","event":"click","ids":["navigation.license.open"]},
|
||||
{"file":"web/src/app/layout.tsx","line":269,"host":"a","prop":"data-analytics-id","event":"click","ids":["navigation.contributing.open"]},
|
||||
{"file":"web/src/app/layout.tsx","line":270,"host":"a","prop":"data-analytics-id","event":"click","ids":["navigation.changelog.open"]},
|
||||
{"file":"web/src/app/layout.tsx","line":271,"host":"a","prop":"data-analytics-id","event":"click","ids":["navigation.conduct.open"]},
|
||||
{"file":"web/src/app/layout.tsx","line":278,"host":"a","prop":"data-analytics-id","event":"click","ids":["navigation.community.open"]},
|
||||
{"file":"web/src/app/layout.tsx","line":279,"host":"Link","prop":"data-analytics-id","event":"click","ids":["navigation.privacy.open"]},
|
||||
{"file":"web/src/app/layout.tsx","line":280,"host":"Link","prop":"data-analytics-id","event":"click","ids":["navigation.terms.open"]},
|
||||
{"file":"web/src/features/auth/auth-shell.tsx","line":19,"host":"a","prop":"data-analytics-id","event":"click","ids":["navigation.home.open"]},
|
||||
{"file":"web/src/features/auth/auth-shell.tsx","line":28,"host":"a","prop":"data-analytics-id","event":"click","ids":["navigation.home.open"]},
|
||||
{"file":"web/src/features/auth/login-button.tsx","line":71,"host":"Button","prop":"data-analytics-id","event":"click","ids":["auth.oauth.request"]},
|
||||
|
|
@ -547,34 +547,34 @@
|
|||
{"file":"web/src/pages/dashboard/my-namespaces.tsx","line":138,"host":"Button","prop":"data-analytics-id","event":"click","ids":["github.namespace.open"]},
|
||||
{"file":"web/src/pages/dashboard/my-namespaces.tsx","line":148,"host":"Pagination","prop":"analyticsId","event":"click","ids":["namespace.list.paginate"]},
|
||||
{"file":"web/src/pages/dashboard/my-namespaces.tsx","line":157,"host":"Button","prop":"data-analytics-id","event":"click","ids":["namespace.create.open"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":362,"host":"form","prop":"data-analytics-id","event":"submit","ids":["skill.owned.search"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":387,"host":"SelectTrigger","prop":"data-analytics-id","event":"click+keydown-intent","ids":["skill.owned.namespace.interact"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":391,"host":"SelectItem","prop":"data-analytics-id","event":"radix-item-intent","ids":["skill.owned.namespace"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":364,"host":"form","prop":"data-analytics-id","event":"submit","ids":["skill.owned.search"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":389,"host":"SelectTrigger","prop":"data-analytics-id","event":"click+keydown-intent","ids":["skill.owned.namespace.interact"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":393,"host":"SelectItem","prop":"data-analytics-id","event":"radix-item-intent","ids":["skill.owned.namespace"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":405,"host":"Button","prop":"data-analytics-id","event":"click","ids":["skill.owned.filter"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":423,"host":"Button","prop":"data-analytics-id","event":"click","ids":["skill.owned.clear"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":457,"host":"Card","prop":"data-analytics-id","event":"click","ids":["skill.owned.open"],"conditional":true},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":481,"host":"Button","prop":"data-analytics-id","event":"click","ids":["skill.update.open"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":507,"host":"DropdownMenuItem","prop":"data-analytics-id","event":"click","ids":["skill.update.open"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":511,"host":"DropdownMenuItem","prop":"data-analytics-id","event":"click","ids":["skill.related.open"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":514,"host":"DropdownMenuItem","prop":"data-analytics-id","event":"click","ids":["skill.visibility.open"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":518,"host":"DropdownMenuItem","prop":"data-analytics-id","event":"click","ids":["skill.review.withdraw"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":520,"host":"DropdownMenuItem","prop":"data-analytics-id","event":"click","ids":["skill.promotion.create.request","skill.promotion.update.request"],"conditional":true},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":523,"host":"DropdownMenuItem","prop":"data-analytics-id","event":"click","ids":["skill.unarchive.request"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":525,"host":"DropdownMenuItem","prop":"data-analytics-id","event":"click","ids":["skill.archive.request"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":528,"host":"DropdownMenuItem","prop":"data-analytics-id","event":"click","ids":["skill.restore.request"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":562,"host":"Pagination","prop":"analyticsId","event":"click","ids":["skill.owned.paginate"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":571,"host":"Button","prop":"data-analytics-id","event":"click","ids":["skill.owned.clear"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":593,"host":"ConfirmDialog","prop":"confirmAnalyticsId","event":"click","ids":["skill.restore.confirm"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":593,"host":"ConfirmDialog","prop":"cancelAnalyticsId","event":"click","ids":["skill.restore.cancel"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":606,"host":"ConfirmDialog","prop":"confirmAnalyticsId","event":"click","ids":["skill.promotion.create.confirm","skill.promotion.update.confirm"],"conditional":true},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":606,"host":"ConfirmDialog","prop":"cancelAnalyticsId","event":"click","ids":["skill.promotion.create.cancel","skill.promotion.update.cancel"],"conditional":true},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":622,"host":"ConfirmDialog","prop":"confirmAnalyticsId","event":"click","ids":["skill.archive.confirm"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":622,"host":"ConfirmDialog","prop":"cancelAnalyticsId","event":"click","ids":["skill.archive.cancel"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":635,"host":"ConfirmDialog","prop":"confirmAnalyticsId","event":"click","ids":["skill.unarchive.confirm"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":635,"host":"ConfirmDialog","prop":"cancelAnalyticsId","event":"click","ids":["skill.unarchive.cancel"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":648,"host":"ConfirmDialog","prop":"confirmAnalyticsId","event":"click","ids":["skill.review.withdraw.confirm"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":648,"host":"ConfirmDialog","prop":"cancelAnalyticsId","event":"click","ids":["skill.review.withdraw.cancel"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":395,"host":"SelectItem","prop":"data-analytics-id","event":"radix-item-intent","ids":["skill.owned.namespace"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":407,"host":"Button","prop":"data-analytics-id","event":"click","ids":["skill.owned.filter"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":425,"host":"Button","prop":"data-analytics-id","event":"click","ids":["skill.owned.clear"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":459,"host":"Card","prop":"data-analytics-id","event":"click","ids":["skill.owned.open"],"conditional":true},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":483,"host":"Button","prop":"data-analytics-id","event":"click","ids":["skill.update.open"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":509,"host":"DropdownMenuItem","prop":"data-analytics-id","event":"click","ids":["skill.update.open"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":513,"host":"DropdownMenuItem","prop":"data-analytics-id","event":"click","ids":["skill.related.open"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":516,"host":"DropdownMenuItem","prop":"data-analytics-id","event":"click","ids":["skill.visibility.open"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":520,"host":"DropdownMenuItem","prop":"data-analytics-id","event":"click","ids":["skill.review.withdraw"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":522,"host":"DropdownMenuItem","prop":"data-analytics-id","event":"click","ids":["skill.promotion.create.request","skill.promotion.update.request"],"conditional":true},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":525,"host":"DropdownMenuItem","prop":"data-analytics-id","event":"click","ids":["skill.unarchive.request"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":527,"host":"DropdownMenuItem","prop":"data-analytics-id","event":"click","ids":["skill.archive.request"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":530,"host":"DropdownMenuItem","prop":"data-analytics-id","event":"click","ids":["skill.restore.request"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":569,"host":"Pagination","prop":"analyticsId","event":"click","ids":["skill.owned.paginate"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":578,"host":"Button","prop":"data-analytics-id","event":"click","ids":["skill.owned.clear"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":600,"host":"ConfirmDialog","prop":"confirmAnalyticsId","event":"click","ids":["skill.restore.confirm"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":600,"host":"ConfirmDialog","prop":"cancelAnalyticsId","event":"click","ids":["skill.restore.cancel"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":613,"host":"ConfirmDialog","prop":"confirmAnalyticsId","event":"click","ids":["skill.promotion.create.confirm","skill.promotion.update.confirm"],"conditional":true},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":613,"host":"ConfirmDialog","prop":"cancelAnalyticsId","event":"click","ids":["skill.promotion.create.cancel","skill.promotion.update.cancel"],"conditional":true},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":629,"host":"ConfirmDialog","prop":"confirmAnalyticsId","event":"click","ids":["skill.archive.confirm"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":629,"host":"ConfirmDialog","prop":"cancelAnalyticsId","event":"click","ids":["skill.archive.cancel"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":642,"host":"ConfirmDialog","prop":"confirmAnalyticsId","event":"click","ids":["skill.unarchive.confirm"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":642,"host":"ConfirmDialog","prop":"cancelAnalyticsId","event":"click","ids":["skill.unarchive.cancel"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":655,"host":"ConfirmDialog","prop":"confirmAnalyticsId","event":"click","ids":["skill.review.withdraw.confirm"]},
|
||||
{"file":"web/src/pages/dashboard/my-skills.tsx","line":655,"host":"ConfirmDialog","prop":"cancelAnalyticsId","event":"click","ids":["skill.review.withdraw.cancel"]},
|
||||
{"file":"web/src/pages/dashboard/my-suites.tsx","line":55,"host":"Button","prop":"data-analytics-id","event":"click","ids":["suite.create.open"]},
|
||||
{"file":"web/src/pages/dashboard/my-suites.tsx","line":57,"host":"form","prop":"data-analytics-id","event":"submit","ids":["suite.owned.search"]},
|
||||
{"file":"web/src/pages/dashboard/my-suites.tsx","line":63,"host":"button","prop":"data-analytics-id","event":"click","ids":["suite.query.clear"]},
|
||||
|
|
@ -766,10 +766,10 @@
|
|||
{"file":"web/src/pages/dashboard/suite-management.tsx","line":210,"host":"Button","prop":"data-analytics-id","event":"click","ids":["suite.content.edit"]},
|
||||
{"file":"web/src/pages/dashboard/suite-management.tsx","line":236,"host":"Button","prop":"data-analytics-id","event":"click","ids":["suite.members.edit"]},
|
||||
{"file":"web/src/pages/dashboard/suite-management.tsx","line":333,"host":"Link","prop":"data-analytics-id","event":"click","ids":["suite.operation.open"]},
|
||||
{"file":"web/src/pages/dashboard.tsx","line":128,"host":"Link","prop":"data-analytics-id","event":"click","forward":"card.analyticsId"},
|
||||
{"file":"web/src/pages/dashboard.tsx","line":176,"host":"button","prop":"data-analytics-id","event":"click","ids":["navigation.dashboard.toggle"]},
|
||||
{"file":"web/src/pages/dashboard.tsx","line":194,"host":"Link","prop":"data-analytics-id","event":"click","forward":"item.analyticsId"},
|
||||
{"file":"web/src/pages/dashboard.tsx","line":245,"host":"Link","prop":"data-analytics-id","event":"click","forward":"item.analyticsId"},
|
||||
{"file":"web/src/pages/dashboard.tsx","line":107,"host":"Link","prop":"data-analytics-id","event":"click","forward":"card.analyticsId"},
|
||||
{"file":"web/src/pages/dashboard.tsx","line":153,"host":"button","prop":"data-analytics-id","event":"click","ids":["navigation.dashboard.toggle"]},
|
||||
{"file":"web/src/pages/dashboard.tsx","line":171,"host":"Link","prop":"data-analytics-id","event":"click","forward":"item.analyticsId"},
|
||||
{"file":"web/src/pages/dashboard.tsx","line":222,"host":"Link","prop":"data-analytics-id","event":"click","forward":"item.analyticsId"},
|
||||
{"file":"web/src/pages/device.tsx","line":106,"host":"form","prop":"data-analytics-id","event":"submit","ids":["auth.device.authorize"]},
|
||||
{"file":"web/src/pages/github-skills.tsx","line":33,"host":"Button","prop":"data-analytics-id","event":"click","ids":["skill.detail.open"]},
|
||||
{"file":"web/src/pages/github-skills.tsx","line":62,"host":"Button","prop":"data-analytics-id","event":"click","ids":["github.catalog.explore"]},
|
||||
|
|
|
|||
|
|
@ -35,6 +35,8 @@ At `/dashboard/skills`, submit a keyword and combine namespace and status capsul
|
|||
|
||||
Select a card title to open its actual detail page; returning preserves filters, page and scroll position. **Update** opens the existing update form. **More** exposes visibility, review withdrawal, promotion or archive actions according to the current permissions and state. Changing the frontend entry does not grant permissions; owners who have left a namespace remain read-only. When a published version and a pending change coexist, one main status describes the change while the effective version or visibility remains separately identified.
|
||||
|
||||
Cards show at most two lines of a long description; open details for the complete content. Promotion relationships, visibility requests, and read-only notices share a supplementary area, with longer notices allowed to wrap. Skills without an uploaded avatar use a stable soft color based on their coordinate: refreshing or changing a display name does not change it. Uploaded images remain preferred.
|
||||
|
||||
Version comparison opens in a centered dialog and only compares accessible published versions; at least two are required. Comparing and closing do not change publication state. Details show content and comparison shows differences; neither replaces installation and execution checks.
|
||||
|
||||
### Recovering from Submission Problems
|
||||
|
|
|
|||
|
|
@ -35,6 +35,8 @@ my-skill/
|
|||
|
||||
卡片标题进入技能详情;返回列表时保留筛选、页码和滚动位置。“更新”进入原更新表单;“更多”菜单按当前权限和状态提供可见范围、撤回审核、推广或归档等操作。不可用操作不能通过更换前端入口获得权限;退出命名空间的所有者继续只读。已发布版本和待审变更共存时,主状态说明本次变更,当前生效版本或范围仍独立标注。
|
||||
|
||||
长说明在卡片中最多显示两行,完整内容可在详情查看;推广关联、可见范围申请和只读提示集中展示,较长提示允许换行。未上传头像的技能按技能坐标显示稳定的柔和配色,刷新或更改展示名不换色;已上传图片继续优先显示。
|
||||
|
||||
版本比较在居中弹窗内进行,仅比较有权读取的已发布版本;不足两个版本时无法比较。比较与关闭弹窗不改变发布状态。详情入口用于查看内容,比较入口用于检查差异;两者都不能替代实际安装和调用验证。
|
||||
|
||||
### 发布失败与处理方法
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
## 范围与复用
|
||||
|
||||
以现有 `web/src/pages/dashboard/my-skills.tsx` 为入口,复用 TanStack Router、Query、现有 shadcn/Radix 组件、Lucide 图标和国际化资源。保留当前列表请求与分页,不增加逐卡片详情查询,不引入 UI 依赖。
|
||||
以现有 `web/src/pages/dashboard/my-skills.tsx` 和 `web/src/app/layout.tsx` 为入口,复用 TanStack Router、Query、现有 shadcn/Radix 组件、Lucide 图标和国际化资源。保留当前列表请求与分页,不增加逐卡片详情查询,不引入 UI 依赖。
|
||||
|
||||
筛选仍以 URL 的 `q`、`namespace`、`filter`、`page` 为事实源。结果数量仅取服务端 `total`;接口未提供完整状态统计,因此不显示各状态的伪数量。清除筛选统一清除四项条件并归零页码。
|
||||
|
||||
|
|
@ -39,16 +39,25 @@
|
|||
先完成一个可执行筛选切片并创建 Draft PR,再完成卡片和操作。实现收敛后执行一次集中独立 Review,随后冻结代码,形成需求追踪矩阵并运行源检查、精确提交镜像及浏览器关键业务路径。截图只证明视觉结果,不替代接口与最终状态断言。
|
||||
|
||||
|
||||
## 原型视觉基线与页面隔离
|
||||
## 原型视觉基线与管理页面范围
|
||||
|
||||
以已确认原生 Figma 稿及最终执行生成脚本为视觉来源,原生 Medium 字重采用500;本地 HTML 仅作辅助对照;旧覆盖前的30px操作高度不作为基线。所有尺寸仅作用于“我的技能”,不改变共享组件或其他管理页。
|
||||
以已确认原生 Figma 稿及最终执行生成脚本为视觉来源,原生 Medium 字重采用500;本地 HTML 仅作辅助对照;旧覆盖前的30px操作高度不作为基线。用户追加确认将外壳和页头推广至控制台与设置页,卡片紧凑字号仅作用于技能列表,不将11px元数据字号套用到表单正文。
|
||||
|
||||
| 区域 | 本页基线 | 生产适配 |
|
||||
| --- | --- | --- |
|
||||
| 标题/简介/元数据/状态 | 15px/24px、12px/20px、11px/18px、10px/20px | 桌面标题列390px、可收缩,后接状态;窄屏不固定列宽且最多两行,完整名称保留于title及详情 |
|
||||
| 状态与图标 | 状态圆角5px、内边距0/7px、低饱和底色;元数据图标13px | 小字前景色加深到4.5:1对比;真实头像或首字回退32px,不推断技能类别 |
|
||||
| 卡片与操作 | 圆角12px、卡片间距12px、桌面内边距18px/20px、移动14px | 更新72×32px、更多32×32px;768px以下更新进入菜单,保留相同权限及预填参数 |
|
||||
| 页面框架 | 仅/dashboard/skills使用1440px外壳,桌面右栏左右40px、顶部36px | 手机左右16px、顶部18px;仅本页侧栏添加桌面20px水平/32px顶部及移动16px内边距,公共导航及侧栏行为和其他管理页保持原样 |
|
||||
| 页面框架 | 控制台及设置页共享1440px外壳、224px侧栏,桌面右栏左右40px、顶部36px | 手机左右16px、顶部18px;侧栏桌面20px水平/32px顶部及移动16px内边距,设置表单限制阅读宽度,审核详情可使用1500px宽版 |
|
||||
| 筛选与菜单 | 筛选控件40px、胶囊32px;菜单宽208px、圆角10px、条目36px、图标16px;发布按钮36px | 不伪造状态计数;浅色与深色分别适配 |
|
||||
|
||||
原型中的彩色类别头像与固定技能数据是演示样例,不能据此生成生产语义;真实技能图片、生命周期、只读状态与审核目标仍来自现有接口。短可见性标签和图标旁保留完整范围说明,长坐标可截断但保留完整悬停文本及复制操作;768px以下隐藏下载统计,桌面仍展示。公共导航高度及内容继续遵循应用外壳,不宣称整个应用逐像素复刻原型。
|
||||
|
||||
## 统一外壳、说明高度与默认头像
|
||||
|
||||
- 控制台首页删除页内重复侧栏,由公共 Layout 与其他管理页统一组织。导航过滤仅保留现有角色与能力规则;外壳和侧栏的路径判断使用同一个已解析路由,防止加载期间组合新外壳与旧内容。
|
||||
- 发布页继续使用无侧栏流程,市场、搜索、登录及公共技能详情不进入本轮管理外壳。设置表单在右栏内左对齐并限制阅读宽度;较宽审核内容以明确模式保留,不为每个页面另写位置特例。
|
||||
- 常规管理与设置页面复用统一页头。命名空间和审核资源详情本轮继承共享外壳,保留资源型标题和专用内容结构,后续随详情页风格改造处理;不得通过管理外壳下的通用 `h1` 选择器污染 Markdown 正文或公共资源组件。
|
||||
- 技能列表常规说明位预留两行;当前页任一卡片有推广关系、申请范围或只读提示时,各卡片统一预留一行补充区。没有提示的整页不保留该空行;提示可换行,手机或风险说明、头像预览允许自然增高,不使用固定高度裁切内容。完整说明保留文本提示及真实详情入口。
|
||||
- 头像组件提供可选稳定种子,以命名空间与技能标识散列映射有限柔和配色,包含明暗主题;不调用渲染期随机数,不按展示名猜测行业或类别。列表显式传入种子,既有未选用此能力的调用保持原样。真实图片优先,失败时使用相同种子的首字回退。
|
||||
- 长短说明样例使用独立命名前缀,通过真实上传和现有审核流程建立;不得修改已有版本差异、状态和分页回归所依赖的数据。验收比较元数据对齐、提示集中、截断后的完整内容可达性,以及刷新或改名后的默认头像一致性。
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
# 优化“我的技能”管理页面
|
||||
# 统一控制台布局并优化“我的技能”管理页面
|
||||
|
||||
## Why
|
||||
|
||||
现有列表将更新、可见范围和治理动作平铺在卡片中,长名称、状态及元数据容易争抢空间。用户已确认独立卡片、统一筛选面板、权限感知的更多菜单,以及跳转详情、弹窗比较版本的交互方向。
|
||||
|
||||
单页原型落地后,用户进一步确认本轮统一控制台与设置页的共享外壳。控制台首页自行组织侧栏、子页使用不同宽度框架,造成跨页标题和侧栏位置不一致;列表还需覆盖长说明、状态补充提示及稳定多色的默认头像。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 统一搜索、命名空间、胶囊状态筛选与结果总数,保留 URL 筛选和分页。
|
||||
|
|
@ -11,7 +13,10 @@
|
|||
- 保留“更新”主要操作,将次要操作集中到更多菜单,遵守现有服务端权限和状态条件。
|
||||
- 进入真实技能详情后支持返回列表上下文;版本比较采用居中弹窗。
|
||||
- 补齐中英文本、键盘操作、窄屏及深色主题验证。
|
||||
- 将共享外壳用于控制台首页、带侧栏的管理子页和设置页,统一侧栏、内容起点、中性背景及页头;表单保留阅读宽度、审核详情保留宽版。
|
||||
- 说明区统一预留两行,当前页存在状态补充提示时统一预留补充区;保留完整的权限与审核信息,不强制裁切风险提示。
|
||||
- 默认技能头像按稳定标识选取柔和明暗配色,真实上传图片优先;以独立真实技能样例验证长短说明的显示。
|
||||
|
||||
## Impact
|
||||
|
||||
仅调整 Web 管理页面及其复用组件、测试和文档。沿用现有接口,不修改数据库、发布审核流程、权限判定和部署配置。PR #1043 的介绍字段缺陷单独处理;本变更不宣称解决该 PR 的业务问题。
|
||||
调整 Web 控制台与设置页的共享布局、技能卡片及其复用组件、测试和文档。沿用现有接口,不修改数据库、发布审核流程、权限判定和部署配置。市场、搜索、登录和公共技能详情不在本轮重设计范围;发布页保留无侧栏专用流程。PR #1043 的介绍字段缺陷单独处理;本变更不宣称解决该 PR 的业务问题。
|
||||
|
|
|
|||
|
|
@ -16,6 +16,11 @@
|
|||
- **THEN** 卡片用一个主状态说明待审变更,同时保留当前生效版本或范围信息
|
||||
- **AND** 不改变公众查看已发布版本的规则
|
||||
|
||||
#### Scenario: 长短说明与补充提示混合
|
||||
- **WHEN** 当前页同时包含短说明、两行以上说明,以及推广关联、申请范围或只读提示的技能
|
||||
- **THEN** 常规信息区预留两行说明,补充提示集中在同一区域,当前页需要时各卡片统一预留该区域
|
||||
- **AND** 完整说明仍可通过既有详情和完整文本提示查看,较长风险说明及待审头像允许自然展开,不裁掉关键内容
|
||||
|
||||
### Requirement: REQ-MYS-03 权限感知的操作菜单
|
||||
系统 SHALL 保留“更新”主要操作,并将可用的次要操作放入带图标、可访问名称及键盘操作的更多菜单。系统 SHALL 保持现有操作条件、互斥关系与确认步骤,不增加授权能力。
|
||||
|
||||
|
|
@ -53,7 +58,31 @@
|
|||
- **AND** 原行保留时恢复原入口;用户已主动聚焦其他控件时不抢夺焦点
|
||||
|
||||
|
||||
#### Scenario: 原型视觉基线与其他页面隔离
|
||||
#### Scenario: 原型视觉基线与页面职责
|
||||
- **WHEN** 用户打开“我的技能”列表
|
||||
- **THEN** 卡片、筛选、标签和图标按设计文档的本页量化视觉基线展示,窄屏保留菜单内更新操作
|
||||
- **AND** 仅该路由使用宽版内容框架,其他管理页及公共导航不受影响
|
||||
- **AND** 控制台与设置页使用统一管理外壳,公共页面及独立发布流程保留各自布局
|
||||
|
||||
### Requirement: REQ-MYS-07 稳定的默认头像配色
|
||||
系统 SHALL 支持按稳定技能标识选择有限的柔和明暗配色,不在渲染时随机换色,不根据技能名称推断类别。真实上传图片 SHALL 优先于默认头像。
|
||||
|
||||
#### Scenario: 刷新、改名与图片失败
|
||||
- **WHEN** 同一技能刷新列表、改变展示名,或头像图片加载失败
|
||||
- **THEN** 在稳定标识不变时默认头像保持相同配色,加载失败时回到该配色
|
||||
- **AND** 没有传入新配色选项的既有头像调用保持原有行为
|
||||
|
||||
### Requirement: REQ-DASH-01 共享管理外壳
|
||||
系统 SHALL 将控制台首页、带侧栏的管理子页及设置页交由同一外壳提供侧栏、内容起点、背景和页头层级;首页不得再嵌套一套侧栏。表单可限制阅读宽度,审核详情可使用声明的宽版模式。
|
||||
|
||||
#### Scenario: 跨页面切换
|
||||
- **WHEN** 用户在控制台首页、技能、套件、命名空间、收藏、订阅、Token、审核与设置页面间切换
|
||||
- **THEN** 同一视口下侧栏及右侧标题起点保持一致,不因空状态或页面切换重复加边距
|
||||
- **AND** 窄屏保留现有可操作的控制台导航,宽表格或长内容不能使整页横向溢出
|
||||
|
||||
### Requirement: REQ-DASH-02 导航权限与专用流程保持
|
||||
系统 SHALL 沿用现有角色、能力及导航入口规则;统一外壳不得扩大访问能力或改变数据请求、审核和发布行为。
|
||||
|
||||
#### Scenario: 不同身份与专用页面
|
||||
- **WHEN** 普通用户、只读所有者或平台管理身份进入管理页面,或跳转到发布、搜索、登录及公共详情
|
||||
- **THEN** 现有权限控制及导航能力保持,发布流程不重复显示侧栏,公共页面不套入管理外壳
|
||||
- **AND** 路由加载期间以一致的已解析路由选择外壳,避免旧内容与新外壳错误组合
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@
|
|||
- [x] 独立卡片、主要操作与更多菜单
|
||||
- [x] 详情返回上下文与居中版本比较
|
||||
- [x] 中英文、窄屏与键盘行为
|
||||
- [ ] 将共享外壳推广到控制台与设置页,统一首页侧栏与页头
|
||||
- [ ] 对齐长短说明及状态补充区,补稳定多色默认头像
|
||||
|
||||
## 3. 验证与交付
|
||||
- [x] 筛选首片针对性测试与静态检查
|
||||
|
|
@ -16,3 +18,4 @@
|
|||
- [ ] 原型视觉专项:图标、字号、标签、路由框架及明暗/移动端浏览器对照
|
||||
- [ ] 冻结代码后的需求场景矩阵、源检查与精确提交运行时验证
|
||||
- [ ] 保留人工预览入口并交付验证结果
|
||||
- [ ] 独立真实长说明样例及跨页布局、权限和响应式验收
|
||||
|
|
|
|||
71
web/src/app/layout-console.test.tsx
Normal file
71
web/src/app/layout-console.test.tsx
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
/** @vitest-environment jsdom */
|
||||
import type { ReactNode } from 'react'
|
||||
import { cleanup, render, screen, within } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
let pathname = '/dashboard'
|
||||
let resolvedPathname = '/dashboard'
|
||||
let roles: string[] = []
|
||||
let canChangePassword = false
|
||||
|
||||
vi.mock('@tanstack/react-router', () => ({
|
||||
Outlet: () => <div data-testid="outlet">page-content</div>,
|
||||
Link: ({ children, to, ...props }: { children: ReactNode; to: string }) => <a href={to} {...props}>{children}</a>,
|
||||
useRouterState: () => ({ pathname, resolvedPathname }),
|
||||
}))
|
||||
vi.mock('react-i18next', async () => ({
|
||||
...await vi.importActual<typeof import('react-i18next')>('react-i18next'),
|
||||
useTranslation: () => ({ t: (key: string) => key, i18n: { language: 'en', resolvedLanguage: 'en' } }),
|
||||
}))
|
||||
vi.mock('@/features/auth/use-auth', () => ({ useAuth: () => ({ user: { userId: 'test-reader', displayName: 'Reader', platformRoles: roles, canChangePassword }, isLoading: false }) }))
|
||||
vi.mock('@/shared/components/language-switcher', () => ({ LanguageSwitcher: () => null }))
|
||||
vi.mock('@/shared/components/theme-toggle', () => ({ ThemeToggle: () => null }))
|
||||
vi.mock('@/shared/components/user-menu', () => ({ UserMenu: () => null }))
|
||||
vi.mock('@/features/notification/notification-bell', () => ({ NotificationBell: () => null }))
|
||||
|
||||
import { Layout } from './layout'
|
||||
|
||||
beforeEach(() => { pathname = '/dashboard'; resolvedPathname = pathname; roles = []; canChangePassword = false })
|
||||
afterEach(cleanup)
|
||||
|
||||
describe('shared console layout composition', () => {
|
||||
it.each(['/dashboard', '/dashboard/skills', '/dashboard/tokens', '/settings/profile'])('renders exactly one sidebar beside the content on %s', (path) => {
|
||||
pathname = path
|
||||
resolvedPathname = path
|
||||
render(<Layout />)
|
||||
expect(screen.getAllByRole('complementary')).toHaveLength(1)
|
||||
expect(within(screen.getByRole('complementary')).queryByTestId('outlet')).toBeNull()
|
||||
expect(screen.getByRole('main').contains(screen.getByTestId('outlet'))).toBe(true)
|
||||
})
|
||||
|
||||
it.each(['/dashboard/publish', '/search', '/login', '/space/team/skill'])('does not add console navigation to %s', (path) => {
|
||||
pathname = path
|
||||
resolvedPathname = path
|
||||
render(<Layout />)
|
||||
expect(screen.queryByRole('complementary')).toBeNull()
|
||||
})
|
||||
|
||||
it('retains role and account-capability filtering in the centralized sidebar', () => {
|
||||
const view = render(<Layout />)
|
||||
const sidebar = screen.getByRole('complementary')
|
||||
expect(within(sidebar).queryByText('sidebar.governance')).toBeNull()
|
||||
expect(within(sidebar).queryByText('sidebar.githubSync')).toBeNull()
|
||||
expect(within(sidebar).queryByText('sidebar.security')).toBeNull()
|
||||
roles = ['SKILL_ADMIN']
|
||||
canChangePassword = true
|
||||
view.rerender(<Layout />)
|
||||
expect(within(sidebar).getByText('sidebar.governance')).toBeTruthy()
|
||||
expect(within(sidebar).queryByText('sidebar.githubSync')).toBeNull()
|
||||
expect(within(sidebar).getByText('sidebar.security')).toBeTruthy()
|
||||
roles = ['SUPER_ADMIN']
|
||||
view.rerender(<Layout />)
|
||||
expect(within(sidebar).getByText('sidebar.githubSync')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('keeps the previous console shell while navigation to standalone publish is pending', () => {
|
||||
pathname = '/dashboard/publish'
|
||||
resolvedPathname = '/dashboard'
|
||||
render(<Layout />)
|
||||
expect(screen.getAllByRole('complementary')).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
|
|
@ -1,14 +1,14 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
AUTH_MAIN_CLASS_NAME,
|
||||
CENTERED_DASHBOARD_CONTENT_CLASS_NAME,
|
||||
PUBLISH_CONTENT_CLASS_NAME,
|
||||
CENTERED_MAIN_CLASS_NAME,
|
||||
CENTERED_SEARCH_CONTENT_CLASS_NAME,
|
||||
DEFAULT_MAIN_CLASS_NAME,
|
||||
MY_SKILLS_MAIN_CLASS_NAME,
|
||||
MY_SKILLS_CONTENT_CLASS_NAME,
|
||||
MY_SKILLS_SIDEBAR_ROW_CLASS_NAME,
|
||||
MY_SKILLS_SIDEBAR_CONTENT_CLASS_NAME,
|
||||
CONSOLE_MAIN_CLASS_NAME,
|
||||
CONSOLE_CONTENT_CLASS_NAME,
|
||||
CONSOLE_SIDEBAR_ROW_CLASS_NAME,
|
||||
CONSOLE_SIDEBAR_CONTENT_CLASS_NAME,
|
||||
SPLIT_AUTH_MAIN_CLASS_NAME,
|
||||
getAppMainContentLayout,
|
||||
resolveAppMainContentPathname,
|
||||
|
|
@ -32,24 +32,35 @@ describe('getAppMainContentLayout', () => {
|
|||
expect(layout.contentClassName).toContain('max-w-[1200px]')
|
||||
})
|
||||
|
||||
it.each(['/dashboard/tokens', '/dashboard/stars', '/settings/profile', '/dashboard/skills/unrelated'])('keeps %s in the existing dashboard frame', (pathname) => {
|
||||
const layout = getAppMainContentLayout(pathname)
|
||||
|
||||
expect(layout).toEqual({
|
||||
mainClassName: CENTERED_MAIN_CLASS_NAME,
|
||||
contentClassName: CENTERED_DASHBOARD_CONTENT_CLASS_NAME,
|
||||
it.each(['/dashboard', '/dashboard/skills', '/dashboard/tokens', '/dashboard/stars', '/dashboard/namespaces', '/dashboard/suites', '/dashboard/reviews', '/dashboard/governance'])('shares the console frame and sidebar gutters on %s', (pathname) => {
|
||||
expect(getAppMainContentLayout(pathname)).toEqual({
|
||||
mainClassName: CONSOLE_MAIN_CLASS_NAME,
|
||||
contentClassName: CONSOLE_CONTENT_CLASS_NAME,
|
||||
sidebarRowClassName: CONSOLE_SIDEBAR_ROW_CLASS_NAME,
|
||||
sidebarContentClassName: CONSOLE_SIDEBAR_CONTENT_CLASS_NAME,
|
||||
})
|
||||
expect(layout.contentClassName).toContain('max-w-[1100px]')
|
||||
})
|
||||
|
||||
it('isolates the prototype-width frame and sidebar gutters to My Skills', () => {
|
||||
expect(getAppMainContentLayout('/dashboard/skills')).toEqual({
|
||||
mainClassName: MY_SKILLS_MAIN_CLASS_NAME,
|
||||
contentClassName: MY_SKILLS_CONTENT_CLASS_NAME,
|
||||
sidebarRowClassName: MY_SKILLS_SIDEBAR_ROW_CLASS_NAME,
|
||||
sidebarContentClassName: MY_SKILLS_SIDEBAR_CONTENT_CLASS_NAME,
|
||||
it.each(['/settings/profile', '/settings/security', '/settings/notifications', '/settings/accounts'])('keeps %s forms readable without shifting their content origin', (pathname) => {
|
||||
expect(getAppMainContentLayout(pathname)).toEqual({
|
||||
mainClassName: CONSOLE_MAIN_CLASS_NAME,
|
||||
contentClassName: CONSOLE_CONTENT_CLASS_NAME,
|
||||
sidebarRowClassName: CONSOLE_SIDEBAR_ROW_CLASS_NAME,
|
||||
sidebarContentClassName: CONSOLE_SIDEBAR_CONTENT_CLASS_NAME,
|
||||
pageClassName: 'w-full max-w-3xl',
|
||||
})
|
||||
expect(MY_SKILLS_CONTENT_CLASS_NAME).toContain('max-w-[1440px]')
|
||||
})
|
||||
|
||||
it('keeps publishing a standalone flow without console navigation', () => {
|
||||
expect(getAppMainContentLayout('/dashboard/publish')).toEqual({
|
||||
mainClassName: CENTERED_MAIN_CLASS_NAME,
|
||||
contentClassName: PUBLISH_CONTENT_CLASS_NAME,
|
||||
})
|
||||
})
|
||||
|
||||
it.each(['/dashboard/reviews/42', '/dashboard/namespaces/team/reviews/42'])('retains a wider console frame for %s', (pathname) => {
|
||||
expect(getAppMainContentLayout(pathname).contentClassName).toContain('max-w-[1500px]')
|
||||
expect(getAppMainContentLayout(pathname).sidebarRowClassName).toBe(CONSOLE_SIDEBAR_ROW_CLASS_NAME)
|
||||
})
|
||||
|
||||
it('uses the full-bleed split layout for login and registration', () => {
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@ export const AUTH_MAIN_CLASS_NAME = 'flex-1 relative z-10 px-4 pb-4 sm:px-6 md:p
|
|||
export const DEFAULT_MAIN_CLASS_NAME = 'flex-1 relative z-10 px-6 py-10 md:px-12'
|
||||
export const CENTERED_MAIN_CLASS_NAME = 'flex-1 relative z-10 px-4 py-8 sm:px-6 md:px-12 md:py-10'
|
||||
export const CENTERED_SEARCH_CONTENT_CLASS_NAME = 'mx-auto w-full max-w-[1200px]'
|
||||
export const CENTERED_DASHBOARD_CONTENT_CLASS_NAME = 'mx-auto min-h-[calc(100vh-11rem)] w-full max-w-[1100px]'
|
||||
export const MY_SKILLS_MAIN_CLASS_NAME = 'flex-1 relative z-10 bg-background'
|
||||
export const MY_SKILLS_CONTENT_CLASS_NAME = 'mx-auto min-h-[calc(100vh-11rem)] w-full max-w-[1440px]'
|
||||
export const MY_SKILLS_SIDEBAR_ROW_CLASS_NAME = 'flex flex-col lg:flex-row gap-0 [&>aside]:p-4 lg:[&>aside]:px-5 lg:[&>aside]:pt-8 lg:[&>aside]:pb-6 lg:[&>aside]:border-r lg:[&>aside]:border-border/40'
|
||||
export const MY_SKILLS_SIDEBAR_CONTENT_CLASS_NAME = 'flex-1 min-w-0 px-4 pb-7 pt-[18px] lg:px-10 lg:pt-9'
|
||||
export const PUBLISH_CONTENT_CLASS_NAME = 'mx-auto min-h-[calc(100vh-11rem)] w-full max-w-[1100px]'
|
||||
export const CONSOLE_MAIN_CLASS_NAME = 'flex-1 relative z-10 bg-background'
|
||||
export const CONSOLE_CONTENT_CLASS_NAME = 'mx-auto min-h-[calc(100vh-11rem)] w-full max-w-[1440px]'
|
||||
export const CONSOLE_SIDEBAR_ROW_CLASS_NAME = 'flex flex-col lg:flex-row gap-0 [&>aside]:p-4 lg:[&>aside]:px-5 lg:[&>aside]:pt-8 lg:[&>aside]:pb-6 lg:[&>aside]:border-r lg:[&>aside]:border-border/40'
|
||||
export const CONSOLE_SIDEBAR_CONTENT_CLASS_NAME = 'flex-1 min-w-0 px-4 pb-7 pt-[18px] lg:px-10 lg:pt-9'
|
||||
export const REVIEW_DETAIL_CONTENT_CLASS_NAME = 'mx-auto min-h-[calc(100vh-11rem)] w-full max-w-[1500px]'
|
||||
|
||||
export const DASHBOARD_PATH_PREFIXES = ['/dashboard', '/settings/'] as const
|
||||
|
|
@ -20,6 +20,7 @@ interface AppMainContentLayout {
|
|||
contentClassName: string
|
||||
sidebarRowClassName?: string
|
||||
sidebarContentClassName?: string
|
||||
pageClassName?: string
|
||||
}
|
||||
|
||||
export function resolveAppMainContentPathname(
|
||||
|
|
@ -58,26 +59,22 @@ export function getAppMainContentLayout(pathname: string): AppMainContentLayout
|
|||
}
|
||||
}
|
||||
|
||||
if (/^\/dashboard\/(?:reviews\/[^/]+|namespaces\/[^/]+\/reviews\/[^/]+)$/.test(pathname)) {
|
||||
// Publishing is a standalone flow and intentionally has no console sidebar.
|
||||
if (pathname === '/dashboard/publish') {
|
||||
return {
|
||||
mainClassName: CENTERED_MAIN_CLASS_NAME,
|
||||
contentClassName: REVIEW_DETAIL_CONTENT_CLASS_NAME,
|
||||
}
|
||||
}
|
||||
|
||||
if (pathname === '/dashboard/skills') {
|
||||
return {
|
||||
mainClassName: MY_SKILLS_MAIN_CLASS_NAME,
|
||||
contentClassName: MY_SKILLS_CONTENT_CLASS_NAME,
|
||||
sidebarRowClassName: MY_SKILLS_SIDEBAR_ROW_CLASS_NAME,
|
||||
sidebarContentClassName: MY_SKILLS_SIDEBAR_CONTENT_CLASS_NAME,
|
||||
contentClassName: PUBLISH_CONTENT_CLASS_NAME,
|
||||
}
|
||||
}
|
||||
|
||||
if (pathname === '/dashboard' || pathname.startsWith('/dashboard/') || pathname.startsWith('/settings/')) {
|
||||
const isReviewDetail = /^\/dashboard\/(?:reviews\/[^/]+|namespaces\/[^/]+\/reviews\/[^/]+)$/.test(pathname)
|
||||
return {
|
||||
mainClassName: CENTERED_MAIN_CLASS_NAME,
|
||||
contentClassName: CENTERED_DASHBOARD_CONTENT_CLASS_NAME,
|
||||
mainClassName: CONSOLE_MAIN_CLASS_NAME,
|
||||
contentClassName: isReviewDetail ? REVIEW_DETAIL_CONTENT_CLASS_NAME : CONSOLE_CONTENT_CLASS_NAME,
|
||||
sidebarRowClassName: CONSOLE_SIDEBAR_ROW_CLASS_NAME,
|
||||
sidebarContentClassName: CONSOLE_SIDEBAR_CONTENT_CLASS_NAME,
|
||||
...(pathname.startsWith('/settings/') ? { pageClassName: 'w-full max-w-3xl' } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -39,8 +39,7 @@ export function Layout() {
|
|||
const mainContentLayout = getAppMainContentLayout(contentLayoutPathname)
|
||||
const isAuthEntryRoute = AUTH_ENTRY_PATHS.includes(contentLayoutPathname as typeof AUTH_ENTRY_PATHS[number])
|
||||
const isSplitAuthRoute = SPLIT_AUTH_PATHS.includes(contentLayoutPathname as typeof SPLIT_AUTH_PATHS[number])
|
||||
const isDashboardSubRoute = pathname !== '/dashboard' && pathname.startsWith('/dashboard')
|
||||
const showSidebar = (isDashboardSubRoute && pathname !== '/dashboard/publish') || pathname.startsWith('/settings/')
|
||||
const showSidebar = mainContentLayout.sidebarRowClassName !== undefined
|
||||
const governanceVisible = canViewGovernanceCenter(user?.platformRoles)
|
||||
const filteredDashboardGroups = SIDEBAR_GROUPS
|
||||
.map((group) => ({
|
||||
|
|
@ -220,9 +219,9 @@ export function Layout() {
|
|||
<div className={mainContentLayout.contentClassName}>
|
||||
{showSidebar ? (
|
||||
<div className={mainContentLayout.sidebarRowClassName ?? "flex flex-col lg:flex-row gap-6"}>
|
||||
<DashboardSidebar groups={filteredDashboardGroups} user={user} t={t} pathname={pathname} />
|
||||
<DashboardSidebar groups={filteredDashboardGroups} user={user} t={t} pathname={contentLayoutPathname} />
|
||||
<div className={mainContentLayout.sidebarContentClassName ?? "flex-1 min-w-0"}>
|
||||
<Outlet />
|
||||
{mainContentLayout.pageClassName ? <div className={mainContentLayout.pageClassName}><Outlet /></div> : <Outlet />}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -19,4 +19,32 @@ describe('SkillAvatar', () => {
|
|||
fireEvent.error(image)
|
||||
expect(screen.getByText('W')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('keeps the same fallback palette when a skill is renamed or remounted', () => {
|
||||
const first = render(<SkillAvatar name="Weather" seed="team/weather" />)
|
||||
const palette = first.container.firstElementChild!.className
|
||||
first.rerender(<SkillAvatar name="Forecast" seed="team/weather" />)
|
||||
expect(first.container.firstElementChild!.className).toBe(palette)
|
||||
expect(screen.getByText('F')).toBeTruthy()
|
||||
first.unmount()
|
||||
const remount = render(<SkillAvatar name="Forecast" seed="team/weather" />)
|
||||
expect(remount.container.firstElementChild!.className).toBe(palette)
|
||||
})
|
||||
|
||||
it('uses varied stable palettes for distinct skill coordinates', () => {
|
||||
const view = render(<><SkillAvatar name="A" seed="team/a" /><SkillAvatar name="B" seed="team/b" /><SkillAvatar name="C" seed="team/c" /></>)
|
||||
expect(new Set(Array.from(view.container.children, child => child.className)).size).toBe(3)
|
||||
})
|
||||
|
||||
it('preserves approved images and returns to the same seeded palette after an image failure', () => {
|
||||
const view = render(<SkillAvatar name="Weather" seed="team/weather" />)
|
||||
const fallback = view.container.firstElementChild!.className
|
||||
view.rerender(<SkillAvatar name="Weather" seed="team/weather" url="/avatar" />)
|
||||
const image = view.container.querySelector('img')!
|
||||
expect(image.getAttribute('src')).toBe('/proxy/avatar')
|
||||
expect(view.container.firstElementChild!.className).not.toBe(fallback)
|
||||
fireEvent.error(image)
|
||||
expect(view.container.querySelector('img')).toBeNull()
|
||||
expect(view.container.firstElementChild!.className).toBe(fallback)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,11 +1,29 @@
|
|||
import { useState } from 'react'
|
||||
import { buildApiUrl } from '@/api/client'
|
||||
|
||||
export function SkillAvatar({ name, url, size = 'h-10 w-10' }: { name: string; url?: string | null; size?: string }) {
|
||||
const fallbackPalettes = [
|
||||
'bg-[#edf3ff] text-[#4d658d] dark:bg-[#202d45] dark:text-[#c0d0ed]',
|
||||
'bg-[#edf5ef] text-[#4c735b] dark:bg-[#22372c] dark:text-[#bad9c5]',
|
||||
'bg-[#f7f1e7] text-[#80643a] dark:bg-[#3b3020] dark:text-[#e5cfaa]',
|
||||
'bg-[#f2eef9] text-[#70578d] dark:bg-[#322840] dark:text-[#d3c1eb]',
|
||||
'bg-[#f9eeee] text-[#8b555b] dark:bg-[#40282d] dark:text-[#edc0c6]',
|
||||
'bg-[#eaf4f5] text-[#426e75] dark:bg-[#20363b] dark:text-[#b4d8de]',
|
||||
] as const
|
||||
|
||||
function getFallbackPalette(seed?: string) {
|
||||
if (!seed) return 'bg-primary/10 text-primary'
|
||||
let hash = 0
|
||||
for (let index = 0; index < seed.length; index += 1) {
|
||||
hash = (Math.imul(hash, 31) + seed.charCodeAt(index)) | 0
|
||||
}
|
||||
return fallbackPalettes[(hash >>> 0) % fallbackPalettes.length]
|
||||
}
|
||||
|
||||
export function SkillAvatar({ name, url, seed, size = 'h-10 w-10' }: { name: string; url?: string | null; seed?: string; size?: string }) {
|
||||
const [failedUrl, setFailedUrl] = useState<string | null>(null)
|
||||
const imageUrl = url && failedUrl !== url ? buildApiUrl(url) : null
|
||||
return (
|
||||
<span className={`inline-flex shrink-0 items-center justify-center overflow-hidden rounded-lg bg-primary/10 text-primary font-semibold ${size}`} aria-hidden="true">
|
||||
<span className={`inline-flex shrink-0 items-center justify-center overflow-hidden rounded-lg font-semibold ${imageUrl ? 'bg-primary/10 text-primary' : getFallbackPalette(seed)} ${size}`} aria-hidden="true">
|
||||
{imageUrl ? <img src={imageUrl} alt="" className="h-full w-full object-cover" onError={() => setFailedUrl(url ?? null)} /> : (Array.from(name.trim())[0] ?? '?').toLocaleUpperCase()}
|
||||
</span>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -62,12 +62,13 @@ describe('DashboardPage', () => {
|
|||
expect(typeof DashboardPage).toBe('function')
|
||||
})
|
||||
|
||||
it('renders the dashboard sidebar and overview cards', () => {
|
||||
it('renders only overview content so Layout supplies the single sidebar', () => {
|
||||
const html = renderToStaticMarkup(<DashboardPage />)
|
||||
|
||||
expect(html).toContain('sidebar.skillsAndData')
|
||||
expect(html).not.toContain('<aside')
|
||||
expect(html).not.toContain('sidebar.skillsAndData')
|
||||
expect(html).toContain('overview.mySkills')
|
||||
expect(html).toContain('sidebar.mySuites')
|
||||
expect(html).not.toContain('sidebar.mySuites')
|
||||
expect(html).toContain('overview.mySuites')
|
||||
expect(html).not.toContain('overview.publish')
|
||||
})
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import { Link } from '@tanstack/react-router'
|
|||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useAuth } from '@/features/auth/use-auth'
|
||||
import { canViewGovernanceCenter } from '@/shared/lib/governance-access'
|
||||
import { APP_SHELL_PAGE_CLASS_NAME } from '@/app/page-shell-style'
|
||||
import { DashboardPageHeader } from '@/shared/components/dashboard-page-header'
|
||||
import {
|
||||
|
|
@ -93,62 +92,40 @@ const OVERVIEW_CARDS = [
|
|||
] as const
|
||||
|
||||
/**
|
||||
* Dashboard home page with sidebar + overview cards.
|
||||
* Dashboard overview content; the shared Layout owns its sidebar.
|
||||
*/
|
||||
export function DashboardPage() {
|
||||
const { t } = useTranslation()
|
||||
const { user } = useAuth()
|
||||
const governanceVisible = canViewGovernanceCenter(user?.platformRoles)
|
||||
|
||||
const filteredGroups = SIDEBAR_GROUPS
|
||||
.map((group) => ({
|
||||
...group,
|
||||
items: group.items.filter((item) => (
|
||||
(!item.admin || governanceVisible)
|
||||
&& (!item.superAdmin || user?.platformRoles?.includes('SUPER_ADMIN'))
|
||||
&& (!item.passwordCapability || user?.canChangePassword === true)
|
||||
)),
|
||||
}))
|
||||
.filter((group) => group.items.length > 0)
|
||||
|
||||
return (
|
||||
<div className={APP_SHELL_PAGE_CLASS_NAME}>
|
||||
<DashboardPageHeader title={t('dashboard.title')} subtitle={t('dashboard.subtitle')} />
|
||||
{/* Two-column layout */}
|
||||
<div className="flex flex-col lg:flex-row gap-6">
|
||||
{/* Left sidebar */}
|
||||
<DashboardSidebar groups={filteredGroups} user={user} t={t} pathname="/dashboard" />
|
||||
|
||||
{/* Right content - overview cards */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
{OVERVIEW_CARDS.filter((card) => card.key !== 'security' || user?.canChangePassword === true).map((card) => {
|
||||
const Icon = card.icon
|
||||
return (
|
||||
<Link data-analytics-id={card.analyticsId}
|
||||
key={card.key}
|
||||
to={card.to}
|
||||
className="group flex items-start gap-4 rounded-xl border border-border/60 p-5 transition-all duration-150 hover:bg-accent hover:border-border hover:shadow-sm"
|
||||
>
|
||||
<div className="flex-shrink-0 w-10 h-10 rounded-lg flex items-center justify-center bg-secondary">
|
||||
<Icon className="w-5 h-5" style={{ color: 'hsl(var(--foreground))' }} />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-semibold" style={{ color: 'hsl(var(--foreground))' }}>
|
||||
{t(card.label)}
|
||||
</span>
|
||||
<ChevronRight className="w-3.5 h-3.5 opacity-0 -translate-x-1 transition-all duration-150 group-hover:opacity-100 group-hover:translate-x-0" style={{ color: 'hsl(var(--muted-foreground))' }} />
|
||||
</div>
|
||||
<p className="mt-1 text-xs leading-relaxed" style={{ color: 'hsl(var(--text-secondary))' }}>
|
||||
{t(card.desc)}
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
{OVERVIEW_CARDS.filter((card) => card.key !== 'security' || user?.canChangePassword === true).map((card) => {
|
||||
const Icon = card.icon
|
||||
return (
|
||||
<Link data-analytics-id={card.analyticsId}
|
||||
key={card.key}
|
||||
to={card.to}
|
||||
className="group flex items-start gap-4 rounded-xl border border-border/60 p-5 transition-all duration-150 hover:bg-accent hover:border-border hover:shadow-sm"
|
||||
>
|
||||
<div className="flex-shrink-0 w-10 h-10 rounded-lg flex items-center justify-center bg-secondary">
|
||||
<Icon className="w-5 h-5" style={{ color: 'hsl(var(--foreground))' }} />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-semibold" style={{ color: 'hsl(var(--foreground))' }}>
|
||||
{t(card.label)}
|
||||
</span>
|
||||
<ChevronRight className="w-3.5 h-3.5 opacity-0 -translate-x-1 transition-all duration-150 group-hover:opacity-100 group-hover:translate-x-0" style={{ color: 'hsl(var(--muted-foreground))' }} />
|
||||
</div>
|
||||
<p className="mt-1 text-xs leading-relaxed" style={{ color: 'hsl(var(--text-secondary))' }}>
|
||||
{t(card.desc)}
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -15,11 +15,11 @@ let showEmpty = false
|
|||
let superAdmin = false
|
||||
let search: { filter?: string; q?: string; namespace?: string; page?: number } = {}
|
||||
|
||||
function makeSkill() {
|
||||
function makeSkill(managementState: { ownerReadOnly: boolean; promotionRole?: string; requestedVisibility?: string } = { ownerReadOnly: false }) {
|
||||
return {
|
||||
id: 1, namespace: 'team', slug: 'demo', displayName: 'Demo', summary: 'Summary',
|
||||
status: 'ACTIVE', visibility: 'PRIVATE', downloadCount: 3, canSubmitPromotion: false,
|
||||
managementState: { ownerReadOnly: false },
|
||||
managementState,
|
||||
publishedVersion: { id: 11, version: '1.1.0', status: 'PUBLISHED' },
|
||||
}
|
||||
}
|
||||
|
|
@ -86,6 +86,28 @@ afterEach(() => { cleanup(); vi.restoreAllMocks(); vi.unstubAllGlobals() })
|
|||
afterAll(() => vi.unstubAllEnvs())
|
||||
|
||||
describe('My Skills interactions with real menu and dialog', () => {
|
||||
it('preserves long descriptions in the card and full-text title without slicing the source text', () => {
|
||||
const summary = '用于汇总跨服务任务的输入、执行条件和人工复核结果。'.repeat(20) + 'UnbrokenIdentifier'.repeat(15)
|
||||
skill.summary = summary
|
||||
render(<MySkillsPage />)
|
||||
const description = screen.getByText(summary)
|
||||
expect(description.getAttribute('title')).toBe(summary)
|
||||
expect(screen.getByRole('link', { name: 'Demo' })).toBeTruthy()
|
||||
})
|
||||
|
||||
it('keeps ownership, promotion and requested visibility explanations in one supplemental group', () => {
|
||||
skill = makeSkill({ ownerReadOnly: true, promotionRole: 'GLOBAL_COPY', requestedVisibility: 'PUBLIC' })
|
||||
render(<MySkillsPage />)
|
||||
const readonly = screen.getByText('mySkills.ownerReadOnly')
|
||||
const promotion = screen.getByText('mySkills.promotionRoles.GLOBAL_COPY')
|
||||
const visibility = screen.getByText('skillVisibility.pendingList')
|
||||
expect(readonly.parentElement).toBe(promotion.parentElement)
|
||||
expect(readonly.parentElement).toBe(visibility.parentElement)
|
||||
expect(readonly.getAttribute('role')).toBe('status')
|
||||
expect(visibility.getAttribute('role')).toBe('status')
|
||||
expect(screen.queryByRole('button', { name: 'mySkills.update' })).toBeNull()
|
||||
})
|
||||
|
||||
it('loads comparison on demand and returns focus after Escape without navigating', async () => {
|
||||
render(<MySkillsPage />)
|
||||
expect(useVersions).not.toHaveBeenCalled()
|
||||
|
|
|
|||
|
|
@ -74,9 +74,19 @@
|
|||
.page .description {
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
min-height: 40px;
|
||||
color: #666670;
|
||||
}
|
||||
|
||||
.page .supplementalArea { display: flex; flex-direction: column; gap: 8px; }
|
||||
.page .supplementalArea:empty { display: none; }
|
||||
.page .supplementalNotes { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 16px; }
|
||||
.page .supplementalNotes .note { margin: 0; min-width: 0; overflow-wrap: anywhere; }
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.page .supplementalArea.reserveSupplemental { display: flex; min-height: 20px; }
|
||||
}
|
||||
|
||||
.page .metadata {
|
||||
gap: 8px 17px;
|
||||
font-size: 11px;
|
||||
|
|
|
|||
|
|
@ -104,6 +104,8 @@ export function MySkillsPage() {
|
|||
const { data: namespaceOptions } = useMyNamespaces()
|
||||
|
||||
const skills = skillPage?.items ?? []
|
||||
const hasSupplementalRows = skills.some(skill => skill.managementState?.ownerReadOnly
|
||||
|| skill.managementState?.promotionRole || skill.managementState?.requestedVisibility)
|
||||
const totalPages = skillPage ? Math.max(Math.ceil(skillPage.total / skillPage.size), 1) : 1
|
||||
const availableFilters = getMySkillFilters(hasRole('SUPER_ADMIN'))
|
||||
const canRestoreHidden = filter === 'HIDDEN' && hasRole('SUPER_ADMIN')
|
||||
|
|
@ -458,7 +460,7 @@ export function MySkillsPage() {
|
|||
className={`${styles.skillCard} relative border-border/70 hover:shadow-none ${hidden ? '' : 'cursor-pointer hover:border-border'}`}
|
||||
onClick={hidden ? undefined : openDetail}>
|
||||
<div className="flex items-start gap-3">
|
||||
<SkillAvatar name={displayName} url={skill.avatarUrl} size="h-8 w-8 text-xs" />
|
||||
<SkillAvatar name={displayName} url={skill.avatarUrl} seed={`${skill.namespace}/${skill.slug}`} size="h-8 w-8 text-xs" />
|
||||
<div className="min-w-0 flex-1 space-y-2">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className={`${styles.titleGroup} flex min-w-0 flex-1 items-center gap-3`}>
|
||||
|
|
@ -532,7 +534,8 @@ export function MySkillsPage() {
|
|||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
{summary && <p className={`${styles.description} line-clamp-2 break-words text-muted-foreground [overflow-wrap:anywhere]`}>{summary}</p>}
|
||||
<p title={summary || undefined} aria-hidden={!summary || undefined}
|
||||
className={`${styles.description} line-clamp-2 break-words text-muted-foreground [overflow-wrap:anywhere]`}>{summary}</p>
|
||||
<div className={`${styles.metadata} flex flex-wrap items-center text-muted-foreground`}>
|
||||
<span className={`${styles.metadataItem} ${styles.coordinate}`} title={coordinate}><Package aria-hidden="true" strokeWidth={1.65} className="h-[13px] w-[13px] shrink-0" /><span className="truncate">{coordinate}</span></span>
|
||||
<span className={styles.metadataItem} title={t('skillVisibility.current', { visibility: t(`skillVisibility.${skill.visibility ?? 'PUBLIC'}`) })}>
|
||||
|
|
@ -545,12 +548,16 @@ export function MySkillsPage() {
|
|||
: headlineVersion ? <span className="font-mono">v{headlineVersion.version}</span> : null}
|
||||
<span className={`${styles.metadataItem} ${styles.downloads}`} title={t('mySkills.downloadCount')}><Download aria-hidden="true" strokeWidth={1.65} className="h-[13px] w-[13px] shrink-0" />{formatCompactCount(skill.downloadCount)}</span>
|
||||
</div>
|
||||
{ownerReadOnly && <p role="status" className={`${styles.note} text-muted-foreground`}>{t('mySkills.ownerReadOnly')}</p>}
|
||||
{skill.managementState?.promotionRole && <p className={`${styles.note} text-muted-foreground`}>{t(`mySkills.promotionRoles.${skill.managementState.promotionRole}`)}</p>}
|
||||
{skill.status === 'ACTIVE' && !hidden && <SkillAvatarManager namespace={skill.namespace} slug={skill.slug} previewOnly />}
|
||||
{skill.managementState?.requestedVisibility && <p role="status" className={`${styles.note} text-muted-foreground`}>
|
||||
{t('skillVisibility.pendingList', { visibility: t(`skillVisibility.${skill.managementState.requestedVisibility}`) })}
|
||||
</p>}
|
||||
<div className={`${styles.supplementalArea} ${hasSupplementalRows ? styles.reserveSupplemental : ''}`}>
|
||||
{(ownerReadOnly || skill.managementState?.promotionRole || skill.managementState?.requestedVisibility) && <div className={styles.supplementalNotes}>
|
||||
{ownerReadOnly && <p role="status" className={`${styles.note} text-muted-foreground`}>{t('mySkills.ownerReadOnly')}</p>}
|
||||
{skill.managementState?.promotionRole && <p className={`${styles.note} text-muted-foreground`}>{t(`mySkills.promotionRoles.${skill.managementState.promotionRole}`)}</p>}
|
||||
{skill.managementState?.requestedVisibility && <p role="status" className={`${styles.note} text-muted-foreground`}>
|
||||
{t('skillVisibility.pendingList', { visibility: t(`skillVisibility.${skill.managementState.requestedVisibility}`) })}
|
||||
</p>}
|
||||
</div>}
|
||||
{skill.status === 'ACTIVE' && !hidden && <SkillAvatarManager namespace={skill.namespace} slug={skill.slug} previewOnly />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { useState } from 'react'
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import { useCancelAccountMerge, useConfirmAccountMerge, useInitiateAccountMerge, useInspectAccountMerge, useVerifyAccountMerge } from '@/features/auth/use-account-merge'
|
||||
import { truncateErrorMessage } from '@/shared/lib/error-display'
|
||||
import { DashboardPageHeader } from '@/shared/components/dashboard-page-header'
|
||||
import { Button } from '@/shared/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/shared/ui/card'
|
||||
import { Input } from '@/shared/ui/input'
|
||||
|
|
@ -106,7 +107,8 @@ export function AccountSettingsPage() {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-3xl space-y-6">
|
||||
<div className="space-y-6">
|
||||
<DashboardPageHeader title={t('sidebar.accounts')} />
|
||||
<Card className="glass-strong">
|
||||
<CardHeader>
|
||||
<CardTitle>{t('accounts.initiateTitle')}</CardTitle>
|
||||
|
|
|
|||
|
|
@ -13,10 +13,10 @@ interface DashboardPageHeaderProps {
|
|||
export function DashboardPageHeader({ title, subtitle, actions }: DashboardPageHeaderProps) {
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold tracking-tight" style={{ color: 'hsl(var(--foreground))' }}>{title}</h1>
|
||||
{subtitle ? <p className="mt-1 text-sm" style={{ color: 'hsl(var(--text-secondary))' }}>{subtitle}</p> : null}
|
||||
<div className="flex flex-wrap items-start justify-between gap-4">
|
||||
<div className="min-w-0">
|
||||
<h1 className="break-words text-[27px] font-medium leading-9 tracking-[-0.8px]" style={{ color: 'hsl(var(--foreground))' }}>{title}</h1>
|
||||
{subtitle ? <p className="mt-1 text-[13px] leading-5" style={{ color: 'hsl(var(--text-secondary))' }}>{subtitle}</p> : null}
|
||||
</div>
|
||||
{actions}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue