mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
fix(web): show published design systems as "Published" on project cards (#2849)
A published design-system project still showed its last generation run status on its card. When that run had failed, the published system read as "Failed" on the home Recent projects strip and in the Projects grid. The system is published and live, so showing a stale run failure there is wrong. Cards now read "Published" when the backing design system's status is published, keyed off the design system summary instead of the project run status. Every other project keeps its run status. A shared isPublishedDesignSystemProject helper lets the home strip and the Designs grid apply one rule. The label uses a new designs.status.published key in the locale files, with a green style that matches the existing succeeded color.
This commit is contained in:
parent
af997b7cf5
commit
085963a1e2
27 changed files with 130 additions and 13 deletions
|
|
@ -19,6 +19,7 @@ import type {
|
|||
SkillSummary,
|
||||
} from "../types";
|
||||
import { Icon } from "./Icon";
|
||||
import { isDesignSystemProject, isPublishedDesignSystemProject } from "./design-system-project";
|
||||
import { LiveArtifactBadges } from "./LiveArtifactBadges";
|
||||
import { Toast } from "./Toast";
|
||||
|
||||
|
|
@ -633,6 +634,7 @@ export function DesignsTab({
|
|||
const cover = projectCover(p, coverByProject[p.id] ?? null);
|
||||
const isSelected = selected.has(p.id);
|
||||
const designSystemProject = isDesignSystemProject(p);
|
||||
const publishedDesignSystem = isPublishedDesignSystemProject(p, designSystems);
|
||||
return (
|
||||
<div
|
||||
key={p.id}
|
||||
|
|
@ -798,9 +800,9 @@ export function DesignsTab({
|
|||
{skill ? ` · ${skill}` : ""}
|
||||
{" · "}
|
||||
<span
|
||||
className={`design-card-status design-card-status-${status}`}
|
||||
className={`design-card-status design-card-status-${publishedDesignSystem ? "published" : status}`}
|
||||
>
|
||||
{statusLabel(status, t)}
|
||||
{publishedDesignSystem ? t("designs.status.published") : statusLabel(status, t)}
|
||||
</span>
|
||||
</span>
|
||||
{sub === "recent" || sub === "yours" ? (
|
||||
|
|
@ -1044,9 +1046,6 @@ function isOrbitProject(project: Project): boolean {
|
|||
return metadata?.kind === 'orbit';
|
||||
}
|
||||
|
||||
function isDesignSystemProject(project: Project): boolean {
|
||||
return project.metadata?.importedFrom === "design-system";
|
||||
}
|
||||
|
||||
function projectCover(
|
||||
project: Project,
|
||||
|
|
|
|||
|
|
@ -1326,6 +1326,7 @@ export function HomeView({
|
|||
|
||||
<RecentProjectsStrip
|
||||
projects={projects}
|
||||
designSystems={designSystems}
|
||||
{...(projectsLoading !== undefined ? { loading: projectsLoading } : {})}
|
||||
onOpen={(id) => {
|
||||
// P0 ui_click area=recent_projects element=project_card — emit
|
||||
|
|
|
|||
|
|
@ -10,12 +10,16 @@ import type { CSSProperties } from 'react';
|
|||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useT } from '../i18n';
|
||||
import { fetchProjectFiles, projectFileUrl } from '../providers/registry';
|
||||
import type { Project, ProjectDisplayStatus, ProjectFile } from '../types';
|
||||
import type { DesignSystemSummary, Project, ProjectDisplayStatus, ProjectFile } from '../types';
|
||||
import { Icon } from './Icon';
|
||||
import { STATUS_LABEL_KEYS } from './DesignsTab';
|
||||
import { isDesignSystemProject, isPublishedDesignSystemProject } from './design-system-project';
|
||||
|
||||
interface Props {
|
||||
projects: Project[];
|
||||
/** Used only to show a "Published" status for design-system projects whose
|
||||
* backing system is published (independent of the project's run status). */
|
||||
designSystems?: DesignSystemSummary[];
|
||||
/** Retained for call-site compatibility; the strip skips rendering
|
||||
* while the list is loading so we never need a loading state. */
|
||||
loading?: boolean;
|
||||
|
|
@ -24,6 +28,8 @@ interface Props {
|
|||
limit?: number;
|
||||
}
|
||||
|
||||
const EMPTY_DESIGN_SYSTEMS: DesignSystemSummary[] = [];
|
||||
|
||||
const DECK_PREVIEW_WIDTH = 1280;
|
||||
const DECK_PREVIEW_HEIGHT = 720;
|
||||
const deckCoverCache = new Map<string, string>();
|
||||
|
|
@ -31,6 +37,7 @@ const deckCoverInflight = new Map<string, Promise<string>>();
|
|||
|
||||
export function RecentProjectsStrip({
|
||||
projects,
|
||||
designSystems = EMPTY_DESIGN_SYSTEMS,
|
||||
onOpen,
|
||||
onViewAll,
|
||||
limit = 6,
|
||||
|
|
@ -142,8 +149,10 @@ export function RecentProjectsStrip({
|
|||
const cover = projectCover(project, coverByProject[project.id] ?? null);
|
||||
const designSystemProject = isDesignSystemProject(project);
|
||||
const status: ProjectDisplayStatus = project.status?.value ?? 'not_started';
|
||||
const publishedDesignSystem = isPublishedDesignSystemProject(project, designSystems);
|
||||
const isActive =
|
||||
status === 'running' || status === 'queued' || status === 'awaiting_input';
|
||||
!publishedDesignSystem &&
|
||||
(status === 'running' || status === 'queued' || status === 'awaiting_input');
|
||||
return (
|
||||
<button
|
||||
key={project.id}
|
||||
|
|
@ -194,12 +203,12 @@ export function RecentProjectsStrip({
|
|||
<div className="recent-projects__card-name">{project.name}</div>
|
||||
<div className="recent-projects__card-time">
|
||||
<span
|
||||
className={`recent-projects__card-status recent-projects__card-status-${status}`}
|
||||
className={`recent-projects__card-status recent-projects__card-status-${publishedDesignSystem ? 'published' : status}`}
|
||||
>
|
||||
{isActive ? (
|
||||
<span className="recent-projects__card-status-dot" aria-hidden />
|
||||
) : null}
|
||||
{statusLabel(status, t)}
|
||||
{publishedDesignSystem ? t('designs.status.published') : statusLabel(status, t)}
|
||||
</span>
|
||||
<span className="recent-projects__card-sep" aria-hidden>·</span>
|
||||
{relativeTime(project.updatedAt, t)}
|
||||
|
|
@ -465,10 +474,6 @@ function ProjectTag({ category }: { category: ProjectCategory }) {
|
|||
return <span className={`design-card-tag tag-${category}`}>{label}</span>;
|
||||
}
|
||||
|
||||
function isDesignSystemProject(project: Project): boolean {
|
||||
return project.metadata?.importedFrom === 'design-system';
|
||||
}
|
||||
|
||||
function DesignSystemProjectTag() {
|
||||
return <span className="design-card-tag tag-design-system">Design System</span>;
|
||||
}
|
||||
|
|
|
|||
22
apps/web/src/components/design-system-project.ts
Normal file
22
apps/web/src/components/design-system-project.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import type { DesignSystemSummary, Project } from '../types';
|
||||
|
||||
/** A project imported from / backing a design system. */
|
||||
export function isDesignSystemProject(project: Project): boolean {
|
||||
return project.metadata?.importedFrom === 'design-system';
|
||||
}
|
||||
|
||||
/**
|
||||
* True when a project is a design system whose backing system is published.
|
||||
* The publish state lives on the DesignSystemSummary (keyed by designSystemId),
|
||||
* not on the project's run status, so a published system whose last generation
|
||||
* run failed should still read as published in project cards.
|
||||
*/
|
||||
export function isPublishedDesignSystemProject(
|
||||
project: Project,
|
||||
designSystems: readonly DesignSystemSummary[],
|
||||
): boolean {
|
||||
if (!isDesignSystemProject(project) || !project.designSystemId) return false;
|
||||
return designSystems.some(
|
||||
(system) => system.id === project.designSystemId && system.status === 'published',
|
||||
);
|
||||
}
|
||||
|
|
@ -596,6 +596,7 @@ export const ar: Dict = {
|
|||
'designs.status.running': 'جاري التشغيل',
|
||||
'designs.status.awaitingInput': 'يحتاج مدخلات',
|
||||
'designs.status.succeeded': 'اكتمل',
|
||||
'designs.status.published': 'تم النشر',
|
||||
'designs.status.failed': 'فشل',
|
||||
'designs.status.canceled': 'ملغى',
|
||||
'designs.viewToggleAria': 'وضع العرض',
|
||||
|
|
|
|||
|
|
@ -484,6 +484,7 @@ export const de: Dict = {
|
|||
'designs.status.running': 'Läuft',
|
||||
'designs.status.awaitingInput': 'Eingabe nötig',
|
||||
'designs.status.succeeded': 'Abgeschlossen',
|
||||
'designs.status.published': 'Veröffentlicht',
|
||||
'designs.status.failed': 'Fehlgeschlagen',
|
||||
'designs.status.canceled': 'Abgebrochen',
|
||||
'designs.viewToggleAria': 'Ansichtsmodus',
|
||||
|
|
|
|||
|
|
@ -1180,6 +1180,7 @@ export const en: Dict = {
|
|||
'designs.status.running': 'Running',
|
||||
'designs.status.awaitingInput': 'Needs input',
|
||||
'designs.status.succeeded': 'Completed',
|
||||
'designs.status.published': 'Published',
|
||||
'designs.status.failed': 'Failed',
|
||||
'designs.status.canceled': 'Canceled',
|
||||
'designs.viewToggleAria': 'View mode',
|
||||
|
|
|
|||
|
|
@ -485,6 +485,7 @@ export const esES: Dict = {
|
|||
'designs.status.running': 'En ejecución',
|
||||
'designs.status.awaitingInput': 'Necesita respuesta',
|
||||
'designs.status.succeeded': 'Completado',
|
||||
'designs.status.published': 'Publicado',
|
||||
'designs.status.failed': 'Fallido',
|
||||
'designs.status.canceled': 'Cancelado',
|
||||
'designs.viewToggleAria': 'Modo de vista',
|
||||
|
|
|
|||
|
|
@ -618,6 +618,7 @@ export const fa: Dict = {
|
|||
'designs.status.running': 'در حال اجرا',
|
||||
'designs.status.awaitingInput': 'نیازمند ورودی',
|
||||
'designs.status.succeeded': 'تکمیل شد',
|
||||
'designs.status.published': 'منتشر شد',
|
||||
'designs.status.failed': 'ناموفق',
|
||||
'designs.status.canceled': 'لغو شد',
|
||||
'designs.viewToggleAria': 'حالت نمایش',
|
||||
|
|
|
|||
|
|
@ -612,6 +612,7 @@ export const fr: Dict = {
|
|||
'designs.status.running': 'En cours',
|
||||
'designs.status.awaitingInput': 'Entrée requise',
|
||||
'designs.status.succeeded': 'Terminé',
|
||||
'designs.status.published': 'Publié',
|
||||
'designs.status.failed': 'Échoué',
|
||||
'designs.status.canceled': 'Annulé',
|
||||
'designs.viewToggleAria': 'Mode d\'affichage',
|
||||
|
|
|
|||
|
|
@ -596,6 +596,7 @@ export const hu: Dict = {
|
|||
'designs.status.running': 'Fut',
|
||||
'designs.status.awaitingInput': 'Bevitelre vár',
|
||||
'designs.status.succeeded': 'Befejezve',
|
||||
'designs.status.published': 'Közzétéve',
|
||||
'designs.status.failed': 'Sikertelen',
|
||||
'designs.status.canceled': 'Megszakítva',
|
||||
'designs.viewToggleAria': 'Nézet módja',
|
||||
|
|
|
|||
|
|
@ -709,6 +709,7 @@ export const id: Dict = {
|
|||
'designs.status.running': 'Berjalan',
|
||||
'designs.status.awaitingInput': 'Menunggu input',
|
||||
'designs.status.succeeded': 'Selesai',
|
||||
'designs.status.published': 'Diterbitkan',
|
||||
'designs.status.failed': 'Gagal',
|
||||
'designs.status.canceled': 'Dibatalkan',
|
||||
'designs.viewToggleAria': 'Mode tampilan',
|
||||
|
|
|
|||
|
|
@ -564,6 +564,7 @@ export const it: Dict = {
|
|||
'designs.status.running': 'In corso',
|
||||
'designs.status.awaitingInput': 'Input richiesto',
|
||||
'designs.status.succeeded': 'Completato',
|
||||
'designs.status.published': 'Pubblicato',
|
||||
'designs.status.failed': 'Fallito',
|
||||
'designs.status.canceled': 'Annullato',
|
||||
'designs.viewToggleAria': 'Modalità di visualizzazione',
|
||||
|
|
|
|||
|
|
@ -483,6 +483,7 @@ export const ja: Dict = {
|
|||
'designs.status.running': '実行中',
|
||||
'designs.status.awaitingInput': '入力待ち',
|
||||
'designs.status.succeeded': '完了',
|
||||
'designs.status.published': '公開済み',
|
||||
'designs.status.failed': '失敗',
|
||||
'designs.status.canceled': 'キャンセル済み',
|
||||
'designs.viewToggleAria': '表示モード',
|
||||
|
|
|
|||
|
|
@ -596,6 +596,7 @@ export const ko: Dict = {
|
|||
'designs.status.running': '실행 중',
|
||||
'designs.status.awaitingInput': '입력 대기 중',
|
||||
'designs.status.succeeded': '완료됨',
|
||||
'designs.status.published': '게시됨',
|
||||
'designs.status.failed': '실패',
|
||||
'designs.status.canceled': '취소됨',
|
||||
'designs.viewToggleAria': '보기 모드',
|
||||
|
|
|
|||
|
|
@ -596,6 +596,7 @@ export const pl: Dict = {
|
|||
'designs.status.running': 'Uruchomiony',
|
||||
'designs.status.awaitingInput': 'Wymaga danych',
|
||||
'designs.status.succeeded': 'Zakończono',
|
||||
'designs.status.published': 'Opublikowano',
|
||||
'designs.status.failed': 'Błąd',
|
||||
'designs.status.canceled': 'Anulowano',
|
||||
'designs.viewToggleAria': 'Tryb widoku',
|
||||
|
|
|
|||
|
|
@ -617,6 +617,7 @@ export const ptBR: Dict = {
|
|||
'designs.status.running': 'Em execução',
|
||||
'designs.status.awaitingInput': 'Aguardando resposta',
|
||||
'designs.status.succeeded': 'Concluído',
|
||||
'designs.status.published': 'Publicado',
|
||||
'designs.status.failed': 'Falhou',
|
||||
'designs.status.canceled': 'Cancelado',
|
||||
'designs.viewToggleAria': 'Modo de visualização',
|
||||
|
|
|
|||
|
|
@ -617,6 +617,7 @@ export const ru: Dict = {
|
|||
'designs.status.running': 'Выполняется',
|
||||
'designs.status.awaitingInput': 'Нужен ввод',
|
||||
'designs.status.succeeded': 'Завершено',
|
||||
'designs.status.published': 'Опубликовано',
|
||||
'designs.status.failed': 'Ошибка',
|
||||
'designs.status.canceled': 'Отменено',
|
||||
'designs.viewToggleAria': 'Режим просмотра',
|
||||
|
|
|
|||
|
|
@ -572,6 +572,7 @@ export const th: Dict = {
|
|||
'designs.status.running': 'กำลังทำงาน',
|
||||
'designs.status.awaitingInput': 'ต้องการข้อมูลเพิ่ม',
|
||||
'designs.status.succeeded': 'สำเร็จ',
|
||||
'designs.status.published': 'เผยแพร่แล้ว',
|
||||
'designs.status.failed': 'ล้มเหลว',
|
||||
'designs.status.canceled': 'ยกเลิกแล้ว',
|
||||
'designs.viewToggleAria': 'โหมดมุมมอง',
|
||||
|
|
|
|||
|
|
@ -585,6 +585,7 @@ export const tr: Dict = {
|
|||
'designs.status.running': 'Çalışıyor',
|
||||
'designs.status.awaitingInput': 'Girdi gerekli',
|
||||
'designs.status.succeeded': 'Tamamlandı',
|
||||
'designs.status.published': 'Yayımlandı',
|
||||
'designs.status.failed': 'Başarısız',
|
||||
'designs.status.canceled': 'İptal edildi',
|
||||
'designs.viewToggleAria': 'Görüntüleme modu',
|
||||
|
|
|
|||
|
|
@ -609,6 +609,7 @@ export const uk: Dict = {
|
|||
'designs.status.running': 'Виконується',
|
||||
'designs.status.awaitingInput': 'Чекає підтвердження',
|
||||
'designs.status.succeeded': 'Завершено',
|
||||
'designs.status.published': 'Опубліковано',
|
||||
'designs.status.failed': 'Помилка',
|
||||
'designs.status.canceled': 'Скасовано',
|
||||
'designs.viewToggleAria': 'Режим перегляду',
|
||||
|
|
|
|||
|
|
@ -1172,6 +1172,7 @@ export const zhCN: Dict = {
|
|||
'designs.status.running': '运行中',
|
||||
'designs.status.awaitingInput': '等待回复',
|
||||
'designs.status.succeeded': '已完成',
|
||||
'designs.status.published': '已发布',
|
||||
'designs.status.failed': '失败',
|
||||
'designs.status.canceled': '已取消',
|
||||
'designs.viewToggleAria': '视图模式',
|
||||
|
|
|
|||
|
|
@ -788,6 +788,7 @@ export const zhTW: Dict = {
|
|||
'designs.status.running': '執行中',
|
||||
'designs.status.awaitingInput': '等待回覆',
|
||||
'designs.status.succeeded': '已完成',
|
||||
'designs.status.published': '已發佈',
|
||||
'designs.status.failed': '失敗',
|
||||
'designs.status.canceled': '已取消',
|
||||
'designs.viewToggleAria': '檢視模式',
|
||||
|
|
|
|||
|
|
@ -1486,6 +1486,7 @@ export interface Dict {
|
|||
'designs.status.succeeded': string;
|
||||
'designs.status.failed': string;
|
||||
'designs.status.canceled': string;
|
||||
'designs.status.published': string;
|
||||
'designs.viewToggleAria': string;
|
||||
'designs.viewGrid': string;
|
||||
'designs.viewKanban': string;
|
||||
|
|
|
|||
|
|
@ -9115,6 +9115,9 @@ button.connector-action.is-loading {
|
|||
.design-card-status-failed {
|
||||
color: var(--red);
|
||||
}
|
||||
.design-card-status-published {
|
||||
color: var(--green);
|
||||
}
|
||||
.design-card-close {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
|
|
|
|||
|
|
@ -199,6 +199,9 @@
|
|||
.recent-projects__card-status-failed {
|
||||
color: var(--red);
|
||||
}
|
||||
.recent-projects__card-status-published {
|
||||
color: var(--green);
|
||||
}
|
||||
.recent-projects__card-status-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
|
|
|
|||
64
apps/web/tests/components/design-system-project.test.ts
Normal file
64
apps/web/tests/components/design-system-project.test.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
import type { DesignSystemSummary, Project } from '../../src/types';
|
||||
|
||||
import {
|
||||
isDesignSystemProject,
|
||||
isPublishedDesignSystemProject,
|
||||
} from '../../src/components/design-system-project';
|
||||
|
||||
function project(overrides: Partial<Project> = {}): Project {
|
||||
return {
|
||||
id: 'p1',
|
||||
name: 'StackMe',
|
||||
updatedAt: 1,
|
||||
designSystemId: null,
|
||||
...overrides,
|
||||
} as Project;
|
||||
}
|
||||
|
||||
function system(overrides: Partial<DesignSystemSummary> = {}): DesignSystemSummary {
|
||||
return {
|
||||
id: 'user:stackme',
|
||||
title: 'StackMe',
|
||||
category: 'Custom',
|
||||
summary: '',
|
||||
swatches: [],
|
||||
surface: 'web',
|
||||
source: 'user',
|
||||
status: 'draft',
|
||||
isEditable: true,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('isDesignSystemProject', () => {
|
||||
it('matches projects imported from a design system', () => {
|
||||
expect(isDesignSystemProject(project({ metadata: { kind: 'other', importedFrom: 'design-system' } }))).toBe(true);
|
||||
expect(isDesignSystemProject(project({ metadata: { kind: 'prototype' } }))).toBe(false);
|
||||
expect(isDesignSystemProject(project())).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isPublishedDesignSystemProject', () => {
|
||||
const dsProject = project({
|
||||
metadata: { kind: 'other', importedFrom: 'design-system' },
|
||||
designSystemId: 'user:stackme',
|
||||
});
|
||||
|
||||
it('is true when the backing design system is published, even if the run failed', () => {
|
||||
expect(isPublishedDesignSystemProject(dsProject, [system({ status: 'published' })])).toBe(true);
|
||||
});
|
||||
|
||||
it('is false while the design system is still a draft', () => {
|
||||
expect(isPublishedDesignSystemProject(dsProject, [system({ status: 'draft' })])).toBe(false);
|
||||
});
|
||||
|
||||
it('is false for non-design-system projects and when the system is missing', () => {
|
||||
expect(
|
||||
isPublishedDesignSystemProject(project({ metadata: { kind: 'prototype' } }), [
|
||||
system({ status: 'published' }),
|
||||
]),
|
||||
).toBe(false);
|
||||
expect(isPublishedDesignSystemProject(dsProject, [])).toBe(false);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue