From 47613f796eb1716d57ef4dd76b053be79784e3b9 Mon Sep 17 00:00:00 2001 From: Eunjae Lee Date: Mon, 5 May 2025 10:15:30 +0200 Subject: [PATCH] feat: add membership creation date to Organization Member List table (CAL-5406) (#21008) * feat: add membership creation date to Organization Member List table (CAL-5406) This reverts commit 5aaca2b7aa1c420af832c9766626cdaa9730e9b0. * fill createdAt --- apps/api/v1/pages/api/memberships/_post.ts | 7 ++- .../memberships/memberships.repository.ts | 1 + ...anizations-teams-memberships.repository.ts | 6 ++- .../teams-memberships.repository.ts | 6 ++- .../membership.repository.fixture.ts | 4 +- apps/web/playwright/fixtures/users.ts | 2 + apps/web/playwright/out-of-office.e2e.ts | 4 +- apps/web/public/static/locales/en/common.json | 3 ++ apps/web/test/lib/generateCsv.test.ts | 2 + .../components/filters/ActiveFilters.tsx | 1 + .../components/filters/DateRangeFilter.tsx | 29 +++++++++-- packages/features/data-table/lib/server.ts | 14 ++++++ packages/features/data-table/lib/types.ts | 3 +- .../ee/dsync/lib/handleGroupEvents.ts | 3 ++ .../createOrganizationFromOnboarding.test.ts | 1 + .../UserTable/PlatformManagedUsersTable.tsx | 6 +-- .../components/UserTable/UserListTable.tsx | 47 +++++++++++++++++- packages/lib/server/repository/membership.ts | 11 ++++- .../lib/server/repository/organization.ts | 2 + packages/lib/server/repository/team.ts | 1 + .../server/assignValueToUser.test.ts | 1 + .../migration.sql | 2 + packages/prisma/schema.prisma | 2 + packages/prisma/seed-insights.ts | 4 ++ packages/prisma/seed-utils.ts | 1 + packages/prisma/seed.ts | 3 ++ .../organizations/listMembers.handler.ts | 49 ++++++++++++++++++- .../routers/viewer/organizations/utils.ts | 1 + .../viewer/teams/inviteMember/utils.ts | 5 ++ 29 files changed, 200 insertions(+), 21 deletions(-) create mode 100644 packages/prisma/migrations/20250407204810_add_membership_creation_date/migration.sql diff --git a/apps/api/v1/pages/api/memberships/_post.ts b/apps/api/v1/pages/api/memberships/_post.ts index 43cab88a80..31b5be10fb 100644 --- a/apps/api/v1/pages/api/memberships/_post.ts +++ b/apps/api/v1/pages/api/memberships/_post.ts @@ -24,7 +24,12 @@ import { membershipCreateBodySchema, schemaMembershipPublic } from "~/lib/valida */ async function postHandler(req: NextApiRequest) { const data = membershipCreateBodySchema.parse(req.body); - const args: Prisma.MembershipCreateArgs = { data }; + const args: Prisma.MembershipCreateArgs = { + data: { + createdAt: new Date(), + ...data, + }, + }; await checkPermissions(req); diff --git a/apps/api/v2/src/modules/memberships/memberships.repository.ts b/apps/api/v2/src/modules/memberships/memberships.repository.ts index 2c289e9243..e5d9d8cfdd 100644 --- a/apps/api/v2/src/modules/memberships/memberships.repository.ts +++ b/apps/api/v2/src/modules/memberships/memberships.repository.ts @@ -63,6 +63,7 @@ export class MembershipsRepository { async createMembership(teamId: number, userId: number, role: MembershipRole, accepted: boolean) { const membership = await this.dbRead.prisma.membership.create({ data: { + createdAt: new Date(), role, teamId, userId, diff --git a/apps/api/v2/src/modules/organizations/teams/memberships/organizations-teams-memberships.repository.ts b/apps/api/v2/src/modules/organizations/teams/memberships/organizations-teams-memberships.repository.ts index 0fa4eac1cd..0019c99946 100644 --- a/apps/api/v2/src/modules/organizations/teams/memberships/organizations-teams-memberships.repository.ts +++ b/apps/api/v2/src/modules/organizations/teams/memberships/organizations-teams-memberships.repository.ts @@ -69,7 +69,11 @@ export class OrganizationsTeamsMembershipsRepository { async createOrgTeamMembership(teamId: number, data: CreateOrgTeamMembershipDto) { return this.dbWrite.prisma.membership.create({ - data: { ...data, teamId: teamId }, + data: { + createdAt: new Date(), + ...data, + teamId: teamId, + }, include: { user: { select: MembershipUserSelect } }, }); } diff --git a/apps/api/v2/src/modules/teams/memberships/teams-memberships.repository.ts b/apps/api/v2/src/modules/teams/memberships/teams-memberships.repository.ts index 53da740238..bcbe0efc8e 100644 --- a/apps/api/v2/src/modules/teams/memberships/teams-memberships.repository.ts +++ b/apps/api/v2/src/modules/teams/memberships/teams-memberships.repository.ts @@ -21,7 +21,11 @@ export class TeamsMembershipsRepository { async createTeamMembership(teamId: number, data: CreateTeamMembershipInput) { return this.dbWrite.prisma.membership.create({ - data: { ...data, teamId: teamId }, + data: { + createdAt: new Date(), + ...data, + teamId: teamId, + }, include: { user: { select: MembershipUserSelect } }, }); } diff --git a/apps/api/v2/test/fixtures/repository/membership.repository.fixture.ts b/apps/api/v2/test/fixtures/repository/membership.repository.fixture.ts index 0943c4001f..74f86ed020 100644 --- a/apps/api/v2/test/fixtures/repository/membership.repository.fixture.ts +++ b/apps/api/v2/test/fixtures/repository/membership.repository.fixture.ts @@ -13,7 +13,7 @@ export class MembershipRepositoryFixture { } async create(data: Prisma.MembershipCreateInput) { - return this.prismaWriteClient.membership.create({ data }); + return this.prismaWriteClient.membership.create({ data: { createdAt: new Date(), ...data } }); } async delete(membershipId: Membership["id"]) { @@ -30,7 +30,7 @@ export class MembershipRepositoryFixture { async addUserToOrg(user: User, org: Team, role: MembershipRole, accepted: boolean) { const membership = await this.prismaWriteClient.membership.create({ - data: { teamId: org.id, userId: user.id, role, accepted }, + data: { createdAt: new Date(), teamId: org.id, userId: user.id, role, accepted }, }); await this.prismaWriteClient.user.update({ where: { id: user.id }, data: { organizationId: org.id } }); return membership; diff --git a/apps/web/playwright/fixtures/users.ts b/apps/web/playwright/fixtures/users.ts index b2b3d90fda..55569252ae 100644 --- a/apps/web/playwright/fixtures/users.ts +++ b/apps/web/playwright/fixtures/users.ts @@ -227,6 +227,7 @@ const createTeamAndAddUser = async ( const { role = MembershipRole.OWNER, id: userId } = user; await prisma.membership.create({ data: { + createdAt: new Date(), teamId: team.id, userId, role: role, @@ -566,6 +567,7 @@ export const createUsersFixture = ( // Add teammates to the team await prisma.membership.create({ data: { + createdAt: new Date(), teamId: team.id, userId: teamUser.id, role: MembershipRole.MEMBER, diff --git a/apps/web/playwright/out-of-office.e2e.ts b/apps/web/playwright/out-of-office.e2e.ts index 2dc6ee8be5..3290994ec7 100644 --- a/apps/web/playwright/out-of-office.e2e.ts +++ b/apps/web/playwright/out-of-office.e2e.ts @@ -660,9 +660,7 @@ test.describe("Out of office", () => { await page.locator('[data-testid="add-filter-button"]').click(); await page.locator('[data-testid="add-filter-item-dateRange"]').click(); await expect( - page - .locator('[data-testid="filter-popover-trigger-dateRange"] span', { hasText: "Last 7 Days" }) - .nth(0) + page.locator('[data-testid="filter-popover-trigger-dateRange"]', { hasText: "Last 7 Days" }).first() ).toBeVisible(); }); diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index cf029b7c3c..42289751b8 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -3107,6 +3107,9 @@ "enable_delegation_credential_description": "Grant Cal.com automatic access to the calendars of all organization members by enabling delegation credential.", "disable_delegation_credential": "Disable Delegation Credential", "disable_delegation_credential_description": "Once delegation credential is disabled, organization members who haven’t connected their calendars will need to do so manually.", + "last_active": "Last Active", + "member_since": "Member Since", + "last_updated": "Last Updated", "salesforce_on_cancel_write_to_event": "On cancelled booking, write to event record instead of deleting event", "salesforce_on_every_cancellation": "On every cancellation", "report_issue": "Report issue", diff --git a/apps/web/test/lib/generateCsv.test.ts b/apps/web/test/lib/generateCsv.test.ts index 3c0b1309f6..65987d2415 100644 --- a/apps/web/test/lib/generateCsv.test.ts +++ b/apps/web/test/lib/generateCsv.test.ts @@ -68,6 +68,8 @@ describe("generate Csv for Org Users Table", () => { teams: [], attributes: [], lastActiveAt: "", + createdAt: null, + updatedAt: null, }; it("should throw if no headers", () => { diff --git a/packages/features/data-table/components/filters/ActiveFilters.tsx b/packages/features/data-table/components/filters/ActiveFilters.tsx index cb7781e3c4..23cd495931 100644 --- a/packages/features/data-table/components/filters/ActiveFilters.tsx +++ b/packages/features/data-table/components/filters/ActiveFilters.tsx @@ -30,6 +30,7 @@ export function ActiveFilters({ table }: ActiveFiltersProps) { key={column.id} column={column} options={column.dateRangeOptions} + showColumnName showClearButton /> ); diff --git a/packages/features/data-table/components/filters/DateRangeFilter.tsx b/packages/features/data-table/components/filters/DateRangeFilter.tsx index 0988e5c5eb..2456400bbd 100644 --- a/packages/features/data-table/components/filters/DateRangeFilter.tsx +++ b/packages/features/data-table/components/filters/DateRangeFilter.tsx @@ -5,6 +5,7 @@ import { useState, useEffect, useCallback } from "react"; import dayjs from "@calcom/dayjs"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import classNames from "@calcom/ui/classNames"; +import { Badge } from "@calcom/ui/components/badge"; import { Button, buttonClasses } from "@calcom/ui/components/button"; import { Command, @@ -35,14 +36,21 @@ import { useFilterPopoverOpen } from "./useFilterPopoverOpen"; type DateRangeFilterProps = { column: Extract; options?: DateRangeFilterOptions; + showColumnName?: boolean; showClearButton?: boolean; }; -export const DateRangeFilter = ({ column, options, showClearButton = false }: DateRangeFilterProps) => { +export const DateRangeFilter = ({ + column, + options, + showColumnName = false, + showClearButton = false, +}: DateRangeFilterProps) => { const { open, onOpenChange } = useFilterPopoverOpen(column.id); const filterValue = useFilterValue(column.id, ZDateRangeFilterValue); const { updateFilter, removeFilter } = useDataTable(); const range = options?.range ?? "past"; + const endOfDay = options?.endOfDay ?? false; const forceCustom = range === "custom"; const forcePast = range === "past"; @@ -73,13 +81,13 @@ export const DateRangeFilter = ({ column, options, showClearButton = false }: Da type: ColumnFilterType.DATE_RANGE, data: { startDate: startDate.toDate().toISOString(), - endDate: endDate.toDate().toISOString(), + endDate: (endOfDay ? endDate.endOf("day") : endDate).toDate().toISOString(), preset: preset.value, }, }); } }, - [column.id] + [column.id, endOfDay] ); useEffect(() => { @@ -137,6 +145,10 @@ export const DateRangeFilter = ({ column, options, showClearButton = false }: Da customButtonLabel = `${format(startDate.toDate(), "LLL dd, y")} - ?`; } + const selectedValue = isCustomPreset + ? customButtonLabel + : t(selectedPreset.labelKey, selectedPreset.i18nOptions); + return ( @@ -146,8 +158,15 @@ export const DateRangeFilter = ({ column, options, showClearButton = false }: Da StartIcon="calendar-range" EndIcon="chevron-down" data-testid={`filter-popover-trigger-${column.id}`}> - {!isCustomPreset && {t(selectedPreset.labelKey, selectedPreset.i18nOptions)}} - {isCustomPreset && {customButtonLabel}} + {showColumnName && ( + <> + {column.title} + + {selectedValue} + + + )} + {!showColumnName && {selectedValue}} diff --git a/packages/features/data-table/lib/server.ts b/packages/features/data-table/lib/server.ts index 1baa98979e..1547653f5b 100644 --- a/packages/features/data-table/lib/server.ts +++ b/packages/features/data-table/lib/server.ts @@ -4,6 +4,7 @@ import { isMultiSelectFilterValue, isTextFilterValue, isNumberFilterValue, + isDateRangeFilterValue, } from "./utils"; type MakeWhereClauseProps = { @@ -154,6 +155,19 @@ export function makeWhereClause(props: MakeWhereClauseProps) { default: throw new Error(`Invalid operator for number filter: ${operator}`); } + } else if (isDateRangeFilterValue(filterValue)) { + const { startDate, endDate } = filterValue.data; + if (!startDate || !endDate) { + throw new Error(`Invalid date range filter: ${JSON.stringify({ columnName, startDate, endDate })}`); + } + + return { + [columnName]: { + ...jsonPathObj, + gte: startDate, + lte: endDate, + }, + }; } throw new Error(`Invalid filter type: ${JSON.stringify({ columnName, filterValue })}`); } diff --git a/packages/features/data-table/lib/types.ts b/packages/features/data-table/lib/types.ts index f7e1603647..2d85785b09 100644 --- a/packages/features/data-table/lib/types.ts +++ b/packages/features/data-table/lib/types.ts @@ -113,7 +113,8 @@ export const ZFilterValue = z.union([ ]); export type DateRangeFilterOptions = { - range: "past" | "custom"; + range?: "past" | "custom"; + endOfDay?: boolean; }; export type TextFilterOptions = { diff --git a/packages/features/ee/dsync/lib/handleGroupEvents.ts b/packages/features/ee/dsync/lib/handleGroupEvents.ts index d58074a53f..9c6bdfd2f1 100644 --- a/packages/features/ee/dsync/lib/handleGroupEvents.ts +++ b/packages/features/ee/dsync/lib/handleGroupEvents.ts @@ -126,6 +126,7 @@ const handleGroupEvents = async (event: DirectorySyncEvent, organizationId: numb }); await prisma.membership.createMany({ data: newUsers.map((user) => ({ + createdAt: new Date(), userId: user.id, teamId: group.teamId, role: MembershipRole.MEMBER, @@ -163,12 +164,14 @@ const handleGroupEvents = async (event: DirectorySyncEvent, organizationId: numb .map((user) => { return [ { + createdAt: new Date(), userId: user.id, teamId: group.teamId, role: MembershipRole.MEMBER, accepted: true, }, { + createdAt: new Date(), userId: user.id, teamId: organizationId, role: MembershipRole.MEMBER, diff --git a/packages/features/ee/organizations/lib/server/createOrganizationFromOnboarding.test.ts b/packages/features/ee/organizations/lib/server/createOrganizationFromOnboarding.test.ts index 4bdd84e916..0f6e8d4500 100644 --- a/packages/features/ee/organizations/lib/server/createOrganizationFromOnboarding.test.ts +++ b/packages/features/ee/organizations/lib/server/createOrganizationFromOnboarding.test.ts @@ -138,6 +138,7 @@ async function createOnboardingEligibleUserAndOnboarding(data: { async function createTestMembership(data: { userId: number; teamId: number; role?: MembershipRole }) { return prismock.membership.create({ data: { + createdAt: new Date(), userId: data.userId, teamId: data.teamId, role: data.role || MembershipRole.MEMBER, diff --git a/packages/features/users/components/UserTable/PlatformManagedUsersTable.tsx b/packages/features/users/components/UserTable/PlatformManagedUsersTable.tsx index 4391da1fd6..8743f44f53 100644 --- a/packages/features/users/components/UserTable/PlatformManagedUsersTable.tsx +++ b/packages/features/users/components/UserTable/PlatformManagedUsersTable.tsx @@ -131,7 +131,7 @@ function UserListTableContent({ oAuthClientId }: PlatformManagedUsersTableProps) enableHiding: false, size: 200, header: () => { - return `Managed Users`; + return t("managed_users"); }, cell: ({ row }) => { if (isPending) { @@ -166,7 +166,7 @@ function UserListTableContent({ oAuthClientId }: PlatformManagedUsersTableProps) { id: "role", accessorFn: (data) => data.role, - header: "Role", + header: t("role"), size: 100, cell: ({ row, table }) => { if (isPending) { @@ -188,7 +188,7 @@ function UserListTableContent({ oAuthClientId }: PlatformManagedUsersTableProps) { id: "teams", accessorFn: (data) => data.teams.map((team) => team.name), - header: "Teams", + header: t("teams"), size: 140, cell: ({ row, table }) => { if (isPending) { diff --git a/packages/features/users/components/UserTable/UserListTable.tsx b/packages/features/users/components/UserTable/UserListTable.tsx index e885b57f30..694e377f1b 100644 --- a/packages/features/users/components/UserTable/UserListTable.tsx +++ b/packages/features/users/components/UserTable/UserListTable.tsx @@ -74,6 +74,8 @@ const initalColumnVisibility = { member: true, role: true, teams: true, + createdAt: false, + updatedAt: false, actions: true, }; @@ -366,9 +368,52 @@ function UserListTableContent() { ...generateAttributeColumns(), { id: "lastActiveAt", - header: "Last Active", + accessorKey: "lastActiveAt", + header: t("last_active"), + enableSorting: false, + enableColumnFilter: true, + meta: { + filter: { + type: ColumnFilterType.DATE_RANGE, + dateRangeOptions: { + endOfDay: true, + }, + }, + }, cell: ({ row }) =>
{row.original.lastActiveAt}
, }, + { + id: "createdAt", + accessorKey: "createdAt", + header: t("member_since"), + enableSorting: false, + enableColumnFilter: true, + meta: { + filter: { + type: ColumnFilterType.DATE_RANGE, + dateRangeOptions: { + endOfDay: true, + }, + }, + }, + cell: ({ row }) =>
{row.original.createdAt || ""}
, + }, + { + id: "updatedAt", + accessorKey: "updatedAt", + header: t("last_updated"), + enableSorting: false, + enableColumnFilter: true, + meta: { + filter: { + type: ColumnFilterType.DATE_RANGE, + dateRangeOptions: { + endOfDay: true, + }, + }, + }, + cell: ({ row }) =>
{row.original.updatedAt || ""}
, + }, { id: "actions", enableHiding: false, diff --git a/packages/lib/server/repository/membership.ts b/packages/lib/server/repository/membership.ts index f336647dde..b6101d6de5 100644 --- a/packages/lib/server/repository/membership.ts +++ b/packages/lib/server/repository/membership.ts @@ -15,6 +15,7 @@ type IMembership = { userId: number; accepted: boolean; role: MembershipRole; + createdAt?: Date; }; const membershipSelect = Prisma.validator()({ @@ -71,13 +72,19 @@ const getWhereForfindAllByUpId = async (upId: string, where?: Prisma.MembershipW export class MembershipRepository { static async create(data: IMembership) { return await prisma.membership.create({ - data, + data: { + createdAt: new Date(), + ...data, + }, }); } static async createMany(data: IMembership[]) { return await prisma.membership.createMany({ - data, + data: data.map((item) => ({ + createdAt: new Date(), + ...item, + })), }); } diff --git a/packages/lib/server/repository/organization.ts b/packages/lib/server/repository/organization.ts index 709aeb3fcf..a800cf2236 100644 --- a/packages/lib/server/repository/organization.ts +++ b/packages/lib/server/repository/organization.ts @@ -57,6 +57,7 @@ export class OrganizationRepository { await prisma.membership.create({ data: { + createdAt: new Date(), userId: owner.id, role: MembershipRole.OWNER, accepted: true, @@ -102,6 +103,7 @@ export class OrganizationRepository { await prisma.membership.create({ data: { + createdAt: new Date(), userId: ownerInDb.id, role: MembershipRole.OWNER, accepted: true, diff --git a/packages/lib/server/repository/team.ts b/packages/lib/server/repository/team.ts index ac33e722ce..653a78f632 100644 --- a/packages/lib/server/repository/team.ts +++ b/packages/lib/server/repository/team.ts @@ -250,6 +250,7 @@ export class TeamRepository { try { await prisma.membership.create({ data: { + createdAt: new Date(), teamId: verificationToken.teamId, userId: userId, role: MembershipRole.MEMBER, diff --git a/packages/lib/service/attribute/server/assignValueToUser.test.ts b/packages/lib/service/attribute/server/assignValueToUser.test.ts index 65c0ba4d50..0ebaf2e5fe 100644 --- a/packages/lib/service/attribute/server/assignValueToUser.test.ts +++ b/packages/lib/service/attribute/server/assignValueToUser.test.ts @@ -54,6 +54,7 @@ async function createMockUserWithMembership({ orgId }: { orgId: number }) { }); const membership = await prismock.membership.create({ data: { + createdAt: new Date(), role: MembershipRole.MEMBER, disableImpersonation: false, accepted: true, diff --git a/packages/prisma/migrations/20250407204810_add_membership_creation_date/migration.sql b/packages/prisma/migrations/20250407204810_add_membership_creation_date/migration.sql new file mode 100644 index 0000000000..c621deb140 --- /dev/null +++ b/packages/prisma/migrations/20250407204810_add_membership_creation_date/migration.sql @@ -0,0 +1,2 @@ +ALTER TABLE "Membership" ADD COLUMN "createdAt" TIMESTAMP(3), + ADD COLUMN "updatedAt" TIMESTAMP(3); diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index 5b4a937d7e..57291bf679 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -549,6 +549,8 @@ model Membership { user User @relation(fields: [userId], references: [id], onDelete: Cascade) disableImpersonation Boolean @default(false) AttributeToUser AttributeToUser[] + createdAt DateTime? + updatedAt DateTime? @updatedAt @@unique([userId, teamId]) @@index([teamId]) diff --git a/packages/prisma/seed-insights.ts b/packages/prisma/seed-insights.ts index 1b1b6749ef..c145dd8dcc 100644 --- a/packages/prisma/seed-insights.ts +++ b/packages/prisma/seed-insights.ts @@ -188,6 +188,8 @@ async function main() { userId: member.user.id, role: member.role, accepted: true, + createdAt: new Date(), + updatedAt: new Date(), })), }); } @@ -424,6 +426,8 @@ async function createPerformanceData() { userId: memberId.id, role: "MEMBER", accepted: true, + createdAt: new Date(), + updatedAt: new Date(), })), }); diff --git a/packages/prisma/seed-utils.ts b/packages/prisma/seed-utils.ts index cfa9cba0bb..4296fc7624 100644 --- a/packages/prisma/seed-utils.ts +++ b/packages/prisma/seed-utils.ts @@ -255,6 +255,7 @@ export async function createTeamAndAddUsers( const { role = MembershipRole.OWNER, id, username } = user; await prisma.membership.create({ data: { + createdAt: new Date(), teamId: team.id, userId: id, role: role, diff --git a/packages/prisma/seed.ts b/packages/prisma/seed.ts index bb67c3e5a8..db6c038b80 100644 --- a/packages/prisma/seed.ts +++ b/packages/prisma/seed.ts @@ -122,6 +122,7 @@ const createTeam = async (team: Prisma.TeamCreateInput) => { const associateUserAndOrg = async ({ teamId, userId, role, username }: AssociateUserAndOrgProps) => { await prisma.membership.create({ data: { + createdAt: new Date(), teamId, userId, role: role as MembershipRole, @@ -250,6 +251,7 @@ async function createTeamAndAddUsers( const { role = MembershipRole.OWNER, id, username } = user; await prisma.membership.create({ data: { + createdAt: new Date(), teamId: team.id, userId: id, role: role, @@ -567,6 +569,7 @@ async function createOrganizationAndAddMembersAndTeams({ } await prisma.membership.create({ data: { + createdAt: new Date(), teamId: team.id, userId: member.id, role: role, diff --git a/packages/trpc/server/routers/viewer/organizations/listMembers.handler.ts b/packages/trpc/server/routers/viewer/organizations/listMembers.handler.ts index bd28cf47e8..b5a6cbcd3b 100644 --- a/packages/trpc/server/routers/viewer/organizations/listMembers.handler.ts +++ b/packages/trpc/server/routers/viewer/organizations/listMembers.handler.ts @@ -83,6 +83,15 @@ export const listMembersHandler = async ({ ctx, input }: GetOptions) => { const teamFilter = filters.find((filter) => filter.id === "teams") as | TypedColumnFilter | undefined; + const lastActiveAtFilter = filters.find((filter) => filter.id === "lastActiveAt") as + | TypedColumnFilter + | undefined; + const createdAtFilter = filters.find((filter) => filter.id === "createdAt") as + | TypedColumnFilter + | undefined; + const updatedAtFilter = filters.find((filter) => filter.id === "updatedAt") as + | TypedColumnFilter + | undefined; const whereClause: Prisma.MembershipWhereInput = { user: { @@ -97,6 +106,11 @@ export const listMembersHandler = async ({ ctx, input }: GetOptions) => { }, }, }), + ...(lastActiveAtFilter && + makeWhereClause({ + columnName: "lastActiveAt", + filterValue: lastActiveAtFilter.value, + })), }, teamId: organizationId, ...(searchTerm && { @@ -112,10 +126,27 @@ export const listMembersHandler = async ({ ctx, input }: GetOptions) => { columnName: "role", filterValue: roleFilter.value, })), + ...(createdAtFilter && + makeWhereClause({ + columnName: "createdAt", + filterValue: createdAtFilter.value, + })), + ...(updatedAtFilter && + makeWhereClause({ + columnName: "updatedAt", + filterValue: updatedAtFilter.value, + })), }; const attributeFilters: Prisma.MembershipWhereInput["AttributeToUser"][] = filters - .filter((filter) => filter.id !== "role" && filter.id !== "teams") + .filter( + (filter) => + filter.id !== "role" && + filter.id !== "teams" && + filter.id !== "lastActiveAt" && + filter.id !== "createdAt" && + filter.id !== "updatedAt" + ) .map((filter) => { if (filter.value.type === ColumnFilterType.MULTI_SELECT && isAllString(filter.value.data)) { const attributeOptionValues: string[] = []; @@ -163,6 +194,8 @@ export const listMembersHandler = async ({ ctx, input }: GetOptions) => { id: true, role: true, accepted: true, + createdAt: true, + updatedAt: true, user: { select: { id: true, @@ -249,6 +282,20 @@ export const listMembersHandler = async ({ ctx, input }: GetOptions) => { .format(membership.user.lastActiveAt) .toLowerCase() : null, + createdAt: membership.createdAt + ? new Intl.DateTimeFormat(ctx.user.locale, { + timeZone: ctx.user.timeZone, + }) + .format(membership.createdAt) + .toLowerCase() + : null, + updatedAt: membership.updatedAt + ? new Intl.DateTimeFormat(ctx.user.locale, { + timeZone: ctx.user.timeZone, + }) + .format(membership.updatedAt) + .toLowerCase() + : null, avatarUrl: user.avatarUrl, teams: user.teams .filter((team) => team.team.id !== organizationId) // In this context we dont want to return the org team diff --git a/packages/trpc/server/routers/viewer/organizations/utils.ts b/packages/trpc/server/routers/viewer/organizations/utils.ts index 7144cdf956..10c433c8a3 100644 --- a/packages/trpc/server/routers/viewer/organizations/utils.ts +++ b/packages/trpc/server/routers/viewer/organizations/utils.ts @@ -65,6 +65,7 @@ export const addMembersToTeams = async ({ user, input }: AddBulkToTeamProps) => const userMembership = usersInOrganization.find((membership) => membership.userId === userId); const accepted = userMembership && userMembership.accepted; return { + createdAt: new Date(), userId, teamId, role: MembershipRole.MEMBER, diff --git a/packages/trpc/server/routers/viewer/teams/inviteMember/utils.ts b/packages/trpc/server/routers/viewer/teams/inviteMember/utils.ts index 8ead22afe7..2f0d3918ab 100644 --- a/packages/trpc/server/routers/viewer/teams/inviteMember/utils.ts +++ b/packages/trpc/server/routers/viewer/teams/inviteMember/utils.ts @@ -376,6 +376,7 @@ export async function createNewUsersConnectToOrgIfExists({ if (parentId) { await tx.membership.create({ data: { + createdAt: new Date(), teamId: parentId, userId: createdUser.id, role: MembershipRole.MEMBER, @@ -413,8 +414,10 @@ export async function createMemberships({ data: invitees.flatMap((invitee) => { const organizationRole = invitee?.teams?.[0]?.role; const data = []; + const createdAt = new Date(); // membership for the team data.push({ + createdAt, teamId, userId: invitee.id, accepted, @@ -424,6 +427,7 @@ export async function createMemberships({ // membership for the org if (parentId && invitee.needToCreateOrgMembership) { data.push({ + createdAt, accepted, teamId: parentId, userId: invitee.id, @@ -866,6 +870,7 @@ export async function handleExistingUsersInvites({ await prisma.membership.create({ data: { + createdAt: new Date(), userId: user.id, teamId: team.id, accepted: shouldAutoAccept,