From b81ea9d5882327f386667bd40322abccea117320 Mon Sep 17 00:00:00 2001 From: Alan Chang Date: Fri, 26 Jan 2024 04:19:57 -0800 Subject: [PATCH 01/17] Add filtering by text/role/status, sorting, pagination, collection prefs via collection hooks --- apps/site/package.json | 1 + .../components/ParticipantsTable.tsx | 246 ++++++++++++++---- pnpm-lock.yaml | 11 +- 3 files changed, 206 insertions(+), 52 deletions(-) diff --git a/apps/site/package.json b/apps/site/package.json index d3d37fb6..875a6080 100644 --- a/apps/site/package.json +++ b/apps/site/package.json @@ -9,6 +9,7 @@ "lint": "next lint" }, "dependencies": { + "@cloudscape-design/collection-hooks": "^1.0.34", "@cloudscape-design/components": "^3.0.475", "@cloudscape-design/global-styles": "^1.0.20", "@fireworks-js/react": "^2.10.7", diff --git a/apps/site/src/app/admin/participants/components/ParticipantsTable.tsx b/apps/site/src/app/admin/participants/components/ParticipantsTable.tsx index 4e7e55a1..502892f1 100644 --- a/apps/site/src/app/admin/participants/components/ParticipantsTable.tsx +++ b/apps/site/src/app/admin/participants/components/ParticipantsTable.tsx @@ -1,7 +1,13 @@ -import { useCallback } from "react"; +import { ReactElement, useCallback, useState } from "react"; +import { useCollection } from "@cloudscape-design/collection-hooks"; import Box from "@cloudscape-design/components/box"; +import Button from "@cloudscape-design/components/button"; +import CollectionPreferences from "@cloudscape-design/components/collection-preferences"; +import FormField from "@cloudscape-design/components/form-field"; import Header from "@cloudscape-design/components/header"; +import Pagination from "@cloudscape-design/components/pagination"; +import Select from "@cloudscape-design/components/select"; import SpaceBetween from "@cloudscape-design/components/space-between"; import Table from "@cloudscape-design/components/table"; import TextFilter from "@cloudscape-design/components/text-filter"; @@ -12,6 +18,12 @@ import { Participant } from "@/lib/admin/useParticipants"; import ParticipantAction from "./ParticipantAction"; import RoleBadge from "./RoleBadge"; +interface EmptyStateProps { + title: string, + subtitle?: string, + action?: ReactElement, +} + interface ParticipantsTableProps { participants: Participant[]; loading: boolean; @@ -19,15 +31,83 @@ interface ParticipantsTableProps { initiatePromotion: (participant: Participant) => void; } +const ALL_ROLES = { value: '0', label: 'All roles' }; +const ALL_STATUSES = { value: '0', label: 'All statuses' }; +const SEARCHABLE_COLUMNS = ['_id', 'first_name', 'last_name', 'role', 'status'] + +function createLabelFunction(columnName: string) { + return ({ sorted, descending }: { sorted: boolean, descending: boolean }) => { + const sortState = sorted ? `sorted ${descending ? 'descending' : 'ascending'}` : 'not sorted'; + return `${columnName}, ${sortState}.`; + }; +} + +function EmptyState({ title, subtitle, action }: EmptyStateProps) { + return ( + + + {title} + + + {subtitle} + + {action} + + ); +} + function ParticipantsTable({ participants, loading, initiateCheckIn, initiatePromotion, }: ParticipantsTableProps) { - // TODO: sorting - // TODO: search functionality - // TODO: role and status filters + const [preferences, setPreferences] = useState({ + pageSize: 20, + visibleContent: ['uid', 'firstName', 'lastName', 'role', 'status', 'action'], + }); + const [filterRole, setFilterRole] = useState(ALL_ROLES); + const [filterStatus, setFilterStatus] = useState(ALL_STATUSES); + const matchesRole = (p: Participant) => filterRole === ALL_ROLES || p.role === filterRole.label; + const matchesStatus = (p: Participant) => filterStatus === ALL_STATUSES || p.status === filterStatus.label; + + const { + items, + actions, + filteredItemsCount, + collectionProps, + filterProps, + paginationProps, + } = useCollection( + participants, + { + filtering: { + empty: , + noMatch: ( + actions.setFiltering('')}>Clear filter} + /> + ), + filteringFunction: (item, filteringText) => { + if (!matchesRole(item)) { return false; } + if (!matchesStatus(item)) { return false; } + const filteringTextLC = filteringText.toLowerCase(); + + return SEARCHABLE_COLUMNS.map(key => item[key]).some( + value => typeof value === 'string' && value.toLowerCase().indexOf(filteringTextLC) > -1 + ) + }, + }, + pagination: { pageSize: preferences.pageSize }, + sorting: {}, + selection: {}, + } + ); + const allRoles = new Set(items.map(p => p.role)); + const roleOptions = Array.from(allRoles).map(r => ({ value: r, label: r })); + const allStatuses = new Set(items.map(p => p.status)); + const statusOptions = Array.from(allStatuses).map(s => ({ value: s, label: s })); const ActionCell = useCallback( (participant: Participant) => ( @@ -40,6 +120,50 @@ function ParticipantsTable({ [initiateCheckIn, initiatePromotion], ); + const columnDefinitions = [ + { + id: "uid", + header: "UID", + cell: (item: Participant) => item._id, + ariaLabel: createLabelFunction("UID"), + sortingField: "_id", + isRowHeader: true, + }, + { + id: "firstName", + header: "First name", + cell: (item: Participant) => item.first_name, + ariaLabel: createLabelFunction("First name"), + sortingField: "first_name", + }, + { + id: "lastName", + header: "Last name", + cell: (item: Participant) => item.last_name, + ariaLabel: createLabelFunction("Last name"), + sortingField: "last_name", + }, + { + id: "role", + header: "Role", + cell: RoleBadge, + ariaLabel: createLabelFunction("Role"), + sortingField: "role", + }, + { + id: "status", + header: "Status", + cell: ApplicantStatus, + ariaLabel: createLabelFunction("status"), + sortingField: "status", + }, + { + id: "action", + header: "Action", + cell: ActionCell, + }, + ]; + const emptyMessage = ( @@ -50,49 +174,13 @@ function ParticipantsTable({ return ( item._id, - sortingField: "uid", - isRowHeader: true, - }, - { - id: "firstName", - header: "First name", - cell: (item) => item.first_name, - sortingField: "firstName", - }, - { - id: "lastName", - header: "Last name", - cell: (item) => item.last_name, - sortingField: "lastName", - }, - { - id: "role", - header: "Role", - cell: RoleBadge, - sortingField: "role", - }, - { - id: "status", - header: "Status", - cell: ApplicantStatus, - sortingField: "status", - }, - { - id: "action", - header: "Action", - cell: ActionCell, - }, - ]} + {...collectionProps} header={
Participants
} - items={participants} + columnDefinitions={columnDefinitions} + visibleColumns={preferences.visibleContent} + items={items} loading={loading} loadingText="Loading participants" resizableColumns @@ -100,11 +188,71 @@ function ParticipantsTable({ stickyColumns={{ first: 1, last: 0 }} trackBy="_id" empty={emptyMessage} - filter={ - - } - // TODO: pagination - // TODO: collection preferences + filter={( + <> + + + setFilterStatus(event.detail.selectedOption as { value: string, label: string })} + expandToViewport={true} + /> + + + )} + pagination={( + `Go to page ${pageNumber}`, + previousPageLabel: "Previous page", + }} + /> + )} + preferences={( + ({ id, label: header })) + } + ], + }} + cancelLabel="Cancel" + confirmLabel="Confirm" + title="Preferences" + preferences={preferences} + onConfirm={({ detail }) => setPreferences(detail as { pageSize: number, visibleContent: Array })} + /> + )} /> ); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 430e3049..2955a92c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -84,6 +84,9 @@ importers: apps/site: dependencies: + '@cloudscape-design/collection-hooks': + specifier: ^1.0.34 + version: 1.0.34(react@18.2.0) '@cloudscape-design/components': specifier: ^3.0.475 version: 3.0.475(react-dom@18.2.0)(react@18.2.0) @@ -1515,8 +1518,8 @@ packages: '@babel/helper-validator-identifier': 7.22.20 to-fast-properties: 2.0.0 - /@cloudscape-design/collection-hooks@1.0.32(react@18.2.0): - resolution: {integrity: sha512-wdOGW+W8+5rYYSGPab/v7BOy0P8mC6aUtwwN3w2AWE3n3LrK/Wqzsx77r/m4zlLdtkhZFnsnSBpT14OIw8ve/Q==} + /@cloudscape-design/collection-hooks@1.0.34(react@18.2.0): + resolution: {integrity: sha512-8ggOJRX4PpiT6YCv7cHSjyx27VUgJX7s25KiLaUbVvm5EeWBpKxrwDwE6w9JjNxrkXeepjs4xyNjaNie2ZiCeQ==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: @@ -1536,7 +1539,7 @@ packages: react: ^16.8 || ^17 || ^18 react-dom: ^16.8 || ^17 || ^18 dependencies: - '@cloudscape-design/collection-hooks': 1.0.32(react@18.2.0) + '@cloudscape-design/collection-hooks': 1.0.34(react@18.2.0) '@cloudscape-design/component-toolkit': 1.0.0-beta.30 '@cloudscape-design/test-utils-core': 1.0.21 '@cloudscape-design/theming-runtime': 1.0.39 @@ -1563,6 +1566,7 @@ packages: /@cloudscape-design/global-styles@1.0.20: resolution: {integrity: sha512-eEU3o7fZSRtIQVcFj1vRtheDFktRdAMMIidihrpNHqWVZzazFe0Z22vll684Yzy9TQ5R0HXKKupZLgKyC+Ia8w==} dev: false + bundledDependencies: false /@cloudscape-design/test-utils-core@1.0.21: resolution: {integrity: sha512-Kjxtl1ImQLmJ5SJ3PNF0hrtEbKidcZqk3E+iY4dLbJOI3sWh2TwoEpH5i4QEfO2GMtkMNzi41V4mmI4e4sydAw==} @@ -5412,6 +5416,7 @@ packages: loose-envify: 1.4.0 object-assign: 4.1.1 dev: false + bundledDependencies: false /create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} From 3d973031c4d0e6ef24d93af37165e3264da87da7 Mon Sep 17 00:00:00 2001 From: Alan Chang Date: Fri, 26 Jan 2024 10:30:32 -0800 Subject: [PATCH 02/17] Fix prettier sgs --- .../components/ParticipantsTable.tsx | 40 ++++++++++++------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/apps/site/src/app/admin/participants/components/ParticipantsTable.tsx b/apps/site/src/app/admin/participants/components/ParticipantsTable.tsx index 502892f1..26c1c893 100644 --- a/apps/site/src/app/admin/participants/components/ParticipantsTable.tsx +++ b/apps/site/src/app/admin/participants/components/ParticipantsTable.tsx @@ -19,9 +19,9 @@ import ParticipantAction from "./ParticipantAction"; import RoleBadge from "./RoleBadge"; interface EmptyStateProps { - title: string, - subtitle?: string, - action?: ReactElement, + title: string; + subtitle?: string; + action?: ReactElement; } interface ParticipantsTableProps { @@ -31,13 +31,15 @@ interface ParticipantsTableProps { initiatePromotion: (participant: Participant) => void; } -const ALL_ROLES = { value: '0', label: 'All roles' }; -const ALL_STATUSES = { value: '0', label: 'All statuses' }; -const SEARCHABLE_COLUMNS = ['_id', 'first_name', 'last_name', 'role', 'status'] +const ALL_ROLES = { value: "0", label: "All roles" }; +const ALL_STATUSES = { value: "0", label: "All roles" }; +const SEARCHABLE_COLUMNS = ["_id", "first_name", "last_name", "role", "status"] function createLabelFunction(columnName: string) { - return ({ sorted, descending }: { sorted: boolean, descending: boolean }) => { - const sortState = sorted ? `sorted ${descending ? 'descending' : 'ascending'}` : 'not sorted'; + return ({ sorted, descending }: { sorted: boolean; descending: boolean }) => { + const sortState = sorted + ? `sorted ${descending ? "descending" : "ascending"}` + : "not sorted"; return `${columnName}, ${sortState}.`; }; } @@ -48,7 +50,7 @@ function EmptyState({ title, subtitle, action }: EmptyStateProps) { {title} - + {subtitle} {action} @@ -64,7 +66,14 @@ function ParticipantsTable({ }: ParticipantsTableProps) { const [preferences, setPreferences] = useState({ pageSize: 20, - visibleContent: ['uid', 'firstName', 'lastName', 'role', 'status', 'action'], + visibleContent: [ + "uid", + "firstName", + "lastName", + "role", + "status", + "action", + ], }); const [filterRole, setFilterRole] = useState(ALL_ROLES); const [filterStatus, setFilterStatus] = useState(ALL_STATUSES); @@ -222,12 +231,12 @@ function ParticipantsTable({ {...paginationProps} ariaLabels={{ nextPageLabel: "Next page", - pageLabel: pageNumber => `Go to page ${pageNumber}`, + pageLabel: (pageNumber) => `Go to page ${pageNumber}`, previousPageLabel: "Previous page", }} /> )} - preferences={( + preferences={ ({ id, label: header })) + options: columnDefinitions.map(({ id, header }) => ({ + id, + label: header, + })) } ], }} @@ -252,7 +264,7 @@ function ParticipantsTable({ preferences={preferences} onConfirm={({ detail }) => setPreferences(detail as { pageSize: number, visibleContent: Array })} /> - )} + } /> ); } From 4c813fcd4bc99545e5aca1400bcb0bd833c61790 Mon Sep 17 00:00:00 2001 From: Alan Chang Date: Fri, 26 Jan 2024 10:32:39 -0800 Subject: [PATCH 03/17] Fix more prettier sgs --- .../participants/components/ParticipantsTable.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/site/src/app/admin/participants/components/ParticipantsTable.tsx b/apps/site/src/app/admin/participants/components/ParticipantsTable.tsx index 26c1c893..73b6e939 100644 --- a/apps/site/src/app/admin/participants/components/ParticipantsTable.tsx +++ b/apps/site/src/app/admin/participants/components/ParticipantsTable.tsx @@ -33,7 +33,7 @@ interface ParticipantsTableProps { const ALL_ROLES = { value: "0", label: "All roles" }; const ALL_STATUSES = { value: "0", label: "All roles" }; -const SEARCHABLE_COLUMNS = ["_id", "first_name", "last_name", "role", "status"] +const SEARCHABLE_COLUMNS = ["_id", "first_name", "last_name", "role", "status"]; function createLabelFunction(columnName: string) { return ({ sorted, descending }: { sorted: boolean; descending: boolean }) => { @@ -226,7 +226,7 @@ function ParticipantsTable({ )} - pagination={( + pagination={ - )} + } preferences={ ({ id, label: header, - })) - } + })), + }, ], }} cancelLabel="Cancel" From feca5aaa7220c0f1dc6278565e5821f41e43ee9f Mon Sep 17 00:00:00 2001 From: Alan Chang Date: Fri, 26 Jan 2024 10:33:52 -0800 Subject: [PATCH 04/17] One more prettier error --- .../app/admin/participants/components/ParticipantsTable.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/site/src/app/admin/participants/components/ParticipantsTable.tsx b/apps/site/src/app/admin/participants/components/ParticipantsTable.tsx index 73b6e939..bb391e2d 100644 --- a/apps/site/src/app/admin/participants/components/ParticipantsTable.tsx +++ b/apps/site/src/app/admin/participants/components/ParticipantsTable.tsx @@ -197,7 +197,7 @@ function ParticipantsTable({ stickyColumns={{ first: 1, last: 0 }} trackBy="_id" empty={emptyMessage} - filter={( + filter={ <> - )} + } pagination={ Date: Fri, 26 Jan 2024 12:44:58 -0800 Subject: [PATCH 05/17] Change select->multi, add searching by composite name, three columns filter --- .../components/ParticipantsFilters.tsx | 69 +++++++++++++++++++ .../components/ParticipantsTable.tsx | 63 ++++++----------- 2 files changed, 91 insertions(+), 41 deletions(-) create mode 100644 apps/site/src/app/admin/participants/components/ParticipantsFilters.tsx diff --git a/apps/site/src/app/admin/participants/components/ParticipantsFilters.tsx b/apps/site/src/app/admin/participants/components/ParticipantsFilters.tsx new file mode 100644 index 00000000..bcbdd268 --- /dev/null +++ b/apps/site/src/app/admin/participants/components/ParticipantsFilters.tsx @@ -0,0 +1,69 @@ +import { Dispatch, SetStateAction } from "react"; + +import FormField from "@cloudscape-design/components/form-field"; +import Multiselect from "@cloudscape-design/components/multiselect"; +import SpaceBetween from "@cloudscape-design/components/space-between"; +import TextFilter, { TextFilterProps } from "@cloudscape-design/components/text-filter"; + +import type { Options } from "./ParticipantsTable"; + +interface ParticipantsFiltersProps { + filteredItemsCount: number | undefined; + filterProps: TextFilterProps; + roles: Options; + selectedRoles: Options; + setSelectedRoles: Dispatch>; // can't figure out how to get this one right + statuses: Options; + selectedStatuses: Options; + setSelectedStatuses: Dispatch>; // can't figure out how to get this one right +} + +function ParticipantsFilters({ + filteredItemsCount, + filterProps, + roles, + selectedRoles, + setSelectedRoles, + statuses, + selectedStatuses, + setSelectedStatuses +}: ParticipantsFiltersProps) { + return ( + +
+ +
+ + setSelectedRoles(event.detail.selectedOptions)} + expandToViewport={true} + /> + + + setSelectedStatuses(event.detail.selectedOptions)} + expandToViewport={true} + /> + +
+ + ) +} + +export default ParticipantsFilters; \ No newline at end of file diff --git a/apps/site/src/app/admin/participants/components/ParticipantsTable.tsx b/apps/site/src/app/admin/participants/components/ParticipantsTable.tsx index bb391e2d..dc7de5a3 100644 --- a/apps/site/src/app/admin/participants/components/ParticipantsTable.tsx +++ b/apps/site/src/app/admin/participants/components/ParticipantsTable.tsx @@ -4,18 +4,17 @@ import { useCollection } from "@cloudscape-design/collection-hooks"; import Box from "@cloudscape-design/components/box"; import Button from "@cloudscape-design/components/button"; import CollectionPreferences from "@cloudscape-design/components/collection-preferences"; -import FormField from "@cloudscape-design/components/form-field"; import Header from "@cloudscape-design/components/header"; +import { MultiselectProps } from "@cloudscape-design/components/multiselect"; import Pagination from "@cloudscape-design/components/pagination"; -import Select from "@cloudscape-design/components/select"; import SpaceBetween from "@cloudscape-design/components/space-between"; import Table from "@cloudscape-design/components/table"; -import TextFilter from "@cloudscape-design/components/text-filter"; import ApplicantStatus from "@/app/admin/applicants/components/ApplicantStatus"; import { Participant } from "@/lib/admin/useParticipants"; import ParticipantAction from "./ParticipantAction"; +import ParticipantsFilters from "./ParticipantsFilters"; import RoleBadge from "./RoleBadge"; interface EmptyStateProps { @@ -31,9 +30,8 @@ interface ParticipantsTableProps { initiatePromotion: (participant: Participant) => void; } -const ALL_ROLES = { value: "0", label: "All roles" }; -const ALL_STATUSES = { value: "0", label: "All roles" }; -const SEARCHABLE_COLUMNS = ["_id", "first_name", "last_name", "role", "status"]; +export type Options = ReadonlyArray; +const SEARCHABLE_COLUMNS: (keyof Participant)[] = ["_id", "first_name", "last_name", "role", "status"]; function createLabelFunction(columnName: string) { return ({ sorted, descending }: { sorted: boolean; descending: boolean }) => { @@ -75,10 +73,10 @@ function ParticipantsTable({ "action", ], }); - const [filterRole, setFilterRole] = useState(ALL_ROLES); - const [filterStatus, setFilterStatus] = useState(ALL_STATUSES); - const matchesRole = (p: Participant) => filterRole === ALL_ROLES || p.role === filterRole.label; - const matchesStatus = (p: Participant) => filterStatus === ALL_STATUSES || p.status === filterStatus.label; + const [filterRole, setFilterRole] = useState([]); + const [filterStatus, setFilterStatus] = useState([]); + const matchesRole = (p: Participant) => filterRole.length === 0 || filterRole.map(r => r.value).includes(p.role); + const matchesStatus = (p: Participant) => filterStatus.length === 0 || filterStatus.map(s => s.value).includes(p.status); const { items, @@ -104,8 +102,8 @@ function ParticipantsTable({ const filteringTextLC = filteringText.toLowerCase(); return SEARCHABLE_COLUMNS.map(key => item[key]).some( - value => typeof value === 'string' && value.toLowerCase().indexOf(filteringTextLC) > -1 - ) + value => typeof value === 'string' && value.toLowerCase().includes(filteringTextLC) + ) || `${item.first_name} ${item.last_name}`.toLowerCase().includes(filteringTextLC) }, }, pagination: { pageSize: preferences.pageSize }, @@ -113,9 +111,9 @@ function ParticipantsTable({ selection: {}, } ); - const allRoles = new Set(items.map(p => p.role)); + const allRoles = new Set(participants.map(p => p.role)); const roleOptions = Array.from(allRoles).map(r => ({ value: r, label: r })); - const allStatuses = new Set(items.map(p => p.status)); + const allStatuses = new Set(participants.map(p => p.status)); const statusOptions = Array.from(allStatuses).map(s => ({ value: s, label: s })); const ActionCell = useCallback( @@ -198,33 +196,16 @@ function ParticipantsTable({ trackBy="_id" empty={emptyMessage} filter={ - <> - - - setFilterStatus(event.detail.selectedOption as { value: string, label: string })} - expandToViewport={true} - /> - - + } pagination={ Date: Fri, 26 Jan 2024 12:46:58 -0800 Subject: [PATCH 06/17] Fix prettiers --- .../app/admin/participants/components/ParticipantsFilters.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/site/src/app/admin/participants/components/ParticipantsFilters.tsx b/apps/site/src/app/admin/participants/components/ParticipantsFilters.tsx index bcbdd268..a3440021 100644 --- a/apps/site/src/app/admin/participants/components/ParticipantsFilters.tsx +++ b/apps/site/src/app/admin/participants/components/ParticipantsFilters.tsx @@ -3,7 +3,9 @@ import { Dispatch, SetStateAction } from "react"; import FormField from "@cloudscape-design/components/form-field"; import Multiselect from "@cloudscape-design/components/multiselect"; import SpaceBetween from "@cloudscape-design/components/space-between"; -import TextFilter, { TextFilterProps } from "@cloudscape-design/components/text-filter"; +import TextFilter, { + TextFilterProps +} from "@cloudscape-design/components/text-filter"; import type { Options } from "./ParticipantsTable"; From bcead567546508534923c69ffbe838c4f8780f6f Mon Sep 17 00:00:00 2001 From: Alan Chang Date: Fri, 26 Jan 2024 12:49:56 -0800 Subject: [PATCH 07/17] Fix text filter --- .../app/admin/participants/components/ParticipantsFilters.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/site/src/app/admin/participants/components/ParticipantsFilters.tsx b/apps/site/src/app/admin/participants/components/ParticipantsFilters.tsx index a3440021..45ae7b7b 100644 --- a/apps/site/src/app/admin/participants/components/ParticipantsFilters.tsx +++ b/apps/site/src/app/admin/participants/components/ParticipantsFilters.tsx @@ -38,7 +38,6 @@ function ParticipantsFilters({ countText={filteredItemsCount === 1 ? '1 participant' : `${filteredItemsCount} participants`} filteringAriaLabel="Filter participants" filteringPlaceholder="Search participants" - filteringText="" /> From 9aa7305a72acf92b01f5b648438994d34ed6f0d7 Mon Sep 17 00:00:00 2001 From: Alan Chang Date: Fri, 26 Jan 2024 12:51:36 -0800 Subject: [PATCH 08/17] More prettiers --- .../admin/participants/components/ParticipantsFilters.tsx | 4 ++-- .../admin/participants/components/ParticipantsTable.tsx | 8 +++++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/apps/site/src/app/admin/participants/components/ParticipantsFilters.tsx b/apps/site/src/app/admin/participants/components/ParticipantsFilters.tsx index 45ae7b7b..ab947371 100644 --- a/apps/site/src/app/admin/participants/components/ParticipantsFilters.tsx +++ b/apps/site/src/app/admin/participants/components/ParticipantsFilters.tsx @@ -14,10 +14,10 @@ interface ParticipantsFiltersProps { filterProps: TextFilterProps; roles: Options; selectedRoles: Options; - setSelectedRoles: Dispatch>; // can't figure out how to get this one right + setSelectedRoles: Dispatch>; statuses: Options; selectedStatuses: Options; - setSelectedStatuses: Dispatch>; // can't figure out how to get this one right + setSelectedStatuses: Dispatch>; } function ParticipantsFilters({ diff --git a/apps/site/src/app/admin/participants/components/ParticipantsTable.tsx b/apps/site/src/app/admin/participants/components/ParticipantsTable.tsx index dc7de5a3..0fa51778 100644 --- a/apps/site/src/app/admin/participants/components/ParticipantsTable.tsx +++ b/apps/site/src/app/admin/participants/components/ParticipantsTable.tsx @@ -31,7 +31,13 @@ interface ParticipantsTableProps { } export type Options = ReadonlyArray; -const SEARCHABLE_COLUMNS: (keyof Participant)[] = ["_id", "first_name", "last_name", "role", "status"]; +const SEARCHABLE_COLUMNS: (keyof Participant)[] = [ + "_id", + "first_name", + "last_name", + "role", + "status" +]; function createLabelFunction(columnName: string) { return ({ sorted, descending }: { sorted: boolean; descending: boolean }) => { From 993baa36de92fc180718d045334d11992bf9e55a Mon Sep 17 00:00:00 2001 From: Alan Chang Date: Fri, 26 Jan 2024 12:56:20 -0800 Subject: [PATCH 09/17] More prettiers --- .../participants/components/ParticipantsTable.tsx | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/apps/site/src/app/admin/participants/components/ParticipantsTable.tsx b/apps/site/src/app/admin/participants/components/ParticipantsTable.tsx index 0fa51778..97f3e18b 100644 --- a/apps/site/src/app/admin/participants/components/ParticipantsTable.tsx +++ b/apps/site/src/app/admin/participants/components/ParticipantsTable.tsx @@ -36,7 +36,7 @@ const SEARCHABLE_COLUMNS: (keyof Participant)[] = [ "first_name", "last_name", "role", - "status" + "status", ]; function createLabelFunction(columnName: string) { @@ -107,9 +107,16 @@ function ParticipantsTable({ if (!matchesStatus(item)) { return false; } const filteringTextLC = filteringText.toLowerCase(); - return SEARCHABLE_COLUMNS.map(key => item[key]).some( - value => typeof value === 'string' && value.toLowerCase().includes(filteringTextLC) - ) || `${item.first_name} ${item.last_name}`.toLowerCase().includes(filteringTextLC) + return ( + SEARCHABLE_COLUMNS.map((key) => item[key]).some( + (value) => + typeof value === "string" && + value.toLowerCase().includes(filteringTextLC), + ) || + `${item.first_name} ${item.last_name}` + .toLowerCase() + .includes(filteringTextLC) + ); }, }, pagination: { pageSize: preferences.pageSize }, From 19640ab0ed7187f899213a1e7490e7703b332cb6 Mon Sep 17 00:00:00 2001 From: Alan Chang Date: Fri, 26 Jan 2024 13:00:59 -0800 Subject: [PATCH 10/17] Merge main, resolve conflicts --- apps/api/src/admin/participant_manager.py | 63 +++++++++++++++++-- apps/api/src/routers/admin.py | 26 ++++++-- .../participants/components/CheckInModal.tsx | 6 +- .../components/CheckinDayIcon.tsx | 38 +++++++++++ .../components/ParticipantsTable.tsx | 18 +++++- .../components/WaitlistPromotionModal.tsx | 13 +++- apps/site/src/lib/admin/useParticipants.ts | 3 + 7 files changed, 150 insertions(+), 17 deletions(-) create mode 100644 apps/site/src/app/admin/participants/components/CheckinDayIcon.tsx diff --git a/apps/api/src/admin/participant_manager.py b/apps/api/src/admin/participant_manager.py index 354aefe9..2362fb9b 100644 --- a/apps/api/src/admin/participant_manager.py +++ b/apps/api/src/admin/participant_manager.py @@ -1,6 +1,9 @@ +from datetime import datetime from logging import getLogger from typing import Any, Optional, Union +from typing_extensions import TypeAlias + from auth.user_identity import User, utc_now from models.ApplicationData import Decision from services import mongodb_handler @@ -9,13 +12,24 @@ log = getLogger(__name__) +Checkin: TypeAlias = tuple[datetime, str] + +NON_HACKER_ROLES = ( + Role.MENTOR, + Role.VOLUNTEER, + Role.SPONSOR, + Role.JUDGE, + Role.WORKSHOP_LEAD, +) + class Participant(UserRecord): """Participants attending the event.""" + checkins: list[Checkin] = [] first_name: str last_name: str - status: Union[Status, Decision] + status: Union[Status, Decision] = Status.REVIEWED async def get_hackers() -> list[Participant]: @@ -39,6 +53,7 @@ async def get_hackers() -> list[Participant]: "_id", "status", "role", + "checkins", "application_data.first_name", "application_data.last_name", ], @@ -47,15 +62,29 @@ async def get_hackers() -> list[Participant]: return [Participant(**user, **user["application_data"]) for user in records] -async def check_in_applicant(uid: str, associate: User) -> None: - """Check in applicant at IrvineHacks""" +async def get_non_hackers() -> list[Participant]: + """Fetch all non-hackers participating in the event.""" + records: list[dict[str, Any]] = await mongodb_handler.retrieve( + Collection.USERS, + {"role": {"$in": NON_HACKER_ROLES}}, + ["_id", "status", "role", "checkins", "first_name", "last_name"], + ) + return [Participant(**user) for user in records] + + +async def check_in_participant(uid: str, associate: User) -> None: + """Check in participant at IrvineHacks""" record: Optional[dict[str, object]] = await mongodb_handler.retrieve_one( - Collection.USERS, {"_id": uid, "role": Role.APPLICANT} + Collection.USERS, {"_id": uid, "role": {"$exists": True}} ) - if not record or record["status"] not in (Status.ATTENDING, Status.CONFIRMED): + + if not record or record.get("status", "") not in ( + Status.ATTENDING, + Status.CONFIRMED, + ): raise ValueError - new_checkin_entry = (utc_now(), associate.uid) + new_checkin_entry: Checkin = (utc_now(), associate.uid) update_status = await mongodb_handler.raw_update_one( Collection.USERS, @@ -68,3 +97,25 @@ async def check_in_applicant(uid: str, associate: User) -> None: raise RuntimeError(f"Could not update check-in record for {uid}.") log.info(f"Applicant {uid} checked in by {associate.uid}") + + +async def confirm_attendance_non_hacker(uid: str, director: User) -> None: + """Update status for Role.Attending for non-hackers.""" + + record: Optional[dict[str, object]] = await mongodb_handler.retrieve_one( + Collection.USERS, {"_id": uid, "status": Status.WAIVER_SIGNED} + ) + + if not record or record["role"] not in NON_HACKER_ROLES: + raise ValueError + + update_status = await mongodb_handler.raw_update_one( + Collection.USERS, + {"_id": uid}, + {"status": Status.ATTENDING}, + ) + + if not update_status: + raise RuntimeError(f"Could not update status to ATTENDING for {uid}.") + + log.info(f"Non-hacker {uid} status updated to attending by {director.uid}") diff --git a/apps/api/src/routers/admin.py b/apps/api/src/routers/admin.py index e8b81be5..0b754e85 100644 --- a/apps/api/src/routers/admin.py +++ b/apps/api/src/routers/admin.py @@ -248,18 +248,18 @@ async def waitlist_release(uid: str) -> None: @router.get("/participants", dependencies=[Depends(require_checkin_associate)]) async def participants() -> list[Participant]: """Get list of participants.""" - # TODO: non-hackers - return await participant_manager.get_hackers() + hackers = await participant_manager.get_hackers() + non_hackers = await participant_manager.get_non_hackers() + return hackers + non_hackers @router.post("/checkin/{uid}") -async def checkin( +async def check_in_participant( uid: str, associate: Annotated[User, Depends(require_checkin_associate)] ) -> None: """Check in participant at IrvineHacks.""" try: - # TODO: non-hackers - await participant_manager.check_in_applicant(uid, associate) + await participant_manager.check_in_participant(uid, associate) except ValueError: raise HTTPException(status.HTTP_404_NOT_FOUND) except RuntimeError as err: @@ -267,6 +267,22 @@ async def checkin( raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR) +@router.post( + "/update-attendance/{uid}", +) +async def update_attendance( + uid: str, director: Annotated[User, Depends(require_role([Role.DIRECTOR]))] +) -> None: + """Update status to Role.ATTENDING for non-hackers.""" + try: + await participant_manager.confirm_attendance_non_hacker(uid, director) + except ValueError: + raise HTTPException(status.HTTP_404_NOT_FOUND) + except RuntimeError as err: + log.exception("While updating participant attendance: %s", err) + raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR) + + async def _process_status(uids: Sequence[str], status: Status) -> None: ok = await mongodb_handler.update( Collection.USERS, {"_id": {"$in": uids}}, {"status": status} diff --git a/apps/site/src/app/admin/participants/components/CheckInModal.tsx b/apps/site/src/app/admin/participants/components/CheckInModal.tsx index 8899636b..a2221bc5 100644 --- a/apps/site/src/app/admin/participants/components/CheckInModal.tsx +++ b/apps/site/src/app/admin/participants/components/CheckInModal.tsx @@ -39,8 +39,10 @@ function CheckInModal({ onDismiss, onConfirm, participant }: ActionModalProps) {
    {/* TODO: actual instructions for check-in associates */} -
  • Create a badge for the participant ...
  • -
  • Ask participant to sign the SPFB sheet ...
  • +
  • Ask for a photo ID and verify name is under attendee list.
  • +
  • Have participant sign the SPFB sheet.
  • +
  • Scan badge barcode with webcam or type in digits.
  • +
  • Fill in badge and give to participant.
{/* TODO: badge barcode input */} diff --git a/apps/site/src/app/admin/participants/components/CheckinDayIcon.tsx b/apps/site/src/app/admin/participants/components/CheckinDayIcon.tsx new file mode 100644 index 00000000..1e7f2ba3 --- /dev/null +++ b/apps/site/src/app/admin/participants/components/CheckinDayIcon.tsx @@ -0,0 +1,38 @@ +import Icon from "@cloudscape-design/components/icon"; +import dayjs from "dayjs"; +import timezone from "dayjs/plugin/timezone"; +import utc from "dayjs/plugin/utc"; + +import { Checkin } from "@/lib/admin/useParticipants"; + +dayjs.extend(utc); +dayjs.extend(timezone); +const EVENT_TIMEZONE = "America/Los_Angeles"; + +interface CheckinDayProps { + checkins: Checkin[]; + date: Date; +} + +const today = dayjs().tz(EVENT_TIMEZONE); + +function CheckinDayIcon({ checkins, date }: CheckinDayProps) { + // Timezones are weird, but comparing the days of the check-ins + const day = dayjs(date).tz(EVENT_TIMEZONE); + const checkinTimes = checkins.map(([datetime]) => + dayjs(datetime).tz(EVENT_TIMEZONE), + ); + + const checkedIn = checkinTimes.some((checkin) => day.isSame(checkin, "date")); + const past = day.isBefore(today, "date"); + + if (checkedIn) { + return ; + } + if (past) { + return ; + } + return ; +} + +export default CheckinDayIcon; diff --git a/apps/site/src/app/admin/participants/components/ParticipantsTable.tsx b/apps/site/src/app/admin/participants/components/ParticipantsTable.tsx index 97f3e18b..694627c3 100644 --- a/apps/site/src/app/admin/participants/components/ParticipantsTable.tsx +++ b/apps/site/src/app/admin/participants/components/ParticipantsTable.tsx @@ -13,10 +13,15 @@ import Table from "@cloudscape-design/components/table"; import ApplicantStatus from "@/app/admin/applicants/components/ApplicantStatus"; import { Participant } from "@/lib/admin/useParticipants"; +import CheckinDayIcon from "./CheckinDayIcon"; import ParticipantAction from "./ParticipantAction"; import ParticipantsFilters from "./ParticipantsFilters"; import RoleBadge from "./RoleBadge"; +const FRIDAY = new Date("2024-01-26T12:00:00"); +const SATURDAY = new Date("2024-01-27T12:00:00"); +const SUNDAY = new Date("2024-01-28T12:00:00"); + interface EmptyStateProps { title: string; subtitle?: string; @@ -203,7 +208,6 @@ function ParticipantsTable({ items={items} loading={loading} loadingText="Loading participants" - resizableColumns variant="full-page" stickyColumns={{ first: 1, last: 0 }} trackBy="_id" @@ -263,4 +267,16 @@ function ParticipantsTable({ ); } +const FridayCheckin = ({ checkins }: Participant) => ( + +); + +const SaturdayCheckin = ({ checkins }: Participant) => ( + +); + +const SundayCheckin = ({ checkins }: Participant) => ( + +); + export default ParticipantsTable; diff --git a/apps/site/src/app/admin/participants/components/WaitlistPromotionModal.tsx b/apps/site/src/app/admin/participants/components/WaitlistPromotionModal.tsx index ca6ae2a7..a25b2005 100644 --- a/apps/site/src/app/admin/participants/components/WaitlistPromotionModal.tsx +++ b/apps/site/src/app/admin/participants/components/WaitlistPromotionModal.tsx @@ -37,9 +37,16 @@ function WaitlistPromotionModal({
    {/* TODO: actual instructions for check-in leads */} -
  • Log into the portal
  • -
  • Sign waiver
  • -
  • Confirm attendance
  • +
  • Ask for a photo ID and verify name is under waitlist.
  • +
  • Promote participant.
  • +
  • + Have participant log into the portal, sign the waiver, and confirm + attendance. +
  • +
  • Follow normal check-in procedures.
  • +
  • + Direct participant to info booth for Slack and event onboarding. +
{/* TODO: badge barcode input */} diff --git a/apps/site/src/lib/admin/useParticipants.ts b/apps/site/src/lib/admin/useParticipants.ts index 7c0e2537..7376524b 100644 --- a/apps/site/src/lib/admin/useParticipants.ts +++ b/apps/site/src/lib/admin/useParticipants.ts @@ -15,11 +15,14 @@ const enum Role { WorkshopLead = "workshop_lead", } +export type Checkin = [string, Uid]; + export interface Participant { _id: Uid; first_name: string; last_name: string; role: Role; + checkins: Checkin[]; status: Status; } From 9bae95a635be7ad81d62de8ddbd8a9af52145de1 Mon Sep 17 00:00:00 2001 From: Alan Chang Date: Fri, 26 Jan 2024 13:04:02 -0800 Subject: [PATCH 11/17] Merge main conflicts again --- .../components/ParticipantsTable.tsx | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/apps/site/src/app/admin/participants/components/ParticipantsTable.tsx b/apps/site/src/app/admin/participants/components/ParticipantsTable.tsx index 694627c3..d26fdef1 100644 --- a/apps/site/src/app/admin/participants/components/ParticipantsTable.tsx +++ b/apps/site/src/app/admin/participants/components/ParticipantsTable.tsx @@ -182,6 +182,24 @@ function ParticipantsTable({ ariaLabel: createLabelFunction("status"), sortingField: "status", }, + { + id: "friday", + header: "Fri", + cell: FridayCheckin, + sortingField: "friday", + }, + { + id: "saturday", + header: "Sat", + cell: SaturdayCheckin, + sortingField: "saturday", + }, + { + id: "sunday", + header: "Sun", + cell: SundayCheckin, + sortingField: "sunday", + }, { id: "action", header: "Action", From d41defc64bbe7f3e1bdcc811a2b17299c33e4a63 Mon Sep 17 00:00:00 2001 From: Alan Chang Date: Fri, 26 Jan 2024 13:05:53 -0800 Subject: [PATCH 12/17] Add day checkins to default visible columns --- .../app/admin/participants/components/ParticipantsTable.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/site/src/app/admin/participants/components/ParticipantsTable.tsx b/apps/site/src/app/admin/participants/components/ParticipantsTable.tsx index d26fdef1..df9c26a5 100644 --- a/apps/site/src/app/admin/participants/components/ParticipantsTable.tsx +++ b/apps/site/src/app/admin/participants/components/ParticipantsTable.tsx @@ -81,6 +81,9 @@ function ParticipantsTable({ "lastName", "role", "status", + "friday", + "saturday", + "sunday", "action", ], }); From c3a8741535de58bb11727e3c363d655d84c83076 Mon Sep 17 00:00:00 2001 From: Alan Chang Date: Fri, 26 Jan 2024 13:08:19 -0800 Subject: [PATCH 13/17] More prettiers --- .../components/ParticipantsFilters.tsx | 14 ++++++++++++++ .../participants/components/ParticipantsTable.tsx | 5 ++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/apps/site/src/app/admin/participants/components/ParticipantsFilters.tsx b/apps/site/src/app/admin/participants/components/ParticipantsFilters.tsx index ab947371..cbd9c6f4 100644 --- a/apps/site/src/app/admin/participants/components/ParticipantsFilters.tsx +++ b/apps/site/src/app/admin/participants/components/ParticipantsFilters.tsx @@ -1,6 +1,7 @@ import { Dispatch, SetStateAction } from "react"; import FormField from "@cloudscape-design/components/form-field"; +import { IconProps } from "@cloudscape-design/components/icon"; import Multiselect from "@cloudscape-design/components/multiselect"; import SpaceBetween from "@cloudscape-design/components/space-between"; import TextFilter, { @@ -20,6 +21,19 @@ interface ParticipantsFiltersProps { setSelectedStatuses: Dispatch>; } +const StatusIcons: Record = { + [ReviewStatus.pending]: "status-pending", + [ReviewStatus.reviewed]: "status-in-progress", + [ReviewStatus.released]: "status-positive", + [Decision.accepted]: "status-positive", + [Decision.rejected]: "status-pending", + [Decision.waitlisted]: "status-negative", + [PostAcceptedStatus.signed]: "status-in-progress", + [PostAcceptedStatus.confirmed]: "status-positive", + [PostAcceptedStatus.attending]: "status-positive", + [PostAcceptedStatus.void]: "status-negative", +}; + function ParticipantsFilters({ filteredItemsCount, filterProps, diff --git a/apps/site/src/app/admin/participants/components/ParticipantsTable.tsx b/apps/site/src/app/admin/participants/components/ParticipantsTable.tsx index df9c26a5..fd2a176f 100644 --- a/apps/site/src/app/admin/participants/components/ParticipantsTable.tsx +++ b/apps/site/src/app/admin/participants/components/ParticipantsTable.tsx @@ -135,7 +135,10 @@ function ParticipantsTable({ const allRoles = new Set(participants.map(p => p.role)); const roleOptions = Array.from(allRoles).map(r => ({ value: r, label: r })); const allStatuses = new Set(participants.map(p => p.status)); - const statusOptions = Array.from(allStatuses).map(s => ({ value: s, label: s })); + const statusOptions = Array.from(allStatuses).map(s => ({ + value: s, + label: s + })); const ActionCell = useCallback( (participant: Participant) => ( From d7c5556b35dbf48eb2a354f78d6da62c1ffd50d5 Mon Sep 17 00:00:00 2001 From: Alan Chang Date: Fri, 26 Jan 2024 13:09:33 -0800 Subject: [PATCH 14/17] More prettiers --- .../app/admin/participants/components/ParticipantsTable.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/site/src/app/admin/participants/components/ParticipantsTable.tsx b/apps/site/src/app/admin/participants/components/ParticipantsTable.tsx index fd2a176f..6bc5cfc6 100644 --- a/apps/site/src/app/admin/participants/components/ParticipantsTable.tsx +++ b/apps/site/src/app/admin/participants/components/ParticipantsTable.tsx @@ -89,8 +89,10 @@ function ParticipantsTable({ }); const [filterRole, setFilterRole] = useState([]); const [filterStatus, setFilterStatus] = useState([]); - const matchesRole = (p: Participant) => filterRole.length === 0 || filterRole.map(r => r.value).includes(p.role); - const matchesStatus = (p: Participant) => filterStatus.length === 0 || filterStatus.map(s => s.value).includes(p.status); + const matchesRole = (p: Participant) => + filterRole.length === 0 || filterRole.map(r => r.value).includes(p.role); + const matchesStatus = (p: Participant) => + filterStatus.length === 0 || filterStatus.map(s => s.value).includes(p.status); const { items, From ebdeac4c34ccfff605d5ca1930c72892cde9b9c7 Mon Sep 17 00:00:00 2001 From: Alan Chang Date: Fri, 26 Jan 2024 13:22:44 -0800 Subject: [PATCH 15/17] Add icons to status filter --- .../participants/components/ParticipantsFilters.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/apps/site/src/app/admin/participants/components/ParticipantsFilters.tsx b/apps/site/src/app/admin/participants/components/ParticipantsFilters.tsx index cbd9c6f4..3345e2c0 100644 --- a/apps/site/src/app/admin/participants/components/ParticipantsFilters.tsx +++ b/apps/site/src/app/admin/participants/components/ParticipantsFilters.tsx @@ -2,13 +2,15 @@ import { Dispatch, SetStateAction } from "react"; import FormField from "@cloudscape-design/components/form-field"; import { IconProps } from "@cloudscape-design/components/icon"; -import Multiselect from "@cloudscape-design/components/multiselect"; +import Multiselect, { MultiselectProps } from "@cloudscape-design/components/multiselect"; import SpaceBetween from "@cloudscape-design/components/space-between"; import TextFilter, { TextFilterProps } from "@cloudscape-design/components/text-filter"; import type { Options } from "./ParticipantsTable"; +import { Decision, PostAcceptedStatus, ReviewStatus, Status } from "@/lib/admin/useApplicant"; +import { StatusLabels } from "../../applicants/components/ApplicantStatus"; interface ParticipantsFiltersProps { filteredItemsCount: number | undefined; @@ -34,6 +36,12 @@ const StatusIcons: Record = { [PostAcceptedStatus.void]: "status-negative", }; +const statusOption = (status: MultiselectProps.Option) => ({ + label: StatusLabels[status.value], + value: status.value, + iconName: StatusIcons[status.value], +}); + function ParticipantsFilters({ filteredItemsCount, filterProps, @@ -69,7 +77,7 @@ function ParticipantsFilters({ setSelectedStatuses(event.detail.selectedOptions)} From d034546eafd037d59de316791879dd511fe298e3 Mon Sep 17 00:00:00 2001 From: Alan Chang Date: Fri, 26 Jan 2024 14:43:51 -0800 Subject: [PATCH 16/17] Prettier run everything --- .../components/ParticipantsFilters.tsx | 122 ++++++++++-------- .../components/ParticipantsTable.tsx | 90 +++++++------ 2 files changed, 117 insertions(+), 95 deletions(-) diff --git a/apps/site/src/app/admin/participants/components/ParticipantsFilters.tsx b/apps/site/src/app/admin/participants/components/ParticipantsFilters.tsx index 3345e2c0..2a5399e0 100644 --- a/apps/site/src/app/admin/participants/components/ParticipantsFilters.tsx +++ b/apps/site/src/app/admin/participants/components/ParticipantsFilters.tsx @@ -2,25 +2,32 @@ import { Dispatch, SetStateAction } from "react"; import FormField from "@cloudscape-design/components/form-field"; import { IconProps } from "@cloudscape-design/components/icon"; -import Multiselect, { MultiselectProps } from "@cloudscape-design/components/multiselect"; +import Multiselect, { + MultiselectProps, +} from "@cloudscape-design/components/multiselect"; import SpaceBetween from "@cloudscape-design/components/space-between"; import TextFilter, { - TextFilterProps + TextFilterProps, } from "@cloudscape-design/components/text-filter"; import type { Options } from "./ParticipantsTable"; -import { Decision, PostAcceptedStatus, ReviewStatus, Status } from "@/lib/admin/useApplicant"; +import { + Decision, + PostAcceptedStatus, + ReviewStatus, + Status, +} from "@/lib/admin/useApplicant"; import { StatusLabels } from "../../applicants/components/ApplicantStatus"; interface ParticipantsFiltersProps { - filteredItemsCount: number | undefined; - filterProps: TextFilterProps; - roles: Options; - selectedRoles: Options; - setSelectedRoles: Dispatch>; - statuses: Options; - selectedStatuses: Options; - setSelectedStatuses: Dispatch>; + filteredItemsCount: number | undefined; + filterProps: TextFilterProps; + roles: Options; + selectedRoles: Options; + setSelectedRoles: Dispatch>; + statuses: Options; + selectedStatuses: Options; + setSelectedStatuses: Dispatch>; } const StatusIcons: Record = { @@ -43,50 +50,55 @@ const statusOption = (status: MultiselectProps.Option) => ({ }); function ParticipantsFilters({ - filteredItemsCount, - filterProps, - roles, - selectedRoles, - setSelectedRoles, - statuses, - selectedStatuses, - setSelectedStatuses + filteredItemsCount, + filterProps, + roles, + selectedRoles, + setSelectedRoles, + statuses, + selectedStatuses, + setSelectedStatuses, }: ParticipantsFiltersProps) { - return ( - -
- -
- - setSelectedRoles(event.detail.selectedOptions)} - expandToViewport={true} - /> - - - setSelectedStatuses(event.detail.selectedOptions)} - expandToViewport={true} - /> - -
- - ) + return ( + +
+ +
+ + setSelectedRoles(event.detail.selectedOptions)} + expandToViewport={true} + /> + + + + setSelectedStatuses(event.detail.selectedOptions) + } + expandToViewport={true} + /> + +
+ ); } -export default ParticipantsFilters; \ No newline at end of file +export default ParticipantsFilters; diff --git a/apps/site/src/app/admin/participants/components/ParticipantsTable.tsx b/apps/site/src/app/admin/participants/components/ParticipantsTable.tsx index 6bc5cfc6..04c411a2 100644 --- a/apps/site/src/app/admin/participants/components/ParticipantsTable.tsx +++ b/apps/site/src/app/admin/participants/components/ParticipantsTable.tsx @@ -90,9 +90,10 @@ function ParticipantsTable({ const [filterRole, setFilterRole] = useState([]); const [filterStatus, setFilterStatus] = useState([]); const matchesRole = (p: Participant) => - filterRole.length === 0 || filterRole.map(r => r.value).includes(p.role); - const matchesStatus = (p: Participant) => - filterStatus.length === 0 || filterStatus.map(s => s.value).includes(p.status); + filterRole.length === 0 || filterRole.map((r) => r.value).includes(p.role); + const matchesStatus = (p: Participant) => + filterStatus.length === 0 || + filterStatus.map((s) => s.value).includes(p.status); const { items, @@ -101,45 +102,50 @@ function ParticipantsTable({ collectionProps, filterProps, paginationProps, - } = useCollection( - participants, - { - filtering: { - empty: , - noMatch: ( - actions.setFiltering('')}>Clear filter} - /> - ), - filteringFunction: (item, filteringText) => { - if (!matchesRole(item)) { return false; } - if (!matchesStatus(item)) { return false; } - const filteringTextLC = filteringText.toLowerCase(); + } = useCollection(participants, { + filtering: { + empty: , + noMatch: ( + actions.setFiltering("")}> + Clear filter + + } + /> + ), + filteringFunction: (item, filteringText) => { + if (!matchesRole(item)) { + return false; + } + if (!matchesStatus(item)) { + return false; + } + const filteringTextLC = filteringText.toLowerCase(); - return ( - SEARCHABLE_COLUMNS.map((key) => item[key]).some( - (value) => - typeof value === "string" && - value.toLowerCase().includes(filteringTextLC), - ) || - `${item.first_name} ${item.last_name}` - .toLowerCase() - .includes(filteringTextLC) - ); - }, + return ( + SEARCHABLE_COLUMNS.map((key) => item[key]).some( + (value) => + typeof value === "string" && + value.toLowerCase().includes(filteringTextLC), + ) || + `${item.first_name} ${item.last_name}` + .toLowerCase() + .includes(filteringTextLC) + ); }, - pagination: { pageSize: preferences.pageSize }, - sorting: {}, - selection: {}, - } - ); - const allRoles = new Set(participants.map(p => p.role)); - const roleOptions = Array.from(allRoles).map(r => ({ value: r, label: r })); - const allStatuses = new Set(participants.map(p => p.status)); - const statusOptions = Array.from(allStatuses).map(s => ({ + }, + pagination: { pageSize: preferences.pageSize }, + sorting: {}, + selection: {}, + }); + const allRoles = new Set(participants.map((p) => p.role)); + const roleOptions = Array.from(allRoles).map((r) => ({ value: r, label: r })); + const allStatuses = new Set(participants.map((p) => p.status)); + const statusOptions = Array.from(allStatuses).map((s) => ({ value: s, - label: s + label: s, })); const ActionCell = useCallback( @@ -286,7 +292,11 @@ function ParticipantsTable({ confirmLabel="Confirm" title="Preferences" preferences={preferences} - onConfirm={({ detail }) => setPreferences(detail as { pageSize: number, visibleContent: Array })} + onConfirm={({ detail }) => + setPreferences( + detail as { pageSize: number; visibleContent: Array }, + ) + } /> } /> From 0198f085eaf00074a9bcdc22cd5925ae91989132 Mon Sep 17 00:00:00 2001 From: Sam Der Date: Fri, 26 Jan 2024 16:14:53 -0800 Subject: [PATCH 17/17] fix: type error --- .../components/ParticipantsFilters.tsx | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/apps/site/src/app/admin/participants/components/ParticipantsFilters.tsx b/apps/site/src/app/admin/participants/components/ParticipantsFilters.tsx index 2a5399e0..0ea4c81d 100644 --- a/apps/site/src/app/admin/participants/components/ParticipantsFilters.tsx +++ b/apps/site/src/app/admin/participants/components/ParticipantsFilters.tsx @@ -10,7 +10,6 @@ import TextFilter, { TextFilterProps, } from "@cloudscape-design/components/text-filter"; -import type { Options } from "./ParticipantsTable"; import { Decision, PostAcceptedStatus, @@ -18,6 +17,7 @@ import { Status, } from "@/lib/admin/useApplicant"; import { StatusLabels } from "../../applicants/components/ApplicantStatus"; +import type { Options } from "./ParticipantsTable"; interface ParticipantsFiltersProps { filteredItemsCount: number | undefined; @@ -43,11 +43,16 @@ const StatusIcons: Record = { [PostAcceptedStatus.void]: "status-negative", }; -const statusOption = (status: MultiselectProps.Option) => ({ - label: StatusLabels[status.value], - value: status.value, - iconName: StatusIcons[status.value], -}); +const statusOption = (status: MultiselectProps.Option) => { + if (status.value === undefined) { + throw Error(); + } + return { + label: StatusLabels[status.value as Status], + value: status.value, + iconName: StatusIcons[status.value as Status], + }; +}; function ParticipantsFilters({ filteredItemsCount,