diff --git a/.github/ISSUE_TEMPLATE/zui.md b/.github/ISSUE_TEMPLATE/zui.md index c7647df7c9..ab890f3b55 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/components` 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/components` for inspiration/reference! Note that there are lots of components with names that start with "ZUI" outside the `src/zui/components` folder, but only the ones in `src/zui/components` are relevant as reference/inspiration for the work you will be doing. diff --git a/src/features/callAssignments/hooks/useCallAssignment.ts b/src/features/callAssignments/hooks/useCallAssignment.ts index 5912db0422..594dda848d 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 91d02cab48..f0153c8bb0 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<{ title: string }>('"{title}" 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 12f91b2566..8b4d8e4d81 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; } @@ -66,6 +79,21 @@ const CallAssignmentLayout: React.FC = ({ /> } defaultTab="/" + ellipsisMenuItems={[ + { + label: messages.actions.delete(), + onSelect: () => { + showConfirmDialog({ + onSubmit: handleDelete, + title: messages.actions.delete(), + warningText: messages.actions.warning({ + title: callAssignment.title, + }), + }); + }, + startIcon: , + }, + ]} subtitle={ diff --git a/src/features/callAssignments/store.ts b/src/features/callAssignments/store.ts index 90602215c2..62ec020c00 100644 --- a/src/features/callAssignments/store.ts +++ b/src/features/callAssignments/store.ts @@ -57,6 +57,13 @@ const callAssignmentsSlice = createSlice({ ]); } }, + callAssignmentDeleted: (state, action: PayloadAction) => { + 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, diff --git a/src/features/duplicates/components/DuplicateCard.tsx b/src/features/duplicates/components/DuplicateCard.tsx index f4ce2eccbc..a5d91d5a5e 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/ManualMergingModal.tsx b/src/features/duplicates/components/ManualMergingModal.tsx new file mode 100644 index 0000000000..1f38bf3d78 --- /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/ConfigureModal.tsx b/src/features/duplicates/components/MergeModal.tsx similarity index 53% rename from src/features/duplicates/components/ConfigureModal.tsx rename to src/features/duplicates/components/MergeModal.tsx index 37c7875806..d70798eb06 100644 --- a/src/features/duplicates/components/ConfigureModal.tsx +++ b/src/features/duplicates/components/MergeModal.tsx @@ -14,39 +14,40 @@ import React, { useEffect } 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'; -interface ConfigureModalProps { - potentialDuplicate: PotentialDuplicate; +type Props = { + initiallyShowManualSearch?: boolean; onClose: () => void; + onMerge: (personIds: number[], overrides: Partial) => void; open: boolean; -} + persons: ZetkinPerson[]; +}; -const ConfigureModal: FC = ({ - potentialDuplicate, +const MergeModal: FC = ({ + initiallyShowManualSearch = false, open, onClose, + onMerge, + persons, }) => { - const { orgId } = useNumericRouteParams(); const fullScreen = useMediaQuery(theme.breakpoints.down('md')); const messages = useMessages(messageIds); - const { mergeDuplicate } = useDuplicatesMutations(orgId); + const [additionalPeople, setAdditionalPeople] = useState([]); const [selectedIds, setSelectedIds] = useState( - potentialDuplicate?.duplicates.map((person) => person.id) ?? [] + persons.map((person) => person.id) ?? [] ); - const peopleToMerge = potentialDuplicate?.duplicates.filter((person) => - selectedIds.includes(person.id) - ); + const peopleToMerge = [ + ...persons.filter((person) => selectedIds.includes(person.id)), + ...additionalPeople, + ]; - const peopleNotToMerge = potentialDuplicate?.duplicates.filter( + const peopleNotToMerge = persons.filter( (person) => !selectedIds.includes(person.id) ); @@ -55,28 +56,45 @@ const ConfigureModal: FC = ({ const [overrides, setOverrides] = useState(initialOverrides); useEffect(() => { - setSelectedIds( - potentialDuplicate?.duplicates.map((person) => person.id) ?? [] - ); + setSelectedIds(persons.map((person) => person.id) ?? []); }, [open]); return ( - + {messages.modal.title()} { - 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} @@ -106,14 +124,27 @@ const ConfigureModal: FC = ({ -