Refactor updateIsUserAdminById and affected code

This commit is contained in:
Filip Sodić 2025-02-19 17:52:16 +01:00
parent fe73757138
commit 15551ab381
6 changed files with 186 additions and 111 deletions

View File

@ -1,33 +0,0 @@
import { type User } from 'wasp/entities';
import { useState } from 'react';
import { cn } from '../../../client/cn';
const SwitcherOne = ({ user, updateIsUserAdminById }: { user?: Partial<User>; updateIsUserAdminById?: any }) => {
const [enabled, setEnabled] = useState<boolean>(user?.isAdmin || false);
return (
<div className='relative'>
<label htmlFor={`toggle1-${user?.id}`} className='flex cursor-pointer select-none items-center'>
<div className='relative'>
<input
type='checkbox'
id={`toggle1-${user?.id}`}
className='sr-only'
onChange={() => {
setEnabled(!enabled);
updateIsUserAdminById && updateIsUserAdminById({ id: user?.id, data: { isAdmin: !enabled } });
}}
/>
<div className='reblock h-8 w-14 rounded-full bg-meta-9 dark:bg-[#5A616B]'></div>
<div
className={cn('absolute left-1 top-1 h-6 w-6 rounded-full bg-white dark:bg-gray-400 transition', {
'!right-1 !translate-x-full !bg-primary dark:!bg-white': enabled,
})}
></div>
</div>
</label>
</div>
);
};
export default SwitcherOne;

View File

@ -1,9 +1,17 @@
import { type SubscriptionStatus } from '../../../payment/plans';
import { updateIsUserAdminById, useQuery, getPaginatedUsers } from 'wasp/client/operations';
import { useQuery, getPaginatedUsers } from 'wasp/client/operations';
import { useState, useEffect } from 'react';
import SwitcherOne from './SwitcherOne';
import SwitcherOne from '../../elements/forms/SwitcherOne';
import LoadingSpinner from '../../layout/LoadingSpinner';
import DropdownEditDelete from './DropdownEditDelete';
import { updateIsUserAdminById } from 'wasp/client/operations';
import { type User } from 'wasp/entities';
const AdminSwitch = ({ id, isAdmin }: Pick<User, 'id' | 'isAdmin'>) => {
return (
<SwitcherOne isOn={isAdmin} onChange={() => updateIsUserAdminById({ id: id, isAdmin: !isAdmin })} />
);
};
const UsersTable = () => {
const [skip, setskip] = useState(0);
@ -11,7 +19,7 @@ const UsersTable = () => {
const [email, setEmail] = useState<string | undefined>(undefined);
const [isAdminFilter, setIsAdminFilter] = useState<boolean | undefined>(undefined);
const [statusOptions, setStatusOptions] = useState<SubscriptionStatus[]>([]);
const { data, isLoading, error } = useQuery(getPaginatedUsers, {
const { data, isLoading } = useQuery(getPaginatedUsers, {
skip,
emailContains: email,
isAdmin: isAdminFilter,
@ -51,7 +59,7 @@ const UsersTable = () => {
<div className='flex-grow relative z-20 rounded border border-stroke pr-8 outline-none bg-white transition focus:border-primary active:border-primary dark:border-form-strokedark dark:bg-form-input'>
<div className='flex items-center'>
{!!statusOptions && statusOptions.length > 0 ? (
statusOptions.map((opt, idx) => (
statusOptions.map((opt) => (
<span
key={opt}
className='z-30 flex items-center my-1 mx-2 py-1 px-2 outline-none transition focus:border-primary active:border-primary disabled:cursor-default disabled:bg-whiter dark:border-form-strokedark dark:bg-form-input dark:focus:border-primary'
@ -109,7 +117,11 @@ const UsersTable = () => {
<option value=''>Select filters</option>
{['past_due', 'canceled', 'active', 'deleted', null].map((status) => {
if (!statusOptions.includes(status as SubscriptionStatus)) {
return <option value={status || ''}>{status ? status : 'has not subscribed'}</option>;
return (
<option key={status} value={status || ''}>
{status ? status : 'has not subscribed'}
</option>
);
}
})}
</select>
@ -226,7 +238,7 @@ const UsersTable = () => {
</div>
<div className='col-span-1 flex items-center'>
<div className='text-sm text-black dark:text-white'>
<SwitcherOne user={user} updateIsUserAdminById={updateIsUserAdminById} />
<AdminSwitch {...user} />
</div>
</div>
<div className='col-span-1 flex items-center'>

View File

@ -2,9 +2,10 @@ import { type AuthUser } from 'wasp/auth';
import Breadcrumb from '../../layout/Breadcrumb';
import DefaultLayout from '../../layout/DefaultLayout';
import CheckboxOne from './CheckboxOne';
import SwitcherOne from '../../dashboards/users/SwitcherOne';
import SwitcherTwo from './SwitcherTwo';
import SwitcherOne from './SwitcherOne';
import { useRedirectHomeUnlessUserIsAdmin } from '../../useRedirectHomeUnlessUserIsAdmin';
import { useState } from 'react';
const FormElements = ({ user }: { user: AuthUser }) => {
useRedirectHomeUnlessUserIsAdmin({ user });
@ -56,10 +57,7 @@ const FormElements = ({ user }: { user: AuthUser }) => {
<div className='border-b border-stroke py-4 px-6.5 dark:border-strokedark'>
<h3 className='font-medium text-black dark:text-white'>Toggle switch input</h3>
</div>
<div className='flex flex-col gap-5.5 p-6.5'>
<SwitcherOne />
<SwitcherTwo />
</div>
<SwitchExamples />
</div>
{/* <!-- Time and date --> */}
@ -172,7 +170,13 @@ const FormElements = ({ user }: { user: AuthUser }) => {
<label className='mb-3 block text-black dark:text-white'>Select Country</label>
<div className='relative z-20 bg-white dark:bg-form-input'>
<span className='absolute top-1/2 left-4 z-30 -translate-y-1/2'>
<svg width='20' height='20' viewBox='0 0 20 20' fill='none' xmlns='http://www.w3.org/2000/svg'>
<svg
width='20'
height='20'
viewBox='0 0 20 20'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<g opacity='0.8'>
<path
fillRule='evenodd'
@ -201,7 +205,13 @@ const FormElements = ({ user }: { user: AuthUser }) => {
<option value=''>Canada</option>
</select>
<span className='absolute top-1/2 right-4 z-10 -translate-y-1/2'>
<svg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'>
<svg
width='24'
height='24'
viewBox='0 0 24 24'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<g opacity='0.8'>
<path
fillRule='evenodd'
@ -222,7 +232,13 @@ const FormElements = ({ user }: { user: AuthUser }) => {
<span className='m-1.5 flex items-center justify-center rounded border-[.5px] border-stroke bg-gray py-1.5 px-2.5 text-sm font-medium dark:border-strokedark dark:bg-white/30'>
Design
<span className='cursor-pointer pl-2 hover:text-danger'>
<svg width='12' height='12' viewBox='0 0 12 12' fill='none' xmlns='http://www.w3.org/2000/svg'>
<svg
width='12'
height='12'
viewBox='0 0 12 12'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<path
fillRule='evenodd'
clipRule='evenodd'
@ -235,7 +251,13 @@ const FormElements = ({ user }: { user: AuthUser }) => {
<span className='m-1.5 flex items-center justify-center rounded border-[.5px] border-stroke bg-gray py-1.5 px-2.5 text-sm font-medium dark:border-strokedark dark:bg-white/30'>
Development
<span className='cursor-pointer pl-2 hover:text-danger'>
<svg width='12' height='12' viewBox='0 0 12 12' fill='none' xmlns='http://www.w3.org/2000/svg'>
<svg
width='12'
height='12'
viewBox='0 0 12 12'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<path
fillRule='evenodd'
clipRule='evenodd'
@ -246,12 +268,22 @@ const FormElements = ({ user }: { user: AuthUser }) => {
</span>
</span>
</div>
<select name='' id='' className='absolute top-0 left-0 z-20 h-full w-full bg-transparent opacity-0'>
<select
name=''
id=''
className='absolute top-0 left-0 z-20 h-full w-full bg-transparent opacity-0'
>
<option value=''>Option</option>
<option value=''>Option</option>
</select>
<span className='absolute top-1/2 right-4 z-10 -translate-y-1/2'>
<svg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'>
<svg
width='24'
height='24'
viewBox='0 0 24 24'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<g opacity='0.8'>
<path
fillRule='evenodd'
@ -272,4 +304,15 @@ const FormElements = ({ user }: { user: AuthUser }) => {
);
};
function SwitchExamples() {
const [isFirstOn, setIsFirstOn] = useState<boolean>(false);
const [isSecondOn, setIsSecondOn] = useState<boolean>(false);
return (
<div className='flex flex-col gap-5.5 p-6.5'>
<SwitcherOne isOn={isFirstOn} onChange={() => setIsFirstOn(!isFirstOn)} />
<SwitcherTwo isOn={isSecondOn} onChange={() => setIsSecondOn(!isSecondOn)} />
</div>
);
}
export default FormElements;

View File

@ -0,0 +1,28 @@
import { cn } from '../../../client/cn';
import { ChangeEventHandler } from 'react';
const SwitcherOne = ({
isOn,
onChange,
}: {
isOn: boolean;
onChange: ChangeEventHandler<HTMLInputElement>;
}) => {
return (
<div className='relative'>
<label className='flex cursor-pointer select-none items-center'>
<div className='relative'>
<input type='checkbox' className='sr-only' onChange={onChange} />
<div className='reblock h-8 w-14 rounded-full bg-meta-9 dark:bg-[#5A616B]'></div>
<div
className={cn('absolute left-1 top-1 h-6 w-6 rounded-full bg-white dark:bg-gray-400 transition', {
'!right-1 !translate-x-full !bg-primary dark:!bg-white': isOn,
})}
></div>
</div>
</label>
</div>
);
};
export default SwitcherOne;

View File

@ -1,60 +1,32 @@
import { useState } from 'react';
import { ChangeEventHandler, useState } from 'react';
import { cn } from '../../../client/cn';
const SwitcherTwo = () => {
const [enabled, setEnabled] = useState(false);
const SwitcherTwo = ({
isOn,
onChange,
}: {
isOn: boolean;
onChange: ChangeEventHandler<HTMLInputElement>;
}) => {
return (
<div>
<label htmlFor='toggle3' className='flex cursor-pointer select-none items-center'>
<label className='flex cursor-pointer select-none items-center'>
<div className='relative'>
<input
type='checkbox'
id='toggle3'
className='sr-only'
onChange={() => {
setEnabled(!enabled);
}}
/>
<input type='checkbox' id='toggle3' className='sr-only' onChange={onChange} />
<div className='block h-8 w-14 rounded-full bg-meta-9 dark:bg-[#5A616B]'></div>
<div
className={cn(
'dot absolute left-1 top-1 flex h-6 w-6 items-center justify-center rounded-full bg-white transition',
{
'!right-1 !translate-x-full !bg-primary dark:!bg-white': enabled,
'!right-1 !translate-x-full !bg-primary dark:!bg-white': isOn,
}
)}
>
<span className={cn('hidden', { '!block': enabled })}>
<svg
className='fill-white dark:fill-black'
width='11'
height='8'
viewBox='0 0 11 8'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<path
d='M10.0915 0.951972L10.0867 0.946075L10.0813 0.940568C9.90076 0.753564 9.61034 0.753146 9.42927 0.939309L4.16201 6.22962L1.58507 3.63469C1.40401 3.44841 1.11351 3.44879 0.932892 3.63584C0.755703 3.81933 0.755703 4.10875 0.932892 4.29224L0.932878 4.29225L0.934851 4.29424L3.58046 6.95832C3.73676 7.11955 3.94983 7.2 4.1473 7.2C4.36196 7.2 4.55963 7.11773 4.71406 6.9584L10.0468 1.60234C10.2436 1.4199 10.2421 1.1339 10.0915 0.951972ZM4.2327 6.30081L4.2317 6.2998C4.23206 6.30015 4.23237 6.30049 4.23269 6.30082L4.2327 6.30081Z'
fill=''
stroke=''
strokeWidth='0.4'
></path>
</svg>
<span className={cn('hidden', { '!block': isOn })}>
<CheckIcon />
</span>
<span
className={cn({
hidden: enabled,
})}
>
<svg
className='h-4 w-4 stroke-current'
fill='none'
viewBox='0 0 24 24'
xmlns='http://www.w3.org/2000/svg'
>
<path strokeLinecap='round' strokeLinejoin='round' strokeWidth='2' d='M6 18L18 6M6 6l12 12'></path>
</svg>
<span className={cn({ hidden: isOn })}>
<XIcon />
</span>
</div>
</div>
@ -63,4 +35,37 @@ const SwitcherTwo = () => {
);
};
const XIcon = () => {
return (
<svg
className='h-4 w-4 stroke-current'
fill='none'
viewBox='0 0 24 24'
xmlns='http://www.w3.org/2000/svg'
>
<path strokeLinecap='round' strokeLinejoin='round' strokeWidth='2' d='M6 18L18 6M6 6l12 12'></path>
</svg>
);
};
const CheckIcon = () => {
return (
<svg
className='fill-white dark:fill-black'
width='11'
height='8'
viewBox='0 0 11 8'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<path
d='M10.0915 0.951972L10.0867 0.946075L10.0813 0.940568C9.90076 0.753564 9.61034 0.753146 9.42927 0.939309L4.16201 6.22962L1.58507 3.63469C1.40401 3.44841 1.11351 3.44879 0.932892 3.63584C0.755703 3.81933 0.755703 4.10875 0.932892 4.29224L0.932878 4.29225L0.934851 4.29424L3.58046 6.95832C3.73676 7.11955 3.94983 7.2 4.1473 7.2C4.36196 7.2 4.55963 7.11773 4.71406 6.9584L10.0468 1.60234C10.2436 1.4199 10.2421 1.1339 10.0915 0.951972ZM4.2327 6.30081L4.2317 6.2998C4.23206 6.30015 4.23237 6.30049 4.23269 6.30082L4.2327 6.30081Z'
fill=''
stroke=''
strokeWidth='0.4'
></path>
</svg>
);
};
export default SwitcherTwo;

View File

@ -7,31 +7,28 @@ import { type User } from 'wasp/entities';
import { HttpError } from 'wasp/server';
import { type SubscriptionStatus } from '../payment/plans';
export const updateIsUserAdminById: UpdateIsUserAdminById<{ id: string; data: Pick<User, 'isAdmin'> }, User> = async (
{ id, data },
export const updateIsUserAdminById: UpdateIsUserAdminById<Pick<User, 'id' | 'isAdmin'>, User> = async (
{ id, isAdmin },
context
) => {
if (!context.user) {
throw new HttpError(401);
throw new HttpError(401, 'Only authenticated users are allowed to perform this operation');
}
if (!context.user.isAdmin) {
throw new HttpError(403);
throw new HttpError(403, 'Only admins are allowed to perform this operation');
}
const updatedUser = await context.entities.User.update({
where: {
id,
},
data: {
isAdmin: data.isAdmin,
},
return context.entities.User.update({
where: { id },
data: { isAdmin },
});
return updatedUser;
};
export const updateCurrentUserLastActiveTimestamp: UpdateCurrentUserLastActiveTimestamp<Pick<User, 'lastActiveTimestamp'>, User> = async ({ lastActiveTimestamp }, context) => {
export const updateCurrentUserLastActiveTimestamp: UpdateCurrentUserLastActiveTimestamp<
Pick<User, 'lastActiveTimestamp'>,
User
> = async ({ lastActiveTimestamp }, context) => {
if (!context.user) {
throw new HttpError(401);
}
@ -40,7 +37,7 @@ export const updateCurrentUserLastActiveTimestamp: UpdateCurrentUserLastActiveTi
where: {
id: context.user.id,
},
data: {lastActiveTimestamp},
data: { lastActiveTimestamp },
});
};
@ -52,7 +49,28 @@ type GetPaginatedUsersInput = {
subscriptionStatus?: SubscriptionStatus[];
};
type GetPaginatedUsersOutput = {
users: Pick<User, 'id' | 'email' | 'username' | 'lastActiveTimestamp' | 'subscriptionStatus' | 'paymentProcessorUserId'>[];
users: Pick<
User,
| 'id'
| 'email'
| 'username'
| 'lastActiveTimestamp'
| 'subscriptionStatus'
| 'paymentProcessorUserId'
| 'isAdmin'
>[];
totalPages: number;
};
type GetPaginatedUsersOutput2 = {
users: Array<{
id: User['id'];
email: User['email'];
username: User['username'];
lastActiveTimestamp: User['lastActiveTimestamp'];
subscriptionStatus: User['subscriptionStatus'];
paymentProcessorUserId: User['paymentProcessorUserId'];
}>;
totalPages: number;
};
@ -65,8 +83,10 @@ export const getPaginatedUsers: GetPaginatedUsers<GetPaginatedUsersInput, GetPag
}
const allSubscriptionStatusOptions = args.subscriptionStatus as Array<string | null> | undefined;
const hasNotSubscribed = allSubscriptionStatusOptions?.find((status) => status === null)
let subscriptionStatusStrings = allSubscriptionStatusOptions?.filter((status) => status !== null) as string[] | undefined
const hasNotSubscribed = allSubscriptionStatusOptions?.find((status) => status === null);
const subscriptionStatusStrings = allSubscriptionStatusOptions?.filter((status) => status !== null) as
| string[]
| undefined;
const queryResults = await context.entities.User.findMany({
skip: args.skip,