Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
643 changes: 643 additions & 0 deletions apps/www/src/app/examples/dataview/page.tsx

Large diffs are not rendered by default.

428 changes: 428 additions & 0 deletions docs/rfcs/002-unified-dataview-component.md

Large diffs are not rendered by default.

407 changes: 407 additions & 0 deletions packages/raystack/components/data-view/components/content.tsx

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
'use client';

import { ReactNode } from 'react';
import { useDataView } from '../hooks/useDataView';

export interface DataViewDisplayAccessProps {
/** Field (column) accessor key. Gates rendering on the column's current visibility state. */
accessorKey: string;
children: ReactNode;
/** Rendered when the referenced field is currently hidden. Defaults to null. */
fallback?: ReactNode;
}

/**
* Gates children on the current column visibility state from DataView context.
* Use inside free-form renderers (Timeline bars, custom renderers, cell overrides)
* so the single DisplayControls toggle reaches the same visibility story that
* Table/List rows get through their column specs.
*/
export function DisplayAccess({
accessorKey,
children,
fallback = null
}: DataViewDisplayAccessProps) {
const { table } = useDataView();
const column = table?.getColumn(accessorKey);
// If the column doesn't exist, default to visible so consumers can wrap JSX
// in DisplayAccess without worrying about typos silently breaking the render.
const isVisible = column ? column.getIsVisible() : true;
return <>{isVisible ? children : fallback}</>;
}

DisplayAccess.displayName = 'DataView.DisplayAccess';
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
'use client';

import { Chip } from '../../chip';
import { Flex } from '../../flex';
import { Text } from '../../text';
import { DataViewField } from '../data-view.types';
import { useDataView } from '../hooks/useDataView';

export function DisplayProperties<TData>({
fields
}: {
fields: DataViewField<TData>[];
}) {
const { table } = useDataView<TData>();
const hidableFields = fields?.filter(f => f.hideable) ?? [];

return (
<Flex direction='column' gap={3}>
<Text>Display Properties</Text>
<Flex gap={3} wrap='wrap'>
{hidableFields.map(field => {
const column = table.getColumn(field.accessorKey);
const isVisible = column ? column.getIsVisible() : true;
return (
<Chip
key={field.accessorKey}
variant='outline'
size='small'
color={isVisible ? 'accent' : 'neutral'}
onClick={() => column?.toggleVisibility()}
>
{field.label}
</Chip>
);
})}
</Flex>
</Flex>
);
}
122 changes: 122 additions & 0 deletions packages/raystack/components/data-view/components/display-settings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
'use client';

import { MixerHorizontalIcon } from '@radix-ui/react-icons';

import { isValidElement, ReactNode } from 'react';
import { Button } from '../../button';
import { Flex } from '../../flex';
import { Popover } from '../../popover';
import styles from '../data-view.module.css';
import { defaultGroupOption, SortOrdersValues } from '../data-view.types';
import { useDataView } from '../hooks/useDataView';
import { DisplayProperties } from './display-properties';
import { Grouping } from './grouping';
import { Ordering } from './ordering';

interface DisplaySettingsProps {
trigger?: ReactNode;
}

export function DisplaySettings<TData>({
trigger = (
<Button
variant='outline'
color='neutral'
size='small'
leadingIcon={<MixerHorizontalIcon />}
>
Display
</Button>
)
}: DisplaySettingsProps) {
const {
fields,
updateTableQuery,
tableQuery,
defaultSort,
onDisplaySettingsReset
} = useDataView<TData>();

const sortableColumns = (fields ?? [])
.filter(f => f.sortable)
.map(f => ({
label: f.label,
id: f.accessorKey
}));

function onSortChange(columnId: string, order: SortOrdersValues) {
updateTableQuery(query => {
return {
...query,
sort: [{ name: columnId, order }]
};
});
}

function onGroupChange(columnId: string) {
updateTableQuery(query => {
return {
...query,
group_by: [columnId]
};
});
}

function onGroupRemove() {
updateTableQuery(query => {
return {
...query,
group_by: []
};
});
}

function onReset() {
onDisplaySettingsReset();
}

return (
<Popover>
<Popover.Trigger
render={isValidElement(trigger) ? trigger : <button>{trigger}</button>}
/>
<Popover.Content
className={styles['display-popover-content']}
align='end'
>
<Flex direction='column'>
<Flex
direction='column'
className={styles['display-popover-properties-container']}
gap={5}
>
<Ordering
columnList={sortableColumns}
onChange={onSortChange}
value={tableQuery?.sort?.[0] || defaultSort}
/>
<Grouping
fields={fields ?? []}
onRemove={onGroupRemove}
onChange={onGroupChange}
value={tableQuery?.group_by?.[0] || defaultGroupOption.id}
/>
</Flex>
<Flex className={styles['display-popover-properties-container']}>
<DisplayProperties fields={fields ?? []} />
</Flex>
<Flex
justify='end'
className={styles['display-popover-reset-container']}
>
<Button variant='text' onClick={onReset} color='neutral'>
Reset to default
</Button>
</Flex>
</Flex>
</Popover.Content>
</Popover>
);
}

DisplaySettings.displayName = 'DataView.DisplayControls';
159 changes: 159 additions & 0 deletions packages/raystack/components/data-view/components/filters.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
'use client';

import { isValidElement, ReactNode, useMemo } from 'react';
import { FilterIcon } from '~/icons';
import { FilterOperatorTypes, FilterType } from '~/types/filters';
import { Button } from '../../button';
import { FilterChip } from '../../filter-chip';
import { Flex } from '../../flex';
import { IconButton } from '../../icon-button';
import { Menu } from '../../menu';
import { DataViewField } from '../data-view.types';
import { useDataView } from '../hooks/useDataView';
import { useFilters } from '../hooks/useFilters';

type Trigger<TData> =
| ReactNode
| (({
availableFilters,
appliedFilters
}: {
availableFilters: DataViewField<TData>[];
appliedFilters: Set<string>;
}) => ReactNode);

interface AddFilterProps<TData> {
fieldList: DataViewField<TData>[];
appliedFiltersSet: Set<string>;
onAddFilter: (field: DataViewField<TData>) => void;
children?: Trigger<TData>;
}

function AddFilter<TData>({
fieldList = [],
appliedFiltersSet,
onAddFilter,
children
}: AddFilterProps<TData>) {
const availableFilters = fieldList?.filter(
f => !appliedFiltersSet.has(f.accessorKey)
);

const trigger = useMemo(() => {
if (typeof children === 'function')
return children({ availableFilters, appliedFilters: appliedFiltersSet });
else if (children) return children;
else if (appliedFiltersSet.size > 0)
return (
<IconButton size={4}>
<FilterIcon />
</IconButton>
);
else
return (
<Button
variant='text'
size='small'
leadingIcon={<FilterIcon />}
color='neutral'
>
Filter
</Button>
);
}, [children, appliedFiltersSet, availableFilters]);

return availableFilters.length > 0 ? (
<Menu>
<Menu.Trigger
render={isValidElement(trigger) ? trigger : <button>{trigger}</button>}
/>
<Menu.Content>
{availableFilters?.map(field => (
<Menu.Item key={field.accessorKey} onClick={() => onAddFilter(field)}>
{field.label}
</Menu.Item>
))}
</Menu.Content>
</Menu>
) : null;
}

export function Filters<TData>({
classNames,
className,
trigger
}: {
classNames?: {
container?: string;
filterChips?: string;
addFilter?: string;
};
className?: string;
trigger?: Trigger<TData>;
}) {
const { fields, tableQuery } = useDataView<TData>();

const {
onAddFilter,
handleRemoveFilter,
handleFilterValueChange,
handleFilterOperationChange
} = useFilters<TData>();

const filterableFields = fields?.filter(f => f.filterable) ?? [];

const appliedFiltersSet = new Set(
tableQuery?.filters?.map(filter => filter.name)
);

const appliedFilters =
tableQuery?.filters?.map(filter => {
const field = fields?.find(f => f.accessorKey === filter.name);
return {
filterType: field?.filterType || FilterType.string,
label: field?.label || '',
options: field?.filterOptions || [],
selectProps: field?.filterProps?.select,
...filter
};
}) || [];

return (
<Flex gap={3} className={className}>
{appliedFilters.length > 0 && (
<Flex gap={3} className={classNames?.container}>
{appliedFilters.map(filter => (
<FilterChip
key={filter.name}
label={filter.label}
value={filter.value}
onRemove={() => handleRemoveFilter(filter.name)}
onValueChange={value =>
handleFilterValueChange(filter.name, value)
}
onOperationChange={operator =>
handleFilterOperationChange(
filter.name,
operator as FilterOperatorTypes
)
}
columnType={filter.filterType}
options={filter.options}
selectProps={filter.selectProps}
className={classNames?.filterChips}
/>
))}
</Flex>
)}
<AddFilter
fieldList={filterableFields}
appliedFiltersSet={appliedFiltersSet}
onAddFilter={onAddFilter}
>
{trigger}
</AddFilter>
</Flex>
);
}

Filters.displayName = 'DataView.Filters';
Loading
Loading