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

Initial support for property & schema dependencies #659

Merged
merged 7 commits into from
Oct 23, 2017

Conversation

mfulton26
Copy link
Contributor

@mfulton26 mfulton26 commented Aug 5, 2017

Reasons for making this change

JSON Schema supports property and schema dependencies which allow changing the schema for an object (even a single object within an array of objects) based on the presence or value of certain properties.

This change brings initial support for property dependencies and schema dependencies which allows developers to define conditionally required fields, conditionally present fields, and dynamic schemas (e.g. changing a property type based on the value of another field).

See #616 and #430 (see also #522).

Checklist

This is a work in progress but I am looking for some initial feedback as I have not contributed to this project before and I am looking for early feedback on the implementation, etc.

  • I'm updating documentation
    • I've checked the rendering of the Markdown text I've added
    • If I'm adding a new section, I've updated the Table of Content
  • I'm adding or updating code
    • I've added and/or updated tests
    • I've updated docs if needed
    • I've run npm run cs-format on my branch to conform my code to prettier coding style
  • I'm adding a new feature
    • I've updated the playground with an example use of the feature

@ksjogo
Copy link

ksjogo commented Aug 16, 2017

Using that in my project now. Works great. Happy to provide any needed help for merging this.

@faassen
Copy link
Contributor

faassen commented Aug 18, 2017

I've seen it argued in other issues such as #430 that instead of using schema dependencies you can dynamically change the schema when data changes. This is indeed very powerful. But I've added a note there with some use cases for dependencies.

Copy link
Collaborator

@n1k0 n1k0 left a comment

Choose a reason for hiding this comment

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

I really like this, thanks for the effort put in this PR. Here are some preliminary remarks before getting further into the patch.

src:
"https://spacetelescope.github.io/understanding-json-schema/reference/object.html#dependencies",
description:
"In the following example, whenever a `credit_card` property is provided, a `billing_address` property must also be present.",
Copy link
Collaborator

Choose a reason for hiding this comment

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

Markdown is not supported, so backticks won't turn into <code> blocks, though you could use JSX instead of a string here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh interesting, I didn't think of that. I'll take a look. Thank you.

const itemSchema = retrieveSchema(
schema.items,
definitions,
formData[index]
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nit: here and in some other places, this could be just item.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point, I felt like I was "hacking" a bit as I am still familiarizing myself with the code base. Thank you for the feedback. I will update.

definitions,
formData
);
} else if (schema.hasOwnProperty("dependencies")) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

How about extracting dependency handling logic into its own dedicated function? This whole retrieveSchema one is getting huge.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was just thinking of this this morning when I started thinking about this pull request again due to others commenting on the pull request. I will see if I can have some updates up today or tomorrow. Thank you.

src/utils.js Outdated
// Drop the dependencies from the source schema.
let { dependencies, ...localSchema } = schema;
// Process dependencies updating the local schema properties as appropriate.
for (const key of Object.keys(dependencies)) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nit: why not just for (const key in dependencies)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Because I'm an EcmaScript noob? 😄

src/utils.js Outdated
true
)
);
if (Array.isArray(value.oneOf)) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Hmm so this introduces partial support for oneOf while fields are still not handled at all, I'd have preferred introducing this after we get proper support for oneOf.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I'm still not certain how oneOf would work in general but I kind of view the usage here as special. The idea is that depending on values in formData the overall schema will dynamically update to one of several options but this still doesn't support a user selecting one of several schemas to interact with as one would need if using oneOf elsewhere in a schema. I have started updating README.md locally and I am planning on explaining just this as implementing support for this kind of oneOf is very independent of general support for oneOf (and vice versa I believe although I could be wrong).

Copy link
Collaborator

Choose a reason for hiding this comment

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

Right, we may just want to document this specific behavior here.

src/utils.js Outdated
};
const { errors } = jsonValidate(formData, conditionSchema);
if (errors.length === 0) {
const dependentSchema = { ...subschema };
Copy link
Collaborator

Choose a reason for hiding this comment

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

Beware as cloning this way isn't recursive.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Correct. I am just trying to not mutate the incoming schema at all so I make copies as needed in the tree instead of recursively coping the entire tree (although I suppose the later could be done for readability/maintainability reasons although I'm not sure how to do that off the top of my head).

Copy link
Collaborator

@n1k0 n1k0 Aug 18, 2017

Choose a reason for hiding this comment

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

Deep copying objects is hard, Json serial/deserialization is known to be quite efficient but maybe not enough here as this operation may be executed on each keystroke in a field with this lib.

src/utils.js Outdated
// Skip this dependency if its trigger property is not present.
if (!formData[key]) {
// fixme: a falsey check may not be appropriate here, I'm not sure
continue;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Yeah, maybe just check for undefined I think.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm, I'll try that but I may already have done so. If a field is left empty does it come in as undefined in the formData or as an empty string ""? I guess I will find out. :-)

Copy link
Collaborator

Choose a reason for hiding this comment

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

AFAICR it depends if the field value has been edited already or not; by default a schema string field value is set to undefined (property is not preset), but as soon as users manipulate the widget, eg. typing then erasing the entered value, it's set to an empty string. So yeah, it might be a little tricky.

});
});
});
describe("true condition", () => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nit: empty line between describe statements.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It can be done! 😄

Copy link
Contributor Author

Choose a reason for hiding this comment

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

FYI: I appreciate the "Nit" comments. Every little bit counts.

Copy link
Contributor Author

@mfulton26 mfulton26 left a comment

Choose a reason for hiding this comment

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

Thank you for the comments so far @n1k0. I very much appreciate it.

src/utils.js Outdated
// Process dependencies updating the local schema properties as appropriate.
for (const [key, value] of Object.entries(dependencies)) {
// Skip this dependency if its trigger property is not present.
if (!formData[key]) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I looked but I have not yet found a more proper way to determine if a field is defined. This is a "falsey" check which may not be appropriate. If someone could give me a suggestion on what to use instead that would be very much appreciated. Thank you.

src:
"https://spacetelescope.github.io/understanding-json-schema/reference/object.html#dependencies",
description:
"In the following example, whenever a `credit_card` property is provided, a `billing_address` property must also be present.",
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh interesting, I didn't think of that. I'll take a look. Thank you.

const itemSchema = retrieveSchema(
schema.items,
definitions,
formData[index]
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point, I felt like I was "hacking" a bit as I am still familiarizing myself with the code base. Thank you for the feedback. I will update.

definitions,
formData
);
} else if (schema.hasOwnProperty("dependencies")) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was just thinking of this this morning when I started thinking about this pull request again due to others commenting on the pull request. I will see if I can have some updates up today or tomorrow. Thank you.

src/utils.js Outdated
// Drop the dependencies from the source schema.
let { dependencies, ...localSchema } = schema;
// Process dependencies updating the local schema properties as appropriate.
for (const key of Object.keys(dependencies)) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Because I'm an EcmaScript noob? 😄

src/utils.js Outdated
// Skip this dependency if its trigger property is not present.
if (!formData[key]) {
// fixme: a falsey check may not be appropriate here, I'm not sure
continue;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm, I'll try that but I may already have done so. If a field is left empty does it come in as undefined in the formData or as an empty string ""? I guess I will find out. :-)

src/utils.js Outdated
true
)
);
if (Array.isArray(value.oneOf)) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I'm still not certain how oneOf would work in general but I kind of view the usage here as special. The idea is that depending on values in formData the overall schema will dynamically update to one of several options but this still doesn't support a user selecting one of several schemas to interact with as one would need if using oneOf elsewhere in a schema. I have started updating README.md locally and I am planning on explaining just this as implementing support for this kind of oneOf is very independent of general support for oneOf (and vice versa I believe although I could be wrong).

src/utils.js Outdated
};
const { errors } = jsonValidate(formData, conditionSchema);
if (errors.length === 0) {
const dependentSchema = { ...subschema };
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Correct. I am just trying to not mutate the incoming schema at all so I make copies as needed in the tree instead of recursively coping the entire tree (although I suppose the later could be done for readability/maintainability reasons although I'm not sure how to do that off the top of my head).

});
});
});
describe("true condition", () => {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

It can be done! 😄

});
});
});
describe("true condition", () => {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

FYI: I appreciate the "Nit" comments. Every little bit counts.

Copy link
Collaborator

@n1k0 n1k0 left a comment

Choose a reason for hiding this comment

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

This looks generally very good to me, though I fear missing something important. I'd like more feedback and reviews from people like @olzraiti @mfulton26 @faassen or @glasserc before giving final approval.

src/utils.js Outdated
} else {
// No $ref or dependencies attribute found, returning the original schema.
return schema;
}
}

function resolveDependencies(schema, definitions, formData) {
// Drop the dependencies from the source schema.
let { dependencies, ...resolvedSchema } = schema;
Copy link
Collaborator

Choose a reason for hiding this comment

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

We should probably default dependencies to an empty object to avoid throwing on Object.keys(undefined), what do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sounds good to me.

src/utils.js Outdated
// Drop the dependencies from the source schema.
let { dependencies, ...resolvedSchema } = schema;
// Process dependencies updating the local schema properties as appropriate.
for (const key of Object.keys(dependencies)) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nit: this could still be for..in

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep, I missed that when I moved things around before.

src/utils.js Outdated
return schema;
}
schema = { ...schema };
schema.required = Array.isArray(schema.required) ? [...schema.required] : [];
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nit: Would be so much nicer to have a way to properly clone the whole schema in a single call... maybe just introduce a simple clone function in utils leveraging JSON.parse(JSON.stringify(obj)), so we'll have a single generic reference impl. we'll be able to tweak whenever required.

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't really feel that cloning is necessary here, especially if you replace the for loop below with a filter call. You could even take it a step further and use a Set.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think using a Set makes for a good solution. I'll try that for now.

src/utils.js Outdated
continue;
}
schema.required.push(property);
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nit: this for loop looks like it could be more legibly replaced by a Array#filter call. Or maybe not, we should at least try.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

With the Set implementation this actually goes away. 👍

Copy link
Contributor

@glasserc glasserc left a comment

Choose a reason for hiding this comment

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

Hi, thanks for tagging me in on this @n1k0 ! It's a great PR, thoroughly documented and adding tests, both of which are great signs.

My biggest concern here is that before this PR, rjsf doesn't support oneOf at all, but after this PR, rjsf "kind of, sort of" supports oneOf under certain very restrictive conditions. Those conditions aren't really expressed anywhere, either. I imagine this behavior is important to you because the PR would be much simpler without it. On the other hand, this lets us explore oneOf in a simpler way without having any questions of what UI would be necessary to let the user change to an alternative schema. I guess I'm OK including this behavior if we document exactly what it does and when. (Something explaining that you can use oneOf to handle more complicated cases, and how we examine oneOf alternatives one at a time, comparing just the initial property against the definition of that property in each alternative, and adding the rest of the schema if it matches.)

I'm a little concerned about weird recursive cases, like what do we do with

{
  "type": "object",
  "properties": {
    "a": {"enum": ["a1", "a2"]},
    "b": {"enum": ["b1", "b2"]}
  },
  "dependencies": {
    "a": {
      "oneOf": [
        {"properties": {
          "a": {"enum": ["a1"]}, 
          "b": {"enum": ["b1"]}
        }},
        {"properties": {
          "a": {"enum": ["a2"]}, 
          "b": {"enum": ["b2"]}
        }}
      ],
    "b": {
      "oneOf": [
        {"properties": {
          "b": {"enum": ["b1"]},
          "a": {"enum": ["a2"]}
        }},
        {"properties": {
          "b": {"enum": ["b2"]},
          "a": {"enum": ["a1"]}
        }}
      ]
    }
  }
}

... but maybe this isn't a fair criticism, because there are certainly an infinite number of invalid JSON schemas that we can't really expect to detect or handle.

I nitpicked a few of your function names. To me resolve carries a meaning similar to reduce -- to take one thing and simplify some aspect of it somehow. But I haven't really been involved with the maintenance of this project much lately so feel free to disregard my opinion on this subject. 😳

src/utils.js Outdated
return schema;
}
schema = { ...schema };
schema.required = Array.isArray(schema.required) ? [...schema.required] : [];
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't really feel that cloning is necessary here, especially if you replace the for loop below with a filter call. You could even take it a step further and use a Set.

src/utils.js Outdated
return resolvedSchema;
}

function resolvePropertyDependency(schema, properties) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't really like this name for this function. How about addRequired ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't like it either but I couldn't think of something better at the time. 😄

I'm actually wonder if withDependentProperties would make sense. That way it is clear that this function returns a new schema object with the additional dependent properties (in the required property which is how property dependencies work).

Thoughts?

src/utils.js Outdated
return schema;
}

function resolveConditionalSchema(schema, definitions, formData, value) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't like this function name either. How about mergeSchemas ? (Changing the value parameter to something like additionalSchema or even conditionalSchema.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agreed.

src/utils.js Outdated
const {
[key]: conditionProperty,
...dependentProperties
} = subschema.properties;
Copy link
Contributor

Choose a reason for hiding this comment

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

I had to stare at this for a little while trying to figure it out.. maybe rename dependentProperties to propertiesToAdd, and/or add a comment like Remove key from the properties we will merge in. Instead, use it as a condition?

src/utils.js Outdated
const { errors } = jsonValidate(formData, conditionSchema);
if (errors.length === 0) {
const dependentSchema = { ...subschema };
dependentSchema.properties = dependentProperties;
Copy link
Contributor

Choose a reason for hiding this comment

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

You can do this in one line: const dependentSchema = { ...subschema, properties: dependentProperties };

src/utils.js Outdated
function resolveConditionalSchema(schema, definitions, formData, value) {
return retrieveSchema(
mergeObjects(schema, retrieveSchema(value, definitions, formData), true)
);
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is the outer retrieveSchema necessary? (A comment might help.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It might not be... I'l do some testing and see. I think I was too liberal with calling retrieveSchema

src/utils.js Outdated
schema = mergeObjects(
schema,
retrieveSchema(dependentSchema, definitions, formData)
);
Copy link
Contributor

Choose a reason for hiding this comment

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

Is an outer retrieveSchema necessary here? Alternately, should we use mergeSchema (above)?

Copy link
Contributor

Choose a reason for hiding this comment

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

(Oops, I posted this comment on the wrong line before...)

I added a test justifying the need for a call to retrieveSchema, plus fixed a minor bug that such a test uncovered.

src/utils.js Outdated
schema = mergeObjects(
schema,
retrieveSchema(dependentSchema, definitions, formData)
);
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we break once we find a oneOf that matches?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This probably actually needs to filter all of the subschemas and confirm that only one is valid. I will update.

src/utils.js Outdated
);
if (Array.isArray(oneOf)) {
schema = resolveDynamicSchema(schema, definitions, formData, key, oneOf);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

If oneOf isn't an array, what is the correct behavior? I don't think silent failure is correct..

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm, I agree. I haven't looked yet throughout this project. Do we ever display to the user that they have created a bad JSON schema? Or do we log warnings for this kind of thing in the console?

Copy link
Contributor

Choose a reason for hiding this comment

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

I've changed this to throw, which we appear to do for some schemas that are too dysfunctional to work. (For example, getDefaultFormState throws on non-object schemas.)

@mfulton26
Copy link
Contributor Author

@glasserc I took a quick glance at your input and I totally agree I've just been pretty busy this week with other things. I should be able to make some updates soon though. Thanks!

@mfulton26
Copy link
Contributor Author

Things I still need to do:

  • update README.md to explain special oneOf usage in dependencies only
  • address "Warning: Failed prop type: The prop id is marked as required in BaseInput, but its value is undefined."
  • re-review previous comments and code and see what else can be cleaned up

Thank you for the feedback, please feel free to reiterate and/or provide additional feedback as some of the decisions I've made in an attempt to "improve" things might not have been the best decisions. 😄

@mfulton26
Copy link
Contributor Author

Hmmm, I'm not sure why the build is working but I'm not going to look at this again until at least tomorrow so it can wait until at least then.

- Property dependencies: Make additional properties required whenever
  a trigger property is specified.
- Schema dependencies: Dynamically add schema properties based on data
  entered.
@mfulton26
Copy link
Contributor Author

mfulton26 commented Oct 23, 2017

Okay, I think this ready to be reviewed again.

@glasserc
Copy link
Contributor

I took the liberty of adding a little bit of documentation to the README explaining in the vaguest possible terms how this works and fixed up a few stylistic nits that bothered me. The tests seem to pass, so I'm merging it. Thanks so much for your work on this!

@glasserc glasserc merged commit 132a6cc into rjsf-team:master Oct 23, 2017
src/utils.js Outdated
@@ -422,7 +422,7 @@ function resolveDependencies(schema, definitions, formData) {
// Process dependencies updating the local schema properties as appropriate.
for (const dependencyKey in dependencies) {
// Skip this dependency if its trigger property is not present.
if (formData[dependencyKey] === undefined) {
if (!formData.hasOwnProperty(dependencyKey)) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@glasserc Unfortunately it looks like this change broke properties and schema dependencies. With the change conditionally required properties are always marked as required (with an asterisk (*)) and conditionally declared properties are always shown. See the playground for the examples.

Note that the dynamic schema still works correctly with the specific oneOf case.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm surprised this passed the tests... I must not have written very good ones. 😄

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, that's unfortunate.. the way we computeDefaults fills missing fields with undefineds. I don't like that design but it's outside the scope of this PR. Thanks for letting me know that I broke it :)

@mfulton26
Copy link
Contributor Author

Awesome. I'm happy you've made the additions. This is really great and thank you all for supporting the change!

@mfulton26
Copy link
Contributor Author

I think the hasOwnProperty change may have caused a hiccup though. Please see my comment on your commit. Thank you.

@mfulton26 mfulton26 deleted the dependencies branch October 24, 2017 01:38
@mfulton26
Copy link
Contributor Author

Just out of curiosity, how soon might we be able to get the latest version deployed to the playground? Thanks!

@cdtinguria
Copy link

Guys, very nice feature to have. Any idea when this will be released?

@mfulton26
Copy link
Contributor Author

@glasserc This appears to be published to npm. I'm curious how we get the playground updated. Do you know? Thanks.

@glasserc
Copy link
Contributor

glasserc commented Nov 1, 2017

Sorry, I forgot to push it to the playground. It should be up now.

@mfulton26
Copy link
Contributor Author

Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants