Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
b924c6c
Add Rooms list
sumo-slonik May 14, 2026
889f549
Merge branch 'kuba/fix/navigation-to-new-workspace' into kuba-nowakow…
sumo-slonik May 18, 2026
3d8e183
Merge remote-tracking branch 'origin/main' into kuba-nowakowski/featu…
sumo-slonik May 18, 2026
6c6788a
Align rooms list with WorkspacesListPage conventions
sumo-slonik May 18, 2026
002a3a8
Render rooms table rows and align with Figma spec
sumo-slonik May 18, 2026
7dc056d
Pass policyID as useOnyx dependency for rooms selector
sumo-slonik May 18, 2026
12a373c
Inline column flex helpers instead of a dedicated module
sumo-slonik May 18, 2026
e5d9903
Address review feedback on rooms list
sumo-slonik May 19, 2026
e3c5d40
Use Profile icon in rooms list header
sumo-slonik May 19, 2026
d0be423
refactor of rooms list
sumo-slonik May 20, 2026
bb5a636
Address PR review on rooms list
sumo-slonik May 20, 2026
b773de5
Include policy expense chat and unjoined rooms in workspace rooms list
sumo-slonik May 20, 2026
84be81e
Exclude threads from workspace rooms list
sumo-slonik May 22, 2026
3a28e41
Migrate workspace rooms list to Table component
sumo-slonik May 22, 2026
ffce874
Switch workspace rooms page to OpenPolicyRoomsPage API command
sumo-slonik May 22, 2026
fe2cb1e
Add search bar and full-width Create button on narrow layout
sumo-slonik May 22, 2026
68569e1
Tighten avatar gap and add fixed width Members column
sumo-slonik May 22, 2026
7777703
Align avatar gaps and balance column widths
sumo-slonik May 22, 2026
587fb1f
Merge branch 'main' into kuba-nowakowski/feature/workspace-rooms-list
sumo-slonik May 27, 2026
35e8ffb
Address PR review feedback
sumo-slonik May 27, 2026
a4522cd
Add label prop to Table.SearchBar usages in tests
sumo-slonik May 27, 2026
e5a366a
make a difference with plural and singular member on narrow
sumo-slonik May 27, 2026
c5b5ed2
change translation key
sumo-slonik May 27, 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
19 changes: 10 additions & 9 deletions src/components/Table/TableHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, {useRef} from 'react';
import {View} from 'react-native';
import type {ViewProps} from 'react-native';
import type {ViewProps, ViewStyle} from 'react-native';
import Icon from '@components/Icon';
import {PressableWithFeedback} from '@components/Pressable';
import Text from '@components/Text';
Expand Down Expand Up @@ -78,7 +78,7 @@ function TableHeader<T, ColumnKey extends string = string>({style, shouldHideHea
styles.gap3,
// Use Grid on web when available (will override flex if supported)
styles.dGrid,
!shouldUseNarrowTableLayout && {gridTemplateColumns: `repeat(${columns.length}, 1fr)`},
!shouldUseNarrowTableLayout && {gridTemplateColumns: columns.map((column) => (column.width ? `${column.width}px` : '1fr')).join(' ')},
style,
]}
{...props}
Expand Down Expand Up @@ -141,13 +141,14 @@ function TableHeaderColumn<T, ColumnKey extends string = string>({column}: {colu
toggleColumnSorting(columnKey);
};

const tableHeaderStyles = [
styles.flexRow,
styles.alignItemsCenter,
styles.tableHeaderContentHeight,
column.styling?.flex ? {flex: column.styling.flex} : styles.flex1,
column.styling?.containerStyles,
];
let columnSizeStyle: ViewStyle | undefined;
if (column.width) {
columnSizeStyle = {width: column.width};
} else if (column.styling?.flex) {
columnSizeStyle = {flex: column.styling.flex};
}

const tableHeaderStyles = [styles.flexRow, styles.alignItemsCenter, styles.tableHeaderContentHeight, columnSizeStyle ?? styles.flex1, column.styling?.containerStyles];

return (
<PressableWithFeedback
Expand Down
3 changes: 1 addition & 2 deletions src/components/Table/TableRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ export default function TableRow({
const styles = useThemeStyles();
const {processedData, columns, shouldUseNarrowTableLayout} = useTableContext();

const columnCount = columns.length;
const rowCount = processedData.length;
const isLastRow = rowIndex === rowCount - 1;
const isInteractive = interactive && !isLoading;
Expand All @@ -81,7 +80,7 @@ export default function TableRow({
styles.gap3,
styles.dFlex,
// Use Grid on web when available (will override flex if supported)
!shouldUseNarrowTableLayout && [styles.dGrid, {gridTemplateColumns: `repeat(${columnCount}, 1fr)`}],
!shouldUseNarrowTableLayout && [styles.dGrid, {gridTemplateColumns: columns.map((column) => (column.width ? `${column.width}px` : '1fr')).join(' ')}],
];

const renderChildren = (state: PressableStateCallbackType) => {
Expand Down
15 changes: 9 additions & 6 deletions src/components/Table/TableSearchBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import React from 'react';
import {View} from 'react-native';
import TextInput from '@components/TextInput';
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
import {useTableContext} from './TableContext';

/**
Expand All @@ -27,13 +26,17 @@ import {useTableContext} from './TableContext';
* item.name.toLowerCase().includes(searchString.toLowerCase())
* }
* >
* <Table.SearchBar />
* <Table.SearchBar label="Find item" />
* <Table.Body />
* </Table>
* ```
*/
function TableSearchBar() {
const {translate} = useLocalize();
type TableSearchBarProps = {
/** Label and accessibility label for the search input. */
label: string;
};

function TableSearchBar({label}: TableSearchBarProps) {
const expensifyIcons = useMemoizedLazyExpensifyIcons(['MagnifyingGlass']);
const {
activeSearchString,
Expand All @@ -43,8 +46,8 @@ function TableSearchBar() {
return (
<View>
<TextInput
label={translate('workspace.companyCards.findCard')}
accessibilityLabel={translate('workspace.companyCards.findCard')}
label={label}
accessibilityLabel={label}
value={activeSearchString}
onChangeText={(text) => updateSearchString(text)}
icon={activeSearchString.length === 0 ? expensifyIcons.MagnifyingGlass : undefined}
Expand Down
3 changes: 3 additions & 0 deletions src/components/Table/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ type TableColumn<ColumnKey extends string = string> = {
/** Display label shown in the table header. */
label: string;

/** Optional fixed width in pixels. When set, the column uses this width instead of an equal-share `1fr` track. */
width?: number;

/** Optional styling configuration for the column. */
styling?: TableColumnStyling;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ function WorkspaceCompanyCardsTableHeaderButtons({policyID, feedName, isLoading,
>
{!isLoading && showTableControls && (
<View style={[styles.mnw200]}>
<Table.SearchBar />
<Table.SearchBar label={translate('workspace.companyCards.findCard')} />
</View>
)}

Expand Down
159 changes: 159 additions & 0 deletions src/components/Tables/WorkspaceRoomsTable/WorkspaceRoomsTableRow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import React from 'react';
import {View} from 'react-native';
import Avatar from '@components/Avatar';
import Icon from '@components/Icon';
import ReportActionAvatars from '@components/ReportActionAvatars';
import Table from '@components/Table';
import Text from '@components/Text';
import TextWithTooltip from '@components/TextWithTooltip';
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import type {AvatarSource} from '@libs/UserUtils';
import variables from '@styles/variables';
import CONST from '@src/CONST';

type WorkspaceRoomRowData = {
/** The room reportID */
reportID: string;

/** The room display name */
name: string;

/** Owner accountID for resolving the avatar */
ownerAccountID?: number;

/** Owner avatar source */
ownerAvatar?: AvatarSource;

/** Pre-formatted owner display name */
ownerDisplayName: string;

/** Number of members in the room */
memberCount: number;

/** Callback fired when the row is pressed */
action: () => void;
};

type WorkspaceRoomsTableRowProps = {
/** The room data */
item: WorkspaceRoomRowData;

/** The index of the row relative to all other rows */
rowIndex: number;

/** Whether to use narrow table row layout */
shouldUseNarrowTableLayout: boolean;
};

function WorkspaceRoomsTableRow({item, rowIndex, shouldUseNarrowTableLayout}: WorkspaceRoomsTableRowProps) {
const theme = useTheme();
const styles = useThemeStyles();
const {translate} = useLocalize();
const icons = useMemoizedLazyExpensifyIcons(['ArrowRight']);

const memberCountSubtitle = translate('domain.groups.memberCount', {count: item.memberCount});
const narrowSubtitle = item.ownerDisplayName ? `${translate('common.createdBy')}: ${item.ownerDisplayName} • ${memberCountSubtitle}` : memberCountSubtitle;

return (
<Table.Row
interactive
rowIndex={rowIndex}
accessibilityLabel={item.name}
skeletonReasonAttributes={{context: 'WorkspaceRoomsTableRow'}}
onPress={item.action}
>
{({hovered}) => (
<>
{shouldUseNarrowTableLayout && (
<View style={[styles.flex1, styles.flexRow, styles.gap3, styles.alignItemsCenter]}>
<ReportActionAvatars
noRightMarginOnSubscriptContainer
singleAvatarContainerStyle={[styles.mr0]}
reportID={item.reportID}
size={CONST.AVATAR_SIZE.DEFAULT}
/>
<View style={[styles.flex1, styles.gap1]}>
<TextWithTooltip
shouldShowTooltip
text={item.name}
style={styles.optionDisplayName}
/>
<Text
numberOfLines={1}
style={styles.textLabelSupporting}
>
{narrowSubtitle}
</Text>
</View>
<Icon
src={icons.ArrowRight}
fill={theme.icon}
additionalStyles={[styles.alignSelfCenter, !hovered && styles.opacitySemiTransparent]}
width={variables.iconSizeNormal}
height={variables.iconSizeNormal}
/>
</View>
)}

{!shouldUseNarrowTableLayout && (
<>
<View style={[styles.flex1, styles.flexRow, styles.gap3, styles.alignItemsCenter]}>
<ReportActionAvatars
noRightMarginOnSubscriptContainer
singleAvatarContainerStyle={[styles.mr0]}
reportID={item.reportID}
size={CONST.AVATAR_SIZE.SMALL}
/>
<TextWithTooltip
shouldShowTooltip
text={item.name}
style={[styles.optionDisplayName, styles.flexShrink1]}
/>
</View>

<View style={[styles.flex1, styles.flexRow, styles.gap3, styles.alignItemsCenter]}>
{!!item.ownerDisplayName && (
<>
{!!item.ownerAccountID && (
<Avatar
source={item.ownerAvatar}
avatarID={item.ownerAccountID}
type={CONST.ICON_TYPE_AVATAR}
size={CONST.AVATAR_SIZE.MID_SUBSCRIPT}
/>
)}
<TextWithTooltip
shouldShowTooltip
text={item.ownerDisplayName}
style={styles.flexShrink1}
/>
</>
)}
</View>

<View style={styles.flex1}>
<Text numberOfLines={1}>{item.memberCount}</Text>
</View>

<View style={[styles.flexRow, styles.alignItemsCenter, styles.justifyContentEnd]}>
<Icon
src={icons.ArrowRight}
fill={theme.icon}
additionalStyles={[styles.alignSelfCenter, !hovered && styles.opacitySemiTransparent]}
width={variables.iconSizeNormal}
height={variables.iconSizeNormal}
/>
</View>
</>
)}
</>
)}
</Table.Row>
);
}

export default WorkspaceRoomsTableRow;
export type {WorkspaceRoomRowData};
78 changes: 78 additions & 0 deletions src/components/Tables/WorkspaceRoomsTable/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import type {ListRenderItemInfo} from '@shopify/flash-list';
import React from 'react';
import {View} from 'react-native';
import type {CompareItemsCallback, IsItemInSearchCallback, TableColumn} from '@components/Table';
import Table from '@components/Table';
import useLocalize from '@hooks/useLocalize';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useThemeStyles from '@hooks/useThemeStyles';
import variables from '@styles/variables';
import WorkspaceRoomsTableRow from './WorkspaceRoomsTableRow';
import type {WorkspaceRoomRowData} from './WorkspaceRoomsTableRow';

type WorkspaceRoomsTableColumnKey = 'name' | 'createdBy' | 'members' | 'actions';

type WorkspaceRoomsTableProps = {
/** Pre-built row data for each room */
rooms: WorkspaceRoomRowData[];
};

function WorkspaceRoomsTable({rooms}: WorkspaceRoomsTableProps) {
const styles = useThemeStyles();
const {translate, localeCompare} = useLocalize();
const {shouldUseNarrowLayout, isMediumScreenWidth} = useResponsiveLayout();
const shouldUseNarrowTableLayout = shouldUseNarrowLayout || isMediumScreenWidth;

const columns: Array<TableColumn<WorkspaceRoomsTableColumnKey>> = [
{key: 'name', label: translate('common.name')},
{key: 'createdBy', label: translate('common.createdBy')},
{key: 'members', label: translate('common.members'), width: variables.workspaceRoomsMembersColumnWidth},
{key: 'actions', label: '', width: variables.workspaceRoomsActionsColumnWidth, styling: {containerStyles: [styles.justifyContentEnd, styles.pr3]}},
];

const compareItems: CompareItemsCallback<WorkspaceRoomRowData, WorkspaceRoomsTableColumnKey> = (a, b, activeSorting) => {
const orderMultiplier = activeSorting.order === 'asc' ? 1 : -1;

if (activeSorting.columnKey === 'createdBy') {
return orderMultiplier * localeCompare(a.ownerDisplayName, b.ownerDisplayName);
}

if (activeSorting.columnKey === 'members') {
return orderMultiplier * (a.memberCount - b.memberCount);
}

return orderMultiplier * localeCompare(a.name, b.name);
};

const isItemInSearch: IsItemInSearchCallback<WorkspaceRoomRowData> = (item, searchValue) => item.name.toLowerCase().includes(searchValue.toLowerCase());

const renderItem = ({item, index}: ListRenderItemInfo<WorkspaceRoomRowData>) => (
<WorkspaceRoomsTableRow
item={item}
rowIndex={index}
shouldUseNarrowTableLayout={shouldUseNarrowTableLayout}
/>
);

return (
<Table
data={rooms}
columns={columns}
renderItem={renderItem}
compareItems={compareItems}
isItemInSearch={isItemInSearch}
initialSortColumn="name"
title={translate('workspace.common.rooms')}
keyExtractor={(row) => row.reportID}
>
<View style={[styles.searchBarMargin, styles.searchBarWidth(shouldUseNarrowTableLayout)]}>
<Table.SearchBar label={translate('workspace.common.findRoom')} />
</View>
<Table.Header />
<Table.Body />
</Table>
);
}

export default WorkspaceRoomsTable;
export type {WorkspaceRoomRowData, WorkspaceRoomsTableColumnKey};
1 change: 1 addition & 0 deletions src/languages/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4252,6 +4252,7 @@ ${amount} für ${merchant} – ${date}`,
workflows: 'Workflows',
workspace: 'Workspace',
findWorkspace: 'Arbeitsbereich finden',
findRoom: 'Raum finden',
edit: 'Arbeitsbereich bearbeiten',
enabled: 'Aktiviert',
disabled: 'Deaktiviert',
Expand Down
1 change: 1 addition & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4328,6 +4328,7 @@ const translations = {
workflows: 'Workflows',
workspace: 'Workspace',
findWorkspace: 'Find workspace',
findRoom: 'Find room',
edit: 'Edit workspace',
enabled: 'Enabled',
disabled: 'Disabled',
Expand Down
1 change: 1 addition & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4132,6 +4132,7 @@ ${amount} para ${merchant} - ${date}`,
workflows: 'Flujos de trabajo',
workspace: 'Espacio de trabajo',
findWorkspace: 'Encontrar espacio de trabajo',
findRoom: 'Encontrar sala',
edit: 'Editar espacio de trabajo',
enabled: 'Activada',
disabled: 'Desactivada',
Expand Down
1 change: 1 addition & 0 deletions src/languages/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4263,6 +4263,7 @@ ${amount} pour ${merchant} - ${date}`,
workflows: 'Workflows',
workspace: 'Espace de travail',
findWorkspace: 'Trouver un espace de travail',
findRoom: 'Trouver un salon',
edit: 'Modifier l’espace de travail',
enabled: 'Activé',
disabled: 'Désactivé',
Expand Down
1 change: 1 addition & 0 deletions src/languages/it.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4238,6 +4238,7 @@ ${amount} per ${merchant} - ${date}`,
workflows: 'Flussi di lavoro',
workspace: 'Spazio di lavoro',
findWorkspace: 'Trova spazio di lavoro',
findRoom: 'Trova stanza',
edit: 'Modifica spazio di lavoro',
enabled: 'Abilitato',
disabled: 'Disattivato',
Expand Down
Loading
Loading