From 5cf3cd5b9077fdd9fb99beee23086c0cac4d1e16 Mon Sep 17 00:00:00 2001 From: Josh Balfour Date: Thu, 28 Jul 2022 15:18:19 +0100 Subject: [PATCH 01/10] fix developername not showing --- packages/app-builder-backend/src/ddb.ts | 3 ++- packages/app-builder-backend/src/resolvers/app-resolver.ts | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/app-builder-backend/src/ddb.ts b/packages/app-builder-backend/src/ddb.ts index 302591bbca..9b2081b39a 100644 --- a/packages/app-builder-backend/src/ddb.ts +++ b/packages/app-builder-backend/src/ddb.ts @@ -66,7 +66,7 @@ export const ensureTables = async () => { export type DDBApp = Omit const ddbItemToApp = (item: { [key: string]: AttributeValue }): DDBApp => { - const { id, createdAt, updatedAt, pages, subdomain, navConfig, customEntities, clientId } = item + const { id, createdAt, updatedAt, pages, subdomain, navConfig, customEntities, clientId, developerName } = item return { id: id?.S as string, @@ -77,6 +77,7 @@ const ddbItemToApp = (item: { [key: string]: AttributeValue }): DDBApp => { pages: (pages?.S && (JSON.parse(pages.S as string) as Array)) || [], customEntities: (customEntities?.S && (JSON.parse(customEntities.S as string) as Array)) || [], navConfig: (navConfig?.S && (JSON.parse(navConfig.S as string) as Array)) || [], + developerName: developerName?.S as string | undefined, } } diff --git a/packages/app-builder-backend/src/resolvers/app-resolver.ts b/packages/app-builder-backend/src/resolvers/app-resolver.ts index ee90300ca0..8bd8fb3870 100644 --- a/packages/app-builder-backend/src/resolvers/app-resolver.ts +++ b/packages/app-builder-backend/src/resolvers/app-resolver.ts @@ -274,14 +274,14 @@ export class AppResolver { await updateApp({ ...app, clientId: externalId as string, - developerName: developer as string, + developerName: developer, }) return { ...app, name: name as string, clientId: externalId as string, - developerName: developer as string, + developerName: developer, } } From 4705c7b7bd56bdfe1a9203e690a29feffca2a64a Mon Sep 17 00:00:00 2001 From: Josh Balfour Date: Thu, 28 Jul 2022 15:22:04 +0100 Subject: [PATCH 02/10] filter out non-top-level entities from typelist --- packages/app-builder-backend/src/entities/appointments.ts | 2 +- packages/app-builder-backend/src/entities/company.ts | 2 +- packages/app-builder-backend/src/entities/department.ts | 2 +- packages/app-builder-backend/src/entities/property-image.ts | 2 +- .../app-builder/src/components/hooks/objects/use-type-list.ts | 2 +- .../components/hooks/use-introspection/parse-introspection.ts | 2 ++ 6 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/app-builder-backend/src/entities/appointments.ts b/packages/app-builder-backend/src/entities/appointments.ts index 41d0205d4c..023d987382 100644 --- a/packages/app-builder-backend/src/entities/appointments.ts +++ b/packages/app-builder-backend/src/entities/appointments.ts @@ -5,7 +5,7 @@ import { Negotiator, NegotiatorFragment } from './negotiator' import { Office, OfficeFragment } from './office' import { Property, PropertyFragment } from './property' -@ObjectType({ description: '@labelKeys(value)' }) +@ObjectType({ description: '@labelKeys(value) @notTopLevel()' }) export class AppointmentType { @Field(() => ID) id: string diff --git a/packages/app-builder-backend/src/entities/company.ts b/packages/app-builder-backend/src/entities/company.ts index 5ad8fc8d68..926c23b389 100644 --- a/packages/app-builder-backend/src/entities/company.ts +++ b/packages/app-builder-backend/src/entities/company.ts @@ -3,7 +3,7 @@ import { gql } from 'apollo-server-core' import { Field, GraphQLISODateTime, ID, InputType, ObjectType } from 'type-graphql' import { ContactAddressType } from './contact' -@ObjectType({ description: '@labelKeys(value)' }) +@ObjectType({ description: '@labelKeys(value) @notTopLevel()' }) export class CompanyType { @Field(() => ID) id: string diff --git a/packages/app-builder-backend/src/entities/department.ts b/packages/app-builder-backend/src/entities/department.ts index c60b1f6bef..87488b81f2 100644 --- a/packages/app-builder-backend/src/entities/department.ts +++ b/packages/app-builder-backend/src/entities/department.ts @@ -16,7 +16,7 @@ export const DepartmentFragment = gql` } ` -@ObjectType({ description: '@labelKeys(name)' }) +@ObjectType({ description: '@labelKeys(name) @notTopLevel()' }) export class Department { @Field(() => ID) id: string diff --git a/packages/app-builder-backend/src/entities/property-image.ts b/packages/app-builder-backend/src/entities/property-image.ts index 7619961f4b..9bff66555f 100644 --- a/packages/app-builder-backend/src/entities/property-image.ts +++ b/packages/app-builder-backend/src/entities/property-image.ts @@ -13,7 +13,7 @@ export const PropertyImageFragment = gql` } ` -@ObjectType() +@ObjectType({ description: '@notTopLevel()' }) export class PropertyImage { @Field() id: string diff --git a/packages/app-builder/src/components/hooks/objects/use-type-list.ts b/packages/app-builder/src/components/hooks/objects/use-type-list.ts index 6cfe3c211e..7b68efa6b8 100644 --- a/packages/app-builder/src/components/hooks/objects/use-type-list.ts +++ b/packages/app-builder/src/components/hooks/objects/use-type-list.ts @@ -2,7 +2,7 @@ import { useIntrospection } from '../use-introspection' export const useTypeList = () => { const { data: introspectionResult, error, loading } = useIntrospection() - const data = introspectionResult?.map(({ object }) => object.name) + const data = introspectionResult?.filter(({ notTopLevel }) => !notTopLevel).map(({ object }) => object.name) return { data, diff --git a/packages/app-builder/src/components/hooks/use-introspection/parse-introspection.ts b/packages/app-builder/src/components/hooks/use-introspection/parse-introspection.ts index 3391018091..15744c929b 100644 --- a/packages/app-builder/src/components/hooks/use-introspection/parse-introspection.ts +++ b/packages/app-builder/src/components/hooks/use-introspection/parse-introspection.ts @@ -25,6 +25,7 @@ export type IntrospectionResult = { update?: GeneratedMutation delete?: GeneratedMutation specials: GeneratedSpecial[] + notTopLevel?: boolean } export const parseIntrospectionResult = (introspection: IntrospectionQuery): IntrospectionResult[] | undefined => { @@ -77,6 +78,7 @@ export const parseIntrospectionResult = (introspection: IntrospectionQuery): Int ...object, fields: object.fields.filter(({ name }) => !name.startsWith('_placeholder')), }, + notTopLevel: object.description?.includes('@notTopLevel') ?? false, labelKeys, acKeyField, supportsCustomFields: !!object.description?.includes('@supportsCustomFields()'), From 7e52957a374941b1891d8dbcc01ee3e963562e9e Mon Sep 17 00:00:00 2001 From: Josh Balfour Date: Fri, 29 Jul 2022 16:39:58 +0100 Subject: [PATCH 03/10] appointment update & fixing stuff up --- .../src/entities/appointments.ts | 19 +-- .../src/resolvers/app-resolver.ts | 99 ++++++--------- .../src/resolvers/appointment-resolver.ts | 37 +++--- .../hooks/objects/use-object-list.ts | 14 ++- .../ui/user/ejectable/form-input.tsx | 86 +++++++++---- .../src/components/ui/user/ejectable/form.tsx | 6 +- .../components/ui/user/ejectable/table.tsx | 117 ++++++++++++++++-- 7 files changed, 249 insertions(+), 129 deletions(-) diff --git a/packages/app-builder-backend/src/entities/appointments.ts b/packages/app-builder-backend/src/entities/appointments.ts index 023d987382..5d23350243 100644 --- a/packages/app-builder-backend/src/entities/appointments.ts +++ b/packages/app-builder-backend/src/entities/appointments.ts @@ -1,8 +1,8 @@ import { gql } from 'apollo-server-core' import { Field, GraphQLISODateTime, ID, InputType, ObjectType } from 'type-graphql' import { Contact } from './contact' -import { Negotiator, NegotiatorFragment } from './negotiator' -import { Office, OfficeFragment } from './office' +import { Negotiator } from './negotiator' +import { Office } from './office' import { Property, PropertyFragment } from './property' @ObjectType({ description: '@labelKeys(value) @notTopLevel()' }) @@ -42,8 +42,8 @@ export class Appointment { @Field(() => Property, { nullable: true }) property?: Property - @Field({ nullable: true }) - organiserId?: string + @Field(() => Negotiator, { nullable: true }) + organiser?: Negotiator @Field(() => [Negotiator]) negotiators?: Negotiator[] @@ -88,16 +88,14 @@ export class AppointmentInput { @Field(() => [String], { description: '@idOf(Office)' }) officeIds: string[] - @Field({ description: '@idOf(Contact)' }) - attendeeId: string + @Field({ description: '@idOf(Contact)', nullable: true }) + attendeeId?: string metadata?: any } export const AppointmentFragment = gql` ${PropertyFragment} - ${NegotiatorFragment} - ${OfficeFragment} fragment AppointmentFragment on AppointmentModel { id created @@ -131,7 +129,7 @@ export const AppointmentFragment = gql` negotiatorConfirmed attendeeConfirmed propertyConfirmed - + _eTag _embedded { offices { ...OfficeFragment @@ -142,6 +140,9 @@ export const AppointmentFragment = gql` property { ...PropertyFragment } + organiser { + ...NegotiatorFragment + } } } ` diff --git a/packages/app-builder-backend/src/resolvers/app-resolver.ts b/packages/app-builder-backend/src/resolvers/app-resolver.ts index 8bd8fb3870..05c54b3fa2 100644 --- a/packages/app-builder-backend/src/resolvers/app-resolver.ts +++ b/packages/app-builder-backend/src/resolvers/app-resolver.ts @@ -117,71 +117,48 @@ const ensureScopes = async (app: DDBApp, accessToken: string) => { if (!objectName || !props) { return null } - if (name === 'Form') { - const childNodes = node.nodes.map((nodeId) => nodes.find(({ nodeId: id }) => id === nodeId)).filter(notEmpty) - const fieldNames = childNodes - .map((node) => node.props.name) - .filter(notEmpty) - .filter(isString) - - const subtypes = fieldNames - .filter((name) => name.endsWith('Id') || name.endsWith('Ids')) - .map((name) => name.replace('Ids', '').replace('Id', '')) - .map((name) => { - if (name.endsWith('y')) { - return name.replace(/y$/, 'ies') - } - return name - }) - .map((name) => { - if (name.toLowerCase().includes('attendee')) { - return 'contact' - } - return name - }) - .filter((fieldName) => acEntities.find((entityName) => entityName.includes(fieldName))) - - return [ - { - objectName, - access: [Access.read, Access.write], - }, - ...subtypes.map((subtype) => ({ - objectName: subtype, - access: [Access.read], - })), - ] - } - if (name === 'Table') { - if (isArray(props.includedFields)) { - const subtypes = props.includedFields.filter((fieldName) => - acEntities.find((entityName) => entityName.includes(fieldName)), - ) - - return [ - { - objectName, - access: props.showControls ? [Access.read, Access.write] : [Access.write], - }, - ...subtypes.map((subtype) => ({ - objectName: subtype, - access: [Access.read], - })), - ] - } - return [ - { - objectName, - access: props.showControls ? [Access.read, Access.write] : [Access.write], - }, - ] - } - return null + const fieldNames = + name === 'Form' + ? node.nodes + .map((nodeId) => nodes.find(({ nodeId: id }) => id === nodeId)) + .filter(notEmpty) + .map((node) => node.props.name) + .filter(notEmpty) + .filter(isString) + : isArray(props.includedFields) + ? props.includedFields + : [] + + const subtypes = fieldNames + .map((name) => name.replace('Ids', '').replace('Id', '')) + .map((name) => { + if (name.endsWith('y')) { + return name.replace(/y$/, 'ies') + } + return name + }) + .map((name) => { + if (name.toLowerCase().includes('attendee')) { + return 'contact' + } + return name + }) + .filter((fieldName) => acEntities.find((entityName) => entityName.includes(fieldName))) + + return [ + { + objectName, + access: props.showControls ? [Access.read, Access.write] : [Access.read], + }, + ...subtypes.map((subtype) => ({ + objectName: subtype, + access: [Access.read], + })), + ] }) .flat() .filter(notEmpty) - const scopes = requiredAccess .map(({ objectName, access }) => { return access.map((access) => getObjectScopes(objectName, access)) diff --git a/packages/app-builder-backend/src/resolvers/appointment-resolver.ts b/packages/app-builder-backend/src/resolvers/appointment-resolver.ts index 25fc7f4744..4c9838d124 100644 --- a/packages/app-builder-backend/src/resolvers/appointment-resolver.ts +++ b/packages/app-builder-backend/src/resolvers/appointment-resolver.ts @@ -1,6 +1,6 @@ import { Appointment, AppointmentFragment, AppointmentInput, AppointmentType } from '../entities/appointments' import { gql } from 'apollo-server-core' -import { Arg, Authorized, Ctx, FieldResolver, Mutation, Query, Resolver, Root } from 'type-graphql' +import { Arg, Authorized, Ctx, FieldResolver, GraphQLISODateTime, Mutation, Query, Resolver, Root } from 'type-graphql' import { Context } from '../types' import { query } from '../utils/graphql-fetch' import { Office } from '../entities/office' @@ -21,7 +21,7 @@ const getAppointmentTypesQuery = gql` const getAppointmentQuery = gql` ${AppointmentFragment} query GetAppointment($id: String!) { - GetAppointmentById(id: $id, embed: [offices, negotiators, property]) { + GetAppointmentById(id: $id, embed: [offices, negotiators, organiser, property]) { ...AppointmentFragment } } @@ -30,7 +30,7 @@ const getAppointmentQuery = gql` const getAppointmentsQuery = gql` ${AppointmentFragment} query GetAppointments($start: String!, $end: String!) { - GetAppointments(start: $start, end: $end, embed: [offices, negotiators, property]) { + GetAppointments(start: $start, end: $end, embed: [offices, negotiators, organiser, property]) { _embedded { ...AppointmentFragment } @@ -43,7 +43,7 @@ const createAppointmentMutation = gql` mutation CreateAppointment( $start: String! $end: String! - $description: String! + $description: String $attendee: CreateAppointmentModelAttendeeInput $organiserId: String! $propertyId: String @@ -75,7 +75,7 @@ const updateAppointmentMutation = gql` $id: String! $start: String! $end: String! - $description: String! + $description: String $attendee: UpdateAppointmentModelAttendeeInput $organiserId: String! $propertyId: String @@ -83,6 +83,7 @@ const updateAppointmentMutation = gql` $negotiatorIds: [String!]! $metadata: JSON $typeId: String! + $_eTag: String! ) { UpdateAppointment( id: $id @@ -96,6 +97,7 @@ const updateAppointmentMutation = gql` officeIds: $officeIds typeId: $typeId metadata: $metadata + _eTag: $_eTag ) { ...AppointmentFragment } @@ -215,7 +217,7 @@ const createAppointment = async ( { ...appointment, type: appointment.typeId, - attendee: { type: 'contact', id: appointment.attendeeId }, + attendee: appointment.attendeeId && { type: 'contact', id: appointment.attendeeId }, }, 'CreateAppointment', { @@ -239,14 +241,14 @@ const updateAppointment = async ( ): Promise => { const existingAppointment = await getApiAppointment(id, accessToken, idToken) if (!existingAppointment) { - throw new Error(`Contact with id ${id} not found`) + throw new Error(`Appointment with id ${id} not found`) } const { _eTag } = existingAppointment await query>( updateAppointmentMutation, { ...appointment, - attendee: { type: 'contact', id: appointment.attendeeId }, + attendee: appointment.attendeeId && { type: 'contact', id: appointment.attendeeId }, id, _eTag, }, @@ -272,10 +274,13 @@ export class AppointmentResolver { @Query(() => [Appointment]) async listAppointments( @Ctx() { accessToken, idToken, storeCachedMetadata }: Context, - @Arg('start') start: string, - @Arg('end') end: string, + @Arg('start', () => GraphQLISODateTime) start: Date, + @Arg('end', () => GraphQLISODateTime) end: Date, ): Promise { - const appointments = await getAppointments(accessToken, idToken, { start, end }) + const appointments = await getAppointments(accessToken, idToken, { + start: start.toISOString(), + end: end.toISOString(), + }) appointments?.forEach((appointment) => { storeCachedMetadata(entityName, appointment.id, appointment.metadata) }) @@ -331,7 +336,7 @@ export class AppointmentResolver { @Arg(entityName) appointment: AppointmentInput, ): Promise { const { [entityName]: metadata } = operationMetadata - const newAppointment = await updateAppointment(accessToken, idToken, id, { + const newAppointment = await updateAppointment(id, accessToken, idToken, { ...appointment, metadata, }) @@ -349,6 +354,9 @@ export class AppointmentResolver { if (appointment.attendee) { return appointment.attendee } + if (appointment.attendeeInfo?.type !== 'contact') { + return undefined + } const contact = await getContact(appointment.attendeeInfo.id, context.accessToken, context.idToken) if (!contact) { return undefined @@ -357,15 +365,12 @@ export class AppointmentResolver { } @FieldResolver(() => AppointmentType) - async type(@Root() appointment: Appointment, @Ctx() context: Context): Promise { + async type(@Root() appointment: Appointment, @Ctx() context: Context): Promise { if (appointment.type) { return appointment.type } const types = await this.listAppointmentTypes(context) const type = types.find((t) => t.id === appointment.typeId) - if (!type) { - throw new Error(`Appointment type with id ${appointment.typeId} not found`) - } return type } } diff --git a/packages/app-builder/src/components/hooks/objects/use-object-list.ts b/packages/app-builder/src/components/hooks/objects/use-object-list.ts index 4c07ab8a57..2801900669 100644 --- a/packages/app-builder/src/components/hooks/objects/use-object-list.ts +++ b/packages/app-builder/src/components/hooks/objects/use-object-list.ts @@ -3,11 +3,21 @@ import { dummyQuery } from '../use-introspection' import { usePageId } from '../use-page-id' import { useObject } from './use-object' -export const useObjectList = (typeName?: string) => { +export const useObjectList = (typeName?: string, filters?: any) => { const { object, error, loading } = useObject(typeName) const { context } = usePageId() const listQuery = object && typeName ? object.list : undefined - const query = useQuery(listQuery?.query || dummyQuery, { skip: !listQuery?.query, variables: context }) + let skip = true + if (listQuery?.query) { + skip = false + } + if (listQuery?.args?.length) { + skip = !Object.keys(filters || {}).length + } + const query = useQuery(listQuery?.query || dummyQuery, { + skip, + variables: { ...context, ...(filters || {}) }, + }) return { data: (query.data && Object.values(query.data)[0]) as any[] | undefined, diff --git a/packages/app-builder/src/components/ui/user/ejectable/form-input.tsx b/packages/app-builder/src/components/ui/user/ejectable/form-input.tsx index e2244fdc9b..f525e6a0c1 100644 --- a/packages/app-builder/src/components/ui/user/ejectable/form-input.tsx +++ b/packages/app-builder/src/components/ui/user/ejectable/form-input.tsx @@ -35,7 +35,7 @@ import { block } from '../../styles' import { styled } from '@linaria/react' import { useObjectGet } from '../../../../components/hooks/objects/use-object-get' -const getLabel = (obj: any, labelKeys?: string[]) => { +export const getLabel = (obj: any, labelKeys?: string[]) => { if (!obj) { return '' } @@ -343,7 +343,13 @@ const resolveIdOfType = (idOfType: string, parentObj: any): string => { return idOfType } -const Input = ({ +const convertDate = (date?: string) => { + if (!date) return undefined + const d = new Date(date) + return d.toISOString().split('.')[0] +} + +export const Input = ({ name, input, fwdRef, @@ -414,6 +420,8 @@ const Input = ({ ) } + const inputType = fieldTypeToInputType(inputTypeName.toLowerCase()) + return ( {enumValues && ( @@ -446,28 +454,54 @@ const Input = ({ )} {!enumValues && !idOfType && !customInputType && ( - { - const value = - inputTypeName === 'Float' || inputTypeName === 'Int' ? parseFloat(e.target.value) : e.target.value - onChange({ - ...e, - target: { - ...e.target, - value, - name, - } as any, - }) - }} - name={name} - defaultValue={defaultValue} - /> + <> + {!inputType.startsWith('date') && ( + { + const value = inputType === 'number' ? parseFloat(e.target.value) : e.target.value + onChange({ + ...e, + target: { + ...e.target, + value, + name, + } as any, + }) + }} + name={name} + defaultValue={defaultValue} + /> + )} + {inputType.startsWith('date') && ( + { + const value = e.target.value + onChange({ + ...e, + target: { + ...e.target, + value, + name, + } as any, + }) + }} + name={name} + defaultValue={convertDate(defaultValue)} + /> + )} + )} {customInputType && customInputType === 'image-upload' && ( { const getDefaultValue = (defaultValues: any, name: string) => { const parts = name.split('.') if (parts.length === 1) { + if (name.toLowerCase().includes('id')) { + return defaultValues[name] + } return defaultValues[name] } const [first, ...rest] = parts @@ -635,6 +672,7 @@ const InnerFormInput = ( const label = friendlyIdName(name) if (isList) { const newDefaultValue = defaultValues[label.toLowerCase()] || defaultValues[name] + return ( { return object } -const lowercaseFirstLetter = (str: string) => str.charAt(0).toLowerCase() + str.slice(1) - const mapDataToArgs = (data: any, args?: ParsedArg[], mapIds?: boolean) => { if (!args) return {} if (!data) return {} const dataCopy = {} const [objInput] = args.filter((a) => a.name !== 'id') objInput.fields?.forEach((arg) => { - const { name, idOfType, isList } = arg + const { name, idOfType } = arg if (data[name]) { dataCopy[name] = data[name] } if (idOfType) { - const obj = data[lowercaseFirstLetter(idOfType) + (isList ? 's' : '')] + const obj = data[name.replace('Id', '')] if (obj) { if (mapIds) { dataCopy[name] = Array.isArray(obj) ? obj.map((o) => o.id) : obj.id diff --git a/packages/app-builder/src/components/ui/user/ejectable/table.tsx b/packages/app-builder/src/components/ui/user/ejectable/table.tsx index 4e6150b337..f65815ee3b 100644 --- a/packages/app-builder/src/components/ui/user/ejectable/table.tsx +++ b/packages/app-builder/src/components/ui/user/ejectable/table.tsx @@ -1,4 +1,4 @@ -import React, { forwardRef } from 'react' +import React, { forwardRef, useEffect, useState } from 'react' import { Button, elFlex, @@ -23,6 +23,10 @@ import { lowercaseFirstLetter, useSubObjects } from '../../../../components/hook import { notEmpty } from '../../../../components/hooks/use-introspection/helpers' import { usePageId } from '../../../../components/hooks/use-page-id' import { useObjectSpecials } from '../../../../components/hooks/objects/use-object-specials' +import { ParsedArg } from '@/components/hooks/use-introspection/query-generators' +import { styled } from '@linaria/react' +import { getLabel, Input as FormInput } from './form-input' +import { useObject } from '@/components/hooks/objects/use-object' export interface TableProps extends ContainerProps { typeName?: string @@ -33,7 +37,8 @@ export interface TableProps extends ContainerProps { [key: string]: any } -const ObjectTableCell = ({ obj }) => { +const ObjectTableCell = ({ obj }: { obj: any }) => { + const { object, loading } = useObject(obj?.__typename) if (!obj) { return null } @@ -52,10 +57,18 @@ const ObjectTableCell = ({ obj }) => { if (typeof obj !== 'object') { return obj } + if (loading) { + return + } + if (object) { + return {getLabel(obj, object.labelKeys)} + } return ( {Object.entries(obj) - .filter(([key, value]) => !key.startsWith('_') && typeof value !== 'object' && key !== 'id') + .filter(([key, value]) => { + return !key.startsWith('_') && typeof value !== 'object' && key !== 'id' + }) .map((kv) => kv[1]) .join(' ')} @@ -97,17 +110,31 @@ const shouldDisplay = ([key, value]: [string, any | undefined | null], subobject return !isHidden && !isId && !isSubObject } +const isDateString = (value: any) => { + return value && typeof value === 'string' && value.match(/^\d{4}-\d{2}-\d{2}/) +} + +const dateToHuman = (date: any) => { + if (!isDateString(date)) { + return date + } + const d = new Date(date) + return d.toLocaleString() +} + const getDataCells = (row: any, subobjectNames: string[]) => Object.entries(row) .filter((entry) => shouldDisplay(entry, subobjectNames)) - .map(([label, value]) => ({ - label: uppercaseSentence(label), - value: (typeof value === 'object' ? undefined : value) as string, - children: typeof value === 'object' ? : undefined, - narrowTable: { - showLabel: true, - }, - })) + .map(([label, value]) => { + return { + label: uppercaseSentence(label), + value: (typeof value === 'object' ? undefined : dateToHuman(value)) as string, + children: typeof value === 'object' ? : undefined, + narrowTable: { + showLabel: true, + }, + } + }) const AdditionalCells = ({ specialsAndSubobjects, @@ -191,11 +218,70 @@ const Controls = ({ ) } +const argsToDefaultFilters = (args?: ParsedArg[]) => { + if (!args) { + return {} + } + const obj = {} + args.forEach((arg) => { + switch (arg.typeName) { + case 'Int': + case 'Float': + obj[arg.name] = 0 + break + case 'Boolean': + obj[arg.name] = false + break + case 'String': + obj[arg.name] = '' + break + case 'DateTime': + obj[arg.name] = new Date() + if (arg.name.toLowerCase() === 'end') { + obj[arg.name].setHours(23, 59, 59, 999) + } + obj[arg.name] = obj[arg.name].toISOString().split('.')[0] + break + default: + obj[arg.name] = null + } + }) + return obj +} + +const FilterContainer = styled.div`` + +const Filters = ({ + filters, + setFilters, + args, +}: { + filters: any + setFilters: (filters: any) => void + args?: ParsedArg[] +}) => { + return ( + + {args?.map((arg) => { + const { name } = arg + const value = filters[name] + const setValue = (value: any) => { + setFilters({ ...filters, [name]: value }) + } + return ( + setValue(e.target.value)} name={name} /> + ) + })} + + ) +} + export const Table = forwardRef( ({ typeName, editPageId, showControls, disabled, showSearch, includedFields = [], ...props }, ref) => { - const { data: listResults, loading: listLoading } = useObjectList(typeName) + const [filters, setFilters] = useState>({}) + const { data: listResults, loading: listLoading, args } = useObjectList(typeName, filters) const subobjects = useSubObjects(typeName) - const [queryStr, setQueryStr] = React.useState('') + const [queryStr, setQueryStr] = useState('') const { available: searchAvailable, data: searchResults, @@ -205,6 +291,10 @@ export const Table = forwardRef { + setFilters(argsToDefaultFilters(args)) + }, [args]) + const subobjectNames = subobjects.data.map(({ object: { name } }) => name) const specialsAndSubobjects = [ ...specials.map(({ name }) => ({ name, label: uppercaseSentence(name) })), @@ -266,6 +356,7 @@ export const Table = forwardRef setQueryStr(e.target.value)} /> )} + {!!args?.length && } {loading && !displayNoType && } {displayTable && ( Date: Mon, 1 Aug 2022 15:14:35 +0100 Subject: [PATCH 04/10] gql fix appointment updating --- packages/graphql-server/src/resolvers/applicants/api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/graphql-server/src/resolvers/applicants/api.ts b/packages/graphql-server/src/resolvers/applicants/api.ts index d0e501f96e..51b0b3443a 100644 --- a/packages/graphql-server/src/resolvers/applicants/api.ts +++ b/packages/graphql-server/src/resolvers/applicants/api.ts @@ -94,7 +94,7 @@ export const callUpdateApplicantAPI = async ( try { const { _eTag, ...payload } = args const updateResponse = await createPlatformAxiosInstance().patch( - `${URLS.appointments}/${args.id}`, + `${URLS.applicants}/${args.id}`, payload, { headers: { From 3e600ba96c4d0a833780cf9f6ab4b19bacb6390f Mon Sep 17 00:00:00 2001 From: Josh Balfour Date: Mon, 1 Aug 2022 15:20:17 +0100 Subject: [PATCH 05/10] fix applicant updating --- .../src/resolvers/applicant-resolver.ts | 38 ++++++++----------- 1 file changed, 15 insertions(+), 23 deletions(-) diff --git a/packages/app-builder-backend/src/resolvers/applicant-resolver.ts b/packages/app-builder-backend/src/resolvers/applicant-resolver.ts index bfc8782a89..c7e779ad2d 100644 --- a/packages/app-builder-backend/src/resolvers/applicant-resolver.ts +++ b/packages/app-builder-backend/src/resolvers/applicant-resolver.ts @@ -179,7 +179,7 @@ const convertDates = (applicant: Applicant): Applicant => ({ modified: new Date(applicant.modified), }) -const getApplicants = async (accessToken: string, idToken: string, name?: string ): Promise => { +const getApplicants = async (accessToken: string, idToken: string, name?: string): Promise => { const applicants = await query<{ _embedded: ApplicantAPIResponse[] }>( getApplicationQuery, { name }, @@ -218,7 +218,11 @@ const getApplicant = async (id: string, accessToken: string, idToken: string): P return convertDates(addDefaultEmbeds(hoistedApplicant)) } -const createApplicant = async (applicant: ApplicantInput, accessToken: string, idToken: string): Promise => { +const createApplicant = async ( + applicant: ApplicantInput, + accessToken: string, + idToken: string, +): Promise<{ id: string }> => { const { contactId, ...app } = applicant const res = await query>( createApplicantMutation, @@ -238,11 +242,7 @@ const createApplicant = async (applicant: ApplicantInput, accessToken: string, i }, ) const { id } = res - const newApplicant = await getApplicant(id, accessToken, idToken) - if (!newApplicant) { - throw new Error('Failed to create applicant') - } - return newApplicant + return { id } } const updateApplicant = async ( @@ -250,7 +250,7 @@ const updateApplicant = async ( applicant: ApplicantInput, accessToken: string, idToken: string, -): Promise => { +): Promise => { const existingApplicant = await getApiApplicant(id, accessToken, idToken) if (!existingApplicant) { throw new Error(`Applicant with id ${id} not found`) @@ -267,12 +267,6 @@ const updateApplicant = async ( idToken, }, ) - - const newApplicant = await getApiApplicant(id, accessToken, idToken) - if (!newApplicant) { - throw new Error('Applicant not found') - } - return newApplicant } const entityName: MetadataSchemaType = 'applicant' @@ -304,14 +298,12 @@ export class ApplicantResolver { @Authorized() @Mutation(() => Applicant) - async createApplicant( - @Ctx() { accessToken, idToken, storeCachedMetadata, operationMetadata }: Context, - @Arg(entityName) applicantInput: ApplicantInput, - ): Promise { + async createApplicant(@Ctx() ctx: Context, @Arg(entityName) applicantInput: ApplicantInput): Promise { + const { accessToken, idToken, storeCachedMetadata, operationMetadata } = ctx const { [entityName]: metadata } = operationMetadata const applicant = await createApplicant({ ...applicantInput, metadata }, accessToken, idToken) - storeCachedMetadata(entityName, applicant.id, applicant.metadata) - return applicant + storeCachedMetadata(entityName, applicant.id, metadata) + return this.getApplicant(ctx, applicant.id) } @Authorized() @@ -337,9 +329,9 @@ export class ApplicantResolver { ): Promise { const { accessToken, idToken, operationMetadata, storeCachedMetadata } = context const { [entityName]: metadata } = operationMetadata - const applicant = await updateApplicant(id, { ...applicantDto, metadata }, accessToken, idToken) - storeCachedMetadata(entityName, applicant.id, applicantDto.metadata) - return applicant + await updateApplicant(id, { ...applicantDto, metadata }, accessToken, idToken) + storeCachedMetadata(entityName, id, applicantDto.metadata) + return this.getApplicant(context, id) } @Authorized() From b0ceae4aa75b94f8a2d7822b0ab7ba8fc515b3c7 Mon Sep 17 00:00:00 2001 From: Josh Balfour Date: Mon, 1 Aug 2022 15:34:37 +0100 Subject: [PATCH 06/10] fix up filters --- .../components/ui/user/ejectable/table.tsx | 48 ++++++++++++++----- 1 file changed, 35 insertions(+), 13 deletions(-) diff --git a/packages/app-builder/src/components/ui/user/ejectable/table.tsx b/packages/app-builder/src/components/ui/user/ejectable/table.tsx index f65815ee3b..9c012fe8cf 100644 --- a/packages/app-builder/src/components/ui/user/ejectable/table.tsx +++ b/packages/app-builder/src/components/ui/user/ejectable/table.tsx @@ -249,7 +249,17 @@ const argsToDefaultFilters = (args?: ParsedArg[]) => { return obj } -const FilterContainer = styled.div`` +const FilterContainer = styled.div` + display: flex; + flex: 1; + + :not(:last-child) { + margin-right: 1rem; + } +` +const FiltersContainer = styled.div` + display: flex; +` const Filters = ({ filters, @@ -261,18 +271,30 @@ const Filters = ({ args?: ParsedArg[] }) => { return ( - - {args?.map((arg) => { - const { name } = arg - const value = filters[name] - const setValue = (value: any) => { - setFilters({ ...filters, [name]: value }) - } - return ( - setValue(e.target.value)} name={name} /> - ) - })} - + + {args + ?.sort((a, b) => { + if (a.name === 'start') { + return -1 + } + if (b.name === 'start') { + return 1 + } + return 0 + }) + .map((arg) => { + const { name } = arg + const value = filters[name] + const setValue = (value: any) => { + setFilters({ ...filters, [name]: value }) + } + return ( + + setValue(e.target.value)} name={name} /> + + ) + })} + ) } From 369b87f62505fa4474b30f04512645b335838f99 Mon Sep 17 00:00:00 2001 From: Josh Balfour Date: Mon, 1 Aug 2022 17:29:56 +0100 Subject: [PATCH 07/10] property image list & update --- .../src/entities/property-image.ts | 33 +++++++++++-------- packages/app-builder-backend/src/index.ts | 1 + .../src/resolvers/app-resolver.ts | 9 +++-- .../src/resolvers/property-image-resolver.ts | 4 +-- .../use-introspection/parse-introspection.ts | 21 +++++++++--- .../use-introspection/query-generators.ts | 9 +++++ .../hooks/use-introspection/types.ts | 4 +++ .../ui/user/ejectable/form-input.tsx | 13 ++++++-- .../src/components/ui/user/ejectable/form.tsx | 2 ++ .../components/ui/user/ejectable/table.tsx | 31 ++++++++++++++--- 10 files changed, 97 insertions(+), 30 deletions(-) diff --git a/packages/app-builder-backend/src/entities/property-image.ts b/packages/app-builder-backend/src/entities/property-image.ts index 9bff66555f..88c0687c90 100644 --- a/packages/app-builder-backend/src/entities/property-image.ts +++ b/packages/app-builder-backend/src/entities/property-image.ts @@ -1,5 +1,5 @@ import { gql } from 'apollo-server-core' -import { Field, GraphQLISODateTime, InputType, ObjectType } from 'type-graphql' +import { Field, GraphQLISODateTime, InputType, ObjectType, registerEnumType } from 'type-graphql' export const PropertyImageFragment = gql` fragment PropertyImageFragment on PropertyImageModel { @@ -13,7 +13,18 @@ export const PropertyImageFragment = gql` } ` -@ObjectType({ description: '@notTopLevel()' }) +export enum PropertyImageType { + photograph = 'photograph', + floorPlan = 'floorPlan', + epc = 'epc', + map = 'map', +} +registerEnumType(PropertyImageType, { + name: 'PropertyImageType', + description: 'The type of image', +}) + +@ObjectType() export class PropertyImage { @Field() id: string @@ -24,14 +35,11 @@ export class PropertyImage { @Field(() => GraphQLISODateTime) modified: Date - @Field() + @Field({ description: '@urlType("image")' }) url: string - @Field() - type: string - - @Field({ nullable: true }) - order: number + @Field(() => PropertyImageType) + type: PropertyImageType @Field() caption: string @@ -42,15 +50,12 @@ export class PropertyImageInput { @Field({ description: '@customInput(image-upload)' }) data: string - @Field({ nullable: true }) + @Field() caption: string @Field({ description: '@idOf(Property)' }) propertyId: string - @Field({ nullable: true }) - type: string - - @Field({ nullable: true }) - order: number + @Field(() => PropertyImageType, { nullable: true }) + type: PropertyImageType } diff --git a/packages/app-builder-backend/src/index.ts b/packages/app-builder-backend/src/index.ts index be93992015..a30dca4b7a 100644 --- a/packages/app-builder-backend/src/index.ts +++ b/packages/app-builder-backend/src/index.ts @@ -54,6 +54,7 @@ const start = async () => { app.disable('x-powered-by') app.use(cors()) + app.use(express.json({ limit: '50mb' })) const httpServer = http.createServer(app) const server = new ExtendedApolloServerExpress({ diff --git a/packages/app-builder-backend/src/resolvers/app-resolver.ts b/packages/app-builder-backend/src/resolvers/app-resolver.ts index 05c54b3fa2..916a784c92 100644 --- a/packages/app-builder-backend/src/resolvers/app-resolver.ts +++ b/packages/app-builder-backend/src/resolvers/app-resolver.ts @@ -57,7 +57,11 @@ enum Access { } const getObjectScopes = (objectName: string, access: Access) => { - return `agencyCloud/${Pluralize.plural(objectName.toLowerCase())}.${access}` + let on = objectName + if (on.toLowerCase() === 'propertyimage') { + on = 'image' + } + return `agencyCloud/${Pluralize.plural(on)}.${access}` } // compare array of strings @@ -114,6 +118,7 @@ const ensureScopes = async (app: DDBApp, accessToken: string) => { const name = node.type.resolvedName const props = node.props const objectName = node.props?.typeName as string | undefined + if (!objectName || !props) { return null } @@ -149,7 +154,7 @@ const ensureScopes = async (app: DDBApp, accessToken: string) => { return [ { objectName, - access: props.showControls ? [Access.read, Access.write] : [Access.read], + access: props.showControls || name === 'Form' ? [Access.read, Access.write] : [Access.read], }, ...subtypes.map((subtype) => ({ objectName: subtype, diff --git a/packages/app-builder-backend/src/resolvers/property-image-resolver.ts b/packages/app-builder-backend/src/resolvers/property-image-resolver.ts index d206283038..486e5393df 100644 --- a/packages/app-builder-backend/src/resolvers/property-image-resolver.ts +++ b/packages/app-builder-backend/src/resolvers/property-image-resolver.ts @@ -88,13 +88,13 @@ export class PropertyImageResolver { @Authorized() async listPropertyImages( @Ctx() { accessToken, idToken }: Context, - @Arg('propertyId') propertyId: string, + @Arg('propertyId', { description: '@idOf(Property)' }) propertyId: string, ): Promise { const propertyImages = await this.service.getEntities({ idToken, accessToken, variables: { - propertyId, + propertyId: [propertyId], }, }) diff --git a/packages/app-builder/src/components/hooks/use-introspection/parse-introspection.ts b/packages/app-builder/src/components/hooks/use-introspection/parse-introspection.ts index 15744c929b..2bba2c9966 100644 --- a/packages/app-builder/src/components/hooks/use-introspection/parse-introspection.ts +++ b/packages/app-builder/src/components/hooks/use-introspection/parse-introspection.ts @@ -1,6 +1,6 @@ import { IntrospectionField, IntrospectionObjectType, IntrospectionQuery } from 'graphql' -import { notEmpty } from './helpers' +import { flatKind, getObjectType, notEmpty } from './helpers' import { getTopLevelFields } from './nested-fields' import { getMutation, @@ -11,10 +11,15 @@ import { GeneratedMutation, GeneratedQuery, } from './query-generators' -import { isIntrospectionEnumType, isIntrospectionInputObjectType, isIntrospectionObjectType } from './types' +import { + isIntrospectionEnumType, + isIntrospectionInputObjectType, + isIntrospectionObjectType, + QueryableField, +} from './types' export type IntrospectionResult = { - object: IntrospectionObjectType + object: Omit & { fields: QueryableField[] } supportsCustomFields: boolean acKeyField?: IntrospectionField & { acKey: string } labelKeys?: string[] @@ -28,6 +33,14 @@ export type IntrospectionResult = { notTopLevel?: boolean } +const fieldToQueryableField = (field: IntrospectionField): QueryableField => { + return { + ...field, + nestedKinds: flatKind(field.type), + nestedType: getObjectType(field.type), + } +} + export const parseIntrospectionResult = (introspection: IntrospectionQuery): IntrospectionResult[] | undefined => { if (!introspection) { return undefined @@ -76,7 +89,7 @@ export const parseIntrospectionResult = (introspection: IntrospectionQuery): Int return { object: { ...object, - fields: object.fields.filter(({ name }) => !name.startsWith('_placeholder')), + fields: object.fields.filter(({ name }) => !name.startsWith('_placeholder')).map(fieldToQueryableField), }, notTopLevel: object.description?.includes('@notTopLevel') ?? false, labelKeys, diff --git a/packages/app-builder/src/components/hooks/use-introspection/query-generators.ts b/packages/app-builder/src/components/hooks/use-introspection/query-generators.ts index e41551bdb3..bd37d2d28e 100644 --- a/packages/app-builder/src/components/hooks/use-introspection/query-generators.ts +++ b/packages/app-builder/src/components/hooks/use-introspection/query-generators.ts @@ -95,6 +95,8 @@ export type CustomInputType = 'image-upload' | 'department-lookup' export type OnlyIf = Record +export type URLType = 'image' + export type ParsedArg = { name: string isRequired: boolean @@ -107,6 +109,7 @@ export type ParsedArg = { customInputType?: CustomInputType onlyIf?: OnlyIf isDepartmentLookup?: boolean + replaces?: string } const parseArgs = ( @@ -123,6 +126,7 @@ const parseArgs = ( let idOfType let isRequired = false let isList = false + let replaces: string | undefined let customInputType: CustomInputType | undefined if (isNonNullInputType(type)) { @@ -171,6 +175,10 @@ const parseArgs = ( customInputType = customInput as CustomInputType } + if (description && description.includes('@replaces')) { + replaces = description.split('@replaces(')[1].split(')')[0] + } + const acKey = description?.split('@acKey(')[1]?.split(')')[0] as DesktopContext const onlyIfStr = description?.split('@onlyIf(')[1]?.split(')')[0] as string | undefined @@ -186,6 +194,7 @@ const parseArgs = ( isList, enumValues, customInputType, + replaces, onlyIf, fields: (actualTypeObject && diff --git a/packages/app-builder/src/components/hooks/use-introspection/types.ts b/packages/app-builder/src/components/hooks/use-introspection/types.ts index 3992b011c9..5188248b99 100644 --- a/packages/app-builder/src/components/hooks/use-introspection/types.ts +++ b/packages/app-builder/src/components/hooks/use-introspection/types.ts @@ -56,6 +56,10 @@ export const isListType = (type: IntrospectionOutputTypeRef): type is Introspect return type.kind === TypeKind.LIST } +export const isListField = (type: IntrospectionOutputTypeRef): type is IntrospectionListTypeRef => { + return type.kind === TypeKind.LIST +} + export const isListInputType = (type: IntrospectionInputTypeRef): type is IntrospectionListTypeRef => { return type.kind === TypeKind.LIST } diff --git a/packages/app-builder/src/components/ui/user/ejectable/form-input.tsx b/packages/app-builder/src/components/ui/user/ejectable/form-input.tsx index f525e6a0c1..1b9e3addf7 100644 --- a/packages/app-builder/src/components/ui/user/ejectable/form-input.tsx +++ b/packages/app-builder/src/components/ui/user/ejectable/form-input.tsx @@ -211,27 +211,33 @@ const FileUploadInput = ({ defaultValue, onChange, disabled, + name, }: { disabled?: boolean label: string value?: string defaultValue?: string + name: string onChange: (event: React.ChangeEvent) => void }) => { const [modalIsOpen, setModalIsOpen] = useState(false) - + const [file, setFile] = useState(value) return (
{ + onChange(e) + setFile(e.target.value) + }} onFileView={() => setModalIsOpen(true)} /> setModalIsOpen(false)}> - {value && } + {file && }