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,18 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
// plane web imports
import { ORDERED_PAGE_NAVIGATION_TABS_LIST } from "@/components/pages/navigation-pane/tab-panels";
export * from "./root";
export * from "./types";
export const PAGE_NAVIGATION_PANE_WIDTH = 294;
export const PAGE_NAVIGATION_PANE_TABS_QUERY_PARAM = "paneTab";
export const PAGE_NAVIGATION_PANE_VERSION_QUERY_PARAM = "version";
export const PAGE_NAVIGATION_PANE_TAB_KEYS = ORDERED_PAGE_NAVIGATION_TABS_LIST.map((tab) => tab.key);

View File

@@ -0,0 +1,127 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { useCallback } from "react";
import { observer } from "mobx-react";
import { useRouter, useSearchParams } from "next/navigation";
import { ArrowRightCircle } from "lucide-react";
// plane imports
import { useTranslation } from "@plane/i18n";
import { Tabs } from "@makeplane/propel/components/tabs";
import { Tooltip } from "@makeplane/propel/components/tooltip";
// hooks
import { useQueryParams } from "@/hooks/use-query-params";
// plane web components
import type { TPageNavigationPaneTab } from "@/components/pages/navigation-pane/tab-panels";
// store
import type { EPageStoreType } from "@/hooks/store";
import type { TPageInstance } from "@/store/pages/base-page";
// local imports
import type { TPageRootHandlers } from "../editor/page-root";
import { PageNavigationPaneTabPanelsRoot } from "./tab-panels/root";
import { PageNavigationPaneTabsList } from "./tabs-list";
import type { INavigationPaneExtension } from "./types/extensions";
import {
PAGE_NAVIGATION_PANE_TABS_QUERY_PARAM,
PAGE_NAVIGATION_PANE_VERSION_QUERY_PARAM,
PAGE_NAVIGATION_PANE_WIDTH,
} from "./index";
type Props = {
handleClose: () => void;
isNavigationPaneOpen: boolean;
page: TPageInstance;
versionHistory: Pick<TPageRootHandlers, "fetchAllVersions" | "fetchVersionDetails">;
// Generic extension system for additional navigation pane content
extensions?: INavigationPaneExtension[];
storeType: EPageStoreType;
};
export const PageNavigationPaneRoot = observer(function PageNavigationPaneRoot(props: Props) {
const { handleClose, isNavigationPaneOpen, page, versionHistory, extensions = [], storeType } = props;
// navigation
const router = useRouter();
const searchParams = useSearchParams();
// query params
const { updateQueryParams } = useQueryParams();
// derived values
const navigationPaneQueryParam = searchParams.get(
PAGE_NAVIGATION_PANE_TABS_QUERY_PARAM
) as TPageNavigationPaneTab | null;
const activeTab: TPageNavigationPaneTab = navigationPaneQueryParam || "outline";
// Check if any extension is currently active based on query parameters
const ActiveExtension = extensions.find((extension) => {
const paneTabValue = searchParams.get(PAGE_NAVIGATION_PANE_TABS_QUERY_PARAM);
const hasVersionParam = searchParams.get(PAGE_NAVIGATION_PANE_VERSION_QUERY_PARAM);
// Extension is active ONLY when paneTab matches AND no regular navigation params are present
return paneTabValue === extension.triggerParam && !hasVersionParam;
});
// Don't show tabs when an extension is active
const showNavigationTabs = !ActiveExtension && isNavigationPaneOpen;
// Use extension width if available, otherwise fall back to default
const paneWidth = ActiveExtension?.width ?? PAGE_NAVIGATION_PANE_WIDTH;
// translation
const { t } = useTranslation();
const handleTabChange = useCallback(
(value: string) => {
const updatedTab = value as TPageNavigationPaneTab;
const isUpdatedTabInfo = updatedTab === "info";
const updatedRoute = updateQueryParams({
paramsToAdd: { [PAGE_NAVIGATION_PANE_TABS_QUERY_PARAM]: updatedTab },
paramsToRemove: !isUpdatedTabInfo ? [PAGE_NAVIGATION_PANE_VERSION_QUERY_PARAM] : undefined,
});
router.push(updatedRoute);
},
[router, updateQueryParams]
);
return (
<aside
className="flex h-full shrink-0 flex-col border-l border-subtle bg-surface-1 pt-3.5 transition-all duration-300 ease-out"
style={{
width: `${paneWidth}px`,
marginRight: isNavigationPaneOpen ? "0px" : `-${paneWidth}px`,
}}
>
<div className="mb-3.5 px-3.5">
<Tooltip label={t("page_navigation_pane.close_button")}>
<button
type="button"
className="grid size-3.5 place-items-center text-secondary transition-colors hover:text-primary"
onClick={handleClose}
aria-label={t("page_navigation_pane.close_button")}
>
<ArrowRightCircle className="size-3.5" />
</button>
</Tooltip>
</div>
<div className="animate-slide-in-right flex flex-1 flex-col overflow-hidden">
{ActiveExtension ? (
<ActiveExtension.component page={page} extensionData={ActiveExtension.data} storeType={storeType} />
) : showNavigationTabs ? (
// Row wrapper: Propel's Tabs root omits className, so it takes its full height by
// stretching as a flex item.
<div className="flex h-full w-full">
<Tabs variant="contained" value={activeTab} onValueChange={handleTabChange}>
<div className="flex h-full w-full flex-col">
<PageNavigationPaneTabsList />
<PageNavigationPaneTabPanelsRoot page={page} versionHistory={versionHistory} />
</div>
</Tabs>
</div>
) : null}
</div>
</aside>
);
});

View File

@@ -0,0 +1,118 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { useMemo } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { DownloadOutline } from "@makeplane/propel/icons";
// plane imports
import { CORE_EXTENSIONS } from "@plane/editor";
import type { TEditorAsset } from "@plane/editor";
import { useTranslation } from "@plane/i18n";
import { getEditorAssetDownloadSrc, getEditorAssetSrc } from "@plane/utils";
// store
import type { TPageInstance } from "@/store/pages/base-page";
// local import
import { PageNavigationPaneAssetsTabEmptyState } from "./empty-state/assets";
type Props = {
page: TPageInstance;
};
type AssetItemProps = {
asset: TEditorAsset;
page: TPageInstance;
};
const AssetItem = observer(function AssetItem(props: AssetItemProps) {
const { asset, page } = props;
// navigation
const { workspaceSlug } = useParams();
// derived values
const { project_ids } = page;
// translation
const { t } = useTranslation();
const assetSrc: string = useMemo(() => {
if (!asset.src || !workspaceSlug) return "";
if (asset.src.startsWith("http")) {
return asset.src;
} else {
return (
getEditorAssetSrc({
assetId: asset.src,
projectId: project_ids?.[0],
workspaceSlug: workspaceSlug.toString(),
}) ?? ""
);
}
}, [asset.src, project_ids, workspaceSlug]);
const assetDownloadSrc: string = useMemo(() => {
if (!asset.src || !workspaceSlug) return "";
if (asset.src.startsWith("http")) {
return asset.src;
} else {
return (
getEditorAssetDownloadSrc({
assetId: asset.src,
projectId: project_ids?.[0],
workspaceSlug: workspaceSlug.toString(),
}) ?? ""
);
}
}, [asset.src, project_ids, workspaceSlug]);
if ([CORE_EXTENSIONS.IMAGE, CORE_EXTENSIONS.CUSTOM_IMAGE].includes(asset.type as CORE_EXTENSIONS))
return (
<a
href={asset.href}
className="group/asset-item relative flex h-12 items-center gap-2 rounded-sm border border-subtle pr-2 transition-colors hover:bg-layer-1"
>
<div
className="h-12 w-11 flex-shrink-0 rounded-l-sm bg-cover bg-center bg-no-repeat"
style={{
backgroundImage: `url('${assetSrc}')`,
}}
/>
<div className="flex-1 space-y-0.5 truncate">
<p className="truncate text-13 font-medium">{asset.name}</p>
<div className="flex items-end justify-between gap-2">
<p className="shrink-0 text-11 text-secondary" />
<a
href={assetDownloadSrc}
target="_blank"
rel="noreferrer noopener"
className="pointer-events-none flex shrink-0 items-center gap-1 rounded-sm px-1 py-0.5 text-secondary opacity-0 transition-opacity group-hover/asset-item:pointer-events-auto group-hover/asset-item:opacity-100 hover:text-primary"
>
<DownloadOutline className="size-3 shrink-0" />
<span className="text-11 font-medium">{t("page_navigation_pane.tabs.assets.download_button")}</span>
</a>
</div>
</div>
</a>
);
return null;
});
export const PageNavigationPaneAssetsTabPanel = observer(function PageNavigationPaneAssetsTabPanel(props: Props) {
const { page } = props;
// derived values
const {
editor: { assetsList },
} = page;
if (assetsList.length === 0) return <PageNavigationPaneAssetsTabEmptyState />;
return (
<div className="mt-5 space-y-4 px-4">
{assetsList?.map((asset) => (
<AssetItem key={asset.id} asset={asset} page={page} />
))}
</div>
);
});

View File

@@ -0,0 +1,35 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { useTheme } from "next-themes";
// plane imports
import { useTranslation } from "@plane/i18n";
// assets
import darkAssetsAsset from "@/app/assets/empty-state/wiki/navigation-pane/assets-dark.webp?url";
import lightAssetsAsset from "@/app/assets/empty-state/wiki/navigation-pane/assets-light.webp?url";
export function PageNavigationPaneAssetsTabEmptyState() {
// theme hook
const { resolvedTheme } = useTheme();
// asset resolved path
const resolvedPath = resolvedTheme === "light" ? lightAssetsAsset : darkAssetsAsset;
// translation
const { t } = useTranslation();
return (
<div className="grid size-full place-items-center">
<div className="flex flex-col items-center gap-y-6 text-center">
<img src={resolvedPath} className="size-40 object-contain" alt="depicts the assets of a page" />
<div className="space-y-2.5">
<h4 className="text-14 font-medium">{t("page_navigation_pane.tabs.assets.empty_state.title")}</h4>
<p className="text-13 font-medium text-secondary">
{t("page_navigation_pane.tabs.assets.empty_state.description")}
</p>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,35 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { useTheme } from "next-themes";
// plane imports
import { useTranslation } from "@plane/i18n";
// assets
import darkOutlineAsset from "@/app/assets/empty-state/wiki/navigation-pane/outline-dark.webp?url";
import lightOutlineAsset from "@/app/assets/empty-state/wiki/navigation-pane/outline-light.webp?url";
export function PageNavigationPaneOutlineTabEmptyState() {
// theme hook
const { resolvedTheme } = useTheme();
// asset resolved path
const resolvedPath = resolvedTheme === "light" ? lightOutlineAsset : darkOutlineAsset;
// translation
const { t } = useTranslation();
return (
<div className="grid size-full place-items-center">
<div className="flex flex-col items-center gap-y-6 text-center">
<img src={resolvedPath} className="size-40 object-contain" alt="depicts the outline of a page" />
<div className="space-y-2.5">
<h4 className="text-14 font-medium">{t("page_navigation_pane.tabs.outline.empty_state.title")}</h4>
<p className="text-13 font-medium text-secondary">
{t("page_navigation_pane.tabs.outline.empty_state.description")}
</p>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,37 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
export type TPageNavigationPaneTab = "outline" | "info" | "assets";
export const PAGE_NAVIGATION_PANE_TABS_LIST: Record<
TPageNavigationPaneTab,
{
key: TPageNavigationPaneTab;
i18n_label: string;
}
> = {
outline: {
key: "outline",
i18n_label: "page_navigation_pane.tabs.outline.label",
},
info: {
key: "info",
i18n_label: "page_navigation_pane.tabs.info.label",
},
assets: {
key: "assets",
i18n_label: "page_navigation_pane.tabs.assets.label",
},
};
export const ORDERED_PAGE_NAVIGATION_TABS_LIST: {
key: TPageNavigationPaneTab;
i18n_label: string;
}[] = [
PAGE_NAVIGATION_PANE_TABS_LIST.outline,
PAGE_NAVIGATION_PANE_TABS_LIST.info,
PAGE_NAVIGATION_PANE_TABS_LIST.assets,
];

View File

@@ -0,0 +1,72 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { observer } from "mobx-react";
import Link from "next/link";
import { useParams } from "next/navigation";
// plane imports
import { Avatar } from "@makeplane/propel/components/avatar";
import { useTranslation } from "@plane/i18n";
import { calculateTimeAgoShort, getFileURL, renderFormattedDate } from "@plane/utils";
// hooks
import { useMember } from "@/hooks/store/use-member";
// store
import type { TPageInstance } from "@/store/pages/base-page";
type Props = {
page: TPageInstance;
};
export const PageNavigationPaneInfoTabActorsInfo = observer(function PageNavigationPaneInfoTabActorsInfo(props: Props) {
const { page } = props;
// navigation
const { workspaceSlug } = useParams();
// store hooks
const { getUserDetails } = useMember();
// derived values
const { owned_by, updated_by } = page;
const editorInformation = updated_by ? getUserDetails(updated_by) : undefined;
const creatorInformation = owned_by ? getUserDetails(owned_by) : undefined;
// translation
const { t } = useTranslation();
return (
<div className="mt-4 space-y-3">
<div>
<p className="text-11 font-medium text-tertiary">{t("page_navigation_pane.tabs.info.actors_info.edited_by")}</p>
<div className="mt-2 flex items-center justify-between gap-2 text-13 font-medium">
<Link href={`/${workspaceSlug?.toString()}/profile/${page.updated_by}`} className="flex items-center gap-1">
<Avatar
alt={editorInformation?.display_name}
fallback={editorInformation?.display_name?.[0]?.toUpperCase()}
src={getFileURL(editorInformation?.avatar_url ?? "")}
size="2xs"
/>
<span>{editorInformation?.display_name ?? t("common.deactivated_user")}</span>
</Link>
<span className="flex-shrink-0 text-tertiary">{calculateTimeAgoShort(page.updated_at ?? "")} ago</span>
</div>
</div>
<div>
<p className="text-11 font-medium text-tertiary">
{t("page_navigation_pane.tabs.info.actors_info.created_by")}
</p>
<div className="mt-2 flex items-center justify-between gap-2 text-13 font-medium">
<Link href={`/${workspaceSlug?.toString()}/profile/${page.created_by}`} className="flex items-center gap-1">
<Avatar
alt={creatorInformation?.display_name}
fallback={creatorInformation?.display_name?.[0]?.toUpperCase()}
src={getFileURL(creatorInformation?.avatar_url ?? "")}
size="2xs"
/>
<span>{creatorInformation?.display_name ?? t("common.deactivated_user")}</span>
</Link>
<span className="flex-shrink-0 text-tertiary">{renderFormattedDate(page.created_at)}</span>
</div>
</div>
</div>
);
});

View File

@@ -0,0 +1,90 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { useCallback, useEffect, useMemo, useState } from "react";
import { observer } from "mobx-react";
// plane imports
import type { TDocumentInfo } from "@plane/editor";
import { useTranslation } from "@plane/i18n";
import { getReadTimeFromWordsCount } from "@plane/utils";
// store
import type { TPageInstance } from "@/store/pages/base-page";
type Props = {
page: TPageInstance;
};
const DEFAULT_DOCUMENT_INFO: TDocumentInfo = {
words: 0,
characters: 0,
paragraphs: 0,
};
export const PageNavigationPaneInfoTabDocumentInfo = observer(function PageNavigationPaneInfoTabDocumentInfo(
props: Props
) {
const { page } = props;
// states
const [documentInfo, setDocumentInfo] = useState<TDocumentInfo>(DEFAULT_DOCUMENT_INFO);
// derived values
const {
editor: { editorRef },
} = page;
// translation
const { t } = useTranslation();
// subscribe to asset changes
useEffect(() => {
const unsubscribe = editorRef?.onDocumentInfoChange(setDocumentInfo);
// for initial render of this component to get the editor assets
setDocumentInfo(editorRef?.getDocumentInfo() ?? DEFAULT_DOCUMENT_INFO);
return () => {
unsubscribe?.();
};
}, [editorRef]);
const secondsToReadableTime = useCallback(() => {
const wordsCount = documentInfo.words;
const readTimeInSeconds = Number(getReadTimeFromWordsCount(wordsCount).toFixed(0));
return readTimeInSeconds < 60 ? `${readTimeInSeconds}s` : `${Math.ceil(readTimeInSeconds / 60)}m`;
}, [documentInfo.words]);
const documentInfoCards = useMemo(
() => [
{
key: "words-count",
title: t("page_navigation_pane.tabs.info.document_info.words"),
info: documentInfo.words,
},
{
key: "characters-count",
title: t("page_navigation_pane.tabs.info.document_info.characters"),
info: documentInfo.characters,
},
{
key: "paragraphs-count",
title: t("page_navigation_pane.tabs.info.document_info.paragraphs"),
info: documentInfo.paragraphs,
},
{
key: "read-time",
title: t("page_navigation_pane.tabs.info.document_info.read_time"),
info: secondsToReadableTime(),
},
],
[documentInfo, secondsToReadableTime, t]
);
return (
<div className="grid grid-cols-2 gap-2">
{documentInfoCards.map((card) => (
<div key={card.key} className="rounded-sm bg-layer-2 p-2">
<h6 className="text-14 font-semibold">{card.info}</h6>
<p className="mt-1.5 text-13 font-medium text-tertiary">{card.title}</p>
</div>
))}
</div>
);
});

View File

@@ -0,0 +1,34 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { observer } from "mobx-react";
// components
import type { TPageRootHandlers } from "@/components/pages/editor/page-root";
// store
import type { TPageInstance } from "@/store/pages/base-page";
// local imports
import { PageNavigationPaneInfoTabActorsInfo } from "./actors-info";
import { PageNavigationPaneInfoTabDocumentInfo } from "./document-info";
import { PageNavigationPaneInfoTabVersionHistory } from "./version-history";
type Props = {
page: TPageInstance;
versionHistory: Pick<TPageRootHandlers, "fetchAllVersions" | "fetchVersionDetails">;
};
export const PageNavigationPaneInfoTabPanel = observer(function PageNavigationPaneInfoTabPanel(props: Props) {
const { page, versionHistory } = props;
return (
<div className="flex h-full flex-col px-4">
<div className="mt-5 flex-1 overflow-y-auto">
<PageNavigationPaneInfoTabDocumentInfo page={page} />
<PageNavigationPaneInfoTabActorsInfo page={page} />
<div className="my-3 h-px flex-shrink-0 bg-layer-1" />
<PageNavigationPaneInfoTabVersionHistory page={page} versionHistory={versionHistory} />
</div>
</div>
);
});

View File

@@ -0,0 +1,148 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { useCallback } from "react";
import { observer } from "mobx-react";
import Link from "next/link";
import { useSearchParams } from "next/navigation";
import useSWR from "swr";
// plane imports
import { Avatar } from "@makeplane/propel/components/avatar";
import { useTranslation } from "@plane/i18n";
import type { TPageVersion } from "@plane/types";
import { cn, getFileURL, renderFormattedDate, renderFormattedTime } from "@plane/utils";
// components
import type { TPageRootHandlers } from "@/components/pages/editor/page-root";
// hooks
import { useMember } from "@/hooks/store/use-member";
import { useQueryParams } from "@/hooks/use-query-params";
// store
import type { TPageInstance } from "@/store/pages/base-page";
// local imports
import { PAGE_NAVIGATION_PANE_VERSION_QUERY_PARAM } from "../..";
type Props = {
page: TPageInstance;
versionHistory: Pick<TPageRootHandlers, "fetchAllVersions" | "fetchVersionDetails">;
};
type VersionHistoryItemProps = {
getVersionLink: (versionID: string) => string;
isVersionActive: boolean;
version: TPageVersion;
};
const VersionHistoryItem = observer(function VersionHistoryItem(props: VersionHistoryItemProps) {
const { getVersionLink, isVersionActive, version } = props;
// store hooks
const { getUserDetails } = useMember();
// derived values
const versionCreator = getUserDetails(version.owned_by);
// translation
const { t } = useTranslation();
return (
<li className="relative flex items-center gap-x-4 text-11 font-medium">
{/* timeline icon */}
<div className="relative grid size-6 flex-none place-items-center">
<div className="size-2 rounded-full bg-layer-3" />
</div>
{/* end timeline icon */}
<Link
href={getVersionLink(version.id)}
className={cn("block flex-1 rounded-md px-1 py-2 hover:bg-layer-transparent-hover", {
"bg-layer-transparent-selected hover:bg-layer-transparent-selected": isVersionActive,
})}
>
<p className="text-tertiary">
{renderFormattedDate(version.last_saved_at)}, {renderFormattedTime(version.last_saved_at)}
</p>
<p className="mt-1 flex items-center gap-1">
<Avatar
alt={versionCreator?.display_name}
fallback={versionCreator?.display_name?.[0]?.toUpperCase()}
size="2xs"
src={getFileURL(versionCreator?.avatar_url ?? "")}
/>
<span>{versionCreator?.display_name ?? t("common.deactivated_user")}</span>
</p>
</Link>
</li>
);
});
export const PageNavigationPaneInfoTabVersionHistory = observer(function PageNavigationPaneInfoTabVersionHistory(
props: Props
) {
const { page, versionHistory } = props;
// navigation
const searchParams = useSearchParams();
const activeVersion = searchParams.get(PAGE_NAVIGATION_PANE_VERSION_QUERY_PARAM);
// derived values
const { id } = page;
// translation
const { t } = useTranslation();
// query params
const { updateQueryParams } = useQueryParams();
// fetch all versions
const { data: versionsList } = useSWR(
id ? `PAGE_VERSIONS_LIST_${id}` : null,
id ? () => versionHistory.fetchAllVersions(id) : null
);
const getVersionLink = useCallback(
(versionID?: string) => {
if (versionID) {
return updateQueryParams({
paramsToAdd: { [PAGE_NAVIGATION_PANE_VERSION_QUERY_PARAM]: versionID },
});
} else {
return updateQueryParams({
paramsToRemove: [PAGE_NAVIGATION_PANE_VERSION_QUERY_PARAM],
});
}
},
[updateQueryParams]
);
return (
<div>
<p className="text-11 font-medium text-secondary">{t("page_navigation_pane.tabs.info.version_history.label")}</p>
<div className="mt-3">
<ul className="relative">
{/* timeline line */}
<div className={cn("absolute top-0 left-0 flex h-full w-6 justify-center")}>
<div className="w-px bg-layer-3" />
</div>
{/* end timeline line */}
<li className="relative flex items-center gap-x-4 text-11 font-medium">
{/* timeline icon */}
<div className="relative grid size-6 flex-none place-items-center rounded-full bg-accent-primary/20">
<div className="size-2.5 rounded-full bg-accent-primary/40" />
</div>
{/* end timeline icon */}
<Link
href={getVersionLink()}
className={cn("flex-1 rounded-md bg-layer-transparent px-1 py-2 hover:bg-layer-transparent-hover", {
"bg-layer-transparent-selected hover:bg-layer-transparent-selected": !activeVersion,
})}
>
{t("page_navigation_pane.tabs.info.version_history.current_version")}
</Link>
</li>
{versionsList?.map((version) => (
<VersionHistoryItem
key={version.id}
getVersionLink={getVersionLink}
isVersionActive={activeVersion === version.id}
version={version}
/>
))}
</ul>
</div>
</div>
);
});

View File

@@ -0,0 +1,41 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
// plane imports
import { ScrollArea } from "@plane/propel/scrollarea";
// store
import type { TPageInstance } from "@/store/pages/base-page";
// local imports
import { PageContentBrowser } from "../../editor/summary";
import { PageNavigationPaneOutlineTabEmptyState } from "./empty-state/outline";
type Props = {
page: TPageInstance;
};
export function PageNavigationPaneOutlineTabPanel(props: Props) {
const { page } = props;
// derived values
const {
editor: { editorRef },
} = page;
return (
<ScrollArea
orientation="vertical"
size="sm"
scrollType="hover"
className="hide-scrollbar size-full overflow-y-auto"
viewportClassName="px-4"
>
<PageContentBrowser
className="mt-0"
editorRef={editorRef}
emptyState={<PageNavigationPaneOutlineTabEmptyState />}
/>
</ScrollArea>
);
}

View File

@@ -0,0 +1,40 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
// plane imports
import { TabsPanel } from "@makeplane/propel/components/tabs";
// components
import type { TPageRootHandlers } from "@/components/pages/editor/page-root";
// store
import type { TPageInstance } from "@/store/pages/base-page";
// local imports
import { PageNavigationPaneAssetsTabPanel } from "./assets";
import { PageNavigationPaneInfoTabPanel } from "./info/root";
import { PageNavigationPaneOutlineTabPanel } from "./outline";
import { ORDERED_PAGE_NAVIGATION_TABS_LIST } from ".";
type Props = {
page: TPageInstance;
versionHistory: Pick<TPageRootHandlers, "fetchAllVersions" | "fetchVersionDetails">;
};
export function PageNavigationPaneTabPanelsRoot(props: Props) {
const { page, versionHistory } = props;
return (
// Grid wrapper: Propel's TabsPanel omits className, so the single mounted panel gets its
// fill height from a one-row grid instead.
<div className="grid min-h-0 w-full flex-1 grid-rows-1 overflow-hidden py-2">
{ORDERED_PAGE_NAVIGATION_TABS_LIST.map((tab) => (
<TabsPanel key={tab.key} value={tab.key}>
{tab.key === "outline" && <PageNavigationPaneOutlineTabPanel page={page} />}
{tab.key === "info" && <PageNavigationPaneInfoTabPanel page={page} versionHistory={versionHistory} />}
{tab.key === "assets" && <PageNavigationPaneAssetsTabPanel page={page} />}
</TabsPanel>
))}
</div>
);
}

View File

@@ -0,0 +1,26 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
// plane imports
import { Tab, TabsList } from "@makeplane/propel/components/tabs";
import { useTranslation } from "@plane/i18n";
// plane web components
import { ORDERED_PAGE_NAVIGATION_TABS_LIST } from "@/components/pages/navigation-pane/tab-panels";
export function PageNavigationPaneTabsList() {
// translation
const { t } = useTranslation();
return (
<div className="mx-3.5">
<TabsList>
{ORDERED_PAGE_NAVIGATION_TABS_LIST.map((tab) => (
<Tab key={tab.key} value={tab.key} label={t(tab.i18n_label)} />
))}
</TabsList>
</div>
);
}

View File

@@ -0,0 +1,27 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import type { ReactNode } from "react";
import type { EPageStoreType } from "@/hooks/store";
import type { TPageInstance } from "@/store/pages/base-page";
export interface INavigationPaneExtensionProps<T = Record<string, unknown>> {
page: TPageInstance;
extensionData?: T;
storeType: EPageStoreType;
}
export interface INavigationPaneExtensionComponent<T = Record<string, unknown>> {
(props: INavigationPaneExtensionProps<T>): ReactNode;
}
export interface INavigationPaneExtension<T = Record<string, unknown>> {
id: string;
triggerParam: string;
component: INavigationPaneExtensionComponent<T>;
data?: T;
width?: number;
}

View File

@@ -0,0 +1,12 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
// Export generic extension system interfaces
export type {
INavigationPaneExtensionProps,
INavigationPaneExtensionComponent,
INavigationPaneExtension,
} from "./extensions";