Skip to content

Commit

Permalink
Add table of contents
Browse files Browse the repository at this point in the history
- Add tab view in question listing
- Group questions based on selected question groups
  • Loading branch information
subinasr committed Aug 18, 2023
1 parent 921e663 commit 5eb94ba
Show file tree
Hide file tree
Showing 9 changed files with 363 additions and 153 deletions.
22 changes: 17 additions & 5 deletions src/components/SortableList/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import React, { useState, memo, useMemo, useCallback } from 'react';
import React, {
useState,
memo,
useMemo,
useCallback,
} from 'react';
import {
Portal,
ListView,
Expand Down Expand Up @@ -67,6 +72,7 @@ interface SortableItemProps<
itemContainerParams?: ItemContainerParams;
}

// eslint-disable-next-line react-refresh/only-export-components
function SortableItem<
D,
P,
Expand Down Expand Up @@ -103,17 +109,20 @@ function SortableItem<
<div
ref={setNodeRef}
style={style}
// eslint-disable-next-line react/jsx-props-no-spreading
{...itemContainerParams ?? {}}
>
<Renderer
attributes={attributes}
listeners={listeners}
// eslint-disable-next-line react/jsx-props-no-spreading
{...rendererParams}
/>
</div>
);
}

// eslint-disable-next-line react-refresh/only-export-components
const MemoizedSortableItem = genericMemo(SortableItem);

export type Props<
Expand Down Expand Up @@ -142,6 +151,7 @@ export type Props<
errored?: boolean;
}

// eslint-disable-next-line react-refresh/only-export-components
function SortableList<
N extends string,
D,
Expand Down Expand Up @@ -183,16 +193,16 @@ function SortableList<

const handleDragStart = useCallback((event: DragStartEvent) => {
const { active } = event;
setActiveId(active.id);
setActiveId(active.id.toString());
}, []);

const handleDragEnd = useCallback((event: DragEndEvent) => {
const { active, over } = event;
setActiveId(undefined);

if (active.id && over?.id && active.id !== over?.id && items && onChange) {
const oldIndex = items.indexOf(active.id);
const newIndex = items.indexOf(over.id);
const oldIndex = items.indexOf(active.id.toString());
const newIndex = items.indexOf(over.id.toString());

const newItems = arrayMove(items, oldIndex, newIndex);
const dataMap = listToMap(
Expand Down Expand Up @@ -226,6 +236,7 @@ function SortableList<
}
return (
<Renderer
// eslint-disable-next-line react/jsx-props-no-spreading
{...params}
/>
);
Expand Down Expand Up @@ -299,7 +310,7 @@ function SortableList<
strategy={sortingStrategy}
>
<ListView
// eslint-disable-next-line jsx-props-no-spreading
// eslint-disable-next-line react/jsx-props-no-spreading
{...otherProps}
className={className}
data={data}
Expand All @@ -323,4 +334,5 @@ function SortableList<
);
}

// eslint-disable-next-line react-refresh/only-export-components
export default genericMemo(SortableList);
30 changes: 30 additions & 0 deletions src/components/TocList/index.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
.sortable-list {
display: flex;
flex-direction: column;

.group-item {
display: flex;
padding: var(--dui-spacing-small);

.header-icons {
display: flex;
align-items: center;
padding-right: var(--dui-spacing-extra-small);
}

.heading {
font-weight: var(--dui-font-weight-normal);
}

.content {
display: flex;
flex-direction: column;
padding-left: var(--dui-spacing-large);
gap: var(--dui-spacing-small);
}
}

&.nested-list {
padding-top: var(--dui-spacing-small);
}
}
192 changes: 158 additions & 34 deletions src/components/TocList/index.tsx
Original file line number Diff line number Diff line change
@@ -1,67 +1,191 @@
import { useState, useCallback } from 'react';
import React, { useCallback } from 'react';
import {
GrDrag,
} from 'react-icons/gr';
import {
isNotDefined,
_cs,
isDefined,
} from '@togglecorp/fujs';
import {
Container,
Checkbox,
QuickActionButton,
} from '@the-deep/deep-ui';
import SortableList, { Attributes, Listeners } from '#components/SortableList';
// import reorder from '#utils/common';
import {
QuestionnaireQuery,
} from '#generated/types';

import styles from './index.module.css';

interface QuestionGroup {
type QuestionGroup = NonNullable<NonNullable<NonNullable<NonNullable<QuestionnaireQuery['private']>['projectScope']>['groups']>['items']>[number];

interface TocProps {
id: string;
label: string;
name: string;
parentId: string;
questionnaireId: string;
relevant?: string;
item: QuestionGroup;
attributes?: Attributes;
listeners?: Listeners;
selectedGroups: string[];
orderedOptions: QuestionGroup[] | undefined;
onOrderedOptionsChange: React.Dispatch<React.SetStateAction<QuestionGroup[]>>;
onSelectedGroupsChange: React.Dispatch<React.SetStateAction<string[]>>;
onActiveTabChange: React.Dispatch<React.SetStateAction<string | undefined>>;
}

function reorder<T extends { order?: number }>(data: T[]) {
return data.map((v, i) => ({ ...v, order: i + 1 }));
function TocRenderer(props: TocProps) {
const {
id,
item,
orderedOptions,
selectedGroups,
onOrderedOptionsChange,
onSelectedGroupsChange,
onActiveTabChange,
attributes,
listeners,
} = props;

const handleGroupSelect = useCallback((val: boolean) => {
onSelectedGroupsChange((oldVal) => {
if (val) {
return ([...oldVal, id]);
}
const newVal = [...oldVal];
newVal.splice(oldVal.indexOf(id), 1);

return newVal;
});
onActiveTabChange((oldActiveTab) => {
if (isNotDefined(oldActiveTab) && val) {
return id;
}
if (!val && oldActiveTab === id) {
return undefined;
}
return oldActiveTab;
});
}, [
onActiveTabChange,
onSelectedGroupsChange,
id,
]);

const filteredOptions = orderedOptions?.filter((group) => id === group.parentId);

return (
<Container
className={styles.groupItem}
headerIcons={(
<div className={styles.headerIcons}>
<QuickActionButton
name={id}
// FIXME: use translation
title="Drag"
variant="transparent"
// eslint-disable-next-line react/jsx-props-no-spreading
{...attributes}
// eslint-disable-next-line react/jsx-props-no-spreading
{...listeners}
>
<GrDrag />
</QuickActionButton>
<Checkbox
name={item.id}
value={selectedGroups.includes(id)}
onChange={handleGroupSelect}
/>
</div>
)}
contentClassName={styles.content}
heading={item.label}
headingClassName={styles.heading}
headingSize="extraSmall"
spacing="none"
>
{isDefined(filteredOptions) && filteredOptions.length > 0 && (
// eslint-disable-next-line @typescript-eslint/no-use-before-define
<TocList
className={styles.nestedList}
parentId={item.id}
orderedOptions={orderedOptions}
onOrderedOptionsChange={onOrderedOptionsChange}
selectedGroups={selectedGroups}
onSelectedGroupsChange={onSelectedGroupsChange}
onActiveTabChange={onActiveTabChange}
/>
)}
</Container>
);
}

const keySelector = (g: QuestionGroup) => g.id;

interface Props<P> {
interface Props {
className?: string;
parentId: string | null;
options: QuestionGroup[];
renderer: (props: P & {
listeners?: Listeners;
attributes?: Attributes;
}) => JSX.Element;
rendererParams: P;
orderedOptions: QuestionGroup[] | undefined;
onOrderedOptionsChange: React.Dispatch<React.SetStateAction<QuestionGroup[]>>
selectedGroups: string[];
onSelectedGroupsChange: React.Dispatch<React.SetStateAction<string[]>>;
onActiveTabChange: React.Dispatch<React.SetStateAction<string | undefined>>;
}

function TocList<P>(props: Props<P>) {
function TocList(props: Props) {
const {
className,
parentId,
options,
renderer,
rendererParams,
orderedOptions,
onOrderedOptionsChange,
selectedGroups,
onSelectedGroupsChange,
onActiveTabChange,
} = props;

const filteredOptions = options?.filter(
const filteredOptions = orderedOptions?.filter(
(group: QuestionGroup) => group.parentId === parentId,
);

const [
orderedFilteredOptions,
setFilteredOrderedOptions,
] = useState<QuestionGroup[] | undefined>(filteredOptions);
const tocRendererParams = useCallback((key: string, datum: QuestionGroup): TocProps => ({
orderedOptions,
onOrderedOptionsChange,
onSelectedGroupsChange,
onActiveTabChange,
selectedGroups,
id: key,
item: datum,
}), [
orderedOptions,
selectedGroups,
onSelectedGroupsChange,
onOrderedOptionsChange,
onActiveTabChange,
]);

const handleGroupOrderChange = useCallback((...args: QuestionGroup[]) => {
setFilteredOrderedOptions(args);
}, []);
const handleGroupOrderChange = useCallback((oldValue: QuestionGroup[]) => {
const nonParentOptions = orderedOptions?.filter((group) => !oldValue.includes(group)) ?? [];
onOrderedOptionsChange([
...nonParentOptions,
...oldValue,
]);
}, [
onOrderedOptionsChange,
orderedOptions,
]);

return (
<SortableList
className={styles.sortableList}
className={_cs(styles.sortableList, className)}
direction="vertical"
name="toc"
onChange={handleGroupOrderChange}
data={orderedFilteredOptions}
data={filteredOptions}
keySelector={keySelector}
renderer={renderer}
rendererParams={rendererParams}
direction="vertical"
renderer={TocRenderer}
rendererParams={tocRendererParams}
emptyMessage="No groups found"
messageShown
messageIconShown
compactEmptyMessage
/>
);
Expand Down
2 changes: 1 addition & 1 deletion src/components/UserSelectInput/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ function UserSelectInput<K extends string, GK extends string>(props: UserSelectI

return (
<SearchSelectInput
// eslint-disable-next-line jsx-props-no-spreading
// eslint-disable-next-line react/jsx-props-no-spreading
{...otherProps}
className={className}
searchOptions={users?.items}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
.preview {
gap: var(--dui-spacing-large);
background-color: var(--dui-color-background);
border: none !important;

.question-item {
padding: var(--dui-spacing-extra-large);
Expand Down
Loading

0 comments on commit 5eb94ba

Please sign in to comment.