Skip to content

Commit 801181c

Browse files
TrySoundkof
andauthored
feat: add project tags in dashboard (#5399)
Ref #860 https://github.com/user-attachments/assets/a85b525a-d43b-4361-aa5f-f2158b3c0211 --------- Co-authored-by: Oleg Isonen <[email protected]>
1 parent c384707 commit 801181c

File tree

62 files changed

+1276
-80
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

62 files changed

+1276
-80
lines changed

apps/builder/app/builder/features/pages/page-settings.stories.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ $project.set({
6363
isDeleted: false,
6464
userId: "userId",
6565
domain: "new-2x9tcd",
66+
tags: [],
6667

6768
marketplaceApprovalStatus: "UNLISTED",
6869

apps/builder/app/canvas/features/webstudio-component/webstudio-component.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ const InvalidCollectionDataStub = forwardRef<
214214
<a
215215
style={{ color: "inherit" }}
216216
target="_blank"
217-
href="https://docs.webstudio.is/university/core-components/collection.md#whats-an-array"
217+
href="https://docs.webstudio.is/university/core-components/collection#whats-an-array"
218218
// avoid preventing click by events interceptor
219219
onClickCapture={(event) => event.stopPropagation()}
220220
>

apps/builder/app/dashboard/dashboard.stories.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const user = {
1818
username: "Taylor",
1919
teamId: null,
2020
provider: "github",
21+
projectsTags: [],
2122
};
2223

2324
const createRouter = (element: JSX.Element, path: string, current?: string) =>
@@ -49,6 +50,7 @@ const projects = [
4950
previewImageAssetId: "",
5051
latestBuildVirtual: null,
5152
marketplaceApprovalStatus: "UNLISTED" as const,
53+
tags: [],
5254
} as DashboardProject,
5355
];
5456

apps/builder/app/dashboard/dashboard.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,7 @@ export const Dashboard = () => {
256256
projects={projects}
257257
hasProPlan={userPlanFeatures.hasProPlan}
258258
publisherHost={publisherHost}
259+
projectsTags={user.projectsTags}
259260
/>
260261
)}
261262
{view === "templates" && <Templates projects={templates} />}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export const colors = Array.from({ length: 50 }, (_, i) => {
2+
const l = 55 + (i % 3) * 3; // Reduced variation in lightness (55-61%) to lower contrast
3+
const c = 0.14 + (i % 2) * 0.02; // Reduced variation in chroma (0.14-0.16) for balance
4+
const h = (i * 137.5) % 360; // Golden angle for pleasing hue distribution
5+
return `oklch(${l}% ${c.toFixed(2)} ${h.toFixed(1)})`;
6+
});

apps/builder/app/dashboard/projects/project-card.tsx

Lines changed: 56 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ import {
3030
} from "../shared/thumbnail";
3131
import { Spinner } from "../shared/spinner";
3232
import { Card, CardContent, CardFooter } from "../shared/card";
33+
import type { User } from "~/shared/db/user.server";
34+
import { TagsDialog } from "./tags";
3335

3436
const infoIconStyle = css({ flexShrink: 0 });
3537

@@ -64,12 +66,14 @@ const Menu = ({
6466
onRename,
6567
onDuplicate,
6668
onShare,
69+
onUpdateTags,
6770
}: {
6871
tabIndex: number;
6972
onDelete: () => void;
7073
onRename: () => void;
7174
onDuplicate: () => void;
7275
onShare: () => void;
76+
onUpdateTags: () => void;
7377
}) => {
7478
const [isOpen, setIsOpen] = useState(false);
7579
return (
@@ -87,6 +91,7 @@ const Menu = ({
8791
<DropdownMenuItem onSelect={onDuplicate}>Duplicate</DropdownMenuItem>
8892
<DropdownMenuItem onSelect={onRename}>Rename</DropdownMenuItem>
8993
<DropdownMenuItem onSelect={onShare}>Share</DropdownMenuItem>
94+
<DropdownMenuItem onSelect={onUpdateTags}>Tags</DropdownMenuItem>
9095
<DropdownMenuItem onSelect={onDelete}>Delete</DropdownMenuItem>
9196
</DropdownMenuContent>
9297
</DropdownMenu>
@@ -105,6 +110,7 @@ type ProjectCardProps = {
105110
project: DashboardProject;
106111
hasProPlan: boolean;
107112
publisherHost: string;
113+
projectsTags: User["projectsTags"];
108114
};
109115

110116
export const ProjectCard = ({
@@ -116,18 +122,30 @@ export const ProjectCard = ({
116122
createdAt,
117123
latestBuildVirtual,
118124
previewImageAsset,
125+
tags,
119126
},
120127
hasProPlan,
121128
publisherHost,
129+
projectsTags,
122130
...props
123131
}: ProjectCardProps) => {
124132
const [isRenameDialogOpen, setIsRenameDialogOpen] = useState(false);
125133
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
126134
const [isShareDialogOpen, setIsShareDialogOpen] = useState(false);
135+
const [isTagsDialogOpen, setIsTagsDialogOpen] = useState(false);
127136
const [isHidden, setIsHidden] = useState(false);
128137
const handleCloneProject = useCloneProject(id);
129138
const [isTransitioning, setIsTransitioning] = useState(false);
130139

140+
// Makes sure there are no project tags that reference deleted User tags.
141+
// We are not deleting project tag from project.tags when deleting User tags.
142+
const projectTagsIds = (tags || [])
143+
.map((tagId) => {
144+
const tag = projectsTags.find((tag) => tag.id === tagId);
145+
return tag ? tag.id : undefined;
146+
})
147+
.filter(Boolean) as string[];
148+
131149
useEffect(() => {
132150
const linkPath = builderUrl({ origin: window.origin, projectId: id });
133151

@@ -173,7 +191,33 @@ export const ProjectCard = ({
173191
opacity: 0,
174192
}}
175193
/>
176-
194+
<Flex
195+
wrap="wrap"
196+
gap={1}
197+
css={{
198+
position: "absolute",
199+
padding: theme.panel.padding,
200+
bottom: 0,
201+
zIndex: 1,
202+
}}
203+
>
204+
{projectsTags.map((tag) => {
205+
const isApplied = projectTagsIds.includes(tag.id);
206+
if (isApplied) {
207+
return (
208+
<Text
209+
color="contrast"
210+
key={tag.id}
211+
css={{
212+
background: "oklch(0 0 0 / 0.3)",
213+
borderRadius: theme.borderRadius[3],
214+
paddingInline: theme.spacing[3],
215+
}}
216+
>{`#${tag.label}`}</Text>
217+
);
218+
}
219+
})}
220+
</Flex>
177221
{previewImageAsset ? (
178222
<ThumbnailLinkWithImage to={linkPath} name={previewImageAsset.name} />
179223
) : (
@@ -225,16 +269,11 @@ export const ProjectCard = ({
225269
</Flex>
226270
<Menu
227271
tabIndex={-1}
228-
onDelete={() => {
229-
setIsDeleteDialogOpen(true);
230-
}}
231-
onRename={() => {
232-
setIsRenameDialogOpen(true);
233-
}}
234-
onShare={() => {
235-
setIsShareDialogOpen(true);
236-
}}
272+
onDelete={() => setIsDeleteDialogOpen(true)}
273+
onRename={() => setIsRenameDialogOpen(true)}
274+
onShare={() => setIsShareDialogOpen(true)}
237275
onDuplicate={handleCloneProject}
276+
onUpdateTags={() => setIsTagsDialogOpen(true)}
238277
/>
239278
</CardFooter>
240279
<RenameProjectDialog
@@ -256,6 +295,13 @@ export const ProjectCard = ({
256295
projectId={id}
257296
hasProPlan={hasProPlan}
258297
/>
298+
<TagsDialog
299+
projectId={id}
300+
projectsTags={projectsTags}
301+
projectTagsIds={projectTagsIds}
302+
isOpen={isTagsDialogOpen}
303+
onOpenChange={setIsTagsDialogOpen}
304+
/>
259305
</Card>
260306
);
261307
};

apps/builder/app/dashboard/projects/projects.tsx

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
Box,
23
Flex,
34
Grid,
45
List,
@@ -11,11 +12,16 @@ import type { DashboardProject } from "@webstudio-is/dashboard";
1112
import { ProjectCard } from "./project-card";
1213
import { CreateProject } from "./project-dialogs";
1314
import { Header, Main } from "../shared/layout";
15+
import { useSearchParams } from "react-router-dom";
16+
import { setIsSubsetOf } from "~/shared/shim";
17+
import type { User } from "~/shared/db/user.server";
18+
import { Tag } from "./tags";
1419

1520
export const ProjectsGrid = ({
1621
projects,
1722
hasProPlan,
1823
publisherHost,
24+
projectsTags,
1925
}: ProjectsProps) => {
2026
return (
2127
<List asChild>
@@ -33,6 +39,7 @@ export const ProjectsGrid = ({
3339
project={project}
3440
hasProPlan={hasProPlan}
3541
publisherHost={publisherHost}
42+
projectsTags={projectsTags}
3643
/>
3744
</ListItem>
3845
);
@@ -46,9 +53,19 @@ type ProjectsProps = {
4653
projects: Array<DashboardProject>;
4754
hasProPlan: boolean;
4855
publisherHost: string;
56+
projectsTags: User["projectsTags"];
4957
};
5058

5159
export const Projects = (props: ProjectsProps) => {
60+
const [searchParams] = useSearchParams();
61+
const selectedTags = searchParams.getAll("tag");
62+
let projects = props.projects;
63+
if (selectedTags.length > 0) {
64+
projects = projects.filter((project) =>
65+
setIsSubsetOf(new Set(selectedTags), new Set(project.tags))
66+
);
67+
}
68+
5269
return (
5370
<Main>
5471
<Header variant="main">
@@ -60,12 +77,43 @@ export const Projects = (props: ProjectsProps) => {
6077
</Flex>
6178
</Header>
6279
<Flex
63-
direction="column"
64-
gap="3"
65-
css={{ paddingInline: theme.spacing[13] }}
80+
gap="2"
81+
wrap="wrap"
82+
css={{
83+
display: "none",
84+
flexShrink: 0,
85+
paddingInline: theme.spacing[13],
86+
paddingBlockStart: theme.spacing[2],
87+
paddingBlockEnd: theme.spacing[10],
88+
"&:has(*:first-child)": {
89+
display: "flex",
90+
},
91+
}}
6692
>
67-
<ProjectsGrid {...props} />
93+
{props.projectsTags.map((tag, index) => {
94+
return (
95+
<Tag
96+
tag={tag}
97+
key={tag.id}
98+
index={index}
99+
state={selectedTags.includes(tag.id) ? "pressed" : "auto"}
100+
>
101+
{tag.label}
102+
</Tag>
103+
);
104+
})}
68105
</Flex>
106+
<Box css={{ paddingInline: theme.spacing[13] }}>
107+
{projects.length === 0 && (
108+
<Text
109+
variant="brandRegular"
110+
css={{ padding: theme.spacing[13], textAlign: "center" }}
111+
>
112+
No projects found
113+
</Text>
114+
)}
115+
<ProjectsGrid {...props} projects={projects} />
116+
</Box>
69117
</Main>
70118
);
71119
};

0 commit comments

Comments
 (0)