parent
57fa871354
commit
9114c0fa03
|
@ -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))))
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}`
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue