Check out the Manifest Editor →
WikiExtending Manifest Editor

Apps - technical overview

An App is a full editor that is loaded into the [[Shell]]. It may be composed of multiple widgets or it may be a standalone entity.

When an App loads a specific widget, it will open the slot it renders the widget to extension.

If I had some code that embedded a widget

function MyApp() {
  const someLocator = { id: currentCanvasId, type: 'Canvas' };
  
  return (
    <div>
      <MyDefaultWidget 
        locator={someLocator} 
        config={ ... } 
      />
    </div>
  );
}

I could instead provide an integration point for the future quite easily:

function MyApp() {
  const someLocator = { id: currentCanvasId, type: 'Canvas' };
  
  return (
    <div>
      <WidgetSlot 
        id="sidebar-slot"
        locator={someLocator} 
        defaultWidget={MyDefaultWidget} 
        config={ ... } 
      />
    </div>
  );
}

In this example, a <WidgetSlot /> is configured, with the default component <MyDefaultWidget />. It has an identifier sidebar-slot and the locator and config is passed down to it.

This could allow the [[Shell]] to configure the application without further action from the App.

Demonstrative config:

const myShellConfig =  {
 
  // Overriding widgets.
  widgets: {
    'sidebar-slot': [MyCustomWidget, MyDefaultWidget], // Multiple, can use the default as a fallback. (see .supports() below)
 
    'another-slot': [
       // Alternatively with custom configuration values.
       [MyOtherCustomWidget, { configValue: 'some value' }]
    ],
 
    'a-nested-slot': [
      // This widget has it's own slots!
      [MyNestedCustomWidget, { 
        widgets: {
          'my-nested-custom-widget-slot': [SomeOtherOverride],
        }
        configValue: 'some value' 
      }]
    ],
  },
}

This is not to say that this will be exactly how this will work, but instead to demonstrate that with the right conventions and models this type of extension is achievable. The MVP non-configurable implementation of WidgetSlot might be:

function WidgetSlot(props) {
  const Component = props.defaultWidget;

  return <Component locator={props.locator} config={props.config} />;
}

But places the integration point for the future.

Widgets - technical overview

When a widget is loaded it will be provided with some standard properties, and some custom configuration - specific to that widget.

// This may not be the actual API, just demonstrative.
const widget = new Widget(vault);
const instance = widget.create({
  locator,
  config
});

The first is the vault instance. This is provided automatically by the [[Shell]] and keeps state across the application in sync. The aim of any extension to Manifest Editor is to display or mutate something inside the vault. The “something” in this case, is a locator.

The locator is the slice of the vault that the extension intends to edit. For example, if I created an widget to edit the label of the top level manifest, the locator may just be the ref:

{
  "id": "htts://example.org/manifest",
  "type": "Manifest"
}

However, the same editor could be used to edit the label of a canvas, simply by changing the locator.

{
  "id": "https://example.org/canvas-1",
  "type": "Canvas"
}

This allows for widgets to be created and reused. The locator is different from a ref however, as it may also specify one or more fields on the resource. My widget could now be used to edit any language map field.

{
  "id": "https://example.org/canvas-1",
  "type": "Canvas",
  "fields": ["label"]
}

Supports

A widget can optionally tell the Shell if it supports a particular locator. This can help validation of configuration, if widgets are being configured incorrectly.

This would also allow for the Shell to suggest the extension in contextual menus. For example, r-click on a canvas in a sidebar could check all extensions, and populate an “open in…” menu.

widget.supports(locator) // bool

If there is a slot in the existing application for the extension, then it could go there - or simply replace the current app in the shell.

Locks

To avoid conflicts between widgets - an app may choose to “lock” a resource while the user is focused on their UI (like a form, for example). This is similar to how in Miro a sticky-note becomes inaccessible when someone is typing.

vaultExtensions.lock('my-app-id', locator); // returns function to unlock.

The extension may also decide to lock the entire locator used to instantiate it. By default, the entire locator will be locked. This will be important for data integrity.

React example (locks)

Here is an example of how this might look to a React developer creating an extension for the Manifest Editor.

import { useLock } from 'docs/pages/wiki/Manifest-Editor';
 
function MyLabelEditor(props) {
    // Use a hook to lock, unlock and get the lock status of a locator.
    const { isLocked, lock, unlock } = useLock(props.locator);
 
    // When we focus the form, lock the resource.
    const onFocus = () => {
        lock();
    };
 
    // When we blur the form, unlock the resource.
    const onBlur = () => {
        unlock();
    };
 
 
    // When we submit the resource, update the vault.
    const onSubmit = (e) => {
        vault.updateFieldValue(...);
        // .. etc
    };
 
 
    return (
        <form onFocus={onFocus} onSubmit={onSubmit} onBlur={onBlur}>
            {/* We can use a fieldset to disable the whole form if it was locked (not by us) */}
            <fieldset disabled={isLocked}>
                <input type="text" [...] />
 
                [ ... other UI ... ]
 
                <button type="submit"/>
            </fieldset>
        </form>
    );
}

Iterative design

The model of locators will allow us to quantify how much of a Manifest our Manifest Editor can edit and plug gaps.