mirror of
https://github.com/enso-org/enso.git
synced 2024-12-18 22:21:48 +03:00
Sticky headers for settings tables (#11826)
- https://github.com/enso-org/cloud-v2/issues/1574 - Make header rows sticky - Use solid background on header rows User Groups and Members tables in settings to avoid overlapping text - https://github.com/enso-org/cloud-v2/issues/895 - Make "name" column sticky in assets table # Important Notes None
This commit is contained in:
parent
ffec1c8004
commit
ffd0de4661
@ -2,6 +2,7 @@
|
||||
import * as React from 'react'
|
||||
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import invariant from 'tiny-invariant'
|
||||
import { useStore } from 'zustand'
|
||||
|
||||
import BlankIcon from '#/assets/blank.svg'
|
||||
@ -33,6 +34,7 @@ import * as localBackend from '#/services/LocalBackend'
|
||||
import * as backendModule from '#/services/Backend'
|
||||
|
||||
import { Text } from '#/components/AriaComponents'
|
||||
import { IndefiniteSpinner } from '#/components/Spinner'
|
||||
import type { AssetEvent } from '#/events/assetEvent'
|
||||
import { useCutAndPaste } from '#/events/assetListEvent'
|
||||
import {
|
||||
@ -56,8 +58,6 @@ import * as permissions from '#/utilities/permissions'
|
||||
import * as set from '#/utilities/set'
|
||||
import * as tailwindMerge from '#/utilities/tailwindMerge'
|
||||
import Visibility from '#/utilities/Visibility'
|
||||
import invariant from 'tiny-invariant'
|
||||
import { IndefiniteSpinner } from '../Spinner'
|
||||
|
||||
// =================
|
||||
// === Constants ===
|
||||
|
@ -69,7 +69,7 @@ const NORMAL_COLUMN_CSS_CLASSES = `px-cell-x py ${COLUMN_CSS_CLASSES}`
|
||||
|
||||
/** CSS classes for every column. */
|
||||
export const COLUMN_CSS_CLASS: Readonly<Record<Column, string>> = {
|
||||
[Column.name]: `rounded-rows-skip-level min-w-drive-name-column h-full p-0 border-l-0 ${COLUMN_CSS_CLASSES}`,
|
||||
[Column.name]: `z-10 sticky left-0 bg-dashboard rounded-rows-skip-level min-w-drive-name-column h-full p-0 border-l-0 ${COLUMN_CSS_CLASSES}`,
|
||||
[Column.modified]: `min-w-drive-modified-column rounded-rows-have-level ${NORMAL_COLUMN_CSS_CLASSES}`,
|
||||
[Column.sharedWith]: `min-w-drive-shared-with-column rounded-rows-have-level ${NORMAL_COLUMN_CSS_CLASSES}`,
|
||||
[Column.labels]: `min-w-drive-labels-column rounded-rows-have-level ${NORMAL_COLUMN_CSS_CLASSES}`,
|
||||
|
@ -41,7 +41,7 @@ export default function NameColumnHeading(props: AssetColumnHeadingProps) {
|
||||
getText('stopSortingByName')
|
||||
: getText('sortByNameDescending')
|
||||
}
|
||||
className="group flex h-table-row w-full items-center justify-start gap-icon-with-text px-name-column-x"
|
||||
className="group sticky left-0 flex h-table-row w-full items-center justify-start gap-icon-with-text bg-dashboard px-name-column-x"
|
||||
onPress={cycleSortDirection}
|
||||
>
|
||||
<Text className="text-sm font-semibold">{getText('nameColumnName')}</Text>
|
||||
|
@ -3,7 +3,6 @@ import { useRef, useState, type MutableRefObject, type RefObject } from 'react'
|
||||
|
||||
import { useSyncRef } from '#/hooks/syncRefHooks'
|
||||
import useOnScroll from '#/hooks/useOnScroll'
|
||||
import { unsafeWriteValue } from '#/utilities/write'
|
||||
|
||||
/** Options for the {@link useStickyTableHeaderOnScroll} hook. */
|
||||
interface UseStickyTableHeaderOnScrollOptions {
|
||||
@ -29,12 +28,7 @@ export function useStickyTableHeaderOnScroll(
|
||||
const trackShadowClassRef = useSyncRef(trackShadowClass)
|
||||
const [shadowClassName, setShadowClass] = useState('')
|
||||
const onScroll = useOnScroll(() => {
|
||||
if (rootRef.current != null && bodyRef.current != null) {
|
||||
unsafeWriteValue(
|
||||
bodyRef.current.style,
|
||||
'clipPath',
|
||||
`inset(${rootRef.current.scrollTop}px 0 0 0)`,
|
||||
)
|
||||
if (rootRef.current && bodyRef.current) {
|
||||
if (trackShadowClassRef.current) {
|
||||
const isAtTop = rootRef.current.scrollTop === 0
|
||||
const isAtBottom =
|
||||
|
@ -44,6 +44,7 @@ import Label from '#/components/dashboard/Label'
|
||||
import { ErrorDisplay } from '#/components/ErrorBoundary'
|
||||
import { IsolateLayout } from '#/components/IsolateLayout'
|
||||
import SelectionBrush from '#/components/SelectionBrush'
|
||||
import { IndefiniteSpinner } from '#/components/Spinner'
|
||||
import FocusArea from '#/components/styled/FocusArea'
|
||||
import SvgMask from '#/components/SvgMask'
|
||||
import { ASSETS_MIME_TYPE } from '#/data/mimeTypes'
|
||||
@ -140,7 +141,6 @@ import { EMPTY_SET, setPresence, withPresence } from '#/utilities/set'
|
||||
import type { SortInfo } from '#/utilities/sorting'
|
||||
import { twJoin, twMerge } from '#/utilities/tailwindMerge'
|
||||
import Visibility from '#/utilities/Visibility'
|
||||
import { IndefiniteSpinner } from '../components/Spinner'
|
||||
|
||||
declare module '#/utilities/LocalStorage' {
|
||||
/** */
|
||||
@ -1806,8 +1806,8 @@ function AssetsTable(props: AssetsTableProps) {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<table className="rr-block isolate table-fixed border-collapse rounded-rows">
|
||||
<thead className="sticky top-0 z-1 bg-dashboard">{headerRow}</thead>
|
||||
<table className="isolate table-fixed border-collapse rounded-rows">
|
||||
<thead className="sticky top-0 z-[11] bg-dashboard">{headerRow}</thead>
|
||||
<tbody ref={bodyRef}>
|
||||
{itemRows}
|
||||
<tr className="hidden h-row first:table-row">
|
||||
|
@ -73,7 +73,7 @@ export default function KeyboardShortcutsSettingsSection() {
|
||||
})}
|
||||
>
|
||||
<table className="table-fixed border-collapse rounded-rows">
|
||||
<thead className="sticky top-0">
|
||||
<thead className="sticky top-0 z-1 bg-dashboard">
|
||||
<tr className="h-row text-left text-sm font-semibold">
|
||||
<th className="min-w-8 pl-cell-x pr-1.5">{/* Icon */}</th>
|
||||
<th className="min-w-36 px-cell-x">{getText('name')}</th>
|
||||
|
@ -16,16 +16,8 @@ import InviteUsersModal from '#/modals/InviteUsersModal'
|
||||
import type * as backendModule from '#/services/Backend'
|
||||
import type RemoteBackend from '#/services/RemoteBackend'
|
||||
|
||||
// =================
|
||||
// === Constants ===
|
||||
// =================
|
||||
|
||||
const LIST_USERS_STALE_TIME_MS = 60_000
|
||||
|
||||
// ==============================
|
||||
// === MembersSettingsSection ===
|
||||
// ==============================
|
||||
|
||||
/** Settings tab for viewing and editing organization members. */
|
||||
export default function MembersSettingsSection() {
|
||||
const { getText } = textProvider.useText()
|
||||
@ -87,71 +79,73 @@ export default function MembersSettingsSection() {
|
||||
</ariaComponents.ButtonGroup>
|
||||
)}
|
||||
|
||||
<table className="table-fixed self-start rounded-rows">
|
||||
<thead>
|
||||
<tr className="h-row">
|
||||
<th className="min-w-48 max-w-80 border-x-2 border-transparent bg-clip-padding px-cell-x text-left text-sm font-semibold last:border-r-0">
|
||||
{getText('name')}
|
||||
</th>
|
||||
<th className="w-48 border-x-2 border-transparent bg-clip-padding px-cell-x text-left text-sm font-semibold last:border-r-0">
|
||||
{getText('status')}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="select-text">
|
||||
{members.map((member) => (
|
||||
<tr key={member.email} className="group h-row rounded-rows-child">
|
||||
<td className="min-w-48 max-w-80 border-x-2 border-transparent bg-clip-padding px-4 py-1 first:rounded-l-full last:rounded-r-full last:border-r-0">
|
||||
<ariaComponents.Text truncate="1" className="block">
|
||||
{member.email}
|
||||
</ariaComponents.Text>
|
||||
<ariaComponents.Text truncate="1" className="block text-2xs text-primary/40">
|
||||
{member.name}
|
||||
</ariaComponents.Text>
|
||||
</td>
|
||||
<td className="border-x-2 border-transparent bg-clip-padding px-cell-x first:rounded-l-full last:rounded-r-full last:border-r-0">
|
||||
<div className="flex flex-col">
|
||||
{getText('active')}
|
||||
{member.email !== user.email && isAdmin && (
|
||||
<ariaComponents.ButtonGroup gap="small" className="mt-0.5">
|
||||
<RemoveMemberButton backend={backend} userId={member.userId} />
|
||||
</ariaComponents.ButtonGroup>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<div className="min-h-0 flex-1 overflow-auto">
|
||||
<table className="table-fixed self-start rounded-rows">
|
||||
<thead className="sticky top-0 z-1 bg-dashboard">
|
||||
<tr className="h-row">
|
||||
<th className="min-w-48 max-w-80 border-x-2 border-transparent bg-clip-padding px-cell-x text-left text-sm font-semibold last:border-r-0">
|
||||
{getText('name')}
|
||||
</th>
|
||||
<th className="w-48 border-x-2 border-transparent bg-clip-padding px-cell-x text-left text-sm font-semibold last:border-r-0">
|
||||
{getText('status')}
|
||||
</th>
|
||||
</tr>
|
||||
))}
|
||||
{invitations.invitations.map((invitation) => (
|
||||
<tr key={invitation.userEmail} className="group h-row rounded-rows-child">
|
||||
<td className="border-x-2 border-transparent bg-clip-padding px-4 py-1 first:rounded-l-full last:rounded-r-full last:border-r-0">
|
||||
<span className="block text-sm">{invitation.userEmail}</span>
|
||||
</td>
|
||||
<td className="border-x-2 border-transparent bg-clip-padding px-cell-x first:rounded-l-full last:rounded-r-full last:border-r-0">
|
||||
<div className="flex flex-col">
|
||||
{getText('pendingInvitation')}
|
||||
{isAdmin && (
|
||||
<ariaComponents.ButtonGroup gap="small" className="mt-0.5">
|
||||
<ariaComponents.CopyButton
|
||||
size="custom"
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention, camelcase
|
||||
copyText={`enso://auth/registration?=${new URLSearchParams({ organization_id: invitation.organizationId }).toString()}`}
|
||||
aria-label={getText('copyInviteLink')}
|
||||
copyIcon={false}
|
||||
>
|
||||
{getText('copyInviteLink')}
|
||||
</ariaComponents.CopyButton>
|
||||
</thead>
|
||||
<tbody className="select-text">
|
||||
{members.map((member) => (
|
||||
<tr key={member.email} className="group h-row rounded-rows-child">
|
||||
<td className="min-w-48 max-w-80 border-x-2 border-transparent bg-clip-padding px-4 py-1 first:rounded-l-full last:rounded-r-full last:border-r-0">
|
||||
<ariaComponents.Text truncate="1" className="block">
|
||||
{member.email}
|
||||
</ariaComponents.Text>
|
||||
<ariaComponents.Text truncate="1" className="block text-2xs text-primary/40">
|
||||
{member.name}
|
||||
</ariaComponents.Text>
|
||||
</td>
|
||||
<td className="border-x-2 border-transparent bg-clip-padding px-cell-x first:rounded-l-full last:rounded-r-full last:border-r-0">
|
||||
<div className="flex flex-col">
|
||||
{getText('active')}
|
||||
{member.email !== user.email && isAdmin && (
|
||||
<ariaComponents.ButtonGroup gap="small" className="mt-0.5">
|
||||
<RemoveMemberButton backend={backend} userId={member.userId} />
|
||||
</ariaComponents.ButtonGroup>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{invitations.invitations.map((invitation) => (
|
||||
<tr key={invitation.userEmail} className="group h-row rounded-rows-child">
|
||||
<td className="border-x-2 border-transparent bg-clip-padding px-4 py-1 first:rounded-l-full last:rounded-r-full last:border-r-0">
|
||||
<span className="block text-sm">{invitation.userEmail}</span>
|
||||
</td>
|
||||
<td className="border-x-2 border-transparent bg-clip-padding px-cell-x first:rounded-l-full last:rounded-r-full last:border-r-0">
|
||||
<div className="flex flex-col">
|
||||
{getText('pendingInvitation')}
|
||||
{isAdmin && (
|
||||
<ariaComponents.ButtonGroup gap="small" className="mt-0.5">
|
||||
<ariaComponents.CopyButton
|
||||
size="custom"
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention, camelcase
|
||||
copyText={`enso://auth/registration?=${new URLSearchParams({ organization_id: invitation.organizationId }).toString()}`}
|
||||
aria-label={getText('copyInviteLink')}
|
||||
copyIcon={false}
|
||||
>
|
||||
{getText('copyInviteLink')}
|
||||
</ariaComponents.CopyButton>
|
||||
|
||||
<ResendInvitationButton invitation={invitation} backend={backend} />
|
||||
<ResendInvitationButton invitation={invitation} backend={backend} />
|
||||
|
||||
<RemoveInvitationButton backend={backend} email={invitation.userEmail} />
|
||||
</ariaComponents.ButtonGroup>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<RemoveInvitationButton backend={backend} email={invitation.userEmail} />
|
||||
</ariaComponents.ButtonGroup>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -21,10 +21,6 @@ import { UserId, type User } from '#/services/Backend'
|
||||
import { twMerge } from '#/utilities/tailwindMerge'
|
||||
import UserRow from './UserRow'
|
||||
|
||||
// ====================
|
||||
// === MembersTable ===
|
||||
// ====================
|
||||
|
||||
/** Props for a {@link MembersTable}. */
|
||||
export interface MembersTableProps {
|
||||
readonly backend: Backend
|
||||
@ -129,7 +125,7 @@ export default function MembersTable(props: MembersTableProps) {
|
||||
className="w-settings-main-section max-w-full table-fixed self-start rounded-rows"
|
||||
{...(draggable ? { dragAndDropHooks } : {})}
|
||||
>
|
||||
<TableHeader className="sticky top h-row">
|
||||
<TableHeader className="sticky top-0 z-1 h-row bg-dashboard">
|
||||
<Column
|
||||
isRowHeader
|
||||
className="w-48 border-x-2 border-transparent bg-clip-padding px-cell-x text-left text-sm font-semibold last:border-r-0"
|
||||
|
@ -29,14 +29,14 @@ function SettingsSection(props: SettingsSectionProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-2.5">
|
||||
<div className="flex w-full flex-1 flex-col gap-2.5 overflow-auto">
|
||||
{!heading ? null : (
|
||||
<Text.Heading level={2} weight="bold">
|
||||
{getText(nameId)}
|
||||
</Text.Heading>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex min-h-0 flex-1 flex-col justify-start gap-2">
|
||||
{entries.map((entry, i) => (
|
||||
<SettingsEntry key={i} context={context} data={entry} />
|
||||
))}
|
||||
|
@ -77,7 +77,7 @@ export default function SettingsTab(props: SettingsTabProps) {
|
||||
{...contentProps}
|
||||
>
|
||||
{columns.map((sectionsInColumn, i) => (
|
||||
<div key={i} className={twMerge('flex h-fit flex-1 flex-col gap-8 pb-12', classes[i])}>
|
||||
<div key={i} className={twMerge('flex h-fit flex-1 flex-col gap-8', classes[i])}>
|
||||
{sectionsInColumn.map((section) => (
|
||||
<SettingsSection key={section.nameId} context={context} data={section} />
|
||||
))}
|
||||
|
@ -139,7 +139,7 @@ export default function UserGroupsSettingsSection(props: UserGroupsSettingsSecti
|
||||
return (
|
||||
<>
|
||||
{isAdmin && (
|
||||
<ButtonGroup verticalAlign="center">
|
||||
<ButtonGroup verticalAlign="center" className="flex-initial">
|
||||
{shouldDisplayPaywall && (
|
||||
<PaywallDialogButton
|
||||
feature="userGroupsFull"
|
||||
@ -178,7 +178,7 @@ export default function UserGroupsSettingsSection(props: UserGroupsSettingsSecti
|
||||
<div
|
||||
ref={rootRef}
|
||||
className={twMerge(
|
||||
'overflow-auto overflow-x-hidden transition-all lg:mb-2',
|
||||
'min-h-0 flex-initial overflow-y-auto overflow-x-hidden transition-all lg:mb-2',
|
||||
shadowClassName,
|
||||
)}
|
||||
onScroll={onUserGroupsTableScroll}
|
||||
@ -188,7 +188,7 @@ export default function UserGroupsSettingsSection(props: UserGroupsSettingsSecti
|
||||
className="w-full max-w-3xl table-fixed self-start rounded-rows"
|
||||
dragAndDropHooks={dragAndDropHooks}
|
||||
>
|
||||
<TableHeader className="sticky top h-row">
|
||||
<TableHeader className="sticky top-0 z-1 h-row bg-dashboard">
|
||||
<Column
|
||||
isRowHeader
|
||||
className="w-full border-x-2 border-transparent bg-clip-padding px-cell-x text-left text-sm font-semibold last:border-r-0"
|
||||
|
@ -391,7 +391,7 @@ export const SETTINGS_TAB_DATA: Readonly<Record<SettingsTabType, SettingsTabData
|
||||
sections: [
|
||||
{
|
||||
nameId: 'userGroupsSettingsSection',
|
||||
columnClassName: 'h-3/5 lg:h-[unset] overflow-auto',
|
||||
columnClassName: 'lg:h-[unset] overflow-auto h-auto',
|
||||
entries: [
|
||||
{
|
||||
type: 'custom',
|
||||
@ -402,7 +402,7 @@ export const SETTINGS_TAB_DATA: Readonly<Record<SettingsTabType, SettingsTabData
|
||||
{
|
||||
nameId: 'userGroupsUsersSettingsSection',
|
||||
column: 2,
|
||||
columnClassName: 'h-2/5 lg:h-[unset] overflow-auto',
|
||||
columnClassName: 'lg:h-[unset] overflow-auto h-auto',
|
||||
entries: [
|
||||
{
|
||||
type: 'custom',
|
||||
|
Loading…
Reference in New Issue
Block a user