Merge pull request #16706 from nextcloud/workflow-frontend

Workflow frontend overhaul
This commit is contained in:
blizzz 2019-09-10 16:05:17 +02:00 committed by GitHub
commit bfec3715ee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 2230 additions and 1619 deletions

View File

@ -425,7 +425,7 @@ class Manager implements IManager {
* @param string $operation
* @throws \UnexpectedValueException
*/
protected function validateOperation($class, $name, array $checks, $operation, string $entity, array $events) {
public function validateOperation($class, $name, array $checks, $operation, string $entity, array $events) {
try {
/** @var IOperation $instance */
$instance = $this->container->query($class);

View File

@ -1,385 +0,0 @@
/**
* @copyright Copyright (c) 2016 Morris Jobke <hey@morrisjobke.de>
*
* @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 OperationTemplate from './templates/operation.handlebars';
import OperationsTemplate from './templates/operations.handlebars';
(function() {
OCA.WorkflowEngine = _.extend(OCA.WorkflowEngine || {}, {
availablePlugins: [],
availableChecks: [],
getCheckByClass: function(className) {
var length = OCA.WorkflowEngine.availableChecks.length;
for (var i = 0; i < length; i++) {
if (OCA.WorkflowEngine.availableChecks[i]['class'] === className) {
return OCA.WorkflowEngine.availableChecks[i];
}
}
return undefined;
}
});
/**
* 888b d888 888 888
* 8888b d8888 888 888
* 88888b.d88888 888 888
* 888Y88888P888 .d88b. .d88888 .d88b. 888 .d8888b
* 888 Y888P 888 d88""88b d88" 888 d8P Y8b 888 88K
* 888 Y8P 888 888 888 888 888 88888888 888 "Y8888b.
* 888 " 888 Y88..88P Y88b 888 Y8b. 888 X88
* 888 888 "Y88P" "Y88888 "Y8888 888 88888P'
*/
/**
* @class OCA.WorkflowEngine.Operation
*/
OCA.WorkflowEngine.Operation =
OC.Backbone.Model.extend({
defaults: {
'class': 'OCA\\WorkflowEngine\\Operation',
'name': '',
'checks': [],
'operation': ''
}
});
/**
* .d8888b. 888 888 888 d8b
* d88P Y88b 888 888 888 Y8P
* 888 888 888 888 888
* 888 .d88b. 888 888 .d88b. .d8888b 888888 888 .d88b. 88888b. .d8888b
* 888 d88""88b 888 888 d8P Y8b d88P" 888 888 d88""88b 888 "88b 88K
* 888 888 888 888 888 888 88888888 888 888 888 888 888 888 888 "Y8888b.
* Y88b d88P Y88..88P 888 888 Y8b. Y88b. Y88b. 888 Y88..88P 888 888 X88
* "Y8888P" "Y88P" 888 888 "Y8888 "Y8888P "Y888 888 "Y88P" 888 888 88888P'
*/
/**
* @class OCA.WorkflowEngine.OperationsCollection
*
* collection for all configurated operations
*/
OCA.WorkflowEngine.OperationsCollection =
OC.Backbone.Collection.extend({
model: OCA.WorkflowEngine.Operation,
url: OC.generateUrl('apps/workflowengine/operations')
});
/**
* 888 888 d8b
* 888 888 Y8P
* 888 888
* Y88b d88P 888 .d88b. 888 888 888 .d8888b
* Y88b d88P 888 d8P Y8b 888 888 888 88K
* Y88o88P 888 88888888 888 888 888 "Y8888b.
* Y888P 888 Y8b. Y88b 888 d88P X88
* Y8P 888 "Y8888 "Y8888888P" 88888P'
*/
/**
* @class OCA.WorkflowEngine.OperationView
*
* this creates the view for a single operation
*/
OCA.WorkflowEngine.OperationView =
OC.Backbone.View.extend({
templateId: '#operation-template',
events: {
'change .check-class': 'checkChanged',
'change .check-operator': 'checkChanged',
'change .check-value': 'checkChanged',
'change .operation-name': 'operationChanged',
'change .operation-operation': 'operationChanged',
'click .button-reset': 'reset',
'click .button-save': 'save',
'click .button-add': 'add',
'click .button-delete': 'delete',
'click .button-delete-check': 'deleteCheck'
},
originalModel: null,
hasChanged: false,
message: '',
errorMessage: '',
saving: false,
groups: [],
template: function(vars) {
return OperationTemplate(_.extend(
{
shortRuleDescTXT: t('workflowengine', 'Short rule description'),
addRuleTXT: t('workflowengine', 'Add rule'),
resetTXT: t('workflowengine', 'Reset'),
saveTXT: t('workflowengine', 'Save'),
savingTXT: t('workflowengine', 'Saving…')
},
vars
));
},
initialize: function() {
// this creates a new copy of the object to definitely have a new reference and being able to reset the model
this.originalModel = JSON.parse(JSON.stringify(this.model));
this.model.on('change', function() {
console.log('model changed');
this.hasChanged = true;
this.render();
}, this);
if (this.model.get('id') === undefined) {
this.hasChanged = true;
}
var self = this;
$.ajax({
url: OC.linkToOCS('cloud/groups', 2) + 'details',
dataType: 'json',
quietMillis: 100,
}).success(function(data) {
if (data.ocs.data.groups && data.ocs.data.groups.length > 0) {
data.ocs.data.groups.forEach(function(group) {
self.groups.push({ id: group.id, displayname: group.displayname });
});
self.render();
} else {
OC.Notification.error(t('workflowengine', 'Group list is empty'), { type: 'error' });
console.log(data);
}
}).error(function(data) {
OC.Notification.error(t('workflowengine', 'Unable to retrieve the group list'), { type: 'error' });
console.log(data);
});
},
delete: function() {
if (OC.PasswordConfirmation.requiresPasswordConfirmation()) {
OC.PasswordConfirmation.requirePasswordConfirmation(_.bind(this.delete, this));
return;
}
this.model.destroy();
this.remove();
},
reset: function() {
this.hasChanged = false;
// silent is need to not trigger the change event which resets the hasChanged attribute
this.model.set(this.originalModel, { silent: true });
this.render();
},
save: function() {
if (OC.PasswordConfirmation.requiresPasswordConfirmation()) {
OC.PasswordConfirmation.requirePasswordConfirmation(_.bind(this.save, this));
return;
}
var success = function(model, response, options) {
this.saving = false;
this.originalModel = JSON.parse(JSON.stringify(this.model));
this.message = t('workflowengine', 'Saved');
this.errorMessage = '';
this.render();
};
var error = function(model, response, options) {
this.saving = false;
this.hasChanged = true;
this.message = t('workflowengine', 'Saving failed:');
this.errorMessage = response.responseText;
this.render();
};
this.hasChanged = false;
this.saving = true;
this.render();
this.model.save(null, { success: success, error: error, context: this });
},
add: function() {
var checks = _.clone(this.model.get('checks')),
classname = OCA.WorkflowEngine.availableChecks[0]['class'],
operators = OCA.WorkflowEngine.availableChecks[0]['operators'];
checks.push({
'class': classname,
'operator': operators[0]['operator'],
'value': ''
});
this.model.set({ 'checks': checks });
},
checkChanged: function(event) {
var value = event.target.value,
id = $(event.target.parentElement).data('id'),
// this creates a new copy of the object to definitely have a new reference
checks = JSON.parse(JSON.stringify(this.model.get('checks'))),
key = null;
for (var i = 0; i < event.target.classList.length; i++) {
var className = event.target.classList[i];
if (className.substr(0, 'check-'.length) === 'check-') {
key = className.substr('check-'.length);
break;
}
}
if (key === null) {
console.warn('checkChanged triggered but element doesn\'t have any "check-" class');
return;
}
if (!_.has(checks[id], key)) {
console.warn('key "' + key + '" is not available in check', check);
return;
}
checks[id][key] = value;
// if the class is changed most likely also the operators have changed
// with this we set the operator to the first possible operator
if (key === 'class') {
var check = OCA.WorkflowEngine.getCheckByClass(value);
if (!_.isUndefined(check)) {
checks[id]['operator'] = check['operators'][0]['operator'];
checks[id]['value'] = '';
}
}
// model change will trigger render
this.model.set({ 'checks': checks });
},
deleteCheck: function(event) {
console.log(arguments);
var id = $(event.target.parentElement).data('id'),
checks = JSON.parse(JSON.stringify(this.model.get('checks')));
// splice removes 1 element at index `id`
checks.splice(id, 1);
// model change will trigger render
this.model.set({ 'checks': checks });
},
operationChanged: function(event) {
var value = event.target.value,
key = null;
for (var i = 0; i < event.target.classList.length; i++) {
var className = event.target.classList[i];
if (className.substr(0, 'operation-'.length) === 'operation-') {
key = className.substr('operation-'.length);
break;
}
}
if (key === null) {
console.warn('operationChanged triggered but element doesn\'t have any "operation-" class');
return;
}
if (key !== 'name' && key !== 'operation') {
console.warn('key "' + key + '" is no valid attribute');
return;
}
// model change will trigger render
this.model.set(key, value);
},
render: function() {
this.$el.html(this.template({
operation: this.model.toJSON(),
classes: OCA.WorkflowEngine.availableChecks,
hasChanged: this.hasChanged,
message: this.message,
errorMessage: this.errorMessage,
saving: this.saving
}));
var checks = this.model.get('checks');
_.each(this.$el.find('.check'), function(element) {
var $element = $(element),
id = $element.data('id'),
check = checks[id],
valueElement = $element.find('.check-value').first();
var self = this;
_.each(OCA.WorkflowEngine.availablePlugins, function(plugin) {
if (_.isFunction(plugin.render)) {
plugin.render(valueElement, check, self.groups);
}
});
}, this);
if (this.message !== '') {
// hide success messages after some time
_.delay(function(elements) {
$(elements).css('opacity', 0);
}, 7000, this.$el.find('.msg.success'));
this.message = '';
}
return this.$el;
}
});
/**
* @class OCA.WorkflowEngine.OperationsView
*
* this creates the view for configured operations
*/
OCA.WorkflowEngine.OperationsView =
OC.Backbone.View.extend({
templateId: '#operations-template',
collection: null,
$el: null,
events: {
'click .button-add-operation': 'add'
},
template: function(vars) {
return OperationsTemplate(_.extend(
{
addRuleGroupTXT: t('workflowengine', 'Add rule group')
},
vars
));
},
initialize: function(classname) {
if (!OCA.WorkflowEngine.availablePlugins.length) {
OCA.WorkflowEngine.availablePlugins = OC.Plugins.getPlugins('OCA.WorkflowEngine.CheckPlugins');
_.each(OCA.WorkflowEngine.availablePlugins, function(plugin) {
if (_.isFunction(plugin.getCheck)) {
OCA.WorkflowEngine.availableChecks.push(plugin.getCheck(classname));
}
});
}
this.collection.fetch({
data: {
'class': classname
}
});
this.collection.once('sync', this.render, this);
},
add: function() {
var operation = this.collection.create();
this.renderOperation(operation);
},
renderOperation: function(subView) {
var operationsElement = this.$el.find('.operations');
operationsElement.append(subView.$el);
subView.render();
},
render: function() {
this.$el.html(this.template());
this.collection.each(this.renderOperation, this);
}
});
})();

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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
}

View File

@ -0,0 +1,4 @@
import MultiselectTag from './MultiselectTag'
export default MultiselectTag
export { MultiselectTag }

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

@ -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]

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -0,0 +1,11 @@
.multiselect::v-deep .multiselect__single {
display: flex;
}
.option__icon {
min-width: 25px;
}
input, .multiselect {
width: 100%;
}

View File

@ -1,72 +0,0 @@
/**
* @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com>
*
* @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/>.
*
*/
(function() {
OCA.WorkflowEngine = OCA.WorkflowEngine || {};
OCA.WorkflowEngine.Plugins = OCA.WorkflowEngine.Plugins || {};
OCA.WorkflowEngine.Plugins.FileMimeTypePlugin = {
getCheck: function() {
return {
'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')}
]
};
},
render: function(element, check) {
if (check['class'] !== 'OCA\\WorkflowEngine\\Check\\FileMimeType') {
return;
}
var placeholder = 'text/plain';
if (check['operator'] === 'matches' || check['operator'] === '!matches') {
placeholder = '/^text\\/(plain|html)$/i';
if (this._validateRegex(check['value'])) {
$(element).removeClass('invalid-input');
} else {
$(element).addClass('invalid-input');
}
}
$(element).css('width', '250px')
.attr('placeholder', placeholder)
.attr('title', t('workflowengine', 'Example: {placeholder}', {placeholder: placeholder}))
.addClass('has-tooltip')
.tooltip({
placement: 'bottom'
});
},
_validateRegex: function(string) {
var regexRegex = /^\/(.*)\/([gui]{0,3})$/,
result = regexRegex.exec(string);
return result !== null;
}
};
})();
OC.Plugins.register('OCA.WorkflowEngine.CheckPlugins', OCA.WorkflowEngine.Plugins.FileMimeTypePlugin);

View File

@ -1,78 +0,0 @@
/**
* @copyright Copyright (c) 2018 Daniel Kesselberg <mail@danielkesselberg.de>
*
* @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/>.
*
*/
(function () {
OCA.WorkflowEngine = OCA.WorkflowEngine || {};
OCA.WorkflowEngine.Plugins = OCA.WorkflowEngine.Plugins || {};
OCA.WorkflowEngine.Plugins.FileNamePlugin = {
getCheck: function () {
return {
'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')
}
]
};
},
render: function (element, check) {
if (check['class'] !== 'OCA\\WorkflowEngine\\Check\\FileName') {
return;
}
var placeholder = 'dummy.jpg';
if (check['operator'] === 'matches' || check['operator'] === '!matches') {
placeholder = '/^dummy-.+$/i';
if (this._validateRegex(check['value'])) {
$(element).removeClass('invalid-input');
} else {
$(element).addClass('invalid-input');
}
}
$(element).css('width', '250px')
.attr('placeholder', placeholder)
.attr('title', t('workflowengine', 'Example: {placeholder}', {placeholder: placeholder}))
.addClass('has-tooltip')
.tooltip({
placement: 'bottom'
});
},
_validateRegex: function (string) {
var regexRegex = /^\/(.*)\/([gui]{0,3})$/,
result = regexRegex.exec(string);
return result !== null;
}
};
})();
OC.Plugins.register('OCA.WorkflowEngine.CheckPlugins', OCA.WorkflowEngine.Plugins.FileNamePlugin);

View File

@ -1,56 +0,0 @@
/**
* @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com>
*
* @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/>.
*
*/
(function() {
OCA.WorkflowEngine = OCA.WorkflowEngine || {};
OCA.WorkflowEngine.Plugins = OCA.WorkflowEngine.Plugins || {};
OCA.WorkflowEngine.Plugins.FileSizePlugin = {
getCheck: function() {
return {
'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')}
]
};
},
render: function(element, check) {
if (check['class'] !== 'OCA\\WorkflowEngine\\Check\\FileSize') {
return;
}
var placeholder = '12 MB'; // Do not translate!!!
$(element).css('width', '250px')
.attr('placeholder', placeholder)
.attr('title', t('workflowengine', 'Example: {placeholder}', {placeholder: placeholder}))
.addClass('has-tooltip')
.tooltip({
placement: 'bottom'
});
}
};
})();
OC.Plugins.register('OCA.WorkflowEngine.CheckPlugins', OCA.WorkflowEngine.Plugins.FileSizePlugin);

View File

@ -1,78 +0,0 @@
/**
* @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com>
*
* @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/>.
*
*/
(function() {
OCA.WorkflowEngine = OCA.WorkflowEngine || {};
OCA.WorkflowEngine.Plugins = OCA.WorkflowEngine.Plugins || {};
OCA.WorkflowEngine.Plugins.FileSystemTagsPlugin = {
getCheck: function() {
this.collection = OC.SystemTags.collection;
return {
'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')}
]
};
},
render: function(element, check) {
if (check['class'] !== 'OCA\\WorkflowEngine\\Check\\FileSystemTags') {
return;
}
$(element).css('width', '400px');
$(element).select2({
allowClear: false,
multiple: false,
placeholder: t('workflowengine', 'Select tag…'),
query: _.debounce(function(query) {
query.callback({
results: OC.SystemTags.collection.filterByName(query.term)
});
}, 100, true),
id: function(element) {
return element.get('id');
},
initSelection: function(element, callback) {
callback($(element).val());
},
formatResult: function (tag) {
return OC.SystemTags.getDescriptiveTag(tag);
},
formatSelection: function (tagId) {
var tag = OC.SystemTags.collection.get(tagId);
if (!_.isUndefined(tag)) {
return OC.SystemTags.getDescriptiveTag(tag);
}
},
escapeMarkup: function(m) {
return m;
}
});
}
};
})();
OC.Plugins.register('OCA.WorkflowEngine.CheckPlugins', OCA.WorkflowEngine.Plugins.FileSystemTagsPlugin);

View File

@ -1,7 +0,0 @@
module.exports = function(classname) {
var check = OCA.WorkflowEngine.getCheckByClass(classname);
if (!_.isUndefined(check)) {
return check['operators'];
}
return [];
}

View File

@ -1,7 +0,0 @@
module.exports = function(currentValue, itemValue) {
if (currentValue === itemValue) {
return 'selected="selected"';
}
return "";
}

View File

@ -0,0 +1,30 @@
/*
* @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/>.
*
*/
const getApiUrl = (url) => {
const scopeValue = OCP.InitialState.loadState('workflowengine', 'scope') === 0 ? 'global' : 'user'
return OC.linkToOCS('apps/workflowengine/api/v1/workflows', 2) + scopeValue + url + '?format=json'
}
export {
getApiUrl
}

View File

@ -0,0 +1,48 @@
/*
* @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/>.
*
*/
const validateRegex = function(string) {
var regexRegex = /^\/(.*)\/([gui]{0,3})$/
var result = regexRegex.exec(string)
return result !== 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
}
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
}
const stringValidator = (check) => {
if (check.operator === 'matches' || check.operator === '!matches') {
return validateRegex(check.value)
}
return true
}
export { validateRegex, stringValidator, validateIPv4, validateIPv6 }

View File

@ -0,0 +1,54 @@
/*
* @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/>.
*
*/
const valueMixin = {
props: {
value: {
type: String,
default: ''
},
check: {
type: Object,
default: () => { return {} }
}
},
data() {
return {
newValue: ''
}
},
watch: {
value: {
immediate: true,
handler: function(value) {
this.updateInternalValue(value)
}
}
},
methods: {
updateInternalValue(value) {
this.newValue = value
}
}
}
export default valueMixin

View File

@ -1,83 +0,0 @@
/**
* @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com>
*
* @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/>.
*
*/
(function() {
OCA.WorkflowEngine = OCA.WorkflowEngine || {};
OCA.WorkflowEngine.Plugins = OCA.WorkflowEngine.Plugins || {};
OCA.WorkflowEngine.Plugins.RequestRemoteAddressPlugin = {
getCheck: function() {
return {
'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')}
]
};
},
render: function(element, check) {
if (check['class'] !== 'OCA\\WorkflowEngine\\Check\\RequestRemoteAddress') {
return;
}
var placeholder = '127.0.0.1/32'; // Do not translate!!!
if (check['operator'] === 'matchesIPv6' || check['operator'] === '!matchesIPv6') {
placeholder = '::1/128'; // Do not translate!!!
if (this._validateIPv6(check['value'])) {
$(element).removeClass('invalid-input');
} else {
$(element).addClass('invalid-input');
}
} else {
if (this._validateIPv4(check['value'])) {
$(element).removeClass('invalid-input');
} else {
$(element).addClass('invalid-input');
}
}
$(element).css('width', '300px')
.attr('placeholder', placeholder)
.attr('title', t('workflowengine', 'Example: {placeholder}', {placeholder: placeholder}))
.addClass('has-tooltip')
.tooltip({
placement: 'bottom'
});
},
_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])$/,
result = regexRegex.exec(string);
return result !== null;
},
_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])$/,
result = regexRegex.exec(string);
return result !== null;
}
};
})();
OC.Plugins.register('OCA.WorkflowEngine.CheckPlugins', OCA.WorkflowEngine.Plugins.RequestRemoteAddressPlugin);

View File

@ -1,196 +0,0 @@
/**
* @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com>
*
* @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/>.
*
*/
(function() {
OCA.WorkflowEngine = OCA.WorkflowEngine || {};
OCA.WorkflowEngine.Plugins = OCA.WorkflowEngine.Plugins || {};
OCA.WorkflowEngine.Plugins.RequestTimePlugin = {
timezones: [
"Europe/Berlin",
"Europe/London"
],
_$element: null,
getCheck: function() {
return {
'class': 'OCA\\WorkflowEngine\\Check\\RequestTime',
'name': t('workflowengine', 'Request time'),
'operators': [
{'operator': 'in', 'name': t('workflowengine', 'between')},
{'operator': '!in', 'name': t('workflowengine', 'not between')}
]
};
},
render: function(element, check) {
if (check['class'] !== 'OCA\\WorkflowEngine\\Check\\RequestTime') {
return;
}
var startTime = '09:00',
endTime = '18:00',
timezone = jstz.determine().name(),
$element = $(element);
if (_.isString(check['value']) && check['value'] !== '') {
var value = JSON.parse(check['value']),
splittedStart = value[0].split(' ', 2),
splittedEnd = value[1].split(' ', 2);
startTime = splittedStart[0];
endTime = splittedEnd[0];
timezone = splittedStart[1];
}
var valueJSON = JSON.stringify([startTime + ' ' + timezone, endTime + ' ' + timezone]);
if (check['value'] !== valueJSON) {
check['value'] = valueJSON;
$element.val(valueJSON);
}
$element.css('display', 'none');
$('<input>')
.attr('type', 'text')
.attr('placeholder', t('workflowengine', 'Start'))
.attr('title', t('workflowengine', 'Example: {placeholder}', {placeholder: '16:00'}))
.addClass('has-tooltip')
.tooltip({
placement: 'bottom'
})
.addClass('start')
.val(startTime)
.insertBefore($element);
$('<input>')
.attr('type', 'text')
.attr('placeholder', t('workflowengine', 'End'))
.attr('title', t('workflowengine', 'Example: {placeholder}', {placeholder: '16:00'}))
.addClass('has-tooltip')
.tooltip({
placement: 'bottom'
})
.addClass('end')
.val(endTime)
.insertBefore($element);
var timezoneInput = $('<input>')
.attr('type', 'hidden')
.css('width', '250px')
.insertBefore($element)
.val(timezone);
timezoneInput.select2({
allowClear: false,
multiple: false,
placeholder: t('workflowengine', 'Select timezone…'),
ajax: {
url: OC.generateUrl('apps/workflowengine/timezones'),
dataType: 'json',
quietMillis: 100,
data: function (term) {
if (term === '') {
// Default search in the same continent...
term = jstz.determine().name().split('/');
term = term[0];
}
return {
search: term
};
},
results: function (response) {
var results = [];
$.each(response, function(timezone) {
results.push({ id: timezone });
});
return {
results: results,
more: false
};
}
},
initSelection: function (element, callback) {
callback(element.val());
},
formatResult: function (element) {
return '<span>' + element.id + '</span>';
},
formatSelection: function (element) {
if (!_.isUndefined(element.id)) {
element = element.id;
}
return '<span>' + element + '</span>';
}
});
// Has to be added after select2 for `event.target.classList`
timezoneInput.addClass('timezone');
$element.parent()
.on('change', '.start', _.bind(this.update, this))
.on('change', '.end', _.bind(this.update, this))
.on('change', '.timezone', _.bind(this.update, this));
this._$element = $element;
},
update: function(event) {
var value = event.target.value,
key = null;
for (var i = 0; i < event.target.classList.length; i++) {
key = event.target.classList[i];
}
if (key === null) {
console.warn('update triggered but element doesn\'t have any class');
return;
}
var data = JSON.parse(this._$element.val()),
startTime = moment(data[0].split(' ', 2)[0], 'H:m Z'),
endTime = moment(data[1].split(' ', 2)[0], 'H:m Z'),
timezone = data[0].split(' ', 2)[1];
if (key === 'start' || key === 'end') {
var parsedDate = moment(value, ['H:m', 'h:m a'], true).format('HH:mm');
if (parsedDate === 'Invalid date') {
return;
}
var indexValue = 0;
if (key === 'end') {
indexValue = 1;
}
data[indexValue] = parsedDate + ' ' + timezone;
}
if (key === 'timezone') {
data[0] = startTime.format('HH:mm') + ' ' + value;
data[1] = endTime.format('HH:mm') + ' ' + value;
}
this._$element.val(JSON.stringify(data));
this._$element.trigger('change');
}
};
})();
OC.Plugins.register('OCA.WorkflowEngine.CheckPlugins', OCA.WorkflowEngine.Plugins.RequestTimePlugin);

View File

@ -1,117 +0,0 @@
/**
* @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com>
*
* @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/>.
*
*/
(function() {
OCA.WorkflowEngine = OCA.WorkflowEngine || {};
OCA.WorkflowEngine.Plugins = OCA.WorkflowEngine.Plugins || {};
OCA.WorkflowEngine.Plugins.RequestURLPlugin = {
predefinedValues: ['webdav'],
getCheck: function() {
return {
'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')}
]
};
},
render: function(element, check) {
if (check['class'] !== 'OCA\\WorkflowEngine\\Check\\RequestURL') {
return;
}
var placeholder = 'https://localhost/index.php';
if (check['operator'] === 'matches' || check['operator'] === '!matches') {
placeholder = '/^https\\:\\/\\/localhost\\/index\\.php$/i';
}
$(element).css('width', '250px')
.attr('placeholder', placeholder)
.attr('title', t('workflowengine', 'Example: {placeholder}', {placeholder: placeholder}))
.addClass('has-tooltip')
.tooltip({
placement: 'bottom'
});
if (check['operator'] === 'matches' || check['operator'] === '!matches') {
if (this._validateRegex(check['value'])) {
$(element).removeClass('invalid-input');
} else {
$(element).addClass('invalid-input');
}
} else {
var self = this,
data = [
{
text: t('workflowengine', 'Predefined URLs'),
children: [
{id: 'webdav', text: t('workflowengine', 'Files WebDAV')}
]
}
];
if (this.predefinedValues.indexOf(check['value']) === -1) {
data.unshift({
id: check['value'],
text: check['value']
})
}
$(element).select2({
data: data,
createSearchChoice: function(term) {
if (self.predefinedValues.indexOf(check['value']) === -1) {
return {
id: term,
text: term
};
}
},
id: function(element) {
return element.id;
},
formatResult: function (tag) {
return tag.text;
},
formatSelection: function (tag) {
return tag.text;
},
escapeMarkup: function(m) {
return m;
}
})
}
},
_validateRegex: function(string) {
var regexRegex = /^\/(.*)\/([gui]{0,3})$/,
result = regexRegex.exec(string);
return result !== null;
}
};
})();
OC.Plugins.register('OCA.WorkflowEngine.CheckPlugins', OCA.WorkflowEngine.Plugins.RequestURLPlugin);

View File

@ -1,119 +0,0 @@
/**
* @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com>
*
* @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/>.
*
*/
(function() {
OCA.WorkflowEngine = OCA.WorkflowEngine || {};
OCA.WorkflowEngine.Plugins = OCA.WorkflowEngine.Plugins || {};
OCA.WorkflowEngine.Plugins.RequestUserAgentPlugin = {
predefinedValues: ['android', 'ios', 'desktop'],
getCheck: function() {
return {
'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')}
]
};
},
render: function(element, check) {
if (check['class'] !== 'OCA\\WorkflowEngine\\Check\\RequestUserAgent') {
return;
}
var placeholder = 'Mozilla/5.0 User Agent';
if (check.operator === 'matches' || check.operator === '!matches') {
placeholder = '/^Mozilla\\/5\\.0 (.*)$/i';
}
$(element).css('width', '250px')
.attr('placeholder', placeholder)
.attr('title', t('workflowengine', 'Example: {placeholder}', {placeholder: placeholder}))
.addClass('has-tooltip')
.tooltip({
placement: 'bottom'
});
if (check.operator === 'matches' || check.operator === '!matches') {
if (this._validateRegex(check.value)) {
$(element).removeClass('invalid-input');
} else {
$(element).addClass('invalid-input');
}
} else {
var self = this,
data = [
{
text: t('workflowengine', 'Sync clients'),
children: [
{id: 'android', text: t('workflowengine', 'Android client')},
{id: 'ios', text: t('workflowengine', 'iOS client')},
{id: 'desktop', text: t('workflowengine', 'Desktop client')},
{id: 'mail', text: t('workflowengine', 'Thunderbird & Outlook addons')}
]
}
];
if (this.predefinedValues.indexOf(check.value) === -1) {
data.unshift({
id: check.value,
text: check.value
});
}
$(element).select2({
data: data,
createSearchChoice: function(term) {
if (self.predefinedValues.indexOf(check.value) === -1) {
return {
id: term,
text: term
};
}
},
id: function(element) {
return element.id;
},
formatResult: function (tag) {
return tag.text;
},
formatSelection: function (tag) {
return tag.text;
},
escapeMarkup: function(m) {
return m;
}
})
}
},
_validateRegex: function(string) {
var regexRegex = /^\/(.*)\/([gui]{0,3})$/,
result = regexRegex.exec(string);
return result !== null;
}
};
})();
OC.Plugins.register('OCA.WorkflowEngine.CheckPlugins', OCA.WorkflowEngine.Plugins.RequestUserAgentPlugin);

View File

@ -0,0 +1,164 @@
/*
* @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 Vue from 'vue'
import Vuex from 'vuex'
import axios from 'nextcloud-axios'
import { getApiUrl } from './helpers/api'
import confirmPassword from 'nextcloud-password-confirmation'
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
rules: [],
scope: OCP.InitialState.loadState('workflowengine', 'scope'),
operations: OCP.InitialState.loadState('workflowengine', 'operators'),
plugins: Vue.observable({
checks: {},
operators: {}
}),
entities: OCP.InitialState.loadState('workflowengine', 'entities'),
events: OCP.InitialState.loadState('workflowengine', 'entities')
.map((entity) => entity.events.map(event => {
return {
id: `${entity.id}::${event.eventName}`,
entity,
...event
}
})).flat(),
checks: OCP.InitialState.loadState('workflowengine', 'checks')
},
mutations: {
addRule(state, rule) {
state.rules.push({ ...rule, valid: true })
},
updateRule(state, rule) {
const index = state.rules.findIndex((item) => rule.id === item.id)
const newRule = Object.assign({}, rule)
Vue.set(state.rules, index, newRule)
},
removeRule(state, rule) {
const index = state.rules.findIndex((item) => rule.id === item.id)
state.rules.splice(index, 1)
},
addPluginCheck(state, plugin) {
Vue.set(state.plugins.checks, plugin.class, plugin)
},
addPluginOperator(state, plugin) {
plugin = Object.assign(
{ color: 'var(--color-primary-element)' },
plugin, state.operations[plugin.id] || {})
Vue.set(state.operations, plugin.id, plugin)
}
},
actions: {
async fetchRules(context) {
const { data } = await axios.get(getApiUrl(''))
Object.values(data.ocs.data).flat().forEach((rule) => {
context.commit('addRule', rule)
})
},
createNewRule(context, rule) {
let entity = null
let events = []
if (rule.isComplex === false && rule.fixedEntity === '') {
entity = context.state.entities.find((item) => rule.entities && rule.entities[0] === item.id)
entity = entity || Object.values(context.state.entities)[0]
events = [entity.events[0].eventName]
}
context.commit('addRule', {
id: -(new Date().getTime()),
class: rule.id,
entity: entity ? entity.id : rule.fixedEntity,
events,
name: '', // unused in the new ui, there for legacy reasons
checks: [],
operation: rule.operation || ''
})
},
updateRule(context, rule) {
context.commit('updateRule', {
...rule,
events: typeof rule.events === 'string' ? JSON.parse(rule.events) : rule.events
})
},
removeRule(context, rule) {
context.commit('removeRule', rule)
},
async pushUpdateRule(context, rule) {
await confirmPassword()
let result
if (rule.id < 0) {
result = await axios.post(getApiUrl(''), rule)
} else {
result = await axios.put(getApiUrl(`/${rule.id}`), rule)
}
Vue.set(rule, 'id', result.data.ocs.data.id)
context.commit('updateRule', rule)
},
async deleteRule(context, rule) {
await confirmPassword()
await axios.delete(getApiUrl(`/${rule.id}`))
context.commit('removeRule', rule)
},
setValid(context, { rule, valid }) {
rule.valid = valid
context.commit('updateRule', rule)
}
},
getters: {
getRules(state) {
return state.rules.sort((rule1, rule2) => {
return rule1.id - rule2.id || rule2.class - rule1.class
})
},
getOperationForRule(state) {
return (rule) => state.operations[rule.class]
},
getEntityForOperation(state) {
return (operation) => state.entities.find((entity) => operation.fixedEntity === entity.id)
},
getEventsForOperation(state) {
return (operation) => state.events
},
/**
* Return all available checker plugins for a given entity class
*/
getChecksForEntity(state) {
return (entity) => {
return state.checks
.filter((check) => check.supportedEntities.indexOf(entity) > -1 || check.supportedEntities.length === 0)
.map((check) => state.plugins.checks[check.id])
.reduce((obj, item) => {
obj[item.class] = item
return obj
}, {})
}
}
}
})
export default store

View File

@ -1,45 +0,0 @@
<div class="operation{{#if hasChanged}} modified{{/if}}">
<div class="operation-header">
<input type="text" class="operation-name" placeholder="{{shortRuleDescTXT}}" value="{{operation.name}}" />
<input type="text" class="operation-operation" value="{{operation.operation}}" />
{{! delete only makes sense if the operation is already saved }}
{{#if operation.id}}
<span class="button-delete icon-delete"></span>
{{/if}}
</div>
<div class="checks">
{{#each operation.checks}}
<div class="check" data-id="{{@index}}">
<select class="check-class">
{{#each ../classes}}
<option value="{{class}}" {{{selectItem class ../class}}}>{{name}}</option>
{{/each}}
</select>
<select class="check-operator">
{{#each (getOperators class)}}
<option value="{{operator}}" {{{selectItem operator ../operator}}}>{{name}}</option>
{{/each}}
</select>
<input type="text" class="check-value" value="{{value}}">
<span class="button-delete-check icon-delete"></span>
</div>
{{/each}}
</div>
<button class="button-add">{{addRuleTXT}}</button>
{{#if hasChanged}}
{{! reset only makes sense if the operation is already saved }}
{{#if operation.id}}
<button class="button-reset pull-right">{{resetTXT}}</button>
{{/if}}
<button class="button-save pull-right">{{saveTXT}}</button>
{{/if}}
{{#if saving}}
<span class="icon-loading-small pull-right"></span>
<span class="pull-right">{{savingTXT}}</span>
{{else}}{{#if message}}
<span class="msg pull-right {{#if errorMessage}}error{{else}}success{{/if}}">
{{message}}{{#if errorMessage}} {{errorMessage}}{{/if}}
</span>
{{/if}}{{/if}}
</div>

View File

@ -1,2 +0,0 @@
<div class="operations"></div>
<button class="button-add-operation">{{addRuleGroupTXT}}</button>

View File

@ -1,75 +0,0 @@
/**
* @copyright Copyright (c) 2016 Morris Jobke <hey@morrisjobke.de>
*
* @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/>.
*
*/
(function() {
OCA.WorkflowEngine = OCA.WorkflowEngine || {};
OCA.WorkflowEngine.Plugins = OCA.WorkflowEngine.Plugins || {};
OCA.WorkflowEngine.Plugins.UserGroupMembershipPlugin = {
getCheck: function() {
return {
'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')}
]
};
},
render: function(element, check, groups) {
if (check['class'] !== 'OCA\\WorkflowEngine\\Check\\UserGroupMembership') {
return;
}
$(element).css('width', '400px');
$(element).select2({
data: { results: groups, text: 'displayname' },
initSelection: function (element, callback) {
var groupId = element.val();
if (groupId && groups.length > 0) {
callback({
id: groupId,
displayname: groups.find(function (group) {
return group.id === groupId;
}).displayname
});
} else if (groupId) {
callback({
id: groupId,
displayname: groupId
});
} else {
callback();
}
},
formatResult: function (element) {
return '<span>' + escapeHTML(element.displayname) + '</span>';
},
formatSelection: function (element) {
return '<span title="'+escapeHTML(element.id)+'">'+escapeHTML(element.displayname)+'</span>';
}
});
}
};
})();
OC.Plugins.register('OCA.WorkflowEngine.CheckPlugins', OCA.WorkflowEngine.Plugins.UserGroupMembershipPlugin);

View File

@ -1,12 +1,70 @@
import './admin'
import './filemimetypeplugin'
import './filenameplugin'
import './filesizeplugin'
import './filesystemtagsplugin'
import './requestremoteaddressplugin'
import './requesttimeplugin'
import './requesturlplugin'
import './requestuseragentplugin'
import './usergroupmembershipplugin'
import Vue from 'vue'
import Vuex from 'vuex'
import store from './store'
import Settings from './components/Workflow'
import ShippedChecks from './components/Checks'
window.OCA.WorkflowEngine = OCA.WorkflowEngine
/**
* A plugin for displaying a custom value field for checks
*
* @typedef {Object} CheckPlugin
* @property {string} class - The PHP class name of the check
* @property {Comparison[]} operators - A list of possible comparison operations running on the check
* @property {Vue} component - A vue component to handle the rendering of options
* The component should handle the v-model directive properly,
* so it needs a value property to receive data and emit an input
* event once the data has changed
* @property {callable} placeholder - Return a placeholder of no custom component is used
* @property {callable} validate - validate a check if no custom component is used
**/
/**
* A plugin for extending the admin page repesentation of a operator
*
* @typedef {Object} OperatorPlugin
* @property {string} id - The PHP class name of the check
* @property {string} operation - Default value for the operation field
* @property {string} color - Custom color code to be applied for the operator selector
* @property {Vue} component - A vue component to handle the rendering of options
* The component should handle the v-model directive properly,
* so it needs a value property to receive data and emit an input
* event once the data has changed
*/
/**
* @typedef {Object} Comparison
* @property {string} operator - value the comparison should have, e.g. !less, greater
* @property {string} name - Translated readable text, e.g. less or equals
**/
/**
* Public javascript api for apps to register custom plugins
*/
window.OCA.WorkflowEngine = Object.assign({}, OCA.WorkflowEngine, {
/**
*
* @param {CheckPlugin} Plugin
*/
registerCheck: function (Plugin) {
store.commit('addPluginCheck', Plugin)
},
/**
*
* @param {OperatorPlugin} Plugin
*/
registerOperator: function (Plugin) {
store.commit('addPluginOperator', Plugin)
}
})
// Register shipped checks
ShippedChecks.forEach((checkPlugin) => window.OCA.WorkflowEngine.registerCheck(checkPlugin))
Vue.use(Vuex)
Vue.prototype.t = t
const View = Vue.extend(Settings)
new View({
store
}).$mount('#workflowengine')

View File

@ -22,4 +22,4 @@
/** @var array $_ */
/** @var \OCP\IL10N $l */
?>
<div id="<?php p($_['appid']); ?>" class="<? p(\OCA\WorkflowEngine\AppInfo\Application::APP_ID); ?>"></div>
<div id="<?php p(\OCA\WorkflowEngine\AppInfo\Application::APP_ID); ?>"></div>

View File

@ -7,17 +7,5 @@ module.exports = {
publicPath: '/js/',
filename: 'workflowengine.js',
jsonpFunction: 'webpackJsonpWorkflowengine'
},
module: {
rules: [
{
test: /\.handlebars/,
loader: "handlebars-loader",
query: {
extensions: '.handlebars',
helperDirs: path.join(__dirname, 'src/hbs_helpers'),
}
}
]
}
}

336
package-lock.json generated
View File

@ -5,9 +5,9 @@
"requires": true,
"dependencies": {
"@babel/code-frame": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz",
"integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==",
"version": "7.5.5",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.5.5.tgz",
"integrity": "sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw==",
"dev": true,
"requires": {
"@babel/highlight": "^7.0.0"
@ -35,73 +35,12 @@
"source-map": "^0.5.0"
},
"dependencies": {
"@babel/code-frame": {
"version": "7.5.5",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.5.5.tgz",
"integrity": "sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw==",
"dev": true,
"requires": {
"@babel/highlight": "^7.0.0"
}
},
"@babel/generator": {
"version": "7.6.0",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.6.0.tgz",
"integrity": "sha512-Ms8Mo7YBdMMn1BYuNtKuP/z0TgEIhbcyB8HVR6PPNYp4P61lMsABiS4A3VG1qznjXVCf3r+fVHhm4efTYVsySA==",
"dev": true,
"requires": {
"@babel/types": "^7.6.0",
"jsesc": "^2.5.1",
"lodash": "^4.17.13",
"source-map": "^0.5.0",
"trim-right": "^1.0.1"
}
},
"@babel/parser": {
"version": "7.6.0",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.6.0.tgz",
"integrity": "sha512-+o2q111WEx4srBs7L9eJmcwi655eD8sXniLqMB93TBK9GrNzGrxDWSjiqz2hLU0Ha8MTXFIP0yd9fNdP+m43ZQ==",
"dev": true
},
"@babel/template": {
"version": "7.6.0",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.6.0.tgz",
"integrity": "sha512-5AEH2EXD8euCk446b7edmgFdub/qfH1SN6Nii3+fyXP807QRx9Q73A2N5hNwRRslC2H9sNzaFhsPubkS4L8oNQ==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.0.0",
"@babel/parser": "^7.6.0",
"@babel/types": "^7.6.0"
}
},
"@babel/traverse": {
"version": "7.6.0",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.6.0.tgz",
"integrity": "sha512-93t52SaOBgml/xY74lsmt7xOR4ufYvhb5c5qiM6lu4J/dWGMAfAh6eKw4PjLes6DI6nQgearoxnFJk60YchpvQ==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.5.5",
"@babel/generator": "^7.6.0",
"@babel/helper-function-name": "^7.1.0",
"@babel/helper-split-export-declaration": "^7.4.4",
"@babel/parser": "^7.6.0",
"@babel/types": "^7.6.0",
"debug": "^4.1.0",
"globals": "^11.1.0",
"lodash": "^4.17.13"
}
},
"@babel/types": {
"version": "7.6.1",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.6.1.tgz",
"integrity": "sha512-X7gdiuaCmA0uRjCmRtYJNAVCc/q+5xSgsfKJHqMN4iNLILX39677fJE1O40arPMh0TTtS9ItH67yre6c7k6t0g==",
"dev": true,
"requires": {
"esutils": "^2.0.2",
"lodash": "^4.17.13",
"to-fast-properties": "^2.0.0"
}
},
"debug": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
@ -132,17 +71,6 @@
"trim-right": "^1.0.1"
},
"dependencies": {
"@babel/types": {
"version": "7.6.1",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.6.1.tgz",
"integrity": "sha512-X7gdiuaCmA0uRjCmRtYJNAVCc/q+5xSgsfKJHqMN4iNLILX39677fJE1O40arPMh0TTtS9ItH67yre6c7k6t0g==",
"dev": true,
"requires": {
"esutils": "^2.0.2",
"lodash": "^4.17.13",
"to-fast-properties": "^2.0.0"
}
},
"source-map": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
@ -190,19 +118,6 @@
"@babel/helper-function-name": "^7.1.0",
"@babel/types": "^7.5.5",
"lodash": "^4.17.13"
},
"dependencies": {
"@babel/types": {
"version": "7.6.1",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.6.1.tgz",
"integrity": "sha512-X7gdiuaCmA0uRjCmRtYJNAVCc/q+5xSgsfKJHqMN4iNLILX39677fJE1O40arPMh0TTtS9ItH67yre6c7k6t0g==",
"dev": true,
"requires": {
"esutils": "^2.0.2",
"lodash": "^4.17.13",
"to-fast-properties": "^2.0.0"
}
}
}
},
"@babel/helper-explode-assignable-expression": {
@ -251,19 +166,6 @@
"dev": true,
"requires": {
"@babel/types": "^7.5.5"
},
"dependencies": {
"@babel/types": {
"version": "7.6.1",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.6.1.tgz",
"integrity": "sha512-X7gdiuaCmA0uRjCmRtYJNAVCc/q+5xSgsfKJHqMN4iNLILX39677fJE1O40arPMh0TTtS9ItH67yre6c7k6t0g==",
"dev": true,
"requires": {
"esutils": "^2.0.2",
"lodash": "^4.17.13",
"to-fast-properties": "^2.0.0"
}
}
}
},
"@babel/helper-module-imports": {
@ -287,19 +189,6 @@
"@babel/template": "^7.4.4",
"@babel/types": "^7.5.5",
"lodash": "^4.17.13"
},
"dependencies": {
"@babel/types": {
"version": "7.6.1",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.6.1.tgz",
"integrity": "sha512-X7gdiuaCmA0uRjCmRtYJNAVCc/q+5xSgsfKJHqMN4iNLILX39677fJE1O40arPMh0TTtS9ItH67yre6c7k6t0g==",
"dev": true,
"requires": {
"esutils": "^2.0.2",
"lodash": "^4.17.13",
"to-fast-properties": "^2.0.0"
}
}
}
},
"@babel/helper-optimise-call-expression": {
@ -349,19 +238,6 @@
"@babel/helper-optimise-call-expression": "^7.0.0",
"@babel/traverse": "^7.5.5",
"@babel/types": "^7.5.5"
},
"dependencies": {
"@babel/types": {
"version": "7.6.1",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.6.1.tgz",
"integrity": "sha512-X7gdiuaCmA0uRjCmRtYJNAVCc/q+5xSgsfKJHqMN4iNLILX39677fJE1O40arPMh0TTtS9ItH67yre6c7k6t0g==",
"dev": true,
"requires": {
"esutils": "^2.0.2",
"lodash": "^4.17.13",
"to-fast-properties": "^2.0.0"
}
}
}
},
"@babel/helper-simple-access": {
@ -404,92 +280,6 @@
"@babel/template": "^7.6.0",
"@babel/traverse": "^7.6.0",
"@babel/types": "^7.6.0"
},
"dependencies": {
"@babel/generator": {
"version": "7.6.0",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.6.0.tgz",
"integrity": "sha512-Ms8Mo7YBdMMn1BYuNtKuP/z0TgEIhbcyB8HVR6PPNYp4P61lMsABiS4A3VG1qznjXVCf3r+fVHhm4efTYVsySA==",
"dev": true,
"requires": {
"@babel/types": "^7.6.0",
"jsesc": "^2.5.1",
"lodash": "^4.17.13",
"source-map": "^0.5.0",
"trim-right": "^1.0.1"
}
},
"@babel/parser": {
"version": "7.6.0",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.6.0.tgz",
"integrity": "sha512-+o2q111WEx4srBs7L9eJmcwi655eD8sXniLqMB93TBK9GrNzGrxDWSjiqz2hLU0Ha8MTXFIP0yd9fNdP+m43ZQ==",
"dev": true
},
"@babel/template": {
"version": "7.6.0",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.6.0.tgz",
"integrity": "sha512-5AEH2EXD8euCk446b7edmgFdub/qfH1SN6Nii3+fyXP807QRx9Q73A2N5hNwRRslC2H9sNzaFhsPubkS4L8oNQ==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.0.0",
"@babel/parser": "^7.6.0",
"@babel/types": "^7.6.0"
}
},
"@babel/traverse": {
"version": "7.6.0",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.6.0.tgz",
"integrity": "sha512-93t52SaOBgml/xY74lsmt7xOR4ufYvhb5c5qiM6lu4J/dWGMAfAh6eKw4PjLes6DI6nQgearoxnFJk60YchpvQ==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.5.5",
"@babel/generator": "^7.6.0",
"@babel/helper-function-name": "^7.1.0",
"@babel/helper-split-export-declaration": "^7.4.4",
"@babel/parser": "^7.6.0",
"@babel/types": "^7.6.0",
"debug": "^4.1.0",
"globals": "^11.1.0",
"lodash": "^4.17.13"
},
"dependencies": {
"@babel/code-frame": {
"version": "7.5.5",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.5.5.tgz",
"integrity": "sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw==",
"dev": true,
"requires": {
"@babel/highlight": "^7.0.0"
}
}
}
},
"@babel/types": {
"version": "7.6.1",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.6.1.tgz",
"integrity": "sha512-X7gdiuaCmA0uRjCmRtYJNAVCc/q+5xSgsfKJHqMN4iNLILX39677fJE1O40arPMh0TTtS9ItH67yre6c7k6t0g==",
"dev": true,
"requires": {
"esutils": "^2.0.2",
"lodash": "^4.17.13",
"to-fast-properties": "^2.0.0"
}
},
"debug": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
"dev": true,
"requires": {
"ms": "^2.1.1"
}
},
"source-map": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
"integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
"dev": true
}
}
},
"@babel/highlight": {
@ -988,30 +778,25 @@
"invariant": "^2.2.2",
"js-levenshtein": "^1.1.3",
"semver": "^5.5.0"
},
"dependencies": {
"@babel/types": {
"version": "7.6.1",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.6.1.tgz",
"integrity": "sha512-X7gdiuaCmA0uRjCmRtYJNAVCc/q+5xSgsfKJHqMN4iNLILX39677fJE1O40arPMh0TTtS9ItH67yre6c7k6t0g==",
"dev": true,
"requires": {
"esutils": "^2.0.2",
"lodash": "^4.17.13",
"to-fast-properties": "^2.0.0"
}
}
}
},
"@babel/template": {
"version": "7.4.4",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.4.4.tgz",
"integrity": "sha512-CiGzLN9KgAvgZsnivND7rkA+AeJ9JB0ciPOD4U59GKbQP2iQl+olF1l76kJOupqidozfZ32ghwBEJDhnk9MEcw==",
"version": "7.6.0",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.6.0.tgz",
"integrity": "sha512-5AEH2EXD8euCk446b7edmgFdub/qfH1SN6Nii3+fyXP807QRx9Q73A2N5hNwRRslC2H9sNzaFhsPubkS4L8oNQ==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.0.0",
"@babel/parser": "^7.4.4",
"@babel/types": "^7.4.4"
"@babel/parser": "^7.6.0",
"@babel/types": "^7.6.0"
},
"dependencies": {
"@babel/parser": {
"version": "7.6.0",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.6.0.tgz",
"integrity": "sha512-+o2q111WEx4srBs7L9eJmcwi655eD8sXniLqMB93TBK9GrNzGrxDWSjiqz2hLU0Ha8MTXFIP0yd9fNdP+m43ZQ==",
"dev": true
}
}
},
"@babel/traverse": {
@ -1031,32 +816,12 @@
"lodash": "^4.17.13"
},
"dependencies": {
"@babel/code-frame": {
"version": "7.5.5",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.5.5.tgz",
"integrity": "sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw==",
"dev": true,
"requires": {
"@babel/highlight": "^7.0.0"
}
},
"@babel/parser": {
"version": "7.6.0",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.6.0.tgz",
"integrity": "sha512-+o2q111WEx4srBs7L9eJmcwi655eD8sXniLqMB93TBK9GrNzGrxDWSjiqz2hLU0Ha8MTXFIP0yd9fNdP+m43ZQ==",
"dev": true
},
"@babel/types": {
"version": "7.6.1",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.6.1.tgz",
"integrity": "sha512-X7gdiuaCmA0uRjCmRtYJNAVCc/q+5xSgsfKJHqMN4iNLILX39677fJE1O40arPMh0TTtS9ItH67yre6c7k6t0g==",
"dev": true,
"requires": {
"esutils": "^2.0.2",
"lodash": "^4.17.13",
"to-fast-properties": "^2.0.0"
}
},
"debug": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
@ -1069,13 +834,13 @@
}
},
"@babel/types": {
"version": "7.5.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.5.0.tgz",
"integrity": "sha512-UFpDVqRABKsW01bvw7/wSUe56uy6RXM5+VJibVVAybDGxEW25jdwiFJEf7ASvSaC7sN7rbE/l3cLp2izav+CtQ==",
"version": "7.6.1",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.6.1.tgz",
"integrity": "sha512-X7gdiuaCmA0uRjCmRtYJNAVCc/q+5xSgsfKJHqMN4iNLILX39677fJE1O40arPMh0TTtS9ItH67yre6c7k6t0g==",
"dev": true,
"requires": {
"esutils": "^2.0.2",
"lodash": "^4.17.11",
"lodash": "^4.17.13",
"to-fast-properties": "^2.0.0"
}
},
@ -3048,9 +2813,9 @@
}
},
"electron-to-chromium": {
"version": "1.3.252",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.252.tgz",
"integrity": "sha512-NWJ5TztDnjExFISZHFwpoJjMbLUifsNBnx7u2JI0gCw6SbKyQYYWWtBHasO/jPtHym69F4EZuTpRNGN11MT/jg==",
"version": "1.3.254",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.254.tgz",
"integrity": "sha512-7I5/OkgR6JKy6RFLJeru0kc0RMmmMu1UnkHBKInFKRrg1/4EQKIqOaUqITSww/SZ1LqWwp1qc/LLoIGy449eYw==",
"dev": true
},
"elliptic": {
@ -3178,9 +2943,9 @@
"dev": true
},
"esutils": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz",
"integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=",
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
"dev": true
},
"eventemitter3": {
@ -3549,9 +3314,9 @@
}
},
"follow-redirects": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.9.0.tgz",
"integrity": "sha512-CRcPzsSIbXyVDl0QI01muNDu69S8trU4jArW9LpOt2WtC6LyUJetcIrmfHsRBx7/Jb6GHJUiuqyYxPooFfNt6A==",
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.8.1.tgz",
"integrity": "sha512-micCIbldHioIegeKs41DoH0KS3AXfFzgS30qVkM6z/XOE/GJgvmsoc839NUqa1B9udYe9dQxgv7KFwng6+p/dw==",
"requires": {
"debug": "^3.0.0"
}
@ -5651,9 +5416,9 @@
}
},
"minimist": {
"version": "0.0.10",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz",
"integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8="
"version": "0.0.8",
"resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0="
},
"mississippi": {
"version": "3.0.0",
@ -5719,6 +5484,14 @@
"resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz",
"integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg=="
},
"moment-timezone": {
"version": "0.5.26",
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.26.tgz",
"integrity": "sha512-sFP4cgEKTCymBBKgoxZjYzlSovC20Y6J7y3nanDc5RoBIXKlZhoYwBoZGe3flwU6A372AcRwScH8KiwV6zjy1g==",
"requires": {
"moment": ">= 2.9.0"
}
},
"move-concurrently": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
@ -5782,6 +5555,21 @@
"resolved": "https://registry.npmjs.org/nextcloud-password-confirmation/-/nextcloud-password-confirmation-0.4.2.tgz",
"integrity": "sha512-DZXsfdk3iCsRWtd0lsYM1nqQ/oD9YlQ2WbC4qRZo20enUQLjJWZ8lYhKftXowmYL41t7spThnznJ7ihMG2/vUQ=="
},
"nextcloud-router": {
"version": "0.0.9",
"resolved": "https://registry.npmjs.org/nextcloud-router/-/nextcloud-router-0.0.9.tgz",
"integrity": "sha512-w0i4xqFwJJuXNWFf9AB9huCWW5XmwdJHSHa7oXlOLTAvP9WxwU3KCm/mcKy8Eb0cT0ElRPg72HLUxl7oyEWoBQ==",
"requires": {
"core-js": "^3.1.4"
},
"dependencies": {
"core-js": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.2.1.tgz",
"integrity": "sha512-Qa5XSVefSVPRxy2XfUC13WbvqkxhkwB3ve+pgCQveNgYzbM/UxZeu1dcOX/xr4UmfUd+muuvsaxilQzCyUurMw=="
}
}
},
"nextcloud-vue": {
"version": "0.12.3",
"resolved": "https://registry.npmjs.org/nextcloud-vue/-/nextcloud-vue-0.12.3.tgz",
@ -5928,9 +5716,9 @@
}
},
"node-releases": {
"version": "1.1.29",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.29.tgz",
"integrity": "sha512-R5bDhzh6I+tpi/9i2hrrvGJ3yKPYzlVOORDkXhnZuwi5D3q1I5w4vYy24PJXTcLk9Q0kws9TO77T75bcK8/ysQ==",
"version": "1.1.30",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.30.tgz",
"integrity": "sha512-BHcr1g6NeUH12IL+X3Flvs4IOnl1TL0JczUhEZjDE+FXXPQcVCNr8NEPb01zqGxzhTpdyJL5GXemaCW7aw6Khw==",
"dev": true,
"requires": {
"semver": "^5.3.0"
@ -6436,9 +6224,9 @@
"integrity": "sha512-w010cY1oCUmI+9KwwlWki+r5jxKfTFDVoadl7MSrIujHU5MJ5OR6HTDj6Xo8aoR/QsA56x8jKjA59qGH4ELtrA=="
},
"portfinder": {
"version": "1.0.24",
"resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.24.tgz",
"integrity": "sha512-ekRl7zD2qxYndYflwiryJwMioBI7LI7rVXg3EnLK3sjkouT5eOuhS3gS255XxBksa30VG8UPZYZCdgfGOfkSUg==",
"version": "1.0.23",
"resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.23.tgz",
"integrity": "sha512-B729mL/uLklxtxuiJKfQ84WPxNw5a7Yhx3geQZdcA4GjNjZSTSSMMWyoennMVnTWSmAR0lMdzWYN0JLnHrg1KQ==",
"requires": {
"async": "^1.5.2",
"debug": "^2.2.0",
@ -9276,7 +9064,7 @@
},
"wrap-ansi": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz",
"resolved": "http://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz",
"integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=",
"dev": true,
"requires": {

View File

@ -41,8 +41,10 @@
"lodash": "^4.17.15",
"marked": "^0.7.0",
"moment": "^2.24.0",
"moment-timezone": "^0.5.26",
"nextcloud-axios": "^0.2.1",
"nextcloud-password-confirmation": "^0.4.2",
"nextcloud-router": "0.0.9",
"nextcloud-vue": "^0.12.3",
"nextcloud-vue-collections": "^0.5.6",
"query-string": "^5.1.1",