feat: init
This commit is contained in:
99
apps/web/core/components/pages/modals/create-page-modal.tsx
Normal file
99
apps/web/core/components/pages/modals/create-page-modal.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
import type { FC } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
// constants
|
||||
import type { EPageAccess } from "@plane/constants";
|
||||
import { PROJECT_PAGE_TRACKER_EVENTS } from "@plane/constants";
|
||||
import type { TPage } from "@plane/types";
|
||||
// ui
|
||||
import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
|
||||
// hooks
|
||||
import { captureSuccess, captureError } from "@/helpers/event-tracker.helper";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
// plane web hooks
|
||||
import type { EPageStoreType } from "@/plane-web/hooks/store";
|
||||
import { usePageStore } from "@/plane-web/hooks/store";
|
||||
// local imports
|
||||
import { PageForm } from "./page-form";
|
||||
|
||||
type Props = {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
isModalOpen: boolean;
|
||||
pageAccess?: EPageAccess;
|
||||
handleModalClose: () => void;
|
||||
redirectionEnabled?: boolean;
|
||||
storeType: EPageStoreType;
|
||||
};
|
||||
|
||||
export const CreatePageModal: FC<Props> = (props) => {
|
||||
const {
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
isModalOpen,
|
||||
pageAccess,
|
||||
handleModalClose,
|
||||
redirectionEnabled = false,
|
||||
storeType,
|
||||
} = props;
|
||||
// states
|
||||
const [pageFormData, setPageFormData] = useState<Partial<TPage>>({
|
||||
id: undefined,
|
||||
name: "",
|
||||
logo_props: undefined,
|
||||
});
|
||||
// router
|
||||
const router = useAppRouter();
|
||||
// store hooks
|
||||
const { createPage } = usePageStore(storeType);
|
||||
const handlePageFormData = <T extends keyof TPage>(key: T, value: TPage[T]) =>
|
||||
setPageFormData((prev) => ({ ...prev, [key]: value }));
|
||||
|
||||
// update page access in form data when page access from the store changes
|
||||
useEffect(() => {
|
||||
setPageFormData((prev) => ({ ...prev, access: pageAccess }));
|
||||
}, [pageAccess]);
|
||||
|
||||
const handleStateClear = () => {
|
||||
setPageFormData({ id: undefined, name: "", access: pageAccess });
|
||||
handleModalClose();
|
||||
};
|
||||
|
||||
const handleFormSubmit = async () => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
try {
|
||||
const pageData = await createPage(pageFormData);
|
||||
if (pageData) {
|
||||
captureSuccess({
|
||||
eventName: PROJECT_PAGE_TRACKER_EVENTS.create,
|
||||
payload: {
|
||||
id: pageData.id,
|
||||
},
|
||||
});
|
||||
handleStateClear();
|
||||
if (redirectionEnabled) router.push(`/${workspaceSlug}/projects/${projectId}/pages/${pageData.id}`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
captureError({
|
||||
eventName: PROJECT_PAGE_TRACKER_EVENTS.create,
|
||||
error,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalCore
|
||||
isOpen={isModalOpen}
|
||||
handleClose={handleModalClose}
|
||||
position={EModalPosition.TOP}
|
||||
width={EModalWidth.XXL}
|
||||
>
|
||||
<PageForm
|
||||
formData={pageFormData}
|
||||
handleFormData={handlePageFormData}
|
||||
handleModalClose={handleStateClear}
|
||||
handleFormSubmit={handleFormSubmit}
|
||||
/>
|
||||
</ModalCore>
|
||||
);
|
||||
};
|
||||
99
apps/web/core/components/pages/modals/delete-page-modal.tsx
Normal file
99
apps/web/core/components/pages/modals/delete-page-modal.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// ui
|
||||
import { useParams } from "next/navigation";
|
||||
import { PROJECT_PAGE_TRACKER_EVENTS } from "@plane/constants";
|
||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||
import { AlertModalCore } from "@plane/ui";
|
||||
// constants
|
||||
// hooks
|
||||
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
|
||||
// plane web hooks
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
import type { EPageStoreType } from "@/plane-web/hooks/store";
|
||||
import { usePageStore } from "@/plane-web/hooks/store";
|
||||
// store
|
||||
import type { TPageInstance } from "@/store/pages/base-page";
|
||||
|
||||
type TConfirmPageDeletionProps = {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
page: TPageInstance;
|
||||
storeType: EPageStoreType;
|
||||
};
|
||||
|
||||
export const DeletePageModal: React.FC<TConfirmPageDeletionProps> = observer((props) => {
|
||||
const { isOpen, onClose, page, storeType } = props;
|
||||
// states
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
// store hooks
|
||||
const { removePage } = usePageStore(storeType);
|
||||
if (!page || !page.id) return null;
|
||||
// derived values
|
||||
const { id: pageId, name } = page;
|
||||
|
||||
const handleClose = () => {
|
||||
setIsDeleting(false);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const router = useAppRouter();
|
||||
const { pageId: routePageId } = useParams();
|
||||
|
||||
const handleDelete = async () => {
|
||||
setIsDeleting(true);
|
||||
await removePage({ pageId })
|
||||
.then(() => {
|
||||
captureSuccess({
|
||||
eventName: PROJECT_PAGE_TRACKER_EVENTS.delete,
|
||||
payload: {
|
||||
id: pageId,
|
||||
},
|
||||
});
|
||||
handleClose();
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Success!",
|
||||
message: "Page deleted successfully.",
|
||||
});
|
||||
|
||||
if (routePageId) {
|
||||
router.back();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
captureError({
|
||||
eventName: PROJECT_PAGE_TRACKER_EVENTS.delete,
|
||||
payload: {
|
||||
id: pageId,
|
||||
},
|
||||
});
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Page could not be deleted. Please try again.",
|
||||
});
|
||||
});
|
||||
|
||||
setIsDeleting(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<AlertModalCore
|
||||
handleClose={handleClose}
|
||||
handleSubmit={handleDelete}
|
||||
isSubmitting={isDeleting}
|
||||
isOpen={isOpen}
|
||||
title="Delete page"
|
||||
content={
|
||||
<>
|
||||
Are you sure you want to delete page-{" "}
|
||||
<span className="break-words font-medium text-custom-text-100 break-all">{name}</span> ? The Page will be
|
||||
deleted permanently. This action cannot be undone.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
});
|
||||
286
apps/web/core/components/pages/modals/export-page-modal.tsx
Normal file
286
apps/web/core/components/pages/modals/export-page-modal.tsx
Normal file
@@ -0,0 +1,286 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import type { PageProps } from "@react-pdf/renderer";
|
||||
import { pdf } from "@react-pdf/renderer";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
// plane editor
|
||||
import type { EditorRefApi } from "@plane/editor";
|
||||
// plane ui
|
||||
import { Button } from "@plane/propel/button";
|
||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||
import { CustomSelect, EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
|
||||
// components
|
||||
import { PDFDocument } from "@/components/editor/pdf";
|
||||
// hooks
|
||||
import { useParseEditorContent } from "@/hooks/use-parse-editor-content";
|
||||
|
||||
type Props = {
|
||||
editorRef: EditorRefApi | null;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
pageTitle: string;
|
||||
};
|
||||
|
||||
type TExportFormats = "pdf" | "markdown";
|
||||
type TPageFormats = Exclude<PageProps["size"], undefined>;
|
||||
type TContentVariety = "everything" | "no-assets";
|
||||
|
||||
type TFormValues = {
|
||||
export_format: TExportFormats;
|
||||
page_format: TPageFormats;
|
||||
content_variety: TContentVariety;
|
||||
};
|
||||
|
||||
const EXPORT_FORMATS: {
|
||||
key: TExportFormats;
|
||||
label: string;
|
||||
}[] = [
|
||||
{
|
||||
key: "pdf",
|
||||
label: "PDF",
|
||||
},
|
||||
{
|
||||
key: "markdown",
|
||||
label: "Markdown",
|
||||
},
|
||||
];
|
||||
|
||||
const PAGE_FORMATS: {
|
||||
key: TPageFormats;
|
||||
label: string;
|
||||
}[] = [
|
||||
{
|
||||
key: "A4",
|
||||
label: "A4",
|
||||
},
|
||||
{
|
||||
key: "A3",
|
||||
label: "A3",
|
||||
},
|
||||
{
|
||||
key: "A2",
|
||||
label: "A2",
|
||||
},
|
||||
{
|
||||
key: "LETTER",
|
||||
label: "Letter",
|
||||
},
|
||||
{
|
||||
key: "LEGAL",
|
||||
label: "Legal",
|
||||
},
|
||||
{
|
||||
key: "TABLOID",
|
||||
label: "Tabloid",
|
||||
},
|
||||
];
|
||||
|
||||
const CONTENT_VARIETY: {
|
||||
key: TContentVariety;
|
||||
label: string;
|
||||
}[] = [
|
||||
{
|
||||
key: "everything",
|
||||
label: "Everything",
|
||||
},
|
||||
{
|
||||
key: "no-assets",
|
||||
label: "No images",
|
||||
},
|
||||
];
|
||||
|
||||
const defaultValues: TFormValues = {
|
||||
export_format: "pdf",
|
||||
page_format: "A4",
|
||||
content_variety: "everything",
|
||||
};
|
||||
|
||||
export const ExportPageModal: React.FC<Props> = (props) => {
|
||||
const { editorRef, isOpen, onClose, pageTitle } = props;
|
||||
// states
|
||||
const [isExporting, setIsExporting] = useState(false);
|
||||
// form info
|
||||
const { control, reset, watch } = useForm<TFormValues>({
|
||||
defaultValues,
|
||||
});
|
||||
// parse editor content
|
||||
const { replaceCustomComponentsFromHTMLContent, replaceCustomComponentsFromMarkdownContent } =
|
||||
useParseEditorContent();
|
||||
// derived values
|
||||
const selectedExportFormat = watch("export_format");
|
||||
const selectedPageFormat = watch("page_format");
|
||||
const selectedContentVariety = watch("content_variety");
|
||||
const isPDFSelected = selectedExportFormat === "pdf";
|
||||
const fileName = pageTitle
|
||||
?.toLowerCase()
|
||||
?.replace(/[^a-z0-9-_]/g, "-")
|
||||
.replace(/-+/g, "-");
|
||||
// handle modal close
|
||||
const handleClose = () => {
|
||||
onClose();
|
||||
setTimeout(() => {
|
||||
reset();
|
||||
}, 300);
|
||||
};
|
||||
|
||||
const initiateDownload = (blob: Blob, filename: string) => {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
link.click();
|
||||
setTimeout(() => {
|
||||
URL.revokeObjectURL(url);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
// handle export as a PDF
|
||||
const handleExportAsPDF = async () => {
|
||||
try {
|
||||
const pageContent = `<h1 class="page-title">${pageTitle}</h1>${editorRef?.getDocument().html ?? "<p></p>"}`;
|
||||
const parsedPageContent = await replaceCustomComponentsFromHTMLContent({
|
||||
htmlContent: pageContent,
|
||||
noAssets: selectedContentVariety === "no-assets",
|
||||
});
|
||||
|
||||
const blob = await pdf(<PDFDocument content={parsedPageContent} pageFormat={selectedPageFormat} />).toBlob();
|
||||
initiateDownload(blob, `${fileName}-${selectedPageFormat.toString().toLowerCase()}.pdf`);
|
||||
} catch (error) {
|
||||
throw new Error(`Error in exporting as a PDF: ${error}`);
|
||||
}
|
||||
};
|
||||
// handle export as markdown
|
||||
const handleExportAsMarkdown = async () => {
|
||||
try {
|
||||
const markdownContent = editorRef?.getMarkDown() ?? "";
|
||||
const parsedMarkdownContent = replaceCustomComponentsFromMarkdownContent({
|
||||
markdownContent,
|
||||
noAssets: selectedContentVariety === "no-assets",
|
||||
});
|
||||
|
||||
const blob = new Blob([parsedMarkdownContent], { type: "text/markdown" });
|
||||
initiateDownload(blob, `${fileName}.md`);
|
||||
} catch (error) {
|
||||
throw new Error(`Error in exporting as markdown: ${error}`);
|
||||
}
|
||||
};
|
||||
// handle export
|
||||
const handleExport = async () => {
|
||||
setIsExporting(true);
|
||||
try {
|
||||
if (selectedExportFormat === "pdf") {
|
||||
await handleExportAsPDF();
|
||||
}
|
||||
if (selectedExportFormat === "markdown") {
|
||||
await handleExportAsMarkdown();
|
||||
}
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Success!",
|
||||
message: "Page exported successfully.",
|
||||
});
|
||||
handleClose();
|
||||
} catch (error) {
|
||||
console.error("Error in exporting page:", error);
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Page could not be exported. Please try again later.",
|
||||
});
|
||||
} finally {
|
||||
setIsExporting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.CENTER} width={EModalWidth.SM}>
|
||||
<div>
|
||||
<div className="p-5 space-y-5">
|
||||
<h3 className="text-xl font-medium text-custom-text-200">Export page</h3>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<h6 className="flex-shrink-0 text-sm text-custom-text-200">Export format</h6>
|
||||
<Controller
|
||||
control={control}
|
||||
name="export_format"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<CustomSelect
|
||||
label={EXPORT_FORMATS.find((format) => format.key === value)?.label}
|
||||
buttonClassName="border-none"
|
||||
value={value}
|
||||
onChange={(val: TExportFormats) => onChange(val)}
|
||||
className="flex-shrink-0"
|
||||
placement="bottom-end"
|
||||
>
|
||||
{EXPORT_FORMATS.map((format) => (
|
||||
<CustomSelect.Option key={format.key} value={format.key}>
|
||||
{format.label}
|
||||
</CustomSelect.Option>
|
||||
))}
|
||||
</CustomSelect>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<h6 className="flex-shrink-0 text-sm text-custom-text-200">Include content</h6>
|
||||
<Controller
|
||||
control={control}
|
||||
name="content_variety"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<CustomSelect
|
||||
label={CONTENT_VARIETY.find((variety) => variety.key === value)?.label}
|
||||
buttonClassName="border-none"
|
||||
value={value}
|
||||
onChange={(val: TContentVariety) => onChange(val)}
|
||||
className="flex-shrink-0"
|
||||
placement="bottom-end"
|
||||
>
|
||||
{CONTENT_VARIETY.map((variety) => (
|
||||
<CustomSelect.Option key={variety.key} value={variety.key}>
|
||||
{variety.label}
|
||||
</CustomSelect.Option>
|
||||
))}
|
||||
</CustomSelect>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{isPDFSelected && (
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<h6 className="flex-shrink-0 text-sm text-custom-text-200">Page format</h6>
|
||||
<Controller
|
||||
control={control}
|
||||
name="page_format"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<CustomSelect
|
||||
label={PAGE_FORMATS.find((format) => format.key === value)?.label}
|
||||
buttonClassName="border-none"
|
||||
value={value}
|
||||
onChange={(val: TPageFormats) => onChange(val)}
|
||||
className="flex-shrink-0"
|
||||
placement="bottom-end"
|
||||
>
|
||||
{PAGE_FORMATS.map((format) => (
|
||||
<CustomSelect.Option key={format.key.toString()} value={format.key}>
|
||||
{format.label}
|
||||
</CustomSelect.Option>
|
||||
))}
|
||||
</CustomSelect>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-5 py-4 flex items-center justify-end gap-2 border-t-[0.5px] border-custom-border-200">
|
||||
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="primary" size="sm" loading={isExporting} onClick={handleExport}>
|
||||
{isExporting ? "Exporting" : "Export"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</ModalCore>
|
||||
);
|
||||
};
|
||||
157
apps/web/core/components/pages/modals/page-form.tsx
Normal file
157
apps/web/core/components/pages/modals/page-form.tsx
Normal file
@@ -0,0 +1,157 @@
|
||||
"use client";
|
||||
|
||||
import type { FormEvent } from "react";
|
||||
import { useState } from "react";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import { Globe2, Lock } from "lucide-react";
|
||||
// plane imports
|
||||
import { ETabIndices, EPageAccess } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { Button } from "@plane/propel/button";
|
||||
import { PageIcon } from "@plane/propel/icons";
|
||||
import type { TPage } from "@plane/types";
|
||||
import { EmojiIconPicker, EmojiIconPickerTypes, Input } from "@plane/ui";
|
||||
import { convertHexEmojiToDecimal, getTabIndex } from "@plane/utils";
|
||||
// components
|
||||
import { AccessField } from "@/components/common/access-field";
|
||||
import { Logo } from "@/components/common/logo";
|
||||
// hooks
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
|
||||
type Props = {
|
||||
formData: Partial<TPage>;
|
||||
handleFormData: <T extends keyof TPage>(key: T, value: TPage[T]) => void;
|
||||
handleModalClose: () => void;
|
||||
handleFormSubmit: () => Promise<void>;
|
||||
};
|
||||
|
||||
const PAGE_ACCESS_SPECIFIERS: {
|
||||
key: EPageAccess;
|
||||
i18n_label: string;
|
||||
icon: LucideIcon;
|
||||
}[] = [
|
||||
{ key: EPageAccess.PUBLIC, i18n_label: "common.access.public", icon: Globe2 },
|
||||
{ key: EPageAccess.PRIVATE, i18n_label: "common.access.private", icon: Lock },
|
||||
];
|
||||
|
||||
export const PageForm: React.FC<Props> = (props) => {
|
||||
const { formData, handleFormData, handleModalClose, handleFormSubmit } = props;
|
||||
// hooks
|
||||
const { isMobile } = usePlatformOS();
|
||||
const { t } = useTranslation();
|
||||
// state
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const i18n_access_label = PAGE_ACCESS_SPECIFIERS.find((access) => access.key === formData.access)?.i18n_label;
|
||||
|
||||
const { getIndex } = getTabIndex(ETabIndices.PROJECT_PAGE, isMobile);
|
||||
|
||||
const handlePageFormSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
await handleFormSubmit();
|
||||
setIsSubmitting(false);
|
||||
} catch {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const isTitleLengthMoreThan255Character = formData.name ? formData.name.length > 255 : false;
|
||||
|
||||
return (
|
||||
<form onSubmit={handlePageFormSubmit}>
|
||||
<div className="space-y-5 p-5">
|
||||
<h3 className="text-xl font-medium text-custom-text-200">Create page</h3>
|
||||
<div className="flex items-start gap-2 h-9 w-full">
|
||||
<EmojiIconPicker
|
||||
isOpen={isOpen}
|
||||
handleToggle={(val: boolean) => setIsOpen(val)}
|
||||
className="flex items-center justify-center flex-shrink0"
|
||||
buttonClassName="flex items-center justify-center"
|
||||
label={
|
||||
<span className="grid h-9 w-9 place-items-center rounded-md bg-custom-background-90">
|
||||
<>
|
||||
{formData?.logo_props?.in_use ? (
|
||||
<Logo logo={formData?.logo_props} size={18} type="lucide" />
|
||||
) : (
|
||||
<PageIcon className="h-4 w-4 text-custom-text-300" />
|
||||
)}
|
||||
</>
|
||||
</span>
|
||||
}
|
||||
onChange={(val: any) => {
|
||||
let logoValue = {};
|
||||
|
||||
if (val?.type === "emoji")
|
||||
logoValue = {
|
||||
value: convertHexEmojiToDecimal(val.value.unified),
|
||||
url: val.value.imageUrl,
|
||||
};
|
||||
else if (val?.type === "icon") logoValue = val.value;
|
||||
|
||||
handleFormData("logo_props", {
|
||||
in_use: val?.type,
|
||||
[val?.type]: logoValue,
|
||||
});
|
||||
setIsOpen(false);
|
||||
}}
|
||||
defaultIconColor={
|
||||
formData?.logo_props?.in_use && formData?.logo_props?.in_use === "icon"
|
||||
? formData?.logo_props?.icon?.color
|
||||
: undefined
|
||||
}
|
||||
defaultOpen={
|
||||
formData?.logo_props?.in_use && formData?.logo_props?.in_use === "emoji"
|
||||
? EmojiIconPickerTypes.EMOJI
|
||||
: EmojiIconPickerTypes.ICON
|
||||
}
|
||||
/>
|
||||
<div className="space-y-1 flew-grow w-full">
|
||||
<Input
|
||||
id="name"
|
||||
type="text"
|
||||
value={formData.name}
|
||||
onChange={(e) => handleFormData("name", e.target.value)}
|
||||
placeholder="Title"
|
||||
className="w-full resize-none text-base"
|
||||
tabIndex={getIndex("name")}
|
||||
required
|
||||
autoFocus
|
||||
/>
|
||||
{isTitleLengthMoreThan255Character && (
|
||||
<span className="text-xs text-red-500">Max length of the name should be less than 255 characters</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-5 py-4 flex items-center justify-between gap-2 border-t-[0.5px] border-custom-border-200">
|
||||
<div className="flex items-center gap-2">
|
||||
<AccessField
|
||||
onChange={(access) => handleFormData("access", access)}
|
||||
value={formData?.access ?? EPageAccess.PUBLIC}
|
||||
accessSpecifiers={PAGE_ACCESS_SPECIFIERS}
|
||||
isMobile={isMobile}
|
||||
/>
|
||||
<h6 className="text-xs font-medium">{t(i18n_access_label || "")}</h6>
|
||||
</div>
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<Button variant="neutral-primary" size="sm" onClick={handleModalClose} tabIndex={getIndex("cancel")}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
type="submit"
|
||||
loading={isSubmitting}
|
||||
disabled={isTitleLengthMoreThan255Character}
|
||||
tabIndex={getIndex("submit")}
|
||||
>
|
||||
{isSubmitting ? "Creating" : "Create Page"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user