mirror of
https://github.com/makeplane/plane.git
synced 2026-07-12 13:29:56 +02:00
Merge branch 'preview' into feat/flat-list
This commit is contained in:
20
.github/workflows/build-branch.yml
vendored
20
.github/workflows/build-branch.yml
vendored
@@ -170,7 +170,7 @@ jobs:
|
||||
name: Checkout Files
|
||||
uses: actions/checkout@v4
|
||||
- name: Admin Build and Push
|
||||
uses: ./.github/actions/buildpush-action
|
||||
uses: ./.github/actions/build-push-ce
|
||||
with:
|
||||
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
|
||||
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
|
||||
@@ -196,7 +196,7 @@ jobs:
|
||||
name: Checkout Files
|
||||
uses: actions/checkout@v4
|
||||
- name: Web Build and Push
|
||||
uses: ./.github/actions/buildpush-action
|
||||
uses: ./.github/actions/build-push-ce
|
||||
with:
|
||||
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
|
||||
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
|
||||
@@ -222,7 +222,7 @@ jobs:
|
||||
name: Checkout Files
|
||||
uses: actions/checkout@v4
|
||||
- name: Space Build and Push
|
||||
uses: ./.github/actions/buildpush-action
|
||||
uses: ./.github/actions/build-push-ce
|
||||
with:
|
||||
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
|
||||
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
|
||||
@@ -248,7 +248,7 @@ jobs:
|
||||
name: Checkout Files
|
||||
uses: actions/checkout@v4
|
||||
- name: Live Build and Push
|
||||
uses: ./.github/actions/buildpush-action
|
||||
uses: ./.github/actions/build-push-ce
|
||||
with:
|
||||
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
|
||||
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
|
||||
@@ -274,7 +274,7 @@ jobs:
|
||||
name: Checkout Files
|
||||
uses: actions/checkout@v4
|
||||
- name: Backend Build and Push
|
||||
uses: ./.github/actions/buildpush-action
|
||||
uses: ./.github/actions/build-push-ce
|
||||
with:
|
||||
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
|
||||
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
|
||||
@@ -300,7 +300,7 @@ jobs:
|
||||
name: Checkout Files
|
||||
uses: actions/checkout@v4
|
||||
- name: Proxy Build and Push
|
||||
uses: ./.github/actions/buildpush-action
|
||||
uses: ./.github/actions/build-push-ce
|
||||
with:
|
||||
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
|
||||
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
|
||||
@@ -320,7 +320,7 @@ jobs:
|
||||
if: ${{ needs.branch_build_setup.outputs.build_type == 'Build' }}
|
||||
name: Attach Assets to Build
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [ branch_build_setup ]
|
||||
needs: [branch_build_setup]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -340,7 +340,7 @@ jobs:
|
||||
${{ github.workspace }}/deploy/selfhost/restore.sh
|
||||
${{ github.workspace }}/deploy/selfhost/docker-compose.yml
|
||||
${{ github.workspace }}/deploy/selfhost/variables.env
|
||||
|
||||
|
||||
publish_release:
|
||||
if: ${{ needs.branch_build_setup.outputs.build_type == 'Release' }}
|
||||
name: Build Release
|
||||
@@ -353,7 +353,7 @@ jobs:
|
||||
branch_build_push_space,
|
||||
branch_build_push_live,
|
||||
branch_build_push_apiserver,
|
||||
branch_build_push_proxy
|
||||
branch_build_push_proxy,
|
||||
]
|
||||
env:
|
||||
REL_VERSION: ${{ needs.branch_build_setup.outputs.release_version }}
|
||||
@@ -380,4 +380,4 @@ jobs:
|
||||
${{ github.workspace }}/deploy/selfhost/setup.sh
|
||||
${{ github.workspace }}/deploy/selfhost/restore.sh
|
||||
${{ github.workspace }}/deploy/selfhost/docker-compose.yml
|
||||
${{ github.workspace }}/deploy/selfhost/variables.env
|
||||
${{ github.workspace }}/deploy/selfhost/variables.env
|
||||
|
||||
@@ -72,6 +72,8 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const containerRect = useRef<DOMRect | null>(null);
|
||||
const imageRef = useRef<HTMLImageElement>(null);
|
||||
const [hasErroredOnFirstLoad, setHasErroredOnFirstLoad] = useState(false);
|
||||
const [hasTriedRestoringImageOnce, setHasTriedRestoringImageOnce] = useState(false);
|
||||
|
||||
const updateAttributesSafely = useCallback(
|
||||
(attributes: Partial<ImageAttributes>, errorMessage: string) => {
|
||||
@@ -145,8 +147,9 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {
|
||||
...prevSize,
|
||||
width: ensurePixelString(nodeWidth),
|
||||
height: ensurePixelString(nodeHeight),
|
||||
aspectRatio: nodeAspectRatio,
|
||||
}));
|
||||
}, [nodeWidth, nodeHeight]);
|
||||
}, [nodeWidth, nodeHeight, nodeAspectRatio]);
|
||||
|
||||
const handleResize = useCallback(
|
||||
(e: MouseEvent | TouchEvent) => {
|
||||
@@ -159,7 +162,7 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {
|
||||
|
||||
setSize((prevSize) => ({ ...prevSize, width: `${newWidth}px`, height: `${newHeight}px` }));
|
||||
},
|
||||
[size]
|
||||
[size.aspectRatio]
|
||||
);
|
||||
|
||||
const handleResizeEnd = useCallback(() => {
|
||||
@@ -182,11 +185,15 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {
|
||||
window.addEventListener("mousemove", handleResize);
|
||||
window.addEventListener("mouseup", handleResizeEnd);
|
||||
window.addEventListener("mouseleave", handleResizeEnd);
|
||||
window.addEventListener("touchmove", handleResize);
|
||||
window.addEventListener("touchend", handleResizeEnd);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("mousemove", handleResize);
|
||||
window.removeEventListener("mouseup", handleResizeEnd);
|
||||
window.removeEventListener("mouseleave", handleResizeEnd);
|
||||
window.removeEventListener("touchmove", handleResize);
|
||||
window.removeEventListener("touchend", handleResizeEnd);
|
||||
};
|
||||
}
|
||||
}, [isResizing, handleResize, handleResizeEnd]);
|
||||
@@ -203,7 +210,7 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {
|
||||
|
||||
// show the image loader if the remote image's src or preview image from filesystem is not set yet (while loading the image post upload) (or)
|
||||
// if the initial resize (from 35% width and "auto" height attrs to the actual size in px) is not complete
|
||||
const showImageLoader = !(remoteImageSrc || imageFromFileSystem) || !initialResizeComplete;
|
||||
const showImageLoader = !(remoteImageSrc || imageFromFileSystem) || !initialResizeComplete || hasErroredOnFirstLoad;
|
||||
// show the image utils only if the remote image's (post upload) src is set and the initial resize is complete (but not while we're showing the preview imageFromFileSystem)
|
||||
const showImageUtils = remoteImageSrc && initialResizeComplete;
|
||||
// show the image resizer only if the editor is editable, the remote image's (post upload) src is set and the initial resize is complete (but not while we're showing the preview imageFromFileSystem)
|
||||
@@ -231,9 +238,26 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {
|
||||
ref={imageRef}
|
||||
src={displayedImageSrc}
|
||||
onLoad={handleImageLoad}
|
||||
onError={(e) => {
|
||||
console.error("Error loading image", e);
|
||||
setFailedToLoadImage(true);
|
||||
onError={async (e) => {
|
||||
// for old image extension this command doesn't exist or if the image failed to load for the first time
|
||||
if (!editor?.commands.restoreImage || hasTriedRestoringImageOnce) {
|
||||
setFailedToLoadImage(true);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setHasErroredOnFirstLoad(true);
|
||||
// this is a type error from tiptap, don't remove await until it's fixed
|
||||
await editor?.commands.restoreImage?.(node.attrs.src);
|
||||
imageRef.current.src = remoteImageSrc;
|
||||
} catch {
|
||||
// if the image failed to even restore, then show the error state
|
||||
setFailedToLoadImage(true);
|
||||
console.error("Error while loading image", e);
|
||||
} finally {
|
||||
setHasErroredOnFirstLoad(false);
|
||||
setHasTriedRestoringImageOnce(true);
|
||||
}
|
||||
}}
|
||||
width={size.width}
|
||||
className={cn("image-component block rounded-md", {
|
||||
@@ -284,6 +308,7 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {
|
||||
}
|
||||
)}
|
||||
onMouseDown={handleResizeStart}
|
||||
onTouchStart={handleResizeStart}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Node as ProsemirrorNode } from "@tiptap/pm/model";
|
||||
import { Editor, NodeViewWrapper } from "@tiptap/react";
|
||||
import { Editor, NodeViewProps, NodeViewWrapper } from "@tiptap/react";
|
||||
// extensions
|
||||
import { CustomImageBlock, CustomImageUploader, ImageAttributes } from "@/extensions/custom-image";
|
||||
|
||||
export type CustomImageNodeViewProps = {
|
||||
export type CustomImageComponentProps = {
|
||||
getPos: () => number;
|
||||
editor: Editor;
|
||||
node: ProsemirrorNode & {
|
||||
node: NodeViewProps["node"] & {
|
||||
attrs: ImageAttributes;
|
||||
};
|
||||
updateAttributes: (attrs: Record<string, any>) => void;
|
||||
updateAttributes: (attrs: ImageAttributes) => void;
|
||||
selected: boolean;
|
||||
};
|
||||
|
||||
export type CustomImageNodeViewProps = NodeViewProps & CustomImageComponentProps;
|
||||
|
||||
export const CustomImageNode = (props: CustomImageNodeViewProps) => {
|
||||
const { getPos, editor, node, updateAttributes, selected } = props;
|
||||
const { src: remoteImageSrc } = node.attrs;
|
||||
|
||||
const [isUploaded, setIsUploaded] = useState(false);
|
||||
const [imageFromFileSystem, setImageFromFileSystem] = useState<string | undefined>(undefined);
|
||||
@@ -37,14 +39,13 @@ export const CustomImageNode = (props: CustomImageNodeViewProps) => {
|
||||
// the image is already uploaded if the image-component node has src attribute
|
||||
// and we need to remove the blob from our file system
|
||||
useEffect(() => {
|
||||
const remoteImageSrc = node.attrs.src;
|
||||
if (remoteImageSrc) {
|
||||
setIsUploaded(true);
|
||||
setImageFromFileSystem(undefined);
|
||||
} else {
|
||||
setIsUploaded(false);
|
||||
}
|
||||
}, [node.attrs.src]);
|
||||
}, [remoteImageSrc]);
|
||||
|
||||
return (
|
||||
<NodeViewWrapper>
|
||||
@@ -55,7 +56,7 @@ export const CustomImageNode = (props: CustomImageNodeViewProps) => {
|
||||
editorContainer={editorContainer}
|
||||
editor={editor}
|
||||
// @ts-expect-error function not expected here, but will still work
|
||||
src={editor?.commands?.getImageSource?.(node.attrs.src)}
|
||||
src={editor?.commands?.getImageSource?.(remoteImageSrc)}
|
||||
getPos={getPos}
|
||||
node={node}
|
||||
setEditorContainer={setEditorContainer}
|
||||
|
||||
@@ -1,27 +1,20 @@
|
||||
import { ChangeEvent, useCallback, useEffect, useMemo, useRef } from "react";
|
||||
import { Node as ProsemirrorNode } from "@tiptap/pm/model";
|
||||
import { Editor } from "@tiptap/core";
|
||||
import { ImageIcon } from "lucide-react";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common";
|
||||
// hooks
|
||||
import { useUploader, useDropZone, uploadFirstImageAndInsertRemaining } from "@/hooks/use-file-upload";
|
||||
// extensions
|
||||
import { getImageComponentImageFileMap, ImageAttributes } from "@/extensions/custom-image";
|
||||
import { type CustomImageComponentProps, getImageComponentImageFileMap } from "@/extensions/custom-image";
|
||||
|
||||
export const CustomImageUploader = (props: {
|
||||
editor: Editor;
|
||||
failedToLoadImage: boolean;
|
||||
getPos: () => number;
|
||||
loadImageFromFileSystem: (file: string) => void;
|
||||
type CustomImageUploaderProps = CustomImageComponentProps & {
|
||||
maxFileSize: number;
|
||||
node: ProsemirrorNode & {
|
||||
attrs: ImageAttributes;
|
||||
};
|
||||
selected: boolean;
|
||||
loadImageFromFileSystem: (file: string) => void;
|
||||
failedToLoadImage: boolean;
|
||||
setIsUploaded: (isUploaded: boolean) => void;
|
||||
updateAttributes: (attrs: Record<string, any>) => void;
|
||||
}) => {
|
||||
};
|
||||
|
||||
export const CustomImageUploader = (props: CustomImageUploaderProps) => {
|
||||
const {
|
||||
editor,
|
||||
failedToLoadImage,
|
||||
@@ -36,8 +29,8 @@ export const CustomImageUploader = (props: {
|
||||
// refs
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const hasTriggeredFilePickerRef = useRef(false);
|
||||
const { id: imageEntityId } = node.attrs;
|
||||
// derived values
|
||||
const imageEntityId = node.attrs.id;
|
||||
const imageComponentImageFileMap = useMemo(() => getImageComponentImageFileMap(editor), [editor]);
|
||||
|
||||
const onUpload = useCallback(
|
||||
|
||||
@@ -22,6 +22,7 @@ declare module "@tiptap/core" {
|
||||
imageComponent: {
|
||||
insertImageComponent: ({ file, pos, event }: InsertImageComponentProps) => ReturnType;
|
||||
uploadImage: (file: File) => () => Promise<string> | undefined;
|
||||
restoreImage: (src: string) => () => Promise<void>;
|
||||
getImageSource?: (path: string) => () => string;
|
||||
};
|
||||
}
|
||||
@@ -40,8 +41,8 @@ export const CustomImageExtension = (props: TFileHandler) => {
|
||||
const {
|
||||
getAssetSrc,
|
||||
upload,
|
||||
delete: deleteImage,
|
||||
restore: restoreImage,
|
||||
delete: deleteImageFn,
|
||||
restore: restoreImageFn,
|
||||
validation: { maxFileSize },
|
||||
} = props;
|
||||
|
||||
@@ -85,22 +86,6 @@ export const CustomImageExtension = (props: TFileHandler) => {
|
||||
return ["image-component", mergeAttributes(HTMLAttributes)];
|
||||
},
|
||||
|
||||
onCreate(this) {
|
||||
const imageSources = new Set<string>();
|
||||
this.editor.state.doc.descendants((node) => {
|
||||
if (node.type.name === this.name) {
|
||||
imageSources.add(node.attrs.src);
|
||||
}
|
||||
});
|
||||
imageSources.forEach(async (src) => {
|
||||
try {
|
||||
await restoreImage(src);
|
||||
} catch (error) {
|
||||
console.error("Error restoring image: ", error);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
addKeyboardShortcuts() {
|
||||
return {
|
||||
ArrowDown: insertEmptyParagraphAtNodeBoundaries("down", this.name),
|
||||
@@ -110,11 +95,29 @@ export const CustomImageExtension = (props: TFileHandler) => {
|
||||
|
||||
addProseMirrorPlugins() {
|
||||
return [
|
||||
TrackImageDeletionPlugin(this.editor, deleteImage, this.name),
|
||||
TrackImageRestorationPlugin(this.editor, restoreImage, this.name),
|
||||
TrackImageDeletionPlugin(this.editor, deleteImageFn, this.name),
|
||||
TrackImageRestorationPlugin(this.editor, restoreImageFn, this.name),
|
||||
];
|
||||
},
|
||||
|
||||
onCreate(this) {
|
||||
const imageSources = new Set<string>();
|
||||
this.editor.state.doc.descendants((node) => {
|
||||
if (node.type.name === this.name) {
|
||||
if (!node.attrs.src?.startsWith("http")) return;
|
||||
|
||||
imageSources.add(node.attrs.src);
|
||||
}
|
||||
});
|
||||
imageSources.forEach(async (src) => {
|
||||
try {
|
||||
await restoreImageFn(src);
|
||||
} catch (error) {
|
||||
console.error("Error restoring image: ", error);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
addStorage() {
|
||||
return {
|
||||
fileMap: new Map(),
|
||||
@@ -179,6 +182,9 @@ export const CustomImageExtension = (props: TFileHandler) => {
|
||||
const fileUrl = await upload(file);
|
||||
return fileUrl;
|
||||
},
|
||||
restoreImage: (src: string) => async () => {
|
||||
await restoreImageFn(src);
|
||||
},
|
||||
getImageSource: (path: string) => () => getAssetSrc(path),
|
||||
};
|
||||
},
|
||||
|
||||
@@ -144,7 +144,10 @@ export const CoreEditorExtensions = (args: TArguments) => {
|
||||
if (editor.storage.imageComponent.uploadInProgress) return "";
|
||||
|
||||
const shouldHidePlaceholder =
|
||||
editor.isActive("table") || editor.isActive("codeBlock") || editor.isActive("image");
|
||||
editor.isActive("table") ||
|
||||
editor.isActive("codeBlock") ||
|
||||
editor.isActive("image") ||
|
||||
editor.isActive("imageComponent");
|
||||
|
||||
if (shouldHidePlaceholder) return "";
|
||||
|
||||
|
||||
@@ -11,9 +11,9 @@ import { CustomImageNode } from "@/extensions";
|
||||
|
||||
export const ImageExtension = (fileHandler: TFileHandler) => {
|
||||
const {
|
||||
delete: deleteImage,
|
||||
getAssetSrc,
|
||||
restore: restoreImage,
|
||||
delete: deleteImageFn,
|
||||
restore: restoreImageFn,
|
||||
validation: { maxFileSize },
|
||||
} = fileHandler;
|
||||
|
||||
@@ -24,10 +24,11 @@ export const ImageExtension = (fileHandler: TFileHandler) => {
|
||||
ArrowUp: insertEmptyParagraphAtNodeBoundaries("up", this.name),
|
||||
};
|
||||
},
|
||||
|
||||
addProseMirrorPlugins() {
|
||||
return [
|
||||
TrackImageDeletionPlugin(this.editor, deleteImage, this.name),
|
||||
TrackImageRestorationPlugin(this.editor, restoreImage, this.name),
|
||||
TrackImageDeletionPlugin(this.editor, deleteImageFn, this.name),
|
||||
TrackImageRestorationPlugin(this.editor, restoreImageFn, this.name),
|
||||
];
|
||||
},
|
||||
|
||||
@@ -35,12 +36,14 @@ export const ImageExtension = (fileHandler: TFileHandler) => {
|
||||
const imageSources = new Set<string>();
|
||||
this.editor.state.doc.descendants((node) => {
|
||||
if (node.type.name === this.name) {
|
||||
if (!node.attrs.src?.startsWith("http")) return;
|
||||
|
||||
imageSources.add(node.attrs.src);
|
||||
}
|
||||
});
|
||||
imageSources.forEach(async (src) => {
|
||||
try {
|
||||
await restoreImage(src);
|
||||
await restoreImageFn(src);
|
||||
} catch (error) {
|
||||
console.error("Error restoring image: ", error);
|
||||
}
|
||||
@@ -65,6 +68,9 @@ export const ImageExtension = (fileHandler: TFileHandler) => {
|
||||
height: {
|
||||
default: null,
|
||||
},
|
||||
aspectRatio: {
|
||||
default: null,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
@@ -11,6 +11,9 @@ export const ImageExtensionWithoutProps = () =>
|
||||
height: {
|
||||
default: null,
|
||||
},
|
||||
aspectRatio: {
|
||||
default: null,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -18,6 +18,9 @@ export const ReadOnlyImageExtension = (props: Pick<TFileHandler, "getAssetSrc">)
|
||||
height: {
|
||||
default: null,
|
||||
},
|
||||
aspectRatio: {
|
||||
default: null,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
@@ -25,6 +25,9 @@ export const TrackImageRestorationPlugin = (editor: Editor, restoreImage: Restor
|
||||
if (node.type.name !== nodeType) return;
|
||||
if (pos < 0 || pos > newState.doc.content.size) return;
|
||||
if (oldImageSources.has(node.attrs.src)) return;
|
||||
// if the src is just a id (private bucket), then we don't need to handle restore from here but
|
||||
// only while it fails to load
|
||||
if (!node.attrs.src?.startsWith("http")) return;
|
||||
addedImages.push(node as ImageNode);
|
||||
});
|
||||
|
||||
|
||||
13
packages/types/src/inbox.d.ts
vendored
13
packages/types/src/inbox.d.ts
vendored
@@ -96,3 +96,16 @@ export type TInboxIssuePaginationInfo = TPaginationInfo & {
|
||||
export type TInboxIssueWithPagination = TInboxIssuePaginationInfo & {
|
||||
results: TInboxIssue[];
|
||||
};
|
||||
|
||||
export type TInboxForm = {
|
||||
anchor: string;
|
||||
id: string;
|
||||
is_disabled: boolean;
|
||||
};
|
||||
|
||||
export type TInboxIssueForm = {
|
||||
name: string;
|
||||
description: string;
|
||||
username: string;
|
||||
email: string;
|
||||
};
|
||||
|
||||
@@ -18,6 +18,7 @@ export const Popover = (props: TPopover) => {
|
||||
panelClassName = "",
|
||||
children,
|
||||
popoverButtonRef,
|
||||
buttonRefClassName = "",
|
||||
} = props;
|
||||
// states
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLDivElement | null>(null);
|
||||
@@ -38,7 +39,7 @@ export const Popover = (props: TPopover) => {
|
||||
|
||||
return (
|
||||
<HeadlessReactPopover className={cn("relative flex h-full w-full items-center justify-center", popoverClassName)}>
|
||||
<div ref={setReferenceElement} className="w-full">
|
||||
<div ref={setReferenceElement} className={cn("w-full", buttonRefClassName)}>
|
||||
<HeadlessReactPopover.Button
|
||||
ref={popoverButtonRef as Ref<HTMLButtonElement>}
|
||||
className={cn(
|
||||
|
||||
@@ -5,6 +5,7 @@ export type TPopoverButtonDefaultOptions = {
|
||||
// button and button styling
|
||||
button?: ReactNode;
|
||||
buttonClassName?: string;
|
||||
buttonRefClassName?: string;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ export class FileUploadService extends APIService {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
cancelToken: this.cancelSource.token,
|
||||
withCredentials: false,
|
||||
})
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
|
||||
@@ -4,7 +4,6 @@ import { TFileHandler } from "@plane/editor";
|
||||
import { MAX_FILE_SIZE } from "@/constants/common";
|
||||
// helpers
|
||||
import { getFileURL } from "@/helpers/file.helper";
|
||||
import { checkURLValidity } from "@/helpers/string.helper";
|
||||
// services
|
||||
import { FileService } from "@/services/file.service";
|
||||
const fileService = new FileService();
|
||||
@@ -34,7 +33,7 @@ export const getEditorFileHandlers = (args: TArgs): TFileHandler => {
|
||||
return {
|
||||
getAssetSrc: (path) => {
|
||||
if (!path) return "";
|
||||
if (checkURLValidity(path)) {
|
||||
if (path?.startsWith("http")) {
|
||||
return path;
|
||||
} else {
|
||||
return getEditorAssetSrc(anchor, path) ?? "";
|
||||
@@ -42,14 +41,14 @@ export const getEditorFileHandlers = (args: TArgs): TFileHandler => {
|
||||
},
|
||||
upload: uploadFile,
|
||||
delete: async (src: string) => {
|
||||
if (checkURLValidity(src)) {
|
||||
if (src?.startsWith("http")) {
|
||||
await fileService.deleteOldEditorAsset(workspaceId, src);
|
||||
} else {
|
||||
await fileService.deleteNewAsset(getEditorAssetSrc(anchor, src) ?? "");
|
||||
}
|
||||
},
|
||||
restore: async (src: string) => {
|
||||
if (checkURLValidity(src)) {
|
||||
if (src?.startsWith("http")) {
|
||||
await fileService.restoreOldEditorAsset(workspaceId, src);
|
||||
} else {
|
||||
await fileService.restoreNewAsset(anchor, src);
|
||||
@@ -73,7 +72,7 @@ export const getReadOnlyEditorFileHandlers = (
|
||||
return {
|
||||
getAssetSrc: (path) => {
|
||||
if (!path) return "";
|
||||
if (checkURLValidity(path)) {
|
||||
if (path?.startsWith("http")) {
|
||||
return path;
|
||||
} else {
|
||||
return getEditorAssetSrc(anchor, path) ?? "";
|
||||
|
||||
BIN
space/public/instance/intake-sent-dark.png
Normal file
BIN
space/public/instance/intake-sent-dark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 135 KiB |
BIN
space/public/instance/intake-sent-light.png
Normal file
BIN
space/public/instance/intake-sent-light.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 190 KiB |
@@ -2,7 +2,7 @@
|
||||
|
||||
// components
|
||||
import { AppHeader, ContentWrapper } from "@/components/core";
|
||||
import { ProjectInboxHeader } from "./header";
|
||||
import { ProjectInboxHeader } from "@/plane-web/components/projects/settings/intake";
|
||||
|
||||
export default function ProjectInboxIssuesLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
|
||||
1
web/ce/components/projects/settings/intake/index.ts
Normal file
1
web/ce/components/projects/settings/intake/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./header";
|
||||
@@ -1,16 +1,23 @@
|
||||
import { ReactNode } from "react";
|
||||
import { FileText, Layers, Timer } from "lucide-react";
|
||||
import { IProject } from "@plane/types";
|
||||
import { ContrastIcon, DiceIcon, Intake } from "@plane/ui";
|
||||
|
||||
export type TProperties = {
|
||||
property: string;
|
||||
title: string;
|
||||
description: string;
|
||||
icon: ReactNode;
|
||||
isPro: boolean;
|
||||
isEnabled: boolean;
|
||||
renderChildren?: (
|
||||
currentProjectDetails: IProject,
|
||||
isAdmin: boolean,
|
||||
handleSubmit: (featureKey: string, featureProperty: string) => Promise<void>
|
||||
) => ReactNode;
|
||||
};
|
||||
export type TFeatureList = {
|
||||
[key: string]: {
|
||||
property: string;
|
||||
title: string;
|
||||
description: string;
|
||||
icon: ReactNode;
|
||||
isPro: boolean;
|
||||
isEnabled: boolean;
|
||||
};
|
||||
[key: string]: TProperties;
|
||||
};
|
||||
|
||||
export type TProjectFeatures = {
|
||||
|
||||
@@ -2,14 +2,15 @@
|
||||
|
||||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { MinusCircle } from "lucide-react";
|
||||
import { TIssue } from "@plane/types";
|
||||
// component
|
||||
// ui
|
||||
import { CustomMenu } from "@plane/ui";
|
||||
import { ControlLink, CustomMenu } from "@plane/ui";
|
||||
// hooks
|
||||
import { useIssues, useProjectState } from "@/hooks/store";
|
||||
import useIssuePeekOverviewRedirection from "@/hooks/use-issue-peek-overview-redirection";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// plane web components
|
||||
import { IssueIdentifier } from "@/plane-web/components/issues";
|
||||
// types
|
||||
@@ -29,6 +30,8 @@ export const IssueParentDetail: FC<TIssueParentDetail> = observer((props) => {
|
||||
// hooks
|
||||
const { issueMap } = useIssues();
|
||||
const { getProjectStates } = useProjectState();
|
||||
const { handleRedirection } = useIssuePeekOverviewRedirection();
|
||||
const { isMobile } = usePlatformOS();
|
||||
|
||||
const parentIssue = issueMap?.[issue.parent_id || ""] || undefined;
|
||||
|
||||
@@ -42,7 +45,10 @@ export const IssueParentDetail: FC<TIssueParentDetail> = observer((props) => {
|
||||
return (
|
||||
<>
|
||||
<div className="mb-5 flex w-min items-center gap-3 whitespace-nowrap rounded-md border border-custom-border-300 bg-custom-background-80 px-2.5 py-1 text-xs">
|
||||
<Link href={`/${workspaceSlug}/projects/${parentIssue?.project_id}/issues/${parentIssue.id}`} target="_blank">
|
||||
<ControlLink
|
||||
href={`/${workspaceSlug}/projects/${parentIssue?.project_id}/issues/${parentIssue.id}`}
|
||||
onClick={() => handleRedirection(workspaceSlug, parentIssue, isMobile)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<span className="block h-2 w-2 rounded-full" style={{ backgroundColor: stateColor }} />
|
||||
@@ -56,7 +62,7 @@ export const IssueParentDetail: FC<TIssueParentDetail> = observer((props) => {
|
||||
</div>
|
||||
<span className="truncate text-custom-text-100">{(parentIssue?.name ?? "").substring(0, 50)}</span>
|
||||
</div>
|
||||
</Link>
|
||||
</ControlLink>
|
||||
|
||||
<CustomMenu ellipsis optionsClassName="p-1.5">
|
||||
<div className="border-b border-custom-border-300 text-xs font-medium text-custom-text-200">
|
||||
|
||||
@@ -7,12 +7,21 @@ import { useParams, usePathname, useSearchParams } from "next/navigation";
|
||||
import { Info, SquareUser } from "lucide-react";
|
||||
// ui
|
||||
import { IModule } from "@plane/types";
|
||||
import { Card, FavoriteStar, LayersIcon, LinearProgressIndicator, TOAST_TYPE, Tooltip, setPromiseToast, setToast } from "@plane/ui";
|
||||
import {
|
||||
Card,
|
||||
FavoriteStar,
|
||||
LayersIcon,
|
||||
LinearProgressIndicator,
|
||||
TOAST_TYPE,
|
||||
Tooltip,
|
||||
setPromiseToast,
|
||||
setToast,
|
||||
} from "@plane/ui";
|
||||
// components
|
||||
import { DateRangeDropdown } from "@/components/dropdowns";
|
||||
import { ButtonAvatars } from "@/components/dropdowns/member/avatar";
|
||||
import { ModuleQuickActions } from "@/components/modules";
|
||||
import { ModuleStatusDropdown } from "@/components/modules/module-status-dropdown";
|
||||
import { ModuleStatusDropdown } from "@/components/modules/module-status-dropdown";
|
||||
// constants
|
||||
import { PROGRESS_STATE_GROUPS_DETAILS } from "@/constants/common";
|
||||
import { MODULE_FAVORITED, MODULE_UNFAVORITED } from "@/constants/event-tracker";
|
||||
@@ -21,11 +30,10 @@ import { MODULE_STATUS } from "@/constants/module";
|
||||
import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper";
|
||||
import { generateQueryParams } from "@/helpers/router.helper";
|
||||
// hooks
|
||||
import { useEventTracker, useMember, useModule, useProjectEstimates, useUserPermissions } from "@/hooks/store";
|
||||
import { useEventTracker, useMember, useModule, useUserPermissions } from "@/hooks/store";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// plane web constants
|
||||
import { EEstimateSystem } from "@/plane-web/constants/estimates";
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
|
||||
|
||||
type Props = {
|
||||
@@ -46,7 +54,6 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
|
||||
const { getModuleById, addModuleToFavorites, removeModuleFromFavorites, updateModuleDetails } = useModule();
|
||||
const { getUserDetails } = useMember();
|
||||
const { captureEvent } = useEventTracker();
|
||||
const { currentActiveEstimateId, areEstimateEnabledByProjectId, estimateById } = useProjectEstimates();
|
||||
|
||||
// derived values
|
||||
const moduleDetails = getModuleById(moduleId);
|
||||
@@ -57,7 +64,6 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
|
||||
const isDisabled = !isEditingAllowed || !!moduleDetails?.archived_at;
|
||||
const renderIcon = Boolean(moduleDetails?.start_date) || Boolean(moduleDetails?.target_date);
|
||||
|
||||
|
||||
const { isMobile } = usePlatformOS();
|
||||
const handleAddToFavorites = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation();
|
||||
@@ -156,29 +162,14 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
|
||||
|
||||
if (!moduleDetails) return null;
|
||||
|
||||
/**
|
||||
* NOTE: This completion percentage calculation is based on the total issues count.
|
||||
* when estimates are available and estimate type is points, we should consider the estimate point count
|
||||
* when estimates are available and estimate type is not points, then by default we consider the issue count
|
||||
*/
|
||||
const isEstimateEnabled =
|
||||
projectId &&
|
||||
currentActiveEstimateId &&
|
||||
areEstimateEnabledByProjectId(projectId.toString()) &&
|
||||
estimateById(currentActiveEstimateId)?.type === EEstimateSystem.POINTS;
|
||||
|
||||
const moduleTotalIssues = isEstimateEnabled
|
||||
? moduleDetails?.total_estimate_points || 0
|
||||
: moduleDetails.backlog_issues +
|
||||
moduleDetails.unstarted_issues +
|
||||
moduleDetails.started_issues +
|
||||
moduleDetails.completed_issues +
|
||||
moduleDetails.cancelled_issues;
|
||||
|
||||
const moduleCompletedIssues = isEstimateEnabled
|
||||
? moduleDetails?.completed_estimate_points || 0
|
||||
: moduleDetails.completed_issues;
|
||||
const moduleTotalIssues =
|
||||
moduleDetails.backlog_issues +
|
||||
moduleDetails.unstarted_issues +
|
||||
moduleDetails.started_issues +
|
||||
moduleDetails.completed_issues +
|
||||
moduleDetails.cancelled_issues;
|
||||
|
||||
const moduleCompletedIssues = moduleDetails.completed_issues;
|
||||
|
||||
// const areYearsEqual = startDate.getFullYear() === endDate.getFullYear();
|
||||
|
||||
@@ -186,11 +177,11 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
|
||||
|
||||
const issueCount = module
|
||||
? !moduleTotalIssues || moduleTotalIssues === 0
|
||||
? `0 ${isEstimateEnabled ? `Point` : `Issue`}`
|
||||
? `0 Issue`
|
||||
: moduleTotalIssues === moduleCompletedIssues
|
||||
? `${moduleTotalIssues} Issue${moduleTotalIssues > 1 ? `s` : ``}`
|
||||
: `${moduleCompletedIssues}/${moduleTotalIssues} ${isEstimateEnabled ? `Points` : `Issues`}`
|
||||
: `0 ${isEstimateEnabled ? `Point` : `Issue`}`;
|
||||
: `${moduleCompletedIssues}/${moduleTotalIssues} Issues`
|
||||
: `0 Issue`;
|
||||
|
||||
const moduleLeadDetails = moduleDetails.lead_id ? getUserDetails(moduleDetails.lead_id) : undefined;
|
||||
|
||||
@@ -213,9 +204,9 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
|
||||
<div className="flex items-center gap-2" onClick={handleEventPropagation}>
|
||||
{moduleStatus && (
|
||||
<ModuleStatusDropdown
|
||||
isDisabled={isDisabled}
|
||||
moduleDetails={moduleDetails}
|
||||
handleModuleDetailsChange={handleModuleDetailsChange}
|
||||
isDisabled={isDisabled}
|
||||
moduleDetails={moduleDetails}
|
||||
handleModuleDetailsChange={handleModuleDetailsChange}
|
||||
/>
|
||||
)}
|
||||
<button onClick={openModuleOverview}>
|
||||
@@ -252,9 +243,9 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
|
||||
}}
|
||||
onSelect={(val) => {
|
||||
handleModuleDetailsChange({
|
||||
start_date: (val?.from ? renderFormattedPayloadDate(val.from) : null),
|
||||
target_date: (val?.to ? renderFormattedPayloadDate(val.to) : null)
|
||||
})
|
||||
start_date: val?.from ? renderFormattedPayloadDate(val.from) : null,
|
||||
target_date: val?.to ? renderFormattedPayloadDate(val.to) : null,
|
||||
});
|
||||
}}
|
||||
placeholder={{
|
||||
from: "Start date",
|
||||
@@ -288,4 +279,4 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,11 +13,9 @@ import { ModuleListItemAction, ModuleQuickActions } from "@/components/modules";
|
||||
// helpers
|
||||
import { generateQueryParams } from "@/helpers/router.helper";
|
||||
// hooks
|
||||
import { useModule, useProjectEstimates } from "@/hooks/store";
|
||||
import { useModule } from "@/hooks/store";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// plane web constants
|
||||
import { EEstimateSystem } from "@/plane-web/constants/estimates";
|
||||
|
||||
type Props = {
|
||||
moduleId: string;
|
||||
@@ -35,27 +33,14 @@ export const ModuleListItem: React.FC<Props> = observer((props) => {
|
||||
// store hooks
|
||||
const { getModuleById } = useModule();
|
||||
const { isMobile } = usePlatformOS();
|
||||
const { currentActiveEstimateId, areEstimateEnabledByProjectId, estimateById } = useProjectEstimates();
|
||||
|
||||
// derived values
|
||||
const moduleDetails = getModuleById(moduleId);
|
||||
|
||||
if (!moduleDetails) return null;
|
||||
|
||||
/**
|
||||
* NOTE: This completion percentage calculation is based on the total issues count.
|
||||
* when estimates are available and estimate type is points, we should consider the estimate point count
|
||||
* when estimates are available and estimate type is not points, then by default we consider the issue count
|
||||
*/
|
||||
const isEstimateEnabled =
|
||||
projectId &&
|
||||
currentActiveEstimateId &&
|
||||
areEstimateEnabledByProjectId(projectId?.toString()) &&
|
||||
estimateById(currentActiveEstimateId)?.type === EEstimateSystem.POINTS;
|
||||
|
||||
const completionPercentage = isEstimateEnabled
|
||||
? ((moduleDetails?.completed_estimate_points || 0) / (moduleDetails?.total_estimate_points || 0)) * 100
|
||||
: ((moduleDetails.completed_issues + moduleDetails.cancelled_issues) / moduleDetails.total_issues) * 100;
|
||||
const completionPercentage =
|
||||
((moduleDetails.completed_issues + moduleDetails.cancelled_issues) / moduleDetails.total_issues) * 100;
|
||||
|
||||
const progress = isNaN(completionPercentage) ? 0 : Math.floor(completionPercentage);
|
||||
|
||||
|
||||
@@ -70,31 +70,40 @@ export const ProjectFeaturesList: FC<Props> = observer((props) => {
|
||||
return (
|
||||
<div
|
||||
key={featureItemKey}
|
||||
className="flex items-center justify-between gap-x-8 gap-y-2 border-b border-custom-border-100 bg-custom-background-100 pb-2 pt-4 last:border-b-0"
|
||||
className="gap-x-8 gap-y-2 border-b border-custom-border-100 bg-custom-background-100 pb-2 pt-4"
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex items-center justify-center rounded bg-custom-background-90 p-3">
|
||||
{featureItem.icon}
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h4 className="text-sm font-medium leading-5">{featureItem.title}</h4>
|
||||
{featureItem.isPro && (
|
||||
<Tooltip tooltipContent="Pro feature" position="top">
|
||||
<UpgradeBadge />
|
||||
</Tooltip>
|
||||
)}
|
||||
<div key={featureItemKey} className="flex items-center justify-between">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex items-center justify-center rounded bg-custom-background-90 p-3">
|
||||
{featureItem.icon}
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h4 className="text-sm font-medium leading-5">{featureItem.title}</h4>
|
||||
{featureItem.isPro && (
|
||||
<Tooltip tooltipContent="Pro feature" position="top">
|
||||
<UpgradeBadge />
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm leading-5 tracking-tight text-custom-text-300">
|
||||
{featureItem.description}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-sm leading-5 tracking-tight text-custom-text-300">{featureItem.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ToggleSwitch
|
||||
value={Boolean(currentProjectDetails?.[featureItem.property as keyof IProject])}
|
||||
onChange={() => handleSubmit(featureItemKey, featureItem.property)}
|
||||
disabled={!featureItem.isEnabled || !isAdmin}
|
||||
size="sm"
|
||||
/>
|
||||
<ToggleSwitch
|
||||
value={Boolean(currentProjectDetails?.[featureItem.property as keyof IProject])}
|
||||
onChange={() => handleSubmit(featureItemKey, featureItem.property)}
|
||||
disabled={!featureItem.isEnabled || !isAdmin}
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="pl-14">
|
||||
{currentProjectDetails?.[featureItem.property as keyof IProject] &&
|
||||
featureItem.renderChildren &&
|
||||
featureItem.renderChildren(currentProjectDetails, isAdmin, handleSubmit)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -16,6 +16,7 @@ export class FileUploadService extends APIService {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
cancelToken: this.cancelSource.token,
|
||||
withCredentials: false,
|
||||
})
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// types
|
||||
import type { TInboxIssue, TIssue, TInboxIssueWithPagination } from "@plane/types";
|
||||
import type { TInboxIssue, TIssue, TInboxIssueWithPagination, TInboxForm } from "@plane/types";
|
||||
import { API_BASE_URL } from "@/helpers/common.helper";
|
||||
import { APIService } from "@/services/api.service";
|
||||
// helpers
|
||||
@@ -75,4 +75,30 @@ export class InboxIssueService extends APIService {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async retrievePublishForm(workspaceSlug: string, projectId: string): Promise<TInboxForm> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/publish-intake/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async updatePublishForm(workspaceSlug: string, projectId: string, is_disabled: boolean): Promise<TInboxIssue> {
|
||||
return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/publish-intake/`, {
|
||||
is_disabled,
|
||||
})
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async regeneratePublishForm(workspaceSlug: string, projectId: string): Promise<TInboxIssue> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/publish-intake-regenerate/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
TInboxIssueSorting,
|
||||
TInboxIssuePaginationInfo,
|
||||
TInboxIssueSortingOrderByQueryParam,
|
||||
TInboxForm,
|
||||
} from "@plane/types";
|
||||
// helpers
|
||||
import { EInboxIssueCurrentTab, EInboxIssueStatus, EPastDurationFilters, getCustomDates } from "@/helpers/inbox.helper";
|
||||
@@ -39,6 +40,7 @@ export interface IProjectInboxStore {
|
||||
inboxIssuePaginationInfo: TInboxIssuePaginationInfo | undefined;
|
||||
inboxIssues: Record<string, IInboxIssueStore>; // issue_id -> IInboxIssueStore
|
||||
inboxIssueIds: string[];
|
||||
intakeForms: Record<string, TInboxForm>;
|
||||
// computed
|
||||
inboxFilters: Partial<TInboxIssueFilter>; // computed project inbox filters
|
||||
inboxSorting: Partial<TInboxIssueSorting>; // computed project inbox sorting
|
||||
@@ -68,6 +70,9 @@ export interface IProjectInboxStore {
|
||||
) => Promise<void>;
|
||||
fetchInboxPaginationIssues: (workspaceSlug: string, projectId: string) => Promise<void>;
|
||||
fetchInboxIssueById: (workspaceSlug: string, projectId: string, inboxIssueId: string) => Promise<TInboxIssue>;
|
||||
fetchIntakeForms: (workspaceSlug: string, projectId: string) => Promise<void>;
|
||||
toggleIntakeForms: (workspaceSlug: string, projectId: string, isDisabled: boolean) => Promise<void>;
|
||||
regenerateIntakeForms: (workspaceSlug: string, projectId: string) => Promise<void>;
|
||||
createInboxIssue: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
@@ -89,6 +94,7 @@ export class ProjectInboxStore implements IProjectInboxStore {
|
||||
inboxIssuePaginationInfo: TInboxIssuePaginationInfo | undefined = undefined;
|
||||
inboxIssues: Record<string, IInboxIssueStore> = {};
|
||||
inboxIssueIds: string[] = [];
|
||||
intakeForms: Record<string, TInboxForm> = {};
|
||||
// services
|
||||
inboxIssueService;
|
||||
|
||||
@@ -103,6 +109,7 @@ export class ProjectInboxStore implements IProjectInboxStore {
|
||||
inboxIssuePaginationInfo: observable,
|
||||
inboxIssues: observable,
|
||||
inboxIssueIds: observable,
|
||||
intakeForms: observable,
|
||||
// computed
|
||||
inboxFilters: computed,
|
||||
inboxSorting: computed,
|
||||
@@ -310,6 +317,45 @@ export class ProjectInboxStore implements IProjectInboxStore {
|
||||
}
|
||||
};
|
||||
|
||||
fetchIntakeForms = async (workspaceSlug: string, projectId: string) => {
|
||||
try {
|
||||
const intakeForms = await this.inboxIssueService.retrievePublishForm(workspaceSlug, projectId);
|
||||
if (intakeForms)
|
||||
runInAction(() => {
|
||||
set(this.intakeForms, projectId, intakeForms);
|
||||
});
|
||||
} catch {
|
||||
console.error("Error fetching the publish forms");
|
||||
}
|
||||
};
|
||||
|
||||
toggleIntakeForms = async (workspaceSlug: string, projectId: string, isDisabled: boolean) => {
|
||||
try {
|
||||
runInAction(() => {
|
||||
set(this.intakeForms, projectId, { ...this.intakeForms[projectId], is_disabled: isDisabled });
|
||||
});
|
||||
await this.inboxIssueService.updatePublishForm(workspaceSlug, projectId, isDisabled);
|
||||
} catch {
|
||||
console.error("Error fetching the publish forms");
|
||||
runInAction(() => {
|
||||
set(this.intakeForms, projectId, { ...this.intakeForms[projectId], is_disabled: !isDisabled });
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
regenerateIntakeForms = async (workspaceSlug: string, projectId: string) => {
|
||||
try {
|
||||
const form = await this.inboxIssueService.regeneratePublishForm(workspaceSlug, projectId);
|
||||
if (form) {
|
||||
runInAction(() => {
|
||||
set(this.intakeForms, projectId, form);
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
console.error("Error fetching the publish forms");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @description fetch intake issues with paginated data
|
||||
* @param workspaceSlug
|
||||
|
||||
1
web/ee/components/projects/settings/intake/index.ts
Normal file
1
web/ee/components/projects/settings/intake/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "ce/components/projects/settings/intake";
|
||||
@@ -2,7 +2,6 @@
|
||||
import { TFileHandler } from "@plane/editor";
|
||||
// helpers
|
||||
import { getBase64Image, getFileURL } from "@/helpers/file.helper";
|
||||
import { checkURLValidity } from "@/helpers/string.helper";
|
||||
// services
|
||||
import { FileService } from "@/services/file.service";
|
||||
const fileService = new FileService();
|
||||
@@ -46,7 +45,7 @@ export const getEditorFileHandlers = (args: TArgs): TFileHandler => {
|
||||
return {
|
||||
getAssetSrc: (path) => {
|
||||
if (!path) return "";
|
||||
if (checkURLValidity(path)) {
|
||||
if (path?.startsWith("http")) {
|
||||
return path;
|
||||
} else {
|
||||
return (
|
||||
@@ -60,7 +59,7 @@ export const getEditorFileHandlers = (args: TArgs): TFileHandler => {
|
||||
},
|
||||
upload: uploadFile,
|
||||
delete: async (src: string) => {
|
||||
if (checkURLValidity(src)) {
|
||||
if (src?.startsWith("http")) {
|
||||
await fileService.deleteOldWorkspaceAsset(workspaceId, src);
|
||||
} else {
|
||||
await fileService.deleteNewAsset(
|
||||
@@ -73,7 +72,7 @@ export const getEditorFileHandlers = (args: TArgs): TFileHandler => {
|
||||
}
|
||||
},
|
||||
restore: async (src: string) => {
|
||||
if (checkURLValidity(src)) {
|
||||
if (src?.startsWith("http")) {
|
||||
await fileService.restoreOldEditorAsset(workspaceId, src);
|
||||
} else {
|
||||
await fileService.restoreNewAsset(workspaceSlug, src);
|
||||
@@ -97,7 +96,7 @@ export const getReadOnlyEditorFileHandlers = (
|
||||
return {
|
||||
getAssetSrc: (path) => {
|
||||
if (!path) return "";
|
||||
if (checkURLValidity(path)) {
|
||||
if (path?.startsWith("http")) {
|
||||
return path;
|
||||
} else {
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user