-
Notifications
You must be signed in to change notification settings - Fork 8
Alex/delete merge activists #359
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
3390243
fix(merging): backend logic resulting in name collision
alexsapps cc17fd0
feat: allow non-sfbay chapters to hide activists
alexsapps 239e275
fix(backend): verify user chapter for various endpoints
alexsapps 77de95a
fix: disallow merging across chapters
alexsapps b731376
feat(activists): merge and delete in frontend-v2
alexsapps be064bb
refactor(frontend): consolidate response types
alexsapps File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
84 changes: 84 additions & 0 deletions
84
frontend-v2/src/app/(authed)/activists/[id]/hide-activist-dialog.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| 'use client' | ||
|
|
||
| import { useRouter } from 'next/navigation' | ||
| import { useMutation, useQueryClient } from '@tanstack/react-query' | ||
| import toast from 'react-hot-toast' | ||
| import { API_PATH, apiClient } from '@/lib/api' | ||
| import { Button } from '@/components/ui/button' | ||
| import { | ||
| Dialog, | ||
| DialogContent, | ||
| DialogFooter, | ||
| DialogHeader, | ||
| DialogTitle, | ||
| } from '@/components/ui/dialog' | ||
|
|
||
| type Props = { | ||
| open: boolean | ||
| onOpenChange: (open: boolean) => void | ||
| activistId: number | ||
| activistName: string | ||
| } | ||
|
|
||
| export function HideActivistDialog({ | ||
| open, | ||
| onOpenChange, | ||
| activistId, | ||
| activistName, | ||
| }: Props) { | ||
| const router = useRouter() | ||
| const queryClient = useQueryClient() | ||
|
|
||
| const mutation = useMutation({ | ||
| mutationFn: () => apiClient.hideActivist(activistId), | ||
| onSuccess: () => { | ||
| toast.success(`${activistName} was hidden`) | ||
| queryClient.invalidateQueries({ queryKey: [API_PATH.ACTIVISTS_SEARCH] }) | ||
| queryClient.invalidateQueries({ | ||
| queryKey: [API_PATH.ACTIVIST_LIST_BASIC], | ||
| }) | ||
| onOpenChange(false) | ||
| router.push('/activists') | ||
| }, | ||
| onError: (err: Error) => { | ||
| toast.error(err.message || 'Failed to hide activist') | ||
| }, | ||
| }) | ||
|
|
||
| const handleOpenChange = (next: boolean) => { | ||
| if (mutation.isPending) return | ||
| onOpenChange(next) | ||
| } | ||
|
|
||
| return ( | ||
| <Dialog open={open} onOpenChange={handleOpenChange}> | ||
| <DialogContent> | ||
| <DialogHeader> | ||
| <DialogTitle>Hide activist</DialogTitle> | ||
| </DialogHeader> | ||
| <p className="text-sm"> | ||
| WARNING: Hiding this activist will make them inaccessible unless they | ||
| are unhidden by Tech. Are you sure you want to hide this activist? | ||
| </p> | ||
| <DialogFooter> | ||
| <Button | ||
| type="button" | ||
| variant="outline" | ||
| onClick={() => handleOpenChange(false)} | ||
| disabled={mutation.isPending} | ||
| > | ||
| Cancel | ||
| </Button> | ||
| <Button | ||
| type="button" | ||
| variant="destructive" | ||
| onClick={() => mutation.mutate()} | ||
| disabled={mutation.isPending} | ||
| > | ||
| {mutation.isPending ? 'Hiding...' : 'Hide activist'} | ||
| </Button> | ||
| </DialogFooter> | ||
| </DialogContent> | ||
| </Dialog> | ||
| ) | ||
| } |
171 changes: 171 additions & 0 deletions
171
frontend-v2/src/app/(authed)/activists/[id]/merge-activist-dialog.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,171 @@ | ||
| 'use client' | ||
|
|
||
| import { useState } from 'react' | ||
| import { useRouter } from 'next/navigation' | ||
| import { useMutation, useQueryClient } from '@tanstack/react-query' | ||
| import toast from 'react-hot-toast' | ||
| import { API_PATH, apiClient } from '@/lib/api' | ||
| import { useAuthedPageContext } from '@/hooks/useAuthedPageContext' | ||
| import { useActivistRegistry } from '../../events/useActivistRegistry' | ||
| import { SuggestionInput } from '../../events/suggestion-input' | ||
| import { Button } from '@/components/ui/button' | ||
| import { Label } from '@/components/ui/label' | ||
| import { | ||
| Dialog, | ||
| DialogContent, | ||
| DialogFooter, | ||
| DialogHeader, | ||
| DialogTitle, | ||
| } from '@/components/ui/dialog' | ||
|
|
||
| type Props = { | ||
| open: boolean | ||
| onOpenChange: (open: boolean) => void | ||
| activistId: number | ||
| activistName: string | ||
| } | ||
|
|
||
| export function MergeActivistDialog({ | ||
| open, | ||
| onOpenChange, | ||
| activistId, | ||
| activistName, | ||
| }: Props) { | ||
| const router = useRouter() | ||
| const queryClient = useQueryClient() | ||
| const { user } = useAuthedPageContext() | ||
| const { registry } = useActivistRegistry(user.ChapterID) | ||
|
|
||
| // Raw text in the input as the user types. | ||
| const [inputValue, setInputValue] = useState('') | ||
| // Name of an activist explicitly selected from the suggestion list. | ||
| const [selectedValue, setSelectedValue] = useState('') | ||
|
|
||
| const mutation = useMutation({ | ||
| mutationFn: (targetName: string) => | ||
| apiClient.mergeActivist(activistId, targetName), | ||
|
alexsapps marked this conversation as resolved.
|
||
| onSuccess: (_data, targetName) => { | ||
| toast.success(`${activistName} was merged into ${targetName}`) | ||
| queryClient.invalidateQueries({ queryKey: [API_PATH.ACTIVISTS_SEARCH] }) | ||
| queryClient.invalidateQueries({ | ||
| queryKey: [API_PATH.ACTIVIST_LIST_BASIC], | ||
| }) | ||
| onOpenChange(false) | ||
| router.push('/activists') | ||
| }, | ||
| onError: (err: Error) => { | ||
| toast.error(err.message || 'Failed to merge activist') | ||
| }, | ||
| }) | ||
|
|
||
| const handleOpenChange = (next: boolean) => { | ||
| if (mutation.isPending) return | ||
| if (!next) { | ||
| setInputValue('') | ||
| setSelectedValue('') | ||
| } | ||
| onOpenChange(next) | ||
| } | ||
|
|
||
| const getSuggestions = (input: string) => | ||
| registry.getSuggestions(input).filter((name) => name !== activistName) | ||
|
|
||
| const isTargetValid = | ||
| selectedValue.trim().length > 0 && | ||
| // Cannot merge activist with self. (Names are unique within chapter, and | ||
| // backend endpoint will not allow merging across chapters.) | ||
| selectedValue !== activistName && | ||
| registry.getActivist(selectedValue) !== null | ||
|
|
||
| const handleSubmit = () => { | ||
| if (!isTargetValid) { | ||
| toast.error('Choose an activist to merge into') | ||
| return | ||
| } | ||
| mutation.mutate(selectedValue) | ||
| } | ||
|
|
||
| return ( | ||
| <Dialog open={open} onOpenChange={handleOpenChange}> | ||
| <DialogContent> | ||
| <DialogHeader> | ||
| <DialogTitle>Merge activist</DialogTitle> | ||
| </DialogHeader> | ||
| <div className="text-sm space-y-2"> | ||
| <p> | ||
| Merging activists is used to combine redundant activist entries. | ||
| </p> | ||
| <p>Merging this activist does the following:</p> | ||
| <ul className="list-disc pl-5 space-y-1"> | ||
| <li> | ||
| All of {activistName}'s event attendance will be moved to the | ||
| target activist. | ||
| </li> | ||
| <li> | ||
| The target's name will be kept — {activistName}'s | ||
| name will not replace it. | ||
| </li> | ||
| <li> | ||
| {activistName}'s email, phone, and address / location will | ||
| replace the corresponding fields in the target{' '} | ||
| <em>if updated more recently</em>. | ||
| </li> | ||
| <li> | ||
| {activistName}'s pronouns, language, notes, and most other | ||
| fields will each replace the corresponding field in the target{' '} | ||
| <em>only if the target's value is blank</em>. | ||
| </li> | ||
| <li> | ||
| Yes / no flags (e.g. MPI, hiatus, voting agreement) will be set to | ||
| “yes” <em>if either activist has them set</em>. | ||
| </li> | ||
| <li> | ||
| The target's activist level will be set to the{' '} | ||
| <em>higher of the two activists' levels</em>. | ||
| </li> | ||
| <li>{activistName} will be hidden.</li> | ||
| </ul> | ||
| <p className="font-semibold pt-2"> | ||
| Merge {activistName} into another activist: | ||
| </p> | ||
| </div> | ||
| <div className="flex flex-col gap-1.5"> | ||
| <Label htmlFor="merge-target">Target activist</Label> | ||
| <SuggestionInput | ||
| id="merge-target" | ||
| value={inputValue} | ||
| onValueChange={(v) => { | ||
| setInputValue(v) | ||
| setSelectedValue('') | ||
| }} | ||
| getSuggestions={getSuggestions} | ||
| onCommit={(meta) => { | ||
| if (meta.fromSuggestion) { | ||
| setSelectedValue(meta.value) | ||
| } | ||
| }} | ||
| placeholder="Start typing a name..." | ||
| /> | ||
| </div> | ||
| <DialogFooter> | ||
| <Button | ||
| type="button" | ||
| variant="outline" | ||
| onClick={() => handleOpenChange(false)} | ||
| disabled={mutation.isPending} | ||
| > | ||
| Cancel | ||
| </Button> | ||
| <Button | ||
| type="button" | ||
| variant="destructive" | ||
| onClick={handleSubmit} | ||
| disabled={!isTargetValid || mutation.isPending} | ||
| > | ||
| {mutation.isPending ? 'Merging...' : 'Merge activist'} | ||
| </Button> | ||
| </DialogFooter> | ||
| </DialogContent> | ||
| </Dialog> | ||
| ) | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.