From 9383f38c18901a1448078a2fbc5d3ee5cfc7556b Mon Sep 17 00:00:00 2001
From: Etienne Laurent <git.sabbath464@passmail.net>
Date: Wed, 29 Jan 2025 17:40:28 +0100
Subject: [PATCH 1/4] pass moduleName to modal

---
 .../piece-type/ui/apos/components/AposUtilityOperations.vue      | 1 +
 1 file changed, 1 insertion(+)

diff --git a/modules/@apostrophecms/piece-type/ui/apos/components/AposUtilityOperations.vue b/modules/@apostrophecms/piece-type/ui/apos/components/AposUtilityOperations.vue
index a46a0fda62..b94c9ae403 100644
--- a/modules/@apostrophecms/piece-type/ui/apos/components/AposUtilityOperations.vue
+++ b/modules/@apostrophecms/piece-type/ui/apos/components/AposUtilityOperations.vue
@@ -80,6 +80,7 @@ export default {
 
       if (modal) {
         await apos.modal.execute(modal, {
+          moduleName: this.moduleOptions.name,
           moduleAction: this.moduleOptions.action,
           action,
           labels: this.moduleLabels,

From 51ebafab9287eb65ab710ae699bdf91a6969db9b Mon Sep 17 00:00:00 2001
From: Etienne Laurent <git.sabbath464@passmail.net>
Date: Wed, 29 Jan 2025 17:40:45 +0100
Subject: [PATCH 2/4] add documentVersions flag

---
 modules/@apostrophecms/file-tag/index.js  | 1 +
 modules/@apostrophecms/file/index.js      | 1 +
 modules/@apostrophecms/image-tag/index.js | 1 +
 modules/@apostrophecms/image/index.js     | 1 +
 4 files changed, 4 insertions(+)

diff --git a/modules/@apostrophecms/file-tag/index.js b/modules/@apostrophecms/file-tag/index.js
index 4e7cd5bde4..78edf3a832 100644
--- a/modules/@apostrophecms/file-tag/index.js
+++ b/modules/@apostrophecms/file-tag/index.js
@@ -5,6 +5,7 @@ module.exports = {
     pluralLabel: 'apostrophe:fileTags',
     quickCreate: false,
     autopublish: true,
+    documentVersions: true,
     editRole: 'editor',
     publishRole: 'editor',
     shortcut: 'G,Shift+F',
diff --git a/modules/@apostrophecms/file/index.js b/modules/@apostrophecms/file/index.js
index 2047416628..16a67dabb6 100644
--- a/modules/@apostrophecms/file/index.js
+++ b/modules/@apostrophecms/file/index.js
@@ -18,6 +18,7 @@ module.exports = {
     insertViaUpload: true,
     slugPrefix: 'file-',
     autopublish: true,
+    documentVersions: true,
     editRole: 'editor',
     publishRole: 'editor',
     showPermissions: true,
diff --git a/modules/@apostrophecms/image-tag/index.js b/modules/@apostrophecms/image-tag/index.js
index 2e9a732566..99f1cc1b79 100644
--- a/modules/@apostrophecms/image-tag/index.js
+++ b/modules/@apostrophecms/image-tag/index.js
@@ -5,6 +5,7 @@ module.exports = {
     pluralLabel: 'apostrophe:imageTags',
     quickCreate: false,
     autopublish: true,
+    documentVersions: true,
     editRole: 'editor',
     publishRole: 'editor',
     shortcut: 'G,Shift+I',
diff --git a/modules/@apostrophecms/image/index.js b/modules/@apostrophecms/image/index.js
index 102987f568..ec6081b4fd 100644
--- a/modules/@apostrophecms/image/index.js
+++ b/modules/@apostrophecms/image/index.js
@@ -24,6 +24,7 @@ module.exports = {
     searchable: false,
     slugPrefix: 'image-',
     autopublish: true,
+    documentVersions: true,
     editRole: 'editor',
     publishRole: 'editor',
     showPermissions: true,

From e5c3ca8a2a1e8665331dc8613d146f2b6af47982 Mon Sep 17 00:00:00 2001
From: Etienne Laurent <git.sabbath464@passmail.net>
Date: Thu, 30 Jan 2025 11:17:12 +0100
Subject: [PATCH 3/4] change documentVersions to versions

---
 modules/@apostrophecms/file-tag/index.js  | 2 +-
 modules/@apostrophecms/file/index.js      | 2 +-
 modules/@apostrophecms/image-tag/index.js | 2 +-
 modules/@apostrophecms/image/index.js     | 2 +-
 4 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/modules/@apostrophecms/file-tag/index.js b/modules/@apostrophecms/file-tag/index.js
index 78edf3a832..db4a836539 100644
--- a/modules/@apostrophecms/file-tag/index.js
+++ b/modules/@apostrophecms/file-tag/index.js
@@ -5,7 +5,7 @@ module.exports = {
     pluralLabel: 'apostrophe:fileTags',
     quickCreate: false,
     autopublish: true,
-    documentVersions: true,
+    versions: true,
     editRole: 'editor',
     publishRole: 'editor',
     shortcut: 'G,Shift+F',
diff --git a/modules/@apostrophecms/file/index.js b/modules/@apostrophecms/file/index.js
index 16a67dabb6..32d679dc73 100644
--- a/modules/@apostrophecms/file/index.js
+++ b/modules/@apostrophecms/file/index.js
@@ -18,7 +18,7 @@ module.exports = {
     insertViaUpload: true,
     slugPrefix: 'file-',
     autopublish: true,
-    documentVersions: true,
+    versions: true,
     editRole: 'editor',
     publishRole: 'editor',
     showPermissions: true,
diff --git a/modules/@apostrophecms/image-tag/index.js b/modules/@apostrophecms/image-tag/index.js
index 99f1cc1b79..e64bed8fdd 100644
--- a/modules/@apostrophecms/image-tag/index.js
+++ b/modules/@apostrophecms/image-tag/index.js
@@ -5,7 +5,7 @@ module.exports = {
     pluralLabel: 'apostrophe:imageTags',
     quickCreate: false,
     autopublish: true,
-    documentVersions: true,
+    versions: true,
     editRole: 'editor',
     publishRole: 'editor',
     shortcut: 'G,Shift+I',
diff --git a/modules/@apostrophecms/image/index.js b/modules/@apostrophecms/image/index.js
index ec6081b4fd..49e98325bb 100644
--- a/modules/@apostrophecms/image/index.js
+++ b/modules/@apostrophecms/image/index.js
@@ -24,7 +24,7 @@ module.exports = {
     searchable: false,
     slugPrefix: 'image-',
     autopublish: true,
-    documentVersions: true,
+    versions: true,
     editRole: 'editor',
     publishRole: 'editor',
     showPermissions: true,

From e81a875b62b5db6b1bf4dee132028342c1ac553a Mon Sep 17 00:00:00 2001
From: Etienne Laurent <git.sabbath464@passmail.net>
Date: Thu, 30 Jan 2025 19:08:58 +0100
Subject: [PATCH 4/4] add custom context menu to images

---
 .../components/AposMediaManagerEditor.vue     | 138 +++++++++++++++++-
 1 file changed, 132 insertions(+), 6 deletions(-)

diff --git a/modules/@apostrophecms/image/ui/apos/components/AposMediaManagerEditor.vue b/modules/@apostrophecms/image/ui/apos/components/AposMediaManagerEditor.vue
index 86449f32b5..e21d629abf 100644
--- a/modules/@apostrophecms/image/ui/apos/components/AposMediaManagerEditor.vue
+++ b/modules/@apostrophecms/image/ui/apos/components/AposMediaManagerEditor.vue
@@ -113,6 +113,7 @@ import dayjs from 'dayjs';
 import { isEqual } from 'lodash';
 import advancedFormat from 'dayjs/plugin/advancedFormat';
 import { createId } from '@paralleldrive/cuid2';
+import checkIfConditions from 'apostrophe/lib/check-if-conditions';
 
 dayjs.extend(advancedFormat);
 
@@ -139,7 +140,7 @@ export default {
       }
     }
   },
-  emits: [ 'back', 'modified' ],
+  emits: [ 'back', 'modified', 'close' ],
   data() {
     return {
       // Primarily use `activeMedia` to support hot-swapping image docs.
@@ -150,7 +151,8 @@ export default {
       original: klona(this.media),
       lipKey: '',
       triggerValidation: false,
-      showReplace: false
+      showReplace: false,
+      customOperations: apos.modules['@apostrophecms/doc'].contextOperations
     };
   },
   computed: {
@@ -160,11 +162,97 @@ export default {
     canLocalize() {
       return this.moduleOptions.canLocalize && this.activeMedia._id;
     },
+    canPublish() {
+      if (this.activeMedia._id) {
+        console.log('this.activeMedia._publish', this.activeMedia._publish);
+        return this.activeMedia._publish;
+      } else {
+        return this.moduleOptions.canPublish;
+      }
+    },
+    canEdit() {
+      if (this.activeMedia._id) {
+        console.log('this.activeMedia._edit', this.activeMedia._edit);
+        return this.activeMedia._edit;
+      }
+      return this.moduleOptions.canEdit;
+    },
+    customMenusByContext() {
+      if (!this.canEdit) {
+        return [];
+      }
+
+      const menus = this.customOperationsByContext
+        .map(op => ({
+          label: op.label,
+          action: op.action,
+          modifiers: op.modifiers || []
+        }));
+      menus.sort((a, b) => a.modifiers.length - b.modifiers.length);
+      return menus;
+    },
+    customOperationsByContext() {
+      console.log(this.customOperations);
+      return this.customOperations.filter(({
+        manuallyPublished, hasUrl, conditions, context, if: ifProps, moduleIf
+      }) => {
+        if (typeof manuallyPublished === 'boolean' && manuallyPublished !== this.manuallyPublished) {
+          return false;
+        }
+
+        if (typeof hasUrl === 'boolean' && hasUrl !== this.hasUrl) {
+          return false;
+        }
+
+        if (conditions) {
+          const notAllowed = conditions.some((action) => !this[action]);
+
+          if (notAllowed) {
+            return false;
+          }
+        }
+
+        ifProps = ifProps || {};
+        moduleIf = moduleIf || {};
+        const canSeeOperation = checkIfConditions(this.activeMedia, ifProps) &&
+            checkIfConditions(this.moduleOptions, moduleIf);
+        console.log(this.moduleName, canSeeOperation);
+
+        if (!canSeeOperation) {
+          return false;
+        }
+
+        return context === 'update' && this.isUpdateOperation;
+      });
+    },
+    moduleName() {
+      console.log('this.activeMedia.type', this.activeMedia.type);
+      return this.activeMedia.type;
+    },
+    isUpdateOperation() {
+      console.log('this.activeMedia._id', this.activeMedia._id);
+      return !!this.activeMedia._id;
+    },
+    hasUrl() {
+      console.log('this.activeMedia._url', this.activeMedia._url);
+      return !!this.activeMedia._url;
+    },
+    manuallyPublished() {
+      return this.moduleOptions.localized && !this.autopublish;
+    },
+    autopublish() {
+      return this.activeMedia._aposAutopublish ?? this.moduleOptions.autopublish;
+    },
     moreMenu() {
-      const menu = [ {
-        label: 'apostrophe:discardChanges',
-        action: 'cancel'
-      } ];
+      console.log(this.customMenusByContext);
+      const menu = [
+        {
+          label: 'apostrophe:discardChanges',
+          action: 'cancel'
+        },
+        ...this.customMenusByContext
+      ];
+
       if (this.canLocalize) {
         menu.push({
           label: 'apostrophe:localize',
@@ -248,8 +336,46 @@ export default {
   },
   methods: {
     moreMenuHandler(action) {
+      const operation = this.customOperations.find(op => op.action === action);
+      if (operation) {
+        this.customAction(this.activeMedia, operation);
+        return;
+      }
+
       this[action]();
     },
+    async customAction(doc, operation) {
+      if (operation.replaces) {
+        const confirm = await apos.confirm({
+          heading: 'apostrophe:replaceHeadingPrompt',
+          description: this.$t('apostrophe:replaceDescPrompt'),
+          affirmativeLabel: 'apostrophe:replace',
+          icon: false
+        });
+        if (!confirm) {
+          return;
+        }
+        this.$emit('close', doc);
+      }
+      const props = {
+        moduleName: operation.moduleName || this.moduleName,
+        moduleLabels: this.moduleLabels,
+        // For backwards compatibility
+        doc,
+        ...docProps(doc),
+        ...operation.props
+      };
+      if (operation.type === 'event') {
+        apos.bus.$emit(operation.action, props);
+        return;
+      }
+      await apos.modal.execute(operation.modal, props);
+      function docProps(doc) {
+        return Object.fromEntries(Object.entries(operation.docProps || {}).map(([ key, value ]) => {
+          return [ key, doc[value] ];
+        }));
+      }
+    },
     async updateActiveDoc(newMedia) {
       newMedia = newMedia || {};
       this.showReplace = false;