DEV Community

Kevin Jump
Kevin Jump

Posted on

Battle scarred developer's guide to Umbraco v17 -Workspaces

All the code for this series of posts is available in the DoStuffWithUmbraco Repository on GitHub

In the last article we got to the point where we had our own custom section, with a side menu (or "sidebarApp") and a basic menu item.
but for now clicking on that menu item just shows us some loading dots 😞

Empty section

What we need to do know is define a workspace.

So what are workspaces ? well to lift the description directly from the umbraco docs :

Workspaces provide dedicated editing environments for specific entity types in Umbraco. They create isolated areas where users can edit content, media, members, or other entities with specialized interfaces and functionality.

basically menu items are assigned an 'entity type' and when you click on them Umbraco looks for the workspace that renders that entity type.

entity types are really just Identifiers at this point, usually you might have in two entity types a 'root' and and 'item' - the root is as you might expect the one at the root, and the item is an item in your tree (if you have one.

Item workspace

For our "simple" time item, we only have a single "do-stuff-time-item" entity type, and that's the one we need to build a workspace for to show information to the user.

Workspaces at their most basic have two things, a workspace context and a workspace element.

Workspace Context

Your workspace context is the code that controls the state and data for your workspace. here you have all your data and ways to fetch and save it.

To be clear, you don't need to do this in a context, you can just fetch , store and manipulate data in the element, but as you will see in a bit, that is probably going to lead to duplication and a more complex bunch of code

So without being to complicated a workspace will look something like this:

export class MySampleWorkspaceContext 
  extends UmbEditableWorkspaceContextBase<TimeSettings>
  implements UmbSubmittableWorkspaceContext
{
  #settingsRepository = new DoStuffTimeSettingsRepository(this);
  readonly unique: Observable<string | null | undefined>;

  public readonly workspaceAlias = DOSTUFF_WORKSPACE_ALIAS;

  constructor(host: UmbControllerHost) {
    super(host, DOSTUFF_WORKSPACE_ALIAS);
    this.provideContext(DOSTUFF_WORKSPACE_CONTEXT, this);
  }

  getUnique() {
    return undefined;
  }

  getData() {
    return this.#timeSettings.value;
  }

  getEntityType() {
    return DOSTUFF_TIME_ITEM_ALIAS;
  }

  protected submit() {
    const settings = this.#timeSettings.value;
    if (!settings) return;
    await this.#settingsRepository.saveTimeSettings(settings);
  }
}
Enter fullscreen mode Exit fullscreen mode

Workspaces can even be simpler than this, but here we are implementing a UmbSubmittableWorkspaceContext because we want to be able to save things that we update on our context, if you don't want to tie into other things like workspace actions, you don't need to do this, but its a relatively simple implementation, so in my opinion it's worth it.

There are few things we are going to pick out in the context :

ProvideContext

You can see that inside the constructor for the context we are calling this.provideContext . this tells the rest of the app, that we are responsible for the DOSTUFF_WORKSPACE_CONTEXT so if anything wants to interact with this context they can. by asking for it in their own constructor.

For example - a view might ask for the context like this.

this.consumeContext(DOSTUFF_WORKSPACE_CONTEXT, (context) => {
  // do stuff with the context here.
});
Enter fullscreen mode Exit fullscreen mode

Workspace contexts have a scope, they will only exist while you are in the workspace, you can't call a workspace context from somewhere else or another section - if you need to do that you might need a global context, which work in a similar way they are just defined slightly differently

Repositories

Another thing you might have noticed about this context is we randomly just call save in a #settingsRepository and we've haven't told you what one of them is yet - we will get to them in a bit.

But for now, that's just the place where the actual calls to API end points and the like are stored - it means our context doesn't need to know the inner workings of the API it can just say go get this or save that.

Views

So now we have registered our workspace and we have a workspace context, you will notice, we still have nothing to show to our user! So now its time to fix that, and present them with something. You define what things show in a workspace via workspaceView elements.

again we define these in a manifest.

   type: "workspaceView",
    alias: "DoStuff.DefaultWorkspaceView",
    name: "DoStuff Default Workspace View",
    js: () => import("./default-workspace-view.element.js"),
    weight: 500,
    meta: {
      label: "#doStuff_defaultWorkspaceViewName",
      pathname: "default",
      icon: "icon-alarm-clock",
    },
    conditions: [
      {
        alias: "Umb.Condition.WorkspaceAlias",
        match: DOSTUFF_WORKSPACE_ALIAS,
      },
    ],
  },
Enter fullscreen mode Exit fullscreen mode

So the things to note :

  • the js entry points the file that will render the element
  • the meta data defines the name and icon for the view
  • the conditions make sure our view only shows up on our workspace.

View element

The view element is a UmbLitElement that renders what you want to show the user

@customElement("do-stuff-default-workspace-view-element")
export class DoStuffDefaultWorkspaceViewElement extends UmbLitElement {

  override render() {
    return html`<umb-body-layout>
      <div class="layout">
        <uui-box
          .headline=${this.localize.term("doStuff_defaultWorkspaceTitle")}
        >
          <h1>Hello Time</h1>
        </uui-box>
      </div>
    </umb-body-layout>`;
  }
}
Enter fullscreen mode Exit fullscreen mode

With this in place we get something to show the user !

Single workspace view

Multiple views, and icons.

So we defined that icon, and name, but it's not anywhere on the screen, what's up with that?.

Well the icon and name are used when we have multiple views in a workspace. so if we add another workspaceView manifest to the workspace

{
  type: "workspaceView",
  alias: "DoStuff.SettingsWorkspaceView",
  name: "DoStuff Settings Workspace View",
  js: () => import("./settings-workspace-view.element.js"),
  weight: 200,
  meta: {
    label: "#doStuff_settingsWorkspaceViewName",
    pathname: "settings",
    icon: "icon-settings",
  },
  conditions: [
    {
      alias: "Umb.Condition.WorkspaceAlias",
      match: DOSTUFF_WORKSPACE_ALIAS,
    },
  ],
},
Enter fullscreen mode Exit fullscreen mode

We get our 'tabs'

Tabs


Summarry

This gets us the skeleton of our workspace up and showing something to the user - there is much more to workspaces , and we will go into them in some later posts, but for now, we have a section, a menu and some workspaces. so 🎉

Top comments (0)