mirror of
https://github.com/makeplane/plane.git
synced 2026-07-14 06:25:58 +02:00
[OAUTH] chore: fix dark mode UI issues #2955
This commit is contained in:
@@ -8,6 +8,7 @@ import { Button, Tooltip, TOAST_TYPE, setToast, ModalCore } from "@plane/ui";
|
||||
import { csvDownload } from "@/helpers/download.helper";
|
||||
import { copyTextToClipboard } from "@/helpers/string.helper";
|
||||
// hooks
|
||||
import { useUserProfile } from "@/hooks/store";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// types
|
||||
|
||||
@@ -22,6 +23,7 @@ export const GeneratedCredentialsModal: React.FC<Props> = (props) => {
|
||||
const { isOpen, clientSecret, clientId, handleClose } = props;
|
||||
const { isMobile } = usePlatformOS();
|
||||
const { t } = useTranslation();
|
||||
const { data: userProfile } = useUserProfile();
|
||||
|
||||
const copyContent = (value: string, label: string) => {
|
||||
copyTextToClipboard(value).then(() =>
|
||||
@@ -42,6 +44,10 @@ export const GeneratedCredentialsModal: React.FC<Props> = (props) => {
|
||||
csvDownload(csvData, `secret-key-${Date.now()}`);
|
||||
};
|
||||
|
||||
// derived values
|
||||
const isDarkMode = userProfile?.theme.theme === "dark";
|
||||
const textColor = isDarkMode ? "text-custom-text-100" : "text-custom-text-400";
|
||||
const bgColor = isDarkMode ? "bg-custom-background-100" : "bg-gray-100";
|
||||
|
||||
return (
|
||||
<ModalCore isOpen={isOpen} handleClose={handleClose}>
|
||||
@@ -56,29 +62,29 @@ export const GeneratedCredentialsModal: React.FC<Props> = (props) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 space-y-2 flex flex-col bg-gray-100 rounded-md p-4">
|
||||
<div className={`mt-4 space-y-2 flex flex-col rounded-md p-4 ${bgColor}`}>
|
||||
<div className="font-medium">{t("workspace_settings.settings.applications.client_id_and_secret")}</div>
|
||||
<div className="text-sm text-custom-text-400">{t("workspace_settings.settings.applications.client_id_and_secret_description")}
|
||||
<br />
|
||||
{t("workspace_settings.settings.applications.client_id_and_secret_download")}
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm text-custom-text-400">{t("workspace_settings.settings.applications.client_id")}</p>
|
||||
<p className={`text-sm ${textColor}`}>{t("workspace_settings.settings.applications.client_id")}</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copyContent(clientId, "clientId")}
|
||||
className="mt-4 bg-white flex truncate w-full items-center justify-between rounded-md border-[0.5px] border-custom-border-200 px-3 py-3 text-sm font-medium outline-none"
|
||||
className={`mt-4 flex truncate w-full items-center justify-between rounded-md border-[0.5px] border-custom-border-200 px-3 py-3 text-sm font-medium outline-none ${bgColor}`}
|
||||
>
|
||||
<span className="truncate pr-2">{clientId}</span>
|
||||
<span className={`truncate pr-2 ${textColor}`}>{clientId}</span>
|
||||
<Tooltip tooltipContent="Copy client id" isMobile={isMobile}>
|
||||
<Copy className="h-4 w-4 text-custom-text-400 flex-shrink-0" />
|
||||
</Tooltip>
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm text-custom-text-400">{t("workspace_settings.settings.applications.client_secret")}</p>
|
||||
<div className="flex items-center justify-between mt-4 bg-white flex truncate w-full items-center justify-between rounded-md border-[0.5px] border-custom-border-200 px-3 py-2 text-sm font-medium outline-none">
|
||||
<span className="truncate pr-2">{clientSecret}</span>
|
||||
<p className={`text-sm ${textColor}`}>{t("workspace_settings.settings.applications.client_secret")}</p>
|
||||
<div className={`flex items-center justify-between mt-4 flex truncate w-full items-center justify-between rounded-md border-[0.5px] border-custom-border-200 px-3 py-2 text-sm font-medium outline-none ${bgColor}`}>
|
||||
<span className={`truncate pr-2 ${textColor}`}>{clientSecret}</span>
|
||||
<div className="flex items-center space-x-2">
|
||||
<button
|
||||
type="button"
|
||||
@@ -91,7 +97,7 @@ export const GeneratedCredentialsModal: React.FC<Props> = (props) => {
|
||||
<button
|
||||
type="button"
|
||||
onClick={downloadSecretKey}
|
||||
className="bg-white flex truncate rounded-md border-[0.5px] border-custom-border-200 px-2 py-1 text-sm font-medium outline-none"
|
||||
className={`flex truncate rounded-md border-[0.5px] border-custom-border-200 px-2 py-1 text-sm font-medium outline-none ${bgColor} ${textColor}`}
|
||||
>
|
||||
{t("workspace_settings.settings.applications.export_as_csv")}
|
||||
</button>
|
||||
|
||||
@@ -11,7 +11,7 @@ import { EFileAssetType } from "@plane/types/src/enums";
|
||||
import { Button, Input, setToast, TOAST_TYPE } from "@plane/ui";
|
||||
import { RichTextEditor } from "@/components/editor";
|
||||
import { getFileURL } from "@/helpers/file.helper";
|
||||
import { useWorkspace, useEditorAsset } from "@/hooks/store";
|
||||
import { useWorkspace, useEditorAsset, useUserProfile } from "@/hooks/store";
|
||||
import { AppImageUploadModal } from "@/plane-web/components/common/modal";
|
||||
import { GeneratedCredentialsModal, RegenerateClientSecret } from "@/plane-web/components/marketplace/applications";
|
||||
import { useApplications } from "@/plane-web/hooks/store";
|
||||
@@ -19,8 +19,8 @@ import { WorkspaceService } from "@/plane-web/services/workspace.service";
|
||||
|
||||
|
||||
type Props = {
|
||||
formData?: Partial<TUserApplication>;
|
||||
handleFormSubmit: (data: Partial<TUserApplication>) => Promise<Partial<TUserApplication> | undefined>;
|
||||
formData?: Partial<TUserApplication>;
|
||||
handleFormSubmit: (data: Partial<TUserApplication>) => Promise<Partial<TUserApplication> | undefined>;
|
||||
}
|
||||
|
||||
const redirectURIsRegex = /^(https?:\/\/(?:www\.)?[a-zA-Z0-9-]+(?:\.[a-zA-Z]{2,})+(?:\/[^\s,]*)?)(?:\s+(?:https?:\/\/(?:www\.)?[a-zA-Z0-9-]+(?:\.[a-zA-Z]{2,})+(?:\/[^\s,]*)?))*$/;
|
||||
@@ -28,341 +28,344 @@ const webhookUrlRegex = /^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a
|
||||
const allowedOriginsRegex = /^(?:(?:https?:\/\/)?(?:[\w-]+(?:\.[\w-]+)+))(?:\s+(?:(?:https?:\/\/)?(?:[\w-]+(?:\.[\w-]+)+)))*$/;
|
||||
|
||||
const defaultFormData: Partial<TUserApplication> = {
|
||||
id: undefined,
|
||||
name: "",
|
||||
short_description: "",
|
||||
description_html: "",
|
||||
slug: "",
|
||||
company_name: "",
|
||||
webhook_url: "",
|
||||
redirect_uris: "",
|
||||
allowed_origins: "",
|
||||
logo_url: ""
|
||||
id: undefined,
|
||||
name: "",
|
||||
short_description: "",
|
||||
description_html: "",
|
||||
slug: "",
|
||||
company_name: "",
|
||||
webhook_url: "",
|
||||
redirect_uris: "",
|
||||
allowed_origins: "",
|
||||
logo_url: ""
|
||||
}
|
||||
|
||||
export const CreateUpdateApplication: React.FC<Props> = observer((props) => {
|
||||
const { formData, handleFormSubmit } = props;
|
||||
const { formData, handleFormSubmit } = props;
|
||||
|
||||
const {
|
||||
watch,
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
setValue,
|
||||
setError,
|
||||
control
|
||||
} = useForm<Partial<TUserApplication>>({
|
||||
defaultValues: formData || defaultFormData,
|
||||
});
|
||||
const {
|
||||
watch,
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
setValue,
|
||||
setError,
|
||||
control
|
||||
} = useForm<Partial<TUserApplication>>({
|
||||
defaultValues: formData || defaultFormData,
|
||||
});
|
||||
|
||||
// state
|
||||
const [isImageModalOpen, setIsImageModalOpen] = useState<boolean>(false);
|
||||
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
|
||||
const [isCredentialsModalOpen, setIsCredentialsModalOpen] = useState<boolean>(false);
|
||||
const [clientId, setClientId] = useState<string | undefined>(undefined);
|
||||
const [clientSecret, setClientSecret] = useState<string | undefined>(undefined);
|
||||
// hooks
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { uploadEditorAsset } = useEditorAsset();
|
||||
const { checkApplicationSlug } = useApplications();
|
||||
// derived values
|
||||
const workspaceSlug = currentWorkspace?.slug;
|
||||
const workspaceId = currentWorkspace?.id;
|
||||
// state
|
||||
const [isImageModalOpen, setIsImageModalOpen] = useState<boolean>(false);
|
||||
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
|
||||
const [isCredentialsModalOpen, setIsCredentialsModalOpen] = useState<boolean>(false);
|
||||
const [clientId, setClientId] = useState<string | undefined>(undefined);
|
||||
const [clientSecret, setClientSecret] = useState<string | undefined>(undefined);
|
||||
// hooks
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { uploadEditorAsset } = useEditorAsset();
|
||||
const { checkApplicationSlug } = useApplications();
|
||||
const { data: userProfile } = useUserProfile();
|
||||
|
||||
const handleTextChange = (key: keyof Partial<TUserApplication>, value: string) => {
|
||||
if (key === "name" && !watch("id")) {
|
||||
setValue("slug", value.toLowerCase().replace(/ /g, "-"), { shouldValidate: true });
|
||||
}
|
||||
setValue(key, value, { shouldValidate: true });
|
||||
// derived values
|
||||
const workspaceSlug = currentWorkspace?.slug;
|
||||
const workspaceId = currentWorkspace?.id;
|
||||
const isDarkMode = userProfile?.theme.theme === "dark";
|
||||
const bgColor = isDarkMode ? "bg-custom-background-100" : "bg-gray-100";
|
||||
|
||||
const handleTextChange = (key: keyof Partial<TUserApplication>, value: string) => {
|
||||
if (key === "name" && !watch("id")) {
|
||||
setValue("slug", value.toLowerCase().replace(/ /g, "-"), { shouldValidate: true });
|
||||
}
|
||||
setValue(key, value, { shouldValidate: true });
|
||||
}
|
||||
|
||||
const isSlugAvailable = async (slug: string): Promise<boolean> => {
|
||||
try {
|
||||
await checkApplicationSlug(slug);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.log("Error in validating application slug:", error);
|
||||
setError("slug", { message: t("workspace_settings.settings.applications.slug_already_exists") });
|
||||
return false;
|
||||
}
|
||||
const isSlugAvailable = async (slug: string): Promise<boolean> => {
|
||||
try {
|
||||
await checkApplicationSlug(slug);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.log("Error in validating application slug:", error);
|
||||
setError("slug", { message: t("workspace_settings.settings.applications.slug_already_exists") });
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const handleAppFormSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setIsSubmitting(true);
|
||||
handleSubmit(async (data) => {
|
||||
try {
|
||||
// validate slug
|
||||
if (!data.slug) return;
|
||||
if (!data.id) {
|
||||
if (!isSlugAvailable(data.slug)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const handleAppFormSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setIsSubmitting(true);
|
||||
handleSubmit(async (data) => {
|
||||
try {
|
||||
// validate slug
|
||||
if (!data.slug) return;
|
||||
if (!data.id) {
|
||||
if (!isSlugAvailable(data.slug)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// submit form
|
||||
const response = await handleFormSubmit(data);
|
||||
// submit form
|
||||
const response = await handleFormSubmit(data);
|
||||
|
||||
// set credentials modal open for new application
|
||||
if (!data.id && response) {
|
||||
setClientId(response.client_id);
|
||||
setClientSecret(response.client_secret);
|
||||
setIsCredentialsModalOpen(true);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error in submitting application form:", error);
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: t("common.error"),
|
||||
message: t("workspace_settings.settings.applications.failed_to_create_application"),
|
||||
});
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
})();
|
||||
// set credentials modal open for new application
|
||||
if (!data.id && response) {
|
||||
setClientId(response.client_id);
|
||||
setClientSecret(response.client_secret);
|
||||
setIsCredentialsModalOpen(true);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error in submitting application form:", error);
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: t("common.error"),
|
||||
message: t("workspace_settings.settings.applications.failed_to_create_application"),
|
||||
});
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
}
|
||||
})();
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
|
||||
const handleRegenerateSuccess = (data: Partial<TUserApplication>) => {
|
||||
setClientId(data.client_id);
|
||||
setClientSecret(data.client_secret);
|
||||
setIsCredentialsModalOpen(true);
|
||||
}
|
||||
const handleRegenerateSuccess = (data: Partial<TUserApplication>) => {
|
||||
setClientId(data.client_id);
|
||||
setClientSecret(data.client_secret);
|
||||
setIsCredentialsModalOpen(true);
|
||||
}
|
||||
|
||||
const handleCredentialsModalClose = () => {
|
||||
router.push(`/${workspaceSlug}/settings/applications`);
|
||||
setClientId(undefined);
|
||||
setClientSecret(undefined);
|
||||
}
|
||||
const handleCredentialsModalClose = () => {
|
||||
router.push(`/${workspaceSlug}/settings/applications`);
|
||||
setClientId(undefined);
|
||||
setClientSecret(undefined);
|
||||
}
|
||||
|
||||
|
||||
if (!workspaceSlug || !workspaceId) return null;
|
||||
if (!workspaceSlug || !workspaceId) return null;
|
||||
|
||||
const workspaceService = new WorkspaceService();
|
||||
const workspaceService = new WorkspaceService();
|
||||
|
||||
|
||||
return (
|
||||
<form onSubmit={handleAppFormSubmit}>
|
||||
<div className="space-y-4 py-5">
|
||||
<h3 className="text-xl font-medium text-custom-text-200">{!watch("id") ? "Let's get all the necessary info" : "Edit your app's details"}</h3>
|
||||
<AppImageUploadModal
|
||||
isOpen={isImageModalOpen}
|
||||
onClose={() => setIsImageModalOpen(false)}
|
||||
onSuccess={(url) => setValue("logo_url", getFileURL(url) ?? "")}
|
||||
initialValue={formData?.logo_url ? getFileURL(formData?.logo_url) ?? null : null}
|
||||
handleRemove={async () => setValue("logo_url", undefined)}
|
||||
entityType={EFileAssetType.OAUTH_APP_LOGO}
|
||||
/>
|
||||
<div className="space-y-5 flew-grow w-full">
|
||||
<div className="space-y-2">
|
||||
return (
|
||||
<form onSubmit={handleAppFormSubmit}>
|
||||
<div className="space-y-4 py-5">
|
||||
<h3 className="text-xl font-medium text-custom-text-200">{!watch("id") ? "Let's get all the necessary info" : "Edit your app's details"}</h3>
|
||||
<AppImageUploadModal
|
||||
isOpen={isImageModalOpen}
|
||||
onClose={() => setIsImageModalOpen(false)}
|
||||
onSuccess={(url) => setValue("logo_url", getFileURL(url) ?? "")}
|
||||
initialValue={formData?.logo_url ? getFileURL(formData?.logo_url) ?? null : null}
|
||||
handleRemove={async () => setValue("logo_url", undefined)}
|
||||
entityType={EFileAssetType.OAUTH_APP_LOGO}
|
||||
/>
|
||||
<div className="space-y-5 flew-grow w-full">
|
||||
<div className="space-y-2">
|
||||
|
||||
{
|
||||
watch("logo_url")
|
||||
? <img loading="lazy" src={getFileURL(watch("logo_url") ?? "")} alt="Logo" className="w-20 h-20 object-cover" />
|
||||
:
|
||||
<div className="flex items-center justify-center w-20 gap-2 p-5 bg-gray-200 rounded">
|
||||
<Camera className="w-5 h-5 text-custom-text-400" />
|
||||
</div>
|
||||
}
|
||||
<Button onClick={() => setIsImageModalOpen(true)} variant="outline-primary" className="text-sm">
|
||||
{t("workspace_settings.settings.applications.upload_logo")}
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<Input
|
||||
id="name"
|
||||
type="text"
|
||||
placeholder={t("workspace_settings.settings.applications.app_name_title")}
|
||||
className="w-full resize-none text-sm"
|
||||
hasError={Boolean(errors.name)}
|
||||
tabIndex={1}
|
||||
{...register("name", { required: t("workspace_settings.settings.applications.app_name_error") })}
|
||||
onChange={(e) => handleTextChange("name", e.target.value)}
|
||||
/>
|
||||
{errors.name && <p className="text-red-500 text-xs">{errors.name.message}</p>}
|
||||
</div>
|
||||
<div>
|
||||
<Input
|
||||
id="short_description"
|
||||
type="text"
|
||||
placeholder={t("workspace_settings.settings.applications.app_short_description_title")}
|
||||
className="w-full resize-none text-sm"
|
||||
hasError={Boolean(errors.short_description)}
|
||||
tabIndex={1}
|
||||
{...register("short_description", { required: t("workspace_settings.settings.applications.app_short_description_error") })}
|
||||
onChange={(e) => handleTextChange("short_description", e.target.value)}
|
||||
/>
|
||||
{errors.short_description && <p className="text-red-500 text-xs">{errors.short_description.message}</p>}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Controller
|
||||
name="description_html"
|
||||
control={control}
|
||||
rules={{ required: t("workspace_settings.settings.applications.app_description_error") }}
|
||||
render={({ field: { onChange } }) =>
|
||||
<RichTextEditor
|
||||
id={workspaceSlug.toString()}
|
||||
initialValue={
|
||||
!watch("description_html") ? "<p></p>" : watch("description_html") || "<p></p>"
|
||||
}
|
||||
workspaceSlug={workspaceSlug}
|
||||
workspaceId={workspaceId}
|
||||
searchMentionCallback={async (payload) =>
|
||||
await workspaceService.searchEntity(workspaceSlug?.toString() ?? "", {
|
||||
...payload,
|
||||
})
|
||||
}
|
||||
dragDropEnabled={false}
|
||||
onChange={(_description: object, description_html: string) => {
|
||||
onChange(description_html);
|
||||
}}
|
||||
placeholder={t("workspace_settings.settings.applications.app_description_title")}
|
||||
editorClassName="text-xs"
|
||||
containerClassName="resize-none min-h-24 text-xs border-[0.5px] border-custom-border-200 rounded-md px-3 py-2"
|
||||
tabIndex={2}
|
||||
uploadFile={async (blockId, file) => {
|
||||
try {
|
||||
const { asset_id } = await uploadEditorAsset({
|
||||
blockId,
|
||||
data: {
|
||||
entity_identifier: workspaceId,
|
||||
entity_type: EFileAssetType.OAUTH_APP_DESCRIPTION,
|
||||
},
|
||||
file,
|
||||
workspaceSlug,
|
||||
});
|
||||
return asset_id;
|
||||
} catch (error) {
|
||||
console.log("Error in uploading application asset:", error);
|
||||
throw new Error("Asset upload failed. Please try again later.");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
}
|
||||
/>
|
||||
{errors.description_html && <p className="text-red-500 text-xs">{errors.description_html.message}</p>}
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-custom-text-300">{t("workspace_settings.settings.applications.app_slug_title")}</div>
|
||||
<Input
|
||||
id="slug"
|
||||
type="text"
|
||||
disabled={!!watch("id")}
|
||||
className="w-full resize-none text-sm"
|
||||
hasError={Boolean(errors.slug)}
|
||||
tabIndex={1}
|
||||
{...register("slug", { required: t("workspace_settings.settings.applications.app_slug_error") })}
|
||||
onChange={(e) => handleTextChange("slug", e.target.value)}
|
||||
/>
|
||||
{errors.slug && <p className="text-red-500 text-xs">{errors.slug.message}</p>}
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-custom-text-300">{t("workspace_settings.settings.applications.app_maker_title")}</div>
|
||||
<Input
|
||||
id="company_name"
|
||||
type="text"
|
||||
placeholder={t("workspace_settings.settings.applications.app_maker_title")}
|
||||
className="w-full resize-none text-sm"
|
||||
hasError={Boolean(errors.company_name)}
|
||||
tabIndex={1}
|
||||
{...register("company_name", { required: t("workspace_settings.settings.applications.app_maker_error") })}
|
||||
onChange={(e) => handleTextChange("company_name", e.target.value)}
|
||||
/>
|
||||
{errors.company_name && <p className="text-red-500 text-xs">{errors.company_name.message}</p>}
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-custom-text-300">{t("workspace_settings.settings.applications.webhook_url_title")}</div>
|
||||
<Input
|
||||
id="webhook_url"
|
||||
type="text"
|
||||
className="w-full resize-none text-sm"
|
||||
hasError={Boolean(errors.webhook_url)}
|
||||
tabIndex={1}
|
||||
{...register("webhook_url", {
|
||||
required: t("workspace_settings.settings.applications.webhook_url_error"), pattern: {
|
||||
value: webhookUrlRegex,
|
||||
message: t("workspace_settings.settings.applications.invalid_webhook_url_error")
|
||||
}
|
||||
})}
|
||||
onChange={(e) => handleTextChange("webhook_url", e.target.value)}
|
||||
/>
|
||||
{errors.webhook_url && <p className="text-red-500 text-xs">{errors.webhook_url.message}</p>}
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-custom-text-300">{t("workspace_settings.settings.applications.redirect_uris_title")}</div>
|
||||
<Input
|
||||
id="redirect_uris"
|
||||
type="text"
|
||||
placeholder={t("workspace_settings.settings.applications.redirect_uris_description")}
|
||||
className="w-full resize-none text-sm"
|
||||
hasError={Boolean(errors.redirect_uris)}
|
||||
tabIndex={1}
|
||||
{...register("redirect_uris", {
|
||||
required: t("workspace_settings.settings.applications.redirect_uris_error"), pattern: {
|
||||
value: redirectURIsRegex,
|
||||
message: t("workspace_settings.settings.applications.invalid_redirect_uris_error")
|
||||
}
|
||||
})}
|
||||
onChange={(e) => handleTextChange("redirect_uris", e.target.value)}
|
||||
/>
|
||||
{errors.redirect_uris && <p className="text-red-500 text-xs">{errors.redirect_uris.message}</p>}
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-custom-text-300">{t("workspace_settings.settings.applications.authorized_javascript_origins_title")}</div>
|
||||
<Input
|
||||
id="allowed_origins"
|
||||
type="text"
|
||||
placeholder={t("workspace_settings.settings.applications.authorized_javascript_origins_description")}
|
||||
className="w-full resize-none text-sm"
|
||||
hasError={Boolean(errors.allowed_origins)}
|
||||
tabIndex={1}
|
||||
{...register("allowed_origins", {
|
||||
required: t("workspace_settings.settings.applications.authorized_javascript_origins_error"), pattern: {
|
||||
value: allowedOriginsRegex,
|
||||
message: t("workspace_settings.settings.applications.invalid_authorized_javascript_origins_error")
|
||||
}
|
||||
})}
|
||||
onChange={(e) => handleTextChange("allowed_origins", e.target.value)}
|
||||
/>
|
||||
{errors.allowed_origins && <p className="text-red-500 text-xs">{errors.allowed_origins.message}</p>}
|
||||
</div>
|
||||
{
|
||||
watch("id") && formData && (
|
||||
<RegenerateClientSecret
|
||||
application={formData}
|
||||
handleRegenerateSuccess={handleRegenerateSuccess}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
watch("logo_url")
|
||||
? <img loading="lazy" src={getFileURL(watch("logo_url") ?? "")} alt="Logo" className="w-20 h-20 object-cover" />
|
||||
:
|
||||
<div className={`flex items-center justify-center w-20 gap-2 p-5 ${bgColor} rounded border border-custom-border-200`}>
|
||||
<Camera className="w-5 h-5 text-custom-text-400" />
|
||||
</div>
|
||||
<div className="flex justify-start gap-2 mt-10">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline-primary"
|
||||
onClick={() => router.push(`/${workspaceSlug}/settings/applications`)}
|
||||
>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
loading={isSubmitting}
|
||||
variant="primary"
|
||||
>
|
||||
{!watch("id") ? t("workspace_settings.settings.applications.create_app") : t("workspace_settings.settings.applications.update_app")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<GeneratedCredentialsModal
|
||||
handleClose={handleCredentialsModalClose}
|
||||
isOpen={isCredentialsModalOpen}
|
||||
clientId={clientId ?? ""}
|
||||
clientSecret={clientSecret ?? ""}
|
||||
}
|
||||
<Button onClick={() => setIsImageModalOpen(true)} variant="outline-primary" className="text-sm">
|
||||
{t("workspace_settings.settings.applications.upload_logo")}
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<Input
|
||||
id="name"
|
||||
type="text"
|
||||
placeholder={t("workspace_settings.settings.applications.app_name_title")}
|
||||
className="w-full resize-none text-sm"
|
||||
hasError={Boolean(errors.name)}
|
||||
tabIndex={1}
|
||||
{...register("name", { required: t("workspace_settings.settings.applications.app_name_error") })}
|
||||
onChange={(e) => handleTextChange("name", e.target.value)}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
{errors.name && <p className="text-red-500 text-xs">{errors.name.message}</p>}
|
||||
</div>
|
||||
<div>
|
||||
<Input
|
||||
id="short_description"
|
||||
type="text"
|
||||
placeholder={t("workspace_settings.settings.applications.app_short_description_title")}
|
||||
className="w-full resize-none text-sm"
|
||||
hasError={Boolean(errors.short_description)}
|
||||
tabIndex={2}
|
||||
{...register("short_description", { required: t("workspace_settings.settings.applications.app_short_description_error") })}
|
||||
onChange={(e) => handleTextChange("short_description", e.target.value)}
|
||||
/>
|
||||
{errors.short_description && <p className="text-red-500 text-xs">{errors.short_description.message}</p>}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Controller
|
||||
name="description_html"
|
||||
control={control}
|
||||
rules={{ required: t("workspace_settings.settings.applications.app_description_error") }}
|
||||
render={({ field: { onChange } }) =>
|
||||
<RichTextEditor
|
||||
id={workspaceSlug.toString()}
|
||||
tabIndex={3}
|
||||
initialValue={
|
||||
!watch("description_html") ? "<p></p>" : watch("description_html") || "<p></p>"
|
||||
}
|
||||
workspaceSlug={workspaceSlug}
|
||||
workspaceId={workspaceId}
|
||||
searchMentionCallback={async (payload) =>
|
||||
await workspaceService.searchEntity(workspaceSlug?.toString() ?? "", {
|
||||
...payload,
|
||||
})
|
||||
}
|
||||
dragDropEnabled={false}
|
||||
onChange={(_description: object, description_html: string) => {
|
||||
onChange(description_html);
|
||||
}}
|
||||
placeholder={t("workspace_settings.settings.applications.app_description_title")}
|
||||
editorClassName="text-xs"
|
||||
containerClassName="resize-none min-h-24 text-xs border-[0.5px] border-custom-border-200 rounded-md px-3 py-2"
|
||||
uploadFile={async (blockId, file) => {
|
||||
try {
|
||||
const { asset_id } = await uploadEditorAsset({
|
||||
blockId,
|
||||
data: {
|
||||
entity_identifier: workspaceId,
|
||||
entity_type: EFileAssetType.OAUTH_APP_DESCRIPTION,
|
||||
},
|
||||
file,
|
||||
workspaceSlug,
|
||||
});
|
||||
return asset_id;
|
||||
} catch (error) {
|
||||
console.log("Error in uploading application asset:", error);
|
||||
throw new Error("Asset upload failed. Please try again later.");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{errors.description_html && <p className="text-red-500 text-xs">{errors.description_html.message}</p>}
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-custom-text-300">{t("workspace_settings.settings.applications.app_slug_title")}</div>
|
||||
<Input
|
||||
id="slug"
|
||||
type="text"
|
||||
disabled={!!watch("id")}
|
||||
className="w-full resize-none text-sm"
|
||||
hasError={Boolean(errors.slug)}
|
||||
tabIndex={4}
|
||||
{...register("slug", { required: t("workspace_settings.settings.applications.app_slug_error") })}
|
||||
onChange={(e) => handleTextChange("slug", e.target.value)}
|
||||
/>
|
||||
{errors.slug && <p className="text-red-500 text-xs">{errors.slug.message}</p>}
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-custom-text-300">{t("workspace_settings.settings.applications.app_maker_title")}</div>
|
||||
<Input
|
||||
id="company_name"
|
||||
type="text"
|
||||
placeholder={t("workspace_settings.settings.applications.app_maker_title")}
|
||||
className="w-full resize-none text-sm"
|
||||
hasError={Boolean(errors.company_name)}
|
||||
tabIndex={5}
|
||||
{...register("company_name", { required: t("workspace_settings.settings.applications.app_maker_error") })}
|
||||
onChange={(e) => handleTextChange("company_name", e.target.value)}
|
||||
/>
|
||||
{errors.company_name && <p className="text-red-500 text-xs">{errors.company_name.message}</p>}
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-custom-text-300">{t("workspace_settings.settings.applications.webhook_url_title")}</div>
|
||||
<Input
|
||||
id="webhook_url"
|
||||
type="text"
|
||||
className="w-full resize-none text-sm"
|
||||
hasError={Boolean(errors.webhook_url)}
|
||||
tabIndex={6}
|
||||
{...register("webhook_url", {
|
||||
required: t("workspace_settings.settings.applications.webhook_url_error"), pattern: {
|
||||
value: webhookUrlRegex,
|
||||
message: t("workspace_settings.settings.applications.invalid_webhook_url_error")
|
||||
}
|
||||
})}
|
||||
onChange={(e) => handleTextChange("webhook_url", e.target.value)}
|
||||
/>
|
||||
{errors.webhook_url && <p className="text-red-500 text-xs">{errors.webhook_url.message}</p>}
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-custom-text-300">{t("workspace_settings.settings.applications.redirect_uris_title")}</div>
|
||||
<Input
|
||||
id="redirect_uris"
|
||||
type="text"
|
||||
placeholder={t("workspace_settings.settings.applications.redirect_uris_description")}
|
||||
className="w-full resize-none text-sm"
|
||||
hasError={Boolean(errors.redirect_uris)}
|
||||
tabIndex={7}
|
||||
{...register("redirect_uris", {
|
||||
required: t("workspace_settings.settings.applications.redirect_uris_error"), pattern: {
|
||||
value: redirectURIsRegex,
|
||||
message: t("workspace_settings.settings.applications.invalid_redirect_uris_error")
|
||||
}
|
||||
})}
|
||||
onChange={(e) => handleTextChange("redirect_uris", e.target.value)}
|
||||
/>
|
||||
{errors.redirect_uris && <p className="text-red-500 text-xs">{errors.redirect_uris.message}</p>}
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-custom-text-300">{t("workspace_settings.settings.applications.authorized_javascript_origins_title")}</div>
|
||||
<Input
|
||||
id="allowed_origins"
|
||||
type="text"
|
||||
placeholder={t("workspace_settings.settings.applications.authorized_javascript_origins_description")}
|
||||
className="w-full resize-none text-sm"
|
||||
hasError={Boolean(errors.allowed_origins)}
|
||||
tabIndex={8}
|
||||
{...register("allowed_origins", {
|
||||
required: t("workspace_settings.settings.applications.authorized_javascript_origins_error"), pattern: {
|
||||
value: allowedOriginsRegex,
|
||||
message: t("workspace_settings.settings.applications.invalid_authorized_javascript_origins_error")
|
||||
}
|
||||
})}
|
||||
onChange={(e) => handleTextChange("allowed_origins", e.target.value)}
|
||||
/>
|
||||
{errors.allowed_origins && <p className="text-red-500 text-xs">{errors.allowed_origins.message}</p>}
|
||||
</div>
|
||||
{
|
||||
watch("id") && formData && (
|
||||
<RegenerateClientSecret
|
||||
application={formData}
|
||||
handleRegenerateSuccess={handleRegenerateSuccess}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<div className="flex justify-start gap-2 mt-10">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline-primary"
|
||||
onClick={() => router.push(`/${workspaceSlug}/settings/applications`)}
|
||||
>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
loading={isSubmitting}
|
||||
variant="primary"
|
||||
>
|
||||
{!watch("id") ? t("workspace_settings.settings.applications.create_app") : t("workspace_settings.settings.applications.update_app")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<GeneratedCredentialsModal
|
||||
handleClose={handleCredentialsModalClose}
|
||||
isOpen={isCredentialsModalOpen}
|
||||
clientId={clientId ?? ""}
|
||||
clientSecret={clientSecret ?? ""}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,92 +1,98 @@
|
||||
"use client"
|
||||
|
||||
import { CheckCircle, Copy } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Copy } from "lucide-react";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// ui
|
||||
import { Button, Tooltip, TOAST_TYPE, setToast, ModalCore } from "@plane/ui";
|
||||
import { TUserApplication } from "@plane/types";
|
||||
import { Button, Tooltip, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// helpers
|
||||
import { copyTextToClipboard } from "@/helpers/string.helper";
|
||||
// hooks
|
||||
import { useUserProfile } from "@/hooks/store";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
import { observer } from "mobx-react";
|
||||
import { useState } from "react";
|
||||
import { RegenerateConfirmModal } from "@/plane-web/components/marketplace";
|
||||
import { TUserApplication } from "@plane/types";
|
||||
// types
|
||||
|
||||
type Props = {
|
||||
application: Partial<TUserApplication>;
|
||||
handleRegenerateSuccess: (data: Partial<TUserApplication>) => void;
|
||||
application: Partial<TUserApplication>;
|
||||
handleRegenerateSuccess: (data: Partial<TUserApplication>) => void;
|
||||
};
|
||||
|
||||
export const RegenerateClientSecret: React.FC<Props> = observer((props) => {
|
||||
const { application, handleRegenerateSuccess } = props;
|
||||
const { application, handleRegenerateSuccess } = props;
|
||||
|
||||
// hooks
|
||||
const { isMobile } = usePlatformOS();
|
||||
const { t } = useTranslation();
|
||||
// hooks
|
||||
const { isMobile } = usePlatformOS();
|
||||
const { t } = useTranslation();
|
||||
const { data: userProfile } = useUserProfile();
|
||||
|
||||
// state
|
||||
const [isRegenerateModalOpen, setIsRegenerateModalOpen] = useState(false);
|
||||
// derived values
|
||||
const isDarkMode = userProfile?.theme.theme === "dark";
|
||||
const bgColor = isDarkMode ? "bg-custom-background-100" : "bg-gray-100";
|
||||
const textColor = isDarkMode ? "text-custom-text-100" : "text-custom-text-400";
|
||||
|
||||
const copyContent = (value: string, label: string) => {
|
||||
copyTextToClipboard(value).then(() =>
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: `${t("success")}!`,
|
||||
message: t(`workspace_settings.settings.applications.${label}_copied`),
|
||||
})
|
||||
);
|
||||
};
|
||||
// state
|
||||
const [isRegenerateModalOpen, setIsRegenerateModalOpen] = useState(false);
|
||||
|
||||
const handleRegenerateModalClose = () => {
|
||||
setIsRegenerateModalOpen(false);
|
||||
};
|
||||
|
||||
const handleRegenerated = (data: Partial<TUserApplication>) => {
|
||||
setIsRegenerateModalOpen(false);
|
||||
handleRegenerateSuccess(data);
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div className="mt-4 space-y-2 flex flex-col bg-gray-100 rounded-md p-4">
|
||||
<div className="font-medium">{t("workspace_settings.settings.applications.client_id_and_secret")}</div>
|
||||
<div className="text-sm text-custom-text-400">{t("workspace_settings.settings.applications.regenerate_client_secret_description")}</div>
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm text-custom-text-400">{t("workspace_settings.settings.applications.client_id")}</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copyContent(application.client_id ?? "", "clientId")}
|
||||
className="mt-4 bg-white flex truncate w-full items-center justify-between rounded-md border-[0.5px] border-custom-border-200 px-3 py-3 text-sm font-medium outline-none"
|
||||
>
|
||||
<span className="truncate pr-2">{application.client_id}</span>
|
||||
<Tooltip tooltipContent="Copy client id" isMobile={isMobile}>
|
||||
<Copy className="h-4 w-4 text-custom-text-400 flex-shrink-0" />
|
||||
</Tooltip>
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm text-custom-text-400">{t("workspace_settings.settings.applications.client_secret")}</p>
|
||||
<div className="flex items-center justify-between mt-4 bg-white flex truncate w-full items-center justify-between rounded-md border-[0.5px] border-custom-border-200 px-3 py-2 text-sm font-medium outline-none">
|
||||
<span className="truncate pr-2">{"******************"}</span>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={() => setIsRegenerateModalOpen(true)}
|
||||
>
|
||||
{t("workspace_settings.settings.applications.regenerate_client_secret")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<RegenerateConfirmModal
|
||||
isOpen={isRegenerateModalOpen}
|
||||
handleRegenerated={handleRegenerated}
|
||||
handleClose={handleRegenerateModalClose}
|
||||
applicationId={application.id ?? ""}
|
||||
/>
|
||||
</div>
|
||||
const copyContent = (value: string, label: string) => {
|
||||
copyTextToClipboard(value).then(() =>
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: `${t("success")}!`,
|
||||
message: t(`workspace_settings.settings.applications.${label}_copied`),
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleRegenerateModalClose = () => {
|
||||
setIsRegenerateModalOpen(false);
|
||||
};
|
||||
|
||||
const handleRegenerated = (data: Partial<TUserApplication>) => {
|
||||
setIsRegenerateModalOpen(false);
|
||||
handleRegenerateSuccess(data);
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div className={`mt-4 space-y-2 flex flex-col rounded-md p-4 ${bgColor}`}>
|
||||
<div className="font-medium">{t("workspace_settings.settings.applications.client_id_and_secret")}</div>
|
||||
<div className="text-sm text-custom-text-400">{t("workspace_settings.settings.applications.regenerate_client_secret_description")}</div>
|
||||
<div className="space-y-1">
|
||||
<p className={`text-sm ${textColor}`}>{t("workspace_settings.settings.applications.client_id")}</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copyContent(application.client_id ?? "", "clientId")}
|
||||
className={`mt-4 flex truncate w-full items-center justify-between rounded-md border-[0.5px] border-custom-border-200 px-3 py-3 text-sm font-medium outline-none ${bgColor}`}
|
||||
>
|
||||
<span className={`truncate pr-2 ${textColor}`}>{application.client_id}</span>
|
||||
<Tooltip tooltipContent="Copy client id" isMobile={isMobile}>
|
||||
<Copy className="h-4 w-4 text-custom-text-400 flex-shrink-0" />
|
||||
</Tooltip>
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p className={`text-sm ${textColor}`}>{t("workspace_settings.settings.applications.client_secret")}</p>
|
||||
<div className={`flex items-center justify-between mt-4 flex truncate w-full items-center justify-between rounded-md border-[0.5px] border-custom-border-200 px-3 py-2 text-sm font-medium outline-none ${bgColor}`}>
|
||||
<span className={`truncate pr-2 ${textColor}`}>{"******************"}</span>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={() => setIsRegenerateModalOpen(true)}
|
||||
>
|
||||
{t("workspace_settings.settings.applications.regenerate_client_secret")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<RegenerateConfirmModal
|
||||
isOpen={isRegenerateModalOpen}
|
||||
handleRegenerated={handleRegenerated}
|
||||
handleClose={handleRegenerateModalClose}
|
||||
applicationId={application.id ?? ""}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,44 +1,44 @@
|
||||
"use client";
|
||||
|
||||
import { useApplications } from "@/plane-web/hooks/store";
|
||||
import { observer } from "mobx-react";
|
||||
import { AlertCircle } from "lucide-react";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { TUserApplication } from "@plane/types";
|
||||
import { Button, ModalCore } from "@plane/ui";
|
||||
import { AlertCircle } from "lucide-react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useApplications } from "@/plane-web/hooks/store";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
handleClose: () => void;
|
||||
applicationId: string;
|
||||
handleRegenerated: (data: TUserApplication) => void;
|
||||
isOpen: boolean;
|
||||
handleClose: () => void;
|
||||
applicationId: string;
|
||||
handleRegenerated: (data: TUserApplication) => void;
|
||||
};
|
||||
|
||||
export const RegenerateConfirmModal: React.FC<Props> = observer((props) => {
|
||||
const { isOpen, handleClose, applicationId, handleRegenerated } = props;
|
||||
const { isOpen, handleClose, applicationId, handleRegenerated } = props;
|
||||
|
||||
const { regenerateApplicationSecret } = useApplications();
|
||||
const { t } = useTranslation();
|
||||
const { regenerateApplicationSecret } = useApplications();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleRegenerate = async () => {
|
||||
const data = await regenerateApplicationSecret(applicationId);
|
||||
if (data) {
|
||||
handleRegenerated(data);
|
||||
}
|
||||
};
|
||||
const handleRegenerate = async () => {
|
||||
const data = await regenerateApplicationSecret(applicationId);
|
||||
if (data) {
|
||||
handleRegenerated(data);
|
||||
}
|
||||
};
|
||||
|
||||
return (<ModalCore isOpen={isOpen} handleClose={handleClose}>
|
||||
<div className="flex flex-col gap-4 p-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<AlertCircle className="h-6 w-6 text-custom-text-400" />
|
||||
<div className="text-lg font-medium">{t("workspace_settings.settings.applications.regenerate_client_secret_confirm_title")}</div>
|
||||
</div>
|
||||
<div className="text-sm text-custom-text-400">{t("workspace_settings.settings.applications.regenerate_client_secret_confirm_description")}</div>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant="link-neutral" onClick={handleClose}>{t("workspace_settings.settings.applications.regenerate_client_secret_confirm_cancel")}</Button>
|
||||
<Button variant="danger" onClick={handleRegenerate}>{t("workspace_settings.settings.applications.regenerate_client_secret_confirm_regenerate")}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</ModalCore>
|
||||
);
|
||||
return (<ModalCore isOpen={isOpen} handleClose={handleClose}>
|
||||
<div className="flex flex-col gap-4 p-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<AlertCircle className="h-6 w-6 text-custom-text-400" />
|
||||
<div className="text-lg font-medium">{t("workspace_settings.settings.applications.regenerate_client_secret_confirm_title")}</div>
|
||||
</div>
|
||||
<div className="text-sm text-custom-text-400">{t("workspace_settings.settings.applications.regenerate_client_secret_confirm_description")}</div>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant="link-neutral" onClick={handleClose}>{t("workspace_settings.settings.applications.regenerate_client_secret_confirm_cancel")}</Button>
|
||||
<Button variant="danger" onClick={handleRegenerate}>{t("workspace_settings.settings.applications.regenerate_client_secret_confirm_regenerate")}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</ModalCore>
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user