aboutsummaryrefslogtreecommitdiffstats
path: root/apps/workflowengine/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'apps/workflowengine/src/components')
-rw-r--r--apps/workflowengine/src/components/Check.vue154
-rw-r--r--apps/workflowengine/src/components/Checks/FileMimeType.vue108
-rw-r--r--apps/workflowengine/src/components/Checks/FileSystemTag.vue73
-rw-r--r--apps/workflowengine/src/components/Checks/MultiselectTag/MultiselectTag.vue127
-rw-r--r--apps/workflowengine/src/components/Checks/MultiselectTag/api.js90
-rw-r--r--apps/workflowengine/src/components/Checks/MultiselectTag/index.js4
-rw-r--r--apps/workflowengine/src/components/Checks/RequestTime.vue94
-rw-r--r--apps/workflowengine/src/components/Checks/RequestURL.vue137
-rw-r--r--apps/workflowengine/src/components/Checks/RequestUserAgent.vue133
-rw-r--r--apps/workflowengine/src/components/Checks/RequestUserGroup.vue77
-rw-r--r--apps/workflowengine/src/components/Checks/file.js105
-rw-r--r--apps/workflowengine/src/components/Checks/index.js26
-rw-r--r--apps/workflowengine/src/components/Checks/request.js71
-rw-r--r--apps/workflowengine/src/components/Event.vue105
-rw-r--r--apps/workflowengine/src/components/Operation.vue109
-rw-r--r--apps/workflowengine/src/components/Rule.vue249
-rw-r--r--apps/workflowengine/src/components/Workflow.vue126
17 files changed, 1788 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..06667b1a7ee
--- /dev/null
+++ b/apps/workflowengine/src/components/Check.vue
@@ -0,0 +1,154 @@
+<template>
+ <div v-click-outside="hideDelete" class="check" @click="showDelete">
+ <Multiselect ref="checkSelector" v-model="currentOption" :options="options"
+ label="name" track-by="class" :allow-empty="false"
+ :placeholder="t('workflowengine', 'Select a filter')" @input="updateCheck" />
+ <Multiselect v-model="currentOperator" :disabled="!currentOption" :options="operators"
+ label="name" track-by="operator" :allow-empty="false"
+ :placeholder="t('workflowengine', 'Select a comparator')" @input="updateCheck" />
+ <component :is="currentOption.component" v-if="currentOperator && currentComponent" v-model="check.value"
+ :disabled="!currentOption" :check="check"
+ @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" @input="updateCheck">
+ <Actions v-if="deleteVisible || !currentOption">
+ <ActionButton icon="icon-delete" @click="$emit('remove')" />
+ </Actions>
+ </div>
+</template>
+
+<script>
+import { Multiselect } from 'nextcloud-vue/dist/Components/Multiselect'
+import { Actions } from 'nextcloud-vue/dist/Components/Actions'
+import { ActionButton } from 'nextcloud-vue/dist/Components/ActionButton'
+import ClickOutside from 'vue-click-outside'
+
+export default {
+ name: 'Check',
+ components: {
+ ActionButton,
+ Actions,
+ Multiselect
+ },
+ directives: {
+ ClickOutside
+ },
+ props: {
+ check: {
+ type: Object,
+ required: true
+ },
+ rule: {
+ type: Object,
+ required: true
+ }
+ },
+ data() {
+ return {
+ deleteVisible: false,
+ currentOption: null,
+ currentOperator: null,
+ options: [],
+ valid: true
+ }
+ },
+ computed: {
+ Checks() {
+ return this.$store.getters.getChecksForEntity(this.rule.entity)
+ },
+ operators() {
+ if (!this.currentOption) { return [] }
+ return this.Checks[this.currentOption.class].operators
+ },
+ currentComponent() {
+ if (!this.currentOption) { return [] }
+ const currentComponent = this.Checks[this.currentOption.class].component
+ return currentComponent
+ },
+ valuePlaceholder() {
+ if (this.currentOption && this.currentOption.placeholder) {
+ return this.currentOption.placeholder(this.check)
+ }
+ return ''
+ }
+ },
+ watch: {
+ 'check.operator': function() {
+ 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)
+ },
+ methods: {
+ showDelete() {
+ this.deleteVisible = true
+ },
+ hideDelete() {
+ this.deleteVisible = false
+ },
+ validate() {
+ if (this.currentOption && this.currentOption.validate) {
+ if (this.currentOption.validate(this.check)) {
+ this.valid = true
+ } else {
+ this.valid = false
+ }
+ }
+ this.$store.dispatch('setValid', { rule: this.rule, valid: this.rule.valid && this.valid })
+ return this.valid
+ },
+ updateCheck() {
+ if (this.check.class !== this.currentOption.class) {
+ this.currentOperator = this.operators[0]
+ }
+ this.check.class = this.currentOption.class
+ this.check.operator = this.currentOperator.operator
+
+ if (!this.validate()) {
+ return
+ }
+ this.$emit('update', this.check)
+ }
+ }
+}
+</script>
+
+<style scoped lang="scss">
+ .check {
+ display: flex;
+ flex-wrap: wrap;
+ width: 100%;
+ padding-right: 20px;
+ & > *:not(.icon-delete) {
+ width: 180px;
+ }
+ & > .multiselect,
+ & > input[type=text] {
+ margin-right: 5px;
+ margin-bottom: 5px;
+ }
+ }
+ input[type=text] {
+ margin: 0;
+ }
+ ::placeholder {
+ font-size: 10px;
+ }
+ .icon-delete {
+ margin-top: -5px;
+ margin-bottom: -5px;
+ }
+ button.action-item.action-item--single.icon-delete {
+ height: 34px;
+ width: 34px;
+ }
+ .invalid {
+ border: 1px solid 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..e99bf679f00
--- /dev/null
+++ b/apps/workflowengine/src/components/Checks/FileMimeType.vue
@@ -0,0 +1,108 @@
+<template>
+ <div>
+ <Multiselect
+ :value="currentValue"
+ :placeholder="t('workflowengine', 'Select a file type')"
+ label="label"
+ track-by="pattern"
+ :options="options" :multiple="false" :tagging="false"
+ @input="setValue">
+ <template slot="singleLabel" slot-scope="props">
+ <span class="option__icon" :class="props.option.icon" />
+ <span class="option__title option__title_single">{{ props.option.label }}</span>
+ </template>
+ <template slot="option" slot-scope="props">
+ <span class="option__icon" :class="props.option.icon" />
+ <span class="option__title">{{ props.option.label }}</span>
+ </template>
+ </Multiselect>
+ <input v-if="!isPredefined" type="text" :value="currentValue.pattern"
+ @input="updateCustom">
+ </div>
+</template>
+
+<script>
+import { Multiselect } from 'nextcloud-vue/dist/Components/Multiselect'
+import valueMixin from './../../mixins/valueMixin'
+
+export default {
+ name: 'FileMimeType',
+ components: {
+ Multiselect
+ },
+ mixins: [
+ valueMixin
+ ],
+ data() {
+ return {
+ predefinedTypes: [
+ {
+ icon: 'icon-picture',
+ label: t('workflowengine', 'Images'),
+ pattern: '/image\\/.*/'
+ },
+ {
+ icon: 'icon-category-office',
+ label: t('workflowengine', 'Office documents'),
+ pattern: '/(vnd\\.(ms-|openxmlformats-).*))$/'
+ },
+ {
+ icon: 'icon-filetype-file',
+ label: t('workflowengine', 'PDF documents'),
+ pattern: 'application/pdf'
+ }
+ ]
+ }
+ },
+ computed: {
+ options() {
+ return [...this.predefinedTypes, this.customValue]
+ },
+ isPredefined() {
+ const matchingPredefined = this.predefinedTypes.find((type) => this.newValue === type.pattern)
+ if (matchingPredefined) {
+ return true
+ }
+ return false
+ },
+ customValue() {
+ return {
+ icon: 'icon-settings-dark',
+ label: t('workflowengine', 'Custom mimetype'),
+ pattern: ''
+ }
+ },
+ currentValue() {
+ const matchingPredefined = this.predefinedTypes.find((type) => this.newValue === type.pattern)
+ if (matchingPredefined) {
+ return matchingPredefined
+ }
+ return {
+ icon: 'icon-settings-dark',
+ label: t('workflowengine', 'Custom mimetype'),
+ pattern: this.newValue
+ }
+ }
+ },
+ methods: {
+ validateRegex(string) {
+ var regexRegex = /^\/(.*)\/([gui]{0,3})$/
+ var 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.pattern
+ this.$emit('input', this.newValue)
+ }
+ },
+ updateCustom(event) {
+ this.newValue = event.target.value
+ this.$emit('input', this.newValue)
+ }
+ }
+}
+</script>
+
+<style scoped src="./../../css/multiselect.css"></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..e2f66b30a4b
--- /dev/null
+++ b/apps/workflowengine/src/components/Checks/FileSystemTag.vue
@@ -0,0 +1,73 @@
+<!--
+ - @copyright Copyright (c) 2019 Julius Härtl <jus@bitgrid.net>
+ -
+ - @author Julius Härtl <jus@bitgrid.net>
+ -
+ - @license GNU AGPL version 3 or any later version
+ -
+ - This program is free software: you can redistribute it and/or modify
+ - it under the terms of the GNU Affero General Public License as
+ - published by the Free Software Foundation, either version 3 of the
+ - License, or (at your option) any later version.
+ -
+ - This program is distributed in the hope that it will be useful,
+ - but WITHOUT ANY WARRANTY; without even the implied warranty of
+ - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ - GNU Affero General Public License for more details.
+ -
+ - You should have received a copy of the GNU Affero General Public License
+ - along with this program. If not, see <http://www.gnu.org/licenses/>.
+ -
+ -->
+
+<template>
+ <MultiselectTag v-model="newValue" :multiple="false"
+ label="Select a tag"
+ @input="update" />
+</template>
+
+<script>
+import { MultiselectTag } from './MultiselectTag'
+
+export default {
+ name: 'FileSystemTag',
+ components: {
+ MultiselectTag
+ },
+ props: {
+ value: {
+ type: String,
+ default: ''
+ }
+ },
+ data() {
+ return {
+ newValue: []
+ }
+ },
+ watch: {
+ value() {
+ this.updateValue()
+ }
+ },
+ beforeMount() {
+ this.updateValue()
+ },
+ methods: {
+ updateValue() {
+ if (this.value !== '') {
+ this.newValue = this.value
+ } else {
+ this.newValue = null
+ }
+ },
+ update() {
+ this.$emit('input', this.newValue || '')
+ }
+ }
+}
+</script>
+
+<style scoped>
+
+</style>
diff --git a/apps/workflowengine/src/components/Checks/MultiselectTag/MultiselectTag.vue b/apps/workflowengine/src/components/Checks/MultiselectTag/MultiselectTag.vue
new file mode 100644
index 00000000000..88b56a1d4e9
--- /dev/null
+++ b/apps/workflowengine/src/components/Checks/MultiselectTag/MultiselectTag.vue
@@ -0,0 +1,127 @@
+<!--
+ - @copyright Copyright (c) 2019 Julius Härtl <jus@bitgrid.net>
+ -
+ - @author Julius Härtl <jus@bitgrid.net>
+ -
+ - @license GNU AGPL version 3 or any later version
+ -
+ - This program is free software: you can redistribute it and/or modify
+ - it under the terms of the GNU Affero General Public License as
+ - published by the Free Software Foundation, either version 3 of the
+ - License, or (at your option) any later version.
+ -
+ - This program is distributed in the hope that it will be useful,
+ - but WITHOUT ANY WARRANTY; without even the implied warranty of
+ - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ - GNU Affero General Public License for more details.
+ -
+ - You should have received a copy of the GNU Affero General Public License
+ - along with this program. If not, see <http://www.gnu.org/licenses/>.
+ -
+ -->
+
+<template>
+ <Multiselect v-model="inputValObjects"
+ :options="tags" :options-limit="5"
+ :placeholder="label"
+ track-by="id"
+ :custom-label="tagLabel"
+ class="multiselect-vue" :multiple="multiple"
+ :close-on-select="false" :tag-width="60"
+ :disabled="disabled" @input="update">
+ <span slot="noResult">{{ t('core', 'No results') }}</span>
+ <template #option="scope">
+ {{ tagLabel(scope.option) }}
+ </template>
+ </multiselect>
+</template>
+
+<script>
+import { Multiselect } from 'nextcloud-vue/dist/Components/Multiselect'
+import { searchTags } from './api'
+
+let uuid = 0
+export default {
+ name: 'MultiselectTag',
+ components: {
+ Multiselect
+ },
+ props: {
+ label: {
+ type: String,
+ required: true
+ },
+ value: {
+ default() {
+ return []
+ }
+ },
+ disabled: {
+ type: Boolean,
+ default: false
+ },
+ multiple: {
+ type: Boolean,
+ default: true
+ }
+ },
+ data() {
+ return {
+ inputValObjects: [],
+ tags: []
+ }
+ },
+ computed: {
+ id() {
+ return 'settings-input-text-' + this.uuid
+ }
+ },
+ watch: {
+ value(newVal) {
+ this.inputValObjects = this.getValueObject()
+ }
+ },
+ beforeCreate: function() {
+ this.uuid = uuid.toString()
+ uuid += 1
+ searchTags().then((result) => {
+ this.tags = result
+ this.inputValObjects = this.getValueObject()
+ }).catch(console.error.bind(this))
+ },
+ methods: {
+ getValueObject() {
+ if (this.tags.length === 0) {
+ return []
+ }
+ if (this.multiple) {
+ return this.value.filter((tag) => tag !== '').map(
+ (id) => this.tags.find((tag2) => tag2.id === id)
+ )
+ } else {
+ return this.tags.find((tag) => tag.id === this.value)
+ }
+ },
+ update() {
+ if (this.multiple) {
+ this.$emit('input', this.inputValObjects.map((element) => element.id))
+ } else {
+ if (this.inputValObjects === null) {
+ this.$emit('input', '')
+ } else {
+ this.$emit('input', this.inputValObjects.id)
+ }
+ }
+ },
+ tagLabel({ displayName, userVisible, userAssignable }) {
+ if (userVisible === false) {
+ return t('systemtags', '%s (invisible)').replace('%s', displayName)
+ }
+ if (userAssignable === false) {
+ return t('systemtags', '%s (restricted)').replace('%s', displayName)
+ }
+ return displayName
+ }
+ }
+}
+</script>
diff --git a/apps/workflowengine/src/components/Checks/MultiselectTag/api.js b/apps/workflowengine/src/components/Checks/MultiselectTag/api.js
new file mode 100644
index 00000000000..bdbab2b974f
--- /dev/null
+++ b/apps/workflowengine/src/components/Checks/MultiselectTag/api.js
@@ -0,0 +1,90 @@
+import axios from 'nextcloud-axios'
+import { generateRemoteUrl } from 'nextcloud-router'
+
+const xmlToJson = (xml) => {
+ let obj = {}
+
+ if (xml.nodeType === 1) {
+ if (xml.attributes.length > 0) {
+ obj['@attributes'] = {}
+ for (let j = 0; j < xml.attributes.length; j++) {
+ const attribute = xml.attributes.item(j)
+ obj['@attributes'][attribute.nodeName] = attribute.nodeValue
+ }
+ }
+ } else if (xml.nodeType === 3) {
+ obj = xml.nodeValue
+ }
+
+ if (xml.hasChildNodes()) {
+ for (let i = 0; i < xml.childNodes.length; i++) {
+ const item = xml.childNodes.item(i)
+ const nodeName = item.nodeName
+ if (typeof (obj[nodeName]) === 'undefined') {
+ obj[nodeName] = xmlToJson(item)
+ } else {
+ if (typeof obj[nodeName].push === 'undefined') {
+ var old = obj[nodeName]
+ obj[nodeName] = []
+ obj[nodeName].push(old)
+ }
+ obj[nodeName].push(xmlToJson(item))
+ }
+ }
+ }
+ return obj
+}
+
+const parseXml = (xml) => {
+ let dom = null
+ try {
+ dom = (new DOMParser()).parseFromString(xml, 'text/xml')
+ } catch (e) {
+ console.error('Failed to parse xml document', e)
+ }
+ return dom
+}
+
+const xmlToTagList = (xml) => {
+ const json = xmlToJson(parseXml(xml))
+ const list = json['d:multistatus']['d:response']
+ const result = []
+ for (const index in list) {
+ const tag = list[index]['d:propstat']
+
+ if (tag['d:status']['#text'] !== 'HTTP/1.1 200 OK') {
+ continue
+ }
+ result.push({
+ id: tag['d:prop']['oc:id']['#text'],
+ displayName: tag['d:prop']['oc:display-name']['#text'],
+ canAssign: tag['d:prop']['oc:can-assign']['#text'] === 'true',
+ userAssignable: tag['d:prop']['oc:user-assignable']['#text'] === 'true',
+ userVisible: tag['d:prop']['oc:user-visible']['#text'] === 'true'
+ })
+ }
+ return result
+}
+
+const searchTags = function() {
+ return axios({
+ method: 'PROPFIND',
+ url: generateRemoteUrl('dav') + '/systemtags/',
+ data: `<?xml version="1.0"?>
+ <d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">
+ <d:prop>
+ <oc:id />
+ <oc:display-name />
+ <oc:user-visible />
+ <oc:user-assignable />
+ <oc:can-assign />
+ </d:prop>
+ </d:propfind>`
+ }).then((response) => {
+ return xmlToTagList(response.data)
+ })
+}
+
+export {
+ searchTags
+}
diff --git a/apps/workflowengine/src/components/Checks/MultiselectTag/index.js b/apps/workflowengine/src/components/Checks/MultiselectTag/index.js
new file mode 100644
index 00000000000..69b7e277e76
--- /dev/null
+++ b/apps/workflowengine/src/components/Checks/MultiselectTag/index.js
@@ -0,0 +1,4 @@
+import MultiselectTag from './MultiselectTag'
+
+export default MultiselectTag
+export { MultiselectTag }
diff --git a/apps/workflowengine/src/components/Checks/RequestTime.vue b/apps/workflowengine/src/components/Checks/RequestTime.vue
new file mode 100644
index 00000000000..ce306c0541e
--- /dev/null
+++ b/apps/workflowengine/src/components/Checks/RequestTime.vue
@@ -0,0 +1,94 @@
+<template>
+ <div class="timeslot">
+ <Multiselect v-model="newValue.timezone" :options="timezones" @input="update" />
+ <input v-model="newValue.startTime" type="text" class="timeslot--start"
+ placeholder="08:00" @input="update">
+ <input v-model="newValue.endTime" type="text" placeholder="18:00"
+ @input="update">
+ </div>
+</template>
+
+<script>
+import { Multiselect } from 'nextcloud-vue/dist/Components/Multiselect'
+import moment from 'moment-timezone'
+import valueMixin from '../../mixins/valueMixin'
+
+const zones = moment.tz.names()
+export default {
+ name: 'RequestTime',
+ components: {
+ Multiselect
+ },
+ mixins: [
+ valueMixin
+ ],
+ props: {
+ value: {
+ type: String,
+ default: '1 MB'
+ }
+ },
+ data() {
+ return {
+ timezones: zones,
+ valid: false,
+ newValue: {
+ startTime: null,
+ endTime: null,
+ timezone: moment.tz.guess()
+ }
+ }
+ },
+ methods: {
+ updateInternalValue(value) {
+ var data = JSON.parse(value)
+ var startTime = data[0].split(' ', 2)[0]
+ var endTime = data[1].split(' ', 2)[0]
+ var timezone = data[0].split(' ', 2)[1]
+ this.newValue = {
+ startTime: startTime,
+ endTime: endTime,
+ timezone: timezone
+ }
+ },
+ validate() {
+ return 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
+ },
+ update() {
+ if (this.validate()) {
+ const output = `["${this.newValue.startTime} ${this.newValue.timezone}","${this.newValue.endTime} ${this.newValue.timezone}"]`
+ this.$emit('input', output)
+ this.valid = true
+ } else {
+ this.valid = false
+ }
+ }
+ }
+}
+</script>
+
+<style scoped lang="scss">
+ .timeslot {
+ display: flex;
+ flex-grow: 1;
+ flex-wrap: wrap;
+ max-width: 180px;
+
+ .multiselect {
+ width: 100%;
+ margin-bottom: 5px;
+ }
+
+ input[type=text] {
+ width: 50%;
+ margin: 0;
+ margin-bottom: 5px;
+ &.timeslot--start {
+ margin-right: 5px;
+ width: calc(50% - 5px);
+ }
+ }
+ }
+</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..2ddba526d1e
--- /dev/null
+++ b/apps/workflowengine/src/components/Checks/RequestURL.vue
@@ -0,0 +1,137 @@
+<!--
+ - @copyright Copyright (c) 2019 Julius Härtl <jus@bitgrid.net>
+ -
+ - @author Julius Härtl <jus@bitgrid.net>
+ -
+ - @license GNU AGPL version 3 or any later version
+ -
+ - This program is free software: you can redistribute it and/or modify
+ - it under the terms of the GNU Affero General Public License as
+ - published by the Free Software Foundation, either version 3 of the
+ - License, or (at your option) any later version.
+ -
+ - This program is distributed in the hope that it will be useful,
+ - but WITHOUT ANY WARRANTY; without even the implied warranty of
+ - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ - GNU Affero General Public License for more details.
+ -
+ - You should have received a copy of the GNU Affero General Public License
+ - along with this program. If not, see <http://www.gnu.org/licenses/>.
+ -
+ -->
+
+<template>
+ <div>
+ <Multiselect
+ :value="currentValue"
+ :placeholder="t('workflowengine', 'Select a request URL')"
+ label="label"
+ track-by="pattern"
+ group-values="children"
+ group-label="label"
+ :options="options" :multiple="false" :tagging="false"
+ @input="setValue">
+ <template slot="singleLabel" slot-scope="props">
+ <span class="option__icon" :class="props.option.icon" />
+ <span class="option__title option__title_single">{{ props.option.label }}</span>
+ </template>
+ <template slot="option" slot-scope="props">
+ <span class="option__icon" :class="props.option.icon" />
+ <span class="option__title">{{ props.option.label }} {{ props.option.$groupLabel }}</span>
+ </template>
+ </Multiselect>
+ <input v-if="!isPredefined" type="text"
+ :value="currentValue.pattern"
+ :placeholder="placeholder" @input="updateCustom">
+ </div>
+</template>
+
+<script>
+import { Multiselect } from 'nextcloud-vue/dist/Components/Multiselect'
+import valueMixin from '../../mixins/valueMixin'
+
+export default {
+ name: 'RequestURL',
+ components: {
+ Multiselect
+ },
+ mixins: [
+ valueMixin
+ ],
+ data() {
+ return {
+ newValue: '',
+ predefinedTypes: [
+ {
+ label: t('workflowengine', 'Predefined URLs'),
+ children: [
+ { pattern: 'webdav', label: t('workflowengine', 'Files WebDAV') }
+ ]
+ }
+ ]
+ }
+ },
+ computed: {
+ options() {
+ return [...this.predefinedTypes, this.customValue]
+ },
+ placeholder() {
+ if (this.check.operator === 'matches' || this.check.operator === '!matches') {
+ return '/^https\\:\\/\\/localhost\\/index\\.php$/i'
+ }
+ return 'https://localhost/index.php'
+ },
+ matchingPredefined() {
+ return this.predefinedTypes
+ .map(groups => groups.children)
+ .flat()
+ .find((type) => this.newValue === type.pattern)
+ },
+ isPredefined() {
+ return !!this.matchingPredefined
+ },
+ customValue() {
+ return {
+ label: t('workflowengine', 'Others'),
+ children: [
+ {
+ icon: 'icon-settings-dark',
+ label: t('workflowengine', 'Custom URL'),
+ pattern: ''
+ }
+ ]
+ }
+ },
+ currentValue() {
+ if (this.matchingPredefined) {
+ return this.matchingPredefined
+ }
+ return {
+ icon: 'icon-settings-dark',
+ label: t('workflowengine', 'Custom URL'),
+ pattern: this.newValue
+ }
+ }
+ },
+ methods: {
+ validateRegex(string) {
+ var regexRegex = /^\/(.*)\/([gui]{0,3})$/
+ var 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.pattern
+ this.$emit('input', this.newValue)
+ }
+ },
+ updateCustom(event) {
+ this.newValue = event.target.value
+ this.$emit('input', this.newValue)
+ }
+ }
+}
+</script>
+
+<style scoped src="./../../css/multiselect.css"></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..e80071ab9fa
--- /dev/null
+++ b/apps/workflowengine/src/components/Checks/RequestUserAgent.vue
@@ -0,0 +1,133 @@
+<!--
+ - @copyright Copyright (c) 2019 Julius Härtl <jus@bitgrid.net>
+ -
+ - @author Julius Härtl <jus@bitgrid.net>
+ -
+ - @license GNU AGPL version 3 or any later version
+ -
+ - This program is free software: you can redistribute it and/or modify
+ - it under the terms of the GNU Affero General Public License as
+ - published by the Free Software Foundation, either version 3 of the
+ - License, or (at your option) any later version.
+ -
+ - This program is distributed in the hope that it will be useful,
+ - but WITHOUT ANY WARRANTY; without even the implied warranty of
+ - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ - GNU Affero General Public License for more details.
+ -
+ - You should have received a copy of the GNU Affero General Public License
+ - along with this program. If not, see <http://www.gnu.org/licenses/>.
+ -
+ -->
+
+<template>
+ <div>
+ <Multiselect
+ :value="currentValue"
+ :placeholder="t('workflowengine', 'Select a user agent')"
+ label="label"
+ track-by="pattern"
+ group-values="children"
+ group-label="label"
+ :options="options" :multiple="false" :tagging="false"
+ @input="setValue">
+ <template slot="singleLabel" slot-scope="props">
+ <span class="option__icon" :class="props.option.icon" />
+ <span class="option__title option__title_single">{{ props.option.label }}</span>
+ </template>
+ <template slot="option" slot-scope="props">
+ <span class="option__icon" :class="props.option.icon" />
+ <span class="option__title">{{ props.option.label }} {{ props.option.$groupLabel }}</span>
+ </template>
+ </Multiselect>
+ <input v-if="!isPredefined" type="text" :value="currentValue.pattern"
+ @input="updateCustom">
+ </div>
+</template>
+
+<script>
+import { Multiselect } from 'nextcloud-vue/dist/Components/Multiselect'
+import valueMixin from '../../mixins/valueMixin'
+
+export default {
+ name: 'RequestUserAgent',
+ components: {
+ Multiselect
+ },
+ mixins: [
+ valueMixin
+ ],
+ data() {
+ return {
+ newValue: '',
+ predefinedTypes: [
+ {
+ label: t('workflowengine', 'Sync clients'),
+ children: [
+ { pattern: 'android', label: t('workflowengine', 'Android client'), icon: 'icon-phone' },
+ { pattern: 'ios', label: t('workflowengine', 'iOS client'), icon: 'icon-phone' },
+ { pattern: 'desktop', label: t('workflowengine', 'Desktop client'), icon: 'icon-desktop' },
+ { pattern: 'mail', label: t('workflowengine', 'Thunderbird & Outlook addons'), icon: 'icon-mail' }
+ ]
+ }
+ ]
+ }
+ },
+ computed: {
+ options() {
+ return [...this.predefinedTypes, this.customValue]
+ },
+ matchingPredefined() {
+ return this.predefinedTypes
+ .map(groups => groups.children)
+ .flat()
+ .find((type) => this.newValue === type.pattern)
+ },
+ isPredefined() {
+ return !!this.matchingPredefined
+ },
+ customValue() {
+ return {
+ label: t('workflowengine', 'Others'),
+ children: [
+ {
+ icon: 'icon-settings-dark',
+ label: t('workflowengine', 'Custom user agent'),
+ pattern: ''
+ }
+ ]
+ }
+ },
+ currentValue() {
+ if (this.matchingPredefined) {
+ return this.matchingPredefined
+ }
+ return {
+ icon: 'icon-settings-dark',
+ label: t('workflowengine', 'Custom user agent'),
+ pattern: this.newValue
+ }
+ }
+ },
+ methods: {
+ validateRegex(string) {
+ var regexRegex = /^\/(.*)\/([gui]{0,3})$/
+ var 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.pattern
+ this.$emit('input', this.newValue)
+ }
+ },
+ updateCustom(event) {
+ this.newValue = event.target.value
+ this.$emit('input', this.newValue)
+ }
+ }
+}
+</script>
+
+<style scoped src="./../../css/multiselect.css"></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..843bbf127e0
--- /dev/null
+++ b/apps/workflowengine/src/components/Checks/RequestUserGroup.vue
@@ -0,0 +1,77 @@
+<!--
+ - @copyright Copyright (c) 2019 Julius Härtl <jus@bitgrid.net>
+ -
+ - @author Julius Härtl <jus@bitgrid.net>
+ -
+ - @license GNU AGPL version 3 or any later version
+ -
+ - This program is free software: you can redistribute it and/or modify
+ - it under the terms of the GNU Affero General Public License as
+ - published by the Free Software Foundation, either version 3 of the
+ - License, or (at your option) any later version.
+ -
+ - This program is distributed in the hope that it will be useful,
+ - but WITHOUT ANY WARRANTY; without even the implied warranty of
+ - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ - GNU Affero General Public License for more details.
+ -
+ - You should have received a copy of the GNU Affero General Public License
+ - along with this program. If not, see <http://www.gnu.org/licenses/>.
+ -
+ -->
+
+<template>
+ <div>
+ <Multiselect v-model="newValue"
+ :class="{'icon-loading-small': groups.length === 0}" :options="groups"
+ :multiple="false"
+ label="displayname" track-by="id"
+ @input="setValue" />
+ </div>
+</template>
+
+<script>
+import { Multiselect } from 'nextcloud-vue/dist/Components/Multiselect'
+import valueMixin from '../../mixins/valueMixin'
+import axios from 'nextcloud-axios'
+export default {
+ name: 'RequestUserGroup',
+ components: {
+ Multiselect
+ },
+ mixins: [
+ valueMixin
+ ],
+ data() {
+ return {
+ groups: []
+ }
+ },
+ beforeMount() {
+ axios.get(OC.linkToOCS('cloud', 2) + 'groups').then((response) => {
+ this.groups = response.data.ocs.data.groups.reduce((obj, item) => {
+ obj.push({
+ id: item,
+ displayname: item
+ })
+ return obj
+ }, [])
+ this.updateInternalValue(this.value)
+ }, (error) => {
+ console.error('Error while loading group list', error.response)
+ })
+ },
+ methods: {
+ updateInternalValue() {
+ this.newValue = this.groups.find(group => group.id === this.value) || null
+ },
+ setValue(value) {
+ if (value !== null) {
+ this.$emit('input', this.newValue.id)
+ }
+ }
+ }
+}
+</script>
+
+<style scoped src="./../../css/multiselect.css"></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..ce6887316c2
--- /dev/null
+++ b/apps/workflowengine/src/components/Checks/file.js
@@ -0,0 +1,105 @@
+/*
+ * @copyright Copyright (c) 2019 Julius Härtl <jus@bitgrid.net>
+ *
+ * @author Julius Härtl <jus@bitgrid.net>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+import { stringValidator, validateIPv4, validateIPv6 } from './../../helpers/validators'
+import FileMimeType from './FileMimeType'
+import FileSystemTag from './FileSystemTag'
+
+const FileChecks = [
+ {
+ class: 'OCA\\WorkflowEngine\\Check\\FileName',
+ name: t('workflowengine', 'File name'),
+ 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') }
+ ],
+ 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: [
+ { 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') }
+ ],
+ component: FileMimeType
+ },
+
+ {
+ 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.match(/^[0-9]+[ ]?[kmgt]?b$/i) !== null
+ },
+
+ {
+ 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') }
+ ],
+ component: FileSystemTag
+ }
+]
+
+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..d20472111b5
--- /dev/null
+++ b/apps/workflowengine/src/components/Checks/index.js
@@ -0,0 +1,26 @@
+/*
+ * @copyright Copyright (c) 2019 Julius Härtl <jus@bitgrid.net>
+ *
+ * @author Julius Härtl <jus@bitgrid.net>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+import FileChecks from './file'
+import RequestChecks from './request'
+
+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..1059bf45b5a
--- /dev/null
+++ b/apps/workflowengine/src/components/Checks/request.js
@@ -0,0 +1,71 @@
+/*
+ * @copyright Copyright (c) 2019 Julius Härtl <jus@bitgrid.net>
+ *
+ * @author Julius Härtl <jus@bitgrid.net>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+import RequestUserAgent from './RequestUserAgent'
+import RequestTime from './RequestTime'
+import RequestURL from './RequestURL'
+import RequestUserGroup from './RequestUserGroup'
+
+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') }
+ ],
+ component: RequestURL
+ },
+ {
+ class: 'OCA\\WorkflowEngine\\Check\\RequestTime',
+ name: t('workflowengine', 'Request time'),
+ operators: [
+ { operator: 'in', name: t('workflowengine', 'between') },
+ { operator: '!in', name: t('workflowengine', 'not between') }
+ ],
+ component: RequestTime
+ },
+ {
+ 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') }
+ ],
+ component: RequestUserAgent
+ },
+ {
+ class: 'OCA\\WorkflowEngine\\Check\\UserGroupMembership',
+ name: t('workflowengine', 'User group membership'),
+ operators: [
+ { operator: 'is', name: t('workflowengine', 'is member of') },
+ { operator: '!is', name: t('workflowengine', 'is not member of') }
+ ],
+ component: RequestUserGroup
+ }
+]
+
+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..ea153758c74
--- /dev/null
+++ b/apps/workflowengine/src/components/Event.vue
@@ -0,0 +1,105 @@
+<template>
+ <div>
+ <div v-if="operation.isComplex && operation.fixedEntity !== ''" class="isComplex">
+ <img class="option__icon" :src="entity.icon">
+ <span class="option__title option__title_single">{{ operation.triggerHint }}</span>
+ </div>
+ <Multiselect v-else :value="currentEvent" :options="allEvents"
+ label="eventName" track-by="id" :allow-empty="false"
+ :disabled="allEvents.length <= 1" @input="updateEvent">
+ <template slot="singleLabel" slot-scope="props">
+ <img class="option__icon" :src="props.option.entity.icon">
+ <span class="option__title option__title_single">{{ props.option.displayName }}</span>
+ </template>
+ <template slot="option" slot-scope="props">
+ <img class="option__icon" :src="props.option.entity.icon">
+ <span class="option__title">{{ props.option.displayName }}</span>
+ </template>
+ </Multiselect>
+ </div>
+</template>
+
+<script>
+import { Multiselect } from 'nextcloud-vue/dist/Components/Multiselect'
+
+export default {
+ name: 'Event',
+ components: {
+ Multiselect
+ },
+ 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() {
+ if (!this.rule.events) {
+ return this.allEvents.length > 0 ? this.allEvents[0] : null
+ }
+ return this.allEvents.find(event => event.entity.id === this.rule.entity && this.rule.events.indexOf(event.eventName) !== -1)
+ }
+ },
+ methods: {
+ updateEvent(event) {
+ this.$set(this.rule, 'entity', event.entity.id)
+ this.$set(this.rule, 'events', [event.eventName])
+ this.$store.dispatch('updateRule', this.rule)
+ }
+ }
+}
+</script>
+
+<style scoped lang="scss">
+ .isComplex {
+ img {
+ vertical-align: top;
+ padding-top: 4px;
+ padding-bottom: 4px;
+ padding-left: 4px;
+ }
+ span {
+ padding-top: 2px;
+ display: inline-block;
+ }
+ }
+ .multiselect::v-deep .multiselect__single {
+ display: flex;
+ }
+ .multiselect:not(.multiselect--active)::v-deep .multiselect__tags {
+ background-color: var(--color-main-background) !important;
+ border: 1px solid transparent;
+ }
+
+ .multiselect::v-deep .multiselect__tags .multiselect__single {
+ background-color: var(--color-main-background) !important;
+ }
+
+ .multiselect:not(.multiselect--disabled)::v-deep .multiselect__tags .multiselect__single {
+ background-image: var(--icon-triangle-s-000);
+ background-repeat: no-repeat;
+ background-position: right center;
+ }
+
+ input {
+ border: 1px solid transparent;
+ }
+
+ .option__title {
+ margin-left: 5px;
+ color: var(--color-main-text);
+ }
+ .option__title_single {
+ font-weight: 900;
+ }
+</style>
diff --git a/apps/workflowengine/src/components/Operation.vue b/apps/workflowengine/src/components/Operation.vue
new file mode 100644
index 00000000000..ad44d376934
--- /dev/null
+++ b/apps/workflowengine/src/components/Operation.vue
@@ -0,0 +1,109 @@
+<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>
+ </div>
+ <div class="actions__item_options">
+ <slot />
+ </div>
+ </div>
+</template>
+
+<script>
+export default {
+ name: 'Operation',
+ props: {
+ operation: {
+ type: Object,
+ required: true
+ },
+ colored: {
+ type: Boolean,
+ default: true
+ }
+ }
+}
+</script>
+
+<style scoped lang="scss">
+ .actions__item {
+ display: flex;
+ flex-wrap: wrap;
+ flex-direction: column;
+ flex-grow: 1;
+ margin-left: -1px;
+ padding: 10px;
+ border-radius: var(--border-radius-large);
+ margin-right: 20px;
+ margin-bottom: 20px;
+ }
+ .icon {
+ display: block;
+ width: 100%;
+ height: 50px;
+ background-size: 50px 50px;
+ background-position: center center;
+ margin-top: 10px;
+ margin-bottom: 20px;
+ background-repeat: no-repeat;
+ }
+ .actions__item__description {
+ text-align: center;
+ }
+ .actions__item_options {
+ width: 100%;
+ margin-top: 10px;
+ }
+ h3, small {
+ padding: 6px;
+ display: block;
+ }
+ h3 {
+ margin: 0;
+ padding: 0;
+ font-weight: 500;
+ }
+ small {
+ font-size: 10pt;
+ }
+
+ .colored {
+ background-color: var(--color-primary-element);
+ * {
+ color: var(--color-primary-text)
+ }
+ }
+
+ .actions__item:not(.colored) {
+ flex-direction: row;
+
+ .actions__item__description {
+ padding-top: 5px;
+ text-align: left;
+ small {
+ padding: 0;
+ }
+ }
+ .icon {
+ width: 50px;
+ margin: 0;
+ margin-right: 10px;
+ &:not(.icon-invert) {
+ filter: invert(1);
+ }
+ }
+ }
+
+ /* TODO: those should be provided by the backend, remove once ready */
+ .icon-block {
+ background-image: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8' standalone='no'%3F%3E%3Csvg xmlns='http://www.w3.org/2000/svg' height='32' width='32' version='1.1' viewBox='0 0 32 32'%3E%3Cpath fill='%23fff' d='m10.203 2-8.203 8.203v11.594l8.203 8.203h11.594l8.203-8.203v-11.594l-8.203-8.203h-11.594zm11.097 5.3092 3.345 3.3448-5.346 5.346 5.346 5.346-3.299 3.299-5.346-5.346-5.346 5.346-3.2992-3.299 5.3462-5.346-5.3462-5.346 3.2992-3.2992 5.346 5.3462 5.3-5.3918z'/%3E%3C/svg%3E");
+ }
+ .icon-convert-pdf {
+ background-image: url("data:image/svg+xml,%3Csvg width='16' height='16' version='1.1' viewBox='0 0 16 16' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='%23fff'%3E%3Cpath d='m7.0624 2.9056c-0.20526 0-0.36653 0.14989-0.36653 0.34066v8.8571c0 0.19077 0.16127 0.34066 0.36653 0.34066h8.0637c0.20526 0 0.36653-0.14989 0.36653-0.34066v-7.1538l-2.1992-2.044zm4.2518 2.6571s0.05132 0.64725-0.10996 1.567c0.52414 1.3987 1.0996 1.5875 1.3562 1.7033 0.54247-0.040873 1.1472-0.068129 1.6861 0.2044 0.36653 0.19486 0.65536 1.022-0.21992 1.022-0.39586-0.023161-1.1267-0.23574-1.6494-0.47692-0.78145 0.081762-1.752 0.21802-2.5657 0.54505-0.91633 1.4308-1.3268 1.6352-1.6494 1.6352-0.89067-0.21802-0.41052-1.3149-0.073304-1.4989 0.40319-0.32022 0.87601-0.50417 1.0263-0.54505 0.065969-0.10221 1.0146-1.8327 1.2462-2.5549-0.21992-0.69767-0.27123-1.4349-0.14661-1.8736 0.57179-0.69358 1.0996-0.23846 1.0996 0.27253zm-0.51315 2.1121c-0.19793 0.72015-0.98817 2.1012-0.95299 2.044 0.81004-0.33044 1.5394-0.42923 2.3458-0.54505-0.38559-0.16011-0.84009-0.17033-1.3928-1.4989z' stroke-width='.70672'/%3E%3Cpath d='m16.246-9.7651c-2.05e-4 0.0144-6e-3 0.027629-6e-3 0.042066-0.0044 2.2592 2.0761 3.742 4.0564 3.6477v1.2349l2.3737-2.2265-2.3377-2.3407-3e-3 1.2289c-1.0287 0.1337-1.8811-0.66867-1.8659-1.5414 2.9e-4 -0.016152 0.0083-0.029062 9e-3 -0.045071z' stroke-width='.67694'/%3E%3Cpath d='m3.2734 5.1094v1.4492h-2.7676v2.5h2.7246l-0.0019532 1.4629 3.0996-2.6387-3.0547-2.7734z'/%3E%3Cpath d='m8.334-11.356c-0.78035-0.78051-1.9205-1.0863-2.9866-0.80073a0.51533 0.51533 0 1 0 0.26293 0.99405c0.71208-0.19075 1.4702 0.01747 1.9914 0.53876 0.46076 0.46083 0.65567 1.1026 0.56688 1.7376a0.61838 0.61838 0 1 0-0.87225 0.87442l0.8687 0.86886a0.61838 0.61838 0 0 0 0.86992 7.91e-5l0.86886-0.8687a0.61838 0.61838 0 0 0 0.0011543-0.88702 0.61838 0.61838 0 0 0-0.67634-0.12303c0.04094-0.86013-0.27221-1.7117-0.89472-2.3343zm-3.3067 1.0814a0.61838 0.61838 0 0 0-0.015967-0.01364l-0.86984-0.87a0.61838 0.61838 0 0 0-0.042126-0.04213 0.61838 0.61838 0 0 0-0.82551 0.04205l-0.87 0.86984a0.61838 0.61838 0 0 0 0.66145 1.0237c-0.024276 0.84049 0.29182 1.6675 0.90045 2.2762 0.78035 0.78052 1.9205 1.0863 2.9866 0.80073a0.51533 0.51533 0 1 0-0.27202-0.99408c-0.71208 0.19075-1.4669-0.011716-1.988-0.53306-0.45484-0.45491-0.65183-1.0905-0.57258-1.7183l0.018216 0.018221a0.61843 0.61843 0 0 0 0.88935-0.85959z' stroke-width='.68342'/%3E%3Cpath d='m31.219 0.33675v0.00113h-6.9286v1.3295l6.9286 0.036145c0.0026-1.821e-4 0.0053 2.074e-4 0.0079 0 0.0053-4.166e-4 0.01058-0.00137 0.01581-0.00113 0.65203-0.00106 1.1749 0.44619 1.1867 1.0392 0.0108 0.5673-0.60099 1.0888-1.3381 1.0019l-0.0013-0.79858-1.6753 1.5203 1.7016 1.4481-0.0013-0.8031c1.419 0.06127 2.9112-0.90236 2.9081-2.3709-0.0029-1.3197-1.2547-2.4007-2.7961-2.4014-0.0023-1e-6 -0.0043-0.00113-0.0066-0.00113z' stroke-width='.462'/%3E%3Crect x='31.116' y='-1.6777' width='4.3279' height='7.5909'/%3E%3C/g%3E%3C/svg%3E");
+ }
+ .colored .icon-invert {
+ filter: invert(1);
+ }
+</style>
diff --git a/apps/workflowengine/src/components/Rule.vue b/apps/workflowengine/src/components/Rule.vue
new file mode 100644
index 00000000000..76d332ac414
--- /dev/null
+++ b/apps/workflowengine/src/components/Rule.vue
@@ -0,0 +1,249 @@
+<template>
+ <div 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"
+ @remove="removeCheck(check)" />
+ </p>
+ <p>
+ <span />
+ <input v-if="lastCheckComplete" type="button" class="check--add"
+ value="Add a new filter" @click="rule.checks.push({class: null, operator: null, value: null})">
+ </p>
+ </div>
+ <div class="flow-icon icon-confirm" />
+ <div class="action">
+ <div class="buttons">
+ <Actions>
+ <ActionButton v-if="rule.id < -1" icon="icon-close" @click="cancelRule">
+ {{ t('workflowengine', 'Cancel rule creation') }}
+ </ActionButton>
+ <ActionButton v-else icon="icon-close" @click="deleteRule">
+ {{ t('workflowengine', 'Remove rule') }}
+ </ActionButton>
+ </Actions>
+ </div>
+ <Operation :operation="operation" :colored="false">
+ <component :is="operation.options" v-if="operation.options" v-model="rule.operation"
+ @input="updateOperation" />
+ </Operation>
+ <button v-tooltip="ruleStatus.tooltip" class="status-button icon" :class="ruleStatus.class"
+ @click="saveRule">
+ {{ ruleStatus.title }}
+ </button>
+ </div>
+ </div>
+</template>
+
+<script>
+import { Tooltip } from 'nextcloud-vue/dist/Directives/Tooltip'
+import { Actions } from 'nextcloud-vue/dist/Components/Actions'
+import { ActionButton } from 'nextcloud-vue/dist/Components/ActionButton'
+import Event from './Event'
+import Check from './Check'
+import Operation from './Operation'
+
+export default {
+ name: 'Rule',
+ components: {
+ Operation, Check, Event, Actions, ActionButton
+ },
+ directives: {
+ Tooltip
+ },
+ props: {
+ rule: {
+ type: Object,
+ required: true
+ }
+ },
+ data() {
+ return {
+ editing: false,
+ checks: [],
+ error: null,
+ dirty: this.rule.id < 0,
+ checking: false
+ }
+ },
+ computed: {
+ operation() {
+ return this.$store.getters.getOperationForRule(this.rule)
+ },
+ ruleStatus() {
+ if (this.error || !this.rule.valid) {
+ return {
+ title: t('workflowengine', 'The configuration is invalid'),
+ class: 'icon-close-white invalid',
+ tooltip: { placement: 'bottom', show: true, content: this.error }
+ }
+ }
+ if (!this.dirty || this.checking) {
+ return { title: 'Active', class: 'icon icon-checkmark' }
+ }
+ return { title: 'Save', class: 'icon-confirm-white primary' }
+
+ },
+ lastCheckComplete() {
+ const lastCheck = this.rule.checks[this.rule.checks.length - 1]
+ return typeof lastCheck === 'undefined' || lastCheck.class !== null
+ }
+ },
+ methods: {
+ async updateOperation(operation) {
+ this.$set(this.rule, 'operation', operation)
+ await this.updateRule()
+ },
+ async updateRule() {
+ this.checking = true
+ if (!this.dirty) {
+ this.dirty = true
+ }
+ try {
+ // TODO: add new verify endpoint
+ // let result = await axios.post(OC.generateUrl(`/apps/workflowengine/operations/test`), this.rule)
+ this.error = null
+ this.checking = false
+ this.$store.dispatch('updateRule', this.rule)
+ } catch (e) {
+ console.error('Failed to update operation', e)
+ this.error = e.response.ocs.meta.message
+ this.checking = false
+ }
+ },
+ async saveRule() {
+ try {
+ await this.$store.dispatch('pushUpdateRule', this.rule)
+ this.dirty = false
+ this.error = null
+ } 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() {
+ this.$store.dispatch('removeRule', this.rule)
+ },
+ 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)
+ }
+ }
+}
+</script>
+
+<style scoped lang="scss">
+ button.icon {
+ padding-left: 32px;
+ background-position: 10px center;
+ }
+
+ .status-button {
+ transition: 0.5s ease all;
+ display: block;
+ margin: auto;
+ margin-right: 0;
+ }
+ .status-button.primary {
+ padding-left: 32px;
+ background-position: 10px center;
+ }
+ .status-button:not(.primary) {
+ background-color: var(--color-main-background);
+ }
+ .status-button.invalid {
+ background-color: var(--color-warning);
+ color: #fff;
+ border: none;
+ }
+
+ .flow-icon {
+ width: 44px;
+ }
+
+ .rule {
+ display: flex;
+ flex-wrap: wrap;
+ border-left: 5px solid var(--color-primary-element);
+
+ .trigger, .action {
+ flex-grow: 1;
+ min-height: 100px;
+ max-width: 700px;
+ }
+ .action {
+ max-width: 400px;
+ position: relative;
+ .buttons {
+ position: absolute;
+ right: 0;
+ display: flex;
+ z-index: 1;
+ }
+ }
+ .icon-confirm {
+ background-position: right 27px;
+ padding-right: 20px;
+ margin-right: 20px;
+ }
+ }
+ .trigger p, .action p {
+ min-height: 34px;
+ display: flex;
+ align-items: center;
+
+ & > span {
+ min-width: 50px;
+ text-align: right;
+ color: var(--color-text-maxcontrast);
+ padding-right: 10px;
+ padding-top: 7px;
+ margin-bottom: auto;
+ }
+ .multiselect {
+ flex-grow: 1;
+ max-width: 300px;
+ }
+ }
+
+ .check--add {
+ background-position: 7px center;
+ background-color: transparent;
+ padding-left: 6px;
+ margin: 0;
+ width: 180px;
+ border-radius: var(--border-radius);
+ font-weight: normal;
+ text-align: left;
+ 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..b4fab5a058c
--- /dev/null
+++ b/apps/workflowengine/src/components/Workflow.vue
@@ -0,0 +1,126 @@
+<template>
+ <div id="workflowengine">
+ <div class="section">
+ <h2>{{ t('workflowengine', 'Workflows') }}</h2>
+
+ <transition-group name="slide" tag="div" class="actions">
+ <Operation v-for="operation in getMainOperations" :key="operation.id" :operation="operation"
+ @click.native="createNewRule(operation)" />
+ </transition-group>
+
+ <div v-if="hasMoreOperations" class="actions__more">
+ <button class="icon" :class="showMoreOperations ? 'icon-triangle-n' : 'icon-triangle-s'"
+ @click="showMoreOperations=!showMoreOperations">
+ {{ showMoreOperations ? t('workflowengine', 'Show less') : t('workflowengine', 'Show more') }}
+ </button>
+ </div>
+ </div>
+
+ <transition-group v-if="rules.length > 0" name="slide">
+ <Rule v-for="rule in rules" :key="rule.id" :rule="rule" />
+ </transition-group>
+ </div>
+</template>
+
+<script>
+import Rule from './Rule'
+import Operation from './Operation'
+import { mapGetters, mapState } from 'vuex'
+
+const ACTION_LIMIT = 3
+
+export default {
+ name: 'Workflow',
+ components: {
+ Operation,
+ Rule
+ },
+ data() {
+ return {
+ showMoreOperations: false
+ }
+ },
+ computed: {
+ ...mapGetters({
+ rules: 'getRules'
+ }),
+ ...mapState({
+ operations: 'operations'
+ }),
+ hasMoreOperations() {
+ return Object.keys(this.operations).length > ACTION_LIMIT
+ },
+ getMainOperations() {
+ if (this.showMoreOperations) {
+ return Object.values(this.operations)
+ }
+ return Object.values(this.operations).slice(0, ACTION_LIMIT)
+ }
+ },
+ mounted() {
+ this.$store.dispatch('fetchRules')
+ },
+ methods: {
+ createNewRule(operation) {
+ this.$store.dispatch('createNewRule', operation)
+ }
+ }
+}
+</script>
+
+<style scoped lang="scss">
+ #workflowengine {
+ border-bottom: 1px solid var(--color-border);
+ }
+ .section {
+ max-width: 100vw;
+ }
+ .actions {
+ display: flex;
+ flex-wrap: wrap;
+ max-width: 900px;
+ .actions__item {
+ max-width: 280px;
+ flex-basis: 250px;
+ }
+ }
+
+ button.icon {
+ padding-left: 32px;
+ background-position: 10px center;
+ }
+
+ .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;
+ }
+</style>