[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:
co-authored by
Saurabhkmr98
Saurabh Kumar
parent
5bad47bea7
commit
4f7a45260d
@@ -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)
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -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}
|
||||
|
||||
@@ -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 數據的專案。專案創建後,在此處選擇它。",
|
||||
|
||||
Reference in New Issue
Block a user