From 96bd54b3edb749b7f6cdd487f89b9ff74c227d66 Mon Sep 17 00:00:00 2001 From: Arthur Schiwon Date: Wed, 12 Feb 2025 21:11:47 +0100 Subject: [PATCH 01/13] fix(workflowengine): require a web component as operation plugin solves an incompatibility issue when the providing app registers their code from an incompatible nextcloud-vue version. Also changes and clarifies WorkflowEngine API. This is necessary to stay compatible with the original way, but also promotes usage of the originally declared but never used "component" attribute on registration. Signed-off-by: Arthur Schiwon --- apps/workflowengine/src/components/Rule.vue | 28 +++++++++++++++++++-- apps/workflowengine/src/store.js | 4 +++ apps/workflowengine/src/workflowengine.js | 12 +++++++-- 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/apps/workflowengine/src/components/Rule.vue b/apps/workflowengine/src/components/Rule.vue index f6f0487f0cc33..c9632d6c19673 100644 --- a/apps/workflowengine/src/components/Rule.vue +++ b/apps/workflowengine/src/components/Rule.vue @@ -29,8 +29,13 @@
+ @@ -95,9 +100,14 @@ export default { error: null, dirty: this.rule.id < 0, originalRule: null, + element: null, + inputValue: '', } }, computed: { + /** + * @return {OperatorPlugin} + */ operation() { return this.$store.getters.getOperationForRule(this.rule) }, @@ -123,11 +133,24 @@ export default { }, mounted() { this.originalRule = JSON.parse(JSON.stringify(this.rule)) + + if (this.operation?.element) { + this.$refs.operationComponent.value = this.rule.operation + } else if (this.operation?.options) { + // keeping this in an else for apps that try to be backwards compatible and may ship both + // to be removed in 03/2028 + console.warn('Developer warning: `OperatorPlugin.options` is deprecated. Use `OperatorPlugin.element` instead.') + } }, methods: { async updateOperation(operation) { this.$set(this.rule, 'operation', operation) - await this.updateRule() + this.updateRule() + }, + async updateOperationByEvent(event) { + this.inputValue = event.detail[0] + this.$set(this.rule, 'operation', event.detail[0]) + this.updateRule() }, validate(/* state */) { this.error = null @@ -164,6 +187,7 @@ export default { if (this.rule.id < 0) { this.$store.dispatch('removeRule', this.rule) } else { + this.inputValue = this.originalRule.operation this.$store.dispatch('updateRule', this.originalRule) this.originalRule = JSON.parse(JSON.stringify(this.rule)) this.dirty = false diff --git a/apps/workflowengine/src/store.js b/apps/workflowengine/src/store.js index a07b0989357d8..84a76a644a8e9 100644 --- a/apps/workflowengine/src/store.js +++ b/apps/workflowengine/src/store.js @@ -127,6 +127,10 @@ const store = new Store({ return rule1.id - rule2.id || rule2.class - rule1.class }) }, + /** + * @param state + * @return {OperatorPlugin} + */ getOperationForRule(state) { return (rule) => state.operations[rule.class] }, diff --git a/apps/workflowengine/src/workflowengine.js b/apps/workflowengine/src/workflowengine.js index df7412f9f4ad9..00612ee04780d 100644 --- a/apps/workflowengine/src/workflowengine.js +++ b/apps/workflowengine/src/workflowengine.js @@ -30,10 +30,18 @@ import ShippedChecks from './components/Checks/index.js' * @property {string} id - The PHP class name of the check * @property {string} operation - Default value for the operation field * @property {string} color - Custom color code to be applied for the operator selector - * @property {Vue} component - A vue component to handle the rendering of options + * @property {object} [options] - Deprecated: **Use `element` instead** + * + * A vue component to handle the rendering of options. * The component should handle the v-model directive properly, * so it needs a value property to receive data and emit an input - * event once the data has changed + * event once the data has changed. + * + * Will be removed in 03/2028. + * @property {string} [element] - A web component id as used in window.customElements.define()`. + * It is expected that the ID is prefixed with the app namespace, e.g. oca-myapp-flow_do_this_operation + * It has to emit the `update:model-value` event when a value was changed. + * The `model-value` property will be set initially with the rule operation value. */ /** From 3e03793e6188720b935aeca301f2f02c7b79cb31 Mon Sep 17 00:00:00 2001 From: Arthur Schiwon Date: Thu, 13 Mar 2025 18:44:12 +0100 Subject: [PATCH 02/13] fix(workflowengine): require a web component as check plugin Similar case as with operator plugins (check previous commit). Although we are not aware of an existign problem, it is there in principle, and asjusting the API we stay consistent with that one from the operations. Signed-off-by: Arthur Schiwon --- apps/workflowengine/src/components/Check.vue | 33 ++++++++++++++++++-- apps/workflowengine/src/workflowengine.js | 12 +++++-- 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/apps/workflowengine/src/components/Check.vue b/apps/workflowengine/src/components/Check.vue index f560303b360c4..ce64d859139ad 100644 --- a/apps/workflowengine/src/components/Check.vue +++ b/apps/workflowengine/src/components/Check.vue @@ -19,8 +19,18 @@ :clearable="false" :placeholder="t('workflowengine', 'Select a comparator')" @input="updateCheck" /> + operator.operator === this.check.operator) + if (this.currentElement) { + console.error(this.$refs) + this.$refs.checkComponent.value = this.currentOption + } else if (this.currentOption?.component) { + // keeping this in an else for apps that try to be backwards compatible and may ship both + // to be removed in 03/2028 + console.warn('Developer warning: `CheckPlugin.options` is deprecated. Use `CheckPlugin.element` instead.') + } + if (this.check.class === null) { this.$nextTick(() => this.$refs.checkSelector.$el.focus()) } @@ -141,11 +165,14 @@ export default { this.check.invalid = !this.valid this.$emit('validate', this.valid) }, - updateCheck() { + updateCheck(event) { const matchingOperator = this.operators.findIndex((operator) => this.check.operator === operator.operator) if (this.check.class !== this.currentOption.class || matchingOperator === -1) { this.currentOperator = this.operators[0] } + if (event?.detail) { + this.check.value = event.detail[0] + } // eslint-disable-next-line vue/no-mutating-props this.check.class = this.currentOption.class // eslint-disable-next-line vue/no-mutating-props diff --git a/apps/workflowengine/src/workflowengine.js b/apps/workflowengine/src/workflowengine.js index 00612ee04780d..5a99ac54ef2c3 100644 --- a/apps/workflowengine/src/workflowengine.js +++ b/apps/workflowengine/src/workflowengine.js @@ -15,12 +15,20 @@ import ShippedChecks from './components/Checks/index.js' * @typedef {object} CheckPlugin * @property {string} class - The PHP class name of the check * @property {Comparison[]} operators - A list of possible comparison operations running on the check - * @property {Vue} component - A vue component to handle the rendering of options + * @property {Vue} component - Deprecated: **Use `element` instead** + * + * A vue component to handle the rendering of options. * The component should handle the v-model directive properly, * so it needs a value property to receive data and emit an input - * event once the data has changed + * event once the data has changed. + * + * Will be removed in 03/2028. * @property {Function} placeholder - Return a placeholder of no custom component is used * @property {Function} validate - validate a check if no custom component is used + * @property {string} [element] - A web component id as used in window.customElements.define()`. + * It is expected that the ID is prefixed with the app namespace, e.g. oca-myapp-flow_do_this_operation + * It has to emit the `update:model-value` event when a value was changed. + * The `model-value` property will be set initially with the rule operation value. */ /** From 492fa1e24c6c3ab05843fb59d89b456ddd1f793f Mon Sep 17 00:00:00 2001 From: Arthur Schiwon Date: Thu, 13 Mar 2025 18:45:37 +0100 Subject: [PATCH 03/13] fix(workflowengine): adapt check operator FileSystemTag to use web component Signed-off-by: Arthur Schiwon --- .../src/components/Checks/FileSystemTag.vue | 13 ++++++++----- .../src/components/Checks/file.js | 19 ++++++++++++++++++- package-lock.json | 7 +++++++ package.json | 1 + 4 files changed, 34 insertions(+), 6 deletions(-) diff --git a/apps/workflowengine/src/components/Checks/FileSystemTag.vue b/apps/workflowengine/src/components/Checks/FileSystemTag.vue index ee880aec075dd..e71b0cd259a2c 100644 --- a/apps/workflowengine/src/components/Checks/FileSystemTag.vue +++ b/apps/workflowengine/src/components/Checks/FileSystemTag.vue @@ -17,18 +17,21 @@ export default { NcSelectTags, }, props: { - value: { + modelValue: { type: String, default: '', }, }, + + emits: ['update:model-value'], + data() { return { newValue: [], } }, watch: { - value() { + modelValue() { this.updateValue() }, }, @@ -37,14 +40,14 @@ export default { }, methods: { updateValue() { - if (this.value !== '') { - this.newValue = parseInt(this.value) + if (this.modelValue !== '') { + this.newValue = parseInt(this.modelValue) } else { this.newValue = null } }, update() { - this.$emit('input', this.newValue || '') + this.$emit('update:model-value', this.newValue || '') }, }, } diff --git a/apps/workflowengine/src/components/Checks/file.js b/apps/workflowengine/src/components/Checks/file.js index 246f46ff55bea..5a5b5d22b41a6 100644 --- a/apps/workflowengine/src/components/Checks/file.js +++ b/apps/workflowengine/src/components/Checks/file.js @@ -80,8 +80,25 @@ const FileChecks = [ { operator: 'is', name: t('workflowengine', 'is tagged with') }, { operator: '!is', name: t('workflowengine', 'is not tagged with') }, ], - component: FileSystemTag, + webComponent: registerWebComponent(FileSystemTag, 'oca-workflowengine-file_system_tag'), }, ] +/** + * + * @param VueComponent + * @param webComponentId + */ +function registerWebComponent(VueComponent, webComponentId) { + const WrappedComponent = wrap(Vue, VueComponent) + window.customElements.define(webComponentId, WrappedComponent) + + // In Vue 2, wrap doesn't support disabling shadow :( + // Disable with a hack + Object.defineProperty(WrappedComponent.prototype, 'attachShadow', { value() { return this } }) + Object.defineProperty(WrappedComponent.prototype, 'shadowRoot', { get() { return this } }) + + return webComponentId +} + export default FileChecks diff --git a/package-lock.json b/package-lock.json index d6d0b7e015070..cd5c7879bf6a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,6 +32,7 @@ "@nextcloud/upload": "^1.9.1", "@nextcloud/vue": "^8.24.0", "@simplewebauthn/browser": "^12.0.0", + "@vue/web-component-wrapper": "^1.3.0", "@vueuse/components": "^11.1.0", "@vueuse/core": "^11.0.1", "@vueuse/integrations": "^11.3.0", @@ -6778,6 +6779,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@vue/web-component-wrapper": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@vue/web-component-wrapper/-/web-component-wrapper-1.3.0.tgz", + "integrity": "sha512-Iu8Tbg3f+emIIMmI2ycSI8QcEuAUgPTgHwesDU1eKMLE4YC/c/sFbGc70QgMq31ijRftV0R7vCm9co6rldCeOA==", + "license": "MIT" + }, "node_modules/@vueuse/components": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/@vueuse/components/-/components-11.1.0.tgz", diff --git a/package.json b/package.json index f86d59f55d18d..a44b085f6f2e3 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ "@nextcloud/upload": "^1.9.1", "@nextcloud/vue": "^8.24.0", "@simplewebauthn/browser": "^12.0.0", + "@vue/web-component-wrapper": "^1.3.0", "@vueuse/components": "^11.1.0", "@vueuse/core": "^11.0.1", "@vueuse/integrations": "^11.3.0", From eaf02682302cf6b95a82bdf187c9155955ec2937 Mon Sep 17 00:00:00 2001 From: Arthur Schiwon Date: Wed, 19 Mar 2025 22:32:36 +0100 Subject: [PATCH 04/13] fix(workflowengine): minimally adapt check operator FileMimeType to use web component Signed-off-by: Arthur Schiwon --- apps/workflowengine/src/components/Check.vue | 4 +-- .../src/components/Checks/FileMimeType.vue | 35 +++++++++++++++---- .../src/components/Checks/file.js | 22 ++---------- apps/workflowengine/src/helpers/window.js | 30 ++++++++++++++++ 4 files changed, 64 insertions(+), 27 deletions(-) create mode 100644 apps/workflowengine/src/helpers/window.js diff --git a/apps/workflowengine/src/components/Check.vue b/apps/workflowengine/src/components/Check.vue index ce64d859139ad..612a177c8c217 100644 --- a/apps/workflowengine/src/components/Check.vue +++ b/apps/workflowengine/src/components/Check.vue @@ -136,8 +136,8 @@ export default { this.currentOperator = this.operators.find((operator) => operator.operator === this.check.operator) if (this.currentElement) { - console.error(this.$refs) - this.$refs.checkComponent.value = this.currentOption + // If we do not set it, the check`s value would remain empty. Unsure why Vue behaves this way. + this.$refs.checkComponent.modelValue = undefined } else if (this.currentOption?.component) { // keeping this in an else for apps that try to be backwards compatible and may ship both // to be removed in 03/2028 diff --git a/apps/workflowengine/src/components/Checks/FileMimeType.vue b/apps/workflowengine/src/components/Checks/FileMimeType.vue index 8a723ffb5b04d..3027367fbb075 100644 --- a/apps/workflowengine/src/components/Checks/FileMimeType.vue +++ b/apps/workflowengine/src/components/Checks/FileMimeType.vue @@ -4,7 +4,8 @@ -->