From 34c6047d807bab8e3d0997d3c298ab1f7adadbe5 Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Tue, 12 Aug 2025 19:37:53 +0530 Subject: [PATCH] [WEB-4677] improvement: add `defaultOpen` property to `CustomSearchSelect` (#7576) * [WEB-4677] improvement: add defaultOpen property to CustomSearchSelect * improvement: add utils to format time duration * improvement: added initializing state for project store * improvement: minor changes in automations page --- .../projects/[projectId]/automations/page.tsx | 9 +++- apps/web/ce/components/automations/root.tsx | 10 +++++ .../automation/auto-close-automation.tsx | 2 +- apps/web/core/store/project/project.store.ts | 9 ++++ .../ui/src/dropdowns/custom-search-select.tsx | 5 ++- packages/ui/src/dropdowns/helper.tsx | 1 + packages/utils/src/datetime.ts | 45 ++++++++++++++++++- 7 files changed, 74 insertions(+), 7 deletions(-) create mode 100644 apps/web/ce/components/automations/root.tsx diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/automations/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/automations/page.tsx index c7542b4f0..99e4bd77d 100644 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/automations/page.tsx +++ b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/[projectId]/automations/page.tsx @@ -15,10 +15,14 @@ import { PageHead } from "@/components/core"; // hooks import { SettingsContentWrapper, SettingsHeading } from "@/components/settings"; import { useProject, useUserPermissions } from "@/hooks/store"; +// plane web imports +import { CustomAutomationsRoot } from "@/plane-web/components/automations/root"; const AutomationSettingsPage = observer(() => { // router - const { workspaceSlug, projectId } = useParams(); + const { workspaceSlug: workspaceSlugParam, projectId: projectIdParam } = useParams(); + const workspaceSlug = workspaceSlugParam?.toString(); + const projectId = projectIdParam?.toString(); // store hooks const { workspaceUserInfo, allowPermissions } = useUserPermissions(); const { currentProjectDetails: projectDetails, updateProject } = useProject(); @@ -48,7 +52,7 @@ const AutomationSettingsPage = observer(() => { } return ( - +
{
+
); }); diff --git a/apps/web/ce/components/automations/root.tsx b/apps/web/ce/components/automations/root.tsx new file mode 100644 index 000000000..658580911 --- /dev/null +++ b/apps/web/ce/components/automations/root.tsx @@ -0,0 +1,10 @@ +"use client"; + +import React, { FC } from "react"; + +export type TCustomAutomationsRootProps = { + projectId: string; + workspaceSlug: string; +}; + +export const CustomAutomationsRoot: FC = () => <>; diff --git a/apps/web/core/components/automation/auto-close-automation.tsx b/apps/web/core/components/automation/auto-close-automation.tsx index 170e5e655..736abbc24 100644 --- a/apps/web/core/components/automation/auto-close-automation.tsx +++ b/apps/web/core/components/automation/auto-close-automation.tsx @@ -84,7 +84,7 @@ export const AutoCloseAutomation: React.FC = observer((props) => { handleClose={() => setmonthModal(false)} handleChange={handleChange} /> -
+
diff --git a/apps/web/core/store/project/project.store.ts b/apps/web/core/store/project/project.store.ts index 47b888636..8082db0e3 100644 --- a/apps/web/core/store/project/project.store.ts +++ b/apps/web/core/store/project/project.store.ts @@ -25,6 +25,7 @@ export interface IProjectStore { projectMap: Record; // projectId: project info projectAnalyticsCountMap: Record; // projectId: project analytics count // computed + isInitializingProjects: boolean; filteredProjectIds: string[] | undefined; workspaceProjectIds: string[] | undefined; archivedProjectIds: string[] | undefined; @@ -101,6 +102,7 @@ export class ProjectStore implements IProjectStore { openCollapsibleSection: observable.ref, lastCollapsibleAction: observable.ref, // computed + isInitializingProjects: computed, filteredProjectIds: computed, workspaceProjectIds: computed, archivedProjectIds: computed, @@ -138,6 +140,13 @@ export class ProjectStore implements IProjectStore { this.stateService = new ProjectStateService(); } + /** + * @description returns true if projects are still initializing + */ + get isInitializingProjects() { + return this.loader === "init-loader"; + } + /** * @description returns filtered projects based on filters and search query */ diff --git a/packages/ui/src/dropdowns/custom-search-select.tsx b/packages/ui/src/dropdowns/custom-search-select.tsx index 39b505ea9..3b1163bd3 100644 --- a/packages/ui/src/dropdowns/custom-search-select.tsx +++ b/packages/ui/src/dropdowns/custom-search-select.tsx @@ -6,9 +6,9 @@ import { usePopper } from "react-popper"; // plane imports import { useOutsideClickDetector } from "@plane/hooks"; // local imports -import { cn } from "../utils"; import { useDropdownKeyDown } from "../hooks/use-dropdown-key-down"; import { Tooltip } from "../tooltip"; +import { cn } from "../utils"; import { ICustomSearchSelectProps } from "./helper"; export const CustomSearchSelect = (props: ICustomSearchSelectProps) => { @@ -34,12 +34,13 @@ export const CustomSearchSelect = (props: ICustomSearchSelectProps) => { value, tabIndex, noResultsMessage = "No matches found", + defaultOpen = false, } = props; const [query, setQuery] = useState(""); const [referenceElement, setReferenceElement] = useState(null); const [popperElement, setPopperElement] = useState(null); - const [isOpen, setIsOpen] = useState(false); + const [isOpen, setIsOpen] = useState(defaultOpen); // refs const dropdownRef = useRef(null); diff --git a/packages/ui/src/dropdowns/helper.tsx b/packages/ui/src/dropdowns/helper.tsx index efff3165a..04d0d522b 100644 --- a/packages/ui/src/dropdowns/helper.tsx +++ b/packages/ui/src/dropdowns/helper.tsx @@ -19,6 +19,7 @@ export interface IDropdownProps { placement?: Placement; tabIndex?: number; useCaptureForOutsideClick?: boolean; + defaultOpen?: boolean; } export interface IPortalProps { diff --git a/packages/utils/src/datetime.ts b/packages/utils/src/datetime.ts index 22241e759..fe306a617 100644 --- a/packages/utils/src/datetime.ts +++ b/packages/utils/src/datetime.ts @@ -24,7 +24,7 @@ export const renderFormattedDate = ( try { // Format the date in the format provided or default format (MMM dd, yyyy) formattedDate = format(parsedDate, formatToken); - } catch (e) { + } catch (_e) { // Format the date in format (MMM dd, yyyy) in case of any error formattedDate = format(parsedDate, "MMM dd, yyyy"); } @@ -287,7 +287,7 @@ export const getDate = (date: string | Date | undefined | null): Date | undefine if (!isNumber(year) || !isNumber(month) || !isNumber(day)) return; return new Date(year, month - 1, day); - } catch (e) { + } catch (_e) { return undefined; } }; @@ -531,3 +531,44 @@ export const formatDateRange = ( return ""; }; + +// Duration Helpers +/** + * @returns {string} formatted duration in human readable format + * @description Converts seconds to human readable duration format (e.g., "1 hr 20 min 5 sec") + * @param {number} seconds - The duration in seconds + * @example formatDuration(3665) // "1 hr 1 min 5 sec" + * @example formatDuration(125) // "2 min 5 sec" + * @example formatDuration(45) // "45 sec" + */ +export const formatDuration = (seconds: number | undefined | null): string => { + // Return "N/A" if seconds is not a valid number + if (!isNumber(seconds) || seconds === null || seconds === undefined || seconds < 0) { + return "N/A"; + } + + // Round to nearest second + const totalSeconds = Math.round(seconds); + + // Calculate hours, minutes, and seconds + const hours = Math.floor(totalSeconds / 3600); + const minutes = Math.floor((totalSeconds % 3600) / 60); + const remainingSeconds = totalSeconds % 60; + + // Build the formatted string + const parts: string[] = []; + + if (hours > 0) { + parts.push(`${hours} hr${hours !== 1 ? "" : ""}`); // Always use "hr" for consistency + } + + if (minutes > 0) { + parts.push(`${minutes} min`); + } + + if (remainingSeconds > 0 || parts.length === 0) { + parts.push(`${remainingSeconds} sec`); + } + + return parts.join(" "); +};