diff --git a/.release-please-manifest.json b/.release-please-manifest.json index dcfe77c1e9a..0e19973322f 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "7.45.2" + ".": "7.46.0" } diff --git a/CHANGELOG.md b/CHANGELOG.md index aaa80d4c0cc..5fe32e47683 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,19 @@ This project adheres to [Semantic Versioning](http://semver.org/). This CHANGELOG follows conventions [outlined here](http://keepachangelog.com/). +## [7.46.0](https://github.com/ParabolInc/parabol/compare/v7.45.2...v7.46.0) (2024-09-04) + + +### Added + +* **orgAdmin:** search in org members page ([#10187](https://github.com/ParabolInc/parabol/issues/10187)) ([968452e](https://github.com/ParabolInc/parabol/commit/968452e28003b188f6706f10b005d84508e11634)) +* **orgAdmins:** Make org members view sortable ([#10146](https://github.com/ParabolInc/parabol/issues/10146)) ([97bb948](https://github.com/ParabolInc/parabol/commit/97bb948330e1e57a331113e267ec5965a84ea6e4)) + + +### Changed + +* **deps:** bump micromatch from 4.0.5 to 4.0.8 ([#10164](https://github.com/ParabolInc/parabol/issues/10164)) ([70f69ce](https://github.com/ParabolInc/parabol/commit/70f69ce039f9550c52f391c0f556919a7fe4589b)) + ## [7.45.2](https://github.com/ParabolInc/parabol/compare/v7.45.1...v7.45.2) (2024-08-29) diff --git a/package.json b/package.json index 872a9104293..d3864155421 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "description": "An open-source app for building smarter, more agile teams.", "author": "Parabol Inc. (http://github.com/ParabolInc)", "license": "AGPL-3.0", - "version": "7.45.2", + "version": "7.46.0", "repository": { "type": "git", "url": "https://github.com/ParabolInc/parabol" diff --git a/packages/chronos/package.json b/packages/chronos/package.json index 83fdd61db90..60f90f1391d 100644 --- a/packages/chronos/package.json +++ b/packages/chronos/package.json @@ -1,6 +1,6 @@ { "name": "chronos", - "version": "7.45.2", + "version": "7.46.0", "description": "A cron job scheduler", "author": "Matt Krick ", "homepage": "https://github.com/ParabolInc/parabol/tree/master/packages/chronos#readme", @@ -25,6 +25,6 @@ }, "dependencies": { "cron": "^2.3.1", - "parabol-server": "7.45.2" + "parabol-server": "7.46.0" } } 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'} - + )} diff --git a/packages/client/modules/userDashboard/components/OrgMembers/OrgMembers.tsx b/packages/client/modules/userDashboard/components/OrgMembers/OrgMembers.tsx index e9534528b3b..8fc04aab8c9 100644 --- a/packages/client/modules/userDashboard/components/OrgMembers/OrgMembers.tsx +++ b/packages/client/modules/userDashboard/components/OrgMembers/OrgMembers.tsx @@ -1,22 +1,16 @@ -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, {useCallback, useMemo, useState} from 'react' import {PreloadedQuery, usePaginationFragment, usePreloadedQuery} from 'react-relay' 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 Panel from '../../../../components/Panel/Panel' -import {ElementWidth} from '../../../../types/constEnums' import {APP_CORS_OPTIONS} from '../../../../types/cors' import OrgMemberRow from '../OrgUserRow/OrgMemberRow' -const StyledPanel = styled(Panel)({ - maxWidth: ElementWidth.PANEL_WIDTH -}) - interface Props { queryRef: PreloadedQuery } @@ -50,6 +44,7 @@ const OrgMembers = (props: Props) => { user { preferredName email + lastSeenAt } ...OrgMemberRow_organizationUser } @@ -70,6 +65,59 @@ const OrgMembers = (props: Props) => { const {organization} = viewer if (!organization) return null const {organizationUsers, name: orgName, isBillingLeader} = organization + const billingLeaderCount = organizationUsers.edges.reduce( + (count, {node}) => + ['BILLING_LEADER', 'ORG_ADMIN'].includes(node.role ?? '') ? count + 1 : count, + 0 + ) + const orgAdminCount = organizationUsers.edges.reduce( + (count, {node}) => (['ORG_ADMIN'].includes(node.role ?? '') ? count + 1 : count), + 0 + ) + const [sortBy, setSortBy] = useState('lastSeenAt') + const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc') + const [searchInput, setSearchInput] = useState('') + + const handleSearchChange = useCallback((e: React.ChangeEvent) => { + setSearchInput(e.target.value) + }, []) + + const filteredOrgUsers = useMemo(() => { + const cleanedSearchInput = searchInput.toLowerCase().trim() + return organizationUsers.edges + .map(({node}) => node) + .filter( + (user) => + user.user.preferredName.toLowerCase().includes(cleanedSearchInput) || + user.user.email.toLowerCase().includes(cleanedSearchInput) + ) + }, [organizationUsers.edges, searchInput]) + + const finalOrgUsers = useMemo(() => { + return [...filteredOrgUsers].sort((a, b) => { + if (sortBy === 'lastSeenAt') { + const aDate = a.user.lastSeenAt ? new Date(a.user.lastSeenAt) : new Date(0) + const bDate = b.user.lastSeenAt ? new Date(b.user.lastSeenAt) : new Date(0) + return sortDirection === 'asc' + ? aDate.getTime() - bDate.getTime() + : bDate.getTime() - aDate.getTime() + } else if (sortBy === 'preferredName') { + return sortDirection === 'asc' + ? a.user.preferredName.localeCompare(b.user.preferredName) + : b.user.preferredName.localeCompare(a.user.preferredName) + } + return 0 + }) + }, [filteredOrgUsers, sortBy, sortDirection]) + + const handleSort = (column: keyof User) => { + if (sortBy === column) { + setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc') + } else { + setSortBy(column) + setSortDirection('asc') + } + } const exportToCSV = async () => { const rows = organizationUsers.edges.map((orgUser, idx) => { @@ -99,24 +147,72 @@ const OrgMembers = (props: Props) => { } return ( - - ) - } - > - {organizationUsers.edges.map(({node: organizationUser}) => { - return ( - - ) - })} - +
+
+
+

Members

+
+
+ {isBillingLeader && ( + + )} +
+
+ +
+ +
+ +
+
+
+
+ {organizationUsers.edges.length} total +
+
+
+
+ + + + + + + + + + {finalOrgUsers.map((organizationUser) => ( + + ))} + +
handleSort('preferredName')} + > + User + {sortBy === 'preferredName' && (sortDirection === 'asc' ? ' ▲' : ' ▼')} + handleSort('lastSeenAt')} + > + Last Seen + {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 3ee6efc2135..0d795ba5f5c 100644 --- a/packages/client/modules/userDashboard/components/OrgUserRow/OrgMemberRow.tsx +++ b/packages/client/modules/userDashboard/components/OrgUserRow/OrgMemberRow.tsx @@ -1,5 +1,6 @@ import styled from '@emotion/styled' import graphql from 'babel-plugin-relay/macro' +import {format} from 'date-fns' import React from 'react' import {useFragment} from 'react-relay' import { @@ -11,7 +12,6 @@ import { OrgMemberRow_organizationUser$key } from '../../../../__generated__/OrgMemberRow_organizationUser.graphql' import Avatar from '../../../../components/Avatar/Avatar' -import Row from '../../../../components/Row/Row' import RowActions from '../../../../components/Row/RowActions' import RowInfo from '../../../../components/Row/RowInfo' import RowInfoHeader from '../../../../components/Row/RowInfoHeader' @@ -22,28 +22,8 @@ import InactiveTag from '../../../../components/Tag/InactiveTag' import RoleTag from '../../../../components/Tag/RoleTag' import useModal from '../../../../hooks/useModal' import defaultUserAvatar from '../../../../styles/theme/images/avatar-user.svg' -import {Breakpoint} from '../../../../types/constEnums' import lazyPreload from '../../../../utils/lazyPreload' -const AvatarBlock = styled('div')({ - display: 'none', - [`@media screen and (min-width: ${Breakpoint.SIDEBAR_LEFT}px)`]: { - display: 'block', - marginRight: 16 - } -}) - -const StyledRow = styled(Row)({ - padding: '12px 8px 12px 16px', - [`@media screen and (min-width: ${Breakpoint.SIDEBAR_LEFT}px)`]: { - padding: '16px 8px 16px 16px' - } -}) - -const StyledRowInfo = styled(RowInfo)({ - paddingLeft: 0 -}) - const ActionsBlock = styled('div')({ alignItems: 'center', display: 'flex', @@ -51,6 +31,8 @@ const ActionsBlock = styled('div')({ }) interface Props { + billingLeaderCount: number + orgAdminCount: number organizationUser: OrgMemberRow_organizationUser$key organization: OrgMemberRow_organization$key } @@ -74,13 +56,13 @@ interface UserAvatarProps { } const UserAvatar: React.FC = ({picture}) => ( - +
{picture ? ( ) : ( default avatar )} - +
) interface UserInfoProps { @@ -98,7 +80,7 @@ const UserInfo: React.FC = ({ isOrgAdmin, inactive }) => ( - + {preferredName} {isBillingLeader && Billing Leader} @@ -108,7 +90,7 @@ const UserInfo: React.FC = ({ {email} - + ) interface UserActionsProps { @@ -168,6 +150,7 @@ const OrgMemberRow = (props: Props) => { inactive picture preferredName + lastSeenAt } role ...OrgAdminActionMenu_organizationUser @@ -177,28 +160,41 @@ const OrgMemberRow = (props: Props) => { ) const { - user: {email, inactive, picture, preferredName}, + user: {email, inactive, picture, preferredName, lastSeenAt}, role } = organizationUser const isBillingLeader = role === 'BILLING_LEADER' const isOrgAdmin = role === 'ORG_ADMIN' + const formattedLastSeenAt = lastSeenAt ? format(new Date(lastSeenAt), 'yyyy-MM-dd') : 'Never' + return ( - - - - - + + +
+ +
+ +
+
+ + + {formattedLastSeenAt} + + + + + ) } diff --git a/packages/client/package.json b/packages/client/package.json index 0304016588a..9c7eecb93f3 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -3,7 +3,7 @@ "description": "An open-source app for building smarter, more agile teams.", "author": "Parabol Inc. (http://github.com/ParabolInc)", "license": "AGPL-3.0", - "version": "7.45.2", + "version": "7.46.0", "repository": { "type": "git", "url": "https://github.com/ParabolInc/parabol" diff --git a/packages/embedder/package.json b/packages/embedder/package.json index 4ad30a514da..833139fa5a0 100644 --- a/packages/embedder/package.json +++ b/packages/embedder/package.json @@ -1,6 +1,6 @@ { "name": "parabol-embedder", - "version": "7.45.2", + "version": "7.46.0", "description": "A service that computes embedding vectors from Parabol objects", "author": "Jordan Husney ", "homepage": "https://github.com/ParabolInc/parabol/tree/master/packages/embedder#readme", diff --git a/packages/gql-executor/package.json b/packages/gql-executor/package.json index 610357c65da..488642e3f6f 100644 --- a/packages/gql-executor/package.json +++ b/packages/gql-executor/package.json @@ -1,6 +1,6 @@ { "name": "gql-executor", - "version": "7.45.2", + "version": "7.46.0", "description": "A Stateless GraphQL Executor", "author": "Matt Krick ", "homepage": "https://github.com/ParabolInc/parabol/tree/master/packages/gqlExecutor#readme", @@ -27,8 +27,8 @@ }, "dependencies": { "dd-trace": "^4.2.0", - "parabol-client": "7.45.2", - "parabol-server": "7.45.2", + "parabol-client": "7.46.0", + "parabol-server": "7.46.0", "undici": "^5.26.2" } } diff --git a/packages/integration-tests/package.json b/packages/integration-tests/package.json index 4b7caae9866..6c027507821 100644 --- a/packages/integration-tests/package.json +++ b/packages/integration-tests/package.json @@ -2,7 +2,7 @@ "name": "integration-tests", "author": "Parabol Inc. (http://github.com/ParabolInc)", "license": "AGPL-3.0", - "version": "7.45.2", + "version": "7.46.0", "description": "", "main": "index.js", "scripts": { diff --git a/packages/server/package.json b/packages/server/package.json index b12149d0c45..b540cb84d97 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -3,7 +3,7 @@ "description": "An open-source app for building smarter, more agile teams.", "author": "Parabol Inc. (http://github.com/ParabolInc)", "license": "AGPL-3.0", - "version": "7.45.2", + "version": "7.46.0", "repository": { "type": "git", "url": "https://github.com/ParabolInc/parabol" @@ -124,7 +124,7 @@ "openai": "^4.53.0", "openapi-fetch": "^0.9.7", "oy-vey": "^0.12.1", - "parabol-client": "7.45.2", + "parabol-client": "7.46.0", "pg": "^8.5.1", "react": "^17.0.2", "react-dom": "^17.0.2", diff --git a/yarn.lock b/yarn.lock index 3ec488bd779..96645f4748c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10665,7 +10665,7 @@ brace-expansion@^2.0.1: dependencies: balanced-match "^1.0.0" -braces@^3.0.2, braces@~3.0.2: +braces@^3.0.3, braces@~3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== @@ -17076,11 +17076,11 @@ methods@^1.1.2, methods@~1.1.2: integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= micromatch@^4.0.0, micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5: - version "4.0.5" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" - integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== + version "4.0.8" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" + integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== dependencies: - braces "^3.0.2" + braces "^3.0.3" picomatch "^2.3.1" mime-db@1.52.0, "mime-db@>= 1.43.0 < 2": @@ -21346,7 +21346,7 @@ string-similarity@^3.0.0: resolved "https://registry.yarnpkg.com/string-similarity/-/string-similarity-3.0.0.tgz#07b0bc69fae200ad88ceef4983878d03793847c7" integrity sha512-7kS7LyTp56OqOI2BDWQNVnLX/rCxIQn+/5M0op1WV6P8Xx6TZNdajpuqQdiJ7Xx+p1C5CsWMvdiBp9ApMhxzEQ== -"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -21364,6 +21364,15 @@ string-width@^1.0.1: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + string-width@^5.0.0: version "5.1.0" resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.0.tgz#5ab00980cfb29f43e736b113a120a73a0fb569d3" @@ -21435,7 +21444,7 @@ stringify-object@^3.3.0: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -21449,6 +21458,13 @@ strip-ansi@^3.0.0, strip-ansi@^3.0.1: dependencies: ansi-regex "^2.0.0" +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + strip-ansi@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.0.1.tgz#61740a08ce36b61e50e65653f07060d000975fb2" @@ -23298,7 +23314,7 @@ workbox-window@6.5.4: "@types/trusted-types" "^2.0.2" workbox-core "6.5.4" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -23316,6 +23332,15 @@ wrap-ansi@^6.0.1, wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"