Repository: vazco/uniforms
Pull request #580 (2019-08-16 12:01:48)

Added GraphQL default labels derived from schema.
Targeting [issue 577](https://github.com/vazco/uniforms/issues/577)

Given schema:
```graphql
scalar Scalar

enum AccessLevel {
    Admin
}

input Author {
    id:        ID!
    confirmed: Boolean
    decimal1:  Float
    decimal2:  Float!
    firstName: String = "John"
    lastName:  String = "Doe"
    level:     AccessLevel
    tags:      [String]!
}

input Category {
    owners: [Author!]
}

input Post {
    id:       Int!
    author:   Author!
    custom:   Scalar
    title:    String
    votes:    Int
    example:  ID
    category: [Category!]!
}

# This is required by buildASTSchema
type Query { anything: ID }
```
## Feature
GraphQL bridge will now derive default labels from schema AST.
Works as follows: ~~splits AST field name string by capital letters `/(?=[A-Z])/` and joins them with spaces. Excessive  spaces are removed~~. Uses lodash's `_.lowerCase` to format string into lowercase word pieces. Finally ~~capitalizes whole string~~ uppercases first letter.
## Example
Schema path `author.firstName` will generate label: `First name`.

codecov[bot]: # [Codecov](https://codecov.io/gh/vazco/uniforms/pull/580?src=pr&el=h1) Report
> Merging [#580](https://codecov.io/gh/vazco/uniforms/pull/580?src=pr&el=desc) into [master](https://codecov.io/gh/vazco/uniforms/commit/819f8f7b511eee14ea1ec69a81b72c52e157c407?src=pr&el=desc) will **increase** coverage by `<.01%`.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/vazco/uniforms/pull/580/graphs/tree.svg?width=650&token=PEY7QrNpKu&height=150&src=pr)](https://codecov.io/gh/vazco/uniforms/pull/580?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #580      +/-   ##
==========================================
+ Coverage   99.86%   99.86%   +<.01%     
==========================================
  Files         163      164       +1     
  Lines        1527     1532       +5     
  Branches       12       13       +1     
==========================================
+ Hits         1525     1530       +5     
  Partials        2        2
```


| [Impacted Files](https://codecov.io/gh/vazco/uniforms/pull/580?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [packages/uniforms/src/toHumanLabel.ts](https://codecov.io/gh/vazco/uniforms/pull/580/diff?src=pr&el=tree#diff-cGFja2FnZXMvdW5pZm9ybXMvc3JjL3RvSHVtYW5MYWJlbC50cw==) | `100% <100%> (ø)` | |
| [...kages/uniforms-bridge-graphql/src/GraphQLBridge.js](https://codecov.io/gh/vazco/uniforms/pull/580/diff?src=pr&el=tree#diff-cGFja2FnZXMvdW5pZm9ybXMtYnJpZGdlLWdyYXBocWwvc3JjL0dyYXBoUUxCcmlkZ2UuanM=) | `100% <100%> (ø)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/vazco/uniforms/pull/580?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/vazco/uniforms/pull/580?src=pr&el=footer). Last update [819f8f7...a9055c3](https://codecov.io/gh/vazco/uniforms/pull/580?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

radekmie: A short follow up on the internal discussion: we'd have to remove merging `props` from `GraphQLBridge.getProps` as it is in the other bridges. It's not needed there, as `BaseField.getFieldProps` is resolving it anyway. We've decided to merge this logic as it is and get back to that later.

Patch:
diff --git a/packages/uniforms-bridge-graphql/src/GraphQLBridge.js b/packages/uniforms-bridge-graphql/src/GraphQLBridge.js
index 6ff1175b..fbfe023a 100644
--- a/packages/uniforms-bridge-graphql/src/GraphQLBridge.js
+++ b/packages/uniforms-bridge-graphql/src/GraphQLBridge.js
@@ -1,5 +1,6 @@
 import * as graphql from 'graphql';
 import invariant from 'invariant';
+import lowerCase from 'lodash/lowerCase';
 
 import Bridge from 'uniforms/Bridge';
 import joinName from 'uniforms/joinName';
@@ -9,6 +10,11 @@ const extractFromNonNull = x =>
     ? { ...x, type: x.type.ofType }
     : x;
 
+const toHumanLabel = label => {
+  label = lowerCase(label);
+  return label[0].toUpperCase() + label.slice(1);
+};
+
 export default class GraphQLBridge extends Bridge {
   constructor(schema, validator, extras = {}) {
     super();
@@ -132,19 +138,21 @@ export default class GraphQLBridge extends Bridge {
       ...this.extras[nameNormal]
     };
 
+    const extract = (x, y) =>
+      x === false || x === null ? '' : x !== true && x !== undefined ? x : y;
+    const label = extract(
+      props.label,
+      extract(extra.label, toHumanLabel(field.name))
+    );
+
     const ready = {
-      label: '',
       required: field.type instanceof graphql.GraphQLNonNull,
 
       ...extra,
-      ...props
-    };
+      ...props, // TODO: GraphQLBridge.getProps shouldn't merge props.
 
-    if (props.label === true && extra.label) {
-      ready.label = extra.label;
-    } else if (props.label !== undefined && !props.label) {
-      ready.label = '';
-    }
+      label
+    };
 
     if (props.placeholder === true && extra.placeholder) {
       ready.placeholder = extra.placeholder;
diff --git a/website/components/Playground/presets.js b/website/components/Playground/presets.js
index a4d1139d..8b7bdbc4 100644
--- a/website/components/Playground/presets.js
+++ b/website/components/Playground/presets.js
@@ -90,10 +90,7 @@ const presets = {
                 throw {details};
             }
         }, {
-            city:   {label: 'City'},
-            state:  {label: 'State'},
-            street: {label: 'Street'},
-            zip:    {label: 'Zip'}
+            zip: {label: 'Zip code'}
         })
     `,
 


Test patch:
diff --git a/packages/uniforms-bridge-graphql/__tests__/GraphQLBridge.js b/packages/uniforms-bridge-graphql/__tests__/GraphQLBridge.js
index 9ce57e41..63d09ac7 100644
--- a/packages/uniforms-bridge-graphql/__tests__/GraphQLBridge.js
+++ b/packages/uniforms-bridge-graphql/__tests__/GraphQLBridge.js
@@ -226,6 +226,60 @@ describe('GraphQLBridge', () => {
   });
 
   describe('#getProps', () => {
+    describe('labels are derived properly', () => {
+      describe('when props.label is undefined or true', () => {
+        describe('and no extra data is passed', () => {
+          it('should use AST field name', () => {
+            expect(bridgeT.getProps('title')).toMatchObject({
+              label: 'Title'
+            });
+            expect(bridgeT.getProps('author.decimal1')).toMatchObject({
+              label: 'Decimal 1'
+            });
+            expect(bridgeT.getProps('author.firstName')).toMatchObject({
+              label: 'First name'
+            });
+          });
+        });
+
+        describe('and extra data is present', () => {
+          it('should use extra data', () => {
+            expect(bridgeT.getProps('id')).toMatchObject({
+              label: 'Post ID'
+            });
+            expect(bridgeT.getProps('id', { label: true })).toMatchObject({
+              label: 'Post ID'
+            });
+          });
+        });
+      });
+
+      describe('when props.label is a string', () => {
+        it('should use label from props', () => {
+          expect(
+            bridgeT.getProps('title', { label: 'Overriden title' })
+          ).toMatchObject({
+            label: 'Overriden title'
+          });
+          expect(bridgeT.getProps('title', { label: '' })).toMatchObject({
+            label: ''
+          });
+        });
+      });
+
+      describe('when props.label is false or null (unset)', () => {
+        it('should display empty label', () => {
+          expect(bridgeT.getProps('id', { label: false })).toMatchObject({
+            label: ''
+          });
+
+          expect(bridgeT.getProps('id', { label: null })).toMatchObject({
+            label: ''
+          });
+        });
+      });
+    });
+
     it('works with allowedValues', () => {
       expect(bridgeI.getProps('id')).toEqual({
         label: 'Post ID',
@@ -246,7 +300,7 @@ describe('GraphQLBridge', () => {
 
     it('works with custom component', () => {
       expect(bridgeI.getProps('author')).toEqual({
-        label: '',
+        label: 'Author',
         required: true,
         component: 'div'
       });
@@ -309,7 +363,7 @@ describe('GraphQLBridge', () => {
     it('works with placeholder (extra.placeholedr === undefined)', () => {
       expect(bridgeI.getProps('title', { placeholder: true })).toEqual({
         allowedValues: ['a', 'b', 'Some Title'],
-        label: '',
+        label: 'Title',
         placeholder: true,
         required: false,
         transform: expect.any(Function),
@@ -324,13 +378,13 @@ describe('GraphQLBridge', () => {
 
     it('works with Number type', () => {
       expect(bridgeI.getProps('author.decimal1')).toEqual({
-        label: '',
+        label: 'Decimal 1',
         required: false,
         decimal: true
       });
 
       expect(bridgeI.getProps('author.decimal2')).toEqual({
-        label: '',
+        label: 'Decimal 2',
         required: true,
         decimal: true
       });
@@ -367,7 +421,7 @@ describe('GraphQLBridge', () => {
 
     it('works with other props', () => {
       expect(bridgeI.getProps('category', { x: 1, y: 1 })).toEqual({
-        label: '',
+        label: 'Category',
         required: true,
         x: 1,
         y: 1