Workspace Views

could really do with some pointers :rofl:

I am attempting to port my Polls package to v17, so far I have managed to add them successfully to the settings sidebar view. What I want to do is display a workspace view when you click on the Poll, how do you do that? I’m just going round in circles at the moment and getting nowhere.

Any info, examples much appreciated.

You probably need a full workspace. A workspace view is what is says: a view in a workspace. But you don’t have a workspace (yet), so you need to use that :slight_smile:

Have done that :slightly_smiling_face: , just can’t figure out how the menu hooks up the workspace

const workspace: UmbExtensionManifest = {
    type: 'workspace',
    kind: 'default',
    alias: 'polls.Workspace',
    name: 'Polls Workspace',
    meta: {
        entityType: 'polls-workspace-view',
        headline: 'Polls',
    },
};
const workspaceView: UmbExtensionManifest =
{
    type: 'workspaceView',
    name: 'Polls Workspace View',
    alias: 'polls.workspaceView',
    element: () => import('../workspace/polls-workspace-view.js'),
    weight: 900,
    meta: {
        label: 'Poll',
        pathname: 'poll',
        icon: 'icon-lab',
    },
    conditions: [
        {
            alias: UMB_WORKSPACE_CONDITION_ALIAS,
            match: 'polls.Workspace',
        },
    ],
};

DOh! keyboard interface error, I had mistyped the view type in my types.ts file :rofl: :rofl:

1 Like

mmm, still getting an issue, although it now opens the workspace view and displays the form to edit the poll, I only seem to be able to select one of the polls, if I cleck on the second poll link nothing happens, if I refresh the page and chose the second poll link first it displays, but then clicking the link for the first poll does nothing :zany_face: really doing my head in!

This has been confusing me for a few days now! First the complexity of trees and now the routing of the views :shaking_face:

I’ve managed to get this to work by lots of faffing about, here’s the key points:

  • set the entity type of the treeItem to the same entityType set in your workspace which is the linking code.
  • in the manifest make sure you have the workspace as a kind routable:
{
  type: "workspace",
  kind: "routable",
  alias: WORKSPACE_ALIAS,
  name: "Example Workspace",
  api: () => import("./workspace.context"),
  meta: {
    entityType: 'example-entity'
  }
}

Then in the actual workspace make sure that you extend from UmbSubmittableWorkspaceContextBase (maybe you don’t have to, IDK!) and then define your routes in the constructor. Rather than trying to pick out everything I’ll just paste my entire TS file below. I can also share the example project in its raw form if that’s helpful.

I’m not sure if I’m confusing the whole context vs workspace ideology here, but this works for me so hopefully can help and is on the right lines!

import { UMB_WORKSPACE_CONTEXT } from "@umbraco-cms/backoffice/workspace";
import { UmbSubmittableWorkspaceContextBase } from "@umbraco-cms/backoffice/workspace";
import { UmbControllerHost } from "@umbraco-cms/backoffice/controller-api";
import { UmbContextToken } from "@umbraco-cms/backoffice/context-api";
import { UmbStringState, UmbObjectState } from "@umbraco-cms/backoffice/observable-api";
import { UmbPathPattern } from "@umbraco-cms/backoffice/router";
import { SECTION_ENTITY, WORKSPACE_ALIAS, CONTEXT_ALIAS } from "./types";

// Define your workspace path patterns
// This should match the section pathname from your manifest (new-section)
// and the entityType (example-entity from SECTION_ENTITY)
const SECTION_WORKSPACE_PATH = '/section/new-section/workspace/example-entity';

export const EDIT_SECTION_ITEM_PATH_PATTERN = new UmbPathPattern<{ unique: string }>(
  'edit/:unique',
  SECTION_WORKSPACE_PATH
);

export const CREATE_SECTION_ITEM_PATH_PATTERN = new UmbPathPattern<{
  parentEntityType: string;
  parentUnique: string | null;
}>(
  'create/parent/:parentEntityType/:parentUnique',
  SECTION_WORKSPACE_PATH
);

// Define your data model type
interface SectionItemModel {
  unique: string;
  name?: string;
  // Add other properties as needed
}

export default class ExampleWorkspace extends UmbSubmittableWorkspaceContextBase<SectionItemModel> {
  #unique = new UmbStringState<string | null>(null);
  readonly unique = this.#unique.asObservable();

  #data = new UmbObjectState<SectionItemModel | undefined>(undefined);
  readonly data = this.#data.asObservable();

  constructor(host: UmbControllerHost) {
    super(host, WORKSPACE_ALIAS);
    console.log("[ExampleWorkspace] Constructor");

    this.provideContext(SECTION_CONTEXT_TOKEN, this);

    // Set up routes with setup callbacks
    // The setup callback will be called EVERY TIME the route is matched,
    // even when navigating between different items of the same entityType
    this.routes.setRoutes([
      {
        path: EDIT_SECTION_ITEM_PATH_PATTERN.toString(),
        component: () => import('./workspace.element'),
        setup: async (_component, info) => {
          console.log("[ExampleWorkspace] Edit route setup callback fired");
          const unique = info.match.params.unique;
          console.log("[ExampleWorkspace] Loading unique:", unique);
          await this.load(unique);
        },
      },
      {
        path: CREATE_SECTION_ITEM_PATH_PATTERN.toString(),
        component: () => import('./workspace.element'),
        setup: async (_component, info) => {
          console.log("[ExampleWorkspace] Create route setup callback fired");
          const parentEntityType = info.match.params.parentEntityType;
          const parentUnique = info.match.params.parentUnique === 'null' ? null : info.match.params.parentUnique;
          await this.createScaffold({ entityType: parentEntityType, unique: parentUnique });
        },
      },
    ]);
  }

  /**
   * Load data for the given unique identifier
   * This is called every time a different tree item is selected
   */
  async load(unique: string): Promise<void> {
    console.log("[ExampleWorkspace] Loading data for unique:", unique);

    // Update the unique
    this.#unique.setValue(unique);
    this.setIsNew(false);


    // Go off and fetch the data somehow, setup with mock data for now so we don't need api
    const mockData: SectionItemModel = {
      unique,
      name: `Item ${unique}`,
    };
    this.#data.setValue(mockData);
  }

  /**
   * Create a new scaffold for creating an item
   */
  async createScaffold(parent: { entityType: string; unique: string | null }): Promise<void> {
    console.log("[ExampleWorkspace] Creating scaffold under parent:", parent);

    this.setIsNew(true);

    // Just some filler code to make a new item...
    const newUnique = 'new-' + Math.random().toString(36).substring(7);
    this.#unique.setValue(newUnique);

    const scaffoldData: SectionItemModel = {
      unique: newUnique,
      name: '',
    };
    this.#data.setValue(scaffoldData);
  }

  // Required by UmbSubmittableWorkspaceContextBase
  getUnique(): string | null {
    return this.#unique.getValue();
  }

  getEntityType(): string {
    return SECTION_ENTITY;
  }

  getData(): SectionItemModel | undefined {
    return this.#data.getValue();
  }

  // This is called when the workspace is submitted (e.g., save button clicked)
  protected async submit(): Promise<void> {
    console.log("[ExampleWorkspace] Submitting data");
    const data = this.getData();
    if (!data) {
      throw new Error('No data to submit');
    }

    // blah blah save logic

    console.log("[ExampleWorkspace] Would save:", data);
  }
}

export const SECTION_CONTEXT_TOKEN = new UmbContextToken<ExampleWorkspace>(
  CONTEXT_ALIAS,
);

it’s opening my workspace views fine, but only the first time a click on one, after that i have to navigate somewhere else before selecting a different poll link,no ide why or where to even look so giving up again for now, just can’t get my head around this stuff, getting too old!

Yeah - that was my exact issue too. I just couldn’t get it to route correctly when you click a new item in the tree! I tried listening to the routes (with the UMB_ROUTE_CONTEXT) and then I created a global context to watch it and make the change (which kinda worked but listened on everything).

Ok thanks, something to play with at least