[SILO-695] chore: Jira server/ Data center change + implemented delete for apps + removed validation from webhooks in create/edit app (#4765)

* chore: Jira server/ Data center change + implemented delete for apps + removed validation from webhooks in create/edit app

* add oauth app delete fix and bgtask

* fix: refactor

* fix: handled delete

---------

Co-authored-by: Saurabhkmr98 <[email protected]>
Co-authored-by: Saurabh Kumar <[email protected]>
This commit is contained in:
Akshita Goyal
2025-11-24 15:27:05 +05:30
committed by GitHub
co-authored by Saurabhkmr98 Saurabh Kumar
parent 5bad47bea7
commit 4f7a45260d
28 changed files with 145 additions and 63 deletions
@@ -0,0 +1,20 @@
from celery import shared_task
import logging
@shared_task
def app_delete_updates(application_id: str):
"""
This function deletes all app installations for an application.
"""
from plane.authentication.models import Application, WorkspaceAppInstallation
application = Application.objects.get(id=application_id)
if not application:
logging.getLogger("plane.worker").info(f"Application {application_id} not found")
return
app_installations = WorkspaceAppInstallation.objects.filter(
application_id=application_id,
)
for app_installation in app_installations:
app_installation.delete()
return
@@ -17,6 +17,7 @@ from oauth2_provider.models import (
from plane.app.permissions.base import ROLE
from plane.authentication.bgtasks.app_webhook_url_updates import app_webhook_url_updates
from plane.authentication.bgtasks.app_delete_updates import app_delete_updates
from plane.authentication.bgtasks.app_logo_asset_updates import app_logo_asset_updates
from plane.db.mixins import SoftDeleteModel, UserAuditModel
from plane.db.models import (
@@ -152,6 +153,11 @@ class Application(AbstractApplication, UserAuditModel, SoftDeleteModel):
super(Application, self).save(*args, **kwargs)
def delete(self, *args, **kwargs):
with transaction.atomic():
app_delete_updates.delay(self.id)
super(Application, self).delete(*args, **kwargs)
class Grant(AbstractGrant):
id = models.UUIDField(default=uuid.uuid4, unique=True, editable=False, db_index=True, primary_key=True)
@@ -168,6 +168,7 @@ class OAuthApplicationEndpoint(BaseAPIView):
| Q(published_at__isnull=False)
| Q(slug__in=enabled_apps_slugs)
)
.filter(deleted_at__isnull=True)
.select_related("logo_asset")
.prefetch_related("attachments", "categories")
)
@@ -213,6 +214,7 @@ class OAuthApplicationEndpoint(BaseAPIView):
.select_related(
"logo_asset",
)
.filter(deleted_at__isnull=True)
.first()
)
@@ -247,15 +249,10 @@ class OAuthApplicationEndpoint(BaseAPIView):
{"error": "Application not found"}, status=status.HTTP_404_NOT_FOUND
)
def delete(self, request, slug, pk):
application = Application.objects.filter(
id=pk, application_owners__workspace__slug=slug
).first()
if not application:
return Response(
{"error": "Application not found"}, status=status.HTTP_404_NOT_FOUND
)
application.delete()
def delete(self, request, slug, app_slug):
application = Application.objects.filter(slug=app_slug, application_owners__workspace__slug=slug).first()
if application:
application.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
@@ -127,7 +127,7 @@ export const PersonalAccessTokenAuth: FC = observer(() => {
<div className="space-y-6 w-full">
<ImporterHeader
config={{
serviceName: "Jira Server",
serviceName: "Jira Server/Data Center",
logo: JiraLogo,
}}
/>
@@ -23,7 +23,7 @@ export const JiraServerDashboardRoot: FC = observer(() => {
getWorkspaceName,
getProjectName,
getPlaneProject,
serviceName: "Jira Server",
serviceName: "Jira Server/Data Center",
logo: JiraLogo,
swrKey: "JIRA_SERVER_IMPORTER",
}}
@@ -18,7 +18,7 @@ export const StepsRoot: FC = observer(() => {
return (
<div className="relative w-full h-full overflow-hidden">
<Stepper
serviceName="Jira Server"
serviceName="Jira Server/Data Center"
logo={JiraLogo}
steps={IMPORTER_STEPS}
currentStepIndex={currentStepIndex}
+1 -1
View File
@@ -34,7 +34,7 @@ export const IMPORTERS_LIST: ImporterProps[] = [
{
flag: E_FEATURE_FLAGS.JIRA_SERVER_IMPORTER,
key: "jira-server",
title: "Jira Server",
title: "Jira Server/Data Center",
i18n_description: "jira_server_importer.jira_server_importer_description",
logo: JiraLogo,
beta: true,
@@ -0,0 +1,67 @@
"use client";
import { useState } from "react";
import { observer } from "mobx-react";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
// types
import type { TUserApplication } from "@plane/types";
// ui
import { AlertModalCore } from "@plane/ui";
// hooks
import { useApplications } from "@/plane-web/hooks/store";
interface IDeleteApplication {
app: TUserApplication;
isOpen: boolean;
handleClose: () => void;
}
export const DeleteApplicationModal: React.FC<IDeleteApplication> = observer((props) => {
const { app, isOpen, handleClose } = props;
// states
const [loader, setLoader] = useState(false);
// store hooks
const { deleteApplication } = useApplications();
// router
const handleSubmit = async () => {
try {
setLoader(true);
await deleteApplication(app.slug);
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success",
message: "Application deleted successfully",
});
handleClose();
} catch (error) {
console.error(error);
setToast({
type: TOAST_TYPE.ERROR,
title: "Error",
message: "Failed to delete application",
});
} finally {
setLoader(false);
}
};
return (
<AlertModalCore
handleClose={handleClose}
handleSubmit={handleSubmit}
isSubmitting={loader}
isOpen={isOpen}
title="Delete Application"
primaryButtonText={{
loading: "Deleting",
default: "Delete",
}}
content={
<>
Are you sure you want to delete this application? This action will remove the app from all workspaces where it
has been installed.
</>
}
/>
);
});
@@ -21,6 +21,7 @@ import { useUserPermissions } from "@/hooks/store/user/user-permissions";
import { ApplicationPublishModal, ApplicationTileMenuItem } from "@/plane-web/components/marketplace";
import { RegenerateClientSecretModal } from "../form/regenerate-client-secret-modal";
import { DeleteApplicationModal } from "./delete-modal";
import { RevokeAccessModal } from "./revoke-modal";
export type TPopoverMenuOptions = {
@@ -45,7 +46,7 @@ export const ApplicationTileMenuOptions: FC<ApplicationTileMenuOptionsProps> = o
const [isPublishModalOpen, setIsPublishModalOpen] = React.useState(false);
const [isRevokeAccessModalOpen, setIsRevokeAccessModalOpen] = React.useState(false);
const [isCredentialsModalOpen, setIsCredentialsModalOpen] = React.useState(false);
const [isDeleteApplicationModalOpen, setIsDeleteApplicationModalOpen] = React.useState(false);
const router = useRouter();
const { currentWorkspace } = useWorkspace();
const { allowPermissions } = useUserPermissions();
@@ -63,6 +64,10 @@ export const ApplicationTileMenuOptions: FC<ApplicationTileMenuOptionsProps> = o
setIsCredentialsModalOpen(flag);
};
const toggleDeleteApplicationModal = (flag: boolean) => {
setIsDeleteApplicationModalOpen(flag);
};
const handleEdit = () => {
router.push(`/${workspaceSlug}/settings/integrations/${app.slug}/edit`);
};
@@ -102,9 +107,11 @@ export const ApplicationTileMenuOptions: FC<ApplicationTileMenuOptionsProps> = o
key: "menu-delete",
type: "menu-item",
label: "Delete",
isActive: false,
isActive: app.is_owned,
prependIcon: <Trash2 className="flex-shrink-0 h-3 w-3" />,
onClick: () => {},
onClick: () => {
toggleDeleteApplicationModal(true);
},
},
{
key: "uninstall",
@@ -129,6 +136,11 @@ export const ApplicationTileMenuOptions: FC<ApplicationTileMenuOptionsProps> = o
render={(item: TPopoverMenuOptions) => <ApplicationTileMenuItem {...item} />}
/>
<ApplicationPublishModal isOpen={isPublishModalOpen} handleClose={() => togglePublishModal(false)} app={app} />
<DeleteApplicationModal
app={app}
isOpen={isDeleteApplicationModalOpen}
handleClose={() => toggleDeleteApplicationModal(false)}
/>
<RevokeAccessModal
app={app}
isOpen={isRevokeAccessModalOpen}
@@ -36,16 +36,9 @@ type Props = {
formData?: Partial<TUserApplication>;
handleFormSubmit: (data: Partial<TUserApplication>) => Promise<Partial<TUserApplication> | undefined>;
};
const redirectURIsRegex =
/^(https?:\/\/(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}(?:\/\S*)?)(?:\s+(https?:\/\/(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}(?:\/\S*)?))*$/gim;
const singleUrlRegex =
/^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&\/=]*)$/;
const allowedOriginsRegex =
/^(?:(?:https?:\/\/)?(?:[\w-]+(?:\.[\w-]+)+))(?:\s+(?:(?:https?:\/\/)?(?:[\w-]+(?:\.[\w-]+)+)))*$/;
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const relativeUrlRegex = /^(\/[a-zA-Z0-9_\-\/]+|https?:\/\/[^\s]+)$/;
const defaultFormData: Partial<TUserApplication> = {
id: undefined,
name: "",
@@ -393,12 +386,6 @@ export const CreateUpdateApplication: React.FC<Props> = observer((props) => {
description={t("workspace_settings.settings.applications.webhook_url.description")}
placeholder={t("workspace_settings.settings.applications.webhook_url.placeholder")}
register={register}
validation={{
pattern: {
value: singleUrlRegex,
message: t("workspace_settings.settings.applications.invalid_webhook_url_error"),
},
}}
onChange={(value) => handleTextChange("webhook_url", value)}
error={errors.webhook_url}
/>
@@ -421,10 +408,6 @@ export const CreateUpdateApplication: React.FC<Props> = observer((props) => {
register={register}
validation={{
required: t("workspace_settings.settings.applications.redirect_uris_error"),
pattern: {
value: redirectURIsRegex,
message: t("workspace_settings.settings.applications.invalid_redirect_uris_error"),
},
}}
onChange={(value) => handleTextChange("redirect_uris", value)}
error={errors.redirect_uris}
@@ -45,8 +45,8 @@ export class ApplicationService extends APIService {
});
}
async deleteApplication(workspaceSlug: string, applicationId: string): Promise<any> {
return this.delete(`/api/workspaces/${workspaceSlug}/applications/${applicationId}/`)
async deleteApplication(workspaceSlug: string, appSlug: string): Promise<any> {
return this.delete(`/api/workspaces/${workspaceSlug}/applications/${appSlug}/`)
.then((res) => res?.data)
.catch((err) => {
throw err?.response?.data;
@@ -25,7 +25,7 @@ export interface IApplicationStore {
fetchApplication: (appSlug: string) => Promise<TUserApplication | undefined>;
createApplication: (data: Partial<TUserApplication>) => Promise<TUserApplication | undefined>;
updateApplication: (appSlug: string, data: Partial<TUserApplication>) => Promise<TUserApplication | undefined>;
deleteApplication: (applicationId: string, appSlug: string) => Promise<TUserApplication | undefined>;
deleteApplication: (appSlug: string) => Promise<void>;
regenerateApplicationSecret: (applicationId: string) => Promise<TUserApplication | undefined>;
checkApplicationSlug: (slug: string) => Promise<any>;
fetchApplicationCategories: () => Promise<TApplicationCategory[] | undefined>;
@@ -162,21 +162,18 @@ export class ApplicationStore implements IApplicationStore {
* @param applicationId - The application ID
* @returns The deleted application
*/
deleteApplication = async (applicationId: string, appSlug: string): Promise<TUserApplication | undefined> => {
deleteApplication = async (appSlug: string): Promise<void> => {
this.applicationsLoader = "init-loader";
try {
const workspaceSlug = this.rootStore.workspaceRoot.currentWorkspace?.slug;
if (!workspaceSlug) {
throw new Error("Workspace not found");
}
const response = await this.applicationService.deleteApplication(workspaceSlug, applicationId);
if (response) {
runInAction(() => {
this.applicationsLoader = "loaded";
delete this.applicationsMap[workspaceSlug][appSlug];
});
}
return response;
await this.applicationService.deleteApplication(workspaceSlug, appSlug);
runInAction(() => {
this.applicationsLoader = "loaded";
delete this.applicationsMap[workspaceSlug][appSlug];
});
} catch (error) {
console.log(error);
throw error;
@@ -1586,7 +1586,7 @@ export default {
},
},
jira_server_importer: {
jira_server_importer_description: "Importujte svá Jira Server data do projektů Plane.",
jira_server_importer_description: "Importujte svá Jira Server/Data Center data do projektů Plane.",
steps: {
title_configure_plane: "Nastavte Plane",
description_configure_plane:
@@ -1793,7 +1793,7 @@ export default {
},
},
jira_server_importer: {
jira_server_importer_description: "Import your Jira Server data into Plane projects.",
jira_server_importer_description: "Import your Jira Server/Data Center data into Plane projects.",
steps: {
title_configure_plane: "Configure Plane",
description_configure_plane:
@@ -1604,7 +1604,7 @@ export default {
},
},
jira_server_importer: {
jira_server_importer_description: "Importa tus datos de Jira Server a proyectos de Plane",
jira_server_importer_description: "Importa tus datos de Jira Server/Data Center a proyectos de Plane",
steps: {
title_configure_plane: "Configurar Plane",
description_configure_plane:
@@ -1607,7 +1607,7 @@ export default {
},
},
jira_server_importer: {
jira_server_importer_description: "Importez vos données Jira Server dans les projets Plane.",
jira_server_importer_description: "Importez vos données Jira Server/Data Center dans les projets Plane.",
steps: {
title_configure_plane: "Configurer Plane",
description_configure_plane:
@@ -1586,7 +1586,7 @@ export default {
},
},
jira_server_importer: {
jira_server_importer_description: "Impor data Jira Server Anda ke dalam projek Plane.",
jira_server_importer_description: "Impor data Jira Server/Data Center Anda ke dalam projek Plane.",
steps: {
title_configure_plane: "Konfigurasi Plane",
description_configure_plane:
@@ -1596,7 +1596,7 @@ export default {
},
},
jira_server_importer: {
jira_server_importer_description: "Importa i tuoi dati Jira Server nel progetto Plane.",
jira_server_importer_description: "Importa i tuoi dati Jira Server/Data Center nel progetto Plane.",
steps: {
title_configure_plane: "Configura Plane",
description_configure_plane:
@@ -1578,7 +1578,7 @@ export default {
},
},
jira_server_importer: {
jira_server_importer_description: "Jira ServerのデータをPlaneプロジェクトにインポートします。",
jira_server_importer_description: "Jira Server/Data CenterのデータをPlaneプロジェクトにインポートします。",
steps: {
title_configure_plane: "Planeを設定",
description_configure_plane:
@@ -1599,7 +1599,7 @@ export default {
},
},
jira_server_importer: {
jira_server_importer_description: "Importe seus dados do Jira Server para projetos do Plane.",
jira_server_importer_description: "Importe seus dados do Jira Server/Data Center para projetos do Plane.",
steps: {
title_configure_plane: "Configurar Plane",
description_configure_plane:
@@ -1600,7 +1600,7 @@ export default {
},
},
jira_server_importer: {
jira_server_importer_description: "Importă datele tale Jira Server în proiectele Plane.",
jira_server_importer_description: "Importă datele tale Jira Server/Data Center în proiectele Plane.",
steps: {
title_configure_plane: "Configurează Plane",
description_configure_plane:
@@ -1594,11 +1594,11 @@ export default {
},
},
jira_server_importer: {
jira_server_importer_description: "Импортируйте ваши данные Jira Server в проекты Plane.",
jira_server_importer_description: "Импортируйте ваши данные Jira Server/Data Center в проекты Plane.",
steps: {
title_configure_plane: "Настроить Plane",
description_configure_plane:
"Пожалуйста, сначала создайте проект в Plane, в который вы собираетесь мигрировать ваши данные Jira Server. После создания проекта выберите его здесь.",
"Пожалуйста, сначала создайте проект в Plane, в который вы собираетесь мигрировать ваши данные Jira Server/Data Center. После создания проекта выберите его здесь.",
title_configure_jira: "Настроить Jira",
description_configure_jira:
"Пожалуйста, выберите рабочее пространство Jira, из которого вы хотите мигрировать ваши данные.",
@@ -1609,7 +1609,7 @@ export default {
description_map_priorities:
"Пожалуйста, выберите приоритеты Jira, которые вы хотите сопоставить с приоритетами проектов Plane.",
title_summary: "Резюме",
description_summary: "Вот резюме данных, которые будут мигрированы из Jira Server в Plane.",
description_summary: "Вот резюме данных, которые будут мигрированы из Jira Server/Data Center в Plane.",
},
},
notion_importer: {
@@ -1601,7 +1601,7 @@ export default {
},
},
jira_server_importer: {
jira_server_importer_description: "Importujte vaše Jira Server dáta do projektov Plane.",
jira_server_importer_description: "Importujte vaše Jira Server/Data Center dáta do projektov Plane.",
steps: {
title_configure_plane: "Nakonfigurovať Plane",
description_configure_plane:
@@ -1599,7 +1599,7 @@ export default {
},
},
jira_server_importer: {
jira_server_importer_description: "Jira Server verilerinizi Plane projelerine import edin.",
jira_server_importer_description: "Jira Server/Data Center verilerinizi Plane projelerine import edin.",
steps: {
title_configure_plane: "Plane'i Yapılandır",
description_configure_plane:
@@ -1587,7 +1587,7 @@ export default {
},
},
jira_server_importer: {
jira_server_importer_description: "Імпортуйте ваші дані Jira Server у проджекти Плейн.",
jira_server_importer_description: "Імпортуйте ваші дані Jira Server/Data Center у проджекти Плейн.",
steps: {
title_configure_plane: "Налаштувати Плейн",
description_configure_plane:
@@ -1570,7 +1570,7 @@ export default {
},
},
jira_server_importer: {
jira_server_importer_description: "Nhập dữ liệu Jira Server của bạn vào các dự án Plane.",
jira_server_importer_description: "Nhập dữ liệu Jira Server/Data Center của bạn vào các dự án Plane.",
steps: {
title_configure_plane: "Cấu hình Plane",
description_configure_plane:
@@ -1530,7 +1530,7 @@ export default {
},
},
jira_server_importer: {
jira_server_importer_description: "将您的 Jira Server 数据导入到 Plane 项目中。",
jira_server_importer_description: "将您的 Jira Server/Data Center 数据导入到 Plane 项目中。",
steps: {
title_configure_plane: "配置 Plane",
description_configure_plane: "请先在 Plane 中创建您打算迁移 Jira 数据的项目。创建项目后,在此处选择它。",
@@ -1528,7 +1528,7 @@ export default {
},
},
jira_server_importer: {
jira_server_importer_description: "將您的 Jira Server 數據導入到 Plane 專案中。",
jira_server_importer_description: "將您的 Jira Server/Data Center 數據導入到 Plane 專案中。",
steps: {
title_configure_plane: "配置 Plane",
description_configure_plane: "請先在 Plane 中創建您打算遷移 Jira 數據的專案。專案創建後,在此處選擇它。",