Skip to content

Commit

Permalink
Add new Location field (#1736)
Browse files Browse the repository at this point in the history
  • Loading branch information
jordanoverbye authored and MadeByMike committed Oct 7, 2019
1 parent 0abfe58 commit 464d757
Show file tree
Hide file tree
Showing 16 changed files with 480 additions and 9 deletions.
1 change: 1 addition & 0 deletions .changeset/friendly-crabs-guess/changes.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{ "releases": [{ "name": "@keystone-alpha/fields", "type": "minor" }], "dependents": [] }
1 change: 1 addition & 0 deletions .changeset/friendly-crabs-guess/changes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add Location field
18 changes: 18 additions & 0 deletions .changeset/sour-kiwis-try/changes.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"releases": [{ "name": "@arch-ui/select", "type": "minor" }],
"dependents": [
{
"name": "@keystone-alpha/app-admin-ui",
"type": "patch",
"dependencies": ["@keystone-alpha/fields", "@arch-ui/select"]
},
{ "name": "@arch-ui/docs", "type": "patch", "dependencies": ["@arch-ui/select"] },
{ "name": "@arch-ui/day-picker", "type": "patch", "dependencies": ["@arch-ui/select"] },
{
"name": "@keystone-alpha/fields",
"type": "patch",
"dependencies": ["@arch-ui/day-picker", "@arch-ui/select"]
},
{ "name": "@keystone-alpha/website", "type": "patch", "dependencies": ["@arch-ui/select"] }
]
}
1 change: 1 addition & 0 deletions .changeset/sour-kiwis-try/changes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Allow async and creatable react-selects
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@
"get-contrast": "^2.0.0",
"get-selection-range": "^0.1.0",
"globby": "^9.1.0",
"google-maps-react": "^2.0.2",
"graphql": "^14.4.2",
"graphql-tag": "^2.10.1",
"graphql-type-json": "^0.2.1",
Expand Down
49 changes: 41 additions & 8 deletions packages/arch/packages/select/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@

import * as React from 'react';
import { useMemo } from 'react';
import ReactSelect from 'react-select';
import BaseSelect from 'react-select';
import AsyncCreatableSelect from 'react-select/async-creatable';
import AsyncSelect from 'react-select/async';
import CreatableSelect from 'react-select/creatable';
import { colors } from '@arch-ui/theme';

// ==============================
Expand Down Expand Up @@ -76,12 +79,42 @@ const selectStyles = {
},
menuPortal: provided => ({ ...provided, zIndex: 3 }),
};
const Select = ({ innerRef, styles, ...props }: { innerRef?: React.Ref<*>, styles?: Object }) => (
<ReactSelect
ref={innerRef}
styles={useMemo(() => ({ ...selectStyles, ...styles }), [styles])}
{...props}
/>
);

const getSelectVariant = ({ isAsync, isCreatable }) => {
if (isAsync && isCreatable) {
return AsyncCreatableSelect;
}
if (isAsync) {
return AsyncSelect;
}
if (isCreatable) {
return CreatableSelect;
}

return BaseSelect;
};

const Select = ({
isAsync,
isCreatable,
innerRef,
styles,
...props
}: {
isAsync?: Boolean,
isCreatable?: Boolean,
innerRef?: React.Ref<*>,
styles?: Object,
}) => {
const ReactSelect = getSelectVariant({ isAsync, isCreatable });

return (
<ReactSelect
ref={innerRef}
styles={useMemo(() => ({ ...selectStyles, ...styles }), [styles])}
{...props}
/>
);
};

export default Select;
1 change: 1 addition & 0 deletions packages/fields/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ KeystoneJS contains a set of primitive fields types that can be imported from `@
- [File](keystone-alpha/fields/src/types/file)
- [Float](keystone-alpha/fields/src/types/float)
- [Integer](keystone-alpha/fields/src/types/integer)
- [Location](keystone-alpha/fields/src/types/location)
- [OEmbed](keystone-alpha/fields/src/types/o-embed)
- [Password](keystone-alpha/fields/src/types/password)
- [Relationship](keystone-alpha/fields/src/types/relationship)
Expand Down
4 changes: 3 additions & 1 deletion packages/fields/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"cuid": "^2.1.6",
"date-fns": "^1.30.1",
"dumb-passwords": "^0.2.1",
"google-maps-react": "^2.0.2",
"graphql": "^14.4.2",
"graphql-tag": "^2.10.1",
"image-extensions": "^1.1.0",
Expand All @@ -64,6 +65,7 @@
"react-popper": "^1.3.3",
"react-popper-tooltip": "^2.8.1",
"react-select": "^3.0.4",
"react-toast-notifications": "^2.2.4",
"slate": "^0.47.4",
"slate-drop-or-paste-images": "^0.9.1",
"slate-react": "^0.22.4",
Expand All @@ -79,4 +81,4 @@
"Controller"
]
}
}
}
1 change: 1 addition & 0 deletions packages/fields/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export { default as Decimal } from './types/Decimal';
export { default as File } from './types/File';
export { default as Float } from './types/Float';
export { default as Integer } from './types/Integer';
export { default as Location } from './types/Location';
export { default as OEmbed } from './types/OEmbed';
export { default as Password } from './types/Password';
export { default as Relationship } from './types/Relationship';
Expand Down
165 changes: 165 additions & 0 deletions packages/fields/src/types/Location/Implementation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import { Implementation } from '../../Implementation';
import { MongooseFieldAdapter } from '@keystone-alpha/adapter-mongoose';
import { KnexFieldAdapter } from '@keystone-alpha/adapter-knex';
import mongoose from 'mongoose';

import fetch from 'node-fetch';

// Disabling the getter of mongoose >= 5.1.0
// https://github.com/Automattic/mongoose/blob/master/migrating_to_5.md#checking-if-a-path-is-populated
mongoose.set('objectIdGetter', false);

const {
Types: { ObjectId },
} = mongoose;

export class Location extends Implementation {
constructor(_, { googleMapsKey }) {
super(...arguments);
this.graphQLOutputType = 'Location';

if (!googleMapsKey) {
throw new Error(
'You must provide a `googleMapsKey` to Location Field. To generate a Google Maps API please visit: https://developers.google.com/maps/documentation/javascript/get-api-key'
);
}

this._googleMapsKey = googleMapsKey;
}

extendAdminMeta(meta) {
return {
...meta,
googleMapsKey: this._googleMapsKey,
};
}

gqlOutputFields() {
return [`${this.path}: ${this.graphQLOutputType}`];
}

gqlQueryInputFields() {
return [
...this.equalityInputFields('String'),
...this.stringInputFields('String'),
...this.inInputFields('String'),
];
}

getGqlAuxTypes() {
return [
`
type ${this.graphQLOutputType} {
id: ID
googlePlaceID: String
formattedAddress: String
lat: Float
lng: Float
}
`,
];
}

// Called on `User.avatar` for example
gqlOutputFieldResolvers() {
return {
[this.path]: item => {
const itemValues = item[this.path];
if (!itemValues) {
return null;
}
return itemValues;
},
};
}

async resolveInput({ resolvedData }) {
const placeId = resolvedData[this.path];

// NOTE: The following two conditions could easily be combined into a
// single `if (!inputId) return inputId`, but that would lose the nuance of
// returning `undefined` vs `null`.
// Premature Optimisers; be ware!
if (typeof placeId === 'undefined') {
// Nothing was passed in, so we can bail early.
return undefined;
}

if (placeId === null) {
// `null` was specifically set, and we should set the field value to null
// To do that we... return `null`
return null;
}

const response = await fetch(
`https://maps.googleapis.com/maps/api/geocode/json?place_id=${placeId}&key=${this._googleMapsKey}`
).then(r => r.json());

if (response.results && response.results[0]) {
const { place_id, formatted_address } = response.results[0];
const { lat, lng } = response.results[0].geometry.location;
return {
id: new ObjectId(),
googlePlaceID: place_id,
formattedAddress: formatted_address,
lat: lat,
lng: lng,
};
}

return null;
}

get gqlUpdateInputFields() {
return [`${this.path}: String`];
}

get gqlCreateInputFields() {
return [`${this.path}: String`];
}
}

const CommonLocationInterface = superclass =>
class extends superclass {
getQueryConditions(dbPath) {
return {
...this.equalityConditions(dbPath),
...this.stringConditions(dbPath),
...this.inConditions(dbPath),
};
}
};

export class MongoLocationInterface extends CommonLocationInterface(MongooseFieldAdapter) {
addToMongooseSchema(schema) {
const schemaOptions = {
type: {
id: ObjectId,
googlePlaceID: String,
formattedAddress: String,
lat: Number,
lng: Number,
},
};
schema.add({ [this.path]: this.mergeSchemaOptions(schemaOptions, this.config) });
}
}

export class KnexLocationInterface extends CommonLocationInterface(KnexFieldAdapter) {
constructor() {
super(...arguments);

// Error rather than ignoring invalid config
// We totally can index these values, it's just not trivial. See issue #1297
if (this.config.isUnique || this.config.isIndexed) {
throw `The Location field type doesn't support indexes on Knex. ` +
`Check the config for ${this.path} on the ${this.field.listKey} list`;
}
}

addToTableSchema(table) {
const column = table.jsonb(this.path);
if (this.isNotNullable) column.notNullable();
if (this.defaultTo) column.defaultTo(this.defaultTo);
}
}
94 changes: 94 additions & 0 deletions packages/fields/src/types/Location/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<!--[meta]
section: api
subSection: field-types
title: Location
[meta]-->

# Location

The Location Field Type enables storing data from the Google Maps API.

## Usage

```javascript
const { Location } = require('@keystone-alpha/fields');
const { Keystone } = require('@keystone-alpha/keystone');

const keystone = new Keystone(/* ... */);

keystone.createList('Event', {
fields: {
venue: {
type: Location,
googleMapsKey: 'GOOGLE_MAPS_KEY',
},
},
});
```

## GraphQL

**Query**

```graphql
query {
allEvents {
venue {
id
googlePlaceID
formattedAddress
lat
lng
}
}
}

# Result:

# {
# "data": {
# "allEvents": [
# {
# "venue": {
# "id": "1",
# googlePlaceID: "ChIJOza7MD-uEmsRrf4t12uji6Y",
# "formattedAddress": "10/191 Clarence St, Sydney NSW 2000, Australia",
# "lat": -33.869374,
# "lng": 151.205097
# }
# }
# ]
# }
# }
```

### Mutations

To create a `Location`, pass the Google `place_id` for the desired field path.

```graphql
mutation {
createEvent(data: { venue: "ChIJOza7MD-uEmsRrf4t12uji6Y" }) {
venue {
id
googlePlaceID
formattedAddress
lat
lng
}
}
}

# Result:
# {
# "createEvent": {
# "venue": {
# "id": "1",
# googlePlaceID: "ChIJOza7MD-uEmsRrf4t12uji6Y",
# "formattedAddress": "10/191 Clarence St, Sydney NSW 2000, Australia",
# "lat": -33.869374,
# "lng": 151.205097
# }
# }
# }
```
Loading

0 comments on commit 464d757

Please sign in to comment.