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
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:
18
apps/web/core/components/pages/navigation-pane/index.ts
Normal file
18
apps/web/core/components/pages/navigation-pane/index.ts
Normal 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);
|
||||
127
apps/web/core/components/pages/navigation-pane/root.tsx
Normal file
127
apps/web/core/components/pages/navigation-pane/root.tsx
Normal 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>
|
||||
);
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
];
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
26
apps/web/core/components/pages/navigation-pane/tabs-list.tsx
Normal file
26
apps/web/core/components/pages/navigation-pane/tabs-list.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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";
|
||||
Reference in New Issue
Block a user