fix: code splitting for workspace delete modal (#6581)
* fix: code splitting for delete modal * fix: redirected to profile post deletion * fix: translations
This commit is contained in:
parent
cc9b448a9b
commit
6a3ccafe35
11 changed files with 264 additions and 221 deletions
|
|
@ -1253,9 +1253,19 @@
|
|||
"company_size": "Company size",
|
||||
"url": "Workspace URL",
|
||||
"update_workspace": "Update workspace",
|
||||
"delete_workspace": "Delete workspace",
|
||||
"delete_workspace": "Delete this workspace",
|
||||
"delete_workspace_description": "When deleting a workspace, all of the data and resources within that workspace will be permanently removed and cannot be recovered.",
|
||||
"delete_btn": "Delete my workspace",
|
||||
"delete_btn": "Delete this workspace",
|
||||
"delete_modal": {
|
||||
"title": "Are you sure you want to delete this workspace?",
|
||||
"description": "You have an active trial to one of our paid plans. Please cancel it first to proceed.",
|
||||
"dismiss": "Dismiss",
|
||||
"cancel": "Cancel trial",
|
||||
"success_title": "Workspace deleted.",
|
||||
"success_message": "You will soon go to your profile page.",
|
||||
"error_title": "That didn't work.",
|
||||
"error_message": "Try again, please."
|
||||
},
|
||||
"errors": {
|
||||
"name": {
|
||||
"required": "Name is required",
|
||||
|
|
|
|||
|
|
@ -1422,9 +1422,19 @@
|
|||
"company_size": "Tamaño de la empresa",
|
||||
"url": "URL del espacio de trabajo",
|
||||
"update_workspace": "Actualizar espacio de trabajo",
|
||||
"delete_workspace": "Eliminar espacio de trabajo",
|
||||
"delete_workspace_description": "Al eliminar un espacio de trabajo, todos los datos y recursos dentro de ese espacio de trabajo se eliminarán permanentemente y no se podrán recuperar.",
|
||||
"delete_btn": "Eliminar mi espacio de trabajo",
|
||||
"delete_workspace": "Eliminar este espacio de trabajo",
|
||||
"delete_workspace_description": "Al eliminar un espacio de trabajo, todos los datos y recursos dentro de ese espacio se eliminarán permanentemente y no podrán recuperarse.",
|
||||
"delete_btn": "Eliminar este espacio de trabajo",
|
||||
"delete_modal": {
|
||||
"title": "¿Está seguro de que desea eliminar este espacio de trabajo?",
|
||||
"description": "Tiene una prueba activa de uno de nuestros planes de pago. Por favor, cancelela primero para continuar.",
|
||||
"dismiss": "Descartar",
|
||||
"cancel": "Cancelar prueba",
|
||||
"success_title": "Espacio de trabajo eliminado.",
|
||||
"success_message": "Pronto irá a su página de perfil.",
|
||||
"error_title": "Eso no funcionó.",
|
||||
"error_message": "Por favor, inténtelo de nuevo."
|
||||
},
|
||||
"errors": {
|
||||
"name": {
|
||||
"required": "El nombre es obligatorio",
|
||||
|
|
|
|||
|
|
@ -1422,9 +1422,19 @@
|
|||
"company_size": "Taille de l'entreprise",
|
||||
"url": "URL de l'espace de travail",
|
||||
"update_workspace": "Mettre à jour l'espace de travail",
|
||||
"delete_workspace": "Supprimer l'espace de travail",
|
||||
"delete_workspace_description": "Lors de la suppression d'un espace de travail, toutes les données et ressources au sein de cet espace de travail seront définitivement supprimées et ne pourront pas être récupérées.",
|
||||
"delete_btn": "Supprimer mon espace de travail",
|
||||
"delete_workspace": "Supprimer cet espace de travail",
|
||||
"delete_workspace_description": "Lors de la suppression d'un espace de travail, toutes les données et ressources au sein de cet espace seront définitivement supprimées et ne pourront pas être récupérées.",
|
||||
"delete_btn": "Supprimer cet espace de travail",
|
||||
"delete_modal": {
|
||||
"title": "Êtes-vous sûr de vouloir supprimer cet espace de travail ?",
|
||||
"description": "Vous avez un essai actif sur l'un de nos forfaits payants. Veuillez d'abord l'annuler pour continuer.",
|
||||
"dismiss": "Fermer",
|
||||
"cancel": "Annuler l'essai",
|
||||
"success_title": "Espace de travail supprimé.",
|
||||
"success_message": "Vous serez bientôt redirigé vers votre page de profil.",
|
||||
"error_title": "Cela n'a pas fonctionné.",
|
||||
"error_message": "Veuillez réessayer."
|
||||
},
|
||||
"errors": {
|
||||
"name": {
|
||||
"required": "Le nom est requis",
|
||||
|
|
|
|||
|
|
@ -1422,9 +1422,19 @@
|
|||
"company_size": "会社の規模",
|
||||
"url": "ワークスペースURL",
|
||||
"update_workspace": "ワークスペースを更新",
|
||||
"delete_workspace": "ワークスペースを削除",
|
||||
"delete_workspace_description": "ワークスペースを削除すると、そのワークスペース内のすべてのデータとリソースが永久に削除され、復元できなくなります。",
|
||||
"delete_btn": "ワークスペースを削除",
|
||||
"delete_workspace": "このワークスペースを削除",
|
||||
"delete_workspace_description": "ワークスペースを削除すると、そのワークスペース内のすべてのデータとリソースが完全に削除され、復元することはできません。",
|
||||
"delete_btn": "このワークスペースを削除",
|
||||
"delete_modal": {
|
||||
"title": "このワークスペースを削除してもよろしいですか?",
|
||||
"description": "有料プランの無料トライアルが有効です。続行するには、まずトライアルをキャンセルしてください。",
|
||||
"dismiss": "閉じる",
|
||||
"cancel": "トライアルをキャンセル",
|
||||
"success_title": "ワークスペースが削除されました。",
|
||||
"success_message": "まもなくプロフィールページに移動します。",
|
||||
"error_title": "操作に失敗しました。",
|
||||
"error_message": "もう一度お試しください。"
|
||||
},
|
||||
"errors": {
|
||||
"name": {
|
||||
"required": "名前は必須です",
|
||||
|
|
|
|||
|
|
@ -1422,9 +1422,19 @@
|
|||
"company_size": "公司规模",
|
||||
"url": "工作区网址",
|
||||
"update_workspace": "更新工作区",
|
||||
"delete_workspace": "删除工作区",
|
||||
"delete_workspace_description": "删除工作区时,该工作区内的所有数据和资源将被永久删除且无法恢复。",
|
||||
"delete_btn": "删除我的工作区",
|
||||
"delete_workspace": "删除此工作区",
|
||||
"delete_workspace_description": "删除工作区时,该工作区内的所有数据和资源将被永久删除,且无法恢复。",
|
||||
"delete_btn": "删除此工作区",
|
||||
"delete_modal": {
|
||||
"title": "确定要删除此工作区吗?",
|
||||
"description": "您目前正在试用我们的付费方案。请先取消试用后再继续。",
|
||||
"dismiss": "关闭",
|
||||
"cancel": "取消试用",
|
||||
"success_title": "工作区已删除。",
|
||||
"success_message": "即将跳转到您的个人资料页面。",
|
||||
"error_title": "操作失败。",
|
||||
"error_message": "请重试。"
|
||||
},
|
||||
"errors": {
|
||||
"name": {
|
||||
"required": "名称为必填项",
|
||||
|
|
|
|||
27
web/ce/components/workspace/delete-workspace-modal.tsx
Normal file
27
web/ce/components/workspace/delete-workspace-modal.tsx
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import type { IWorkspace } from "@plane/types";
|
||||
// ui
|
||||
import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
|
||||
// constants
|
||||
// hooks
|
||||
|
||||
import { DeleteWorkspaceForm } from "@/components/workspace/delete-workspace-form";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
data: IWorkspace | null;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export const DeleteWorkspaceModal: React.FC<Props> = observer((props) => {
|
||||
const { isOpen, data, onClose } = props;
|
||||
|
||||
return (
|
||||
<ModalCore isOpen={isOpen} handleClose={() => onClose()} position={EModalPosition.CENTER} width={EModalWidth.XL}>
|
||||
<DeleteWorkspaceForm data={data} onClose={onClose} />
|
||||
</ModalCore>
|
||||
);
|
||||
});
|
||||
|
|
@ -6,8 +6,8 @@ import { useTranslation } from "@plane/i18n";
|
|||
import { IWorkspace } from "@plane/types";
|
||||
// ui
|
||||
import { Button, Collapsible } from "@plane/ui";
|
||||
import { DeleteWorkspaceModal } from "./delete-workspace-modal";
|
||||
// components
|
||||
import { DeleteWorkspaceModal } from "@/components/workspace";
|
||||
|
||||
type TDeleteWorkspace = {
|
||||
workspace: IWorkspace | null;
|
||||
|
|
|
|||
171
web/core/components/workspace/delete-workspace-form.tsx
Normal file
171
web/core/components/workspace/delete-workspace-form.tsx
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
// types
|
||||
import { WORKSPACE_DELETED } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import type { IWorkspace } from "@plane/types";
|
||||
// ui
|
||||
import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// constants
|
||||
// hooks
|
||||
import { cn } from "@plane/utils";
|
||||
import { useEventTracker, useWorkspace } from "@/hooks/store";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
|
||||
type Props = {
|
||||
data: IWorkspace | null;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
const defaultValues = {
|
||||
workspaceName: "",
|
||||
confirmDelete: "",
|
||||
};
|
||||
|
||||
export const DeleteWorkspaceForm: React.FC<Props> = observer((props) => {
|
||||
const { data, onClose } = props;
|
||||
// router
|
||||
const router = useAppRouter();
|
||||
// store hooks
|
||||
const { captureWorkspaceEvent } = useEventTracker();
|
||||
const { deleteWorkspace } = useWorkspace();
|
||||
const { t } = useTranslation();
|
||||
// form info
|
||||
const {
|
||||
control,
|
||||
formState: { errors, isSubmitting },
|
||||
handleSubmit,
|
||||
reset,
|
||||
watch,
|
||||
} = useForm({ defaultValues });
|
||||
|
||||
const canDelete = watch("workspaceName") === data?.name && watch("confirmDelete") === "delete my workspace";
|
||||
|
||||
const handleClose = () => {
|
||||
onClose();
|
||||
reset(defaultValues);
|
||||
};
|
||||
|
||||
const onSubmit = async () => {
|
||||
if (!data || !canDelete) return;
|
||||
|
||||
await deleteWorkspace(data.slug)
|
||||
.then(() => {
|
||||
handleClose();
|
||||
router.push("/profile");
|
||||
captureWorkspaceEvent({
|
||||
eventName: WORKSPACE_DELETED,
|
||||
payload: {
|
||||
...data,
|
||||
state: "SUCCESS",
|
||||
element: "Workspace general settings page",
|
||||
},
|
||||
});
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: t("workspace_settings.settings.general.delete_modal.success_title"),
|
||||
message: t("workspace_settings.settings.general.delete_modal.success_message"),
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: t("workspace_settings.settings.general.delete_modal.error_title"),
|
||||
message: t("workspace_settings.settings.general.delete_modal.error_message"),
|
||||
});
|
||||
captureWorkspaceEvent({
|
||||
eventName: WORKSPACE_DELETED,
|
||||
payload: {
|
||||
...data,
|
||||
state: "FAILED",
|
||||
element: "Workspace general settings page",
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="flex flex-col gap-6 p-6">
|
||||
<div className="flex flex-col sm:flex-row items-center sm:items-start gap-4">
|
||||
<span
|
||||
className={cn(
|
||||
"flex-shrink-0 grid place-items-center rounded-full size-12 sm:size-10 bg-red-500/20 text-red-100"
|
||||
)}
|
||||
>
|
||||
<AlertTriangle className="size-5 text-red-600" aria-hidden="true" />
|
||||
</span>
|
||||
<div>
|
||||
<div className="text-center sm:text-left">
|
||||
<h3 className="text-lg font-medium">{t("workspace_settings.settings.general.delete_modal.title")}</h3>
|
||||
<p className="mt-1 text-sm text-custom-text-200">
|
||||
You are about to delete the workspace <span className="break-words font-semibold">{data?.name}</span>. If
|
||||
you confirm, you will lose access to all your work data in this workspace without any way to restore it.
|
||||
Tread very carefully.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="text-custom-text-200 mt-4">
|
||||
<p className="break-words text-sm ">Type in this workspace's name to continue.</p>
|
||||
<Controller
|
||||
control={control}
|
||||
name="workspaceName"
|
||||
render={({ field: { value, onChange, ref } }) => (
|
||||
<Input
|
||||
id="workspaceName"
|
||||
name="workspaceName"
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.workspaceName)}
|
||||
placeholder={data?.name}
|
||||
className="mt-2 w-full"
|
||||
autoComplete="off"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="text-custom-text-200 mt-4">
|
||||
<p className="text-sm">
|
||||
For final confirmation, type{" "}
|
||||
<span className="font-medium text-custom-text-100">delete my workspace </span>
|
||||
below.
|
||||
</p>
|
||||
<Controller
|
||||
control={control}
|
||||
name="confirmDelete"
|
||||
render={({ field: { value, onChange, ref } }) => (
|
||||
<Input
|
||||
id="confirmDelete"
|
||||
name="confirmDelete"
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.confirmDelete)}
|
||||
placeholder=""
|
||||
className="mt-2 w-full"
|
||||
autoComplete="off"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
<Button variant="danger" size="sm" type="submit" disabled={!canDelete} loading={isSubmitting}>
|
||||
{isSubmitting ? t("deleting") : t("confirm")}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
});
|
||||
|
|
@ -1,205 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// types
|
||||
import { WORKSPACE_DELETED } from "@plane/constants";
|
||||
import type { IWorkspace } from "@plane/types";
|
||||
// ui
|
||||
import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// constants
|
||||
// hooks
|
||||
import { useEventTracker, useWorkspace } from "@/hooks/store";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
data: IWorkspace | null;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
const defaultValues = {
|
||||
workspaceName: "",
|
||||
confirmDelete: "",
|
||||
};
|
||||
|
||||
export const DeleteWorkspaceModal: React.FC<Props> = observer((props) => {
|
||||
const { isOpen, data, onClose } = props;
|
||||
// router
|
||||
const router = useAppRouter();
|
||||
// store hooks
|
||||
const { captureWorkspaceEvent } = useEventTracker();
|
||||
const { deleteWorkspace } = useWorkspace();
|
||||
// form info
|
||||
const {
|
||||
control,
|
||||
formState: { errors, isSubmitting },
|
||||
handleSubmit,
|
||||
reset,
|
||||
watch,
|
||||
} = useForm({ defaultValues });
|
||||
|
||||
const canDelete = watch("workspaceName") === data?.name && watch("confirmDelete") === "delete my workspace";
|
||||
|
||||
const handleClose = () => {
|
||||
const timer = setTimeout(() => {
|
||||
reset(defaultValues);
|
||||
clearTimeout(timer);
|
||||
}, 350);
|
||||
|
||||
onClose();
|
||||
};
|
||||
|
||||
const onSubmit = async () => {
|
||||
if (!data || !canDelete) return;
|
||||
|
||||
await deleteWorkspace(data.slug)
|
||||
.then(() => {
|
||||
handleClose();
|
||||
router.push("/");
|
||||
captureWorkspaceEvent({
|
||||
eventName: WORKSPACE_DELETED,
|
||||
payload: {
|
||||
...data,
|
||||
state: "SUCCESS",
|
||||
element: "Workspace general settings page",
|
||||
},
|
||||
});
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Success!",
|
||||
message: "Workspace deleted successfully.",
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Something went wrong. Please try again later.",
|
||||
});
|
||||
captureWorkspaceEvent({
|
||||
eventName: WORKSPACE_DELETED,
|
||||
payload: {
|
||||
...data,
|
||||
state: "FAILED",
|
||||
element: "Workspace general settings page",
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Transition.Root show={isOpen} as={React.Fragment}>
|
||||
<Dialog as="div" className="relative z-20" onClose={handleClose}>
|
||||
<Transition.Child
|
||||
as={React.Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-custom-backdrop transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 z-20 overflow-y-auto">
|
||||
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
|
||||
<Transition.Child
|
||||
as={React.Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-custom-background-100 text-left shadow-custom-shadow-md transition-all sm:my-8 sm:w-full sm:max-w-2xl">
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="flex flex-col gap-6 p-6">
|
||||
<div className="flex w-full items-center justify-start gap-6">
|
||||
<span className="place-items-center rounded-full bg-red-500/20 p-4">
|
||||
<AlertTriangle className="h-6 w-6 text-red-600" aria-hidden="true" />
|
||||
</span>
|
||||
<span className="flex items-center justify-start">
|
||||
<h3 className="text-xl font-medium 2xl:text-2xl">Delete workspace</h3>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<span>
|
||||
<p className="text-sm leading-7 text-custom-text-200">
|
||||
Are you sure you want to delete workspace{" "}
|
||||
<span className="break-words font-semibold">{data?.name}</span>? All of the data related to the
|
||||
workspace will be permanently removed. This action cannot be undone.
|
||||
</p>
|
||||
</span>
|
||||
|
||||
<div className="text-custom-text-200">
|
||||
<p className="break-words text-sm ">
|
||||
Enter the workspace name <span className="font-medium text-custom-text-100">{data?.name}</span> to
|
||||
continue:
|
||||
</p>
|
||||
<Controller
|
||||
control={control}
|
||||
name="workspaceName"
|
||||
render={({ field: { value, onChange, ref } }) => (
|
||||
<Input
|
||||
id="workspaceName"
|
||||
name="workspaceName"
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.workspaceName)}
|
||||
placeholder="Workspace name"
|
||||
className="mt-2 w-full"
|
||||
autoComplete="off"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="text-custom-text-200">
|
||||
<p className="text-sm">
|
||||
To confirm, type <span className="font-medium text-custom-text-100">delete my workspace</span>{" "}
|
||||
below:
|
||||
</p>
|
||||
<Controller
|
||||
control={control}
|
||||
name="confirmDelete"
|
||||
render={({ field: { value, onChange, ref } }) => (
|
||||
<Input
|
||||
id="confirmDelete"
|
||||
name="confirmDelete"
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.confirmDelete)}
|
||||
placeholder="Enter 'delete my workspace'"
|
||||
className="mt-2 w-full"
|
||||
autoComplete="off"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="danger" size="sm" type="submit" disabled={!canDelete} loading={isSubmitting}>
|
||||
{isSubmitting ? "Deleting" : "Delete workspace"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
);
|
||||
});
|
||||
|
|
@ -3,6 +3,5 @@ export * from "./sidebar";
|
|||
export * from "./views";
|
||||
export * from "./confirm-workspace-member-remove";
|
||||
export * from "./create-workspace-form";
|
||||
export * from "./delete-workspace-modal";
|
||||
export * from "./logo";
|
||||
export * from "./invite-modal";
|
||||
|
|
|
|||
1
web/ee/components/workspace/delete-workspace-modal.tsx
Normal file
1
web/ee/components/workspace/delete-workspace-modal.tsx
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from "ce/components/workspace/delete-workspace-modal";
|
||||
Loading…
Add table
Add a link
Reference in a new issue