From 7d164dfdef6c9dc0a3bde4c5f7ca17aaf4d9bc39 Mon Sep 17 00:00:00 2001 From: Eduardo Asafe Date: Sun, 23 Feb 2020 17:21:16 +0800 Subject: [PATCH] Fixed warnings --- package.json | 2 +- src/frontend/components/EditEvent.js | 12 +++- src/frontend/components/EditEvent.test.js | 38 +++++++++++ src/frontend/components/SObjectForm.js | 76 +++++++++++++++------ src/frontend/components/SObjectForm.test.js | 41 +++++++++++ src/frontend/containers/Events.js | 63 ++++++++++++++--- src/frontend/containers/Events.test.js | 42 ++++++++++++ src/frontend/models/Event.js | 3 +- src/objects/Event.object | 5 ++ 9 files changed, 251 insertions(+), 31 deletions(-) diff --git a/package.json b/package.json index bf4f1e5..4e2a1da 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "homepage": ".", "scripts": { "start": "react-app-rewired start", - "prebuild": "flow check && cross-env CI=true yarn test", + "check": "flow check && cross-env CI=true yarn test", "build": "react-app-rewired build && yarn run static", "test": "react-app-rewired test --env=jsdom", "eject": "react-scripts eject", diff --git a/src/frontend/components/EditEvent.js b/src/frontend/components/EditEvent.js index c9aeb52..729c24d 100644 --- a/src/frontend/components/EditEvent.js +++ b/src/frontend/components/EditEvent.js @@ -30,12 +30,21 @@ export default function EditEvent(props: Props) { } function EventForm({ events, spinner }: *) { + const { eventCreateFieldSet, eventDraft } = events.state + + if (eventDraft && eventDraft.RecordTypeId) { + events.setEventLayout(eventDraft.RecordTypeId) + } else { + events.setDefaultEventLayout() + } + const description = events.getEventDescription() const layout = events.getEventLayout() + if (!description || !layout) { return null } - const { eventCreateFieldSet, eventDraft } = events.state + const initialValues = { ...getDefaultValues(description, layout, eventCreateFieldSet), ...eventDraft @@ -170,6 +179,7 @@ function EventModal({ getReference={fieldName => events.getReference(fieldName, eventDraft && eventDraft.Id) } + eventRecordTypeInfos={events.state.eventRecordTypeInfos} layout={layout} timezone={events.state.timezone} /> diff --git a/src/frontend/components/EditEvent.test.js b/src/frontend/components/EditEvent.test.js index 5c3fd11..4377b45 100644 --- a/src/frontend/components/EditEvent.test.js +++ b/src/frontend/components/EditEvent.test.js @@ -9,6 +9,7 @@ import { Provider } from "unstated" import Events from "../containers/Events" import * as af from "../models/Account.testFixtures" import * as clf from "../models/CustomLabel.testFixtures" +import * as ef from "../models/Event.testFixtures" import { eventCreateFieldSet, eventDescription, @@ -66,6 +67,43 @@ it("renders a form", async () => { }) }) +it("sets layout from default record type for new events", async () => { + const events = new Events(eventsOpts) + await prepopulate(events) + const wrapper = mount(, events) + const recordType = events.state.eventRecordTypeInfos.find( + ({ defaultRecordTypeMapping }) => defaultRecordTypeMapping + ) + if (recordType) { + expect(events.state.eventLayout).toBe( + ef.eventLayouts[recordType.urls.layout] + ) + } else { + throw new Error("expected to match layout") + } +}) + +it("sets layout from custom record type for saved events", async () => { + const events = new Events(eventsOpts) + const recordType = events.state.eventRecordTypeInfos.find( + ({ recordTypeId }) => recordTypeId == "012000000000000AAA" + ) + if (recordType) { + await events.setEventDraft({ + ...draft, + RecordTypeId: recordType.recordTypeId + }) + await prepopulate(events) + const wrapper = mount(, events) + + expect(events.state.eventLayout).toBe( + ef.eventLayouts[recordType.urls.layout] + ) + } else { + throw new Error("expected to match layout") + } +}) + it("saves an event", async () => { const events = new Events(eventsOpts) await events.setEventDraft(draft) diff --git a/src/frontend/components/SObjectForm.js b/src/frontend/components/SObjectForm.js index 2e40134..c1926c4 100644 --- a/src/frontend/components/SObjectForm.js +++ b/src/frontend/components/SObjectForm.js @@ -6,8 +6,12 @@ import * as React from "react" import * as FS from "../models/FieldSet" import { type Layout, getPicklistValues } from "../models/Layout" import { type Record } from "../models/QueryResult" -import { type SObjectDescription } from "../models/SObjectDescription" +import { + type SObjectDescription, + type PickListValue +} from "../models/SObjectDescription" import Checkbox from "./forms/Checkbox" +import { type RecordTypeInfo } from "../models/RecordType" import Combobox from "./forms/Combobox" import DateTime from "./forms/DateTime" import { getErrorText } from "./i18n/errorMessages" @@ -20,15 +24,19 @@ type Props = { fieldSet: FS.FieldSet, getReference?: (fieldName: string) => ?Record, layout: Layout, + eventRecordTypeInfos: RecordTypeInfo[], timezone: string } +const recordTypeFieldName = "RecordTypeId" + export default function SObjectForm({ description, errors, fieldSet, getReference, layout, + eventRecordTypeInfos, timezone }: Props) { return ( @@ -39,7 +47,8 @@ export default function SObjectForm({ description, getReference, layout, - timezone + timezone, + eventRecordTypeInfos )} ) @@ -52,7 +61,8 @@ function inputsForFieldSet( description: SObjectDescription, getReference: ?(fieldName: string) => ?Record, layout: Layout, - timezone: string + timezone: string, + eventRecordTypeInfos: RecordTypeInfo[] ): React.Node { const inputs = [] for (let i = 0; i < fieldSet.length; i += 2) { @@ -73,7 +83,8 @@ function inputsForFieldSet( description, getReference, layout, - timezone + timezone, + eventRecordTypeInfos )} ))} @@ -122,7 +133,8 @@ function inputFor( description: SObjectDescription, getReference: ?(fieldName: string) => ?Record, layout: Layout, - timezone: string + timezone: string, + eventRecordTypeInfos: RecordTypeInfo[] ): React.Node { switch (type) { case "boolean": @@ -170,22 +182,28 @@ function inputFor( ) case "picklist": const values = getPicklistValues(description, layout, name) || [] - return ( - - - {values.map(({ label, value }) => ( - - ))} - - - ) + + return picklistFor({ label, name, required, type }, errorMessage, values) case "reference": + if (name === recordTypeFieldName) { + const values = eventRecordTypeInfos + .filter( + ({ active, available, master }) => active && available && !master + ) + .map(({ recordTypeId, name, defaultRecordTypeMapping }) => ({ + active: true, + defaultValue: defaultRecordTypeMapping, + label: name, + value: recordTypeId + })) + + return picklistFor( + { label, name, required, type }, + errorMessage, + values + ) + } + const record = getReference && getReference(name) const href = record && hrefFromApiUrl(record.attributes.url) const address = record && @@ -223,6 +241,24 @@ function inputFor( } } +function picklistFor( + { label, name, required }: FS.Field, + errorMessage: ?string, + values: PickListValue[] +) { + return ( + + + {values.map(({ label, value }) => ( + + ))} + + + ) +} + function Address({ city, country, postalCode, state, street }: FS.Address) { return ( diff --git a/src/frontend/components/SObjectForm.test.js b/src/frontend/components/SObjectForm.test.js index 0979d19..779b21d 100644 --- a/src/frontend/components/SObjectForm.test.js +++ b/src/frontend/components/SObjectForm.test.js @@ -15,6 +15,7 @@ import { offsiteEventLayout } from "../models/Event.testFixtures" import { delay, inputElement } from "../testHelpers" +import * as ef from "../models/Event.testFixtures" import Combobox from "./forms/Combobox" import DateTime from "./forms/DateTime" import SObjectForm from "./SObjectForm" @@ -38,6 +39,7 @@ afterEach(() => { it("presents inputs based on a given field set", () => { const wrapper = mount( { it("presents a checkbox input", () => { const wrapper = mount( { it("presents a checked checkbox when the corresponding value is `true`", () => { const wrapper = mount( { it("presents a combobox input", () => { const wrapper = mount( { it("gets combobox values from the given layout", () => { const wrapper = mount( { it("displays validation error message with combobox input", () => { const wrapper = mount( { ] const wrapper = mount( { } }) +it("presents record types as a picklist input", () => { + // Values from events fixtures + const values = [ + { label: "First Event Record Type", value: "012f2000000lw2FAAQ" }, + { label: "Offsite Events", value: "012f2000000lw2PAAQ" } + ] + const wrapper = mount( + + ) + const input = wrapper.find("select") + expect(input.closest("label").text()).toMatch("Record Type Id") + for (const { label, value } of values) { + const option = input.find(`option[value='${value}']`) + expect(option.text()).toBe(label) + } +}) + it("gets a boolean value from a checkbox input", async () => { const wrapper = mount( { it("presents a date input", () => { const wrapper = mount( { it("presents a datetime input", () => { const wrapper = mount( { it("gets a Date value from a datetime input", async () => { const wrapper = mount( { it("presents a textarea input", () => { const wrapper = mount( { it("gets a string value from a textarea input", async () => { const wrapper = mount( { it("displays an asterisk in labels for required fields", () => { const wrapper = mount( { it("displays validation errors with form inputs", () => { const wrapper = mount( , eventLayout: ?Layout, + cachedEventLayouts: { [key: string]: ?Layout }, eventRecordTypeInfos: RecordTypeInfo[], referenceData: { [key: Id]: Record }, // map from Event IDs to What and Who properties timezone: string, @@ -64,8 +65,9 @@ export default class EventContainer extends Container { // Memoized API interfaces _fetchEvents: (query: Criteria) => Promise + _setEventLayout: (recordType: RecordTypeInfo) => void _fetchEventDescription: () => Promise - _fetchEventLayout: () => Promise + _fetchEventLayout: (recordType?: RecordTypeInfo) => Promise _fetchReferenceData: (eventId: Id) => Promise _requestEvents: (query: Criteria) => Promise @@ -87,6 +89,7 @@ export default class EventContainer extends Container { eventCreateFieldSet: opts.eventCreateFieldSet, eventDescription: null, eventDraft: null, + cachedEventLayouts: {}, eventLayout: null, eventRecordTypeInfos: opts.eventRecordTypeInfos, referenceData: {}, @@ -101,6 +104,7 @@ export default class EventContainer extends Container { // multiple times with the same inputs - e.g. when navigating forward // a page of results and navigating back. this._fetchEvents = skipDuplicateInputs(this._fetchEvents.bind(this)) + this._setEventLayout = skipDuplicateInputs(this._setEventLayout.bind(this)) this._fetchEventDescription = memoize( this._fetchEventDescription.bind(this) ) @@ -185,6 +189,38 @@ export default class EventContainer extends Container { } } + setDefaultEventLayout() { + const defaultRecordType = this.state.eventRecordTypeInfos.find( + rt => rt.defaultRecordTypeMapping + ) + + if (defaultRecordType) { + this._setEventLayout(defaultRecordType) + } + } + + setEventLayout(recordTypeId: string) { + const customRecordType = this.state.eventRecordTypeInfos.find( + rt => rt.recordTypeId === recordTypeId + ) + + if (customRecordType) { + this._setEventLayout(customRecordType) + } + } + + _setEventLayout(recordType: RecordTypeInfo) { + const eventLayout = this.state.cachedEventLayouts[recordType.recordTypeId] + + if (eventLayout) { + asyncAction(this, async () => { + await this.setState({ eventLayout }) + }) + } else { + this._fetchEventLayout(recordType) + } + } + getEventsForFullcalendar(): EventObjectInput[] { return this.state.events.map(e => forFullcalendar(this.state.timezone, e)) } @@ -257,17 +293,27 @@ export default class EventContainer extends Container { }) } - async _fetchEventLayout(): Promise { + async _fetchEventLayout(recordType?: RecordTypeInfo): Promise { await asyncAction(this, async () => { const client = await this._restClient - const recordType = this.state.eventRecordTypeInfos.find( - rt => rt.defaultRecordTypeMapping - ) - if (!recordType) { + const recordTypeInfo = + recordType || + this.state.eventRecordTypeInfos.find(rt => rt.defaultRecordTypeMapping) + if (!recordTypeInfo) { throw new Error("Could not determine your event record type.") } - const eventLayout = await client.fetchLayout(recordType) - await this.setState({ eventLayout }) + + const eventLayout = await client.fetchLayout(recordTypeInfo) + + await this.setState(state => { + const cachedEventLayouts = state.cachedEventLayouts + cachedEventLayouts[recordTypeInfo.recordTypeId] = eventLayout + + return { + eventLayout, + cachedEventLayouts + } + }) }) } @@ -333,6 +379,7 @@ export default class EventContainer extends Container { const referenceData = accountWithAddress ? { ...state.referenceData, draft: { What: accountWithAddress } } : state.referenceData + return { eventDraft: details, referenceData diff --git a/src/frontend/containers/Events.test.js b/src/frontend/containers/Events.test.js index bfe68f4..0380c18 100644 --- a/src/frontend/containers/Events.test.js +++ b/src/frontend/containers/Events.test.js @@ -232,6 +232,48 @@ it("does not transmit request for layout more than once", async () => { expect(client.fetchLayout).toHaveBeenCalledTimes(1) }) +it("event layout is cached", async () => { + const events = new Events(eventsOpts) + //sets the layout for default record type + events.getEventLayout() + await delay() + + expect(events.state.cachedEventLayouts).toEqual({ + "012f2000000lw2FAAQ": ef.eventLayout + }) +}) + +it("set event layout for record type", async () => { + const events = new Events({ + ...eventsOpts, + eventRecordTypeInfos: ef.eventRecordTypeInfos + }) + //set offsite events layout + const recordTypeToBeSet = ef.eventRecordTypeInfos.find( + e => e.developerName === "Offsite_Events" + ) + if (recordTypeToBeSet) { + events.setEventLayout(recordTypeToBeSet.recordTypeId) + } + await delay() + const layout = events.getEventLayout() + expect(layout).toEqual(ef.offsiteEventLayout) +}) + +it("set event layout for default record type", async () => { + const events = new Events({ + ...eventsOpts, + eventRecordTypeInfos: ef.eventRecordTypeInfos + }) + expect(events.state.eventLayout).toBeNull() + + events.setDefaultEventLayout() + + await delay() + const layout = events.getEventLayout() + expect(layout).toEqual(ef.eventLayout) +}) + it("gets data from a referenced record", async () => { const referencedRecords = { What: { diff --git a/src/frontend/models/Event.js b/src/frontend/models/Event.js index 14597af..fcb5786 100644 --- a/src/frontend/models/Event.js +++ b/src/frontend/models/Event.js @@ -16,7 +16,8 @@ export type Event = { ShowAs?: string, StartDateTime: Date | number, Subject: string, - WhatId?: Id + WhatId?: Id, + RecordTypeId?: string } export const defaultTimedEventDuration: moment$MomentDuration = moment.duration( diff --git a/src/objects/Event.object b/src/objects/Event.object index 976588c..5a06009 100644 --- a/src/objects/Event.object +++ b/src/objects/Event.object @@ -53,6 +53,11 @@ false false + + RecordTypeId + false + false + Description false