Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ $project.set({
isDeleted: false,
userId: "userId",
domain: "new-2x9tcd",
tags: [],

marketplaceApprovalStatus: "UNLISTED",

Expand Down
2 changes: 2 additions & 0 deletions apps/builder/app/dashboard/dashboard.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ const projects = [
previewImageAssetId: "",
latestBuildVirtual: null,
marketplaceApprovalStatus: "UNLISTED" as const,
tags: [],
} as DashboardProject,
];

Expand All @@ -58,6 +59,7 @@ const data = {
userPlanFeatures,
publisherHost: "https://wstd.work",
projects,
tags: [],
};

export const Welcome: StoryFn<typeof Dashboard> = () => {
Expand Down
2 changes: 2 additions & 0 deletions apps/builder/app/dashboard/dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ export const Dashboard = () => {
projectToClone,
projects,
templates,
tags,
} = data;
const hasProjects = projects.length > 0;
const view = getView(location.pathname, hasProjects);
Expand Down Expand Up @@ -235,6 +236,7 @@ export const Dashboard = () => {
projects={projects}
hasProPlan={userPlanFeatures.hasProPlan}
publisherHost={publisherHost}
tags={tags}
/>
)}
{view === "templates" && <Templates projects={templates} />}
Expand Down
180 changes: 169 additions & 11 deletions apps/builder/app/dashboard/projects/project-card.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useEffect, useState } from "react";
import { useRevalidator } from "react-router-dom";
import { useEffect, useId, useState } from "react";
import {
DropdownMenu,
DropdownMenuTrigger,
Expand All @@ -14,10 +15,23 @@ import {
rawTheme,
Link,
Box,
Dialog,
DialogContent,
DialogTitle,
Button,
DialogActions,
DialogClose,
Checkbox,
CheckboxAndLabel,
Label,
InputField,
DialogTitleActions,
Grid,
} from "@webstudio-is/design-system";
import { InfoCircleIcon, EllipsesIcon } from "@webstudio-is/icons";
import { InfoCircleIcon, EllipsesIcon, PlusIcon } from "@webstudio-is/icons";
import type { DashboardProject } from "@webstudio-is/dashboard";
import { builderUrl } from "~/shared/router-utils";
import { nativeClient } from "~/shared/trpc/trpc-client";
import {
RenameProjectDialog,
DeleteProjectDialog,
Expand All @@ -31,6 +45,128 @@ import {
import { Spinner } from "../shared/spinner";
import { Card, CardContent, CardFooter } from "../shared/card";

const TagsDialogContent = ({
projectId,
availableTags,
projectTags,
onOpenChange,
}: {
projectId: string;
availableTags: string[];
projectTags: string[];
onOpenChange: (isOpen: boolean) => void;
}) => {
const revalidator = useRevalidator();
const tagId = useId();
const [tags, setTags] = useState(availableTags);
return (
<form
onSubmit={async (event) => {
event.preventDefault();
const formData = new FormData(event.currentTarget);
const newTags = formData
.getAll("tag")
.map((item) => String(item).trim())
.filter((item) => item);
await nativeClient.project.updateTags.mutate({
projectId,
tags: newTags,
});
revalidator.revalidate();
onOpenChange(false);
}}
>
<DialogTitle
suffix={
<DialogTitleActions>
<Button
type="button"
aria-label="Add tag"
prefix={<PlusIcon />}
color="ghost"
onClick={(event) => {
event.preventDefault();
setTags((tags) => {
let newTag = "New tag";
let number = 1;
while (tags.includes(newTag)) {
number += 1;
newTag = `New tag ${number}`;
}
return [...tags, newTag];
});
}}
/>
<DialogClose />
</DialogTitleActions>
}
>
Project tags
</DialogTitle>
<Grid gap={1} css={{ padding: theme.panel.padding }}>
{tags.map((tag, index) =>
availableTags.includes(tag) ? (
<CheckboxAndLabel key={tag}>
<Checkbox
id={tagId + index}
name="tag"
value={tag}
defaultChecked={projectTags.includes(tag)}
/>
<Label htmlFor={tagId + index}>{tag}</Label>
</CheckboxAndLabel>
) : (
<InputField
key={tag}
name="tag"
autoFocus
placeholder="New tag"
defaultValue={tag}
autoComplete="off"
/>
)
)}
{tags.length === 0 && <Text align="center">No tags found</Text>}
</Grid>
<DialogActions>
<Button type="submit">Update</Button>
<DialogClose>
<Button color="ghost" type="button">
Cancel
</Button>
</DialogClose>
</DialogActions>
</form>
);
};

const TagsDialog = ({
projectId,
availableTags,
projectTags,
isOpen,
onOpenChange,
}: {
projectId: string;
availableTags: string[];
projectTags: string[];
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
}) => {
return (
<Dialog open={isOpen} onOpenChange={onOpenChange}>
<DialogContent aria-describedby={undefined}>
<TagsDialogContent
projectId={projectId}
availableTags={availableTags}
projectTags={projectTags}
onOpenChange={onOpenChange}
/>
</DialogContent>
</Dialog>
);
};

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

const PublishedLink = ({
Expand Down Expand Up @@ -64,12 +200,14 @@ const Menu = ({
onRename,
onDuplicate,
onShare,
onUpdateTags,
}: {
tabIndex: number;
onDelete: () => void;
onRename: () => void;
onDuplicate: () => void;
onShare: () => void;
onUpdateTags: () => void;
}) => {
const [isOpen, setIsOpen] = useState(false);
return (
Expand All @@ -87,6 +225,7 @@ const Menu = ({
<DropdownMenuItem onSelect={onDuplicate}>Duplicate</DropdownMenuItem>
<DropdownMenuItem onSelect={onRename}>Rename</DropdownMenuItem>
<DropdownMenuItem onSelect={onShare}>Share</DropdownMenuItem>
<DropdownMenuItem onSelect={onUpdateTags}>Tags</DropdownMenuItem>
<DropdownMenuItem onSelect={onDelete}>Delete</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
Expand All @@ -105,6 +244,7 @@ type ProjectCardProps = {
project: DashboardProject;
hasProPlan: boolean;
publisherHost: string;
tags: string[];
};

export const ProjectCard = ({
Expand All @@ -116,6 +256,7 @@ export const ProjectCard = ({
createdAt,
latestBuildVirtual,
previewImageAsset,
tags,
},
hasProPlan,
publisherHost,
Expand All @@ -124,6 +265,7 @@ export const ProjectCard = ({
const [isRenameDialogOpen, setIsRenameDialogOpen] = useState(false);
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
const [isShareDialogOpen, setIsShareDialogOpen] = useState(false);
const [isTagsDialogOpen, setIsTagsDialogOpen] = useState(false);
const [isHidden, setIsHidden] = useState(false);
const handleCloneProject = useCloneProject(id);
const [isTransitioning, setIsTransitioning] = useState(false);
Expand Down Expand Up @@ -180,6 +322,20 @@ export const ProjectCard = ({
<ThumbnailLinkWithAbbr title={title} to={linkPath} />
)}
{isTransitioning && <Spinner delay={0} />}
<Flex
wrap="wrap"
gap={1}
css={{
position: "absolute",
inset: 0,
padding: theme.panel.padding,
alignContent: "start",
}}
>
{tags?.map((tag) => (
<div key={tag}>{tag}</div>
))}
</Flex>
</CardContent>
<CardFooter>
<Flex direction="column" justify="around" grow>
Expand Down Expand Up @@ -225,16 +381,11 @@ export const ProjectCard = ({
</Flex>
<Menu
tabIndex={-1}
onDelete={() => {
setIsDeleteDialogOpen(true);
}}
onRename={() => {
setIsRenameDialogOpen(true);
}}
onShare={() => {
setIsShareDialogOpen(true);
}}
onDelete={() => setIsDeleteDialogOpen(true)}
onRename={() => setIsRenameDialogOpen(true)}
onShare={() => setIsShareDialogOpen(true)}
onDuplicate={handleCloneProject}
onUpdateTags={() => setIsTagsDialogOpen(true)}
/>
</CardFooter>
<RenameProjectDialog
Expand All @@ -256,6 +407,13 @@ export const ProjectCard = ({
projectId={id}
hasProPlan={hasProPlan}
/>
<TagsDialog
projectId={id}
availableTags={props.tags}
projectTags={tags ?? []}
isOpen={isTagsDialogOpen}
onOpenChange={setIsTagsDialogOpen}
/>
</Card>
);
};
Loading
Loading