mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-19 22:25:22 +02:00
feat: add project tags for organizing services
Add tag management system that allows users to create, edit, and delete tags scoped to their organization, and assign them to projects for better organization and filtering. - Add tag and project_tag database schemas with Drizzle migration - Add tRPC router for tag CRUD and project-tag assignment operations - Add tag management page in Settings with color picker - Add tag selector to project create/edit form - Add tag filter to project list with localStorage persistence - Display tag badges on project cards
This commit is contained in:
@@ -33,6 +33,7 @@ export * from "./session";
|
||||
export * from "./shared";
|
||||
export * from "./ssh-key";
|
||||
export * from "./sso";
|
||||
export * from "./tag";
|
||||
export * from "./user";
|
||||
export * from "./utils";
|
||||
export * from "./volume-backups";
|
||||
|
||||
@@ -5,6 +5,7 @@ import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import { organization } from "./account";
|
||||
import { environments } from "./environment";
|
||||
import { projectTags } from "./tag";
|
||||
|
||||
export const projects = pgTable("project", {
|
||||
projectId: text("projectId")
|
||||
@@ -25,6 +26,7 @@ export const projects = pgTable("project", {
|
||||
|
||||
export const projectRelations = relations(projects, ({ many, one }) => ({
|
||||
environments: many(environments),
|
||||
projectTags: many(projectTags),
|
||||
organization: one(organization, {
|
||||
fields: [projects.organizationId],
|
||||
references: [organization.id],
|
||||
|
||||
101
packages/server/src/db/schema/tag.ts
Normal file
101
packages/server/src/db/schema/tag.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import { pgTable, text, unique } from "drizzle-orm/pg-core";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import { organization } from "./account";
|
||||
import { projects } from "./project";
|
||||
|
||||
export const tags = pgTable(
|
||||
"tag",
|
||||
{
|
||||
tagId: text("tagId")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
name: text("name").notNull(),
|
||||
color: text("color"),
|
||||
createdAt: text("createdAt")
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date().toISOString()),
|
||||
|
||||
organizationId: text("organizationId")
|
||||
.notNull()
|
||||
.references(() => organization.id, { onDelete: "cascade" }),
|
||||
},
|
||||
(table) => ({
|
||||
// Unique index on (organizationId, name) to prevent duplicate tag names per organization
|
||||
uniqueOrgName: unique("unique_org_tag_name").on(
|
||||
table.organizationId,
|
||||
table.name,
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
export const projectTags = pgTable(
|
||||
"project_tag",
|
||||
{
|
||||
id: text("id")
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
projectId: text("projectId")
|
||||
.notNull()
|
||||
.references(() => projects.projectId, { onDelete: "cascade" }),
|
||||
tagId: text("tagId")
|
||||
.notNull()
|
||||
.references(() => tags.tagId, { onDelete: "cascade" }),
|
||||
},
|
||||
(table) => ({
|
||||
// Unique constraint to prevent duplicate project-tag associations
|
||||
uniqueProjectTag: unique("unique_project_tag").on(
|
||||
table.projectId,
|
||||
table.tagId,
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
export const tagRelations = relations(tags, ({ one, many }) => ({
|
||||
organization: one(organization, {
|
||||
fields: [tags.organizationId],
|
||||
references: [organization.id],
|
||||
}),
|
||||
projectTags: many(projectTags),
|
||||
}));
|
||||
|
||||
export const projectTagRelations = relations(projectTags, ({ one }) => ({
|
||||
project: one(projects, {
|
||||
fields: [projectTags.projectId],
|
||||
references: [projects.projectId],
|
||||
}),
|
||||
tag: one(tags, {
|
||||
fields: [projectTags.tagId],
|
||||
references: [tags.tagId],
|
||||
}),
|
||||
}));
|
||||
|
||||
const createSchema = createInsertSchema(tags, {
|
||||
tagId: z.string().min(1),
|
||||
name: z.string().min(1),
|
||||
color: z.string().optional(),
|
||||
});
|
||||
|
||||
export const apiCreateTag = createSchema.pick({
|
||||
name: true,
|
||||
color: true,
|
||||
});
|
||||
|
||||
export const apiFindOneTag = createSchema
|
||||
.pick({
|
||||
tagId: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiRemoveTag = createSchema
|
||||
.pick({
|
||||
tagId: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiUpdateTag = createSchema.partial().extend({
|
||||
tagId: z.string().min(1),
|
||||
});
|
||||
@@ -60,6 +60,11 @@ export const findProjectById = async (projectId: string) => {
|
||||
compose: true,
|
||||
},
|
||||
},
|
||||
projectTags: {
|
||||
with: {
|
||||
tag: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!project) {
|
||||
|
||||
Reference in New Issue
Block a user