Skip to content

mutation editor: add open in new tab button#2394

Merged
MetRonnie merged 7 commits intocylc:masterfrom
oliver-sanders:2390
Jan 7, 2026
Merged

mutation editor: add open in new tab button#2394
MetRonnie merged 7 commits intocylc:masterfrom
oliver-sanders:2390

Conversation

@oliver-sanders
Copy link
Member

@oliver-sanders oliver-sanders commented Dec 31, 2025

Check List

  • I have read CONTRIBUTING.md and added my name as a Code Contributor.
  • Contains logically grouped changes (else tidy your branch by rebase).
  • Does not contain off-topic changes (use other PRs for other changes).
  • Applied any dependency changes to both setup.cfg (and conda-environment.yml if present).
  • Tests are included (or explain why tests are not needed).
  • Changelog entry included if this is a change that can affect users
  • Cylc-Doc pull request opened if required at cylc/cylc-doc/pull/XXXX.
  • If this is a bug fix, PR should be raised against the relevant ?.?.x branch.

* Closes cylc#2390
* Add a button which opens the mutation editor in a new tab, preserving
  any edits the user had made to that point.
* Reinstates the old developer Mutation View (removed in cylc#1029)
// For now, Info view cannot be opened for a workflow, but this will be resolved by
// https://github.com/cylc/cylc-ui/issues/1898
['Info', { component: InfoView, icon: mdiInformationOutline }],
['Command', { component: MutationView, icon: mdiInformationOutline }],
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note, "Command" is the user-facing term, "Mutation" comes from the GraphQL implementation. We should probably rename the "Mutation" component etc one day...

},
cylcObject: {
// data store node
initialOptions: {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved props into initialOptions to allow this component to be used as a view.

With this state will be restored as normal when the user navigates to another workflow then back.

Comment on lines +290 to +297
// Navigate to the corresponding workflow then open the log view
// (no nav occurs if already on the correct workflow page)
this.$router.push({
name: 'Workspace',
params: {
workflowName: this.cylcObject.tokens.workflow
}
}).then(() => {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same thing we do in Menu.vue for the benefit of the standalone views (otherwise the open in new tab button would do nothing in these contexts).

@oliver-sanders oliver-sanders self-assigned this Dec 31, 2025
@oliver-sanders
Copy link
Member Author

oliver-sanders commented Dec 31, 2025

One test was failing in CI in 2/3 browsers (but not locally for me?) due to the mutation editor form remaining open (but not visible) after being closed.

2d7141c changes the logic to destroy the dialog when not in use which fixes this test.

@oliver-sanders oliver-sanders added this to the 2.12.0 milestone Dec 31, 2025
// set the tab title to something informative
eventBus.emit(
`lumino:update-tab:${props.widgetID}`,
{ title: `Command: ${props.initialOptions.mutation._title}` },
Copy link
Member

@wxtim wxtim Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has the potential to create rather long titles -
image

Might we consider borrowing the pencil icon from the Menu to replace "command" and so shorten this?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I wanted to raise this.

Icons would be possible, and might make sense in general (e.g, for log views), but we haven't added an interface for achieving this as yet.

Screenshot from Jupyter Lab for comparison:

Screenshot from 2026-01-05 13-05-53

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Long term, we may be able to use the Jupyter Lab API for this (LabIcon) though it's react, so we may end up going our own way.

Short term, the only solution I can come up with is a unicode icon which would probably require a special icon font.

content-class="c-mutation-dialog mx-0"
>
<Mutation
:mutation="dialogMutation"
Copy link
Member

@wxtim wxtim Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you do something like

image

I feel that if people like this feature they probably won't like the extra click.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would be possible, but it would make the menus wider and more complicated.

Copy link
Member

@wxtim wxtim left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blockers. Some UX suggestions.

Copy link
Member

@MetRonnie MetRonnie left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems to work well, however I have noticed and am looking into how this is now storing 300+ kB in cache storage as part of the initialOptions. This is mainly due to storing introspection query data.

Also, getting a warning when the form is mounted as a tab:

[Vue warn]: Extraneous non-props attributes (workflow-name, class) were passed to component but could not be automatically inherited because component renders fragment or text or teleport root nodes. 
  at <VDefaultsProvider defaults= {global: {…}, VAutocomplete: {…}, VCombobox: {…}, VSelect: {…}, VTextarea: {…}, …} workflow-name="one" class="h-100" > 
  at <Mutation workflow-name="one" initial-options= {mutation: Proxy(Object), cylcObject: Proxy(Object), data: {…}, types: Proxy(Array), initialData: {…}, …} onUpdate:initialOptions=fn<onUpdate:initialOptions>  ... > 
  at <AsyncComponentWrapper workflow-name="one" initial-options= {mutation: Proxy(Object), cylcObject: Proxy(Object), data: {…}, types: Proxy(Array), initialData: {…}, …} onUpdate:initialOptions=fn<onUpdate:initialOptions>  ... > 
  at <Widget key="widget70" id="widget70" > 
  at <Lumino key="one" onEmptied=fn<bound onEmptied> workflow-name="one"  ... > 
  at <Workspace workflowName="one" onVnodeUnmounted=fn<onVnodeUnmounted> ref=Ref< Proxy(Object) {setAlert: ƒ, onEmptied: ƒ, …} > > 
  at <RouterView> 
  at <VMain> 
  at <Default showSidebar=true > 
  at <VApp class="job_theme--default" > 
  at <VDefaultsProvider defaults= {global: {…}} > 
  at <App>


// and close this menu
this.close()
this.$emit('success')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why emit success here?

Copy link
Member Author

@oliver-sanders oliver-sanders Jan 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are closing both the menu and the overlay which are handled in two different ways.

@oliver-sanders
Copy link
Member Author

oliver-sanders commented Jan 6, 2026

Also, getting a warning when the form is mounted as a tab

Addressed in 4c19030, tldr, don't use vDefaultsProvider as a component's root node.

@oliver-sanders
Copy link
Member Author

I have noticed and am looking into how this is now storing 300+ kB in cache storage

TBH, I'm not too concerned by this personally:

  • Having a command tab open is relatively unusual. Having multiple open is going to be very unusual.
  • It's unlikely a command tab would be left open long-term (as they serve a short term purpose).
  • Layouts are housekept.
  • Technically, to restore a generated form we really need to restore both the form data and structure, otherwise the two could end up out of sync.

If worried, we could:

  • Write our own introspection query which only requests the things we actually need (rather than using the prefab query which requests a lot more besides).
  • Process the introspection query up-front, so the form builder works off of an intermediate format, not directly off of GraphQL.
  • Re-acquire types on restoration (bind the mutation component into the workflowService), accept any inconsistency this may cause with form data (unlikely?).

* The `vDefaultsProvider` component has multiple "root" nodes which
  makes it unsuitable for use as a component's "root" node in some
  situations.
@MetRonnie
Copy link
Member

Unfortunately I have found that opening a command editor in a tab and then navigating away to a different workflow results in

dockpanel.ts:361 Uncaught (in promise) TypeError: Cannot read properties of null (reading 'saveLayout')
    at _DockPanel.saveLayout (dockpanel.ts:361:40)
    at saveLayout (Lumino.vue:197:23)
    at reactivity.js:90:78
    at callWithErrorHandling (runtime-core.esm-bundler.js:199:19)
    at callWithAsyncErrorHandling (runtime-core.esm-bundler.js:206:17)
    at baseWatchOptions.call (runtime-core.esm-bundler.js:6295:47)
    at job (reactivity.esm-bundler.js:1838:18)
    at callWithErrorHandling (runtime-core.esm-bundler.js:199:33)
    at flushJobs (runtime-core.esm-bundler.js:408:9)
    at flushJobs (runtime-core.esm-bundler.js:430:7)

@oliver-sanders
Copy link
Member Author

Unfortunately I have found that opening a command editor in a tab and then navigating away to a different workflow results in

This seems to be a harmless error (layout restoration including form state still works fine) which is confusing.

It seems that saveLayout is somehow being called before the dockPanel has been initialised? Not sure, but, just skipping if the layout is undefined suppresses the error fcf6a19 and doesn't cause any issues with restoration.

Comment on lines +96 to 100
data: {
type: Object,
required: false,
default: () => { return {} }, // for ease of testing
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI: This is passing data as a prop, however, this object is modified from either end.

If you were to do this with an Boolean, Array or whatever, Vue would complain. However, when you pass an Object, it doesn't complain and seems to work fine.

But I'm not sure the absence of a warning means it's ok? The Object is constant and reactivity seems fine, however, the more Vue'ey way to work is using a model.

These components already implement a model (for form validity). It is possible for a component to implement multiple models (or just encapsulate validity into the data?), but I wasn't able to work out how to do this in time.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When objects and arrays are passed as props, while the child component cannot mutate the prop binding, it will be able to mutate the object or array's nested properties. This is because in JavaScript objects and arrays are passed by reference, and it is unreasonably expensive for Vue to prevent such mutations.

The main drawback of such mutations is that it allows the child component to affect parent state in a way that isn't obvious to the parent component, potentially making it more difficult to reason about the data flow in the future. As a best practice, you should avoid such mutations unless the parent and child are tightly coupled by design. In most cases, the child should emit an event to let the parent perform the mutation.

https://vuejs.org/guide/components/props.html#mutating-object-array-props

It is probably ok for now in this PR

@wxtim
Copy link
Member

wxtim commented Jan 7, 2026

I haven't been able to replicate the problem from at 4c19030 either in dev mode or with a full build.

I have now also tried with the latest version.

@oliver-sanders
Copy link
Member Author

oliver-sanders commented Jan 7, 2026

I haven't been able to replicate the problem from at 4c19030 either in dev mode or with a full build.

I replicated the issue with a dev build:

  • Open a workflow.
  • Open the command editor.
  • Open the command in a new tab.
  • Navigate to another workflow.
  • Check the console (gotta look for the error, no functional symptoms, no red box, no issues restoring the layout).

FYI, the layout is saved when the editor is opened in a tab, I think it tries again on navigate, but there's some sort of lifecycle issue with how saveLayout is called? The routine was failing part way through so never completed, hence didn't cause issues.

@wxtim
Copy link
Member

wxtim commented Jan 7, 2026

This is what I did - including the F12!

I eventually got something similar:

TypeError: can't access property "saveLayout", this.layout is null

Which goes away with your fix. 😃

Copy link
Member

@MetRonnie MetRonnie left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got a few suggestions at oliver-sanders#125

Copy link
Member

@wxtim wxtim left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed

@wxtim wxtim requested a review from MetRonnie January 7, 2026 15:47
@MetRonnie MetRonnie merged commit c3c5987 into cylc:master Jan 7, 2026
7 checks passed
@oliver-sanders oliver-sanders deleted the 2390 branch January 7, 2026 16:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

commands: add option to open the form editor overlay in a new tab

3 participants