mdelobelle / metadatamenu

For data management enthusiasts : type and manage the metadata of your notes.
https://mdelobelle.github.io/metadatamenu
MIT License
512 stars 28 forks source link

FR: Field modifiers for nested values #484

Open mcxim opened 9 months ago

mcxim commented 9 months ago

Currently the createDvField fuction contains:

if (!field?.isRoot()) {
    /*
    field modifiers are only available for root fields
    */
    dv.el('span', p[field!.name], attrs);
    return
}

This means that we can't modify nested values using dataviewjs tables. Is there some limitation that prevents this from being possible?

mdelobelle commented 9 months ago

It's on the roadmap. The challenge is to find a way to describe the "path" to the nested field in the fieldModifier function

Option1: a slugify kind of path

Person:
  Address:
    City: ....

could be identified by f(dv, p, "Person__Address__City)"

That sounds reasonable but there's always a risk the those "separator" (__) may be used in the field name by the user....

for object lists there's an additionnal challenge: target the proper item in the list...

Participants
  - name: John
    infos:
      company: Acme
      role: CEO
  - name: Anne
    infos:
      company: NewCO
      role: CTO

what would the user like to do in this case?

Option 1.1: modify an "item at index" f(dv, p, "Participants[1]__infos__company") would manage the company field for Anne. But this index may change when an item is added or removed → not suitable IMO

Option 1.2: have a filtering mecanism f(dv, p, "Participants__infos__company", {options: {filters: [{"Participants__name", "Anne"}]}} would display and modify any item's infos' company field whose name is Anne. But this is very limiter, so I would go for a filtering function.

f(dv, p, "Participants__infos__company", {options: {filters: [{"Participants__name", (value)=>{return !value.includes("John")}}]}}

we could cumulate filters

f(dv, p, "Participants__infos__company", {options: {filters: [{"Participants__infos__role"}, (value)=>{return value === "CTO"}], [{"Participants__name", (value)=>{return !value.includes("John")}}]}}

The two filters would be combined with an && operator

This would be very flexible but... complex to manage for javascript newbies

Option 2: ... still looking for an option2, that's the reason why it isn't proposed as a feature yet 😄

WDYT?

mcxim commented 8 months ago

Glad to hear it's on the roadmap!

As per the risk that __ is part of the field name, I think we can use the : character that can't be a part of the name, so something like f(dv, p, "Person:Address:City") would probably be both readable and safe in that regard.

As per lists of objects, my vision is to be able to edit such a list using a dataview table in the note, which can even be possible without indexing. For example, returning a list of fields, so something like f(dv, p, "Participants:infos:company") would return both John's and Ann's modifiers, in a list.

This would make it possible to write something like this:

let page = dv.current()

dv.table(
  ["name", "company", "role"],
  transpose([
    f(dv, page, "Participants:name"),
    f(dv, page, "Participants:infos:company"),
    f(dv, page, "Participants:infos:role"),
  ])
)

Sometimes we will need the filtering/indexing feature, I think something of the likes of option 1.2 you mentioned would be a good idea, when formatted it looks pretty readable:

f(dv, p, "Participants:infos:company", {
  options: {
    filters: [
      {
        "Participants:infos:role": (value) => value === "CTO",
        "Participants:name": (value) => !value.includes("John"),
      },
    ],
  },
})

Here, too, returning a list of all matching fields would also be a good idea IMO.

There's a fair chance I don't understand the API correctly and this won't work, I'd love to hear your opinion.

WhiskeyJack96 commented 8 months ago

Is there a reason not to use the same "dot" notation that objects/dv use? ie why not f(dv, p, "Person.Address.City)

WhiskeyJack96 commented 8 months ago

https://github.com/mdelobelle/metadatamenu/compare/master...WhiskeyJack96:metadatamenu:master

something like this is what I would think! I'm sure this isnt the proper/elegant way to accomplish, but it got nested objects working for bool values in my vault :D

mcxim commented 8 months ago

Looks cool! I managed to somehow make input fields work too, but my solution isn't elegant either (I exposed the FieldManager constructors in the API, and manually constructed the HTML elements from the fields I got from fileFields...)

I don't see a reason not to use dots, I didn't know dataview uses them, seems to me that if it's the convention then we shouldn't break it.

And still, I don't think it solves everything, because, if we have a list of objects, then using your syntax the user would have to write something like "Participants.0.infos.role" and then "Participants.1.infos.role", which means constructing those strings programmatically (since you don't know the number of participants beforehand) and that, in turn, means iterating through the fields, which is a process that the plugin already does each time you call fieldManager()... This doesn't seem scalable in terms of time complexity...

Maybe we can reserve a character, for example *, meaning "all items fo the list", so that a table can be created like this:

dv.table(
  ["name", "company", "role"],
  transpose([
    f(dv, page, "Participants.*.name"),
    f(dv, page, "Participants.*.infos.company"),
    f(dv, page, "Participants.*.infos.role"),
  ])
)

While still allowing users to choose an indexed item from the list if they want by specifying a number instead of the star.

I think that the challenge here is to come up with a way to specify the fields I'm interested in while both allowing as much flexibility to the user as he may need, and not making the whole process inefficient with a fast-growing number of traversals, and I don't think we're there yet.

If I have more time I will probably think about it more thoroughly.

WhiskeyJack96 commented 8 months ago

I think for repeated fields using the normal "index" characters would probably be more straightforward (so something like Participants[0].infos.role but I do agree some syntax for "all" would make sense, perhaps we could steal the syntax of jq and do Participants[].infos.role?

WhiskeyJack96 commented 8 months ago

also FYI @mdelobelle I've updated my fork after extending the functionality to other base types. I dont currently use object lists but if you're willing to merge partial support I could open a pr?

Edit: I decided to try out the proposed "transpose" option and it works well for getting all of an object list! Going to open a pr and let you decide if you want it in mainline!

hammonia-io commented 6 months ago

From what i understand, this feature request talks about creating editor widgets for nested metadata values, right? I'm looking for a way to use the fieldModifiers function on inline fields of Dataview tasks and lists items.

I have a markdown list in a note and a dataviewjs query that creates a table from that list. The list items have inline fields, which are mapped to columns of the table. Now i would like to use metadatamenu to create editor widgets for those fields.

Are there any plans on that, or should that be working already?