snapshot of https://github.com/makeplane/plane.git @ d868349a55de79979eb8b98701bf1702c67ad634 (2026-09-15)
Some checks failed
CodeQL / Analyze (javascript) (push) Failing after 16s
CodeQL / Analyze (python) (push) Failing after 3s
i18n sync check / check:sync (push) Failing after 3s
React Doctor / react-doctor (push) Failing after 16s
Branch Build CE / Build-Push Admin Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Admin Manifest (push) Has been cancelled
Branch Build CE / Build-Push Web Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Web Manifest (push) Has been cancelled
Branch Build CE / Build-Push Space Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Proxy Manifest (push) Has been cancelled
Branch Build CE / Merge Space Manifest (push) Has been cancelled
Branch Build CE / Prepare AIO Assets (push) Has been cancelled
Branch Build CE / Build Setup (push) Has been cancelled
Branch Build CE / Build-Push Live Collaboration Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Live Collaboration Manifest (push) Has been cancelled
Branch Build CE / Build-Push API Server Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge API Server Manifest (push) Has been cancelled
Branch Build CE / Build-Push Proxy Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Build-Push AIO Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge AIO Manifest (push) Has been cancelled
Branch Build CE / Upload Build Assets (push) Has been cancelled
Branch Build CE / Build Release (push) Has been cancelled

This commit is contained in:
upstream-mirror
2026-09-15 15:14:58 +05:00
commit 23059a1de3
5252 changed files with 573834 additions and 0 deletions

View File

@@ -0,0 +1,93 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { useEffect, useState } from "react";
// constants
import type { EPageAccess } from "@plane/constants";
import type { TPage } from "@plane/types";
// ui
import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
// hooks
import { useAppRouter } from "@/hooks/use-app-router";
// plane web hooks
import type { EPageStoreType } from "@/hooks/store";
import { usePageStore } from "@/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 function CreatePageModal(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) {
handleStateClear();
if (redirectionEnabled) router.push(`/${workspaceSlug}/projects/${projectId}/pages/${pageData.id}`);
}
} catch (error) {
console.error(error);
}
};
return (
<ModalCore
isOpen={isModalOpen}
handleClose={handleModalClose}
position={EModalPosition.TOP}
width={EModalWidth.XXL}
>
<PageForm
formData={pageFormData}
handleFormData={handlePageFormData}
handleModalClose={handleStateClear}
handleFormSubmit={handleFormSubmit}
/>
</ModalCore>
);
}

View File

@@ -0,0 +1,92 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { useState } from "react";
import { observer } from "mobx-react";
// ui
import { useParams } from "next/navigation";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import { AlertModalCore } from "@plane/ui";
import { getPageName } from "@plane/utils";
// constants
// plane web hooks
import { useAppRouter } from "@/hooks/use-app-router";
import type { EPageStoreType } from "@/hooks/store";
import { usePageStore } from "@/hooks/store";
// store
import type { TPageInstance } from "@/store/pages/base-page";
type TConfirmPageDeletionProps = {
isOpen: boolean;
onClose: () => void;
page: TPageInstance;
storeType: EPageStoreType;
};
export const DeletePageModal = observer(function DeletePageModal(props: TConfirmPageDeletionProps) {
const { isOpen, onClose, page, storeType } = props;
// states
const [isDeleting, setIsDeleting] = useState(false);
// store hooks
const { removePage } = usePageStore(storeType);
// derived values
const { id: pageId, name } = page;
const handleClose = () => {
setIsDeleting(false);
onClose();
};
const router = useAppRouter();
const { pageId: routePageId } = useParams();
const handleDelete = async () => {
if (!pageId) return;
setIsDeleting(true);
await removePage({ pageId })
.then(() => {
handleClose();
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
message: "Page deleted successfully.",
});
if (routePageId) {
router.back();
}
})
.catch(() => {
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Page could not be deleted. Please try again.",
});
});
setIsDeleting(false);
};
if (!page || !page.id) return null;
return (
<AlertModalCore
handleClose={handleClose}
handleSubmit={handleDelete}
isSubmitting={isDeleting}
isOpen={isOpen}
title="Delete page"
content={
<>
Are you sure you want to delete page-{" "}
<span className="font-medium break-words break-all text-primary">{getPageName(name)}</span> ? The Page will be
deleted permanently. This action cannot be undone.
</>
}
/>
);
});

View File

@@ -0,0 +1,295 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { useState } from "react";
import type { PageProps } from "@react-pdf/renderer";
import { pdf } from "@react-pdf/renderer";
import { Controller, useForm } from "react-hook-form";
import { useParams } from "react-router";
// 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 function ExportPageModal(props: Props) {
const { editorRef, isOpen, onClose, pageTitle } = props;
// states
const [isExporting, setIsExporting] = useState(false);
// params
const { workspaceSlug, projectId } = useParams();
// form info
const { control, reset, watch } = useForm<TFormValues>({
defaultValues,
});
// parse editor content
const { replaceCustomComponentsFromHTMLContent, replaceCustomComponentsFromMarkdownContent } = useParseEditorContent({
projectId,
workspaceSlug: workspaceSlug ?? "",
});
// 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="space-y-5 p-5">
<h3 className="text-18 font-medium text-secondary">Export page</h3>
<div className="space-y-3">
<div className="flex items-center justify-between gap-2">
<h6 className="flex-shrink-0 text-13 text-secondary">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-13 text-secondary">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-13 text-secondary">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="flex items-center justify-end gap-2 border-t-[0.5px] border-subtle px-5 py-4">
<Button variant="secondary" size="lg" onClick={handleClose}>
Cancel
</Button>
<Button variant="primary" size="lg" loading={isExporting} onClick={handleExport}>
{isExporting ? "Exporting" : "Export"}
</Button>
</div>
</div>
</ModalCore>
);
}

View File

@@ -0,0 +1,167 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import type { FormEvent } from "react";
import { useState } from "react";
import type { LucideIcon } from "lucide-react";
// plane imports
import { Input, InputGroup } from "@makeplane/propel/components/input";
import { ETabIndices, EPageAccess } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Button } from "@plane/propel/button";
import { EmojiPicker, EmojiIconPickerTypes, Logo } from "@plane/propel/emoji-icon-picker";
import { GlobeOutline, LockOutline, PagesOutline } from "@makeplane/propel/icons";
import type { ISvgIcons } from "@plane/propel/icons";
import type { TPage } from "@plane/types";
import { getTabIndex } from "@plane/utils";
// components
import { AccessField } from "@/components/common/access-field";
// 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 | React.FC<ISvgIcons>;
}[] = [
{ key: EPageAccess.PUBLIC, i18n_label: "common.access.public", icon: GlobeOutline },
{ key: EPageAccess.PRIVATE, i18n_label: "common.access.private", icon: LockOutline },
];
export function PageForm(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-18 font-medium text-secondary">Create page</h3>
<div className="flex h-9 w-full items-start gap-2">
<EmojiPicker
isOpen={isOpen}
handleToggle={(val: boolean) => setIsOpen(val)}
className="flex-shrink0 flex items-center justify-center"
buttonClassName="flex items-center justify-center bg-layer-2 hover:bg-layer-2-hover rounded-md"
label={
<span className="grid h-9 w-9 place-items-center rounded-md">
<>
{formData?.logo_props?.in_use ? (
<Logo logo={formData?.logo_props} size={18} type="lucide" />
) : (
<PagesOutline className="h-4 w-4 text-tertiary" />
)}
</>
</span>
}
onChange={(val: any) => {
let logoValue = {};
if (val?.type === "emoji")
logoValue = {
value: val.value,
url: undefined,
};
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="flew-grow w-full space-y-1">
<InputGroup size="2xl">
<Input
size="2xl"
id="name"
type="text"
value={formData.name}
onChange={(e) => handleFormData("name", e.target.value)}
placeholder="Title"
tabIndex={getIndex("name")}
required
autoFocus
/>
</InputGroup>
{isTitleLengthMoreThan255Character && (
<span className="text-11 text-danger-primary">
Max length of the name should be less than 255 characters
</span>
)}
</div>
</div>
</div>
<div className="flex items-center justify-between gap-2 border-t-[0.5px] border-subtle px-5 py-4">
<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-11 font-medium">{t(i18n_access_label || "")}</h6>
</div>
<div className="flex items-center justify-end gap-2">
<Button variant="secondary" size="lg" onClick={handleModalClose} tabIndex={getIndex("cancel")}>
Cancel
</Button>
<Button
variant="primary"
size="lg"
type="submit"
loading={isSubmitting}
disabled={isTitleLengthMoreThan255Character}
tabIndex={getIndex("submit")}
>
{isSubmitting ? "Creating" : "Create Page"}
</Button>
</div>
</div>
</form>
);
}