-
}>
- {flagsLoading ? (
-
- ) : activeFlags.length === 0 ? (
-
-
}
- title="No feature flags yet"
- variant="minimal"
+
+ {activeFlags.length > 0 && (
+
+ )}
+
+ }>
+ {renderContent()}
+
+
+ {isFlagSheetOpen && (
+
+
-
- ) : (
-
+
)}
-
-
- {isFlagSheetOpen && (
-
-
-
- )}
-
setFlagToDelete(null)}
- onConfirm={handleConfirmDelete}
- title="Delete Feature Flag"
- />
+ setFlagToDelete(null)}
+ onConfirm={handleConfirmDelete}
+ title="Delete Feature Flag"
+ />
+
diff --git a/bun.lock b/bun.lock
index 0c3078115..24a9034c2 100644
--- a/bun.lock
+++ b/bun.lock
@@ -27,7 +27,7 @@
},
"apps/api": {
"name": "@databuddy/api",
- "version": "1.0.0",
+ "version": "1.0.1",
"dependencies": {
"@ai-sdk-tools/agents": "^1.2.0",
"@ai-sdk-tools/artifacts": "^1.2.0",
diff --git a/packages/db/src/drizzle/schema.ts b/packages/db/src/drizzle/schema.ts
index 474e87bc3..df83274bf 100644
--- a/packages/db/src/drizzle/schema.ts
+++ b/packages/db/src/drizzle/schema.ts
@@ -658,6 +658,7 @@ export const flags = pgTable(
persistAcrossAuth: boolean("persist_across_auth").default(false).notNull(),
rolloutPercentage: integer("rollout_percentage").default(0),
rolloutBy: text("rollout_by"),
+ folder: text(),
websiteId: text("website_id"),
organizationId: text("organization_id"),
userId: text("user_id"),
diff --git a/packages/rpc/src/routers/flags.ts b/packages/rpc/src/routers/flags.ts
index 88de78a79..e575d9e3d 100644
--- a/packages/rpc/src/routers/flags.ts
+++ b/packages/rpc/src/routers/flags.ts
@@ -66,6 +66,7 @@ const listFlagsSchema = z
websiteId: z.string().optional(),
organizationId: z.string().optional(),
status: z.enum(["active", "inactive", "archived"]).optional(),
+ folder: z.string().optional(),
})
.refine((data) => data.websiteId || data.organizationId, {
message: "Either websiteId or organizationId must be provided",
@@ -100,6 +101,7 @@ const createFlagSchema = z
organizationId: z.string().optional(),
payload: z.any().optional(),
persistAcrossAuth: z.boolean().optional(),
+ folder: z.string().optional(),
...flagFormSchema.shape,
})
.refine((data) => data.websiteId || data.organizationId, {
@@ -124,6 +126,7 @@ const updateFlagSchema = z
dependencies: z.array(z.string()).optional(),
environment: z.string().optional(),
targetGroupIds: z.array(z.string()).optional(),
+ folder: z.string().optional(),
})
.superRefine((data, ctx) => {
if (data.type === "multivariant" && data.variants) {
@@ -264,6 +267,10 @@ export const flagsRouter = {
conditions.push(eq(flags.status, input.status));
}
+ if (input.folder) {
+ conditions.push(eq(flags.folder, input.folder));
+ }
+
const flagsList = await context.db.query.flags.findMany({
where: and(...conditions),
orderBy: desc(flags.createdAt),
@@ -584,6 +591,7 @@ export const flagsRouter = {
persistAcrossAuth: input.persistAcrossAuth ?? false,
rolloutPercentage: input.rolloutPercentage || 0,
rolloutBy: input.rolloutBy || null,
+ folder: input.folder || null,
variants: input.variants || [],
dependencies: input.dependencies || [],
websiteId: input.websiteId || null,
@@ -827,4 +835,115 @@ export const flagsRouter = {
return { success: true };
}),
+
+ renameFolder: protectedProcedure
+ .input(
+ z
+ .object({
+ websiteId: z.string().optional(),
+ organizationId: z.string().optional(),
+ oldName: z.string(),
+ newName: z.string(),
+ })
+ .refine((data) => data.websiteId || data.organizationId, {
+ message: "Either websiteId or organizationId must be provided",
+ path: ["websiteId"],
+ })
+ )
+ .handler(async ({ context, input }) => {
+ await authorizeScope(
+ context,
+ input.websiteId,
+ input.organizationId,
+ "update"
+ );
+
+ const { oldName, newName } = input;
+
+ const updatedFlags = await context.db
+ .update(flags)
+ .set({
+ folder: newName,
+ updatedAt: new Date(),
+ })
+ .where(
+ and(
+ getScopeCondition(
+ input.websiteId,
+ input.organizationId,
+ context.user.id
+ ),
+ eq(flags.folder, oldName),
+ isNull(flags.deletedAt)
+ )
+ )
+ .returning({ id: flags.id, key: flags.key });
+
+ // Invalidate cache for all affected flags
+ for (const flag of updatedFlags) {
+ await invalidateFlagCache(
+ flag.id,
+ input.websiteId,
+ input.organizationId,
+ flag.key
+ );
+ }
+
+ return { success: true, count: updatedFlags.length };
+ }),
+
+ deleteFolder: protectedProcedure
+ .input(
+ z
+ .object({
+ websiteId: z.string().optional(),
+ organizationId: z.string().optional(),
+ folder: z.string(),
+ })
+ .refine((data) => data.websiteId || data.organizationId, {
+ message: "Either websiteId or organizationId must be provided",
+ path: ["websiteId"],
+ })
+ )
+ .handler(async ({ context, input }) => {
+ await authorizeScope(
+ context,
+ input.websiteId,
+ input.organizationId,
+ "update"
+ );
+
+ const { folder } = input;
+
+ const updatedFlags = await context.db
+ .update(flags)
+ .set({
+ folder: null,
+ updatedAt: new Date(),
+ })
+ .where(
+ and(
+ getScopeCondition(
+ input.websiteId,
+ input.organizationId,
+ context.user.id
+ ),
+ eq(flags.folder, folder),
+ isNull(flags.deletedAt)
+ )
+ )
+ .returning({ id: flags.id, key: flags.key });
+
+ // Invalidate cache for all affected flags
+ for (const flag of updatedFlags) {
+ await invalidateFlagCache(
+ flag.id,
+ input.websiteId,
+ input.organizationId,
+ flag.key
+ );
+ }
+
+ return { success: true, count: updatedFlags.length };
+ }),
};
diff --git a/packages/shared/src/flags/index.ts b/packages/shared/src/flags/index.ts
index 069b6fbf7..dd7bf93f6 100644
--- a/packages/shared/src/flags/index.ts
+++ b/packages/shared/src/flags/index.ts
@@ -61,6 +61,7 @@ export const flagFormSchema = z
.array(z.string().min(1, "Invalid dependency value"))
.optional(),
environment: z.string().nullable().optional(),
+ folder: z.string().optional(),
targetGroupIds: z.array(z.string()).optional(),
})
.superRefine((data, ctx) => {