Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
9e9322c
Refactor members dashboard experience.
sarimrmalik Apr 8, 2026
1b2d384
Enhance member management features and UI.
sarimrmalik Apr 8, 2026
fa6f937
Refactor member management UI components.
sarimrmalik Apr 8, 2026
d3f41b8
Refactor member table responsiveness and layout.
sarimrmalik Apr 8, 2026
58dc5a8
Refactor member UI components for consistency and clarity.
sarimrmalik Apr 8, 2026
71af2ca
Run biome format
sarimrmalik Apr 8, 2026
b3a7a33
Refactor MembersPage layout to utilize new Page component.
sarimrmalik Apr 8, 2026
09ac3a8
Refactor Add Member components for clarity and consistency.
sarimrmalik Apr 8, 2026
10453df
Implement date formatting utility and enhance member table components.
sarimrmalik Apr 8, 2026
1755e96
Run biome format
sarimrmalik Apr 8, 2026
096f6a9
Undo uppercase for layout titles
sarimrmalik Apr 9, 2026
0c1f82b
Refactor member removal dialog for improved clarity and structure.
sarimrmalik Apr 9, 2026
0adaed1
Refactor member table utility functions for clarity and consistency.
sarimrmalik Apr 9, 2026
680c26c
Add pluralization utility and update member table components.
sarimrmalik Apr 9, 2026
69020e7
Update comment for consistency with other formatting utils
sarimrmalik Apr 9, 2026
7dde923
Refactor InvoicesEmpty component to use TableEmptyState for improved …
sarimrmalik Apr 9, 2026
78b562f
Run biome format
sarimrmalik Apr 9, 2026
62bf529
Merge branch 'main' into refactor/team-members-dashboard
sarimrmalik Apr 9, 2026
3bd14d2
Refactor member components for improved consistency and responsiveness.
sarimrmalik Apr 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions src/__test__/unit/formatting.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ import {
formatChartTimestampUTC,
formatCompactDate,
formatCPUCores,
formatDate,
formatDecimal,
formatDuration,
formatMemory,
formatNumber,
formatTimeAxisLabel,
parseUTCDateComponents,
pluralize,
} from '@/lib/utils/formatting'

describe('Date & Time Formatting', () => {
Expand Down Expand Up @@ -75,6 +77,22 @@ describe('Date & Time Formatting', () => {
})
})

describe('formatDate', () => {
it('formats a date with the requested structure', () => {
const date = new Date('2024-01-05T14:30:00Z')
expect(formatDate(date, 'MMM d')).toBe('Jan 5')
})

it('supports a format that includes the year', () => {
const date = new Date('2024-01-05T14:30:00Z')
expect(formatDate(date, 'MMM d, yyyy')).toBe('Jan 5, 2024')
})

it('returns null for invalid dates', () => {
expect(formatDate(new Date('not-a-date'), 'MMM d')).toBeNull()
})
})

describe('parseUTCDateComponents', () => {
it('parses UTC date into components', () => {
const date = new Date('2024-01-05T14:30:45Z')
Expand Down Expand Up @@ -185,4 +203,23 @@ describe('Number Formatting', () => {
expect(formatCPUCores(4)).toBe('4 cores')
})
})

describe('pluralize', () => {
it('returns the singular word for a count of one', () => {
expect(pluralize(1, 'member')).toBe('member')
})

it('adds es for words ending in s-like sounds', () => {
expect(pluralize(2, 'class')).toBe('classes')
expect(pluralize(2, 'match')).toBe('matches')
})

it('changes a trailing consonant y to ies', () => {
expect(pluralize(2, 'company')).toBe('companies')
})

it('supports an explicit plural override for irregular nouns', () => {
expect(pluralize(2, 'person', 'people')).toBe('people')
})
})
})
105 changes: 105 additions & 0 deletions src/__test__/unit/member-table-utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { describe, expect, it } from 'vitest'
import type { TeamMember } from '@/core/modules/teams/models'
import {
getAddedByMember,
shouldShowRemoveMemberAction,
wasAddedBySystem,
} from '@/features/dashboard/members/member-table.utils'

const createMember = ({
addedBy = null,
email,
id,
isDefault = false,
name,
providers,
}: {
addedBy?: string | null
email: string
id: string
isDefault?: boolean
name?: string
providers?: string[]
}): TeamMember => ({
info: {
id,
email,
name,
providers,
createdAt: '2026-04-08T00:00:00.000Z',
},
relation: {
added_by: addedBy,
is_default: isDefault,
},
})

describe('member table utils', () => {
it('finds the inviter from the full member list', () => {
const owner = createMember({
email: 'owner@example.com',
id: 'owner-id',
isDefault: true,
name: 'Owner',
})
const invited = createMember({
addedBy: owner.info.id,
email: 'invited@example.com',
id: 'invited-id',
name: 'Invited',
})

expect(getAddedByMember([owner, invited], invited.relation.added_by)).toBe(
owner
)
})

it('hides removal for default members and the current user', () => {
const defaultMember = createMember({
email: 'default@example.com',
id: 'default-id',
isDefault: true,
})
const currentUser = createMember({
email: 'me@example.com',
id: 'me-id',
})
const invited = createMember({
email: 'invited@example.com',
id: 'invited-id',
})

expect(shouldShowRemoveMemberAction(defaultMember, 'someone-else')).toBe(
false
)
expect(shouldShowRemoveMemberAction(currentUser, currentUser.info.id)).toBe(
false
)
expect(shouldShowRemoveMemberAction(invited, currentUser.info.id)).toBe(
true
)
})

it('treats self-added or unresolved rows as system-added', () => {
const owner = createMember({
email: 'owner@example.com',
id: 'owner-id',
isDefault: true,
})
const selfAdded = createMember({
addedBy: owner.info.id,
email: 'owner@example.com',
id: 'owner-id',
isDefault: true,
})
const invited = createMember({
addedBy: owner.info.id,
email: 'invited@example.com',
id: 'invited-id',
})

expect(wasAddedBySystem(selfAdded, owner)).toBe(true)
expect(wasAddedBySystem(invited, owner)).toBe(false)
expect(wasAddedBySystem(invited, undefined)).toBe(true)
})
})
15 changes: 4 additions & 11 deletions src/app/dashboard/[teamSlug]/members/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Page } from '@/features/dashboard/layouts/page'
import { MemberCard } from '@/features/dashboard/members/member-card'
import Frame from '@/ui/frame'

interface MembersPageProps {
params: Promise<{
Expand All @@ -9,15 +9,8 @@ interface MembersPageProps {

export default async function MembersPage({ params }: MembersPageProps) {
return (
<Frame
classNames={{
wrapper: 'w-full max-md:p-0',
frame: 'max-md:border-none max-md:p-0',
}}
>
<section className="col-span-full">
<MemberCard params={params} />
</section>
</Frame>
<Page>
<MemberCard params={params} />
</Page>
)
}
1 change: 1 addition & 0 deletions src/core/modules/teams/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export type TeamMemberInfo = {
name?: string
avatar_url?: string
providers?: string[]
createdAt: string | null
}

export type TeamMemberRelation = {
Expand Down
1 change: 1 addition & 0 deletions src/core/modules/teams/teams-repository.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ export function createTeamsRepository(
name: user?.user_metadata?.name,
avatar_url: user?.user_metadata?.avatar_url,
providers: extractSignInProviders(user),
createdAt: member.createdAt,
},
relation: {
added_by: member.addedBy ?? null,
Expand Down
51 changes: 15 additions & 36 deletions src/features/dashboard/billing/invoices.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@ import {
Table,
TableBody,
TableCell,
TableEmptyState,
TableHead,
TableHeader,
TableRow,
} from '@/ui/primitives/table'
import { useInvoices } from './hooks'
import { TableEmptyRowBorder } from './table-empty-row-border'

const COLUMN_WIDTHS = {
date: 120,
Expand All @@ -48,34 +48,19 @@ interface InvoicesEmptyProps {

function InvoicesEmpty({ error }: InvoicesEmptyProps) {
return (
<div className="w-full gap-2 relative flex flex-col justify-center items-center">
{Array.from({ length: 3 }).map((_, index) => (
<div
key={index}
className="h-11 w-full border border-bg-highlight relative flex items-center gap-2 justify-center overflow-hidden"
>
<TableEmptyRowBorder className="absolute bottom-0 left-0 rotate-180 opacity-99" />
<TableEmptyRowBorder className="absolute bottom-0 right-0 opacity-99" />

{index === 1 && (
<>
<InvoiceIcon
className={cn('size-4', error && 'text-accent-error-highlight')}
/>

<p
className={cn(
'prose-body-highlight',
error && 'text-accent-error-highlight'
)}
>
{error ? error : 'No invoices yet'}
</p>
</>
)}
</div>
))}
</div>
<TableEmptyState colSpan={4}>
<InvoiceIcon
className={cn('size-4', error && 'text-accent-error-highlight')}
/>
<p
className={cn(
'prose-body-highlight',
error && 'text-accent-error-highlight'
)}
>
{error ? error : 'No invoices yet'}
</p>
</TableEmptyState>
)
}

Expand Down Expand Up @@ -146,13 +131,7 @@ export default function BillingInvoicesTable() {
</TableRow>
)}

{showEmpty && (
<TableRow>
<TableCell colSpan={4} className="p-0">
<InvoicesEmpty error={error?.message} />
</TableCell>
</TableRow>
)}
{showEmpty && <InvoicesEmpty error={error?.message} />}

{hasData &&
invoices.map((invoice) => (
Expand Down
13 changes: 13 additions & 0 deletions src/features/dashboard/layouts/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { ReactNode } from 'react'
import { cn } from '@/lib/utils'

interface PageProps {
children: ReactNode
className?: string
}

export const Page = ({ children, className }: PageProps) => (
<div className={cn('mx-auto w-full max-w-[900px]', className)}>
{children}
</div>
)
39 changes: 39 additions & 0 deletions src/features/dashboard/members/add-member-dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
'use client'

import { Plus } from 'lucide-react'
import { useState } from 'react'
import { AddMemberForm } from '@/features/dashboard/members/add-member-form'
import { Button } from '@/ui/primitives/button'
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/ui/primitives/dialog'

export const AddMemberDialog = () => {
const [open, setOpen] = useState(false)

return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button
className="normal-case font-sans"
size="md"
type="button"
variant="default"
>
<Plus aria-hidden className="size-4 shrink-0" />
Add new member
</Button>
</DialogTrigger>
<DialogContent className="gap-2" hideClose>
<DialogHeader>
<DialogTitle>Add new member</DialogTitle>
</DialogHeader>
<AddMemberForm onSuccess={() => setOpen(false)} />
</DialogContent>
</Dialog>
)
}
Loading
Loading