bhch / django-jsonform

A better, user-friendly JSON editing form field for Django admin. Also supports Postgres ArrayField.
https://django-jsonform.rtfd.io
BSD 3-Clause "New" or "Revised" License
307 stars 31 forks source link

Can not select option from list #126

Closed maciejpolanczyk closed 7 months ago

maciejpolanczyk commented 9 months ago

Problem exists when two objects from the lists are dicts with the same names of the fields. If you add additional field to one of the dicts or change name of at least one of the fields in one of the dicts, all works fine. Schema to reproduce the problem (you can select A or C but you can not select B, you will be "redirected" to A):

def get_schema() -> dict:
    return {
        'type': 'array',
        'title': 'Choices',
        'items': {
            'anyOf': [
                {'title': 'A', '$ref': '#/$defs/a'},
                {'title': 'B', '$ref': '#/$defs/b'},
                {'title': 'C', '$ref': '#/$defs/c'},
            ]
        },
        '$defs': {
            'a': {
                'type': 'object',
                'description': 'Description for A',
                'keys': {
                    'number': {'type': 'integer', 'minimum': 1, 'default': 1},
                },
            },
            'b': {
                'type': 'object',
                'description': 'Description for B',
                'keys': {
                    'number': {'type': 'integer', 'minimum': 1, 'default': 1},
                },
            },
            'c': {
                'type': 'object',
                'description': 'Description for C',
                'keys': {
                    'value': {'type': 'integer', 'minimum': 1, 'default': 1},
                },
            },
        },
    }

Used: django = "==4.2.3" django-jsonform = "==2.19.1" chrome 116.0.5845.187 (Official Build) (x86_64)

bhch commented 9 months ago

Even though you manually select Option B, the UI generator doesn't generate the form UI directly for the selected option.

What really happens is when you select an option, in the background the widget first updates the underlying form data. Then the UI generator looks at the data and tries to find a match for that data from the available schema options.

Since a and b, both, have the same key called number and of the same type integer, so data like {"number": 1}, will always get matched to the first found option (i.e. Option A).


But what is the purpose of having two subschemas with same keys, types, etc? Aren't a and b effectively describing the same object?

maciejpolanczyk commented 9 months ago

Thanks for explanation.

They are similar from dev point of view but they are not the same in the business meaning. The meaning of A is different from the meaning of B. So although they look the same, I need user to be able to choose between A and B.

bhch commented 9 months ago

I think the best way to solve cases like this is to have some unique hidden keys to differentiate the schema.

I'll add support for constants values which will allow doing this:

"$defs": {
    "a": {
        "keys": {
+           "option_id": {"const": "a", "widget": "hidden"},
            "number": {"type": "integer", "minimum": 1, "default": 1}
        }
    },
    "b": {
        "keys": {
+           "option_id": {"const": "b", "widget": "hidden"},
            "number": {"type": "integer", "minimum": 1, "default": 1}
        }
    }
}

If have you some other ideas, please let me know.

maciejpolanczyk commented 9 months ago

Best would be to make it without any additional fields, for example distinguish base on "a" and "b" keys in $defs. If this is not possible and the only way is to use option_id please add it to the docs in the way that everyone will easily notice it, to avoid loosing time on debugging the issue which is already solved.

bhch commented 9 months ago

for example distinguish base on "a" and "b" keys in $defs.

This won't work because the form data will not have any information about the keys.

This is the data that your schema will result in:

[
    {"number": 1},
    {"number": 2},
]

May I ask how you will know on the server side which item belongs to which option since it has no keys?

maciejpolanczyk commented 9 months ago

I use additional key for each of them: 'name': {'type': 'string', 'widget': 'hidden', 'default': 'a'}

bhch commented 9 months ago

I will provide support for the const keyword which will solve this issue.

Something like: 'name': {'const': 'a', 'widget': 'hidden'}.

It will be released in the next version.

bhch commented 7 months ago

Support for const keyword has been added in v2.20.0.

I believe Using a constant value will solve this issue. Please upgrade and test it out.

Thank you.

MooseV2 commented 7 months ago

Is there any way to resolve the problem when using additionalProperties?

{
  "type": "dict",
  "keys": {},
  "flexfield": {
    "anyOf": [
      {"type": "string", "title": "String"},
      {"type": "number", "title": "Number"},
      {"type": "list", "title": "Number List", "items": {"type": "number"}},
      {"type": "list", "title": "String List", "items": {"type": "string"}},
      {"type": "dict", "title": "Dictionary", "additionalProperties": {"$ref": "#/flexfield"}}
    ]
  },
  "additionalProperties": {
    "$ref": "#/flexfield"
  }
}

I want to have a flexible schema so that I can use the form controls to create, for example:

{
    "list_a": [1, 2, 3, 4, 5],
    "list_b": ["A", "B", "C"]
}

However, this doesn't seem possible since both "Number List" and "String List" are of type list (even though the contained data types are different).

The only workaround I found is to replace "Number List" and "String List" with a single list type with another anyOf inside, but that means I have to select the data type for every list element, and it can accidentally make a list like:

{
   "list_c": [1, "B", 2, "C"]
}
bhch commented 7 months ago

@MooseV2 You can set a default value for the string list with minItems to 1.

{
    "type": "list", 
    "title": "String List", 
    "items": {"type": "string"},
    "default": [""],
    "minItems": 1 
}