Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
0fe3fbe
fix: make dropdown menus scrollable
imdeaconu Sep 6, 2024
4ca1a44
fix: truncate overflowing table columns
imdeaconu Sep 6, 2024
921aa13
Merge branch 'commitglobal:main' into main
imdeaconu Sep 6, 2024
e5a2869
Merge branch 'commitglobal:main' into main
imdeaconu Sep 6, 2024
7866a67
Merge branch 'commitglobal:main' into main
imdeaconu Sep 9, 2024
9ea6c42
Merge branch 'commitglobal:main' into main
imdeaconu Sep 10, 2024
1bd449d
Merge branch 'commitglobal:main' into main
imdeaconu Sep 11, 2024
c9874d1
Squashed commit of the following:
imdeaconu Sep 11, 2024
b7715f3
Merge branch 'commitglobal:main' into main
imdeaconu Sep 12, 2024
0facf65
Squashed commit of the following:
imdeaconu Sep 13, 2024
67f681d
chore: remove unused import
imdeaconu Sep 13, 2024
8d73252
chore: delete duplicated / unused classes
imdeaconu Sep 16, 2024
63b21b9
Merge branch 'commitglobal:main' into main
imdeaconu Sep 17, 2024
1892e6e
Merge branch 'commitglobal:main' into main
imdeaconu Sep 18, 2024
abb7c01
feature: add searching to MonitoringObserversTagFilter
imdeaconu Sep 19, 2024
9d0b8ae
Merge branch 'commitglobal:main' into main
imdeaconu Sep 20, 2024
c9fcd3e
chore: update config files
imdeaconu Sep 20, 2024
333ba49
Revert "[NGO Admin] Rewrite the tag selector component (#675)"
imdeaconu Sep 23, 2024
580b68e
Merge branch 'main' of https://github.com/commitglobal/votemonitor
imdeaconu Sep 23, 2024
ba2dad9
Merge branch 'commitglobal:main' into main
imdeaconu Sep 25, 2024
eea4faa
Merge branch 'main' of https://github.com/commitglobal/votemonitor in…
imdeaconu Sep 26, 2024
29b8163
Merge branch 'main' of https://github.com/commitglobal/votemonitor in…
imdeaconu Sep 26, 2024
68a44ee
Merge branch 'commitglobal-main'
imdeaconu Sep 26, 2024
7cf3244
Merge branch 'main' of https://github.com/commitglobal/votemonitor in…
imdeaconu Oct 1, 2024
b6abee7
Merge branch 'commitglobal-main-s1'
imdeaconu Oct 1, 2024
cc71856
Merge branch 'commitglobal:main' into main
imdeaconu Oct 2, 2024
e45ea22
Merge branch 'commitglobal:main' into main
imdeaconu Oct 2, 2024
50d15b6
Merge branch 'commitglobal:main' into main
imdeaconu Oct 2, 2024
1ed8e99
Merge branch 'commitglobal:main' into main
imdeaconu Oct 3, 2024
c2f1395
Merge branch 'commitglobal:main' into main
imdeaconu Oct 7, 2024
2c6d5f0
Merge branch 'commitglobal:main' into main
imdeaconu Oct 9, 2024
8c8e18f
Merge branch 'commitglobal:main' into main
imdeaconu Oct 9, 2024
db46a6d
Merge branch 'commitglobal:main' into main
imdeaconu Oct 10, 2024
d4b0263
Merge branch 'commitglobal:main' into main
imdeaconu Oct 12, 2024
79864cd
Merge branch 'commitglobal:main' into main
imdeaconu Oct 14, 2024
5ae7edf
Merge branch 'commitglobal:main' into main
imdeaconu Oct 15, 2024
c0fe98a
Merge branch 'commitglobal:main' into main
imdeaconu Oct 17, 2024
b7b3c5c
Merge branch 'commitglobal:main' into main
imdeaconu Oct 18, 2024
a892349
Merge branch 'commitglobal:main' into main
imdeaconu Oct 19, 2024
2d98793
Merge branch 'commitglobal:main' into main
imdeaconu Oct 20, 2024
66ba8d0
Merge branch 'commitglobal:main' into main
imdeaconu Oct 21, 2024
a86608d
Merge branch 'commitglobal:main' into main
imdeaconu Oct 22, 2024
aa1745c
Merge branch 'commitglobal:main' into main
imdeaconu Oct 23, 2024
9942029
Merge branch 'commitglobal:main' into main
imdeaconu Oct 23, 2024
d855c24
Merge branch 'commitglobal:main' into main
imdeaconu Oct 25, 2024
5a2b99d
Merge branch 'commitglobal:main' into main
imdeaconu Oct 28, 2024
e9ea9a3
Merge branch 'commitglobal:main' into main
imdeaconu Oct 29, 2024
fdaba4b
Merge branch 'commitglobal:main' into main
imdeaconu Oct 29, 2024
777ab43
Merge branch 'commitglobal:main' into main
imdeaconu Nov 4, 2024
15101d6
Merge branch 'commitglobal:main' into main
imdeaconu Nov 6, 2024
9289b11
Merge branch 'commitglobal:main' into main
imdeaconu Nov 28, 2024
a82ea16
Merge branch 'commitglobal:main' into main
imdeaconu Nov 30, 2024
914fc58
Merge branch 'commitglobal:main' into main
imdeaconu Dec 10, 2024
d9640c9
Merge branch 'commitglobal:main' into main
imdeaconu Jan 27, 2025
3e82dc6
Merge branch 'commitglobal:main' into main
imdeaconu Jan 27, 2025
315dfe9
Merge branch 'commitglobal:main' into main
imdeaconu Jan 29, 2025
6e96bf5
Merge branch 'commitglobal:main' into main
imdeaconu Feb 5, 2025
740f04b
WIP: rename prop for alternative filter key in Observer Tags and add …
imdeaconu Feb 14, 2025
fff3a05
WIP: fix push messages receipients query not invalidating after edits
imdeaconu Feb 14, 2025
a3771c5
invalidate targeted observers query after a morning observer is added
imdeaconu Feb 14, 2025
bd2e6bd
Merge remote-tracking branch 'upstream/main'
imdeaconu Feb 18, 2025
3592375
Merge branch 'commitglobal:main' into main
imdeaconu Feb 28, 2025
c580139
Merge branch 'commitglobal:main' into main
imdeaconu Mar 1, 2025
bbb18d4
WIP: add password input
imdeaconu Mar 1, 2025
ea4602e
add PasswordSetterDialog
imdeaconu Mar 1, 2025
ba4bd40
fix types and extract logic for adding backend validation info to for…
imdeaconu Mar 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions web/src/common/form-backend-validation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { AxiosError } from 'axios';
import { FieldValues, Path, UseFormReturn } from 'react-hook-form';
import { ProblemDetails } from './types';

export const addFormValidationErrorsFromBackend = <T extends FieldValues>(
form: UseFormReturn<T>,
error: AxiosError<ProblemDetails>
) => {
error?.response?.data.errors?.forEach((error) => {
form.setError(error.name as Path<T>, { type: 'custom', message: error.reason });
});
};
75 changes: 75 additions & 0 deletions web/src/components/PasswordSetterDialog/PasswordSetterDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { Button } from '@/components/ui/button';
import { Dialog, DialogClose, DialogContent, DialogFooter, DialogTitle } from '@/components/ui/dialog';
import { Form, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
import { PasswordInput } from '@/components/ui/password-input';
import { UseFormReturn } from 'react-hook-form';
import { PasswordSetterFormData } from './usePasswordSetterDialog';

export interface CreateNGODialogProps {
Comment thread
idormenco marked this conversation as resolved.
displayName: string;
open: boolean;
internalOnOpenChange: (open: any) => void;
form: UseFormReturn<PasswordSetterFormData>;
onSubmit: (values: PasswordSetterFormData) => void;
}

function PasswordSetterDialog({ displayName, open, form, internalOnOpenChange, onSubmit }: CreateNGODialogProps) {
return (
<Dialog open={open} onOpenChange={internalOnOpenChange} modal={true}>
<DialogContent
className='min-w-[650px]'
onInteractOutside={(e) => {
e.preventDefault();
}}
onEscapeKeyDown={(e) => {
e.preventDefault();
}}>
<DialogTitle className='mb-3.5'>Set password for {displayName}</DialogTitle>
<div className='flex flex-col gap-3'>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className='space-y-4'>
<FormField
control={form.control}
name='newPassword'
render={({ field, fieldState }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<PasswordInput placeholder='Password' {...field} {...fieldState} />

<FormMessage />
</FormItem>
)}
/>

<FormField
control={form.control}
name='confirmNewPassword'
render={({ field, fieldState }) => (
<FormItem>
<FormLabel>Confirm password</FormLabel>
<PasswordInput placeholder='Confirm password' {...field} {...fieldState} />

<FormMessage />
</FormItem>
)}
/>

<DialogFooter>
<DialogClose asChild>
<Button className='text-purple-900 border border-purple-900 border-input bg-background hover:bg-purple-50 hover:text-purple-600'>
Cancel
</Button>
</DialogClose>
<Button title='Set password' type='submit' className='px-6'>
Set password
</Button>
</DialogFooter>
</form>
</Form>
</div>
</DialogContent>
</Dialog>
);
}

export default PasswordSetterDialog;
105 changes: 105 additions & 0 deletions web/src/components/PasswordSetterDialog/usePasswordSetterDialog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { authApi } from '@/common/auth-api';
import { addFormValidationErrorsFromBackend } from '@/common/form-backend-validation';
import { ProblemDetails } from '@/common/types';
import { zodResolver } from '@hookform/resolvers/zod';
import { useMutation } from '@tanstack/react-query';
import { AxiosError } from 'axios';
import { useCallback, useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { useDialog } from '../ui/use-dialog';
import { toast } from '../ui/use-toast';

const passwordSetterSchema = z
.object({
newPassword: z.string().min(8, 'Password must be at least 8 characters long'),
confirmNewPassword: z.string(),
})
.refine((data) => data.newPassword === data.confirmNewPassword, {
message: 'Passwords do not match',
path: ['confirmNewPassword'],
});

type PasswordSetterUserInfoParams = {
userId: string;
displayName: string;
};

export type PasswordSetterFormData = z.infer<typeof passwordSetterSchema>;

export const usePasswordSetterDialog = () => {
const [userId, setUserId] = useState<string>('');
const [displayName, setDisplayName] = useState<string>('');
const passwordSetterDialog = useDialog();
const { open, onOpenChange } = passwordSetterDialog.dialogProps;

const form = useForm<PasswordSetterFormData>({
resolver: zodResolver(passwordSetterSchema),
defaultValues: {
newPassword: '',
confirmNewPassword: '',
},
});

useEffect(() => {
if (form.formState.isSubmitSuccessful) {
form.reset(undefined, { keepValues: true });
}
}, [form.formState.isSubmitSuccessful]);

const internalOnOpenChange = useCallback(
(open: boolean) => {
if (!open) {
form.reset({
newPassword: '',
confirmNewPassword: '',
});
}
onOpenChange(open);
},
[onOpenChange]
);

const setPasswordMutation = useMutation({
mutationFn: (data: PasswordSetterFormData) => {
return authApi.post<PasswordSetterFormData>(`auth/set-password`, {
aspNetUserId: userId,
newPassword: data.newPassword,
});
},
onSuccess: () => {
form.reset({});
internalOnOpenChange(false);
toast({
title: 'Success',
description: 'Password set',
});
},

onError: (error: AxiosError<ProblemDetails>) => {
addFormValidationErrorsFromBackend<PasswordSetterFormData>(form, error);
toast({
title: 'Error setting password',
description: 'Please contact Platform admins',
variant: 'destructive',
});
},
});

const handlePasswordSet = (params: PasswordSetterUserInfoParams) => {
setUserId(params.userId);
setDisplayName(params.displayName);
passwordSetterDialog.trigger();
};

const onSubmit = (values: PasswordSetterFormData) => {
setPasswordMutation.mutate(values);
};

const passwordSetterDialogProps = { open, form, userId, displayName, onOpenChange, internalOnOpenChange, onSubmit };

return {
passwordSetterDialogProps,
handlePasswordSet,
};
};
28 changes: 14 additions & 14 deletions web/src/components/ui/input.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import { Input as HeadlessInput, type InputProps as HeadlessInputProps } from '@headlessui/react'
import { clsx } from 'clsx'
import { forwardRef } from 'react'
import { Input as HeadlessInput, type InputProps as HeadlessInputProps } from '@headlessui/react';
import { clsx } from 'clsx';
import { forwardRef } from 'react';

const dateTypes = ['date', 'datetime-local', 'month', 'time', 'week']
type DateType = (typeof dateTypes)[number]
const dateTypes = ['date', 'datetime-local', 'month', 'time', 'week'];
type DateType = (typeof dateTypes)[number];

export const Input = forwardRef<
HTMLInputElement,
{ type?: 'email' | 'number' | 'password' | 'search' | 'tel' | 'text' | 'url' | DateType } & HeadlessInputProps
>(function Input({ className, ...props }, ref) {
export type InputProps = {
type?: 'email' | 'number' | 'password' | 'search' | 'tel' | 'text' | 'url' | DateType;
} & HeadlessInputProps;

export const Input = forwardRef<HTMLInputElement, InputProps>(function Input({ className, ...props }, ref) {
return (
<span
data-slot="control"
data-slot='control'
className={clsx([
className,

Expand All @@ -32,8 +33,7 @@ export const Input = forwardRef<

// Invalid state
'before:has-[[data-invalid]]:shadow-red-500/10',
])}
>
])}>
<HeadlessInput
ref={ref}
className={clsx([
Expand Down Expand Up @@ -78,5 +78,5 @@ export const Input = forwardRef<
{...props}
/>
</span>
)
})
);
});
48 changes: 48 additions & 0 deletions web/src/components/ui/password-input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Button } from '@/components/ui/button';
import { Input, InputProps } from '@/components/ui/input';
import { cn } from '@/lib/utils';
import { EyeIcon, EyeOffIcon } from 'lucide-react';
import { forwardRef, useState } from 'react';

const PasswordInput = forwardRef<HTMLInputElement, InputProps>(({ className, ...props }, ref) => {
const [showPassword, setShowPassword] = useState(false);
const disabled = props['value'] === '' || props['value'] === undefined || props['disabled'];

return (
<div className='relative'>
<Input
type={showPassword ? 'text' : 'password'}
className={cn('hide-password-toggle pr-10', className)}
ref={ref}
{...props}
/>
<Button
type='button'
variant='ghost'
size='sm'
className='absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent'
onClick={() => setShowPassword((prev) => !prev)}
disabled={disabled}>
{showPassword && !disabled ? (
<EyeIcon className='h-4 w-4' aria-hidden='true' />
) : (
<EyeOffIcon className='h-4 w-4' aria-hidden='true' />
)}
<span className='sr-only'>{showPassword ? 'Hide password' : 'Show password'}</span>
</Button>

{/* hides browsers password toggles */}
<style>{`
.hide-password-toggle::-ms-reveal,
.hide-password-toggle::-ms-clear {
visibility: hidden;
pointer-events: none;
display: none;
}
`}</style>
</div>
);
});
PasswordInput.displayName = 'PasswordInput';

export { PasswordInput };
4 changes: 2 additions & 2 deletions web/src/features/ngos/components/admins/EditNgoAdmin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,8 @@ export const EditNgoAdmin: FC<EditNgoAdminProps> = ({ ngoAdmin }) => {

const handleDelete = async () => {
await deleteNgoAdminWithConfirmation({
adminId,
name: displayName,
userId: adminId,
displayName,
onMutationSuccess: () => {
navigate({ to: '/ngos/view/$ngoId/$tab', params: { ngoId, tab: 'admins' } });
},
Expand Down
36 changes: 26 additions & 10 deletions web/src/features/ngos/components/admins/NGOAdmins.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import PasswordSetterDialog from '@/components/PasswordSetterDialog/PasswordSetterDialog';
import { usePasswordSetterDialog } from '@/components/PasswordSetterDialog/usePasswordSetterDialog';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { DataTableColumnHeader } from '@/components/ui/DataTable/DataTableColumnHeader';
Expand Down Expand Up @@ -36,6 +38,7 @@ export const NGOAdminsView: FC<NGOAdminsViewProps> = ({ ngoId }) => {
const { isFilteringContainerVisible, toggleFilteringContainerVisibility } = useFilteringContainer();
const addNgoAdminDialog = useDialog();
const { queryParams, searchText, handleSearchInput } = useDebouncedSearch(Route.id, ngoAdminsSearchParamsSchema);
const { passwordSetterDialogProps, handlePasswordSet } = usePasswordSetterDialog();

const navigateToViewNgoAdmin = useCallback(
(adminId: string) => navigate({ to: '/ngos/admin/$ngoId/$adminId/view', params: { ngoId, adminId } }),
Expand Down Expand Up @@ -85,9 +88,9 @@ export const NGOAdminsView: FC<NGOAdminsViewProps> = ({ ngoId }) => {
{
id: 'actions',
cell: ({ row }) => {
const adminId = row.original.id;
const adminName = `${row.original.firstName} ${row.original.lastName}`;
const isAdminActive = row.original.status === NgoAdminStatus.Active;
const userId = row.original.id;
const displayName = `${row.original.firstName} ${row.original.lastName}`;
const isUserActive = row.original.status === NgoAdminStatus.Active;

return (
<div className='text-right'>
Expand All @@ -99,22 +102,34 @@ export const NGOAdminsView: FC<NGOAdminsViewProps> = ({ ngoId }) => {
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
<DropdownMenuItem onClick={() => navigateToViewNgoAdmin(adminId)}>View</DropdownMenuItem>
<DropdownMenuItem onClick={() => navigateToEditNgoAdmin(adminId)}>Edit</DropdownMenuItem>
<DropdownMenuItem onClick={() => navigateToViewNgoAdmin(userId)}>View</DropdownMenuItem>
<DropdownMenuItem
onClick={(e) => {
navigateToEditNgoAdmin(userId);
}}>
Edit
</DropdownMenuItem>
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
isAdminActive
? deactivateNgoAdminMutation.mutate(adminId)
: activateNgoAdminMutation.mutate(adminId);
isUserActive ? deactivateNgoAdminMutation.mutate(userId) : activateNgoAdminMutation.mutate(userId);
}}>
{!isAdminActive ? 'Activate' : 'Deactivate'}
{!isUserActive ? 'Activate' : 'Deactivate'}
</DropdownMenuItem>

<DropdownMenuItem
onClick={async (e) => {
e.stopPropagation();
handlePasswordSet({ userId, displayName });
}}>
Set password
</DropdownMenuItem>

<DropdownMenuItem
className='text-red-600'
onClick={async (e) => {
e.stopPropagation();
await deleteNgoAdminWithConfirmation({ adminId, name: adminName });
await deleteNgoAdminWithConfirmation({ userId, displayName });
}}>
Delete
</DropdownMenuItem>
Expand Down Expand Up @@ -158,6 +173,7 @@ export const NGOAdminsView: FC<NGOAdminsViewProps> = ({ ngoId }) => {
queryParams={queryParams}
// onRowClick={(id) => navigateToViewNgoAdmin(id)}
/>
<PasswordSetterDialog {...passwordSetterDialogProps} />
</CardContent>
</Card>
);
Expand Down
Loading