diff --git a/package.json b/package.json index 3e4b317c8c..bba1da8a41 100644 --- a/package.json +++ b/package.json @@ -153,7 +153,7 @@ "ts-node-dev": "^2.0.0", "tsc-alias": "^1.8.6", "turndown": "^7.1.1", - "typescript": "^4.6.3", + "typescript": "^5.7.2", "webpack": "^5.71.0", "webpack-bundle-analyzer": "^4.4.2", "webpack-cli": "^4.7.2", diff --git a/src/client/components/EditUserForm/UserCountryRoleSelector/hooks/useOnChange.ts b/src/client/components/EditUserForm/UserCountryRoleSelector/hooks/useOnChange.ts index d9388e5880..301419ab82 100644 --- a/src/client/components/EditUserForm/UserCountryRoleSelector/hooks/useOnChange.ts +++ b/src/client/components/EditUserForm/UserCountryRoleSelector/hooks/useOnChange.ts @@ -25,7 +25,7 @@ export const useOnChange = (user: User): Returned => { return userRole }) - const params = { assessmentName, cycleName, roles, userId: user.id } + const params = { assessmentName, cycleName, roles, userUuid: user.uuid } dispatch(UserManagementActions.updateUserRoles(params)) }, [assessmentName, countryIso, cycle, cycleName, dispatch, user] diff --git a/src/client/components/TablePaginated/Body/Body.scss b/src/client/components/TablePaginated/Body/Body.scss new file mode 100644 index 0000000000..421da90a6b --- /dev/null +++ b/src/client/components/TablePaginated/Body/Body.scss @@ -0,0 +1,6 @@ +@import 'src/client/style/partials'; + +.table-paginated__groups { + display: grid; + grid-row-gap: $spacing-xs; +} diff --git a/src/client/components/TablePaginated/Body/Body.tsx b/src/client/components/TablePaginated/Body/Body.tsx index a512f8be43..5b2ede00fa 100644 --- a/src/client/components/TablePaginated/Body/Body.tsx +++ b/src/client/components/TablePaginated/Body/Body.tsx @@ -1,43 +1,42 @@ +import './Body.scss' import React from 'react' -import classNames from 'classnames' import { Objects } from 'utils/objects' -import { useTablePaginatedData } from 'client/store/ui/tablePaginated' -import DataColumn from 'client/components/DataGridDeprecated/DataColumn' +import Rows from 'client/components/TablePaginated/Body/Rows' +import RowsGroup from 'client/components/TablePaginated/Body/RowsGroup' import RowsSkeleton from 'client/components/TablePaginated/Body/RowsSkeleton' import { Props as BaseProps } from 'client/components/TablePaginated/types' +import { useTablePaginatedBodyData } from './hooks/useTablePaginatedBodyData' + const Body = (props: BaseProps) => { - const { columns, compareFn, limit, path, wrapCells, skeleton } = props + const { columns, groups, limit, wrapCells, skeleton } = props - const data = useTablePaginatedData(path, compareFn) + const data = useTablePaginatedBodyData(props) if (Objects.isNil(data)) { return } - return ( - <> - {data.map((datum, rowIndex) => ( - - {columns.map((column) => { - const { component: Component, key } = column - - if (wrapCells) { - return ( - - - - ) - } - - return - })} - - ))} - - ) + if (!Objects.isNil(groups)) { + return ( +
+ {(data as Array<[PropertyKey, Array]>).map(([propertyKey, dataRows]) => ( + + ))} +
+ ) + } + + return } wrapCells={wrapCells} /> } export default Body diff --git a/src/client/components/TablePaginated/Body/Rows/Rows.tsx b/src/client/components/TablePaginated/Body/Rows/Rows.tsx new file mode 100644 index 0000000000..d109b9c7eb --- /dev/null +++ b/src/client/components/TablePaginated/Body/Rows/Rows.tsx @@ -0,0 +1,34 @@ +import React from 'react' + +import classNames from 'classnames' + +import DataColumn from 'client/components/DataGridDeprecated/DataColumn' +import { Props as BaseProps } from 'client/components/TablePaginated/types' + +type Props = Pick, 'columns' | 'wrapCells'> & { + data: Array +} + +const Rows = (props: Props) => { + const { columns, data, wrapCells } = props + + return data.map((datum, rowIndex) => ( + + {columns.map((column) => { + const { component: Component, key } = column + + if (wrapCells) { + return ( + + + + ) + } + + return + })} + + )) +} + +export default Rows diff --git a/src/client/components/TablePaginated/Body/Rows/index.ts b/src/client/components/TablePaginated/Body/Rows/index.ts new file mode 100644 index 0000000000..a0cf7ded52 --- /dev/null +++ b/src/client/components/TablePaginated/Body/Rows/index.ts @@ -0,0 +1 @@ +export { default } from './Rows' diff --git a/src/client/components/TablePaginated/Body/RowsGroup/RowsGroup.scss b/src/client/components/TablePaginated/Body/RowsGroup/RowsGroup.scss new file mode 100644 index 0000000000..5424b4f7cb --- /dev/null +++ b/src/client/components/TablePaginated/Body/RowsGroup/RowsGroup.scss @@ -0,0 +1,31 @@ +@import 'src/client/style/partials'; + +button.rows-group__header.inverse { + background-color: $ui-bg-light; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + border-bottom: 1px solid transparent; + border-left: 1px solid $ui-border; + border-right: 1px solid $ui-border; + border-top: 1px solid $ui-border; + + &.collapsed { + border-bottom-left-radius: 2px; + border-bottom-right-radius: 2px; + border-bottom: 1px solid $ui-border; + + svg { + transform: rotate(-90deg); + } + } +} + +.rows-group__rows { + background-color: $ui-bg-light; + border-bottom-left-radius: 2px; + border-bottom-right-radius: 2px; + border-top-right-radius: 2px; + border: 1px solid $ui-border; + margin-top: -1px; + padding: $spacing-xxs; +} diff --git a/src/client/components/TablePaginated/Body/RowsGroup/RowsGroup.tsx b/src/client/components/TablePaginated/Body/RowsGroup/RowsGroup.tsx new file mode 100644 index 0000000000..4cd5e13b80 --- /dev/null +++ b/src/client/components/TablePaginated/Body/RowsGroup/RowsGroup.tsx @@ -0,0 +1,41 @@ +import './RowsGroup.scss' +import React, { useCallback, useState } from 'react' + +import classNames from 'classnames' + +import Button, { ButtonSize, ButtonType } from 'client/components/Buttons/Button' +import Rows from 'client/components/TablePaginated/Body/Rows' +import { Props as BaseProps } from 'client/components/TablePaginated/types' + +type Props = Pick, 'columns' | 'groups' | 'wrapCells'> & { + data: Array + propertyKey: PropertyKey +} + +const RowsGroup = (props: Props) => { + const { columns, data, groups, propertyKey, wrapCells } = props + + const [collapsed, setCollapsed] = useState(false) + const toggleView = useCallback(() => setCollapsed((prevState) => !prevState), []) + + return ( +
+
+ ) +} + +export default RowsGroup diff --git a/src/client/components/TablePaginated/Body/RowsGroup/index.ts b/src/client/components/TablePaginated/Body/RowsGroup/index.ts new file mode 100644 index 0000000000..ce68fa6b78 --- /dev/null +++ b/src/client/components/TablePaginated/Body/RowsGroup/index.ts @@ -0,0 +1 @@ +export { default } from './RowsGroup' diff --git a/src/client/components/TablePaginated/Body/hooks/useTablePaginatedBodyData.ts b/src/client/components/TablePaginated/Body/hooks/useTablePaginatedBodyData.ts new file mode 100644 index 0000000000..643f846c28 --- /dev/null +++ b/src/client/components/TablePaginated/Body/hooks/useTablePaginatedBodyData.ts @@ -0,0 +1,18 @@ +import { Objects } from 'utils/objects' + +import { useTablePaginatedData } from 'client/store/ui/tablePaginated' +import { Props as BaseProps } from 'client/components/TablePaginated/types' + +type Returned = Array | Array<[PropertyKey, Array]> | undefined + +export const useTablePaginatedBodyData = (props: BaseProps): Returned => { + const { compareFn, groups, path } = props + + const data = useTablePaginatedData(path, compareFn) + + if (Objects.isNil(data) || Objects.isNil(groups)) { + return data + } + + return Object.entries(Object.groupBy(data ?? [], groups.keySelector)) +} diff --git a/src/client/components/TablePaginated/TablePaginated.tsx b/src/client/components/TablePaginated/TablePaginated.tsx index 1197e53efb..1967123cd7 100644 --- a/src/client/components/TablePaginated/TablePaginated.tsx +++ b/src/client/components/TablePaginated/TablePaginated.tsx @@ -3,10 +3,8 @@ import React, { HTMLAttributes, useMemo, useRef } from 'react' import Skeleton from 'react-loading-skeleton' import classNames from 'classnames' -import { Objects } from 'utils/objects' -import { useTablePaginatedCount, useTablePaginatedData, useTablePaginatedPage } from 'client/store/ui/tablePaginated' -import { useOnUpdate } from 'client/hooks' +import { useTablePaginatedCount } from 'client/store/ui/tablePaginated' import DataGrid from 'client/components/DataGridDeprecated' import { PaginatorProps } from 'client/components/Paginator' import Filters from 'client/components/TablePaginated/Filters/Filters' @@ -15,6 +13,7 @@ import ExportButton from './ExportButton/ExportButton' import { useFetchData } from './hooks/useFetchData' import { useInitTablePaginated } from './hooks/useInitTablePaginated' import { useResetOnUnmount } from './hooks/useResetOnUnmount' +import { useScrollToTopOnPageUpdate } from './hooks/useScrollToTopOnPageUpdate' import Body from './Body' import Count from './Count' import DefaultEmptyList from './DefaultEmptyList' @@ -33,31 +32,24 @@ type Props = Pick, 'classNa } const TablePaginated = (props: Props) => { - const { className, gridTemplateColumns } = props // HTMLDivElement Props + const { className, gridTemplateColumns: gridTemplateColumnsProps } = props // HTMLDivElement Props const { marginPagesDisplayed, pageRangeDisplayed } = props // Paginator Props - const { columns, filters, limit, path } = props // Base Props + const { columns, filters, groups, limit, path } = props // Base Props const { counter, EmptyListComponent, export: exportTable, header, skeleton, wrapCells, compareFn } = props // Component Props + const divRef = useRef() + useInitTablePaginated({ filters, path }) useFetchData({ counter, limit, path }) useResetOnUnmount({ path }) - + useScrollToTopOnPageUpdate({ divRef, path }) const count = useTablePaginatedCount(path) - const data = useTablePaginatedData(path) - const page = useTablePaginatedPage(path) + const gridTemplateColumns = useMemo( + () => gridTemplateColumnsProps ?? `repeat(${columns.length}, auto)`, + [columns.length, gridTemplateColumnsProps] + ) const withFilters = useMemo(() => filters.filter((filter) => !filter.hidden).length > 0, [filters]) - const divRef = useRef() - - // on page update -> scroll on top - useOnUpdate(() => { - if (!Objects.isNil(data)) { - setTimeout(() => { - const opts: ScrollIntoViewOptions = { behavior: 'smooth', block: 'start', inline: 'nearest' } - divRef.current?.parentElement?.parentElement?.scrollIntoView(opts) - }) - } - }, [page]) return (
@@ -69,15 +61,13 @@ const TablePaginated = (props: Props) => { {withFilters && }
)} - + {header &&
} {count?.total === 0 && } + path: string +} + +export const useScrollToTopOnPageUpdate = (props: Props): void => { + const { divRef, path } = props + + const data = useTablePaginatedData(path) + const page = useTablePaginatedPage(path) + + useOnUpdate(() => { + if (!Objects.isNil(data)) { + setTimeout(() => { + const opts: ScrollIntoViewOptions = { behavior: 'smooth', block: 'start', inline: 'nearest' } + divRef.current?.parentElement?.parentElement?.scrollIntoView(opts) + }) + } + }, [page]) +} diff --git a/src/client/components/TablePaginated/types.ts b/src/client/components/TablePaginated/types.ts index 1e411a5bd7..e4bacf855f 100644 --- a/src/client/components/TablePaginated/types.ts +++ b/src/client/components/TablePaginated/types.ts @@ -18,6 +18,7 @@ export type Props = { columns: Array> compareFn?: TablePaginatedCompareFn filters?: Array> + groups?: { headerLabel: (key: PropertyKey) => string; keySelector: (datum: Datum) => PropertyKey } limit?: number path: string skeleton?: TablePaginatedSkeleton diff --git a/src/client/pages/CountryHome/Collaborators/UserList/UserCard/Info/Info.scss b/src/client/pages/CountryHome/Collaborators/UserList/UserCard/Info/Info.scss index eb9ba3c6e5..ec5df203b6 100644 --- a/src/client/pages/CountryHome/Collaborators/UserList/UserCard/Info/Info.scss +++ b/src/client/pages/CountryHome/Collaborators/UserList/UserCard/Info/Info.scss @@ -2,26 +2,19 @@ .home-user-info { &.expired { - .home-user-name { + .home-user-name .name { text-decoration: line-through; } } } .home-user-name { - font-weight: 700; - margin-top: -6px; -} - -.home-user-role { align-items: center; display: flex; gap: 8px; - .role { - color: lighten($text-mute, 10%); - font-size: $font-xs; - text-transform: uppercase; + .name { + font-weight: 700; } } @@ -33,9 +26,9 @@ border: 1px solid rgba($ui-alert, 0.8); color: darken($ui-alert, 20%); display: grid; - font-size: 9px; + font-size: 8px; font-weight: 600; - height: 18px; + height: 16px; justify-content: center; letter-spacing: 0.4px; line-height: 0; diff --git a/src/client/pages/CountryHome/Collaborators/UserList/UserCard/Info/Info.tsx b/src/client/pages/CountryHome/Collaborators/UserList/UserCard/Info/Info.tsx index edf94a9067..bc2a7ffd1c 100644 --- a/src/client/pages/CountryHome/Collaborators/UserList/UserCard/Info/Info.tsx +++ b/src/client/pages/CountryHome/Collaborators/UserList/UserCard/Info/Info.tsx @@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next' import classNames from 'classnames' import { CountryIso } from 'meta/area' -import { UserInvitations, Users } from 'meta/user' +import { UserInvitations } from 'meta/user' import { CountryUserSummaries } from 'meta/user/countryUserSummaries' import { useCountryRouteParams } from 'client/hooks/useRouteParams' @@ -23,16 +23,15 @@ const Info: React.FC = (props: Props) => { return (
-
-
{t(Users.getI18nRoleLabelKey(CountryUserSummaries.getRoleName(user, countryIso)))}
+
+
{user.fullName}
+ {isInvitation && (
{expired ? t('common.expired') : t('common.pending')}
)}
- -
{user.fullName}
) } diff --git a/src/client/pages/CountryHome/Collaborators/UserList/UserCard/UserCard.scss b/src/client/pages/CountryHome/Collaborators/UserList/UserCard/UserCard.scss index 217b09c8fa..3248337761 100644 --- a/src/client/pages/CountryHome/Collaborators/UserList/UserCard/UserCard.scss +++ b/src/client/pages/CountryHome/Collaborators/UserList/UserCard/UserCard.scss @@ -8,15 +8,17 @@ grid-template-columns: auto auto; justify-content: start; padding: $spacing-xxs; + background-color: $ui-bg-light; + transition: background-color 0.2s ease; .user-avatar { align-self: center; + grid-row: span 2; } &.invitation { .user-avatar, - .home-user-role .role, - .home-user-name { + .home-user-name .name { opacity: 0.6; } } @@ -28,7 +30,7 @@ } &:hover { - background-color: rgba($ui-accent-light-extra, 0.2); + background-color: darken($ui-bg-light, 2%); [class*='home-user-action-button-']:not([class*='home-user-action-button-message']) { opacity: 1; diff --git a/src/client/pages/CountryHome/Collaborators/UserList/UserList.tsx b/src/client/pages/CountryHome/Collaborators/UserList/UserList.tsx index 8d8c4d3ec9..9fece93b25 100644 --- a/src/client/pages/CountryHome/Collaborators/UserList/UserList.tsx +++ b/src/client/pages/CountryHome/Collaborators/UserList/UserList.tsx @@ -1,8 +1,13 @@ import './UserList.scss' import React from 'react' +import { useTranslation } from 'react-i18next' import { ApiEndPoint } from 'meta/api/endpoint' +import { CountryIso } from 'meta/area' +import { CountryUserSummary } from 'meta/user' +import { CountryUserSummaries } from 'meta/user/countryUserSummaries' +import { useCountryRouteParams } from 'client/hooks/useRouteParams' import TablePaginated from 'client/components/TablePaginated' import { useColumns } from './hooks/useColumns' @@ -14,6 +19,8 @@ const header = false const path = ApiEndPoint.User.many() const UserList: React.FC = () => { + const { t } = useTranslation() + const { countryIso } = useCountryRouteParams() const columns = useColumns() const compareFn = useUserCompareFn() @@ -21,7 +28,17 @@ const UserList: React.FC = () => {
- + t(`user.roles.${roleName.toString()}`, { count: 2 }), + keySelector: (user: CountryUserSummary) => CountryUserSummaries.getRoleName(user, countryIso), + }} + header={header} + path={path} + />
) } diff --git a/src/server/api/user/updateRoleProps.ts b/src/server/api/user/updateRoleProps.ts index c2e6d871a8..8390cc9a0f 100644 --- a/src/server/api/user/updateRoleProps.ts +++ b/src/server/api/user/updateRoleProps.ts @@ -7,10 +7,7 @@ export const updateRoleProps = async (req: Request, res: Response) => { try { const { id, props } = req.body.role - const userRole = await UserController.updateRoleProps({ - id, - props, - }) + const userRole = await UserController.updateRoleProps({ id, props }) Requests.sendOk(res, userRole) } catch (e) { diff --git a/tsconfig.json b/tsconfig.json index 69465e1b9c..2b447cf9dc 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,6 +6,7 @@ "allowSyntheticDefaultImports": true, "baseUrl": "./src", "isolatedModules": true, + "lib": ["ESNext", "DOM", "DOM.Iterable"], "jsx": "react", "module": "CommonJS", "moduleResolution": "Node", diff --git a/tsconfig.server.json b/tsconfig.server.json index 5b8955dfee..ebd09d42d7 100644 --- a/tsconfig.server.json +++ b/tsconfig.server.json @@ -5,15 +5,15 @@ "compilerOptions": { "allowJs": true, "allowSyntheticDefaultImports": true, - "skipLibCheck": true, + "baseUrl": "./src", + "lib": ["ESNext", "DOM"], "module": "CommonJS", "moduleResolution": "Node", "noImplicitAny": true, "outDir": "./dist/", - "baseUrl": "./src", - "rootDir": "./src", - "target": "es2020", "resolveJsonModule": true, - "lib": ["es2020", "esnext", "dom"] + "rootDir": "./src", + "skipLibCheck": true, + "target": "ESNext" } } diff --git a/yarn.lock b/yarn.lock index feba34d21d..14abd2f40e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13716,10 +13716,10 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== -typescript@^4.6.3: - version "4.9.5" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" - integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== +typescript@^5.7.2: + version "5.7.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.7.2.tgz#3169cf8c4c8a828cde53ba9ecb3d2b1d5dd67be6" + integrity sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg== uberproto@^1.1.0: version "1.2.0"