feat: refine Beam attachments and peek layouts
This commit is contained in:
parent
ff30eeb8fd
commit
81abcb5b36
|
|
@ -213,10 +213,7 @@ export const DateDropdown = observer(function DateDropdown(props: Props) {
|
||||||
createPortal(
|
createPortal(
|
||||||
<Combobox.Options data-prevent-outside-click static>
|
<Combobox.Options data-prevent-outside-click static>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn("nodedc-dropdown-surface z-[760] my-1 overflow-hidden", optionsClassName)}
|
||||||
"nodedc-dropdown-surface z-30 my-1 overflow-hidden",
|
|
||||||
optionsClassName
|
|
||||||
)}
|
|
||||||
ref={setPopperElement}
|
ref={setPopperElement}
|
||||||
style={styles.popper}
|
style={styles.popper}
|
||||||
{...attributes.popper}
|
{...attributes.popper}
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,12 @@ import { useCallback, useState } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import type { FileRejection } from "react-dropzone";
|
import type { FileRejection } from "react-dropzone";
|
||||||
import { useDropzone } from "react-dropzone";
|
import { useDropzone } from "react-dropzone";
|
||||||
import { UploadCloud } from "lucide-react";
|
import { LayoutGrid, List, UploadCloud } from "lucide-react";
|
||||||
import { useTranslation } from "@plane/i18n";
|
import { useTranslation } from "@plane/i18n";
|
||||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||||
import type { TIssueServiceType } from "@plane/types";
|
import type { TIssueServiceType } from "@plane/types";
|
||||||
import { EIssueServiceType } from "@plane/types";
|
import { EIssueServiceType } from "@plane/types";
|
||||||
|
import { convertBytesToSize, getFileExtension } from "@plane/utils";
|
||||||
// hooks
|
// hooks
|
||||||
import { useIssueDetail } from "@/hooks/store/use-issue-detail";
|
import { useIssueDetail } from "@/hooks/store/use-issue-detail";
|
||||||
// plane web hooks
|
// plane web hooks
|
||||||
|
|
@ -25,6 +26,8 @@ import { IssueAttachmentsUploadItem } from "./attachment-list-upload-item";
|
||||||
// types
|
// types
|
||||||
import { IssueAttachmentDeleteModal } from "./delete-attachment-modal";
|
import { IssueAttachmentDeleteModal } from "./delete-attachment-modal";
|
||||||
|
|
||||||
|
type TAttachmentViewMode = "grid" | "list";
|
||||||
|
|
||||||
type TIssueAttachmentItemList = {
|
type TIssueAttachmentItemList = {
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
|
|
@ -46,6 +49,10 @@ export const IssueAttachmentItemList = observer(function IssueAttachmentItemList
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
// states
|
// states
|
||||||
const [isUploading, setIsUploading] = useState(false);
|
const [isUploading, setIsUploading] = useState(false);
|
||||||
|
const [attachmentViewMode, setAttachmentViewMode] = useState<TAttachmentViewMode>(() => {
|
||||||
|
if (typeof window === "undefined") return "grid";
|
||||||
|
return window.localStorage.getItem("nodedc-issue-attachment-view-mode") === "list" ? "list" : "grid";
|
||||||
|
});
|
||||||
// store hooks
|
// store hooks
|
||||||
const {
|
const {
|
||||||
attachment: { getAttachmentsByIssueId },
|
attachment: { getAttachmentsByIssueId },
|
||||||
|
|
@ -67,6 +74,7 @@ export const IssueAttachmentItemList = observer(function IssueAttachmentItemList
|
||||||
activeUploads.length > 0
|
activeUploads.length > 0
|
||||||
? Math.round(activeUploads.reduce((progressSum, item) => progressSum + item.progress, 0) / activeUploads.length)
|
? Math.round(activeUploads.reduce((progressSum, item) => progressSum + item.progress, 0) / activeUploads.length)
|
||||||
: 0;
|
: 0;
|
||||||
|
const isListView = attachmentViewMode === "list";
|
||||||
|
|
||||||
// handlers
|
// handlers
|
||||||
const handleFetchPropertyActivities = useCallback(() => {
|
const handleFetchPropertyActivities = useCallback(() => {
|
||||||
|
|
@ -122,6 +130,14 @@ export const IssueAttachmentItemList = observer(function IssueAttachmentItemList
|
||||||
disabled: isUploading || disabled,
|
disabled: isUploading || disabled,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const handleToggleAttachmentViewMode = useCallback(() => {
|
||||||
|
setAttachmentViewMode((currentMode) => {
|
||||||
|
const nextMode: TAttachmentViewMode = currentMode === "grid" ? "list" : "grid";
|
||||||
|
if (typeof window !== "undefined") window.localStorage.setItem("nodedc-issue-attachment-view-mode", nextMode);
|
||||||
|
return nextMode;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{attachmentDeleteModalId && (
|
{attachmentDeleteModalId && (
|
||||||
|
|
@ -173,29 +189,90 @@ export const IssueAttachmentItemList = observer(function IssueAttachmentItemList
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{hasAttachmentRows && (
|
{hasAttachmentRows && (
|
||||||
<div className="rounded-[1.35rem] border border-subtle bg-surface-1/60 p-3 shadow-[0_12px_28px_rgba(0,0,0,0.08)]">
|
<div
|
||||||
|
className="nodedc-attachments-panel rounded-[1.35rem] border border-subtle bg-surface-1/60 p-3 shadow-[0_12px_28px_rgba(0,0,0,0.08)]"
|
||||||
|
data-view-mode={attachmentViewMode}
|
||||||
|
>
|
||||||
<div className="mb-2 flex items-center justify-between gap-3 px-1">
|
<div className="mb-2 flex items-center justify-between gap-3 px-1">
|
||||||
<div className="text-12 font-semibold tracking-[0.08em] text-tertiary uppercase">Вложения</div>
|
<div className="text-12 font-semibold tracking-[0.08em] text-tertiary uppercase">Вложения</div>
|
||||||
<div className="text-12 text-tertiary">
|
<div className="flex items-center gap-2">
|
||||||
{activeUploads.length > 0 ? `Загрузка ${uploadProgress}%` : issueAttachments.length}
|
<div className="text-12 text-tertiary">
|
||||||
|
{activeUploads.length > 0 ? `Загрузка ${uploadProgress}%` : issueAttachments.length}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="grid size-8 place-items-center rounded-full bg-surface-2/80 text-tertiary transition hover:bg-surface-2 hover:text-primary"
|
||||||
|
onClick={handleToggleAttachmentViewMode}
|
||||||
|
title={isListView ? "Показать плитками" : "Показать списком"}
|
||||||
|
>
|
||||||
|
{isListView ? <LayoutGrid className="size-4" /> : <List className="size-4" />}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-3 overflow-x-auto pb-1">
|
{isListView ? (
|
||||||
{uploadStatus?.map((status) => (
|
<div className="nodedc-attachments-list-scroll overflow-auto rounded-2xl bg-surface-2/40">
|
||||||
<IssueAttachmentsUploadItem key={status.id} uploadStatus={status} />
|
<div className="min-w-[760px]">
|
||||||
))}
|
<div className="grid grid-cols-[minmax(240px,1.45fr)_120px_86px_minmax(130px,0.9fr)_112px] gap-3 border-b border-subtle px-4 py-2 text-11 font-semibold tracking-[0.05em] text-tertiary uppercase">
|
||||||
{issueAttachments.map((attachmentId) => (
|
<div>Файл</div>
|
||||||
<IssueAttachmentsListItem
|
<div>Дата</div>
|
||||||
key={attachmentId}
|
<div>Версия</div>
|
||||||
attachmentId={attachmentId}
|
<div>Добавил</div>
|
||||||
disabled={disabled}
|
<div className="text-right">Действия</div>
|
||||||
issueId={issueId}
|
</div>
|
||||||
issueServiceType={issueServiceType}
|
{uploadStatus?.map((status) => (
|
||||||
projectId={projectId}
|
<div
|
||||||
workspaceSlug={workspaceSlug}
|
key={status.id}
|
||||||
/>
|
className="grid grid-cols-[minmax(240px,1.45fr)_120px_86px_minmax(130px,0.9fr)_112px] items-center gap-3 border-b border-subtle/70 px-4 py-3 text-12"
|
||||||
))}
|
>
|
||||||
</div>
|
<div className="min-w-0">
|
||||||
|
<div className="truncate font-semibold text-secondary">{status.name}</div>
|
||||||
|
<div className="mt-1 text-11 text-tertiary">
|
||||||
|
{(getFileExtension(status.name) || "file").toUpperCase()} · {convertBytesToSize(status.size)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-tertiary">Загрузка</div>
|
||||||
|
<div className="text-secondary">v1</div>
|
||||||
|
<div className="text-tertiary">{status.progress}%</div>
|
||||||
|
<div className="h-1 overflow-hidden rounded-full bg-white/8">
|
||||||
|
<div
|
||||||
|
className="h-full rounded-full bg-[rgb(var(--nodedc-accent-rgb))]"
|
||||||
|
style={{ width: `${status.progress}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{issueAttachments.map((attachmentId) => (
|
||||||
|
<IssueAttachmentsListItem
|
||||||
|
key={attachmentId}
|
||||||
|
attachmentId={attachmentId}
|
||||||
|
disabled={disabled}
|
||||||
|
issueId={issueId}
|
||||||
|
issueServiceType={issueServiceType}
|
||||||
|
projectId={projectId}
|
||||||
|
viewMode="list"
|
||||||
|
workspaceSlug={workspaceSlug}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex gap-3 overflow-x-auto pb-1">
|
||||||
|
{uploadStatus?.map((status) => (
|
||||||
|
<IssueAttachmentsUploadItem key={status.id} uploadStatus={status} />
|
||||||
|
))}
|
||||||
|
{issueAttachments.map((attachmentId) => (
|
||||||
|
<IssueAttachmentsListItem
|
||||||
|
key={attachmentId}
|
||||||
|
attachmentId={attachmentId}
|
||||||
|
disabled={disabled}
|
||||||
|
issueId={issueId}
|
||||||
|
issueServiceType={issueServiceType}
|
||||||
|
projectId={projectId}
|
||||||
|
workspaceSlug={workspaceSlug}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
|
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
import { useTranslation } from "@plane/i18n";
|
import { useTranslation } from "@plane/i18n";
|
||||||
import { getIconButtonStyling } from "@plane/propel/icon-button";
|
import { getIconButtonStyling } from "@plane/propel/icon-button";
|
||||||
|
|
@ -45,6 +46,7 @@ type TIssueAttachmentsListItem = {
|
||||||
issueServiceType?: TIssueServiceType;
|
issueServiceType?: TIssueServiceType;
|
||||||
issueId: string;
|
issueId: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
|
viewMode?: "grid" | "list";
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -144,6 +146,7 @@ export const IssueAttachmentsListItem = observer(function IssueAttachmentsListIt
|
||||||
issueId,
|
issueId,
|
||||||
issueServiceType = EIssueServiceType.ISSUES,
|
issueServiceType = EIssueServiceType.ISSUES,
|
||||||
projectId,
|
projectId,
|
||||||
|
viewMode = "grid",
|
||||||
workspaceSlug,
|
workspaceSlug,
|
||||||
} = props;
|
} = props;
|
||||||
// store hooks
|
// store hooks
|
||||||
|
|
@ -199,6 +202,17 @@ export const IssueAttachmentsListItem = observer(function IssueAttachmentsListIt
|
||||||
"Оригинальная модель загружена в Beam. Preview появится после подготовки GLB/XKT и дерева компонентов.";
|
"Оригинальная модель загружена в Beam. Preview появится после подготовки GLB/XKT и дерева компонентов.";
|
||||||
const [isPreviewOpen, setIsPreviewOpen] = useState(false);
|
const [isPreviewOpen, setIsPreviewOpen] = useState(false);
|
||||||
const [isThumbnailError, setIsThumbnailError] = useState(false);
|
const [isThumbnailError, setIsThumbnailError] = useState(false);
|
||||||
|
const rawVersion = attachment?.attributes.version;
|
||||||
|
const versionLabel =
|
||||||
|
typeof rawVersion === "number"
|
||||||
|
? `v${rawVersion}`
|
||||||
|
: typeof rawVersion === "string"
|
||||||
|
? rawVersion.toLowerCase().startsWith("v")
|
||||||
|
? rawVersion
|
||||||
|
: `v${rawVersion}`
|
||||||
|
: "v1";
|
||||||
|
const uploadedAt = renderFormattedDate(attachment?.created_at ?? attachment?.updated_at ?? "");
|
||||||
|
const uploadedBy = attachment?.created_by ? (getUserDetails(attachment.created_by)?.display_name ?? "—") : "—";
|
||||||
const openModelViewer = () => {
|
const openModelViewer = () => {
|
||||||
if (!modelViewerUrl) return;
|
if (!modelViewerUrl) return;
|
||||||
dispatchBeamViewerOpenEvent({
|
dispatchBeamViewerOpenEvent({
|
||||||
|
|
@ -294,6 +308,148 @@ export const IssueAttachmentsListItem = observer(function IssueAttachmentsListIt
|
||||||
|
|
||||||
if (!attachment) return <></>;
|
if (!attachment) return <></>;
|
||||||
|
|
||||||
|
if (viewMode === "list") {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="grid grid-cols-[minmax(240px,1.45fr)_120px_86px_minmax(130px,0.9fr)_112px] items-center gap-3 border-b border-subtle/70 px-4 py-3 text-12 transition hover:bg-white/[0.035]">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="flex min-w-0 items-center gap-3 text-left"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
if (modelViewerUrl) openModelViewer();
|
||||||
|
else setIsPreviewOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="grid size-9 flex-shrink-0 place-items-center rounded-xl bg-surface-1 text-tertiary">
|
||||||
|
{isBeamModel ? (
|
||||||
|
isBeamConversionFailed ? (
|
||||||
|
<AlertTriangle className="text-red-400 size-5" />
|
||||||
|
) : isBeamAwaitingPreview ? (
|
||||||
|
<Spinner
|
||||||
|
height="20px"
|
||||||
|
width="20px"
|
||||||
|
className="fill-[rgb(var(--nodedc-accent-rgb))] text-[rgba(var(--nodedc-accent-rgb),0.18)]"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Box className="size-5 text-[rgb(var(--nodedc-accent-rgb))]" />
|
||||||
|
)
|
||||||
|
) : previewType === "file" ? (
|
||||||
|
getFileIcon(fileExtension, 18)
|
||||||
|
) : (
|
||||||
|
<ImageIcon className="size-5" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<Tooltip tooltipContent={fullFileName} isMobile={isMobile}>
|
||||||
|
<div className="truncate font-semibold text-secondary">{fullFileName}</div>
|
||||||
|
</Tooltip>
|
||||||
|
<div className="mt-1 flex min-w-0 items-center gap-2 text-11 text-tertiary">
|
||||||
|
<span>{fileExtension ? fileExtension.toUpperCase() : "FILE"}</span>
|
||||||
|
<span className="size-1 rounded-full bg-layer-1" />
|
||||||
|
<span>{convertBytesToSize(attachment.attributes.size)}</span>
|
||||||
|
{beamConversionStatusLabel && (
|
||||||
|
<>
|
||||||
|
<span className="size-1 rounded-full bg-layer-1" />
|
||||||
|
<Tooltip tooltipContent={beamStatusTooltipContent} isMobile={isMobile}>
|
||||||
|
<span
|
||||||
|
className={
|
||||||
|
isBeamConversionFailed
|
||||||
|
? "text-red-400"
|
||||||
|
: isBeamAwaitingPreview
|
||||||
|
? "text-[rgb(var(--nodedc-accent-rgb))]"
|
||||||
|
: "text-tertiary"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{beamConversionStatusLabel}
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<div className="truncate text-tertiary">{uploadedAt}</div>
|
||||||
|
<div className="text-secondary">{versionLabel}</div>
|
||||||
|
<div className="truncate text-tertiary">{uploadedBy}</div>
|
||||||
|
<div className="flex items-center justify-end gap-1">
|
||||||
|
{(modelViewerUrl || previewURL) && (
|
||||||
|
<Tooltip
|
||||||
|
tooltipContent={modelViewerUrl ? "Посмотреть модель" : "Открыть предпросмотр"}
|
||||||
|
isMobile={isMobile}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="grid size-8 place-items-center rounded-full text-tertiary transition hover:bg-surface-1 hover:text-primary"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
if (modelViewerUrl) openModelViewer();
|
||||||
|
else setIsPreviewOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Eye className="size-4" />
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
{previewDownloadUrl && (
|
||||||
|
<Tooltip tooltipContent={modelDownloadUrl ? "Скачать оригинал" : "Скачать файл"} isMobile={isMobile}>
|
||||||
|
<a
|
||||||
|
href={previewDownloadUrl}
|
||||||
|
download={fullFileName}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="grid size-8 place-items-center rounded-full text-tertiary transition hover:bg-surface-1 hover:text-primary"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<Download className="size-4" />
|
||||||
|
</a>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
<Tooltip tooltipContent={t("common.actions.delete")} isMobile={isMobile}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="hover:bg-red-500/10 hover:text-red-400 grid size-8 place-items-center rounded-full text-tertiary transition disabled:pointer-events-none disabled:opacity-40"
|
||||||
|
disabled={!!disabled}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
toggleDeleteAttachmentModal(attachmentId);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TrashIcon className="size-4" />
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ModalCore
|
||||||
|
isOpen={isPreviewOpen}
|
||||||
|
handleClose={() => setIsPreviewOpen(false)}
|
||||||
|
position={EModalPosition.CENTER}
|
||||||
|
width={EModalWidth.VIIXL}
|
||||||
|
className="h-[calc(100vh-4rem)] max-w-[calc(100vw-4rem)] overflow-hidden border border-subtle bg-surface-1"
|
||||||
|
>
|
||||||
|
<AttachmentPreviewContent
|
||||||
|
beamConversionStatusLabel={beamConversionStatusLabel}
|
||||||
|
beamStatusTooltipContent={beamStatusTooltipContent}
|
||||||
|
fileIcon={fileIcon}
|
||||||
|
fullFileName={fullFileName}
|
||||||
|
isBeamAwaitingPreview={isBeamAwaitingPreview}
|
||||||
|
isBeamConversionFailed={isBeamConversionFailed}
|
||||||
|
modelDownloadUrl={modelDownloadUrl}
|
||||||
|
previewDownloadUrl={previewDownloadUrl}
|
||||||
|
previewType={previewType}
|
||||||
|
previewURL={previewURL}
|
||||||
|
setIsPreviewOpen={setIsPreviewOpen}
|
||||||
|
size={attachment.attributes.size}
|
||||||
|
/>
|
||||||
|
</ModalCore>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="group flex h-32 w-72 flex-shrink-0 overflow-hidden rounded-2xl border border-subtle bg-surface-2/80 transition hover:border-[rgba(var(--nodedc-accent-rgb),0.45)] hover:bg-surface-2">
|
<div className="group flex h-32 w-72 flex-shrink-0 overflow-hidden rounded-2xl border border-subtle bg-surface-2/80 transition hover:border-[rgba(var(--nodedc-accent-rgb),0.45)] hover:bg-surface-2">
|
||||||
|
|
@ -400,100 +556,149 @@ export const IssueAttachmentsListItem = observer(function IssueAttachmentsListIt
|
||||||
width={EModalWidth.VIIXL}
|
width={EModalWidth.VIIXL}
|
||||||
className="h-[calc(100vh-4rem)] max-w-[calc(100vw-4rem)] overflow-hidden border border-subtle bg-surface-1"
|
className="h-[calc(100vh-4rem)] max-w-[calc(100vw-4rem)] overflow-hidden border border-subtle bg-surface-1"
|
||||||
>
|
>
|
||||||
<div className="relative flex h-full min-h-0 flex-col bg-surface-1 p-4">
|
<AttachmentPreviewContent
|
||||||
<div className="absolute top-4 right-4 z-10 flex items-center gap-2">
|
beamConversionStatusLabel={beamConversionStatusLabel}
|
||||||
{previewDownloadUrl && (
|
beamStatusTooltipContent={beamStatusTooltipContent}
|
||||||
<a
|
fileIcon={fileIcon}
|
||||||
href={previewDownloadUrl}
|
fullFileName={fullFileName}
|
||||||
download={fullFileName}
|
isBeamAwaitingPreview={isBeamAwaitingPreview}
|
||||||
target="_blank"
|
isBeamConversionFailed={isBeamConversionFailed}
|
||||||
rel="noopener noreferrer"
|
modelDownloadUrl={modelDownloadUrl}
|
||||||
className="grid size-10 place-items-center rounded-full bg-[rgb(var(--nodedc-accent-rgb))] text-[rgb(var(--nodedc-on-accent-rgb))] shadow-[0_14px_30px_rgba(0,0,0,0.3)] transition hover:brightness-110"
|
previewDownloadUrl={previewDownloadUrl}
|
||||||
onClick={(e) => e.stopPropagation()}
|
previewType={previewType}
|
||||||
>
|
previewURL={previewURL}
|
||||||
<Download className="size-5" />
|
setIsPreviewOpen={setIsPreviewOpen}
|
||||||
</a>
|
size={attachment.attributes.size}
|
||||||
)}
|
/>
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="grid size-10 place-items-center rounded-full bg-black/35 text-white backdrop-blur-md transition hover:bg-black/50"
|
|
||||||
onClick={() => setIsPreviewOpen(false)}
|
|
||||||
>
|
|
||||||
<X className="size-5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex-shrink-0 pr-24">
|
|
||||||
<div className="text-15 font-semibold text-primary">{fullFileName}</div>
|
|
||||||
<div className="mt-1 text-12 text-tertiary">{convertBytesToSize(attachment.attributes.size)}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-4 min-h-0 flex-1 overflow-hidden rounded-2xl bg-black/20">
|
|
||||||
{previewType === "image" && previewURL ? (
|
|
||||||
<div className="flex h-full w-full items-center justify-center">
|
|
||||||
<img src={previewURL} alt={fullFileName} className="max-h-full max-w-full object-contain" />
|
|
||||||
</div>
|
|
||||||
) : previewType === "video" && previewURL ? (
|
|
||||||
<div className="flex h-full w-full items-center justify-center">
|
|
||||||
<video src={previewURL} className="max-h-full max-w-full" controls autoPlay>
|
|
||||||
<track kind="captions" />
|
|
||||||
</video>
|
|
||||||
</div>
|
|
||||||
) : previewType === "pdf" && previewURL ? (
|
|
||||||
<IssueAttachmentPdfPreview fileURL={previewURL} />
|
|
||||||
) : isBeamAwaitingPreview || isBeamConversionFailed ? (
|
|
||||||
<div className="flex h-full flex-col items-center justify-center gap-4 p-8 text-center">
|
|
||||||
<div
|
|
||||||
className={`grid size-20 place-items-center rounded-3xl ${
|
|
||||||
isBeamConversionFailed
|
|
||||||
? "bg-red-500/10 text-red-400"
|
|
||||||
: "bg-[rgba(var(--nodedc-accent-rgb),0.12)] text-[rgb(var(--nodedc-accent-rgb))]"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{isBeamConversionFailed ? (
|
|
||||||
<AlertTriangle className="size-10" />
|
|
||||||
) : (
|
|
||||||
<Spinner
|
|
||||||
height="40px"
|
|
||||||
width="40px"
|
|
||||||
className="fill-[rgb(var(--nodedc-accent-rgb))] text-[rgba(var(--nodedc-accent-rgb),0.18)]"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="max-w-md">
|
|
||||||
<div className="text-15 font-semibold text-primary">
|
|
||||||
{isBeamConversionFailed ? "Ошибка дерева" : (beamConversionStatusLabel ?? "Ожидает дерево")}
|
|
||||||
</div>
|
|
||||||
<div className="mt-2 text-14 text-secondary">
|
|
||||||
{isBeamConversionFailed
|
|
||||||
? beamStatusTooltipContent
|
|
||||||
: "Оригинальный STEP загружен в Beam. Preview появится после подготовки GLB/XKT и дерева компонентов."}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{modelDownloadUrl && (
|
|
||||||
<a
|
|
||||||
href={modelDownloadUrl}
|
|
||||||
download={fullFileName}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="inline-flex items-center gap-2 rounded-full bg-[rgb(var(--nodedc-accent-rgb))] px-4 py-2 text-13 font-semibold text-[rgb(var(--nodedc-on-accent-rgb))] transition hover:brightness-110"
|
|
||||||
>
|
|
||||||
<Download className="size-4" />
|
|
||||||
Скачать оригинал
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex h-full flex-col items-center justify-center gap-4 p-8 text-center">
|
|
||||||
<div className="grid size-20 place-items-center rounded-3xl bg-surface-2">{fileIcon}</div>
|
|
||||||
<div className="max-w-md text-14 text-secondary">
|
|
||||||
Предпросмотр для этого формата недоступен. Файл можно скачать или открыть в новой вкладке.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ModalCore>
|
</ModalCore>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
type TAttachmentPreviewContent = {
|
||||||
|
beamConversionStatusLabel: string | null;
|
||||||
|
beamStatusTooltipContent: string;
|
||||||
|
fileIcon: ReactNode;
|
||||||
|
fullFileName: string;
|
||||||
|
isBeamAwaitingPreview: boolean;
|
||||||
|
isBeamConversionFailed: boolean;
|
||||||
|
modelDownloadUrl: string | undefined;
|
||||||
|
previewDownloadUrl: string | undefined;
|
||||||
|
previewType: "image" | "video" | "pdf" | "file";
|
||||||
|
previewURL: string;
|
||||||
|
setIsPreviewOpen: (isOpen: boolean) => void;
|
||||||
|
size: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const AttachmentPreviewContent = (props: TAttachmentPreviewContent) => {
|
||||||
|
const {
|
||||||
|
beamConversionStatusLabel,
|
||||||
|
beamStatusTooltipContent,
|
||||||
|
fileIcon,
|
||||||
|
fullFileName,
|
||||||
|
isBeamAwaitingPreview,
|
||||||
|
isBeamConversionFailed,
|
||||||
|
modelDownloadUrl,
|
||||||
|
previewDownloadUrl,
|
||||||
|
previewType,
|
||||||
|
previewURL,
|
||||||
|
setIsPreviewOpen,
|
||||||
|
size,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative flex h-full min-h-0 flex-col bg-surface-1 p-4">
|
||||||
|
<div className="absolute top-4 right-4 z-10 flex items-center gap-2">
|
||||||
|
{previewDownloadUrl && (
|
||||||
|
<a
|
||||||
|
href={previewDownloadUrl}
|
||||||
|
download={fullFileName}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="grid size-10 place-items-center rounded-full bg-[rgb(var(--nodedc-accent-rgb))] text-[rgb(var(--nodedc-on-accent-rgb))] shadow-[0_14px_30px_rgba(0,0,0,0.3)] transition hover:brightness-110"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<Download className="size-5" />
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="grid size-10 place-items-center rounded-full bg-black/35 text-white backdrop-blur-md transition hover:bg-black/50"
|
||||||
|
onClick={() => setIsPreviewOpen(false)}
|
||||||
|
>
|
||||||
|
<X className="size-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-shrink-0 pr-24">
|
||||||
|
<div className="text-15 font-semibold text-primary">{fullFileName}</div>
|
||||||
|
<div className="mt-1 text-12 text-tertiary">{convertBytesToSize(size)}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 min-h-0 flex-1 overflow-hidden rounded-2xl bg-black/20">
|
||||||
|
{previewType === "image" && previewURL ? (
|
||||||
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
|
<img src={previewURL} alt={fullFileName} className="max-h-full max-w-full object-contain" />
|
||||||
|
</div>
|
||||||
|
) : previewType === "video" && previewURL ? (
|
||||||
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
|
<video src={previewURL} className="max-h-full max-w-full" controls autoPlay>
|
||||||
|
<track kind="captions" />
|
||||||
|
</video>
|
||||||
|
</div>
|
||||||
|
) : previewType === "pdf" && previewURL ? (
|
||||||
|
<IssueAttachmentPdfPreview fileURL={previewURL} />
|
||||||
|
) : isBeamAwaitingPreview || isBeamConversionFailed ? (
|
||||||
|
<div className="flex h-full flex-col items-center justify-center gap-4 p-8 text-center">
|
||||||
|
<div
|
||||||
|
className={`grid size-20 place-items-center rounded-3xl ${
|
||||||
|
isBeamConversionFailed
|
||||||
|
? "bg-red-500/10 text-red-400"
|
||||||
|
: "bg-[rgba(var(--nodedc-accent-rgb),0.12)] text-[rgb(var(--nodedc-accent-rgb))]"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{isBeamConversionFailed ? (
|
||||||
|
<AlertTriangle className="size-10" />
|
||||||
|
) : (
|
||||||
|
<Spinner
|
||||||
|
height="40px"
|
||||||
|
width="40px"
|
||||||
|
className="fill-[rgb(var(--nodedc-accent-rgb))] text-[rgba(var(--nodedc-accent-rgb),0.18)]"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="max-w-md">
|
||||||
|
<div className="text-15 font-semibold text-primary">
|
||||||
|
{isBeamConversionFailed ? "Ошибка дерева" : (beamConversionStatusLabel ?? "Ожидает дерево")}
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 text-14 text-secondary">
|
||||||
|
{isBeamConversionFailed
|
||||||
|
? beamStatusTooltipContent
|
||||||
|
: "Оригинальный STEP загружен в Beam. Preview появится после подготовки GLB/XKT и дерева компонентов."}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{modelDownloadUrl && (
|
||||||
|
<a
|
||||||
|
href={modelDownloadUrl}
|
||||||
|
download={fullFileName}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="inline-flex items-center gap-2 rounded-full bg-[rgb(var(--nodedc-accent-rgb))] px-4 py-2 text-13 font-semibold text-[rgb(var(--nodedc-on-accent-rgb))] transition hover:brightness-110"
|
||||||
|
>
|
||||||
|
<Download className="size-4" />
|
||||||
|
Скачать оригинал
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex h-full flex-col items-center justify-center gap-4 p-8 text-center">
|
||||||
|
<div className="grid size-20 place-items-center rounded-3xl bg-surface-2">{fileIcon}</div>
|
||||||
|
<div className="max-w-md text-14 text-secondary">
|
||||||
|
Предпросмотр для этого формата недоступен. Файл можно скачать или открыть в новой вкладке.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -6,16 +6,14 @@
|
||||||
|
|
||||||
import { useRef, type ReactNode } from "react";
|
import { useRef, type ReactNode } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import Link from "next/link";
|
import { Maximize2, Minimize2, MoveRight, X } from "lucide-react";
|
||||||
import { MoveDiagonal, MoveRight } from "lucide-react";
|
|
||||||
// plane imports
|
// plane imports
|
||||||
import { useTranslation } from "@plane/i18n";
|
import { useTranslation } from "@plane/i18n";
|
||||||
import { CenterPanelIcon, CopyLinkIcon, FullScreenPanelIcon, SidePanelIcon } from "@plane/propel/icons";
|
import { CopyLinkIcon } from "@plane/propel/icons";
|
||||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||||
import { Tooltip } from "@plane/propel/tooltip";
|
import { Tooltip } from "@plane/propel/tooltip";
|
||||||
import type { TNameDescriptionLoader } from "@plane/types";
|
import type { TNameDescriptionLoader } from "@plane/types";
|
||||||
import { EIssuesStoreType } from "@plane/types";
|
import { EIssuesStoreType } from "@plane/types";
|
||||||
import { SelectionDropdown } from "@/components/common/selection-dropdown";
|
|
||||||
import { copyUrlToClipboard, generateWorkItemLink } from "@plane/utils";
|
import { copyUrlToClipboard, generateWorkItemLink } from "@plane/utils";
|
||||||
// hooks
|
// hooks
|
||||||
import { useIssueDetail } from "@/hooks/store/use-issue-detail";
|
import { useIssueDetail } from "@/hooks/store/use-issue-detail";
|
||||||
|
|
@ -31,24 +29,6 @@ import { IconButton } from "@plane/propel/icon-button";
|
||||||
|
|
||||||
export type TPeekModes = "side-peek" | "modal" | "full-screen";
|
export type TPeekModes = "side-peek" | "modal" | "full-screen";
|
||||||
|
|
||||||
const PEEK_OPTIONS: { key: TPeekModes; icon: any; i18n_title: string }[] = [
|
|
||||||
{
|
|
||||||
key: "side-peek",
|
|
||||||
icon: SidePanelIcon,
|
|
||||||
i18n_title: "common.side_peek",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "modal",
|
|
||||||
icon: CenterPanelIcon,
|
|
||||||
i18n_title: "common.modal",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "full-screen",
|
|
||||||
icon: FullScreenPanelIcon,
|
|
||||||
i18n_title: "common.full_screen",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export type PeekOverviewHeaderProps = {
|
export type PeekOverviewHeaderProps = {
|
||||||
peekMode: TPeekModes;
|
peekMode: TPeekModes;
|
||||||
setPeekMode: (value: TPeekModes) => void;
|
setPeekMode: (value: TPeekModes) => void;
|
||||||
|
|
@ -68,7 +48,6 @@ export type PeekOverviewHeaderProps = {
|
||||||
actionSlot?: ReactNode;
|
actionSlot?: ReactNode;
|
||||||
metaSlot?: ReactNode;
|
metaSlot?: ReactNode;
|
||||||
showCopyLink?: boolean;
|
showCopyLink?: boolean;
|
||||||
showLayoutSwitcher?: boolean;
|
|
||||||
showQuickActions?: boolean;
|
showQuickActions?: boolean;
|
||||||
showSubscription?: boolean;
|
showSubscription?: boolean;
|
||||||
};
|
};
|
||||||
|
|
@ -82,7 +61,6 @@ export const IssuePeekOverviewHeader = observer(function IssuePeekOverviewHeader
|
||||||
issueId,
|
issueId,
|
||||||
isArchived,
|
isArchived,
|
||||||
disabled,
|
disabled,
|
||||||
embedIssue = false,
|
|
||||||
removeRoutePeekId,
|
removeRoutePeekId,
|
||||||
toggleDeleteIssueModal,
|
toggleDeleteIssueModal,
|
||||||
toggleArchiveIssueModal,
|
toggleArchiveIssueModal,
|
||||||
|
|
@ -93,7 +71,6 @@ export const IssuePeekOverviewHeader = observer(function IssuePeekOverviewHeader
|
||||||
actionSlot,
|
actionSlot,
|
||||||
metaSlot,
|
metaSlot,
|
||||||
showCopyLink = true,
|
showCopyLink = true,
|
||||||
showLayoutSwitcher = !embedIssue,
|
|
||||||
showQuickActions = true,
|
showQuickActions = true,
|
||||||
showSubscription = true,
|
showSubscription = true,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
@ -113,7 +90,6 @@ export const IssuePeekOverviewHeader = observer(function IssuePeekOverviewHeader
|
||||||
const { getProjectIdentifierById } = useProject();
|
const { getProjectIdentifierById } = useProject();
|
||||||
// derived values
|
// derived values
|
||||||
const issueDetails = getIssueById(issueId);
|
const issueDetails = getIssueById(issueId);
|
||||||
const currentMode = PEEK_OPTIONS.find((m) => m.key === peekMode);
|
|
||||||
const projectIdentifier = getProjectIdentifierById(issueDetails?.project_id);
|
const projectIdentifier = getProjectIdentifierById(issueDetails?.project_id);
|
||||||
const {
|
const {
|
||||||
issues: { removeIssue: removeArchivedIssue },
|
issues: { removeIssue: removeArchivedIssue },
|
||||||
|
|
@ -168,50 +144,29 @@ export const IssuePeekOverviewHeader = observer(function IssuePeekOverviewHeader
|
||||||
<div
|
<div
|
||||||
ref={parentRef}
|
ref={parentRef}
|
||||||
className={`relative flex flex-wrap items-start justify-between gap-3 px-6 py-5 ${
|
className={`relative flex flex-wrap items-start justify-between gap-3 px-6 py-5 ${
|
||||||
currentMode?.key === "full-screen" ? "border-b border-subtle/70" : ""
|
peekMode === "full-screen" ? "border-b border-subtle/70" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex min-w-0 flex-1 flex-wrap items-center gap-3 md:gap-4">
|
<div className="flex min-w-0 flex-1 flex-wrap items-center gap-3 md:gap-4">
|
||||||
<Tooltip tooltipContent={t("common.close_peek_view")} isMobile={isMobile}>
|
<Tooltip tooltipContent={t("common.close_peek_view")} isMobile={isMobile}>
|
||||||
<button onClick={removeRoutePeekId}>
|
<button onClick={removeRoutePeekId}>
|
||||||
<MoveRight className="h-4 w-4 text-tertiary hover:text-secondary" />
|
{peekMode === "side-peek" ? (
|
||||||
|
<MoveRight className="h-4 w-4 text-tertiary hover:text-secondary" />
|
||||||
|
) : (
|
||||||
|
<X className="h-4 w-4 text-tertiary hover:text-secondary" />
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip tooltipContent={t("issue.open_in_full_screen")} isMobile={isMobile}>
|
<Tooltip tooltipContent={t("issue.open_in_full_screen")} isMobile={isMobile}>
|
||||||
<Link href={workItemLink} onClick={() => removeRoutePeekId()}>
|
<button type="button" onClick={() => setPeekMode(peekMode === "full-screen" ? "side-peek" : "full-screen")}>
|
||||||
<MoveDiagonal className="h-4 w-4 text-tertiary hover:text-secondary" />
|
{peekMode === "full-screen" ? (
|
||||||
</Link>
|
<Minimize2 className="h-4 w-4 text-tertiary hover:text-secondary" />
|
||||||
|
) : (
|
||||||
|
<Maximize2 className="h-4 w-4 text-tertiary hover:text-secondary" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{currentMode && showLayoutSwitcher && (
|
|
||||||
<div className="flex flex-shrink-0 items-center gap-2">
|
|
||||||
<SelectionDropdown
|
|
||||||
menuButton={
|
|
||||||
<Tooltip tooltipContent={t("common.toggle_peek_view_layout")} isMobile={isMobile}>
|
|
||||||
<span>
|
|
||||||
<currentMode.icon className="h-4 w-4 text-tertiary hover:text-secondary" />
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
}
|
|
||||||
menuButtonWrapperClassName="flex items-center"
|
|
||||||
options={PEEK_OPTIONS.map((mode) => ({
|
|
||||||
key: mode.key,
|
|
||||||
isChecked: currentMode.key === mode.key,
|
|
||||||
onClick: () => setPeekMode(mode.key),
|
|
||||||
title: (
|
|
||||||
<div
|
|
||||||
className={`flex items-center gap-1.5 ${
|
|
||||||
currentMode.key === mode.key ? "text-secondary" : "text-placeholder hover:text-secondary"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<mode.icon className="-my-1 h-4 w-4 flex-shrink-0" />
|
|
||||||
{t(mode.i18n_title)}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
}))}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{metaSlot}
|
{metaSlot}
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-auto flex min-w-0 shrink-0 flex-wrap items-center justify-end gap-x-4 gap-y-2">
|
<div className="ml-auto flex min-w-0 shrink-0 flex-wrap items-center justify-end gap-x-4 gap-y-2">
|
||||||
|
|
|
||||||
|
|
@ -121,7 +121,6 @@ export const IssueView = observer(function IssueView(props: IIssueView) {
|
||||||
const issue = getIssueById(issueId);
|
const issue = getIssueById(issueId);
|
||||||
const shouldUseInteractiveEmbeddedLayout = embedIssue && interactiveEmbeddedLayout;
|
const shouldUseInteractiveEmbeddedLayout = embedIssue && interactiveEmbeddedLayout;
|
||||||
const shouldRenderPeekSurface = !embedIssue || shouldUseInteractiveEmbeddedLayout;
|
const shouldRenderPeekSurface = !embedIssue || shouldUseInteractiveEmbeddedLayout;
|
||||||
const shouldAllowPeekModeToggle = !embedIssue || shouldUseInteractiveEmbeddedLayout;
|
|
||||||
const shouldAllowPeekResize = !embedIssue || shouldUseInteractiveEmbeddedLayout;
|
const shouldAllowPeekResize = !embedIssue || shouldUseInteractiveEmbeddedLayout;
|
||||||
// remove peek id
|
// remove peek id
|
||||||
const removeRoutePeekId = () => {
|
const removeRoutePeekId = () => {
|
||||||
|
|
@ -371,8 +370,8 @@ export const IssueView = observer(function IssueView(props: IIssueView) {
|
||||||
!shouldUseBeamPeekShell && {
|
!shouldUseBeamPeekShell && {
|
||||||
"nodedc-peek-side-bound top-[5.35rem] right-3 w-[calc(100%-1.5rem)] rounded-[28px] border md:max-w-[calc(100vw-1.5rem)] md:min-w-[640px]":
|
"nodedc-peek-side-bound top-[5.35rem] right-3 w-[calc(100%-1.5rem)] rounded-[28px] border md:max-w-[calc(100vw-1.5rem)] md:min-w-[640px]":
|
||||||
peekMode === "side-peek",
|
peekMode === "side-peek",
|
||||||
"top-[8.33%] left-[8.33%] size-5/6 rounded-[28px]": peekMode === "modal",
|
"nodedc-peek-modal-bound rounded-[28px]": peekMode === "modal",
|
||||||
"absolute inset-0 m-4 rounded-[28px]": peekMode === "full-screen",
|
"nodedc-peek-full-bound rounded-[28px]": peekMode === "full-screen",
|
||||||
},
|
},
|
||||||
!embedIssue &&
|
!embedIssue &&
|
||||||
shouldUseBeamPeekShell &&
|
shouldUseBeamPeekShell &&
|
||||||
|
|
@ -448,7 +447,6 @@ export const IssueView = observer(function IssueView(props: IIssueView) {
|
||||||
isSubmitting={isSubmitting}
|
isSubmitting={isSubmitting}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
embedIssue={embedIssue}
|
embedIssue={embedIssue}
|
||||||
showLayoutSwitcher={shouldAllowPeekModeToggle}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{/* content */}
|
{/* content */}
|
||||||
|
|
|
||||||
|
|
@ -386,6 +386,31 @@
|
||||||
bottom: var(--nodedc-peek-bottom-offset) !important;
|
bottom: var(--nodedc-peek-bottom-offset) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nodedc-peek-modal-bound {
|
||||||
|
position: fixed !important;
|
||||||
|
top: 5.35rem !important;
|
||||||
|
bottom: var(--nodedc-peek-bottom-offset) !important;
|
||||||
|
left: 50% !important;
|
||||||
|
right: auto !important;
|
||||||
|
width: min(76rem, calc(100vw - 3rem)) !important;
|
||||||
|
max-width: calc(100vw - 3rem) !important;
|
||||||
|
height: auto !important;
|
||||||
|
min-height: 0 !important;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-peek-full-bound {
|
||||||
|
position: fixed !important;
|
||||||
|
top: 5.35rem !important;
|
||||||
|
right: 0.75rem !important;
|
||||||
|
bottom: var(--nodedc-peek-bottom-offset) !important;
|
||||||
|
left: 0.75rem !important;
|
||||||
|
width: auto !important;
|
||||||
|
max-width: none !important;
|
||||||
|
height: auto !important;
|
||||||
|
min-height: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
.nodedc-beam-peek-shell[data-fullscreen="true"] {
|
.nodedc-beam-peek-shell[data-fullscreen="true"] {
|
||||||
grid-template-columns: minmax(0, 1fr) 0;
|
grid-template-columns: minmax(0, 1fr) 0;
|
||||||
column-gap: 0;
|
column-gap: 0;
|
||||||
|
|
@ -5806,6 +5831,23 @@
|
||||||
color: var(--text-color-primary) !important;
|
color: var(--text-color-primary) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nodedc-attachments-panel[data-view-mode="list"] {
|
||||||
|
padding-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-attachments-list-scroll {
|
||||||
|
min-height: 13.5rem;
|
||||||
|
max-height: min(34rem, calc(100vh - 20rem));
|
||||||
|
resize: vertical;
|
||||||
|
scrollbar-gutter: stable;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-attachments-list-scroll::-webkit-resizer {
|
||||||
|
background:
|
||||||
|
linear-gradient(135deg, transparent 0 45%, rgba(var(--nodedc-accent-rgb), 0.65) 46% 54%, transparent 55%),
|
||||||
|
linear-gradient(135deg, transparent 0 62%, rgba(255, 255, 255, 0.22) 63% 70%, transparent 71%);
|
||||||
|
}
|
||||||
|
|
||||||
.nodedc-processing-loader,
|
.nodedc-processing-loader,
|
||||||
.nodedc-voice-task-processing-loader {
|
.nodedc-voice-task-processing-loader {
|
||||||
--nodedc-processing-loader-rgb: var(--nodedc-accent-rgb);
|
--nodedc-processing-loader-rgb: var(--nodedc-accent-rgb);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue