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

feat: dependent fields #3891

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open

feat: dependent fields #3891

wants to merge 2 commits into from

Conversation

barthc
Copy link
Contributor

@barthc barthc commented Jun 12, 2020

Closes #565

Based on comment #565 (comment), came up with the following:

  • Added support for a new field config option conditions which should hold an array of objects.
  • Each object should have a fieldPath attribute to reference the field, wildcards * are supported as well similar to relation widget attrs. And any one of equal ,notEqual , oneOf and pattern config option.

So based on the issue's OP, a config like so would work.

- label: 'structure'
  name: structure
  widget: 'list'
  fields: 
    - {label: "type", name: "type", widget: "select", default: '', options: ['copy', 'divider', 'image', 'infographic', 'productCircle']}
    - label: content
      name: content
      widget: object
      fields:
        - {label: 'text', name: 'text', widget: 'markdown', conditions: [{ fieldPath: 'structure.*.type', oneOf: ['divider', 'productCircle', infographic, image, ''] }] }
        - {label: 'src', name: 'src', widget: 'image', conditions: [{ fieldPath: 'structure.*.type', oneOf: ['divider', 'productCircle', copy, ''] }] }
        - {label: 'overlay', name: 'overlay', widget: 'image', conditions: [{ fieldPath: 'structure.*.type', oneOf: ['divider', 'productCircle', copy, image, ''] }] }

Example config that tries to mimic dynamic dropdown:

- {label: "Continent", name: "continent", widget: "select", default: '', options: ['Europe', 'Asia']}
- {label: "Europe", name: "countries_europe", widget: "select", options: ['spain', 'italy', 'france'], conditions: [{ fieldPath: 'continent', oneOf: ['Asia', ''] }] }
- {label: "Asia", name: "countries_asia", widget: "select", options: ['india', 'japan', 'china'], conditions: [{ fieldPath: 'continent', oneOf: ['Europe', ''] }] }

To do:

  • Add cypress test
  • Update docs

@barthc barthc requested a review from a team June 12, 2020 13:47
@github-actions github-actions bot added the type: feature code contributing to the implementation of a feature and/or user facing functionality label Jun 12, 2020
@erezrokah erezrokah self-requested a review June 15, 2020 12:08
Copy link
Contributor

@erezrokah erezrokah left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks again @barthc for pushing another highly requested feature.
Sorry for taking so long to reply.
Looks like the field will be hidden when the condition is met and not shown only when the condition is met. Am I correct?

Doing the following seems more intuitive to me (and matches #565 (comment)):

- {label: "Continent", name: "continent", widget: "select", default: '', options: ['Europe', 'Asia']}
- {label: "Europe", name: "countries_europe", widget: "select", options: ['spain', 'italy', 'france'], conditions: [{ fieldPath: 'continent', oneOf: ['Europe'] }] }
- {label: "Asia", name: "countries_asia", widget: "select", options: ['india', 'japan', 'china'], conditions: [{ fieldPath: 'continent', oneOf: ['Asia'] }] }

WDYT?

Using the wildcard * is great for supporting lists. I wonder if we should create a dedicated data structure to hold the tree of widgets since we already pass parentIds for the nested validation.

@barthc
Copy link
Contributor Author

barthc commented Jun 23, 2020

Looks like the field will be hidden when the condition is met and not shown only when the condition is met. Am I correct?

The field will be hidden when any(not all) of the conditions are met, I think we should change conditions config to something like hideConditions for clarity.

Doing the following seems more intuitive to me (and matches #565 (comment)):

The empty string is just to hide both Europe and Asia dropdown( and then show the appropriate dropdown as the continent dropdown value changes) for new entries since the default value of the continent dropdown is an empty string, the empty string can be omitted, it's up to the user.

Using the wildcard * is great for supporting lists. I wonder if we should create a dedicated data structure to hold the tree of widgets since we already pass parentIds for the nested validation.

Not sure how the tree structure will be useful here if you can explain with some codes, better. The fieldPath value is used directly on the entry data. The trick here is for the user to match the fieldPath correctly especially when using wildcard for list.

@erezrokah
Copy link
Contributor

The field will be hidden when any(not all) of the conditions are met, I think we should change conditions config to something like hideConditions for clarity.

With hide notation each time someone adds a select option it will require editing all other fields or the condition will break right?

With show notation you'd only need to do:

- {label: "Continent", name: "continent", widget: "select", default: '', options: ['Europe', 'Asia']}
- {label: "Europe", name: "countries_europe", widget: "select", options: ['spain', 'italy', 'france'], conditions: [{ fieldPath: 'continent', equals: 'Europe' }] }
- {label: "Asia", name: "countries_asia", widget: "select", options: ['india', 'japan', 'china'], conditions: [{ fieldPath: 'continent', equals: 'Asia' }] }

Then adding a new option:

- {label: "Continent", name: "continent", widget: "select", default: '', options: ['Europe', 'Asia','America']}
- {label: "Europe", name: "countries_europe", widget: "select", options: ['spain', 'italy', 'france'], conditions: [{ fieldPath: 'continent', equal: 'Europe' }] }
- {label: "Asia", name: "countries_asia", widget: "select", options: ['india', 'japan', 'china'], conditions: [{ fieldPath: 'continent', equal: 'Asia' }] }
- {label: "America", name: "countries_america", widget: "select", options: ['mexico', 'us'], conditions: [{ fieldPath: 'continent', equal: 'America' }] }

Not sure how the tree structure will be useful here if you can explain with some codes, better. The fieldPath value is used directly on the entry data. The trick here is for the user to match the fieldPath correctly especially when using wildcard for list.

Let me dig into that a bit more.

@barthc
Copy link
Contributor Author

barthc commented Jun 23, 2020

With hide notation each time someone adds a select option it will require editing all other fields or the condition will break right?

With show notation you'd only need to do:

Agreed.

@miguelt1
Copy link

When will this feature will be available?

@erezrokah
Copy link
Contributor

When will this feature will be available?

See my comment here #565 (comment)

@erezrokah
Copy link
Contributor

Closing this as stale. We can revisit in the future, or if someone wants to pick it up per #3891 (review)

@larenelg
Copy link

Hey there! I'm in need of this so happy to try to pick this up again from #3891 (review)

@martinjagodic martinjagodic deleted the feat/dependent-fields branch April 28, 2023 13:34
@bobgravity1
Copy link

sooo is this not a feature? seems like a really basic need for ANY CMS lol

@martinjagodic martinjagodic restored the feat/dependent-fields branch May 18, 2023 12:41
@martinjagodic martinjagodic reopened this May 18, 2023
@martinjagodic
Copy link
Member

@larenelg I reopened this PR if you're still interested

@mmkal
Copy link
Contributor

mmkal commented May 18, 2023

Could this use case be solved by making variable types work for object widgets? That's effectively what we do, by having lists with min and/or max set to 1.

It would have the benefits of being (presumably) easier to implement/maintain and easier to understand by users.

@martinjagodic
Copy link
Member

@mmkal can you explain how that would enable the conditional feature? Maybe with an example? Thanks

@mmkal
Copy link
Contributor

mmkal commented May 19, 2023

Sure, I'll have to adjust the original example, because the example from the OP, and proposed solution, is already a list so can just use variable types right now: #565 (comment)

- label: structure
  name: structure
  widget: list
  types:
     - name: copy
       widget: object
       fields:
       - name: text
         widget: markdown
     - name: productCircle
       widget: object
       fields:
       - name: text
         widget: markdown
       - name: src
         widget: image
     - name: infographic
       widget: object
       fields:
       - name: text
         widget: markdown
       - name: src
         widget: image
       - name: overlay
         widget: image
       ...

I think this is much easier to follow when looking at the config too. Answering the question "what subfields does an infographic have" doesn't involve darting around and looking at all of the condition labels, and mentally parsing the custom condition syntax (which has to be learned and remembered).

Worth noting, there is one slight downside which is that fields in common between the various types have to be defined explicitly on each. That can be solved fairly easily with yaml references (or my team generate the config from typescript anyway so we can use helper methods and strong types), but also more importantly, by the same token, it allows for the different types to independently define their fields. markdown on productCircle might have a different hint than markdown on infographic.

So, IMO the original issue could be closed with a suggestion like this. But if someone wanted to make a non-list field like this, that is a missing feature. It'd be nice to be able to do the same thing for objects, e.g.:

- label: structure
  name: structure
  widget: object # 👈 not supported right now, only `widget: list` is 
  types:
    - name: copy
      widget: object
      fields:
        - name: text
          widget: markdown
    - name: productCircle
      widget: object
      fields:
        - name: text
          widget: markdown
        - name: src
          widget: image
    - name: infographic
      widget: object
      fields:
        - name: text
          widget: markdown
        - name: src
          widget: image
        - name: overlay
          widget: image
      ...

I think the use case for that is smaller, but it could make sense on folder collections - different entries could use different structures with the example above.

@martinjagodic
Copy link
Member

Thanks for the comment @mmkal. This is a feature that has been discussed many times with many proposed solutions, so I would like to take some time with the team to carefully review them and decide where we want to go. It is one of the most requested features, so it's important not to rush.

@patrulea
Copy link

any updates? come on

@mmkal
Copy link
Contributor

mmkal commented Jan 11, 2024

Here's something we've started doing, along the lines of my above comment, but also adds support for objects. Basically, just inspect the field and use a shimmed list widget control instead of object when the types field is defined:

import {List} from 'immutable'
import React from 'react'
import CMS from 'decap-cms-app'

export function optionalizeObject() {
  const listWidget = CMS.getWidget('list')
  const objectWidget = CMS.getWidget('object')

  class NewObjectControl extends React.Component<any> {
    render() {
      if (this.props.field.get('required') === false || this.props.field.get('types')) {
        return (
          <listWidget.control
            {...this.props}
            onChange={(e: List<any>) => this.props.onChange(e.get(0))}
            value={List([this.props.value].filter(Boolean))}
          />
        )
      }
      return <objectWidget.control {...this.props} />
    }
  }

  CMS.registerWidget('object', NewObjectControl, listWidget.preview)
}

Here's a demo:

collection definition:

{
  label: 'test test',
  name: 'test',
  folder: 'shared/content/src/data/marketing/test',
  slug: '{{slug}}',
  format: 'yml',
  create: true,
  fields: [
    {
      name: 'some_string_field',
      widget: 'string',
    },
    {
      name: 'structure',
      widget: 'object',
      types: [
        {
          name: 'copy',
          widget: 'object',
          fields: [
            {name: 'text', widget: 'markdown'},
          ],
        },
        {
          name: 'productCircle',
          widget: 'object',
          fields: [
            {name: 'text', widget: 'markdown'},
            {name: 'src', widget: 'image'},
          ],
        },
        {
          name: 'infographic',
          widget: 'object',
          fields: [
            {name: 'text', widget: 'markdown'},
            {name: 'src', widget: 'image'},
            {name: 'overlay', widget: 'image'},
          ],
        },
      ],
    }
  ]
},

How it renders in the UI:

Screen.Recording.2024-01-11.at.11.25.01.AM.mov

Files generated by the above:

image

Bonus: this also improves support for object widgets with required: false - they become "lists" too, visually - the add/remove buttons define the object and set it to null respectively. Before this, we had trouble with objects that were optional, but had required subfields, when defined.


In the spirit of not adding complexity to both the product and the codebase, I think the above would be a better solution than this pull request, so I would propose the following changes instead:

  1. Make the above shim built into decapcms, so it doesn't have to be done in userland. This should be easy to do, and improves support for optional object, for all users, as a bonus.
  2. Allow for types to be define on folder collections, instead of requiring fields. Without this, there's a limitation of the above method that you can't have the type field on the top level (i.e. it has to be {structure: {type: copy, text: abc}} and can't be {type: copy, text: abc}. If we're lucky this wouldn't be too hard to achieve either.

1 can be done first, then 2. I'd be happy to open a PR.

@martinjagodic what do you think?

@martinjagodic
Copy link
Member

@mmkal I like this a lot! It achieves a lot with very little intervention. A PR for solution 1 would be amazing.

@mmkal mmkal mentioned this pull request Jan 12, 2024
1 task
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: feature code contributing to the implementation of a feature and/or user facing functionality
Projects
None yet
Development

Successfully merging this pull request may close these issues.

dependent fields in collection
8 participants