Clipboard copyText not working. Plugin is not permitted to make changes from the background

I got this error in developer console:

Plugin Error: Plugin ******** is not permitted to make changes from the background. Return a Promise to continue execution asynchronously.
    at convertPluginErrorToString (plugins/PluginErrorUtil.js:1:198)
    at internalFormatPluginError (plugins/PluginErrorUtil.js:1:1073)
    at internalReportPluginError (plugins/PluginErrorUtil.js:1:1180)
    at Object.reportPluginError (plugins/PluginErrorUtil.js:1:1612)
    at Object.checkAllowedToEdit (plugins/ScenegraphGuard.js:1:1577)
    at Object.<anonymous> (plugins/ClipboardWrapper.js:1:194)
    at VueComponent.copySCSSVariablesToClipboard (C:\...\main.js:1344:23)

my code

copySCSSVariablesToClipboard() {
            clipboard.copyText(this.scssVariables); // CRUSH HERE

            this.showNotification({
                text: 'Colors for SCSS is now available on the clipboard',
                color: 'green'
            });
        },

Repository link https://github.com/LeXX/xd-component-to-vue/blob/master/src/app.vue (145 row)

Apparently, even for copying, you have to wrap that in an editDocument(...).

So maybe something like

application.editDocument(() => clipboard.copyText(this.scssVariables))

would work (no options given to editDocument).

Yes, i’ve tried to do this before. But got this error:

[Vue warn]: Error in v-on handler: "Error: Panel plugin edits must be initiated from a supported UI event"

I use “@click” event

That sure makes no sense, as your copy-to-clipboard routine is being invoked as the result of a click.

Perhaps there’s some Vue-specific interaction that’s not triggering the “UI-invoked action” detection in UXP? @kerrishotts?

One possibility is that Vue is delaying the handler a tick after the actual event. XD only holds the scenegraph open for a single tick. So things like wrapping editDocument with a setTimeout won’t work, because by the time the timeout happens, XD has already closed the scenegraph.

Unfortunately, I’m not familiar enough with Vue to know if that’s what’s going on here – just bringing it up as a possibility.

@LeXX Does this also happen when you have a simple button with a @click event (and nothing more) and only happens in more “complex” scenarios or if it already happens in a minimal reproducable example? (if the latter, could you possibly create some sort of repo with this minimal example?)

I’m no expert in Vue myself, but to my knowledge, there is some sort of “tick” system (possibly deferring some events to the next document.requestAnimationFrame() or something like that) that could interfere here (again: I’m far from being versed in Vue, that’s just something I’ve heard of), especially if some event gets “passed up” the component chain.

Thank you very much in advance :+1: .

@pklaschka Of cource, i use this repo for simple sample https://github.com/AdobeXD/plugin-samples/tree/master/ui-hello-vue

My repo for testing with one button and input :slight_smile:

Current state of repository has an error

Error: Panel plugin edits must be initiated from a supported UI event
1 Like

@kerrishotts hello, what news?

Can you explain to us non-Vue experts why this would work?

@LeXX @cpryland

I deleted my previous post because I realized it still doesn’t resolve the issue upon further testing.

I replicated the issue and found a solution on my end.

Issue: Error: Panel plugin edits must be initiated from a supported UI event
Reason: The reason for this is you are calling editDocument() on a child component which isn’t wrapped with an edit batch. Plugin edits must be initiated on a panel UI or on a menu item.
image

Solution: My workaround on this is, I created a Vue Event Bus. I can now pass the data from the dialog to my main file and initiate the clipboard.copyText() since I am now at the plugin menu item commands scope.

Steps:

  1. Create a event-bus.js file
  2. Import vue import Vue from 'vue';
  3. Create a vue instance called EventBus const EventBus = new Vue();
  4. And export EventBus export default EventBus;

image

  1. On your hello.vue component require the EventBus
    const EventBus = require("./event-bus.js").default;

  2. On your copy method use the EventBus
    copy() { EventBus.$emit('copy', this.message); }
    This line will emit a custom event called copy with the message as the payload.

  3. On your main.js file, require the EventBus
    const EventBus = require("./event-bus.js").default;

  4. Then add a mounted method on your Vue instance.
    mounted() { EventBus.$on('copy', (message) => { clipboard.copyText(message); }); },
    This will create an event listener to copy, receive the message and initiate clipboard.copyText(message);

  5. I made the menuCommand async and await getDialog().showModal(); to ensure that further actions cant still be initiated.

I apologize if my post is a bit long. I’m still new at creating XD plugins and I’m still not an expert on VueJS.

I also created a pull request on your repo to see how I fixed the issue.

OK, but where does the application.editDocument() call go?

Seems like an awful lot of machinery for such a simple thing. You’d think the Vue click handler would be in a direct call path to the eventual .editDocument() call.

@elmer I checked your version, you are so right! I don’t know how you found a solution, but it solves the problem.

I make solution more simple, without event bus.
In main.js:

...
const copyToClipboard = string => {
    clipboard.copyText(string);
};

const getDialog = (selection, documentRoot) => {
    if (!dialog) {
        ...
        new Vue({
            ...
            render(h) {
                return h(app, {
                    props: {
                        ...
                        copyToClipboard, // now this.copyToClipboard('hello world') available on app.vue
                    },
                });
            }
        });
    }

    return dialog;
};

module.exports = {
    commands: {
        exportToVue: async (selection, documentRoot) => {
            await getDialog(selection, documentRoot).showModal(); // async await very important
        }
    }
};

@cpryland @kerrishotts application.editDocument() is no longer needed, because showModal() return Promise and we are write async/await-operation for waiting execution of this Promise. This means that any code inside main.js and getDialog() automatically wrapped as editDocument(). Now we should not use application.editDocument(), and we can write clipboard.copyText without editDocument(). Otherwise it will be a mistake, like this:

application.editDocument(() => {
    application.editDocument(() => {
        clipboard.copyText(string);
    });
});

I think any operations inside the application no longer need application.editDocument(). But I can be wrong because didn’t check the idea. I also didn’t check event type dependency as ‘click’ or ‘input’ and other types something else, but I guess the ‘mouseenter’ will work too.