Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new Location field #1736

Merged
merged 23 commits into from
Oct 7, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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