ui: remove search field, show filters picker by default for assist

This commit is contained in:
nick-delirium 2024-10-31 10:36:02 +01:00
parent b941ea2cd8
commit da632304a1
No known key found for this signature in database
GPG key ID: 93ABD695DF5FDBA0
5 changed files with 304 additions and 312 deletions

View file

@ -6,7 +6,6 @@ import AssistSearchField from './AssistSearchField';
function AssistView() {
return (
<div className="w-full mx-auto" style={{ maxWidth: '1360px'}}>
<AssistSearchField />
<LiveSessionSearch />
<div className="my-4" />
<LiveSessionList />

View file

@ -1,223 +1,240 @@
import {Space} from 'antd';
import {List} from 'immutable';
import {GripHorizontal} from 'lucide-react';
import {observer} from 'mobx-react-lite';
import React, {useEffect} from 'react';
import { Space } from 'antd';
import { List } from 'immutable';
import { GripHorizontal } from 'lucide-react';
import { observer } from 'mobx-react-lite';
import React, { useEffect } from 'react';
import { Button, Card } from 'antd';
import { Icon } from 'UI';
import FilterItem from '../FilterItem';
import EventsOrder from "Shared/Filters/FilterList/EventsOrder";
import EventsOrder from 'Shared/Filters/FilterList/EventsOrder';
import FilterSelection from '../FilterSelection/FilterSelection';
interface Props {
filter?: any; // event/filter
onUpdateFilter: (filterIndex: any, filter: any) => void;
onFilterMove?: (filters: any) => void;
onRemoveFilter: (filterIndex: any) => void;
onChangeEventsOrder: (e: any, {name, value}: any) => void;
hideEventsOrder?: boolean;
observeChanges?: () => void;
saveRequestPayloads?: boolean;
supportsEmpty?: boolean;
readonly?: boolean;
excludeFilterKeys?: Array<string>;
isConditional?: boolean;
actions?: React.ReactNode[];
filter?: any; // event/filter
onUpdateFilter: (filterIndex: any, filter: any) => void;
onFilterMove?: (filters: any) => void;
onRemoveFilter: (filterIndex: any) => void;
onChangeEventsOrder: (e: any, { name, value }: any) => void;
hideEventsOrder?: boolean;
observeChanges?: () => void;
saveRequestPayloads?: boolean;
supportsEmpty?: boolean;
readonly?: boolean;
excludeFilterKeys?: Array<string>;
isConditional?: boolean;
actions?: React.ReactNode[];
onlyFilters?: boolean;
onAddFilter: (filter: any) => void;
}
function FilterList(props: Props) {
const {
observeChanges = () => {
},
filter,
hideEventsOrder = false,
saveRequestPayloads,
supportsEmpty = true,
excludeFilterKeys = [],
isConditional,
actions = []
} = props;
const {
observeChanges = () => {},
filter,
hideEventsOrder = false,
saveRequestPayloads,
supportsEmpty = true,
excludeFilterKeys = [],
isConditional,
actions = [],
onAddFilter,
onlyFilters,
} = props;
const filters = filter.filters;
const hasEvents = filters.filter((i: any) => i.isEvent).length > 0;
const hasFilters = filters.filter((i: any) => !i.isEvent).length > 0;
const filters = filter.filters;
const hasEvents = filters.filter((i: any) => i.isEvent).length > 0;
const hasFilters = filters.filter((i: any) => !i.isEvent).length > 0;
let rowIndex = 0;
const cannotDeleteFilter = hasEvents && !supportsEmpty;
let rowIndex = 0;
const cannotDeleteFilter = hasEvents && !supportsEmpty;
useEffect(observeChanges, [filters]);
useEffect(observeChanges, [filters]);
const onRemoveFilter = (filterIndex: any) => {
props.onRemoveFilter(filterIndex);
};
const onRemoveFilter = (filterIndex: any) => {
props.onRemoveFilter(filterIndex);
};
const [hoveredItem, setHoveredItem] = React.useState<Record<string, any>>({
i: null,
position: null,
});
const [draggedInd, setDraggedItem] = React.useState<number | null>(null);
const [hoveredItem, setHoveredItem] = React.useState<Record<string, any>>({
i: null,
position: null,
});
const [draggedInd, setDraggedItem] = React.useState<number | null>(null);
const handleDragOverEv = (event: Record<string, any>, i: number) => {
event.preventDefault();
const target = event.currentTarget.getBoundingClientRect();
const hoverMiddleY = (target.bottom - target.top) / 2;
const hoverClientY = event.clientY - target.top;
const handleDragOverEv = (event: Record<string, any>, i: number) => {
event.preventDefault();
const target = event.currentTarget.getBoundingClientRect();
const hoverMiddleY = (target.bottom - target.top) / 2;
const hoverClientY = event.clientY - target.top;
const position = hoverClientY < hoverMiddleY ? 'top' : 'bottom';
setHoveredItem({position, i});
};
const position = hoverClientY < hoverMiddleY ? 'top' : 'bottom';
setHoveredItem({ position, i });
};
const calculateNewPosition = React.useCallback(
(draggedInd: number, hoveredIndex: number, hoveredPosition: string) => {
if (hoveredPosition === 'bottom') {
hoveredIndex++;
}
return draggedInd < hoveredIndex ? hoveredIndex - 1 : hoveredIndex;
},
[]
);
const calculateNewPosition = React.useCallback(
(draggedInd: number, hoveredIndex: number, hoveredPosition: string) => {
if (hoveredPosition === 'bottom') {
hoveredIndex++;
}
return draggedInd < hoveredIndex ? hoveredIndex - 1 : hoveredIndex;
},
[]
);
const handleDragStart = React.useCallback((
ev: Record<string, any>,
index: number,
elId: string
) => {
ev.dataTransfer.setData("text/plain", index.toString());
setDraggedItem(index);
const el = document.getElementById(elId);
if (el) {
ev.dataTransfer.setDragImage(el, 0, 0);
}
}, [])
const handleDragStart = React.useCallback(
(ev: Record<string, any>, index: number, elId: string) => {
ev.dataTransfer.setData('text/plain', index.toString());
setDraggedItem(index);
const el = document.getElementById(elId);
if (el) {
ev.dataTransfer.setDragImage(el, 0, 0);
}
},
[]
);
const handleDrop = React.useCallback(
(event: Record<string, any>) => {
event.preventDefault();
if (draggedInd === null) return;
const newItems = filters.toArray();
const newPosition = calculateNewPosition(
draggedInd,
hoveredItem.i,
hoveredItem.position
);
const handleDrop = React.useCallback(
(event: Record<string, any>) => {
event.preventDefault();
if (draggedInd === null) return;
const newItems = filters.toArray();
const newPosition = calculateNewPosition(
draggedInd,
hoveredItem.i,
hoveredItem.position
);
const reorderedItem = newItems.splice(draggedInd, 1)[0];
newItems.splice(newPosition, 0, reorderedItem);
const reorderedItem = newItems.splice(draggedInd, 1)[0];
newItems.splice(newPosition, 0, reorderedItem);
props.onFilterMove?.(List(newItems));
setHoveredItem({i: null, position: null});
setDraggedItem(null);
},
[draggedInd, hoveredItem, filters, props.onFilterMove]
);
props.onFilterMove?.(List(newItems));
setHoveredItem({ i: null, position: null });
setDraggedItem(null);
},
[draggedInd, hoveredItem, filters, props.onFilterMove]
);
const eventsNum = filters.filter((i: any) => i.isEvent).size
return (
<div className="flex flex-col">
{hasEvents && (
<>
<div className="flex items-center mb-2">
<div className="text-sm color-gray-medium mr-auto">
{filter.eventsHeader || 'EVENTS'}
</div>
const eventsNum = filters.filter((i: any) => i.isEvent).size;
return (
<div className="flex flex-col">
{onlyFilters ? null : (<Card size={'small'}>
<div className="flex items-center mb-2">
<div className="font-semibold">
{filter.eventsHeader || 'Events'}
</div>
<Space>
{!hideEventsOrder && <EventsOrder filter={filter}
onChange={props.onChangeEventsOrder}/>}
{actions && actions.map((action, index) => (
<div key={index}>{action}</div>
))}
</Space>
<Space>
{!hideEventsOrder && (
<EventsOrder
filter={filter}
onChange={props.onChangeEventsOrder}
/>
)}
{actions &&
actions.map((action, index) => <div key={index}>{action}</div>)}
</Space>
</div>
<div className={'flex flex-col'}>
{filters.map((filter: any, filterIndex: number) =>
filter.isEvent ? (
<div
style={{
pointerEvents: 'unset',
paddingTop:
hoveredItem.i === filterIndex &&
hoveredItem.position === 'top'
? '1.5rem'
: '0.5rem',
paddingBottom:
hoveredItem.i === filterIndex &&
hoveredItem.position === 'bottom'
? '1.5rem'
: '0.5rem',
marginLeft: '-1.25rem',
width: 'calc(100% + 2.5rem)',
}}
className={
'hover:bg-active-blue px-5 gap-2 items-center flex'
}
id={`${filter.key}-${filterIndex}`}
onDragOver={(e) => handleDragOverEv(e, filterIndex)}
onDrop={(e) => handleDrop(e)}
key={`${filter.key}-${filterIndex}`}
>
{!!props.onFilterMove && eventsNum > 1 ? (
<div
className={'p-2 cursor-grab'}
draggable={!!props.onFilterMove}
onDragStart={(e) =>
handleDragStart(
e,
filterIndex,
`${filter.key}-${filterIndex}`
)
}
>
<GripHorizontal size={16} />
</div>
<div className={'flex flex-col'}>
{filters.map((filter: any, filterIndex: number) =>
filter.isEvent ? (
<div
style={{
pointerEvents: 'unset',
paddingTop:
hoveredItem.i === filterIndex &&
hoveredItem.position === 'top'
? '1.5rem'
: '0.5rem',
paddingBottom:
hoveredItem.i === filterIndex &&
hoveredItem.position === 'bottom'
? '1.5rem'
: '0.5rem',
marginLeft: '-1.25rem',
width: 'calc(100% + 2.5rem)',
}}
className={
'hover:bg-active-blue px-5 gap-2 items-center flex'
}
id={`${filter.key}-${filterIndex}`}
onDragOver={(e) => handleDragOverEv(e, filterIndex)}
onDrop={(e) => handleDrop(e)}
key={`${filter.key}-${filterIndex}`}
>
{!!props.onFilterMove && eventsNum > 1 ? (
<div
className={'p-2 cursor-grab'}
draggable={!!props.onFilterMove}
onDragStart={(e) =>
handleDragStart(
e,
filterIndex,
`${filter.key}-${filterIndex}`
)
}
>
<GripHorizontal size={16}/>
</div>
) : null}
<FilterItem
filterIndex={rowIndex++}
filter={filter}
onUpdate={(filter) =>
props.onUpdateFilter(filterIndex, filter)
}
onRemoveFilter={() => onRemoveFilter(filterIndex)}
saveRequestPayloads={saveRequestPayloads}
disableDelete={cannotDeleteFilter}
excludeFilterKeys={excludeFilterKeys}
readonly={props.readonly}
isConditional={isConditional}
/>
</div>
) : null
)}
</div>
<div className="mb-2"/>
</>
) : null}
<FilterItem
filterIndex={rowIndex++}
filter={filter}
onUpdate={(filter) =>
props.onUpdateFilter(filterIndex, filter)
}
onRemoveFilter={() => onRemoveFilter(filterIndex)}
saveRequestPayloads={saveRequestPayloads}
disableDelete={cannotDeleteFilter}
excludeFilterKeys={excludeFilterKeys}
readonly={props.readonly}
isConditional={isConditional}
/>
</div>
) : null
)}
</div>
<div className="mb-2" />
</Card>)}
{hasFilters && (
<>
{hasEvents && <div className="border-t -mx-5 mb-4"/>}
<div className="mb-2 text-sm color-gray-medium mr-auto">FILTERS</div>
{filters.map((filter: any, filterIndex: any) =>
!filter.isEvent ? (
<div className={'py-2 hover:bg-active-blue px-5'} style={{
marginLeft: '-1.25rem',
width: 'calc(100% + 2.5rem)',
}}>
<FilterItem
key={filterIndex}
readonly={props.readonly}
isFilter={true}
filterIndex={filterIndex}
filter={filter}
onUpdate={(filter) => props.onUpdateFilter(filterIndex, filter)}
onRemoveFilter={() => onRemoveFilter(filterIndex)}
excludeFilterKeys={excludeFilterKeys}
isConditional={isConditional}
/>
</div>
) : null
)}
</>
)}
<Card size={'small'}>
<div className={'flex items-center gap-2 mb-2'}>
<div className="font-semibold">Filters</div>
<FilterSelection filter={undefined} onFilterClick={onAddFilter}>
<Button
icon={<Icon name={'filter'} />}
type="default"
size={'small'}
>
Add
</Button>
</FilterSelection>
</div>
);
{filters.map((filter: any, filterIndex: any) =>
!filter.isEvent ? (
<div
className={'py-2 hover:bg-active-blue px-5'}
style={{
marginLeft: '-1.25rem',
width: 'calc(100% + 2.5rem)',
}}
>
<FilterItem
key={filterIndex}
readonly={props.readonly}
isFilter={true}
filterIndex={filterIndex}
filter={filter}
onUpdate={(filter) => props.onUpdateFilter(filterIndex, filter)}
onRemoveFilter={() => onRemoveFilter(filterIndex)}
excludeFilterKeys={excludeFilterKeys}
isConditional={isConditional}
/>
</div>
) : null
)}
</Card>
</div>
);
}
export default observer(FilterList);

View file

@ -9,8 +9,8 @@ function LiveSessionSearch() {
const { projectsStore, searchStoreLive, sessionStore } = useStore();
const saveRequestPayloads = projectsStore.active?.saveRequestPayloads;
const appliedFilter = searchStoreLive.instance;
const hasEvents = appliedFilter.filters.filter(i => i.isEvent).length > 0;
const hasFilters = appliedFilter.filters.filter(i => !i.isEvent).length > 0;
const hasEvents = appliedFilter.filters.filter((i) => i.isEvent).length > 0;
const hasFilters = appliedFilter.filters.filter((i) => !i.isEvent).length > 0;
useEffect(() => {
void searchStoreLive.fetchSessions();
@ -31,7 +31,7 @@ function LiveSessionSearch() {
});
searchStoreLive.edit({
filters: newFilters
filters: newFilters,
});
void searchStoreLive.fetchSessions();
@ -39,45 +39,23 @@ function LiveSessionSearch() {
const onChangeEventsOrder = (e: any, { name, value }: any) => {
searchStoreLive.edit({
eventsOrder: value
eventsOrder: value,
});
void searchStoreLive.fetchSessions();
};
return (hasEvents || hasFilters) ? (
<div className="border bg-white rounded mt-4">
<div className="p-5">
<FilterList
filter={appliedFilter}
onUpdateFilter={onUpdateFilter}
onRemoveFilter={onRemoveFilter}
onChangeEventsOrder={onChangeEventsOrder}
saveRequestPayloads={saveRequestPayloads}
/>
</div>
<div className="border-t px-5 py-1 flex items-center -mx-2">
<div>
<FilterSelection
filter={undefined}
onFilterClick={onAddFilter}
>
{/* <IconButton primaryText label="ADD STEP" icon="plus" /> */}
<Button
variant="text-primary"
className="mr-2"
// onClick={() => setshowModal(true)}
icon="plus">
ADD STEP
</Button>
</FilterSelection>
</div>
<div className="ml-auto flex items-center">
</div>
</div>
</div>
) : <></>;
return (
<FilterList
filter={appliedFilter}
onAddFilter={onAddFilter}
onUpdateFilter={onUpdateFilter}
onRemoveFilter={onRemoveFilter}
onChangeEventsOrder={onChangeEventsOrder}
saveRequestPayloads={saveRequestPayloads}
onlyFilters={true}
/>
);
}
export default observer(LiveSessionSearch);

View file

@ -10,11 +10,11 @@ interface Props {
export default function MetaItem(props: Props) {
const { className = '', label, value } = props
return (
<div className={cn("flex items-center rounded", className)}>
<span className="rounded-tl rounded-bl bg-gray-light-shade px-2 color-gray-medium capitalize" style={{ maxWidth: "150px"}}>
<div className={cn("flex items-center rounded border border-gray-light-shade", className)}>
<span className="rounded-tl rounded-bl bg-gray-light-shade px-2" style={{ maxWidth: "150px"}}>
<TextEllipsis text={label} className="p-0" popupProps={{ size: 'small', disabled: true }} />
</span>
<span className="rounded-tr rounded-br bg-gray-lightest px-2 color-gray-dark" style={{ maxWidth: "150px"}}>
<span className="rounded-tr rounded-br bg-white px-2" style={{ maxWidth: "150px"}}>
<TextEllipsis text={value} className="p-0" popupProps={{ size: 'small', disabled: true }} />
</span>
</div>

View file

@ -13,14 +13,7 @@ import {
sessions as sessionsRoute,
} from 'App/routes';
import { capitalize } from 'App/utils';
import {
Avatar,
CountryFlag,
Icon,
Label,
TextEllipsis,
Tooltip,
} from 'UI';
import { Avatar, CountryFlag, Icon, Label, TextEllipsis, Tooltip } from 'UI';
import Counter from './Counter';
import ErrorBars from './ErrorBars';
@ -83,7 +76,7 @@ const PREFETCH_STATE = {
none: 0,
loading: 1,
fetched: 2,
}
};
function SessionItem(props: RouteComponentProps & Props) {
const { settingsStore, sessionStore } = useStore();
@ -130,32 +123,32 @@ function SessionItem(props: RouteComponentProps & Props) {
const location = props.location;
const queryParams = Object.fromEntries(new URLSearchParams(location.search));
const isMobile = platform !== 'web'
const isMobile = platform !== 'web';
const formattedDuration = durationFormatted(duration);
const hasUserId = userId || userAnonymousId;
const isSessions = isRoute(SESSIONS_ROUTE, location.pathname);
const isAssist =
(!ignoreAssist &&
(isRoute(ASSIST_ROUTE, location.pathname) ||
isRoute(ASSIST_LIVE_SESSION, location.pathname) ||
location.pathname.includes('multiview'))) ||
(isRoute(ASSIST_ROUTE, location.pathname) ||
isRoute(ASSIST_LIVE_SESSION, location.pathname) ||
location.pathname.includes('multiview'))) ||
props.live;
const isLastPlayed = lastPlayedSessionId === sessionId;
const _metaList = Object.keys(metadata)
.map((key) => {
const value = metadata[key];
return { label: key, value };
});
const _metaList = Object.keys(metadata).map((key) => {
const value = metadata[key];
return { label: key, value };
});
const handleHover = async () => {
if (
prefetchState !== PREFETCH_STATE.none
|| props.live
|| isAssist
|| isMobile
) return;
prefetchState !== PREFETCH_STATE.none ||
props.live ||
isAssist ||
isMobile
)
return;
setPrefetched(PREFETCH_STATE.loading);
try {
@ -167,10 +160,10 @@ function SessionItem(props: RouteComponentProps & Props) {
};
const populateData = () => {
if (
props.live
|| isAssist
|| prefetchState === PREFETCH_STATE.none
|| isMobile
props.live ||
isAssist ||
prefetchState === PREFETCH_STATE.none ||
isMobile
) {
return;
}
@ -193,38 +186,46 @@ function SessionItem(props: RouteComponentProps & Props) {
<div className={cn('flex items-center w-full')}>
{!compact && (
<div
className="flex items-center pr-2 shrink-0"
className={'flex flex-col shrink-0 pr-2 gap-2'}
style={{ width: '40%' }}
>
<div>
<Avatar
isActive={active}
seed={userNumericHash}
isAssist={isAssist}
/>
</div>
<div className="flex flex-col overflow-hidden color-gray-medium ml-3 justify-between items-center shrink-0">
<div
className={cn('text-lg', {
'color-teal cursor-pointer':
!disableUser && hasUserId && !props.isDisabled,
[stl.userName]:
!disableUser && hasUserId && !props.isDisabled,
'color-gray-medium': disableUser || !hasUserId,
})}
onClick={() =>
!disableUser && !hasUserFilter && hasUserId
? onUserClick(userId, userAnonymousId)
: null
}
>
<TextEllipsis
text={userDisplayName}
maxWidth="200px"
popupProps={{ inverted: true, size: 'tiny' }}
<div className="flex items-center pr-2 shrink-0">
<div>
<Avatar
width={'24px'}
height={'24px'}
iconSize={12}
isActive={active}
seed={userNumericHash}
isAssist={isAssist}
/>
</div>
<div className="overflow-hidden color-gray-medium ml-3 justify-between items-center shrink-0">
<div
className={cn('text-lg', {
'color-teal cursor-pointer':
!disableUser && hasUserId && !props.isDisabled,
[stl.userName]:
!disableUser && hasUserId && !props.isDisabled,
'color-gray-medium': disableUser || !hasUserId,
})}
onClick={() =>
!disableUser && !hasUserFilter && hasUserId
? onUserClick(userId, userAnonymousId)
: null
}
>
<TextEllipsis
text={userDisplayName}
maxWidth="200px"
popupProps={{ inverted: true, size: 'tiny' }}
/>
</div>
</div>
</div>
{_metaList.length > 0 && (
<SessionMetaList metaList={_metaList} />
)}
</div>
)}
<div
@ -264,11 +265,11 @@ function SessionItem(props: RouteComponentProps & Props) {
text={formatTimeOrDate(
startedAt,
shownTimezone === 'user' && userTimezone
? {
label: userTimezone.split('+').join(' +'),
value: userTimezone.split(':')[0],
}
: timezone
? {
label: userTimezone.split('+').join(' +'),
value: userTimezone.split(':')[0],
}
: timezone
)}
popupProps={{ inverted: true, size: 'tiny' }}
/>
@ -281,8 +282,8 @@ function SessionItem(props: RouteComponentProps & Props) {
<span className="mr-1">{eventsCount}</span>
<span>
{eventsCount === 0 || eventsCount > 1
? 'Events'
: 'Event'}
? 'Events'
: 'Event'}
</span>
</div>
<Icon name="circle-fill" size={3} className="mx-4" />
@ -292,8 +293,8 @@ function SessionItem(props: RouteComponentProps & Props) {
{live || props.live ? (
<Counter startTime={startedAt} />
) : (
formattedDuration
)}
formattedDuration
)}
</div>
</div>
</div>
@ -394,22 +395,19 @@ function SessionItem(props: RouteComponentProps & Props) {
</div>
</div>
) : (
<PlayLink
isAssist={isAssist}
sessionId={sessionId}
viewed={viewed}
onClick={onClick}
queryParams={queryParams}
query={query}
beforeOpen={props.live || isAssist ? undefined : populateData}
/>
)}
<PlayLink
isAssist={isAssist}
sessionId={sessionId}
viewed={viewed}
onClick={onClick}
queryParams={queryParams}
query={query}
beforeOpen={props.live || isAssist ? undefined : populateData}
/>
)}
</div>
</div>
</div>
{_metaList.length > 0 && (
<SessionMetaList className="mt-4" metaList={_metaList} />
)}
</div>
</Tooltip>
);