-
Notifications
You must be signed in to change notification settings - Fork 66
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
(feat) Restore side navigation (#417)
- Loading branch information
1 parent
f5fe919
commit e4d6204
Showing
22 changed files
with
939 additions
and
241 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
import { BehaviorSubject } from 'rxjs'; | ||
import { type FormPage } from '../../types'; | ||
|
||
class PageObserver { | ||
private scrollablePagesSubject = new BehaviorSubject<Array<FormPage>>([]); | ||
private pagesWithErrorsSubject = new BehaviorSubject<Set<string>>(new Set()); | ||
private activePagesSubject = new BehaviorSubject<Set<string>>(new Set()); | ||
private evaluatedPagesVisibilitySubject = new BehaviorSubject<boolean>(null); | ||
|
||
setEvaluatedPagesVisibility(evaluatedPagesVisibility: boolean) { | ||
this.evaluatedPagesVisibilitySubject.next(evaluatedPagesVisibility); | ||
} | ||
|
||
updateScrollablePages(newPages: Array<FormPage>) { | ||
this.scrollablePagesSubject.next(newPages); | ||
} | ||
|
||
updatePagesWithErrors(newErrors: string[]) { | ||
this.pagesWithErrorsSubject.next(new Set(newErrors)); | ||
} | ||
|
||
addActivePage(pageId: string) { | ||
const currentActivePages = this.activePagesSubject.value; | ||
currentActivePages.add(pageId); | ||
this.activePagesSubject.next(currentActivePages); | ||
} | ||
|
||
removeInactivePage(pageId: string) { | ||
const currentActivePages = this.activePagesSubject.value; | ||
currentActivePages.delete(pageId); | ||
this.activePagesSubject.next(currentActivePages); | ||
} | ||
|
||
getActivePagesObservable() { | ||
return this.activePagesSubject.asObservable(); | ||
} | ||
|
||
getScrollablePagesObservable() { | ||
return this.scrollablePagesSubject.asObservable(); | ||
} | ||
|
||
getPagesWithErrorsObservable() { | ||
return this.pagesWithErrorsSubject.asObservable(); | ||
} | ||
|
||
getEvaluatedPagesVisibilityObservable() { | ||
return this.evaluatedPagesVisibilitySubject.asObservable(); | ||
} | ||
|
||
clear() { | ||
this.scrollablePagesSubject.next([]); | ||
this.pagesWithErrorsSubject.next(new Set()); | ||
this.activePagesSubject.next(new Set()); | ||
this.evaluatedPagesVisibilitySubject.next(false); | ||
} | ||
} | ||
|
||
export const pageObserver = new PageObserver(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,134 +1,108 @@ | ||
import React, { useCallback, useEffect, useMemo } from 'react'; | ||
import React from 'react'; | ||
import classNames from 'classnames'; | ||
import { useTranslation } from 'react-i18next'; | ||
import { Button, Toggle } from '@carbon/react'; | ||
import { isEmpty } from '../../validators/form-validator'; | ||
import { type FormPage } from '../../types'; | ||
import { Button } from '@carbon/react'; | ||
import { type FormPage, type SessionMode } from '../../types'; | ||
import styles from './sidebar.scss'; | ||
import { scrollIntoView } from '../../utils/form-helper'; | ||
import { usePageObserver } from './usePageObserver'; | ||
import { useCurrentActivePage } from './useCurrentActivePage'; | ||
import { isPageContentVisible } from '../../utils/form-helper'; | ||
import { InlineLoading } from '@carbon/react'; | ||
|
||
interface SidebarProps { | ||
allowUnspecifiedAll: boolean; | ||
defaultPage: string; | ||
handleClose: () => void; | ||
hideFormCollapseToggle: () => void; | ||
isFormSubmitting: boolean; | ||
mode: string; | ||
sessionMode: SessionMode; | ||
onCancel: () => void; | ||
pagesWithErrors: string[]; | ||
scrollablePages: Set<FormPage>; | ||
selectedPage: string; | ||
setValues: (values: unknown) => void; | ||
values: object; | ||
handleClose: () => void; | ||
hideFormCollapseToggle: () => void; | ||
} | ||
|
||
const Sidebar: React.FC<SidebarProps> = ({ | ||
allowUnspecifiedAll, | ||
defaultPage, | ||
handleClose, | ||
hideFormCollapseToggle, | ||
isFormSubmitting, | ||
mode, | ||
sessionMode, | ||
onCancel, | ||
pagesWithErrors, | ||
scrollablePages, | ||
selectedPage, | ||
setValues, | ||
values, | ||
handleClose, | ||
hideFormCollapseToggle, | ||
}) => { | ||
const { t } = useTranslation(); | ||
const pages: Array<FormPage> = Array.from(scrollablePages); | ||
|
||
useEffect(() => { | ||
if (defaultPage && pages.some(({ label, isHidden }) => label === defaultPage && !isHidden)) { | ||
scrollIntoView(joinWord(defaultPage)); | ||
} | ||
}, [defaultPage, scrollablePages]); | ||
|
||
const unspecifiedFields = useMemo( | ||
() => | ||
Object.keys(values).filter( | ||
(key) => key.endsWith('-unspecified') && isEmpty(values[key.split('-unspecified')[0]]), | ||
), | ||
[values], | ||
); | ||
|
||
const handleClick = (selected) => { | ||
const activeId = joinWord(selected); | ||
scrollIntoView(activeId); | ||
}; | ||
|
||
const markAllAsUnspecified = useCallback( | ||
(toggled) => { | ||
const updatedValues = { ...values }; | ||
unspecifiedFields.forEach((field) => { | ||
updatedValues[field] = toggled; | ||
}); | ||
setValues(updatedValues); | ||
}, | ||
[unspecifiedFields, values, setValues], | ||
); | ||
const { pages, pagesWithErrors, activePages, evaluatedPagesVisibility } = usePageObserver(); | ||
const { currentActivePage, requestPage } = useCurrentActivePage({ | ||
pages, | ||
defaultPage, | ||
activePages, | ||
evaluatedPagesVisibility, | ||
}); | ||
|
||
return ( | ||
<div className={styles.sidebar}> | ||
{pages.map((page, index) => { | ||
if (page.isHidden) return null; | ||
{pages | ||
.filter((page) => isPageContentVisible(page)) | ||
.map((page) => ( | ||
<PageLink | ||
key={page.id} | ||
page={page} | ||
currentActivePage={currentActivePage} | ||
pagesWithErrors={pagesWithErrors} | ||
requestPage={requestPage} | ||
/> | ||
))} | ||
{sessionMode !== 'view' && <hr className={styles.divider} />} | ||
|
||
const isCurrentlySelected = joinWord(page.label) === selectedPage; | ||
const hasError = pagesWithErrors.includes(page.label); | ||
|
||
return ( | ||
<div | ||
aria-hidden="true" | ||
className={classNames({ | ||
[styles.erroredSection]: isCurrentlySelected && hasError, | ||
[styles.activeSection]: isCurrentlySelected && !hasError, | ||
[styles.activeErroredSection]: !isCurrentlySelected && hasError, | ||
[styles.section]: !isCurrentlySelected && !hasError, | ||
})} | ||
key={index} | ||
onClick={() => handleClick(page.label)}> | ||
<div className={styles.sectionLink}>{page.label}</div> | ||
</div> | ||
); | ||
})} | ||
{mode !== 'view' && <hr className={styles.divider} />} | ||
<div className={styles.sidenavActions}> | ||
{allowUnspecifiedAll && mode !== 'view' && ( | ||
<div className={styles.toggleContainer}> | ||
<Toggle | ||
id="auto-unspecifier" | ||
labelA={t('unspecifyAll', 'Unspecify All')} | ||
labelB={t('revert', 'Revert')} | ||
labelText="" | ||
onToggle={markAllAsUnspecified} | ||
/> | ||
</div> | ||
)} | ||
{mode !== 'view' && ( | ||
<div className={styles.sideNavActions}> | ||
{sessionMode !== 'view' && ( | ||
<Button className={styles.saveButton} disabled={isFormSubmitting} type="submit"> | ||
{t('save', 'Save')} | ||
{isFormSubmitting ? ( | ||
<InlineLoading description={t('submitting', 'Submitting') + '...'} /> | ||
) : ( | ||
<span>{`${t('save', 'Save')}`}</span> | ||
)} | ||
</Button> | ||
)} | ||
<Button | ||
className={classNames(styles.saveButton, { | ||
[styles.topMargin]: mode === 'view', | ||
className={classNames(styles.closeButton, { | ||
[styles.topMargin]: sessionMode === 'view', | ||
})} | ||
kind="tertiary" | ||
onClick={() => { | ||
onCancel?.(); | ||
handleClose?.(); | ||
hideFormCollapseToggle(); | ||
}}> | ||
{mode === 'view' ? t('close', 'Close') : t('cancel', 'Cancel')} | ||
{sessionMode === 'view' ? t('close', 'Close') : t('cancel', 'Cancel')} | ||
</Button> | ||
</div> | ||
</div> | ||
); | ||
}; | ||
|
||
function joinWord(value) { | ||
return value.replace(/\s/g, ''); | ||
interface PageLinkProps { | ||
page: FormPage; | ||
currentActivePage: string; | ||
pagesWithErrors: string[]; | ||
requestPage: (page: string) => void; | ||
} | ||
|
||
function PageLink({ page, currentActivePage, pagesWithErrors, requestPage }: PageLinkProps) { | ||
const isActive = page.id === currentActivePage; | ||
const hasError = pagesWithErrors.includes(page.id); | ||
return ( | ||
<div | ||
className={classNames(styles.pageLink, { | ||
[styles.activePage]: isActive && !hasError, | ||
[styles.errorPage]: hasError && !isActive, | ||
[styles.activeErrorPage]: hasError && isActive, | ||
})}> | ||
<button | ||
onClick={(e) => { | ||
e.preventDefault(); | ||
requestPage(page.id); | ||
}}> | ||
<span>{page.label}</span> | ||
</button> | ||
</div> | ||
); | ||
} | ||
|
||
export default Sidebar; |
Oops, something went wrong.