From 57368c332fd7fea25f6cf5ed1c81df6c1ffcb40d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Heli=20H=C3=A5rd=20K=C3=A4lloff?= Date: Wed, 16 Oct 2024 16:22:41 +0200 Subject: [PATCH 01/18] change event state from published to draft when date is cleared in date picker --- src/features/events/components/EventActionButtons.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/features/events/components/EventActionButtons.tsx b/src/features/events/components/EventActionButtons.tsx index 4d5d961b3..2563010dd 100644 --- a/src/features/events/components/EventActionButtons.tsx +++ b/src/features/events/components/EventActionButtons.tsx @@ -49,9 +49,7 @@ const EventActionButtons: React.FunctionComponent = ({ const newDateIsDifferent = date && date != dayjs(event.published).format('YYYY-MM-DD'); - if (newDateIsDifferent) { - setPublished(date); - } + date && newDateIsDifferent ? setPublished(date) : setPublished(null); }; const handleDelete = () => { From a5a9a67cef496aefe6b4b20bac51bbef2bc3ef05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Heli=20H=C3=A5rd=20K=C3=A4lloff?= Date: Wed, 16 Oct 2024 20:21:10 +0200 Subject: [PATCH 02/18] fix my own bug --- src/features/events/components/EventActionButtons.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/features/events/components/EventActionButtons.tsx b/src/features/events/components/EventActionButtons.tsx index 2563010dd..178f46fee 100644 --- a/src/features/events/components/EventActionButtons.tsx +++ b/src/features/events/components/EventActionButtons.tsx @@ -49,7 +49,13 @@ const EventActionButtons: React.FunctionComponent = ({ const newDateIsDifferent = date && date != dayjs(event.published).format('YYYY-MM-DD'); - date && newDateIsDifferent ? setPublished(date) : setPublished(null); + if (newDateIsDifferent) { + setPublished(date); + } + + if (!date) { + setPublished(null); + } }; const handleDelete = () => { From 8103817f660730a6dcf6d60b8f3af6a7fa29c333 Mon Sep 17 00:00:00 2001 From: Richard Olsson Date: Sat, 26 Oct 2024 12:01:30 +0200 Subject: [PATCH 03/18] Rename ConfigureModal to PotentialDuplicateModal --- src/features/duplicates/components/DuplicateCard.tsx | 4 ++-- .../{ConfigureModal.tsx => PotentialDuplicateModal.tsx} | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) rename src/features/duplicates/components/{ConfigureModal.tsx => PotentialDuplicateModal.tsx} (97%) diff --git a/src/features/duplicates/components/DuplicateCard.tsx b/src/features/duplicates/components/DuplicateCard.tsx index f4ce2eccb..a5d91d5a5 100644 --- a/src/features/duplicates/components/DuplicateCard.tsx +++ b/src/features/duplicates/components/DuplicateCard.tsx @@ -2,7 +2,7 @@ import { Box, Button, Paper, Typography } from '@mui/material'; import { FC, useContext, useState } from 'react'; import theme from 'theme'; -import ConfigureModal from './ConfigureModal'; +import PotentialDuplicateModal from './PotentialDuplicateModal'; import messageIds from '../l10n/messageIds'; import { PotentialDuplicate } from '../store'; import useDuplicatesMutations from '../hooks/useDuplicatesMutations'; @@ -64,7 +64,7 @@ const DuplicateCard: FC = ({ cluster }) => { - setOpenModal(false)} open={openModal} potentialDuplicate={cluster} diff --git a/src/features/duplicates/components/ConfigureModal.tsx b/src/features/duplicates/components/PotentialDuplicateModal.tsx similarity index 97% rename from src/features/duplicates/components/ConfigureModal.tsx rename to src/features/duplicates/components/PotentialDuplicateModal.tsx index 37c787580..0255f4b98 100644 --- a/src/features/duplicates/components/ConfigureModal.tsx +++ b/src/features/duplicates/components/PotentialDuplicateModal.tsx @@ -22,13 +22,13 @@ import { useMessages } from 'core/i18n'; import { useNumericRouteParams } from 'core/hooks'; import { ZetkinPerson } from 'utils/types/zetkin'; -interface ConfigureModalProps { +interface Props { potentialDuplicate: PotentialDuplicate; onClose: () => void; open: boolean; } -const ConfigureModal: FC = ({ +const PotentialDuplicateModal: FC = ({ potentialDuplicate, open, onClose, @@ -123,4 +123,4 @@ const ConfigureModal: FC = ({ ); }; -export default ConfigureModal; +export default PotentialDuplicateModal; From c3e4dc11e49894fc3d859d70b7d81e9425e24422 Mon Sep 17 00:00:00 2001 From: Alexandra Koch Date: Sat, 26 Oct 2024 12:15:01 +0200 Subject: [PATCH 04/18] Enable deleting call assignments --- .../hooks/useCallAssignment.ts | 10 +++++++ .../callAssignments/l10n/messageIds.ts | 2 ++ .../layout/CallAssignmentLayout.tsx | 30 ++++++++++++++++++- src/features/callAssignments/store.ts | 8 +++++ 4 files changed, 49 insertions(+), 1 deletion(-) diff --git a/src/features/callAssignments/hooks/useCallAssignment.ts b/src/features/callAssignments/hooks/useCallAssignment.ts index 5912db042..594dda848 100644 --- a/src/features/callAssignments/hooks/useCallAssignment.ts +++ b/src/features/callAssignments/hooks/useCallAssignment.ts @@ -4,6 +4,7 @@ import { CallAssignmentData } from '../apiTypes'; import { futureToObject } from 'core/caching/futures'; import { loadItemIfNecessary } from 'core/caching/cacheUtils'; import { + callAssignmentDeleted, callAssignmentLoad, callAssignmentLoaded, callAssignmentUpdate, @@ -23,6 +24,7 @@ interface UseCallAssignmentReturn { updateTargets: (query: Partial) => void; start: () => void; updateCallAssignment: (data: Partial) => void; + deleteAssignment: () => void; } export default function useCallAssignment( @@ -186,8 +188,16 @@ export default function useCallAssignment( }); }; + const deleteAssignment = async () => { + await apiClient.delete( + `/api/orgs/${orgId}/call_assignments/${assignmentId}` + ); + dispatch(callAssignmentDeleted(assignmentId)); + }; + return { ...futureToObject(callAssignmentFuture), + deleteAssignment, end, isTargeted, start, diff --git a/src/features/callAssignments/l10n/messageIds.ts b/src/features/callAssignments/l10n/messageIds.ts index 91d02cab4..d9eacc682 100644 --- a/src/features/callAssignments/l10n/messageIds.ts +++ b/src/features/callAssignments/l10n/messageIds.ts @@ -4,8 +4,10 @@ import { m, makeMessages } from 'core/i18n'; export default makeMessages('feat.callAssignments', { actions: { + delete: m('Delete'), end: m('End assignment'), start: m('Start assignment'), + warning: m<{ eventTitle: string }>('"{eventTitle}" will be deleted.'), }, blocked: { callBackLater: m('Asked us to call back later'), diff --git a/src/features/callAssignments/layout/CallAssignmentLayout.tsx b/src/features/callAssignments/layout/CallAssignmentLayout.tsx index 12f91b256..3e4b84862 100644 --- a/src/features/callAssignments/layout/CallAssignmentLayout.tsx +++ b/src/features/callAssignments/layout/CallAssignmentLayout.tsx @@ -1,5 +1,7 @@ +import React, { useContext } from 'react'; import { Box, Button, Typography } from '@mui/material'; -import { Headset, People } from '@mui/icons-material'; +import { Delete, Headset, People } from '@mui/icons-material'; +import { useRouter } from 'next/router'; import CallAssignmentStatusChip from '../components/CallAssignmentStatusChip'; import getCallAssignmentUrl from '../utils/getCallAssignmentUrl'; @@ -16,6 +18,7 @@ import { Msg, useMessages } from 'core/i18n'; import useCallAssignmentState, { CallAssignmentState, } from '../hooks/useCallAssignmentState'; +import { ZUIConfirmDialogContext } from 'zui/ZUIConfirmDialogProvider'; interface CallAssignmentLayoutProps { children: React.ReactNode; @@ -26,17 +29,27 @@ const CallAssignmentLayout: React.FC = ({ }) => { const messages = useMessages(messageIds); const { orgId, callAssId } = useNumericRouteParams(); + const router = useRouter(); + const { showConfirmDialog } = useContext(ZUIConfirmDialogContext); const { data: callAssignment, end, start, updateCallAssignment, + deleteAssignment, } = useCallAssignment(orgId, callAssId); const { statsFuture } = useCallAssignmentStats(orgId, callAssId); const { filteredCallersFuture } = useCallers(orgId, callAssId); const state = useCallAssignmentState(orgId, callAssId); + const handleDelete = () => { + deleteAssignment(); + router.push( + `/organize/${orgId}/projects/${callAssignment?.campaign?.id || ''} ` + ); + }; + if (!callAssignment) { return null; } @@ -55,6 +68,21 @@ const CallAssignmentLayout: React.FC = ({ ) } + ellipsisMenuItems={[ + { + label: messages.actions.delete(), + onSelect: () => { + showConfirmDialog({ + onSubmit: handleDelete, + title: messages.actions.delete(), + warningText: messages.actions.warning({ + eventTitle: callAssignment.title, + }), + }); + }, + startIcon: , + }, + ]} baseHref={getCallAssignmentUrl(callAssignment)} belowActionButtons={ ) => { + const id = action.payload; + const item = state.assignmentList.items.find((item) => item.id == id); + if (item) { + item.deleted; + } + }, callAssignmentLoad: (state, action: PayloadAction) => { const id = action.payload; const item = state.assignmentList.items.find((item) => item.id == id); @@ -291,6 +298,7 @@ export default callAssignmentsSlice; export const { callAssignmentCreate, callAssignmentCreated, + callAssignmentDeleted, callAssignmentLoad, callAssignmentLoaded, callAssignmentUpdate, From 4151a6e85217ebc53f9f37a45e282807b52f5515 Mon Sep 17 00:00:00 2001 From: Alexandra Koch Date: Sat, 26 Oct 2024 12:15:40 +0200 Subject: [PATCH 05/18] Alphabetise props --- .../layout/CallAssignmentLayout.tsx | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/features/callAssignments/layout/CallAssignmentLayout.tsx b/src/features/callAssignments/layout/CallAssignmentLayout.tsx index 3e4b84862..a545ee857 100644 --- a/src/features/callAssignments/layout/CallAssignmentLayout.tsx +++ b/src/features/callAssignments/layout/CallAssignmentLayout.tsx @@ -68,6 +68,17 @@ const CallAssignmentLayout: React.FC = ({ ) } + baseHref={getCallAssignmentUrl(callAssignment)} + belowActionButtons={ + { + updateCallAssignment({ end_date: endDate, start_date: startDate }); + }} + startDate={callAssignment.start_date || null} + /> + } + defaultTab="/" ellipsisMenuItems={[ { label: messages.actions.delete(), @@ -83,17 +94,6 @@ const CallAssignmentLayout: React.FC = ({ startIcon: , }, ]} - baseHref={getCallAssignmentUrl(callAssignment)} - belowActionButtons={ - { - updateCallAssignment({ end_date: endDate, start_date: startDate }); - }} - startDate={callAssignment.start_date || null} - /> - } - defaultTab="/" subtitle={ From a4ef6634385ba8d81eb02f5a33e6581e007e5820 Mon Sep 17 00:00:00 2001 From: Alexandra Koch Date: Sat, 26 Oct 2024 14:29:14 +0200 Subject: [PATCH 06/18] Add deletion of surveys --- .../surveys/hooks/useSurveyMutations.ts | 8 +++++ src/features/surveys/l10n/messageIds.ts | 3 ++ src/features/surveys/layout/SurveyLayout.tsx | 30 +++++++++++++++++-- src/features/surveys/store.ts | 8 +++++ 4 files changed, 47 insertions(+), 2 deletions(-) diff --git a/src/features/surveys/hooks/useSurveyMutations.ts b/src/features/surveys/hooks/useSurveyMutations.ts index 4316390c4..578bd87b3 100644 --- a/src/features/surveys/hooks/useSurveyMutations.ts +++ b/src/features/surveys/hooks/useSurveyMutations.ts @@ -21,6 +21,7 @@ import { elementOptionUpdated, elementsReordered, elementUpdated, + surveyDeleted, surveyUpdate, surveyUpdated, } from '../store'; @@ -84,6 +85,7 @@ type UseSurveyEditingReturn = { ) => Promise; deleteElement: (elemId: number) => Promise; deleteElementOption: (elemId: number, optionId: number) => Promise; + deleteSurvey: () => Promise; publish: () => Promise; unpublish: () => Promise; updateElement: ( @@ -181,6 +183,11 @@ export default function useSurveyMutations( dispatch(elementOptionDeleted([surveyId, elemId, optionId])); } + async function deleteSurvey() { + await apiClient.delete(`/api/orgs/${orgId}/surveys/${surveyId}`); + dispatch(surveyDeleted(surveyId)); + } + async function publish() { if (!surveyData) { return; @@ -302,6 +309,7 @@ export default function useSurveyMutations( addElementOptionsFromText, deleteElement, deleteElementOption, + deleteSurvey, publish, unpublish, updateElement, diff --git a/src/features/surveys/l10n/messageIds.ts b/src/features/surveys/l10n/messageIds.ts index 054c19c13..f83e84f67 100644 --- a/src/features/surveys/l10n/messageIds.ts +++ b/src/features/surveys/l10n/messageIds.ts @@ -110,8 +110,10 @@ export default makeMessages('feat.surveys', { }, layout: { actions: { + delete: m('Delete survey'), publish: m('Publish survey'), unpublish: m('Unpublish survey'), + warning: m<{ surveyTitle: string }>('"{surveyTitle}" will be deleted.'), }, stats: { questions: m<{ numQuestions: number }>( @@ -121,6 +123,7 @@ export default makeMessages('feat.surveys', { '{numSubmissions, plural, one {1 submission} other {# submissions}}' ), }, + unknownTitle: m('Untitled survey'), }, optionCollapse: { collapse: m('Collapse'), diff --git a/src/features/surveys/layout/SurveyLayout.tsx b/src/features/surveys/layout/SurveyLayout.tsx index 36c3bd256..ba14e9a4c 100644 --- a/src/features/surveys/layout/SurveyLayout.tsx +++ b/src/features/surveys/layout/SurveyLayout.tsx @@ -1,6 +1,7 @@ import { useRouter } from 'next/router'; import { Box, Button } from '@mui/material'; -import { ChatBubbleOutline, QuizOutlined } from '@mui/icons-material'; +import { ChatBubbleOutline, Delete, QuizOutlined } from '@mui/icons-material'; +import { useContext } from 'react'; import { ELEMENT_TYPE } from 'utils/types/zetkin'; import getSurveyUrl from '../utils/getSurveyUrl'; @@ -12,6 +13,7 @@ import useSurvey from '../hooks/useSurvey'; import useSurveyElements from '../hooks/useSurveyElements'; import useSurveyMutations from '../hooks/useSurveyMutations'; import useSurveyStats from '../hooks/useSurveyStats'; +import { ZUIConfirmDialogContext } from 'zui/ZUIConfirmDialogProvider'; import ZUIDateRangePicker from 'zui/ZUIDateRangePicker/ZUIDateRangePicker'; import ZUIEditTextinPlace from 'zui/ZUIEditTextInPlace'; import ZUIFuture from 'zui/ZUIFuture'; @@ -37,9 +39,10 @@ const SurveyLayout: React.FC = ({ const router = useRouter(); const parsedOrg = parseInt(orgId); const messages = useMessages(messageIds); + const { showConfirmDialog } = useContext(ZUIConfirmDialogContext); const statsFuture = useSurveyStats(parsedOrg, parseInt(surveyId)); const surveyFuture = useSurvey(parsedOrg, parseInt(surveyId)); - const { publish, unpublish, updateSurvey } = useSurveyMutations( + const { publish, unpublish, updateSurvey, deleteSurvey } = useSurveyMutations( parsedOrg, parseInt(surveyId) ); @@ -71,6 +74,13 @@ const SurveyLayout: React.FC = ({ /> ); }; + const handleDelete = () => { + deleteSurvey(); + router.push( + `/organize/${orgId}/projects/${surveyFuture.data?.campaign?.id || ''} ` + ); + }; + return ( = ({ /> } defaultTab="/" + ellipsisMenuItems={[ + { + label: messages.layout.actions.delete(), + onSelect: () => { + showConfirmDialog({ + onSubmit: handleDelete, + title: messages.layout.actions.delete(), + warningText: messages.layout.actions.warning({ + surveyTitle: + surveyFuture.data?.title || messages.layout.unknownTitle(), + }), + }); + }, + startIcon: , + }, + ]} onClickAlertBtn={() => { router.push( `/organize/${originalOrgId}/projects/${surveyFuture.data?.campaign?.id}/surveys/${surveyId}` diff --git a/src/features/surveys/store.ts b/src/features/surveys/store.ts index 39a30aba2..db0513878 100644 --- a/src/features/surveys/store.ts +++ b/src/features/surveys/store.ts @@ -252,6 +252,13 @@ const surveysSlice = createSlice({ ); } }, + surveyDeleted: (state, action: PayloadAction) => { + const id = action.payload; + const item = state.surveyList.items.find((item) => item.id == id); + if (item) { + item.deleted = true; + } + }, surveyLoad: (state, action: PayloadAction) => { const id = action.payload; const item = state.surveyList.items.find((item) => item.id == id); @@ -419,6 +426,7 @@ export const { statsLoaded, surveyCreate, surveyCreated, + surveyDeleted, surveyLoad, surveyLoaded, surveySubmissionUpdate, From 1c165bfbc1f8b6b05acf258a3329b6d6ab0e4d4b Mon Sep 17 00:00:00 2001 From: Richard Olsson Date: Sat, 26 Oct 2024 15:22:01 +0200 Subject: [PATCH 07/18] Refactor PotentialDuplicateModal by breaking out UI as MergeModal --- .../duplicates/components/MergeModal.tsx | 114 +++++++++++++++++ .../components/PotentialDuplicateModal.tsx | 115 ++---------------- 2 files changed, 126 insertions(+), 103 deletions(-) create mode 100644 src/features/duplicates/components/MergeModal.tsx diff --git a/src/features/duplicates/components/MergeModal.tsx b/src/features/duplicates/components/MergeModal.tsx new file mode 100644 index 000000000..1a65c06f8 --- /dev/null +++ b/src/features/duplicates/components/MergeModal.tsx @@ -0,0 +1,114 @@ +import { + Alert, + AlertTitle, + Box, + Button, + Dialog, + DialogActions, + DialogTitle, + useMediaQuery, +} from '@mui/material'; +import { FC, useState } from 'react'; +import React, { useEffect } from 'react'; + +import theme from 'theme'; +import FieldSettings from './FieldSettings'; +import messageIds from '../l10n/messageIds'; +import PotentialDuplicatesLists from './PotentialDuplicatesLists'; +import useFieldSettings from '../hooks/useFieldSettings'; +import { useMessages } from 'core/i18n'; +import { ZetkinPerson } from 'utils/types/zetkin'; + +type Props = { + onClose: () => void; + onMerge: (personIds: number[], overrides: Partial) => void; + open: boolean; + persons: ZetkinPerson[]; +}; + +const MergeModal: FC = ({ open, onClose, onMerge, persons }) => { + const fullScreen = useMediaQuery(theme.breakpoints.down('md')); + const messages = useMessages(messageIds); + + const [selectedIds, setSelectedIds] = useState( + persons.map((person) => person.id) ?? [] + ); + + const peopleToMerge = persons.filter((person) => + selectedIds.includes(person.id) + ); + + const peopleNotToMerge = persons.filter( + (person) => !selectedIds.includes(person.id) + ); + + const { hasConflictingValues, fieldValues, initialOverrides } = + useFieldSettings(peopleToMerge); + const [overrides, setOverrides] = useState(initialOverrides); + + useEffect(() => { + setSelectedIds(persons.map((person) => person.id) ?? []); + }, [open]); + + return ( + + + {messages.modal.title()} + + + + { + const filteredIds = selectedIds.filter( + (item) => item !== person.id + ); + setSelectedIds(filteredIds); + }} + onSelect={(person: ZetkinPerson) => { + const selectedIdsUpdated = [...selectedIds, person.id]; + setSelectedIds(selectedIdsUpdated); + }} + peopleNotToMerge={peopleNotToMerge} + peopleToMerge={peopleToMerge} + /> + + + { + setOverrides({ ...overrides, [`${field}`]: value }); + }} + /> + + {hasConflictingValues && ( + + {messages.modal.warningTitle()} + {messages.modal.warningMessage()} + + )} + + + + + + + + ); +}; + +export default MergeModal; diff --git a/src/features/duplicates/components/PotentialDuplicateModal.tsx b/src/features/duplicates/components/PotentialDuplicateModal.tsx index 0255f4b98..d3405e427 100644 --- a/src/features/duplicates/components/PotentialDuplicateModal.tsx +++ b/src/features/duplicates/components/PotentialDuplicateModal.tsx @@ -1,31 +1,15 @@ -import { - Alert, - AlertTitle, - Box, - Button, - Dialog, - DialogActions, - DialogTitle, - useMediaQuery, -} from '@mui/material'; -import { FC, useState } from 'react'; -import React, { useEffect } from 'react'; +import { FC } from 'react'; +import React from 'react'; -import theme from 'theme'; -import FieldSettings from './FieldSettings'; -import messageIds from '../l10n/messageIds'; import { PotentialDuplicate } from '../store'; -import PotentialDuplicatesLists from './PotentialDuplicatesLists'; import useDuplicatesMutations from '../hooks/useDuplicatesMutations'; -import useFieldSettings from '../hooks/useFieldSettings'; -import { useMessages } from 'core/i18n'; import { useNumericRouteParams } from 'core/hooks'; -import { ZetkinPerson } from 'utils/types/zetkin'; +import MergeModal from './MergeModal'; interface Props { - potentialDuplicate: PotentialDuplicate; onClose: () => void; open: boolean; + potentialDuplicate: PotentialDuplicate; } const PotentialDuplicateModal: FC = ({ @@ -34,92 +18,17 @@ const PotentialDuplicateModal: FC = ({ onClose, }) => { const { orgId } = useNumericRouteParams(); - const fullScreen = useMediaQuery(theme.breakpoints.down('md')); - const messages = useMessages(messageIds); const { mergeDuplicate } = useDuplicatesMutations(orgId); - const [selectedIds, setSelectedIds] = useState( - potentialDuplicate?.duplicates.map((person) => person.id) ?? [] - ); - - const peopleToMerge = potentialDuplicate?.duplicates.filter((person) => - selectedIds.includes(person.id) - ); - - const peopleNotToMerge = potentialDuplicate?.duplicates.filter( - (person) => !selectedIds.includes(person.id) - ); - - const { hasConflictingValues, fieldValues, initialOverrides } = - useFieldSettings(peopleToMerge); - const [overrides, setOverrides] = useState(initialOverrides); - - useEffect(() => { - setSelectedIds( - potentialDuplicate?.duplicates.map((person) => person.id) ?? [] - ); - }, [open]); - return ( - - - {messages.modal.title()} - - - - { - const filteredIds = selectedIds.filter( - (item) => item !== person.id - ); - setSelectedIds(filteredIds); - }} - onSelect={(person: ZetkinPerson) => { - const selectedIdsUpdated = [...selectedIds, person.id]; - setSelectedIds(selectedIdsUpdated); - }} - peopleNotToMerge={peopleNotToMerge} - peopleToMerge={peopleToMerge} - /> - - - { - setOverrides({ ...overrides, [`${field}`]: value }); - }} - /> - - {hasConflictingValues && ( - - {messages.modal.warningTitle()} - {messages.modal.warningMessage()} - - )} - - - - - - - + { + mergeDuplicate(potentialDuplicate.id, personIds, overrides); + }} + open={open} + persons={potentialDuplicate.duplicates} + /> ); }; From 931d631484a885631af364b4736e27939437498a Mon Sep 17 00:00:00 2001 From: Richard Olsson Date: Sat, 26 Oct 2024 15:46:16 +0200 Subject: [PATCH 08/18] Add UI for manually adding a candidate for merging --- .../duplicates/components/MergeModal.tsx | 36 ++++++++++++++----- .../components/PotentialDuplicatesLists.tsx | 32 +++++++++++++++-- src/features/duplicates/l10n/messageIds.ts | 5 +++ 3 files changed, 63 insertions(+), 10 deletions(-) diff --git a/src/features/duplicates/components/MergeModal.tsx b/src/features/duplicates/components/MergeModal.tsx index 1a65c06f8..8bc4d7626 100644 --- a/src/features/duplicates/components/MergeModal.tsx +++ b/src/features/duplicates/components/MergeModal.tsx @@ -29,14 +29,16 @@ type Props = { const MergeModal: FC = ({ open, onClose, onMerge, persons }) => { const fullScreen = useMediaQuery(theme.breakpoints.down('md')); const messages = useMessages(messageIds); + const [additionalPeople, setAdditionalPeople] = useState([]); const [selectedIds, setSelectedIds] = useState( persons.map((person) => person.id) ?? [] ); - const peopleToMerge = persons.filter((person) => - selectedIds.includes(person.id) - ); + const peopleToMerge = [ + ...persons.filter((person) => selectedIds.includes(person.id)), + ...additionalPeople, + ]; const peopleNotToMerge = persons.filter( (person) => !selectedIds.includes(person.id) @@ -59,14 +61,32 @@ const MergeModal: FC = ({ open, onClose, onMerge, persons }) => { { - const filteredIds = selectedIds.filter( - (item) => item !== person.id + const isPredefined = persons.some( + (predefinedPerson) => predefinedPerson.id == person.id ); - setSelectedIds(filteredIds); + + if (isPredefined) { + const filteredIds = selectedIds.filter( + (item) => item !== person.id + ); + setSelectedIds(filteredIds); + } else { + const filteredAdditionals = additionalPeople.filter( + (item) => item.id != person.id + ); + setAdditionalPeople(filteredAdditionals); + } }} onSelect={(person: ZetkinPerson) => { - const selectedIdsUpdated = [...selectedIds, person.id]; - setSelectedIds(selectedIdsUpdated); + const isPredefined = persons.some( + (predefinedPerson) => predefinedPerson.id == person.id + ); + if (isPredefined) { + const selectedIdsUpdated = [...selectedIds, person.id]; + setSelectedIds(selectedIdsUpdated); + } else { + setAdditionalPeople([...additionalPeople, person]); + } }} peopleNotToMerge={peopleNotToMerge} peopleToMerge={peopleToMerge} diff --git a/src/features/duplicates/components/PotentialDuplicatesLists.tsx b/src/features/duplicates/components/PotentialDuplicatesLists.tsx index 5e01d0f16..9f4f97a4d 100644 --- a/src/features/duplicates/components/PotentialDuplicatesLists.tsx +++ b/src/features/duplicates/components/PotentialDuplicatesLists.tsx @@ -1,10 +1,12 @@ -import { FC } from 'react'; +import { FC, useState } from 'react'; import { Box, Divider, Typography } from '@mui/material'; import MergeCandidateList from './MergeCandidateList'; import messageIds from '../l10n/messageIds'; import { useMessages } from 'core/i18n'; import { ZetkinPerson } from 'utils/types/zetkin'; +import ZUIEllipsisMenu from 'zui/ZUIEllipsisMenu'; +import { MUIOnlyPersonSelect } from 'zui/ZUIPersonSelect'; interface PotentialDuplicatesListsProps { onDeselect: (person: ZetkinPerson) => void; @@ -20,10 +22,36 @@ const PotentialDuplicatesLists: FC = ({ peopleToMerge, }) => { const messages = useMessages(messageIds); + const [addingManually, setAddingManually] = useState(false); return ( <> - {messages.modal.peopleToMerge()} + + {messages.modal.peopleToMerge()} + + + {addingManually && ( + + + + )} Date: Sat, 26 Oct 2024 16:12:24 +0200 Subject: [PATCH 09/18] Update ZUI github issue template to include more info about file structure and Storybook. --- .github/ISSUE_TEMPLATE/zui.md | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/zui.md b/.github/ISSUE_TEMPLATE/zui.md index c7647df7c..976c8725c 100644 --- a/.github/ISSUE_TEMPLATE/zui.md +++ b/.github/ISSUE_TEMPLATE/zui.md @@ -20,14 +20,19 @@ You need to be logged into a Figma account to properly view the Figma content. ## Open questions -## Possible implementations +## Workflow -Develop this using Storybook. We want all the design system components to be documented through their own Storybook stories. - -For reference, you can look at the already existing ZUI components created on the `undocumented/new-design-system` branch (not ZUI components that exist on `main`, as they are not part of the new design system.) - -## Git +### Git The main git branch for the work on the new design system is `undocumented/new-design-system`. Unless otherwise instructed, do your work on a new branch branched off from this branch. Name your branch `issue-number/zui-name`, ex: `issue-928/zui-button` for a branch where work is done that is documented in the issue with number 928, where a button component is being made. + +### Storybook + +Use [Storybook](https://storybook.js.org/) to develop the new design system components. If you are not familiar with working with Storybook, please ask and Ziggi or someone else will be happy to introduce you! +When you have checked out the branch `undocumented/new-design-system` (and, as always when checking out a branch just to be sure, run `yarn install`), run `yarn storybook` in the terminal. This starts Storybook locally, and should open your browser to `localhost:6006` where you see all the components. Note that you want to only look at the ones under the "New design system" headline. + +### Files + +Create a folder in `src/zui/newDesignSystem` and give it a name for your component, like `ZUIButton`. Inside that folder, create one file `index.tsx` (this is where you write your component) and one `index.stories.tsx` (this is where you write your Storybook stories). Look at the components in `src/zui/newDesignSystem` for inspiration/reference! From 1590b1eab751cee0b3c6210d74af9b8e2e230729 Mon Sep 17 00:00:00 2001 From: Richard Olsson Date: Sat, 26 Oct 2024 17:33:04 +0200 Subject: [PATCH 10/18] Create separate hook for merging people, regardless of source --- .../hooks/useDuplicatesMutations.tsx | 19 ++++------------ .../duplicates/hooks/useMergePersons.tsx | 22 +++++++++++++++++++ src/features/duplicates/types.ts | 7 ++++++ src/features/profile/store.ts | 16 ++++++++++++++ 4 files changed, 49 insertions(+), 15 deletions(-) create mode 100644 src/features/duplicates/hooks/useMergePersons.tsx create mode 100644 src/features/duplicates/types.ts diff --git a/src/features/duplicates/hooks/useDuplicatesMutations.tsx b/src/features/duplicates/hooks/useDuplicatesMutations.tsx index 5dd627e8b..76030b3ce 100644 --- a/src/features/duplicates/hooks/useDuplicatesMutations.tsx +++ b/src/features/duplicates/hooks/useDuplicatesMutations.tsx @@ -5,6 +5,7 @@ import { PotentialDuplicate, } from '../store'; import { useApiClient, useAppDispatch } from 'core/hooks'; +import useMergePersons from './useMergePersons'; type DuplicatesMutationsReturn = { dismissDuplicate: (duplicateId: number) => void; @@ -20,17 +21,12 @@ export default function useDuplicatesMutations( ): DuplicatesMutationsReturn { const apiClient = useApiClient(); const dispatch = useAppDispatch(); + const mergePersons = useMergePersons(orgId); type PotentialDuplicatePatchBody = { dismissed?: boolean; }; - type MergePostBody = { - objects: number[]; - override: Partial; - type: 'person'; - }; - const dismissDuplicate = async (duplicateId: number) => { await apiClient .patch( @@ -47,15 +43,8 @@ export default function useDuplicatesMutations( duplicatesIds: number[], override: Partial ) => { - await apiClient - .post(`/api/orgs/${orgId}/merges`, { - objects: duplicatesIds, - override, - type: 'person', - }) - .then(() => { - dispatch(duplicateMerged(potentialDuplicateId)); - }); + await mergePersons(duplicatesIds, override); + dispatch(duplicateMerged(potentialDuplicateId)); }; return { diff --git a/src/features/duplicates/hooks/useMergePersons.tsx b/src/features/duplicates/hooks/useMergePersons.tsx new file mode 100644 index 000000000..adbf8374e --- /dev/null +++ b/src/features/duplicates/hooks/useMergePersons.tsx @@ -0,0 +1,22 @@ +import { useApiClient, useAppDispatch } from 'core/hooks'; +import { ZetkinPerson } from 'utils/types/zetkin'; +import { MergePostBody } from '../types'; +import { personsMerged } from 'features/profile/store'; + +export default function useMergePersons(orgId: number) { + const apiClient = useApiClient(); + const dispatch = useAppDispatch(); + + return async (personIds: number[], overrides: Partial) => { + await apiClient.post( + `/api/orgs/${orgId}/merges`, + { + objects: personIds, + override: overrides, + type: 'person', + } + ); + + dispatch(personsMerged(personIds)); + }; +} diff --git a/src/features/duplicates/types.ts b/src/features/duplicates/types.ts new file mode 100644 index 000000000..5448c04aa --- /dev/null +++ b/src/features/duplicates/types.ts @@ -0,0 +1,7 @@ +import { ZetkinPerson } from 'utils/types/zetkin'; + +export type MergePostBody = { + objects: number[]; + override: Partial; + type: 'person'; +}; diff --git a/src/features/profile/store.ts b/src/features/profile/store.ts index 8ed2863c7..f38d69c48 100644 --- a/src/features/profile/store.ts +++ b/src/features/profile/store.ts @@ -98,6 +98,21 @@ const profilesSlice = createSlice({ item.mutating = []; } }, + personsMerged: (state, action: PayloadAction) => { + const ids = action.payload; + + // The first one might be stale + ids.forEach((id, index) => { + const personItem = state.personById[id]; + if (personItem) { + if (index == 0) { + personItem.isStale = true; + } else { + personItem.deleted = true; + } + } + }); + }, }, }); @@ -113,4 +128,5 @@ export const { personOrgRemoved, personUpdate, personUpdated, + personsMerged, } = profilesSlice.actions; From 6f777f5e6d5f47071e3e020c6602c91c236b189b Mon Sep 17 00:00:00 2001 From: Richard Olsson Date: Sat, 26 Oct 2024 17:37:28 +0200 Subject: [PATCH 11/18] Add MergeModal variant ManualMergingModal to manually select and merge people --- .../components/ManualMergingModal.tsx | 33 +++++++++++++++ .../duplicates/components/MergeModal.tsx | 31 ++++++++++++-- .../components/PotentialDuplicatesLists.tsx | 6 ++- .../components/PersonActionButtons.tsx | 40 +++++++++++++++++++ src/features/profile/l10n/messageIds.ts | 3 ++ .../profile/layout/SinglePersonLayout.tsx | 2 + 6 files changed, 110 insertions(+), 5 deletions(-) create mode 100644 src/features/duplicates/components/ManualMergingModal.tsx create mode 100644 src/features/profile/components/PersonActionButtons.tsx diff --git a/src/features/duplicates/components/ManualMergingModal.tsx b/src/features/duplicates/components/ManualMergingModal.tsx new file mode 100644 index 000000000..1f38bf3d7 --- /dev/null +++ b/src/features/duplicates/components/ManualMergingModal.tsx @@ -0,0 +1,33 @@ +import { FC } from 'react'; +import React from 'react'; + +import MergeModal from './MergeModal'; +import { ZetkinPerson } from 'utils/types/zetkin'; +import useMergePersons from '../hooks/useMergePersons'; +import { useNumericRouteParams } from 'core/hooks'; + +interface Props { + initialPersons: ZetkinPerson[]; + onClose: () => void; + open: boolean; +} + +const ManualMergingModal: FC = ({ initialPersons, open, onClose }) => { + const { orgId } = useNumericRouteParams(); + const mergePersons = useMergePersons(orgId); + + return ( + { + mergePersons(personIds, overrides); + onClose(); + }} + open={open} + persons={initialPersons} + /> + ); +}; + +export default ManualMergingModal; diff --git a/src/features/duplicates/components/MergeModal.tsx b/src/features/duplicates/components/MergeModal.tsx index 8bc4d7626..5b8a07b07 100644 --- a/src/features/duplicates/components/MergeModal.tsx +++ b/src/features/duplicates/components/MergeModal.tsx @@ -20,13 +20,20 @@ import { useMessages } from 'core/i18n'; import { ZetkinPerson } from 'utils/types/zetkin'; type Props = { + initiallyShowManualSearch?: boolean; onClose: () => void; onMerge: (personIds: number[], overrides: Partial) => void; open: boolean; persons: ZetkinPerson[]; }; -const MergeModal: FC = ({ open, onClose, onMerge, persons }) => { +const MergeModal: FC = ({ + initiallyShowManualSearch = false, + open, + onClose, + onMerge, + persons, +}) => { const fullScreen = useMediaQuery(theme.breakpoints.down('md')); const messages = useMessages(messageIds); const [additionalPeople, setAdditionalPeople] = useState([]); @@ -60,6 +67,7 @@ const MergeModal: FC = ({ open, onClose, onMerge, persons }) => { { const isPredefined = persons.some( (predefinedPerson) => predefinedPerson.id == person.id @@ -116,12 +124,27 @@ const MergeModal: FC = ({ open, onClose, onMerge, persons }) => { -