From e8daa4f92f5d116ffdc2dee08f4cc984bb7a7b95 Mon Sep 17 00:00:00 2001 From: Eunjae Lee Date: Fri, 29 Nov 2024 11:00:20 +0100 Subject: [PATCH] fix: bulk attribute assignment (#17896) remove comment --- apps/web/public/static/locales/en/common.json | 3 + .../UserTable/BulkActions/EventTypesList.tsx | 6 +- .../BulkActions/MassAssignAttributes.tsx | 394 ++++++++++-------- 3 files changed, 235 insertions(+), 168 deletions(-) diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 7a031c0be0..08b4923889 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -2823,5 +2823,8 @@ "exclude_emails_match_found_error_message": "Please enter a valid work email address", "disable_org_url_label": "Disable public organization profile and redirect", "disable_org_url_description": "Redirects {{orgSlug}}.cal.com to {{destination}}", + "single_select": "Single Select", + "multi_select": "Multi Select", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } + diff --git a/packages/features/users/components/UserTable/BulkActions/EventTypesList.tsx b/packages/features/users/components/UserTable/BulkActions/EventTypesList.tsx index 7760c207e1..1b634fc8af 100644 --- a/packages/features/users/components/UserTable/BulkActions/EventTypesList.tsx +++ b/packages/features/users/components/UserTable/BulkActions/EventTypesList.tsx @@ -1,6 +1,6 @@ import type { Table } from "@tanstack/react-table"; import type { Dispatch, SetStateAction } from "react"; -import { useState } from "react"; +import { useState, Fragment } from "react"; import classNames from "@calcom/lib/classNames"; import { useLocale } from "@calcom/lib/hooks/useLocale"; @@ -121,7 +121,7 @@ export function EventTypesList({ table, orgTeams }: Props) { selectedEvents.has(id) || (areAllUsersHostForTeam && !removeHostFromEvents.has(id)) ); return ( - <> + { @@ -200,7 +200,7 @@ export function EventTypesList({ table, orgTeams }: Props) { /> ); })} - + ); })} diff --git a/packages/features/users/components/UserTable/BulkActions/MassAssignAttributes.tsx b/packages/features/users/components/UserTable/BulkActions/MassAssignAttributes.tsx index e4c27bc42b..861c5ab977 100644 --- a/packages/features/users/components/UserTable/BulkActions/MassAssignAttributes.tsx +++ b/packages/features/users/components/UserTable/BulkActions/MassAssignAttributes.tsx @@ -1,11 +1,12 @@ import type { Table } from "@tanstack/react-table"; import type { ColumnFiltersState } from "@tanstack/react-table"; -import { parseAsString, useQueryState, parseAsArrayOf } from "nuqs"; -import { useState } from "react"; +import { createContext, useContext, useState, useMemo, type PropsWithChildren } from "react"; +import type { Dispatch, SetStateAction } from "react"; import classNames from "@calcom/lib/classNames"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import slugify from "@calcom/lib/slugify"; +import type { Attribute as _Attribute, AttributeOption } from "@calcom/prisma/client"; import { trpc } from "@calcom/trpc"; import { Alert, @@ -31,50 +32,78 @@ interface Props { filters: ColumnFiltersState; } -function useSelectedAttributes() { - const [selectedAttribute, setSelectedAttribute] = useQueryState("a", parseAsString); - const utils = trpc.useUtils(); - const attributeData = utils.viewer.attributes.list.getData(); - const foundAttribute = attributeData?.find((attr) => attr.id === selectedAttribute); +type Attribute = _Attribute & { options: AttributeOption[] }; - return { +type AttributesContextType = { + selectedAttribute: string | undefined; + setSelectedAttribute: Dispatch>; + foundAttributeInCache: Attribute | undefined; + + selectedAttributeOptions: string[]; + setSelectedAttributeOptions: Dispatch>; + + attributes: Attribute[] | undefined; +}; + +const AttributesContext = createContext(null); + +function AttributesProvider({ children }: PropsWithChildren) { + const { data: attributes } = trpc.viewer.attributes.list.useQuery(); + const [selectedAttribute, setSelectedAttribute] = useState(); + const [selectedAttributeOptions, setSelectedAttributeOptions] = useState([]); + + const foundAttributeInCache = useMemo( + () => attributes?.find((attr) => attr.id === selectedAttribute), + [selectedAttribute, attributes] + ); + + const value: AttributesContextType = { selectedAttribute, setSelectedAttribute, - foundAttributeInCache: foundAttribute, + selectedAttributeOptions, + setSelectedAttributeOptions, + foundAttributeInCache, + attributes, }; + + return {children}; } -function useSelectedAttributeOption() { - return useQueryState("ao", parseAsArrayOf(parseAsString).withDefault([])); +function useAttributes() { + const context = useContext(AttributesContext); + if (!context) { + throw new Error("useAttributes must be used within an AttributesProvider"); + } + return context; +} + +function getTranslateableStringFromType(type: string) { + switch (type) { + case "SINGLE_SELECT": + return "single_select"; + case "MULTI_SELECT": + return "multi_select"; + case "TEXT": + return "text"; + case "NUMBER": + return "number"; + default: + return undefined; + } } function SelectedAttributeToAssign() { const { t } = useLocale(); - const [selectedAttributeOption, setSelectedAttributeOption] = useSelectedAttributeOption(); - const { selectedAttribute, setSelectedAttribute } = useSelectedAttributes(); - const utils = trpc.useUtils(); - const attributeData = utils.viewer.attributes.list.getData(); - const foundAttribute = attributeData?.find((attr) => attr.id === selectedAttribute); + const { + foundAttributeInCache: foundAttribute, + selectedAttributeOptions, + setSelectedAttributeOptions, + } = useAttributes(); if (!foundAttribute) { - setSelectedAttribute(null); return null; } - function getTranslateableStringFromType(type: string) { - switch (type) { - case "SINGLE_SELECT": - return "single_select"; - case "MULTI_SELECT": - return "multi_select"; - case "TEXT": - return "text"; - case "NUMBER": - return "number"; - default: - return undefined; - } - } const translateableType = getTranslateableStringFromType(foundAttribute.type); const isSelectable = foundAttribute.type === "SINGLE_SELECT" || foundAttribute.type === "MULTI_SELECT"; @@ -95,11 +124,11 @@ function SelectedAttributeToAssign() { className="hover:cursor-pointer" onSelect={() => { if (foundAttribute.type === "SINGLE_SELECT") { - setSelectedAttributeOption([option.id]); + setSelectedAttributeOptions([option.id]); } else { - setSelectedAttributeOption((prev) => { + setSelectedAttributeOptions((prev: string[]) => { if (prev.includes(option.id)) { - return prev.filter((id) => id !== option.id); + return prev.filter((id: string) => id !== option.id); } return [...prev, option.id]; }); @@ -110,7 +139,7 @@ function SelectedAttributeToAssign() { className={classNames( "ml-auto flex h-4 w-4 items-center justify-center rounded-sm border" )}> - {selectedAttributeOption?.includes(option.id) ? ( + {selectedAttributeOptions?.includes(option.id) ? ( ) : null} @@ -122,12 +151,12 @@ function SelectedAttributeToAssign() { <> { // trigger onBlur so it's set as Apply is pressed (but not onChange) which triggers // a re-render which also loses focus. - setSelectedAttributeOption([e.target.value]); + setSelectedAttributeOptions([e.target.value]); }} /> @@ -138,12 +167,78 @@ function SelectedAttributeToAssign() { ); } -export function MassAssignAttributesBulkAction({ table, filters }: Props) { - const { selectedAttribute, setSelectedAttribute, foundAttributeInCache } = useSelectedAttributes(); - const [selectedAttributeOptions, setSelectedAttributeOptions] = useSelectedAttributeOption(); +function Content({ showMultiSelectWarning }: { showMultiSelectWarning: boolean }) { + const { t } = useLocale(); + const { + selectedAttribute, + setSelectedAttribute, + selectedAttributeOptions, + setSelectedAttributeOptions, + attributes, + foundAttributeInCache, + } = useAttributes(); + if (!selectedAttribute) { + return ( + <> + + + No attributes found + + {attributes && + attributes.map((option) => { + return ( + { + setSelectedAttribute(option.id); + setSelectedAttributeOptions([]); + }}> + {option.name} +
+ +
+
+ ); + })} +
+
+ + ); + } + + if (showMultiSelectWarning) { + return ( +
+ +
+ ); + } + + if (selectedAttribute) { + return ; + } + + return null; +} + +function MassAssignAttributesBulkActionComponent({ table, filters }: Props) { + const { + selectedAttribute, + setSelectedAttribute, + selectedAttributeOptions, + setSelectedAttributeOptions, + foundAttributeInCache, + } = useAttributes(); + const [showMultiSelectWarning, setShowMultiSelectWarning] = useState(false); const { t } = useLocale(); - const utils = trpc.useContext(); + const utils = trpc.useUtils(); const bulkAssignAttributes = trpc.viewer.attributes.bulkAssignAttributes.useMutation({ onSuccess: (success) => { // Optimistically update the infinite query data @@ -161,6 +256,13 @@ export function MassAssignAttributesBulkAction({ table, filters }: Props) { }, // @ts-expect-error i really dont know how to type this (oldData) => { + if (!oldData) { + return { + pages: [], + pageParams: [], + }; + } + const newPages = oldData?.pages.map((page) => ({ ...page, rows: page.rows.map((row) => { @@ -208,7 +310,7 @@ export function MassAssignAttributesBulkAction({ table, filters }: Props) { } ); - setSelectedAttribute(null); + setSelectedAttribute(undefined); setSelectedAttributeOptions([]); showToast(success.message, "success"); }, @@ -216,131 +318,93 @@ export function MassAssignAttributesBulkAction({ table, filters }: Props) { showToast(`Error assigning attributes: ${error.message}`, "error"); }, }); - const { data } = trpc.viewer.attributes.list.useQuery(); - - function Content() { - if (!selectedAttribute) { - return ( - <> - - - No attributes found - - {data && - data.map((option) => { - return ( - { - setSelectedAttribute(option.id); - }}> - {option.name} -
- -
-
- ); - })} -
-
- - ); - } - - if (showMultiSelectWarning) { - return ( -
- -
- ); - } - - if (selectedAttribute) { - return ; - } - - return null; - } return ( - <> - - - - - {/* We dont really use shadows much - but its needed here */} - - - - -
- {selectedAttribute ? ( - <> - - + + {/* We dont really use shadows much - but its needed here */} + + + + +
+ {selectedAttribute ? ( + <> + + - - ) : null} -
-
- - + setShowMultiSelectWarning(false); + let attributesToAssign; + if ( + foundAttributeInCache?.type === "MULTI_SELECT" || + foundAttributeInCache?.type === "SINGLE_SELECT" + ) { + attributesToAssign = [ + { + id: foundAttributeInCache.id, + options: selectedAttributeOptions.map((v) => ({ + value: v, + })), + }, + ]; + } else { + attributesToAssign = [ + { id: foundAttributeInCache.id, value: selectedAttributeOptions[0] }, + ]; + } + + bulkAssignAttributes.mutate({ + attributes: attributesToAssign, + userIds: table.getSelectedRowModel().rows.map((row) => row.original.id), + }); + } + }}> + {t("apply")} + + + ) : null} +
+
+
+ ); +} + +export function MassAssignAttributesBulkAction({ table, filters }: Props) { + return ( + + + ); }