From 7a8aef45993df4c6de60e34c98eb981da7c5befb Mon Sep 17 00:00:00 2001 From: guru_sainath Date: Thu, 7 Mar 2024 15:56:02 +0530 Subject: [PATCH 01/38] Fix: rendering issue in kanban swimlanes when the cycle or module is not assigned to an issue (#3899) --- web/store/issue/helpers/issue-helper.store.ts | 56 ++++++++++--------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/web/store/issue/helpers/issue-helper.store.ts b/web/store/issue/helpers/issue-helper.store.ts index 50e04e890..cc86560bd 100644 --- a/web/store/issue/helpers/issue-helper.store.ts +++ b/web/store/issue/helpers/issue-helper.store.ts @@ -62,35 +62,35 @@ export class IssueHelperStore implements TIssueHelperStore { issues: TIssueMap, isCalendarIssues: boolean = false ) => { - const _issues: { [group_id: string]: string[] } = {}; - if (!groupBy) return _issues; + const currentIssues: { [group_id: string]: string[] } = {}; + if (!groupBy) return currentIssues; this.issueDisplayFiltersDefaultData(groupBy).forEach((group) => { - _issues[group] = []; + currentIssues[group] = []; }); const projectIssues = this.issuesSortWithOrderBy(issues, orderBy); for (const issue in projectIssues) { - const _issue = projectIssues[issue]; + const currentIssue = projectIssues[issue]; let groupArray = []; if (groupBy === "state_detail.group") { // if groupBy state_detail.group is coming from the project level the we are using stateDetails from root store else we are looping through the stateMap - const state_group = (this.rootStore?.stateMap || {})?.[_issue?.state_id]?.group || "None"; + const state_group = (this.rootStore?.stateMap || {})?.[currentIssue?.state_id]?.group || "None"; groupArray = [state_group]; } else { - const groupValue = get(_issue, ISSUE_FILTER_DEFAULT_DATA[groupBy]); - groupArray = groupValue !== undefined ? this.getGroupArray(groupValue, isCalendarIssues) : []; + const groupValue = get(currentIssue, ISSUE_FILTER_DEFAULT_DATA[groupBy]); + groupArray = groupValue !== undefined ? this.getGroupArray(groupValue, isCalendarIssues) : ["None"]; } for (const group of groupArray) { - if (group && _issues[group]) _issues[group].push(_issue.id); - else if (group) _issues[group] = [_issue.id]; + if (group && currentIssues[group]) currentIssues[group].push(currentIssue.id); + else if (group) currentIssues[group] = [currentIssue.id]; } } - return _issues; + return currentIssues; }; subGroupedIssues = ( @@ -99,45 +99,47 @@ export class IssueHelperStore implements TIssueHelperStore { orderBy: TIssueOrderByOptions, issues: TIssueMap ) => { - const _issues: { [sub_group_id: string]: { [group_id: string]: string[] } } = {}; - if (!subGroupBy || !groupBy) return _issues; + const currentIssues: { [sub_group_id: string]: { [group_id: string]: string[] } } = {}; + if (!subGroupBy || !groupBy) return currentIssues; - this.issueDisplayFiltersDefaultData(subGroupBy).forEach((sub_group: any) => { + this.issueDisplayFiltersDefaultData(subGroupBy).forEach((sub_group) => { const groupByIssues: { [group_id: string]: string[] } = {}; this.issueDisplayFiltersDefaultData(groupBy).forEach((group) => { groupByIssues[group] = []; }); - _issues[sub_group] = groupByIssues; + currentIssues[sub_group] = groupByIssues; }); const projectIssues = this.issuesSortWithOrderBy(issues, orderBy); for (const issue in projectIssues) { - const _issue = projectIssues[issue]; + const currentIssue = projectIssues[issue]; let subGroupArray = []; let groupArray = []; if (subGroupBy === "state_detail.group" || groupBy === "state_detail.group") { - const state_group = (this.rootStore?.stateMap || {})?.[_issue?.state_id]?.group || "None"; + const state_group = (this.rootStore?.stateMap || {})?.[currentIssue?.state_id]?.group || "None"; subGroupArray = [state_group]; groupArray = [state_group]; } else { - const subGroupValue = get(_issue, ISSUE_FILTER_DEFAULT_DATA[subGroupBy]); - const groupValue = get(_issue, ISSUE_FILTER_DEFAULT_DATA[groupBy]); - subGroupArray = subGroupValue != undefined ? this.getGroupArray(subGroupValue) : []; - groupArray = groupValue != undefined ? this.getGroupArray(groupValue) : []; + const subGroupValue = get(currentIssue, ISSUE_FILTER_DEFAULT_DATA[subGroupBy]); + const groupValue = get(currentIssue, ISSUE_FILTER_DEFAULT_DATA[groupBy]); + + subGroupArray = subGroupValue != undefined ? this.getGroupArray(subGroupValue) : ["None"]; + groupArray = groupValue != undefined ? this.getGroupArray(groupValue) : ["None"]; } for (const subGroup of subGroupArray) { for (const group of groupArray) { - if (subGroup && group && _issues?.[subGroup]?.[group]) _issues[subGroup][group].push(_issue.id); - else if (subGroup && group && _issues[subGroup]) _issues[subGroup][group] = [_issue.id]; - else if (subGroup && group) _issues[subGroup] = { [group]: [_issue.id] }; + if (subGroup && group && currentIssues?.[subGroup]?.[group]) + currentIssues[subGroup][group].push(currentIssue.id); + else if (subGroup && group && currentIssues[subGroup]) currentIssues[subGroup][group] = [currentIssue.id]; + else if (subGroup && group) currentIssues[subGroup] = { [group]: [currentIssue.id] }; } } } - return _issues; + return currentIssues; }; unGroupedIssues = (orderBy: TIssueOrderByOptions, issues: TIssueMap) => @@ -215,8 +217,8 @@ export class IssueHelperStore implements TIssueHelperStore { const moduleMap = this.rootStore?.moduleMap; if (!moduleMap) break; for (const dataId of dataIdsArray) { - const _module = moduleMap[dataId]; - if (_module && _module.name) dataValues.push(_module.name.toLocaleLowerCase()); + const currentModule = moduleMap[dataId]; + if (currentModule && currentModule.name) dataValues.push(currentModule.name.toLocaleLowerCase()); } break; case "cycle_id": @@ -388,7 +390,7 @@ export class IssueHelperStore implements TIssueHelperStore { getGroupArray(value: boolean | number | string | string[] | null, isDate: boolean = false): string[] { if (!value || value === null || value === undefined) return ["None"]; if (Array.isArray(value)) - if (value.length) return value; + if (value && value.length) return value; else return ["None"]; else if (typeof value === "boolean") return [value ? "True" : "False"]; else if (typeof value === "number") return [value.toString()]; From 47a7f60611239f0b58b8b340d7ba6af15b97aaf4 Mon Sep 17 00:00:00 2001 From: Lakhan Baheti <94619783+1akhanBaheti@users.noreply.github.com> Date: Thu, 7 Mar 2024 16:53:43 +0530 Subject: [PATCH 02/38] [WEB-667] fix: unable to deselect project lead in create project modal (#3898) * fix: unable to deselect project lead in create project modal * removed unneccessary code --- web/components/project/create-project-modal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/components/project/create-project-modal.tsx b/web/components/project/create-project-modal.tsx index 4d30c3dec..b8e59f032 100644 --- a/web/components/project/create-project-modal.tsx +++ b/web/components/project/create-project-modal.tsx @@ -406,7 +406,7 @@ export const CreateProjectModal: FC = observer((props) => {
onChange(lead === value ? null : lead)} placeholder="Lead" multiple={false} buttonVariant="border-with-text" From 7b88a2a88cc777be716a3afdd80bc82ea791bad5 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Thu, 7 Mar 2024 16:53:59 +0530 Subject: [PATCH 03/38] [WEB-469] chore: selected filter sorting added in filter dropdown (#3869) * chore: selected filter sorting added in filter dropdown * chore: handleClearAllFilters function updated * chore: filter dropdown sorting updated * chore: filter dropdown sorting updated * chore: filter dropdown sorting updated --------- Co-authored-by: gurusainath --- .../empty-states/archived-issues.tsx | 2 +- .../empty-states/draft-issues.tsx | 2 +- .../empty-states/project-issues.tsx | 2 +- .../applied-filters/roots/archived-issue.tsx | 2 +- .../applied-filters/roots/cycle-root.tsx | 2 +- .../applied-filters/roots/draft-issue.tsx | 2 +- .../roots/global-view-root.tsx | 2 +- .../applied-filters/roots/module-root.tsx | 2 +- .../roots/profile-issues-root.tsx | 2 +- .../applied-filters/roots/project-root.tsx | 2 +- .../roots/project-view-root.tsx | 2 +- .../filters/header/filters/assignee.tsx | 37 ++++++++++++------- .../filters/header/filters/created-by.tsx | 34 +++++++++++------ .../filters/header/filters/cycle.tsx | 34 ++++++++++------- .../filters/header/filters/labels.tsx | 31 +++++++++++----- .../filters/header/filters/mentions.tsx | 33 +++++++++++------ .../filters/header/filters/module.tsx | 34 ++++++++++------- .../filters/header/filters/project.tsx | 31 +++++++++++----- .../filters/header/filters/state-group.tsx | 2 +- .../filters/header/filters/state.tsx | 26 ++++++++----- .../issue-layouts/roots/cycle-layout-root.tsx | 2 +- .../roots/module-layout-root.tsx | 2 +- 22 files changed, 180 insertions(+), 108 deletions(-) diff --git a/web/components/issues/issue-layouts/empty-states/archived-issues.tsx b/web/components/issues/issue-layouts/empty-states/archived-issues.tsx index c9de2279c..36c895de3 100644 --- a/web/components/issues/issue-layouts/empty-states/archived-issues.tsx +++ b/web/components/issues/issue-layouts/empty-states/archived-issues.tsx @@ -32,7 +32,7 @@ export const ProjectArchivedEmptyState: React.FC = observer(() => { if (!workspaceSlug || !projectId) return; const newFilters: IIssueFilterOptions = {}; Object.keys(userFilters ?? {}).forEach((key) => { - newFilters[key as keyof IIssueFilterOptions] = null; + newFilters[key as keyof IIssueFilterOptions] = []; }); issuesFilter.updateFilters(workspaceSlug.toString(), projectId.toString(), EIssueFilterType.FILTERS, { ...newFilters, diff --git a/web/components/issues/issue-layouts/empty-states/draft-issues.tsx b/web/components/issues/issue-layouts/empty-states/draft-issues.tsx index 0968ed07a..c23fea100 100644 --- a/web/components/issues/issue-layouts/empty-states/draft-issues.tsx +++ b/web/components/issues/issue-layouts/empty-states/draft-issues.tsx @@ -31,7 +31,7 @@ export const ProjectDraftEmptyState: React.FC = observer(() => { if (!workspaceSlug || !projectId) return; const newFilters: IIssueFilterOptions = {}; Object.keys(userFilters ?? {}).forEach((key) => { - newFilters[key as keyof IIssueFilterOptions] = null; + newFilters[key as keyof IIssueFilterOptions] = []; }); issuesFilter.updateFilters(workspaceSlug.toString(), projectId.toString(), EIssueFilterType.FILTERS, { ...newFilters, diff --git a/web/components/issues/issue-layouts/empty-states/project-issues.tsx b/web/components/issues/issue-layouts/empty-states/project-issues.tsx index 12642d364..58929e48d 100644 --- a/web/components/issues/issue-layouts/empty-states/project-issues.tsx +++ b/web/components/issues/issue-layouts/empty-states/project-issues.tsx @@ -34,7 +34,7 @@ export const ProjectEmptyState: React.FC = observer(() => { if (!workspaceSlug || !projectId) return; const newFilters: IIssueFilterOptions = {}; Object.keys(userFilters ?? {}).forEach((key) => { - newFilters[key as keyof IIssueFilterOptions] = null; + newFilters[key as keyof IIssueFilterOptions] = []; }); issuesFilter.updateFilters(workspaceSlug.toString(), projectId.toString(), EIssueFilterType.FILTERS, { ...newFilters, diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/archived-issue.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/archived-issue.tsx index 35651d870..7e6926fa5 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/archived-issue.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/archived-issue.tsx @@ -56,7 +56,7 @@ export const ArchivedIssueAppliedFiltersRoot: React.FC = observer(() => { const newFilters: IIssueFilterOptions = {}; Object.keys(userFilters ?? {}).forEach((key) => { - newFilters[key as keyof IIssueFilterOptions] = null; + newFilters[key as keyof IIssueFilterOptions] = []; }); updateFilters(workspaceSlug.toString(), projectId.toString(), EIssueFilterType.FILTERS, { diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/cycle-root.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/cycle-root.tsx index 6a741b73d..ee6b4e694 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/cycle-root.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/cycle-root.tsx @@ -62,7 +62,7 @@ export const CycleAppliedFiltersRoot: React.FC = observer(() => { if (!workspaceSlug || !projectId || !cycleId) return; const newFilters: IIssueFilterOptions = {}; Object.keys(userFilters ?? {}).forEach((key) => { - newFilters[key as keyof IIssueFilterOptions] = null; + newFilters[key as keyof IIssueFilterOptions] = []; }); updateFilters( workspaceSlug.toString(), diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/draft-issue.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/draft-issue.tsx index a075d59d2..61c0e346a 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/draft-issue.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/draft-issue.tsx @@ -53,7 +53,7 @@ export const DraftIssueAppliedFiltersRoot: React.FC = observer(() => { const newFilters: IIssueFilterOptions = {}; Object.keys(userFilters ?? {}).forEach((key) => { - newFilters[key as keyof IIssueFilterOptions] = null; + newFilters[key as keyof IIssueFilterOptions] = []; }); updateFilters(workspaceSlug.toString(), projectId.toString(), EIssueFilterType.FILTERS, { ...newFilters }); diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/global-view-root.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/global-view-root.tsx index a431652f1..d907cf168 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/global-view-root.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/global-view-root.tsx @@ -76,7 +76,7 @@ export const GlobalViewsAppliedFiltersRoot = observer((props: Props) => { if (!workspaceSlug || !globalViewId) return; const newFilters: IIssueFilterOptions = {}; Object.keys(userFilters ?? {}).forEach((key) => { - newFilters[key as keyof IIssueFilterOptions] = null; + newFilters[key as keyof IIssueFilterOptions] = []; }); updateFilters( workspaceSlug.toString(), diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/module-root.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/module-root.tsx index b49ddf4d6..fc78d79ea 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/module-root.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/module-root.tsx @@ -61,7 +61,7 @@ export const ModuleAppliedFiltersRoot: React.FC = observer(() => { if (!workspaceSlug || !projectId || !moduleId) return; const newFilters: IIssueFilterOptions = {}; Object.keys(userFilters ?? {}).forEach((key) => { - newFilters[key as keyof IIssueFilterOptions] = null; + newFilters[key as keyof IIssueFilterOptions] = []; }); updateFilters( workspaceSlug.toString(), diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/profile-issues-root.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/profile-issues-root.tsx index 91eeef423..b0c496a7b 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/profile-issues-root.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/profile-issues-root.tsx @@ -57,7 +57,7 @@ export const ProfileIssuesAppliedFiltersRoot: React.FC = observer(() => { if (!workspaceSlug || !userId) return; const newFilters: IIssueFilterOptions = {}; Object.keys(userFilters ?? {}).forEach((key) => { - newFilters[key as keyof IIssueFilterOptions] = null; + newFilters[key as keyof IIssueFilterOptions] = []; }); updateFilters(workspaceSlug.toString(), undefined, EIssueFilterType.FILTERS, { ...newFilters }, userId.toString()); }; diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/project-root.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/project-root.tsx index c0b67043a..602f3fa2d 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/project-root.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/project-root.tsx @@ -59,7 +59,7 @@ export const ProjectAppliedFiltersRoot: React.FC = observer(() => { if (!workspaceSlug || !projectId) return; const newFilters: IIssueFilterOptions = {}; Object.keys(userFilters ?? {}).forEach((key) => { - newFilters[key as keyof IIssueFilterOptions] = null; + newFilters[key as keyof IIssueFilterOptions] = []; }); updateFilters(workspaceSlug.toString(), projectId.toString(), EIssueFilterType.FILTERS, { ...newFilters }); }; diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/project-view-root.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/project-view-root.tsx index 760d2e7e4..6586159fa 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/roots/project-view-root.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/project-view-root.tsx @@ -67,7 +67,7 @@ export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => { if (!workspaceSlug || !projectId || !viewId) return; const newFilters: IIssueFilterOptions = {}; Object.keys(userFilters ?? {}).forEach((key) => { - newFilters[key as keyof IIssueFilterOptions] = null; + newFilters[key as keyof IIssueFilterOptions] = []; }); updateFilters( workspaceSlug.toString(), diff --git a/web/components/issues/issue-layouts/filters/header/filters/assignee.tsx b/web/components/issues/issue-layouts/filters/header/filters/assignee.tsx index b26b688af..c51fcf7ab 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/assignee.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/assignee.tsx @@ -1,11 +1,12 @@ -import { useState } from "react"; +import { useMemo, useState } from "react"; import { observer } from "mobx-react-lite"; +import sortBy from "lodash/sortBy"; // hooks -import { Avatar, Loader } from "@plane/ui"; -import { FilterHeader, FilterOption } from "components/issues"; import { useMember } from "hooks/store"; // components +import { FilterHeader, FilterOption } from "components/issues"; // ui +import { Avatar, Loader } from "@plane/ui"; type Props = { appliedFilters: string[] | null; @@ -24,15 +25,23 @@ export const FilterAssignees: React.FC = observer((props: Props) => { const appliedFiltersCount = appliedFilters?.length ?? 0; - const filteredOptions = memberIds?.filter( - (memberId) => getUserDetails(memberId)?.display_name.toLowerCase().includes(searchQuery.toLowerCase()) - ); + const sortedOptions = useMemo(() => { + const filteredOptions = (memberIds || []).filter((memberId) => + getUserDetails(memberId)?.display_name.toLowerCase().includes(searchQuery.toLowerCase()) + ); + + return sortBy(filteredOptions, [ + (memberId) => !(appliedFilters ?? []).includes(memberId), + (memberId) => getUserDetails(memberId)?.display_name.toLowerCase(), + ]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [searchQuery]); const handleViewToggle = () => { - if (!filteredOptions) return; + if (!sortedOptions) return; - if (itemsToRender === filteredOptions.length) setItemsToRender(5); - else setItemsToRender(filteredOptions.length); + if (itemsToRender === sortedOptions.length) setItemsToRender(5); + else setItemsToRender(sortedOptions.length); }; return ( @@ -44,10 +53,10 @@ export const FilterAssignees: React.FC = observer((props: Props) => { /> {previewEnabled && (
- {filteredOptions ? ( - filteredOptions.length > 0 ? ( + {sortedOptions ? ( + sortedOptions.length > 0 ? ( <> - {filteredOptions.slice(0, itemsToRender).map((memberId) => { + {sortedOptions.slice(0, itemsToRender).map((memberId) => { const member = getUserDetails(memberId); if (!member) return null; @@ -61,13 +70,13 @@ export const FilterAssignees: React.FC = observer((props: Props) => { /> ); })} - {filteredOptions.length > 5 && ( + {sortedOptions.length > 5 && ( )} diff --git a/web/components/issues/issue-layouts/filters/header/filters/created-by.tsx b/web/components/issues/issue-layouts/filters/header/filters/created-by.tsx index 45e3309a9..765955bf9 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/created-by.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/created-by.tsx @@ -1,5 +1,6 @@ -import { useState } from "react"; +import { useMemo, useState } from "react"; import { observer } from "mobx-react-lite"; +import sortBy from "lodash/sortBy"; // hooks import { Avatar, Loader } from "@plane/ui"; import { FilterHeader, FilterOption } from "components/issues"; @@ -22,16 +23,25 @@ export const FilterCreatedBy: React.FC = observer((props: Props) => { // store hooks const { getUserDetails } = useMember(); - const filteredOptions = memberIds?.filter( - (memberId) => getUserDetails(memberId)?.display_name.toLowerCase().includes(searchQuery.toLowerCase()) - ); + const sortedOptions = useMemo(() => { + const filteredOptions = (memberIds || []).filter((memberId) => + getUserDetails(memberId)?.display_name.toLowerCase().includes(searchQuery.toLowerCase()) + ); + + return sortBy(filteredOptions, [ + (memberId) => !(appliedFilters ?? []).includes(memberId), + (memberId) => getUserDetails(memberId)?.display_name.toLowerCase(), + ]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [searchQuery]); + const appliedFiltersCount = appliedFilters?.length ?? 0; const handleViewToggle = () => { - if (!filteredOptions) return; + if (!sortedOptions) return; - if (itemsToRender === filteredOptions.length) setItemsToRender(5); - else setItemsToRender(filteredOptions.length); + if (itemsToRender === sortedOptions.length) setItemsToRender(5); + else setItemsToRender(sortedOptions.length); }; return ( @@ -43,10 +53,10 @@ export const FilterCreatedBy: React.FC = observer((props: Props) => { /> {previewEnabled && (
- {filteredOptions ? ( - filteredOptions.length > 0 ? ( + {sortedOptions ? ( + sortedOptions.length > 0 ? ( <> - {filteredOptions.slice(0, itemsToRender).map((memberId) => { + {sortedOptions.slice(0, itemsToRender).map((memberId) => { const member = getUserDetails(memberId); if (!member) return null; @@ -60,13 +70,13 @@ export const FilterCreatedBy: React.FC = observer((props: Props) => { /> ); })} - {filteredOptions.length > 5 && ( + {sortedOptions.length > 5 && ( )} diff --git a/web/components/issues/issue-layouts/filters/header/filters/cycle.tsx b/web/components/issues/issue-layouts/filters/header/filters/cycle.tsx index 396addde6..b3a65a399 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/cycle.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/cycle.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useMemo, useState } from "react"; import sortBy from "lodash/sortBy"; import { observer } from "mobx-react"; // components @@ -31,16 +31,24 @@ export const FilterCycle: React.FC = observer((props) => { const cycleIds = projectId ? getProjectCycleIds(projectId) : undefined; const cycles = cycleIds?.map((projectId) => getCycleById(projectId)!) ?? null; const appliedFiltersCount = appliedFilters?.length ?? 0; - const filteredOptions = sortBy( - cycles?.filter((cycle) => cycle.name.toLowerCase().includes(searchQuery.toLowerCase())), - (cycle) => cycle.name.toLowerCase() - ); + + const sortedOptions = useMemo(() => { + const filteredOptions = (cycles || []).filter((cycle) => + cycle.name.toLowerCase().includes(searchQuery.toLowerCase()) + ); + + return sortBy(filteredOptions, [ + (cycle) => !appliedFilters?.includes(cycle.id), + (cycle) => cycle.name.toLowerCase(), + ]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [searchQuery]); const handleViewToggle = () => { - if (!filteredOptions) return; + if (!sortedOptions) return; - if (itemsToRender === filteredOptions.length) setItemsToRender(5); - else setItemsToRender(filteredOptions.length); + if (itemsToRender === sortedOptions.length) setItemsToRender(5); + else setItemsToRender(sortedOptions.length); }; const cycleStatus = (status: TCycleGroups) => (status ? status.toLocaleLowerCase() : "draft") as TCycleGroups; @@ -54,10 +62,10 @@ export const FilterCycle: React.FC = observer((props) => { /> {previewEnabled && (
- {filteredOptions ? ( - filteredOptions.length > 0 ? ( + {sortedOptions ? ( + sortedOptions.length > 0 ? ( <> - {filteredOptions.slice(0, itemsToRender).map((cycle) => ( + {sortedOptions.slice(0, itemsToRender).map((cycle) => ( = observer((props) => { activePulse={cycleStatus(cycle?.status) === "current" ? true : false} /> ))} - {filteredOptions.length > 5 && ( + {sortedOptions.length > 5 && ( )} diff --git a/web/components/issues/issue-layouts/filters/header/filters/labels.tsx b/web/components/issues/issue-layouts/filters/header/filters/labels.tsx index 42e955535..7097b1337 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/labels.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/labels.tsx @@ -1,5 +1,6 @@ -import React, { useState } from "react"; +import React, { useMemo, useState } from "react"; import { observer } from "mobx-react"; +import sortBy from "lodash/sortBy"; // components import { Loader } from "@plane/ui"; import { FilterHeader, FilterOption } from "components/issues"; @@ -26,13 +27,23 @@ export const FilterLabels: React.FC = observer((props) => { const appliedFiltersCount = appliedFilters?.length ?? 0; - const filteredOptions = labels?.filter((label) => label.name.toLowerCase().includes(searchQuery.toLowerCase())); + const sortedOptions = useMemo(() => { + const filteredOptions = (labels || []).filter((label) => + label.name.toLowerCase().includes(searchQuery.toLowerCase()) + ); + + return sortBy(filteredOptions, [ + (label) => !(appliedFilters ?? []).includes(label.id), + (label) => label.name.toLowerCase(), + ]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [searchQuery]); const handleViewToggle = () => { - if (!filteredOptions) return; + if (!sortedOptions) return; - if (itemsToRender === filteredOptions.length) setItemsToRender(5); - else setItemsToRender(filteredOptions.length); + if (itemsToRender === sortedOptions.length) setItemsToRender(5); + else setItemsToRender(sortedOptions.length); }; return ( @@ -44,10 +55,10 @@ export const FilterLabels: React.FC = observer((props) => { /> {previewEnabled && (
- {filteredOptions ? ( - filteredOptions.length > 0 ? ( + {sortedOptions ? ( + sortedOptions.length > 0 ? ( <> - {filteredOptions.slice(0, itemsToRender).map((label) => ( + {sortedOptions.slice(0, itemsToRender).map((label) => ( = observer((props) => { title={label.name} /> ))} - {filteredOptions.length > 5 && ( + {sortedOptions.length > 5 && ( )} diff --git a/web/components/issues/issue-layouts/filters/header/filters/mentions.tsx b/web/components/issues/issue-layouts/filters/header/filters/mentions.tsx index 4d2839b2c..80c16478a 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/mentions.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/mentions.tsx @@ -1,5 +1,6 @@ -import { useState } from "react"; +import { useMemo, useState } from "react"; import { observer } from "mobx-react-lite"; +import sortBy from "lodash/sortBy"; // hooks import { Loader, Avatar } from "@plane/ui"; import { FilterHeader, FilterOption } from "components/issues"; @@ -24,15 +25,23 @@ export const FilterMentions: React.FC = observer((props: Props) => { const appliedFiltersCount = appliedFilters?.length ?? 0; - const filteredOptions = memberIds?.filter( - (memberId) => getUserDetails(memberId)?.display_name.toLowerCase().includes(searchQuery.toLowerCase()) - ); + const sortedOptions = useMemo(() => { + const filteredOptions = (memberIds || []).filter((memberId) => + getUserDetails(memberId)?.display_name.toLowerCase().includes(searchQuery.toLowerCase()) + ); + + return sortBy(filteredOptions, [ + (memberId) => !(appliedFilters ?? []).includes(memberId), + (memberId) => getUserDetails(memberId)?.display_name.toLowerCase(), + ]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [searchQuery]); const handleViewToggle = () => { - if (!filteredOptions) return; + if (!sortedOptions) return; - if (itemsToRender === filteredOptions.length) setItemsToRender(5); - else setItemsToRender(filteredOptions.length); + if (itemsToRender === sortedOptions.length) setItemsToRender(5); + else setItemsToRender(sortedOptions.length); }; return ( @@ -44,10 +53,10 @@ export const FilterMentions: React.FC = observer((props: Props) => { /> {previewEnabled && (
- {filteredOptions ? ( - filteredOptions.length > 0 ? ( + {sortedOptions ? ( + sortedOptions.length > 0 ? ( <> - {filteredOptions.slice(0, itemsToRender).map((memberId) => { + {sortedOptions.slice(0, itemsToRender).map((memberId) => { const member = getUserDetails(memberId); if (!member) return null; @@ -61,13 +70,13 @@ export const FilterMentions: React.FC = observer((props: Props) => { /> ); })} - {filteredOptions.length > 5 && ( + {sortedOptions.length > 5 && ( )} diff --git a/web/components/issues/issue-layouts/filters/header/filters/module.tsx b/web/components/issues/issue-layouts/filters/header/filters/module.tsx index 812cf939f..6b6cd2b4d 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/module.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/module.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useMemo, useState } from "react"; import sortBy from "lodash/sortBy"; import { observer } from "mobx-react"; // components @@ -29,16 +29,24 @@ export const FilterModule: React.FC = observer((props) => { const moduleIds = projectId ? getProjectModuleIds(projectId) : undefined; const modules = moduleIds?.map((projectId) => getModuleById(projectId)!) ?? null; const appliedFiltersCount = appliedFilters?.length ?? 0; - const filteredOptions = sortBy( - modules?.filter((module) => module.name.toLowerCase().includes(searchQuery.toLowerCase())), - (module) => module.name.toLowerCase() - ); + + const sortedOptions = useMemo(() => { + const filteredOptions = (modules || []).filter((module) => + module.name.toLowerCase().includes(searchQuery.toLowerCase()) + ); + + return sortBy(filteredOptions, [ + (module) => !appliedFilters?.includes(module.id), + (module) => module.name.toLowerCase(), + ]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [searchQuery]); const handleViewToggle = () => { - if (!filteredOptions) return; + if (!sortedOptions) return; - if (itemsToRender === filteredOptions.length) setItemsToRender(5); - else setItemsToRender(filteredOptions.length); + if (itemsToRender === sortedOptions.length) setItemsToRender(5); + else setItemsToRender(sortedOptions.length); }; return ( @@ -50,10 +58,10 @@ export const FilterModule: React.FC = observer((props) => { /> {previewEnabled && (
- {filteredOptions ? ( - filteredOptions.length > 0 ? ( + {sortedOptions ? ( + sortedOptions.length > 0 ? ( <> - {filteredOptions.slice(0, itemsToRender).map((cycle) => ( + {sortedOptions.slice(0, itemsToRender).map((cycle) => ( = observer((props) => { title={cycle.name} /> ))} - {filteredOptions.length > 5 && ( + {sortedOptions.length > 5 && ( )} diff --git a/web/components/issues/issue-layouts/filters/header/filters/project.tsx b/web/components/issues/issue-layouts/filters/header/filters/project.tsx index b9f864b4b..b97001b00 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/project.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/project.tsx @@ -1,5 +1,6 @@ -import React, { useState } from "react"; +import React, { useMemo, useState } from "react"; import { observer } from "mobx-react"; +import sortBy from "lodash/sortBy"; // components import { Loader } from "@plane/ui"; import { FilterHeader, FilterOption } from "components/issues"; @@ -26,13 +27,23 @@ export const FilterProjects: React.FC = observer((props) => { // derived values const projects = workspaceProjectIds?.map((projectId) => getProjectById(projectId)!) ?? null; const appliedFiltersCount = appliedFilters?.length ?? 0; - const filteredOptions = projects?.filter((project) => project.name.toLowerCase().includes(searchQuery.toLowerCase())); + + const sortedOptions = useMemo(() => { + const filteredOptions = (projects || []).filter((project) => + project.name.toLowerCase().includes(searchQuery.toLowerCase()) + ); + return sortBy(filteredOptions, [ + (project) => !(appliedFilters ?? []).includes(project.id), + (project) => project.name.toLowerCase(), + ]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [searchQuery]); const handleViewToggle = () => { - if (!filteredOptions) return; + if (!sortedOptions) return; - if (itemsToRender === filteredOptions.length) setItemsToRender(5); - else setItemsToRender(filteredOptions.length); + if (itemsToRender === sortedOptions.length) setItemsToRender(5); + else setItemsToRender(sortedOptions.length); }; return ( @@ -44,10 +55,10 @@ export const FilterProjects: React.FC = observer((props) => { /> {previewEnabled && (
- {filteredOptions ? ( - filteredOptions.length > 0 ? ( + {sortedOptions ? ( + sortedOptions.length > 0 ? ( <> - {filteredOptions.slice(0, itemsToRender).map((project) => ( + {sortedOptions.slice(0, itemsToRender).map((project) => ( = observer((props) => { title={project.name} /> ))} - {filteredOptions.length > 5 && ( + {sortedOptions.length > 5 && ( )} diff --git a/web/components/issues/issue-layouts/filters/header/filters/state-group.tsx b/web/components/issues/issue-layouts/filters/header/filters/state-group.tsx index 06c1aae9f..e283112be 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/state-group.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/state-group.tsx @@ -1,6 +1,6 @@ import React, { useState } from "react"; import { observer } from "mobx-react-lite"; - +import sortBy from "lodash/sortBy"; // components import { StateGroupIcon } from "@plane/ui"; import { FilterHeader, FilterOption } from "components/issues"; diff --git a/web/components/issues/issue-layouts/filters/header/filters/state.tsx b/web/components/issues/issue-layouts/filters/header/filters/state.tsx index 5dde1d279..2c2cca53b 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/state.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/state.tsx @@ -1,5 +1,6 @@ -import React, { useState } from "react"; +import React, { useMemo, useState } from "react"; import { observer } from "mobx-react"; +import sortBy from "lodash/sortBy"; // components import { Loader, StateGroupIcon } from "@plane/ui"; import { FilterHeader, FilterOption } from "components/issues"; @@ -22,13 +23,18 @@ export const FilterState: React.FC = observer((props) => { const appliedFiltersCount = appliedFilters?.length ?? 0; - const filteredOptions = states?.filter((s) => s.name.toLowerCase().includes(searchQuery.toLowerCase())); + const sortedOptions = useMemo(() => { + const filteredOptions = (states ?? []).filter((s) => s.name.toLowerCase().includes(searchQuery.toLowerCase())); + + return sortBy(filteredOptions, [(s) => !(appliedFilters ?? []).includes(s.id), (s) => s.name.toLowerCase()]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [searchQuery]); const handleViewToggle = () => { - if (!filteredOptions) return; + if (!sortedOptions) return; - if (itemsToRender === filteredOptions.length) setItemsToRender(5); - else setItemsToRender(filteredOptions.length); + if (itemsToRender === sortedOptions.length) setItemsToRender(5); + else setItemsToRender(sortedOptions.length); }; return ( @@ -40,10 +46,10 @@ export const FilterState: React.FC = observer((props) => { /> {previewEnabled && (
- {filteredOptions ? ( - filteredOptions.length > 0 ? ( + {sortedOptions ? ( + sortedOptions.length > 0 ? ( <> - {filteredOptions.slice(0, itemsToRender).map((state) => ( + {sortedOptions.slice(0, itemsToRender).map((state) => ( = observer((props) => { title={state.name} /> ))} - {filteredOptions.length > 5 && ( + {sortedOptions.length > 5 && ( )} diff --git a/web/components/issues/issue-layouts/roots/cycle-layout-root.tsx b/web/components/issues/issue-layouts/roots/cycle-layout-root.tsx index 5f308fbd1..ce0a9943e 100644 --- a/web/components/issues/issue-layouts/roots/cycle-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/cycle-layout-root.tsx @@ -68,7 +68,7 @@ export const CycleLayoutRoot: React.FC = observer(() => { if (!workspaceSlug || !projectId || !cycleId) return; const newFilters: IIssueFilterOptions = {}; Object.keys(userFilters ?? {}).forEach((key) => { - newFilters[key as keyof IIssueFilterOptions] = null; + newFilters[key as keyof IIssueFilterOptions] = []; }); issuesFilter.updateFilters( workspaceSlug.toString(), diff --git a/web/components/issues/issue-layouts/roots/module-layout-root.tsx b/web/components/issues/issue-layouts/roots/module-layout-root.tsx index 0c6ba3b66..268a2c60c 100644 --- a/web/components/issues/issue-layouts/roots/module-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/module-layout-root.tsx @@ -59,7 +59,7 @@ export const ModuleLayoutRoot: React.FC = observer(() => { if (!workspaceSlug || !projectId || !moduleId) return; const newFilters: IIssueFilterOptions = {}; Object.keys(userFilters ?? {}).forEach((key) => { - newFilters[key as keyof IIssueFilterOptions] = null; + newFilters[key as keyof IIssueFilterOptions] = []; }); issuesFilter.updateFilters( workspaceSlug.toString(), From 94327b83112d9e5c22366aa2538839d6ac6e04bd Mon Sep 17 00:00:00 2001 From: Manish Gupta <59428681+mguptahub@users.noreply.github.com> Date: Fri, 8 Mar 2024 15:16:32 +0530 Subject: [PATCH 04/38] chore: feature build process optimization (#3907) * process changed to build tar and use for deployment * fixes --- .github/workflows/feature-deployment.yml | 186 +++++++++++++++++++---- 1 file changed, 155 insertions(+), 31 deletions(-) diff --git a/.github/workflows/feature-deployment.yml b/.github/workflows/feature-deployment.yml index 7b9f5ffcc..12549cff5 100644 --- a/.github/workflows/feature-deployment.yml +++ b/.github/workflows/feature-deployment.yml @@ -4,70 +4,194 @@ on: workflow_dispatch: inputs: web-build: - required: true + required: false + description: 'Build Web' type: boolean default: true space-build: - required: true + required: false + description: 'Build Space' type: boolean default: false +env: + BUILD_WEB: ${{ github.event.inputs.web-build }} + BUILD_SPACE: ${{ github.event.inputs.space-build }} + jobs: + setup-feature-build: + name: Feature Build Setup + runs-on: ubuntu-latest + steps: + - name: Checkout + run: | + echo "BUILD_WEB=$BUILD_WEB" + echo "BUILD_SPACE=$BUILD_SPACE" + outputs: + web-build: ${{ env.BUILD_WEB}} + space-build: ${{env.BUILD_SPACE}} + + feature-build-web: + if: ${{ needs.setup-feature-build.outputs.web-build == 'true' }} + needs: setup-feature-build + name: Feature Build Web + runs-on: ubuntu-latest + env: + AWS_ACCESS_KEY_ID: ${{ vars.FEATURE_PREVIEW_AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.FEATURE_PREVIEW_AWS_SECRET_ACCESS_KEY }} + AWS_BUCKET: ${{ vars.FEATURE_PREVIEW_AWS_BUCKET }} + NEXT_PUBLIC_API_URL: ${{ vars.FEATURE_PREVIEW_NEXT_PUBLIC_API_BASE_URL }} + steps: + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + - name: Install AWS cli + run: | + sudo apt-get update + sudo apt-get install -y python3-pip + pip3 install awscli + - name: Checkout + uses: actions/checkout@v4 + with: + path: feature-preview + - name: Install Dependencies + run: | + cd $GITHUB_WORKSPACE/feature-preview + yarn install + - name: Build Web + id: build-web + run: | + cd $GITHUB_WORKSPACE/feature-preview + yarn build --filter=web + cd $GITHUB_WORKSPACE + + TAR_NAME="web.tar.gz" + tar -czf $TAR_NAME ./feature-preview + + FILE_EXPIRY=$(date -u -d "+2 days" +"%Y-%m-%dT%H:%M:%SZ") + aws s3 cp $TAR_NAME s3://${{ env.AWS_BUCKET }}/${{github.sha}}/$TAR_NAME --expires $FILE_EXPIRY + + feature-build-space: + if: ${{ needs.setup-feature-build.outputs.space-build == 'true' }} + needs: setup-feature-build + name: Feature Build Space + runs-on: ubuntu-latest + env: + AWS_ACCESS_KEY_ID: ${{ vars.FEATURE_PREVIEW_AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.FEATURE_PREVIEW_AWS_SECRET_ACCESS_KEY }} + AWS_BUCKET: ${{ vars.FEATURE_PREVIEW_AWS_BUCKET }} + NEXT_PUBLIC_DEPLOY_WITH_NGINX: 1 + outputs: + do-build: ${{ needs.setup-feature-build.outputs.space-build }} + s3-url: ${{ steps.build-space.outputs.S3_PRESIGNED_URL }} + steps: + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + - name: Install AWS cli + run: | + sudo apt-get update + sudo apt-get install -y python3-pip + pip3 install awscli + - name: Checkout + uses: actions/checkout@v4 + with: + path: plane + - name: Install Dependencies + run: | + cd $GITHUB_WORKSPACE/plane + yarn install + - name: Build Space + id: build-space + run: | + cd $GITHUB_WORKSPACE/plane + yarn build --filter=space + cd $GITHUB_WORKSPACE + + TAR_NAME="space.tar.gz" + tar -czf $TAR_NAME ./plane + + FILE_EXPIRY=$(date -u -d "+2 days" +"%Y-%m-%dT%H:%M:%SZ") + aws s3 cp $TAR_NAME s3://${{ env.AWS_BUCKET }}/${{github.sha}}/$TAR_NAME --expires $FILE_EXPIRY + feature-deploy: + if: ${{ always() && (needs.setup-feature-build.outputs.web-build == 'true' || needs.setup-feature-build.outputs.space-build == 'true') }} + needs: [feature-build-web, feature-build-space] name: Feature Deploy runs-on: ubuntu-latest env: - KUBE_CONFIG_FILE: ${{ secrets.KUBE_CONFIG }} - BUILD_WEB: ${{ (github.event.inputs.web-build == '' && true) || github.event.inputs.web-build }} - BUILD_SPACE: ${{ (github.event.inputs.space-build == '' && false) || github.event.inputs.space-build }} - + AWS_ACCESS_KEY_ID: ${{ vars.FEATURE_PREVIEW_AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.FEATURE_PREVIEW_AWS_SECRET_ACCESS_KEY }} + AWS_BUCKET: ${{ vars.FEATURE_PREVIEW_AWS_BUCKET }} + KUBE_CONFIG_FILE: ${{ secrets.FEATURE_PREVIEW_KUBE_CONFIG }} steps: + - name: Install AWS cli + run: | + sudo apt-get update + sudo apt-get install -y python3-pip + pip3 install awscli - name: Tailscale uses: tailscale/github-action@v2 with: oauth-client-id: ${{ secrets.TAILSCALE_OAUTH_CLIENT_ID }} oauth-secret: ${{ secrets.TAILSCALE_OAUTH_SECRET }} tags: tag:ci - - name: Kubectl Setup run: | - curl -LO "https://dl.k8s.io/release/${{secrets.KUBE_VERSION}}/bin/linux/amd64/kubectl" + curl -LO "https://dl.k8s.io/release/${{ vars.FEATURE_PREVIEW_KUBE_VERSION }}/bin/linux/amd64/kubectl" chmod +x kubectl mkdir -p ~/.kube echo "$KUBE_CONFIG_FILE" > ~/.kube/config chmod 600 ~/.kube/config - - name: HELM Setup run: | curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 chmod 700 get_helm.sh ./get_helm.sh - - name: App Deploy run: | - helm --kube-insecure-skip-tls-verify repo add feature-preview ${{ secrets.FEATURE_PREVIEW_HELM_CHART_URL }} - GIT_BRANCH=${{ github.ref_name }} - APP_NAMESPACE=${{ secrets.FEATURE_PREVIEW_NAMESPACE }} + WEB_S3_URL="" + if [ ${{ env.BUILD_WEB }} == true ]; then + WEB_S3_URL=$(aws s3 presign s3://${{ vars.FEATURE_PREVIEW_AWS_BUCKET }}/${{github.sha}}/web.tar.gz --expires-in 3600) + fi - METADATA=$(helm install feature-preview/${{ secrets.FEATURE_PREVIEW_HELM_CHART_NAME }} \ - --kube-insecure-skip-tls-verify \ - --generate-name \ - --namespace $APP_NAMESPACE \ - --set shared_config.git_repo=${{github.server_url}}/${{ github.repository }}.git \ - --set shared_config.git_branch="$GIT_BRANCH" \ - --set web.enabled=${{ env.BUILD_WEB }} \ - --set space.enabled=${{ env.BUILD_SPACE }} \ - --output json \ - --timeout 1000s) + SPACE_S3_URL="" + if [ ${{ env.BUILD_SPACE }} == true ]; then + SPACE_S3_URL=$(aws s3 presign s3://${{ vars.FEATURE_PREVIEW_AWS_BUCKET }}/${{github.sha}}/space.tar.gz --expires-in 3600) + fi - APP_NAME=$(echo $METADATA | jq -r '.name') + if [ ${{ env.BUILD_WEB }} == true ] || [ ${{ env.BUILD_SPACE }} == true ]; then - INGRESS_HOSTNAME=$(kubectl get ingress -n feature-builds --insecure-skip-tls-verify \ - -o jsonpath='{.items[?(@.metadata.annotations.meta\.helm\.sh\/release-name=="'$APP_NAME'")]}' | \ - jq -r '.spec.rules[0].host') + helm --kube-insecure-skip-tls-verify repo add feature-preview ${{ vars.FEATURE_PREVIEW_HELM_CHART_URL }} - echo "****************************************" - echo "APP NAME ::: $APP_NAME" - echo "INGRESS HOSTNAME ::: $INGRESS_HOSTNAME" - echo "****************************************" + APP_NAMESPACE="${{ vars.FEATURE_PREVIEW_NAMESPACE }}" + DEPLOY_SCRIPT_URL="${{ vars.FEATURE_PREVIEW_DEPLOY_SCRIPT_URL }}" + + METADATA=$(helm --kube-insecure-skip-tls-verify install feature-preview/${{ vars.FEATURE_PREVIEW_HELM_CHART_NAME }} \ + --generate-name \ + --namespace $APP_NAMESPACE \ + --set ingress.primaryDomain=${{vars.FEATURE_PREVIEW_PRIMARY_DOMAIN || 'feature.plane.tools' }} \ + --set web.image=${{vars.FEATURE_PREVIEW_DOCKER_BASE}} \ + --set web.enabled=${{ env.BUILD_WEB || false }} \ + --set web.artifact_url=$WEB_S3_URL \ + --set space.image=${{vars.FEATURE_PREVIEW_DOCKER_BASE}} \ + --set space.enabled=${{ env.BUILD_SPACE || false }} \ + --set space.artifact_url=$SPACE_S3_URL \ + --set shared_config.deploy_script_url=$DEPLOY_SCRIPT_URL \ + --output json \ + --timeout 1000s) + + APP_NAME=$(echo $METADATA | jq -r '.name') + + INGRESS_HOSTNAME=$(kubectl get ingress -n feature-builds --insecure-skip-tls-verify \ + -o jsonpath='{.items[?(@.metadata.annotations.meta\.helm\.sh\/release-name=="'$APP_NAME'")]}' | \ + jq -r '.spec.rules[0].host') + + echo "****************************************" + echo "APP NAME ::: $APP_NAME" + echo "INGRESS HOSTNAME ::: $INGRESS_HOSTNAME" + echo "****************************************" + fi From 8cc372679c526c096b144af1b9a2b990b1e0db2b Mon Sep 17 00:00:00 2001 From: Manish Gupta Date: Fri, 8 Mar 2024 15:56:05 +0530 Subject: [PATCH 05/38] Web Build fixes --- .github/workflows/feature-deployment.yml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/feature-deployment.yml b/.github/workflows/feature-deployment.yml index 12549cff5..766f30514 100644 --- a/.github/workflows/feature-deployment.yml +++ b/.github/workflows/feature-deployment.yml @@ -40,7 +40,7 @@ jobs: AWS_ACCESS_KEY_ID: ${{ vars.FEATURE_PREVIEW_AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.FEATURE_PREVIEW_AWS_SECRET_ACCESS_KEY }} AWS_BUCKET: ${{ vars.FEATURE_PREVIEW_AWS_BUCKET }} - NEXT_PUBLIC_API_URL: ${{ vars.FEATURE_PREVIEW_NEXT_PUBLIC_API_BASE_URL }} + NEXT_PUBLIC_API_BASE_URL: ${{ vars.FEATURE_PREVIEW_NEXT_PUBLIC_API_BASE_URL }} steps: - name: Set up Node.js uses: actions/setup-node@v4 @@ -54,21 +54,21 @@ jobs: - name: Checkout uses: actions/checkout@v4 with: - path: feature-preview + path: plane - name: Install Dependencies run: | - cd $GITHUB_WORKSPACE/feature-preview + cd $GITHUB_WORKSPACE/plane yarn install - name: Build Web id: build-web run: | - cd $GITHUB_WORKSPACE/feature-preview + cd $GITHUB_WORKSPACE/plane yarn build --filter=web cd $GITHUB_WORKSPACE TAR_NAME="web.tar.gz" - tar -czf $TAR_NAME ./feature-preview - + tar -czf $TAR_NAME ./plane + FILE_EXPIRY=$(date -u -d "+2 days" +"%Y-%m-%dT%H:%M:%SZ") aws s3 cp $TAR_NAME s3://${{ env.AWS_BUCKET }}/${{github.sha}}/$TAR_NAME --expires $FILE_EXPIRY @@ -82,6 +82,7 @@ jobs: AWS_SECRET_ACCESS_KEY: ${{ secrets.FEATURE_PREVIEW_AWS_SECRET_ACCESS_KEY }} AWS_BUCKET: ${{ vars.FEATURE_PREVIEW_AWS_BUCKET }} NEXT_PUBLIC_DEPLOY_WITH_NGINX: 1 + NEXT_PUBLIC_API_BASE_URL: ${{ vars.FEATURE_PREVIEW_NEXT_PUBLIC_API_BASE_URL }} outputs: do-build: ${{ needs.setup-feature-build.outputs.space-build }} s3-url: ${{ steps.build-space.outputs.S3_PRESIGNED_URL }} From 6c6b7156bbb46fa5d2ebbd4e0d851b264d5a2cd3 Mon Sep 17 00:00:00 2001 From: Manish Gupta Date: Fri, 8 Mar 2024 16:00:18 +0530 Subject: [PATCH 06/38] helm variable update --- .github/workflows/feature-deployment.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/feature-deployment.yml b/.github/workflows/feature-deployment.yml index 766f30514..c5eec3cd3 100644 --- a/.github/workflows/feature-deployment.yml +++ b/.github/workflows/feature-deployment.yml @@ -182,6 +182,7 @@ jobs: --set space.enabled=${{ env.BUILD_SPACE || false }} \ --set space.artifact_url=$SPACE_S3_URL \ --set shared_config.deploy_script_url=$DEPLOY_SCRIPT_URL \ + --set shared_config.api_base_url=${{vars.FEATURE_PREVIEW_NEXT_PUBLIC_API_BASE_URL}} \ --output json \ --timeout 1000s) From cb78ccad1f9f0d9266dda5c7fc365a59d562a5d1 Mon Sep 17 00:00:00 2001 From: sriram veeraghanta Date: Fri, 8 Mar 2024 16:58:37 +0530 Subject: [PATCH 07/38] fix: 1click deployment fixes --- deploy/1-click/plane-app | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/deploy/1-click/plane-app b/deploy/1-click/plane-app index e6bd24b9e..ace0a0b79 100644 --- a/deploy/1-click/plane-app +++ b/deploy/1-click/plane-app @@ -494,13 +494,6 @@ function install() { update_env "CORS_ALLOWED_ORIGINS" "http://$MY_IP" update_config "INSTALLATION_DATE" "$(date '+%Y-%m-%d')" - - if command -v crontab &> /dev/null; then - sudo touch /etc/cron.daily/makeplane - sudo chmod +x /etc/cron.daily/makeplane - sudo echo "0 2 * * * root /usr/local/bin/plane-app --upgrade" > /etc/cron.daily/makeplane - sudo crontab /etc/cron.daily/makeplane - fi show_message "Plane Installed Successfully ✅" show_message "" @@ -606,11 +599,6 @@ function uninstall() { sudo rm $PLANE_INSTALL_DIR/variables-upgrade.env &> /dev/null sudo rm $PLANE_INSTALL_DIR/config.env &> /dev/null sudo rm $PLANE_INSTALL_DIR/docker-compose.yaml &> /dev/null - - if command -v crontab &> /dev/null; then - sudo crontab -r &> /dev/null - sudo rm /etc/cron.daily/makeplane &> /dev/null - fi # rm -rf $PLANE_INSTALL_DIR &> /dev/null show_message "- Configuration Cleaned ✅" From cead56cc526af19638141f9ed25d200c8209cf4e Mon Sep 17 00:00:00 2001 From: Ramesh Kumar Chandra <31303617+rameshkumarchandra@users.noreply.github.com> Date: Fri, 8 Mar 2024 17:32:00 +0530 Subject: [PATCH 08/38] [WEB - 671] chore: remove unused images in the app (#3901) --- .../empty-state/Project_full_screen.svg | 31 ---------- web/public/empty-state/analytics.svg | 21 ------- web/public/empty-state/dashboard.svg | 23 -------- web/public/empty-state/estimate.svg | 19 ------ web/public/empty-state/integration.svg | 15 ----- web/public/empty-state/issue-archive.svg | 3 - web/public/empty-state/my-issues.svg | 15 ----- web/public/empty-state/page.svg | 21 ------- web/public/onboarding/sign-in.webp | Bin 21740 -> 0 bytes web/public/theme-mode/custom-mode.svg | 54 ------------------ web/public/theme-mode/custom-theme-banner.svg | 33 ----------- web/public/theme-mode/dark-high-contrast.svg | 34 ----------- web/public/theme-mode/dark-mode.svg | 41 ------------- web/public/theme-mode/light-high-contrast.svg | 42 -------------- web/public/theme-mode/light-mode.svg | 42 -------------- web/public/web-view-spinner.png | Bin 1456 -> 0 bytes 16 files changed, 394 deletions(-) delete mode 100644 web/public/empty-state/Project_full_screen.svg delete mode 100644 web/public/empty-state/analytics.svg delete mode 100644 web/public/empty-state/dashboard.svg delete mode 100644 web/public/empty-state/estimate.svg delete mode 100644 web/public/empty-state/integration.svg delete mode 100644 web/public/empty-state/issue-archive.svg delete mode 100644 web/public/empty-state/my-issues.svg delete mode 100644 web/public/empty-state/page.svg delete mode 100644 web/public/onboarding/sign-in.webp delete mode 100644 web/public/theme-mode/custom-mode.svg delete mode 100644 web/public/theme-mode/custom-theme-banner.svg delete mode 100644 web/public/theme-mode/dark-high-contrast.svg delete mode 100644 web/public/theme-mode/dark-mode.svg delete mode 100644 web/public/theme-mode/light-high-contrast.svg delete mode 100644 web/public/theme-mode/light-mode.svg delete mode 100644 web/public/web-view-spinner.png diff --git a/web/public/empty-state/Project_full_screen.svg b/web/public/empty-state/Project_full_screen.svg deleted file mode 100644 index ce3738507..000000000 --- a/web/public/empty-state/Project_full_screen.svg +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/web/public/empty-state/analytics.svg b/web/public/empty-state/analytics.svg deleted file mode 100644 index 4f7cd4b7d..000000000 --- a/web/public/empty-state/analytics.svg +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/web/public/empty-state/dashboard.svg b/web/public/empty-state/dashboard.svg deleted file mode 100644 index 1e45c69ee..000000000 --- a/web/public/empty-state/dashboard.svg +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - diff --git a/web/public/empty-state/estimate.svg b/web/public/empty-state/estimate.svg deleted file mode 100644 index 604b2f143..000000000 --- a/web/public/empty-state/estimate.svg +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/web/public/empty-state/integration.svg b/web/public/empty-state/integration.svg deleted file mode 100644 index d4f8ec5e8..000000000 --- a/web/public/empty-state/integration.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/web/public/empty-state/issue-archive.svg b/web/public/empty-state/issue-archive.svg deleted file mode 100644 index eee79ff3a..000000000 --- a/web/public/empty-state/issue-archive.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/web/public/empty-state/my-issues.svg b/web/public/empty-state/my-issues.svg deleted file mode 100644 index 98a420d75..000000000 --- a/web/public/empty-state/my-issues.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/web/public/empty-state/page.svg b/web/public/empty-state/page.svg deleted file mode 100644 index 2b0e88fcf..000000000 --- a/web/public/empty-state/page.svg +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/web/public/onboarding/sign-in.webp b/web/public/onboarding/sign-in.webp deleted file mode 100644 index 0c7fb571c55d1f0ccbfed2af6554c8d0cbd9e958..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21740 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 diff --git a/web/public/theme-mode/custom-mode.svg b/web/public/theme-mode/custom-mode.svg deleted file mode 100644 index 01be18d5b..000000000 --- a/web/public/theme-mode/custom-mode.svg +++ /dev/null @@ -1,54 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/web/public/theme-mode/custom-theme-banner.svg b/web/public/theme-mode/custom-theme-banner.svg deleted file mode 100644 index b7434cd76..000000000 --- a/web/public/theme-mode/custom-theme-banner.svg +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/web/public/theme-mode/dark-high-contrast.svg b/web/public/theme-mode/dark-high-contrast.svg deleted file mode 100644 index e5839b286..000000000 --- a/web/public/theme-mode/dark-high-contrast.svg +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/web/public/theme-mode/dark-mode.svg b/web/public/theme-mode/dark-mode.svg deleted file mode 100644 index b8c14711c..000000000 --- a/web/public/theme-mode/dark-mode.svg +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/web/public/theme-mode/light-high-contrast.svg b/web/public/theme-mode/light-high-contrast.svg deleted file mode 100644 index c2f5ded22..000000000 --- a/web/public/theme-mode/light-high-contrast.svg +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/web/public/theme-mode/light-mode.svg b/web/public/theme-mode/light-mode.svg deleted file mode 100644 index 9c7fdae4b..000000000 --- a/web/public/theme-mode/light-mode.svg +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/web/public/web-view-spinner.png b/web/public/web-view-spinner.png deleted file mode 100644 index 527f307c29f4f3ce814f7768090612a65f402123..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1456 zcmV;h1yA~kP)q;0MLRchA~g_{S)1UAU?!6ZxcvuWGI##3+c;HKUDs#?A%a9Ko6Vkfbaa$+xm=Qc z5^HN~g{P;d?>J^e=ZGp8LX*(~P$vb#GV=(KQgMjbm+I>3sw6T`l%RoTRxY5VClzh8 z#`*O&HZ}~Axk3aJiG%^={^Po2s2E{_fLZB*B(5tZIMq{-kcoH|h5X1irOI`c*4NiJ znIMD-LPZlQCb%vMfzy&C9wg6SJJx848iM(JKCrX1Q=o=(iPe+TaL~Cf1*VqV7GGIe zsRUPrtOnoHo#W=`=i{@pv!`=&bA_e^ySuw9krwIc=_xlN>QErS1&rxj-0%1EH#D$* zjd}|U3o?jNa?fHNneGd+|F{xt03ur4-Q7)+y3_#`X&XD_#whfI8{}+~CeG9FJ(<-8 zMh-4vJp@RFpqBRj{!Y0y#NQBo-}vjgy?A?jJB3M<5}X;|KuawyE^d-pF{WsbDItxF zIoF}og6O3&5){&5+A#Hl7%84&^|038+uPf0N{|4>Xi*A)MyaHP8;4EPw2A{Lsxuc5 z$;ZdX5cwUchOWF*F&pXU=jZI<;bEPdG_7W!W(W(CA4z;a$mf=pmQ>~e7Smp;#snqr(;K=2#;8fY)ifMjuEVnS{^hQO3j+UOBoYc$Z zGWlbrn$ZUa2CPa2{g%qXV9+FTRPn&1^d@K(7=~e(P)(tS$A6MzeTv+))9f`SMn$1E zAZMU23Swa*M;&;G6#Hp%OOgBgd!?_h?>k!g?(U8rfQVURNHr*E6&D~tbs&{zImdm- zzcVnoaKTE2rv_3%OsW>BH#avjOjZ4IK$VSsTImbG1!Rn)f}q5Wi`yn=Fr|5MaiQWN zg1BtDFFQ+qk~cv&MGJ#iU8FkR=FEqO2iHi%*aUYTuxrdJ$mc**0Z~#^LsJl#HJgT~ zuUogbw@Lc4z;!9`KT83a_>}z&s#t%2zeXp+!@~vMZJ>VS>gr0k8WMi(q`&|M?gRf*B}o#lD}@PSVR}!ID%T(@l&C3kIX@-Hco9>WAcP3o>hUGMHaIw_ zGeKYvCqFf~uB51nLIj-!Celorc>t-1Lv;=X4AM^f2-<3qndl=@Vsv!WU> Date: Fri, 8 Mar 2024 17:36:28 +0530 Subject: [PATCH 09/38] [WEB-665] fix Padding in the project dropdown. (#3905) --- web/components/project/sidebar-list.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/web/components/project/sidebar-list.tsx b/web/components/project/sidebar-list.tsx index 2cee91e6b..5744ee331 100644 --- a/web/components/project/sidebar-list.tsx +++ b/web/components/project/sidebar-list.tsx @@ -116,9 +116,14 @@ export const ProjectSidebarList: FC = observer(() => { )}
From f5151ae717de6d96a3bd2b7c4d68e5dc8cc97506 Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Fri, 8 Mar 2024 17:37:01 +0530 Subject: [PATCH 10/38] [WEB-666] chore: rename `View profile` to `My activity`. (#3906) --- web/components/workspace/sidebar-dropdown.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web/components/workspace/sidebar-dropdown.tsx b/web/components/workspace/sidebar-dropdown.tsx index 5d1695b33..5d07ff0f9 100644 --- a/web/components/workspace/sidebar-dropdown.tsx +++ b/web/components/workspace/sidebar-dropdown.tsx @@ -24,8 +24,8 @@ const userLinks = (workspaceSlug: string, userId: string) => [ icon: Mails, }, { - key: "view_profile", - name: "View profile", + key: "my_activity", + name: "My activity", href: `/${workspaceSlug}/profile/${userId}`, icon: CircleUserRound, }, @@ -38,7 +38,7 @@ const userLinks = (workspaceSlug: string, userId: string) => [ ]; const profileLinks = (workspaceSlug: string, userId: string) => [ { - name: "View profile", + name: "My activity", icon: UserCircle2, link: `/${workspaceSlug}/profile/${userId}`, }, From 2074bb97dbed9a55a65275cab0708e00deed013d Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Fri, 8 Mar 2024 17:38:42 +0530 Subject: [PATCH 11/38] [WEB-440] feat: create project feature selection modal. (#3909) * [WEB-440] feat: create project feature selection modal. * [WEB-399] chore: explain project identifier. * chore: use `Link` component for redirection to project page. --- .../project/create-project-form.tsx | 392 +++++++++++++++++ .../project/create-project-modal.tsx | 399 ++---------------- web/components/project/index.ts | 2 + .../project/project-feature-update.tsx | 57 +++ .../project/settings/features-list.tsx | 69 +-- .../[projectId]/settings/features.tsx | 12 +- 6 files changed, 535 insertions(+), 396 deletions(-) create mode 100644 web/components/project/create-project-form.tsx create mode 100644 web/components/project/project-feature-update.tsx diff --git a/web/components/project/create-project-form.tsx b/web/components/project/create-project-form.tsx new file mode 100644 index 000000000..509cf310c --- /dev/null +++ b/web/components/project/create-project-form.tsx @@ -0,0 +1,392 @@ +import { useState, FC, ChangeEvent } from "react"; +import { observer } from "mobx-react-lite"; +import { useForm, Controller } from "react-hook-form"; +import { Info, X } from "lucide-react"; +// ui +import { + Button, + CustomEmojiIconPicker, + CustomSelect, + EmojiIconPickerTypes, + Input, + setToast, + TextArea, + TOAST_TYPE, + Tooltip, +} from "@plane/ui"; +// components +import { ImagePickerPopover } from "components/core"; +import { MemberDropdown } from "components/dropdowns"; +import { ProjectLogo } from "./project-logo"; +// constants +import { PROJECT_CREATED } from "constants/event-tracker"; +import { NETWORK_CHOICES, PROJECT_UNSPLASH_COVERS } from "constants/project"; +// helpers +import { convertHexEmojiToDecimal, getRandomEmoji } from "helpers/emoji.helper"; +import { cn } from "helpers/common.helper"; +import { projectIdentifierSanitizer } from "helpers/project.helper"; +// hooks +import { useEventTracker, useProject } from "hooks/store"; +// types +import { IProject } from "@plane/types"; + +type Props = { + setToFavorite?: boolean; + workspaceSlug: string; + onClose: () => void; + handleNextStep: (projectId: string) => void; +}; + +const defaultValues: Partial = { + cover_image: PROJECT_UNSPLASH_COVERS[Math.floor(Math.random() * PROJECT_UNSPLASH_COVERS.length)], + description: "", + logo_props: { + in_use: "emoji", + emoji: { + value: getRandomEmoji(), + }, + }, + identifier: "", + name: "", + network: 2, + project_lead: null, +}; + +export const CreateProjectForm: FC = observer((props) => { + const { setToFavorite, workspaceSlug, onClose, handleNextStep } = props; + // store + const { captureProjectEvent } = useEventTracker(); + const { addProjectToFavorites, createProject } = useProject(); + // states + const [isChangeInIdentifierRequired, setIsChangeInIdentifierRequired] = useState(true); + // form info + const { + formState: { errors, isSubmitting }, + handleSubmit, + reset, + control, + watch, + setValue, + } = useForm({ + defaultValues, + reValidateMode: "onChange", + }); + + const handleAddToFavorites = (projectId: string) => { + if (!workspaceSlug) return; + + addProjectToFavorites(workspaceSlug.toString(), projectId).catch(() => { + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: "Couldn't remove the project from favorites. Please try again.", + }); + }); + }; + + const onSubmit = async (formData: Partial) => { + // Upper case identifier + formData.identifier = formData.identifier?.toUpperCase(); + + return createProject(workspaceSlug.toString(), formData) + .then((res) => { + const newPayload = { + ...res, + state: "SUCCESS", + }; + captureProjectEvent({ + eventName: PROJECT_CREATED, + payload: newPayload, + }); + setToast({ + type: TOAST_TYPE.SUCCESS, + title: "Success!", + message: "Project created successfully.", + }); + if (setToFavorite) { + handleAddToFavorites(res.id); + } + handleNextStep(res.id); + }) + .catch((err) => { + Object.keys(err.data).map((key) => { + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: err.data[key], + }); + captureProjectEvent({ + eventName: PROJECT_CREATED, + payload: { + ...formData, + state: "FAILED", + }, + }); + }); + }); + }; + + const handleNameChange = (onChange: any) => (e: ChangeEvent) => { + if (!isChangeInIdentifierRequired) { + onChange(e); + return; + } + if (e.target.value === "") setValue("identifier", ""); + else setValue("identifier", projectIdentifierSanitizer(e.target.value).substring(0, 5)); + onChange(e); + }; + + const handleIdentifierChange = (onChange: any) => (e: ChangeEvent) => { + const { value } = e.target; + const alphanumericValue = projectIdentifierSanitizer(value); + setIsChangeInIdentifierRequired(false); + onChange(alphanumericValue); + }; + + const handleClose = () => { + onClose(); + setIsChangeInIdentifierRequired(true); + setTimeout(() => { + reset(); + }, 300); + }; + + return ( + <> +
+ {watch("cover_image") && ( + Cover image + )} + +
+ +
+
+ ( + + )} + /> +
+
+ ( + + + + } + onChange={(val: any) => { + let logoValue = {}; + + if (val.type === "emoji") + logoValue = { + value: convertHexEmojiToDecimal(val.value.unified), + url: val.value.imageUrl, + }; + else if (val.type === "icon") logoValue = val.value; + + onChange({ + in_use: val.type, + [val.type]: logoValue, + }); + }} + defaultIconColor={value.in_use === "icon" ? value.icon?.color : undefined} + defaultOpen={value.in_use === "emoji" ? EmojiIconPickerTypes.EMOJI : EmojiIconPickerTypes.ICON} + /> + )} + /> +
+
+
+
+
+
+ ( + + )} + /> + + <>{errors?.name?.message} + +
+
+ + /^[ÇŞĞIİÖÜA-Z0-9]+$/.test(value.toUpperCase()) || + "Only Alphanumeric & Non-latin characters are allowed.", + minLength: { + value: 1, + message: "Project ID must at least be of 1 character", + }, + maxLength: { + value: 5, + message: "Project ID must at most be of 5 characters", + }, + }} + render={({ field: { value, onChange } }) => ( + + )} + /> + + + + {errors?.identifier?.message} +
+
+ ( +