diff --git a/apps/workflowengine/appinfo/routes.php b/apps/workflowengine/appinfo/routes.php index 3798c2a852caea916f7085528342b6de84abaae7..ea44fb6cda1b88765fb5b43a799f9a64e7327834 100644 --- a/apps/workflowengine/appinfo/routes.php +++ b/apps/workflowengine/appinfo/routes.php @@ -25,5 +25,6 @@ return [ ], 'ocs-resources' => [ 'global_workflows' => ['url' => '/api/v1/workflows/global'], + 'user_workflows' => ['url' => '/api/v1/workflows/user'], ], ]; diff --git a/apps/workflowengine/css/admin.scss b/apps/workflowengine/css/admin.scss deleted file mode 100644 index 523bb7f8ba62b8e7cbab516a34d8d47a6b6129a0..0000000000000000000000000000000000000000 --- a/apps/workflowengine/css/admin.scss +++ /dev/null @@ -1,86 +0,0 @@ -.workflowengine { - .pull-right { - float: right - } - - .invalid-input { - border-color: var(--color-error); - } - - .operation { - padding: 5px 5px 20px; - margin-bottom: 20px; - border-bottom: var(--color-border) 1px solid; - border-left: rgba(0, 0, 0, 0) 2px solid; - - &.modified { - border-left: var(--color-warning) 2px solid; - } - - button { - margin-bottom: 0; - } - - span.info { - padding: 7px; - color: var(--color-border); - } - - .msg { - border-radius: 3px; - margin: 3px 3px 3px 0; - padding: 5px; - transition: opacity .5s; - } - - .check { - padding-left: 5px; - &:hover { - background-color: var(--color-background-dark); - } - } - - .button-delete, - .button-delete-check { - opacity: 0.5; - padding: 11px; - - &:hover, - &:focus { - opacity: 1; - cursor: pointer; - } - } - } - - .rules { - .icon-loading-small { - display: inline-block; - margin-right: 5px; - } - - .operation:nth-last-child(2) { - margin-bottom: 5px; - } - } - - .operation-header { - display: flex; - margin-left: 5px; - - .operation-name { - width: 100%; - max-width: 500px; - align-self: flex-start; - padding: 10px; - } - - .select2-container { - align-self: flex-end; - } - - .icon-delete { - margin-left: auto; - } - } -} diff --git a/apps/workflowengine/css/multiselect.css b/apps/workflowengine/css/multiselect.css deleted file mode 100644 index 0c3b8b009eb24bb36aa7457e39c2619b2b1ff279..0000000000000000000000000000000000000000 --- a/apps/workflowengine/css/multiselect.css +++ /dev/null @@ -1,12 +0,0 @@ -#workflowengine .multiselect .multiselect__single { - display: flex; -} - -#workflowengine .option__icon { - min-width: 25px; -} - -#workflowengine input, -#workflowengine .multiselect { - width: 100%; -} diff --git a/apps/workflowengine/js/workflowengine.js b/apps/workflowengine/js/workflowengine.js index 00f319c94c8aa8e3cb42a1a1adb0123859b07636..34684abd5edea3d983d7bf3de56d9db3cd020a07 100644 Binary files a/apps/workflowengine/js/workflowengine.js and b/apps/workflowengine/js/workflowengine.js differ diff --git a/apps/workflowengine/js/workflowengine.js.map b/apps/workflowengine/js/workflowengine.js.map index ea545fe501248c3afa22c0f6526a925975cc91b9..d13b6b66df1427ba04ba5d3ac4aac8ab3d81196d 100644 Binary files a/apps/workflowengine/js/workflowengine.js.map and b/apps/workflowengine/js/workflowengine.js.map differ diff --git a/apps/workflowengine/lib/Controller/UserWorkflowsController.php b/apps/workflowengine/lib/Controller/UserWorkflowsController.php index 179e6b1ad11bdf00023f0abd24f1a79bd1a0ecd6..3e907d22696b6a81ed93ea9ab180f1584a6b7de9 100644 --- a/apps/workflowengine/lib/Controller/UserWorkflowsController.php +++ b/apps/workflowengine/lib/Controller/UserWorkflowsController.php @@ -79,8 +79,8 @@ class UserWorkflowsController extends AWorkflowController { * @throws OCSBadRequestException * @throws OCSForbiddenException */ - public function create(string $class, string $name, array $checks, string $operation): DataResponse { - return parent::create($class, $name, $checks, $operation); + public function create(string $class, string $name, array $checks, string $operation, string $entity, array $events): DataResponse { + return parent::create($class, $name, $checks, $operation, $entity, $events); } /** @@ -88,8 +88,8 @@ class UserWorkflowsController extends AWorkflowController { * @throws OCSBadRequestException * @throws OCSForbiddenException */ - public function update(int $id, string $name, array $checks, string $operation): DataResponse { - return parent::update($id, $name, $checks, $operation); + public function update(int $id, string $name, array $checks, string $operation, string $entity, array $events): DataResponse { + return parent::update($id, $name, $checks, $operation, $entity, $events); } /** diff --git a/apps/workflowengine/lib/Manager.php b/apps/workflowengine/lib/Manager.php index 07438b2f7cb130cfd8733895510e6c6941dd9045..1c2c76a94c4dd7f7ecf141e6f8e803cbfece62e6 100644 --- a/apps/workflowengine/lib/Manager.php +++ b/apps/workflowengine/lib/Manager.php @@ -561,6 +561,8 @@ class Manager implements IManager { $operation['checks'][] = $check; } + $operation['events'] = json_decode($operation['events'], true); + return $operation; } diff --git a/apps/workflowengine/lib/Service/RuleMatcher.php b/apps/workflowengine/lib/Service/RuleMatcher.php index bcfcd5dd21909b102c569acf5839ca78f640d48d..95c68b6337091d031f7357b89973f70fe4350a4b 100644 --- a/apps/workflowengine/lib/Service/RuleMatcher.php +++ b/apps/workflowengine/lib/Service/RuleMatcher.php @@ -24,12 +24,9 @@ declare(strict_types=1); namespace OCA\WorkflowEngine\Service; -use OCA\WorkflowEngine\AppInfo\Application; -use OCA\WorkflowEngine\Entity\File; use OCA\WorkflowEngine\Helper\ScopeContext; use OCA\WorkflowEngine\Manager; use OCP\AppFramework\QueryException; -use OCP\Files\Node; use OCP\Files\Storage\IStorage; use OCP\IL10N; use OCP\IServerContainer; @@ -128,7 +125,7 @@ class RuleMatcher implements IRuleMatcher { list($entity, $subject) = $entityInfo; $checkInstance->setEntitySubject($entity, $subject); } - } else { + } else if(!$checkInstance instanceof ICheck) { // Check is invalid throw new \UnexpectedValueException($this->l->t('Check %s is invalid or does not exist', $check['class'])); } diff --git a/apps/workflowengine/src/components/Check.vue b/apps/workflowengine/src/components/Check.vue index 4f68e3944956dc76b52ef7f468fffeee00622b3e..10828c1dd8aaec09e0c8361c519c4ee370d77217 100644 --- a/apps/workflowengine/src/components/Check.vue +++ b/apps/workflowengine/src/components/Check.vue @@ -11,6 +11,7 @@ <Multiselect v-model="currentOperator" :disabled="!currentOption" :options="operators" + class="comparator" label="name" track-by="operator" :allow-empty="false" @@ -21,6 +22,7 @@ v-model="check.value" :disabled="!currentOption" :check="check" + class="option" @input="updateCheck" @valid="(valid=true) && validate()" @invalid="(valid=false) && validate()" /> @@ -30,9 +32,10 @@ :class="{ invalid: !valid }" :disabled="!currentOption" :placeholder="valuePlaceholder" + class="option" @input="updateCheck"> <Actions v-if="deleteVisible || !currentOption"> - <ActionButton icon="icon-delete" @click="$emit('remove')" /> + <ActionButton icon="icon-close" @click="$emit('remove')" /> </Actions> </div> </template> @@ -73,17 +76,16 @@ export default { } }, computed: { - Checks() { + checks() { return this.$store.getters.getChecksForEntity(this.rule.entity) }, operators() { if (!this.currentOption) { return [] } - return this.Checks[this.currentOption.class].operators + return this.checks[this.currentOption.class].operators }, currentComponent() { if (!this.currentOption) { return [] } - const currentComponent = this.Checks[this.currentOption.class].component - return currentComponent + return this.checks[this.currentOption.class].component }, valuePlaceholder() { if (this.currentOption && this.currentOption.placeholder) { @@ -98,8 +100,8 @@ export default { } }, mounted() { - this.options = Object.values(this.Checks) - this.currentOption = this.Checks[this.check.class] + 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: { @@ -111,13 +113,8 @@ export default { }, validate() { if (this.currentOption && this.currentOption.validate) { - if (this.currentOption.validate(this.check)) { - this.valid = true - } else { - this.valid = false - } + this.valid = !!this.currentOption.validate(this.check) } - this.$store.dispatch('setValid', { rule: this.rule, valid: this.rule.valid && this.valid }) return this.valid }, updateCheck() { @@ -128,7 +125,7 @@ export default { this.check.operator = this.currentOperator.operator if (!this.validate()) { - return + this.check.invalid = !this.valid } this.$emit('update', this.check) } @@ -142,14 +139,30 @@ export default { flex-wrap: wrap; width: 100%; padding-right: 20px; - & > *:not(.icon-delete) { + & > *:not(.close) { width: 180px; } + & > .comparator { + min-width: 130px; + width: 130px; + } + & > .option { + min-width: 230px; + width: 230px; + } & > .multiselect, & > input[type=text] { margin-right: 5px; margin-bottom: 5px; } + + .multiselect::v-deep .multiselect__content-wrapper li>span, + .multiselect::v-deep .multiselect__single { + display: block; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } } input[type=text] { margin: 0; @@ -157,14 +170,12 @@ export default { ::placeholder { font-size: 10px; } - .icon-delete { + button.action-item.action-item--single.icon-close { + height: 44px; + width: 44px; 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; } diff --git a/apps/workflowengine/src/components/Checks/FileMimeType.vue b/apps/workflowengine/src/components/Checks/FileMimeType.vue index 2f2487c9adf43ba2affc61a85db4691d66d0b87b..e91636f5130a0a54da5d881f435627aea5dcd697 100644 --- a/apps/workflowengine/src/components/Checks/FileMimeType.vue +++ b/apps/workflowengine/src/components/Checks/FileMimeType.vue @@ -32,17 +32,20 @@ :tagging="false" @input="setValue"> <template slot="singleLabel" slot-scope="props"> - <span class="option__icon" :class="props.option.icon" /> + <span v-if="props.option.icon" class="option__icon" :class="props.option.icon" /> + <img v-else :src="props.option.iconUrl"> <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 v-if="props.option.icon" class="option__icon" :class="props.option.icon" /> + <img v-else :src="props.option.iconUrl"> <span class="option__title">{{ props.option.label }}</span> </template> </Multiselect> <input v-if="!isPredefined" type="text" :value="currentValue.pattern" + :placeholder="t('workflowengine', 'e.g. httpd/unix-directory')" @input="updateCustom"> </div> </template> @@ -68,12 +71,12 @@ export default { pattern: '/image\\/.*/' }, { - icon: 'icon-category-office', + iconUrl: OC.imagePath('core', 'filetypes/x-office-document'), label: t('workflowengine', 'Office documents'), pattern: '/(vnd\\.(ms-|openxmlformats-).*))$/' }, { - icon: 'icon-filetype-file', + iconUrl: OC.imagePath('core', 'filetypes/application-pdf'), label: t('workflowengine', 'PDF documents'), pattern: 'application/pdf' } @@ -130,3 +133,15 @@ export default { } } </script> +<style scoped> + .multiselect, input[type='text'] { + width: 100%; + } + .multiselect >>> .multiselect__content-wrapper li>span, + .multiselect >>> .multiselect__single { + display: flex; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } +</style> diff --git a/apps/workflowengine/src/components/Checks/FileSystemTag.vue b/apps/workflowengine/src/components/Checks/FileSystemTag.vue index d3fd440f1b19e6613912f11eb1df8924f2f49d85..268f8c4e33fe7e572471e1f49d8eb020b7fbfbe4 100644 --- a/apps/workflowengine/src/components/Checks/FileSystemTag.vue +++ b/apps/workflowengine/src/components/Checks/FileSystemTag.vue @@ -23,7 +23,7 @@ <template> <MultiselectTag v-model="newValue" :multiple="false" - label="Select a tag" + :label="t('workflowengine', 'Select a tag')" @input="update" /> </template> diff --git a/apps/workflowengine/src/components/Checks/MultiselectTag/MultiselectTag.vue b/apps/workflowengine/src/components/Checks/MultiselectTag/MultiselectTag.vue index c58f53c8e97ea3aec01cc1d45640ed6e8a3b043e..804025dc0e5c49ce92602ae04935df92bb5ed4f1 100644 --- a/apps/workflowengine/src/components/Checks/MultiselectTag/MultiselectTag.vue +++ b/apps/workflowengine/src/components/Checks/MultiselectTag/MultiselectTag.vue @@ -56,10 +56,8 @@ export default { required: true }, value: { - type: Array, - default() { - return [] - } + type: [String, Array], + default: null }, disabled: { type: Boolean, @@ -67,7 +65,7 @@ export default { }, multiple: { type: Boolean, - default: true + default: false } }, data() { diff --git a/apps/workflowengine/src/components/Checks/RequestTime.vue b/apps/workflowengine/src/components/Checks/RequestTime.vue index 1d7950f64f8ff769978d2bba0206428260cdc4ba..26a4907fd1844037e3ef1f8052d792ac7da3efb6 100644 --- a/apps/workflowengine/src/components/Checks/RequestTime.vue +++ b/apps/workflowengine/src/components/Checks/RequestTime.vue @@ -1,15 +1,21 @@ <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" + placeholder="e.g. 08:00" @input="update"> <input v-model="newValue.endTime" type="text" - placeholder="18:00" + placeholder="e.g. 18:00" @input="update"> + <p v-if="!valid" class="invalid-hint"> + {{ t('workflowengine', 'Please enter a valid time span') }} + </p> + <Multiselect v-show="valid" + v-model="newValue.timezone" + :options="timezones" + @input="update" /> </div> </template> @@ -30,7 +36,7 @@ export default { props: { value: { type: String, - default: '1 MB' + default: '' } }, data() { @@ -46,14 +52,17 @@ export default { }, 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 + try { + const data = JSON.parse(value) + if (data.length === 2) { + this.newValue = { + startTime: data[0].split(' ', 2)[0], + endTime: data[1].split(' ', 2)[0], + timezone: data[0].split(' ', 2)[1] + } + } + } catch (e) { + // ignore invalid values } }, validate() { @@ -86,14 +95,23 @@ export default { margin-bottom: 5px; } + .multiselect::v-deep .multiselect__tags:not(:hover):not(:focus):not(:active) { + border: 1px solid transparent; + } + input[type=text] { width: 50%; margin: 0; margin-bottom: 5px; + &.timeslot--start { margin-right: 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 index 5f337a669bdfc5e5da62dc6dd28c657e94a660f6..f63f7d29114e08cb3b1f2877f764595321aea503 100644 --- a/apps/workflowengine/src/components/Checks/RequestURL.vue +++ b/apps/workflowengine/src/components/Checks/RequestURL.vue @@ -137,3 +137,8 @@ export default { } } </script> +<style scoped> + .multiselect, input[type='text'] { + width: 100%; + } +</style> diff --git a/apps/workflowengine/src/components/Checks/RequestUserAgent.vue b/apps/workflowengine/src/components/Checks/RequestUserAgent.vue index f06aac2e8f76b901cbbe4b0819de5ec244b59fc0..4e0edb6bf49e2718e2bb8bf97f6dd2b4a1872850 100644 --- a/apps/workflowengine/src/components/Checks/RequestUserAgent.vue +++ b/apps/workflowengine/src/components/Checks/RequestUserAgent.vue @@ -27,19 +27,22 @@ :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> + <!-- v-html can be used here as t() always passes our translated strings though DOMPurify.sanitize --> + <!-- eslint-disable-next-line vue/no-v-html --> + <span class="option__title option__title_single" v-html="props.option.label" /> </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> + <!-- eslint-disable-next-line vue/no-v-html --> + <span v-if="props.option.$groupLabel" class="option__title" v-html="props.option.$groupLabel" /> + <!-- eslint-disable-next-line vue/no-v-html --> + <span v-else class="option__title" v-html="props.option.label" /> </template> </Multiselect> <input v-if="!isPredefined" @@ -65,15 +68,10 @@ export default { 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' } - ] - } + { 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' } ] } }, @@ -83,8 +81,6 @@ export default { }, matchingPredefined() { return this.predefinedTypes - .map(groups => groups.children) - .flat() .find((type) => this.newValue === type.pattern) }, isPredefined() { @@ -92,14 +88,9 @@ export default { }, customValue() { return { - label: t('workflowengine', 'Others'), - children: [ - { - icon: 'icon-settings-dark', - label: t('workflowengine', 'Custom user agent'), - pattern: '' - } - ] + icon: 'icon-settings-dark', + label: t('workflowengine', 'Custom user agent'), + pattern: '' } }, currentValue() { @@ -115,8 +106,8 @@ export default { }, methods: { validateRegex(string) { - var regexRegex = /^\/(.*)\/([gui]{0,3})$/ - var result = regexRegex.exec(string) + const regexRegex = /^\/(.*)\/([gui]{0,3})$/ + const result = regexRegex.exec(string) return result !== null }, setValue(value) { @@ -133,3 +124,32 @@ export default { } } </script> +<style scoped> + .multiselect, input[type='text'] { + width: 100%; + } + + .multiselect .multiselect__content-wrapper li>span { + display: flex; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + .multiselect::v-deep .multiselect__single { + width: 100%; + display: flex; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + .option__icon { + display: inline-block; + min-width: 30px; + background-position: left; + } + .option__title { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } +</style> diff --git a/apps/workflowengine/src/components/Checks/RequestUserGroup.vue b/apps/workflowengine/src/components/Checks/RequestUserGroup.vue index 1ab06d9b84d26b56ac3b139d16663e17c1fae133..f254a5185f6e89609e369253342a1643d447670a 100644 --- a/apps/workflowengine/src/components/Checks/RequestUserGroup.vue +++ b/apps/workflowengine/src/components/Checks/RequestUserGroup.vue @@ -22,56 +22,91 @@ <template> <div> - <Multiselect v-model="newValue" - :class="{'icon-loading-small': groups.length === 0}" + <Multiselect :value="currentValue" + :loading="status.isLoading && groups.length === 0" :options="groups" :multiple="false" label="displayname" track-by="id" - @input="setValue" /> + @search-change="searchAsync" + @input="(value) => $emit('input', value.id)" /> </div> </template> <script> import { Multiselect } from 'nextcloud-vue/dist/Components/Multiselect' -import valueMixin from '../../mixins/valueMixin' import axios from '@nextcloud/axios' + +const groups = [] +const status = { + isLoading: false +} + export default { name: 'RequestUserGroup', components: { Multiselect }, - mixins: [ - valueMixin - ], + props: { + value: { + type: String, + default: '' + }, + check: { + type: Object, + default: () => { return {} } + } + }, data() { return { - groups: [] + groups: groups, + status: status } }, - 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) - }) + computed: { + currentValue() { + return this.groups.find(group => group.id === this.value) || null + } + }, + async mounted() { + if (this.groups.length === 0) { + await this.searchAsync('') + } + if (this.currentValue === null) { + await this.searchAsync(this.value) + } }, methods: { - updateInternalValue() { - this.newValue = this.groups.find(group => group.id === this.value) || null + searchAsync(searchQuery) { + if (this.status.isLoading) { + return + } + + this.status.isLoading = true + return axios.get(OC.linkToOCS('cloud', 2) + 'groups?limit=20&search=' + encodeURI(searchQuery)).then((response) => { + response.data.ocs.data.groups.reduce((obj, item) => { + obj.push({ + id: item, + displayname: item + }) + return obj + }, []).forEach((group) => this.addGroup(group)) + this.status.isLoading = false + }, (error) => { + console.error('Error while loading group list', error.response) + }) }, - setValue(value) { - if (value !== null) { - this.$emit('input', this.newValue.id) + addGroup(group) { + const index = this.groups.findIndex((item) => item.id === group.id) + if (index === -1) { + this.groups.push(group) } } } } </script> +<style scoped> + .multiselect { + width: 100%; + } +</style> diff --git a/apps/workflowengine/src/components/Checks/file.js b/apps/workflowengine/src/components/Checks/file.js index 76f998da0078a0e984050ed4ac10efe415e6bee9..0cc49c2d4c1f226d4b20e37dc04bdf2f74bbf10f 100644 --- a/apps/workflowengine/src/components/Checks/file.js +++ b/apps/workflowengine/src/components/Checks/file.js @@ -65,7 +65,7 @@ const FileChecks = [ { operator: 'greater', name: t('workflowengine', 'greater') } ], placeholder: (check) => '5 MB', - validate: (check) => check.value.match(/^[0-9]+[ ]?[kmgt]?b$/i) !== null + validate: (check) => check.value ? check.value.match(/^[0-9]+[ ]?[kmgt]?b$/i) !== null : false }, { diff --git a/apps/workflowengine/src/components/Event.vue b/apps/workflowengine/src/components/Event.vue index a06835f5f8230fcdcf0c732859071bbf036308aa..97608fde062d716e8cb1a6dd9b3c08207d393d42 100644 --- a/apps/workflowengine/src/components/Event.vue +++ b/apps/workflowengine/src/components/Event.vue @@ -1,5 +1,5 @@ <template> - <div> + <div class="event"> <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> @@ -7,14 +7,16 @@ <Multiselect v-else :value="currentEvent" :options="allEvents" - label="eventName" track-by="id" - :allow-empty="false" + :multiple="true" + :auto-limit="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 slot="selection" slot-scope="{ values, search, isOpen }"> + <div v-if="values.length && !isOpen" class="eventlist"> + <img class="option__icon" :src="values[0].entity.icon"> + <span v-for="(value, index) in values" :key="value.id" class="text option__title option__title_single">{{ value.displayName }} <span v-if="index+1 < values.length">, </span></span> + </div> </template> <template slot="option" slot-scope="props"> <img class="option__icon" :src="props.option.entity.icon"> @@ -49,23 +51,22 @@ export default { 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) + return this.allEvents.filter(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) + updateEvent(events) { + this.$set(this.rule, 'events', events.map(event => event.eventName)) + this.$emit('update', this.rule) } } } </script> <style scoped lang="scss"> + .event { + margin-bottom: 5px; + } .isComplex { img { vertical-align: top; @@ -78,6 +79,11 @@ export default { display: inline-block; } } + .multiselect { + width: 100%; + max-width: 550px; + margin-top: 4px; + } .multiselect::v-deep .multiselect__single { display: flex; } @@ -86,8 +92,10 @@ export default { border: 1px solid transparent; } - .multiselect::v-deep .multiselect__tags .multiselect__single { + .multiselect::v-deep .multiselect__tags { background-color: var(--color-main-background) !important; + height: auto; + min-height: 34px; } .multiselect:not(.multiselect--disabled)::v-deep .multiselect__tags .multiselect__single { @@ -107,4 +115,9 @@ export default { .option__title_single { font-weight: 900; } + + .eventlist img, + .eventlist .text { + vertical-align: middle; + } </style> diff --git a/apps/workflowengine/src/components/Operation.vue b/apps/workflowengine/src/components/Operation.vue index ad44d376934b50637dc2fb73551c0a9e90fc27b1..ae0a67ae53d088460a05bf80d4ad40543669f96b 100644 --- a/apps/workflowengine/src/components/Operation.vue +++ b/apps/workflowengine/src/components/Operation.vue @@ -55,6 +55,7 @@ export default { .actions__item_options { width: 100%; margin-top: 10px; + padding-left: 60px; } h3, small { padding: 6px; @@ -63,7 +64,7 @@ export default { h3 { margin: 0; padding: 0; - font-weight: 500; + font-weight: 600; } small { font-size: 10pt; @@ -82,6 +83,7 @@ export default { .actions__item__description { padding-top: 5px; text-align: left; + width: calc(100% - 105px); small { padding: 0; } diff --git a/apps/workflowengine/src/components/Rule.vue b/apps/workflowengine/src/components/Rule.vue index 2be9b0fc5e5279104670781fee3978db56d3b512..703b7832afaf4d7d7673bdebb136be192fffc402 100644 --- a/apps/workflowengine/src/components/Rule.vue +++ b/apps/workflowengine/src/components/Rule.vue @@ -1,5 +1,5 @@ <template> - <div class="section rule" :style="{ borderLeftColor: operation.color || '' }"> + <div v-if="operation" class="section rule" :style="{ borderLeftColor: operation.color || '' }"> <div class="trigger"> <p> <span>{{ t('workflowengine', 'When') }}</span> @@ -23,28 +23,26 @@ </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 class="buttons"> + <button v-tooltip="ruleStatus.tooltip" + class="status-button icon" + :class="ruleStatus.class" + @click="saveRule"> + {{ ruleStatus.title }} + </button> + <button v-if="rule.id < -1" @click="cancelRule"> + {{ t('workflowengine', 'Cancel') }} + </button> + <button v-else @click="deleteRule"> + {{ t('workflowengine', 'Delete') }} + </button> + </div> </div> </div> </template> @@ -85,7 +83,7 @@ export default { return this.$store.getters.getOperationForRule(this.rule) }, ruleStatus() { - if (this.error || !this.rule.valid) { + if (this.error || !this.rule.valid || this.rule.checks.some((check) => check.invalid === true)) { return { title: t('workflowengine', 'The configuration is invalid'), class: 'icon-close-white invalid', @@ -163,11 +161,18 @@ export default { background-position: 10px center; } + .buttons { + display: block; + button { + float: right; + height: 34px; + } + } + .status-button { transition: 0.5s ease all; display: block; - margin: auto; - margin-right: 0; + margin: 3px 10px 3px auto; } .status-button.primary { padding-left: 32px; @@ -199,12 +204,6 @@ export default { .action { max-width: 400px; position: relative; - .buttons { - position: absolute; - right: 0; - display: flex; - z-index: 1; - } } .icon-confirm { background-position: right 27px; @@ -238,6 +237,7 @@ export default { margin: 0; width: 180px; border-radius: var(--border-radius); + color: var(--color-text-maxcontrast); font-weight: normal; text-align: left; font-size: 1em; diff --git a/apps/workflowengine/src/helpers/api.js b/apps/workflowengine/src/helpers/api.js index 2b2bb40a7e28a2b74c6e42ecee1d7a731717ad10..76861d3bb35311dc716504b126e9b138790f1365 100644 --- a/apps/workflowengine/src/helpers/api.js +++ b/apps/workflowengine/src/helpers/api.js @@ -22,8 +22,9 @@ import { loadState } from '@nextcloud/initial-state' +const scopeValue = loadState('workflowengine', 'scope') === 0 ? 'global' : 'user' + const getApiUrl = (url) => { - const scopeValue = loadState('workflowengine', 'scope') === 0 ? 'global' : 'user' return OC.linkToOCS('apps/workflowengine/api/v1/workflows', 2) + scopeValue + url + '?format=json' } diff --git a/apps/workflowengine/src/helpers/validators.js b/apps/workflowengine/src/helpers/validators.js index 68ced4ae6faf8649dc5ba04ab1927285a16e1dd6..d64adfa1385da74ea7314f18c44f6942083fbcc7 100644 --- a/apps/workflowengine/src/helpers/validators.js +++ b/apps/workflowengine/src/helpers/validators.js @@ -19,23 +19,29 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. * */ +const regexRegex = /^\/(.*)\/([gui]{0,3})$/ +const regexIPv4 = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\/(3[0-2]|[1-2][0-9]|[1-9])$/ +const regexIPv6 = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))\/(1([01][0-9]|2[0-8])|[1-9][0-9]|[0-9])$/ const validateRegex = function(string) { - var regexRegex = /^\/(.*)\/([gui]{0,3})$/ - var result = regexRegex.exec(string) - return result !== null + if (!string) { + return false + } + return regexRegex.exec(string) !== null } const validateIPv4 = function(string) { - var regexRegex = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\/(3[0-2]|[1-2][0-9]|[1-9])$/ - var result = regexRegex.exec(string) - return result !== null + if (!string) { + return false + } + return regexIPv4.exec(string) !== null } const validateIPv6 = function(string) { - var regexRegex = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))\/(1([01][0-9]|2[0-8])|[1-9][0-9]|[0-9])$/ - var result = regexRegex.exec(string) - return result !== null + if (!string) { + return false + } + return regexIPv6.exec(string) !== null } const stringValidator = (check) => { diff --git a/apps/workflowengine/src/store.js b/apps/workflowengine/src/store.js index cbd9b29c81c1a63fa53afd7c1074b2998c6979f1..a18540f8035f23c3eb6a74acc346067de2b768da 100644 --- a/apps/workflowengine/src/store.js +++ b/apps/workflowengine/src/store.js @@ -71,7 +71,9 @@ const store = new Vuex.Store({ plugin = Object.assign( { color: 'var(--color-primary-element)' }, plugin, state.operations[plugin.id] || {}) - Vue.set(state.operations, plugin.id, plugin) + if (typeof state.operations[plugin.id] !== 'undefined') { + Vue.set(state.operations, plugin.id, plugin) + } } }, actions: { diff --git a/apps/workflowengine/templates/settings.php b/apps/workflowengine/templates/settings.php index 2b8a825269524063714f37647424c55b103d828d..f306bb9e1f1eae14ad3b0bde0d15e5be87b04674 100644 --- a/apps/workflowengine/templates/settings.php +++ b/apps/workflowengine/templates/settings.php @@ -19,7 +19,6 @@ * */ use OCA\WorkflowEngine\AppInfo\Application; -style(Application::APP_ID, 'multiselect'); /** @var array $_ */ /** @var \OCP\IL10N $l */ diff --git a/lib/public/WorkflowEngine/IEntityCheck.php b/lib/public/WorkflowEngine/IEntityCheck.php index 7a4df0afd5f44403ca6f7d681f4b55ad148a852a..d90535b4c999317dd1ae8d36e3599aaf3d88122b 100644 --- a/lib/public/WorkflowEngine/IEntityCheck.php +++ b/lib/public/WorkflowEngine/IEntityCheck.php @@ -24,9 +24,6 @@ declare(strict_types=1); namespace OCP\WorkflowEngine; - -use OCP\Files\Storage\IStorage; - /** * Interface IFileCheck *