From a9f91d50cef181ceacaaf5dfa1706510dbf8e951 Mon Sep 17 00:00:00 2001 From: Bruce Tian Date: Tue, 20 Aug 2024 14:31:24 -0700 Subject: [PATCH 01/16] Add column: last met at in OrgMemberRow --- .../components/OrgUserRow/OrgMemberRow.tsx | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/client/modules/userDashboard/components/OrgUserRow/OrgMemberRow.tsx b/packages/client/modules/userDashboard/components/OrgUserRow/OrgMemberRow.tsx index d6b0862a091..af4e0011bee 100644 --- a/packages/client/modules/userDashboard/components/OrgUserRow/OrgMemberRow.tsx +++ b/packages/client/modules/userDashboard/components/OrgUserRow/OrgMemberRow.tsx @@ -41,6 +41,10 @@ const AvatarBlock = styled('div')({ const StyledRow = styled(Row)({ padding: '12px 8px 12px 16px', + display: 'grid', + gridTemplateColumns: 'auto 1fr auto 1fr', + alignItems: 'center', + gap: '16px', [`@media screen and (min-width: ${Breakpoint.SIDEBAR_LEFT}px)`]: { padding: '16px 8px 16px 16px' } @@ -147,6 +151,16 @@ const UserInfo: React.FC = ({ ) +interface LastMetInfoProps { + lastMetAt: string | null +} + +const LastMetInfo: React.FC = ({lastMetAt}) => ( +
+ {lastMetAt ? new Date(lastMetAt).toLocaleDateString() : 'Never'} +
+) + interface UserActionsProps { isViewerOrgAdmin: boolean isViewerBillingLeader: boolean @@ -252,6 +266,7 @@ const OrgMemberRow = (props: Props) => { inactive picture preferredName + lastMetAt } role ...BillingLeaderActionMenu_organizationUser @@ -264,7 +279,7 @@ const OrgMemberRow = (props: Props) => { const {isViewerBillingLeader, isViewerOrgAdmin} = organization const { - user: {email, inactive, picture, preferredName}, + user: {email, inactive, picture, preferredName, lastMetAt}, role } = organizationUser @@ -286,6 +301,7 @@ const OrgMemberRow = (props: Props) => { isOrgAdmin={isOrgAdmin} inactive={inactive} /> + Date: Tue, 20 Aug 2024 14:55:41 -0700 Subject: [PATCH 02/16] dummy table view for OrgMembers --- .../components/OrgMembers/OrgMemberTable.tsx | 66 +++++++++++++++++++ .../components/OrgMembers/OrgMembers.tsx | 19 ++---- .../components/OrgUserRow/OrgMemberRow.tsx | 61 +++++++++-------- 3 files changed, 105 insertions(+), 41 deletions(-) create mode 100644 packages/client/modules/userDashboard/components/OrgMembers/OrgMemberTable.tsx diff --git a/packages/client/modules/userDashboard/components/OrgMembers/OrgMemberTable.tsx b/packages/client/modules/userDashboard/components/OrgMembers/OrgMemberTable.tsx new file mode 100644 index 00000000000..edd02df3ca9 --- /dev/null +++ b/packages/client/modules/userDashboard/components/OrgMembers/OrgMemberTable.tsx @@ -0,0 +1,66 @@ +import styled from '@emotion/styled' +import React from 'react' +import {Breakpoint} from '../../../../types/constEnums' +import OrgMemberRow from '../OrgUserRow/OrgMemberRow' + +const TableWrapper = styled('div')({ + overflowX: 'auto', + width: '100%' +}) + +const StyledTable = styled('table')({ + width: '100%', + borderCollapse: 'collapse', + [`@media screen and (min-width: ${Breakpoint.SIDEBAR_LEFT}px)`]: { + tableLayout: 'fixed' + } +}) + +const TableHeader = styled('th')({ + padding: '12px 8px', + textAlign: 'left', + fontWeight: 600, + borderBottom: '1px solid #e0e0e0' +}) + +interface Props { + organization: any // Replace with the correct type + organizationUsers: any[] // Replace with the correct type + billingLeaderCount: number + orgAdminCount: number +} + +const OrgMemberTable: React.FC = ({ + organization, + organizationUsers, + billingLeaderCount, + orgAdminCount +}) => { + return ( + + + + + Avatar + Name + Last Active Date + Actions + + + + {organizationUsers.map((orgUser) => ( + + ))} + + + + ) +} + +export default OrgMemberTable diff --git a/packages/client/modules/userDashboard/components/OrgMembers/OrgMembers.tsx b/packages/client/modules/userDashboard/components/OrgMembers/OrgMembers.tsx index e56be6deb45..eebb35558ec 100644 --- a/packages/client/modules/userDashboard/components/OrgMembers/OrgMembers.tsx +++ b/packages/client/modules/userDashboard/components/OrgMembers/OrgMembers.tsx @@ -11,7 +11,7 @@ import ExportToCSVButton from '../../../../components/ExportToCSVButton' import Panel from '../../../../components/Panel/Panel' import {ElementWidth} from '../../../../types/constEnums' import {APP_CORS_OPTIONS} from '../../../../types/cors' -import OrgMemberRow from '../OrgUserRow/OrgMemberRow' +import OrgMemberTable from './OrgMemberTable' const StyledPanel = styled(Panel)({ maxWidth: ElementWidth.PANEL_WIDTH @@ -116,17 +116,12 @@ const OrgMembers = (props: Props) => { ) } > - {organizationUsers.edges.map(({node: organizationUser}) => { - return ( - - ) - })} + edge.node)} + billingLeaderCount={billingLeaderCount} + orgAdminCount={orgAdminCount} + /> ) } diff --git a/packages/client/modules/userDashboard/components/OrgUserRow/OrgMemberRow.tsx b/packages/client/modules/userDashboard/components/OrgUserRow/OrgMemberRow.tsx index af4e0011bee..4644c975a67 100644 --- a/packages/client/modules/userDashboard/components/OrgUserRow/OrgMemberRow.tsx +++ b/packages/client/modules/userDashboard/components/OrgUserRow/OrgMemberRow.tsx @@ -14,7 +14,6 @@ import { import Avatar from '../../../../components/Avatar/Avatar' import FlatButton, {FlatButtonProps} from '../../../../components/FlatButton' import IconLabel from '../../../../components/IconLabel' -import Row from '../../../../components/Row/Row' import RowActions from '../../../../components/Row/RowActions' import RowInfo from '../../../../components/Row/RowInfo' import RowInfoHeader from '../../../../components/Row/RowInfoHeader' @@ -39,15 +38,13 @@ const AvatarBlock = styled('div')({ } }) -const StyledRow = styled(Row)({ - padding: '12px 8px 12px 16px', - display: 'grid', - gridTemplateColumns: 'auto 1fr auto 1fr', - alignItems: 'center', - gap: '16px', - [`@media screen and (min-width: ${Breakpoint.SIDEBAR_LEFT}px)`]: { - padding: '16px 8px 16px 16px' - } +const StyledRow = styled('tr')({ + borderBottom: '1px solid #e0e0e0' +}) + +const TableCell = styled('td')({ + padding: '12px 8px', + verticalAlign: 'middle' }) const StyledRowInfo = styled(RowInfo)({ @@ -293,25 +290,31 @@ const OrgMemberRow = (props: Props) => { return ( - - - - + + + + + + + + + + ) } From 56525fbf2b34a8279f6138dff286d16f805b4629 Mon Sep 17 00:00:00 2001 From: Bruce Tian Date: Tue, 20 Aug 2024 15:18:19 -0700 Subject: [PATCH 03/16] Fix some style issues about the table --- .../components/OrgMembers/OrgMemberTable.tsx | 13 +++++++------ .../components/OrgUserRow/OrgMemberRow.tsx | 12 +++++++----- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/packages/client/modules/userDashboard/components/OrgMembers/OrgMemberTable.tsx b/packages/client/modules/userDashboard/components/OrgMembers/OrgMemberTable.tsx index edd02df3ca9..92d9d0fb53b 100644 --- a/packages/client/modules/userDashboard/components/OrgMembers/OrgMemberTable.tsx +++ b/packages/client/modules/userDashboard/components/OrgMembers/OrgMemberTable.tsx @@ -5,7 +5,8 @@ import OrgMemberRow from '../OrgUserRow/OrgMemberRow' const TableWrapper = styled('div')({ overflowX: 'auto', - width: '100%' + width: '100%', + padding: '0 16px' }) const StyledTable = styled('table')({ @@ -41,14 +42,14 @@ const OrgMemberTable: React.FC = ({ - Avatar - Name - Last Active Date - Actions + Avatar + Name & Email + Last Meeting Date + Actions - {organizationUsers.map((orgUser) => ( + {organizationUsers.map((orgUser: any) => ( = ({lastMetAt}) => ( -
- {lastMetAt ? new Date(lastMetAt).toLocaleDateString() : 'Never'} -
-) +const LastMetInfo: React.FC = ({lastMetAt}) => { + const formattedLastMetAt = lastMetAt ? format(new Date(lastMetAt), 'MMM do, yyyy') : 'Never' + return {formattedLastMetAt} +} interface UserActionsProps { isViewerOrgAdmin: boolean isViewerBillingLeader: boolean @@ -292,6 +292,8 @@ const OrgMemberRow = (props: Props) => { + + Date: Tue, 20 Aug 2024 15:29:52 -0700 Subject: [PATCH 04/16] Add sort by last meeting date --- .../components/OrgMembers/OrgMemberTable.tsx | 13 ++++++-- .../components/OrgMembers/OrgMembers.tsx | 30 +++++++++++++++++-- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/packages/client/modules/userDashboard/components/OrgMembers/OrgMemberTable.tsx b/packages/client/modules/userDashboard/components/OrgMembers/OrgMemberTable.tsx index 92d9d0fb53b..facc22c1eeb 100644 --- a/packages/client/modules/userDashboard/components/OrgMembers/OrgMemberTable.tsx +++ b/packages/client/modules/userDashboard/components/OrgMembers/OrgMemberTable.tsx @@ -29,13 +29,19 @@ interface Props { organizationUsers: any[] // Replace with the correct type billingLeaderCount: number orgAdminCount: number + onSort: () => void + sortBy: 'lastMetAt' | null + sortDirection: 'asc' | 'desc' } const OrgMemberTable: React.FC = ({ organization, organizationUsers, billingLeaderCount, - orgAdminCount + orgAdminCount, + onSort, + sortBy, + sortDirection }) => { return ( @@ -44,7 +50,10 @@ const OrgMemberTable: React.FC = ({ Avatar Name & Email - Last Meeting Date + + Last Meeting Date + {sortBy === 'lastMetAt' && (sortDirection === 'asc' ? ' ▲' : ' ▼')} + Actions diff --git a/packages/client/modules/userDashboard/components/OrgMembers/OrgMembers.tsx b/packages/client/modules/userDashboard/components/OrgMembers/OrgMembers.tsx index eebb35558ec..67c455deebf 100644 --- a/packages/client/modules/userDashboard/components/OrgMembers/OrgMembers.tsx +++ b/packages/client/modules/userDashboard/components/OrgMembers/OrgMembers.tsx @@ -2,7 +2,7 @@ import styled from '@emotion/styled' import graphql from 'babel-plugin-relay/macro' import type {Parser as JSON2CSVParser} from 'json2csv' import Parser from 'json2csv/lib/JSON2CSVParser' // only grab the sync parser -import React from 'react' +import React, {useState} from 'react' import {PreloadedQuery, usePaginationFragment, usePreloadedQuery} from 'react-relay' import {OrgMembersPaginationQuery} from '~/__generated__/OrgMembersPaginationQuery.graphql' import {OrgMembersQuery} from '~/__generated__/OrgMembersQuery.graphql' @@ -50,6 +50,7 @@ const OrgMembers = (props: Props) => { user { preferredName email + lastMetAt } ...OrgMemberRow_organizationUser } @@ -79,6 +80,28 @@ const OrgMembers = (props: Props) => { (count, {node}) => (['ORG_ADMIN'].includes(node.role ?? '') ? count + 1 : count), 0 ) + const [sortBy, setSortBy] = useState<'lastMetAt' | null>('lastMetAt') + const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc') + + const sortedOrganizationUsers = [...organizationUsers.edges].sort((a, b) => { + if (sortBy === 'lastMetAt') { + const aDate = a.node.user.lastMetAt ? new Date(a.node.user.lastMetAt) : new Date(0) + const bDate = b.node.user.lastMetAt ? new Date(b.node.user.lastMetAt) : new Date(0) + return sortDirection === 'asc' + ? aDate.getTime() - bDate.getTime() + : bDate.getTime() - aDate.getTime() + } + return 0 + }) + + const handleSort = () => { + if (sortBy === 'lastMetAt') { + setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc') + } else { + setSortBy('lastMetAt') + setSortDirection('desc') + } + } const exportToCSV = async () => { const rows = organizationUsers.edges.map((orgUser, idx) => { @@ -118,9 +141,12 @@ const OrgMembers = (props: Props) => { > edge.node)} + organizationUsers={sortedOrganizationUsers.map((edge) => edge.node)} billingLeaderCount={billingLeaderCount} orgAdminCount={orgAdminCount} + onSort={handleSort} + sortBy={sortBy} + sortDirection={sortDirection} /> ) From 0408846c3e847b1dcde8d552fcc3f7850dfd618f Mon Sep 17 00:00:00 2001 From: Bruce Tian Date: Tue, 20 Aug 2024 15:50:24 -0700 Subject: [PATCH 05/16] feat(orgAdmins): Modify the org members table to be sortable by From 77756363e7dd5743ca07747b00cd4063a50f896d Mon Sep 17 00:00:00 2001 From: Bruce Tian Date: Wed, 21 Aug 2024 13:59:00 -0700 Subject: [PATCH 06/16] Style changes on Members header --- .../components/OrgMembers/OrgMembers.tsx | 56 ++++++++++--------- 1 file changed, 31 insertions(+), 25 deletions(-) diff --git a/packages/client/modules/userDashboard/components/OrgMembers/OrgMembers.tsx b/packages/client/modules/userDashboard/components/OrgMembers/OrgMembers.tsx index 67c455deebf..d0c77cf2d4d 100644 --- a/packages/client/modules/userDashboard/components/OrgMembers/OrgMembers.tsx +++ b/packages/client/modules/userDashboard/components/OrgMembers/OrgMembers.tsx @@ -1,4 +1,3 @@ -import styled from '@emotion/styled' import graphql from 'babel-plugin-relay/macro' import type {Parser as JSON2CSVParser} from 'json2csv' import Parser from 'json2csv/lib/JSON2CSVParser' // only grab the sync parser @@ -8,15 +7,9 @@ import {OrgMembersPaginationQuery} from '~/__generated__/OrgMembersPaginationQue import {OrgMembersQuery} from '~/__generated__/OrgMembersQuery.graphql' import {OrgMembers_viewer$key} from '~/__generated__/OrgMembers_viewer.graphql' import ExportToCSVButton from '../../../../components/ExportToCSVButton' -import Panel from '../../../../components/Panel/Panel' -import {ElementWidth} from '../../../../types/constEnums' import {APP_CORS_OPTIONS} from '../../../../types/cors' import OrgMemberTable from './OrgMemberTable' -const StyledPanel = styled(Panel)({ - maxWidth: ElementWidth.PANEL_WIDTH -}) - interface Props { queryRef: PreloadedQuery } @@ -131,24 +124,37 @@ const OrgMembers = (props: Props) => { } return ( - - ) - } - > - edge.node)} - billingLeaderCount={billingLeaderCount} - orgAdminCount={orgAdminCount} - onSort={handleSort} - sortBy={sortBy} - sortDirection={sortDirection} - /> - +
+
+
+

Members

+
+
+ {isBillingLeader && ( + + )} +
+
+ +
+
+
+
+ {organizationUsers.edges.length} total +
+
+
+ edge.node)} + billingLeaderCount={billingLeaderCount} + orgAdminCount={orgAdminCount} + onSort={handleSort} + sortBy={sortBy} + sortDirection={sortDirection} + /> +
+
) } From 3b46755f5d15fd8cc65251b916bcbdb19d4704cf Mon Sep 17 00:00:00 2001 From: Bruce Tian Date: Wed, 21 Aug 2024 14:17:17 -0700 Subject: [PATCH 07/16] Use lastSeenAt than lastMetAt --- .../components/OrgMembers/OrgMemberTable.tsx | 7 ++++--- .../components/OrgMembers/OrgMembers.tsx | 14 +++++++------- .../components/OrgUserRow/OrgMemberRow.tsx | 16 ++++++++-------- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/packages/client/modules/userDashboard/components/OrgMembers/OrgMemberTable.tsx b/packages/client/modules/userDashboard/components/OrgMembers/OrgMemberTable.tsx index facc22c1eeb..8ebb0620b21 100644 --- a/packages/client/modules/userDashboard/components/OrgMembers/OrgMemberTable.tsx +++ b/packages/client/modules/userDashboard/components/OrgMembers/OrgMemberTable.tsx @@ -1,5 +1,6 @@ import styled from '@emotion/styled' import React from 'react' +import User from '../../../../../server/database/types/User' import {Breakpoint} from '../../../../types/constEnums' import OrgMemberRow from '../OrgUserRow/OrgMemberRow' @@ -30,7 +31,7 @@ interface Props { billingLeaderCount: number orgAdminCount: number onSort: () => void - sortBy: 'lastMetAt' | null + sortBy: keyof User | null sortDirection: 'asc' | 'desc' } @@ -51,8 +52,8 @@ const OrgMemberTable: React.FC = ({ Avatar Name & Email - Last Meeting Date - {sortBy === 'lastMetAt' && (sortDirection === 'asc' ? ' ▲' : ' ▼')} + Last Seen Date + {sortBy === 'lastSeenAt' && (sortDirection === 'asc' ? ' ▲' : ' ▼')} Actions diff --git a/packages/client/modules/userDashboard/components/OrgMembers/OrgMembers.tsx b/packages/client/modules/userDashboard/components/OrgMembers/OrgMembers.tsx index d0c77cf2d4d..3c6a8de6b6c 100644 --- a/packages/client/modules/userDashboard/components/OrgMembers/OrgMembers.tsx +++ b/packages/client/modules/userDashboard/components/OrgMembers/OrgMembers.tsx @@ -43,7 +43,7 @@ const OrgMembers = (props: Props) => { user { preferredName email - lastMetAt + lastSeenAt } ...OrgMemberRow_organizationUser } @@ -73,13 +73,13 @@ const OrgMembers = (props: Props) => { (count, {node}) => (['ORG_ADMIN'].includes(node.role ?? '') ? count + 1 : count), 0 ) - const [sortBy, setSortBy] = useState<'lastMetAt' | null>('lastMetAt') + const [sortBy, setSortBy] = useState<'lastSeenAt' | null>('lastSeenAt') const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc') const sortedOrganizationUsers = [...organizationUsers.edges].sort((a, b) => { - if (sortBy === 'lastMetAt') { - const aDate = a.node.user.lastMetAt ? new Date(a.node.user.lastMetAt) : new Date(0) - const bDate = b.node.user.lastMetAt ? new Date(b.node.user.lastMetAt) : new Date(0) + if (sortBy === 'lastSeenAt') { + const aDate = a.node.user.lastSeenAt ? new Date(a.node.user.lastSeenAt) : new Date(0) + const bDate = b.node.user.lastSeenAt ? new Date(b.node.user.lastSeenAt) : new Date(0) return sortDirection === 'asc' ? aDate.getTime() - bDate.getTime() : bDate.getTime() - aDate.getTime() @@ -88,10 +88,10 @@ const OrgMembers = (props: Props) => { }) const handleSort = () => { - if (sortBy === 'lastMetAt') { + if (sortBy === 'lastSeenAt') { setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc') } else { - setSortBy('lastMetAt') + setSortBy('lastSeenAt') setSortDirection('desc') } } diff --git a/packages/client/modules/userDashboard/components/OrgUserRow/OrgMemberRow.tsx b/packages/client/modules/userDashboard/components/OrgUserRow/OrgMemberRow.tsx index d8d623c9f95..81906269e36 100644 --- a/packages/client/modules/userDashboard/components/OrgUserRow/OrgMemberRow.tsx +++ b/packages/client/modules/userDashboard/components/OrgUserRow/OrgMemberRow.tsx @@ -149,14 +149,14 @@ const UserInfo: React.FC = ({ ) -interface LastMetInfoProps { - lastMetAt: string | null +interface LastSeenInfoProps { + lastSeenAt: string | null } -const LastMetInfo: React.FC = ({lastMetAt}) => { - const formattedLastMetAt = lastMetAt ? format(new Date(lastMetAt), 'MMM do, yyyy') : 'Never' +const LastSeenInfo: React.FC = ({lastSeenAt}) => { + const formattedLastSeenAt = lastSeenAt ? format(new Date(lastSeenAt), 'yyyy-MM-dd') : 'Never' - return {formattedLastMetAt} + return {formattedLastSeenAt} } interface UserActionsProps { isViewerOrgAdmin: boolean @@ -263,7 +263,7 @@ const OrgMemberRow = (props: Props) => { inactive picture preferredName - lastMetAt + lastSeenAt } role ...BillingLeaderActionMenu_organizationUser @@ -276,7 +276,7 @@ const OrgMemberRow = (props: Props) => { const {isViewerBillingLeader, isViewerOrgAdmin} = organization const { - user: {email, inactive, picture, preferredName, lastMetAt}, + user: {email, inactive, picture, preferredName, lastSeenAt}, role } = organizationUser @@ -303,7 +303,7 @@ const OrgMemberRow = (props: Props) => { />
- + Date: Thu, 22 Aug 2024 10:57:45 -0700 Subject: [PATCH 08/16] Squeeze avatar and name, email --- .../components/OrgMembers/OrgMemberTable.tsx | 3 +- .../components/OrgUserRow/OrgMemberRow.tsx | 30 ++++++++----------- 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/packages/client/modules/userDashboard/components/OrgMembers/OrgMemberTable.tsx b/packages/client/modules/userDashboard/components/OrgMembers/OrgMemberTable.tsx index 8ebb0620b21..f6812ed0c81 100644 --- a/packages/client/modules/userDashboard/components/OrgMembers/OrgMemberTable.tsx +++ b/packages/client/modules/userDashboard/components/OrgMembers/OrgMemberTable.tsx @@ -49,8 +49,7 @@ const OrgMemberTable: React.FC = ({ - Avatar - Name & Email + User Last Seen Date {sortBy === 'lastSeenAt' && (sortDirection === 'asc' ? ' ▲' : ' ▼')} diff --git a/packages/client/modules/userDashboard/components/OrgUserRow/OrgMemberRow.tsx b/packages/client/modules/userDashboard/components/OrgUserRow/OrgMemberRow.tsx index 81906269e36..5a870adcafb 100644 --- a/packages/client/modules/userDashboard/components/OrgUserRow/OrgMemberRow.tsx +++ b/packages/client/modules/userDashboard/components/OrgUserRow/OrgMemberRow.tsx @@ -48,10 +48,6 @@ const TableCell = styled('td')({ verticalAlign: 'middle' }) -const StyledRowInfo = styled(RowInfo)({ - paddingLeft: 0 -}) - const ActionsBlock = styled('div')({ alignItems: 'center', display: 'flex', @@ -136,7 +132,7 @@ const UserInfo: React.FC = ({ isOrgAdmin, inactive }) => ( - + {preferredName} {isBillingLeader && Billing Leader} @@ -146,7 +142,7 @@ const UserInfo: React.FC = ({ {email} - + ) interface LastSeenInfoProps { @@ -156,7 +152,7 @@ interface LastSeenInfoProps { const LastSeenInfo: React.FC = ({lastSeenAt}) => { const formattedLastSeenAt = lastSeenAt ? format(new Date(lastSeenAt), 'yyyy-MM-dd') : 'Never' - return {formattedLastSeenAt} + return {formattedLastSeenAt} } interface UserActionsProps { isViewerOrgAdmin: boolean @@ -291,16 +287,16 @@ const OrgMemberRow = (props: Props) => { return ( - - - - +
+ + +
From 6552f2e8b58a283544f097fa18d730f9ef601024 Mon Sep 17 00:00:00 2001 From: Bruce Tian Date: Thu, 22 Aug 2024 11:02:59 -0700 Subject: [PATCH 09/16] Add sort by email --- .../components/OrgMembers/OrgMemberTable.tsx | 11 +++++++---- .../components/OrgMembers/OrgMembers.tsx | 15 ++++++++++----- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/packages/client/modules/userDashboard/components/OrgMembers/OrgMemberTable.tsx b/packages/client/modules/userDashboard/components/OrgMembers/OrgMemberTable.tsx index f6812ed0c81..2887a573d9c 100644 --- a/packages/client/modules/userDashboard/components/OrgMembers/OrgMemberTable.tsx +++ b/packages/client/modules/userDashboard/components/OrgMembers/OrgMemberTable.tsx @@ -30,8 +30,8 @@ interface Props { organizationUsers: any[] // Replace with the correct type billingLeaderCount: number orgAdminCount: number - onSort: () => void - sortBy: keyof User | null + onSort: (column: keyof User) => void + sortBy: keyof User sortDirection: 'asc' | 'desc' } @@ -49,8 +49,11 @@ const OrgMemberTable: React.FC = ({ - User - + onSort('email')} style={{cursor: 'pointer'}}> + User + {sortBy === 'email' && (sortDirection === 'asc' ? ' ▲' : ' ▼')} + + onSort('lastSeenAt')} style={{cursor: 'pointer'}}> Last Seen Date {sortBy === 'lastSeenAt' && (sortDirection === 'asc' ? ' ▲' : ' ▼')} diff --git a/packages/client/modules/userDashboard/components/OrgMembers/OrgMembers.tsx b/packages/client/modules/userDashboard/components/OrgMembers/OrgMembers.tsx index 3c6a8de6b6c..5dc18ca8a8b 100644 --- a/packages/client/modules/userDashboard/components/OrgMembers/OrgMembers.tsx +++ b/packages/client/modules/userDashboard/components/OrgMembers/OrgMembers.tsx @@ -6,6 +6,7 @@ import {PreloadedQuery, usePaginationFragment, usePreloadedQuery} from 'react-re import {OrgMembersPaginationQuery} from '~/__generated__/OrgMembersPaginationQuery.graphql' import {OrgMembersQuery} from '~/__generated__/OrgMembersQuery.graphql' import {OrgMembers_viewer$key} from '~/__generated__/OrgMembers_viewer.graphql' +import User from '../../../../../server/database/types/User' import ExportToCSVButton from '../../../../components/ExportToCSVButton' import {APP_CORS_OPTIONS} from '../../../../types/cors' import OrgMemberTable from './OrgMemberTable' @@ -73,7 +74,7 @@ const OrgMembers = (props: Props) => { (count, {node}) => (['ORG_ADMIN'].includes(node.role ?? '') ? count + 1 : count), 0 ) - const [sortBy, setSortBy] = useState<'lastSeenAt' | null>('lastSeenAt') + const [sortBy, setSortBy] = useState('lastSeenAt') const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc') const sortedOrganizationUsers = [...organizationUsers.edges].sort((a, b) => { @@ -83,16 +84,20 @@ const OrgMembers = (props: Props) => { return sortDirection === 'asc' ? aDate.getTime() - bDate.getTime() : bDate.getTime() - aDate.getTime() + } else if (sortBy === 'email') { + return sortDirection === 'asc' + ? a.node.user.email.localeCompare(b.node.user.email) + : b.node.user.email.localeCompare(a.node.user.email) } return 0 }) - const handleSort = () => { - if (sortBy === 'lastSeenAt') { + const handleSort = (column: keyof User) => { + if (sortBy === column) { setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc') } else { - setSortBy('lastSeenAt') - setSortDirection('desc') + setSortBy(column) + setSortDirection('asc') } } From 4230e8fae7278dfeabd90f02edcd3660a18353e7 Mon Sep 17 00:00:00 2001 From: Bruce Tian Date: Thu, 22 Aug 2024 11:06:51 -0700 Subject: [PATCH 10/16] Update width --- .../components/OrgMembers/OrgMemberTable.tsx | 18 +++++--- .../components/OrgUserRow/OrgMemberRow.tsx | 45 ++++++++++++------- 2 files changed, 42 insertions(+), 21 deletions(-) diff --git a/packages/client/modules/userDashboard/components/OrgMembers/OrgMemberTable.tsx b/packages/client/modules/userDashboard/components/OrgMembers/OrgMemberTable.tsx index 2887a573d9c..87dda9b6b06 100644 --- a/packages/client/modules/userDashboard/components/OrgMembers/OrgMemberTable.tsx +++ b/packages/client/modules/userDashboard/components/OrgMembers/OrgMemberTable.tsx @@ -13,17 +13,19 @@ const TableWrapper = styled('div')({ const StyledTable = styled('table')({ width: '100%', borderCollapse: 'collapse', + tableLayout: 'fixed', [`@media screen and (min-width: ${Breakpoint.SIDEBAR_LEFT}px)`]: { tableLayout: 'fixed' } }) -const TableHeader = styled('th')({ +const TableHeader = styled('th')<{width: string}>(({width}) => ({ padding: '12px 8px', textAlign: 'left', fontWeight: 600, - borderBottom: '1px solid #e0e0e0' -}) + borderBottom: '1px solid #e0e0e0', + width: width +})) interface Props { organization: any // Replace with the correct type @@ -49,15 +51,19 @@ const OrgMemberTable: React.FC = ({ - onSort('email')} style={{cursor: 'pointer'}}> + onSort('email')} style={{cursor: 'pointer'}}> User {sortBy === 'email' && (sortDirection === 'asc' ? ' ▲' : ' ▼')} - onSort('lastSeenAt')} style={{cursor: 'pointer'}}> + onSort('lastSeenAt')} + style={{cursor: 'pointer'}} + > Last Seen Date {sortBy === 'lastSeenAt' && (sortDirection === 'asc' ? ' ▲' : ' ▼')} - Actions + diff --git a/packages/client/modules/userDashboard/components/OrgUserRow/OrgMemberRow.tsx b/packages/client/modules/userDashboard/components/OrgUserRow/OrgMemberRow.tsx index 5a870adcafb..97a4d761d90 100644 --- a/packages/client/modules/userDashboard/components/OrgUserRow/OrgMemberRow.tsx +++ b/packages/client/modules/userDashboard/components/OrgUserRow/OrgMemberRow.tsx @@ -43,10 +43,11 @@ const StyledRow = styled('tr')({ borderBottom: '1px solid #e0e0e0' }) -const TableCell = styled('td')({ +const TableCell = styled('td')<{width?: string}>(({width}) => ({ padding: '12px 8px', - verticalAlign: 'middle' -}) + verticalAlign: 'middle', + width: width +})) const ActionsBlock = styled('div')({ alignItems: 'center', @@ -228,6 +229,18 @@ const UserActions: React.FC = ({ ) } +const UserInfoWrapper = styled('div')({ + display: 'flex', + alignItems: 'center', + width: '100%', + overflow: 'hidden' +}) + +const UserInfoContent = styled('div')({ + flexGrow: 1, + minWidth: 0 +}) + const OrgMemberRow = (props: Props) => { const atmosphere = useAtmosphere() const { @@ -286,22 +299,24 @@ const OrgMemberRow = (props: Props) => { return ( - -
+ + - -
+ + + +
- + - + Date: Thu, 22 Aug 2024 13:41:10 -0700 Subject: [PATCH 11/16] Replace some styled elements with standard div using Tailwind classes --- .../components/OrgMembers/OrgMemberTable.tsx | 51 +++++----------- .../components/OrgUserRow/OrgMemberRow.tsx | 58 +++++-------------- 2 files changed, 28 insertions(+), 81 deletions(-) diff --git a/packages/client/modules/userDashboard/components/OrgMembers/OrgMemberTable.tsx b/packages/client/modules/userDashboard/components/OrgMembers/OrgMemberTable.tsx index 87dda9b6b06..40595d76365 100644 --- a/packages/client/modules/userDashboard/components/OrgMembers/OrgMemberTable.tsx +++ b/packages/client/modules/userDashboard/components/OrgMembers/OrgMemberTable.tsx @@ -1,32 +1,7 @@ -import styled from '@emotion/styled' import React from 'react' import User from '../../../../../server/database/types/User' -import {Breakpoint} from '../../../../types/constEnums' import OrgMemberRow from '../OrgUserRow/OrgMemberRow' -const TableWrapper = styled('div')({ - overflowX: 'auto', - width: '100%', - padding: '0 16px' -}) - -const StyledTable = styled('table')({ - width: '100%', - borderCollapse: 'collapse', - tableLayout: 'fixed', - [`@media screen and (min-width: ${Breakpoint.SIDEBAR_LEFT}px)`]: { - tableLayout: 'fixed' - } -}) - -const TableHeader = styled('th')<{width: string}>(({width}) => ({ - padding: '12px 8px', - textAlign: 'left', - fontWeight: 600, - borderBottom: '1px solid #e0e0e0', - width: width -})) - interface Props { organization: any // Replace with the correct type organizationUsers: any[] // Replace with the correct type @@ -47,23 +22,25 @@ const OrgMemberTable: React.FC = ({ sortDirection }) => { return ( - - +
+ - - onSort('email')} style={{cursor: 'pointer'}}> + + + @@ -77,8 +54,8 @@ const OrgMemberTable: React.FC = ({ /> ))} - - +
onSort('email')} + > User {sortBy === 'email' && (sortDirection === 'asc' ? ' ▲' : ' ▼')} - - + onSort('lastSeenAt')} - style={{cursor: 'pointer'}} > Last Seen Date {sortBy === 'lastSeenAt' && (sortDirection === 'asc' ? ' ▲' : ' ▼')} - - +
+
) } diff --git a/packages/client/modules/userDashboard/components/OrgUserRow/OrgMemberRow.tsx b/packages/client/modules/userDashboard/components/OrgUserRow/OrgMemberRow.tsx index 97a4d761d90..005489e7552 100644 --- a/packages/client/modules/userDashboard/components/OrgUserRow/OrgMemberRow.tsx +++ b/packages/client/modules/userDashboard/components/OrgUserRow/OrgMemberRow.tsx @@ -27,39 +27,9 @@ import {MenuPosition} from '../../../../hooks/useCoords' import useMenu from '../../../../hooks/useMenu' import useModal from '../../../../hooks/useModal' import defaultUserAvatar from '../../../../styles/theme/images/avatar-user.svg' -import {Breakpoint} from '../../../../types/constEnums' import lazyPreload from '../../../../utils/lazyPreload' import withMutationProps, {WithMutationProps} from '../../../../utils/relay/withMutationProps' -const AvatarBlock = styled('div')({ - display: 'none', - [`@media screen and (min-width: ${Breakpoint.SIDEBAR_LEFT}px)`]: { - display: 'block', - marginRight: 16 - } -}) - -const StyledRow = styled('tr')({ - borderBottom: '1px solid #e0e0e0' -}) - -const TableCell = styled('td')<{width?: string}>(({width}) => ({ - padding: '12px 8px', - verticalAlign: 'middle', - width: width -})) - -const ActionsBlock = styled('div')({ - alignItems: 'center', - display: 'flex', - justifyContent: 'flex-end' -}) - -const MenuToggleBlock = styled('div')({ - marginLeft: 8, - width: '2rem' -}) - interface Props extends WithMutationProps { billingLeaderCount: number orgAdminCount: number @@ -109,13 +79,13 @@ interface UserAvatarProps { } const UserAvatar: React.FC = ({picture}) => ( - +
{picture ? ( ) : ( default avatar )} - +
) interface UserInfoProps { @@ -199,14 +169,14 @@ const UserActions: React.FC = ({ return ( - +
{showLeaveButton && ( Leave Organization )} {(isViewerOrgAdmin || (isViewerBillingLeader && !isViewerLastBillingLeader)) && ( - +
= ({ } ref={originRef} /> - +
)} {isViewerOrgAdmin && menuPortal()} {!isViewerOrgAdmin && @@ -224,7 +194,7 @@ const UserActions: React.FC = ({ {removeModal( )} - +
) } @@ -298,8 +268,8 @@ const OrgMemberRow = (props: Props) => { const isViewerLastOrgAdmin = isViewerOrgAdmin && isOrgAdmin && orgAdminCount === 1 return ( - - + + @@ -312,11 +282,11 @@ const OrgMemberRow = (props: Props) => { /> - - + + - - + + { preferredName={preferredName} viewerId={viewerId} /> - - + + ) } From 530b46fb815bb3c1191c243a43521757d33c0517 Mon Sep 17 00:00:00 2001 From: Bruce Tian Date: Thu, 22 Aug 2024 14:22:15 -0700 Subject: [PATCH 12/16] Remove OrgMemberTable --- .../components/OrgMembers/OrgMemberTable.tsx | 62 ------------------- .../components/OrgMembers/OrgMembers.tsx | 45 +++++++++++--- 2 files changed, 35 insertions(+), 72 deletions(-) delete mode 100644 packages/client/modules/userDashboard/components/OrgMembers/OrgMemberTable.tsx diff --git a/packages/client/modules/userDashboard/components/OrgMembers/OrgMemberTable.tsx b/packages/client/modules/userDashboard/components/OrgMembers/OrgMemberTable.tsx deleted file mode 100644 index 40595d76365..00000000000 --- a/packages/client/modules/userDashboard/components/OrgMembers/OrgMemberTable.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import React from 'react' -import User from '../../../../../server/database/types/User' -import OrgMemberRow from '../OrgUserRow/OrgMemberRow' - -interface Props { - organization: any // Replace with the correct type - organizationUsers: any[] // Replace with the correct type - billingLeaderCount: number - orgAdminCount: number - onSort: (column: keyof User) => void - sortBy: keyof User - sortDirection: 'asc' | 'desc' -} - -const OrgMemberTable: React.FC = ({ - organization, - organizationUsers, - billingLeaderCount, - orgAdminCount, - onSort, - sortBy, - sortDirection -}) => { - return ( -
- - - - - - - - - - {organizationUsers.map((orgUser: any) => ( - - ))} - -
onSort('email')} - > - User - {sortBy === 'email' && (sortDirection === 'asc' ? ' ▲' : ' ▼')} - onSort('lastSeenAt')} - > - Last Seen Date - {sortBy === 'lastSeenAt' && (sortDirection === 'asc' ? ' ▲' : ' ▼')} -
-
- ) -} - -export default OrgMemberTable diff --git a/packages/client/modules/userDashboard/components/OrgMembers/OrgMembers.tsx b/packages/client/modules/userDashboard/components/OrgMembers/OrgMembers.tsx index 5dc18ca8a8b..af3e3fe53f9 100644 --- a/packages/client/modules/userDashboard/components/OrgMembers/OrgMembers.tsx +++ b/packages/client/modules/userDashboard/components/OrgMembers/OrgMembers.tsx @@ -9,7 +9,7 @@ import {OrgMembers_viewer$key} from '~/__generated__/OrgMembers_viewer.graphql' import User from '../../../../../server/database/types/User' import ExportToCSVButton from '../../../../components/ExportToCSVButton' import {APP_CORS_OPTIONS} from '../../../../types/cors' -import OrgMemberTable from './OrgMemberTable' +import OrgMemberRow from '../OrgUserRow/OrgMemberRow' interface Props { queryRef: PreloadedQuery @@ -149,15 +149,40 @@ const OrgMembers = (props: Props) => { - edge.node)} - billingLeaderCount={billingLeaderCount} - orgAdminCount={orgAdminCount} - onSort={handleSort} - sortBy={sortBy} - sortDirection={sortDirection} - /> +
+ + + + + + + + + + {sortedOrganizationUsers.map(({node: organizationUser}) => ( + + ))} + +
handleSort('email')} + > + User + {sortBy === 'email' && (sortDirection === 'asc' ? ' ▲' : ' ▼')} + handleSort('lastSeenAt')} + > + Last Seen Date + {sortBy === 'lastSeenAt' && (sortDirection === 'asc' ? ' ▲' : ' ▼')} +
+
) From 720ba76639e719f1238149a0ad07910a370af66b Mon Sep 17 00:00:00 2001 From: Bruce Tian Date: Thu, 22 Aug 2024 14:27:26 -0700 Subject: [PATCH 13/16] More cleanup --- .../components/OrgUserRow/OrgMemberRow.tsx | 32 ++++--------------- 1 file changed, 6 insertions(+), 26 deletions(-) diff --git a/packages/client/modules/userDashboard/components/OrgUserRow/OrgMemberRow.tsx b/packages/client/modules/userDashboard/components/OrgUserRow/OrgMemberRow.tsx index 005489e7552..b4282356cd0 100644 --- a/packages/client/modules/userDashboard/components/OrgUserRow/OrgMemberRow.tsx +++ b/packages/client/modules/userDashboard/components/OrgUserRow/OrgMemberRow.tsx @@ -116,15 +116,6 @@ const UserInfo: React.FC = ({ ) -interface LastSeenInfoProps { - lastSeenAt: string | null -} - -const LastSeenInfo: React.FC = ({lastSeenAt}) => { - const formattedLastSeenAt = lastSeenAt ? format(new Date(lastSeenAt), 'yyyy-MM-dd') : 'Never' - - return {formattedLastSeenAt} -} interface UserActionsProps { isViewerOrgAdmin: boolean isViewerBillingLeader: boolean @@ -199,18 +190,6 @@ const UserActions: React.FC = ({ ) } -const UserInfoWrapper = styled('div')({ - display: 'flex', - alignItems: 'center', - width: '100%', - overflow: 'hidden' -}) - -const UserInfoContent = styled('div')({ - flexGrow: 1, - minWidth: 0 -}) - const OrgMemberRow = (props: Props) => { const atmosphere = useAtmosphere() const { @@ -266,13 +245,14 @@ const OrgMemberRow = (props: Props) => { const isViewerLastBillingLeader = isViewerBillingLeader && isBillingLeader && billingLeaderCount === 1 const isViewerLastOrgAdmin = isViewerOrgAdmin && isOrgAdmin && orgAdminCount === 1 + const formattedLastSeenAt = lastSeenAt ? format(new Date(lastSeenAt), 'yyyy-MM-dd') : 'Never' return ( - +
- +
{ isOrgAdmin={isOrgAdmin} inactive={inactive} /> - - +
+
- + {formattedLastSeenAt} Date: Tue, 3 Sep 2024 10:35:29 -0700 Subject: [PATCH 14/16] Use useMemo hook for sortedOrganizationUsers --- .../components/OrgMembers/OrgMembers.tsx | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/packages/client/modules/userDashboard/components/OrgMembers/OrgMembers.tsx b/packages/client/modules/userDashboard/components/OrgMembers/OrgMembers.tsx index af3e3fe53f9..018ff1f6c50 100644 --- a/packages/client/modules/userDashboard/components/OrgMembers/OrgMembers.tsx +++ b/packages/client/modules/userDashboard/components/OrgMembers/OrgMembers.tsx @@ -1,7 +1,7 @@ import graphql from 'babel-plugin-relay/macro' import type {Parser as JSON2CSVParser} from 'json2csv' import Parser from 'json2csv/lib/JSON2CSVParser' // only grab the sync parser -import React, {useState} from 'react' +import React, {useMemo, useState} from 'react' import {PreloadedQuery, usePaginationFragment, usePreloadedQuery} from 'react-relay' import {OrgMembersPaginationQuery} from '~/__generated__/OrgMembersPaginationQuery.graphql' import {OrgMembersQuery} from '~/__generated__/OrgMembersQuery.graphql' @@ -77,20 +77,22 @@ const OrgMembers = (props: Props) => { const [sortBy, setSortBy] = useState('lastSeenAt') const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc') - const sortedOrganizationUsers = [...organizationUsers.edges].sort((a, b) => { - if (sortBy === 'lastSeenAt') { - const aDate = a.node.user.lastSeenAt ? new Date(a.node.user.lastSeenAt) : new Date(0) - const bDate = b.node.user.lastSeenAt ? new Date(b.node.user.lastSeenAt) : new Date(0) - return sortDirection === 'asc' - ? aDate.getTime() - bDate.getTime() - : bDate.getTime() - aDate.getTime() - } else if (sortBy === 'email') { - return sortDirection === 'asc' - ? a.node.user.email.localeCompare(b.node.user.email) - : b.node.user.email.localeCompare(a.node.user.email) - } - return 0 - }) + const sortedOrganizationUsers = useMemo(() => { + return [...organizationUsers.edges].sort((a, b) => { + if (sortBy === 'lastSeenAt') { + const aDate = a.node.user.lastSeenAt ? new Date(a.node.user.lastSeenAt) : new Date(0) + const bDate = b.node.user.lastSeenAt ? new Date(b.node.user.lastSeenAt) : new Date(0) + return sortDirection === 'asc' + ? aDate.getTime() - bDate.getTime() + : bDate.getTime() - aDate.getTime() + } else if (sortBy === 'email') { + return sortDirection === 'asc' + ? a.node.user.email.localeCompare(b.node.user.email) + : b.node.user.email.localeCompare(a.node.user.email) + } + return 0 + }) + }, [organizationUsers.edges, sortBy, sortDirection]) const handleSort = (column: keyof User) => { if (sortBy === column) { From 88cad2255d2d7570eb8eabf30a68eb7a0d7b35e2 Mon Sep 17 00:00:00 2001 From: Bruce Tian Date: Tue, 3 Sep 2024 10:40:01 -0700 Subject: [PATCH 15/16] Change from sort by email to sort by name --- .../components/OrgMembers/OrgMembers.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/client/modules/userDashboard/components/OrgMembers/OrgMembers.tsx b/packages/client/modules/userDashboard/components/OrgMembers/OrgMembers.tsx index 018ff1f6c50..0e3e0b19a4f 100644 --- a/packages/client/modules/userDashboard/components/OrgMembers/OrgMembers.tsx +++ b/packages/client/modules/userDashboard/components/OrgMembers/OrgMembers.tsx @@ -85,10 +85,10 @@ const OrgMembers = (props: Props) => { return sortDirection === 'asc' ? aDate.getTime() - bDate.getTime() : bDate.getTime() - aDate.getTime() - } else if (sortBy === 'email') { + } else if (sortBy === 'preferredName') { return sortDirection === 'asc' - ? a.node.user.email.localeCompare(b.node.user.email) - : b.node.user.email.localeCompare(a.node.user.email) + ? a.node.user.preferredName.localeCompare(b.node.user.preferredName) + : b.node.user.preferredName.localeCompare(a.node.user.preferredName) } return 0 }) @@ -157,16 +157,16 @@ const OrgMembers = (props: Props) => { handleSort('email')} + onClick={() => handleSort('preferredName')} > User - {sortBy === 'email' && (sortDirection === 'asc' ? ' ▲' : ' ▼')} + {sortBy === 'preferredName' && (sortDirection === 'asc' ? ' ▲' : ' ▼')} handleSort('lastSeenAt')} > - Last Seen Date + Last Seen {sortBy === 'lastSeenAt' && (sortDirection === 'asc' ? ' ▲' : ' ▼')} From 8e91ab52d6f6ab76a9e97695d307500488a58494 Mon Sep 17 00:00:00 2001 From: Bruce Tian Date: Tue, 3 Sep 2024 11:19:13 -0700 Subject: [PATCH 16/16] Change text style for contacting support@parabol.co --- packages/client/components/OrgAdminActionMenu.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/client/components/OrgAdminActionMenu.tsx b/packages/client/components/OrgAdminActionMenu.tsx index 37119d67ba5..c61a4679d15 100644 --- a/packages/client/components/OrgAdminActionMenu.tsx +++ b/packages/client/components/OrgAdminActionMenu.tsx @@ -1,5 +1,4 @@ import {MoreVert} from '@mui/icons-material' -import * as DropdownMenu from '@radix-ui/react-dropdown-menu' import graphql from 'babel-plugin-relay/macro' import React from 'react' import {useFragment} from 'react-relay' @@ -123,9 +122,14 @@ export const OrgAdminActionMenu = (props: Props) => { )} {isSelf && ((isOrgAdmin && isViewerLastOrgAdmin) || (isBillingLeader && isViewerLastRole)) && ( - + { + window.location.href = + 'mailto:support@parabol.co?subject=Request to be removed from organization' + }} + > {'Contact support@parabol.co to be removed'} - + )}