mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Allow project slug edition and refactor frontend components to reduce duplicated code
This commit is contained in:
@@ -459,7 +459,8 @@ export const PROJECTS = {
|
|||||||
workspaceId: "The ID of the project to update.",
|
workspaceId: "The ID of the project to update.",
|
||||||
name: "The new name of the project.",
|
name: "The new name of the project.",
|
||||||
projectDescription: "An optional description label for the project.",
|
projectDescription: "An optional description label for the project.",
|
||||||
autoCapitalization: "Disable or enable auto-capitalization for the project."
|
autoCapitalization: "Disable or enable auto-capitalization for the project.",
|
||||||
|
slug: "An optional slug for the project. (must be unique within the server)"
|
||||||
},
|
},
|
||||||
GET_KEY: {
|
GET_KEY: {
|
||||||
workspaceId: "The ID of the project to get the key from."
|
workspaceId: "The ID of the project to get the key from."
|
||||||
|
|||||||
@@ -307,7 +307,13 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
.max(256, { message: "Description must be 256 or fewer characters" })
|
.max(256, { message: "Description must be 256 or fewer characters" })
|
||||||
.optional()
|
.optional()
|
||||||
.describe(PROJECTS.UPDATE.projectDescription),
|
.describe(PROJECTS.UPDATE.projectDescription),
|
||||||
autoCapitalization: z.boolean().optional().describe(PROJECTS.UPDATE.autoCapitalization)
|
autoCapitalization: z.boolean().optional().describe(PROJECTS.UPDATE.autoCapitalization),
|
||||||
|
slug: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.max(64, { message: "Slug must be 64 characters or fewer" })
|
||||||
|
.optional()
|
||||||
|
.describe(PROJECTS.UPDATE.slug)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@@ -325,7 +331,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
update: {
|
update: {
|
||||||
name: req.body.name,
|
name: req.body.name,
|
||||||
description: req.body.description,
|
description: req.body.description,
|
||||||
autoCapitalization: req.body.autoCapitalization
|
autoCapitalization: req.body.autoCapitalization,
|
||||||
|
slug: req.body.slug
|
||||||
},
|
},
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
|
|||||||
@@ -563,11 +563,24 @@ export const projectServiceFactory = ({
|
|||||||
});
|
});
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings);
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings);
|
||||||
|
|
||||||
|
if (update.slug) {
|
||||||
|
const existingProject = await projectDAL.findOne({
|
||||||
|
slug: update.slug,
|
||||||
|
orgId: actorOrgId
|
||||||
|
});
|
||||||
|
if (existingProject && existingProject.id !== project.id) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Failed to update project slug. The project "${existingProject.name}" with the slug "${existingProject.slug}" already exists in your organization. Please choose a unique slug for your project.`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const updatedProject = await projectDAL.updateById(project.id, {
|
const updatedProject = await projectDAL.updateById(project.id, {
|
||||||
name: update.name,
|
name: update.name,
|
||||||
description: update.description,
|
description: update.description,
|
||||||
autoCapitalization: update.autoCapitalization,
|
autoCapitalization: update.autoCapitalization,
|
||||||
enforceCapitalization: update.autoCapitalization
|
enforceCapitalization: update.autoCapitalization,
|
||||||
|
slug: update.slug
|
||||||
});
|
});
|
||||||
|
|
||||||
return updatedProject;
|
return updatedProject;
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ export type TUpdateProjectDTO = {
|
|||||||
name?: string;
|
name?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
autoCapitalization?: boolean;
|
autoCapitalization?: boolean;
|
||||||
|
slug?: string;
|
||||||
};
|
};
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect, useState } 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";
|
||||||
@@ -9,9 +9,7 @@ import { Button, FormControl, Input, TextArea } from "@app/components/v2";
|
|||||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||||
import { useUpdateProject } from "@app/hooks/api";
|
import { useUpdateProject } from "@app/hooks/api";
|
||||||
|
|
||||||
import { CopyButton } from "./CopyButton";
|
const baseFormSchema = z.object({
|
||||||
|
|
||||||
const formSchema = z.object({
|
|
||||||
name: z.string().min(1, "Required").max(64, "Too long, maximum length is 64 characters"),
|
name: z.string().min(1, "Required").max(64, "Too long, maximum length is 64 characters"),
|
||||||
description: z
|
description: z
|
||||||
.string()
|
.string()
|
||||||
@@ -20,33 +18,59 @@ const formSchema = z.object({
|
|||||||
.optional()
|
.optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
type FormData = z.infer<typeof formSchema>;
|
const formSchemaWithSlug = baseFormSchema.extend({
|
||||||
|
slug: z
|
||||||
|
.string()
|
||||||
|
.min(1, "Required")
|
||||||
|
.max(64, "Too long, maximum length is 64 characters")
|
||||||
|
.regex(/^[a-zA-Z0-9-]+$/, "Only letters, numbers and hyphens are allowed")
|
||||||
|
});
|
||||||
|
|
||||||
export const ProjectOverviewChangeSection = () => {
|
type BaseFormData = z.infer<typeof baseFormSchema>;
|
||||||
|
type FormDataWithSlug = z.infer<typeof formSchemaWithSlug>;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
showSlugField?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ProjectOverviewChangeSection = ({ showSlugField = false }: Props) => {
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
const [currentSlug, setCurrentSlug] = useState(currentWorkspace?.slug);
|
||||||
const { mutateAsync, isPending } = useUpdateProject();
|
const { mutateAsync, isPending } = useUpdateProject();
|
||||||
|
|
||||||
const { handleSubmit, control, reset } = useForm<FormData>({ resolver: zodResolver(formSchema) });
|
const { handleSubmit, control, reset } = useForm<BaseFormData | FormDataWithSlug>({
|
||||||
|
resolver: zodResolver(showSlugField ? formSchemaWithSlug : baseFormSchema)
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentWorkspace) {
|
if (currentWorkspace) {
|
||||||
reset({
|
reset({
|
||||||
name: currentWorkspace.name,
|
name: currentWorkspace.name,
|
||||||
description: currentWorkspace.description ?? ""
|
description: currentWorkspace.description ?? "",
|
||||||
|
...(showSlugField && { slug: currentWorkspace.slug })
|
||||||
});
|
});
|
||||||
|
setCurrentSlug(currentWorkspace.slug);
|
||||||
}
|
}
|
||||||
}, [currentWorkspace]);
|
}, [currentWorkspace, showSlugField]);
|
||||||
|
|
||||||
const onFormSubmit = async ({ name, description }: FormData) => {
|
const onFormSubmit = async (data: BaseFormData | FormDataWithSlug) => {
|
||||||
try {
|
try {
|
||||||
if (!currentWorkspace?.id) return;
|
if (!currentWorkspace?.id) return;
|
||||||
|
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
projectID: currentWorkspace.id,
|
projectID: currentWorkspace.id,
|
||||||
newProjectName: name,
|
newProjectName: data.name,
|
||||||
newProjectDescription: description
|
newProjectDescription: data.description,
|
||||||
|
...(showSlugField &&
|
||||||
|
"slug" in data && {
|
||||||
|
newSlug: data.slug !== currentWorkspace.slug ? data.slug : undefined
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (showSlugField && "slug" in data) {
|
||||||
|
setCurrentSlug(data.slug);
|
||||||
|
}
|
||||||
|
|
||||||
createNotification({
|
createNotification({
|
||||||
text: "Successfully updated project overview",
|
text: "Successfully updated project overview",
|
||||||
type: "success"
|
type: "success"
|
||||||
@@ -65,20 +89,34 @@ export const ProjectOverviewChangeSection = () => {
|
|||||||
<div className="justify-betweens flex">
|
<div className="justify-betweens flex">
|
||||||
<h2 className="mb-8 flex-1 text-xl font-semibold text-mineshaft-100">Project Overview</h2>
|
<h2 className="mb-8 flex-1 text-xl font-semibold text-mineshaft-100">Project Overview</h2>
|
||||||
<div className="space-x-2">
|
<div className="space-x-2">
|
||||||
<CopyButton
|
<Button
|
||||||
value={currentWorkspace?.slug || ""}
|
variant="outline_bg"
|
||||||
hoverText="Click to project slug"
|
size="sm"
|
||||||
notificationText="Copied project slug to clipboard"
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(currentSlug || "");
|
||||||
|
createNotification({
|
||||||
|
text: "Copied project slug to clipboard",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
title="Click to copy project slug"
|
||||||
>
|
>
|
||||||
Copy Project Slug
|
Copy Project Slug
|
||||||
</CopyButton>
|
</Button>
|
||||||
<CopyButton
|
<Button
|
||||||
value={currentWorkspace?.id || ""}
|
variant="outline_bg"
|
||||||
hoverText="Click to project ID"
|
size="sm"
|
||||||
notificationText="Copied project ID to clipboard"
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(currentWorkspace?.id || "");
|
||||||
|
createNotification({
|
||||||
|
text: "Copied project ID to clipboard",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
title="Click to copy project ID"
|
||||||
>
|
>
|
||||||
Copy Project ID
|
Copy Project ID
|
||||||
</CopyButton>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -113,6 +151,38 @@ export const ProjectOverviewChangeSection = () => {
|
|||||||
</ProjectPermissionCan>
|
</ProjectPermissionCan>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{showSlugField && (
|
||||||
|
<div className="flex w-full flex-row items-end gap-4">
|
||||||
|
<div className="w-full max-w-md">
|
||||||
|
<ProjectPermissionCan
|
||||||
|
I={ProjectPermissionActions.Edit}
|
||||||
|
a={ProjectPermissionSub.Project}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<Controller
|
||||||
|
defaultValue=""
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
label="Project slug"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
placeholder="Project slug"
|
||||||
|
{...field}
|
||||||
|
className="bg-mineshaft-800"
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
control={control}
|
||||||
|
name="slug"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="flex w-full flex-row items-end gap-4">
|
<div className="flex w-full flex-row items-end gap-4">
|
||||||
<div className="w-full max-w-md">
|
<div className="w-full max-w-md">
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
@@ -251,12 +251,13 @@ export const useUpdateProject = () => {
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation<Workspace, object, UpdateProjectDTO>({
|
return useMutation<Workspace, object, UpdateProjectDTO>({
|
||||||
mutationFn: async ({ projectID, newProjectName, newProjectDescription }) => {
|
mutationFn: async ({ projectID, newProjectName, newProjectDescription, newSlug }) => {
|
||||||
const { data } = await apiRequest.patch<{ workspace: Workspace }>(
|
const { data } = await apiRequest.patch<{ workspace: Workspace }>(
|
||||||
`/api/v1/workspace/${projectID}`,
|
`/api/v1/workspace/${projectID}`,
|
||||||
{
|
{
|
||||||
name: newProjectName,
|
name: newProjectName,
|
||||||
description: newProjectDescription
|
description: newProjectDescription,
|
||||||
|
slug: newSlug
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
return data.workspace;
|
return data.workspace;
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ export type UpdateProjectDTO = {
|
|||||||
projectID: string;
|
projectID: string;
|
||||||
newProjectName: string;
|
newProjectName: string;
|
||||||
newProjectDescription?: string;
|
newProjectDescription?: string;
|
||||||
|
newSlug?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UpdatePitVersionLimitDTO = { projectSlug: string; pitVersionLimit: number };
|
export type UpdatePitVersionLimitDTO = { projectSlug: string; pitVersionLimit: number };
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
|
import { ProjectOverviewChangeSection } from "@app/components/project/ProjectOverviewChangeSection";
|
||||||
|
|
||||||
import { AuditLogsRetentionSection } from "../AuditLogsRetentionSection";
|
import { AuditLogsRetentionSection } from "../AuditLogsRetentionSection";
|
||||||
import { DeleteProjectSection } from "../DeleteProjectSection";
|
import { DeleteProjectSection } from "../DeleteProjectSection";
|
||||||
import { ProjectOverviewChangeSection } from "../ProjectOverviewChangeSection";
|
|
||||||
|
|
||||||
export const ProjectGeneralTab = () => {
|
export const ProjectGeneralTab = () => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<ProjectOverviewChangeSection />
|
<ProjectOverviewChangeSection showSlugField />
|
||||||
<AuditLogsRetentionSection />
|
<AuditLogsRetentionSection />
|
||||||
<DeleteProjectSection />
|
<DeleteProjectSection />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
import { useCallback } from "react";
|
|
||||||
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
|
||||||
import { Button } from "@app/components/v2";
|
|
||||||
import { useToggle } from "@app/hooks";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
value: string;
|
|
||||||
hoverText: string;
|
|
||||||
notificationText: string;
|
|
||||||
children: React.ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const CopyButton = ({ value, children, hoverText, notificationText }: Props) => {
|
|
||||||
const [isProjectIdCopied, setIsProjectIdCopied] = useToggle(false);
|
|
||||||
|
|
||||||
const copyToClipboard = useCallback(() => {
|
|
||||||
if (isProjectIdCopied) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsProjectIdCopied.on();
|
|
||||||
navigator.clipboard.writeText(value);
|
|
||||||
|
|
||||||
createNotification({
|
|
||||||
text: notificationText,
|
|
||||||
type: "success"
|
|
||||||
});
|
|
||||||
|
|
||||||
const timer = setTimeout(() => setIsProjectIdCopied.off(), 2000);
|
|
||||||
|
|
||||||
// eslint-disable-next-line consistent-return
|
|
||||||
return () => clearTimeout(timer);
|
|
||||||
}, [isProjectIdCopied]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
colorSchema="secondary"
|
|
||||||
className="group relative"
|
|
||||||
leftIcon={<FontAwesomeIcon icon={isProjectIdCopied ? faCheck : faCopy} />}
|
|
||||||
onClick={copyToClipboard}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<span className="absolute -left-8 -top-20 hidden translate-y-full justify-center rounded-md bg-bunker-800 px-3 py-2 text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
|
||||||
{hoverText}
|
|
||||||
</span>
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,168 +0,0 @@
|
|||||||
import { useEffect } from "react";
|
|
||||||
import { Controller, useForm } from "react-hook-form";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
|
||||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
|
||||||
import { Button, FormControl, Input, TextArea } from "@app/components/v2";
|
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
|
||||||
import { useUpdateProject } from "@app/hooks/api";
|
|
||||||
|
|
||||||
import { CopyButton } from "./CopyButton";
|
|
||||||
|
|
||||||
const formSchema = z.object({
|
|
||||||
name: z.string().min(1, "Required").max(64, "Too long, maximum length is 64 characters"),
|
|
||||||
description: z
|
|
||||||
.string()
|
|
||||||
.trim()
|
|
||||||
.max(256, "Description too long, max length is 256 characters")
|
|
||||||
.optional()
|
|
||||||
});
|
|
||||||
|
|
||||||
type FormData = z.infer<typeof formSchema>;
|
|
||||||
|
|
||||||
export const ProjectOverviewChangeSection = () => {
|
|
||||||
const { currentWorkspace } = useWorkspace();
|
|
||||||
const { mutateAsync, isPending } = useUpdateProject();
|
|
||||||
|
|
||||||
const { handleSubmit, control, reset } = useForm<FormData>({ resolver: zodResolver(formSchema) });
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (currentWorkspace) {
|
|
||||||
reset({
|
|
||||||
name: currentWorkspace.name,
|
|
||||||
description: currentWorkspace.description ?? ""
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [currentWorkspace]);
|
|
||||||
|
|
||||||
const onFormSubmit = async ({ name, description }: FormData) => {
|
|
||||||
try {
|
|
||||||
if (!currentWorkspace?.id) return;
|
|
||||||
|
|
||||||
await mutateAsync({
|
|
||||||
projectID: currentWorkspace.id,
|
|
||||||
newProjectName: name,
|
|
||||||
newProjectDescription: description
|
|
||||||
});
|
|
||||||
|
|
||||||
createNotification({
|
|
||||||
text: "Successfully updated project overview",
|
|
||||||
type: "success"
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
createNotification({
|
|
||||||
text: "Failed to update project overview",
|
|
||||||
type: "error"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
|
||||||
<div className="justify-betweens flex">
|
|
||||||
<h2 className="mb-8 flex-1 text-xl font-semibold text-mineshaft-100">Project Overview</h2>
|
|
||||||
<div className="space-x-2">
|
|
||||||
<CopyButton
|
|
||||||
value={currentWorkspace?.slug || ""}
|
|
||||||
hoverText="Click to project slug"
|
|
||||||
notificationText="Copied project slug to clipboard"
|
|
||||||
>
|
|
||||||
Copy Project Slug
|
|
||||||
</CopyButton>
|
|
||||||
<CopyButton
|
|
||||||
value={currentWorkspace?.id || ""}
|
|
||||||
hoverText="Click to project ID"
|
|
||||||
notificationText="Copied project ID to clipboard"
|
|
||||||
>
|
|
||||||
Copy Project ID
|
|
||||||
</CopyButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<form onSubmit={handleSubmit(onFormSubmit)} className="flex w-full flex-col gap-0">
|
|
||||||
<div className="flex w-full flex-row items-end gap-4">
|
|
||||||
<div className="w-full max-w-md">
|
|
||||||
<ProjectPermissionCan
|
|
||||||
I={ProjectPermissionActions.Edit}
|
|
||||||
a={ProjectPermissionSub.Project}
|
|
||||||
>
|
|
||||||
{(isAllowed) => (
|
|
||||||
<Controller
|
|
||||||
defaultValue=""
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
label="Project name"
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
placeholder="Project name"
|
|
||||||
{...field}
|
|
||||||
className="bg-mineshaft-800"
|
|
||||||
isDisabled={!isAllowed}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
control={control}
|
|
||||||
name="name"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</ProjectPermissionCan>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex w-full flex-row items-end gap-4">
|
|
||||||
<div className="w-full max-w-md">
|
|
||||||
<ProjectPermissionCan
|
|
||||||
I={ProjectPermissionActions.Edit}
|
|
||||||
a={ProjectPermissionSub.Project}
|
|
||||||
>
|
|
||||||
{(isAllowed) => (
|
|
||||||
<Controller
|
|
||||||
defaultValue=""
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
label="Project description"
|
|
||||||
>
|
|
||||||
<TextArea
|
|
||||||
placeholder="Project description"
|
|
||||||
{...field}
|
|
||||||
rows={3}
|
|
||||||
className="thin-scrollbar max-w-md !resize-none bg-mineshaft-800"
|
|
||||||
isDisabled={!isAllowed}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
control={control}
|
|
||||||
name="description"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</ProjectPermissionCan>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<ProjectPermissionCan
|
|
||||||
I={ProjectPermissionActions.Edit}
|
|
||||||
a={ProjectPermissionSub.Project}
|
|
||||||
>
|
|
||||||
{(isAllowed) => (
|
|
||||||
<Button
|
|
||||||
colorSchema="secondary"
|
|
||||||
type="submit"
|
|
||||||
isLoading={isPending}
|
|
||||||
isDisabled={isPending || !isAllowed}
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</ProjectPermissionCan>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { ProjectOverviewChangeSection } from "./ProjectOverviewChangeSection";
|
|
||||||
@@ -1,2 +1 @@
|
|||||||
export { DeleteProjectSection } from "./DeleteProjectSection";
|
export { DeleteProjectSection } from "./DeleteProjectSection";
|
||||||
export { ProjectOverviewChangeSection } from "./ProjectOverviewChangeSection";
|
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
|
import { ProjectOverviewChangeSection } from "@app/components/project/ProjectOverviewChangeSection";
|
||||||
|
|
||||||
import { AuditLogsRetentionSection } from "../AuditLogsRetentionSection";
|
import { AuditLogsRetentionSection } from "../AuditLogsRetentionSection";
|
||||||
import { DeleteProjectSection } from "../DeleteProjectSection";
|
import { DeleteProjectSection } from "../DeleteProjectSection";
|
||||||
import { ProjectOverviewChangeSection } from "../ProjectOverviewChangeSection";
|
|
||||||
|
|
||||||
export const ProjectGeneralTab = () => {
|
export const ProjectGeneralTab = () => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<ProjectOverviewChangeSection />
|
<ProjectOverviewChangeSection showSlugField />
|
||||||
<AuditLogsRetentionSection />
|
<AuditLogsRetentionSection />
|
||||||
<DeleteProjectSection />
|
<DeleteProjectSection />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
import { useCallback } from "react";
|
|
||||||
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
|
||||||
import { Button } from "@app/components/v2";
|
|
||||||
import { useToggle } from "@app/hooks";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
value: string;
|
|
||||||
hoverText: string;
|
|
||||||
notificationText: string;
|
|
||||||
children: React.ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const CopyButton = ({ value, children, hoverText, notificationText }: Props) => {
|
|
||||||
const [isProjectIdCopied, setIsProjectIdCopied] = useToggle(false);
|
|
||||||
|
|
||||||
const copyToClipboard = useCallback(() => {
|
|
||||||
if (isProjectIdCopied) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsProjectIdCopied.on();
|
|
||||||
navigator.clipboard.writeText(value);
|
|
||||||
|
|
||||||
createNotification({
|
|
||||||
text: notificationText,
|
|
||||||
type: "success"
|
|
||||||
});
|
|
||||||
|
|
||||||
const timer = setTimeout(() => setIsProjectIdCopied.off(), 2000);
|
|
||||||
|
|
||||||
// eslint-disable-next-line consistent-return
|
|
||||||
return () => clearTimeout(timer);
|
|
||||||
}, [isProjectIdCopied]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
colorSchema="secondary"
|
|
||||||
className="group relative"
|
|
||||||
leftIcon={<FontAwesomeIcon icon={isProjectIdCopied ? faCheck : faCopy} />}
|
|
||||||
onClick={copyToClipboard}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<span className="absolute -left-8 -top-20 hidden translate-y-full justify-center rounded-md bg-bunker-800 px-3 py-2 text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
|
||||||
{hoverText}
|
|
||||||
</span>
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { ProjectOverviewChangeSection } from "./ProjectOverviewChangeSection";
|
|
||||||
@@ -1,2 +1 @@
|
|||||||
export { DeleteProjectSection } from "./DeleteProjectSection";
|
export { DeleteProjectSection } from "./DeleteProjectSection";
|
||||||
export { ProjectOverviewChangeSection } from "./ProjectOverviewChangeSection";
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { ProjectOverviewChangeSection } from "@app/components/project/ProjectOverviewChangeSection";
|
||||||
import { useWorkspace } from "@app/context";
|
import { useWorkspace } from "@app/context";
|
||||||
import { ProjectType, ProjectVersion } from "@app/hooks/api/workspace/types";
|
import { ProjectType, ProjectVersion } from "@app/hooks/api/workspace/types";
|
||||||
|
|
||||||
@@ -7,7 +8,6 @@ import { BackfillSecretReferenceSecretion } from "../BackfillSecretReferenceSect
|
|||||||
import { DeleteProjectSection } from "../DeleteProjectSection";
|
import { DeleteProjectSection } from "../DeleteProjectSection";
|
||||||
import { EnvironmentSection } from "../EnvironmentSection";
|
import { EnvironmentSection } from "../EnvironmentSection";
|
||||||
import { PointInTimeVersionLimitSection } from "../PointInTimeVersionLimitSection";
|
import { PointInTimeVersionLimitSection } from "../PointInTimeVersionLimitSection";
|
||||||
import { ProjectOverviewChangeSection } from "../ProjectOverviewChangeSection";
|
|
||||||
import { RebuildSecretIndicesSection } from "../RebuildSecretIndicesSection/RebuildSecretIndicesSection";
|
import { RebuildSecretIndicesSection } from "../RebuildSecretIndicesSection/RebuildSecretIndicesSection";
|
||||||
import { SecretTagsSection } from "../SecretTagsSection";
|
import { SecretTagsSection } from "../SecretTagsSection";
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ export const ProjectGeneralTab = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<ProjectOverviewChangeSection />
|
<ProjectOverviewChangeSection showSlugField />
|
||||||
{isSecretManager && <EnvironmentSection />}
|
{isSecretManager && <EnvironmentSection />}
|
||||||
{isSecretManager && <SecretTagsSection />}
|
{isSecretManager && <SecretTagsSection />}
|
||||||
{isSecretManager && <AutoCapitalizationSection />}
|
{isSecretManager && <AutoCapitalizationSection />}
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
import { useCallback } from "react";
|
|
||||||
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
|
||||||
import { Button } from "@app/components/v2";
|
|
||||||
import { useToggle } from "@app/hooks";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
value: string;
|
|
||||||
hoverText: string;
|
|
||||||
notificationText: string;
|
|
||||||
children: React.ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const CopyButton = ({ value, children, hoverText, notificationText }: Props) => {
|
|
||||||
const [isProjectIdCopied, setIsProjectIdCopied] = useToggle(false);
|
|
||||||
|
|
||||||
const copyToClipboard = useCallback(() => {
|
|
||||||
if (isProjectIdCopied) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsProjectIdCopied.on();
|
|
||||||
navigator.clipboard.writeText(value);
|
|
||||||
|
|
||||||
createNotification({
|
|
||||||
text: notificationText,
|
|
||||||
type: "success"
|
|
||||||
});
|
|
||||||
|
|
||||||
const timer = setTimeout(() => setIsProjectIdCopied.off(), 2000);
|
|
||||||
|
|
||||||
// eslint-disable-next-line consistent-return
|
|
||||||
return () => clearTimeout(timer);
|
|
||||||
}, [isProjectIdCopied]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
colorSchema="secondary"
|
|
||||||
className="group relative"
|
|
||||||
leftIcon={<FontAwesomeIcon icon={isProjectIdCopied ? faCheck : faCopy} />}
|
|
||||||
onClick={copyToClipboard}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<span className="absolute -left-8 -top-20 hidden translate-y-full justify-center rounded-md bg-bunker-800 px-3 py-2 text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
|
||||||
{hoverText}
|
|
||||||
</span>
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,168 +0,0 @@
|
|||||||
import { useEffect } from "react";
|
|
||||||
import { Controller, useForm } from "react-hook-form";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
|
||||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
|
||||||
import { Button, FormControl, Input, TextArea } from "@app/components/v2";
|
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
|
||||||
import { useUpdateProject } from "@app/hooks/api";
|
|
||||||
|
|
||||||
import { CopyButton } from "./CopyButton";
|
|
||||||
|
|
||||||
const formSchema = z.object({
|
|
||||||
name: z.string().min(1, "Required").max(64, "Too long, maximum length is 64 characters"),
|
|
||||||
description: z
|
|
||||||
.string()
|
|
||||||
.trim()
|
|
||||||
.max(256, "Description too long, max length is 256 characters")
|
|
||||||
.optional()
|
|
||||||
});
|
|
||||||
|
|
||||||
type FormData = z.infer<typeof formSchema>;
|
|
||||||
|
|
||||||
export const ProjectOverviewChangeSection = () => {
|
|
||||||
const { currentWorkspace } = useWorkspace();
|
|
||||||
const { mutateAsync, isPending } = useUpdateProject();
|
|
||||||
|
|
||||||
const { handleSubmit, control, reset } = useForm<FormData>({ resolver: zodResolver(formSchema) });
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (currentWorkspace) {
|
|
||||||
reset({
|
|
||||||
name: currentWorkspace.name,
|
|
||||||
description: currentWorkspace.description ?? ""
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [currentWorkspace]);
|
|
||||||
|
|
||||||
const onFormSubmit = async ({ name, description }: FormData) => {
|
|
||||||
try {
|
|
||||||
if (!currentWorkspace?.id) return;
|
|
||||||
|
|
||||||
await mutateAsync({
|
|
||||||
projectID: currentWorkspace.id,
|
|
||||||
newProjectName: name,
|
|
||||||
newProjectDescription: description
|
|
||||||
});
|
|
||||||
|
|
||||||
createNotification({
|
|
||||||
text: "Successfully updated project overview",
|
|
||||||
type: "success"
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
createNotification({
|
|
||||||
text: "Failed to update project overview",
|
|
||||||
type: "error"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
|
||||||
<div className="justify-betweens flex">
|
|
||||||
<h2 className="mb-8 flex-1 text-xl font-semibold text-mineshaft-100">Project Overview</h2>
|
|
||||||
<div className="space-x-2">
|
|
||||||
<CopyButton
|
|
||||||
value={currentWorkspace?.slug || ""}
|
|
||||||
hoverText="Click to project slug"
|
|
||||||
notificationText="Copied project slug to clipboard"
|
|
||||||
>
|
|
||||||
Copy Project Slug
|
|
||||||
</CopyButton>
|
|
||||||
<CopyButton
|
|
||||||
value={currentWorkspace?.id || ""}
|
|
||||||
hoverText="Click to project ID"
|
|
||||||
notificationText="Copied project ID to clipboard"
|
|
||||||
>
|
|
||||||
Copy Project ID
|
|
||||||
</CopyButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<form onSubmit={handleSubmit(onFormSubmit)} className="flex w-full flex-col gap-0">
|
|
||||||
<div className="flex w-full flex-row items-end gap-4">
|
|
||||||
<div className="w-full max-w-md">
|
|
||||||
<ProjectPermissionCan
|
|
||||||
I={ProjectPermissionActions.Edit}
|
|
||||||
a={ProjectPermissionSub.Project}
|
|
||||||
>
|
|
||||||
{(isAllowed) => (
|
|
||||||
<Controller
|
|
||||||
defaultValue=""
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
label="Project name"
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
placeholder="Project name"
|
|
||||||
{...field}
|
|
||||||
className="bg-mineshaft-800"
|
|
||||||
isDisabled={!isAllowed}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
control={control}
|
|
||||||
name="name"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</ProjectPermissionCan>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex w-full flex-row items-end gap-4">
|
|
||||||
<div className="w-full max-w-md">
|
|
||||||
<ProjectPermissionCan
|
|
||||||
I={ProjectPermissionActions.Edit}
|
|
||||||
a={ProjectPermissionSub.Project}
|
|
||||||
>
|
|
||||||
{(isAllowed) => (
|
|
||||||
<Controller
|
|
||||||
defaultValue=""
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
label="Project description"
|
|
||||||
>
|
|
||||||
<TextArea
|
|
||||||
placeholder="Project description"
|
|
||||||
{...field}
|
|
||||||
rows={3}
|
|
||||||
className="thin-scrollbar max-w-md !resize-none bg-mineshaft-800"
|
|
||||||
isDisabled={!isAllowed}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
control={control}
|
|
||||||
name="description"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</ProjectPermissionCan>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<ProjectPermissionCan
|
|
||||||
I={ProjectPermissionActions.Edit}
|
|
||||||
a={ProjectPermissionSub.Project}
|
|
||||||
>
|
|
||||||
{(isAllowed) => (
|
|
||||||
<Button
|
|
||||||
colorSchema="secondary"
|
|
||||||
type="submit"
|
|
||||||
isLoading={isPending}
|
|
||||||
isDisabled={isPending || !isAllowed}
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</ProjectPermissionCan>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { ProjectOverviewChangeSection } from "./ProjectOverviewChangeSection";
|
|
||||||
@@ -2,5 +2,4 @@ export { AutoCapitalizationSection } from "./AutoCapitalizationSection";
|
|||||||
export { BackfillSecretReferenceSecretion } from "./BackfillSecretReferenceSection";
|
export { BackfillSecretReferenceSecretion } from "./BackfillSecretReferenceSection";
|
||||||
export { DeleteProjectSection } from "./DeleteProjectSection";
|
export { DeleteProjectSection } from "./DeleteProjectSection";
|
||||||
export { EnvironmentSection } from "./EnvironmentSection";
|
export { EnvironmentSection } from "./EnvironmentSection";
|
||||||
export { ProjectOverviewChangeSection } from "./ProjectOverviewChangeSection";
|
|
||||||
export { SecretTagsSection } from "./SecretTagsSection";
|
export { SecretTagsSection } from "./SecretTagsSection";
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
|
import { ProjectOverviewChangeSection } from "@app/components/project/ProjectOverviewChangeSection";
|
||||||
|
|
||||||
import { AuditLogsRetentionSection } from "../AuditLogsRetentionSection";
|
import { AuditLogsRetentionSection } from "../AuditLogsRetentionSection";
|
||||||
import { DeleteProjectSection } from "../DeleteProjectSection";
|
import { DeleteProjectSection } from "../DeleteProjectSection";
|
||||||
import { ProjectOverviewChangeSection } from "../ProjectOverviewChangeSection";
|
|
||||||
|
|
||||||
export const ProjectGeneralTab = () => {
|
export const ProjectGeneralTab = () => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<ProjectOverviewChangeSection />
|
<ProjectOverviewChangeSection showSlugField />
|
||||||
<AuditLogsRetentionSection />
|
<AuditLogsRetentionSection />
|
||||||
<DeleteProjectSection />
|
<DeleteProjectSection />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
import { useCallback } from "react";
|
|
||||||
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
|
||||||
import { Button } from "@app/components/v2";
|
|
||||||
import { useToggle } from "@app/hooks";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
value: string;
|
|
||||||
hoverText: string;
|
|
||||||
notificationText: string;
|
|
||||||
children: React.ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const CopyButton = ({ value, children, hoverText, notificationText }: Props) => {
|
|
||||||
const [isProjectIdCopied, setIsProjectIdCopied] = useToggle(false);
|
|
||||||
|
|
||||||
const copyToClipboard = useCallback(() => {
|
|
||||||
if (isProjectIdCopied) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsProjectIdCopied.on();
|
|
||||||
navigator.clipboard.writeText(value);
|
|
||||||
|
|
||||||
createNotification({
|
|
||||||
text: notificationText,
|
|
||||||
type: "success"
|
|
||||||
});
|
|
||||||
|
|
||||||
const timer = setTimeout(() => setIsProjectIdCopied.off(), 2000);
|
|
||||||
|
|
||||||
// eslint-disable-next-line consistent-return
|
|
||||||
return () => clearTimeout(timer);
|
|
||||||
}, [isProjectIdCopied]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
colorSchema="secondary"
|
|
||||||
className="group relative"
|
|
||||||
leftIcon={<FontAwesomeIcon icon={isProjectIdCopied ? faCheck : faCopy} />}
|
|
||||||
onClick={copyToClipboard}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<span className="absolute -left-8 -top-20 hidden translate-y-full justify-center rounded-md bg-bunker-800 px-3 py-2 text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
|
||||||
{hoverText}
|
|
||||||
</span>
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,168 +0,0 @@
|
|||||||
import { useEffect } from "react";
|
|
||||||
import { Controller, useForm } from "react-hook-form";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
|
||||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
|
||||||
import { Button, FormControl, Input, TextArea } from "@app/components/v2";
|
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
|
||||||
import { useUpdateProject } from "@app/hooks/api";
|
|
||||||
|
|
||||||
import { CopyButton } from "./CopyButton";
|
|
||||||
|
|
||||||
const formSchema = z.object({
|
|
||||||
name: z.string().min(1, "Required").max(64, "Too long, maximum length is 64 characters"),
|
|
||||||
description: z
|
|
||||||
.string()
|
|
||||||
.trim()
|
|
||||||
.max(256, "Description too long, max length is 256 characters")
|
|
||||||
.optional()
|
|
||||||
});
|
|
||||||
|
|
||||||
type FormData = z.infer<typeof formSchema>;
|
|
||||||
|
|
||||||
export const ProjectOverviewChangeSection = () => {
|
|
||||||
const { currentWorkspace } = useWorkspace();
|
|
||||||
const { mutateAsync, isPending } = useUpdateProject();
|
|
||||||
|
|
||||||
const { handleSubmit, control, reset } = useForm<FormData>({ resolver: zodResolver(formSchema) });
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (currentWorkspace) {
|
|
||||||
reset({
|
|
||||||
name: currentWorkspace.name,
|
|
||||||
description: currentWorkspace.description ?? ""
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [currentWorkspace]);
|
|
||||||
|
|
||||||
const onFormSubmit = async ({ name, description }: FormData) => {
|
|
||||||
try {
|
|
||||||
if (!currentWorkspace?.id) return;
|
|
||||||
|
|
||||||
await mutateAsync({
|
|
||||||
projectID: currentWorkspace.id,
|
|
||||||
newProjectName: name,
|
|
||||||
newProjectDescription: description
|
|
||||||
});
|
|
||||||
|
|
||||||
createNotification({
|
|
||||||
text: "Successfully updated project overview",
|
|
||||||
type: "success"
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
createNotification({
|
|
||||||
text: "Failed to update project overview",
|
|
||||||
type: "error"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
|
||||||
<div className="justify-betweens flex">
|
|
||||||
<h2 className="mb-8 flex-1 text-xl font-semibold text-mineshaft-100">Project Overview</h2>
|
|
||||||
<div className="space-x-2">
|
|
||||||
<CopyButton
|
|
||||||
value={currentWorkspace?.slug || ""}
|
|
||||||
hoverText="Click to project slug"
|
|
||||||
notificationText="Copied project slug to clipboard"
|
|
||||||
>
|
|
||||||
Copy Project Slug
|
|
||||||
</CopyButton>
|
|
||||||
<CopyButton
|
|
||||||
value={currentWorkspace?.id || ""}
|
|
||||||
hoverText="Click to project ID"
|
|
||||||
notificationText="Copied project ID to clipboard"
|
|
||||||
>
|
|
||||||
Copy Project ID
|
|
||||||
</CopyButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<form onSubmit={handleSubmit(onFormSubmit)} className="flex w-full flex-col gap-0">
|
|
||||||
<div className="flex w-full flex-row items-end gap-4">
|
|
||||||
<div className="w-full max-w-md">
|
|
||||||
<ProjectPermissionCan
|
|
||||||
I={ProjectPermissionActions.Edit}
|
|
||||||
a={ProjectPermissionSub.Project}
|
|
||||||
>
|
|
||||||
{(isAllowed) => (
|
|
||||||
<Controller
|
|
||||||
defaultValue=""
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
label="Project name"
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
placeholder="Project name"
|
|
||||||
{...field}
|
|
||||||
className="bg-mineshaft-800"
|
|
||||||
isDisabled={!isAllowed}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
control={control}
|
|
||||||
name="name"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</ProjectPermissionCan>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex w-full flex-row items-end gap-4">
|
|
||||||
<div className="w-full max-w-md">
|
|
||||||
<ProjectPermissionCan
|
|
||||||
I={ProjectPermissionActions.Edit}
|
|
||||||
a={ProjectPermissionSub.Project}
|
|
||||||
>
|
|
||||||
{(isAllowed) => (
|
|
||||||
<Controller
|
|
||||||
defaultValue=""
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
label="Project description"
|
|
||||||
>
|
|
||||||
<TextArea
|
|
||||||
placeholder="Project description"
|
|
||||||
{...field}
|
|
||||||
rows={3}
|
|
||||||
className="thin-scrollbar max-w-md !resize-none bg-mineshaft-800"
|
|
||||||
isDisabled={!isAllowed}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
control={control}
|
|
||||||
name="description"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</ProjectPermissionCan>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<ProjectPermissionCan
|
|
||||||
I={ProjectPermissionActions.Edit}
|
|
||||||
a={ProjectPermissionSub.Project}
|
|
||||||
>
|
|
||||||
{(isAllowed) => (
|
|
||||||
<Button
|
|
||||||
colorSchema="secondary"
|
|
||||||
type="submit"
|
|
||||||
isLoading={isPending}
|
|
||||||
isDisabled={isPending || !isAllowed}
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</ProjectPermissionCan>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { ProjectOverviewChangeSection } from "./ProjectOverviewChangeSection";
|
|
||||||
@@ -1,2 +1 @@
|
|||||||
export { DeleteProjectSection } from "./DeleteProjectSection";
|
export { DeleteProjectSection } from "./DeleteProjectSection";
|
||||||
export { ProjectOverviewChangeSection } from "./ProjectOverviewChangeSection";
|
|
||||||
|
|||||||
Reference in New Issue
Block a user