From 5fc99c9ce559dd3411b069be3e86290d5a2b6a15 Mon Sep 17 00:00:00 2001 From: Akshita Goyal <36129505+gakshita@users.noreply.github.com> Date: Wed, 21 Aug 2024 13:20:22 +0530 Subject: [PATCH 1/2] [WEB-1986] fix: remove the user favourites when archived a particular entity (#5388) * chore: pages custom error codes * fix: project archive issue * fix: delete issue + dropdown z-index fix * fix: import issue --------- Co-authored-by: NarayanBavisetti --- .../dropdowns/member/member-options.tsx | 2 +- .../sidebar/favorites/favorite-folder.tsx | 13 +- .../sidebar/favorites/favorites-menu.tsx | 12 +- web/core/store/cycle.store.ts | 1 + web/core/store/favorite.store.ts | 128 +++++++----------- web/core/store/module.store.ts | 1 + web/core/store/pages/page.ts | 1 + web/core/store/project/project.store.ts | 1 + 8 files changed, 71 insertions(+), 88 deletions(-) diff --git a/web/core/components/dropdowns/member/member-options.tsx b/web/core/components/dropdowns/member/member-options.tsx index 8e7003f24a..a2d262130e 100644 --- a/web/core/components/dropdowns/member/member-options.tsx +++ b/web/core/components/dropdowns/member/member-options.tsx @@ -88,7 +88,7 @@ export const MemberOptions = observer((props: Props) => { return createPortal(
= (props) => { const { sidebarCollapsed: isSidebarCollapsed } = useAppTheme(); const { isMobile } = usePlatformOS(); - const { moveFavorite, getGroupedFavorites, favoriteMap, moveFavoriteFolder } = useFavorite(); + const { moveFavorite, getGroupedFavorites, groupedFavorites, moveFavoriteFolder } = useFavorite(); const { workspaceSlug } = useParams(); // states const [isMenuActive, setIsMenuActive] = useState(false); @@ -110,7 +111,9 @@ export const FavoriteFolder: React.FC = (props) => { const edge = extractClosestEdge(destinationData) || undefined; const payload = { id: favorite.id, - sequence: Math.round(getDestinationStateSequence(favoriteMap, destinationData.id as string, edge) || 0), + sequence: Math.round( + getDestinationStateSequence(groupedFavorites, destinationData.id as string, edge) || 0 + ), }; handleOnDropFolder(payload); @@ -146,7 +149,7 @@ export const FavoriteFolder: React.FC = (props) => { if (source.data.is_folder) return; if (sourceId === destinationId) return; if (!sourceId || !destinationId) return; - if (favoriteMap[sourceId].parent === destinationId) return; + if (groupedFavorites[sourceId].parent === destinationId) return; handleOnDrop(sourceId, destinationId); }, }) @@ -313,14 +316,14 @@ export const FavoriteFolder: React.FC = (props) => { "px-2": !isSidebarCollapsed, })} > - {favorite.children.map((child) => ( + {uniqBy(favorite.children, "id").map((child) => ( ))} diff --git a/web/core/components/workspace/sidebar/favorites/favorites-menu.tsx b/web/core/components/workspace/sidebar/favorites/favorites-menu.tsx index 17380b079c..f7b620f7a4 100644 --- a/web/core/components/workspace/sidebar/favorites/favorites-menu.tsx +++ b/web/core/components/workspace/sidebar/favorites/favorites-menu.tsx @@ -3,7 +3,7 @@ import React, { useEffect, useRef, useState } from "react"; import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine"; import { dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; -import { orderBy, uniqBy } from "lodash"; +import orderBy from "lodash/orderBy"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; import { ChevronRight, FolderPlus } from "lucide-react"; @@ -33,7 +33,7 @@ export const SidebarFavoritesMenu = observer(() => { // store hooks const { sidebarCollapsed } = useAppTheme(); - const { favoriteIds, favoriteMap, deleteFavorite, removeFromFavoriteFolder } = useFavorite(); + const { favoriteIds, groupedFavorites, deleteFavorite, removeFromFavoriteFolder } = useFavorite(); const { workspaceSlug } = useParams(); const { isMobile } = usePlatformOS(); @@ -108,7 +108,7 @@ export const SidebarFavoritesMenu = observer(() => { setIsDragging(false); const sourceId = source?.data?.id as string | undefined; console.log({ sourceId }); - if (!sourceId || !favoriteMap[sourceId].parent) return; + if (!sourceId || !groupedFavorites[sourceId].parent) return; handleRemoveFromFavoritesFolder(sourceId); }, }) @@ -170,14 +170,14 @@ export const SidebarFavoritesMenu = observer(() => { static > {createNewFolder && } - {Object.keys(favoriteMap).length === 0 ? ( + {Object.keys(groupedFavorites).length === 0 ? ( <> {!sidebarCollapsed && ( No favorites yet )} ) : ( - uniqBy(orderBy(Object.values(favoriteMap), "sequence", "desc"), "id") + orderBy(Object.values(groupedFavorites), "sequence", "desc") .filter((fav) => !fav.parent) .map((fav, index) => ( { favorite={fav} handleRemoveFromFavorites={handleRemoveFromFavorites} handleRemoveFromFavoritesFolder={handleRemoveFromFavoritesFolder} - favoriteMap={favoriteMap} + favoriteMap={groupedFavorites} /> )} diff --git a/web/core/store/cycle.store.ts b/web/core/store/cycle.store.ts index 2845527c23..b9a1f14764 100644 --- a/web/core/store/cycle.store.ts +++ b/web/core/store/cycle.store.ts @@ -624,6 +624,7 @@ export class CycleStore implements ICycleStore { .then((response) => { runInAction(() => { set(this.cycleMap, [cycleId, "archived_at"], response.archived_at); + if (this.rootStore.favorite.entityMap[cycleId]) this.rootStore.favorite.removeFavoriteFromStore(cycleId); }); }) .catch((error) => { diff --git a/web/core/store/favorite.store.ts b/web/core/store/favorite.store.ts index 30917afcb2..9d04ed1303 100644 --- a/web/core/store/favorite.store.ts +++ b/web/core/store/favorite.store.ts @@ -18,6 +18,7 @@ export interface IFavoriteStore { }; // computed actions existingFolders: string[]; + groupedFavorites: { [favoriteId: string]: IFavorite }; // actions fetchFavorite: (workspaceSlug: string) => Promise; // CRUD actions @@ -57,6 +58,7 @@ export class FavoriteStore implements IFavoriteStore { favoriteIds: observable, //computed existingFolders: computed, + groupedFavorites: computed, // action fetchFavorite: action, // CRUD actions @@ -80,6 +82,23 @@ export class FavoriteStore implements IFavoriteStore { return Object.values(this.favoriteMap).map((fav) => fav.name); } + get groupedFavorites() { + const data: { [favoriteId: string]: IFavorite } = JSON.parse(JSON.stringify(this.favoriteMap)); + + Object.values(data).forEach((fav) => { + if (fav.parent && data[fav.parent]) { + if (data[fav.parent].children) { + if (!data[fav.parent].children.some((f) => f.id === fav.id)) { + data[fav.parent].children.push(fav); + } + } else { + data[fav.parent].children = [fav]; + } + } + }); + return data; + } + /** * Creates a favorite in the workspace and adds it to the store * @param workspaceSlug @@ -151,22 +170,8 @@ export class FavoriteStore implements IFavoriteStore { */ moveFavorite = async (workspaceSlug: string, favoriteId: string, data: Partial) => { const oldParent = this.favoriteMap[favoriteId].parent; - const favorite = this.favoriteMap[favoriteId]; try { runInAction(() => { - // add the favorite to the new parent - if (!data.parent) return; - set(this.favoriteMap, [data.parent, "children"], [favorite, ...this.favoriteMap[data.parent].children]); - - // remove the favorite from the old parent - if (oldParent) { - set( - this.favoriteMap, - [oldParent, "children"], - this.favoriteMap[oldParent].children.filter((child) => child.id !== favoriteId) - ); - } - // add parent of the favorite set(this.favoriteMap, [favoriteId, "parent"], data.parent); }); @@ -177,21 +182,6 @@ export class FavoriteStore implements IFavoriteStore { // revert the changes runInAction(() => { if (!data.parent) return; - // remove the favorite from the new parent - set( - this.favoriteMap, - [data.parent, "children"], - this.favoriteMap[data.parent].children.filter((child) => child.id !== favoriteId) - ); - - // add the favorite back to the old parent - if (oldParent) { - set( - this.favoriteMap, - [oldParent, "children"], - [...this.favoriteMap[oldParent].children, this.favoriteMap[favoriteId]] - ); - } // revert the parent set(this.favoriteMap, [favoriteId, "parent"], oldParent); @@ -223,28 +213,13 @@ export class FavoriteStore implements IFavoriteStore { runInAction(() => { //remove parent set(this.favoriteMap, [favoriteId, "parent"], null); - - //remove children from parent - if (parent) { - set( - this.favoriteMap, - [parent, "children"], - this.favoriteMap[parent].children.filter((child) => child.id !== favoriteId) - ); - } }); await this.favoriteService.updateFavorite(workspaceSlug, favoriteId, data); } catch (error) { console.error("Failed to move favorite"); runInAction(() => { set(this.favoriteMap, [favoriteId, "parent"], parent); - if (parent) { - set( - this.favoriteMap, - [parent, "children"], - [...this.favoriteMap[parent].children, this.favoriteMap[favoriteId]] - ); - } + throw error; }); throw error; @@ -254,15 +229,26 @@ export class FavoriteStore implements IFavoriteStore { removeFavoriteEntityFromStore = (entity_identifier: string, entity_type: string) => { switch (entity_type) { case "view": - return (this.viewStore.viewMap[entity_identifier].is_favorite = false); + return ( + this.viewStore.viewMap[entity_identifier] && (this.viewStore.viewMap[entity_identifier].is_favorite = false) + ); case "module": - return (this.moduleStore.moduleMap[entity_identifier].is_favorite = false); + return ( + this.moduleStore.moduleMap[entity_identifier] && + (this.moduleStore.moduleMap[entity_identifier].is_favorite = false) + ); case "page": - return (this.pageStore.data[entity_identifier].is_favorite = false); + return this.pageStore.data[entity_identifier] && (this.pageStore.data[entity_identifier].is_favorite = false); case "cycle": - return (this.cycleStore.cycleMap[entity_identifier].is_favorite = false); + return ( + this.cycleStore.cycleMap[entity_identifier] && + (this.cycleStore.cycleMap[entity_identifier].is_favorite = false) + ); case "project": - return (this.projectStore.projectMap[entity_identifier].is_favorite = false); + return ( + this.projectStore.projectMap[entity_identifier] && + (this.projectStore.projectMap[entity_identifier].is_favorite = false) + ); default: return; } @@ -276,29 +262,21 @@ export class FavoriteStore implements IFavoriteStore { */ deleteFavorite = async (workspaceSlug: string, favoriteId: string) => { const parent = this.favoriteMap[favoriteId].parent; - const children = this.favoriteMap[favoriteId].children; + const children = this.groupedFavorites[favoriteId].children; const entity_identifier = this.favoriteMap[favoriteId].entity_identifier; const initialState = this.favoriteMap[favoriteId]; try { + await this.favoriteService.deleteFavorite(workspaceSlug, favoriteId); runInAction(() => { - if (parent) { - set( - this.favoriteMap, - [parent, "children"], - this.favoriteMap[parent].children.filter((child) => child.id !== favoriteId) - ); - } delete this.favoriteMap[favoriteId]; entity_identifier && delete this.entityMap[entity_identifier]; this.favoriteIds = this.favoriteIds.filter((id) => id !== favoriteId); }); - await this.favoriteService.deleteFavorite(workspaceSlug, favoriteId); runInAction(() => { entity_identifier && this.removeFavoriteEntityFromStore(entity_identifier, initialState.entity_type); if (children) { children.forEach((child) => { - console.log(child.entity_type); if (!child.entity_identifier) return; this.removeFavoriteEntityFromStore(child.entity_identifier, child.entity_type); }); @@ -326,9 +304,6 @@ export class FavoriteStore implements IFavoriteStore { const initialState = this.entityMap[entityId]; try { const favoriteId = this.entityMap[entityId].id; - runInAction(() => { - delete this.entityMap[entityId]; - }); await this.deleteFavorite(workspaceSlug, favoriteId); } catch (error) { console.error("Failed to remove favorite entity from favorite store", error); @@ -341,19 +316,22 @@ export class FavoriteStore implements IFavoriteStore { removeFavoriteFromStore = (entity_identifier: string) => { try { - const favoriteId = this.entityMap[entity_identifier].id; - const favorite = this.favoriteMap[favoriteId]; - const parent = favorite.parent; - + const favoriteId = this.entityMap[entity_identifier]?.id; + const oldData = this.favoriteMap[favoriteId]; + const projectData = Object.values(this.favoriteMap).filter( + (fav) => fav.project_id === entity_identifier && fav.entity_type !== "project" + ); runInAction(() => { - if (parent) { - set( - this.favoriteMap, - [parent, "children"], - this.favoriteMap[parent].children.filter((child) => child.id !== favoriteId) - ); - } + projectData && + projectData.forEach(async (fav) => { + this.removeFavoriteFromStore(fav.entity_identifier!); + this.removeFavoriteEntityFromStore(fav.entity_identifier!, fav.entity_type); + }); + + if (!favoriteId) return; delete this.favoriteMap[favoriteId]; + this.removeFavoriteEntityFromStore(entity_identifier!, oldData.entity_type); + delete this.entityMap[entity_identifier]; this.favoriteIds = this.favoriteIds.filter((id) => id !== favoriteId); }); @@ -373,8 +351,6 @@ export class FavoriteStore implements IFavoriteStore { try { const response = await this.favoriteService.getGroupedFavorites(workspaceSlug, favoriteId); runInAction(() => { - // add children to the favorite - set(this.favoriteMap, [favoriteId, "children"], response); // add the favorites to the map response.forEach((favorite) => { set(this.favoriteMap, [favorite.id], favorite); diff --git a/web/core/store/module.store.ts b/web/core/store/module.store.ts index 23e5cb0127..c3401dfbe9 100644 --- a/web/core/store/module.store.ts +++ b/web/core/store/module.store.ts @@ -557,6 +557,7 @@ export class ModulesStore implements IModuleStore { .then((response) => { runInAction(() => { set(this.moduleMap, [moduleId, "archived_at"], response.archived_at); + if (this.rootStore.favorite.entityMap[moduleId]) this.rootStore.favorite.removeFavoriteFromStore(moduleId); }); }) .catch((error) => { diff --git a/web/core/store/pages/page.ts b/web/core/store/pages/page.ts index 507259de75..4709691265 100644 --- a/web/core/store/pages/page.ts +++ b/web/core/store/pages/page.ts @@ -443,6 +443,7 @@ export class Page implements IPage { runInAction(() => { this.archived_at = response.archived_at; }); + if (this.rootStore.favorite.entityMap[this.id]) this.rootStore.favorite.removeFavoriteFromStore(this.id); }; /** diff --git a/web/core/store/project/project.store.ts b/web/core/store/project/project.store.ts index 6e1751f71c..ef817dab12 100644 --- a/web/core/store/project/project.store.ts +++ b/web/core/store/project/project.store.ts @@ -418,6 +418,7 @@ export class ProjectStore implements IProjectStore { .then((response) => { runInAction(() => { set(this.projectMap, [projectId, "archived_at"], response.archived_at); + this.rootStore.favorite.removeFavoriteFromStore(projectId); }); }) .catch((error) => { From fbcf15737e48ca04cc735551a2df752fce3e5fcf Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Wed, 21 Aug 2024 14:00:29 +0530 Subject: [PATCH 2/2] [WEB-1185] Issue types updates (#887) * chore: admin role validation for issue types settings. * chore: reset `is_required`, if property type is switched to `read_only`. * chore: trim option name and fix cursor behaviour in options. * chore: fix issue property description info icon. * fix: property title overflow. * chore: issue properties UI and UX copy updates. * chore: remove option to disable the property if it's already disabled while deleting. * chore: update lodash import. --- .../[projectId]/settings/issue-types/page.tsx | 10 ++-- .../properties/attributes/options/option.tsx | 12 +++-- .../properties/attributes/text.tsx | 3 ++ .../properties/delete-confirmation-modal.tsx | 27 +++++----- .../properties/dropdowns/property-title.tsx | 6 +-- .../properties/property-list-item.tsx | 1 + .../issue-types/properties/quick-actions.tsx | 3 ++ .../issue-properties-activity/boolean.tsx | 5 +- .../issue-properties-activity/date.tsx | 16 +++--- .../issue-properties-activity/dropdown.tsx | 17 ++++--- .../issue-properties-activity/member.tsx | 12 +++-- .../issue-properties-activity/number.tsx | 18 ++++--- .../issue-properties-activity/text.tsx | 11 ++-- .../issues/worklog/activity/root.tsx | 50 +++++++++---------- web/ee/constants/project/settings/tabs.ts | 2 +- 15 files changed, 115 insertions(+), 78 deletions(-) diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/issue-types/page.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/issue-types/page.tsx index ec8fd3236a..860c562378 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/issue-types/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/issue-types/page.tsx @@ -2,26 +2,30 @@ import { observer } from "mobx-react"; // components +import { NotAuthorizedView } from "@/components/auth-screens"; import { PageHead } from "@/components/core"; // hooks -import { EUserProjectRoles } from "@/constants/project"; import { useProject, useUser } from "@/hooks/store"; import { IssueTypesRoot } from "@/plane-web/components/issue-types"; const IssueTypesSettingsPage = observer(() => { // store hooks const { + canPerformProjectAdminActions, membership: { currentProjectRole }, } = useUser(); const { currentProjectDetails } = useProject(); // derived values - const isAdmin = currentProjectRole === EUserProjectRoles.ADMIN; const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Issue Types` : undefined; + if (currentProjectRole && !canPerformProjectAdminActions) { + return ; + } + return ( <> -
+
diff --git a/web/ee/components/issue-types/properties/attributes/options/option.tsx b/web/ee/components/issue-types/properties/attributes/options/option.tsx index 49a36f3cb6..52f7473ef6 100644 --- a/web/ee/components/issue-types/properties/attributes/options/option.tsx +++ b/web/ee/components/issue-types/properties/attributes/options/option.tsx @@ -1,4 +1,5 @@ import { FC, useEffect, useState } from "react"; +import isEqual from "lodash/isEqual"; import { observer } from "mobx-react"; import { Info } from "lucide-react"; // ui @@ -45,12 +46,17 @@ export const IssuePropertyOptionItem: FC = observer((p // handle create/ update operation const handleCreateUpdate = async () => { + // return if no change in data + if (isEqual(propertyOptionCreateData.name, optionData.name)) return; + // trim option name + const optionDataToUpdate = { ...optionData, name: optionData.name?.trim() }; + setOptionData(optionDataToUpdate); // return if option name is same as previous or empty - if (!optionData.name) return; + if (!optionDataToUpdate.name) return; // check for duplicate option name - if (checkForDuplicate({ identifier: optionData.id ?? key, value: optionData.name })) return; + if (checkForDuplicate({ identifier: optionDataToUpdate.id ?? key, value: optionDataToUpdate.name })) return; // handle option data update - updateOptionData({ key, ...optionData }); + updateOptionData({ key, ...optionDataToUpdate }); }; // handle changes in option local data diff --git a/web/ee/components/issue-types/properties/attributes/text.tsx b/web/ee/components/issue-types/properties/attributes/text.tsx index 432c65b3bb..fce7eb1f4e 100644 --- a/web/ee/components/issue-types/properties/attributes/text.tsx +++ b/web/ee/components/issue-types/properties/attributes/text.tsx @@ -39,6 +39,9 @@ export const TextAttributes = observer((props: TTextAttributesProps) => { onChange={(value) => { onTextDetailChange("settings", value as TIssueProperty["settings"]); onTextDetailChange("default_value", []); + if (value?.display_format === "readonly") { + onTextDetailChange("is_required", false); + } }} isDisabled={!configurations.allowedEditingModes.includes(currentOperationMode) && isAnyIssueAttached} /> diff --git a/web/ee/components/issue-types/properties/delete-confirmation-modal.tsx b/web/ee/components/issue-types/properties/delete-confirmation-modal.tsx index 30570b0acc..d9f9ede8bf 100644 --- a/web/ee/components/issue-types/properties/delete-confirmation-modal.tsx +++ b/web/ee/components/issue-types/properties/delete-confirmation-modal.tsx @@ -8,13 +8,14 @@ import { cn } from "@/helpers/common.helper"; type TProps = { isOpen: boolean; + isDisabledAlready: boolean; onClose: () => void; onDisable: () => Promise; onDelete: () => Promise; }; export const DeleteConfirmationModal: React.FC = observer((props) => { - const { isOpen, onClose, onDisable, onDelete } = props; + const { isOpen, isDisabledAlready, onClose, onDisable, onDelete } = props; // states const [isSubmitting, setIsSubmitting] = useState(false); @@ -50,7 +51,7 @@ export const DeleteConfirmationModal: React.FC = observer((props) => {

Delete this property

Deletion of properties may lead to loss of existing data.

-

Do you want to disable the property instead?

+ {!isDisabledAlready &&

Do you want to disable the property instead?

}
@@ -59,15 +60,17 @@ export const DeleteConfirmationModal: React.FC = observer((props) => { Cancel
- + {!isDisabledAlready && ( + + )}
diff --git a/web/ee/components/issue-types/properties/dropdowns/property-title.tsx b/web/ee/components/issue-types/properties/dropdowns/property-title.tsx index b26c86e7de..aae6888ac9 100644 --- a/web/ee/components/issue-types/properties/dropdowns/property-title.tsx +++ b/web/ee/components/issue-types/properties/dropdowns/property-title.tsx @@ -39,7 +39,7 @@ export const PropertyTitleDropdown = observer((props: TPropertyTitleDropdownProp {propertyDetail.display_name ?? ""} {propertyDetail.description && ( - + )} @@ -58,8 +58,8 @@ export const PropertyTitleDropdown = observer((props: TPropertyTitleDropdownProp )} ref={setReferenceElement} > - {propertyDetail.display_name ?? ""} -