mirror of https://github.com/nextcloud/calendar
Add attachments to events
Signed-off-by: Mikhail Sazanov <m@sazanof.ru>
This commit is contained in:
parent
5cedb44a75
commit
9d964452e5
|
@ -90,6 +90,8 @@ class SettingsController extends Controller {
|
|||
return $this->setDefaultReminder($value);
|
||||
case 'showTasks':
|
||||
return $this->setShowTasks($value);
|
||||
case 'attachmentsFolder':
|
||||
return $this->setAttachmentsFolder($value);
|
||||
default:
|
||||
return new JSONResponse([], Http::STATUS_BAD_REQUEST);
|
||||
}
|
||||
|
@ -171,6 +173,27 @@ class SettingsController extends Controller {
|
|||
return new JSONResponse();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set config for attachments folder
|
||||
*
|
||||
* @param string $value
|
||||
* @return JSONResponse
|
||||
*/
|
||||
private function setAttachmentsFolder(string $value):JSONResponse {
|
||||
try {
|
||||
$this->config->setUserValue(
|
||||
$this->userId,
|
||||
'dav',
|
||||
'attachmentsFolder',
|
||||
$value
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
return new JSONResponse([], Http::STATUS_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
return new JSONResponse();
|
||||
}
|
||||
|
||||
/**
|
||||
* set config value for showing week numbers
|
||||
*
|
||||
|
|
|
@ -99,6 +99,7 @@ class ViewController extends Controller {
|
|||
$showWeekNumbers = $this->config->getUserValue($this->userId, $this->appName, 'showWeekNr', $defaultWeekNumbers) === 'yes';
|
||||
$skipPopover = $this->config->getUserValue($this->userId, $this->appName, 'skipPopover', $defaultSkipPopover) === 'yes';
|
||||
$timezone = $this->config->getUserValue($this->userId, $this->appName, 'timezone', $defaultTimezone);
|
||||
$attachmentsFolder = $this->config->getUserValue($this->userId, 'dav', 'attachmentsFolder', '/Calendar');
|
||||
$slotDuration = $this->config->getUserValue($this->userId, $this->appName, 'slotDuration', $defaultSlotDuration);
|
||||
$defaultReminder = $this->config->getUserValue($this->userId, $this->appName, 'defaultReminder', $defaultDefaultReminder);
|
||||
$showTasks = $this->config->getUserValue($this->userId, $this->appName, 'showTasks', $defaultShowTasks) === 'yes';
|
||||
|
@ -124,6 +125,7 @@ class ViewController extends Controller {
|
|||
$this->initialStateService->provideInitialState('talk_enabled', $talkEnabled);
|
||||
$this->initialStateService->provideInitialState('talk_api_version', $talkApiVersion);
|
||||
$this->initialStateService->provideInitialState('timezone', $timezone);
|
||||
$this->initialStateService->provideInitialState('attachments_folder', $attachmentsFolder);
|
||||
$this->initialStateService->provideInitialState('slot_duration', $slotDuration);
|
||||
$this->initialStateService->provideInitialState('default_reminder', $defaultReminder);
|
||||
$this->initialStateService->provideInitialState('show_tasks', $showTasks);
|
||||
|
|
|
@ -48,7 +48,8 @@
|
|||
"vue-shortkey": "^3.1.7",
|
||||
"vuedraggable": "^2.24.3",
|
||||
"vuex": "^3.6.2",
|
||||
"vuex-router-sync": "^5.0.0"
|
||||
"vuex-router-sync": "^5.0.0",
|
||||
"webdav": "^4.10.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nextcloud/babel-config": "^1.0.0",
|
||||
|
@ -5170,8 +5171,12 @@
|
|||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"dev": true
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
|
||||
},
|
||||
"node_modules/base-64": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/base-64/-/base-64-1.0.0.tgz",
|
||||
"integrity": "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg=="
|
||||
},
|
||||
"node_modules/base64-js": {
|
||||
"version": "1.5.1",
|
||||
|
@ -5553,6 +5558,11 @@
|
|||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/byte-length": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/byte-length/-/byte-length-1.0.2.tgz",
|
||||
"integrity": "sha512-ovBpjmsgd/teRmgcPh23d4gJvxDoXtAzEL9xTfMU8Yc2kqCDb7L9jAG0XHl1nzuGl+h3ebCIF1i62UFyA9V/2Q=="
|
||||
},
|
||||
"node_modules/bytes": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
|
||||
|
@ -8790,7 +8800,6 @@
|
|||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
|
||||
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"he": "bin/he"
|
||||
}
|
||||
|
@ -8840,6 +8849,11 @@
|
|||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/hot-patcher": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/hot-patcher/-/hot-patcher-1.0.0.tgz",
|
||||
"integrity": "sha512-3H8VH0PreeNsKMZw16nTHbUp4YoHCnPlawpsPXGJUR4qENDynl79b6Xk9CIFvLcH1qungBsCuzKcWyzoPPalTw=="
|
||||
},
|
||||
"node_modules/hpack.js": {
|
||||
"version": "2.1.6",
|
||||
"resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz",
|
||||
|
@ -11519,6 +11533,11 @@
|
|||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/layerr": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/layerr/-/layerr-0.1.2.tgz",
|
||||
"integrity": "sha512-ob5kTd9H3S4GOG2nVXyQhOu9O8nBgP555XxWPkJI0tR0JeRilfyTp8WtPdIJHLXBmHMSdEq5+KMxiYABeScsIQ=="
|
||||
},
|
||||
"node_modules/leven": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
|
||||
|
@ -12053,6 +12072,11 @@
|
|||
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/nested-property": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/nested-property/-/nested-property-4.0.0.tgz",
|
||||
"integrity": "sha512-yFehXNWRs4cM0+dz7QxCd06hTbWbSkV0ISsqBfkntU6TOY4Qm3Q88fRRLOddkGh2Qq6dZvnKVAahfhjcUvLnyA=="
|
||||
},
|
||||
"node_modules/node-forge": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
|
||||
|
@ -12589,6 +12613,11 @@
|
|||
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/path-posix": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-posix/-/path-posix-1.0.0.tgz",
|
||||
"integrity": "sha512-1gJ0WpNIiYcQydgg3Ed8KzvIqTsDpNwq+cjBCssvBtuTWjEqY1AW+i+OepiEMqDCzyro9B2sLAe4RBPajMYFiA=="
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "0.1.7",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
|
||||
|
@ -13071,8 +13100,7 @@
|
|||
"node_modules/querystringify": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
|
||||
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ=="
|
||||
},
|
||||
"node_modules/queue-microtask": {
|
||||
"version": "1.2.3",
|
||||
|
@ -13484,8 +13512,7 @@
|
|||
"node_modules/requires-port": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
|
||||
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="
|
||||
},
|
||||
"node_modules/resolve": {
|
||||
"version": "1.22.1",
|
||||
|
@ -15344,11 +15371,15 @@
|
|||
"querystring": "0.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/url-join": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz",
|
||||
"integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA=="
|
||||
},
|
||||
"node_modules/url-parse": {
|
||||
"version": "1.5.10",
|
||||
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
|
||||
"integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"querystringify": "^2.1.1",
|
||||
"requires-port": "^1.0.0"
|
||||
|
@ -15863,6 +15894,48 @@
|
|||
"minimalistic-assert": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/webdav": {
|
||||
"version": "4.11.2",
|
||||
"resolved": "https://registry.npmjs.org/webdav/-/webdav-4.11.2.tgz",
|
||||
"integrity": "sha512-Ht9TPD5EB7gYW0YmhRcE5NW0/dn/HQfyLSPQY1Rw1coQ5MQTUooAQ9Bpqt4EU7QLw0b95tX4cU59R+SIojs9KQ==",
|
||||
"dependencies": {
|
||||
"axios": "^0.27.2",
|
||||
"base-64": "^1.0.0",
|
||||
"byte-length": "^1.0.2",
|
||||
"fast-xml-parser": "^3.19.0",
|
||||
"he": "^1.2.0",
|
||||
"hot-patcher": "^1.0.0",
|
||||
"layerr": "^0.1.2",
|
||||
"md5": "^2.3.0",
|
||||
"minimatch": "^5.1.0",
|
||||
"nested-property": "^4.0.0",
|
||||
"path-posix": "^1.0.0",
|
||||
"url-join": "^4.0.1",
|
||||
"url-parse": "^1.5.10"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/webdav/node_modules/brace-expansion": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/webdav/node_modules/minimatch": {
|
||||
"version": "5.1.6",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
|
||||
"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
|
||||
|
@ -20383,8 +20456,12 @@
|
|||
"balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"dev": true
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
|
||||
},
|
||||
"base-64": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/base-64/-/base-64-1.0.0.tgz",
|
||||
"integrity": "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg=="
|
||||
},
|
||||
"base64-js": {
|
||||
"version": "1.5.1",
|
||||
|
@ -20700,6 +20777,11 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"byte-length": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/byte-length/-/byte-length-1.0.2.tgz",
|
||||
"integrity": "sha512-ovBpjmsgd/teRmgcPh23d4gJvxDoXtAzEL9xTfMU8Yc2kqCDb7L9jAG0XHl1nzuGl+h3ebCIF1i62UFyA9V/2Q=="
|
||||
},
|
||||
"bytes": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
|
||||
|
@ -23208,8 +23290,7 @@
|
|||
"he": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
|
||||
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
|
||||
"dev": true
|
||||
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="
|
||||
},
|
||||
"hmac-drbg": {
|
||||
"version": "1.0.1",
|
||||
|
@ -23252,6 +23333,11 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"hot-patcher": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/hot-patcher/-/hot-patcher-1.0.0.tgz",
|
||||
"integrity": "sha512-3H8VH0PreeNsKMZw16nTHbUp4YoHCnPlawpsPXGJUR4qENDynl79b6Xk9CIFvLcH1qungBsCuzKcWyzoPPalTw=="
|
||||
},
|
||||
"hpack.js": {
|
||||
"version": "2.1.6",
|
||||
"resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz",
|
||||
|
@ -25191,6 +25277,11 @@
|
|||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"layerr": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/layerr/-/layerr-0.1.2.tgz",
|
||||
"integrity": "sha512-ob5kTd9H3S4GOG2nVXyQhOu9O8nBgP555XxWPkJI0tR0JeRilfyTp8WtPdIJHLXBmHMSdEq5+KMxiYABeScsIQ=="
|
||||
},
|
||||
"leven": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
|
||||
|
@ -25620,6 +25711,11 @@
|
|||
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
|
||||
"peer": true
|
||||
},
|
||||
"nested-property": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/nested-property/-/nested-property-4.0.0.tgz",
|
||||
"integrity": "sha512-yFehXNWRs4cM0+dz7QxCd06hTbWbSkV0ISsqBfkntU6TOY4Qm3Q88fRRLOddkGh2Qq6dZvnKVAahfhjcUvLnyA=="
|
||||
},
|
||||
"node-forge": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
|
||||
|
@ -26030,6 +26126,11 @@
|
|||
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
|
||||
"dev": true
|
||||
},
|
||||
"path-posix": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-posix/-/path-posix-1.0.0.tgz",
|
||||
"integrity": "sha512-1gJ0WpNIiYcQydgg3Ed8KzvIqTsDpNwq+cjBCssvBtuTWjEqY1AW+i+OepiEMqDCzyro9B2sLAe4RBPajMYFiA=="
|
||||
},
|
||||
"path-to-regexp": {
|
||||
"version": "0.1.7",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
|
||||
|
@ -26375,8 +26476,7 @@
|
|||
"querystringify": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
|
||||
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ=="
|
||||
},
|
||||
"queue-microtask": {
|
||||
"version": "1.2.3",
|
||||
|
@ -26701,8 +26801,7 @@
|
|||
"requires-port": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
|
||||
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="
|
||||
},
|
||||
"resolve": {
|
||||
"version": "1.22.1",
|
||||
|
@ -28141,11 +28240,15 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"url-join": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz",
|
||||
"integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA=="
|
||||
},
|
||||
"url-parse": {
|
||||
"version": "1.5.10",
|
||||
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
|
||||
"integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"querystringify": "^2.1.1",
|
||||
"requires-port": "^1.0.0"
|
||||
|
@ -28567,6 +28670,44 @@
|
|||
"minimalistic-assert": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"webdav": {
|
||||
"version": "4.11.2",
|
||||
"resolved": "https://registry.npmjs.org/webdav/-/webdav-4.11.2.tgz",
|
||||
"integrity": "sha512-Ht9TPD5EB7gYW0YmhRcE5NW0/dn/HQfyLSPQY1Rw1coQ5MQTUooAQ9Bpqt4EU7QLw0b95tX4cU59R+SIojs9KQ==",
|
||||
"requires": {
|
||||
"axios": "^0.27.2",
|
||||
"base-64": "^1.0.0",
|
||||
"byte-length": "^1.0.2",
|
||||
"fast-xml-parser": "^3.19.0",
|
||||
"he": "^1.2.0",
|
||||
"hot-patcher": "^1.0.0",
|
||||
"layerr": "^0.1.2",
|
||||
"md5": "^2.3.0",
|
||||
"minimatch": "^5.1.0",
|
||||
"nested-property": "^4.0.0",
|
||||
"path-posix": "^1.0.0",
|
||||
"url-join": "^4.0.1",
|
||||
"url-parse": "^1.5.10"
|
||||
},
|
||||
"dependencies": {
|
||||
"brace-expansion": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "5.1.6",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
|
||||
"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
|
||||
"requires": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"webidl-conversions": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
|
||||
|
|
|
@ -74,7 +74,8 @@
|
|||
"vue-shortkey": "^3.1.7",
|
||||
"vuedraggable": "^2.24.3",
|
||||
"vuex": "^3.6.2",
|
||||
"vuex-router-sync": "^5.0.0"
|
||||
"vuex-router-sync": "^5.0.0",
|
||||
"webdav": "^4.10.0"
|
||||
},
|
||||
"browserslist": [
|
||||
"extends @nextcloud/browserslist-config"
|
||||
|
|
|
@ -83,6 +83,7 @@
|
|||
@select="changeDefaultReminder" />
|
||||
</li>
|
||||
<SettingsTimezoneSelect :is-disabled="loadingCalendars" />
|
||||
<SettingsAttachmentsFolder />
|
||||
<ActionButton @click.prevent.stop="copyPrimaryCalDAV">
|
||||
<template #icon>
|
||||
<ClipboardArrowLeftOutline :size="20" decorative />
|
||||
|
@ -137,6 +138,7 @@ import {
|
|||
|
||||
import SettingsImportSection from './Settings/SettingsImportSection.vue'
|
||||
import SettingsTimezoneSelect from './Settings/SettingsTimezoneSelect.vue'
|
||||
import SettingsAttachmentsFolder from './Settings/SettingsAttachmentsFolder.vue'
|
||||
|
||||
import { getCurrentUserPrincipal } from '../../services/caldavService.js'
|
||||
import ShortcutOverview from './Settings/ShortcutOverview.vue'
|
||||
|
@ -163,6 +165,7 @@ export default {
|
|||
Multiselect,
|
||||
SettingsImportSection,
|
||||
SettingsTimezoneSelect,
|
||||
SettingsAttachmentsFolder,
|
||||
ClipboardArrowLeftOutline,
|
||||
InformationVariant,
|
||||
OpenInNewIcon,
|
||||
|
@ -200,6 +203,7 @@ export default {
|
|||
defaultReminder: state => state.settings.defaultReminder,
|
||||
timezone: state => state.settings.timezone,
|
||||
locale: (state) => state.settings.momentLocale,
|
||||
attachmentsFolder: (state) => state.settings.attachmentsFolder,
|
||||
}),
|
||||
isBirthdayCalendarDisabled() {
|
||||
return this.savingBirthdayCalendar || this.loadingCalendars
|
||||
|
|
|
@ -0,0 +1,96 @@
|
|||
<!--
|
||||
- @copyright Copyright (c) 2022 Mikhail Sazanov <m@sazanof.ru>
|
||||
- @author Mikhail Sazanov <m@sazanof.ru>
|
||||
-
|
||||
- @license AGPL-3.0-or-later
|
||||
-
|
||||
- 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>
|
||||
<li class="settings-fieldset-interior-item settings-fieldset-interior-item--folder">
|
||||
<label for="attachmentsFolder">
|
||||
{{ $t('calendar', 'Default attachments location') }}
|
||||
</label>
|
||||
<div class="form-group">
|
||||
<NcInputField v-model="attachmentsFolder"
|
||||
type="text"
|
||||
@input="debounceSaveAttachmentsFolder(attachmentsFolder)"
|
||||
@change="debounceSaveAttachmentsFolder(attachmentsFolder)"
|
||||
@click="selectCalendarFolder"
|
||||
@focus.once="selectCalendarFolder"
|
||||
@keyboard.enter="selectCalendarFolder" />
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import debounce from 'debounce'
|
||||
import { mapState } from 'vuex'
|
||||
import { getFilePickerBuilder, showError, showSuccess } from '@nextcloud/dialogs'
|
||||
import NcInputField from '@nextcloud/vue/dist/Components/NcInputField.js'
|
||||
|
||||
export default {
|
||||
name: 'SettingsAttachmentsFolder',
|
||||
components: {
|
||||
NcInputField,
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
attachmentsFolder: state => (state.settings.attachmentsFolder || '/'),
|
||||
}),
|
||||
},
|
||||
methods: {
|
||||
async selectCalendarFolder() {
|
||||
const picker = getFilePickerBuilder(t('calendar', 'Select the default location for attachments'))
|
||||
.setMultiSelect(false)
|
||||
.setModal(true)
|
||||
.setType(1)
|
||||
.addMimeTypeFilter('httpd/unix-directory')
|
||||
.allowDirectories()
|
||||
.build()
|
||||
const path = await picker.pick()
|
||||
this.saveAttachmentsFolder(path)
|
||||
},
|
||||
debounceSaveAttachmentsFolder: debounce(function(...args) {
|
||||
this.saveAttachmentsFolder(...args)
|
||||
}, 300),
|
||||
saveAttachmentsFolder(path) {
|
||||
if (typeof path !== 'string' || path.trim() === '' || !path.startsWith('/')) {
|
||||
showError(t('calendar', 'Invalid location selected'))
|
||||
return
|
||||
}
|
||||
|
||||
if (path.includes('//')) {
|
||||
path = path.replace(/\/\//gi, '/')
|
||||
}
|
||||
|
||||
this.$store.dispatch('setAttachmentsFolder', { attachmentsFolder: path })
|
||||
.then(() => {
|
||||
showSuccess(this.$t('calendar', 'Attachments folder successfully saved.'))
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error)
|
||||
showError(this.$t('calendar', 'Error on saving attachments folder.'))
|
||||
})
|
||||
},
|
||||
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -0,0 +1,248 @@
|
|||
<template>
|
||||
<div id="attachments">
|
||||
<input ref="localAttachments"
|
||||
class="attachments-input"
|
||||
type="file"
|
||||
multiple
|
||||
@change="onLocalAttachmentSelected">
|
||||
<div class="attachments-summary">
|
||||
<div class="attachments-summary-inner">
|
||||
<Paperclip :size="20" />
|
||||
<div v-if="attachments.length > 0">
|
||||
{{ n('calendar', '{count} attachment', '{count} attachments', attachments.length, { count: attachments.length }) }}
|
||||
</div>
|
||||
<div v-else>
|
||||
{{ t('calendar', 'No attachments') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<NcActions>
|
||||
<template #icon>
|
||||
<Plus :size="20" />
|
||||
</template>
|
||||
<NcActionButton @click="openFilesModal()">
|
||||
<template #icon>
|
||||
<Folder :size="20" />
|
||||
</template>
|
||||
{{ t('calendar', 'Add from Files') }}
|
||||
</NcActionButton>
|
||||
<NcActionButton @click="clickOnUploadButton">
|
||||
<template #icon>
|
||||
<Upload :size="20" />
|
||||
</template>
|
||||
{{ t('calendar', 'Upload from device') }}
|
||||
</NcActionButton>
|
||||
</NcActions>
|
||||
</div>
|
||||
<div v-if="attachments.length > 0">
|
||||
<ul class="attachments-list-item">
|
||||
<NcListItem v-for="attachment in attachments"
|
||||
:key="attachment.path"
|
||||
:force-display-actions="true"
|
||||
:title="getBaseName(attachment.fileName)"
|
||||
@click="openFile(attachment.uri)">
|
||||
<template #icon>
|
||||
<img :src="getPreview(attachment)" class="attachment-icon">
|
||||
</template>
|
||||
<template #actions>
|
||||
<NcActionButton @click="deleteAttachmentFromEvent(attachment)">
|
||||
<template #icon>
|
||||
<Close :size="20" />
|
||||
</template>
|
||||
{{ t('calendar', 'Delete file') }}
|
||||
</NcActionButton>
|
||||
</template>
|
||||
</NcListItem>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
NcListItem,
|
||||
NcActions,
|
||||
NcActionButton,
|
||||
} from '@nextcloud/vue'
|
||||
|
||||
import {
|
||||
mapState,
|
||||
} from 'vuex'
|
||||
|
||||
import Upload from 'vue-material-design-icons/Upload.vue'
|
||||
import Close from 'vue-material-design-icons/Close.vue'
|
||||
import Folder from 'vue-material-design-icons/Folder.vue'
|
||||
import Paperclip from 'vue-material-design-icons/Paperclip.vue'
|
||||
import Plus from 'vue-material-design-icons/Plus.vue'
|
||||
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import { getFilePickerBuilder, showError } from '@nextcloud/dialogs'
|
||||
import {
|
||||
uploadLocalAttachment,
|
||||
getFileInfo,
|
||||
} from '../../../services/attachmentService.js'
|
||||
import { parseXML } from 'webdav/dist/node/tools/dav.js'
|
||||
|
||||
export default {
|
||||
name: 'AttachmentsList',
|
||||
components: {
|
||||
NcListItem,
|
||||
NcActions,
|
||||
NcActionButton,
|
||||
Upload,
|
||||
Close,
|
||||
Folder,
|
||||
Paperclip,
|
||||
Plus,
|
||||
},
|
||||
props: {
|
||||
calendarObjectInstance: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
isReadOnly: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
uploading: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
currentUser() {
|
||||
return this.$store.getters.getCurrentUserPrincipal
|
||||
},
|
||||
attachments() {
|
||||
return this.calendarObjectInstance.attachments
|
||||
},
|
||||
...mapState({
|
||||
attachmentsFolder: state => state.settings.attachmentsFolder,
|
||||
}),
|
||||
},
|
||||
methods: {
|
||||
addAttachmentWithProperty(calendarObjectInstance, sharedData) {
|
||||
this.$store.commit('addAttachmentWithProperty', {
|
||||
calendarObjectInstance,
|
||||
sharedData,
|
||||
})
|
||||
},
|
||||
deleteAttachmentFromEvent(attachment) {
|
||||
this.$store.commit('deleteAttachment', {
|
||||
calendarObjectInstance: this.calendarObjectInstance,
|
||||
attachment,
|
||||
})
|
||||
},
|
||||
async openFilesModal() {
|
||||
const picker = getFilePickerBuilder(t('calendar', 'Choose a file to add as attachment')).setMultiSelect(false).build()
|
||||
try {
|
||||
const filename = await picker.pick(t('calendar', 'Choose a file to share as a link'))
|
||||
if (!this.isDuplicateAttachment(filename)) {
|
||||
// TODO do not share Move this to PHP
|
||||
const data = await getFileInfo(filename, this.currentUser.dav)
|
||||
const davRes = await parseXML(data)
|
||||
const davRespObj = davRes?.multistatus?.response[0]?.propstat?.prop
|
||||
davRespObj.fileName = filename
|
||||
davRespObj.url = generateUrl(`/f/${davRespObj.fileid}`)
|
||||
davRespObj.value = davRespObj.url
|
||||
this.addAttachmentWithProperty(this.calendarObjectInstance, davRespObj)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
|
||||
}
|
||||
},
|
||||
isDuplicateAttachment(path) {
|
||||
return this.attachments.find(attachment => {
|
||||
if (attachment.fileName === path) {
|
||||
showError(t('calendar', 'Attachment {name} already exist!', { name: this.getBaseName(path) }))
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
},
|
||||
clickOnUploadButton() {
|
||||
this.$refs.localAttachments.click()
|
||||
},
|
||||
async onLocalAttachmentSelected(e) {
|
||||
const attachments = await uploadLocalAttachment(this.attachmentsFolder, e, this.currentUser.dav, this.attachments)
|
||||
// TODO do not share file, move to PHP
|
||||
attachments.map(async attachment => {
|
||||
const data = await getFileInfo(`${this.attachmentsFolder}/${attachment.path}`, this.currentUser.dav)
|
||||
const davRes = await parseXML(data)
|
||||
const davRespObj = davRes?.multistatus?.response[0]?.propstat?.prop
|
||||
davRespObj.fileName = attachment.path
|
||||
davRespObj.url = generateUrl(`/f/${davRespObj.fileid}`)
|
||||
davRespObj.value = davRespObj.url
|
||||
this.addAttachmentWithProperty(this.calendarObjectInstance, davRespObj)
|
||||
})
|
||||
|
||||
e.target.value = ''
|
||||
|
||||
},
|
||||
getIcon(mime) {
|
||||
return OC.MimeType.getIconUrl(mime)
|
||||
},
|
||||
getPreview(attachment) {
|
||||
if (attachment.xNcHasPreview) {
|
||||
return generateUrl(`/core/preview?fileId=${attachment.xNcFileId}&x=100&y=100&a=0`)
|
||||
}
|
||||
return attachment.formatType ? OC.MimeType.getIconUrl(attachment.formatType) : null
|
||||
},
|
||||
getBaseName(name) {
|
||||
return name.split('/').pop()
|
||||
},
|
||||
openFile(url) {
|
||||
window.open(url, '_blank', 'noopener noreferrer')
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.attachments-input {
|
||||
display: none;
|
||||
}
|
||||
.attachments-summary {
|
||||
display:flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-left: 6px;
|
||||
|
||||
.attachments-summary-inner {
|
||||
display:flex;
|
||||
align-items: center;
|
||||
|
||||
span {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
margin-left: -10px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.attachments-list-item {
|
||||
margin: 0 -8px;
|
||||
}
|
||||
|
||||
#attachments .empty-content {
|
||||
margin-top: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
.button-group {
|
||||
display: flex;
|
||||
align-content: center;
|
||||
justify-content: center;
|
||||
|
||||
button:first-child {
|
||||
margin-right: 6px;
|
||||
}
|
||||
}
|
||||
.attachment-icon {
|
||||
width: 40px;
|
||||
height: auto;
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,67 @@
|
|||
/**
|
||||
* @copyright Copyright (c) 2022 Mikhail Sazanov
|
||||
*
|
||||
* @author Mikhail Sazanov <m@sazanof.ru>
|
||||
*
|
||||
* @license AGPL-3.0-or-later
|
||||
*
|
||||
* 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* Creates a complete attachment object based on given props
|
||||
*
|
||||
* @param {object} props The attachment properties already provided
|
||||
* @return {object}
|
||||
*/
|
||||
const getDefaultAttachmentObject = (props = {}) => Object.assign({}, {
|
||||
// The calendar-js attachment property
|
||||
attachmentProperty: null,
|
||||
// The file name of the attachment
|
||||
fileName: null,
|
||||
// The attachment mime type
|
||||
formatType: null,
|
||||
// The uri of the attachment
|
||||
uri: null,
|
||||
// The value from calendar object
|
||||
value: null,
|
||||
// Preview of file
|
||||
xNcHasPreview: null,
|
||||
// File id in NC
|
||||
xNcFileId: null,
|
||||
}, props)
|
||||
|
||||
/**
|
||||
* Maps a calendar-js attachment property to our attachment object
|
||||
*
|
||||
* @param {attachmentProperty} attachmentProperty The calendar-js attachmentProperty to turn into a attachment object
|
||||
* @return {object}
|
||||
*/
|
||||
const mapAttachmentPropertyToAttchmentObject = (attachmentProperty) => {
|
||||
return getDefaultAttachmentObject({
|
||||
attachmentProperty,
|
||||
fileName: attachmentProperty.getParameterFirstValue('FILENAME'),
|
||||
formatType: attachmentProperty.formatType,
|
||||
uri: attachmentProperty.uri,
|
||||
value: attachmentProperty.value,
|
||||
xNcHasPreview: attachmentProperty.getParameterFirstValue('X-NC-HAS-PREVIEW') === 'true',
|
||||
xNcFileId: attachmentProperty.getParameterFirstValue('X-NC-FILE-ID'),
|
||||
})
|
||||
}
|
||||
|
||||
export {
|
||||
getDefaultAttachmentObject,
|
||||
mapAttachmentPropertyToAttchmentObject,
|
||||
}
|
|
@ -25,6 +25,7 @@ import { DurationValue, DateTimeValue } from '@nextcloud/calendar-js'
|
|||
import { getHexForColorName, getClosestCSS3ColorNameForHex } from '../utils/color.js'
|
||||
import { mapAlarmComponentToAlarmObject } from './alarm.js'
|
||||
import { mapAttendeePropertyToAttendeeObject } from './attendee.js'
|
||||
import { mapAttachmentPropertyToAttchmentObject } from './attachment.js'
|
||||
import {
|
||||
getDefaultRecurrenceRuleObject,
|
||||
mapRecurrenceRuleValueToRecurrenceRuleObject,
|
||||
|
@ -85,6 +86,8 @@ const getDefaultEventObject = (props = {}) => Object.assign({}, {
|
|||
customColor: null,
|
||||
// Categories
|
||||
categories: [],
|
||||
// Attachments of this event
|
||||
attachments: [],
|
||||
}, props)
|
||||
|
||||
/**
|
||||
|
@ -154,6 +157,14 @@ const mapEventComponentToEventObject = (eventComponent) => {
|
|||
eventObject.attendees.push(mapAttendeePropertyToAttendeeObject(attendee))
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract attachments
|
||||
*/
|
||||
|
||||
for (const attachment of eventComponent.getPropertyIterator('ATTACH')) {
|
||||
eventObject.attachments.push(mapAttachmentPropertyToAttchmentObject(attachment))
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract recurrence-rule
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,173 @@
|
|||
/**
|
||||
* @copyright 2022 Mikhail Sazanov <m@sazanof.ru>
|
||||
*
|
||||
* @author 2022 Mikhail Sazanov <m@sazanof.ru>
|
||||
*
|
||||
* @license AGPL-3.0-or-later
|
||||
*
|
||||
* 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 axios from '@nextcloud/axios'
|
||||
import { generateOcsUrl } from '@nextcloud/router'
|
||||
import { showError, showSuccess } from '@nextcloud/dialogs'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
|
||||
/**
|
||||
* Makes a share link for a given file or directory.
|
||||
*
|
||||
* @param {string} path The file path from the user's root directory. e.g. `/myfile.txt`
|
||||
* @return {string} url share link
|
||||
*/
|
||||
const shareFile = async function(path) {
|
||||
try {
|
||||
const res = await axios.post(generateOcsUrl('apps/files_sharing/api/v1/', 2) + 'shares', {
|
||||
shareType: OC.Share.SHARE_TYPE_LINK,
|
||||
path,
|
||||
})
|
||||
return res.data.ocs.data
|
||||
} catch (error) {
|
||||
if (error?.response?.data?.ocs?.meta?.message) {
|
||||
console.error(`Error while sharing file: ${error.response.data.ocs.meta.message}`)
|
||||
showError(error.response.data.ocs.meta.message)
|
||||
throw error
|
||||
} else {
|
||||
console.error('Error while sharing file: Unknown error')
|
||||
showError(t('calendar', 'Error while sharing file'))
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Share file with a user with permissions
|
||||
*
|
||||
* @param path
|
||||
* @param sharedWith
|
||||
* @param permissions
|
||||
* @return {Promise<[{path: string, permissions, scope: string, name: string, backend: string, type: string},{path: string, permissions: *, scope: string, name: string, backend: string, type: string}]>}
|
||||
*/
|
||||
const shareFileWith = async function(path, sharedWith, permissions = 17) {
|
||||
try {
|
||||
const url = generateOcsUrl('apps/files_sharing/api/v1/', 2)
|
||||
const res = await axios.post(`${url}shares`, {
|
||||
password: null,
|
||||
shareType: OC.Share.SHARE_TYPE_USER, // WITH USERS,
|
||||
permissions, // 14 - edit, 17 - view
|
||||
path,
|
||||
shareWith: sharedWith,
|
||||
})
|
||||
return res.data.ocs.data
|
||||
} catch (error) {
|
||||
if (error?.response?.data?.ocs?.meta?.message) {
|
||||
console.error(`Error while sharing file with user: ${error.response.data.ocs.meta.message}`)
|
||||
showError(error.response.data.ocs.meta.message)
|
||||
throw error
|
||||
} else {
|
||||
console.error('Error while sharing file with user: Unknown error')
|
||||
showError(t('calendar', 'Error while sharing file with user'))
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const createFolder = async function(folderName, userId) {
|
||||
const url = `/remote.php/dav/files/${userId}/${folderName}`
|
||||
await axios({
|
||||
method: 'MKCOL',
|
||||
url: url.replace('//', '/'),
|
||||
}).catch(e => {
|
||||
if (e.response.status !== 405) {
|
||||
showError(t('calendar', 'Error creating a folder {folder}', {
|
||||
folder: folderName,
|
||||
}))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const uploadLocalAttachment = async function(folder, event, dav, componentAttachments) {
|
||||
const files = event.target.files
|
||||
const attachments = []
|
||||
const promises = []
|
||||
|
||||
files.forEach(file => {
|
||||
// temp fix, until we decide where to save the attachments
|
||||
if (componentAttachments.map(attachment => attachment.fileName.split('/').pop()).indexOf(file.name) !== -1) {
|
||||
// TODO may be show user confirmation dialog to create a file named Existing_File_(2) ?
|
||||
showError(t('calendar', 'Attachment {fileName} already exists!', {
|
||||
fileName: file.name,
|
||||
}))
|
||||
} else {
|
||||
const url = `/remote.php/dav/files/${dav.userId}/${folder}/${file.name}`
|
||||
const res = axios.put(url, file).then(resp => {
|
||||
const data = {
|
||||
fileName: file.name,
|
||||
formatType: file.type,
|
||||
uri: url,
|
||||
value: url,
|
||||
path: `/${file.name}`,
|
||||
}
|
||||
if (resp.status === 204 || resp.status === 201) {
|
||||
showSuccess(t('calendar', 'Attachment {fileName} added!', {
|
||||
fileName: file.name,
|
||||
}))
|
||||
attachments.push(data)
|
||||
}
|
||||
}).catch(() => {
|
||||
showError(t('calendar', 'An error occurred during uploading file {fileName}', {
|
||||
fileName: file.name,
|
||||
}))
|
||||
})
|
||||
promises.push(res)
|
||||
}
|
||||
|
||||
})
|
||||
await Promise.all(promises)
|
||||
return attachments
|
||||
|
||||
}
|
||||
|
||||
// TODO is shared or not @share-types@
|
||||
const getFileInfo = async function(path, dav) {
|
||||
const url = `/remote.php/dav/files/${dav.userId}/${path}`
|
||||
const res = await axios({
|
||||
method: 'PROPFIND',
|
||||
url,
|
||||
data: `<?xml version="1.0"?>
|
||||
<d:propfind
|
||||
xmlns:d="DAV:"
|
||||
xmlns:oc="http://owncloud.org/ns"
|
||||
xmlns:nc="http://nextcloud.org/ns">
|
||||
<d:prop>
|
||||
<d:getcontenttype />
|
||||
<oc:size />
|
||||
<oc:fileid />
|
||||
<oc:share-types />
|
||||
<nc:has-preview />
|
||||
</d:prop>
|
||||
</d:propfind>`,
|
||||
}).catch(() => {
|
||||
showError(t('calendar', 'An error occurred during getting file information'))
|
||||
})
|
||||
return res.data
|
||||
}
|
||||
|
||||
export {
|
||||
getFileInfo,
|
||||
shareFile,
|
||||
shareFileWith,
|
||||
uploadLocalAttachment,
|
||||
createFolder,
|
||||
}
|
|
@ -24,7 +24,7 @@ import getTimezoneManager from '../services/timezoneDataProviderService.js'
|
|||
import {
|
||||
getDateFromDateTimeValue,
|
||||
} from '../utils/date.js'
|
||||
import { AttendeeProperty, Property, DateTimeValue, DurationValue, RecurValue } from '@nextcloud/calendar-js'
|
||||
import { AttendeeProperty, Property, DateTimeValue, DurationValue, RecurValue, AttachmentProperty, Parameter } from '@nextcloud/calendar-js'
|
||||
import { getBySetPositionAndBySetFromDate, getWeekDayFromDate } from '../utils/recurrence.js'
|
||||
import {
|
||||
copyCalendarObjectInstanceIntoEventComponent,
|
||||
|
@ -47,6 +47,7 @@ import { getObjectAtRecurrenceId } from '../utils/calendarObject.js'
|
|||
import logger from '../utils/logger.js'
|
||||
import settings from './settings.js'
|
||||
import { getRFCProperties } from '../models/rfcProps.js'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
|
||||
const state = {
|
||||
isNew: null,
|
||||
|
@ -1356,6 +1357,101 @@ const mutations = {
|
|||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* @param state
|
||||
* @param calendarObjectInstance.calendarObjectInstance
|
||||
* @param calendarObjectInstance
|
||||
* @param calendarObjectInstance.sharedData
|
||||
* @param sharedData
|
||||
*/
|
||||
addAttachmentBySharedData(state, { calendarObjectInstance, sharedData }) {
|
||||
const attachment = AttachmentProperty.fromLink(sharedData.url)
|
||||
const fileName = sharedData.fileName
|
||||
|
||||
// hot-fix needed temporary, becase calendar-js has no fileName get-setter
|
||||
const parameterFileName = new Parameter('FILENAME', fileName)
|
||||
// custom has-preview parameter from dav file
|
||||
const xNcHasPreview = new Parameter('X-NC-HAS-PREVIEW', sharedData['has-preview'].toString())
|
||||
// custom file id parameter from dav file
|
||||
const xNcFileId = new Parameter('X-NC-FILE-ID', sharedData.fileid.toString())
|
||||
// custom share-types parameter from dav file
|
||||
const xNcSharedTypes = new Parameter('X-NC-SHARED-TYPES', sharedData['share-types']['share-type']
|
||||
? sharedData['share-types']['share-type'].join(',')
|
||||
: '')
|
||||
attachment.setParameter(parameterFileName)
|
||||
attachment.setParameter(xNcFileId)
|
||||
attachment.setParameter(xNcHasPreview)
|
||||
attachment.setParameter(xNcSharedTypes)
|
||||
attachment.isNew = true
|
||||
attachment.shareTypes = sharedData['share-types']['share-type']
|
||||
? sharedData['share-types']['share-type'].join(',')
|
||||
: ''
|
||||
attachment.fileName = fileName
|
||||
attachment.xNcFileId = sharedData.fileid
|
||||
attachment.xNcHasPreview = sharedData['has-preview']
|
||||
attachment.formatType = sharedData.getcontenttype
|
||||
attachment.uri = sharedData.url ? sharedData.url : generateUrl(`/f/${sharedData.fileid}`)
|
||||
|
||||
calendarObjectInstance.eventComponent.addProperty(attachment)
|
||||
calendarObjectInstance.attachments.push(attachment)
|
||||
|
||||
// console.log(attachment)
|
||||
},
|
||||
|
||||
addAttachmentWithProperty(state, { calendarObjectInstance, sharedData }) {
|
||||
const attachment = {}
|
||||
const fileName = sharedData.fileName
|
||||
attachment.isNew = true
|
||||
attachment.shareTypes = (typeof sharedData?.['share-types']?.['share-type'] === 'number'
|
||||
? sharedData?.['share-types']?.['share-type']?.toString()
|
||||
: sharedData?.['share-types']?.['share-type']?.join(',')) ?? null
|
||||
attachment.fileName = fileName
|
||||
attachment.xNcFileId = sharedData.fileid
|
||||
attachment.xNcHasPreview = sharedData['has-preview']
|
||||
attachment.formatType = sharedData.getcontenttype
|
||||
attachment.uri = sharedData.url ? sharedData.url : generateUrl(`/f/${sharedData.fileid}`)
|
||||
|
||||
const attachmentProperty = AttachmentProperty.fromLink(attachment.uri, attachment.formatType)
|
||||
const parameterFileName = new Parameter('FILENAME', fileName)
|
||||
const xNcHasPreview = new Parameter('X-NC-HAS-PREVIEW', attachment.xNcHasPreview.toString())
|
||||
const xNcFileId = new Parameter('X-NC-FILE-ID', attachment.xNcFileId.toString())
|
||||
// ADD X-NC-SHARED-TYPES only if sharet-type not empty
|
||||
if (attachment.shareTypes !== null) {
|
||||
const xNcSharedTypes = new Parameter('X-NC-SHARED-TYPES', attachment.shareTypes)
|
||||
attachmentProperty.setParameter(xNcSharedTypes)
|
||||
}
|
||||
|
||||
attachmentProperty.setParameter(parameterFileName)
|
||||
attachmentProperty.setParameter(xNcFileId)
|
||||
attachmentProperty.setParameter(xNcHasPreview)
|
||||
attachmentProperty.uri = attachment.uri
|
||||
|
||||
attachment.attachmentProperty = attachmentProperty
|
||||
|
||||
calendarObjectInstance.eventComponent.addProperty(attachmentProperty)
|
||||
calendarObjectInstance.attachments.push(attachment)
|
||||
},
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {object} state The Vuex state
|
||||
* @param {object} data The destructuring object
|
||||
* @param {object} data.calendarObjectInstance The calendarObjectInstance object
|
||||
* @param {object} data.attachment The attachment object
|
||||
*/
|
||||
deleteAttachment(state, { calendarObjectInstance, attachment }) {
|
||||
try {
|
||||
const index = calendarObjectInstance.attachments.indexOf(attachment)
|
||||
if (index !== -1) {
|
||||
calendarObjectInstance.attachments.splice(index, 1)
|
||||
}
|
||||
calendarObjectInstance.eventComponent.removeAttachment(attachment.attachmentProperty)
|
||||
} catch {
|
||||
}
|
||||
|
||||
},
|
||||
}
|
||||
|
||||
const getters = {}
|
||||
|
|
|
@ -49,6 +49,7 @@ const state = {
|
|||
canSubscribeLink: true,
|
||||
// user-defined Nextcloud settings
|
||||
momentLocale: 'en',
|
||||
attachmentsFolder: '/Calendar',
|
||||
}
|
||||
|
||||
const mutations = {
|
||||
|
@ -131,6 +132,17 @@ const mutations = {
|
|||
state.timezone = timezoneId
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates the user's attachments folder
|
||||
*
|
||||
* @param {object} state The Vuex state
|
||||
* @param {object} data The destructuring object
|
||||
* @param {string} data.attachmentsFolder The new attachments folder
|
||||
*/
|
||||
setAttachmentsFolder(state, { attachmentsFolder }) {
|
||||
state.attachmentsFolder = attachmentsFolder
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialize settings
|
||||
*
|
||||
|
@ -152,8 +164,9 @@ const mutations = {
|
|||
* @param {string} data.forceEventAlarmType
|
||||
* @param {boolean} data.disableAppointments Allow to disable the appointments feature
|
||||
* @param {boolean} data.canSubscribeLink
|
||||
* @param {string} data.attachmentsFolder Default user's attachments folder
|
||||
*/
|
||||
loadSettingsFromServer(state, { appVersion, eventLimit, firstRun, showWeekNumbers, showTasks, showWeekends, skipPopover, slotDuration, defaultReminder, talkEnabled, tasksEnabled, timezone, hideEventExport, forceEventAlarmType, disableAppointments, canSubscribeLink }) {
|
||||
loadSettingsFromServer(state, { appVersion, eventLimit, firstRun, showWeekNumbers, showTasks, showWeekends, skipPopover, slotDuration, defaultReminder, talkEnabled, tasksEnabled, timezone, hideEventExport, forceEventAlarmType, disableAppointments, canSubscribeLink, attachmentsFolder }) {
|
||||
logInfo(`
|
||||
Initial settings:
|
||||
- AppVersion: ${appVersion}
|
||||
|
@ -172,6 +185,7 @@ Initial settings:
|
|||
- ForceEventAlarmType: ${forceEventAlarmType}
|
||||
- disableAppointments: ${disableAppointments}
|
||||
- CanSubscribeLink: ${canSubscribeLink}
|
||||
- attachmentsFolder: ${attachmentsFolder}
|
||||
`)
|
||||
|
||||
state.appVersion = appVersion
|
||||
|
@ -190,6 +204,7 @@ Initial settings:
|
|||
state.forceEventAlarmType = forceEventAlarmType
|
||||
state.disableAppointments = disableAppointments
|
||||
state.canSubscribeLink = canSubscribeLink
|
||||
state.attachmentsFolder = attachmentsFolder
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -410,6 +425,25 @@ const actions = {
|
|||
commit('setTimezone', { timezoneId })
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates the user's attachments folder
|
||||
*
|
||||
* @param {object} vuex The Vuex destructuring object
|
||||
* @param {object} vuex.state The Vuex state
|
||||
* @param {Function} vuex.commit The Vuex commit Function
|
||||
* @param {object} data The destructuring object
|
||||
* @param {string} data.attachmentsFolder The new attachments folder
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async setAttachmentsFolder({ state, commit }, { attachmentsFolder }) {
|
||||
if (state.attachmentsFolder === attachmentsFolder) {
|
||||
return
|
||||
}
|
||||
|
||||
await setConfig('attachmentsFolder', attachmentsFolder)
|
||||
commit('setAttachmentsFolder', { attachmentsFolder })
|
||||
},
|
||||
|
||||
/**
|
||||
* Initializes the calendar-js configuration
|
||||
*
|
||||
|
|
|
@ -104,6 +104,8 @@ import '@nextcloud/dialogs/styles/toast.scss'
|
|||
import Trashbin from '../components/AppNavigation/CalendarList/Trashbin.vue'
|
||||
import AppointmentConfigList from '../components/AppNavigation/AppointmentConfigList.vue'
|
||||
|
||||
import { createFolder } from '../services/attachmentService.js'
|
||||
|
||||
export default {
|
||||
name: 'Calendar',
|
||||
components: {
|
||||
|
@ -125,6 +127,7 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
loadingCalendars: true,
|
||||
loadingUser: true,
|
||||
timeFrameCacheExpiryJob: null,
|
||||
showEmptyCalendarScreen: false,
|
||||
}
|
||||
|
@ -133,6 +136,7 @@ export default {
|
|||
...mapGetters({
|
||||
timezoneId: 'getResolvedTimezone',
|
||||
hasTrashBin: 'hasTrashBin',
|
||||
currentUserPrincipal: 'getCurrentUserPrincipal',
|
||||
},
|
||||
),
|
||||
...mapState({
|
||||
|
@ -146,6 +150,7 @@ export default {
|
|||
timezone: state => state.settings.timezone,
|
||||
modificationCount: state => state.calendarObjects.modificationCount,
|
||||
disableAppointments: state => state.settings.disableAppointments,
|
||||
attachmentsFolder: state => state.settings.attachmentsFolder,
|
||||
}),
|
||||
defaultDate() {
|
||||
return getYYYYMMDDFromFirstdayParam(this.$route.params?.firstDay ?? 'now')
|
||||
|
@ -183,6 +188,14 @@ export default {
|
|||
return null
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
currentUserPrincipal() {
|
||||
if (this.currentUserPrincipal !== undefined && this.loadingUser) {
|
||||
createFolder(this.attachmentsFolder, this.currentUserPrincipal.userId)
|
||||
this.loadingUser = false
|
||||
}
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.timeFrameCacheExpiryJob = setInterval(() => {
|
||||
const timestamp = (getUnixTimestampFromDate(dateFactory()) - 60 * 10)
|
||||
|
@ -219,6 +232,7 @@ export default {
|
|||
forceEventAlarmType: loadState('calendar', 'force_event_alarm_type', false),
|
||||
disableAppointments: loadState('calendar', 'disable_appointments', false),
|
||||
canSubscribeLink: loadState('calendar', 'can_subscribe_link', false),
|
||||
attachmentsFolder: loadState('calendar', 'attachments_folder', false),
|
||||
})
|
||||
this.$store.dispatch('initializeCalendarJsConfig')
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
-->
|
||||
|
||||
<template>
|
||||
<AppSidebar :title="title"
|
||||
<NcAppSidebar :title="title"
|
||||
:title-editable="!isReadOnly && !isLoading"
|
||||
:title-placeholder="$t('calendar', 'Event title')"
|
||||
:subtitle="subTitle"
|
||||
|
@ -37,13 +37,12 @@
|
|||
<div class="icon icon-loading app-sidebar-tab-loading-indicator__icon" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="isError">
|
||||
<EmptyContent :title="$t('calendar', 'Event does not exist')" :description="error">
|
||||
<NcEmptyContent :title="$t('calendar', 'Event does not exist')" :description="error">
|
||||
<template #icon>
|
||||
<CalendarBlank :size="20" decorative />
|
||||
</template>
|
||||
</EmptyContent>
|
||||
</NcEmptyContent>
|
||||
</template>
|
||||
|
||||
<template #header>
|
||||
|
@ -52,37 +51,37 @@
|
|||
|
||||
<template v-if="!isLoading && !isError && !isNew"
|
||||
#secondary-actions>
|
||||
<ActionLink v-if="!hideEventExport && hasDownloadURL"
|
||||
<NcActionLink v-if="!hideEventExport && hasDownloadURL"
|
||||
:href="downloadURL">
|
||||
<template #icon>
|
||||
<Download :size="20" decorative />
|
||||
</template>
|
||||
{{ $t('calendar', 'Export') }}
|
||||
</ActionLink>
|
||||
<ActionButton v-if="!canCreateRecurrenceException && !isReadOnly" @click="duplicateEvent()">
|
||||
</NcActionLink>
|
||||
<NcActionButton v-if="!canCreateRecurrenceException && !isReadOnly" @click="duplicateEvent()">
|
||||
<template #icon>
|
||||
<ContentDuplicate :size="20" decorative />
|
||||
</template>
|
||||
{{ $t('calendar', 'Duplicate') }}
|
||||
</ActionButton>
|
||||
<ActionButton v-if="canDelete && !canCreateRecurrenceException" @click="deleteAndLeave(false)">
|
||||
</NcActionButton>
|
||||
<NcActionButton v-if="canDelete && !canCreateRecurrenceException" @click="deleteAndLeave(false)">
|
||||
<template #icon>
|
||||
<Delete :size="20" decorative />
|
||||
</template>
|
||||
{{ $t('calendar', 'Delete') }}
|
||||
</ActionButton>
|
||||
<ActionButton v-if="canDelete && canCreateRecurrenceException" @click="deleteAndLeave(false)">
|
||||
</NcActionButton>
|
||||
<NcActionButton v-if="canDelete && canCreateRecurrenceException" @click="deleteAndLeave(false)">
|
||||
<template #icon>
|
||||
<Delete :size="20" decorative />
|
||||
</template>
|
||||
{{ $t('calendar', 'Delete this occurrence') }}
|
||||
</ActionButton>
|
||||
<ActionButton v-if="canDelete && canCreateRecurrenceException" @click="deleteAndLeave(true)">
|
||||
</NcActionButton>
|
||||
<NcActionButton v-if="canDelete && canCreateRecurrenceException" @click="deleteAndLeave(true)">
|
||||
<template #icon>
|
||||
<Delete :size="20" decorative />
|
||||
</template>
|
||||
{{ $t('calendar', 'Delete this and all future') }}
|
||||
</ActionButton>
|
||||
</NcActionButton>
|
||||
</template>
|
||||
|
||||
<template v-if="!isLoading && !isError"
|
||||
|
@ -115,7 +114,7 @@
|
|||
@close="closeEditorAndSkipAction" />
|
||||
</template>
|
||||
|
||||
<AppSidebarTab v-if="!isLoading && !isError"
|
||||
<NcAppSidebarTab v-if="!isLoading && !isError"
|
||||
id="app-sidebar-tab-details"
|
||||
class="app-sidebar-tab"
|
||||
:name="$t('calendar', 'Details')"
|
||||
|
@ -170,16 +169,69 @@
|
|||
:is-editing-master-item="isEditingMasterItem"
|
||||
:is-recurrence-exception="isRecurrenceException"
|
||||
@force-this-and-all-future="forceModifyingFuture" />
|
||||
|
||||
<AttachmentsList v-if="!isLoading"
|
||||
:calendar-object-instance="calendarObjectInstance"
|
||||
:is-read-only="isReadOnly" />
|
||||
|
||||
<NcModal v-if="showModal && !isPrivate()"
|
||||
:title="t('calendar', 'Managing shared access')"
|
||||
@close="closeAttachmentsModal">
|
||||
<div class="modal-content">
|
||||
<div v-if="showPreloader" class="modal-content-preloader">
|
||||
<div :style="`width:${sharedProgress}%`" />
|
||||
</div>
|
||||
<div class="modal-h">
|
||||
{{ n('calendar', 'User requires access to your file', 'Users requires access to your file', showModalUsers.length) }}
|
||||
</div>
|
||||
<div class="users">
|
||||
<NcListItemIcon v-for="attendee in showModalUsers"
|
||||
:key="attendee.uri"
|
||||
class="user-list-item"
|
||||
:title="attendee.commonName"
|
||||
:subtitle="emailWithoutMailto(attendee.uri)"
|
||||
:is-no-user="true" />
|
||||
</div>
|
||||
<div class="modal-subtitle">
|
||||
{{ n('calendar', 'Attachment requiring shared access', 'Attachments requiring shared access', showModalNewAttachments.length) }}
|
||||
</div>
|
||||
<div class="attachments">
|
||||
<NcListItemIcon v-for="attachment in showModalNewAttachments"
|
||||
:key="attachment.xNcFileId"
|
||||
class="attachment-list-item"
|
||||
:title="getBaseName(attachment.fileName)"
|
||||
:url="getPreview(attachment)"
|
||||
:force-display-actions="false" />
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="modal-footer-checkbox">
|
||||
<NcCheckboxRadioSwitch v-if="!isPrivate()" :checked.sync="doNotShare">
|
||||
{{ t('calendar', 'Deny access') }}
|
||||
</NcCheckboxRadioSwitch>
|
||||
</div>
|
||||
<div class="modal-footer-buttons">
|
||||
<NcButton @click="closeAttachmentsModal">
|
||||
{{ t('calendar', 'Cancel') }}
|
||||
</NcButton>
|
||||
<NcButton type="primary"
|
||||
:disabled="showPreloader"
|
||||
@click="acceptAttachmentsModal(thisAndAllFuture)">
|
||||
{{ t('calendar', 'Invite') }}
|
||||
</NcButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</NcModal>
|
||||
</div>
|
||||
<SaveButtons v-if="showSaveButtons"
|
||||
class="app-sidebar-tab__buttons"
|
||||
:can-create-recurrence-exception="canCreateRecurrenceException"
|
||||
:is-new="isNew"
|
||||
:force-this-and-all-future="forceThisAndAllFuture"
|
||||
@save-this-only="saveAndLeave(false)"
|
||||
@save-this-and-all-future="saveAndLeave(true)" />
|
||||
</AppSidebarTab>
|
||||
<AppSidebarTab v-if="!isLoading && !isError"
|
||||
@save-this-only="prepareAccessForAttachments(false)"
|
||||
@save-this-and-all-future="prepareAccessForAttachments(true)" />
|
||||
</NcAppSidebarTab>
|
||||
<NcAppSidebarTab v-if="!isLoading && !isError"
|
||||
id="app-sidebar-tab-attendees"
|
||||
class="app-sidebar-tab"
|
||||
:name="$t('calendar', 'Attendees')"
|
||||
|
@ -197,14 +249,14 @@
|
|||
:can-create-recurrence-exception="canCreateRecurrenceException"
|
||||
:is-new="isNew"
|
||||
:force-this-and-all-future="forceThisAndAllFuture"
|
||||
@save-this-only="saveAndLeave(false)"
|
||||
@save-this-and-all-future="saveAndLeave(true)" />
|
||||
</AppSidebarTab>
|
||||
<AppSidebarTab v-if="!isLoading && !isError"
|
||||
@save-this-only="prepareAccessForAttachments(false)"
|
||||
@save-this-and-all-future="prepareAccessForAttachments(true)" />
|
||||
</NcAppSidebarTab>
|
||||
<NcAppSidebarTab v-if="!isLoading && !isError"
|
||||
id="app-sidebar-tab-resources"
|
||||
class="app-sidebar-tab"
|
||||
:name="$t('calendar', 'Resources')"
|
||||
:order="2">
|
||||
:order="3">
|
||||
<template #icon>
|
||||
<MapMarker :size="20" decorative />
|
||||
</template>
|
||||
|
@ -218,19 +270,27 @@
|
|||
:can-create-recurrence-exception="canCreateRecurrenceException"
|
||||
:is-new="isNew"
|
||||
:force-this-and-all-future="forceThisAndAllFuture"
|
||||
@save-this-only="saveAndLeave(false)"
|
||||
@save-this-and-all-future="saveAndLeave(true)" />
|
||||
</AppSidebarTab>
|
||||
</AppSidebar>
|
||||
@save-this-only="prepareAccessForAttachments(false)"
|
||||
@save-this-and-all-future="prepareAccessForAttachments(true)" />
|
||||
</NcAppSidebarTab>
|
||||
</NcAppSidebar>
|
||||
</template>
|
||||
<script>
|
||||
import AppSidebar from '@nextcloud/vue/dist/Components/NcAppSidebar.js'
|
||||
import AppSidebarTab from '@nextcloud/vue/dist/Components/NcAppSidebarTab.js'
|
||||
import ActionLink from '@nextcloud/vue/dist/Components/NcActionLink.js'
|
||||
import ActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js'
|
||||
import EmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js'
|
||||
import {
|
||||
NcAppSidebar,
|
||||
NcAppSidebarTab,
|
||||
NcActionLink,
|
||||
NcActionButton,
|
||||
NcEmptyContent,
|
||||
NcModal,
|
||||
NcListItemIcon,
|
||||
NcButton,
|
||||
NcCheckboxRadioSwitch,
|
||||
|
||||
} from '@nextcloud/vue'
|
||||
|
||||
import { mapState } from 'vuex'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
|
||||
import AlarmList from '../components/Editor/Alarm/AlarmList.vue'
|
||||
|
||||
|
@ -249,6 +309,7 @@ import PropertySelectMultiple from '../components/Editor/Properties/PropertySele
|
|||
import PropertyColor from '../components/Editor/Properties/PropertyColor.vue'
|
||||
import ResourceList from '../components/Editor/Resources/ResourceList.vue'
|
||||
import InvitationResponseButtons from '../components/Editor/InvitationResponseButtons.vue'
|
||||
import AttachmentsList from '../components/Editor/Attachments/AttachmentsList.vue'
|
||||
|
||||
import AccountMultiple from 'vue-material-design-icons/AccountMultiple.vue'
|
||||
import CalendarBlank from 'vue-material-design-icons/CalendarBlank.vue'
|
||||
|
@ -258,6 +319,9 @@ import ContentDuplicate from 'vue-material-design-icons/ContentDuplicate.vue'
|
|||
import InformationOutline from 'vue-material-design-icons/InformationOutline.vue'
|
||||
import MapMarker from 'vue-material-design-icons/MapMarker.vue'
|
||||
|
||||
import { shareFile } from '../services/attachmentService.js'
|
||||
import { Parameter } from '@nextcloud/calendar-js'
|
||||
|
||||
export default {
|
||||
name: 'EditSidebar',
|
||||
components: {
|
||||
|
@ -267,11 +331,15 @@ export default {
|
|||
SaveButtons,
|
||||
IllustrationHeader,
|
||||
AlarmList,
|
||||
AppSidebar,
|
||||
AppSidebarTab,
|
||||
ActionLink,
|
||||
ActionButton,
|
||||
EmptyContent,
|
||||
NcAppSidebar,
|
||||
NcAppSidebarTab,
|
||||
NcActionLink,
|
||||
NcActionButton,
|
||||
NcEmptyContent,
|
||||
NcModal,
|
||||
NcListItemIcon,
|
||||
NcButton,
|
||||
NcCheckboxRadioSwitch,
|
||||
InviteesList,
|
||||
PropertyCalendarPicker,
|
||||
PropertySelect,
|
||||
|
@ -286,14 +354,27 @@ export default {
|
|||
InformationOutline,
|
||||
MapMarker,
|
||||
InvitationResponseButtons,
|
||||
AttachmentsList,
|
||||
},
|
||||
mixins: [
|
||||
EditorMixin,
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
thisAndAllFuture: false,
|
||||
doNotShare: false,
|
||||
showModal: false,
|
||||
showModalNewAttachments: [],
|
||||
showModalUsers: [],
|
||||
sharedProgress: 0,
|
||||
showPreloader: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
locale: (state) => state.settings.momentLocale,
|
||||
hideEventExport: (state) => state.settings.hideEventExport,
|
||||
attachmentsFolder: state => state.settings.attachmentsFolder,
|
||||
}),
|
||||
accessClass() {
|
||||
return this.calendarObjectInstance?.accessClass || null
|
||||
|
@ -314,6 +395,12 @@ export default {
|
|||
|
||||
return moment(this.calendarObjectInstance.startDate).locale(this.locale).fromNow()
|
||||
},
|
||||
attachments() {
|
||||
return this.calendarObjectInstance?.attachments || null
|
||||
},
|
||||
currentUser() {
|
||||
return this.$store.getters.getCurrentUserPrincipal || null
|
||||
},
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
|
@ -405,11 +492,189 @@ export default {
|
|||
customColor,
|
||||
})
|
||||
},
|
||||
/**
|
||||
* Checks is the calendar event has attendees, but organizer or not
|
||||
*
|
||||
* @return {boolean}
|
||||
*/
|
||||
isPrivate() {
|
||||
return this.calendarObjectInstance.attendees.filter((attendee) => {
|
||||
if (this.currentUser.emailAddress.toLowerCase() !== (
|
||||
attendee.uri.split('mailto:').length === 2
|
||||
? attendee.uri.split('mailto:')[1].toLowerCase()
|
||||
: attendee.uri.toLowerCase()
|
||||
)) {
|
||||
return attendee
|
||||
}
|
||||
return false
|
||||
}).length === 0
|
||||
},
|
||||
getPreview(attachment) {
|
||||
if (attachment.xNcHasPreview) {
|
||||
return generateUrl(`/core/preview?fileId=${attachment.xNcFileId}&x=100&y=100&a=0`)
|
||||
}
|
||||
return attachment.formatType ? OC.MimeType.getIconUrl(attachment.formatType) : null
|
||||
},
|
||||
acceptAttachmentsModal() {
|
||||
if (!this.doNotShare) {
|
||||
const total = this.showModalNewAttachments.length
|
||||
this.showPreloader = true
|
||||
if (!this.isPrivate()) {
|
||||
this.showModalNewAttachments.map(async (attachment, i) => {
|
||||
// console.log('Add share', attachment)
|
||||
this.sharedProgress = Math.ceil(100 * (i + 1) / total)
|
||||
|
||||
// add share + change attachment
|
||||
try {
|
||||
const data = await shareFile(`${this.attachmentsFolder}${attachment.fileName}`)
|
||||
attachment.shareTypes = data?.share_type?.toString()
|
||||
if (typeof attachment.attachmentProperty.getParameter('X-NC-SHARED-TYPES') === 'undefined') {
|
||||
const xNcSharedTypes = new Parameter('X-NC-SHARED-TYPES', attachment.shareTypes)
|
||||
attachment.attachmentProperty.setParameter(xNcSharedTypes)
|
||||
}
|
||||
attachment.attachmentProperty.uri = data?.url
|
||||
attachment.uri = data?.url
|
||||
// toastify success
|
||||
} catch (e) {
|
||||
// toastify err
|
||||
console.error(e)
|
||||
}
|
||||
return attachment
|
||||
})
|
||||
|
||||
} else {
|
||||
// TODO it is not possible to delete shares, because share ID needed
|
||||
/* this.showModalNewAttachments.map((attachment, i) => {
|
||||
this.sharedProgress += Math.ceil(100 * (i + 1) / total)
|
||||
return attachment
|
||||
}) */
|
||||
}
|
||||
}
|
||||
setTimeout(() => {
|
||||
this.showPreloader = false
|
||||
this.sharedProgress = 0
|
||||
this.showModal = false
|
||||
this.showModalNewAttachments = []
|
||||
this.showModalUsers = []
|
||||
this.saveEvent(this.thisAndAllFuture)
|
||||
}, 500)
|
||||
// trigger save event after make each attachment access
|
||||
// 1) if !isPrivate get attachments NOT SHARED and SharedType is empry -> API ADD SHARE
|
||||
// 2) if isPrivate get attachments SHARED and SharedType is not empty -> API DELETE SHARE
|
||||
// 3) update calendarObject while pending access change
|
||||
// 4) after all access changes, save Event trigger
|
||||
// 5) done
|
||||
},
|
||||
closeAttachmentsModal() {
|
||||
this.showModal = false
|
||||
},
|
||||
emailWithoutMailto(mailto) {
|
||||
return mailto.split('mailto:').length === 2
|
||||
? mailto.split('mailto:')[1].toLowerCase()
|
||||
: mailto.toLowerCase()
|
||||
},
|
||||
getBaseName(name) {
|
||||
return name.split('/').pop()
|
||||
},
|
||||
prepareAccessForAttachments(thisAndAllFuture = false) {
|
||||
this.thisAndAllFuture = thisAndAllFuture
|
||||
const newAttachments = this.calendarObjectInstance.attachments.filter(attachment => {
|
||||
// get only new attachments
|
||||
// TODO get NOT only new attachments =) Maybe we should filter all attachments without share-type, 'cause event can be private and AFTER save owner could add new participant
|
||||
return !this.isPrivate() ? attachment.isNew && attachment.shareTypes === null : attachment.isNew && attachment.shareTypes !== null
|
||||
})
|
||||
// if there are new attachment and event not saved
|
||||
if (newAttachments.length > 0 && !this.isPrivate()) {
|
||||
// and is event NOT private,
|
||||
// then add share to each attachment
|
||||
// only if attachment['share-types'] is null or empty
|
||||
this.showModal = true
|
||||
this.showModalNewAttachments = newAttachments
|
||||
this.showModalUsers = this.calendarObjectInstance.attendees.filter((attendee) => {
|
||||
if (this.currentUser.emailAddress.toLowerCase() !== this.emailWithoutMailto(attendee.uri)) {
|
||||
return attendee
|
||||
}
|
||||
return false
|
||||
})
|
||||
} else {
|
||||
this.saveEvent(thisAndAllFuture)
|
||||
}
|
||||
},
|
||||
saveEvent(thisAndAllFuture = false) {
|
||||
// if there is new attachments and !private, then make modal with users and files/
|
||||
// maybe check shared access before add file
|
||||
this.saveAndLeave(thisAndAllFuture)
|
||||
this.calendarObjectInstance.attachments = this.calendarObjectInstance.attachments.map(attachment => {
|
||||
if (attachment.isNew) {
|
||||
delete attachment.isNew
|
||||
}
|
||||
return attachment
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.modal-content {
|
||||
padding: 16px;
|
||||
position: relative;
|
||||
|
||||
.modal-content-preloader {
|
||||
position: absolute;
|
||||
top:0;
|
||||
left:0;
|
||||
right:0;
|
||||
height: 6px;
|
||||
|
||||
div {
|
||||
position: absolute;
|
||||
top:0;
|
||||
left: 0;
|
||||
background: var(--color-primary-element);
|
||||
height: 6px;
|
||||
transition: width 0.3s linear;
|
||||
}
|
||||
}
|
||||
}
|
||||
.modal-subtitle {
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
.modal-h {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.modal-footer-buttons {
|
||||
display: flex;
|
||||
|
||||
:first-child {
|
||||
margin-right: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.attachments, .users {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
::v-deep .attachments .avatardiv img {
|
||||
border-radius: 0;
|
||||
}
|
||||
.attachment-list-item, .user-list-item {
|
||||
width: 50%
|
||||
}
|
||||
.attachment-icon {
|
||||
width: 40px;
|
||||
height: auto;
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
::v-deep .app-sidebar-header__description {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
|
|
@ -78,6 +78,7 @@ describe('Test suite: Event model (models/event.js)', () => {
|
|||
alarms: [],
|
||||
customColor: null,
|
||||
categories: [],
|
||||
attachments: [],
|
||||
})
|
||||
|
||||
expect(getDefaultRecurrenceRuleObject).toHaveBeenCalledTimes(1)
|
||||
|
@ -119,6 +120,7 @@ describe('Test suite: Event model (models/event.js)', () => {
|
|||
alarms: [],
|
||||
customColor: null,
|
||||
categories: [],
|
||||
attachments: [],
|
||||
otherProp: 'foo',
|
||||
})
|
||||
|
||||
|
@ -168,6 +170,7 @@ describe('Test suite: Event model (models/event.js)', () => {
|
|||
alarms: [],
|
||||
customColor: null,
|
||||
categories: [],
|
||||
attachments: [],
|
||||
})
|
||||
|
||||
expect(getDateFromDateTimeValue).toHaveBeenCalledTimes(2)
|
||||
|
@ -235,6 +238,7 @@ describe('Test suite: Event model (models/event.js)', () => {
|
|||
alarms: [],
|
||||
customColor: null,
|
||||
categories: [],
|
||||
attachments: []
|
||||
})
|
||||
|
||||
expect(getDateFromDateTimeValue).toHaveBeenCalledTimes(2)
|
||||
|
@ -301,6 +305,7 @@ describe('Test suite: Event model (models/event.js)', () => {
|
|||
],
|
||||
customColor: null,
|
||||
categories: [],
|
||||
attachments: [],
|
||||
})
|
||||
|
||||
const alarms = eventComponent.getAlarmList()
|
||||
|
@ -354,6 +359,7 @@ describe('Test suite: Event model (models/event.js)', () => {
|
|||
alarms: [],
|
||||
customColor: null,
|
||||
categories: ['BUSINESS', 'HUMAN RESOURCES'],
|
||||
attachments: [],
|
||||
})
|
||||
|
||||
expect(getDateFromDateTimeValue).toHaveBeenCalledTimes(2)
|
||||
|
@ -409,6 +415,7 @@ describe('Test suite: Event model (models/event.js)', () => {
|
|||
alarms: [],
|
||||
customColor: '#eeffee',
|
||||
categories: [],
|
||||
attachments: [],
|
||||
})
|
||||
|
||||
expect(getDateFromDateTimeValue).toHaveBeenCalledTimes(2)
|
||||
|
@ -467,6 +474,7 @@ describe('Test suite: Event model (models/event.js)', () => {
|
|||
alarms: [],
|
||||
customColor: null,
|
||||
categories: [],
|
||||
attachments: [],
|
||||
})
|
||||
|
||||
expect(getDateFromDateTimeValue).toHaveBeenCalledTimes(2)
|
||||
|
@ -522,6 +530,7 @@ describe('Test suite: Event model (models/event.js)', () => {
|
|||
alarms: [],
|
||||
customColor: null,
|
||||
categories: [],
|
||||
attachments: [],
|
||||
})
|
||||
|
||||
expect(getDateFromDateTimeValue).toHaveBeenCalledTimes(2)
|
||||
|
@ -574,6 +583,7 @@ describe('Test suite: Event model (models/event.js)', () => {
|
|||
alarms: [],
|
||||
customColor: null,
|
||||
categories: [],
|
||||
attachments: [],
|
||||
})
|
||||
|
||||
expect(getDateFromDateTimeValue).toHaveBeenCalledTimes(2)
|
||||
|
@ -626,6 +636,7 @@ describe('Test suite: Event model (models/event.js)', () => {
|
|||
alarms: [],
|
||||
customColor: null,
|
||||
categories: [],
|
||||
attachments: [],
|
||||
})
|
||||
|
||||
expect(getDateFromDateTimeValue).toHaveBeenCalledTimes(2)
|
||||
|
@ -682,6 +693,7 @@ describe('Test suite: Event model (models/event.js)', () => {
|
|||
alarms: [],
|
||||
customColor: null,
|
||||
categories: [],
|
||||
attachments: [],
|
||||
})
|
||||
|
||||
expect(getDateFromDateTimeValue).toHaveBeenCalledTimes(2)
|
||||
|
@ -737,6 +749,7 @@ describe('Test suite: Event model (models/event.js)', () => {
|
|||
alarms: [],
|
||||
customColor: null,
|
||||
categories: [],
|
||||
attachments: [],
|
||||
})
|
||||
|
||||
expect(getDateFromDateTimeValue).toHaveBeenCalledTimes(2)
|
||||
|
@ -790,6 +803,7 @@ describe('Test suite: Event model (models/event.js)', () => {
|
|||
alarms: [],
|
||||
customColor: null,
|
||||
categories: [],
|
||||
attachments: [],
|
||||
})
|
||||
|
||||
expect(getDateFromDateTimeValue).toHaveBeenCalledTimes(2)
|
||||
|
@ -846,6 +860,7 @@ describe('Test suite: Event model (models/event.js)', () => {
|
|||
alarms: [],
|
||||
customColor: null,
|
||||
categories: [],
|
||||
attachments: [],
|
||||
})
|
||||
|
||||
expect(getDateFromDateTimeValue).toHaveBeenCalledTimes(2)
|
||||
|
|
|
@ -65,6 +65,7 @@ describe('store/settings test suite', () => {
|
|||
momentLocale: 'en',
|
||||
disableAppointments: false,
|
||||
canSubscribeLink: true,
|
||||
attachmentsFolder: '/Calendar',
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -175,6 +176,7 @@ describe('store/settings test suite', () => {
|
|||
forceEventAlarmType: false,
|
||||
disableAppointments: false,
|
||||
canSubscribeLink: true,
|
||||
attachmentsFolder: '/Calendar',
|
||||
}
|
||||
|
||||
const settings = {
|
||||
|
@ -195,6 +197,7 @@ describe('store/settings test suite', () => {
|
|||
forceEventAlarmType: false,
|
||||
disableAppointments: false,
|
||||
canSubscribeLink: true,
|
||||
attachmentsFolder: '/Attachments',
|
||||
}
|
||||
|
||||
settingsStore.mutations.loadSettingsFromServer(state, settings)
|
||||
|
@ -218,6 +221,7 @@ Initial settings:
|
|||
- ForceEventAlarmType: false
|
||||
- disableAppointments: false
|
||||
- CanSubscribeLink: true
|
||||
- attachmentsFolder: /Attachments
|
||||
`)
|
||||
expect(state).toEqual({
|
||||
appVersion: '2.1.0',
|
||||
|
@ -238,6 +242,7 @@ Initial settings:
|
|||
forceEventAlarmType: false,
|
||||
disableAppointments: false,
|
||||
canSubscribeLink: true,
|
||||
attachmentsFolder: '/Attachments',
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
@ -113,6 +113,7 @@ class ViewControllerTest extends TestCase {
|
|||
['user123', 'calendar', 'showWeekNr', 'defaultShowWeekNr', 'yes'],
|
||||
['user123', 'calendar', 'skipPopover', 'defaultSkipPopover', 'yes'],
|
||||
['user123', 'calendar', 'timezone', 'defaultTimezone', 'Europe/Berlin'],
|
||||
['user123', 'dav', 'attachmentsFolder', '/Calendar', '/Calendar'],
|
||||
['user123', 'calendar', 'slotDuration', 'defaultSlotDuration', '00:15:00'],
|
||||
['user123', 'calendar', 'defaultReminder', 'defaultDefaultReminder', '00:10:00'],
|
||||
['user123', 'calendar', 'showTasks', 'defaultShowTasks', '00:15:00'],
|
||||
|
@ -146,6 +147,7 @@ class ViewControllerTest extends TestCase {
|
|||
['talk_enabled', true],
|
||||
['talk_api_version', 'v4'],
|
||||
['timezone', 'Europe/Berlin'],
|
||||
['attachments_folder', '/Calendar'],
|
||||
['slot_duration', '00:15:00'],
|
||||
['default_reminder', '00:10:00'],
|
||||
['show_tasks', false],
|
||||
|
@ -194,6 +196,7 @@ class ViewControllerTest extends TestCase {
|
|||
['user123', 'calendar', 'showWeekNr', 'defaultShowWeekNr', 'yes'],
|
||||
['user123', 'calendar', 'skipPopover', 'defaultSkipPopover', 'yes'],
|
||||
['user123', 'calendar', 'timezone', 'defaultTimezone', 'Europe/Berlin'],
|
||||
['user123', 'dav', 'attachmentsFolder', '/Calendar', '/Calendar'],
|
||||
['user123', 'calendar', 'slotDuration', 'defaultSlotDuration', '00:15:00'],
|
||||
['user123', 'calendar', 'defaultReminder', 'defaultDefaultReminder', '00:10:00'],
|
||||
['user123', 'calendar', 'showTasks', 'defaultShowTasks', '00:15:00'],
|
||||
|
@ -223,6 +226,7 @@ class ViewControllerTest extends TestCase {
|
|||
['talk_enabled', false],
|
||||
['talk_api_version', 'v1'],
|
||||
['timezone', 'Europe/Berlin'],
|
||||
['attachments_folder', '/Calendar'],
|
||||
['slot_duration', '00:15:00'],
|
||||
['default_reminder', '00:10:00'],
|
||||
['show_tasks', false],
|
||||
|
|
Loading…
Reference in New Issue