feat: init
This commit is contained in:
12
apps/web/core/components/pages/navigation-pane/index.ts
Normal file
12
apps/web/core/components/pages/navigation-pane/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
// plane web imports
|
||||
import { ORDERED_PAGE_NAVIGATION_TABS_LIST } from "@/plane-web/components/pages/navigation-pane";
|
||||
|
||||
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);
|
||||
117
apps/web/core/components/pages/navigation-pane/root.tsx
Normal file
117
apps/web/core/components/pages/navigation-pane/root.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
import React, { useCallback } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { ArrowRightCircle } from "lucide-react";
|
||||
import { Tab } from "@headlessui/react";
|
||||
// plane imports
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { Tooltip } from "@plane/propel/tooltip";
|
||||
// hooks
|
||||
import { useQueryParams } from "@/hooks/use-query-params";
|
||||
// plane web components
|
||||
import type { TPageNavigationPaneTab } from "@/plane-web/components/pages/navigation-pane";
|
||||
// store
|
||||
import type { EPageStoreType } from "@/plane-web/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_TAB_KEYS,
|
||||
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: React.FC<Props> = observer((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";
|
||||
const selectedIndex = PAGE_NAVIGATION_PANE_TAB_KEYS.indexOf(activeTab);
|
||||
|
||||
// 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(
|
||||
(index: number) => {
|
||||
const updatedTab = PAGE_NAVIGATION_PANE_TAB_KEYS[index];
|
||||
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-shrink-0 h-full flex flex-col bg-custom-background-100 pt-3.5 border-l border-custom-border-200 transition-all duration-300 ease-out"
|
||||
style={{
|
||||
width: `${paneWidth}px`,
|
||||
marginRight: isNavigationPaneOpen ? "0px" : `-${paneWidth}px`,
|
||||
}}
|
||||
>
|
||||
<div className="mb-3.5 px-3.5">
|
||||
<Tooltip tooltipContent={t("page_navigation_pane.close_button")}>
|
||||
<button
|
||||
type="button"
|
||||
className="size-3.5 grid place-items-center text-custom-text-200 hover:text-custom-text-100 transition-colors"
|
||||
onClick={handleClose}
|
||||
aria-label={t("page_navigation_pane.close_button")}
|
||||
>
|
||||
<ArrowRightCircle className="size-3.5" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 flex flex-col overflow-hidden animate-slide-in-right">
|
||||
{ActiveExtension ? (
|
||||
<ActiveExtension.component page={page} extensionData={ActiveExtension.data} storeType={storeType} />
|
||||
) : showNavigationTabs ? (
|
||||
<Tab.Group as={React.Fragment} selectedIndex={selectedIndex} onChange={handleTabChange}>
|
||||
<PageNavigationPaneTabsList />
|
||||
<PageNavigationPaneTabPanelsRoot page={page} versionHistory={versionHistory} />
|
||||
</Tab.Group>
|
||||
) : null}
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,120 @@
|
||||
import { useMemo } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { Download } from "lucide-react";
|
||||
// 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";
|
||||
// plane web imports
|
||||
import { AdditionalPageNavigationPaneAssetItem } from "@/plane-web/components/pages/navigation-pane/tab-panels/assets";
|
||||
import { PageNavigationPaneAssetsTabEmptyState } from "@/plane-web/components/pages/navigation-pane/tab-panels/empty-states/assets";
|
||||
// store
|
||||
import type { TPageInstance } from "@/store/pages/base-page";
|
||||
|
||||
type Props = {
|
||||
page: TPageInstance;
|
||||
};
|
||||
|
||||
type AssetItemProps = {
|
||||
asset: TEditorAsset;
|
||||
page: TPageInstance;
|
||||
};
|
||||
|
||||
const AssetItem = observer((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="relative group/asset-item h-12 flex items-center gap-2 pr-2 rounded border border-custom-border-200 hover:bg-custom-background-80 transition-colors"
|
||||
>
|
||||
<div
|
||||
className="flex-shrink-0 w-11 h-12 rounded-l bg-cover bg-no-repeat bg-center"
|
||||
style={{
|
||||
backgroundImage: `url('${assetSrc}')`,
|
||||
}}
|
||||
/>
|
||||
<div className="flex-1 space-y-0.5 truncate">
|
||||
<p className="text-sm font-medium truncate">{asset.name}</p>
|
||||
<div className="flex items-end justify-between gap-2">
|
||||
<p className="shrink-0 text-xs text-custom-text-200" />
|
||||
<a
|
||||
href={assetDownloadSrc}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
className="shrink-0 py-0.5 px-1 flex items-center gap-1 rounded text-custom-text-200 hover:text-custom-text-100 opacity-0 pointer-events-none group-hover/asset-item:opacity-100 group-hover/asset-item:pointer-events-auto transition-opacity"
|
||||
>
|
||||
<Download className="shrink-0 size-3" />
|
||||
<span className="text-xs font-medium">{t("page_navigation_pane.tabs.assets.download_button")}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
|
||||
return (
|
||||
<AdditionalPageNavigationPaneAssetItem
|
||||
asset={asset}
|
||||
assetSrc={assetSrc}
|
||||
assetDownloadSrc={assetDownloadSrc}
|
||||
page={page}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export const PageNavigationPaneAssetsTabPanel: React.FC<Props> = observer((props) => {
|
||||
const { page } = props;
|
||||
// derived values
|
||||
const {
|
||||
editor: { assetsList },
|
||||
} = page;
|
||||
|
||||
if (assetsList.length === 0) return <PageNavigationPaneAssetsTabEmptyState />;
|
||||
|
||||
return (
|
||||
<div className="mt-5 space-y-4">
|
||||
{assetsList?.map((asset) => (
|
||||
<AssetItem key={asset.id} asset={asset} page={page} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { useParams } from "next/navigation";
|
||||
// plane imports
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { Avatar } from "@plane/ui";
|
||||
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: React.FC<Props> = observer((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="space-y-3 mt-4">
|
||||
<div>
|
||||
<p className="text-xs font-medium text-custom-text-300">
|
||||
{t("page_navigation_pane.tabs.info.actors_info.edited_by")}
|
||||
</p>
|
||||
<div className="mt-2 flex items-center justify-between gap-2 text-sm font-medium">
|
||||
<Link href={`/${workspaceSlug?.toString()}/profile/${page.updated_by}`} className="flex items-center gap-1">
|
||||
<Avatar
|
||||
src={getFileURL(editorInformation?.avatar_url ?? "")}
|
||||
name={editorInformation?.display_name}
|
||||
className="flex-shrink-0"
|
||||
size="sm"
|
||||
/>
|
||||
<span>{editorInformation?.display_name ?? t("common.deactivated_user")}</span>
|
||||
</Link>
|
||||
<span className="flex-shrink-0 text-custom-text-300">{calculateTimeAgoShort(page.updated_at ?? "")} ago</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-medium text-custom-text-300">
|
||||
{t("page_navigation_pane.tabs.info.actors_info.created_by")}
|
||||
</p>
|
||||
<div className="mt-2 flex items-center justify-between gap-2 text-sm font-medium">
|
||||
<Link href={`/${workspaceSlug?.toString()}/profile/${page.created_by}`} className="flex items-center gap-1">
|
||||
<Avatar
|
||||
src={getFileURL(creatorInformation?.avatar_url ?? "")}
|
||||
name={creatorInformation?.display_name}
|
||||
className="flex-shrink-0"
|
||||
size="sm"
|
||||
/>
|
||||
<span>{creatorInformation?.display_name ?? t("common.deactivated_user")}</span>
|
||||
</Link>
|
||||
<span className="flex-shrink-0 text-custom-text-300">{renderFormattedDate(page.created_at)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,82 @@
|
||||
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: React.FC<Props> = observer((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="p-2 bg-custom-background-90 rounded">
|
||||
<h6 className="text-base font-semibold">{card.info}</h6>
|
||||
<p className="mt-1.5 text-sm text-custom-text-300 font-medium">{card.title}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
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: React.FC<Props> = observer((props) => {
|
||||
const { page, versionHistory } = props;
|
||||
|
||||
return (
|
||||
<div className="mt-5">
|
||||
<PageNavigationPaneInfoTabDocumentInfo page={page} />
|
||||
<PageNavigationPaneInfoTabActorsInfo page={page} />
|
||||
<div className="flex-shrink-0 h-px bg-custom-background-80 my-3" />
|
||||
<PageNavigationPaneInfoTabVersionHistory page={page} versionHistory={versionHistory} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,142 @@
|
||||
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 { useTranslation } from "@plane/i18n";
|
||||
import type { TPageVersion } from "@plane/types";
|
||||
import { Avatar } from "@plane/ui";
|
||||
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((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-xs font-medium">
|
||||
{/* timeline icon */}
|
||||
<div className="relative size-6 flex-none grid place-items-center">
|
||||
<div className="size-2 rounded-full bg-custom-background-80" />
|
||||
</div>
|
||||
{/* end timeline icon */}
|
||||
<Link
|
||||
href={getVersionLink(version.id)}
|
||||
className={cn("block flex-1 hover:bg-custom-background-90 rounded-md py-2 px-1", {
|
||||
"bg-custom-background-80 hover:bg-custom-background-80": isVersionActive,
|
||||
})}
|
||||
>
|
||||
<p className="text-custom-text-300">
|
||||
{renderFormattedDate(version.last_saved_at)}, {renderFormattedTime(version.last_saved_at)}
|
||||
</p>
|
||||
<p className="mt-1 flex items-center gap-1">
|
||||
<Avatar
|
||||
size="sm"
|
||||
src={getFileURL(versionCreator?.avatar_url ?? "")}
|
||||
name={versionCreator?.display_name}
|
||||
className="flex-shrink-0"
|
||||
/>
|
||||
<span>{versionCreator?.display_name ?? t("common.deactivated_user")}</span>
|
||||
</p>
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
});
|
||||
|
||||
export const PageNavigationPaneInfoTabVersionHistory: React.FC<Props> = observer((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-xs font-medium text-custom-text-200">
|
||||
{t("page_navigation_pane.tabs.info.version_history.label")}
|
||||
</p>
|
||||
<div className="mt-3">
|
||||
<ul role="list" className="relative">
|
||||
{/* timeline line */}
|
||||
<div className={cn("absolute left-0 top-0 h-full flex w-6 justify-center")}>
|
||||
<div className="w-px bg-custom-background-80" />
|
||||
</div>
|
||||
{/* end timeline line */}
|
||||
<li className="relative flex items-center gap-x-4 text-xs font-medium">
|
||||
{/* timeline icon */}
|
||||
<div className="relative size-6 flex-none rounded-full grid place-items-center bg-custom-primary-100/20">
|
||||
<div className="size-2.5 rounded-full bg-custom-primary-100/40" />
|
||||
</div>
|
||||
{/* end timeline icon */}
|
||||
<Link
|
||||
href={getVersionLink()}
|
||||
className={cn("flex-1 hover:bg-custom-background-90 rounded-md py-2 px-1", {
|
||||
"bg-custom-background-80 hover:bg-custom-background-80": !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>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
// plane web imports
|
||||
import { PageNavigationPaneOutlineTabEmptyState } from "@/plane-web/components/pages/navigation-pane/tab-panels/empty-states/outline";
|
||||
// store
|
||||
import type { TPageInstance } from "@/store/pages/base-page";
|
||||
// local imports
|
||||
import { PageContentBrowser } from "../../editor/summary";
|
||||
|
||||
type Props = {
|
||||
page: TPageInstance;
|
||||
};
|
||||
|
||||
export const PageNavigationPaneOutlineTabPanel: React.FC<Props> = (props) => {
|
||||
const { page } = props;
|
||||
// derived values
|
||||
const {
|
||||
editor: { editorRef },
|
||||
} = page;
|
||||
|
||||
return (
|
||||
<div className="size-full pt-3 space-y-1">
|
||||
<PageContentBrowser
|
||||
className="mt-0"
|
||||
editorRef={editorRef}
|
||||
emptyState={<PageNavigationPaneOutlineTabEmptyState />}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
import React from "react";
|
||||
import { Tab } from "@headlessui/react";
|
||||
// components
|
||||
import type { TPageRootHandlers } from "@/components/pages/editor/page-root";
|
||||
// plane web imports
|
||||
import { ORDERED_PAGE_NAVIGATION_TABS_LIST } from "@/plane-web/components/pages/navigation-pane";
|
||||
import { PageNavigationPaneAdditionalTabPanelsRoot } from "@/plane-web/components/pages/navigation-pane/tab-panels/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";
|
||||
|
||||
type Props = {
|
||||
page: TPageInstance;
|
||||
versionHistory: Pick<TPageRootHandlers, "fetchAllVersions" | "fetchVersionDetails">;
|
||||
};
|
||||
|
||||
export const PageNavigationPaneTabPanelsRoot: React.FC<Props> = (props) => {
|
||||
const { page, versionHistory } = props;
|
||||
|
||||
return (
|
||||
<Tab.Panels as={React.Fragment}>
|
||||
{ORDERED_PAGE_NAVIGATION_TABS_LIST.map((tab) => (
|
||||
<Tab.Panel
|
||||
key={tab.key}
|
||||
as="div"
|
||||
className="size-full p-3.5 pt-0 overflow-y-auto vertical-scrollbar scrollbar-sm outline-none"
|
||||
>
|
||||
{tab.key === "outline" && <PageNavigationPaneOutlineTabPanel page={page} />}
|
||||
{tab.key === "info" && <PageNavigationPaneInfoTabPanel page={page} versionHistory={versionHistory} />}
|
||||
{tab.key === "assets" && <PageNavigationPaneAssetsTabPanel page={page} />}
|
||||
<PageNavigationPaneAdditionalTabPanelsRoot activeTab={tab.key} page={page} />
|
||||
</Tab.Panel>
|
||||
))}
|
||||
</Tab.Panels>
|
||||
);
|
||||
};
|
||||
37
apps/web/core/components/pages/navigation-pane/tabs-list.tsx
Normal file
37
apps/web/core/components/pages/navigation-pane/tabs-list.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Tab } from "@headlessui/react";
|
||||
// plane imports
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// plane web components
|
||||
import { ORDERED_PAGE_NAVIGATION_TABS_LIST } from "@/plane-web/components/pages/navigation-pane";
|
||||
|
||||
export const PageNavigationPaneTabsList = () => {
|
||||
// translation
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Tab.List className="relative flex items-center p-[2px] rounded-md bg-custom-background-80 mx-3.5">
|
||||
{({ selectedIndex }) => (
|
||||
<>
|
||||
{ORDERED_PAGE_NAVIGATION_TABS_LIST.map((tab) => (
|
||||
<Tab
|
||||
key={tab.key}
|
||||
type="button"
|
||||
className="relative z-[1] flex-1 py-1.5 text-sm font-semibold outline-none"
|
||||
>
|
||||
{t(tab.i18n_label)}
|
||||
</Tab>
|
||||
))}
|
||||
{/* active tab indicator */}
|
||||
<div
|
||||
className="absolute top-1/2 -translate-y-1/2 bg-custom-background-90 rounded transition-all duration-500 ease-in-out pointer-events-none"
|
||||
style={{
|
||||
left: `calc(${(selectedIndex / ORDERED_PAGE_NAVIGATION_TABS_LIST.length) * 100}% + 2px)`,
|
||||
height: "calc(100% - 4px)",
|
||||
width: `calc(${100 / ORDERED_PAGE_NAVIGATION_TABS_LIST.length}% - 4px)`,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Tab.List>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { ReactNode } from "react";
|
||||
import type { EPageStoreType } from "@/plane-web/hooks/store";
|
||||
import type { TPageInstance } from "@/store/pages/base-page";
|
||||
|
||||
export interface INavigationPaneExtensionProps<T = any> {
|
||||
page: TPageInstance;
|
||||
extensionData?: T;
|
||||
storeType: EPageStoreType;
|
||||
}
|
||||
|
||||
export interface INavigationPaneExtensionComponent<T = any> {
|
||||
(props: INavigationPaneExtensionProps<T>): ReactNode;
|
||||
}
|
||||
|
||||
export interface INavigationPaneExtension<T = any> {
|
||||
id: string;
|
||||
triggerParam: string;
|
||||
component: INavigationPaneExtensionComponent<T>;
|
||||
data?: T;
|
||||
width?: number;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// Export generic extension system interfaces
|
||||
export type {
|
||||
INavigationPaneExtensionProps,
|
||||
INavigationPaneExtensionComponent,
|
||||
INavigationPaneExtension,
|
||||
} from "./extensions";
|
||||
Reference in New Issue
Block a user