Skip to content

Commit

Permalink
Merge pull request #310 from itenium-be/sorting-tables
Browse files Browse the repository at this point in the history
Sorting tables
  • Loading branch information
Laoujin authored Jan 28, 2025
2 parents 5a47875 + a2dfc3e commit b90e98a
Show file tree
Hide file tree
Showing 13 changed files with 145 additions and 10 deletions.
2 changes: 2 additions & 0 deletions frontend/src/components/client/models/getClientFeature.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const clientListConfig = (config: ClientFeatureBuilderConfig): IList<ClientModel
<span>{client.btw}</span>
</>
),
sort: (c1, c2) => c1.name.localeCompare(c2.name)
}, {
key: 'contact',
header: 'client.contact',
Expand All @@ -45,6 +46,7 @@ const clientListConfig = (config: ClientFeatureBuilderConfig): IList<ClientModel
<span>{client.telephone}</span>
</>
),
sort: (c1, c2) => c1.address.localeCompare(c2.address)
}, {
key: 'time-invested',
header: 'client.timeTitle',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
table.table-consultants {
td:nth-child(1) { width: 30%; }
td:nth-child(2) { min-width: 110px; }
td:nth-child(3) { min-width: 110px; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {EditIcon} from '../controls/Icon';
import {DeleteIcon} from '../controls/icons/DeleteIcon';
import {Claim} from '../users/models/UserModel';

import './ConsultantProjectsList.scss';

type ConsultantProject = {
consultant: ConsultantModel,
Expand Down Expand Up @@ -89,18 +90,22 @@ const consultantListConfig = (config: ConsultantFeatureBuilderConfig): IList<Con
header: 'project.consultant',
value: p => <ConsultantLinkWithModal consultant={p.consultant} showType />,
footer: (models: ConsultantProject[]) => <ConsultantCountFooter consultants={models.map(x => x.consultant)} />,
sort: (cp, cp1) => cp.consultant.firstName.localeCompare(cp1.consultant.firstName),
}, {
key: 'startDate',
header: 'project.startDate',
value: p => formatDate(p.project?.startDate),
sort: (cp, cp1) => (cp.project?.startDate?.valueOf() ?? 0) - (cp1.project?.startDate?.valueOf() ?? 0),
}, {
key: 'endDate',
header: 'project.endDate',
value: p => p.project?.endDate && formatDate(p.project?.endDate),
sort: (cp, cp1) => (cp.project?.endDate?.valueOf() ?? 0) - (cp1.project?.endDate?.valueOf() ?? 0),
}, {
key: 'client',
header: 'project.client.clientId',
value: p => <InvoiceClientCell client={p.client} />,
sort: (cp, cp1) => (cp.client?.name ?? '').localeCompare((cp1.client?.name ?? '')),
}, {
key: 'clientTariff',
header: 'project.client.tariff',
Expand Down Expand Up @@ -168,7 +173,6 @@ const consultantFeature = (config: ConsultantFeatureBuilderConfig): IFeature<Con
};



export const ConsultantProjectsList = () => {
useDocumentTitle('consultantList');

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,15 @@ function getRowClassName(m: ConsultantModel): string | undefined {
return undefined;
}


const consultantListConfig = (config: ConsultantFeatureBuilderConfig): IList<ConsultantModel, ConsultantListFilters> => {
const cells: IListCell<ConsultantModel>[] = [{
key: 'name',
value: m => <ConsultantLinkWithModal consultant={m} />,
sort: (c, c1) => c.firstName.localeCompare(c1.firstName)
}, {
key: 'type',
value: m => t(`consultant.types.${m.type}`),
sort: (c, c1) => c.type.localeCompare(c1.type)
}, {
key: 'email',
value: m => {
Expand All @@ -52,6 +53,7 @@ const consultantListConfig = (config: ConsultantFeatureBuilderConfig): IList<Con
}
return '';
},
sort: (c, c1) => c.email.localeCompare(c1.email)
}, {
key: 'telephone',
value: m => m.telephone,
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/components/controls/Icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,5 @@ export const EmailedIcon = ({...props}) => (
<Icon fa="fas fa-check fa-stack-2x" size={1} color="green" />
</Icon>
);

export const SortIcon = ({...props}: IconProps) => <Icon className="tst-sort" {...props} />;
12 changes: 11 additions & 1 deletion frontend/src/components/controls/table/List.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {IFeature} from '../feature/feature-models';
import {useSelector} from 'react-redux';
import {ConfacState} from '../../../reducers/app-state';
import { Pagination } from './Pagination';
import { SortDirections } from './table-models';
import { sortResult } from '../../utils';


type ListProps = {
Expand All @@ -27,7 +29,15 @@ export const List = ({feature}: ListProps) => {
}
}

if (feature.list.sorter) {
if(feature.list.filter?.state?.sort) {
const key = feature.list.filter?.state?.sort.columnName;
const cell = feature.list.rows.cells.find(col => col.key === key)
if(cell && cell.sort){
const asc = feature.list.filter?.state?.sort.direction === SortDirections.ASC;
data = data.slice().sort(sortResult(cell.sort, asc));
}
}
else if (feature.list.sorter) {
data = data.slice().sort(feature.list.sorter);
}

Expand Down
38 changes: 32 additions & 6 deletions frontend/src/components/controls/table/ListHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import {t} from '../../utils';
import {IFeature} from '../feature/feature-models';
import { useDispatch } from 'react-redux';
import { IFeature} from '../feature/feature-models';
import { ListHeaderCell } from './ListHeaderCell';
import { updateAppFilters } from '../../../actions';
import { ListFilters, SortDirections } from './table-models';


type ListHeaderProps<TModel> = {
feature: IFeature<TModel>;
feature: IFeature<TModel>
}


// eslint-disable-next-line arrow-body-style
export const ListHeader = ({feature}: ListHeaderProps<any>) => {
const dispatch = useDispatch();
return (
<thead>
<tr>
Expand All @@ -24,10 +28,32 @@ export const ListHeader = ({feature}: ListHeaderProps<any>) => {
width = col.header.width;
}

let handleSort : ((asc: boolean | undefined) => void) | undefined = undefined;
const filter = feature.list.filter?.state as ListFilters;
if(col.sort){
handleSort = (asc: boolean | undefined) => {
if(filter){
const newFilter = {
...filter,
sort: asc !== undefined ? {
direction: asc ? SortDirections.ASC : SortDirections.DESC,
columnName: col.key
} : undefined
}

dispatch(updateAppFilters(feature.key, newFilter))
}
}
}

return (
<th key={col.key} style={{width}}>
{header ? t(header) : <>&nbsp;</>}
</th>
<ListHeaderCell
key={col.key}
columnName={col.key}
width={width}
header={header}
filter={filter}
onSort={handleSort}/>
);
})}
</tr>
Expand Down
48 changes: 48 additions & 0 deletions frontend/src/components/controls/table/ListHeaderCell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import useHover from "../../hooks/useHover";
import { t } from "../../utils";
import { SortIcon } from "../Icon";
import { ListFilters, SortDirection, SortDirections } from "./table-models";

type ListHeaderCellProps = {
width: string | undefined | number
columnName: string,
header:string,
filter: ListFilters
onSort?: (asc: boolean | undefined) => void
}

export const ListHeaderCell = ({width, columnName, header, filter, onSort}: ListHeaderCellProps) => {
const [hovered, eventHandlers] = useHover();
//showing sort icon when hovering or having a direction and dealing with the same column
const showSortIcon = hovered || (filter?.sort?.direction !== undefined && filter?.sort?.columnName === columnName)
return (
<th style={{width}} {...eventHandlers}>
{header ? t(header) : <>&nbsp;</>}
{onSort && showSortIcon ? <SortIcon
fa={filter.sort?.columnName !== columnName || filter.sort?.direction === SortDirections.ASC ? "fa fa-arrow-up" : "fa fa-arrow-down"}
onClick={() => {
let isAsc :boolean | undefined;
//only change direction is we are dealing with same column
//otherwise always begin sorting ascending order
if(filter.sort?.columnName === columnName){
isAsc = switchDirection(filter.sort?.direction)
}else{
isAsc = true
}
onSort(isAsc);
}}
style={{marginLeft: "3px"}}
size={1}/> : <>&nbsp;</>}
</th>
)
}

const switchDirection = (direction: SortDirection) : boolean | undefined => {
if(direction === SortDirections.DESC){
return undefined;
}else if (direction === undefined){
return true;
}else {
return false;
}
}
13 changes: 13 additions & 0 deletions frontend/src/components/controls/table/table-models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,19 @@ export interface IList<TModel, TFilterModel extends ListFilters = {}, TTag = {}>
export type ListFilters = {
freeText?: string;
showInactive?: boolean;
sort?: {
direction: SortDirection
columnName: string
}
}

export const SortDirections = {
ASC: "asc",
DESC: "desc",
} as const;

export type SortDirection = typeof SortDirections[keyof typeof SortDirections];

export type ProjectListFilters = ListFilters;
export type ProjectMonthListFilters = ListFilters & {
/** Format: {YYYY-MM: true} */
Expand Down Expand Up @@ -86,6 +97,8 @@ export interface IListCell<TModel> {
// BUG: GroupedInvoiceTable footer is wrong
/** Will span until next cell with a footer */
footer?: string | ((models: TModel[]) => string | React.ReactNode);

sort?: (a: TModel, b: TModel) => number
}


Expand Down
14 changes: 14 additions & 0 deletions frontend/src/components/hooks/useHover.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { useState, useMemo } from 'react';

const useHover = () : [boolean, { onMouseOver(): void, onMouseOut(): void}] => {
const [hovered, setHovered] = useState<boolean>(false);

const eventHandlers = useMemo(() => ({
onMouseOver() { setHovered(true); },
onMouseOut() { setHovered(false); }
}), []);

return [hovered, eventHandlers];
}

export default useHover;
1 change: 0 additions & 1 deletion frontend/src/components/project/ProjectsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ export const ProjectsList = () => {
</>
);


const feature = projectFeature(config);
return <ListPage feature={feature} topToolbar={TopToolbar} />;
};
6 changes: 6 additions & 0 deletions frontend/src/components/project/models/getProjectFeature.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -115,19 +115,23 @@ const projectListConfig = (config: ProjectFeatureBuilderConfig): IList<FullProje
header: 'project.consultant',
value: p => <ConsultantLinkWithModal consultant={p.consultant} showType />,
footer: (models: FullProjectModel[]) => <ConsultantCountFooter consultants={models.map(x => x.consultant)} />,
sort: (p, p2) => p.consultantName.localeCompare(p2.consultantName)
}, {
key: 'startDate',
header: 'project.startDate',
value: p => formatDate(p.details.startDate),
sort: (p, p2) => p.details.startDate.valueOf() - p2.details.startDate.valueOf()
}, {
key: 'endDate',
header: 'project.endDate',
value: p => p.details.endDate && formatDate(p.details.endDate),
sort: (p, p2) => (p.details.endDate?.valueOf() ?? 0) - (p2.details.endDate?.valueOf() ?? 0)
}, {
key: 'partner',
header: 'project.partner.clientId',
value: p => <InvoiceClientCell client={p.partner} />,
footer: (models: FullProjectModel[]) => <ProjectForecastPartnerFooter models={models} />,
sort: (p, p2) => (p.partner?.name ?? '').localeCompare(p2.partner?.name ?? '')
}, {
key: 'partnerTariff',
header: 'project.partner.tariff',
Expand All @@ -137,10 +141,12 @@ const projectListConfig = (config: ProjectFeatureBuilderConfig): IList<FullProje
header: '',
value: p => <ProjectEndCustomerIcon endCustomer={p.details.endCustomer} endCustomerClientModel={p.endCustomer}/>,
footer: (models: FullProjectModel[]) => <ProjectClientForecastFooter models={models} />,
sort: (p, p2) => (p.endCustomer?.name ?? '').localeCompare(p2.endCustomer?.name ?? '')
}, {
key: 'client',
header: 'project.client.clientId',
value: p => <InvoiceClientCell client={p.client} />,
sort:(p, p2) => (p.client?.name ?? '').localeCompare(p2.client?.name ?? '')
}, {
key: 'clientTariff',
header: 'project.client.tariff',
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/components/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,8 @@ export const searchinize = (str: string): string => {
return latinize(str).trim().toLowerCase();
};

export const sortResult = (sorter: (a: any, b: any) => number, asc: boolean): (a: any, b:any) => number => {
return (a, b) => asc ? sorter(a, b) : sorter(b, a);
}

export {default as t} from '../trans';

0 comments on commit b90e98a

Please sign in to comment.