diff options
Diffstat (limited to 'apps/workflowengine/src/components')
-rw-r--r-- | apps/workflowengine/src/components/Check.vue | 229 | ||||
-rw-r--r-- | apps/workflowengine/src/components/Checks/FileMimeType.vue | 172 | ||||
-rw-r--r-- | apps/workflowengine/src/components/Checks/FileSystemTag.vue | 54 | ||||
-rw-r--r-- | apps/workflowengine/src/components/Checks/RequestTime.vue | 139 | ||||
-rw-r--r-- | apps/workflowengine/src/components/Checks/RequestURL.vue | 151 | ||||
-rw-r--r-- | apps/workflowengine/src/components/Checks/RequestUserAgent.vue | 141 | ||||
-rw-r--r-- | apps/workflowengine/src/components/Checks/RequestUserGroup.vue | 156 | ||||
-rw-r--r-- | apps/workflowengine/src/components/Checks/file.js | 88 | ||||
-rw-r--r-- | apps/workflowengine/src/components/Checks/index.js | 9 | ||||
-rw-r--r-- | apps/workflowengine/src/components/Checks/request.js | 55 | ||||
-rw-r--r-- | apps/workflowengine/src/components/Event.vue | 118 | ||||
-rw-r--r-- | apps/workflowengine/src/components/Operation.vue | 44 | ||||
-rw-r--r-- | apps/workflowengine/src/components/Rule.vue | 306 | ||||
-rw-r--r-- | apps/workflowengine/src/components/Workflow.vue | 209 |
14 files changed, 1871 insertions, 0 deletions
diff --git a/apps/workflowengine/src/components/Check.vue b/apps/workflowengine/src/components/Check.vue new file mode 100644 index 00000000000..136f6d21280 --- /dev/null +++ b/apps/workflowengine/src/components/Check.vue @@ -0,0 +1,229 @@ +<!-- + - SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> +<template> + <div v-click-outside="hideDelete" class="check" @click="showDelete"> + <NcSelect ref="checkSelector" + v-model="currentOption" + :options="options" + label="name" + :clearable="false" + :placeholder="t('workflowengine', 'Select a filter')" + @input="updateCheck" /> + <NcSelect v-model="currentOperator" + :disabled="!currentOption" + :options="operators" + class="comparator" + label="name" + :clearable="false" + :placeholder="t('workflowengine', 'Select a comparator')" + @input="updateCheck" /> + <component :is="currentElement" + v-if="currentElement" + ref="checkComponent" + :disabled="!currentOption" + :operator="check.operator" + :model-value="check.value" + class="option" + @update:model-value="updateCheck" + @valid="(valid=true) && validate()" + @invalid="!(valid=false) && validate()" /> + <component :is="currentOption.component" + v-else-if="currentOperator && currentComponent" + v-model="check.value" + :disabled="!currentOption" + :check="check" + class="option" + @input="updateCheck" + @valid="(valid=true) && validate()" + @invalid="!(valid=false) && validate()" /> + <input v-else + v-model="check.value" + type="text" + :class="{ invalid: !valid }" + :disabled="!currentOption" + :placeholder="valuePlaceholder" + class="option" + @input="updateCheck"> + <NcActions v-if="deleteVisible || !currentOption"> + <NcActionButton :title="t('workflowengine', 'Remove filter')" @click="$emit('remove')"> + <template #icon> + <CloseIcon :size="20" /> + </template> + </NcActionButton> + </NcActions> + </div> +</template> + +<script> +import NcActions from '@nextcloud/vue/components/NcActions' +import NcActionButton from '@nextcloud/vue/components/NcActionButton' +import NcSelect from '@nextcloud/vue/components/NcSelect' + +import CloseIcon from 'vue-material-design-icons/Close.vue' +import ClickOutside from 'vue-click-outside' + +export default { + name: 'Check', + components: { + NcActionButton, + NcActions, + NcSelect, + + // Icons + CloseIcon, + }, + directives: { + ClickOutside, + }, + props: { + check: { + type: Object, + required: true, + }, + rule: { + type: Object, + required: true, + }, + }, + data() { + return { + deleteVisible: false, + currentOption: null, + currentOperator: null, + options: [], + valid: false, + } + }, + computed: { + checks() { + return this.$store.getters.getChecksForEntity(this.rule.entity) + }, + operators() { + if (!this.currentOption) { return [] } + const operators = this.checks[this.currentOption.class].operators + if (typeof operators === 'function') { + return operators(this.check) + } + return operators + }, + currentElement() { + if (!this.check.class) { + return false + } + return this.checks[this.check.class].element + }, + currentComponent() { + if (!this.currentOption) { return [] } + return this.checks[this.currentOption.class].component + }, + valuePlaceholder() { + if (this.currentOption && this.currentOption.placeholder) { + return this.currentOption.placeholder(this.check) + } + return '' + }, + }, + watch: { + 'check.operator'() { + this.validate() + }, + }, + mounted() { + this.options = Object.values(this.checks) + this.currentOption = this.checks[this.check.class] + this.currentOperator = this.operators.find((operator) => operator.operator === this.check.operator) + + if (this.currentElement) { + // 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 + console.warn('Developer warning: `CheckPlugin.options` is deprecated. Use `CheckPlugin.element` instead.') + } + + if (this.check.class === null) { + this.$nextTick(() => this.$refs.checkSelector.$el.focus()) + } + this.validate() + }, + methods: { + showDelete() { + this.deleteVisible = true + }, + hideDelete() { + this.deleteVisible = false + }, + validate() { + this.valid = true + if (this.currentOption && this.currentOption.validate) { + this.valid = !!this.currentOption.validate(this.check) + } + // eslint-disable-next-line vue/no-mutating-props + this.check.invalid = !this.valid + this.$emit('validate', this.valid) + }, + updateCheck(event) { + const selectedOperator = event?.operator || this.currentOperator?.operator || this.check.operator + const matchingOperator = this.operators.findIndex((operator) => selectedOperator === 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 + this.check.operator = this.currentOperator.operator + + this.validate() + + this.$emit('update', this.check) + }, + }, +} +</script> + +<style scoped lang="scss"> + .check { + display: flex; + flex-wrap: wrap; + align-items: flex-start; // to not stretch components vertically + width: 100%; + padding-inline-end: 20px; + + & > *:not(.close) { + width: 180px; + } + & > .comparator { + min-width: 200px; + width: 200px; + } + & > .option { + min-width: 260px; + width: 260px; + min-height: 48px; + + & > input[type=text] { + min-height: 48px; + } + } + & > .v-select, + & > .button-vue, + & > input[type=text] { + margin-inline-end: 5px; + margin-bottom: 5px; + } + } + + input[type=text] { + margin: 0; + } + + .invalid { + border-color: var(--color-error) !important; + } +</style> diff --git a/apps/workflowengine/src/components/Checks/FileMimeType.vue b/apps/workflowengine/src/components/Checks/FileMimeType.vue new file mode 100644 index 00000000000..6817b128e27 --- /dev/null +++ b/apps/workflowengine/src/components/Checks/FileMimeType.vue @@ -0,0 +1,172 @@ +<!-- + - SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> +<template> + <div> + <NcSelect :model-value="currentValue" + :placeholder="t('workflowengine', 'Select a file type')" + label="label" + :options="options" + :clearable="false" + @input="setValue"> + <template #option="option"> + <span v-if="option.icon" class="option__icon" :class="option.icon" /> + <span v-else class="option__icon-img"> + <img :src="option.iconUrl" alt=""> + </span> + <span class="option__title"> + <NcEllipsisedOption :name="String(option.label)" /> + </span> + </template> + <template #selected-option="selectedOption"> + <span v-if="selectedOption.icon" class="option__icon" :class="selectedOption.icon" /> + <span v-else class="option__icon-img"> + <img :src="selectedOption.iconUrl" alt=""> + </span> + <span class="option__title"> + <NcEllipsisedOption :name="String(selectedOption.label)" /> + </span> + </template> + </NcSelect> + <input v-if="!isPredefined" + :value="currentValue.id" + type="text" + :placeholder="t('workflowengine', 'e.g. httpd/unix-directory')" + @input="updateCustom"> + </div> +</template> + +<script> +import NcEllipsisedOption from '@nextcloud/vue/components/NcEllipsisedOption' +import NcSelect from '@nextcloud/vue/components/NcSelect' +import { imagePath } from '@nextcloud/router' + +export default { + name: 'FileMimeType', + components: { + NcEllipsisedOption, + NcSelect, + }, + props: { + modelValue: { + type: String, + default: '', + }, + }, + + emits: ['update:model-value'], + + data() { + return { + predefinedTypes: [ + { + icon: 'icon-folder', + label: t('workflowengine', 'Folder'), + id: 'httpd/unix-directory', + }, + { + icon: 'icon-picture', + label: t('workflowengine', 'Images'), + id: '/image\\/.*/', + }, + { + iconUrl: imagePath('core', 'filetypes/x-office-document'), + label: t('workflowengine', 'Office documents'), + id: '/(vnd\\.(ms-|openxmlformats-|oasis\\.opendocument).*)$/', + }, + { + iconUrl: imagePath('core', 'filetypes/application-pdf'), + label: t('workflowengine', 'PDF documents'), + id: 'application/pdf', + }, + ], + newValue: '', + } + }, + computed: { + options() { + return [...this.predefinedTypes, this.customValue] + }, + isPredefined() { + const matchingPredefined = this.predefinedTypes.find((type) => this.newValue === type.id) + if (matchingPredefined) { + return true + } + return false + }, + customValue() { + return { + icon: 'icon-settings-dark', + label: t('workflowengine', 'Custom MIME type'), + id: '', + } + }, + currentValue() { + const matchingPredefined = this.predefinedTypes.find((type) => this.newValue === type.id) + if (matchingPredefined) { + return matchingPredefined + } + return { + icon: 'icon-settings-dark', + label: t('workflowengine', 'Custom mimetype'), + id: this.newValue, + } + }, + }, + watch: { + modelValue() { + this.updateInternalValue() + }, + }, + + methods: { + validateRegex(string) { + const regexRegex = /^\/(.*)\/([gui]{0,3})$/ + const result = regexRegex.exec(string) + return result !== null + }, + updateInternalValue() { + this.newValue = this.modelValue + }, + setValue(value) { + if (value !== null) { + this.newValue = value.id + this.$emit('update:model-value', this.newValue) + } + }, + updateCustom(event) { + this.newValue = event.target.value || event.detail[0] + this.$emit('update:model-value', this.newValue) + }, + }, +} +</script> +<style scoped lang="scss"> +.v-select, +input[type='text'] { + width: 100%; +} + +input[type=text] { + min-height: 48px; +} + +.option__icon, +.option__icon-img { + display: inline-block; + min-width: 30px; + background-position: center; + vertical-align: middle; +} + +.option__icon-img { + text-align: center; +} + +.option__title { + display: inline-flex; + width: calc(100% - 36px); + vertical-align: middle; +} +</style> diff --git a/apps/workflowengine/src/components/Checks/FileSystemTag.vue b/apps/workflowengine/src/components/Checks/FileSystemTag.vue new file mode 100644 index 00000000000..e71b0cd259a --- /dev/null +++ b/apps/workflowengine/src/components/Checks/FileSystemTag.vue @@ -0,0 +1,54 @@ +<!-- + - SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> +<template> + <NcSelectTags v-model="newValue" + :multiple="false" + @input="update" /> +</template> + +<script> +import NcSelectTags from '@nextcloud/vue/components/NcSelectTags' + +export default { + name: 'FileSystemTag', + components: { + NcSelectTags, + }, + props: { + modelValue: { + type: String, + default: '', + }, + }, + + emits: ['update:model-value'], + + data() { + return { + newValue: [], + } + }, + watch: { + modelValue() { + this.updateValue() + }, + }, + beforeMount() { + this.updateValue() + }, + methods: { + updateValue() { + if (this.modelValue !== '') { + this.newValue = parseInt(this.modelValue) + } else { + this.newValue = null + } + }, + update() { + this.$emit('update:model-value', this.newValue || '') + }, + }, +} +</script> diff --git a/apps/workflowengine/src/components/Checks/RequestTime.vue b/apps/workflowengine/src/components/Checks/RequestTime.vue new file mode 100644 index 00000000000..5b1a4ef1cfa --- /dev/null +++ b/apps/workflowengine/src/components/Checks/RequestTime.vue @@ -0,0 +1,139 @@ +<!-- + - SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> +<template> + <div class="timeslot"> + <input v-model="newValue.startTime" + type="text" + class="timeslot--start" + placeholder="e.g. 08:00" + @input="update"> + <input v-model="newValue.endTime" + type="text" + placeholder="e.g. 18:00" + @input="update"> + <p v-if="!valid" class="invalid-hint"> + {{ t('workflowengine', 'Please enter a valid time span') }} + </p> + <NcSelect v-show="valid" + v-model="newValue.timezone" + :clearable="false" + :options="timezones" + @input="update" /> + </div> +</template> + +<script> +import NcSelect from '@nextcloud/vue/components/NcSelect' +import moment from 'moment-timezone' + +const zones = moment.tz.names() +export default { + name: 'RequestTime', + components: { + NcSelect, + }, + props: { + modelValue: { + type: String, + default: '[]', + }, + }, + emits: ['update:model-value'], + data() { + return { + timezones: zones, + valid: false, + newValue: { + startTime: null, + endTime: null, + timezone: moment.tz.guess(), + }, + stringifiedValue: '[]', + } + }, + watch: { + modelValue() { + this.updateInternalValue() + }, + }, + beforeMount() { + // this is necessary to keep so the value is re-applied when a different + // check is being removed. + this.updateInternalValue() + }, + methods: { + updateInternalValue() { + try { + const data = JSON.parse(this.modelValue) + if (data.length === 2) { + this.newValue = { + startTime: data[0].split(' ', 2)[0], + endTime: data[1].split(' ', 2)[0], + timezone: data[0].split(' ', 2)[1], + } + this.stringifiedValue = `["${this.newValue.startTime} ${this.newValue.timezone}","${this.newValue.endTime} ${this.newValue.timezone}"]` + this.validate() + } + } catch (e) { + // ignore invalid values + } + }, + validate() { + this.valid = this.newValue.startTime && this.newValue.startTime.match(/^(0[0-9]|1[0-9]|2[0-3]|[0-9]):[0-5][0-9]$/i) !== null + && this.newValue.endTime && this.newValue.endTime.match(/^(0[0-9]|1[0-9]|2[0-3]|[0-9]):[0-5][0-9]$/i) !== null + && moment.tz.zone(this.newValue.timezone) !== null + if (this.valid) { + this.$emit('valid') + } else { + this.$emit('invalid') + } + return this.valid + }, + update() { + if (this.newValue.timezone === null) { + this.newValue.timezone = moment.tz.guess() + } + if (this.validate()) { + this.stringifiedValue = `["${this.newValue.startTime} ${this.newValue.timezone}","${this.newValue.endTime} ${this.newValue.timezone}"]` + this.$emit('update:model-value', this.stringifiedValue) + } + }, + }, +} +</script> + +<style scoped lang="scss"> + .timeslot { + display: flex; + flex-grow: 1; + flex-wrap: wrap; + max-width: 180px; + + .multiselect { + width: 100%; + margin-bottom: 5px; + } + + .multiselect:deep(.multiselect__tags:not(:hover):not(:focus):not(:active)) { + border: 1px solid transparent; + } + + input[type=text] { + width: 50%; + margin: 0; + margin-bottom: 5px; + min-height: 48px; + + &.timeslot--start { + margin-inline-end: 5px; + width: calc(50% - 5px); + } + } + + .invalid-hint { + color: var(--color-text-maxcontrast); + } + } +</style> diff --git a/apps/workflowengine/src/components/Checks/RequestURL.vue b/apps/workflowengine/src/components/Checks/RequestURL.vue new file mode 100644 index 00000000000..21b3a9cacbe --- /dev/null +++ b/apps/workflowengine/src/components/Checks/RequestURL.vue @@ -0,0 +1,151 @@ +<!-- + - SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> +<template> + <div> + <NcSelect v-model="newValue" + :value="currentValue" + :placeholder="t('workflowengine', 'Select a request URL')" + label="label" + :clearable="false" + :options="options" + @input="setValue"> + <template #option="option"> + <span class="option__icon" :class="option.icon" /> + <span class="option__title"> + <NcEllipsisedOption :name="String(option.label)" /> + </span> + </template> + <template #selected-option="selectedOption"> + <span class="option__icon" :class="selectedOption.icon" /> + <span class="option__title"> + <NcEllipsisedOption :name="String(selectedOption.label)" /> + </span> + </template> + </NcSelect> + <input v-if="!isPredefined" + type="text" + :value="currentValue.id" + :placeholder="placeholder" + @input="updateCustom"> + </div> +</template> + +<script> +import NcEllipsisedOption from '@nextcloud/vue/components/NcEllipsisedOption' +import NcSelect from '@nextcloud/vue/components/NcSelect' +import valueMixin from '../../mixins/valueMixin.js' + +export default { + name: 'RequestURL', + components: { + NcEllipsisedOption, + NcSelect, + }, + mixins: [ + valueMixin, + ], + props: { + modelValue: { + type: String, + default: '', + }, + operator: { + type: String, + default: '', + }, + }, + + emits: ['update:model-value'], + + data() { + return { + newValue: '', + predefinedTypes: [ + { + icon: 'icon-files-dark', + id: 'webdav', + label: t('workflowengine', 'Files WebDAV'), + }, + ], + } + }, + computed: { + options() { + return [...this.predefinedTypes, this.customValue] + }, + placeholder() { + if (this.operator === 'matches' || this.operator === '!matches') { + return '/^https\\:\\/\\/localhost\\/index\\.php$/i' + } + return 'https://localhost/index.php' + }, + matchingPredefined() { + return this.predefinedTypes + .find((type) => this.newValue === type.id) + }, + isPredefined() { + return !!this.matchingPredefined + }, + customValue() { + return { + icon: 'icon-settings-dark', + label: t('workflowengine', 'Custom URL'), + id: '', + } + }, + currentValue() { + if (this.matchingPredefined) { + return this.matchingPredefined + } + return { + icon: 'icon-settings-dark', + label: t('workflowengine', 'Custom URL'), + id: this.newValue, + } + }, + }, + methods: { + validateRegex(string) { + const regexRegex = /^\/(.*)\/([gui]{0,3})$/ + const result = regexRegex.exec(string) + return result !== null + }, + setValue(value) { + // TODO: check if value requires a regex and set the check operator according to that + if (value !== null) { + this.newValue = value.id + this.$emit('update:model-value', this.newValue) + } + }, + updateCustom(event) { + this.newValue = event.target.value + this.$emit('update:model-value', this.newValue) + }, + }, +} +</script> +<style scoped lang="scss"> + .v-select, + input[type='text'] { + width: 100%; + } + + input[type='text'] { + min-height: 48px; + } + + .option__icon { + display: inline-block; + min-width: 30px; + background-position: center; + vertical-align: middle; + } + + .option__title { + display: inline-flex; + width: calc(100% - 36px); + vertical-align: middle; + } +</style> diff --git a/apps/workflowengine/src/components/Checks/RequestUserAgent.vue b/apps/workflowengine/src/components/Checks/RequestUserAgent.vue new file mode 100644 index 00000000000..825a112f6fc --- /dev/null +++ b/apps/workflowengine/src/components/Checks/RequestUserAgent.vue @@ -0,0 +1,141 @@ +<!-- + - SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> +<template> + <div> + <NcSelect v-model="currentValue" + :placeholder="t('workflowengine', 'Select a user agent')" + label="label" + :options="options" + :clearable="false" + @input="setValue"> + <template #option="option"> + <span class="option__icon" :class="option.icon" /> + <span class="option__title"> + <NcEllipsisedOption :name="String(option.label)" /> + </span> + </template> + <template #selected-option="selectedOption"> + <span class="option__icon" :class="selectedOption.icon" /> + <span class="option__title"> + <NcEllipsisedOption :name="String(selectedOption.label)" /> + </span> + </template> + </NcSelect> + <input v-if="!isPredefined" + v-model="newValue" + type="text" + @input="updateCustom"> + </div> +</template> + +<script> +import NcEllipsisedOption from '@nextcloud/vue/components/NcEllipsisedOption' +import NcSelect from '@nextcloud/vue/components/NcSelect' +import valueMixin from '../../mixins/valueMixin.js' + +export default { + name: 'RequestUserAgent', + components: { + NcEllipsisedOption, + NcSelect, + }, + mixins: [ + valueMixin, + ], + props: { + modelValue: { + type: String, + default: '', + }, + }, + emits: ['update:model-value'], + data() { + return { + newValue: '', + predefinedTypes: [ + { id: 'android', label: t('workflowengine', 'Android client'), icon: 'icon-phone' }, + { id: 'ios', label: t('workflowengine', 'iOS client'), icon: 'icon-phone' }, + { id: 'desktop', label: t('workflowengine', 'Desktop client'), icon: 'icon-desktop' }, + { id: 'mail', label: t('workflowengine', 'Thunderbird & Outlook addons'), icon: 'icon-mail' }, + ], + } + }, + computed: { + options() { + return [...this.predefinedTypes, this.customValue] + }, + matchingPredefined() { + return this.predefinedTypes + .find((type) => this.newValue === type.id) + }, + isPredefined() { + return !!this.matchingPredefined + }, + customValue() { + return { + icon: 'icon-settings-dark', + label: t('workflowengine', 'Custom user agent'), + id: '', + } + }, + currentValue: { + get() { + if (this.matchingPredefined) { + return this.matchingPredefined + } + return { + icon: 'icon-settings-dark', + label: t('workflowengine', 'Custom user agent'), + id: this.newValue, + } + }, + set(value) { + this.newValue = value + }, + }, + }, + methods: { + validateRegex(string) { + const regexRegex = /^\/(.*)\/([gui]{0,3})$/ + const result = regexRegex.exec(string) + return result !== null + }, + setValue(value) { + // TODO: check if value requires a regex and set the check operator according to that + if (value !== null) { + this.newValue = value.id + this.$emit('update:model-value', this.newValue) + } + }, + updateCustom() { + this.newValue = this.currentValue.id + this.$emit('update:model-value', this.newValue) + }, + }, +} +</script> +<style scoped> + .v-select, + input[type='text'] { + width: 100%; + } + + input[type='text'] { + min-height: 48px; + } + + .option__icon { + display: inline-block; + min-width: 30px; + background-position: center; + vertical-align: middle; + } + + .option__title { + display: inline-flex; + width: calc(100% - 36px); + vertical-align: middle; + } +</style> diff --git a/apps/workflowengine/src/components/Checks/RequestUserGroup.vue b/apps/workflowengine/src/components/Checks/RequestUserGroup.vue new file mode 100644 index 00000000000..f9606b7ca26 --- /dev/null +++ b/apps/workflowengine/src/components/Checks/RequestUserGroup.vue @@ -0,0 +1,156 @@ +<!-- + - SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> +<template> + <div> + <NcSelect :aria-label-combobox="t('workflowengine', 'Select groups')" + :aria-label-listbox="t('workflowengine', 'Groups')" + :clearable="false" + :loading="status.isLoading && groups.length === 0" + :placeholder="t('workflowengine', 'Type to search for group …')" + :options="groups" + :model-value="currentValue" + label="displayname" + @search="searchAsync" + @input="update" /> + </div> +</template> + +<script> +import { translate as t } from '@nextcloud/l10n' +import { generateOcsUrl } from '@nextcloud/router' + +import axios from '@nextcloud/axios' +import NcSelect from '@nextcloud/vue/components/NcSelect' + +const groups = [] +const wantedGroups = [] +const status = { + isLoading: false, +} + +export default { + name: 'RequestUserGroup', + components: { + NcSelect, + }, + props: { + modelValue: { + type: String, + default: '', + }, + check: { + type: Object, + default: () => { return {} }, + }, + }, + emits: ['update:model-value'], + data() { + return { + groups, + status, + wantedGroups, + newValue: '', + } + }, + computed: { + currentValue: { + get() { + return this.groups.find(group => group.id === this.newValue) || null + }, + set(value) { + this.newValue = value + }, + }, + }, + watch: { + modelValue() { + this.updateInternalValue() + }, + }, + async mounted() { + // If empty, load first chunk of groups + if (this.groups.length === 0) { + await this.searchAsync('') + } + // If a current group is set but not in our list of groups then search for that group + if (this.currentValue === null && this.newValue) { + await this.searchAsync(this.newValue) + } + }, + methods: { + t, + + searchAsync(searchQuery) { + if (this.status.isLoading) { + if (searchQuery) { + // The first 20 groups are loaded up front (indicated by an + // empty searchQuery parameter), afterwards we may load + // groups that have not been fetched yet, but are used + // in existing rules. + this.enqueueWantedGroup(searchQuery) + } + return + } + + this.status.isLoading = true + return axios.get(generateOcsUrl('cloud/groups/details?limit=20&search={searchQuery}', { searchQuery })).then((response) => { + response.data.ocs.data.groups.forEach((group) => { + this.addGroup({ + id: group.id, + displayname: group.displayname, + }) + }) + this.status.isLoading = false + this.findGroupByQueue() + }, (error) => { + console.error('Error while loading group list', error.response) + }) + }, + async updateInternalValue() { + if (!this.newValue) { + await this.searchAsync(this.modelValue) + } + this.newValue = this.modelValue + }, + addGroup(group) { + const index = this.groups.findIndex((item) => item.id === group.id) + if (index === -1) { + this.groups.push(group) + } + }, + hasGroup(group) { + const index = this.groups.findIndex((item) => item.id === group) + return index > -1 + }, + update(value) { + this.newValue = value.id + this.$emit('update:model-value', this.newValue) + }, + enqueueWantedGroup(expectedGroupId) { + const index = this.wantedGroups.findIndex((groupId) => groupId === expectedGroupId) + if (index === -1) { + this.wantedGroups.push(expectedGroupId) + } + }, + async findGroupByQueue() { + let nextQuery + do { + nextQuery = this.wantedGroups.shift() + if (this.hasGroup(nextQuery)) { + nextQuery = undefined + } + } while (!nextQuery && this.wantedGroups.length > 0) + if (nextQuery) { + await this.searchAsync(nextQuery) + } + }, + }, +} +</script> +<style scoped> +.v-select { + width: 100%; +} +</style> diff --git a/apps/workflowengine/src/components/Checks/file.js b/apps/workflowengine/src/components/Checks/file.js new file mode 100644 index 00000000000..568efc81cd3 --- /dev/null +++ b/apps/workflowengine/src/components/Checks/file.js @@ -0,0 +1,88 @@ +/** + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { stringValidator, validateIPv4, validateIPv6 } from '../../helpers/validators.js' +import { registerCustomElement } from '../../helpers/window.js' +import FileMimeType from './FileMimeType.vue' +import FileSystemTag from './FileSystemTag.vue' + +const stringOrRegexOperators = () => { + return [ + { operator: 'matches', name: t('workflowengine', 'matches') }, + { operator: '!matches', name: t('workflowengine', 'does not match') }, + { operator: 'is', name: t('workflowengine', 'is') }, + { operator: '!is', name: t('workflowengine', 'is not') }, + ] +} + +const FileChecks = [ + { + class: 'OCA\\WorkflowEngine\\Check\\FileName', + name: t('workflowengine', 'File name'), + operators: stringOrRegexOperators, + placeholder: (check) => { + if (check.operator === 'matches' || check.operator === '!matches') { + return '/^dummy-.+$/i' + } + return 'filename.txt' + }, + validate: stringValidator, + }, + + { + class: 'OCA\\WorkflowEngine\\Check\\FileMimeType', + name: t('workflowengine', 'File MIME type'), + operators: stringOrRegexOperators, + element: registerCustomElement(FileMimeType, 'oca-workflowengine-checks-file_mime_type'), + }, + + { + class: 'OCA\\WorkflowEngine\\Check\\FileSize', + name: t('workflowengine', 'File size (upload)'), + operators: [ + { operator: 'less', name: t('workflowengine', 'less') }, + { operator: '!greater', name: t('workflowengine', 'less or equals') }, + { operator: '!less', name: t('workflowengine', 'greater or equals') }, + { operator: 'greater', name: t('workflowengine', 'greater') }, + ], + placeholder: (check) => '5 MB', + validate: (check) => check.value ? check.value.match(/^[0-9]+[ ]?[kmgt]?b$/i) !== null : false, + }, + + { + class: 'OCA\\WorkflowEngine\\Check\\RequestRemoteAddress', + name: t('workflowengine', 'Request remote address'), + operators: [ + { operator: 'matchesIPv4', name: t('workflowengine', 'matches IPv4') }, + { operator: '!matchesIPv4', name: t('workflowengine', 'does not match IPv4') }, + { operator: 'matchesIPv6', name: t('workflowengine', 'matches IPv6') }, + { operator: '!matchesIPv6', name: t('workflowengine', 'does not match IPv6') }, + ], + placeholder: (check) => { + if (check.operator === 'matchesIPv6' || check.operator === '!matchesIPv6') { + return '::1/128' + } + return '127.0.0.1/32' + }, + validate: (check) => { + if (check.operator === 'matchesIPv6' || check.operator === '!matchesIPv6') { + return validateIPv6(check.value) + } + return validateIPv4(check.value) + }, + }, + + { + class: 'OCA\\WorkflowEngine\\Check\\FileSystemTags', + name: t('workflowengine', 'File system tag'), + operators: [ + { operator: 'is', name: t('workflowengine', 'is tagged with') }, + { operator: '!is', name: t('workflowengine', 'is not tagged with') }, + ], + element: registerCustomElement(FileSystemTag, 'oca-workflowengine-file_system_tag'), + }, +] + +export default FileChecks diff --git a/apps/workflowengine/src/components/Checks/index.js b/apps/workflowengine/src/components/Checks/index.js new file mode 100644 index 00000000000..fc52f95f78a --- /dev/null +++ b/apps/workflowengine/src/components/Checks/index.js @@ -0,0 +1,9 @@ +/** + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import FileChecks from './file.js' +import RequestChecks from './request.js' + +export default [...FileChecks, ...RequestChecks] diff --git a/apps/workflowengine/src/components/Checks/request.js b/apps/workflowengine/src/components/Checks/request.js new file mode 100644 index 00000000000..b91f00baef0 --- /dev/null +++ b/apps/workflowengine/src/components/Checks/request.js @@ -0,0 +1,55 @@ +/** + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { registerCustomElement } from '../../helpers/window.js' +import RequestUserAgent from './RequestUserAgent.vue' +import RequestTime from './RequestTime.vue' +import RequestURL from './RequestURL.vue' +import RequestUserGroup from './RequestUserGroup.vue' + +const RequestChecks = [ + { + class: 'OCA\\WorkflowEngine\\Check\\RequestURL', + name: t('workflowengine', 'Request URL'), + operators: [ + { operator: 'is', name: t('workflowengine', 'is') }, + { operator: '!is', name: t('workflowengine', 'is not') }, + { operator: 'matches', name: t('workflowengine', 'matches') }, + { operator: '!matches', name: t('workflowengine', 'does not match') }, + ], + element: registerCustomElement(RequestURL, 'oca-workflowengine-checks-request_url'), + }, + { + class: 'OCA\\WorkflowEngine\\Check\\RequestTime', + name: t('workflowengine', 'Request time'), + operators: [ + { operator: 'in', name: t('workflowengine', 'between') }, + { operator: '!in', name: t('workflowengine', 'not between') }, + ], + element: registerCustomElement(RequestTime, 'oca-workflowengine-checks-request_time'), + }, + { + class: 'OCA\\WorkflowEngine\\Check\\RequestUserAgent', + name: t('workflowengine', 'Request user agent'), + operators: [ + { operator: 'is', name: t('workflowengine', 'is') }, + { operator: '!is', name: t('workflowengine', 'is not') }, + { operator: 'matches', name: t('workflowengine', 'matches') }, + { operator: '!matches', name: t('workflowengine', 'does not match') }, + ], + element: registerCustomElement(RequestUserAgent, 'oca-workflowengine-checks-request_user_agent'), + }, + { + class: 'OCA\\WorkflowEngine\\Check\\UserGroupMembership', + name: t('workflowengine', 'Group membership'), + operators: [ + { operator: 'is', name: t('workflowengine', 'is member of') }, + { operator: '!is', name: t('workflowengine', 'is not member of') }, + ], + element: registerCustomElement(RequestUserGroup, 'oca-workflowengine-checks-request_user_group'), + }, +] + +export default RequestChecks diff --git a/apps/workflowengine/src/components/Event.vue b/apps/workflowengine/src/components/Event.vue new file mode 100644 index 00000000000..f170101b4e9 --- /dev/null +++ b/apps/workflowengine/src/components/Event.vue @@ -0,0 +1,118 @@ +<!-- + - SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> +<template> + <div class="event"> + <div v-if="operation.isComplex && operation.fixedEntity !== ''" class="isComplex"> + <img class="option__icon" :src="entity.icon" alt=""> + <span class="option__title option__title_single">{{ operation.triggerHint }}</span> + </div> + <NcSelect v-else + :disabled="allEvents.length <= 1" + :multiple="true" + :options="allEvents" + :value="currentEvent" + :placeholder="placeholderString" + class="event__trigger" + label="displayName" + @input="updateEvent"> + <template #option="option"> + <img class="option__icon" :src="option.entity.icon" alt=""> + <span class="option__title">{{ option.displayName }}</span> + </template> + <template #selected-option="option"> + <img class="option__icon" :src="option.entity.icon" alt=""> + <span class="option__title">{{ option.displayName }}</span> + </template> + </NcSelect> + </div> +</template> + +<script> +import NcSelect from '@nextcloud/vue/components/NcSelect' +import { showWarning } from '@nextcloud/dialogs' + +export default { + name: 'Event', + components: { + NcSelect, + }, + props: { + rule: { + type: Object, + required: true, + }, + }, + computed: { + entity() { + return this.$store.getters.getEntityForOperation(this.operation) + }, + operation() { + return this.$store.getters.getOperationForRule(this.rule) + }, + allEvents() { + return this.$store.getters.getEventsForOperation(this.operation) + }, + currentEvent() { + return this.allEvents.filter(event => event.entity.id === this.rule.entity && this.rule.events.indexOf(event.eventName) !== -1) + }, + placeholderString() { + // TRANSLATORS: Users should select a trigger for a workflow action + return t('workflowengine', 'Select a trigger') + }, + }, + methods: { + updateEvent(events) { + if (events.length === 0) { + // TRANSLATORS: Users must select an event as of "happening" or "incident" which triggers an action + showWarning(t('workflowengine', 'At least one event must be selected')) + return + } + const existingEntity = this.rule.entity + const newEntities = events.map(event => event.entity.id).filter((value, index, self) => self.indexOf(value) === index) + let newEntity = null + if (newEntities.length > 1) { + newEntity = newEntities.filter(entity => entity !== existingEntity)[0] + } else { + newEntity = newEntities[0] + } + + this.$set(this.rule, 'entity', newEntity) + this.$set(this.rule, 'events', events.filter(event => event.entity.id === newEntity).map(event => event.eventName)) + this.$emit('update', this.rule) + }, + }, +} +</script> + +<style scoped lang="scss"> + .event { + margin-bottom: 5px; + + &__trigger { + max-width: 550px; + } + } + + .isComplex { + img { + vertical-align: text-top; + } + span { + padding-top: 2px; + display: inline-block; + } + } + + .option__title { + margin-inline-start: 5px; + color: var(--color-main-text); + } + + .option__icon { + width: 16px; + height: 16px; + filter: var(--background-invert-if-dark); + } +</style> diff --git a/apps/workflowengine/src/components/Operation.vue b/apps/workflowengine/src/components/Operation.vue new file mode 100644 index 00000000000..df0b78dad89 --- /dev/null +++ b/apps/workflowengine/src/components/Operation.vue @@ -0,0 +1,44 @@ +<!-- + - SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> +<template> + <div class="actions__item" :class="{'colored': colored}" :style="{ backgroundColor: colored ? operation.color : 'transparent' }"> + <div class="icon" :class="operation.iconClass" :style="{ backgroundImage: operation.iconClass ? '' : `url(${operation.icon})` }" /> + <div class="actions__item__description"> + <h3>{{ operation.name }}</h3> + <small>{{ operation.description }}</small> + <NcButton v-if="colored"> + {{ t('workflowengine', 'Add new flow') }} + </NcButton> + </div> + <div class="actions__item_options"> + <slot /> + </div> + </div> +</template> + +<script> +import NcButton from '@nextcloud/vue/components/NcButton' + +export default { + name: 'Operation', + components: { + NcButton, + }, + props: { + operation: { + type: Object, + required: true, + }, + colored: { + type: Boolean, + default: true, + }, + }, +} +</script> + +<style scoped lang="scss"> +@use "./../styles/operation" as *; +</style> diff --git a/apps/workflowengine/src/components/Rule.vue b/apps/workflowengine/src/components/Rule.vue new file mode 100644 index 00000000000..1c321fd014c --- /dev/null +++ b/apps/workflowengine/src/components/Rule.vue @@ -0,0 +1,306 @@ +<!-- + - SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> +<template> + <div v-if="operation" class="section rule" :style="{ borderLeftColor: operation.color || '' }"> + <div class="trigger"> + <p> + <span>{{ t('workflowengine', 'When') }}</span> + <Event :rule="rule" @update="updateRule" /> + </p> + <p v-for="(check, index) in rule.checks" :key="index"> + <span>{{ t('workflowengine', 'and') }}</span> + <Check :check="check" + :rule="rule" + @update="updateRule" + @validate="validate" + @remove="removeCheck(check)" /> + </p> + <p> + <span /> + <input v-if="lastCheckComplete" + type="button" + class="check--add" + :value="t('workflowengine', 'Add a new filter')" + @click="onAddFilter"> + </p> + </div> + <div class="flow-icon icon-confirm" /> + <div class="action"> + <Operation :operation="operation" :colored="false"> + <component :is="operation.element" + v-if="operation.element" + :model-value="inputValue" + @update:model-value="updateOperationByEvent" /> + <component :is="operation.options" + v-else-if="operation.options" + v-model="rule.operation" + @input="updateOperation" /> + </Operation> + <div class="buttons"> + <NcButton v-if="rule.id < -1 || dirty" @click="cancelRule"> + {{ t('workflowengine', 'Cancel') }} + </NcButton> + <NcButton v-else-if="!dirty" @click="deleteRule"> + {{ t('workflowengine', 'Delete') }} + </NcButton> + <NcButton :type="ruleStatus.type" + @click="saveRule"> + <template #icon> + <component :is="ruleStatus.icon" :size="20" /> + </template> + {{ ruleStatus.title }} + </NcButton> + </div> + <p v-if="error" class="error-message"> + {{ error }} + </p> + </div> + </div> +</template> + +<script> +import NcActions from '@nextcloud/vue/components/NcActions' +import NcActionButton from '@nextcloud/vue/components/NcActionButton' +import NcButton from '@nextcloud/vue/components/NcButton' +import Tooltip from '@nextcloud/vue/directives/Tooltip' +import IconArrowRight from 'vue-material-design-icons/ArrowRight.vue' +import IconCheckMark from 'vue-material-design-icons/Check.vue' +import IconClose from 'vue-material-design-icons/Close.vue' + +import Event from './Event.vue' +import Check from './Check.vue' +import Operation from './Operation.vue' + +export default { + name: 'Rule', + components: { + Check, + Event, + NcActionButton, + NcActions, + NcButton, + Operation, + }, + directives: { + Tooltip, + }, + props: { + rule: { + type: Object, + required: true, + }, + }, + data() { + return { + editing: false, + checks: [], + error: null, + dirty: this.rule.id < 0, + originalRule: null, + element: null, + inputValue: '', + } + }, + computed: { + /** + * @return {OperatorPlugin} + */ + operation() { + return this.$store.getters.getOperationForRule(this.rule) + }, + ruleStatus() { + if (this.error || !this.rule.valid || this.rule.checks.length === 0 || this.rule.checks.some((check) => check.invalid === true)) { + return { + title: t('workflowengine', 'The configuration is invalid'), + icon: IconClose, + type: 'warning', + tooltip: { placement: 'bottom', show: true, content: this.error }, + } + } + if (!this.dirty) { + return { title: t('workflowengine', 'Active'), icon: IconCheckMark, type: 'success' } + } + return { title: t('workflowengine', 'Save'), icon: IconArrowRight, type: 'primary' } + + }, + lastCheckComplete() { + const lastCheck = this.rule.checks[this.rule.checks.length - 1] + return typeof lastCheck === 'undefined' || lastCheck.class !== null + }, + }, + mounted() { + this.originalRule = JSON.parse(JSON.stringify(this.rule)) + if (this.operation?.element) { + this.inputValue = 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) + 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 + this.$store.dispatch('updateRule', this.rule) + }, + updateRule() { + if (!this.dirty) { + this.dirty = true + } + + this.error = null + this.$store.dispatch('updateRule', this.rule) + }, + async saveRule() { + try { + await this.$store.dispatch('pushUpdateRule', this.rule) + this.dirty = false + this.error = null + this.originalRule = JSON.parse(JSON.stringify(this.rule)) + } catch (e) { + console.error('Failed to save operation') + this.error = e.response.data.ocs.meta.message + } + }, + async deleteRule() { + try { + await this.$store.dispatch('deleteRule', this.rule) + } catch (e) { + console.error('Failed to delete operation') + this.error = e.response.data.ocs.meta.message + } + }, + cancelRule() { + 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 + } + }, + + async removeCheck(check) { + const index = this.rule.checks.findIndex(item => item === check) + if (index > -1) { + this.$delete(this.rule.checks, index) + } + this.$store.dispatch('updateRule', this.rule) + }, + + onAddFilter() { + // eslint-disable-next-line vue/no-mutating-props + this.rule.checks.push({ class: null, operator: null, value: '' }) + }, + }, +} +</script> + +<style scoped lang="scss"> + + .buttons { + display: flex; + justify-content: end; + + button { + margin-inline-start: 5px; + } + button:last-child{ + margin-inline-end: 10px; + } + } + + .error-message { + float: right; + margin-inline-end: 10px; + } + + .flow-icon { + width: 44px; + } + + .rule { + display: flex; + flex-wrap: wrap; + border-inline-start: 5px solid var(--color-primary-element); + + .trigger, + .action { + flex-grow: 1; + min-height: 100px; + max-width: 920px; + } + .action { + max-width: 400px; + position: relative; + } + .icon-confirm { + background-position: right 27px; + padding-inline-end: 20px; + margin-inline-end: 20px; + } + } + + .trigger p, .action p { + min-height: 34px; + display: flex; + + & > span { + min-width: 50px; + text-align: end; + color: var(--color-text-maxcontrast); + padding-inline-end: 10px; + padding-top: 6px; + } + .multiselect { + flex-grow: 1; + max-width: 300px; + } + } + + .trigger p:first-child span { + padding-top: 3px; + } + + .trigger p:last-child { + padding-top: 8px; + } + + .check--add { + background-position: 7px center; + background-color: transparent; + padding-inline-start: 6px; + margin: 0; + width: 180px; + border-radius: var(--border-radius); + color: var(--color-text-maxcontrast); + font-weight: normal; + text-align: start; + font-size: 1em; + } + + @media (max-width:1400px) { + .rule { + &, .trigger, .action { + width: 100%; + max-width: 100%; + } + .flow-icon { + display: none; + } + } + } + +</style> diff --git a/apps/workflowengine/src/components/Workflow.vue b/apps/workflowengine/src/components/Workflow.vue new file mode 100644 index 00000000000..03ec2a79324 --- /dev/null +++ b/apps/workflowengine/src/components/Workflow.vue @@ -0,0 +1,209 @@ +<!-- + - SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> +<template> + <div id="workflowengine"> + <NcSettingsSection :name="t('workflowengine', 'Available flows')" + :doc-url="workflowDocUrl"> + <p v-if="isAdminScope" class="settings-hint"> + <a href="https://nextcloud.com/developer/">{{ t('workflowengine', 'For details on how to write your own flow, check out the development documentation.') }}</a> + </p> + + <NcEmptyContent v-if="!isUserAdmin && mainOperations.length === 0" + :name="t('workflowengine', 'No flows installed')" + :description="!isUserAdmin ? t('workflowengine', 'Ask your administrator to install new flows.') : undefined"> + <template #icon> + <NcIconSvgWrapper :svg="WorkflowOffSvg" :size="20" /> + </template> + </NcEmptyContent> + <transition-group v-else + name="slide" + tag="div" + class="actions"> + <Operation v-for="operation in mainOperations" + :key="operation.id" + :operation="operation" + @click.native="createNewRule(operation)" /> + <a v-if="showAppStoreHint" + key="add" + :href="appstoreUrl" + class="actions__item colored more"> + <div class="icon icon-add" /> + <div class="actions__item__description"> + <h3>{{ t('workflowengine', 'More flows') }}</h3> + <small>{{ t('workflowengine', 'Browse the App Store') }}</small> + </div> + </a> + </transition-group> + + <div v-if="hasMoreOperations" class="actions__more"> + <NcButton @click="showMoreOperations = !showMoreOperations"> + <template #icon> + <MenuUp v-if="showMoreOperations" :size="20" /> + <MenuDown v-else :size="20" /> + </template> + {{ showMoreOperations ? t('workflowengine', 'Show less') : t('workflowengine', 'Show more') }} + </NcButton> + </div> + </NcSettingsSection> + + <NcSettingsSection v-if="mainOperations.length > 0" + :name="isAdminScope ? t('workflowengine', 'Configured flows') : t('workflowengine', 'Your flows')"> + <transition-group v-if="rules.length > 0" name="slide"> + <Rule v-for="rule in rules" :key="rule.id" :rule="rule" /> + </transition-group> + <NcEmptyContent v-else :name="t('workflowengine', 'No flows configured')"> + <template #icon> + <NcIconSvgWrapper :svg="WorkflowOffSvg" :size="20" /> + </template> + </NcEmptyContent> + </NcSettingsSection> + </div> +</template> + +<script> +import Rule from './Rule.vue' +import Operation from './Operation.vue' +import NcButton from '@nextcloud/vue/components/NcButton' +import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent' +import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper' +import NcSettingsSection from '@nextcloud/vue/components/NcSettingsSection' +import { mapGetters, mapState } from 'vuex' +import { generateUrl } from '@nextcloud/router' +import { loadState } from '@nextcloud/initial-state' +import MenuUp from 'vue-material-design-icons/MenuUp.vue' +import MenuDown from 'vue-material-design-icons/MenuDown.vue' +import WorkflowOffSvg from '../../img/workflow-off.svg?raw' + +const ACTION_LIMIT = 3 +const ADMIN_SCOPE = 0 +// const PERSONAL_SCOPE = 1 + +export default { + name: 'Workflow', + components: { + MenuDown, + MenuUp, + NcButton, + NcEmptyContent, + NcIconSvgWrapper, + NcSettingsSection, + Operation, + Rule, + }, + data() { + return { + showMoreOperations: false, + appstoreUrl: generateUrl('settings/apps/workflow'), + workflowDocUrl: loadState('workflowengine', 'doc-url'), + WorkflowOffSvg, + } + }, + computed: { + ...mapGetters({ + rules: 'getRules', + }), + ...mapState({ + appstoreEnabled: 'appstoreEnabled', + scope: 'scope', + operations: 'operations', + }), + hasMoreOperations() { + return Object.keys(this.operations).length > ACTION_LIMIT + }, + mainOperations() { + if (this.showMoreOperations) { + return Object.values(this.operations) + } + return Object.values(this.operations).slice(0, ACTION_LIMIT) + }, + showAppStoreHint() { + return this.appstoreEnabled && OC.isUserAdmin() + }, + isUserAdmin() { + return OC.isUserAdmin() + }, + isAdminScope() { + return this.scope === ADMIN_SCOPE + }, + }, + mounted() { + this.$store.dispatch('fetchRules') + }, + methods: { + createNewRule(operation) { + this.$store.dispatch('createNewRule', operation) + }, + }, +} +</script> + +<style scoped lang="scss"> + @use "./../styles/operation"; + + #workflowengine { + border-bottom: 1px solid var(--color-border); + } + + .section { + max-width: 100vw; + + h2.configured-flows { + margin-top: 50px; + margin-bottom: 0; + } + } + + .actions { + display: flex; + flex-wrap: wrap; + max-width: 1200px; + .actions__item { + max-width: 280px; + flex-basis: 250px; + } + } + + .actions__more { + margin-bottom: 10px; + } + + .slide-enter-active { + -moz-transition-duration: 0.3s; + -webkit-transition-duration: 0.3s; + -o-transition-duration: 0.3s; + transition-duration: 0.3s; + -moz-transition-timing-function: ease-in; + -webkit-transition-timing-function: ease-in; + -o-transition-timing-function: ease-in; + transition-timing-function: ease-in; + } + + .slide-leave-active { + -moz-transition-duration: 0.3s; + -webkit-transition-duration: 0.3s; + -o-transition-duration: 0.3s; + transition-duration: 0.3s; + -moz-transition-timing-function: cubic-bezier(0, 1, 0.5, 1); + -webkit-transition-timing-function: cubic-bezier(0, 1, 0.5, 1); + -o-transition-timing-function: cubic-bezier(0, 1, 0.5, 1); + transition-timing-function: cubic-bezier(0, 1, 0.5, 1); + } + + .slide-enter-to, .slide-leave { + max-height: 500px; + overflow: hidden; + } + + .slide-enter, .slide-leave-to { + overflow: hidden; + max-height: 0; + padding-top: 0; + padding-bottom: 0; + } + + .actions__item.more { + background-color: var(--color-background-dark); + } +</style> |