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

Really custom messages are impossible #546

Closed
ivan-kleshnin opened this issue Feb 7, 2015 · 48 comments
Closed

Really custom messages are impossible #546

ivan-kleshnin opened this issue Feb 7, 2015 · 48 comments
Assignees
Labels
feature New functionality or improvement
Milestone

Comments

@ivan-kleshnin
Copy link

I want to stick "Passwords don't match" message to verifyPassword field.

First attempt:

verifyPassword: Joi.any().valid(Joi.ref('password')).options({language: {any: {allowOnly: "Passwords don't match"}}}) 

Failed. Result is "verifyPassword Passwords don't match". Joi prefixes message with label and it seems there is no way to turn it off.

Second attempt:

verifyPassword: Joi.any().valid(Joi.ref('password')).options({language: {any: {allowOnly: "Passwords don't match"}}}).label('')

Failed. "smart" check inside Joi prohibits empty labels...

@Marsup
Copy link
Collaborator

Marsup commented Feb 7, 2015

This is intended because the message is supposed to provide context for consumers of the API, not to be presented to end-users.

@agraddy
Copy link

agraddy commented Feb 8, 2015

I agree with @ivan-kleshnin. It would really help if Joi took into account front-end uses. I use Joi to help power front-end validation, and it's always frustrating to see responses in Joi discussions that simply say "That's a front-end issue; Joi isn't meant for front-end validation so it's not getting added."

On the front page, Joi is described as an "Object schema description language and validator for JavaScript objects." It does not mention anywhere that Joi is only designed for API uses. This creates a disconnect and frustration when a developer starts off with Hapi and uses what they think is a "validator for Javascript objects" but when they start browsing issues find out it's actually a "validator for Javascript objects used in an API."

To my knowledge there is not a front-end validation library for Hapi so Joi is used by front-end Hapi developers. Is there another validation library for the front-end that I'm missing? What do you recommend for front-end validation? Why is there such a strong aversion to adding a simple addition like the "customMessage" mentioned in the other thread that would greatly benefit front-end Hapi/Joi developers?

@ivan-kleshnin
Copy link
Author

@agraddy, oh man I share your pain...

Validation is one of the truly platform agnostic processes. It's a great opportunity to reuse existing knowledge (rare luxury). We want to use the same language, the same library everywhere being it frontend, backend, hybrid, react native or whatever comes whenever it's possible. Memory footprint is a most valueable resource.

No single validation aspect is platform bound. Those "messages" issue is a cake to solve.

But as Joi developers insist on support "API-only" users and are deaf to multiple requests to change it... Well maybe it's time to reconsider library choice. I'm perfectly sure I don't want to use multiple validation libraries in single project just for the matter of syntax and stupid limitations.

@hueniverse
Copy link
Contributor

Seems reasonable to add some sort of message prefix that will indicate "don't add context". Something like !!.

@Marsup
Copy link
Collaborator

Marsup commented Feb 8, 2015

I'm not saying it can't be done, I'm saying the reason why you want it is bogus to me. I know from experience that this will never be enough, we'll have to add i18n to joi to satisfy everyone, and that's something that'll probably never be part of joi.

Now ignoring the reason for that request, yes removing the prefix is technically perfectly achievable, I'll get on it.

@Marsup Marsup added request and removed discussion labels Feb 8, 2015
@Marsup Marsup added this to the 6.0 milestone Feb 8, 2015
@ivan-kleshnin
Copy link
Author

Couldn't disagree more with your reasoning but I'll better skip on this.

@Marsup
Copy link
Collaborator

Marsup commented Feb 8, 2015

Disagree on what ? The need for i18n or the fact that I don't see i18n coming to joi ?

@ivan-kleshnin
Copy link
Author

I didn't say a word about i18n. You were given enough arguments for message control in all related threads, including this one, which are boring to repeat. I18n should be in client code. Mechanism to change messages belongs to library.

@Marsup
Copy link
Collaborator

Marsup commented Feb 8, 2015

That's a real pleasure to argue with you. Wanting custom messages and not considering i18n is very short sighted, anyway it's almost done.

@Marsup Marsup closed this as completed in efa01ae Feb 8, 2015
@hueniverse
Copy link
Contributor

@ivan-kleshnin the technical details of your request are fine (removing the automatically added prefix) but the reason is not. You want joi to manage your client interface and @Marsup's point is that this change will not accomplish that.

No reason to make this discussion unpleasant. We're resolving it the best we can.

@ivan-kleshnin
Copy link
Author

@hueniverse, my reason was actually quite else but that somehow lost in discussion. Thanks anyway.

@ivan-kleshnin
Copy link
Author

I'll make a fourth try to explain.

You guys make a serie of assumptions. Every one of them is dubious.

  1. Joi was designed for API, so it must be used only for API ever.
  2. Joi intended usage (API) will never require i18n / custom messaging.
  3. Custom messaging is out of scope of validation library.
  4. Custom messaging always require i18n implementation on library level.

Compromise solution is quite easy.
Joi imaginary customMessage function accepts a function from user, which just get called with all your placeholders filled. In this way you transfer all pluralization and internationalization problems to client code. Any message may be assembled there.

Then argument
"that's impossible, because its for API"
becames
"this is not so convenient because it was designed for API at first"
which is much more user friendly.

@Marsup
Copy link
Collaborator

Marsup commented Feb 9, 2015

I've never said it was designed for API, I've said messages shouldn't be forwarded to end-users as is, mainly for i18n reasons, and I maintain that i18n can't be part of the core joi features, it doesn't mean it can't be i18n-friendly.

What's keeping your from making a utility function to fulfill your need ? Something like this :

function validate (schema, object) {
  var result = schema.validate(object);

  if (result.errors) {
    // You have everything Joi uses for messages at your disposal in result.errors.details
    return i18n.transform(result.errors);
  }

  return true;
}

If this is not enough I'll try to think of something else.

@ivan-kleshnin
Copy link
Author

Thanks, I'll try to experiment with this approach.

@agraddy
Copy link

agraddy commented Feb 9, 2015

Wow, it seems to have gotten heated in here for a bit. I think this is an important discussion. This is a busy week for me, but I'm going to try to put together some thoughts later in the week.

Thanks everybody for taking the time to work through issues.

@Marsup
Copy link
Collaborator

Marsup commented Feb 9, 2015

Now that it's possible to remove the prefix, what do you think is missing ?

@dan-dr
Copy link

dan-dr commented Feb 15, 2015

Hmm, not yet had the chance to use Joi, but it looks like it's exactly what I need.
Regarding the discussion, I tend to agree with the library authors.

On the other hand maybe I would allow passing custom numeric codes for errors.

verifyPassword: Joi.any().valid('pass').options({ errCode: 5 })

and put it in the err object.
then in the front-end you could have simple mapping object of errorMessages[errCode]
also eliminates any need for i18n

@Marsup
Copy link
Collaborator

Marsup commented Feb 15, 2015

Errors already contain a property details which contains some kind of error codes, I think the confusion comes from the fact that hapi strips it.

@dan-dr
Copy link

dan-dr commented Feb 15, 2015

some kind?
is it documented?

@Marsup
Copy link
Collaborator

Marsup commented Feb 15, 2015

Nope but probably should be, the shape is the following :

 {
   message: 'failure message',
   path: 'dotted path to the property that failed',
   type: 'type of failure',
   context: 'object with some context, depends on the type of check'
 }

I've yet to find a good way to document it.

@agraddy
Copy link

agraddy commented Feb 18, 2015

Sorry for the block of text and code. The more I've thought about the issue, I think the problems arise from the way Hapi and Joi interface (similar to @Marsup's last couple messages). On it's own, Joi works well as a validation library. However, when it gets paired with Hapi and the way Hapi uses validation, some confusion gets presented.

A quick preface, one of the reasons I chose to start working with Hapi was because of it's simple and straightforward validation functionality. Working with Joi validation in Hapi is a breath of fresh air compared to other frameworks. The problem is that there are still a few sharp edges in the interface.

I think a lot of developers when they come across one of these sharp edges assume "Well, this is a Joi issue" so we create Joi issues about messaging that end up with responses like "It seems to me you are hoping this message would end up somewhere on the front-end, Joi is not for that." (@Marsup - #484 (comment) ) and "The problem is that you are trying to use joi for direct end-user interaction and it is not what it was designed for. It is for API input validation." (@hueniverse #193 (comment) ).

It's clear that the maintainers have a clear purpose for Joi in mind but that purpose is not clearly communicated to users (other than saying "that's not what's its for" in issues). No where in the Joi readme does it state that the purpose of Joi is for "API input validation" or that Joi is not for messages that appear "on the front-end". As a Hapi user, when I read messages like that, it gets really confusing/frustrating because all the Hapi/Joi documentation and tutorials I see seem to indicate that Joi should be used to display end user information. It seems to me that either the messaging on how to achieve end-user errors should be improved or Hapi and/or Joi need to be improved to eliminate the sharp edges.

I think the easiest way to explain where I'm coming from is to provide a couple simple examples. For the first one, let's assume that I want to create a pirate themed ajax email newsletter signup form. Ideally, I would want to write it something like this:

var Hapi = require('hapi');
var Joi = require('joi');
var routes = [];

// Create a server with a host and port
var server = new Hapi.Server();
server.connection({ 
    host: 'localhost', 
    port: 8000
});

// Create routes
routes.push({
    method: 'GET',
    path: '/', 
    handler: function (request, reply) {
        var html = '';
        html += '<!DOCTYPE html>';
        html += '<html>';
        html += '<head>';
        html += '<meta charset="UTF-8" />';
        html += '<title>The Pirate List</title>';
        html += '<script>';
        html += 'function init() {';
            html += 'document.getElementById("form_email_add").onsubmit = function(e) {';
            html += 'e.preventDefault();';
                html += 'var httpRequest = new XMLHttpRequest();';

                html += 'httpRequest.onreadystatechange = function(){';
                    html += 'var data;';
                    html += 'if(httpRequest.readyState == 4){';
                        html += 'data = JSON.parse(httpRequest.responseText);';
                        html += 'if (httpRequest.status === 200) {';
                            html += 'document.getElementById("form_box").innerHTML = data.message;';
                        html += '} else {';
                            html += 'document.getElementById("error").innerHTML = data.message;';
                        html += '}';
                    html += '}';
                html += '};';

                html += 'httpRequest.open("POST", "/ajax/email-add");';
                html += 'httpRequest.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");';
                html += 'httpRequest.send("email=" + encodeURIComponent(document.getElementById("input_email").value));';

                html += 'return false;';
            html += '};';
        html += '}';

        html += '</script>';
        html += '</head>';
        html += '<body onload="init()">';

        html += '<p>Join the Pirate List matey!</p>';
        html += '<div id="form_box">';
        html += '<p id="error"></p>';
        html += '<form id="form_email_add" action="/ajax/email-add" method="post">';
        html += '<input id="input_email" type="text" name="email" value="" placeholder="Enter your email" />';
        html += '<input type="submit" value="Sign Me Up!" />';
        html += '</form>';
        html += '</div>';

        html += '</body>';
        html += '</html>';
        reply(html);
    }
});

// If you change customMessage to label, the entire form will work.
routes.push({
    method: 'POST',
    path: '/ajax/email-add', 
    config: {
        validate: {
            payload: {
                email: Joi.string().email().required().customMessage('Aaarrrrrggghhhhh!!! It needs to be valid matey!')
            },
            options: { abortEarly: false }
        }
    },
    handler: function (request, reply) {
        console.log('Add ' + request.payload.email + ' to the list.');
        reply({status: 'success', message: 'Welcome aboard matey!'});
    }
});

// Add the routes
server.route(routes);

// Start the server
server.start(function () {
    console.log('Server running at:', server.info.uri);
});

How would you recommend setting up a custom message like that?

And here's a typical way I currently use Hapi/Joi. Is this an incorrect way to do things? I'm using the validation messages directly because I don't see any need to change them. Assume this is a fully working contact form:

var Hapi = require('hapi');
var Joi = require('joi');
var routes = [];

// Create a server with a host and port
var server = new Hapi.Server();
server.connection({ 
    host: 'localhost', 
    port: 8000
});

// Create routes
routes.push({
    method: 'GET',
    path: '/', 
    handler: function (request, reply) {
        var html = '';
        html += '<!DOCTYPE html>';
        html += '<html>';
        html += '<head>';
        html += '<meta charset="UTF-8" />';
        html += '<title>Contact Form</title>';
        html += '<script>';
        html += 'function init() {';
            html += 'document.getElementById("form_contact").onsubmit = function(e) {';
            html += 'e.preventDefault();';
                html += 'var httpRequest = new XMLHttpRequest();';

                html += 'httpRequest.onreadystatechange = function(){';
                    html += 'var data;';
                    html += 'if(httpRequest.readyState == 4){';
                        html += 'data = JSON.parse(httpRequest.responseText);';
                        html += 'if (httpRequest.status === 200) {';
                            html += 'document.getElementById("form_box").innerHTML = data.message;';
                        html += '} else {';
                            html += 'document.getElementById("error").innerHTML = data.message + ".";';
                        html += '}';
                    html += '}';
                html += '};';

                html += 'var output = "";';
                html += 'output += "email=" + encodeURIComponent(document.getElementById("input_email").value);';
                html += 'output += "&subject=" + encodeURIComponent(document.getElementById("input_subject").value);';
                html += 'output += "&message=" + encodeURIComponent(document.getElementById("textarea_message").value);';

                html += 'httpRequest.open("POST", "/ajax/contact-send");';
                html += 'httpRequest.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");';
                html += 'httpRequest.send(output);';

                html += 'return false;';
            html += '};';
        html += '}';

        html += '</script>';
        html += '</head>';
        html += '<body onload="init()">';

        html += '<p>Contact Form:</p>';
        html += '<div id="form_box">';
        html += '<p id="error"></p>';
        html += '<form id="form_contact" action="/ajax/contact-send" method="post">';
        html += '<input id="input_email" type="text" name="email" value="" placeholder="Your email" /><br/>';
        html += '<input id="input_subject" type="text" name="subject" value="" placeholder="Subject" /><br/>';
        html += '<textarea id="textarea_message" name="message" placeholder="Your message"></textarea><br/>';
        html += '<input type="submit" value="Send" />';
        html += '</form>';
        html += '</div>';

        html += '</body>';
        html += '</html>';
        reply(html);
    }
});

routes.push({
    method: 'POST',
    path: '/ajax/contact-send', 
    config: {
        validate: {
            payload: {
                email: Joi.string().email().required().label('Your email'),
                subject: Joi.string().required().label('Subject'),
                message: Joi.string().required().label('Message')
            },
            options: { abortEarly: false }
        }
    },
    handler: function (request, reply) {
        console.log('Handle the contact form.');
        reply({status: 'success', message: 'Thank you! Your message has been sent.'});
    }
});

// Add the routes
server.route(routes);

// Start the server
server.start(function () {
    console.log('Server running at:', server.info.uri);
});

I may just be completely misunderstanding how to provide errors to end users but it would definitely help if Hapi and/or Joi clearly documented this. I'm guessing the above examples are the incorrect way to handle things so if you get a chance, let me know how those examples should be written. Thanks for all the great work on Hapi and Joi!

@AdriVanHoudt
Copy link
Contributor

I agree on there being confusion about how to handle things and to me it comes down to the fact that there are not many examples and blog posts etc about how to do things with Hapi. The most popular one (i think) is https://medium.com/@_expr/the-pursuit-of-hapi-ness-d82777afaa4b and is from Hapi 6.0 so yeah...

I think the best way to handle errors in a custom way is to use failAction option in config.validate. there you can provide a custom handler with the whole Joi error in it. (instead of the stripped one Hapi provides)

@Marsup
Copy link
Collaborator

Marsup commented Feb 18, 2015

@agraddy I think the customMessage won't be necessary with the next version, the problem I have with such API is this doesn't give any hint about what really went wrong. The correct way to write your pirate example in the future would be :

email: Joi.string().email().required().language({
  string: {
    email: '!!Aaarrrrrggghhhhh!!! It needs to be valid matey!' // Note the !! escape prefix
  }
})

Eventually adding the same message in any.required if you really don't care.

Now if your property is nested into objects or arrays, joi will of course give context around that sentence, because that's what it's made for, targeted errors.

I do consider any more advanced error transformation should be outside of joi (be it hapi or anything else), using the details property of the error joi provides.

@AdriVanHoudt
Copy link
Contributor

I think documenting the format would be a good start

@hueniverse
Copy link
Contributor

The problem with these examples is that they are completely unrealistic for any user-facing site. hapi's default configuration assumes an API server, not a user-facing HTML site. This means the default validation error is JSON with enough information a developer or machine can figure out what is wrong.

What you are asking is clearly intended for normal users, not developers. This means the JSON response is not going to be useful either way. You will need to use either the failAction setting or a onPreResponse extension hook to rework the joi error into something pretty. I agree we should do better documenting this with examples and better reference, but that's something plenty of people have figured out and can contribute to the hapijs.com site.

@agraddy
Copy link

agraddy commented Feb 19, 2015

I'm not sure how these examples are unrealistic. Could you explain? I've been using hapi for almost a year in a similar manner to the examples I provided.

Meaning, I first create routes for a base API, then I create AJAX routes that use server.methods (for caching) and inject() to access the API routes, and I send the JSON data back in AJAX to jQuery/Handlebar-based views (the work is private otherwise I would provide links). With the addition of label(), I've found the error messages work well for normal users.

Take the newsletter sign up. Are you saying that Hapi shouldn't be used at all in a newsletter sign up page (wrong tool for the job) or are you saying a newsletter sign up page should be structured different in Hapi (right tool but example is coded wrong)?

@Marsup, thanks for the "!!" addition. "!!" feels a little dirty/unintuitive to me, but it definitely works, although it sounds like I'm doing things wrong.

I haven't looked at the details property which seems like it would help a lot. Do you know why hapi strips out the details property? Is it a security issue or code cruft? Is the stripping of details an item we should create an issue and/or pull request for in hapi or is there a specific reason it's stripped off?

@hueniverse
Copy link
Contributor

@agraddy I missed the part where your client is an AJAX code that gets JSON error messages, extracts them and shows them to the user. This use case was never part of the design as the errors generated by joi were meant to be consumed by developers or machines, not end users.

I am not arguing against the use case, just that it was never our goal here. The assumption was that anything exposed to users will go through a failAction or other handlers manipulating the error into something friendly.

Your approach tried to accomplish this directly via the joi schema and some logic in your client code to handle multiple errors etc. That's neat but since it was not part of the use cases, a bit rough and limited (the language settings can't really handle proper grammar and complex errors).

I think the right approach here is to label things and then have a post validation method that performs the actual user interface response. I don't remember the state of meta() and other description flags in joi, but that would be my approach - put as much data in the schema and then use it when it fails to build proper user error.

@Marsup
Copy link
Collaborator

Marsup commented Feb 19, 2015

It shouldn't feel dirtier than any other templating engine out there.

For reference by stripping I meant this line, that's actually more a cherry picking of the path alone.

@agraddy
Copy link

agraddy commented Feb 20, 2015

Your response times are really impressive! Thanks! I'm going to go through everything and put together a response when I get a chance to look closer at the responses and dig into the code @Marsup linked to (it may take about a week or two for me to go through everything with my current schedule).

@djensen47
Copy link

This is intended because the message is supposed to provide context for consumers of the API, not to be presented to end-users.

path already provides context so adding it to the error message is seemingly redundant

[ { message: '"password1" length must be at least 6 characters long',
    path: 'password1',
    type: 'string.min',
    context: { limit: 6, value: '12', encoding: 'utf8', key: 'password1' } },
  { message: '"password2" password\'s do not match.',
    path: 'password2',
    type: 'any.allowOnly',
    context: { valids: [Object], key: 'password2' } } ]

Depends where your i18n can be done. If you have everything on the server, I'd base it on a map on error.details[index].type, taking into account other properties if need be, if it's on the client I'd send untransformed type, path and optionally context.

Regarding type, it doesn't always work out so smoothly. In my example above 'any.allowOnly' doesn't really tell me what the error was. The syntax does not match the semantics, which is that two fields don't match.

I think my question is, can I create a more meaningful custom type any.mismatch (or something like that) instead of any.allowOnly.

@djensen47
Copy link

Another type of validation where generic type responses are not enough (even for a JSON response) would be credit card validation.

I'm probably missing something, thus the comments. 😄

@Marsup
Copy link
Collaborator

Marsup commented Aug 31, 2015

The semantic is good enough I think, the context tells you that the only valid values are in context.valids, which should be an array of 1 element being a reference (Joi.isRef to check that, which I now realize is not documented) which gives you all the details about the reference it's trying to match.

I don't see your point about CC validation.

@djensen47
Copy link

The semantic is good enough I think

It's a corner case, which occurs on nearly every site out there, but the semantics here definitely do not implicate that two these two (password) fields should match.

Intuitively context.valids makes sense for constant values or regex not this "corner case."

I don't see your point about CC validation.

Yeah, I was little confused myself when I re-read that. The point I wanted to make was that if I created a custom validation for credit cards, the error type would not result in something useful to an external developer. A credit card validation should result in something like "creditCard.invalid".

I don't intend to be overly harsh but my overall feeling for the validation result object is that I cannot expose it to developers using an API that I provide to them. Basically, I cannot use this format out of the box if I write an API for 3rd party use. I need to re-translate nearly everything to make it more intuitive. Reason being that I don't want the developer using my API to have to learn my API plus the Joi API just to understand validation.

However, it might be that this is not something important to the project and that is understandable. You can't be everything for everybody. 😄

@Marsup
Copy link
Collaborator

Marsup commented Aug 31, 2015

Valids makes sense for any equality (single or multi), be it values or references. In your case putting a reference doesn't make it a rule, it's transformed into Joi.valid(Joi.ref('password2')). References will probably never be/have rules by themselves since they can be multiple types and it would be overly complicated to find what's what.

Your CC example is even more confusing because that's a case we deal with, and it has the correct error type. Having a translator for joi errors doesn't strike me as overly complicated, but I welcome any suggestions to help you do your task.

It is indeed important to the project, errors are here to help, and I believe custom types and validations will achieve this better than any new API I could make.

@djensen47
Copy link

Maybe what I'm really looking for is #577.

Even though I used CC validation as an example, after re-reviewing the docs I'm actually not sure how I would implement this in Joi without the proposed changes in #577.

💭 If you're not using a crazy regex, maybe you can provide an example of CC validation in the examples folder?

@Marsup
Copy link
Collaborator

Marsup commented Sep 2, 2015

If the current CC validation isn't enough can you create another issue about what's wrong ?

@MarkAurit
Copy link

I was going to jump on the "messages aren't user friendly" bandwagon, but the more I think about it I don't see how it is doable for the joi developers. If you could return a seperate error code for each type of sub-error level, i.e. "210= integer below minimum" or "220 = integer above maximum", or at the error code level i.e. "200=integer failed (to cover both of the above)", the developer can write his own return message library. (Im using joi 6.9.0)

@Marsup
Copy link
Collaborator

Marsup commented Nov 11, 2015

We do return errors codes, just not ints. I'm in the process of documenting it but any help is welcome.

@jamesdixon
Copy link

For anyone who may need a solution to this: https://github.com/dialexa/relish

@BenjaminConant
Copy link

@jamesdixon Love relish as a plugin solution. I have also come across https://github.com/eddyystop/joi-errors-for-forms which is simpler but really does the trick for forms. Personally, I like the idea of joi haveing hard standards on how error messages are formatted and using plugins to read those error messages and translate them however you want.

@stephengardner
Copy link

That's not a bug, it's a feature? Thanks for the solution to this conundrum @jamesdixon. ✌️

@yuschick
Copy link

yuschick commented May 4, 2018

Custom messages, including i18n, are possible in Joi: https://medium.com/@Yuschick/building-custom-localised-error-messages-with-joi-4a348d8cc2ba

@woutrbe
Copy link

woutrbe commented Jun 11, 2018

I'm also really struggling to see the use-case of this, especially if a custom error message is provided, it really should be using that message. A context object is already provided, which users can use.

I've solved this issue by using a regex to replace the key: replace(/"([^"]+)"/g, ''). It's not perfect, but it does the job for me.

@Marsup
Copy link
Collaborator

Marsup commented Jun 29, 2018

I'm going to lock this topic as it's not going anywhere. If you have a problem with the current way of handling errors, open another issue.

@hapijs hapijs locked as resolved and limited conversation to collaborators Jun 29, 2018
@hueniverse hueniverse added feature New functionality or improvement and removed request labels Sep 19, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
feature New functionality or improvement
Projects
None yet
Development

No branches or pull requests