Skip to content

Commit

Permalink
Merge pull request #76 from maykinmedia/feature/#73-list-view
Browse files Browse the repository at this point in the history
💄 #73 - style: use @maykin-ui/admin-ui for list view styling
  • Loading branch information
svenvandescheur authored Mar 4, 2024
2 parents 9924fbf + 2cfba11 commit 5af2673
Show file tree
Hide file tree
Showing 7 changed files with 173 additions and 118 deletions.
8 changes: 4 additions & 4 deletions ui/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@jridgewell/sourcemap-codec": "^1.4.15",
"@maykin-ui/admin-ui": "^0.0.0",
"@maykin-ui/admin-ui": "^0.0.3",
"@mui/icons-material": "^5.15.0",
"@mui/material": "^5.15.0",
"@mui/styles": "^5.15.0",
Expand Down
61 changes: 49 additions & 12 deletions ui/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Routes, Route } from 'react-router-dom';
import { AuthProvider, RequireAuth, RequireNoAuth } from './components/Auth/Auth';
import { Routes, Route, useNavigate } from 'react-router-dom';
import { AuthProvider, RequireAuth, RequireNoAuth, useAuth } from './components/Auth/Auth';
import BaseView from './views/BaseView';
import LoginView from './views/LoginView';
import DashboardView from './views/DashboardView/DashboardView';
Expand All @@ -10,6 +10,36 @@ import ReportComplete from './components/Snackbar/Snackbar';
import { SnackbarProvider } from 'notistack';
import ZaaktypeView from './views/ZaaktypenView';
import { ConfigProvider } from './components/Config/Config';
import { A, Button, Navbar } from '@maykin-ui/admin-ui/components';
import { NavigationContext } from '@maykin-ui/admin-ui/contexts';
import { getSiteTree } from './utils/siteTree.tsx';

/**
* Create the navigation
*/
function Nav() {
const auth = useAuth();
const navigate = useNavigate();
const siteTreeOptions = getSiteTree(navigate, auth);

return (
<Navbar>
{siteTreeOptions.map(({ href, Icon, label, onClick }) => {
return href ? (
<A key={label} href={href} onClick={onClick}>
{Icon}
{label}
</A>
) : (
<Button key={label} onClick={onClick}>
{Icon}
{label}
</Button>
);
})}
</Navbar>
);
}

function App() {
return (
Expand All @@ -24,19 +54,26 @@ function App() {
}}
>
<AuthProvider>
<Routes>
<Route element={<RequireAuth />}>
<Route path="/" element={<BaseView />}>
<NavigationContext.Provider value={{ primaryNavigation: <Nav /> }}>
<Routes>
<Route element={<RequireAuth />}>
<Route path="/" element={<DashboardView />} />
<Route path="/zaaktypen" element={<DashboardView />} />
<Route path="/zaaktypen/:zaaktypeUuid" element={<ZaaktypeView />} />
<Route path="/zaaktypen/:zaaktypeUuid/wijzigen" element={<ZaaktypeEditView />} />

<Route path="/" element={<BaseView />}>
<Route path="/zaaktypen/:zaaktypeUuid" element={<ZaaktypeView />} />
<Route
path="/zaaktypen/:zaaktypeUuid/wijzigen"
element={<ZaaktypeEditView />}
/>
</Route>
</Route>

<Route element={<RequireNoAuth />}>
<Route path="/login" element={<LoginView />} />
</Route>
</Route>
<Route element={<RequireNoAuth />}>
<Route path="/login" element={<LoginView />} />
</Route>
</Routes>
</Routes>
</NavigationContext.Provider>
</AuthProvider>
</SnackbarProvider>
</ThemeProvider>
Expand Down
1 change: 1 addition & 0 deletions ui/src/types/types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,7 @@ export type GetSiteTreeFunction = (
export type SiteTreeOptions = SiteTreeOptionT[];

export interface SiteTreeOptionT {
href?: string;
label: string;
Icon?: JSX.Element;
onClick?: () => void;
Expand Down
22 changes: 17 additions & 5 deletions ui/src/utils/filter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,28 @@ export function onQueryFilter(query: string, array: string[]) {
}

/**
* Filter data by comparing an specified attribute value to the query.
* Filter data by comparing one or more attribute values to the query.
* @param query string
* @param data array of Zaaktype or InformationObject
* @param attribute attribute of Zaaktype or InformationObject
* @param attributes attribute or attributes of Zaaktype or InformationObject
* @returns filtered array
*/
export function attributeOnQueryFilter<T>(query: string, data: T[], attribute: keyof T) {
export function attributeOnQueryFilter<T>(
query: string,
data: T[],
attributes: keyof T | (keyof T)[]
) {
if (!data) return data;
return data.filter((dataItem) => {
let valueFromAttribute = dataItem[attribute];
if (typeof valueFromAttribute === 'string') return partStringCompare(valueFromAttribute, query);
// Array of attributes
if (Array.isArray(attributes))
return attributes.find((attribute) => {
const value = dataItem[attribute];
return partStringCompare(String(value), query);
});

// Single attribute
const value = dataItem[attributes];
return partStringCompare(String(value), query);
});
}
24 changes: 12 additions & 12 deletions ui/src/utils/siteTree.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,28 @@
import LogoutIcon from '@mui/icons-material/Logout';
import AppRegistrationIcon from '@mui/icons-material/AppRegistration';
import AttachFileIcon from '@mui/icons-material/AttachFile';
import AdminPanelSettingsIcon from '@mui/icons-material/AdminPanelSettings';
import { GetSiteTreeFunction } from '../types/types';
import { Outline } from '@maykin-ui/admin-ui/components';

export const getSiteTree: GetSiteTreeFunction = (navigate, auth) => [
{
label: 'Zaaktypen',
Icon: <AppRegistrationIcon />,
Icon: <Outline.CubeTransparentIcon />,
href: '/zaaktypen',
onClick: () => navigate('/zaaktypen'),
},
{
label: 'Documenttypen',
Icon: <AttachFileIcon />,
onClick: () => navigate('/documenttypen'),
},
// {
// label: 'Documenttypen',
// Icon: <Outline.PuzzlePieceIcon/>,
// href: '/documenttypen',
// onClick: () => navigate('/documenttypen'),
// },
{
label: 'Admin',
Icon: <AdminPanelSettingsIcon />,
Icon: <Outline.ShieldCheckIcon />,
href: '/admin',
onClick: () => navigate('/admin'),
},
{
label: 'Log uit',
Icon: <LogoutIcon />,
Icon: <Outline.ArrowRightOnRectangleIcon />,
onClick: () => {
auth.onSignOut();
},
Expand Down
173 changes: 89 additions & 84 deletions ui/src/views/DashboardView/DashboardView.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,32 @@
import { Alert, Box, Stack, Typography } from '@mui/material';
import { useEffect, useMemo, useState } from 'react';
import Search from '../../components/Search/Search';
import ToggleButton from '../../components/ToggleButton/ToggleButton';
import React, { useEffect, useMemo, useState } from 'react';
import { attributeOnQueryFilter } from '../../utils/filter';
import { Query, DataVisualLayout, ZaaktypeT, CatalogusT, SavedCatalogusT } from '../../types/types';
import { spacings } from '../../components/DesignSystem/DesignSystem';
import { Query, ZaaktypeT, CatalogusT, SavedCatalogusT } from '../../types/types';
import { useAsync } from 'react-use';
import { get } from '../../api/api';
import Select from '../../components/Select/Select';
import { uuidExtract } from '../../utils/extract';
import ErrorAlert from '../../components/Errors/ErrorAlert.tsx';
import GridAndStack from './GridAndStack.tsx';

const ARRAY_FOR_SKELETON = new Array(10).fill({}).map((_item, i) => ({ id: i }));
import {
Body,
Card,
ErrorMessage,
Form,
H1,
Outline,
Toolbar,
} from '@maykin-ui/admin-ui/components';
import { AttributeData, Attribute } from '@maykin-ui/admin-ui/lib';
import { List } from '@maykin-ui/admin-ui/templates';
import { useNavigate } from 'react-router-dom';

const DashboardView = () => {
const [selectedCatalogus, setSelectedCatalogus] = useState<SavedCatalogusT>('');
const fields = ['identificatie', 'omschrijving', 'beginGeldigheid', 'eindeGeldigheid'];

const navigate = useNavigate();

const [selectedCatalogus, setSelectedCatalogus] = useState<SavedCatalogusT>('all');
const [catalogussen, setCatalogussen] = useState<CatalogusT[]>([]);
const [zaaktypen, setZaaktypen] = useState<ZaaktypeT[]>([]);

const [query, setQuery] = useState<Query>('');
const [dataVisualLayout, setDataVisualLayout] = useState<DataVisualLayout>('blocks');

useEffect(() => {
if (selectedCatalogus) return;
Expand Down Expand Up @@ -54,86 +60,85 @@ const DashboardView = () => {
[catalogussen]
);

const filteredZaaktypen = attributeOnQueryFilter(query, zaaktypen, 'omschrijving');

const filteredZaaktypen = attributeOnQueryFilter(
query,
zaaktypen,
fields as (keyof ZaaktypeT)[]
).map((z) => {
const url = z.url ? `/zaaktypen/${uuidExtract(z.url)}` : undefined;
return {
absolute_url: url,
...z,
};
});
const onCatalogusChange = (event: any) => {
const catalogusUrl = event.target.value;
localStorage.setItem('catalogus', catalogusUrl);
setSelectedCatalogus(catalogusUrl);
};

const hasError = errorCatalogi || errorZaaktypen;
const isLoading = loadingZaaktypen || loadingCatalogi;
const showNumberOfZaaktypen = !errorZaaktypen && !loadingZaaktypen && catalogussen;

const selectOptions = [
{ label: 'Alle catalogussen', value: 'all' },
...(loadingCatalogi ? [] : catalogiOptions).map(([value, label]) => ({ value, label })),
];

return (
<Stack component={'article'} spacing={spacings.large} useFlexGap>
<Box width={'100%'}>
<Typography variant="h1">Dashboard</Typography>
</Box>
<Stack
width={'100%'}
direction={'row'}
flexWrap={'wrap'}
spacing={spacings.medium}
useFlexGap
position={'sticky'}
top={64}
zIndex={1}
sx={{
pb: 2,
backgroundColor: 'white',
borderBottom: '1px solid',
borderColor: 'divider',
}}
>
<Search label="Zoek naar zaaktypen" query={query} setQuery={setQuery} />
<ToggleButton
dataVisualLayout={dataVisualLayout}
setDataVisualLayout={setDataVisualLayout}
/>
<Select
sx={{
ml: { md: 'auto' },
}}
selectedValue={selectedCatalogus}
onChange={onCatalogusChange}
disabled={loadingCatalogi}
data={loadingCatalogi ? [] : catalogiOptions}
label="Selecteer catalogus"
defaultValue={{ label: 'Alle catalogussen', value: 'all' }}
optionSort="asc"
/>

<Box width={'100%'}>
<Typography variant={'h2'}>
Zaaktypen {showNumberOfZaaktypen && `(${filteredZaaktypen.length})`}
</Typography>
</Box>
</Stack>
{errorCatalogi && (
<ErrorAlert
title={errorCatalogi.message}
message="Er is een fout opgetreden bij het ophalen van de catalogi. Probeer het opnieuw."
/>
)}
{errorZaaktypen && (
<ErrorAlert
title={errorZaaktypen.message}
message="Er is een fout opgetreden bij het ophalen van de zaaktypen. Probeer het opnieuw."
/>
)}
{!hasError && !selectedCatalogus && (
<Alert severity="info">Selecteer eerst een catalogus</Alert>
)}
{!hasError && selectedCatalogus && (
<GridAndStack
loading={isLoading}
layout={dataVisualLayout}
data={isLoading ? ARRAY_FOR_SKELETON : filteredZaaktypen}
/>
)}
</Stack>
<List
fields={fields}
loading={isLoading}
results={filteredZaaktypen as unknown as AttributeData<Attribute>[]}
sort={true}
onClick={(_: React.MouseEvent, attributeData: AttributeData) => {
navigate('/zaaktypen/' + uuidExtract(attributeData.url as string));
}}
>
<Card>
<Body>
{errorCatalogi && (
<ErrorMessage>
Er is een fout opgetreden bij het ophalen van de catalogi. Probeer het opnieuw.
</ErrorMessage>
)}
{errorZaaktypen && (
<ErrorMessage>
Er is een fout opgetreden bij het ophalen van de zaaktypen. Probeer het opnieuw.
</ErrorMessage>
)}

<Toolbar align="space-between">
<H1>
Zaaktypen&nbsp;
{isLoading && <Outline.ArrowPathIcon spin={true} aria-label={'Bezig met laden'} />}
</H1>

<Form
autoComplete="off"
direction="horizontal"
showActions={false}
fields={[
{
disabled: loadingCatalogi,
defaultValue: 'all',
label: 'Selecteer catalogus',
options: selectOptions,
value: selectedCatalogus,
onChange: onCatalogusChange,
},
{
label: 'Filter resultaten',
placeholder: 'ZAAKTYPE-XXXX-XXXXXXXXXX',
size: 24,
value: query,
onChange: (e: React.ChangeEvent<HTMLInputElement>) => setQuery(e.target.value),
},
]}
/>
</Toolbar>
</Body>
</Card>
</List>
);
};

Expand Down

0 comments on commit 5af2673

Please sign in to comment.