diff --git a/admin/Dockerfile.admin b/admin/Dockerfile.admin index ad9469110e7..3d9897705e8 100644 --- a/admin/Dockerfile.admin +++ b/admin/Dockerfile.admin @@ -83,4 +83,6 @@ ENV NEXT_PUBLIC_WEB_BASE_URL=$NEXT_PUBLIC_WEB_BASE_URL ENV NEXT_TELEMETRY_DISABLED 1 ENV TURBO_TELEMETRY_DISABLED 1 -EXPOSE 3000 \ No newline at end of file +EXPOSE 3000 + +CMD node admin/server.js admin \ No newline at end of file diff --git a/admin/ce/components/common/upgrade-button.tsx b/admin/ce/components/common/upgrade-button.tsx index aa3c95fdbed..cdbe32a5baf 100644 --- a/admin/ce/components/common/upgrade-button.tsx +++ b/admin/ce/components/common/upgrade-button.tsx @@ -9,8 +9,9 @@ import { getButtonStyling } from "@plane/ui"; import { cn } from "@/helpers/common.helper"; export const UpgradeButton: React.FC = () => ( - - Available on One - - + + // + // Available on One + // + // ); diff --git a/admin/core/components/admin-sidebar/help-section.tsx b/admin/core/components/admin-sidebar/help-section.tsx index abba68e3eae..dcb96d0a512 100644 --- a/admin/core/components/admin-sidebar/help-section.tsx +++ b/admin/core/components/admin-sidebar/help-section.tsx @@ -61,17 +61,6 @@ export const HelpSection: FC = observer(() => { {!isSidebarCollapsed && "Redirect to plane"} - - - + + ))} + + ); +}); diff --git a/web/core/components/issues/issue-layouts/filters/applied-filters/filters-list.tsx b/web/core/components/issues/issue-layouts/filters/applied-filters/filters-list.tsx index 268358efea2..64acb2ccac6 100644 --- a/web/core/components/issues/issue-layouts/filters/applied-filters/filters-list.tsx +++ b/web/core/components/issues/issue-layouts/filters/applied-filters/filters-list.tsx @@ -14,6 +14,7 @@ import { AppliedProjectFilters, AppliedStateFilters, AppliedStateGroupFilters, + AppliedAdditionalPropertiesFilters } from "@/components/issues"; // constants // helpers @@ -36,6 +37,7 @@ type Props = { const membersFilters = ["assignees", "mentions", "created_by", "subscriber"]; const dateFilters = ["start_date", "target_date"]; +const additionalFilters = ["hub_code", "customer_code", "reference_number", "trip_reference_number", "vendor_code", "worker_code"]; export const AppliedFiltersList: React.FC = observer((props) => { const { @@ -134,6 +136,13 @@ export const AppliedFiltersList: React.FC = observer((props) => { values={value} /> )} + {additionalFilters?.includes(filterKey) && ( + handleRemoveFilter(filterKey, val)} + values={value} + /> + )} {isEditingAllowed && ( + )} + + ) : ( +

No Matches Found

+ )} + + )} + + ); +}); \ No newline at end of file diff --git a/web/core/components/issues/issue-layouts/filters/header/filters/custom-properties.tsx b/web/core/components/issues/issue-layouts/filters/header/filters/custom-properties.tsx new file mode 100644 index 00000000000..e67eaaac70d --- /dev/null +++ b/web/core/components/issues/issue-layouts/filters/header/filters/custom-properties.tsx @@ -0,0 +1,142 @@ +"use client"; + +import React, { useMemo, useState } from "react"; +// import sortBy from "lodash/sortBy"; +import { observer } from "mobx-react"; +import { useParams } from "next/navigation"; +import { IState } from "@plane/types"; +import { WorkspaceService } from "@/services/workspace.service"; +import { API_BASE_URL } from "@/helpers/common.helper"; +// import { Loader, StateGroupIcon } from "@plane/ui"; +import { FilterHeader, FilterOption } from "@/components/issues"; + +type Props = { + appliedFilters: string[] | null; + handleUpdate: (val: string) => void; + searchQuery: string; +}; + +export const FilterCustomProperty: React.FC = observer((props) => { + const { workspaceSlug } = useParams(); + const workspaceService = new WorkspaceService(API_BASE_URL); + const { appliedFilters, handleUpdate, searchQuery } = props; + + const [mainPreviewEnabled, setMainPreviewEnabled] = useState(true); + const [groupPreviewEnabled, setGroupPreviewEnabled] = useState>({}); + const [renderMoreGroupItems, setRenderMoreGroupItems] = useState>({}); + const [groupedProperties, setGroupedProperties] = useState>({}); + + const appliedFiltersCount = appliedFilters?.length ?? 0; + + const handleViewToggle = (groupKey: string) => { + setRenderMoreGroupItems((prev) => { + return { + ...prev, + [groupKey]: !prev[groupKey] + }; + }); + }; + + React.useEffect(() => { + const fetchCustomProperties = async (): Promise => { + try { + const data = await workspaceService.getIssuesCustomProperties(workspaceSlug.toString()); + + if (Array.isArray(data)) { + // Convert array to an object with group keys + const groupedData: Record = data.reduce((acc, item) => { + const key = item.group || "default"; // Adjust based on API response + acc[key] = acc[key] ? [...acc[key], item] : [item]; + return acc; + }, {} as Record); + + setGroupedProperties(groupedData); + } else { + setGroupedProperties(data); + } + + Object.keys(data).forEach((groupKey) => { + setGroupPreviewEnabled((prev) => ({ + ...prev, + [groupKey]: true, + })); + }); + } catch (error) { + console.error("Error fetching custom properties:", error); + } + }; + + + fetchCustomProperties(); + }, [workspaceSlug]); + + const filteredGroupOptions = useMemo(() => { + return Object.keys(groupedProperties).reduce>((acc, groupKey) => { + const properties = groupedProperties[groupKey]; + + const filteredValues = properties + .filter((property) => property?.toLowerCase()?.includes(searchQuery.toLowerCase())) + .map((property) => property); + + if (filteredValues?.length > 0) { + acc[groupKey] = filteredValues; + } + + return acc; + }, {}); + }, [searchQuery, groupedProperties]); + + const toggleGroupPreview = (groupKey: string) => { + setGroupPreviewEnabled((prev) => ({ + ...prev, + [groupKey]: !prev[groupKey], + })); + }; + + return ( + <> + 0 ? ` (${appliedFiltersCount})` : ""}`} + isPreviewEnabled={mainPreviewEnabled} + handleIsPreviewEnabled={() => setMainPreviewEnabled(!mainPreviewEnabled)} + /> + {mainPreviewEnabled && ( +
+ {Object.keys(filteredGroupOptions).map((groupKey) => { + const properties = filteredGroupOptions[groupKey]; + return ( +
+ toggleGroupPreview(groupKey)} + /> + {groupPreviewEnabled[groupKey] && ( +
+ {properties + .slice(0, renderMoreGroupItems[groupKey] ? properties.length : 5) + .map((property) => ( + handleUpdate(`${groupKey}:${property}`)} + title={property} + /> + ))} + {properties.length > 5 ? : null} +
+ )} +
+ ); + })} +
+ )} + + ); +}); diff --git a/web/core/components/issues/issue-layouts/filters/header/filters/filters-selection.tsx b/web/core/components/issues/issue-layouts/filters/header/filters/filters-selection.tsx index 0bf180aefa7..71ace44f61a 100644 --- a/web/core/components/issues/issue-layouts/filters/header/filters/filters-selection.tsx +++ b/web/core/components/issues/issue-layouts/filters/header/filters/filters-selection.tsx @@ -16,16 +16,18 @@ import { FilterState, FilterStateGroup, FilterTargetDate, + FilterAdditionalProperties, FilterCycle, FilterModule, FilterIssueGrouping, } from "@/components/issues"; // constants -import { ILayoutDisplayFiltersOptions } from "@/constants/issue"; +import { ILayoutDisplayFiltersOptions, ISSUE_ADDITIONAL_PROPERTIES } from "@/constants/issue"; // hooks import { usePlatformOS } from "@/hooks/use-platform-os"; // plane web components import { FilterIssueTypes } from "@/plane-web/components/issues"; +import { FilterCustomProperty } from "./custom-properties"; type Props = { filters: IIssueFilterOptions; @@ -242,6 +244,31 @@ export const FilterSelection: React.FC = observer((props) => { /> )} + + {ISSUE_ADDITIONAL_PROPERTIES.map((prop: any) => ( +
+ handleFiltersUpdate(prop.key as keyof IIssueFilterOptions, val)} + searchQuery={filtersSearchQuery} + /> +
+ ))} + + {/* custom_properties */} + {isFilterEnabled("custom_properties") && ( +
+ handleFiltersUpdate("custom_properties", val)} + // handleUpdate={(val) => handleFiltersUpdate("custom_properties", null, val)} + searchQuery={filtersSearchQuery} + /> +
+ )} ); diff --git a/web/core/components/issues/issue-layouts/filters/header/filters/index.ts b/web/core/components/issues/issue-layouts/filters/header/filters/index.ts index ab5756bf4d7..495020f9244 100644 --- a/web/core/components/issues/issue-layouts/filters/header/filters/index.ts +++ b/web/core/components/issues/issue-layouts/filters/header/filters/index.ts @@ -11,3 +11,4 @@ export * from "./state"; export * from "./cycle"; export * from "./module"; export * from "./target-date"; +export * from "./additional-properties"; diff --git a/web/core/components/issues/issue-layouts/spreadsheet/columns/index.ts b/web/core/components/issues/issue-layouts/spreadsheet/columns/index.ts index 3439d398b41..eec02b8ca18 100644 --- a/web/core/components/issues/issue-layouts/spreadsheet/columns/index.ts +++ b/web/core/components/issues/issue-layouts/spreadsheet/columns/index.ts @@ -12,3 +12,4 @@ export * from "./sub-issue-column"; export * from "./updated-on-column"; export * from "./module-column"; export * from "./cycle-column"; +export * from "./standard-property-column"; diff --git a/web/core/components/issues/issue-layouts/spreadsheet/columns/standard-property-column.tsx b/web/core/components/issues/issue-layouts/spreadsheet/columns/standard-property-column.tsx new file mode 100644 index 00000000000..4974f6c38e2 --- /dev/null +++ b/web/core/components/issues/issue-layouts/spreadsheet/columns/standard-property-column.tsx @@ -0,0 +1,21 @@ +import React from "react"; +import { observer } from "mobx-react"; +// types +import { TIssue } from "@plane/types"; +import { Row, Tooltip } from "@plane/ui"; + +type Props = { + issue: TIssue; +}; + +export const SpreadsheetStandardPropertyColumn: React.FC = observer((props) => { + const { issue, property } = props; + + return ( + + + {issue?.[property]} + + + ); +}); diff --git a/web/core/components/issues/issue-layouts/spreadsheet/issue-column.tsx b/web/core/components/issues/issue-layouts/spreadsheet/issue-column.tsx index dcd8773a0f1..9b8fb27e3db 100644 --- a/web/core/components/issues/issue-layouts/spreadsheet/issue-column.tsx +++ b/web/core/components/issues/issue-layouts/spreadsheet/issue-column.tsx @@ -43,6 +43,7 @@ export const IssueColumn = observer((props: Props) => { > , updates: any) => updateIssue && updateIssue(issue.project_id, issue.id, data).then(() => { diff --git a/web/core/components/issues/peek-overview/properties.tsx b/web/core/components/issues/peek-overview/properties.tsx index fcc706cbb86..daf1929351a 100644 --- a/web/core/components/issues/peek-overview/properties.tsx +++ b/web/core/components/issues/peek-overview/properties.tsx @@ -2,7 +2,7 @@ import { FC } from "react"; import { observer } from "mobx-react"; -import { Signal, Tag, Triangle, LayoutPanelTop, CalendarClock, CalendarCheck2, Users, UserCircle2 } from "lucide-react"; +import { Signal, Tag, Triangle, LayoutPanelTop, CalendarClock, CalendarCheck2, Users, UserCircle2, Info } from "lucide-react"; // hooks // ui icons import { DiceIcon, DoubleCircleIcon, ContrastIcon } from "@plane/ui"; @@ -21,6 +21,7 @@ import { IssueParentSelect, IssueLabel, TIssueOperations, + CustomProperties } from "@/components/issues"; // helpers import { cn } from "@/helpers/common.helper"; @@ -29,7 +30,8 @@ import { shouldHighlightIssueDueDate } from "@/helpers/issue.helper"; import { useIssueDetail, useMember, useProject, useProjectState } from "@/hooks/store"; // plane web components import { IssueAdditionalPropertyValuesUpdate } from "@/plane-web/components/issue-types/values"; -import { IssueWorklogProperty } from "@/plane-web/components/issues"; +import { IssueWorklogProperty} from "@/plane-web/components/issues"; +import { ISSUE_ADDITIONAL_PROPERTIES } from "@/constants/issue"; interface IPeekOverviewProperties { workspaceSlug: string; @@ -39,6 +41,7 @@ interface IPeekOverviewProperties { issueOperations: TIssueOperations; } + export const PeekOverviewProperties: FC = observer((props) => { const { workspaceSlug, projectId, issueId, issueOperations, disabled } = props; // store hooks @@ -53,9 +56,9 @@ export const PeekOverviewProperties: FC = observer((pro if (!issue) return <>; const createdByDetails = getUserDetails(issue?.created_by); const projectDetails = getProjectById(issue.project_id); + const customProperties = issue?.custom_properties || []; const isEstimateEnabled = projectDetails?.estimate; const stateDetails = getStateById(issue.state_id); - const minDate = getDate(issue.start_date); minDate?.setDate(minDate.getDate()); @@ -299,6 +302,20 @@ export const PeekOverviewProperties: FC = observer((pro isDisabled={disabled} /> )} + + {ISSUE_ADDITIONAL_PROPERTIES.map((prop: any) => + issue[prop.key] ? ( +
+
+ + {prop?.title} +
+
{issue[prop.key]}
+
+ ) : null + )} + + ); diff --git a/web/core/components/workspace/sidebar/dropdown.tsx b/web/core/components/workspace/sidebar/dropdown.tsx index a60a7d7e1b1..15d2a079df7 100644 --- a/web/core/components/workspace/sidebar/dropdown.tsx +++ b/web/core/components/workspace/sidebar/dropdown.tsx @@ -205,15 +205,6 @@ export const SidebarDropdown = observer(() => { )}
- - - - Create workspace - - {userLinks(workspaceSlug?.toString() ?? "").map( (link, index) => allowPermissions(link.access, EUserPermissionsLevel.WORKSPACE) && ( diff --git a/web/core/components/workspace/sidebar/help-section.tsx b/web/core/components/workspace/sidebar/help-section.tsx index 5570b5727a0..ed3dcd6dd93 100644 --- a/web/core/components/workspace/sidebar/help-section.tsx +++ b/web/core/components/workspace/sidebar/help-section.tsx @@ -15,7 +15,6 @@ import { useAppTheme, useCommandPalette, useInstance, useTransient, useUserSetti import { usePlatformOS } from "@/hooks/use-platform-os"; // plane web components import { PlaneVersionNumber } from "@/plane-web/components/global"; -import { WorkspaceEditionBadge } from "@/plane-web/components/workspace"; import { ENABLE_LOCAL_DB_CACHE } from "@/plane-web/constants/issues"; export interface WorkspaceHelpSectionProps { @@ -52,123 +51,9 @@ export const SidebarHelpSection: React.FC = observer( } )} > +
- - - - - - } - customButtonClassName={`relative grid place-items-center rounded-md p-1.5 outline-none ${isCollapsed ? "w-full" : ""}`} - menuButtonOnClick={() => !isNeedHelpOpen && setIsNeedHelpOpen(true)} - onMenuClose={() => setIsNeedHelpOpen(false)} - placement={isCollapsed ? "left-end" : "top-end"} - maxHeight="lg" - closeOnSelect - > - - - - Documentation - - - {config?.intercom_app_id && config?.is_intercom_enabled && ( - - - - )} - - - - Contact sales - - -
- {ENABLE_LOCAL_DB_CACHE && ( - -
{ - e.preventDefault(); - e.stopPropagation(); - }} - className="flex w-full items-center justify-between text-xs hover:bg-custom-background-80" - > - Local Cache - toggleLocalDB(workspaceSlug?.toString(), projectId?.toString())} - /> -
-
- )} - - - - - - - - - Discord - - -
- -
- -
-
- -
-