refactor: move web utils to packages (#7145)
* refactor: move web utils to packages * fix: build and lint errors * chore: update drag handle plugin * chore: update table cell type to fix build errors * fix: build errors * chore: sync few changes * fix: build errors * chore: minor fixes related to duplicate assets imports * fix: build errors * chore: minor changes
This commit is contained in:
parent
dffcc6dc10
commit
2014400bed
614 changed files with 1999 additions and 3030 deletions
|
|
@ -1,11 +1,11 @@
|
|||
import { FC } from "react";
|
||||
import { Info, X } from "lucide-react";
|
||||
// plane constants
|
||||
import { TAuthErrorInfo } from "@plane/constants";
|
||||
import { TAdminAuthErrorInfo } from "@plane/constants";
|
||||
|
||||
type TAuthBanner = {
|
||||
bannerData: TAuthErrorInfo | undefined;
|
||||
handleBannerData?: (bannerData: TAuthErrorInfo | undefined) => void;
|
||||
bannerData: TAdminAuthErrorInfo | undefined;
|
||||
handleBannerData?: (bannerData: TAdminAuthErrorInfo | undefined) => void;
|
||||
};
|
||||
|
||||
export const AuthBanner: FC<TAuthBanner> = (props) => {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { FC, useEffect, useMemo, useState } from "react";
|
|||
import { useSearchParams } from "next/navigation";
|
||||
import { Eye, EyeOff } from "lucide-react";
|
||||
// plane internal packages
|
||||
import { API_BASE_URL, EAdminAuthErrorCodes, TAuthErrorInfo } from "@plane/constants";
|
||||
import { API_BASE_URL, EAdminAuthErrorCodes, TAdminAuthErrorInfo } from "@plane/constants";
|
||||
import { AuthService } from "@plane/services";
|
||||
import { Button, Input, Spinner } from "@plane/ui";
|
||||
// components
|
||||
|
|
@ -54,7 +54,7 @@ export const InstanceSignInForm: FC = (props) => {
|
|||
const [csrfToken, setCsrfToken] = useState<string | undefined>(undefined);
|
||||
const [formData, setFormData] = useState<TFormData>(defaultFromData);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [errorInfo, setErrorInfo] = useState<TAuthErrorInfo | undefined>(undefined);
|
||||
const [errorInfo, setErrorInfo] = useState<TAdminAuthErrorInfo | undefined>(undefined);
|
||||
|
||||
const handleFormChange = (key: keyof TFormData, value: string | boolean) =>
|
||||
setFormData((prev) => ({ ...prev, [key]: value }));
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import Image from "next/image";
|
|||
import Link from "next/link";
|
||||
import { KeyRound, Mails } from "lucide-react";
|
||||
// plane packages
|
||||
import { SUPPORT_EMAIL, EAdminAuthErrorCodes, TAuthErrorInfo } from "@plane/constants";
|
||||
import { SUPPORT_EMAIL, EAdminAuthErrorCodes, TAdminAuthErrorInfo } from "@plane/constants";
|
||||
import { TGetBaseAuthenticationModeProps, TInstanceAuthenticationModes } from "@plane/types";
|
||||
import { resolveGeneralTheme } from "@plane/utils";
|
||||
// components
|
||||
|
|
@ -89,7 +89,7 @@ const errorCodeMessages: {
|
|||
export const authErrorHandler = (
|
||||
errorCode: EAdminAuthErrorCodes,
|
||||
email?: string | undefined
|
||||
): TAuthErrorInfo | undefined => {
|
||||
): TAdminAuthErrorInfo | undefined => {
|
||||
const bannerAlertErrorCodes = [
|
||||
EAdminAuthErrorCodes.ADMIN_ALREADY_EXIST,
|
||||
EAdminAuthErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD_FIRST_NAME,
|
||||
|
|
|
|||
|
|
@ -69,11 +69,12 @@ export enum EErrorAlertType {
|
|||
|
||||
export type TAuthErrorInfo = {
|
||||
type: EErrorAlertType;
|
||||
code: EAdminAuthErrorCodes;
|
||||
code: EAuthErrorCodes;
|
||||
title: string;
|
||||
message: any;
|
||||
message: React.ReactNode;
|
||||
};
|
||||
|
||||
|
||||
export enum EAdminAuthErrorCodes {
|
||||
// Admin
|
||||
ADMIN_ALREADY_EXIST = "5150",
|
||||
|
|
@ -87,6 +88,13 @@ export enum EAdminAuthErrorCodes {
|
|||
ADMIN_USER_DEACTIVATED = "5190",
|
||||
}
|
||||
|
||||
export type TAdminAuthErrorInfo = {
|
||||
type: EErrorAlertType;
|
||||
code: EAdminAuthErrorCodes;
|
||||
title: string;
|
||||
message: React.ReactNode;
|
||||
};
|
||||
|
||||
export enum EAuthErrorCodes {
|
||||
// Global
|
||||
INSTANCE_NOT_CONFIGURED = "5000",
|
||||
|
|
|
|||
|
|
@ -1,28 +1,26 @@
|
|||
export const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || "";
|
||||
export const API_BASE_PATH = process.env.NEXT_PUBLIC_API_BASE_PATH || "/";
|
||||
export const API_BASE_PATH = process.env.NEXT_PUBLIC_API_BASE_PATH || "";
|
||||
export const API_URL = encodeURI(`${API_BASE_URL}${API_BASE_PATH}`);
|
||||
// God Mode Admin App Base Url
|
||||
export const ADMIN_BASE_URL = process.env.NEXT_PUBLIC_ADMIN_BASE_URL || "";
|
||||
export const ADMIN_BASE_PATH = process.env.NEXT_PUBLIC_ADMIN_BASE_PATH || "/";
|
||||
export const ADMIN_BASE_PATH = process.env.NEXT_PUBLIC_ADMIN_BASE_PATH || "";
|
||||
export const GOD_MODE_URL = encodeURI(`${ADMIN_BASE_URL}${ADMIN_BASE_PATH}`);
|
||||
// Publish App Base Url
|
||||
export const SPACE_BASE_URL = process.env.NEXT_PUBLIC_SPACE_BASE_URL || "";
|
||||
export const SPACE_BASE_PATH = process.env.NEXT_PUBLIC_SPACE_BASE_PATH || "/";
|
||||
export const SPACE_BASE_PATH = process.env.NEXT_PUBLIC_SPACE_BASE_PATH || "";
|
||||
export const SITES_URL = encodeURI(`${SPACE_BASE_URL}${SPACE_BASE_PATH}`);
|
||||
// Live App Base Url
|
||||
export const LIVE_BASE_URL = process.env.NEXT_PUBLIC_LIVE_BASE_URL || "";
|
||||
export const LIVE_BASE_PATH = process.env.NEXT_PUBLIC_LIVE_BASE_PATH || "/";
|
||||
export const LIVE_BASE_PATH = process.env.NEXT_PUBLIC_LIVE_BASE_PATH || "";
|
||||
export const LIVE_URL = encodeURI(`${LIVE_BASE_URL}${LIVE_BASE_PATH}`);
|
||||
// Web App Base Url
|
||||
export const WEB_BASE_URL = process.env.NEXT_PUBLIC_WEB_BASE_URL || "";
|
||||
export const WEB_BASE_PATH = process.env.NEXT_PUBLIC_WEB_BASE_PATH || "/";
|
||||
export const WEB_BASE_PATH = process.env.NEXT_PUBLIC_WEB_BASE_PATH || "";
|
||||
export const WEB_URL = encodeURI(`${WEB_BASE_URL}${WEB_BASE_PATH}`);
|
||||
// plane website url
|
||||
export const WEBSITE_URL =
|
||||
process.env.NEXT_PUBLIC_WEBSITE_URL || "https://plane.so";
|
||||
export const WEBSITE_URL = process.env.NEXT_PUBLIC_WEBSITE_URL || "https://plane.so";
|
||||
// support email
|
||||
export const SUPPORT_EMAIL =
|
||||
process.env.NEXT_PUBLIC_SUPPORT_EMAIL || "support@plane.so";
|
||||
export const SUPPORT_EMAIL = process.env.NEXT_PUBLIC_SUPPORT_EMAIL || "support@plane.so";
|
||||
// marketing links
|
||||
export const MARKETING_PRICING_PAGE_LINK = "https://plane.so/pricing";
|
||||
export const MARKETING_CONTACT_US_PAGE_LINK = "https://plane.so/contact";
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// types
|
||||
// plane imports
|
||||
import { TEstimateSystems } from "@plane/types";
|
||||
|
||||
export const MAX_ESTIMATE_POINT_INPUT_LENGTH = 20;
|
||||
|
|
@ -5,6 +5,7 @@ export * from "./endpoints";
|
|||
export * from "./file";
|
||||
export * from "./filter";
|
||||
export * from "./graph";
|
||||
export * from "./icons";
|
||||
export * from "./instance";
|
||||
export * from "./issue";
|
||||
export * from "./metadata";
|
||||
|
|
@ -21,7 +22,7 @@ export * from "./module";
|
|||
export * from "./project";
|
||||
export * from "./views";
|
||||
export * from "./themes";
|
||||
export * from "./inbox";
|
||||
export * from "./intake";
|
||||
export * from "./profile";
|
||||
export * from "./workspace-drafts";
|
||||
export * from "./label";
|
||||
|
|
@ -33,4 +34,5 @@ export * from "./emoji";
|
|||
export * from "./subscription";
|
||||
export * from "./settings";
|
||||
export * from "./icon";
|
||||
export * from "./estimates";
|
||||
export * from "./analytics";
|
||||
|
|
|
|||
|
|
@ -95,3 +95,32 @@ export const INBOX_ISSUE_SORT_BY_OPTIONS = [
|
|||
i18n_label: "common.sort.desc",
|
||||
},
|
||||
];
|
||||
|
||||
export enum EPastDurationFilters {
|
||||
TODAY = "today",
|
||||
YESTERDAY = "yesterday",
|
||||
LAST_7_DAYS = "last_7_days",
|
||||
LAST_30_DAYS = "last_30_days",
|
||||
}
|
||||
|
||||
export const PAST_DURATION_FILTER_OPTIONS: {
|
||||
name: string;
|
||||
value: string;
|
||||
}[] = [
|
||||
{
|
||||
name: "Today",
|
||||
value: EPastDurationFilters.TODAY,
|
||||
},
|
||||
{
|
||||
name: "Yesterday",
|
||||
value: EPastDurationFilters.YESTERDAY,
|
||||
},
|
||||
{
|
||||
name: "Last 7 days",
|
||||
value: EPastDurationFilters.LAST_7_DAYS,
|
||||
},
|
||||
{
|
||||
name: "Last 30 days",
|
||||
value: EPastDurationFilters.LAST_30_DAYS,
|
||||
},
|
||||
];
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
"use client"
|
||||
|
||||
export type TStateGroups = "backlog" | "unstarted" | "started" | "completed" | "cancelled";
|
||||
|
||||
export type TDraggableData = {
|
||||
|
|
|
|||
89
packages/editor/src/core/helpers/parser.ts
Normal file
89
packages/editor/src/core/helpers/parser.ts
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
// plane imports
|
||||
import { TDocumentPayload, TDuplicateAssetData, TDuplicateAssetResponse } from "@plane/types";
|
||||
import { TEditorAssetType } from "@plane/types/src/enums";
|
||||
// local imports
|
||||
import { convertHTMLDocumentToAllFormats } from "./yjs-utils";
|
||||
|
||||
/**
|
||||
* @description function to extract all image assets from HTML content
|
||||
* @param htmlContent
|
||||
* @returns {string[]} array of image asset sources
|
||||
*/
|
||||
export const extractImageAssetsFromHTMLContent = (htmlContent: string): string[] => {
|
||||
// create a DOM parser
|
||||
const parser = new DOMParser();
|
||||
// parse the HTML string into a DOM document
|
||||
const doc = parser.parseFromString(htmlContent, "text/html");
|
||||
// get all image components
|
||||
const imageComponents = doc.querySelectorAll("image-component");
|
||||
// collect all unique image sources
|
||||
const imageSources = new Set<string>();
|
||||
// extract sources from image components
|
||||
imageComponents.forEach((component) => {
|
||||
const src = component.getAttribute("src");
|
||||
if (src) imageSources.add(src);
|
||||
});
|
||||
return Array.from(imageSources);
|
||||
};
|
||||
|
||||
/**
|
||||
* @description function to replace image assets in HTML content with new IDs
|
||||
* @param props
|
||||
* @returns {string} HTML content with replaced image assets
|
||||
*/
|
||||
export const replaceImageAssetsInHTMLContent = (props: {
|
||||
htmlContent: string;
|
||||
assetMap: Record<string, string>;
|
||||
}): string => {
|
||||
const { htmlContent, assetMap } = props;
|
||||
// create a DOM parser
|
||||
const parser = new DOMParser();
|
||||
// parse the HTML string into a DOM document
|
||||
const doc = parser.parseFromString(htmlContent, "text/html");
|
||||
// replace sources in image components
|
||||
const imageComponents = doc.querySelectorAll("image-component");
|
||||
imageComponents.forEach((component) => {
|
||||
const oldSrc = component.getAttribute("src");
|
||||
if (oldSrc && assetMap[oldSrc]) {
|
||||
component.setAttribute("src", assetMap[oldSrc]);
|
||||
}
|
||||
});
|
||||
// serialize the document back into a string
|
||||
return doc.body.innerHTML;
|
||||
};
|
||||
|
||||
export const getEditorContentWithReplacedImageAssets = async (props: {
|
||||
descriptionHTML: string;
|
||||
entityId: string;
|
||||
entityType: TEditorAssetType;
|
||||
projectId: string | undefined;
|
||||
variant: "rich" | "document";
|
||||
duplicateAssetService: (params: TDuplicateAssetData) => Promise<TDuplicateAssetResponse>;
|
||||
}): Promise<TDocumentPayload> => {
|
||||
const { descriptionHTML, entityId, entityType, projectId, variant, duplicateAssetService } = props;
|
||||
let replacedDescription = descriptionHTML;
|
||||
// step 1: extract image assets from the description
|
||||
const imageAssets = extractImageAssetsFromHTMLContent(descriptionHTML);
|
||||
if (imageAssets.length !== 0) {
|
||||
// step 2: duplicate the image assets
|
||||
const duplicateAssetsResponse = await duplicateAssetService({
|
||||
entity_id: entityId,
|
||||
entity_type: entityType,
|
||||
project_id: projectId,
|
||||
asset_ids: imageAssets,
|
||||
});
|
||||
if (Object.keys(duplicateAssetsResponse ?? {}).length > 0) {
|
||||
// step 3: replace the image assets in the description
|
||||
replacedDescription = replaceImageAssetsInHTMLContent({
|
||||
htmlContent: descriptionHTML,
|
||||
assetMap: duplicateAssetsResponse,
|
||||
});
|
||||
}
|
||||
}
|
||||
// step 4: convert the description to the document payload
|
||||
const documentPayload = convertHTMLDocumentToAllFormats({
|
||||
document_html: replacedDescription,
|
||||
variant,
|
||||
});
|
||||
return documentPayload;
|
||||
};
|
||||
|
|
@ -3,6 +3,7 @@ import { generateHTML, generateJSON } from "@tiptap/html";
|
|||
import { prosemirrorJSONToYDoc, yXmlFragmentToProseMirrorRootNode } from "y-prosemirror";
|
||||
import * as Y from "yjs";
|
||||
// extensions
|
||||
import { TDocumentPayload } from "@plane/types";
|
||||
import {
|
||||
CoreEditorExtensionsWithoutProps,
|
||||
DocumentEditorExtensionsWithoutProps,
|
||||
|
|
@ -140,3 +141,50 @@ export const getAllDocumentFormatsFromDocumentEditorBinaryData = (
|
|||
contentHTML,
|
||||
};
|
||||
};
|
||||
|
||||
type TConvertHTMLDocumentToAllFormatsArgs = {
|
||||
document_html: string;
|
||||
variant: "rich" | "document";
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Converts HTML content to all supported document formats (JSON, HTML, and binary)
|
||||
* @param {TConvertHTMLDocumentToAllFormatsArgs} args - Arguments containing HTML content and variant type
|
||||
* @param {string} args.document_html - The HTML content to convert
|
||||
* @param {"rich" | "document"} args.variant - The type of editor variant to use for conversion
|
||||
* @returns {TDocumentPayload} Object containing the document in all supported formats
|
||||
* @throws {Error} If an invalid variant is provided
|
||||
*/
|
||||
export const convertHTMLDocumentToAllFormats = (args: TConvertHTMLDocumentToAllFormatsArgs): TDocumentPayload => {
|
||||
const { document_html, variant } = args;
|
||||
|
||||
let allFormats: TDocumentPayload;
|
||||
|
||||
if (variant === "rich") {
|
||||
// Convert HTML to binary format for rich text editor
|
||||
const contentBinary = getBinaryDataFromRichTextEditorHTMLString(document_html);
|
||||
// Generate all document formats from the binary data
|
||||
const { contentBinaryEncoded, contentHTML, contentJSON } =
|
||||
getAllDocumentFormatsFromRichTextEditorBinaryData(contentBinary);
|
||||
allFormats = {
|
||||
description: contentJSON,
|
||||
description_html: contentHTML,
|
||||
description_binary: contentBinaryEncoded,
|
||||
};
|
||||
} else if (variant === "document") {
|
||||
// Convert HTML to binary format for document editor
|
||||
const contentBinary = getBinaryDataFromDocumentEditorHTMLString(document_html);
|
||||
// Generate all document formats from the binary data
|
||||
const { contentBinaryEncoded, contentHTML, contentJSON } =
|
||||
getAllDocumentFormatsFromDocumentEditorBinaryData(contentBinary);
|
||||
allFormats = {
|
||||
description: contentJSON,
|
||||
description_html: contentHTML,
|
||||
description_binary: contentBinaryEncoded,
|
||||
};
|
||||
} else {
|
||||
throw new Error(`Invalid variant provided: ${variant}`);
|
||||
}
|
||||
|
||||
return allFormats;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { Fragment, Slice, Node, Schema } from "@tiptap/pm/model";
|
||||
import { NodeSelection } from "@tiptap/pm/state";
|
||||
// @ts-expect-error __serializeForClipboard's is not exported
|
||||
import { __serializeForClipboard, EditorView } from "@tiptap/pm/view";
|
||||
import { EditorView } from "@tiptap/pm/view";
|
||||
// constants
|
||||
import { CORE_EXTENSIONS } from "@/constants/extension";
|
||||
// extensions
|
||||
|
|
@ -417,7 +416,7 @@ const handleNodeSelection = (
|
|||
}
|
||||
|
||||
const slice = view.state.selection.content();
|
||||
const { dom, text } = __serializeForClipboard(view, slice);
|
||||
const { dom, text } = view.serializeForClipboard(slice);
|
||||
|
||||
if (event instanceof DragEvent && event.dataTransfer) {
|
||||
event.dataTransfer.clearData();
|
||||
|
|
|
|||
|
|
@ -1,120 +1,76 @@
|
|||
import * as React from "react"
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@plane/utils"
|
||||
import { cn } from "@plane/utils";
|
||||
|
||||
const Table = React.forwardRef<
|
||||
HTMLTableElement,
|
||||
React.HTMLAttributes<HTMLTableElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
const Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div className="relative w-full overflow-auto">
|
||||
<table
|
||||
ref={ref}
|
||||
className={cn("w-full caption-bottom text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
<table ref={ref} className={cn("w-full caption-bottom text-sm", className)} {...props} />
|
||||
</div>
|
||||
))
|
||||
Table.displayName = "Table"
|
||||
)
|
||||
);
|
||||
Table.displayName = "Table";
|
||||
|
||||
const TableHeader = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<thead ref={ref} className={cn("bg-custom-background-80 py-4 border-y border-custom-border-200", className)} {...props} />
|
||||
))
|
||||
TableHeader.displayName = "TableHeader"
|
||||
|
||||
const TableBody = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tbody
|
||||
ref={ref}
|
||||
className={cn("", className)}
|
||||
{...props}
|
||||
const TableHeader = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<thead
|
||||
ref={ref}
|
||||
className={cn("bg-custom-background-80 py-4 border-y border-custom-border-200", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableBody.displayName = "TableBody"
|
||||
)
|
||||
);
|
||||
TableHeader.displayName = "TableHeader";
|
||||
|
||||
const TableFooter = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tfoot
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"bg-custom-background-300 font-medium",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableFooter.displayName = "TableFooter"
|
||||
const TableBody = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
|
||||
({ className, ...props }, ref) => <tbody ref={ref} className={cn("", className)} {...props} />
|
||||
);
|
||||
TableBody.displayName = "TableBody";
|
||||
|
||||
const TableRow = React.forwardRef<
|
||||
HTMLTableRowElement,
|
||||
React.HTMLAttributes<HTMLTableRowElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
const TableFooter = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<tfoot ref={ref} className={cn("bg-custom-background-300 font-medium", className)} {...props} />
|
||||
)
|
||||
);
|
||||
TableFooter.displayName = "TableFooter";
|
||||
|
||||
const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<tr
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"transition-colors data-[state=selected]:bg-custom-background-100",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
className={cn("transition-colors data-[state=selected]:bg-custom-background-100", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableRow.displayName = "TableRow"
|
||||
)
|
||||
);
|
||||
TableRow.displayName = "TableRow";
|
||||
|
||||
const TableHead = React.forwardRef<
|
||||
HTMLTableHeaderCellElement,
|
||||
React.ThHTMLAttributes<HTMLTableHeaderCellElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<th
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"h-10 px-2 text-left align-middle font-medium text-custom-text-300 [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableHead.displayName = "TableHead"
|
||||
const TableHead = React.forwardRef<HTMLElement, React.ThHTMLAttributes<HTMLElement>>(({ className, ...props }, ref) => (
|
||||
<th
|
||||
ref={ref as any}
|
||||
className={cn(
|
||||
"h-10 px-2 text-left align-middle font-medium text-custom-text-300 [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableHead.displayName = "TableHead";
|
||||
|
||||
const TableCell = React.forwardRef<
|
||||
HTMLTableDataCellElement,
|
||||
React.TdHTMLAttributes<HTMLTableDataCellElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<td
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableCell.displayName = "TableCell"
|
||||
const TableCell = React.forwardRef<HTMLElement, React.TdHTMLAttributes<HTMLElement>>(({ className, ...props }, ref) => (
|
||||
<td
|
||||
ref={ref as any}
|
||||
className={cn("p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableCell.displayName = "TableCell";
|
||||
|
||||
const TableCaption = React.forwardRef<
|
||||
HTMLTableDataCellElement,
|
||||
React.HTMLAttributes<HTMLTableDataCellElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<caption
|
||||
ref={ref}
|
||||
className={cn("mt-4 text-sm text-custom-text-300", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableCaption.displayName = "TableCaption"
|
||||
const TableCaption = React.forwardRef<HTMLElement, React.HTMLAttributes<HTMLElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<caption ref={ref as any} className={cn("mt-4 text-sm text-custom-text-300", className)} {...props} />
|
||||
)
|
||||
);
|
||||
TableCaption.displayName = "TableCaption";
|
||||
|
||||
export {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableCaption,
|
||||
}
|
||||
export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption };
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
{
|
||||
"extends": "@plane/typescript-config/react-library.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist"
|
||||
"jsx": "react",
|
||||
"lib": ["esnext", "dom"]
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// plane imports
|
||||
import { API_BASE_URL } from "@plane/constants";
|
||||
// api service
|
||||
import { TDuplicateAssetData, TDuplicateAssetResponse } from "@plane/types";
|
||||
import { APIService } from "../api.service";
|
||||
// helpers
|
||||
import { getAssetIdFromUrl } from "./helper";
|
||||
|
|
@ -64,4 +65,19 @@ export class FileService extends APIService {
|
|||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Duplicates assets
|
||||
* @param {string} workspaceSlug - The workspace slug
|
||||
* @param {TDuplicateAssetData} data - The data for the duplicate assets
|
||||
* @returns {Promise<TDuplicateAssetResponse>} Promise resolving to a record of asset IDs
|
||||
* @throws {Error} If the request fails
|
||||
*/
|
||||
async duplicateAssets(workspaceSlug: string, data: TDuplicateAssetData): Promise<TDuplicateAssetResponse> {
|
||||
return this.post(`/api/assets/v2/workspaces/${workspaceSlug}/duplicate-assets/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
25
packages/types/src/calendar.d.ts
vendored
25
packages/types/src/calendar.d.ts
vendored
|
|
@ -2,3 +2,28 @@ export interface ICalendarRange {
|
|||
startDate: Date;
|
||||
endDate: Date;
|
||||
}
|
||||
|
||||
export interface ICalendarDate {
|
||||
date: Date;
|
||||
year: number;
|
||||
month: number;
|
||||
day: number;
|
||||
week: number; // week number wrt year, eg- 51, 52
|
||||
is_current_month: boolean;
|
||||
is_current_week: boolean;
|
||||
is_today: boolean;
|
||||
}
|
||||
|
||||
export interface ICalendarWeek {
|
||||
[date: string]: ICalendarDate;
|
||||
}
|
||||
|
||||
export interface ICalendarMonth {
|
||||
[monthIndex: string]: {
|
||||
[weekNumber: string]: ICalendarWeek;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ICalendarPayload {
|
||||
[year: string]: ICalendarMonth;
|
||||
}
|
||||
|
|
|
|||
13
packages/types/src/cycle/cycle.d.ts
vendored
13
packages/types/src/cycle/cycle.d.ts
vendored
|
|
@ -136,3 +136,16 @@ export type TPublicCycle = {
|
|||
name: string;
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type TProgressChartData = {
|
||||
date: string;
|
||||
scope: number;
|
||||
completed: number;
|
||||
backlog: number;
|
||||
started: number;
|
||||
unstarted: number;
|
||||
cancelled: number;
|
||||
pending: number;
|
||||
ideal: number;
|
||||
actual: number;
|
||||
}[];
|
||||
|
|
|
|||
|
|
@ -68,8 +68,18 @@ export enum EFileAssetType {
|
|||
TEAM_SPACE_COMMENT_DESCRIPTION = "TEAM_SPACE_COMMENT_DESCRIPTION",
|
||||
}
|
||||
|
||||
export type TEditorAssetType =
|
||||
| EFileAssetType.COMMENT_DESCRIPTION
|
||||
| EFileAssetType.ISSUE_DESCRIPTION
|
||||
| EFileAssetType.DRAFT_ISSUE_DESCRIPTION
|
||||
| EFileAssetType.PAGE_DESCRIPTION
|
||||
| EFileAssetType.TEAM_SPACE_DESCRIPTION
|
||||
| EFileAssetType.INITIATIVE_DESCRIPTION
|
||||
| EFileAssetType.PROJECT_DESCRIPTION
|
||||
| EFileAssetType.TEAM_SPACE_COMMENT_DESCRIPTION;
|
||||
|
||||
export enum EUpdateStatus {
|
||||
OFF_TRACK = "OFF-TRACK",
|
||||
ON_TRACK = "ON-TRACK",
|
||||
AT_RISK = "AT-RISK",
|
||||
}
|
||||
}
|
||||
|
|
|
|||
17
packages/types/src/file.d.ts
vendored
17
packages/types/src/file.d.ts
vendored
|
|
@ -1,16 +1,16 @@
|
|||
import { EFileAssetType } from "./enums"
|
||||
import { EFileAssetType } from "./enums";
|
||||
|
||||
export type TFileMetaDataLite = {
|
||||
name: string;
|
||||
// file size in bytes
|
||||
size: number;
|
||||
type: string;
|
||||
}
|
||||
};
|
||||
|
||||
export type TFileEntityInfo = {
|
||||
entity_identifier: string;
|
||||
entity_type: EFileAssetType;
|
||||
}
|
||||
};
|
||||
|
||||
export type TFileMetaData = TFileMetaDataLite & TFileEntityInfo;
|
||||
|
||||
|
|
@ -29,4 +29,13 @@ export type TFileSignedURLResponse = {
|
|||
"x-amz-signature": string;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export type TDuplicateAssetData = {
|
||||
entity_id: string;
|
||||
entity_type: EFileAssetType;
|
||||
project_id?: string;
|
||||
asset_ids: string[];
|
||||
};
|
||||
|
||||
export type TDuplicateAssetResponse = Record<string, string>; // asset_id -> new_asset_id
|
||||
|
|
|
|||
1
packages/types/src/index.d.ts
vendored
1
packages/types/src/index.d.ts
vendored
|
|
@ -43,4 +43,5 @@ export * from "./home";
|
|||
export * from "./stickies";
|
||||
export * from "./utils";
|
||||
export * from "./payment";
|
||||
export * from "./layout";
|
||||
export * from "./analytics";
|
||||
|
|
|
|||
1
packages/types/src/layout/index.ts
Normal file
1
packages/types/src/layout/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from "./gantt";
|
||||
4
packages/types/src/project/projects.d.ts
vendored
4
packages/types/src/project/projects.d.ts
vendored
|
|
@ -141,3 +141,7 @@ export interface ISearchIssueResponse {
|
|||
workspace__slug: string;
|
||||
type_id: string;
|
||||
}
|
||||
|
||||
export type TPartialProject = IPartialProject;
|
||||
|
||||
export type TProject = TPartialProject & IProject;
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
import { Search } from "lucide-react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
// icons
|
||||
import { Search } from "lucide-react";
|
||||
import { MATERIAL_ICONS_LIST } from "./icons";
|
||||
import useFontFaceObserver from "use-font-face-observer";
|
||||
import { MATERIAL_ICONS_LIST } from "@plane/constants";
|
||||
import { cn } from "../../helpers";
|
||||
import { Input } from "../form-fields";
|
||||
import { InfoIcon } from "../icons";
|
||||
// components
|
||||
import { Input } from "../form-fields";
|
||||
// hooks
|
||||
import useFontFaceObserver from "use-font-face-observer";
|
||||
// helpers
|
||||
import { cn } from "../../helpers";
|
||||
import { DEFAULT_COLORS, TIconsListProps, adjustColorForContrast } from "./emoji-icon-helper";
|
||||
|
||||
export const IconsList: React.FC<TIconsListProps> = (props) => {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
export * from "./emoji-icon-picker-new";
|
||||
export * from "./emoji-icon-picker";
|
||||
export * from "./emoji-icon-helper";
|
||||
export * from "./icons";
|
||||
export * from "./logo";
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import React, { FC } from "react";
|
||||
import { Emoji } from "emoji-picker-react";
|
||||
import React, { FC } from "react";
|
||||
import useFontFaceObserver from "use-font-face-observer";
|
||||
// icons
|
||||
import { LUCIDE_ICONS_LIST } from "./icons";
|
||||
// helpers
|
||||
// plane imports
|
||||
import { LUCIDE_ICONS_LIST } from "@plane/constants";
|
||||
// local imports
|
||||
import { emojiCodeToUnicode } from "./helpers";
|
||||
|
||||
export type TEmojiLogoProps = {
|
||||
|
|
|
|||
|
|
@ -1,14 +1,12 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
// components
|
||||
import { Input } from "../form-fields";
|
||||
// helpers
|
||||
import { cn } from "../../helpers";
|
||||
import { DEFAULT_COLORS, TIconsListProps, adjustColorForContrast } from "./emoji-icon-helper";
|
||||
// icons
|
||||
import { InfoIcon } from "../icons";
|
||||
// constants
|
||||
import { LUCIDE_ICONS_LIST } from "./icons";
|
||||
import { Search } from "lucide-react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
// plane imports
|
||||
import { LUCIDE_ICONS_LIST } from "@plane/constants";
|
||||
// local imports
|
||||
import { cn } from "../../helpers";
|
||||
import { Input } from "../form-fields";
|
||||
import { InfoIcon } from "../icons";
|
||||
import { DEFAULT_COLORS, TIconsListProps, adjustColorForContrast } from "./emoji-icon-helper";
|
||||
|
||||
export const LucideIconsList: React.FC<TIconsListProps> = (props) => {
|
||||
const { defaultColor, onChange, searchDisabled = false } = props;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,14 @@
|
|||
"use client";
|
||||
|
||||
import { ReactNode } from "react";
|
||||
import zxcvbn from "zxcvbn";
|
||||
// plane imports
|
||||
import {
|
||||
E_PASSWORD_STRENGTH,
|
||||
SPACE_PASSWORD_CRITERIA,
|
||||
PASSWORD_MIN_LENGTH,
|
||||
EErrorAlertType,
|
||||
EAuthErrorCodes,
|
||||
TAuthErrorInfo,
|
||||
} from "@plane/constants";
|
||||
|
||||
/**
|
||||
|
|
@ -30,50 +33,29 @@ export type PasswordCriterion = {
|
|||
/**
|
||||
* @description Password strength criteria
|
||||
*/
|
||||
export const PASSWORD_CRITERIA: PasswordCriterion[] = [
|
||||
{ regex: /[a-z]/, description: "lowercase" },
|
||||
{ regex: /[A-Z]/, description: "uppercase" },
|
||||
{ regex: /[0-9]/, description: "number" },
|
||||
{ regex: /[^a-zA-Z0-9]/, description: "special character" },
|
||||
export const PASSWORD_CRITERIA = [
|
||||
{
|
||||
key: "min_8_char",
|
||||
label: "Min 8 characters",
|
||||
isCriteriaValid: (password: string) => password.length >= PASSWORD_MIN_LENGTH,
|
||||
},
|
||||
// {
|
||||
// key: "min_1_upper_case",
|
||||
// label: "Min 1 upper-case letter",
|
||||
// isCriteriaValid: (password: string) => PASSWORD_NUMBER_REGEX.test(password),
|
||||
// },
|
||||
// {
|
||||
// key: "min_1_number",
|
||||
// label: "Min 1 number",
|
||||
// isCriteriaValid: (password: string) => PASSWORD_CHAR_CAPS_REGEX.test(password),
|
||||
// },
|
||||
// {
|
||||
// key: "min_1_special_char",
|
||||
// label: "Min 1 special character",
|
||||
// isCriteriaValid: (password: string) => PASSWORD_SPECIAL_CHAR_REGEX.test(password),
|
||||
// },
|
||||
];
|
||||
|
||||
/**
|
||||
* @description Checks if password meets all criteria
|
||||
* @param {string} password - Password to check
|
||||
* @returns {boolean} Whether password meets all criteria
|
||||
*/
|
||||
export const checkPasswordCriteria = (password: string): boolean =>
|
||||
PASSWORD_CRITERIA.every((criterion) => criterion.regex.test(password));
|
||||
|
||||
/**
|
||||
* @description Checks password strength against criteria
|
||||
* @param {string} password - Password to check
|
||||
* @returns {PasswordStrength} Password strength level
|
||||
* @example
|
||||
* checkPasswordStrength("abc") // returns PasswordStrength.WEAK
|
||||
* checkPasswordStrength("Abc123!@#") // returns PasswordStrength.STRONG
|
||||
*/
|
||||
export const checkPasswordStrength = (password: string): PasswordStrength => {
|
||||
if (!password || password.length === 0) return PasswordStrength.EMPTY;
|
||||
if (password.length < PASSWORD_MIN_LENGTH) return PasswordStrength.WEAK;
|
||||
|
||||
const criteriaCount = PASSWORD_CRITERIA.filter((criterion) => criterion.regex.test(password)).length;
|
||||
|
||||
const zxcvbnScore = zxcvbn(password).score;
|
||||
|
||||
if (criteriaCount <= 1 || zxcvbnScore <= 1) return PasswordStrength.WEAK;
|
||||
if (criteriaCount === 2 || zxcvbnScore === 2) return PasswordStrength.FAIR;
|
||||
if (criteriaCount === 3 || zxcvbnScore === 3) return PasswordStrength.GOOD;
|
||||
return PasswordStrength.STRONG;
|
||||
};
|
||||
|
||||
export type TAuthErrorInfo = {
|
||||
type: EErrorAlertType;
|
||||
code: EAuthErrorCodes;
|
||||
title: string;
|
||||
message: ReactNode;
|
||||
};
|
||||
|
||||
// Password strength check
|
||||
export const getPasswordStrength = (password: string): E_PASSWORD_STRENGTH => {
|
||||
let passwordStrength: E_PASSWORD_STRENGTH = E_PASSWORD_STRENGTH.EMPTY;
|
||||
|
|
@ -89,9 +71,9 @@ export const getPasswordStrength = (password: string): E_PASSWORD_STRENGTH => {
|
|||
return passwordStrength;
|
||||
}
|
||||
|
||||
const passwordCriteriaValidation = SPACE_PASSWORD_CRITERIA.map((criteria) =>
|
||||
criteria.isCriteriaValid(password)
|
||||
).every((criterion) => criterion);
|
||||
const passwordCriteriaValidation = PASSWORD_CRITERIA.map((criteria) => criteria.isCriteriaValid(password)).every(
|
||||
(criterion) => criterion
|
||||
);
|
||||
const passwordStrengthScore = zxcvbn(password).score;
|
||||
|
||||
if (passwordCriteriaValidation === false || passwordStrengthScore <= 2) {
|
||||
|
|
|
|||
|
|
@ -1,46 +1,8 @@
|
|||
// plane imports
|
||||
import { EStartOfTheWeek } from "@plane/constants";
|
||||
// helpers
|
||||
import { ICalendarDate, ICalendarPayload } from "@/components/issues";
|
||||
import { DAYS_LIST } from "@/constants/calendar";
|
||||
import { getWeekNumberOfDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper";
|
||||
// types
|
||||
|
||||
export const formatDate = (date: Date, format: string): string => {
|
||||
const day = date.getDate();
|
||||
const month = date.getMonth() + 1;
|
||||
const year = date.getFullYear();
|
||||
const hours = date.getHours();
|
||||
const minutes = date.getMinutes();
|
||||
const seconds = date.getSeconds();
|
||||
const daysOfWeek = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
||||
const monthsOfYear = [
|
||||
"January",
|
||||
"February",
|
||||
"March",
|
||||
"April",
|
||||
"May",
|
||||
"June",
|
||||
"July",
|
||||
"August",
|
||||
"September",
|
||||
"October",
|
||||
"November",
|
||||
"December",
|
||||
];
|
||||
|
||||
const formattedDate = format
|
||||
.replace("dd", day.toString().padStart(2, "0"))
|
||||
.replace("d", day.toString())
|
||||
.replace("eee", daysOfWeek[date.getDay()])
|
||||
.replace("Month", monthsOfYear[month - 1])
|
||||
.replace("yyyy", year.toString())
|
||||
.replace("yyy", year.toString().slice(-3))
|
||||
.replace("hh", hours.toString().padStart(2, "0"))
|
||||
.replace("mm", minutes.toString().padStart(2, "0"))
|
||||
.replace("ss", seconds.toString().padStart(2, "0"));
|
||||
|
||||
return formattedDate;
|
||||
};
|
||||
import { ICalendarDate, ICalendarPayload } from "@plane/types";
|
||||
// local imports
|
||||
import { getWeekNumberOfDate, renderFormattedPayloadDate } from "./datetime";
|
||||
|
||||
/**
|
||||
* @returns {ICalendarPayload} calendar payload to render the calendar
|
||||
|
|
@ -101,14 +63,12 @@ export const generateCalendarData = (currentStructure: ICalendarPayload | null,
|
|||
* @param getDayIndex Function to get the day index (0-6) from an item.
|
||||
* @param startOfWeek The day to start the week on.
|
||||
*/
|
||||
export function getOrderedDays<T>(
|
||||
export const getOrderedDays = <T>(
|
||||
items: T[],
|
||||
getDayIndex: (item: T) => number,
|
||||
startOfWeek: EStartOfTheWeek = EStartOfTheWeek.SUNDAY
|
||||
): T[] {
|
||||
return [...items].sort((a, b) => {
|
||||
): T[] => [...items].sort((a, b) => {
|
||||
const dayA = (7 + getDayIndex(a) - startOfWeek) % 7;
|
||||
const dayB = (7 + getDayIndex(b) - startOfWeek) % 7;
|
||||
return dayA - dayB;
|
||||
});
|
||||
}
|
||||
})
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
/**
|
||||
* Represents an RGB color with numeric values for red, green, and blue components
|
||||
* @typedef {Object} RGB
|
||||
* @typedef {Object} TRgb
|
||||
* @property {number} r - Red component (0-255)
|
||||
* @property {number} g - Green component (0-255)
|
||||
* @property {number} b - Blue component (0-255)
|
||||
*/
|
||||
export type RGB = { r: number; g: number; b: number };
|
||||
export type TRgb = { r: number; g: number; b: number };
|
||||
|
||||
export type HSL = { h: number; s: number; l: number };
|
||||
export type THsl = { h: number; s: number; l: number };
|
||||
|
||||
/**
|
||||
* @description Validates and clamps color values to RGB range (0-255)
|
||||
|
|
@ -40,7 +40,7 @@ export const toHex = (value: number) => validateColor(value).toString(16).padSta
|
|||
* hexToRgb("#00ff00") // returns { r: 0, g: 255, b: 0 }
|
||||
* hexToRgb("#0000ff") // returns { r: 0, g: 0, b: 255 }
|
||||
*/
|
||||
export const hexToRgb = (hex: string): RGB => {
|
||||
export const hexToRgb = (hex: string): TRgb => {
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex.trim());
|
||||
return result
|
||||
? {
|
||||
|
|
@ -63,7 +63,7 @@ export const hexToRgb = (hex: string): RGB => {
|
|||
* rgbToHex({ r: 0, g: 255, b: 0 }) // returns "#00ff00"
|
||||
* rgbToHex({ r: 0, g: 0, b: 255 }) // returns "#0000ff"
|
||||
*/
|
||||
export const rgbToHex = ({ r, g, b }: RGB): string => `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
||||
export const rgbToHex = ({ r, g, b }: TRgb): string => `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
||||
|
||||
/**
|
||||
* Converts Hex values to HSL values
|
||||
|
|
@ -74,7 +74,7 @@ export const rgbToHex = ({ r, g, b }: RGB): string => `#${toHex(r)}${toHex(g)}${
|
|||
* hexToHsl("#00ff00") // returns { h: 120, s: 100, l: 50 }
|
||||
* hexToHsl("#0000ff") // returns { h: 240, s: 100, l: 50 }
|
||||
*/
|
||||
export const hexToHsl = (hex: string): HSL => {
|
||||
export const hexToHsl = (hex: string): THsl => {
|
||||
// return default value for invalid hex
|
||||
if (!/^#[0-9A-Fa-f]{6}$/.test(hex)) return { h: 0, s: 0, l: 0 };
|
||||
|
||||
|
|
@ -124,7 +124,7 @@ export const hexToHsl = (hex: string): HSL => {
|
|||
* hslToHex({ h: 120, s: 100, l: 50 }) // returns "#00ff00"
|
||||
* hslToHex({ h: 240, s: 100, l: 50 }) // returns "#0000ff"
|
||||
*/
|
||||
export const hslToHex = ({ h, s, l }: HSL): string => {
|
||||
export const hslToHex = ({ h, s, l }: THsl): string => {
|
||||
if (h < 0 || h > 360) return "#000000";
|
||||
if (s < 0 || s > 100) return "#000000";
|
||||
if (l < 0 || l > 100) return "#000000";
|
||||
|
|
@ -142,3 +142,158 @@ export const hslToHex = ({ h, s, l }: HSL): string => {
|
|||
|
||||
return `#${f(0)}${f(8)}${f(4)}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculate relative luminance of a color according to WCAG
|
||||
* @param {Object} rgb - RGB color object with r, g, b properties
|
||||
* @returns {number} Relative luminance value
|
||||
*/
|
||||
export const getLuminance = ({ r, g, b }: TRgb) => {
|
||||
// Convert RGB to sRGB
|
||||
const sR = r / 255;
|
||||
const sG = g / 255;
|
||||
const sB = b / 255;
|
||||
|
||||
// Convert sRGB to linear RGB with gamma correction
|
||||
const R = sR <= 0.03928 ? sR / 12.92 : Math.pow((sR + 0.055) / 1.055, 2.4);
|
||||
const G = sG <= 0.03928 ? sG / 12.92 : Math.pow((sG + 0.055) / 1.055, 2.4);
|
||||
const B = sB <= 0.03928 ? sB / 12.92 : Math.pow((sB + 0.055) / 1.055, 2.4);
|
||||
|
||||
// Calculate luminance
|
||||
return 0.2126 * R + 0.7152 * G + 0.0722 * B;
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculate contrast ratio between two colors
|
||||
* @param {Object} rgb1 - First RGB color object
|
||||
* @param {Object} rgb2 - Second RGB color object
|
||||
* @returns {number} Contrast ratio between the colors
|
||||
*/
|
||||
export function getContrastRatio(rgb1: { r: number; g: number; b: number }, rgb2: { r: number; g: number; b: number }) {
|
||||
const luminance1 = getLuminance(rgb1);
|
||||
const luminance2 = getLuminance(rgb2);
|
||||
|
||||
const lighter = Math.max(luminance1, luminance2);
|
||||
const darker = Math.min(luminance1, luminance2);
|
||||
|
||||
return (lighter + 0.05) / (darker + 0.05);
|
||||
}
|
||||
|
||||
/**
|
||||
* Lighten a color by a specified amount
|
||||
* @param {Object} rgb - RGB color object
|
||||
* @param {number} amount - Amount to lighten (0-1)
|
||||
* @returns {Object} Lightened RGB color
|
||||
*/
|
||||
export function lightenColor(rgb: { r: number; g: number; b: number }, amount: number) {
|
||||
return {
|
||||
r: rgb.r + (255 - rgb.r) * amount,
|
||||
g: rgb.g + (255 - rgb.g) * amount,
|
||||
b: rgb.b + (255 - rgb.b) * amount,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Darken a color by a specified amount
|
||||
* @param {Object} rgb - RGB color object
|
||||
* @param {number} amount - Amount to darken (0-1)
|
||||
* @returns {Object} Darkened RGB color
|
||||
*/
|
||||
export function darkenColor(rgb: { r: number; g: number; b: number }, amount: number) {
|
||||
return {
|
||||
r: rgb.r * (1 - amount),
|
||||
g: rgb.g * (1 - amount),
|
||||
b: rgb.b * (1 - amount),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate appropriate foreground and background colors based on input color
|
||||
* @param {string} color - Input color in hex format
|
||||
* @returns {Object} Object containing foreground and background colors in hex format
|
||||
*/
|
||||
export function generateIconColors(color: string) {
|
||||
// Parse input color
|
||||
const rgbColor = hexToRgb(color);
|
||||
const luminance = getLuminance(rgbColor);
|
||||
|
||||
// Initialize output colors
|
||||
let foregroundColor = rgbColor;
|
||||
|
||||
// Constants for color adjustment
|
||||
const MIN_CONTRAST_RATIO = 3.0; // Minimum acceptable contrast ratio
|
||||
|
||||
// For light colors, use as foreground and darken for background
|
||||
if (luminance > 0.5) {
|
||||
// Make sure the foreground color is dark enough for visibility
|
||||
let adjustedForeground = foregroundColor;
|
||||
const whiteContrast = getContrastRatio(foregroundColor, { r: 255, g: 255, b: 255 });
|
||||
|
||||
if (whiteContrast < MIN_CONTRAST_RATIO) {
|
||||
// Darken the foreground color until it has enough contrast
|
||||
let darkenAmount = 0.1;
|
||||
while (darkenAmount <= 0.9) {
|
||||
adjustedForeground = darkenColor(foregroundColor, darkenAmount);
|
||||
if (getContrastRatio(adjustedForeground, { r: 255, g: 255, b: 255 }) >= MIN_CONTRAST_RATIO) {
|
||||
break;
|
||||
}
|
||||
darkenAmount += 0.1;
|
||||
}
|
||||
foregroundColor = adjustedForeground;
|
||||
}
|
||||
}
|
||||
// For dark colors, use as foreground and lighten for background
|
||||
else {
|
||||
// Make sure the foreground color is light enough for visibility
|
||||
let adjustedForeground = foregroundColor;
|
||||
const blackContrast = getContrastRatio(foregroundColor, { r: 0, g: 0, b: 0 });
|
||||
|
||||
if (blackContrast < MIN_CONTRAST_RATIO) {
|
||||
// Lighten the foreground color until it has enough contrast
|
||||
let lightenAmount = 0.1;
|
||||
while (lightenAmount <= 0.9) {
|
||||
adjustedForeground = lightenColor(foregroundColor, lightenAmount);
|
||||
if (getContrastRatio(adjustedForeground, { r: 0, g: 0, b: 0 }) >= MIN_CONTRAST_RATIO) {
|
||||
break;
|
||||
}
|
||||
lightenAmount += 0.1;
|
||||
}
|
||||
foregroundColor = adjustedForeground;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
foreground: rgbToHex({ r: foregroundColor.r, g: foregroundColor.g, b: foregroundColor.b }),
|
||||
background: `rgba(${foregroundColor.r}, ${foregroundColor.g}, ${foregroundColor.b}, 0.25)`,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Generates a deterministic HSL color based on input string
|
||||
* @param {string} input - Input string to generate color from
|
||||
* @returns {THsl} An object containing the HSL values
|
||||
* @example
|
||||
* generateRandomColor("hello") // returns consistent HSL color for "hello"
|
||||
* generateRandomColor("") // returns { h: 0, s: 0, l: 0 }
|
||||
*/
|
||||
export const generateRandomColor = (input: string): THsl => {
|
||||
// If input is falsy, generate a random seed string.
|
||||
// The random seed is created by converting a random number to base-36 and taking a substring.
|
||||
const seed = input || Math.random().toString(36).substring(2, 8);
|
||||
|
||||
const uniqueId = seed.length.toString() + seed; // Unique identifier based on string length
|
||||
const combinedString = uniqueId + seed;
|
||||
|
||||
// Create a hash value from the combined string.
|
||||
const hash = Array.from(combinedString).reduce((acc, char) => {
|
||||
const charCode = char.charCodeAt(0);
|
||||
return (acc << 5) - acc + charCode;
|
||||
}, 0);
|
||||
|
||||
// Derive the HSL values from the hash.
|
||||
const hue = Math.abs(hash % 360);
|
||||
const saturation = 70; // Maintains a good amount of color
|
||||
const lightness = 70; // Increased lightness for a pastel look
|
||||
|
||||
return { h: hue, s: saturation, l: lightness };
|
||||
};
|
||||
|
|
|
|||
|
|
@ -58,3 +58,5 @@ export const isComplete = <T>(obj: CompleteOrEmpty<T>): obj is T => {
|
|||
// Check if it has any own properties
|
||||
return Object.keys(obj).length > 0;
|
||||
};
|
||||
|
||||
export const convertRemToPixel = (rem: number): number => rem * 0.9 * 16;
|
||||
|
|
|
|||
|
|
@ -1,28 +1,19 @@
|
|||
import { startOfToday, format } from "date-fns";
|
||||
import { isEmpty, orderBy, uniqBy } from "lodash";
|
||||
import isEmpty from "lodash/isEmpty";
|
||||
import orderBy from "lodash/orderBy";
|
||||
import sortBy from "lodash/sortBy";
|
||||
import uniqBy from "lodash/uniqBy";
|
||||
// plane imports
|
||||
import { ICycle, TCycleFilters } from "@plane/types";
|
||||
// helpers
|
||||
import { findTotalDaysInRange, generateDateArray, getDate } from "@/helpers/date-time.helper";
|
||||
import { satisfiesDateFilter } from "@/helpers/filter.helper";
|
||||
|
||||
export type TProgressChartData = {
|
||||
date: string;
|
||||
scope: number;
|
||||
completed: number;
|
||||
backlog: number;
|
||||
started: number;
|
||||
unstarted: number;
|
||||
cancelled: number;
|
||||
pending: number;
|
||||
ideal: number;
|
||||
actual: number;
|
||||
}[];
|
||||
// local imports
|
||||
import { findTotalDaysInRange, generateDateArray, getDate } from "./datetime";
|
||||
import { satisfiesDateFilter } from "./filter";
|
||||
|
||||
/**
|
||||
* @description orders cycles based on their status
|
||||
* @param {ICycle[]} cycles
|
||||
* @returns {ICycle[]}
|
||||
* Orders cycles based on their status
|
||||
* @param {ICycle[]} cycles - Array of cycles to be ordered
|
||||
* @param {boolean} sortByManual - Whether to sort by manual order
|
||||
* @returns {ICycle[]} Ordered array of cycles
|
||||
*/
|
||||
export const orderCycles = (cycles: ICycle[], sortByManual: boolean): ICycle[] => {
|
||||
if (cycles.length === 0) return [];
|
||||
|
|
@ -48,10 +39,10 @@ export const orderCycles = (cycles: ICycle[], sortByManual: boolean): ICycle[] =
|
|||
};
|
||||
|
||||
/**
|
||||
* @description filters cycles based on the filter
|
||||
* @param {ICycle} cycle
|
||||
* @param {TCycleFilters} filter
|
||||
* @returns {boolean}
|
||||
* Filters cycles based on provided filter criteria
|
||||
* @param {ICycle} cycle - The cycle to be filtered
|
||||
* @param {TCycleFilters} filter - Filter criteria to apply
|
||||
* @returns {boolean} Whether the cycle passes the filter
|
||||
*/
|
||||
export const shouldFilterCycle = (cycle: ICycle, filter: TCycleFilters): boolean => {
|
||||
let fallsInFilters = true;
|
||||
|
|
@ -76,7 +67,21 @@ export const shouldFilterCycle = (cycle: ICycle, filter: TCycleFilters): boolean
|
|||
return fallsInFilters;
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculates the scope based on whether it's an issue or estimate points
|
||||
* @param {any} p - Progress data
|
||||
* @param {boolean} isTypeIssue - Whether the type is an issue
|
||||
* @returns {number} Calculated scope
|
||||
*/
|
||||
const scope = (p: any, isTypeIssue: boolean) => (isTypeIssue ? p.total_issues : p.total_estimate_points);
|
||||
|
||||
/**
|
||||
* Calculates the ideal progress value
|
||||
* @param {string} date - Current date
|
||||
* @param {number} scope - Total scope
|
||||
* @param {ICycle} cycle - Cycle data
|
||||
* @returns {number} Ideal progress value
|
||||
*/
|
||||
const ideal = (date: string, scope: number, cycle: ICycle) =>
|
||||
Math.floor(
|
||||
((findTotalDaysInRange(date, cycle.end_date) || 0) /
|
||||
|
|
@ -84,6 +89,14 @@ const ideal = (date: string, scope: number, cycle: ICycle) =>
|
|||
scope
|
||||
);
|
||||
|
||||
/**
|
||||
* Formats cycle data for version 1
|
||||
* @param {boolean} isTypeIssue - Whether the type is an issue
|
||||
* @param {ICycle} cycle - Cycle data
|
||||
* @param {boolean} isBurnDown - Whether it's a burn down chart
|
||||
* @param {Date|string} endDate - End date
|
||||
* @returns {TProgressChartData} Formatted progress data
|
||||
*/
|
||||
const formatV1Data = (isTypeIssue: boolean, cycle: ICycle, isBurnDown: boolean, endDate: Date | string) => {
|
||||
const today = format(startOfToday(), "yyyy-MM-dd");
|
||||
const data = isTypeIssue ? cycle.distribution : cycle.estimate_distribution;
|
||||
|
|
@ -117,6 +130,14 @@ const formatV1Data = (isTypeIssue: boolean, cycle: ICycle, isBurnDown: boolean,
|
|||
return progress;
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats cycle data for version 2
|
||||
* @param {boolean} isTypeIssue - Whether the type is an issue
|
||||
* @param {ICycle} cycle - Cycle data
|
||||
* @param {boolean} isBurnDown - Whether it's a burn down chart
|
||||
* @param {Date|string} endDate - End date
|
||||
* @returns {TProgressChartData} Formatted progress data
|
||||
*/
|
||||
const formatV2Data = (isTypeIssue: boolean, cycle: ICycle, isBurnDown: boolean, endDate: Date | string) => {
|
||||
if (!cycle.progress) return [];
|
||||
let today: Date | string = startOfToday();
|
||||
|
|
@ -1,45 +1,15 @@
|
|||
import { differenceInDays, format, formatDistanceToNow, isAfter, isEqual, isValid, parseISO } from "date-fns";
|
||||
import isNumber from "lodash/isNumber";
|
||||
|
||||
// Format Date Helpers
|
||||
/**
|
||||
* This method returns a date from string of type yyyy-mm-dd
|
||||
* This method is recommended to use instead of new Date() as this does not introduce any timezone offsets
|
||||
* @param date
|
||||
* @returns date or undefined
|
||||
*/
|
||||
export const getDate = (date: string | Date | undefined | null): Date | undefined => {
|
||||
try {
|
||||
if (!date || date === "") return;
|
||||
|
||||
if (typeof date !== "string" && !(date instanceof String)) return date;
|
||||
|
||||
const [yearString, monthString, dayString] = date.substring(0, 10).split("-");
|
||||
const year = parseInt(yearString);
|
||||
const month = parseInt(monthString);
|
||||
const day = parseInt(dayString);
|
||||
// Using Number.isInteger instead of lodash's isNumber for better specificity and no external dependency
|
||||
if (!Number.isInteger(year) || !Number.isInteger(month) || !Number.isInteger(day)) return;
|
||||
|
||||
return new Date(year, month - 1, day);
|
||||
} catch (e) {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @returns {string | null} formatted date in the format of MMM dd, yyyy
|
||||
* @returns {string | null} formatted date in the desired format or platform default format (MMM dd, yyyy)
|
||||
* @description Returns date in the formatted format
|
||||
* @param {Date | string} date
|
||||
* @param {string} formatToken (optional) // default MMM dd, yyyy
|
||||
* @example renderFormattedDate("2024-01-01", "MM-DD-YYYY") // Jan 01, 2024
|
||||
* @example renderFormattedDate("2024-01-01") // Jan 01, 2024
|
||||
*/
|
||||
/**
|
||||
* @description Returns date in the formatted format
|
||||
* @param {Date | string} date Date to format
|
||||
* @param {string} formatToken Format token (optional, default: MMM dd, yyyy)
|
||||
* @returns {string | undefined} Formatted date in the desired format
|
||||
* @example
|
||||
* renderFormattedDate("2024-01-01") // returns "Jan 01, 2024"
|
||||
* renderFormattedDate("2024-01-01", "MM-DD-YYYY") // returns "01-01-2024"
|
||||
*/
|
||||
export const renderFormattedDate = (
|
||||
date: string | Date | undefined | null,
|
||||
formatToken: string = "MMM dd, yyyy"
|
||||
|
|
@ -49,7 +19,7 @@ export const renderFormattedDate = (
|
|||
// return if undefined
|
||||
if (!parsedDate) return;
|
||||
// Check if the parsed date is valid before formatting
|
||||
if (!isValid(parsedDate)) return; // Return undefined for invalid dates
|
||||
if (!isValid(parsedDate)) return; // Return null for invalid dates
|
||||
let formattedDate;
|
||||
try {
|
||||
// Format the date in the format provided or default format (MMM dd, yyyy)
|
||||
|
|
@ -62,13 +32,75 @@ export const renderFormattedDate = (
|
|||
};
|
||||
|
||||
/**
|
||||
* @returns {string} formatted date in the format of MMM dd
|
||||
* @description Returns date in the formatted format
|
||||
* @param {string | Date} date
|
||||
* @example renderShortDateFormat("2024-01-01") // Jan 01
|
||||
*/
|
||||
export const renderFormattedDateWithoutYear = (date: string | Date): string => {
|
||||
// Parse the date to check if it is valid
|
||||
const parsedDate = getDate(date);
|
||||
// return if undefined
|
||||
if (!parsedDate) return "";
|
||||
// Check if the parsed date is valid before formatting
|
||||
if (!isValid(parsedDate)) return ""; // Return empty string for invalid dates
|
||||
// Format the date in short format (MMM dd)
|
||||
const formattedDate = format(parsedDate, "MMM dd");
|
||||
return formattedDate;
|
||||
};
|
||||
|
||||
/**
|
||||
* @returns {string | null} formatted date in the format of yyyy-mm-dd to be used in payload
|
||||
* @description Returns date in the formatted format to be used in payload
|
||||
* @param {Date | string} date
|
||||
* @example renderFormattedPayloadDate("Jan 01, 20224") // "2024-01-01"
|
||||
*/
|
||||
export const renderFormattedPayloadDate = (date: Date | string | undefined | null): string | undefined => {
|
||||
// Parse the date to check if it is valid
|
||||
const parsedDate = getDate(date);
|
||||
// return if undefined
|
||||
if (!parsedDate) return;
|
||||
// Check if the parsed date is valid before formatting
|
||||
if (!isValid(parsedDate)) return; // Return null for invalid dates
|
||||
// Format the date in payload format (yyyy-mm-dd)
|
||||
const formattedDate = format(parsedDate, "yyyy-MM-dd");
|
||||
return formattedDate;
|
||||
};
|
||||
|
||||
// Format Time Helpers
|
||||
/**
|
||||
* @returns {string} formatted date in the format of hh:mm a or HH:mm
|
||||
* @description Returns date in 12 hour format if in12HourFormat is true else 24 hour format
|
||||
* @param {string | Date} date
|
||||
* @param {boolean} timeFormat (optional) // default 24 hour
|
||||
* @example renderFormattedTime("2024-01-01 13:00:00") // 13:00
|
||||
* @example renderFormattedTime("2024-01-01 13:00:00", "12-hour") // 01:00 PM
|
||||
*/
|
||||
export const renderFormattedTime = (date: string | Date, timeFormat: "12-hour" | "24-hour" = "24-hour"): string => {
|
||||
// Parse the date to check if it is valid
|
||||
const parsedDate = new Date(date);
|
||||
// return if undefined
|
||||
if (!parsedDate) return "";
|
||||
// Check if the parsed date is valid
|
||||
if (!isValid(parsedDate)) return ""; // Return empty string for invalid dates
|
||||
// Format the date in 12 hour format if in12HourFormat is true
|
||||
if (timeFormat === "12-hour") {
|
||||
const formattedTime = format(parsedDate, "hh:mm a");
|
||||
return formattedTime;
|
||||
}
|
||||
// Format the date in 24 hour format
|
||||
const formattedTime = format(parsedDate, "HH:mm");
|
||||
return formattedTime;
|
||||
};
|
||||
|
||||
// Date Difference Helpers
|
||||
/**
|
||||
* @returns {number} total number of days in range
|
||||
* @description Returns total number of days in range
|
||||
* @param {string | Date} startDate - Start date
|
||||
* @param {string | Date} endDate - End date
|
||||
* @param {boolean} inclusive - Include start and end dates (optional, default: true)
|
||||
* @returns {number | undefined} Total number of days
|
||||
* @example
|
||||
* findTotalDaysInRange("2024-01-01", "2024-01-08") // returns 8
|
||||
* @param {string} startDate
|
||||
* @param {string} endDate
|
||||
* @param {boolean} inclusive
|
||||
* @example checkIfStringIsDate("2021-01-01", "2021-01-08") // 8
|
||||
*/
|
||||
export const findTotalDaysInRange = (
|
||||
startDate: Date | string | undefined | null,
|
||||
|
|
@ -89,118 +121,139 @@ export const findTotalDaysInRange = (
|
|||
};
|
||||
|
||||
/**
|
||||
* @description Add number of days to the provided date
|
||||
* @param {string | Date} startDate - Start date
|
||||
* @param {number} numberOfDays - Number of days to add
|
||||
* @returns {Date | undefined} Resulting date
|
||||
* @example
|
||||
* addDaysToDate("2024-01-01", 7) // returns Date(2024-01-08)
|
||||
* Add number of days to the provided date and return a resulting new date
|
||||
* @param startDate
|
||||
* @param numberOfDays
|
||||
* @returns
|
||||
*/
|
||||
export const addDaysToDate = (startDate: Date | string | undefined | null, numberOfDays: number): Date | undefined => {
|
||||
export const addDaysToDate = (startDate: Date | string | undefined | null, numberOfDays: number) => {
|
||||
// Parse the dates to check if they are valid
|
||||
const parsedStartDate = getDate(startDate);
|
||||
|
||||
// return if undefined
|
||||
if (!parsedStartDate) return;
|
||||
|
||||
const newDate = new Date(parsedStartDate);
|
||||
newDate.setDate(newDate.getDate() + numberOfDays);
|
||||
|
||||
return newDate;
|
||||
};
|
||||
|
||||
/**
|
||||
* @returns {number} number of days left from today
|
||||
* @description Returns number of days left from today
|
||||
* @param {string | Date} date - Target date
|
||||
* @param {boolean} inclusive - Include today (optional, default: true)
|
||||
* @returns {number | undefined} Number of days left
|
||||
* @example
|
||||
* findHowManyDaysLeft("2024-01-08") // returns days between today and Jan 8, 2024
|
||||
* @param {string | Date} date
|
||||
* @param {boolean} inclusive (optional) // default true
|
||||
* @example findHowManyDaysLeft("2024-01-01") // 3
|
||||
*/
|
||||
export const findHowManyDaysLeft = (
|
||||
date: Date | string | undefined | null,
|
||||
inclusive: boolean = true
|
||||
): number | undefined => {
|
||||
if (!date) return undefined;
|
||||
// Pass the date to findTotalDaysInRange function to find the total number of days in range from today
|
||||
return findTotalDaysInRange(new Date(), date, inclusive);
|
||||
};
|
||||
|
||||
// Time Difference Helpers
|
||||
/**
|
||||
* @returns {string} formatted date in the form of amount of time passed since the event happened
|
||||
* @description Returns time passed since the event happened
|
||||
* @param {string | number | Date} time - Time to calculate from
|
||||
* @returns {string} Formatted time ago string
|
||||
* @example
|
||||
* calculateTimeAgo("2023-01-01") // returns "1 year ago"
|
||||
* @param {string | Date} time
|
||||
* @example calculateTimeAgo("2023-01-01") // 1 year ago
|
||||
*/
|
||||
export const calculateTimeAgo = (time: string | number | Date | null): string => {
|
||||
if (!time) return "";
|
||||
// Parse the time to check if it is valid
|
||||
const parsedTime = typeof time === "string" || typeof time === "number" ? parseISO(String(time)) : time;
|
||||
if (!parsedTime) return "";
|
||||
// return if undefined
|
||||
if (!parsedTime) return ""; // Return empty string for invalid dates
|
||||
// Format the time in the form of amount of time passed since the event happened
|
||||
const distance = formatDistanceToNow(parsedTime, { addSuffix: true });
|
||||
return distance;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Returns short form of time passed (e.g., 1y, 2mo, 3d)
|
||||
* @param {string | number | Date} date - Date to calculate from
|
||||
* @returns {string} Short form time ago
|
||||
* @example
|
||||
* calculateTimeAgoShort("2023-01-01") // returns "1y"
|
||||
*/
|
||||
export const calculateTimeAgoShort = (date: string | number | Date | null): string => {
|
||||
if (!date) return "";
|
||||
export function calculateTimeAgoShort(date: string | number | Date | null): string {
|
||||
if (!date) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const parsedDate = typeof date === "string" ? parseISO(date) : new Date(date);
|
||||
const now = new Date();
|
||||
const diffInSeconds = (now.getTime() - parsedDate.getTime()) / 1000;
|
||||
|
||||
if (diffInSeconds < 60) return `${Math.floor(diffInSeconds)}s`;
|
||||
if (diffInSeconds < 60) {
|
||||
return `${Math.floor(diffInSeconds)}s`;
|
||||
}
|
||||
|
||||
const diffInMinutes = diffInSeconds / 60;
|
||||
if (diffInMinutes < 60) return `${Math.floor(diffInMinutes)}m`;
|
||||
if (diffInMinutes < 60) {
|
||||
return `${Math.floor(diffInMinutes)}m`;
|
||||
}
|
||||
|
||||
const diffInHours = diffInMinutes / 60;
|
||||
if (diffInHours < 24) return `${Math.floor(diffInHours)}h`;
|
||||
if (diffInHours < 24) {
|
||||
return `${Math.floor(diffInHours)}h`;
|
||||
}
|
||||
|
||||
const diffInDays = diffInHours / 24;
|
||||
if (diffInDays < 30) return `${Math.floor(diffInDays)}d`;
|
||||
if (diffInDays < 30) {
|
||||
return `${Math.floor(diffInDays)}d`;
|
||||
}
|
||||
|
||||
const diffInMonths = diffInDays / 30;
|
||||
if (diffInMonths < 12) return `${Math.floor(diffInMonths)}mo`;
|
||||
if (diffInMonths < 12) {
|
||||
return `${Math.floor(diffInMonths)}mo`;
|
||||
}
|
||||
|
||||
const diffInYears = diffInMonths / 12;
|
||||
return `${Math.floor(diffInYears)}y`;
|
||||
};
|
||||
}
|
||||
|
||||
// Date Validation Helpers
|
||||
/**
|
||||
* @description Checks if a date is greater than today
|
||||
* @param {string} dateStr - Date string to check
|
||||
* @returns {boolean} True if date is greater than today
|
||||
* @example
|
||||
* isDateGreaterThanToday("2024-12-31") // returns true
|
||||
* @returns {string} boolean value depending on whether the date is greater than today
|
||||
* @description Returns boolean value depending on whether the date is greater than today
|
||||
* @param {string} dateStr
|
||||
* @example isDateGreaterThanToday("2024-01-01") // true
|
||||
*/
|
||||
export const isDateGreaterThanToday = (dateStr: string): boolean => {
|
||||
// Return false if dateStr is not present
|
||||
if (!dateStr) return false;
|
||||
// Parse the date to check if it is valid
|
||||
const date = parseISO(dateStr);
|
||||
const today = new Date();
|
||||
if (!isValid(date)) return false;
|
||||
// Check if the parsed date is valid
|
||||
if (!isValid(date)) return false; // Return false for invalid dates
|
||||
// Return true if the date is greater than today
|
||||
return isAfter(date, today);
|
||||
};
|
||||
|
||||
// Week Related Helpers
|
||||
/**
|
||||
* @returns {number} week number of date
|
||||
* @description Returns week number of date
|
||||
* @param {Date} date - Date to get week number from
|
||||
* @returns {number} Week number (1-52)
|
||||
* @example
|
||||
* getWeekNumberOfDate(new Date("2023-09-01")) // returns 35
|
||||
* @param {Date} date
|
||||
* @example getWeekNumber(new Date("2023-09-01")) // 35
|
||||
*/
|
||||
export const getWeekNumberOfDate = (date: Date): number => {
|
||||
const currentDate = date;
|
||||
// Adjust the starting day to Sunday (0) instead of Monday (1)
|
||||
const startDate = new Date(currentDate.getFullYear(), 0, 1);
|
||||
// Calculate the number of days between currentDate and startDate
|
||||
const days = Math.floor((currentDate.getTime() - startDate.getTime()) / (24 * 60 * 60 * 1000));
|
||||
// Adjust the calculation for weekNumber
|
||||
const weekNumber = Math.ceil((days + 1) / 7);
|
||||
return weekNumber;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Checks if two dates are equal
|
||||
* @param {Date | string} date1 - First date
|
||||
* @param {Date | string} date2 - Second date
|
||||
* @returns {boolean} True if dates are equal
|
||||
* @example
|
||||
* checkIfDatesAreEqual("2024-01-01", "2024-01-01") // returns true
|
||||
* @returns {boolean} boolean value depending on whether the dates are equal
|
||||
* @description Returns boolean value depending on whether the dates are equal
|
||||
* @param date1
|
||||
* @param date2
|
||||
* @example checkIfDatesAreEqual("2024-01-01", "2024-01-01") // true
|
||||
* @example checkIfDatesAreEqual("2024-01-01", "2024-01-02") // false
|
||||
*/
|
||||
export const checkIfDatesAreEqual = (
|
||||
date1: Date | string | null | undefined,
|
||||
|
|
@ -208,101 +261,115 @@ export const checkIfDatesAreEqual = (
|
|||
): boolean => {
|
||||
const parsedDate1 = getDate(date1);
|
||||
const parsedDate2 = getDate(date2);
|
||||
// return if undefined
|
||||
if (!parsedDate1 && !parsedDate2) return true;
|
||||
if (!parsedDate1 || !parsedDate2) return false;
|
||||
|
||||
return isEqual(parsedDate1, parsedDate2);
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Checks if a string matches date format YYYY-MM-DD
|
||||
* @param {string} date - Date string to check
|
||||
* @returns {boolean} True if string matches date format
|
||||
* @example
|
||||
* isInDateFormat("2024-01-01") // returns true
|
||||
* This method returns a date from string of type yyyy-mm-dd
|
||||
* This method is recommended to use instead of new Date() as this does not introduce any timezone offsets
|
||||
* @param date
|
||||
* @returns date or undefined
|
||||
*/
|
||||
export const isInDateFormat = (date: string): boolean => {
|
||||
export const getDate = (date: string | Date | undefined | null): Date | undefined => {
|
||||
try {
|
||||
if (!date || date === "") return;
|
||||
|
||||
if (typeof date !== "string" && !(date instanceof String)) return date;
|
||||
|
||||
const [yearString, monthString, dayString] = date.substring(0, 10).split("-");
|
||||
const year = parseInt(yearString);
|
||||
const month = parseInt(monthString);
|
||||
const day = parseInt(dayString);
|
||||
if (!isNumber(year) || !isNumber(month) || !isNumber(day)) return;
|
||||
|
||||
return new Date(year, month - 1, day);
|
||||
} catch (e) {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
export const isInDateFormat = (date: string) => {
|
||||
const datePattern = /^\d{4}-\d{2}-\d{2}$/;
|
||||
return datePattern.test(date);
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Converts date string to ISO format
|
||||
* @param {string} dateString - Date string to convert
|
||||
* @returns {string | undefined} ISO date string
|
||||
* @example
|
||||
* convertToISODateString("2024-01-01") // returns "2024-01-01T00:00:00.000Z"
|
||||
* returns the date string in ISO format regardless of the timezone in input date string
|
||||
* @param dateString
|
||||
* @returns
|
||||
*/
|
||||
export const convertToISODateString = (dateString: string | undefined): string | undefined => {
|
||||
export const convertToISODateString = (dateString: string | undefined) => {
|
||||
if (!dateString) return dateString;
|
||||
|
||||
const date = new Date(dateString);
|
||||
return date.toISOString();
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Converts date string to epoch timestamp
|
||||
* @param {string} dateString - Date string to convert
|
||||
* @returns {number | undefined} Epoch timestamp
|
||||
* @example
|
||||
* convertToEpoch("2024-01-01") // returns 1704067200000
|
||||
* returns the date string in Epoch regardless of the timezone in input date string
|
||||
* @param dateString
|
||||
* @returns
|
||||
*/
|
||||
export const convertToEpoch = (dateString: string | undefined): number | undefined => {
|
||||
if (!dateString) return undefined;
|
||||
export const convertToEpoch = (dateString: string | undefined) => {
|
||||
if (!dateString) return dateString;
|
||||
|
||||
const date = new Date(dateString);
|
||||
return date.getTime();
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Gets current date time in ISO format
|
||||
* @returns {string} Current date time in ISO format
|
||||
* @example
|
||||
* getCurrentDateTimeInISO() // returns "2024-01-01T12:00:00.000Z"
|
||||
* get current Date time in UTC ISO format
|
||||
* @returns
|
||||
*/
|
||||
export const getCurrentDateTimeInISO = (): string => {
|
||||
export const getCurrentDateTimeInISO = () => {
|
||||
const date = new Date();
|
||||
return date.toISOString();
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Converts hours and minutes to total minutes
|
||||
* @param {number} hours - Number of hours
|
||||
* @param {number} minutes - Number of minutes
|
||||
* @returns {number} Total minutes
|
||||
* @example
|
||||
* convertHoursMinutesToMinutes(2, 30) // returns 150
|
||||
* @description converts hours and minutes to minutes
|
||||
* @param { number } hours
|
||||
* @param { number } minutes
|
||||
* @returns { number } minutes
|
||||
* @example convertHoursMinutesToMinutes(2, 30) // Output: 150
|
||||
*/
|
||||
export const convertHoursMinutesToMinutes = (hours: number, minutes: number): number => hours * 60 + minutes;
|
||||
|
||||
/**
|
||||
* @description Converts total minutes to hours and minutes
|
||||
* @param {number} mins - Total minutes
|
||||
* @returns {{ hours: number; minutes: number }} Hours and minutes
|
||||
* @example
|
||||
* convertMinutesToHoursAndMinutes(150) // returns { hours: 2, minutes: 30 }
|
||||
* @description converts minutes to hours and minutes
|
||||
* @param { number } mins
|
||||
* @returns { number, number } hours and minutes
|
||||
* @example convertMinutesToHoursAndMinutes(150) // Output: { hours: 2, minutes: 30 }
|
||||
*/
|
||||
export const convertMinutesToHoursAndMinutes = (mins: number): { hours: number; minutes: number } => {
|
||||
const hours = Math.floor(mins / 60);
|
||||
const minutes = Math.floor(mins % 60);
|
||||
return { hours, minutes };
|
||||
|
||||
return { hours: hours, minutes: minutes };
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Converts minutes to hours and minutes string
|
||||
* @param {number} totalMinutes - Total minutes
|
||||
* @returns {string} Formatted string (e.g., "2h 30m")
|
||||
* @example
|
||||
* convertMinutesToHoursMinutesString(150) // returns "2h 30m"
|
||||
* @description converts minutes to hours and minutes string
|
||||
* @param { number } totalMinutes
|
||||
* @returns { string } 0h 0m
|
||||
* @example convertMinutesToHoursAndMinutes(150) // Output: 2h 10m
|
||||
*/
|
||||
export const convertMinutesToHoursMinutesString = (totalMinutes: number): string => {
|
||||
const { hours, minutes } = convertMinutesToHoursAndMinutes(totalMinutes);
|
||||
|
||||
return `${hours ? `${hours}h ` : ``}${minutes ? `${minutes}m ` : ``}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Calculates read time in seconds from word count
|
||||
* @param {number} wordsCount - Number of words
|
||||
* @returns {number} Read time in seconds
|
||||
* @example
|
||||
* getReadTimeFromWordsCount(400) // returns 120
|
||||
* @description calculates the read time for a document using the words count
|
||||
* @param {number} wordsCount
|
||||
* @returns {number} total number of seconds
|
||||
* @example getReadTimeFromWordsCount(400) // Output: 120
|
||||
* @example getReadTimeFromWordsCount(100) // Output: 30s
|
||||
*/
|
||||
export const getReadTimeFromWordsCount = (wordsCount: number): number => {
|
||||
const wordsPerMinute = 200;
|
||||
|
|
@ -311,29 +378,104 @@ export const getReadTimeFromWordsCount = (wordsCount: number): number => {
|
|||
};
|
||||
|
||||
/**
|
||||
* @description Generates array of dates between start and end dates
|
||||
* @param {string | Date} startDate - Start date
|
||||
* @param {string | Date} endDate - End date
|
||||
* @returns {Array<{ date: string }>} Array of dates
|
||||
* @example
|
||||
* generateDateArray("2024-01-01", "2024-01-03")
|
||||
* // returns [{ date: "2024-01-02" }, { date: "2024-01-03" }]
|
||||
* @description generates an array of dates between the start and end dates
|
||||
* @param startDate
|
||||
* @param endDate
|
||||
* @returns
|
||||
*/
|
||||
export const generateDateArray = (startDate: string | Date, endDate: string | Date): Array<{ date: string }> => {
|
||||
export const generateDateArray = (startDate: string | Date, endDate: string | Date) => {
|
||||
// Convert the start and end dates to Date objects if they aren't already
|
||||
const start = new Date(startDate);
|
||||
// start.setDate(start.getDate() + 1);
|
||||
const end = new Date(endDate);
|
||||
end.setDate(end.getDate() + 1);
|
||||
end.setDate(end.getDate() + 2);
|
||||
|
||||
// Create an empty array to store the dates
|
||||
const dateArray = [];
|
||||
|
||||
// Use a while loop to generate dates between the range
|
||||
while (start <= end) {
|
||||
start.setDate(start.getDate() + 1);
|
||||
// Push the current date (converted to ISO string for consistency)
|
||||
dateArray.push({
|
||||
date: new Date(start).toISOString().split("T")[0],
|
||||
});
|
||||
// Increment the date by 1 day (86400000 milliseconds)
|
||||
start.setDate(start.getDate() + 1);
|
||||
}
|
||||
|
||||
return dateArray;
|
||||
};
|
||||
|
||||
/**
|
||||
* Processes relative date strings like "1_weeks", "2_months" etc and returns a Date
|
||||
* @param value The relative date string (e.g., "1_weeks", "2_months")
|
||||
* @returns Date object representing the calculated date
|
||||
*/
|
||||
export const processRelativeDate = (value: string): Date => {
|
||||
const [amountStr, unit] = value.split("_");
|
||||
const amount = parseInt(amountStr, 10);
|
||||
if (isNaN(amount)) {
|
||||
throw new Error(`Invalid relative amount: ${amountStr}`);
|
||||
}
|
||||
const date = new Date();
|
||||
|
||||
switch (unit) {
|
||||
case "days":
|
||||
date.setDate(date.getDate() + amount);
|
||||
break;
|
||||
case "weeks":
|
||||
date.setDate(date.getDate() + amount * 7);
|
||||
break;
|
||||
case "months":
|
||||
date.setMonth(date.getMonth() + amount);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported time unit: ${unit}`);
|
||||
}
|
||||
|
||||
return date;
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses a date filter string and returns the comparison type and date
|
||||
* @param filterValue The date filter string (e.g., "1_weeks;after;fromnow" or "2024-12-01;after")
|
||||
* @returns Object containing the comparison type and target date
|
||||
*/
|
||||
export const parseDateFilter = (filterValue: string): { type: "after" | "before"; date: Date } => {
|
||||
const parts = filterValue.split(";");
|
||||
const dateStr = parts[0];
|
||||
const type = parts[1] as "after" | "before";
|
||||
|
||||
let date: Date;
|
||||
if (dateStr.includes("_")) {
|
||||
// Handle relative dates (e.g., "1_weeks;after;fromnow")
|
||||
date = processRelativeDate(dateStr);
|
||||
} else {
|
||||
// Handle absolute dates (e.g., "2024-12-01;after")
|
||||
date = new Date(dateStr);
|
||||
}
|
||||
|
||||
return { type, date };
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if a date meets the filter criteria
|
||||
* @param dateToCheck The date to check
|
||||
* @param filterDate The filter date to compare against
|
||||
* @param type The type of comparison ('after' or 'before')
|
||||
* @returns boolean indicating if the date meets the criteria
|
||||
*/
|
||||
export const checkDateCriteria = (dateToCheck: Date | null, filterDate: Date, type: "after" | "before"): boolean => {
|
||||
if (!dateToCheck) return false;
|
||||
|
||||
const checkDate = new Date(dateToCheck);
|
||||
const normalizedCheck = new Date(checkDate.setHours(0, 0, 0, 0));
|
||||
const normalizedFilter = new Date(filterDate.getTime());
|
||||
normalizedFilter.setHours(0, 0, 0, 0);
|
||||
|
||||
return type === "after" ? normalizedCheck >= normalizedFilter : normalizedCheck <= normalizedFilter;
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats merged date range display with smart formatting
|
||||
* - Single date: "Jan 24, 2025"
|
||||
|
|
@ -388,4 +530,4 @@ export const formatDateRange = (
|
|||
}
|
||||
|
||||
return "";
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
"use client";
|
||||
|
||||
import { format } from "date-fns";
|
||||
import get from "lodash/get";
|
||||
import set from "lodash/set";
|
||||
// plane imports
|
||||
import { STATE_GROUPS, COMPLETED_STATE_GROUPS } from "@plane/constants";
|
||||
// types
|
||||
import { ICycle, IEstimatePoint, IModule, IState, TIssue } from "@plane/types";
|
||||
// helper
|
||||
import { getDate } from "./date-time.helper";
|
||||
import { getDate } from "./datetime";
|
||||
|
||||
export type DistributionObjectUpdate = {
|
||||
id: string;
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
// helpers
|
||||
import { getFileURL } from "@/helpers/file.helper";
|
||||
// local imports
|
||||
import { getFileURL } from "./file";
|
||||
|
||||
type TEditorSrcArgs = {
|
||||
assetId: string;
|
||||
|
|
@ -1,3 +1,8 @@
|
|||
"use client";
|
||||
|
||||
// plane imports
|
||||
import { LUCIDE_ICONS_LIST, RANDOM_EMOJI_CODES } from "@plane/constants";
|
||||
|
||||
/**
|
||||
* Converts a hyphen-separated hexadecimal emoji code to its decimal representation
|
||||
* @param {string} emojiUnified - The unified emoji code in hexadecimal format (e.g., "1f600" or "1f1e6-1f1e8")
|
||||
|
|
@ -41,24 +46,46 @@ export const emojiCodeToUnicode = (emoji: string): string => {
|
|||
|
||||
/**
|
||||
* Groups reactions by a specified key
|
||||
* @param {T[]} reactions - Array of reaction objects
|
||||
* @param {any[]} reactions - Array of reaction objects
|
||||
* @param {string} key - Key to group reactions by
|
||||
* @returns {Object} Object with reactions grouped by the specified key
|
||||
* @example
|
||||
* const reactions = [{ reaction: "👍", id: 1 }, { reaction: "👍", id: 2 }, { reaction: "❤️", id: 3 }];
|
||||
* groupReactions(reactions, "reaction") // returns { "👍": [{ reaction: "👍", id: 1 }, { reaction: "👍", id: 2 }], "❤️": [{ reaction: "❤️", id: 3 }] }
|
||||
*/
|
||||
export const groupReactions = <T extends { reaction: string }>(reactions: T[], key: string): { [key: string]: T[] } => {
|
||||
export const groupReactions: (reactions: any[], key: string) => { [key: string]: any[] } = (
|
||||
reactions: any,
|
||||
key: string
|
||||
) => {
|
||||
if (!Array.isArray(reactions)) {
|
||||
console.error("Expected an array of reactions, but got:", reactions);
|
||||
return {};
|
||||
}
|
||||
|
||||
const groupedReactions = reactions.reduce(
|
||||
(acc: { [key: string]: T[] }, reaction: T) => {
|
||||
if (!acc[reaction[key as keyof T] as string]) {
|
||||
acc[reaction[key as keyof T] as string] = [];
|
||||
(acc: any, reaction: any) => {
|
||||
if (!reaction || typeof reaction !== "object" || !Object.prototype.hasOwnProperty.call(reaction, key)) {
|
||||
console.warn("Skipping undefined reaction or missing key:", reaction);
|
||||
return acc; // Skip undefined reactions or those without the specified key
|
||||
}
|
||||
acc[reaction[key as keyof T] as string].push(reaction);
|
||||
|
||||
if (!acc[reaction[key]]) {
|
||||
acc[reaction[key]] = [];
|
||||
}
|
||||
acc[reaction[key]].push(reaction);
|
||||
return acc;
|
||||
},
|
||||
{} as { [key: string]: T[] }
|
||||
{} as { [key: string]: any[] }
|
||||
);
|
||||
|
||||
return groupedReactions;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a random emoji code from the RANDOM_EMOJI_CODES array
|
||||
* @returns {string} A random emoji code
|
||||
*/
|
||||
export const getRandomEmoji = (): string => RANDOM_EMOJI_CODES[Math.floor(Math.random() * RANDOM_EMOJI_CODES.length)];
|
||||
|
||||
/**
|
||||
* Returns a random icon name from the LUCIDE_ICONS_LIST array
|
||||
*/
|
||||
export const getRandomIconName = (): string =>
|
||||
LUCIDE_ICONS_LIST[Math.floor(Math.random() * LUCIDE_ICONS_LIST.length)].name;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// plane web constants
|
||||
import { EEstimateSystem } from "@/plane-web/constants/estimates";
|
||||
import { EEstimateSystem } from "@plane/constants";
|
||||
|
||||
export const isEstimatePointValuesRepeated = (
|
||||
estimatePoints: string[],
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
// plane imports
|
||||
import { API_BASE_URL } from "@plane/constants";
|
||||
import { TFileMetaDataLite, TFileSignedURLResponse } from "@plane/types";
|
||||
|
||||
|
|
@ -47,3 +48,66 @@ export const getAssetIdFromUrl = (src: string): string => {
|
|||
const assetUrl = sourcePaths[sourcePaths.length - 1];
|
||||
return assetUrl;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description encode image via URL to base64
|
||||
* @param {string} url
|
||||
* @returns
|
||||
*/
|
||||
export const getBase64Image = async (url: string): Promise<string> => {
|
||||
if (!url || typeof url !== "string") {
|
||||
throw new Error("Invalid URL provided");
|
||||
}
|
||||
|
||||
// Try to create a URL object to validate the URL
|
||||
try {
|
||||
new URL(url);
|
||||
} catch {
|
||||
throw new Error("Invalid URL format");
|
||||
}
|
||||
|
||||
const response = await fetch(url);
|
||||
// check if the response is OK
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch image: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onloadend = () => {
|
||||
if (reader.result) {
|
||||
resolve(reader.result as string);
|
||||
} else {
|
||||
reject(new Error("Failed to convert image to base64."));
|
||||
}
|
||||
};
|
||||
|
||||
reader.onerror = () => {
|
||||
reject(new Error("Failed to read the image file."));
|
||||
};
|
||||
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @description downloads a CSV file
|
||||
* @param {Array<Array<string>> | { [key: string]: string }} data - The data to be exported to CSV
|
||||
* @param {string} name - The name of the file to be downloaded
|
||||
*/
|
||||
export const csvDownload = (data: Array<Array<string>> | { [key: string]: string }, name: string) => {
|
||||
const rows = Array.isArray(data) ? [...data] : [Object.keys(data), Object.values(data)];
|
||||
|
||||
const csvContent = "data:text/csv;charset=utf-8," + rows.map((e) => e.join(",")).join("\n");
|
||||
const encodedUri = encodeURI(csvContent);
|
||||
|
||||
const link = document.createElement("a");
|
||||
link.href = encodedUri;
|
||||
link.download = `${name}.csv`;
|
||||
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { differenceInCalendarDays } from "date-fns/differenceInCalendarDays";
|
||||
// helpers
|
||||
// plane imports
|
||||
import { IIssueFilters } from "@plane/types";
|
||||
import { getDate } from "./date-time.helper";
|
||||
// import { IIssueFilterOptions } from "@plane/types";
|
||||
// local imports
|
||||
import { getDate } from "./datetime";
|
||||
|
||||
/**
|
||||
* @description calculates the total number of filters applied
|
||||
|
|
@ -21,6 +21,7 @@ export const calculateTotalFilters = <T>(filters: T): number =>
|
|||
})
|
||||
.reduce((curr, prev) => curr + prev, 0)
|
||||
: 0;
|
||||
|
||||
/**
|
||||
* @description checks if the date satisfies the filter
|
||||
* @param {Date} date
|
||||
|
|
@ -1,17 +1,30 @@
|
|||
export * from "./array";
|
||||
export * from "./attachment";
|
||||
export * from "./auth";
|
||||
export * from "./calendar";
|
||||
export * from "./color";
|
||||
export * from "./common";
|
||||
export * from "./cycle";
|
||||
export * from "./datetime";
|
||||
export * from "./distribution-update";
|
||||
export * from "./editor";
|
||||
export * from "./emoji";
|
||||
export * from "./estimates";
|
||||
export * from "./file";
|
||||
export * from "./filter";
|
||||
export * from "./get-icon-for-link";
|
||||
export * from "./issue";
|
||||
export * from "./intake";
|
||||
export * from "./math";
|
||||
export * from "./module";
|
||||
export * from "./notification";
|
||||
export * from "./page";
|
||||
export * from "./permission";
|
||||
export * from "./state";
|
||||
export * from "./project";
|
||||
export * from "./project-views";
|
||||
export * from "./router";
|
||||
export * from "./string";
|
||||
export * from "./subscription";
|
||||
export * from "./tab-indices";
|
||||
export * from "./theme";
|
||||
export * from "./work-item";
|
||||
export * from "./workspace";
|
||||
export * from "./workspace";
|
||||
|
|
|
|||
|
|
@ -1,25 +1,8 @@
|
|||
import { subDays } from "date-fns";
|
||||
import { renderFormattedPayloadDate } from "./date-time.helper";
|
||||
|
||||
export enum EInboxIssueCurrentTab {
|
||||
OPEN = "open",
|
||||
CLOSED = "closed",
|
||||
}
|
||||
|
||||
export enum EInboxIssueStatus {
|
||||
PENDING = -2,
|
||||
DECLINED = -1,
|
||||
SNOOZED = 0,
|
||||
ACCEPTED = 1,
|
||||
DUPLICATE = 2,
|
||||
}
|
||||
|
||||
export enum EPastDurationFilters {
|
||||
TODAY = "today",
|
||||
YESTERDAY = "yesterday",
|
||||
LAST_7_DAYS = "last_7_days",
|
||||
LAST_30_DAYS = "last_30_days",
|
||||
}
|
||||
// plane imports
|
||||
import { EPastDurationFilters } from "@plane/constants";
|
||||
// local imports
|
||||
import { renderFormattedPayloadDate } from "./datetime";
|
||||
|
||||
export const getCustomDates = (duration: EPastDurationFilters): string => {
|
||||
const today = new Date();
|
||||
|
|
@ -49,25 +32,3 @@ export const getCustomDates = (duration: EPastDurationFilters): string => {
|
|||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const PAST_DURATION_FILTER_OPTIONS: {
|
||||
name: string;
|
||||
value: string;
|
||||
}[] = [
|
||||
{
|
||||
name: "Today",
|
||||
value: EPastDurationFilters.TODAY,
|
||||
},
|
||||
{
|
||||
name: "Yesterday",
|
||||
value: EPastDurationFilters.YESTERDAY,
|
||||
},
|
||||
{
|
||||
name: "Last 7 days",
|
||||
value: EPastDurationFilters.LAST_7_DAYS,
|
||||
},
|
||||
{
|
||||
name: "Last 30 days",
|
||||
value: EPastDurationFilters.LAST_30_DAYS,
|
||||
},
|
||||
];
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
import { differenceInCalendarDays } from "date-fns/differenceInCalendarDays";
|
||||
import { ISSUE_PRIORITY_FILTERS, STATE_GROUPS, TIssuePriorities, TIssueFilterPriorityObject } from "@plane/constants";
|
||||
import { TStateGroups } from "@plane/types";
|
||||
import { getDate } from "./datetime";
|
||||
|
||||
export const getIssuePriorityFilters = (priorityKey: TIssuePriorities): TIssueFilterPriorityObject | undefined => {
|
||||
const currentIssuePriority: TIssueFilterPriorityObject | undefined =
|
||||
ISSUE_PRIORITY_FILTERS && ISSUE_PRIORITY_FILTERS.length > 0
|
||||
? ISSUE_PRIORITY_FILTERS.find((_priority) => _priority.key === priorityKey)
|
||||
: undefined;
|
||||
|
||||
if (currentIssuePriority) return currentIssuePriority;
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description check if the issue due date should be highlighted
|
||||
* @param date
|
||||
* @param stateGroup
|
||||
* @returns boolean
|
||||
*/
|
||||
export const shouldHighlightIssueDueDate = (
|
||||
date: string | Date | null,
|
||||
stateGroup: TStateGroups | undefined
|
||||
): boolean => {
|
||||
if (!date || !stateGroup) return false;
|
||||
// if the issue is completed or cancelled, don't highlight the due date
|
||||
if ([STATE_GROUPS.completed.key, STATE_GROUPS.cancelled.key].includes(stateGroup)) return false;
|
||||
|
||||
const parsedDate = getDate(date);
|
||||
if (!parsedDate) return false;
|
||||
|
||||
const targetDateDistance = differenceInCalendarDays(parsedDate, new Date());
|
||||
|
||||
// if the issue is overdue, highlight the due date
|
||||
return targetDateDistance <= 0;
|
||||
};
|
||||
2
packages/utils/src/math.ts
Normal file
2
packages/utils/src/math.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export const getProgress = (completed: number | undefined, total: number | undefined) =>
|
||||
total && total > 0 ? Math.round(((completed ?? 0) / total) * 100) : 0;
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
import sortBy from "lodash/sortBy";
|
||||
// plane imports
|
||||
import { IModule, TModuleDisplayFilters, TModuleFilters, TModuleOrderByOptions } from "@plane/types";
|
||||
// helpers
|
||||
import { getDate } from "@/helpers/date-time.helper";
|
||||
import { satisfiesDateFilter } from "@/helpers/filter.helper";
|
||||
// local imports
|
||||
import { getDate } from "./datetime";
|
||||
import { satisfiesDateFilter } from "./filter";
|
||||
|
||||
/**
|
||||
* @description orders modules based on their status
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { stripAndTruncateHTML } from "./string.helper";
|
||||
import { stripAndTruncateHTML } from "./string";
|
||||
|
||||
export const sanitizeCommentForNotification = (mentionContent: string | undefined) =>
|
||||
mentionContent
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
import sortBy from "lodash/sortBy";
|
||||
// plane imports
|
||||
import { TPage, TPageFilterProps, TPageFiltersSortBy, TPageFiltersSortKey, TPageNavigationTabs } from "@plane/types";
|
||||
// helpers
|
||||
import { getDate } from "@/helpers/date-time.helper";
|
||||
import { satisfiesDateFilter } from "@/helpers/filter.helper";
|
||||
// local imports
|
||||
import { getDate } from "./datetime";
|
||||
import { satisfiesDateFilter } from "./filter";
|
||||
|
||||
/**
|
||||
* @description filters pages based on the page type
|
||||
|
|
@ -83,3 +84,4 @@ export const getPageName = (name: string | undefined) => {
|
|||
if (!name || name.trim() === "") return "Untitled";
|
||||
return name;
|
||||
};
|
||||
|
||||
1
packages/utils/src/permission/index.ts
Normal file
1
packages/utils/src/permission/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from "./role";
|
||||
|
|
@ -1,4 +1,16 @@
|
|||
import { EUserPermissions, EUserProjectRoles, EUserWorkspaceRoles } from "@plane/constants";
|
||||
// plane imports
|
||||
import { EUserProjectRoles, EUserWorkspaceRoles, EUserPermissions } from "@plane/constants";
|
||||
|
||||
export const getUserRole = (role: EUserPermissions | EUserWorkspaceRoles | EUserProjectRoles) => {
|
||||
switch (role) {
|
||||
case EUserPermissions.GUEST:
|
||||
return "GUEST";
|
||||
case EUserPermissions.MEMBER:
|
||||
return "MEMBER";
|
||||
case EUserPermissions.ADMIN:
|
||||
return "ADMIN";
|
||||
}
|
||||
};
|
||||
|
||||
type TSupportedRole = EUserPermissions | EUserProjectRoles | EUserWorkspaceRoles;
|
||||
|
||||
|
|
@ -1,9 +1,11 @@
|
|||
import isNil from "lodash/isNil";
|
||||
import orderBy from "lodash/orderBy";
|
||||
// plane imports
|
||||
import { SPACE_BASE_PATH, SPACE_BASE_URL } from "@plane/constants";
|
||||
import { IProjectView, TViewFilterProps, TViewFiltersSortBy, TViewFiltersSortKey } from "@plane/types";
|
||||
import { getDate } from "@/helpers/date-time.helper";
|
||||
import { SPACE_BASE_PATH, SPACE_BASE_URL } from "./common.helper";
|
||||
import { satisfiesDateFilter } from "./filter.helper";
|
||||
// local imports
|
||||
import { getDate } from "./datetime";
|
||||
import { satisfiesDateFilter } from "./filter";
|
||||
|
||||
/**
|
||||
* order views base on TViewFiltersSortKey
|
||||
|
|
@ -100,4 +102,4 @@ export const getPublishViewLink = (anchor: string | undefined) => {
|
|||
|
||||
const SPACE_APP_URL = (SPACE_BASE_URL.trim() === "" ? window.location.origin : SPACE_BASE_URL) + SPACE_BASE_PATH;
|
||||
return `${SPACE_APP_URL}/views/${anchor}`;
|
||||
};
|
||||
};
|
||||
|
|
@ -1,11 +1,9 @@
|
|||
import sortBy from "lodash/sortBy";
|
||||
// types
|
||||
import { TProjectDisplayFilters, TProjectFilters, TProjectOrderByOptions } from "@plane/types";
|
||||
// helpers
|
||||
import { getDate } from "@/helpers/date-time.helper";
|
||||
import { satisfiesDateFilter } from "@/helpers/filter.helper";
|
||||
// plane web imports
|
||||
import { TProject } from "@/plane-web/types";
|
||||
// plane imports
|
||||
import { TProject, TProjectDisplayFilters, TProjectFilters, TProjectOrderByOptions } from "@plane/types";
|
||||
// local imports
|
||||
import { getDate } from "./datetime";
|
||||
import { satisfiesDateFilter } from "./filter";
|
||||
|
||||
/**
|
||||
* Updates the sort order of the project.
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
import { STATE_GROUPS } from "@plane/constants";
|
||||
import { IState } from "@plane/types";
|
||||
|
||||
export const sortStates = (states: IState[]) => {
|
||||
if (!states || states.length === 0) return;
|
||||
|
||||
return states.sort((stateA, stateB) => {
|
||||
if (stateA.group === stateB.group) {
|
||||
return stateA.sequence - stateB.sequence;
|
||||
}
|
||||
return Object.keys(STATE_GROUPS).indexOf(stateA.group) - Object.keys(STATE_GROUPS).indexOf(stateB.group);
|
||||
});
|
||||
};
|
||||
|
|
@ -55,22 +55,6 @@ export const createSimilarString = (str: string) => {
|
|||
return shuffled;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Copies text to clipboard
|
||||
* @param {string} text - Text to copy
|
||||
* @returns {Promise<void>} Promise that resolves when copying is complete
|
||||
* @example
|
||||
* await copyTextToClipboard("Hello, World!") // copies "Hello, World!" to clipboard
|
||||
*/
|
||||
export const copyTextToClipboard = async (text: string): Promise<void> => {
|
||||
if (typeof navigator === "undefined") return;
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
} catch (err) {
|
||||
console.error("Failed to copy text: ", err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Copies full URL (origin + path) to clipboard
|
||||
* @param {string} path - URL path to copy
|
||||
|
|
@ -146,39 +130,30 @@ export const objToQueryParams = (obj: any) => {
|
|||
export const capitalizeFirstLetter = (str: string) => str.charAt(0).toUpperCase() + str.slice(1);
|
||||
|
||||
/**
|
||||
* @description: This function will remove all the HTML tags from the string
|
||||
* @description : This function will remove all the HTML tags from the string
|
||||
* @param {string} htmlString
|
||||
* @return {string}
|
||||
* @example :
|
||||
* const html = "<p>Some text</p>";
|
||||
const text = stripHTML(html);
|
||||
console.log(text); // Some text
|
||||
*/
|
||||
export const sanitizeHTML = (htmlString: string) => {
|
||||
const sanitizedText = DOMPurify.sanitize(htmlString, { ALLOWED_TAGS: [] }); // sanitize the string to remove all HTML tags
|
||||
return sanitizedText.trim(); // trim the string to remove leading and trailing whitespaces
|
||||
};
|
||||
|
||||
/**
|
||||
* @description: This function will remove all the HTML tags from the string and truncate the string to the specified length
|
||||
* @param {string} html
|
||||
* @param {number} length
|
||||
* @return {string}
|
||||
* @example:
|
||||
* const html = "<p>Some text</p>";
|
||||
* const text = stripHTML(html);
|
||||
* const text = stripAndTruncateHTML(html);
|
||||
* console.log(text); // Some text
|
||||
*/
|
||||
/**
|
||||
* @description Sanitizes HTML string by removing tags and properly escaping entities
|
||||
* @param {string} htmlString - HTML string to sanitize
|
||||
* @returns {string} Sanitized string with escaped HTML entities
|
||||
* @example
|
||||
* sanitizeHTML("<p>Hello & 'world'</p>") // returns "Hello & 'world'"
|
||||
*/
|
||||
export const sanitizeHTML = (htmlString: string) => {
|
||||
if (!htmlString) return "";
|
||||
|
||||
// First use DOMPurify to remove all HTML tags while preserving text content
|
||||
const sanitizedText = DOMPurify.sanitize(htmlString, {
|
||||
ALLOWED_TAGS: [],
|
||||
ALLOWED_ATTR: [],
|
||||
USE_PROFILES: {
|
||||
html: false,
|
||||
svg: false,
|
||||
svgFilters: false,
|
||||
mathMl: false,
|
||||
},
|
||||
});
|
||||
|
||||
// Additional escaping for quotes and apostrophes
|
||||
return sanitizedText.trim().replace(/'/g, "'").replace(/"/g, """);
|
||||
};
|
||||
export const stripAndTruncateHTML = (html: string, length: number = 55) => truncateText(sanitizeHTML(html), length);
|
||||
|
||||
/**
|
||||
* @returns {boolean} true if email is valid, false otherwise
|
||||
|
|
@ -272,43 +247,74 @@ export const joinWithConjunction = (array: string[], separator: string = ", ", c
|
|||
*/
|
||||
export const ensureUrlHasProtocol = (url: string): string => (url.startsWith("http") ? url : `http://${url}`);
|
||||
|
||||
// Browser-only clipboard functions
|
||||
// let copyTextToClipboard: (text: string) => Promise<void>;
|
||||
/**
|
||||
* @returns {boolean} true if searchQuery is substring of text in the same order, false otherwise
|
||||
* @description Returns true if searchQuery is substring of text in the same order, false otherwise
|
||||
* @param {string} text string to compare from
|
||||
* @param {string} searchQuery
|
||||
* @example substringMatch("hello world", "hlo") => true
|
||||
* @example substringMatch("hello world", "hoe") => false
|
||||
*/
|
||||
export const substringMatch = (text: string, searchQuery: string): boolean => {
|
||||
try {
|
||||
let searchIndex = 0;
|
||||
|
||||
// if (typeof window !== "undefined") {
|
||||
// const fallbackCopyTextToClipboard = (text: string) => {
|
||||
// const textArea = document.createElement("textarea");
|
||||
// textArea.value = text;
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
if (text[i].toLowerCase() === searchQuery[searchIndex]?.toLowerCase()) searchIndex++;
|
||||
|
||||
// // Avoid scrolling to bottom
|
||||
// textArea.style.top = "0";
|
||||
// textArea.style.left = "0";
|
||||
// textArea.style.position = "fixed";
|
||||
// All characters of searchQuery found in order
|
||||
if (searchIndex === searchQuery.length) return true;
|
||||
}
|
||||
|
||||
// document.body.appendChild(textArea);
|
||||
// textArea.focus();
|
||||
// textArea.select();
|
||||
// Not all characters of searchQuery found in order
|
||||
return false;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// try {
|
||||
// // FIXME: Even though we are using this as a fallback, execCommand is deprecated 👎. We should find a better way to do this.
|
||||
// // https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand
|
||||
// document.execCommand("copy");
|
||||
// } catch (err) {}
|
||||
/**
|
||||
* @description Copies text to clipboard
|
||||
* @param {string} text - Text to copy
|
||||
* @returns {Promise<void>} Promise that resolves when copying is complete
|
||||
* @example
|
||||
* await copyTextToClipboard("Hello, World!") // copies "Hello, World!" to clipboard
|
||||
*/
|
||||
const fallbackCopyTextToClipboard = (text: string) => {
|
||||
const textArea = document.createElement("textarea");
|
||||
textArea.value = text;
|
||||
|
||||
// document.body.removeChild(textArea);
|
||||
// };
|
||||
// Avoid scrolling to bottom
|
||||
textArea.style.top = "0";
|
||||
textArea.style.left = "0";
|
||||
textArea.style.position = "fixed";
|
||||
|
||||
// copyTextToClipboard = async (text: string) => {
|
||||
// if (!navigator.clipboard) {
|
||||
// fallbackCopyTextToClipboard(text);
|
||||
// return;
|
||||
// }
|
||||
// await navigator.clipboard.writeText(text);
|
||||
// };
|
||||
// } else {
|
||||
// copyTextToClipboard = async () => {
|
||||
// throw new Error("copyTextToClipboard is only available in browser environments");
|
||||
// };
|
||||
// }
|
||||
document.body.appendChild(textArea);
|
||||
textArea.focus();
|
||||
textArea.select();
|
||||
|
||||
// export { copyTextToClipboard };
|
||||
try {
|
||||
// FIXME: Even though we are using this as a fallback, execCommand is deprecated 👎. We should find a better way to do this.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand
|
||||
document.execCommand("copy");
|
||||
} catch (err) {
|
||||
// catch fallback error
|
||||
}
|
||||
|
||||
document.body.removeChild(textArea);
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Copies text to clipboard
|
||||
* @param {string} text - Text to copy
|
||||
* @returns {Promise<void>} Promise that resolves when copying is complete
|
||||
* @example
|
||||
* await copyTextToClipboard("Hello, World!") // copies "Hello, World!" to clipboard
|
||||
*/
|
||||
export const copyTextToClipboard = async (text: string): Promise<void> => {
|
||||
if (!navigator.clipboard) {
|
||||
fallbackCopyTextToClipboard(text);
|
||||
return;
|
||||
}
|
||||
await navigator.clipboard.writeText(text);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
// plane imports
|
||||
import { ETabIndices, TAB_INDEX_MAP } from "@plane/constants";
|
||||
|
||||
export const getTabIndex = (type?: ETabIndices, isMobile: boolean = false) => {
|
||||
|
|
@ -1,2 +1,124 @@
|
|||
// local imports
|
||||
import { TRgb, hexToRgb } from "./color";
|
||||
|
||||
type TShades = {
|
||||
10: TRgb;
|
||||
20: TRgb;
|
||||
30: TRgb;
|
||||
40: TRgb;
|
||||
50: TRgb;
|
||||
60: TRgb;
|
||||
70: TRgb;
|
||||
80: TRgb;
|
||||
90: TRgb;
|
||||
100: TRgb;
|
||||
200: TRgb;
|
||||
300: TRgb;
|
||||
400: TRgb;
|
||||
500: TRgb;
|
||||
600: TRgb;
|
||||
700: TRgb;
|
||||
800: TRgb;
|
||||
900: TRgb;
|
||||
};
|
||||
|
||||
const calculateShades = (hexValue: string): TShades => {
|
||||
const shades: Partial<TShades> = {};
|
||||
const { r, g, b } = hexToRgb(hexValue);
|
||||
|
||||
const convertHexToSpecificShade = (shade: number): TRgb => {
|
||||
if (shade <= 100) {
|
||||
const decimalValue = (100 - shade) / 100;
|
||||
|
||||
const newR = Math.floor(r + (255 - r) * decimalValue);
|
||||
const newG = Math.floor(g + (255 - g) * decimalValue);
|
||||
const newB = Math.floor(b + (255 - b) * decimalValue);
|
||||
|
||||
return {
|
||||
r: newR,
|
||||
g: newG,
|
||||
b: newB,
|
||||
};
|
||||
} else {
|
||||
const decimalValue = 1 - Math.ceil((shade - 100) / 100) / 10;
|
||||
|
||||
const newR = Math.ceil(r * decimalValue);
|
||||
const newG = Math.ceil(g * decimalValue);
|
||||
const newB = Math.ceil(b * decimalValue);
|
||||
|
||||
return {
|
||||
r: newR,
|
||||
g: newG,
|
||||
b: newB,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
for (let i = 10; i <= 900; i >= 100 ? (i += 100) : (i += 10))
|
||||
shades[i as keyof TShades] = convertHexToSpecificShade(i);
|
||||
|
||||
return shades as TShades;
|
||||
};
|
||||
|
||||
export const applyTheme = (palette: string, isDarkPalette: boolean) => {
|
||||
if (!palette) return;
|
||||
const themeElement = document?.querySelector("html");
|
||||
// palette: [bg, text, primary, sidebarBg, sidebarText]
|
||||
const values: string[] = palette.split(",");
|
||||
values.push(isDarkPalette ? "dark" : "light");
|
||||
|
||||
const bgShades = calculateShades(values[0]);
|
||||
const textShades = calculateShades(values[1]);
|
||||
const primaryShades = calculateShades(values[2]);
|
||||
const sidebarBackgroundShades = calculateShades(values[3]);
|
||||
const sidebarTextShades = calculateShades(values[4]);
|
||||
|
||||
for (let i = 10; i <= 900; i >= 100 ? (i += 100) : (i += 10)) {
|
||||
const shade = i as keyof TShades;
|
||||
|
||||
const bgRgbValues = `${bgShades[shade].r}, ${bgShades[shade].g}, ${bgShades[shade].b}`;
|
||||
const textRgbValues = `${textShades[shade].r}, ${textShades[shade].g}, ${textShades[shade].b}`;
|
||||
const primaryRgbValues = `${primaryShades[shade].r}, ${primaryShades[shade].g}, ${primaryShades[shade].b}`;
|
||||
const sidebarBackgroundRgbValues = `${sidebarBackgroundShades[shade].r}, ${sidebarBackgroundShades[shade].g}, ${sidebarBackgroundShades[shade].b}`;
|
||||
const sidebarTextRgbValues = `${sidebarTextShades[shade].r}, ${sidebarTextShades[shade].g}, ${sidebarTextShades[shade].b}`;
|
||||
|
||||
themeElement?.style.setProperty(`--color-background-${shade}`, bgRgbValues);
|
||||
themeElement?.style.setProperty(`--color-text-${shade}`, textRgbValues);
|
||||
themeElement?.style.setProperty(`--color-primary-${shade}`, primaryRgbValues);
|
||||
themeElement?.style.setProperty(`--color-sidebar-background-${shade}`, sidebarBackgroundRgbValues);
|
||||
themeElement?.style.setProperty(`--color-sidebar-text-${shade}`, sidebarTextRgbValues);
|
||||
|
||||
if (i >= 100 && i <= 400) {
|
||||
const borderShade = i === 100 ? 70 : i === 200 ? 80 : i === 300 ? 90 : 100;
|
||||
|
||||
themeElement?.style.setProperty(
|
||||
`--color-border-${shade}`,
|
||||
`${bgShades[borderShade].r}, ${bgShades[borderShade].g}, ${bgShades[borderShade].b}`
|
||||
);
|
||||
themeElement?.style.setProperty(
|
||||
`--color-sidebar-border-${shade}`,
|
||||
`${sidebarBackgroundShades[borderShade].r}, ${sidebarBackgroundShades[borderShade].g}, ${sidebarBackgroundShades[borderShade].b}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
themeElement?.style.setProperty("--color-scheme", values[5]);
|
||||
};
|
||||
|
||||
export const unsetCustomCssVariables = () => {
|
||||
for (let i = 10; i <= 900; i >= 100 ? (i += 100) : (i += 10)) {
|
||||
const dom = document.querySelector<HTMLElement>("[data-theme='custom']");
|
||||
|
||||
dom?.style.removeProperty(`--color-background-${i}`);
|
||||
dom?.style.removeProperty(`--color-text-${i}`);
|
||||
dom?.style.removeProperty(`--color-border-${i}`);
|
||||
dom?.style.removeProperty(`--color-primary-${i}`);
|
||||
dom?.style.removeProperty(`--color-sidebar-background-${i}`);
|
||||
dom?.style.removeProperty(`--color-sidebar-text-${i}`);
|
||||
dom?.style.removeProperty(`--color-sidebar-border-${i}`);
|
||||
dom?.style.removeProperty("--color-scheme");
|
||||
}
|
||||
};
|
||||
|
||||
export const resolveGeneralTheme = (resolvedTheme: string | undefined) =>
|
||||
resolvedTheme?.includes("light") ? "light" : resolvedTheme?.includes("dark") ? "dark" : "system";
|
||||
|
|
|
|||
|
|
@ -1,12 +1,19 @@
|
|||
import { differenceInCalendarDays } from "date-fns/differenceInCalendarDays";
|
||||
import isEmpty from "lodash/isEmpty";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
// plane constants
|
||||
import { EIssueLayoutTypes, ISSUE_DISPLAY_FILTERS_BY_PAGE, STATE_GROUPS } from "@plane/constants";
|
||||
// types
|
||||
// plane imports
|
||||
import {
|
||||
EIssueLayoutTypes,
|
||||
ISSUE_DISPLAY_FILTERS_BY_PAGE,
|
||||
STATE_GROUPS,
|
||||
TIssuePriorities,
|
||||
ISSUE_PRIORITY_FILTERS,
|
||||
TIssueFilterPriorityObject,
|
||||
} from "@plane/constants";
|
||||
import {
|
||||
IIssueDisplayFilterOptions,
|
||||
IIssueDisplayProperties,
|
||||
IGanttBlock,
|
||||
TGroupedIssues,
|
||||
TIssue,
|
||||
TIssueGroupByOptions,
|
||||
|
|
@ -16,11 +23,10 @@ import {
|
|||
TSubGroupedIssues,
|
||||
TUnGroupedIssues,
|
||||
} from "@plane/types";
|
||||
import { IGanttBlock } from "@/components/gantt-chart";
|
||||
// helpers
|
||||
import { orderArrayBy } from "@/helpers/array.helper";
|
||||
import { getDate } from "@/helpers/date-time.helper";
|
||||
import { isEditorEmpty } from "@/helpers/editor.helper";
|
||||
// local imports
|
||||
import { orderArrayBy } from "../array";
|
||||
import { getDate } from "../datetime";
|
||||
import { isEditorEmpty } from "../editor";
|
||||
|
||||
type THandleIssuesMutation = (
|
||||
formData: Partial<TIssue>,
|
||||
|
|
@ -171,6 +177,7 @@ export const shouldHighlightIssueDueDate = (
|
|||
// if the issue is overdue, highlight the due date
|
||||
return targetDateDistance <= 0;
|
||||
};
|
||||
|
||||
export const getIssueBlocksStructure = (block: TIssue): IGanttBlock => ({
|
||||
data: block,
|
||||
id: block?.id,
|
||||
|
|
@ -333,3 +340,13 @@ export const generateWorkItemLink = ({
|
|||
|
||||
return isArchived ? archiveIssueLink : isEpic ? epicLink : workItemLink;
|
||||
};
|
||||
|
||||
export const getIssuePriorityFilters = (priorityKey: TIssuePriorities): TIssueFilterPriorityObject | undefined => {
|
||||
const currentIssuePriority: TIssueFilterPriorityObject | undefined =
|
||||
ISSUE_PRIORITY_FILTERS && ISSUE_PRIORITY_FILTERS.length > 0
|
||||
? ISSUE_PRIORITY_FILTERS.find((_priority) => _priority.key === priorityKey)
|
||||
: undefined;
|
||||
|
||||
if (currentIssuePriority) return currentIssuePriority;
|
||||
return undefined;
|
||||
};
|
||||
|
|
@ -1 +1,3 @@
|
|||
export * from "./base";
|
||||
export * from "./modal";
|
||||
export * from "./state";
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import set from "lodash/set";
|
||||
// plane imports
|
||||
import { DEFAULT_WORK_ITEM_FORM_VALUES } from "@plane/constants";
|
||||
import { IPartialProject, ISearchIssueResponse, IState, TIssue } from "@plane/types";
|
||||
|
|
@ -31,3 +32,17 @@ export const convertWorkItemDataToSearchResponse = (
|
|||
state__name: state?.name ?? "",
|
||||
workspace__slug: workspaceSlug,
|
||||
});
|
||||
|
||||
|
||||
export function getChangedIssuefields(formData: Partial<TIssue>, dirtyFields: { [key: string]: boolean | undefined }) {
|
||||
const changedFields = {};
|
||||
|
||||
const dirtyFieldKeys = Object.keys(dirtyFields) as (keyof TIssue)[];
|
||||
for (const dirtyField of dirtyFieldKeys) {
|
||||
if (!!dirtyFields[dirtyField]) {
|
||||
set(changedFields, [dirtyField], formData[dirtyField]);
|
||||
}
|
||||
}
|
||||
|
||||
return changedFields as Partial<TIssue>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
// plane imports
|
||||
import { STATE_GROUPS, TDraggableData } from "@plane/constants";
|
||||
// types
|
||||
import { IState, IStateResponse } from "@plane/types";
|
||||
|
||||
export const orderStateGroups = (unorderedStateGroups: IStateResponse | undefined): IStateResponse | undefined => {
|
||||
|
|
@ -9,7 +9,7 @@ import { IEmailCheckData } from "@plane/types";
|
|||
// ui
|
||||
import { Button, Input, Spinner } from "@plane/ui";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { cn } from "@plane/utils";
|
||||
import { checkEmailValidity } from "@/helpers/string.helper";
|
||||
|
||||
type TAuthEmailForm = {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
import { FC, useMemo } from "react";
|
||||
// import { CircleCheck } from "lucide-react";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { cn } from "@plane/utils";
|
||||
import {
|
||||
E_PASSWORD_STRENGTH,
|
||||
// PASSWORD_CRITERIA,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// types
|
||||
import { TLogoProps } from "@plane/types";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { cn } from "@plane/utils";
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { observer } from "mobx-react";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { cn } from "@plane/utils";
|
||||
// hooks
|
||||
import { useMember, useUser } from "@/hooks/store";
|
||||
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@ import React from "react";
|
|||
// plane imports
|
||||
import { EditorRefApi, ILiteTextEditor, LiteTextEditorWithRef, TFileHandler } from "@plane/editor";
|
||||
import { MakeOptional } from "@plane/types";
|
||||
import { cn } from "@plane/utils";
|
||||
// components
|
||||
import { EditorMentionsRoot, IssueCommentToolbar } from "@/components/editor";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { getEditorFileHandlers } from "@/helpers/editor.helper";
|
||||
import { isCommentEmpty } from "@/helpers/string.helper";
|
||||
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@ import React from "react";
|
|||
// plane imports
|
||||
import { EditorReadOnlyRefApi, ILiteTextReadOnlyEditor, LiteTextReadOnlyEditorWithRef } from "@plane/editor";
|
||||
import { MakeOptional } from "@plane/types";
|
||||
import { cn } from "@plane/utils";
|
||||
// components
|
||||
import { EditorMentionsRoot } from "@/components/editor";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { getReadOnlyEditorFileHandlers } from "@/helpers/editor.helper";
|
||||
// store hooks
|
||||
import { useMember } from "@/hooks/store";
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@ import React from "react";
|
|||
// plane imports
|
||||
import { EditorReadOnlyRefApi, IRichTextReadOnlyEditor, RichTextReadOnlyEditorWithRef } from "@plane/editor";
|
||||
import { MakeOptional } from "@plane/types";
|
||||
import { cn } from "@plane/utils";
|
||||
// components
|
||||
import { EditorMentionsRoot } from "@/components/editor";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { getReadOnlyEditorFileHandlers } from "@/helpers/editor.helper";
|
||||
// store hooks
|
||||
import { useMember } from "@/hooks/store";
|
||||
|
|
|
|||
|
|
@ -1,12 +1,10 @@
|
|||
"use client";
|
||||
|
||||
import React, { useEffect, useState, useCallback } from "react";
|
||||
// editor
|
||||
// plane imports
|
||||
import { TOOLBAR_ITEMS, ToolbarMenuItem, EditorRefApi } from "@plane/editor";
|
||||
// ui
|
||||
import { Button, Tooltip } from "@plane/ui";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { cn } from "@plane/utils";
|
||||
|
||||
type Props = {
|
||||
executeCommand: (item: ToolbarMenuItem) => void;
|
||||
|
|
|
|||
|
|
@ -11,8 +11,7 @@ import {
|
|||
TPaginationData,
|
||||
TLoader,
|
||||
} from "@plane/types";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { cn } from "@plane/utils";
|
||||
// hooks
|
||||
import { useIntersectionObserver } from "@/hooks/use-intersection-observer";
|
||||
//
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@
|
|||
import { observer } from "mobx-react";
|
||||
import { CalendarCheck2 } from "lucide-react";
|
||||
import { Tooltip } from "@plane/ui";
|
||||
import { cn } from "@plane/utils";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { renderFormattedDate } from "@/helpers/date-time.helper";
|
||||
import { shouldHighlightIssueDueDate } from "@/helpers/issue.helper";
|
||||
// hooks
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ import { Popover, Transition } from "@headlessui/react";
|
|||
import { API_BASE_URL } from "@plane/constants";
|
||||
import { AuthService } from "@plane/services";
|
||||
import { Avatar, Button } from "@plane/ui";
|
||||
import { getFileURL } from "@plane/utils";
|
||||
// helpers
|
||||
import { getFileURL } from "@/helpers/file.helper";
|
||||
import { queryParamGenerator } from "@/helpers/query-param-generator";
|
||||
// hooks
|
||||
import { useUser } from "@/hooks/store";
|
||||
|
|
|
|||
|
|
@ -6,12 +6,12 @@ import { Menu, Transition } from "@headlessui/react";
|
|||
// plane imports
|
||||
import { EditorRefApi } from "@plane/editor";
|
||||
import { TIssuePublicComment } from "@plane/types";
|
||||
import { getFileURL } from "@plane/utils";
|
||||
// components
|
||||
import { LiteTextEditor, LiteTextReadOnlyEditor } from "@/components/editor";
|
||||
import { CommentReactions } from "@/components/issues/peek-overview";
|
||||
// helpers
|
||||
import { timeAgo } from "@/helpers/date-time.helper";
|
||||
import { getFileURL } from "@/helpers/file.helper";
|
||||
// hooks
|
||||
import { useIssueDetails, usePublish, useUser } from "@/hooks/store";
|
||||
import useIsInIframe from "@/hooks/use-is-in-iframe";
|
||||
|
|
|
|||
|
|
@ -4,10 +4,11 @@ import React from "react";
|
|||
import { observer } from "mobx-react";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import { Tooltip } from "@plane/ui";
|
||||
// plane imports
|
||||
import { cn } from "@plane/utils";
|
||||
// ui
|
||||
import { ReactionSelector } from "@/components/ui";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { groupReactions, renderEmoji } from "@/helpers/emoji.helper";
|
||||
import { queryParamGenerator } from "@/helpers/query-param-generator";
|
||||
// hooks
|
||||
|
|
|
|||
|
|
@ -3,14 +3,13 @@
|
|||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { CalendarCheck2, Signal } from "lucide-react";
|
||||
// plane imports
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// ui
|
||||
import { DoubleCircleIcon, StateGroupIcon, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { getIssuePriorityFilters } from "@plane/utils";
|
||||
import { cn, getIssuePriorityFilters } from "@plane/utils";
|
||||
// components
|
||||
import { Icon } from "@/components/ui";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { renderFormattedDate } from "@/helpers/date-time.helper";
|
||||
import { shouldHighlightIssueDueDate } from "@/helpers/issue.helper";
|
||||
import { copyTextToClipboard, addSpaceIfCamelCase } from "@/helpers/string.helper";
|
||||
|
|
|
|||
|
|
@ -3,9 +3,10 @@
|
|||
import { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
// plane imports
|
||||
import { Tooltip } from "@plane/ui";
|
||||
import { cn } from "@plane/utils";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { queryParamGenerator } from "@/helpers/query-param-generator";
|
||||
// hooks
|
||||
import { useIssueDetails, useUser } from "@/hooks/store";
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
"use client";
|
||||
|
||||
import { ReactNode } from "react";
|
||||
import { useTheme } from "next-themes"
|
||||
// ui
|
||||
import { useTheme } from "next-themes";
|
||||
// plane imports
|
||||
import { Toast } from "@plane/ui";
|
||||
// helpers
|
||||
import { resolveGeneralTheme } from "@/helpers/common.helper";
|
||||
import { resolveGeneralTheme } from "@plane/utils";
|
||||
|
||||
export const ToastProvider = ({ children }: { children: ReactNode }) => {
|
||||
// themes
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
// plane internal
|
||||
// plane imports
|
||||
import { MAX_FILE_SIZE } from "@plane/constants";
|
||||
import { TFileHandler, TReadOnlyFileHandler } from "@plane/editor";
|
||||
import { SitesFileService } from "@plane/services";
|
||||
// helpers
|
||||
import { getFileURL } from "@/helpers/file.helper";
|
||||
import { getFileURL } from "@plane/utils";
|
||||
// services
|
||||
const sitesFileService = new SitesFileService();
|
||||
|
||||
|
|
|
|||
|
|
@ -4,15 +4,15 @@ import { useEffect } from "react";
|
|||
import { observer } from "mobx-react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { BarChart2, PanelRight } from "lucide-react";
|
||||
// plane imports
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// ui
|
||||
import { Breadcrumbs, Header } from "@plane/ui";
|
||||
import { cn } from "@plane/utils";
|
||||
// components
|
||||
import { BreadcrumbLink } from "@/components/common";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { BreadcrumbLink } from "@/components/common/breadcrumb-link";
|
||||
// hooks
|
||||
import { useAppTheme } from "@/hooks/store";
|
||||
|
||||
export const WorkspaceAnalyticsHeader = observer(() => {
|
||||
const { t } = useTranslation();
|
||||
const searchParams = useSearchParams();
|
||||
|
|
|
|||
|
|
@ -8,12 +8,11 @@ import { Plus, Search } from "lucide-react";
|
|||
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { setToast, TOAST_TYPE, Tooltip } from "@plane/ui";
|
||||
import { cn, copyUrlToClipboard } from "@plane/utils";
|
||||
import { cn, copyUrlToClipboard, orderJoinedProjects } from "@plane/utils";
|
||||
// components
|
||||
import { CreateProjectModal } from "@/components/project";
|
||||
import { SidebarProjectsListItem } from "@/components/workspace";
|
||||
// hooks
|
||||
import { orderJoinedProjects } from "@/helpers/project.helper";
|
||||
import { useAppTheme, useProject, useUserPermissions } from "@/hooks/store";
|
||||
import useExtendedSidebarOutsideClickDetector from "@/hooks/use-extended-sidebar-overview-outside-click";
|
||||
import { TProject } from "@/plane-web/types";
|
||||
|
|
|
|||
|
|
@ -10,10 +10,11 @@ import { PROFILE_VIEWER_TAB, PROFILE_ADMINS_TAB, EUserPermissions, EUserPermissi
|
|||
import { useTranslation } from "@plane/i18n";
|
||||
import { IUserProfileProjectSegregation } from "@plane/types";
|
||||
import { Breadcrumbs, Header, CustomMenu, UserActivityIcon } from "@plane/ui";
|
||||
import { BreadcrumbLink } from "@/components/common";
|
||||
import { cn } from "@plane/utils";
|
||||
// components
|
||||
import { BreadcrumbLink } from "@/components/common/breadcrumb-link";
|
||||
import { ProfileIssuesFilter } from "@/components/profile";
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
// hooks
|
||||
import { useAppTheme, useUser, useUserPermissions } from "@/hooks/store";
|
||||
|
||||
type TUserProfileHeader = {
|
||||
|
|
|
|||
|
|
@ -20,10 +20,10 @@ import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOption
|
|||
// ui
|
||||
import { CustomMenu } from "@plane/ui";
|
||||
// components
|
||||
import { isIssueFilterActive } from "@plane/utils";
|
||||
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown, IssueLayoutIcon } from "@/components/issues";
|
||||
|
||||
// helpers
|
||||
import { isIssueFilterActive } from "@/helpers/filter.helper";
|
||||
// hooks
|
||||
import { useIssues, useLabel } from "@/hooks/store";
|
||||
|
||||
|
|
|
|||
|
|
@ -3,13 +3,13 @@
|
|||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
// components
|
||||
import { cn } from "@plane/utils";
|
||||
import { EmptyState } from "@/components/common";
|
||||
import { PageHead } from "@/components/core";
|
||||
import { CycleDetailsSidebar } from "@/components/cycles";
|
||||
import useCyclesDetails from "@/components/cycles/active-cycle/use-cycles-details";
|
||||
import { CycleLayoutRoot } from "@/components/issues/issue-layouts";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
// hooks
|
||||
import { useCycle, useProject } from "@/hooks/store";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
|
|
|
|||
|
|
@ -6,50 +6,45 @@ import Link from "next/link";
|
|||
import { useParams } from "next/navigation";
|
||||
// icons
|
||||
import { PanelRight } from "lucide-react";
|
||||
// plane constants
|
||||
// plane imports
|
||||
import {
|
||||
EIssueLayoutTypes,
|
||||
EIssueFilterType,
|
||||
EIssueLayoutTypes,
|
||||
EIssuesStoreType,
|
||||
ISSUE_DISPLAY_FILTERS_BY_PAGE,
|
||||
EUserPermissions,
|
||||
EUserPermissionsLevel,
|
||||
ISSUE_DISPLAY_FILTERS_BY_PAGE,
|
||||
} from "@plane/constants";
|
||||
// i18n
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// types
|
||||
import {
|
||||
ICustomSearchSelectOption,
|
||||
IIssueDisplayFilterOptions,
|
||||
IIssueDisplayProperties,
|
||||
IIssueFilterOptions,
|
||||
} from "@plane/types";
|
||||
// ui
|
||||
import { Breadcrumbs, Button, ContrastIcon, Tooltip, Header, CustomSearchSelect } from "@plane/ui";
|
||||
import { Breadcrumbs, Button, ContrastIcon, CustomSearchSelect, Header, Tooltip } from "@plane/ui";
|
||||
import { cn, isIssueFilterActive } from "@plane/utils";
|
||||
// components
|
||||
import { WorkItemsModal } from "@/components/analytics/work-items/modal";
|
||||
import { BreadcrumbLink, SwitcherLabel } from "@/components/common";
|
||||
import { CycleQuickActions } from "@/components/cycles";
|
||||
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { isIssueFilterActive } from "@/helpers/filter.helper";
|
||||
// hooks
|
||||
import {
|
||||
useEventTracker,
|
||||
useCommandPalette,
|
||||
useCycle,
|
||||
useEventTracker,
|
||||
useIssues,
|
||||
useLabel,
|
||||
useMember,
|
||||
useProject,
|
||||
useProjectState,
|
||||
useIssues,
|
||||
useCommandPalette,
|
||||
useUserPermissions,
|
||||
} from "@/hooks/store";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
import useLocalStorage from "@/hooks/use-local-storage";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// plane web
|
||||
// plane web imports
|
||||
import { ProjectBreadcrumb } from "@/plane-web/components/breadcrumbs";
|
||||
|
||||
export const CycleIssuesHeader: React.FC = observer(() => {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { useCallback, useState } from "react";
|
|||
import { useParams } from "next/navigation";
|
||||
// icons
|
||||
import { Calendar, ChevronDown, Kanban, List } from "lucide-react";
|
||||
// plane constants
|
||||
// plane imports
|
||||
import {
|
||||
EIssueLayoutTypes,
|
||||
EIssueFilterType,
|
||||
|
|
@ -12,17 +12,14 @@ import {
|
|||
ISSUE_LAYOUTS,
|
||||
ISSUE_DISPLAY_FILTERS_BY_PAGE,
|
||||
} from "@plane/constants";
|
||||
// i18n
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// types
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
|
||||
// ui
|
||||
import { CustomMenu } from "@plane/ui";
|
||||
import { isIssueFilterActive } from "@plane/utils";
|
||||
// components
|
||||
import { WorkItemsModal } from "@/components/analytics/work-items/modal";
|
||||
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown, IssueLayoutIcon } from "@/components/issues";
|
||||
// helpers
|
||||
import { isIssueFilterActive } from "@/helpers/filter.helper";
|
||||
// hooks
|
||||
import { useIssues, useCycle, useProjectState, useLabel, useMember, useProject } from "@/hooks/store";
|
||||
|
||||
|
|
|
|||
|
|
@ -9,12 +9,12 @@ import { useTranslation } from "@plane/i18n";
|
|||
import { TCycleFilters } from "@plane/types";
|
||||
// components
|
||||
import { Header, EHeaderVariant } from "@plane/ui";
|
||||
import { PageHead } from "@/components/core";
|
||||
import { calculateTotalFilters } from "@plane/utils";
|
||||
import { PageHead } from "@/components/core/page-title";
|
||||
import { CyclesView, CycleCreateUpdateModal, CycleAppliedFiltersList } from "@/components/cycles";
|
||||
import { ComicBoxButton, DetailedEmptyState } from "@/components/empty-state";
|
||||
import { CycleModuleListLayout } from "@/components/ui";
|
||||
// helpers
|
||||
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||
// hooks
|
||||
import { useEventTracker, useCycle, useProject, useCycleFilter, useUserPermissions } from "@/hooks/store";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
|
|
|
|||
|
|
@ -12,10 +12,10 @@ import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOption
|
|||
// ui
|
||||
import { Breadcrumbs, LayersIcon, Tooltip } from "@plane/ui";
|
||||
// components
|
||||
import { isIssueFilterActive } from "@plane/utils";
|
||||
import { BreadcrumbLink } from "@/components/common";
|
||||
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
|
||||
// helpers
|
||||
import { isIssueFilterActive } from "@/helpers/filter.helper";
|
||||
// hooks
|
||||
import { useIssues, useLabel, useMember, useProject, useProjectState } from "@/hooks/store";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
"use client";
|
||||
import { observer } from "mobx-react";
|
||||
// components
|
||||
import { useParams, useSearchParams } from "next/navigation";
|
||||
import { EUserPermissionsLevel } from "@plane/constants";
|
||||
import { EUserPermissionsLevel, EInboxIssueCurrentTab } from "@plane/constants";
|
||||
// components
|
||||
import { EUserProjectRoles } from "@plane/constants/src/user";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { PageHead } from "@/components/core";
|
||||
import { DetailedEmptyState } from "@/components/empty-state";
|
||||
import { InboxIssueRoot } from "@/components/inbox";
|
||||
// helpers
|
||||
import { EInboxIssueCurrentTab } from "@/helpers/inbox.helper";
|
||||
// hooks
|
||||
import { useProject, useUserPermissions } from "@/hooks/store";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { observer } from "mobx-react";
|
|||
import { useParams } from "next/navigation";
|
||||
// icons
|
||||
import { Calendar, ChevronDown, Kanban, List } from "lucide-react";
|
||||
// plane constants
|
||||
// plane imports
|
||||
import {
|
||||
EIssueLayoutTypes,
|
||||
EIssueFilterType,
|
||||
|
|
@ -13,12 +13,10 @@ import {
|
|||
ISSUE_LAYOUTS,
|
||||
ISSUE_DISPLAY_FILTERS_BY_PAGE,
|
||||
} from "@plane/constants";
|
||||
// i18n
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// types
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
|
||||
// ui
|
||||
import { CustomMenu } from "@plane/ui";
|
||||
import { isIssueFilterActive } from "@plane/utils";
|
||||
// components
|
||||
import { WorkItemsModal } from "@/components/analytics/work-items/modal";
|
||||
import {
|
||||
|
|
@ -28,7 +26,6 @@ import {
|
|||
IssueLayoutIcon,
|
||||
} from "@/components/issues/issue-layouts";
|
||||
// helpers
|
||||
import { isIssueFilterActive } from "@/helpers/filter.helper";
|
||||
// hooks
|
||||
import { useIssues, useLabel, useMember, useProject, useProjectState } from "@/hooks/store";
|
||||
|
||||
|
|
|
|||
|
|
@ -4,12 +4,12 @@ import { observer } from "mobx-react";
|
|||
import { useParams } from "next/navigation";
|
||||
import useSWR from "swr";
|
||||
// components
|
||||
import { cn } from "@plane/utils";
|
||||
import { EmptyState } from "@/components/common";
|
||||
import { PageHead } from "@/components/core";
|
||||
import { ModuleLayoutRoot } from "@/components/issues";
|
||||
import { ModuleAnalyticsSidebar } from "@/components/modules";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
// hooks
|
||||
import { useModule, useProject } from "@/hooks/store";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import Link from "next/link";
|
|||
import { useParams } from "next/navigation";
|
||||
// icons
|
||||
import { PanelRight } from "lucide-react";
|
||||
// plane constants
|
||||
// plane imports
|
||||
import {
|
||||
EIssueLayoutTypes,
|
||||
EIssuesStoreType,
|
||||
|
|
@ -15,23 +15,20 @@ import {
|
|||
EUserPermissions,
|
||||
EUserPermissionsLevel,
|
||||
} from "@plane/constants";
|
||||
// types
|
||||
import {
|
||||
ICustomSearchSelectOption,
|
||||
IIssueDisplayFilterOptions,
|
||||
IIssueDisplayProperties,
|
||||
IIssueFilterOptions,
|
||||
} from "@plane/types";
|
||||
// ui
|
||||
import { Breadcrumbs, Button, DiceIcon, Tooltip, Header, CustomSearchSelect } from "@plane/ui";
|
||||
import { cn, isIssueFilterActive } from "@plane/utils";
|
||||
// components
|
||||
import { WorkItemsModal } from "@/components/analytics/work-items/modal";
|
||||
import { BreadcrumbLink, SwitcherLabel } from "@/components/common";
|
||||
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
|
||||
// helpers
|
||||
import { ModuleQuickActions } from "@/components/modules";
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { isIssueFilterActive } from "@/helpers/filter.helper";
|
||||
// hooks
|
||||
import {
|
||||
useEventTracker,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { observer } from "mobx-react";
|
|||
import { useParams } from "next/navigation";
|
||||
// icons
|
||||
import { Calendar, ChevronDown, Kanban, List } from "lucide-react";
|
||||
// plane constants
|
||||
// plane imports
|
||||
import {
|
||||
EIssueLayoutTypes,
|
||||
EIssueFilterType,
|
||||
|
|
@ -13,12 +13,10 @@ import {
|
|||
ISSUE_LAYOUTS,
|
||||
ISSUE_DISPLAY_FILTERS_BY_PAGE,
|
||||
} from "@plane/constants";
|
||||
// plane i18n
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// types
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
|
||||
// ui
|
||||
import { CustomMenu } from "@plane/ui";
|
||||
import { isIssueFilterActive } from "@plane/utils";
|
||||
// components
|
||||
import { WorkItemsModal } from "@/components/analytics/work-items/modal";
|
||||
import {
|
||||
|
|
@ -28,7 +26,6 @@ import {
|
|||
IssueLayoutIcon,
|
||||
} from "@/components/issues/issue-layouts";
|
||||
// helpers
|
||||
import { isIssueFilterActive } from "@/helpers/filter.helper";
|
||||
// hooks
|
||||
import { useIssues, useLabel, useMember, useModule, useProject, useProjectState } from "@/hooks/store";
|
||||
|
||||
|
|
|
|||
|
|
@ -8,11 +8,11 @@ import { EUserPermissionsLevel, EUserProjectRoles } from "@plane/constants";
|
|||
import { useTranslation } from "@plane/i18n";
|
||||
import { TModuleFilters } from "@plane/types";
|
||||
// components
|
||||
import { PageHead } from "@/components/core";
|
||||
import { calculateTotalFilters } from "@plane/utils";
|
||||
import { PageHead } from "@/components/core/page-title";
|
||||
import { DetailedEmptyState } from "@/components/empty-state";
|
||||
import { ModuleAppliedFiltersList, ModulesListView } from "@/components/modules";
|
||||
// helpers
|
||||
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||
// hooks
|
||||
import { useModuleFilter, useProject, useUserPermissions } from "@/hooks/store";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
|
|
|
|||
|
|
@ -7,10 +7,10 @@ import { ICustomSearchSelectOption } from "@plane/types";
|
|||
// ui
|
||||
import { Breadcrumbs, Header, CustomSearchSelect } from "@plane/ui";
|
||||
// components
|
||||
import { getPageName } from "@plane/utils";
|
||||
import { BreadcrumbLink, PageAccessIcon, SwitcherLabel } from "@/components/common";
|
||||
import { PageHeaderActions } from "@/components/pages/header/actions";
|
||||
// helpers
|
||||
import { getPageName } from "@/helpers/page.helper";
|
||||
// hooks
|
||||
import { useProject } from "@/hooks/store";
|
||||
// plane web components
|
||||
|
|
|
|||
|
|
@ -24,12 +24,12 @@ import {
|
|||
// ui
|
||||
import { Breadcrumbs, Button, Tooltip, Header, CustomSearchSelect } from "@plane/ui";
|
||||
// components
|
||||
import { isIssueFilterActive } from "@plane/utils";
|
||||
import { BreadcrumbLink, SwitcherLabel } from "@/components/common";
|
||||
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
|
||||
// constants
|
||||
import { ViewQuickActions } from "@/components/views";
|
||||
// helpers
|
||||
import { isIssueFilterActive } from "@/helpers/filter.helper";
|
||||
// hooks
|
||||
import {
|
||||
useCommandPalette,
|
||||
|
|
|
|||
|
|
@ -8,13 +8,13 @@ import { EUserPermissionsLevel, EUserProjectRoles, EViewAccess } from "@plane/co
|
|||
import { useTranslation } from "@plane/i18n";
|
||||
import { TViewFilterProps } from "@plane/types";
|
||||
import { Header, EHeaderVariant } from "@plane/ui";
|
||||
import { PageHead } from "@/components/core";
|
||||
import { calculateTotalFilters } from "@plane/utils";
|
||||
import { PageHead } from "@/components/core/page-title";
|
||||
import { DetailedEmptyState } from "@/components/empty-state";
|
||||
import { ProjectViewsList } from "@/components/views";
|
||||
import { ViewAppliedFiltersList } from "@/components/views/applied-filters";
|
||||
// constants
|
||||
// helpers
|
||||
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||
// hooks
|
||||
import { useProject, useProjectView, useUserPermissions } from "@/hooks/store";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
|
|
|
|||
|
|
@ -5,11 +5,11 @@ import { observer } from "mobx-react";
|
|||
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { useOutsideClickDetector } from "@plane/hooks";
|
||||
// components
|
||||
import { cn } from "@plane/utils";
|
||||
import { SidebarDropdown, SidebarHelpSection, SidebarProjectsList, SidebarQuickActions } from "@/components/workspace";
|
||||
import { SidebarFavoritesMenu } from "@/components/workspace/sidebar/favorites/favorites-menu";
|
||||
import { SidebarMenuItems } from "@/components/workspace/sidebar/sidebar-menu-items";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
// hooks
|
||||
import { useAppTheme, useUserPermissions } from "@/hooks/store";
|
||||
import { useFavorite } from "@/hooks/store/use-favorite";
|
||||
|
|
|
|||
|
|
@ -12,11 +12,11 @@ import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOption
|
|||
// ui
|
||||
import { Breadcrumbs, Button, Header } from "@plane/ui";
|
||||
// components
|
||||
import { isIssueFilterActive } from "@plane/utils";
|
||||
import { BreadcrumbLink } from "@/components/common";
|
||||
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection } from "@/components/issues";
|
||||
import { CreateUpdateWorkspaceViewModal } from "@/components/workspace";
|
||||
// helpers
|
||||
import { isIssueFilterActive } from "@/helpers/filter.helper";
|
||||
// hooks
|
||||
import { useLabel, useMember, useIssues, useGlobalView } from "@/hooks/store";
|
||||
|
||||
|
|
|
|||
|
|
@ -4,13 +4,14 @@ import { observer } from "mobx-react";
|
|||
// components
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { cn } from "@plane/utils";
|
||||
import { NotAuthorizedView } from "@/components/auth-screens";
|
||||
import { PageHead } from "@/components/core";
|
||||
import ExportGuide from "@/components/exporter/guide";
|
||||
// helpers
|
||||
import { SettingsContentWrapper, SettingsHeading } from "@/components/settings";
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
// hooks
|
||||
import { SettingsContentWrapper } from "@/components/settings/content-wrapper";
|
||||
import SettingsHeading from "@/components/settings/heading";
|
||||
import { useUserPermissions, useWorkspace } from "@/hooks/store";
|
||||
|
||||
const ExportsPage = observer(() => {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { useTranslation } from "@plane/i18n";
|
|||
import { IWorkspaceBulkInviteFormData } from "@plane/types";
|
||||
// ui
|
||||
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { cn, getUserRole } from "@plane/utils";
|
||||
// components
|
||||
import { NotAuthorizedView } from "@/components/auth-screens";
|
||||
import { CountChip } from "@/components/common";
|
||||
|
|
@ -17,8 +18,6 @@ import { PageHead } from "@/components/core";
|
|||
import { SettingsContentWrapper } from "@/components/settings";
|
||||
import { WorkspaceMembersList } from "@/components/workspace";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { getUserRole } from "@/helpers/user.helper";
|
||||
// hooks
|
||||
import { useEventTracker, useMember, useUserPermissions, useWorkspace } from "@/hooks/store";
|
||||
// plane web components
|
||||
|
|
|
|||
|
|
@ -6,10 +6,12 @@ import { useTranslation } from "@plane/i18n";
|
|||
import { LogoSpinner } from "@/components/common";
|
||||
import { PageHead } from "@/components/core";
|
||||
import { PreferencesList } from "@/components/preferences/list";
|
||||
import { LanguageTimezone, ProfileSettingContentHeader } from "@/components/profile";
|
||||
import { ProfileSettingContentHeader } from "@/components/profile";
|
||||
// hooks
|
||||
import { LanguageTimezone } from "@/components/profile/preferences/language-timezone";
|
||||
import { SettingsHeading } from "@/components/settings";
|
||||
import { useUserProfile } from "@/hooks/store";
|
||||
|
||||
const ProfileAppearancePage = observer(() => {
|
||||
const { t } = useTranslation();
|
||||
// hooks
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue