mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 16:27:46 +00:00
Merge pull request #3174 from Infisical/feat/addFolderDescription
Add descriptions to secret folders
This commit is contained in:
@@ -0,0 +1,23 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
const hasProjectDescription = await knex.schema.hasColumn(TableName.SecretFolder, "description");
|
||||||
|
|
||||||
|
if (!hasProjectDescription) {
|
||||||
|
await knex.schema.alterTable(TableName.SecretFolder, (t) => {
|
||||||
|
t.string("description");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
const hasProjectDescription = await knex.schema.hasColumn(TableName.SecretFolder, "description");
|
||||||
|
|
||||||
|
if (hasProjectDescription) {
|
||||||
|
await knex.schema.alterTable(TableName.SecretFolder, (t) => {
|
||||||
|
t.dropColumn("description");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,7 +15,8 @@ export const SecretFoldersSchema = z.object({
|
|||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
envId: z.string().uuid(),
|
envId: z.string().uuid(),
|
||||||
parentId: z.string().uuid().nullable().optional(),
|
parentId: z.string().uuid().nullable().optional(),
|
||||||
isReserved: z.boolean().default(false).nullable().optional()
|
isReserved: z.boolean().default(false).nullable().optional(),
|
||||||
|
description: z.string().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TSecretFolders = z.infer<typeof SecretFoldersSchema>;
|
export type TSecretFolders = z.infer<typeof SecretFoldersSchema>;
|
||||||
|
|||||||
@@ -1142,6 +1142,7 @@ interface CreateFolderEvent {
|
|||||||
folderId: string;
|
folderId: string;
|
||||||
folderName: string;
|
folderName: string;
|
||||||
folderPath: string;
|
folderPath: string;
|
||||||
|
description?: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -638,7 +638,8 @@ export const FOLDERS = {
|
|||||||
environment: "The slug of the environment to create the folder in.",
|
environment: "The slug of the environment to create the folder in.",
|
||||||
name: "The name of the folder to create.",
|
name: "The name of the folder to create.",
|
||||||
path: "The path of the folder to create.",
|
path: "The path of the folder to create.",
|
||||||
directory: "The directory of the folder to create. (Deprecated in favor of path)"
|
directory: "The directory of the folder to create. (Deprecated in favor of path)",
|
||||||
|
description: "An optional description label for the folder."
|
||||||
},
|
},
|
||||||
UPDATE: {
|
UPDATE: {
|
||||||
folderId: "The ID of the folder to update.",
|
folderId: "The ID of the folder to update.",
|
||||||
@@ -647,7 +648,8 @@ export const FOLDERS = {
|
|||||||
path: "The path of the folder to update.",
|
path: "The path of the folder to update.",
|
||||||
directory: "The new directory of the folder to update. (Deprecated in favor of path)",
|
directory: "The new directory of the folder to update. (Deprecated in favor of path)",
|
||||||
projectSlug: "The slug of the project where the folder is located.",
|
projectSlug: "The slug of the project where the folder is located.",
|
||||||
workspaceId: "The ID of the project where the folder is located."
|
workspaceId: "The ID of the project where the folder is located.",
|
||||||
|
description: "An optional description label for the folder."
|
||||||
},
|
},
|
||||||
DELETE: {
|
DELETE: {
|
||||||
folderIdOrName: "The ID or name of the folder to delete.",
|
folderIdOrName: "The ID or name of the folder to delete.",
|
||||||
|
|||||||
@@ -47,7 +47,8 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
|
|||||||
.default("/")
|
.default("/")
|
||||||
.transform(prefixWithSlash)
|
.transform(prefixWithSlash)
|
||||||
.transform(removeTrailingSlash)
|
.transform(removeTrailingSlash)
|
||||||
.describe(FOLDERS.CREATE.directory)
|
.describe(FOLDERS.CREATE.directory),
|
||||||
|
description: z.string().optional().nullable().describe(FOLDERS.CREATE.description)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@@ -65,7 +66,8 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
|
|||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
...req.body,
|
...req.body,
|
||||||
projectId: req.body.workspaceId,
|
projectId: req.body.workspaceId,
|
||||||
path
|
path,
|
||||||
|
description: req.body.description
|
||||||
});
|
});
|
||||||
await server.services.auditLog.createAuditLog({
|
await server.services.auditLog.createAuditLog({
|
||||||
...req.auditLogInfo,
|
...req.auditLogInfo,
|
||||||
@@ -76,7 +78,8 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
|
|||||||
environment: req.body.environment,
|
environment: req.body.environment,
|
||||||
folderId: folder.id,
|
folderId: folder.id,
|
||||||
folderName: folder.name,
|
folderName: folder.name,
|
||||||
folderPath: path
|
folderPath: path,
|
||||||
|
...(req.body.description ? { description: req.body.description } : {})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -125,7 +128,8 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
|
|||||||
.default("/")
|
.default("/")
|
||||||
.transform(prefixWithSlash)
|
.transform(prefixWithSlash)
|
||||||
.transform(removeTrailingSlash)
|
.transform(removeTrailingSlash)
|
||||||
.describe(FOLDERS.UPDATE.directory)
|
.describe(FOLDERS.UPDATE.directory),
|
||||||
|
description: z.string().optional().nullable().describe(FOLDERS.UPDATE.description)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@@ -196,7 +200,8 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
|
|||||||
.default("/")
|
.default("/")
|
||||||
.transform(prefixWithSlash)
|
.transform(prefixWithSlash)
|
||||||
.transform(removeTrailingSlash)
|
.transform(removeTrailingSlash)
|
||||||
.describe(FOLDERS.UPDATE.path)
|
.describe(FOLDERS.UPDATE.path),
|
||||||
|
description: z.string().optional().nullable().describe(FOLDERS.UPDATE.description)
|
||||||
})
|
})
|
||||||
.array()
|
.array()
|
||||||
.min(1)
|
.min(1)
|
||||||
|
|||||||
@@ -50,7 +50,8 @@ export const secretFolderServiceFactory = ({
|
|||||||
actorOrgId,
|
actorOrgId,
|
||||||
name,
|
name,
|
||||||
environment,
|
environment,
|
||||||
path: secretPath
|
path: secretPath,
|
||||||
|
description
|
||||||
}: TCreateFolderDTO) => {
|
}: TCreateFolderDTO) => {
|
||||||
const { permission } = await permissionService.getProjectPermission({
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
actor,
|
actor,
|
||||||
@@ -121,7 +122,10 @@ export const secretFolderServiceFactory = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const doc = await folderDAL.create({ name, envId: env.id, version: 1, parentId: parentFolderId }, tx);
|
const doc = await folderDAL.create(
|
||||||
|
{ name, envId: env.id, version: 1, parentId: parentFolderId, description },
|
||||||
|
tx
|
||||||
|
);
|
||||||
await folderVersionDAL.create(
|
await folderVersionDAL.create(
|
||||||
{
|
{
|
||||||
name: doc.name,
|
name: doc.name,
|
||||||
@@ -170,7 +174,7 @@ export const secretFolderServiceFactory = ({
|
|||||||
const result = await folderDAL.transaction(async (tx) =>
|
const result = await folderDAL.transaction(async (tx) =>
|
||||||
Promise.all(
|
Promise.all(
|
||||||
folders.map(async (newFolder) => {
|
folders.map(async (newFolder) => {
|
||||||
const { environment, path: secretPath, id, name } = newFolder;
|
const { environment, path: secretPath, id, name, description } = newFolder;
|
||||||
|
|
||||||
const parentFolder = await folderDAL.findBySecretPath(project.id, environment, secretPath);
|
const parentFolder = await folderDAL.findBySecretPath(project.id, environment, secretPath);
|
||||||
if (!parentFolder) {
|
if (!parentFolder) {
|
||||||
@@ -217,7 +221,7 @@ export const secretFolderServiceFactory = ({
|
|||||||
|
|
||||||
const [doc] = await folderDAL.update(
|
const [doc] = await folderDAL.update(
|
||||||
{ envId: env.id, id: folder.id, parentId: parentFolder.id },
|
{ envId: env.id, id: folder.id, parentId: parentFolder.id },
|
||||||
{ name },
|
{ name, description },
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
await folderVersionDAL.create(
|
await folderVersionDAL.create(
|
||||||
@@ -259,7 +263,8 @@ export const secretFolderServiceFactory = ({
|
|||||||
name,
|
name,
|
||||||
environment,
|
environment,
|
||||||
path: secretPath,
|
path: secretPath,
|
||||||
id
|
id,
|
||||||
|
description
|
||||||
}: TUpdateFolderDTO) => {
|
}: TUpdateFolderDTO) => {
|
||||||
const { permission } = await permissionService.getProjectPermission({
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
actor,
|
actor,
|
||||||
@@ -312,7 +317,7 @@ export const secretFolderServiceFactory = ({
|
|||||||
const newFolder = await folderDAL.transaction(async (tx) => {
|
const newFolder = await folderDAL.transaction(async (tx) => {
|
||||||
const [doc] = await folderDAL.update(
|
const [doc] = await folderDAL.update(
|
||||||
{ envId: env.id, id: folder.id, parentId: parentFolder.id, isReserved: false },
|
{ envId: env.id, id: folder.id, parentId: parentFolder.id, isReserved: false },
|
||||||
{ name },
|
{ name, description },
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
await folderVersionDAL.create(
|
await folderVersionDAL.create(
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export type TCreateFolderDTO = {
|
|||||||
environment: string;
|
environment: string;
|
||||||
path: string;
|
path: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
description?: string | null;
|
||||||
} & TProjectPermission;
|
} & TProjectPermission;
|
||||||
|
|
||||||
export type TUpdateFolderDTO = {
|
export type TUpdateFolderDTO = {
|
||||||
@@ -16,6 +17,7 @@ export type TUpdateFolderDTO = {
|
|||||||
path: string;
|
path: string;
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
description?: string | null;
|
||||||
} & TProjectPermission;
|
} & TProjectPermission;
|
||||||
|
|
||||||
export type TUpdateManyFoldersDTO = {
|
export type TUpdateManyFoldersDTO = {
|
||||||
@@ -25,6 +27,7 @@ export type TUpdateManyFoldersDTO = {
|
|||||||
path: string;
|
path: string;
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
description?: string | null;
|
||||||
}[];
|
}[];
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
|||||||
@@ -148,12 +148,13 @@ export const useUpdateFolder = () => {
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation<object, object, TUpdateFolderDTO>({
|
return useMutation<object, object, TUpdateFolderDTO>({
|
||||||
mutationFn: async ({ path = "/", folderId, name, environment, projectId }) => {
|
mutationFn: async ({ path = "/", folderId, name, environment, projectId, description }) => {
|
||||||
const { data } = await apiRequest.patch(`/api/v1/folders/${folderId}`, {
|
const { data } = await apiRequest.patch(`/api/v1/folders/${folderId}`, {
|
||||||
name,
|
name,
|
||||||
environment,
|
environment,
|
||||||
workspaceId: projectId,
|
workspaceId: projectId,
|
||||||
path
|
path,
|
||||||
|
description
|
||||||
});
|
});
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export enum ReservedFolders {
|
|||||||
export type TSecretFolder = {
|
export type TSecretFolder = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
description?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TGetProjectFoldersDTO = {
|
export type TGetProjectFoldersDTO = {
|
||||||
@@ -24,6 +25,7 @@ export type TCreateFolderDTO = {
|
|||||||
environment: string;
|
environment: string;
|
||||||
name: string;
|
name: string;
|
||||||
path?: string;
|
path?: string;
|
||||||
|
description?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TUpdateFolderDTO = {
|
export type TUpdateFolderDTO = {
|
||||||
@@ -32,6 +34,7 @@ export type TUpdateFolderDTO = {
|
|||||||
name: string;
|
name: string;
|
||||||
folderId: string;
|
folderId: string;
|
||||||
path?: string;
|
path?: string;
|
||||||
|
description?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TDeleteFolderDTO = {
|
export type TDeleteFolderDTO = {
|
||||||
@@ -49,5 +52,6 @@ export type TUpdateFolderBatchDTO = {
|
|||||||
environment: string;
|
environment: string;
|
||||||
id: string;
|
id: string;
|
||||||
path?: string;
|
path?: string;
|
||||||
|
description?: string | null;
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,13 +2,22 @@ import { useCallback, useMemo } from "react";
|
|||||||
|
|
||||||
import { DashboardProjectSecretsOverview } from "@app/hooks/api/dashboard/types";
|
import { DashboardProjectSecretsOverview } from "@app/hooks/api/dashboard/types";
|
||||||
|
|
||||||
|
type FolderNameAndDescription = {
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export const useFolderOverview = (folders: DashboardProjectSecretsOverview["folders"]) => {
|
export const useFolderOverview = (folders: DashboardProjectSecretsOverview["folders"]) => {
|
||||||
const folderNames = useMemo(() => {
|
const folderNamesAndDescriptions = useMemo(() => {
|
||||||
const names = new Set<string>();
|
const namesAndDescriptions = new Map<string, FolderNameAndDescription>();
|
||||||
|
|
||||||
folders?.forEach((folder) => {
|
folders?.forEach((folder) => {
|
||||||
names.add(folder.name);
|
if (!namesAndDescriptions.has(folder.name)) {
|
||||||
|
namesAndDescriptions.set(folder.name, { name: folder.name, description: folder.description });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return [...names];
|
|
||||||
|
return Array.from(namesAndDescriptions.values());
|
||||||
}, [folders]);
|
}, [folders]);
|
||||||
|
|
||||||
const isFolderPresentInEnv = useCallback(
|
const isFolderPresentInEnv = useCallback(
|
||||||
@@ -31,7 +40,7 @@ export const useFolderOverview = (folders: DashboardProjectSecretsOverview["fold
|
|||||||
[folders]
|
[folders]
|
||||||
);
|
);
|
||||||
|
|
||||||
return { folderNames, isFolderPresentInEnv, getFolderByNameAndEnv };
|
return { folderNamesAndDescriptions, isFolderPresentInEnv, getFolderByNameAndEnv };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useDynamicSecretOverview = (
|
export const useDynamicSecretOverview = (
|
||||||
|
|||||||
@@ -228,7 +228,7 @@ export const OverviewPage = () => {
|
|||||||
setPage
|
setPage
|
||||||
});
|
});
|
||||||
|
|
||||||
const { folderNames, getFolderByNameAndEnv, isFolderPresentInEnv } = useFolderOverview(folders);
|
const { folderNamesAndDescriptions, getFolderByNameAndEnv, isFolderPresentInEnv } = useFolderOverview(folders);
|
||||||
|
|
||||||
const { dynamicSecretNames, isDynamicSecretPresentInEnv } =
|
const { dynamicSecretNames, isDynamicSecretPresentInEnv } =
|
||||||
useDynamicSecretOverview(dynamicSecrets);
|
useDynamicSecretOverview(dynamicSecrets);
|
||||||
@@ -251,14 +251,15 @@ export const OverviewPage = () => {
|
|||||||
"updateFolder"
|
"updateFolder"
|
||||||
] as const);
|
] as const);
|
||||||
|
|
||||||
const handleFolderCreate = async (folderName: string) => {
|
const handleFolderCreate = async (folderName: string, description: string | null) => {
|
||||||
const promises = userAvailableEnvs.map((env) => {
|
const promises = userAvailableEnvs.map((env) => {
|
||||||
const environment = env.slug;
|
const environment = env.slug;
|
||||||
return createFolder({
|
return createFolder({
|
||||||
name: folderName,
|
name: folderName,
|
||||||
path: secretPath,
|
path: secretPath,
|
||||||
environment,
|
environment,
|
||||||
projectId: workspaceId
|
projectId: workspaceId,
|
||||||
|
description
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -279,7 +280,7 @@ export const OverviewPage = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFolderUpdate = async (newFolderName: string) => {
|
const handleFolderUpdate = async (newFolderName: string, description: string | null) => {
|
||||||
const { name: oldFolderName } = popUp.updateFolder.data as TSecretFolder;
|
const { name: oldFolderName } = popUp.updateFolder.data as TSecretFolder;
|
||||||
|
|
||||||
const updatedFolders: TUpdateFolderBatchDTO["folders"] = [];
|
const updatedFolders: TUpdateFolderBatchDTO["folders"] = [];
|
||||||
@@ -296,7 +297,8 @@ export const OverviewPage = () => {
|
|||||||
environment: env.slug,
|
environment: env.slug,
|
||||||
name: newFolderName,
|
name: newFolderName,
|
||||||
id: folder.id,
|
id: folder.id,
|
||||||
path: secretPath
|
path: secretPath,
|
||||||
|
description
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1027,7 +1029,7 @@ export const OverviewPage = () => {
|
|||||||
)}
|
)}
|
||||||
{!isOverviewLoading && visibleEnvs.length > 0 && (
|
{!isOverviewLoading && visibleEnvs.length > 0 && (
|
||||||
<>
|
<>
|
||||||
{folderNames.map((folderName, index) => (
|
{folderNamesAndDescriptions.map(({name: folderName, description}, index) => (
|
||||||
<SecretOverviewFolderRow
|
<SecretOverviewFolderRow
|
||||||
folderName={folderName}
|
folderName={folderName}
|
||||||
isFolderPresentInEnv={isFolderPresentInEnv}
|
isFolderPresentInEnv={isFolderPresentInEnv}
|
||||||
@@ -1039,7 +1041,7 @@ export const OverviewPage = () => {
|
|||||||
key={`overview-${folderName}-${index + 1}`}
|
key={`overview-${folderName}-${index + 1}`}
|
||||||
onClick={handleFolderClick}
|
onClick={handleFolderClick}
|
||||||
onToggleFolderEdit={(name: string) =>
|
onToggleFolderEdit={(name: string) =>
|
||||||
handlePopUpOpen("updateFolder", { name })
|
handlePopUpOpen("updateFolder", { name, description })
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -1159,7 +1161,9 @@ export const OverviewPage = () => {
|
|||||||
<FolderForm
|
<FolderForm
|
||||||
isEdit
|
isEdit
|
||||||
defaultFolderName={(popUp.updateFolder?.data as Pick<TSecretFolder, "name">)?.name}
|
defaultFolderName={(popUp.updateFolder?.data as Pick<TSecretFolder, "name">)?.name}
|
||||||
|
defaultDescription={(popUp.updateFolder?.data as Pick<TSecretFolder, "description">)?.description}
|
||||||
onUpdateFolder={handleFolderUpdate}
|
onUpdateFolder={handleFolderUpdate}
|
||||||
|
showDescriptionOverwriteWarning
|
||||||
/>
|
/>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
+3
-2
@@ -128,13 +128,14 @@ export const ActionBar = ({
|
|||||||
|
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
|
||||||
const handleFolderCreate = async (folderName: string) => {
|
const handleFolderCreate = async (folderName: string, description: string | null) => {
|
||||||
try {
|
try {
|
||||||
await createFolder({
|
await createFolder({
|
||||||
name: folderName,
|
name: folderName,
|
||||||
path: secretPath,
|
path: secretPath,
|
||||||
environment,
|
environment,
|
||||||
projectId: workspaceId
|
projectId: workspaceId,
|
||||||
|
description
|
||||||
});
|
});
|
||||||
handlePopUpClose("addFolder");
|
handlePopUpClose("addFolder");
|
||||||
createNotification({
|
createNotification({
|
||||||
|
|||||||
+61
-8
@@ -1,16 +1,22 @@
|
|||||||
|
import { useRef } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { Button, FormControl, Input, ModalClose } from "@app/components/v2";
|
import { Button, FormControl, Input, ModalClose } from "@app/components/v2";
|
||||||
|
import { TextArea } from "@app/components/v2/TextArea/TextArea";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onCreateFolder?: (folderName: string) => Promise<void>;
|
onCreateFolder?: (folderName: string, description: string | null) => Promise<void>;
|
||||||
onUpdateFolder?: (folderName: string) => Promise<void>;
|
onUpdateFolder?: (folderName: string, description: string | null) => Promise<void>;
|
||||||
isEdit?: boolean;
|
isEdit?: boolean;
|
||||||
defaultFolderName?: string;
|
defaultFolderName?: string;
|
||||||
|
defaultDescription?: string;
|
||||||
|
showDescriptionOverwriteWarning?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const descriptionOverwriteWarningMessage = "Warning: Any changes made here will overwrite any custom edits in individual environment folders."
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
name: z
|
name: z
|
||||||
.string()
|
.string()
|
||||||
@@ -18,15 +24,20 @@ const formSchema = z.object({
|
|||||||
.regex(
|
.regex(
|
||||||
/^[a-zA-Z0-9-_]+$/,
|
/^[a-zA-Z0-9-_]+$/,
|
||||||
"Folder name can only contain letters, numbers, dashes, and underscores"
|
"Folder name can only contain letters, numbers, dashes, and underscores"
|
||||||
)
|
),
|
||||||
|
description: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
});
|
});
|
||||||
type TFormData = z.infer<typeof formSchema>;
|
type TFormData = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
export const FolderForm = ({
|
export const FolderForm = ({
|
||||||
isEdit,
|
isEdit,
|
||||||
defaultFolderName,
|
defaultFolderName,
|
||||||
|
defaultDescription,
|
||||||
onCreateFolder,
|
onCreateFolder,
|
||||||
onUpdateFolder
|
onUpdateFolder,
|
||||||
|
showDescriptionOverwriteWarning = false
|
||||||
}: Props): JSX.Element => {
|
}: Props): JSX.Element => {
|
||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
@@ -36,15 +47,32 @@ export const FolderForm = ({
|
|||||||
} = useForm<TFormData>({
|
} = useForm<TFormData>({
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: defaultFolderName
|
name: defaultFolderName,
|
||||||
|
description: defaultDescription || ""
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const onSubmit = async ({ name }: TFormData) => {
|
const descriptionRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
|
||||||
|
const handleInput = () => {
|
||||||
|
const textarea = descriptionRef.current;
|
||||||
|
if (textarea) {
|
||||||
|
const lines = textarea.value.split("\n");
|
||||||
|
const maxDescriptionLines = 10;
|
||||||
|
|
||||||
|
if (lines.length > maxDescriptionLines) {
|
||||||
|
textarea.value = lines.slice(0, maxDescriptionLines).join("\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = async ({ name, description }: TFormData) => {
|
||||||
|
const descriptionShaped = description && description.trim() !== "" ? description : null;
|
||||||
|
|
||||||
if (isEdit) {
|
if (isEdit) {
|
||||||
await onUpdateFolder?.(name);
|
await onUpdateFolder?.(name, descriptionShaped);
|
||||||
} else {
|
} else {
|
||||||
await onCreateFolder?.(name);
|
await onCreateFolder?.(name, descriptionShaped);
|
||||||
}
|
}
|
||||||
reset();
|
reset();
|
||||||
};
|
};
|
||||||
@@ -61,6 +89,31 @@ export const FolderForm = ({
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="description"
|
||||||
|
defaultValue=""
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Folder Description"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
tooltipText={showDescriptionOverwriteWarning ? descriptionOverwriteWarningMessage : undefined}
|
||||||
|
isOptional
|
||||||
|
errorText={error?.message}
|
||||||
|
className="flex-1"
|
||||||
|
>
|
||||||
|
<TextArea
|
||||||
|
placeholder="Folder description"
|
||||||
|
{...field}
|
||||||
|
rows={3}
|
||||||
|
ref={descriptionRef}
|
||||||
|
onInput={handleInput}
|
||||||
|
className="thin-scrollbar w-full !resize-none bg-mineshaft-900"
|
||||||
|
maxLength={255}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<div className="mt-8 flex items-center">
|
<div className="mt-8 flex items-center">
|
||||||
<Button className="mr-4" type="submit" isDisabled={isSubmitting} isLoading={isSubmitting}>
|
<Button className="mr-4" type="submit" isDisabled={isSubmitting} isLoading={isSubmitting}>
|
||||||
{isEdit ? "Save" : "Create"}
|
{isEdit ? "Save" : "Create"}
|
||||||
|
|||||||
+18
-5
@@ -1,5 +1,5 @@
|
|||||||
import { subject } from "@casl/ability";
|
import { subject } from "@casl/ability";
|
||||||
import { faClose, faFolder, faPencilSquare } from "@fortawesome/free-solid-svg-icons";
|
import { faClose, faFolder, faPencilSquare, faInfoCircle } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { useNavigate, useSearch } from "@tanstack/react-router";
|
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||||
|
|
||||||
@@ -11,6 +11,7 @@ import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
|||||||
import { usePopUp } from "@app/hooks";
|
import { usePopUp } from "@app/hooks";
|
||||||
import { useDeleteFolder, useUpdateFolder } from "@app/hooks/api";
|
import { useDeleteFolder, useUpdateFolder } from "@app/hooks/api";
|
||||||
import { TSecretFolder } from "@app/hooks/api/secretFolders/types";
|
import { TSecretFolder } from "@app/hooks/api/secretFolders/types";
|
||||||
|
import { Tooltip } from "@app/components/v2/Tooltip/Tooltip";
|
||||||
|
|
||||||
import { FolderForm } from "../ActionBar/FolderForm";
|
import { FolderForm } from "../ActionBar/FolderForm";
|
||||||
|
|
||||||
@@ -42,7 +43,7 @@ export const FolderListView = ({
|
|||||||
const { mutateAsync: updateFolder } = useUpdateFolder();
|
const { mutateAsync: updateFolder } = useUpdateFolder();
|
||||||
const { mutateAsync: deleteFolder } = useDeleteFolder();
|
const { mutateAsync: deleteFolder } = useDeleteFolder();
|
||||||
|
|
||||||
const handleFolderUpdate = async (newFolderName: string) => {
|
const handleFolderUpdate = async (newFolderName: string, newFolderDescription: string | null) => {
|
||||||
try {
|
try {
|
||||||
const { id: folderId } = popUp.updateFolder.data as TSecretFolder;
|
const { id: folderId } = popUp.updateFolder.data as TSecretFolder;
|
||||||
await updateFolder({
|
await updateFolder({
|
||||||
@@ -50,7 +51,8 @@ export const FolderListView = ({
|
|||||||
name: newFolderName,
|
name: newFolderName,
|
||||||
path: secretPath,
|
path: secretPath,
|
||||||
environment,
|
environment,
|
||||||
projectId: workspaceId
|
projectId: workspaceId,
|
||||||
|
description: newFolderDescription
|
||||||
});
|
});
|
||||||
handlePopUpClose("updateFolder");
|
handlePopUpClose("updateFolder");
|
||||||
createNotification({
|
createNotification({
|
||||||
@@ -98,7 +100,7 @@ export const FolderListView = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{folders.map(({ name, id }) => (
|
{folders.map(({ name, id, description }) => (
|
||||||
<div
|
<div
|
||||||
key={id}
|
key={id}
|
||||||
className="group flex cursor-pointer border-b border-mineshaft-600 hover:bg-mineshaft-700"
|
className="group flex cursor-pointer border-b border-mineshaft-600 hover:bg-mineshaft-700"
|
||||||
@@ -116,6 +118,16 @@ export const FolderListView = ({
|
|||||||
onClick={() => handleFolderClick(name)}
|
onClick={() => handleFolderClick(name)}
|
||||||
>
|
>
|
||||||
{name}
|
{name}
|
||||||
|
{
|
||||||
|
description &&
|
||||||
|
<Tooltip
|
||||||
|
position="right"
|
||||||
|
className="flex items-center space-x-4 max-w-lg py-4 whitespace-pre-wrap"
|
||||||
|
content={description}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faInfoCircle} className="text-mineshaft-400 ml-1" />
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-4 border-l border-mineshaft-600 px-3 py-3">
|
<div className="flex items-center space-x-4 border-l border-mineshaft-600 px-3 py-3">
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
@@ -130,7 +142,7 @@ export const FolderListView = ({
|
|||||||
variant="plain"
|
variant="plain"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="p-0 opacity-0 group-hover:opacity-100"
|
className="p-0 opacity-0 group-hover:opacity-100"
|
||||||
onClick={() => handlePopUpOpen("updateFolder", { id, name })}
|
onClick={() => handlePopUpOpen("updateFolder", { id, name, description })}
|
||||||
isDisabled={!isAllowed}
|
isDisabled={!isAllowed}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faPencilSquare} size="lg" />
|
<FontAwesomeIcon icon={faPencilSquare} size="lg" />
|
||||||
@@ -167,6 +179,7 @@ export const FolderListView = ({
|
|||||||
<FolderForm
|
<FolderForm
|
||||||
isEdit
|
isEdit
|
||||||
defaultFolderName={(popUp.updateFolder?.data as TSecretFolder)?.name}
|
defaultFolderName={(popUp.updateFolder?.data as TSecretFolder)?.name}
|
||||||
|
defaultDescription={(popUp.updateFolder?.data as TSecretFolder)?.description}
|
||||||
onUpdateFolder={handleFolderUpdate}
|
onUpdateFolder={handleFolderUpdate}
|
||||||
/>
|
/>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|||||||
Reference in New Issue
Block a user