Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
affd17d
feat: add project tags for organizing services
cucumber-sp Feb 13, 2026
1cb1b50
fix: remove tag badges next to filter button to save space
cucumber-sp Feb 13, 2026
e049352
fix: correct tag badge sizing in filter dropdown
cucumber-sp Feb 13, 2026
1da9ef8
refactor: extract TagBadge into shared component
cucumber-sp Feb 13, 2026
2b4604d
fix: simplify tag filter button label
cucumber-sp Feb 13, 2026
0df6cc5
fix: clear stale tag filter when tags are deleted
cucumber-sp Feb 13, 2026
e4d9fd3
chore: Format and lint codebase with format-and-lint:fix
imran-vz Feb 16, 2026
1f3936f
Adjust version text size and layout in collapsed sidebar
imran-vz Feb 16, 2026
ebbbd39
feat(ui): add Vercel-style breadcrumb navigation with project/service
imran-vz Feb 16, 2026
938b0b4
chore: Reorder and clean up imports, update openapi schema, and improve
imran-vz Feb 16, 2026
355d469
chore: resolved greptile review comments
imran-vz Feb 16, 2026
b1b1dbc
Merge branch 'canary' into feat/quick-service-switcher
Siumauricio Feb 27, 2026
ebf5f48
refactor: simplify AdvanceBreadcrumb component by removing props and …
Siumauricio Feb 27, 2026
a1cf552
refactor: remove props being passes to AdvanceBreadcrumb
imran-vz Mar 1, 2026
f95b29a
Export findGitea as default to fix typecheck
imran-vz Mar 1, 2026
86feda1
Merge branch 'canary' into feat/quick-service-switcher
imran-vz Mar 1, 2026
1c5b927
refactor: resolved type errors in advance-breadcrumb.ts
imran-vz Mar 2, 2026
c75cfa2
Merge branch 'canary' of github.com:imran-vz/dokploy into feat/quick-…
imran-vz Mar 4, 2026
7feb406
feat: expose dropDeployment endpoint in public API
fdarian Jan 12, 2026
66931fe
feat: use zod-form-data schema for dropDeployment input
fdarian Mar 7, 2026
653e5fa
fix: validate applicationId
fdarian Mar 7, 2026
1203d05
fix: use dedicated schema
fdarian Mar 8, 2026
5e6e5ba
Merge branch 'Dokploy:canary' into feat/quick-service-switcher
imran-vz Mar 9, 2026
ee42a39
fix: wrap trustedOrigins callback with try/catch to prevent unhandled…
RchrdHndrcks Mar 15, 2026
2880327
feat: add settings configuration for command permissions in Claude
Siumauricio Mar 18, 2026
00f3853
chore: remove settings.json file for command permissions in Claude
Siumauricio Mar 18, 2026
ad2e53a
fix: truncate error message in backup notifications to 1010 characters
Siumauricio Mar 18, 2026
9f9c8fc
Update packages/server/src/utils/notifications/database-backup.ts
Siumauricio Mar 18, 2026
cccee05
Merge pull request #4023 from Dokploy/4021-discord-error-notification…
Siumauricio Mar 18, 2026
0c22041
refactor: update billing component to manage server quantities for ho…
Siumauricio Mar 18, 2026
bade36e
feat: add alert for users with custom roles without a valid license
Siumauricio Mar 18, 2026
1fa4d5b
refactor: improve formatting and readability in billing and users com…
Siumauricio Mar 18, 2026
9067452
feat: add role presets for custom role management
Siumauricio Mar 18, 2026
a45d8ee
feat: update apikey schema and relationships
Siumauricio Mar 18, 2026
d96e2bb
chore: bump version to v0.28.8 in package.json
Siumauricio Mar 18, 2026
72974e0
Merge pull request #4028 from Dokploy/4024-api-keys-not-working-and-u…
Siumauricio Mar 18, 2026
d0c92d8
fix: update API key deletion authorization check
Siumauricio Mar 18, 2026
cddb06f
feat: enhance web server update process with health checks
Siumauricio Mar 19, 2026
b139d6f
[autofix.ci] apply automated fixes
autofix-ci[bot] Mar 19, 2026
e47263a
Merge pull request #4033 from Dokploy/feat/improve-update-process-to-…
Siumauricio Mar 19, 2026
1b70763
Merge branch 'canary' into feat/expose-drop-deployment-api
Siumauricio Mar 19, 2026
7878bf2
chore: update @dokploy/trpc-openapi to version 0.0.18
Siumauricio Mar 19, 2026
7c55eba
Merge pull request #3923 from fdarian/feat/expose-drop-deployment-api
Siumauricio Mar 19, 2026
c2d3763
Merge branch 'canary' into feat/quick-service-switcher
Siumauricio Mar 19, 2026
81ecf21
fix: update input focus styles in AdvanceBreadcrumb component
Siumauricio Mar 19, 2026
51d744b
refactor: remove unused AdvanceBreadcrumb import from project show co…
Siumauricio Mar 19, 2026
72c15ac
Merge pull request #3716 from imran-vz/feat/quick-service-switcher
Siumauricio Mar 19, 2026
7d2d7fc
Merge pull request #4004 from RchrdHndrcks/fix/trusted-origins-unhand…
Siumauricio Mar 19, 2026
837373f
fix: update font size in AdvanceBreadcrumb component for better reada…
Siumauricio Mar 19, 2026
bc11e87
chore: remove unused database migration and snapshot files for projec…
Siumauricio Mar 19, 2026
43f9c11
Merge branch 'canary' into cucumber-sp/canary
Siumauricio Mar 19, 2026
b3579d1
feat(database): add project_tag and tag tables with foreign key const…
Siumauricio Mar 19, 2026
e9650de
feat(tags): implement HandleTag component for creating and updating tags
Siumauricio Mar 19, 2026
aca1c6f
fix(tag-selector): add background color to tag selector for improved …
Siumauricio Mar 19, 2026
fff9115
feat(tags): enhance tag management with permission checks
Siumauricio Mar 19, 2026
2809cd6
[autofix.ci] apply automated fixes
autofix-ci[bot] Mar 19, 2026
8304513
refactor(tags): update permission checks for tag access
Siumauricio Mar 19, 2026
1d7509d
[autofix.ci] apply automated fixes
autofix-ci[bot] Mar 19, 2026
7f60000
refactor(tags): improve server-side permission handling for tag access
Siumauricio Mar 19, 2026
bd18461
refactor(HandleTag): streamline tag submission logic
Siumauricio Mar 19, 2026
8a8688c
Merge pull request #3706 from cucumber-sp/canary
Siumauricio Mar 19, 2026
6fb4a13
chore: update dependencies in pnpm-lock.yaml and package.json
Siumauricio Mar 19, 2026
4045437
feat: add SFTP, FTP, and Google Drive backup destinations via rclone
Gengyscan Mar 20, 2026
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
4 changes: 3 additions & 1 deletion apps/dokploy/components/dashboard/project/add-compose.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ export const AddCompose = ({ environmentId, projectName }: Props) => {
api.compose.create.useMutation();

// Get environment data to extract projectId
const { data: environment } = api.environment.one.useQuery({ environmentId });
// const { data: environment } = api.environment.one.useQuery({ environmentId });

const hasServers = servers && servers.length > 0;
// Show dropdown logic based on cloud environment
Expand Down Expand Up @@ -117,6 +117,8 @@ export const AddCompose = ({ environmentId, projectName }: Props) => {
await utils.environment.one.invalidate({
environmentId,
});
// Invalidate the project query to refresh the project data for the advance-breadcrumb
await utils.project.all.invalidate();
})
.catch(() => {
toast.error("Error creating the compose");
Expand Down
1 change: 1 addition & 0 deletions apps/dokploy/components/dashboard/project/add-template.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,7 @@ export const AddTemplate = ({ environmentId, baseUrl }: Props) => {
viewMode === "detailed" && "border-b",
)}
>
{/** biome-ignore lint/performance/noImgElement: this is a valid use for img tag */}
<img
src={`${customBaseUrl || "https://templates.dokploy.com/"}/blueprints/${template?.id}/${template?.logo}`}
className={cn(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ export const AdvancedEnvironmentSelector = ({

toast.success("Environment created successfully");
utils.environment.byProjectId.invalidate({ projectId });
// Invalidate the project query to refresh the project data for the advance-breadcrumb
utils.project.all.invalidate();
setIsCreateDialogOpen(false);
setName("");
setDescription("");
Expand Down
45 changes: 43 additions & 2 deletions apps/dokploy/components/dashboard/projects/handle-project.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { AlertBlock } from "@/components/shared/alert-block";
import { TagSelector } from "@/components/shared/tag-selector";
import { Button } from "@/components/ui/button";
import {
Dialog,
Expand Down Expand Up @@ -62,6 +63,7 @@ interface Props {
export const HandleProject = ({ projectId }: Props) => {
const utils = api.useUtils();
const [isOpen, setIsOpen] = useState(false);
const [selectedTagIds, setSelectedTagIds] = useState<string[]>([]);

const { mutateAsync, error, isError } = projectId
? api.project.update.useMutation()
Expand All @@ -75,6 +77,10 @@ export const HandleProject = ({ projectId }: Props) => {
enabled: !!projectId,
},
);

const { data: availableTags = [] } = api.tag.all.useQuery();
const bulkAssignMutation = api.tag.bulkAssign.useMutation();

const router = useRouter();
const form = useForm<AddProject>({
defaultValues: {
Expand All @@ -89,6 +95,13 @@ export const HandleProject = ({ projectId }: Props) => {
description: data?.description ?? "",
name: data?.name ?? "",
});
// Load existing tags when editing a project
if (data?.projectTags) {
const tagIds = data.projectTags.map((pt) => pt.tagId);
setSelectedTagIds(tagIds);
} else {
setSelectedTagIds([]);
}
}, [form, form.reset, form.formState.isSubmitSuccessful, data]);

const onSubmit = async (data: AddProject) => {
Expand All @@ -98,12 +111,26 @@ export const HandleProject = ({ projectId }: Props) => {
projectId: projectId || "",
})
.then(async (data) => {
// Assign tags to the project (both create and update)
const projectIdToUse =
projectId ||
(data && "project" in data ? data.project.projectId : undefined);

if (projectIdToUse) {
try {
await bulkAssignMutation.mutateAsync({
projectId: projectIdToUse,
tagIds: selectedTagIds,
});
} catch (error) {
toast.error("Failed to assign tags to project");
}
}

await utils.project.all.invalidate();
toast.success(projectId ? "Project Updated" : "Project Created");
setIsOpen(false);
if (!projectId) {
const projectIdToUse =
data && "project" in data ? data.project.projectId : undefined;
const environmentIdToUse =
data && "environment" in data
? data.environment.environmentId
Expand Down Expand Up @@ -190,6 +217,20 @@ export const HandleProject = ({ projectId }: Props) => {
</FormItem>
)}
/>

<div className="space-y-2">
<FormLabel>Tags</FormLabel>
<TagSelector
tags={availableTags.map((tag) => ({
id: tag.tagId,
name: tag.name,
color: tag.color ?? undefined,
}))}
selectedTags={selectedTagIds}
onTagsChange={setSelectedTagIds}
placeholder="Select tags..."
/>
</div>
</form>

<DialogFooter>
Expand Down
118 changes: 92 additions & 26 deletions apps/dokploy/components/dashboard/projects/show.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import { toast } from "sonner";
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
import { DateTooltip } from "@/components/shared/date-tooltip";
import { FocusShortcutInput } from "@/components/shared/focus-shortcut-input";
import { TagBadge } from "@/components/shared/tag-badge";
import { TagFilter } from "@/components/shared/tag-filter";
import {
AlertDialog,
AlertDialogAction,
Expand Down Expand Up @@ -63,6 +65,7 @@ export const ShowProjects = () => {
const { data: auth } = api.user.get.useQuery();
const { data: permissions } = api.user.getPermissions.useQuery();
const { mutateAsync } = api.project.remove.useMutation();
const { data: availableTags } = api.tag.all.useQuery();

const [searchQuery, setSearchQuery] = useState(
router.isReady && typeof router.query.q === "string" ? router.query.q : "",
Expand All @@ -76,10 +79,31 @@ export const ShowProjects = () => {
return "createdAt-desc";
});

const [selectedTagIds, setSelectedTagIds] = useState<string[]>(() => {
if (typeof window !== "undefined") {
const saved = localStorage.getItem("projectsTagFilter");
return saved ? JSON.parse(saved) : [];
}
return [];
});

useEffect(() => {
localStorage.setItem("projectsSort", sortBy);
}, [sortBy]);

useEffect(() => {
localStorage.setItem("projectsTagFilter", JSON.stringify(selectedTagIds));
}, [selectedTagIds]);

useEffect(() => {
if (!availableTags) return;
const validIds = new Set(availableTags.map((t) => t.tagId));
setSelectedTagIds((prev) => {
const filtered = prev.filter((id) => validIds.has(id));
return filtered.length === prev.length ? prev : filtered;
});
}, [availableTags]);

useEffect(() => {
if (!router.isReady) return;
const urlQuery = typeof router.query.q === "string" ? router.query.q : "";
Expand Down Expand Up @@ -107,7 +131,7 @@ export const ShowProjects = () => {
const filteredProjects = useMemo(() => {
if (!data) return [];

const filtered = data.filter(
let filtered = data.filter(
(project) =>
project.name
.toLowerCase()
Expand All @@ -117,6 +141,15 @@ export const ShowProjects = () => {
.includes(debouncedSearchQuery.toLowerCase()),
);

// Filter by selected tags (OR logic: show projects with ANY selected tag)
if (selectedTagIds.length > 0) {
filtered = filtered.filter((project) =>
project.projectTags?.some((pt) =>
selectedTagIds.includes(pt.tag.tagId),
),
);
}

// Then sort the filtered results
const [field, direction] = sortBy.split("-");
return [...filtered].sort((a, b) => {
Expand Down Expand Up @@ -162,10 +195,15 @@ export const ShowProjects = () => {
}
return direction === "asc" ? comparison : -comparison;
});
}, [data, debouncedSearchQuery, sortBy]);
}, [data, debouncedSearchQuery, sortBy, selectedTagIds]);

return (
<>
{!isCloud && (
<div className="absolute top-4 right-4">
<TimeBadge />
</div>
)}
<BreadcrumbSidebar
list={[{ name: "Projects", href: "/dashboard/projects" }]}
/>
Expand Down Expand Up @@ -208,29 +246,44 @@ export const ShowProjects = () => {

<Search className="absolute right-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
</div>
<div className="flex items-center gap-2 min-w-48 max-sm:w-full">
<ArrowUpDown className="size-4 text-muted-foreground" />
<Select value={sortBy} onValueChange={setSortBy}>
<SelectTrigger className="w-full">
<SelectValue placeholder="Sort by..." />
</SelectTrigger>
<SelectContent>
<SelectItem value="name-asc">Name (A-Z)</SelectItem>
<SelectItem value="name-desc">Name (Z-A)</SelectItem>
<SelectItem value="createdAt-desc">
Newest first
</SelectItem>
<SelectItem value="createdAt-asc">
Oldest first
</SelectItem>
<SelectItem value="services-desc">
Most services
</SelectItem>
<SelectItem value="services-asc">
Least services
</SelectItem>
</SelectContent>
</Select>
<div className="flex items-center gap-2">
<TagFilter
tags={
availableTags?.map((tag) => ({
id: tag.tagId,
name: tag.name,
color: tag.color || undefined,
})) || []
}
selectedTags={selectedTagIds}
onTagsChange={setSelectedTagIds}
/>
<div className="flex items-center gap-2 min-w-48 max-sm:w-full">
<ArrowUpDown className="size-4 text-muted-foreground" />
<Select value={sortBy} onValueChange={setSortBy}>
<SelectTrigger className="w-full">
<SelectValue placeholder="Sort by..." />
</SelectTrigger>
<SelectContent>
<SelectItem value="name-asc">Name (A-Z)</SelectItem>
<SelectItem value="name-desc">
Name (Z-A)
</SelectItem>
<SelectItem value="createdAt-desc">
Newest first
</SelectItem>
<SelectItem value="createdAt-asc">
Oldest first
</SelectItem>
<SelectItem value="services-desc">
Most services
</SelectItem>
<SelectItem value="services-asc">
Least services
</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
{filteredProjects?.length === 0 && (
Expand Down Expand Up @@ -309,6 +362,19 @@ export const ShowProjects = () => {
{project.description}
</span>

{project.projectTags &&
project.projectTags.length > 0 && (
<div className="flex flex-wrap gap-1.5 mt-2">
{project.projectTags.map((pt) => (
<TagBadge
key={pt.tag.tagId}
name={pt.tag.name}
color={pt.tag.color}
/>
))}
</div>
)}

{hasNoEnvironments && (
<div className="flex flex-row gap-2 items-center rounded-lg bg-yellow-50 p-2 mt-2 dark:bg-yellow-950">
<AlertTriangle className="size-4 text-yellow-600 dark:text-yellow-400 shrink-0" />
Expand Down Expand Up @@ -429,7 +495,7 @@ export const ShowProjects = () => {
</CardTitle>
</CardHeader>
<CardFooter className="pt-4">
<div className="space-y-1 text-sm flex flex-row justify-between max-sm:flex-wrap w-full gap-2 sm:gap-4">
<div className="space-y-1 text-xs flex flex-row justify-between max-sm:flex-wrap w-full gap-2 sm:gap-4">
<DateTooltip date={project.createdAt}>
Created
</DateTooltip>
Expand Down
Loading