Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 3 additions & 3 deletions e2e/admin-rbac.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ test('admin RBAC controls access, role assignment, and privacy boundaries', asyn

await page.goto(`/admin/users?q=rbac-${runId}`)
await expect(
page.getByRole('button', { name: memberUser.username }),
page.getByRole('link', { name: memberUser.username }),
).toBeVisible()
await expect(page.getByText(memberUser.email)).toBeVisible()
await expect(page.getByText('memberPrivateSecret')).toHaveCount(0)
Expand All @@ -92,13 +92,13 @@ test('admin RBAC controls access, role assignment, and privacy boundaries', asyn
expect(JSON.stringify(memberRecord)).not.toContain('memberPrivateSecret')
expect(JSON.stringify(memberRecord)).not.toContain('super-secret-value')

await page.getByRole('button', { name: memberUser.username }).click()
await page.getByRole('link', { name: memberUser.username }).click()
await expect(page.getByText('Account metadata only')).toBeVisible()
await expect(page.getByText('memberPrivateSecret')).toHaveCount(0)
await expect(page.getByText('super-secret-value')).toHaveCount(0)

await page.goto(`/admin/users?q=rbac-${runId}`)
await page.getByRole('button', { name: memberUser.username }).click()
await page.getByRole('link', { name: memberUser.username }).click()
const roleSelect = page.getByLabel('Role', { exact: true })
await roleSelect.selectOption('admin')
await page.getByRole('button', { name: 'Assign', exact: true }).click()
Expand Down
14 changes: 12 additions & 2 deletions packages/worker/client/client-router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,17 @@ export function matchRoute(
)
}

function shouldHandleClick(event: MouseEvent, anchor: HTMLAnchorElement) {
/**
* True when the router will intercept this anchor click and run an SPA
* navigation (plain left-click on a same-origin, non-download, self-target
* link). Components that reset local state before an in-page navigation use
* this so modified clicks (open in new tab, download) leave the current page
* untouched.
*/
export function shouldRouterHandleClick(
event: MouseEvent,
anchor: HTMLAnchorElement,
) {
if (event.defaultPrevented) return false
if (event.button !== 0) return false
if (event.metaKey || event.altKey || event.ctrlKey || event.shiftKey)
Expand All @@ -259,7 +269,7 @@ function handleDocumentClick(event: MouseEvent) {
const target = event.target as Element | null
const anchor = target?.closest('a') as HTMLAnchorElement | null
if (!anchor || typeof window === 'undefined') return
if (!shouldHandleClick(event, anchor)) return
if (!shouldRouterHandleClick(event, anchor)) return

event.preventDefault()
const destination = new URL(anchor.href, window.location.href)
Expand Down
20 changes: 8 additions & 12 deletions packages/worker/client/routes/account-activity.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {
accountManagementNarrowMq,
AccountManagementLayout,
AccountManagementList,
AccountManagementListItemButton,
AccountManagementListItemLink,
AccountManagementMessage,
AccountManagementShell,
AccountManagementSidebar,
Expand Down Expand Up @@ -683,17 +683,13 @@ export function AccountActivityRoute(handle: Handle) {
<AccountManagementList>
{runs.map((item) => (
<li key={item.id}>
<AccountManagementListItemButton
<AccountManagementListItemLink
href={activityRoute.buildDetailHref(
item.id,
filterSearch,
)}
active={selection.selectedId === item.id}
onClick={() => {
setMessage(null)
navigate(
activityRoute.buildDetailHref(
item.id,
filterSearch,
),
)
}}
onNavigate={() => setMessage(null)}
>
<span
mix={css({
Expand Down Expand Up @@ -753,7 +749,7 @@ export function AccountActivityRoute(handle: Handle) {
{item.errorMessage}
</span>
) : null}
</AccountManagementListItemButton>
</AccountManagementListItemLink>
</li>
))}
</AccountManagementList>
Expand Down
20 changes: 8 additions & 12 deletions packages/worker/client/routes/account-email.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { formatNullableTimestamp } from '#client/format-timestamp.ts'
import { type Handle, css } from 'remix/ui'
import { Tab, TabList, TabPanel, Tabs } from 'remix/ui/tabs'
import { on } from '#client/event-mixin.ts'
import { navigate, readCurrentRouterHref } from '#client/client-router.tsx'
import { readCurrentRouterHref } from '#client/client-router.tsx'
import { createListDetailRoute } from '#client/list-detail-route.ts'
import { replaceLocation } from '#client/replace-location.ts'
import { tryConsumeRouteLoaderData } from '#client/loader-data-context.tsx'
Expand All @@ -16,7 +16,7 @@ import {
import {
AccountManagementLayout,
AccountManagementList,
AccountManagementListItemButton,
AccountManagementListItemLink,
AccountManagementMessage,
AccountManagementSearchField,
AccountManagementShell,
Expand Down Expand Up @@ -494,19 +494,15 @@ export function AccountEmailRoute(handle: Handle) {
<AccountManagementList>
{data.messages.map((emailMessage) => (
<li key={emailMessage.id} mix={css({ minWidth: 0 })}>
<AccountManagementListItemButton
<AccountManagementListItemLink
href={emailRoute.buildDetailHref(
emailMessage.id,
getCurrentSearch(),
)}
active={
selectedMessageId === emailMessage.id ||
selectedMessage?.id === emailMessage.id
}
onClick={() => {
navigate(
emailRoute.buildDetailHref(
emailMessage.id,
getCurrentSearch(),
),
)
}}
>
<div
mix={css({
Expand Down Expand Up @@ -576,7 +572,7 @@ export function AccountEmailRoute(handle: Handle) {
</span>
) : null}
</span>
</AccountManagementListItemButton>
</AccountManagementListItemLink>
</li>
))}
</AccountManagementList>
Expand Down
159 changes: 73 additions & 86 deletions packages/worker/client/routes/account-integrations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
} from '#app/loader-data.ts'
import { routes } from '#app/routes.ts'
import { type Handle, css } from 'remix/ui'
import { navigate, readCurrentRouterHref } from '#client/client-router.tsx'
import { readCurrentRouterHref } from '#client/client-router.tsx'
import { CopyTextButton } from '#client/copy-text-button.tsx'
import { on } from '#client/event-mixin.ts'
import { createListDetailRoute } from '#client/list-detail-route.ts'
Expand All @@ -27,7 +27,7 @@ import {
import {
AccountManagementLayout,
AccountManagementList,
AccountManagementListItemButton,
AccountManagementListItemLink,
AccountManagementMessage,
AccountManagementSearchField,
AccountManagementShell,
Expand Down Expand Up @@ -513,44 +513,39 @@ export function AccountIntegrationsRoute(handle: Handle) {
key={group.appSlug}
mix={css({ display: 'grid', gap: spacing.xs })}
>
<button
type="button"
mix={[
on('click', () => {
navigate(
buildOauthAppHref(
group.appSlug,
getCurrentSearch(),
),
)
}),
css({
display: 'grid',
gap: spacing.xs,
padding: `${spacing.xs} ${spacing.sm}`,
border: 'none',
borderRadius: radius.md,
backgroundColor: appActive
? colors.primarySoftest
: 'transparent',
color: colors.text,
textAlign: 'left',
cursor: 'pointer',
transition: `background-color ${transitions.fast}, scale ${transitions.fast}`,
// Selecting a connection swaps the panel
// beside this list, which is easy to miss —
// the press itself has to register.
'&:active': { scale: '0.98' },
[hoverMq]: {
'&:hover': {
backgroundColor: colors.primarySoftest,
},
},
'@media (prefers-reduced-motion: reduce)': {
'&:active': { scale: 'none' },
<a
href={buildOauthAppHref(
group.appSlug,
getCurrentSearch(),
)}
data-prevent-scroll-reset
mix={css({
display: 'grid',
gap: spacing.xs,
padding: `${spacing.xs} ${spacing.sm}`,
border: 'none',
borderRadius: radius.md,
backgroundColor: appActive
? colors.primarySoftest
: 'transparent',
color: colors.text,
textAlign: 'left',
textDecoration: 'none',
cursor: 'pointer',
transition: `background-color ${transitions.fast}, scale ${transitions.fast}`,
// Selecting a connection swaps the panel
// beside this list, which is easy to miss —
// the press itself has to register.
'&:active': { scale: '0.98' },
[hoverMq]: {
'&:hover': {
backgroundColor: colors.primarySoftest,
},
}),
]}
},
'@media (prefers-reduced-motion: reduce)': {
'&:active': { scale: 'none' },
},
})}
>
<strong
mix={css({
Expand All @@ -571,25 +566,21 @@ export function AccountIntegrationsRoute(handle: Handle) {
? `${group.connections.length} connections · shared OAuth app`
: 'OAuth app'}
</span>
</button>
</a>
<AccountManagementList>
{group.connections.map((integration) => (
<li
key={integration.name}
mix={css({ minWidth: 0 })}
>
<AccountManagementListItemButton
<AccountManagementListItemLink
href={integrationsRoute.buildDetailHref(
integration.name,
getCurrentSearch(),
)}
active={
selection.selectedId === integration.name
}
onClick={() => {
navigate(
integrationsRoute.buildDetailHref(
integration.name,
getCurrentSearch(),
),
)
}}
>
<strong
mix={css({
Expand Down Expand Up @@ -633,7 +624,7 @@ export function AccountIntegrationsRoute(handle: Handle) {
{appTitle}
</span>
)}
</AccountManagementListItemButton>
</AccountManagementListItemLink>
</li>
))}
</AccountManagementList>
Expand All @@ -646,42 +637,37 @@ export function AccountIntegrationsRoute(handle: Handle) {
key={app.slug}
mix={css({ display: 'grid', gap: spacing.xs })}
>
<button
type="button"
mix={[
on('click', () => {
navigate(
buildOauthAppHref(app.slug, getCurrentSearch()),
)
}),
css({
display: 'grid',
gap: spacing.xs,
padding: `${spacing.xs} ${spacing.sm}`,
border: 'none',
borderRadius: radius.md,
backgroundColor:
selectedAppSlug === app.slug
? colors.primarySoftest
: 'transparent',
color: colors.text,
textAlign: 'left',
cursor: 'pointer',
transition: `background-color ${transitions.fast}, scale ${transitions.fast}`,
// Selecting an app swaps the panel beside this
// list, which is easy to miss — the press itself
// has to register.
'&:active': { scale: '0.98' },
[hoverMq]: {
'&:hover': {
backgroundColor: colors.primarySoftest,
},
},
'@media (prefers-reduced-motion: reduce)': {
'&:active': { scale: 'none' },
<a
href={buildOauthAppHref(app.slug, getCurrentSearch())}
data-prevent-scroll-reset
mix={css({
display: 'grid',
gap: spacing.xs,
padding: `${spacing.xs} ${spacing.sm}`,
border: 'none',
borderRadius: radius.md,
backgroundColor:
selectedAppSlug === app.slug
? colors.primarySoftest
: 'transparent',
color: colors.text,
textAlign: 'left',
textDecoration: 'none',
cursor: 'pointer',
transition: `background-color ${transitions.fast}, scale ${transitions.fast}`,
// Selecting an app swaps the panel beside this
// list, which is easy to miss — the press itself
// has to register.
'&:active': { scale: '0.98' },
[hoverMq]: {
'&:hover': {
backgroundColor: colors.primarySoftest,
},
}),
]}
},
'@media (prefers-reduced-motion: reduce)': {
'&:active': { scale: 'none' },
},
})}
>
<strong
mix={css({
Expand All @@ -700,7 +686,7 @@ export function AccountIntegrationsRoute(handle: Handle) {
>
OAuth app · no connections yet
</span>
</button>
</a>
</div>
))}
</div>
Expand Down Expand Up @@ -814,6 +800,7 @@ export function AccountIntegrationsRoute(handle: Handle) {
connection.name,
getCurrentSearch(),
)}
data-prevent-scroll-reset
mix={css(primaryLinkCss)}
>
{connection.accountLabel?.trim() || connection.name}
Expand Down
Loading
Loading