Manifest v5 "The command “Make” is not currently available"

So I finally got my hands on migration from manifest v4 to v5 (been using API v2 since the beginning, but not new manifest). Seemed fine at first, but ended up spending a whole day just to find, that getters (which don’t even remotely change Ps state) also need to be inside executeAsModal()

Is that true or am I missing something?

This script runs perfectly fine from conslole, but when I put it into a plugin on button click it doesn’t work any more:

const photoshop = require("photoshop")

const layerId = await photoshop.action.batchPlay([{
    "_obj": "get",
    "_target": [
        { "_property": "targetLayersIDs" },
        { "_ref": "document", "_enum": "ordinal", "_value": "targetEnum" },
    ],
}], {
    synchronousExecution: true,
    modalBehavior: "execute",
})[0]["targetLayersIDs"][0]["_id"]

const rs = await photoshop.core.executeAsModal(async (executionContext) => {
    let suspensionID
    const hostControl = executionContext.hostControl

    suspensionID = await hostControl.suspendHistory({
        historyStateInfo: {
            name: "HISTORY",
            target: [{
                _ref: "document",
                _id: photoshop.app.activeDocument.id,
            }],
        },
    })

    await photoshop.action.batchPlay([
        {
            "_obj": "select",
            "_target": [{ "_ref": "layer", "_id": layerId }],
            "makeVisible": false,
        },
        {
            "_obj": "select",
            "_target": [{ "_ref": "channel", "_enum": "channel", "_value": "mask" }],
        },
        {
            "_obj": "delete",
            "_target": [{ "_ref": "channel", "_enum": "ordinal", "_value": "targetEnum" }],
        },
        {
            "_obj": "select",
            "_target": [{ "_ref": "layer", "_id": layerId }],
            "makeVisible": false,
        },
    ], {
        synchronousExecution: true,
        modalBehavior: "execute",
    })

    await photoshop.action.batchPlay([
        {
            "_obj": "select",
            "_target": [{ "_ref": "layer", "_id": layerId }],
            "makeVisible": false,
        },
        {
            "_obj": "make",
            "new": { "_class": "channel" },
            "at": { "_ref": "channel", "_enum": "channel", "_value": "mask" },
            "using": { "_enum": "userMaskEnabled", "_value": "hideAll" },
        },
        { "_obj": "select", "_target": [{ "_ref": "layer", "_id": layerId }], "makeVisible": false },
    ], {
        synchronousExecution: true,
        modalBehavior: "execute",
    })

    hostControl.resumeHistory(suspensionID)
}, { commandName: "COMMAND NAME" })

In a nutshell:

  • It gets selected layer
  • Tries to delete a mask if it’s there
  • Tries to create a new black mask on that layer
  • Reselects the layer

As mentioned, from console it works every time.
If I put it into a plugin, I get these errors on two BatchPlays inside executeAsModal():


BpErr1 = { // From the first chunk of descriptors (delete mask)
    "message": "My plugin: The command “Select” is not currently available.",
    "failedDescriptor": {
        "_obj": "select",
        "_target": [
            { "_ref": "channel", "_enum": "channel", "_value": "mask" },
        ],
    },
}

BpErr2 = { // From the second chunk of descriptors (create mask)
    "message": "My plugin: The command “Make” is not currently available.",
    "failedDescriptor": {
        "_obj": "make",
        "new": { "_class": "channel" },
        "at": { "_ref": "channel", "_enum": "channel", "_value": "mask" },
        "using": { "_enum": "userMaskEnabled", "_value": "hideAll" },
    },
}

What I found after hours and hours of debugging, that if I move this part:

const layerId = await photoshop.action.batchPlay([{
    "_obj": "get",
    "_target": [
        { "_property": "targetLayersIDs" },
        { "_ref": "document", "_enum": "ordinal", "_value": "targetEnum" },
    ],
}], {
    synchronousExecution: true,
    modalBehavior: "execute",
})[0]["targetLayersIDs"][0]["_id"]

inside the same executeAsModal() a bit lower, it starts working fine…

At some point I tried just setting manifest version back to 4, but that didn’t help and the only thing I changed, was panels part in entrypoints.setup(). Also noticed, that even with getter outside execModal maybe once in 50 times it creates a mask (if there’s a mask already, it deletes every time)

What’s the deal? :melting_face:

So I was going through this 2.5 yars old thread and it seems it’s the exact same issue I’m having and there’s no solution :disappointed: Looks like I’m gonna have to stay on manifest v4 for the time being… Unfortunately this prevents further development of the plugin

I’ll try a bit more of just guessing things and seeing if something works, but I’m pretty hopeless now

Or I think I’ll just move this:

const layerId = await photoshop.action.batchPlay([{
    "_obj": "get",
    "_target": [
        { "_property": "targetLayersIDs" },
        { "_ref": "document", "_enum": "ordinal", "_value": "targetEnum" },
    ],
}], {
    synchronousExecution: true,
    modalBehavior: "execute",
})[0]["targetLayersIDs"][0]["_id"]

into the same execModal, which seems the only way that makes script work, but doesn’t really make sense, because it doesn’t work only for mask creation (mask delete and other state changing actions work fine with targetLayersIDs getter outside execModal)

I believe there’s something still broken with this since Ps v23.4.1
Not sure whom to ping :confused: @Erin_Finnegan, could you please jump in?

I mean we can ping @indranil and @samgannaway, perhaps?

I literally copied and pasted your code into a button click function or even outside just when the plugin loads. it works without any errors.

This is my manifest maybe something here helps?

{
  "id": "ONIRICUXP",
  "name": "Oniric v2.3.0 (UXP)",
  "version": "2.3.0",
  "main": "index.js",
  "host": [
    {
      "app": "PS",
      "minVersion": "23.0.0",
      "data": {
        "apiVersion": 2,
        "loadEvent": "use"
      }
    }
  ],
  "requiredPermissions": {
    "localFileSystem": "fullAccess",
    "launchProcess": {
      "extensions": [
        ""
      ],
      "schemes": [
        "https"
      ]
    },
    "network": {
      "domains": "all"
    },
    "allowCodeGenerationFromStrings": true
  },
  "manifestVersion": 5,
  "entrypoints": [
    {
      "type": "panel",
      "id": "oniricUXP",
      "minimumSize": {
        "width": 260,
        "height": 300
      },
      "maximumSize": {
        "width": 1000,
        "height": 1920
      },
      "preferredDockedSize": {
        "width": 260,
        "height": 1010
      },
      "preferredFloatingSize": {
        "width": 260,
        "height": 1010
      },
      "label": {
        "default": "Oniric v2.3.0 (UXP)"
      },
      "icons": [
        {
          "width": 23,
          "height": 23,
          "path": "icons/oniric.png",
          "scale": [
            1,
            2
          ],
          "theme": [
            "dark",
            "darkest",
            "medium",
            "light",
            "lightest",
            "all"
          ]
        }
      ]
    }
  ],
  "icons": [
    {
      "width": 48,
      "height": 48,
      "path": "icons/on.png",
      "scale": [
        1,
        2
      ],
      "theme": [
        "darkest",
        "dark",
        "medium",
        "lightest",
        "light"
      ],
      "species": [ "pluginList" ]
    }
  ],
  "runOnStartup": true
}
1 Like

Thanks, @photonic ! :raised_hands:

I’m now going step by step to see what exactly I’ve changed in my code. I did Node modules update almost all to latest versions and did quite a bit of refactoring, which was exclusively only moving files/folders. I didn’t change any code at all, but clearly something broke :frowning:

Will definitely let you know when/if I find something. I’m literally now trying to move in very small steps and after each step check if it still works or not. Might take a while… :disappointed:

Here’s my version of what you’re likely trying to achieve. I wrap every function I use and execute it as a model, as I’ve found that with version 5, most Photoshop functionalities require it.


 const app = require("photoshop").app;
 const batchPlay = require("photoshop").action.batchPlay;
const ExecuteAsModal = require("photoshop").core.executeAsModal;

function getSelectedLayerInfo() {
  if (!app.activeDocument) {
    console.warn("No active document found.");
    return [];
  }

  // Retrieve the names and IDs of selected layers
  return app.activeDocument.layers
    .filter((layer) => layer.selected) 
    .map((layer) => ({
      id: layer.id, // Layer ID
      name: layer.name, 
    })); 
}
 

async function checkAndDeleteLayerMask(layerId, documentId) {
  // check if active layer has a layer mask
  const result = await batchPlay(
    [
      {
        _obj: "get",
        _target: [
          {
            _property: "hasUserMask",
          },
          {
            _ref: "layer",
            _id: layerId,
          },
          {
            _ref: "document",
            _id: documentId,
          },
        ],
        _options: {
          dialogOptions: "dontDisplay",
        },
      },
    ],
    {}
  );

  const hasLayerMask = result[0].hasUserMask;
  console.log("hasUserMask?", hasLayerMask);

  if (hasLayerMask === true) {
    await deleteChannel();
  }


}


// add layer mask   Black = "hideAll" / white mask "revealAll"
async function makeMask(mask) {
  const makeLayerMask = {
    _obj: "make",
    at: {
      _enum: "channel",
      _ref: "channel",
      _value: "mask",
    },
    new: {
      _class: "channel",
    },
    using: {
      _enum: "userMaskEnabled",
      _value: mask,
    },
  };
  await batchPlay([makeLayerMask], {});
}


async function FunctionsX(executionContext) {
  if (app.documents.length == 0) {
    app.showAlert("Please open at least one document.");

    return;
  }
  let hostControl = executionContext.hostControl;
  let suspensionID = await hostControl.suspendHistory({
    documentID: app.activeDocument?.id,
    name: "History",
  });


  try {
    const selectedLayerInfo = getSelectedLayerInfo();
    console.log("Selected Layer Info:", selectedLayerInfo);

    if (selectedLayerInfo.length > 0) {
      await selectLayer({ id: selectedLayerInfo[0].id });
    } else {
      console.log("No layers selected.");
    }

    await checkAndDeleteLayerMask(
      app.activeDocument.activeLayers[0]._id,
      app.activeDocument?.id
    );

  makeMask("hideAll");

    await hostControl.resumeHistory(suspensionID);
  } catch (error) {
    console.log(error);
 
  }
}

document
  .getElementById("SlectLayer")
  .addEventListener("click", async function () {
    await ExecuteAsModal(FunctionsX, {
      commandName: "Fucntions X command",
    });

  });

Yep, but only those, which change the state. Getting selected layers doesn’t require that :confused: I’m still trying to find the exact moment it stops working while re-refactoring

I’m using "manifestVersion": 5, in my manifest.json and the following getter code does NOT require executeAsModal:

let selection = await batchPlay([
//Check to see if there is a selection
{ _obj: "multiGet", _target: {_ref: "document", _enum: "ordinal", _value: "targetEnum"}, extendedReference: [["selection"]], "_options": { "dialogOptions": "silent" }}], {})
1 Like

:man_facepalming: :man_facepalming: :man_facepalming:

So I used to have this (full execModal() function):

export const execModal = async (
    func, { commandName, historyState },
) => {
    return photoshop.core.executeAsModal(async (executionContext) => {
        let suspensionID
        const hostControl = executionContext.hostControl

        if (historyState && photoshop.app.activeDocument) {
            suspensionID = await hostControl.suspendHistory({
                historyStateInfo: {
                    name: historyState,
                    target: [{
                        _ref: "document",
                        _id: photoshop.app.activeDocument.id,
                    }],
                },
            })
        }

        await func()

        if (historyState && photoshop.app.activeDocument) {
            hostControl.resumeHistory(suspensionID)
        }

        return
    }, { commandName })
}

After refactoring I changed the if history part to this:

        if (historyState && photoshop.app.activeDocument) {
            suspensionID = await hostControl.suspendHistory({
                historyStateInfo: {
                    name: historyState,
                    target: [{
                        _ref: "document",
                        _id: photoshop.app.activeDocument.id,
                    }],
                },
            })

            await func()
            hostControl.resumeHistory(suspensionID)
        } else {
            func()
        }

Lost the await on func() call when without history suspension…

So what happens is, when I click the button, for whatever reason I get two calls to execModal() - first one without history suspension (I assume it’s some getter maybe) and the next one with suspension (the mask delete and create). Before the first one finishes, second one executes and fails…

Will need to find out why there are two calls to execModal() in my code. Any ideas how to trace back where from I’m calling it?

But now it’s 3AM and time to get some sleep :pensive:

This can save you a lot of hair: no-floating-promises | typescript-eslint :smiley:

I set the es-lint to check before build and make build fail if there is floating promise.

1 Like

Nice :slight_smile: Noticed in the docs some things I might benefit from so I updated my config, but it seems I already had everything covered. It’s just that the callback func in my case may be both async and not, so it doesn’t trigger the error :frowning: But I guess to be safe, if I’m inside async funcion, to almost always add await as it will instantly resolve anyway :smiley:

If anyone is interested, I had nested execModal() calls and it was something like:

execModal( without_history_suspension )
|- getSelectedLayers()
'- execModal( with_history_suspension )
   |- deleteMask()
   '- createMask()