Fileclass

by mdelobelle
5
4
3
2
1
Score: 46/100

Description

Give your notes typed, validated properties with guided input — define reusable note types, like a schema for your frontmatter. Successor to Metadata Menu, powered by Bases.

Reviews

No reviews yet.

Stats

49
stars
1,696
downloads
2
forks
28
days
1
days
2
days
135
total PRs
0
open PRs
1
closed PRs
134
merged PRs
51
total issues
14
open issues
37
closed issues
212
commits

Latest Version

3 days ago

Changelog

Added

  • A class can offer several destinations for its new notes. One folder and one template per class could not express what a class usually means: a Person met professionally starts from one template and lands in one folder, the same class for an artist starts from another and lands elsewhere. A class now keeps a list, each entry with a name, a folder and a template:

    newNotes:
      - name: Professional
        folder: 1_People/Contacts
        template: Templates/Person pro.md
      - name: Artist
        folder: 2_Artists
        template: Templates/Person artist.md
    

    Options → New notes lists them, one named row per destination showing its paths underneath, with Edit and Remove (which asks first, and never touches the folder or the template themselves). Add new opens the same modal as Edit.

    Creating a note asks which destination only when there is a choice — with one, it goes straight to the name.

    0.2.13's single fileClassNotesFolder/fileClassNoteTemplate pair is still read, as a list of one, so a vault configured then keeps working; saving the class through its options rewrites it as a list and clears the old keys, so a vault never carries two answers to the same question.

  • The wrench and the New button on every fileclass-table, not just the declared one. A vault keeps several tables of one class — Todo, Ongoing, Done, each with its own filter — and only one of them can be the view a class declares. The class is now read from whichever answers first: the class that declared the view, then the class the filter names, then the classes of the rows.

    The filter is read at both levels. Bases combines a base-wide filter with each view's own, and a base written before its views usually carries the class clause at the top — or: [fileClass.containsAny("Task"), file.hasTag("Task")] above a dozen status views, each filtering only on status. Reading the view alone found no class there, so a Tasks base got the generic buttons on every one of its views.

    A table about several classes keeps both buttons too: fileClass.containsAny("Book", "Comic") cannot say which class a new note should be, so the buttons read Manage fileClass and New note and ask once, offering only that table's classes.

    That fixes two reported cases the rows could not answer: an empty view showed no wrench at all (and kept the New button of the view before it, a leftover now cleared), and a view whose rows carried two classes — one note being both a Book and an Article — lost its New button and got a generic wrench, while its filter said fileClass.containsAny("Book") in plain sight.

  • A note created from a filtered table starts inside that table. From a Todo view — filtered status == "Todo" — the new note carries that status, instead of being made and immediately disappearing from the view that made it. Only what a filter fixes is used: an equality, or a single-value contains on a list field. !=, >, isEmpty() and a containsAny offering two values narrow without deciding, and nothing is invented there. A view that both filters and reads a relation applies both, the relation last.

    A value is only taken from a clause the filter always applies: anything under an or or a not is skipped, as is a formula clause (status == x || status == y). A view listing four statuses that all count as ongoing does not decide between them, and a note created from it would otherwise have been born with whichever one happened to be written first.

  • A fileclass-table groups its rows, from the view's own Group by. The setting sat in the Sort menu of every base and did nothing on an editable table: the rows came out flat while the native table beside them showed headings. Bases already hands a plugin view the grouped rows — the same groups, in the same order, measured — so this reads them rather than computing anything. Each run carries a heading naming the property and the value, in the native table's own classes so a theme styles it the same way; notes with nothing there group under None, last. Cells stay editable inside a group.

  • A link in a table cell shows its page preview on hover (with Ctrl/Cmd, if that is how the Page preview plugin is set). Obsidian does not watch the DOM for links a plugin drew — it listens for a hover-link event — so a link in a cell was the only link in the app that showed nothing.

  • A class's relatedViews row reads as relations, not as JSON. The panel printed the raw list of objects in the warning colour Obsidian keeps for values nobody can interpret — the same treatment the fields row used to get, on the other row where that value is the subject. It now shows one line per relation, author → Books › Book by author, each opening that base on that view. A relation whose base is missing is struck through and says so on hover; nothing is repaired, as with any other broken declaration.

  • A File or Media picker reaches every candidate, not the first hundred. Obsidian's suggester draws limit rows and says nothing about the rest — measured on a vault with 412 authors, the picker offered 100 of them, and the missing 312 looked exactly like notes that do not exist. It now draws a page at a time and adds the next when you reach the bottom, with 100 of 412 — scroll for more under the list while anything is held back. Typing still narrows first, and starts from one page again.

    MultiFile and MultiMedia never had this cap — they draw every candidate, and measured at 2000 options the modal opens in 179 ms and a keystroke in its filter costs 29 ms, so nothing there needed changing.

  • An image gets the room it deserves, and the file name gets out of the way. Two surfaces changed, on use:

    • both media pickers are galleries — four per row, the picture large, the name under it — where they were lists of 20px thumbnails beside full-width file names, which is the one thing a cover is not chosen by. Picking several drops the switches with the rows: a card that is on says so by looking picked, and the whole card was already the target;
    • a table cell holding an image shows the picture alone. Dune.png next to the picture of Dune's cover says nothing the picture does not, and in a table it is the name that takes the width. The value is still there on hover.

    Unchanged where it was already right: the Properties row and the note-fields modal keep the thumbnail beside the link — that is where you check which file a value points at. A media value with no thumbnail (audio, a PDF) keeps its name everywhere, since there the name is all there is, and a picker over notes rather than pictures stays a list.

  • A class orders its inherited fields as freely as its own. When Book extends Media, the resolved order now runs from the root of the chain downMedia's fields, then Book's, which is the order the thing was built in, rather than the reverse. And that is only the default: a class's editor lists its whole resolved set, marks what it inherits from Media, and the arrows move any row, so Media's title, Book's author and Media's year can sit in that order if that is how the note reads best.

    A move writes fieldsOrder on that class:

    fieldsOrder:
      - title
      - author
      - year
      - editions
      - editions.format
    

    so the order belongs to the class declaring it: Media is untouched, and Comic — extending the same parent — keeps its own. Children are ordered among their siblings.

    An inherited row moves but does not edit: changing its type there would change it for every class extending Media, so the row offers to open Media instead.

    The declaration is built to age. A key naming a field that no longer resolves is ignored; a field the order does not name — because an ancestor gained one since — appears where the ancestor put it, behind the field it follows by default, not at the end below your own; and renaming a field carries its entry along, in every class that had placed it.

    On the class note, fieldsOrder is shown rather than edited: a list of strings is drawn as pills with a remove button each, and a stray click there would drop a field back to its default position without saying so. The row reads 20 fields, in this class's order, names them on hover, and opens the schema editor — where the arrows write it.

    One place applies it — where a resolved field set is built — so every surface follows: the note-fields modal, a synced view's columns, Reorder properties, the fields inserted into a new note. Measured on Book extends Media with Comic beside it: all four agree, Media and Comic unchanged.

  • A field can be read backwards by several views. A relation is often shown more than one way — Task.delegate as Delegate's ongoing tasks and as Delegate's done tasks — and only the first was a declared view: adopting the second silently replaced it, so one of the two lost its New task with … button and its seed. A class now keeps both:

    relatedViews:
      - field: delegate
        view: Tasks.base#Delegate's ongoing tasks
      - field: delegate
        view: Tasks.base#Delegate's done tasks
    

    Both get the button, and a note created from either arrives already pointing at the note the table is embedded in.

    A relation view about several classes now says so too. It cannot know what it will create — containsAny("Book", "Comic") decides nothing — but it knows what the note will be linked to, so the button reads New with <note> instead of New note. The class is asked for on click, and the link follows it: one of the table's classes that does not read this view backwards makes an ordinary note, which is what the tooltip says rather than promising the link. The pair is the identity, so declaring the same view twice for one field changes nothing. Where a single view used to be assumed — inserting a reverse relation into a note — you are asked which one this note should show, since guessing would be silent.

  • A class note's newNotes row reads as destinations, not as JSON. Third list of objects on that note, third time Obsidian printed the raw value in the colour it keeps for things nobody can interpret. One line per destination now, its name in front and the folder and template behind by their basenames — Reading list · Reading list › Book — with a click opening the class's options on the screen that manages them.

  • Related views are managed from the class's options — added, edited, and at last removed. A declaration could only be made from a view, by running Use this view for a relation while looking at it, and could not be removed at all short of editing the frontmatter by hand. That is a strange shape for something a class owns. Options → Related views now lists them (author → Books › Book by author), with Edit, Remove (which asks first) and Add new: pick the field, pick the view among every view of every base in the vault.

    Nothing there touches a base. A view that does not filter on the note it is read from is named as such, live, under the picker — embedded in a note it would show every row to every note — and left alone: adding that clause belongs to the command that adopts a view, where you are looking at the view itself.

    Removing the last one clears the key rather than leaving relatedViews: [].

  • A multiline option on Input and MultiInput (#177). Some Input fields hold a paragraph — a summary, an abstract, a rationale — and a one-line prompt is the wrong box for one: the text scrolls sideways past the edge, Enter submits instead of wrapping, and a line break has to be pasted in from somewhere else. With the option on, entry opens the text area the plugin already ships, in the vault's text font rather than the monospace of the JSON editor — prose is not code.

    Nothing else moves: one scalar string, the same validation, the same column, written as a YAML block scalar when it holds line breaks. On MultiInput it applies to each item, and the list editor around them is untouched. A template wins over it when both are set, since a template is a shape made of single-line parts.

  • The schema, in the editor (#185). In source mode the frontmatter is plain text, and typing there was the one path the schema never saw: no candidates, no validation, no allowed set — so the fastest way through a note was also the only unchecked one. Two things close it, split the way the field types are:

    • Values as you type them, for Select, Cycle and Multi — the types with a list to choose from, which is the same set Metadata Menu's own suggester covered. The candidates come from wherever the field declares them. A list works item by item (Enter on - Religion continues it, and what is already in the list is not offered again), and an inline themes: [] becomes the block form on choosing: the [] goes, the value arrives on its own line. When what you have typed matches none of them — which is what happens when you type after a value that is already there — the field's values are offered anyway rather than nothing, and since a choice replaces the whole value, picking one repairs the line.
    • Manage the field at the cursor, a command to bind to a hotkey, for everything else: a Date, a Number, a Duration, an Object have a controller rather than a list. It reads the caret the way the note is written, so a caret on year: 1990 inside the second edition opens that edition's editor. On a key no class declares it opens nothing and says so — a generic text box there would be a second way to write frontmatter unchecked. The caret returns to the field's line once the write lands, so a pass through a note survives the rewrite.

Changed

  • A sync keeps the columns you added to a managed view, and leaves them where they are. It set the order to file.name + the class's fields and deleted everything else, so a formula.Editions had to be re-added after every sync. Now every file.* and formula.* column stays in the slot it occupies — a formula.Room sitting third stays third, file.name stays wherever you put it — and only the remaining slots are refilled with the class's fields, in the class's order. Fields the view had no room for are appended; a second sync moves nothing.

    A bare column over a property no class declares is still removed: nothing can tell it apart from the leftover of a field the class used to have, and that same rule is what makes removing a field remove its column.

  • A single value written as a scalar is no longer flagged, unless it costs something. themes: Ecology instead of themes: [Ecology] was reported as invalid for every list type, which is a lot of noise in a vault migrated from Metadata Menu — it wrote single values that way. Measured on Bases: for a list of values, contains, containsAny and == match the scalar exactly as they match a list of one, so nothing can tell them apart and Multi/MultiInput now accept it.

    For a list of links it stays an error, because there the difference is visible: with the same link stored as a list on one note and a scalar on another, contributors.contains(this.file.asLink()) — the filter a reverse-relation view is built from — returns the first and skips the second, so the note vanishes from the table meant to list it. The message now names that instead of the shape: "contributors" is a single link, not a list — views filtering on it skip this note.

Fixed

  • A fieldsOrder written as field ids is read, instead of doing nothing. Metadata Menu wrote that very key as a list of ids (fieldsOrder: [jlBZN1, JKrPnA, …]), and 0.2.14 reads it as names — so a vault migrated from it declared an order and silently got the default. An entry is now read as a name at its level first and as a field id second, and the class note's row names the fields instead of repeating the ids: author · acquired · rating rather than jlBZN1 · JKrPnA · Fnb8cC. The next move through the schema editor rewrites the key in names, since ids are unique only within one class.

  • "Open its base" lands on the class's own view. It opened the base file and let Bases pick, which means the view listed first — usually not the class's, since a base is free to hold several and the managed one is generally appended. The class already declares which view is its own (baseView), so the tab now opens there. A class whose base does not hold that view — declared but never synced — still opens the base rather than asking Bases for something that is not there.

README file from

Github

Fileclass

Give your notes typed, validated properties with guided input — define reusable note types, like a schema for your frontmatter.

You define reusable note types (called fileClasses), each with a fixed set of typed fields. For example, a Book type where author must be a link to a Person note, status is one of Reading / Read / Abandoned, and rating is a number from 1 to 5. Every note of that type then gets guided input for those fields (dropdowns, date pickers, link autocomplete), and Fileclass flags any note where a field is missing or has the wrong type.

In short: a schema and input forms for your frontmatter. You define the fields and fill them in; the core Bases plugin queries and displays them. If you have used Notion databases or Metadata Menu, it is that idea — but frontmatter-only, with no Dataview dependency.

📖 Documentation: https://mdelobelle.github.io/fileclass/

🎬 Start here — Tour #1 (5 min): from a vault where every note types its own properties by hand to a typed library, install and setup included: https://www.youtube.com/watch?v=rScC86I2vlg

Then one short video per feature, a couple of minutes each: the whole series — 44 of them.

📝 Rather read? Your first fileClass in five minutes, below: five steps, five screenshots, no video.

It is the successor to Metadata Menu (same author). If you rely on Dataview inline fields (key:: value), stay on Metadata Menu; Fileclass is frontmatter-only.

Why Fileclass

Metadata Menu fed field values through Dataview. Obsidian now ships its own query engine, Bases, so Fileclass uses that: point a field at a .base view and the notes or values it returns become the field's candidates. No Dataview dependency, and field-value filtering runs entirely on core Obsidian.

  • Frontmatter-only — reads via the metadata cache, writes via processFrontMatter; note text is never parsed or edited.
  • Your existing fileClass definitions work as-is — the Metadata Menu format is unchanged.
  • Rebuilt with quality in mind — 541 unit tests, TypeScript strict.

Features

  • fileClasses: typed schemas with inheritance (extends / excludes), bound by alias, tag, path, bookmark group, Base view, or a global default.
  • Typed fields: Input, Number, Boolean, Select, Cycle, Multi, Date/DateTime/ Time, File/MultiFile, Media/MultiMedia, Object/ObjectList, JSON/YAML, and Canvas fields — with guided input everywhere (modal, native Properties editor, context menus, indicators).
  • Data quality: required fields and per-note validation, surfaced in the table view and via the CLI/API.
  • Views: generate a .base for a fileClass and keep it in one-way sync; an editable fileclass-table Bases view with in-cell typed editing.
  • Terminal: a public plugin API (on the plugin instance), plus a standalone CLI and interactive TUI — in their own repo, fileclass-cli — to inspect, validate and edit typed frontmatter from the command line.

Requirements

Obsidian 1.12.7+ with the core Bases plugin enabled. Schemas and typed input work without Bases; the query-dependent parts (File/Media candidates, generated views) need it and degrade gracefully rather than erroring.

Installation

Settings → Community plugins → Browse, search for Fileclass, install and enable it. Then point it at a folder for your class notes — the first minute of Tour #1 does exactly that.

To install a build by hand instead, copy main.js, manifest.json and styles.css from the latest release into <your-vault>/.obsidian/plugins/fileclass/, then reload Obsidian.

Quickstart — your first fileClass in five minutes

Five steps, from an empty vault to a note with a working typed field. Everything here happens in Obsidian; nothing needs the terminal.

1. Tell Fileclass where your classes live

Settings → Community plugins → Fileclass → Class files folder — a folder in your vault, say Classes. It does not have to exist yet.

This one is not optional: until it is set, every command refuses with "Fileclass: set the class files folder in settings first." A fileClass is just a note in that folder, and this is where Fileclass looks for them.

2. Create a class with one field

Command palette → Fileclass: create a class, and name it Book. Its schema opens; click Add field and fill in three things:

  • Name: status
  • Type: Select (single value)
  • Add value, three times: Reading, Read, Abandoned

Then Save.

Adding a Select field named status, with three values

Select values are not the Template option. Template (on some types) composes a string like pg. {{page}}. The list a Select offers lives under Values source → Inline list, which is where Add value writes.

One field is enough to see the idea. Fileclass has twenty-six types, but a Select shows the point immediately: from now on, status accepts those three values and nothing else.

3. Point a note at the class

Two ways, and you will use both.

One note — open it, then command palette → Fileclass: add a class to this noteBook. The fileClass property is written, and the class's fields come with it:

A note's Properties panel showing fileClass Book and an empty status

A whole folder — open the class note (Classes/Book.md), click Options in its Properties panel, then Files paths → Choose… and tick the folder. Every note in it is a Book, with no fileClass line to write anywhere:

The folder picker, with Books ticked and the class folder greyed out

Your class folder is listed too, greyed: binding it would make every class a note of a class.

4. Fill the field

In the Properties panel, the small button between a key and its value opens that field's own input — for a Select, the values you allowed:

The status field's picker, offering Reading, Read and Abandoned

A note claimed by a folder may have no frontmatter at all yet. The class still knows its fields: Fileclass: manage note fields lists them, and Insert missing fields writes the keys in one go.

The note-fields modal listing status on a note with no frontmatter

5. Where to go from here

That is the whole loop: a class defines fields, a note is bound to a class, and the field's input is guided. What to reach for next:

See the model your classes make

Your classes form a model: what inherits from what, which fields draw their values from a base, which folders and tags each class claims. Fileclass: draw the schema canvas puts it on an Obsidian canvas: each class shows its fields and their types, the bases it depends on are previewed beside it, and the whole thing is arranged by you — a later sync keeps every position you gave it.

A schema canvas: Media above its four children with the fields they drop, cards listing what each class claims, and the bases and canvas their fields draw from

It also reports what silently does not work: a tag that can never bind — the index skips any tag containing a space — is struck through with the reason. More.

Coming from Metadata Menu

Your fileClass notes are read as-is. Lookup and Formula are out of scope (use Bases views for reverse relations and computed columns; existing ones load read-only), and the old FileClassView is replaced by fileclass-table, a Bases view with editable cells. The details are in the docs.

Metadata Menu still works, but don't run both at the same time. It is in maintenance mode and won't receive further features.

Feedback

Bug reports, ideas and recommendations are very welcome — please open an issue: https://github.com/mdelobelle/fileclass/issues

License

MIT