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

Add ValueComponent to viewerOptions to allow customizing examples, enums, consts, and defaults #224

Merged
merged 12 commits into from
Dec 19, 2023

Conversation

gnidan
Copy link
Contributor

@gnidan gnidan commented Dec 14, 2023

This PR adds the ValueComponent option to viewerOptions to allow customizing how raw values get rendered. This applies to use via "examples", "const", "enum", and "default".

... to display CodeBlock examples with formatted/indented JSON instead
of all in one line
Copy link

netlify bot commented Dec 14, 2023

Deploy Preview for delicate-torrone-5d35ee ready!

Name Link
🔨 Latest commit d5307bd
🔍 Latest deploy log https://app.netlify.com/sites/delicate-torrone-5d35ee/deploys/6580fff0f5187a00080328b3
😎 Deploy Preview https://deploy-preview-224--delicate-torrone-5d35ee.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify site configuration.

@gnidan gnidan changed the title Add showMultilineExamples to JSVOptions Allow displaying example values with formatting/indentation Dec 14, 2023
@jy95
Copy link
Owner

jy95 commented Dec 14, 2023

Hello @gnidan ,

Welcome ;)

I thought a bit about your issue and a smarter approach I have in mind would be the introduction of something called ExampleComponent in ExamplesQualifierMessage.

In essence, it is similar to the DescriptionComponent I introduced in 1.4.0 for people putting markdown description instead of plain text :

export default function CreateDescription(props: Props): JSX.Element {
  const { description } = props
  const { DescriptionComponent } = useJSVOptionsContext()

  return (
    <div style={{ marginTop: "var(--ifm-table-cell-padding)" }}>
      {DescriptionComponent ? (
        <DescriptionComponent description={description} />
      ) : (
        description
      )}
    </div>
  )

so ExamplesQualifierMessage return instruction could be something like that :

  return (
    <div key={"examples"}>
      {examplesLabel}&nbsp;
      <Tabs>
        {items.map((item) => (
          <TabItem key={item.id} value={item.id.toString()} label={item.label}>
             {ExampleComponent? (
                  <ExampleComponent example={item.value} />
              ) : (
                {printSchemaType(item.value)}
             )}
          </TabItem>
        ))}
      </Tabs>
    </div>
  )

ExampleComponent introduction has several reasons :

  • It gives users the possibility to customize to their likings (e.g. you are happy with JSON.stringify(obj, null, 2) but some people would like JSON.stringify(obj, null, 3) / JSON.stringify(obj, null, "\t") for example)
  • If not provided, ExamplesQualifierMessage fallback to printSchemaType for each element of the examples array (so we can keep backwards compatibility )
  • We can keep untouched the printSchemaType function which is used in others components

What do you think ? Let me know if you need something

@gnidan
Copy link
Contributor Author

gnidan commented Dec 14, 2023

Great thinking. I much prefer your suggested approach. I will switch this PR to that method when I get back on my computer tomorrow. Thanks for the quick eyes!

@jy95
Copy link
Owner

jy95 commented Dec 15, 2023

An idea I got after a relaxing night is that we can even turn this ExampleComponent into something more generic like ValueComponent so that we use the same idea for :

That way, we cover cases where users would like that custom logic in other parts. What do you think ?

@gnidan
Copy link
Contributor Author

gnidan commented Dec 16, 2023

Looking into this now. I notice that descriptions don't seem to be implemented via the "qualifier messages" system, so I'm trying to reconcile my understanding of all this. I assume you expect to see a CreateValue component analogous to CreateDescription?

(I'll be hacking a bit this weekend because I've got some time; I'll plan to push up more commits in time for your Monday morning so you can tell me where I'm going wrong :)

@gnidan gnidan changed the title Allow displaying example values with formatting/indentation Add ValueComponent to viewerOptions to allow customizing examples, enums, consts, and defaults Dec 16, 2023
@gnidan gnidan marked this pull request as draft December 16, 2023 23:29
- Add `ValueComponent` as JSVOption
- Replace use of hardcoded `printSchemaType` with new `<CreateValue />`
  component for the following qualifier messages:
  - enum
  - constant
  - examples
  - default
@gnidan gnidan force-pushed the multiline-examples branch from d45ed4b to d859a1e Compare December 16, 2023 23:33
@gnidan
Copy link
Contributor Author

gnidan commented Dec 16, 2023

I've pushed a first pass at this approach and it seems to work fine. In the current implementation here, I'm leaving the simple/complex object distinction to be a concern of ValueComponent, so users who like printSchemaType's distinction to use <code> for primitives and <CodeBlock> for arrays, etc. will need to code this themselves.

Here's what I mean
<JSONSchemaViewer
  viewerOptions={{
    showExamples: true,
    ValueComponent: ({ value }) => {
      // deal with simple types first
      if (["string", "number", "bigint", "boolean"].includes(typeof value)) {
        return <code>{
          (value as string | number | bigint | boolean).toString()
        }</code>;
      }

      // for complex types use a whole CodeBlock
      return <CodeBlock language="json">{`${
        JSON.stringify(value, undefined, 2)
      }`}</CodeBlock>;
    }
  }} />

I can see the pros/cons here... there's simplicity in just allowing one <ValueComponent />, but we could always take a different approach and have <PrimitiveValueComponent /> separate from <ComplexValueComponent />, but I suspect that's less favorable.

Anyway, here's a screenshot Screenshot 2023-12-16 at 18 42 24

I'm leaving this as draft because I think there's two open concerns still:

  1. I was lazy with enums and use a single <CreateValue value={schema.enum!} />, but I suspect it may be better to render each enumerated value in its own <CreateValue /> component (although the inline vs. block distinction may create some edge cases...)
  2. Although I noted this option in the JSVOptions page, I imagine this behavior should get more thorough treatment where const/enum/etc. are documented in the demo-site. Would you like a single page for this behavior, or should it just get mentioned separately for each of those "generic keywords"?

@jy95
Copy link
Owner

jy95 commented Dec 17, 2023

Hello @gnidan ,

A small explanation about "qualifier messages" system :
image

You see, the rendering component takes parameters - inside options we will find out ValueComponent .
(Most of the time, it is the schema object for obvious reasons but sometimes I don't need any extra props)

So the changes I have in mind :

  • Add the type declaration of ValueComponent in JSVOptions
  • In the CHECKS_MAP , pass in each Component: the props for examples / default / const / enum
  examples: {
    match: ({ schema, options }) =>
      options.showExamples === true && schema.examples !== undefined,
    Component: (props) => (
      <QMS.ExamplesQM key={"examples"} {...props} />
    ),
  },
 .....

I think the standalone ValueComponent path would be better if people would like to introduce fancy logic like

  • Strings have to be in italic
  • Booleans have to be in bold
  • Implement their own rendering logic like "But my string represents a xml, not a json" (I'm open to add in ValueComponent the schema prop next to your value - let's say I have in mind the people using format like that )
  • ...

To answer your concern :

  1. enum is indeed a bit of a challenge, but don't worry : nothing is surmountable. I think almost the same as you. Using the strategy I explained above, we can EnumQualifierMessage a bit different from the 3 others
1. If ValueComponent isn't here, return the as-it
    It is to ensure backwards compatibility
2. Otherwise, 
     Use Docusaurus Tabs  https://docusaurus.io/docs/markdown-features/tabs
3. iterate on each value of the enum array
    Use Docusaurus Tab component with ValueComponent as children
    For the Tab value, it could be the array index
    For the Tab label, it could be "Value ${idx}"
         ( I did that for examples as you saw : https://jy95.github.io/docusaurus-json-schema-plugin/docs/demo-viewer/generic_keywords/annotations )

Notice : https://json-schema.org/understanding-json-schema/reference/enum
  1. Having a dedicated page showcasting the new feature would be better, I had the same remark about the Description component if you wonder Show description before the type #80 (reply in thread)

Feel free to share your thoughts ;)

@gnidan
Copy link
Contributor Author

gnidan commented Dec 17, 2023

Quick comment re: enums,

  1. Otherwise,
    Use Docusaurus Tabs https://docusaurus.io/docs/markdown-features/tabs

I'm not sure Tabs are the best way to do this. Even if I want to customize ValueComponent for most things, I might not want to display enum values in a big horizontal list like that. Besides, what value does a TabItem body offer besides the label? OK, I might have complex enum values... enum: [{ a: 1 }, { a: 2 }, { b: 3 }] or such, but that seems really uncommon. Much more common would be enum: ["a", "b", "c"], and then tabs seem way overkill.

Maybe we just want a regular <ul> or <ol> for enum values? There's the concern for enums that have a very long list of values, but maybe this isn't a priority concern?

(Anyway, I haven't fully digested the rest of your message, but I'll follow-up tomorrow (as in: Sunday). I appreciate your taking the time to get back to me on the weekend!)

@jy95
Copy link
Owner

jy95 commented Dec 17, 2023

Quick comment re: enums,

  1. Otherwise,
    Use Docusaurus Tabs https://docusaurus.io/docs/markdown-features/tabs

I'm not sure Tabs are the best way to do this. Even if I want to customize ValueComponent for most things, I might not want to display enum values in a big horizontal list like that. Besides, what value does a TabItem body offer besides the label? OK, I might have complex enum values... enum: [{ a: 1 }, { a: 2 }, { b: 3 }] or such, but that seems really uncommon. Much more common would be enum: ["a", "b", "c"], and then tabs seem way overkill.

Maybe we just want a regular <ul> or <ol> for enum values? There's the concern for enums that have a very long list of values, but maybe this isn't a priority concern?

(Anyway, I haven't fully digested the rest of your message, but I'll follow-up tomorrow (as in: Sunday). I appreciate your taking the time to get back to me on the weekend!)

Guess we can give a try to the regular <ul> or <ol> for enum values and decide afterwards if it holds

@gnidan gnidan marked this pull request as ready for review December 18, 2023 00:48
@gnidan
Copy link
Contributor Author

gnidan commented Dec 18, 2023

I might be able to tweak some things further, but I think my implementation is back to you now for review.

  • Regarding your message:

    So the changes I have in mind, ...

    I am not entirely sure what you mean by passing in each Component "the props", but perhaps you are suggesting a more manual approach than I have taken. If you look at my approach, I've created a CreateValue component analogous to CreateDescription, which uses the jsvOptionsContext. I was glad to find this as a way to make this change pretty low-touch; perhaps you would agree.

  • Regarding enums and their backwards compatibility:

    I did something hacky with the JSV Options context, just to ensure we keep the same old behavior if ValueComponent is not specified. I don't think you will like it... I didn't like writing it. I'm wondering if the simplicity elsewhere is worth the code smell in Enum.tsx, since perhaps you will be inclined to change the default in your next breaking release. Please advise.

  • Note I added schema as a prop to ValueComponent like you suggested.

  • Regarding separate docs page:
    I added a separate docs page, but I reused the schema for annotations, which doesn't show all four cases (enum/constant/examples/default). Do you have a schema that I could use already, or should I add a new one? If I want to show off use of the schema prop, do you mind if the main schema for this docs page is nested? or should I have separate schemas with separate JSONSchemaViewers?

    (Additionally: please advise on page title/description/filename!)

I think that's all I've got for now. Looking forward to reviewing what you came up with in #225 to see if it covers my use case :)

Thank you!

@jy95
Copy link
Owner

jy95 commented Dec 18, 2023

I am not entirely sure what you mean by passing in each Component "the props", but perhaps you are suggesting a more manual approach than I have taken. If you look at my approach, I've created a CreateValue component analogous to CreateDescription, which uses the jsvOptionsContext. I was glad to find this as a way to make this change pretty low-touch; perhaps you would agree.

It could work so I will give it a chance ;)

Regarding enums and their backwards compatibility:
I did something hacky with the JSV Options context, just to ensure we keep the same old behavior if ValueComponent is not specified. I don't think you will like it... I didn't like writing it. I'm wondering if the simplicity elsewhere is worth the code smell in Enum.tsx, since perhaps you will be inclined to change the default in your next breaking release. Please advise.

Good question. You know what ? Let's use the ul for all cases now. Each value will be passed to ValueComponent.
Note that you likely will have to update snapshots in __snapshots__ (in short git rebase, delete all snapshots and relauch npm test )
In essence, it is a style change that doesn't mean that the lib doesn't support enum. People could still swizzle the component if they would like something else (like using tabs)

Note I added schema as a prop to ValueComponent like you suggested.

Thanks.

Regarding separate docs page:

Mmm I think it would be better to have a new category called "🛠️ Customizations" instead of put it in Generic keywords.
Inside it, we can put the case of the markdown 📋 Custom description Component and your new 🌈 Custom value component. I just created in main branch for the 📋 Custom description Component case

For info, I changed a bit the structure of the docs to use tabs (so that users are not overwhelmed of info, specially in case where the JSON Schema is quite long)

# Annotations

<Tabs>
    <TabItem value="Viewer" label="Viewer" default>
        <JSONSchemaViewer 
            schema={ Schema } 
            viewerOptions={{ 
                showExamples: true,
                DescriptionComponent: ({description}) => <ReactMarkdown children={description} /> 
            }}
        />
    </TabItem>
    <TabItem value="viewerOptions" label='viewerOptions'>
        ```js
        {
            showExamples: true,
            DescriptionComponent: ({description}) => <ReactMarkdown children={description} />
        }
        ```
    </TabItem>
    <TabItem value="JSON Schema" label='JSON Schema'>
        <CodeBlock language="json">{JSON.stringify(Schema, null, 2)}</CodeBlock>
    </TabItem>
</Tabs>

About the schema, I think this one could fit - feel free to update it to your wishes :

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "CustomizationOptions",
  "description": "JSON schema for customized options",
  "type": "object",
  "properties": {
    "customField": {
      "type": "string",
      "description": "A customized or personalized field",
      "enum": ["palette", "teddyBear", "tools", "laptop", "thread", "phone", "puzzle", "scissors", "hammer", "note"],
      "default": "palette",
      "examples": ["tools", "note"]
    }
  },
  "required": ["customField"],
  "additionalProperties": false
}

For UnresolvedRefsComponent, I added an example in the docs as well

... and update test snapshots
- Move to new home in "Customizations" section
- Switch to using tabs
- Define example schema that features enum, default, and const
- Include example ValueComponent that references schema in logic
@gnidan
Copy link
Contributor Author

gnidan commented Dec 19, 2023

I can't request reviewers in this repo, so I'll pass this back to you by way of a comment :)

  • I got rid of the old-style enum rendering. I concur with your reasoning that it's not really breaking :)
  • I fixed up the docs to match your new direction. You'll see my somewhat contrived example for displaying the use of schema prop. (I like the tabs and new customizations section, btw!)
  • I re-updated the snapshots
  • I ran prettier but there's still one code quality issue (it looks like an issue you've ignored previously)

How's it all look? Thanks for your help guiding me with this one.

@jy95
Copy link
Owner

jy95 commented Dec 19, 2023

Thanks @gnidan ,with our joint efforts, we've come up with something that should satisfy every use case that might come to mind. I will create the release in a couple of minutes.

For your example, I will update it in another MR turn from if value === schema.default to if schema.default && value === schema.default as technically speaking undefined is still a value you could have in some place like const ;)

@jy95 jy95 merged commit 4481b14 into jy95:main Dec 19, 2023
5 checks passed
jy95 added a commit that referenced this pull request Dec 19, 2023
Copy link

🎉 This PR is included in version 1.9.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

@gnidan gnidan deleted the multiline-examples branch December 19, 2023 11:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants