additional writable columns

+ stacked columns
This commit is contained in:
Johannes Lötzsch 2022-03-13 23:37:29 +01:00
parent 57fa871354
commit 9114c0fa03
7 changed files with 182 additions and 54 deletions

View File

@ -2,11 +2,24 @@
(:require [clojure.spec.alpha :as s]
[specialist-server.type :as t]))
(s/def :xt/id t/string)
(s/def ::rw_contacted (s/nilable t/string))
(s/def ::rw_contact_replied (s/nilable t/string))
(s/def ::rw_offer_occupied (s/nilable t/string))
(s/def ::rw_note (s/nilable t/string))
(s/def ::record (s/keys :req-un [::rw_note]))
(s/def ::record (s/keys :opt-un [:xt/id ::rw_contacted ::rw_contact_replied ::rw_offer_occupied ::rw_note]))
(defn add_missing
"TODO: provide this as a reusable feature of specialist-server"
[record]
(-> record
(update :rw_contacted identity)
(update :rw_contact_replied identity)
(update :rw_offer_occupied identity)
(update :rw_note identity)))
(defn db->graphql [record]
(some-> record
;(select-keys [:xt/id :rw_note])
(add_missing)
(assoc :id (:xt/id record))))

View File

@ -4,9 +4,11 @@
(s/def ::rowId t/string)
(s/def ::columnId t/string)
(s/def ::value t/string)
(s/def ::value_string (s/nilable t/string))
(s/def ::value_boolean (s/nilable t/boolean))
(t/defobject OnEditComplete {:name "OnEditComplete" :kind t/input-object-kind :description "https://reactdatagrid.io/docs/api-reference#props-onEditComplete"}
:req-un [::rowId ::columnId ::value])
(t/defobject OnEditCompleteByType {:name "OnEditCompleteByType" :kind t/input-object-kind :description "https://reactdatagrid.io/docs/api-reference#props-onEditComplete"}
:req-un [::rowId ::columnId
::value_string ::value_boolean])
(s/def ::onEditComplete OnEditComplete)
(s/def ::onEditCompleteByType OnEditCompleteByType)

View File

@ -7,14 +7,9 @@
[beherbergung.model.offer-rw :as offer-rw :refer [db->graphql]]
[clojure.edn]))
(s/def :xt/id t/string)
(s/def ::rw_note (s/nilable t/string))
(s/def ::rw (s/keys :req-un [:xt/id
::rw_note]))
(s/fdef get_rw
:args (s/tuple map? (s/keys :req-un [::auth/auth]) map? map?)
:ret (s/nilable (s/* ::rw)))
:ret (s/nilable (s/* ::offer-rw/record)))
(defn get_rw
[_node opt ctx _info]

View File

@ -9,19 +9,27 @@
[clojure.edn]))
(s/fdef write_rw
:args (s/tuple map? (s/keys :req-un [::auth/auth ::oneditcomplete/onEditComplete]) map? map?)
:args (s/tuple map? (s/keys :req-un [::auth/auth ::oneditcomplete/onEditCompleteByType]) map? map?)
:ret (s/nilable t/boolean))
(defn write_rw
[_node opt ctx _info]
(let [{:keys [tx_sync tx-committed?]} (:db_ctx ctx)
[ngo:id] (auth+role->entity ctx (:auth opt) ::ngo/record)]
(boolean (when ngo:id
(let [{:keys [value columnId rowId]} (:onEditComplete opt)
t (tx_sync [;; TODO use :xtdb.api/match to verify we update the latest version instead of last write wins
[:xtdb.api/put {:xt/id rowId
:xt/spec ::offer-rw/record
(keyword columnId) value}]])]
(tx-committed? t))))))
(let [{:keys [tx-fn-put tx-fn-call sync]} (:db_ctx ctx)
[ngo:id] (auth+role->entity ctx (:auth opt) ::ngo/record)
tx_result (when ngo:id
(let [{:keys [rowId columnId
value_boolean value_string]} (:onEditCompleteByType opt)
value (or value_boolean value_string)
doc {(keyword columnId) value}]
(tx-fn-put :write_rw
'(fn [ctx eid doc]
(let [db (xtdb.api/db ctx)
entity (xtdb.api/entity db eid)]
[[:xtdb.api/put (assoc (merge entity doc)
:xt/id eid
:xt/spec ::offer-rw/record)]])))
(tx-fn-call :write_rw rowId doc)))]
(sync)
(boolean (:xtdb.api/tx-id tx_result))))
(s/def ::get_offers (t/resolver #'write_rw))

View File

@ -26,6 +26,29 @@ export type Auth = {
password: Scalars['String'];
};
/** If this server supports mutation, the type that mutation operations will be rooted at. */
export type MutationType = {
__typename?: 'MutationType';
write_rw?: Maybe<Scalars['Boolean']>;
};
/** If this server supports mutation, the type that mutation operations will be rooted at. */
export type MutationTypeWrite_RwArgs = {
auth: Auth;
onEditComplete: OnEditComplete;
};
/** https://reactdatagrid.io/docs/api-reference#props-onEditComplete */
export type OnEditComplete = {
/** Self descriptive. */
columnId: Scalars['String'];
/** Self descriptive. */
rowId: Scalars['String'];
/** Self descriptive. */
value: Scalars['String'];
};
/** The type that query operations will be rooted at. */
export type QueryType = {
__typename?: 'QueryType';
@ -95,8 +118,12 @@ export type Get_Offers = {
export type Get_Rw = {
__typename?: 'get_rw';
id?: Maybe<Scalars['String']>;
/** Self descriptive. */
id: Scalars['String'];
rw_contact_replied?: Maybe<Scalars['String']>;
rw_contacted?: Maybe<Scalars['String']>;
rw_note?: Maybe<Scalars['String']>;
rw_offer_occupied?: Maybe<Scalars['String']>;
};
/** For a username+password get a jwt containing the login:id */
@ -124,7 +151,7 @@ export type GetRwQueryVariables = Exact<{
}>;
export type GetRwQuery = { __typename?: 'QueryType', get_rw?: Array<{ __typename?: 'get_rw', id?: string | null, rw_note?: string | null }> | null };
export type GetRwQuery = { __typename?: 'QueryType', get_rw?: Array<{ __typename?: 'get_rw', id: string, rw_contacted?: string | null, rw_contact_replied?: string | null, rw_offer_occupied?: string | null, rw_note?: string | null }> | null };
export const LoginDocument = `
@ -185,6 +212,9 @@ export const GetRwDocument = `
query GetRw($auth: Auth!) {
get_rw(auth: $auth) {
id
rw_contacted
rw_contact_replied
rw_offer_occupied
rw_note
}
}

View File

@ -32,6 +32,9 @@ export const get_rw = gql`
query GetRw($auth: Auth!) {
get_rw(auth: $auth) {
id
rw_contacted
rw_contact_replied
rw_offer_occupied
rw_note
}
}`

View File

@ -1,21 +1,22 @@
import React, {ReactNode, useCallback} from 'react'
import {CheckBoxOutlineBlank, CheckBox} from '@mui/icons-material';
import {CheckBoxOutlineBlank, CheckBox} from '@mui/icons-material'
import '@inovua/reactdatagrid-community/index.css'
import DataGrid from '@inovua/reactdatagrid-community'
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 {GetOffersQuery} from "../../codegen/generates";
import {TypeColumn, TypeFilterValue, TypeSingleFilterValue} from "@inovua/reactdatagrid-community/types";
import NumberFilter from "@inovua/reactdatagrid-community/NumberFilter";
import moment from "moment";
import NumberFilter from "@inovua/reactdatagrid-community/NumberFilter"
import BoolEditor from '@inovua/reactdatagrid-community/BoolEditor'
import {GetOffersQuery} from "../../codegen/generates"
import moment from "moment"
import {useTranslation} from "react-i18next";
import {resources} from '../../i18n/config';
import {Box} from "@mui/material";
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'
@ -34,6 +35,7 @@ interface ColumnRaw {
type: string;
editable: boolean;
defaultWidth: number;
group: string;
}
/**
@ -48,14 +50,47 @@ const makeColumnDefinition = (data: any) => Object.keys(data)
}))
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"
},
@ -66,45 +101,43 @@ const columnsRaw: Partial<ColumnRaw>[] = [
},
{
"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",
"header": "Languages",
"type": "object",
"defaultWidth": 200
},
{
"name": "accessible",
"header": "accessible",
"group": "features",
"header": "Accessible",
"type": "boolean",
"defaultWidth": 80
"defaultWidth": 120
},
{
"name": "animals_allowed",
"header": "allows animals",
"group": "animals",
"header": "Allowed",
"type": "boolean",
"defaultWidth": 80
"defaultWidth": 100
},
{
"name": "animals_present",
"header": "has animals",
"group": "animals",
"header": "Present",
"type": "boolean",
"defaultWidth": 80
},
{
"name": "rw_note",
"header": "Our notes",
"type": "string",
"editable": true,
"defaultWidth": 400
"defaultWidth": 95
},
{
"name": "note",
@ -114,44 +147,67 @@ const columnsRaw: Partial<ColumnRaw>[] = [
},
{
"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",
"header": "Street number",
"group": "address",
"header": "Number",
"type": "string",
"defaultWidth": 80
"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,
number: NumberFilter,
date: DateFilter,
}
const editorMappings = {
string: null,
boolean: BoolEditor,
number: null,
date: null
}
const operatorsForType = {
number: 'gte',
string: 'contains',
@ -164,6 +220,16 @@ type CustomRendererMatcher = {
render: (...args: any[]) => ReactNode
}
function Email({value}: {value: string}) {
const href = `mailto:${value}`
return <a href={href}>{value}</a>
}
function Phone({value}: {value: string}) {
const href = `tel:${value}`
return <a href={href}>{value}</a>
}
const customRendererForType: CustomRendererMatcher[] = [
{
match: {type: 'boolean'},
@ -171,7 +237,11 @@ const customRendererForType: CustomRendererMatcher[] = [
},
{
match: {type: 'string', name: 'contact_email'},
render: ({value}) => (<a href={`mailto:${value}`}>{value}</a>)
render: Email
},
{
match: {type: 'string', name: 'contact_phone'},
render: Phone
}
]
@ -187,7 +257,8 @@ const columns: TypeColumn[] = columnsRaw
.map(c => ({
...c,
render: findMatchingRenderer(c) || undefined,
filterEditor: filterMappings[c.type as 'string' | 'number' | 'boolean' | 'date']
filterEditor: filterMappings[c.type as 'string' | 'number' | 'boolean' | 'date'],
editor: editorMappings[c.type as 'string' | 'number' | 'boolean' | 'date']
}))
const defaultFilterValue: TypeFilterValue = columns
@ -202,9 +273,14 @@ const defaultFilterValue: TypeFilterValue = columns
})
async function mutate(auth: AuthState, onEditComplete: {value: string, columnId: string, rowId: string}) {
const result = await fetcher<any, any>(`mutation WriteRW($auth: Auth!, $onEditComplete: Boolean) {
write_rw(auth: $auth, onEditComplete: $onEditComplete) }`,
{auth, onEditComplete})()
const type = typeof(onEditComplete.value)
const onEditCompleteByType = {rowId: onEditComplete.rowId,
columnId: onEditComplete.columnId,
value_boolean: type === 'boolean' && onEditComplete.value || null,
value_string: type === 'string' && onEditComplete.value || null}
const result = await fetcher<any, any>(`mutation WriteRW($auth: Auth!, $onEditCompleteByType: Boolean) {
write_rw(auth: $auth, onEditCompleteByType: $onEditCompleteByType) }`,
{auth, onEditCompleteByType})()
return result?.write_rw
}
@ -216,7 +292,7 @@ const HostOfferLookupTable = ({data_ro, data_rw, refetch_rw}: HostOfferLookupTab
const onEditComplete = useCallback(async ({value, columnId, rowId}) => {
/** For now the easiest way to ensure the user can see if data was updated in the db is by calling `refetch_rw()`
TODO: error handling **/
(value || value==='') && await mutate(auth, {value, columnId, rowId}) && refetch_rw()
await mutate(auth, {value, columnId, rowId}) && refetch_rw()
}, [dataSource])
const {i18n: {language}} = useTranslation()
@ -245,6 +321,7 @@ const HostOfferLookupTable = ({data_ro, data_rw, refetch_rw}: HostOfferLookupTab
i18n={reactdatagridi18n || undefined}
style={{height: '100%'}}
onEditComplete={onEditComplete}
groups={groups}
/>
</div>
</Box>