Merge pull request #7 from internet4refugees/feature/split-pane-map

clean up and split pane with map
This commit is contained in:
Johannes Lötzsch 2022-03-14 16:24:25 +01:00 committed by GitHub
commit c0c7da457c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 262 additions and 220 deletions

3
.gitignore vendored
View File

@ -1 +1,4 @@
**/data
.idea
*.iml

View File

@ -0,0 +1,12 @@
const groups = [
{ name: 'contactStatus', header: 'Contact Status' },
{ name: 'offerStatus', header: 'Offer Status' },
{ name: 'locationCoarse', header: 'Location' },
{ name: 'time', header: 'Time' },
{ name: 'features', header: 'Limitations / Features' },
{ name: 'animals', group: 'features', header: 'Animals' },
{ name: 'contact', header: 'Contact' },
{ name: 'address', group: 'contact', header: 'Address' },
]
export default groups

View File

@ -0,0 +1,140 @@
import {ColumnRaw} from "../util/datagrid/columnRaw";
const columnsRaw: ColumnRaw[] = [
{
"name": "rw_contacted",
"group": "contactStatus",
"header": "Asked",
"type": "boolean",
"editable": true,
"defaultWidth": 85
},
{
"name": "rw_contact_replied",
"group": "contactStatus",
"header": "Answered",
"type": "boolean",
"editable": true,
"defaultWidth": 110
},
{
"name": "rw_offer_occupied",
"group": "offerStatus",
"header": "Occupied",
"type": "boolean",
"editable": true,
"defaultWidth": 110
},
{
"name": "rw_note",
"header": "Our notes",
"type": "string",
"editable": true,
"defaultWidth": 400
},
{
"name": "place_country",
"group": "locationCoarse",
"header": "Country",
"type": "string",
"defaultWidth": 10
},
{
"name": "place_city",
"group": "locationCoarse",
"header": "City",
"type": "string"
},
{
"name": "beds",
"header": "Beds",
"type": "number"
},
{
"name": "time_from_str",
"group": "time",
"header": "From",
"type": "date",
"defaultWidth": 90
},
{
"name": "time_duration_str",
"group": "time",
"header": "Duration",
"type": "string"
},
{
"name": "languages",
"header": "Languages",
"type": "object",
"defaultWidth": 200
},
{
"name": "accessible",
"group": "features",
"header": "Accessible",
"type": "boolean",
"defaultWidth": 120
},
{
"name": "animals_allowed",
"group": "animals",
"header": "Allowed",
"type": "boolean",
"defaultWidth": 100
},
{
"name": "animals_present",
"group": "animals",
"header": "Present",
"type": "boolean",
"defaultWidth": 95
},
{
"name": "note",
"header": "User comment",
"type": "string",
"defaultWidth": 400
},
{
"name": "contact_name_full",
"group": "contact",
"header": "Name",
"type": "string"
},
{
"name": "contact_phone",
"group": "contact",
"header": "Phone",
"type": "string"
},
{
"name": "contact_email",
"group": "contact",
"header": "EMail",
"type": "string"
},
{
"name": "place_street",
"group": "address",
"header": "Street",
"type": "string"
},
{
"name": "place_street_number",
"group": "address",
"header": "Number",
"type": "string",
"defaultWidth": 100
},
{
"name": "place_zip",
"group": "address",
"header": "Zip",
"type": "string",
"defaultWidth": 80
},
]
export default columnsRaw

View File

@ -1,201 +1,42 @@
import React, {ReactNode, useCallback} from 'react'
import React, {ReactNode, useCallback, useEffect, useState} from 'react'
import {CheckBoxOutlineBlank, CheckBox} from '@mui/icons-material'
import '@inovua/reactdatagrid-community/index.css'
import DataGrid from '@inovua/reactdatagrid-community'
import filter from '@inovua/reactdatagrid-community/filter'
import {TypeColumn, TypeFilterValue, TypeSingleFilterValue} from "@inovua/reactdatagrid-community/types"
import DateFilter from '@inovua/reactdatagrid-community/DateFilter'
import StringFilter from '@inovua/reactdatagrid-community/StringFilter'
import BoolFilter from '@inovua/reactdatagrid-community/BoolFilter'
import NumberFilter from "@inovua/reactdatagrid-community/NumberFilter"
import BoolEditor from '@inovua/reactdatagrid-community/BoolEditor'
import {GetOffersQuery} from "../../codegen/generates"
import { GetOffersQuery, GetRwQuery} from "../../codegen/generates"
import moment from "moment"
import {useTranslation} from "react-i18next"
import {resources} from '../../i18n/config'
import {Box} from "@mui/material"
import { fetcher } from '../../codegen/fetcher'
import { useAuthStore, AuthState } from '../Login'
import defaultColumnRawDefinition from "../config/defaultColumnRawDefinition";
import defaultColumnGroups from "../config/defaultColumnGroups";
import { ColumnRaw } from '../util/datagrid/columnRaw'
import columnsRaw from "../config/defaultColumnRawDefinition";
import {transformValue} from "../util/tableValueMapper";
import {filterUndefOrNull} from "../util/notEmpty";
global.moment = moment
type HostOfferLookupTableProps = {
data_ro: GetOffersQuery,
data_rw: any, // TODO
export type HostOfferLookupTableDataType = GetOffersQuery["get_offers"] & GetRwQuery["get_rw"];
export type HostOfferLookupTableProps = {
data_ro?: GetOffersQuery["get_offers"],
data_rw?: GetRwQuery["get_rw"], // TODO
refetch_rw: any,
onFilteredDataChange?: (data: HostOfferLookupTableDataType[]) => void
}
interface ColumnRaw {
name: string;
header: string;
type: string;
editable: boolean;
defaultWidth: number;
group: string;
}
/**
* you can generate an inital raw column json by running the following
* function
*/
const makeColumnDefinition = (data: any) => Object.keys(data)
.map(k => ({
name: k,
header: k.replace(/_/g, ' '),
type: typeof data[k]
}))
const columnsRaw: Partial<ColumnRaw>[] = [
{
"name": "rw_contacted",
"group": "contactStatus",
"header": "Asked",
"type": "boolean",
"editable": true,
"defaultWidth": 85
},
{
"name": "rw_contact_replied",
"group": "contactStatus",
"header": "Answered",
"type": "boolean",
"editable": true,
"defaultWidth": 110
},
{
"name": "rw_offer_occupied",
"group": "offerStatus",
"header": "Occupied",
"type": "boolean",
"editable": true,
"defaultWidth": 110
},
{
"name": "rw_note",
"header": "Our notes",
"type": "string",
"editable": true,
"defaultWidth": 400
},
{
"name": "place_country",
"group": "locationCoarse",
"header": "Country",
"type": "string",
"defaultWidth": 10
},
{
"name": "place_city",
"group": "locationCoarse",
"header": "City",
"type": "string"
},
{
"name": "beds",
"header": "Beds",
"type": "number"
},
{
"name": "time_from_str",
"group": "time",
"header": "From",
"type": "date",
"defaultWidth": 90
},
{
"name": "time_duration_str",
"group": "time",
"header": "Duration",
"type": "string"
},
{
"name": "languages",
"header": "Languages",
"type": "object",
"defaultWidth": 200
},
{
"name": "accessible",
"group": "features",
"header": "Accessible",
"type": "boolean",
"defaultWidth": 120
},
{
"name": "animals_allowed",
"group": "animals",
"header": "Allowed",
"type": "boolean",
"defaultWidth": 100
},
{
"name": "animals_present",
"group": "animals",
"header": "Present",
"type": "boolean",
"defaultWidth": 95
},
{
"name": "note",
"header": "User comment",
"type": "string",
"defaultWidth": 400
},
{
"name": "contact_name_full",
"group": "contact",
"header": "Name",
"type": "string"
},
{
"name": "contact_phone",
"group": "contact",
"header": "Phone",
"type": "string"
},
{
"name": "contact_email",
"group": "contact",
"header": "EMail",
"type": "string"
},
{
"name": "place_street",
"group": "address",
"header": "Street",
"type": "string"
},
{
"name": "place_street_number",
"group": "address",
"header": "Number",
"type": "string",
"defaultWidth": 100
},
{
"name": "place_zip",
"group": "address",
"header": "Zip",
"type": "string",
"defaultWidth": 80
},
]
const groups = [
{ name: 'contactStatus', header: 'Contact Status' },
{ name: 'offerStatus', header: 'Offer Status' },
{ name: 'locationCoarse', header: 'Location' },
{ name: 'time', header: 'Time' },
{ name: 'features', header: 'Limitations / Features' },
{ name: 'animals', group: 'features', header: 'Animals' },
{ name: 'contact', header: 'Contact' },
{ name: 'address', group: 'contact', header: 'Address' },
]
const filterMappings = {
string: StringFilter,
boolean: BoolFilter,
@ -253,7 +94,7 @@ const findMatchingRenderer = (c: Partial<ColumnRaw>) => {
return customRenderer?.render
}
const columns: TypeColumn[] = columnsRaw
const columns: TypeColumn[] = defaultColumnRawDefinition
.map(c => ({
...c,
render: findMatchingRenderer(c) || undefined,
@ -286,8 +127,30 @@ async function mutate(auth: AuthState, onEditComplete: {value: string, columnId:
const rw_default = {rw_note: ''} // Required for filtering 'Not empty'. TODO: Should be fixed in StringFilter
const HostOfferLookupTable = ({data_ro, data_rw, refetch_rw}: HostOfferLookupTableProps) => {
const dataSource = !data_ro.get_offers ? [] : data_ro.get_offers.map( e_ro => ({...e_ro, ...(data_rw.find((e_rw: any) => e_rw.id === e_ro.id) || rw_default)}) )
const HostOfferLookupTable = ({data_ro, data_rw, refetch_rw, onFilteredDataChange}: HostOfferLookupTableProps) => {
const [dataSource, setDataSource] = useState<HostOfferLookupTableDataType[]>([]);
const [filteredData, setFilteredData] = useState<HostOfferLookupTableDataType[]>([]);
const [filterValue, setFilterValue] = useState(defaultFilterValue);
const filterValueChangeHandler = useCallback((_filterValue) => {
const data = filter(dataSource, filterValue) as HostOfferLookupTableDataType[]
setFilterValue(_filterValue);
setFilteredData(data)
onFilteredDataChange && onFilteredDataChange(data)
}, [dataSource])
useEffect(() => {
// @ts-ignore
const data = filterUndefOrNull( data_ro
?.map( e_ro => ({
...e_ro,
...((data_rw?.find((e_rw) => e_rw.id === e_ro.id) || rw_default))}) ) || [])
// @ts-ignore
data && setDataSource(data)
//setDataSource((/*data_rw?.get_rw || */ data_ro?.get_offers || []).map(v => transformValue(v, columnsRaw)))
}, [data_ro, data_rw]);
const auth = useAuthStore()
@ -301,15 +164,7 @@ const HostOfferLookupTable = ({data_ro, data_rw, refetch_rw}: HostOfferLookupTab
// @ts-ignore
const reactdatagridi18n = resources[language]?.translation?.reactdatagrid
return <Box sx={{
display: 'flex',
alignItems: 'stretch',
flexDirection: 'column',
height: '100%'}}>
<div
style={{flex: '1 1', height: '100%'}}>
<DataGrid
return <DataGrid
idProperty="id"
filterable
showColumnMenuFilterOptions={true}
@ -322,11 +177,9 @@ const HostOfferLookupTable = ({data_ro, data_rw, refetch_rw}: HostOfferLookupTab
dataSource={dataSource}
i18n={reactdatagridi18n || undefined}
style={{height: '100%'}}
onEditComplete={onEditComplete}
groups={groups}
onEditComplete={onEditComplete}
groups={defaultColumnGroups}
/>
</div>
</Box>
}
export default HostOfferLookupTable

View File

@ -1,13 +1,13 @@
import React from 'react'
import { useGetOffersQuery, useGetRwQuery } from "../../codegen/generates"
import HostOfferLookupTable from "./HostOfferLookupTable"
import HostOfferLookupTable, {HostOfferLookupTableProps} from "./HostOfferLookupTable"
import { Box } from "@mui/material"
import { useTranslation } from 'react-i18next'
import { Login, useAuthStore } from '../Login'
type HostLookupWrapperProps = Record<string, never>
type HostOfferLookupWrapperProps = Partial<HostOfferLookupTableProps>
const HostOfferLookupWrapper = ({}: HostLookupWrapperProps) => {
const HostOfferLookupWrapper = (props: HostOfferLookupWrapperProps) => {
const { t } = useTranslation()
const auth = useAuthStore()
@ -17,27 +17,28 @@ const HostOfferLookupWrapper = ({}: HostLookupWrapperProps) => {
const queryResult_rw = useGetRwQuery({auth}, {staleTime: staleTimeMinutes_rw * 60 * 1000})
return <>
<div style={{minHeight: '5vh', display: 'flex'}}> {/** TODO: a proper Header (css class) **/}
<Box sx={{
display: 'flex',
alignItems: 'stretch',
flexDirection: 'column',
height: '100%'}}>
<div>
{ (queryResult_ro.isFetching || queryResult_rw.isFetching) && t('loading…') }
{ (queryResult_ro.error || queryResult_rw.error) && t('An error occurred while trying to get data from the backend.') }
{ (queryResult_ro.data && !queryResult_ro.data.get_offers || queryResult_rw.data && !queryResult_rw.data.get_rw)
&& t('Seems like you have no permissions. Please try to login again.') }
&& t('Seems like you have no permissions. Please try to login again.') }
</div>
<Login/>
</div>
{ queryResult_ro.data?.get_offers && <Box sx={{
display: 'flex',
alignItems: 'stretch',
flexDirection: 'column',
height: '95vh'}}>
<div
{queryResult_ro.data && <div
style={{flex: '1 1', height: '100%'}}>
<HostOfferLookupTable data_ro={queryResult_ro.data}
data_rw={queryResult_rw.data?.get_rw||[]}
refetch_rw={queryResult_rw.refetch}/>
</div>
</Box> }
<HostOfferLookupTable
{...props}
data_ro={queryResult_ro.data.get_offers}
data_rw={queryResult_rw.data?.get_rw}
refetch_rw={queryResult_rw.refetch}
/>
</div>}
</Box>
</>
}

View File

@ -0,0 +1,18 @@
import {Array2StringTransformOptions} from "../tableValueMapper";
export type ColumnOptions = {
transform?: {
array2string?: Array2StringTransformOptions
}
}
export interface ColumnRaw {
name: string;
header: string;
type: string;
editable?: boolean;
defaultWidth?: number;
group?: string;
options?: ColumnOptions
}

View File

@ -0,0 +1,12 @@
/**
* you can generate an inital raw column json by running the following
* function
*/
const makeColumnDefinition = (data: any) => Object.keys(data)
.map(k => ({
name: k,
header: k.replace(/_/g, ' '),
type: typeof data[k]
}))
export default makeColumnDefinition

View File

@ -0,0 +1,8 @@
/**
* filter out nullish values
* https://stackoverflow.com/a/46700791/2726641
* @param value
*/
export default <TValue>(value: TValue | null | undefined): value is TValue => !(value === null || value === undefined)
export const filterUndefOrNull = <T>(ts?: (T | undefined | null)[] | null): T[] => ts?.filter((t: T | undefined | null): t is T => t !== undefined && t !== null) || []

View File

@ -1,23 +1,13 @@
import {ColumnRaw} from "./datagrid/columnRaw";
export type Array2StringTransformOptions = {
join?: string
}
export type ColumnOptions = {
transform?: {
array2string?: Array2StringTransformOptions
}
}
const array2string = (value: string[], options: Array2StringTransformOptions) => value.join(options.join || ',')
export type ColumnRaw = {
name: string;
header: string;
type: string;
editable?: boolean;
options?: ColumnOptions }
export const transformValue = <T>(values: T, columnsRaw: ColumnRaw[]) => {
export const transformValue: <T>(values: T, columnsRaw: ColumnRaw[]) => T = (values, columnsRaw) => {
const newValues = {...values}
columnsRaw
.forEach(c => {

View File

@ -3,6 +3,8 @@ import Head from 'next/head'
import HostOfferLookupWrapper from '../components/ngo/HostOfferLookupWrapper'
import styles from '../styles/Home.module.css'
import { useTranslation } from 'react-i18next'
import {SplitPane} from "react-collapse-pane";
import {LeafletMapWithoutSSR} from "../components/ngo/LeafletMapWithoutSSR";
const Home: NextPage = () => {
const { t } = useTranslation()
@ -15,7 +17,10 @@ const Home: NextPage = () => {
</Head>
<main className={styles.main}>
<HostOfferLookupWrapper />
<SplitPane split={"horizontal"}>
<LeafletMapWithoutSSR />
<HostOfferLookupWrapper />
</SplitPane>
</main>
</div>