add Calendar list

Signed-off-by: Georg Ehrke <developer@georgehrke.com>
This commit is contained in:
Georg Ehrke 2018-11-07 11:36:21 +01:00
parent c43116302b
commit 3f8874a64f
No known key found for this signature in database
GPG Key ID: 9D98FD9380A1CB43
30 changed files with 2075 additions and 805 deletions

View File

@ -3,7 +3,7 @@
* Calendar App
*
* @author Georg Ehrke
* @copyright 2016 Georg Ehrke <oc.list@georgehrke.com>
* @copyright 2018 Georg Ehrke <oc.list@georgehrke.com>
* @author Thomas Müller
* @copyright 2016 Thomas Müller <thomas.mueller@tmit.eu>
*
@ -28,14 +28,14 @@ return [
// we need to reflect all our javascript routes here as well,
// so that you don't get forwarded to the files app on reload
['name' => 'view#indexViewTimerange', 'url' => '/month/{timeRange}/', 'verb' => 'GET'],
['name' => 'view#indexViewTimerange', 'url' => '/agendaDay/{timeRange}/', 'verb' => 'GET'],
['name' => 'view#indexViewTimerange', 'url' => '/agendaWeek/{timeRange}/', 'verb' => 'GET'],
// ['name' => 'view#indexViewTimerange', 'url' => '/agendaDay/{timeRange}/', 'verb' => 'GET'],
// ['name' => 'view#indexViewTimerange', 'url' => '/agendaWeek/{timeRange}/', 'verb' => 'GET'],
['name' => 'view#indexViewTimerangeNew', 'url' => '/month/{timeRange}/new/{mode}/{recurrenceId}/', 'verb' => 'GET'],
['name' => 'view#indexViewTimerangeNew', 'url' => '/agendaDay/{timeRange}/new/{mode}/{recurrenceId}/', 'verb' => 'GET'],
['name' => 'view#indexViewTimerangeNew', 'url' => '/agendaWeek/{timeRange}/new/{mode}/{recurrenceId}/', 'verb' => 'GET'],
// ['name' => 'view#indexViewTimerangeNew', 'url' => '/agendaDay/{timeRange}/new/{mode}/{recurrenceId}/', 'verb' => 'GET'],
// ['name' => 'view#indexViewTimerangeNew', 'url' => '/agendaWeek/{timeRange}/new/{mode}/{recurrenceId}/', 'verb' => 'GET'],
['name' => 'view#indexViewTimerangeEdit', 'url' => '/month/{timeRange}/edit/{mode}/{objectId}/{recurrenceId}/', 'verb' => 'GET'],
['name' => 'view#indexViewTimerangeEdit', 'url' => '/agendaDay/{timeRange}/edit/{mode}/{objectId}/{recurrenceId}/', 'verb' => 'GET'],
['name' => 'view#indexViewTimerangeEdit', 'url' => '/agendaWeek/{timeRange}/edit/{mode}/{objectId}/{recurrenceId}/', 'verb' => 'GET'],
// ['name' => 'view#indexViewTimerangeEdit', 'url' => '/agendaDay/{timeRange}/edit/{mode}/{objectId}/{recurrenceId}/', 'verb' => 'GET'],
// ['name' => 'view#indexViewTimerangeEdit', 'url' => '/agendaWeek/{timeRange}/edit/{mode}/{objectId}/{recurrenceId}/', 'verb' => 'GET'],
['name' => 'view#public_index_with_branding', 'url' => '/p/{token}', 'verb' => 'GET'],
['name' => 'view#public_index_with_branding_and_fancy_name', 'url' => '/p/{token}/{fancyName}', 'verb' => 'GET'],

View File

@ -63,6 +63,7 @@
@import 'confirmation.scss';
@import 'datepicker.scss';
@import 'eventdialog.scss';
@import 'fullcalendar.scss';
@import 'globals.scss';
@import 'print.scss';
@import 'settings.scss';

View File

@ -22,575 +22,208 @@
*
*/
/* fallback, TODO remove when min nc version >=13 */
$color-border: nc-darken($color-main-background, 8%) !default;
#app-navigation {
#app-navigation .app-navigation-input {
width: 95%;
}
// This is used for the Datepicker, the view buttons and the today button
.button-group {
#app-navigation .calendarlist-fieldset {
padding: 0 5px;
}
display: flex;
margin: 0 5px;
width: calc(100% - 10px);
#app-navigation .color-button {
height: 25px;
border: 0;
}
&:first-of-type {
margin-top: 3px;
}
#app-navigation .new-entity,
#app-navigation .subscription-title {
display: block;
width: 100%;
line-height: 44px;
padding: 0 44px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
opacity: .5;
-webkit-box-sizing: border-box;
box-sizing: border-box;
box-shadow: none !important; /* override server */
}
.button {
// this border-radius affects the button in the middle of the group
// for the rounded corner buttons on the sides, see further below
border-radius: 0;
font-weight: normal;
margin: 3px -1px 3px 0;
padding: 8px;
flex-grow: 1;
}
#app-navigation .new-entity,
#app-navigation .new-entity .new-entity-title {
cursor: pointer;
}
.button.datepicker-label {
flex-grow: 4;
text-align: center;
}
#app-navigation .add-new .new-accept-button {
position: relative;
}
.button.active {
background-color: $color-primary;
color: $color-primary-text;
}
#app-navigation .add-new .add-new-is-processing {
margin-left: -16px;
left: -5px;
}
.button:hover,
.button:focus,
.button.active {
z-index: 50;
}
#app-navigation .add-new .color-button {
height: 25px;
border: 0;
background: #1a1a1a;
}
.button:only-child {
border-radius: 3px;
}
.add-new-subscription .calendarlist-fieldset input {
width: 130px;
margin-right: 2px;
}
.button:last-child {
margin-right: 0;
}
.add-new-subscription .calendarlist-fieldset select {
width: 80px;
}
.button:first-child:not(:only-of-type) {
border-radius: 3px 0 0 3px;
}
#app-navigation .app-navigation-list {
width: 100%;
}
.button:last-child:not(:only-of-type) {
border-radius: 0 3px 3px 0;
}
/* TODO remove when 13 is lower version: ref app-navigation-entry-bullet */
#app-navigation .app-navigation-list-item .calendarCheckbox,
#publicinformationscontainer .calendarCheckbox {
position: absolute;
margin-left: 17px;
margin-top: 16px;
width: 12px;
height: 12px;
border-radius: 50%;
cursor: pointer;
border: 1px solid var(--color-border-dark);
}
.button:not(:only-of-type):not(:first-child):not(:last-child) {
border-radius: 0;
}
#app-navigation .app-navigation-list-item .calendarCheckbox.unchecked {
background: transparent !important;
}
.mx-datepicker {
margin-left: 50%;
margin-top: 48px;
position: absolute;
width: 0;
}
#app-navigation .app-navigation-list-item .app-navigation-entry-utils-menu-button button {
opacity: .3 !important;
}
}
#app-navigation .app-navigation-list-item .app-navigation-entry-utils-menu-button button:hover,
#app-navigation .app-navigation-list-item .app-navigation-entry-utils-menu-button button.icon-public,
#app-navigation .app-navigation-list-item .app-navigation-entry-utils-menu-button button.icon-shared.shared-style {
opacity: .7 !important;
}
ul {
/* z-index of "New subscription" is 250. Prevent overlapping issues by setting z-index of menu to something higher */
#app-navigation .app-navigation-list-item .app-navigation-entry-menu {
z-index: 300;
}
// New Calendar / New Subscription
> li.new-entity-container {
#app-navigation .app-navigation-list-item .icon-loading-small {
position: absolute;
margin-left: 15px;
margin-top: 14px;
}
div.app-navigation-entry-edit {
padding-left: 5px !important;
}
#app-navigation .icon-loading-small.loader-list:hover,
#app-navigation .icon-loading-small.loader-list:hover a {
box-shadow: none;
}
input:not([type='text']):last-child {
border-top-right-radius: 3px;
border-bottom-right-radius: 3px;
}
#app-navigation .app-navigation-list-item .utils {
height: 44px;
padding: 0 5px 0 0;
}
span.icon-loading-small {
position: absolute;
margin-top: 14px;
margin-left: calc(50% - 36px);
}
}
#app-navigation .app-navigation-list-item .utils .action span {
opacity: .3;
}
// Calendar list items / Subscription list items
> li.app-navigation-list-item {
#app-navigation .app-navigation-list-item .utils .action.withitems span {
opacity: .7 !important;
}
&.icon.icon-loading {
margin-top: 22px;
margin-bottom: 44px;
}
#app-navigation .app-navigation-list-item .utils .action span:hover {
opacity: 1;
}
&.separator {
height: 44px;
}
#app-navigation .app-navigation-list-item .active {
/* Core Override */
background: transparent !important;
}
li.app-navigation-entry-utils-menu-button {
#app-navigation .app-navigation-list-item .active a {
/* Core Override */
background-color: transparent !important;
}
button.icon-public,
button.icon-shared {
opacity: 1 !important;
}
#app-navigation .app-navigation-list-item .active:hover .calendarlist-icon {
/* Core Override */
background-color: transparent !important;
}
button:hover,
button:focus {
opacity: 1 !important;
}
#app-navigation .app-navigation-list-item .editfieldset {
padding: 0 4px;
}
}
#app-navigation .app-navigation-list-item .editfieldset .calendartype {
width: 100%;
}
div.sharing-section {
#app-navigation .buttongroups .accept-button {
width: 47%;
padding: 17px;
margin-right: 7px;
}
#app-navigation .editfieldset .close-button {
width: 47%;
padding: 17px;
}
#app-navigation .app-navigation-list-item .caldavURL input {
width: 75%;
}
#app-navigation .app-navigation-entry-menu li {
width: auto !important;
float: inherit;
height: 36px;
}
#app-navigation .app-navigation-entry-menu li button {
float: inherit !important;
margin: 0;
padding: 0;
width: 100% !important;
}
#app-navigation .app-navigation-entry-menu li span {
display: inline-block;
height: 36px;
line-height: 36px;
padding-right: 10px;
font-weight: 400;
float: left;
}
#app-navigation .app-navigation-entry-menu li span.svg {
padding: 5px;
width: 36px;
box-sizing: border-box;
}
#app-navigation .app-navigation-list-item .utils {
display: inline-block;
}
#app-navigation .app-navigation-list-item .utils span.action.withitems {
display: inline-block;
vertical-align: top;
}
#app-navigation .app-navigation-list-item .utils .action span {
display: inline-block;
line-height: 20px;
height: 20px;
width: 16px;
padding: 12px 6px;
cursor: pointer;
vertical-align: top;
}
#app-navigation .app-navigation-list-item .utils .action .icon-shared,
#app-navigation .app-navigation-list-item .utils .action .icon-public {
padding: 12px 0;
}
#app-navigation .app-navigation-list-item span.calendarlist-icon.shared {
width: 40px;
opacity: 1;
padding-left: 0;
padding-right: 5px;
}
#app-navigation .app-navigation-list-item .calendar-share-list .utils .action span {
padding: 0 6px;
}
#app-navigation .app-navigation-list-item:hover .utils.active {
display: inline-block;
}
#app-navigation .app-navigation-list-item:hover .utils > a {
background-color: transparent !important;
}
/* Workaround for input edit */
#app-navigation > ul > li > .app-navigation-entry-edit {
padding-left: 5px !important;
}
#app-navigation .refresh-shares {
position: absolute;
right: 15px;
height: 15px;
width: 15px;
top: 15px;
z-index: 1000;
}
#app-navigation .loader-list::after {
left: 50% !important;
top: 50% !important;
}
/* ColorPicker overrides. */
.colorpicker {
display: block;
height: auto;
padding-bottom: 3px;
padding-top: 3px;
}
.colorpicker .colorpicker-list {
display: flex;
width: 100%;
height: 100%;
text-align: center;
justify-content: center;
align-items: center;
}
.colorpicker .colorpicker-list li {
height: 24px;
width: 24px !important;
}
.colorpicker .colorpicker-list li.selected {
border: 1px solid #333;
}
.colorpicker .colorpicker-list li.randomcolour {
background-image: url('../../img/random.svg');
background-repeat: no-repeat;
background-position: center center;
}
.colorpicker .colorpicker-list .color-selector-label {
display: block;
height: 24px;
width: 24px !important;
background-image: url('../../img/color_picker.svg');
background-repeat: no-repeat;
background-position: center center;
}
.colorpicker .colorpicker-list .color-selector-label .color-selector {
visibility: hidden;
}
ul#calendarlistcontainer {
margin-bottom: 44px;
}
#datepicker-ng-show-container {
display: block !important;
height: 205px;
overflow: hidden;
transition: 200ms ease-in-out all;
}
#datepicker-ng-show-container.ng-hide {
height: 0;
}
div.uib-daypicker {
display: flex;
table {
flex: 1;
tbody {
flex: 1;
display: flex;
flex-direction: column;
tr.uib-weeks {
flex: 1;
display: flex;
box-shadow: inset 4px 0 var(--color-primary);
padding-left: 12px;
padding-right: 12px;
width: 100%;
justify-content: space-evenly;
td.uib-day {
div.multiselect {
width: calc(100% - 14px);
max-width: none;
#users-groups-search {
padding-left: 6px !important;
}
}
.oneline {
white-space: nowrap;
position: relative;
}
.shareWithList {
list-style-type: none;
display: flex;
flex: 1;
align-items: center;
justify-content: center;
flex-direction: column;
> li {
height: 44px;
white-space: normal;
display: inline-flex;
align-items: center;
position: relative;
.avatar {
width: 32px;
height: 32px;
background-color: #DBDBDB;
}
.avatar.published {
background-color: var(--color-primary);
}
.username {
padding: 0 8px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
> .sharingOptionsGroup {
margin-left: auto;
display: flex;
align-items: center;
white-space: nowrap;
> a:hover,
> a:focus,
> .share-menu > a:hover,
> .share-menu > a:focus {
box-shadow: none !important;
opacity: 1 !important;
}
> .icon:not(.hidden),
> .share-menu .icon:not(.hidden){
padding: 14px;
height: 44px;
width: 44px;
opacity: 0.5;
display: block;
cursor: pointer;
}
> .share-menu {
position: relative;
display: block;
}
}
}
}
}
}
li.spacer {
box-shadow: none !important;
}
}
}
/* Spacing */
#spacer {
height: 44px;
box-shadow: none !important;
}
/* Public */
#app-navigation .davbuttons {
width: 100%;
height: 44px;
}
#app-navigation .davbuttons .button {
font-weight: normal;
padding: 8px;
width: 48%;
}
#app-navigation .davbuttons .button.first {
margin-left: 5px;
-webkit-border-radius: 3px 0 0 3px;
-ms-border-radius: 3px 0 0 3px;
border-radius: 3px 0 0 3px;
}
#app-navigation .davbuttons .button.last {
margin-left: -8px;
-webkit-border-radius: 0 3px 3px 0;
-moz-border-radius: 0 3px 3px 0;
-ms-border-radius: 0 3px 3px 0;
border-radius: 0 3px 3px 0;
}
.public-calendar-name {
font-size: 20px;
padding: 5px;
border-radius: 5px;
margin: 5px auto;
}
input.public-linkinput {
width: 94%;
margin: 6px auto;
}
#publicinformationscontainer .calendarCheckbox {
cursor: default;
margin-top: 5px;
}
.public-ics-download {
width: 100%;
display: block;
height: 25px;
font-weight: 300;
font-size: 14px;
cursor: pointer;
z-index: 10;
padding: 7px 0 0;
text-align: center;
background-color: rgba(240, 240, 240, .9);
border: 1px solid rgba(240, 240, 240, .9);
background-position: 5%;
border-radius: 3px;
margin: 7px auto;
}
.integration-code {
width: 93%;
height: 100px;
margin: 5px auto;
}
.public-left-side {
margin: 30px 10px auto;
}
.public-left-side .avatardiv {
margin: 0 auto;
}
.public-left-side h3 {
text-align: center;
margin: 10px 0 5px;
}
.public-left-side .icon-loading-small {
margin: 10px auto;
}
/* Publishing */
.publishing {
margin: auto 10px;
}
.publication-tools {
cursor: pointer;
margin: 5px auto auto 5px;
}
input.mailerInput {
width: 94%;
}
.mailerInput + button {
width: 100%;
padding: 6px;
}
/* Calendar Sharing */
.calendarShares {
width: 100%;
}
input.shareeInput {
width: calc(100% - 12px);
margin: 6px;
}
li.calendar-share-item {
margin-left: 12px;
width: 95% !important;
text-overflow: ellipsis;
overflow: hidden;
padding-right: 33px;
}
li.calendar-share-item span {
white-space: nowrap;
}
ul.dropdown-menu {
max-height: 200px;
overflow-y: scroll;
overflow-x: hidden;
border: 1px #ddd solid;
border-radius: 0 0 4px 4px;
margin-top: -7px;
margin-left: 6px;
width: 89%;
}
ul.dropdown-menu li:last-child {
border-bottom: none !important;
}
#app-navigation ul.dropdown-menu a {
padding: 6px;
}
ul.dropdown-menu li > a:hover {
background-color: grey !important;
}
/* Fullcalendar modifications */
.fc th,
.fc .fc-axis,
.fc-day-grid-event .fc-time,
.fc-ltr .fc-basic-view .fc-day-number,
.fc-ltr .fc-basic-view .fc-week-number,
.fc-time-grid-event .fc-time {
opacity: .8;
font-size: 80%;
font-weight: normal;
}
.fc-basic-view .fc-day-top .fc-week-number {
background-color: $color-border;
color: $color-main-text;
}
.fc-day-number.fc-other-month {
opacity: .1 !important;
}
.fc td.fc-widget-header {
border-top: none;
}
.fc th,
.fc td {
border-left: none;
border-bottom: none;
}
/* border styles for grid, highlight full-hour horizontal lines */
.fc-unthemed tr td {
border-top-color: $color-border;
}
.fc-unthemed tr:nth-child(even) td {
border-top-color: $color-border;
}
.fc-unthemed th,
.fc-unthemed td,
.fc-unthemed thead,
.fc-unthemed tbody,
.fc-unthemed .fc-divider,
.fc-unthemed .fc-row,
.fc-unthemed .fc-popover {
/* fallback, TODO remove when min nc version >=13 */
border-color: $color-border;
border-left-color: $color-border;
border-right-color: $color-border;
}
/* properly size events */
.fc-event {
font-size: 13px;
line-height: initial;
padding-left: 3px;
}
.app-navigation-entry-menu .icon-link {
background-size: 16px;
}
/* highlight today day number */
.fc-today > .fc-day-number {
background: $color-primary;
color: $color-primary-text;
border-radius: 50%;
padding: 3px 8px;
min-width: 12px;
margin: 3px;
text-align: center;
font-weight: bold !important;
}

70
css/app/fullcalendar.scss Normal file
View File

@ -0,0 +1,70 @@
/* Fullcalendar modifications */
.fc th,
.fc .fc-axis,
.fc-day-grid-event .fc-time,
.fc-ltr .fc-basic-view .fc-day-number,
.fc-ltr .fc-basic-view .fc-week-number,
.fc-time-grid-event .fc-time {
opacity: .8;
font-size: 80%;
font-weight: normal;
}
.fc-basic-view .fc-day-top .fc-week-number {
background-color: $color-border;
color: $color-main-text;
}
.fc-day-number.fc-other-month {
opacity: .1 !important;
}
.fc td.fc-widget-header {
border-top: none;
}
.fc th,
.fc td {
border-left: none;
border-bottom: none;
}
/* border styles for grid, highlight full-hour horizontal lines */
.fc-unthemed tr td {
border-top-color: $color-border;
}
.fc-unthemed tr:nth-child(even) td {
border-top-color: $color-border;
}
.fc-unthemed th,
.fc-unthemed td,
.fc-unthemed thead,
.fc-unthemed tbody,
.fc-unthemed .fc-divider,
.fc-unthemed .fc-row,
.fc-unthemed .fc-popover {
/* fallback, TODO remove when min nc version >=13 */
border-color: $color-border;
border-left-color: $color-border;
border-right-color: $color-border;
}
/* properly size events */
.fc-event {
font-size: 13px;
line-height: initial;
padding-left: 3px;
}
.app-navigation-entry-menu .icon-link {
background-size: 16px;
}
.fc-unthemed td.fc-today {
background: #fcf8e3 !important;
}

39
package-lock.json generated
View File

@ -2633,6 +2633,11 @@
"integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I=",
"dev": true
},
"charenc": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz",
"integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc="
},
"chokidar": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.4.tgz",
@ -3296,6 +3301,11 @@
"which": "^1.2.9"
}
},
"crypt": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz",
"integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs="
},
"crypto-browserify": {
"version": "3.12.0",
"resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz",
@ -4600,7 +4610,7 @@
},
"fecha": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fecha/-/fecha-2.3.3.tgz",
"resolved": "http://registry.npmjs.org/fecha/-/fecha-2.3.3.tgz",
"integrity": "sha512-lUGBnIamTAwk4znq5BcqsDaxSmZ9nDVJaij6NvRt/Tg4R69gERA+otPKbS86ROw9nxVMw2/mp1fnaiWqbs6Sdg=="
},
"figures": {
@ -7463,6 +7473,16 @@
"integrity": "sha512-3Zs9P/0zzwTob2pdgT0CHZuMbnSUSp8MB1bddfm+HDmnFWHGT4jvEZRf+2RuPoa+cjdn/z25SEt5gFTqdhvJAg==",
"dev": true
},
"md5": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/md5/-/md5-2.2.1.tgz",
"integrity": "sha1-U6s41f48iJG6RlMp6iP6wFQBJvk=",
"requires": {
"charenc": "~0.0.1",
"crypt": "~0.0.1",
"is-buffer": "~1.1.1"
}
},
"md5.js": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
@ -7834,15 +7854,18 @@
}
},
"nextcloud-vue": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/nextcloud-vue/-/nextcloud-vue-0.1.5.tgz",
"integrity": "sha512-2tFfPPzhTMtZnbBmUk91o2o+jiri3X6BEgNs+iAWf9WZq4Gcpb6kIFW2ckizZuPFccmV1rA4Ts18IpU25vGERw==",
"version": "0.4.3",
"resolved": "https://registry.npmjs.org/nextcloud-vue/-/nextcloud-vue-0.4.3.tgz",
"integrity": "sha512-HjDhD99AyWnXaRCjsLer2IyfW4zZI070Bn+IC2WixVShUh2oAridE/X6YpmC++qGnXuYofpostkgBfRM1tCvpw==",
"requires": {
"@babel/polyfill": "^7.0.0",
"md5": "^2.2.1",
"nextcloud-axios": "^0.1.2",
"v-tooltip": "^2.0.0-rc.33",
"vue": "^2.5.16",
"vue-click-outside": "^1.0.7",
"vue2-datepicker": "^2.4.1"
"vue-multiselect": "^2.1.0",
"vue2-datepicker": "^2.6.1"
}
},
"nice-try": {
@ -12916,9 +12939,9 @@
"dev": true
},
"vue2-datepicker": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/vue2-datepicker/-/vue2-datepicker-2.6.1.tgz",
"integrity": "sha512-XTQH8ah8l96sjofO7njSN2xb05vM3NsRVCifTCfhQvFTl+IIwQlyomN1erMVPfh/Du6ILogv4WUcSuMvhKu7cQ==",
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/vue2-datepicker/-/vue2-datepicker-2.6.2.tgz",
"integrity": "sha512-Z1XaBCBi3oiLm26WSBeYpUtQecFsz8FRZ9CtBofjmlkCNdu45smkzcaRjHsPEtOaV4yhkhJ2fr8EBe/7GVrfyA==",
"requires": {
"fecha": "^2.3.3"
}

View File

@ -43,7 +43,7 @@
"ical.js": "^1.2.2",
"moment": "^2.22.2",
"nextcloud-axios": "^0.1.2",
"nextcloud-vue": "^0.1.5",
"nextcloud-vue": "^0.4.3",
"p-limit": "^2.0.0",
"uuid": "^3.3.2",
"v-tooltip": "^2.0.0-rc.33",

View File

@ -22,7 +22,7 @@
<template>
<div class="app">
<app-navigation />
<app-navigation :loading-calendars="loadingCalendars" />
<router-view />
</div>
</template>
@ -38,6 +38,7 @@ export default {
},
data() {
return {
loadingCalendars: true
}
},
computed: {
@ -52,6 +53,7 @@ export default {
console.debug('Connected to dav!', client)
this.$store.dispatch('getCalendars')
.then((calendars) => {
this.loadingCalendars = false
// No calendars? Create a new one!
if (calendars.length === 0) {

View File

@ -1,7 +1,11 @@
<template>
<div id="app-navigation">
<date-picker />
<view-buttons />
<today-button />
<calendar-list :loading-calendars="loadingCalendars" />
<div v-click-outside="closeMenu" id="app-settings" :class="{open: opened}">
<div id="app-settings-header">
<button class="settings-button"
@ -16,8 +20,10 @@
</template>
<script>
import DatePicker from './AppNavigation/DatePicker.vue'
import ViewButtons from './AppNavigation/ViewButtons.vue'
import TodayButton from './AppNavigation/TodayButton.vue'
import CalendarList from './AppNavigation/CalendarList.vue'
import Settings from './AppNavigation/Settings.vue'
import ClickOutside from 'vue-click-outside'
@ -25,13 +31,21 @@ import ClickOutside from 'vue-click-outside'
export default {
name: 'AppNavigation',
components: {
DatePicker,
ViewButtons,
TodayButton,
CalendarList,
Settings
},
directives: {
ClickOutside
},
props: {
loadingCalendars: {
type: Boolean,
default: false
}
},
data() {
return {
opened: false

View File

@ -1,24 +1,50 @@
<template>
<transition-group id="calendars-list" :class="{'icon-loading': loading}" class="app-content-list"
name="list" tag="ul">
<transition-group id="calendars-list" name="list" tag="ul">
<calendar-list-new :key="'calendar-list-new'" :disabled="loadingCalendars" />
<li v-if="loadingCalendars" key="'calendar-list-loading'" class="app-navigation-list-item icon icon-loading" />
<calendar-list-item v-for="calendar in calendars" :key="calendar.id" :calendar="calendar" />
<li key="'calendar-list-separator" class="app-navigation-list-item separator" />
<subscription-list-new :key="'subscription-list-new'" />
<li v-if="loadingCalendars" key="'subscription-list-loading'" class="app-navigation-list-item icon icon-loading" />
<calendar-list-item v-for="subscription in subscriptions" :key="subscription.id" :calendar="subscription" />
</transition-group>
</template>
<script>
import CalendarListNew from './CalendarListNew.vue'
import SubscriptionListNew from './SubscriptionListNew.vue'
import CalendarListItem from './CalendarListItem.vue'
export default {
name: "CalendarList",
props: {
},
computed: {
},
methods: {
export default {
name: 'CalendarList',
components: {
CalendarListNew,
SubscriptionListNew,
CalendarListItem,
},
props: {
loadingCalendars: {
type: Boolean,
default: false
}
}
},
data: function() {
return {
loading: false
}
},
computed: {
calendars() {
return this.$store.getters.sortedCalendars
},
subscriptions() {
return this.$store.getters.sortedSubscriptions
}
},
methods: {
}
}
</script>
<style scoped>

View File

@ -1,13 +1,280 @@
<template>
<li v-click-outside="closeShareMenu" :class="{enabled: enabled, 'icon-loading-small': loading}" class="app-navigation-list-item">
<div :style="{ backgroundColor: calendarColor }" class="app-navigation-entry-bullet" />
<a :class="{selected: shareMenuOpen}" href="#" @click="toggleEnabled">{{ displayName }}</a>
<div class="app-navigation-entry-utils">
<ul>
<!-- share popovermenu -->
<li v-if="showSharingIcon" class="app-navigation-entry-utils-menu-button">
<button :class="{'icon-public': isPublished, 'icon-shared': !isPublished && isShared, 'icon-share': !isPublished && !isShared}" @click="toggleShareMenu" />
</li>
<!-- more popovermenu -->
<li v-click-outside="closeMoreMenu" class="app-navigation-entry-utils-menu-button">
<button class="icon-more" @click="toggleMoreMenu" />
</li>
</ul>
</div>
<calendar-list-item-sharing v-if="shareMenuOpen" :calendar="calendar" />
<div :class="{open: moreMenuOpen}" class="app-navigation-entry-menu popover-menu-container">
<popover-menu :menu="moreMenu" />
</div>
</li>
</template>
<script>
export default {
name: "CalendarListItem"
}
import { PopoverMenu } from 'nextcloud-vue'
import ClickOutside from 'vue-click-outside'
import CalendarListItemSharing from './CalendarListItemSharing.vue'
export default {
name: 'CalendarListItem',
components: {
CalendarListItemSharing,
PopoverMenu
},
directives: {
ClickOutside
},
props: {
calendar: {
type: Object,
required: true
}
},
data: function() {
return {
shareMenuOpen: false,
moreMenuOpen: false,
// Edit button from menu
editingName: false,
savingName: false,
editingColor: false,
savingColor: false,
// Copy button from menu
copyLoading: false,
copied: false,
copySuccess: false,
// Delete button
deleteLoading: false
}
},
computed: {
displayName() {
return this.calendar.displayName
},
calendarColor() {
return this.enabled ? this.calendar.color : 'transparent'
},
enabled() {
return this.calendar.enabled
},
loading() {
return this.calendar.loading
},
showSharingIcon() {
return this.calendar.canBeShared || this.calendar.canBePublished
},
canBeShared() {
return this.calendar.canBeShared
},
isShared() {
return !!this.calendar.shares.length
},
canBePublished() {
return this.calendar.canBePublished
},
isPublished() {
return !!this.calendar.publishURL
},
moreMenu() {
return [
{
text: this.savingName
? t('calendar', 'Saving name ...')
: t('calendar', 'Edit name'),
icon: this.savingName
? 'icon-loading-small'
: 'icon-rename',
input: this.editingName ? 'text' : false,
action: this.editingName ? this.saveNameInput : this.openNameInput,
value: this.calendar.displayName
},
{
text: this.savingColor
? t('calendar', 'Saving color ...')
: t('calendar', 'Edit color'),
icon: this.savingColor
? 'icon-loading-small'
: 'icon-edit', // TODO use color picker icon
input: this.editingColor ? 'text' : false,
action: this.editingColor ? this.saveColorInput : this.openColorInput,
value: this.calendar.color
},
{
href: this.calendar.url,
icon: this.copyLoading ? 'icon-loading-small' : 'icon-clippy',
text: !this.copied
? t('calendar', 'Copy private link')
: this.copySuccess
? t('calendar', 'Copied')
: t('calendar', 'Can not copy'),
action: this.copyLink
},
{
href: this.calendar.url + '?export',
icon: 'icon-download',
text: t('calendar', 'Download')
},
{
text: this.deleteLoading
? (this.calendar.isSharedWithMe ? t('calendar', 'Unsharing from me ...') : t('calendar', 'Deleting ...'))
: (this.calendar.isSharedWithMe ? t('calendar', 'Unshare from me') : t('calendar', 'Delete')),
icon: this.deleteLoading
? 'icon-loading-small'
: this.calendar.isSharedWithMe
? 'icon-unshare'
: 'icon-delete',
action: this.deleteCalendar
}
]
}
},
methods: {
toggleEnabled() {
this.$store.dispatch('toggleCalendarEnabled', { calendar: this.calendar })
.catch((error) => {
console.error(error)
OC.Notification.showTemporary(t('calendar', 'An error occurred, unable to change visibility of the calendar.'))
})
},
deleteCalendar() {
this.deleteLoading = true
this.$store.dispatch('deleteCalendar', { calendar: this.calendar })
.then(() => {
this.deleteLoading = false
})
.catch((error) => {
console.error(error)
OC.Notification.showTemporary(t('calendar', 'An error occurred, unable to delete the calendar.'))
this.deleteLoading = false
})
},
closeShareMenu() {
this.shareMenuOpen = false
},
toggleShareMenu() {
this.shareMenuOpen = !this.shareMenuOpen
},
closeMoreMenu(event) {
if (this.$el.querySelector('.popover-menu-container').contains(event.target)) {
return
}
this.moreMenuOpen = false
this.editingName = false
this.editingColor = false
},
toggleMoreMenu() {
this.moreMenuOpen = !this.moreMenuOpen
if (!this.moreMenuOpen) {
this.editingName = false
this.editingColor = false
}
},
copyLink(event) {
// change to loading status
this.copyLoading = true
event.stopPropagation()
const rootURL = OC.linkToRemote('dav')
const url = new URL(this.calendar.url, rootURL)
// copy link for calendar to clipboard
this.$copyText(url)
.then(e => {
event.preventDefault()
this.copySuccess = true
this.copied = true
// Notify calendar url was copied
OC.Notification.showTemporary(t('calendar', 'Calendar link copied to clipboard'))
}, e => {
this.copySuccess = false
this.copied = true
OC.Notification.showTemporary(t('calendar', 'Calendar link was not copied to clipboard.'))
}).then(() => {
this.copyLoading = false
setTimeout(() => {
// stop loading status regardless of outcome
this.copied = false
}, 2000)
})
},
openNameInput() {
event.stopPropagation()
if (this.savingName) {
return
}
this.editingColor = false
this.editingName = true
},
saveNameInput(event) {
event.stopPropagation()
this.savingName = true
this.editingName = false
const value = event.target.querySelector('input[type=text]').value
this.$store.dispatch('renameCalendar', { calendar: this.calendar, newName: value })
.then(() => {
this.savingName = false
})
.catch((error) => {
console.error(error)
OC.Notification.showTemporary(t('calendar', 'An error occurred, unable to rename the calendar.'))
this.savingName = false
this.editingName = true
})
},
openColorInput() {
event.stopPropagation()
if (this.savingColor) {
return
}
this.editingName = false
this.editingColor = true
},
saveColorInput(event) {
event.stopPropagation()
this.savingColor = true
this.editingColor = false
const value = event.target.querySelector('input[type=text]').value
this.$store.dispatch('changeCalendarColor', { calendar: this.calendar, newColor: value })
.then(() => {
this.savingColor = false
})
.catch((error) => {
console.error(error)
OC.Notification.showTemporary(t('calendar', 'An error occurred, unable to change the calendar\'s color.'))
this.savingColor = false
this.editingColor = true
})
}
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,117 @@
<template>
<div class="sharing-section">
<multiselect
id="users-groups-search"
:options="usersOrGroups"
:searchable="true"
:internal-search="false"
:max-height="600"
:show-no-results="true"
:placeholder="placeholder"
:class="{ 'showContent': inputGiven, 'icon-loading': isLoading }"
:user-select="true"
open-direction="bottom"
track-by="user"
label="user"
@search-change="findSharee"
@input="shareCalendar" />
<!-- list of user or groups addressbook is shared with -->
<ul v-if="calendar.shares.length > 0 || calendar.canBePublished" class="shareWithList">
<calendar-list-item-sharing-publish-item :calendar="calendar " />
<calendar-list-item-sharing-item v-for="sharee in calendar.shares" :key="sharee.uri"
:sharee="sharee" :calendar="calendar" />
</ul>
</div>
</template>
<script>
import client from '../../services/cdav'
import debounce from 'debounce'
import CalendarListItemSharingPublishItem from './CalendarListItemSharingPublishItem.vue'
import CalendarListItemSharingItem from './CalendarListItemSharingItem.vue'
import { Multiselect } from 'nextcloud-vue'
export default {
name: 'CalendarListItemSharing',
components: {
CalendarListItemSharingItem,
CalendarListItemSharingPublishItem,
Multiselect
},
props: {
calendar: {
type: Object,
default() {
return {}
}
},
},
data() {
return {
isLoading: false,
inputGiven: false,
usersOrGroups: []
}
},
computed: {
placeholder() {
return t('calendar', 'Share with users or groups')
},
noResult() {
return t('calendar', 'No users or groups')
}
},
// mounted() {
// // This ensures that the multiselect input is in focus as soon as the user clicks share
// document.getElementById('users-groups-search').focus()
// },
methods: {
/**
* Share calendar
*
* @param {Object} data destructuring object
* @param {string} data.user the userId
* @param {string} data.displayName the displayName
* @param {string} data.uri the sharing principalScheme uri
* @param {Boolean} data.isGroup is this a group ?
*/
shareCalendar({ user, displayName, uri, isGroup }) {
const calendar = this.calendar
this.$store.dispatch('shareCalendar', { calendar, user, displayName, uri, isGroup })
},
/**
* Use the cdav client call to find matches to the query from the existing Users & Groups
*
* @param {String} query
*/
findSharee: debounce(async function(query) {
this.isLoading = true
this.usersOrGroups = []
if (query.length > 0) {
const results = await client.principalPropertySearchByDisplayname(query)
this.usersOrGroups = results.reduce((list, result) => {
if (['GROUP', 'INDIVIDUAL'].indexOf(result.calendarUserType) > -1) {
const isGroup = result.calendarUserType === 'GROUP'
list.push({
user: result[isGroup ? 'groupId' : 'userId'],
displayName: result.displayname,
icon: isGroup ? 'icon-group' : 'icon-user',
uri: result.principalScheme,
isGroup
})
}
return list
}, [])
this.isLoading = false
this.inputGiven = true
} else {
this.inputGiven = false
this.isLoading = false
}
}, 500)
}
}
</script>

View File

@ -0,0 +1,98 @@
<template>
<li>
<div v-if="isGroup" class="avatar icon-group" />
<avatar v-else :user="userId" :display-name="displayName" />
<span class="username">{{ displayName }}</span>
<div class="sharingOptionsGroup">
<span>
<input :id="uid" :checked="writeable" :disabled="updatingSharee"
type="checkbox" class="checkbox" @change="updatePermission">
<label :for="uid">{{ label }}</label>
</span>
<a href="#" class="icon icon-delete" @click="unshare" />
</div>
</li>
</template>
<script>
import { Avatar } from 'nextcloud-vue'
export default {
name: 'CalendarListItemSharingItem',
components: {
Avatar
},
props: {
calendar: {
type: Object,
required: true
},
sharee: {
type: Object,
required: true
}
},
data() {
return {
updatingSharee: false,
}
},
computed: {
label() {
return t('calendar', 'Can edit')
},
displayName() {
return this.sharee.displayName
},
isGroup() {
return this.sharee.isGroup
},
userId() {
return this.sharee.id
},
writeable() {
return this.sharee.writeable
},
// generated id for this sharee
uid() {
return this.sharee.id + this.calendar.id + Math.floor(Math.random() * 1000)
}
},
methods: {
async unshare() {
if (this.updatingSharee) {
return false
}
this.updatingSharee = true
this.$store.dispatch('unshareCalendar', { calendar: this.calendar, uri: this.sharee.uri })
.then(() => {
this.updatingSharee = false
})
.catch((error) => {
console.error(error)
OC.Notification.showTemporary(t('calendar', 'An error occurred, unable to change the unshare the calendar.'))
this.updatingSharee = false
})
},
async updatePermission() {
if (this.updatingSharee) {
return false
}
this.updatingSharee = true
this.$store.dispatch('toggleCalendarShareWritable', { calendar: this.calendar, uri: this.sharee.uri })
.then(() => {
this.updatingSharee = false
})
.catch((error) => {
console.error(error)
OC.Notification.showTemporary(t('calendar', 'An error occurred, unable to change the permission of the share.'))
this.updatingSharee = false
})
}
}
}
</script>

View File

@ -0,0 +1,264 @@
<template>
<li>
<div :class="{published: isPublished, 'icon-public': !isPublished, 'icon-public-white': isPublished}" class="avatar" />
<span class="username">{{ label }}</span>
<span class="sharingOptionsGroup">
<a v-if="isPublished" :class="{'icon-clippy': !copyingShareLink, 'icon-loading-small': copyingShareLink}" href="#"
class="icon icon-clippy" @click="copyPublicLink" />
<div v-click-outside="closeMenu" v-if="isPublished" class="share-menu">
<a href="#" class="icon icon-more" title="Copy public link"
@click="toggleMenu" />
<div :class="{open: menuOpen}" class="popovermenu">
<popover-menu :menu="menu" />
</div>
</div>
<div v-if="!isPublished" class="share-menu">
<a :class="{hidden: publishingCalendar}" href="#" class="icon icon-add"
@click="publishCalendar" />
<a :class="{hidden: !publishingCalendar}" href="#" class="icon icon-loading-small" />
</div>
</span>
</li>
</template>
<script>
import { PopoverMenu } from 'nextcloud-vue'
import ClickOutside from 'vue-click-outside'
export default {
name: 'CalendarListItemSharingPublishItem',
components: {
PopoverMenu
},
directives: {
ClickOutside
},
props: {
calendar: {
type: Object,
required: true
}
},
data() {
return {
// is the calendar being published right now?
publishingCalendar: false,
// Copy public link
copyingShareLink: false,
// Is the options menu open?
menuOpen: false,
// send sharing link as email
showEMailLinkInput: false,
sendingEMailLink: false,
// Copy subscription link
copyingSubscriptionLink: false,
copiedSubscriptionLink: false,
copySubscriptionLinkSuccess: false,
// Copy embed code
copyingEmbedCode: false,
copiedEmbedCode: false,
copyEmbedCodeSuccess: false,
// unpublish
unpublishingCalendar: false
}
},
computed: {
isPublished() {
return this.calendar.publishURL !== null
},
label() {
return t('calendar', 'Share link')
},
menu() {
return [
{
text: this.sendingEMailLink
? t('calendar', 'Sending email ...')
: t('calendar', 'Email link'),
icon: this.sendingEMailLink
? 'icon-loading-small'
: 'icon-mail',
input: this.showEMailLinkInput ? 'text' : false,
action: this.showEMailLinkInput ? this.sendLinkViaEMail : this.openEMailLinkInput,
value: ''
},
{
href: '#',
icon: this.copyingSubscriptionLink
? 'icon-loading-small'
: 'icon-calendar-dark',
text: !this.copiedSubscriptionLink
? t('calendar', 'Copy subscription link')
: this.copySubscriptionLinkSuccess
? t('calendar', 'Copied')
: t('calendar', 'Can not copy'),
action: this.copySubscriptionLink
},
{
href: '#',
icon: this.copyingEmbedCode
? 'icon-loading-small'
: 'icon-embed',
text: !this.copiedEmbedCode
? t('calendar', 'Copy embed code')
: this.copyEmbedCodeSuccess
? t('calendar', 'Copied')
: t('calendar', 'Can not copy'),
action: this.copyEmbedCode
},
{
text: this.unpublishingCalendar
? t('calendar', 'Deleting share link ...')
: t('calendar', 'Delete share link'),
icon: this.unpublishingCalendar
? 'icon-loading-small'
: 'icon-delete',
action: this.unpublishCalendar
}
]
}
},
methods: {
toggleMenu() {
this.menuOpen = !this.menuOpen
},
closeMenu(event) {
if (this.$el.querySelector('.share-menu').contains(event.target)) {
return
}
this.menuOpen = false
this.showEMailLinkInput = false
},
publishCalendar() {
this.publishingCalendar = true
const calendar = this.calendar
this.$store.dispatch('publishCalendar', { calendar }).then(() => {
this.publishingCalendar = false
}).catch((e) => {
this.publishingCalendar = false
OC.Notification.showTemporary(t('calendar', 'Publishing calendar failed'))
})
},
openEMailLinkInput() {
event.stopPropagation()
if (this.sendingEMailLink) {
return
}
this.showEMailLinkInput = true
},
sendLinkViaEMail(event) {
event.stopPropagation()
this.sendingEMailLink = true
this.showEMailLinkInput = false
const value = event.target.querySelector('input[type=text]').value
console.debug(value)
},
copyPublicLink() {
// change to loading status
this.copyLoading = true
event.stopPropagation()
const rootURL = OC.linkToRemote('dav')
const token = this.calendar.publishURL.split('/').slice(-1)[0]
const url = new URL(OC.linkTo('calendar', 'index.php') + '/p/' + token, rootURL)
// copy link for calendar to clipboard
this.$copyText(url)
.then(e => {
event.preventDefault()
this.copySuccess = true
this.copied = true
// Notify calendar url was copied
OC.Notification.showTemporary(t('calendar', 'Calendar link copied to clipboard'))
}, e => {
this.copySuccess = false
this.copied = true
OC.Notification.showTemporary(t('calendar', 'Calendar link was not copied to clipboard.'))
}).then(() => {
this.copyLoading = false
setTimeout(() => {
// stop loading status regardless of outcome
this.copied = false
}, 2000)
})
},
copySubscriptionLink() {
// change to loading status
this.copyingSubscriptionLink = true
event.stopPropagation()
const rootURL = OC.linkToRemote('dav')
const url = new URL(this.calendar.publishURL + '?export', rootURL)
// copy link for calendar to clipboard
this.$copyText(url)
.then(e => {
event.preventDefault()
this.copySubscriptionLinkSuccess = true
this.copiedSubscriptionLink = true
// Notify calendar url was copied
OC.Notification.showTemporary(t('calendar', 'Subscription link copied to clipboard'))
}, e => {
this.copySubscriptionLinkSuccess = false
this.copiedSubscriptionLink = true
OC.Notification.showTemporary(t('calendar', 'Subscription link was not copied to clipboard.'))
}).then(() => {
this.copyingSubscriptionLink = false
setTimeout(() => {
// stop loading status regardless of outcome
this.copiedSubscriptionLink = false
}, 2000)
})
},
copyEmbedCode() {
// change to loading status
this.copyingEmbedCode = true
event.stopPropagation()
const rootURL = OC.linkToRemote('dav')
const token = this.calendar.publishURL.split('/').slice(-1)[0]
const url = new URL(OC.linkTo('calendar', 'index.php') + '/e/' + token, rootURL)
const code = '<iframe width="400" height="215" src="' + url + '"></iframe>'
// copy link for calendar to clipboard
this.$copyText(code)
.then(e => {
event.preventDefault()
this.copyEmbedCodeSuccess = true
this.copiedEmbedCode = true
// Notify calendar url was copied
OC.Notification.showTemporary(t('calendar', 'Embed code copied to clipboard'))
}, e => {
this.copyEmbedCodeSuccess = false
this.copiedEmbedCode = true
OC.Notification.showTemporary(t('calendar', 'Embed code was not copied to clipboard.'))
}).then(() => {
this.copyingEmbedCode = false
setTimeout(() => {
// stop loading status regardless of outcome
this.copiedEmbedCode = false
}, 2000)
})
},
unpublishCalendar() {
this.unpublishingCalendar = true
const calendar = this.calendar
this.$store.dispatch('unpublishCalendar', { calendar }).then(() => {
this.unpublishingCalendar = false
}).catch((e) => {
this.unpublishingCalendar = false
OC.Notification.showTemporary(t('calendar', 'Unpublishing calendar failed'))
})
}
}
}
</script>

View File

@ -1,13 +1,88 @@
<template>
<li v-click-outside="closeNewCalendarForm" :class="{editing: showForm}" class="new-entity-container">
<a id="new-calendar-button" href="#" class="icon-add"
@click="openDialog">{{ label }}</a>
<div class="app-navigation-entry-edit">
<form @submit.prevent="addCalendar()">
<input id="new-calendar-form-input" v-model="displayName" :placeholder="inputPlaceholder"
:disabled="isCreating" class="app-navigation-input" type="text"
required>
<span :class="{'hidden': !isCreating}" class="icon-loading-small" />
<input :disabled="isCreating" class="icon-close" type="button"
value="" @click="dismiss">
<input :disabled="isCreating" class="icon-checkmark accept-button new-accept-button" type="submit"
value="">
</form>
</div>
</li>
</template>
<script>
export default {
name: "CalendarListNew"
}
import ClickOutside from 'vue-click-outside'
import { randomColor } from '../../services/colorService'
console.debug(randomColor)
export default {
name: 'CalendarListNew',
directives: {
ClickOutside
},
props: {
disabled: {
type: Boolean,
default: false
}
},
data: function() {
return {
displayName: '',
isCreating: false,
showForm: false
}
},
computed: {
label() {
return t('calendar', 'New Calendar')
},
inputPlaceholder() {
return t('calendar', 'Name of calendar')
}
},
methods: {
openDialog() {
if (this.disabled) {
return false
}
this.showForm = true
document.getElementById('new-calendar-form-input').focus()
},
addCalendar() {
this.isCreating = true
this.$store.dispatch('appendCalendar', { calendar: { displayName: this.displayName, color: randomColor() } })
.then(() => {
this.displayName = ''
this.showForm = false
this.isCreating = false
})
.catch((error) => {
console.error(error)
OC.Notification.showTemporary(t('calendar', 'An error occurred, unable to create the calendar.'))
this.isCreating = false
})
},
dismiss() {
this.name = ''
this.showForm = false
},
closeNewCalendarForm() {
// Close only when input is empty
if (this.name === '') {
this.showForm = false
}
}
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,97 @@
<!--
Nextcloud - Tasks
@author Raimund Schlüßler
@copyright 2018 Raimund Schlüßler <raimund.schluessler@mailbox.org>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
License as published by the Free Software Foundation; either
version 3 of the License, or any later version.
This library 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 library. If not, see <http://www.gnu.org/licenses/>.
-->
<template>
<li v-click-outside="reset" :class="{confirmed: activated, active: armed}">
<a class="confirmation-default" @click="activate($event)">
<span class="icon-delete" />
<span>{{ t('calendar', 'Delete') }}</span>
</a>
<a :title="t('calendar', 'Cancel')" class="confirmation-abort icon-close" @click="reset">
<span />
</a>
<a v-tooltip="{
placement: 'left',
show: activated,
trigger: 'manual',
boundariesElement: 'body',
content: message
}"
class="confirmation-confirm icon-delete-white no-permission" @click="deleteCalendar($event)">
<span class="countdown">{{ remaining }}</span>
</a>
</li>
</template>
<script>
import clickOutside from 'vue-click-outside'
export default {
name: 'PopoverMenu',
components: {
clickOutside
},
directives: {
clickOutside
},
props: {
message: {
type: String,
default: ''
}
},
data() {
return {
remaining: 3,
activated: false,
armed: false
}
},
methods: {
reset: function() {
this.activated = false
this.armed = false
this.remaining = 3
},
activate: function(e) {
this.activated = true
this.countdown()
e.stopPropagation()
},
countdown: function() {
this.remaining--
if (this.remaining > 0) {
setTimeout(this.countdown, 1000)
} else {
this.armed = true
}
},
deleteCalendar: function(e) {
if (this.armed) {
this.$emit('delete-calendar')
this.activated = false
} else {
e.stopPropagation()
}
}
}
}
</script>

View File

@ -1,11 +1,143 @@
<template>
<div class="button-group">
<button :aria-label="goBackLabel" :title="goBackLabel" type="button"
class="button" @click="prev()">
<i class="glyphicon glyphicon-chevron-left" />
</button>
<label for="app-navigation-datepicker-input" class="button datepicker-label">{{ label }}</label>
<datetime-picker v-model="date" :lang="lang" :first-day-of-week="firstDay"
:not-before="min" :not-after="max" @change="selectInDatepicker" />
<button :aria-label="goForwardLabel" :title="goForwardLabel" type="button"
class="button" @click="next()">
<i class="glyphicon glyphicon-chevron-right" />
</button>
</div>
</template>
<script>
export default {
name: "DatePicker"
}
import { DatetimePicker } from 'nextcloud-vue'
import { dateFactory, getYYYYMMDDFromDate } from '../../services/date.js'
import moment from 'moment'
export default {
name: 'DatePicker',
components: {
DatetimePicker
},
data: function() {
return {
min: new Date('1970-01-01T00:00:00Z'),
max: new Date('2036-12-31T23:59:59Z'),
date: dateFactory(),
locale: 'en', // this is just during initialization
firstDay: window.firstDay + 1, // provided by nextcloud
lang: {
days: window.dayNamesShort, // provided by nextcloud
months: window.monthNamesShort, // provided by nextcloud
placeholder: {
// this should never be visible in theory
// just have something to replace the chinese default
date: t('calendar', 'Select date to navigate to')
}
}
}
},
computed: {
label() {
switch (this.$route.params.view) {
case 'agendaDay':
return moment(this.date).format('ll')
case 'agendaWeek':
return t('calendar', 'Week {number} of {year}', {
number: moment(this.date).week(),
year: moment(this.date).year()
})
case 'month':
default:
return moment(this.date).format('MMMM YYYY')
}
},
goBackLabel() {
return t('calendar', 'Go back')
},
goForwardLabel() {
return t('calendar', 'Go forward')
}
},
watch: {
'$route'(to) {
if (to.params.firstday) {
this.date = new Date(to.params.firstday)
}
}
},
mounted() {
this.$el.querySelector('.mx-input').id = 'app-navigation-datepicker-input'
this.$el.querySelector('.mx-input-wrapper').style.display = 'none'
// Load the locale
// convert format like en_GB to en-gb for `moment.js`
let locale = OC.getLocale().replace('_', '-').toLowerCase()
// default load e.g. fr-fr
import('moment/locale/' + this.locale)
.then(e => {
// force locale change to update
// the component once done loading
this.locale = locale
})
.catch(e => {
// failure: fallback to fr
import('moment/locale/' + locale.split('-')[0])
.then(e => {
this.locale = locale.split('-')[0]
})
.catch(e => {
// failure, fallback to english
this.locale = 'en'
})
})
},
methods: {
prev() {
this.goTo(this.nav(-1))
},
next() {
this.goTo(this.nav(1))
},
selectInDatepicker() {
this.goTo(this.date)
},
goTo(date) {
const name = this.$route.name
const params = this.$route.params
params.firstday = getYYYYMMDDFromDate(date)
this.$router.push({ name, params })
},
nav(dir) {
switch (this.$route.params.view) {
case 'agendaDay':
return moment(this.date)
.add(dir, 'day')
.toDate()
case 'agendaWeek':
return moment(this.date)
.add(dir, 'week')
// .startOf('week')
.toDate()
case 'month':
return moment(this.date)
.add(dir, 'month')
// .startOf('month')
.toDate()
}
},
}
}
</script>
<style scoped>

View File

@ -50,6 +50,8 @@
</template>
<script>
import client from '../../services/cdav'
export default {
name: 'Settings',
data: function() {
@ -148,8 +150,10 @@ export default {
this.$copyText(OC.linkToRemote('dav'))
},
copyAppleCalDAV() {
// TODO - return current user principal from davClient
this.$copyText('TODO implement me')
const rootURL = OC.linkToRemote('dav')
const url = new URL(client.currentUserPrincipal.principalUrl, rootURL)
this.$copyText(url)
}
}
}

View File

@ -1,13 +0,0 @@
<template>
</template>
<script>
export default {
name: "SubscriptionList"
}
</script>
<style scoped>
</style>

View File

@ -1,13 +0,0 @@
<template>
</template>
<script>
export default {
name: "SubscriptionListItem"
}
</script>
<style scoped>
</style>

View File

@ -1,11 +1,88 @@
<template>
<li v-click-outside="closeNewCalendarForm" :class="{editing: showForm}" class="new-entity-container">
<a id="new-subscription-button" href="#" class="icon-add"
@click="openDialog">{{ label }}</a>
<div class="app-navigation-entry-edit">
<form @submit.prevent="addCalendar()">
<input id="new-subscription-form-input" v-model="link" :placeholder="inputPlaceholder"
:disabled="isCreating" class="app-navigation-input" type="text"
required>
<span :class="{'hidden': !isCreating}" class="icon-loading-small" />
<input :disabled="isCreating" class="icon-close" type="button"
value="" @click="dismiss">
<input :disabled="isCreating" class="icon-checkmark accept-button new-accept-button" type="submit"
value="">
</form>
</div>
</li>
</template>
<script>
export default {
name: "SubscriptionListNew"
}
import ClickOutside from 'vue-click-outside'
import { randomColor } from '../../services/colorService'
export default {
name: 'SubscriptionListNew',
directives: {
ClickOutside
},
props: {
disabled: {
type: Boolean,
default: false
}
},
data: function() {
return {
link: '',
isCreating: false,
showForm: false
}
},
computed: {
label() {
return t('calendar', 'New Subscription')
},
inputPlaceholder() {
return t('calendar', 'Link to ical')
}
},
methods: {
openDialog() {
if (this.disabled) {
return false
}
this.showForm = true
document.getElementById('new-subscription-form-input').focus()
},
addCalendar() {
this.isCreating = true
this.$store.dispatch('appendSubscription', { calendar: { displayName: '', color: randomColor() }, source: this.link })
.then(() => {
this.displayName = ''
this.showForm = false
this.isCreating = false
})
.catch((error) => {
console.error(error)
OC.Notification.showTemporary(t('calendar', 'An error occurred, unable to create the calendar.'))
this.isCreating = false
})
},
dismiss() {
this.link = ''
this.showForm = false
},
closeNewCalendarForm() {
// Close only when input is empty
if (this.link === '') {
this.showForm = false
}
}
}
}
</script>
<style scoped>

View File

@ -21,8 +21,8 @@
-->
<template>
<div class="togglebuttons">
<button class="button today" @click="today()">{{ label }}</button>
<div class="button-group">
<button class="button" @click="today()">{{ label }}</button>
</div>
</template>

View File

@ -21,10 +21,10 @@
-->
<template>
<div class="togglebuttons">
<button :class="{active: (selectedView === 'agendaDay')}" class="button first" @click="view('agendaDay')">{{ labelAgendaDay }}</button>
<button :class="{active: (selectedView === 'agendaWeek')}" class="button middle" @click="view('agendaWeek')">{{ labelAgendaWeek }}</button>
<button :class="{active: (selectedView === 'month')}" class="button last" @click="view('month')">{{ labelMonth }}</button>
<div class="button-group">
<button :class="{active: (selectedView === 'agendaDay')}" class="button" @click="view('agendaDay')">{{ labelAgendaDay }}</button>
<button :class="{active: (selectedView === 'agendaWeek')}" class="button" @click="view('agendaWeek')">{{ labelAgendaWeek }}</button>
<button :class="{active: (selectedView === 'month')}" class="button" @click="view('month')">{{ labelMonth }}</button>
</div>
</template>
@ -56,7 +56,3 @@ export default {
}
}
</script>
<style scoped>
</style>

View File

@ -25,10 +25,14 @@ import Vue from 'vue'
import App from './App'
import router from './router'
import store from './store'
import { Multiselect } from 'nextcloud-vue'
// import { sync } from 'vuex-router-sync'
import VueClipboard from 'vue-clipboard2'
Vue.use(VueClipboard)
Vue.component('Multiselect', Multiselect)
// CSP config for webpack dynamic chunk loading
// eslint-disable-next-line

View File

@ -1,7 +1,7 @@
/**
* @copyright Copyright (c) 2018 John Molakvoæ <skjnldsv@protonmail.com>
* @copyright Copyright (c) 2018 Georg Ehrke
*
* @author John Molakvoæ <skjnldsv@protonmail.com>
* @author Georg Ehrke <oc.list@georgehrke.com>
*
* @license GNU AGPL version 3 or any later version
*
@ -19,16 +19,15 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import DavClient from 'cdav-library'
function xhrProvider() {
var headers = {
const headers = {
'X-Requested-With': 'XMLHttpRequest',
'requesttoken': OC.requestToken
}
var xhr = new XMLHttpRequest()
var oldOpen = xhr.open
const xhr = new XMLHttpRequest()
const oldOpen = xhr.open
// override open() method to add headers
xhr.open = function() {

View File

@ -0,0 +1,134 @@
/**
* @copyright Copyright (c) 2018 Georg Ehrke
*
* @author Georg Ehrke <oc.list@georgehrke.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/>.
*
*/
/** @type String [] */
const colors = []
/**
* get the appropriate text color to be used on top of an rgb value
*
* @param {Number} red decimal value for red
* @param {Number} green decimal value for green
* @param {Number} blue decimal value for blue
* @returns {string}
*/
export function generateTextColorFromRGB(red, green, blue) {
const brightness = (((red * 299) + (green * 587) + (blue * 114)) / 1000)
return (brightness > 130) ? '#000000' : '#FAFAFA'
}
/**
* returns a random color
*
* @returns {String}
*/
export function randomColor() {
if (typeof String.prototype.toRgb === 'function') {
const { r, g, b } = Math.random().toString().toRgb()
return rgbToHex(r, g, b)
} else {
return colors[Math.floor(Math.random() * colors.length)]
}
}
/**
* extracts decimal rgb values from a hexadecimal string
*
* @param {String} colorString the hex rgb string
* @returns {{red: Number, green: Number, blue: Number}}
*/
export function extractRGBFromHexString(colorString) {
const fallbackColor = { red: 255, green: 255, blue: 255 }
let matchedString
if (typeof colorString !== 'string') {
return fallbackColor
}
switch (colorString.length) {
case 4: {
matchedString = colorString.match(/^#([0-9a-f]{3})$/i)
return (Array.isArray(matchedString) && matchedString[1])
? ({
red: parseInt(matchedString[1].charAt(0), 16) * 0x11,
green: parseInt(matchedString[1].charAt(1), 16) * 0x11,
blue: parseInt(matchedString[1].charAt(2), 16) * 0x11
})
: fallbackColor
}
case 7:
case 9: {
const regex = new RegExp('^#([0-9a-f]{' + (colorString.length - 1) + '})$', 'i')
matchedString = colorString.match(regex)
return (Array.isArray(matchedString) && matchedString[1])
? ({
red: parseInt(matchedString[1].substr(0, 2), 16),
green: parseInt(matchedString[1].substr(2, 2), 16),
blue: parseInt(matchedString[1].substr(4, 2), 16)
})
: fallbackColor
}
default:
return fallbackColor
}
}
/**
*
* @param {String[]|String} red Value from 0 to 255
* @param {String} green Value from 0 to 255
* @param {String} blue Value from 0 to 255
* @returns {string}
*/
export function rgbToHex(red, green, blue) {
if (Array.isArray(red)) {
[red, green, blue] = red
}
return [
'#',
('0' + parseInt(red, 10).toString(16)).slice(-2),
('0' + parseInt(green, 10).toString(16)).slice(-2),
('0' + parseInt(blue, 10).toString(16)).slice(-2)
].join('')
}
// initialize default colors
if (typeof String.prototype.toRgb === 'function') {
['15', '9', '4', 'b', '6', '11', '74', 'f', '57'].forEach((hashValue) => {
const { r, g, b } = hashValue.toRgb()
colors.push(rgbToHex(r, g, b))
})
} else {
colors.push(
'#31CC7C',
'#317CCC',
'#FF7A66',
'#F1DB50',
'#7C31CC',
'#CC317C',
'#3A3B3D',
'#CACBCD'
)
}

View File

@ -1,7 +1,40 @@
/**
* @copyright Copyright (c) 2018 Georg Ehrke
*
* @author Georg Ehrke <oc.list@georgehrke.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/>.
*
*/
/**
* returns a new Date object
*
* @returns {Date}
*/
export function dateFactory() {
return new Date()
}
/**
* formats a Date object as YYYYMMDD
*
* @param {Date} date Date to format
* @returns {string}
*/
export function getYYYYMMDDFromDate(date) {
return new Date(date.getTime() - (date.getTimezoneOffset() * 60000))
.toISOString()

View File

@ -0,0 +1,36 @@
/**
* @copyright Copyright (c) 2018 Georg Ehrke
*
* @author Georg Ehrke <oc.list@georgehrke.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/>.
*
*/
/**
* Returns a default color
*
* @returns {string}
*/
export default function defaultColor() {
const fallback = '#000000'
if (!OCA.Theming) {
return fallback
}
return OCA.Theming.color || fallback
}

View File

@ -1,6 +1,32 @@
/**
* @copyright Copyright (c) 2018 Georg Ehrke
* @copyright Copyright (c) 2018 John Molakvoæ
* @copyright Copyright (c) 2018 Thomas Citharel
*
* @author Georg Ehrke <oc.list@georgehrke.com>
* @author John Molakvoæ <skjnldsv@protonmail.com>
* @author Thomas Citharel <tcit@tcit.fr>
*
* @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 ICAL from 'ical.js'
import parseIcs from '../services/parseIcs'
import defaultColor from '../services/defaultColor'
import client from '../services/cdav'
import Event from '../models/event'
import pLimit from 'p-limit'
@ -8,12 +34,19 @@ import pLimit from 'p-limit'
const calendarModel = {
id: '',
displayName: '',
enabled: true,
color: '',
enabled: true, // is the calendar visible in the grid
loading: false, // is the calendar loading events
components: [],
owner: '',
shares: [],
events: {},
publishURL: null,
url: '',
readOnly: false,
order: 0,
isSharedWithMe: false,
canBeShared: false,
canBePublished: false,
dav: false
}
@ -30,13 +63,44 @@ const state = {
export function mapDavCollectionToCalendar(calendar) {
return {
// get last part of url
id: calendar.url.split('/').slice(-2, -1)[0],
id: calendar.url.split('/').slice(-2, -1)[0], // TODO - improve me
displayName: calendar.displayname,
enabled: calendar.enabled !== false,
color: calendar.color || defaultColor(),
enabled: !!calendar.enabled,
components: calendar.components,
owner: calendar.owner,
readOnly: calendar.readOnly !== false,
readOnly: !calendar.isWriteable(),
order: calendar.order || 0,
url: calendar.url,
dav: calendar
dav: calendar,
shares: calendar.shares
.filter((sharee) => sharee.href !== client.currentUserPrincipal.principalScheme) // public shares create a share with yourself ... should be fixed in server
.map(sharee => Object.assign({}, mapDavShareeToSharee(sharee))),
publishURL: calendar.publishURL || null,
isSharedWithMe: calendar.owner !== client.currentUserPrincipal.principalUrl,
canBeShared: calendar.isShareable(),
canBePublished: calendar.isPublishable(),
}
}
/**
* map a dav collection to our calendar object model
*
* @param {Object} sharee the sharee object from the cdav library shares
* @returns {Object}
*/
export function mapDavShareeToSharee(sharee) {
const id = sharee.href.split('/').slice(-1)[0]
const name = sharee['common-name']
? sharee['common-name']
: id
return {
displayName: name,
id: id,
writeable: sharee.access[0].endsWith('read-write'),
isGroup: sharee.href.indexOf('principal:principals/groups/') === 0,
uri: sharee.href
}
}
@ -46,9 +110,10 @@ const mutations = {
* Add calendar into state
*
* @param {Object} state the store data
* @param {Object} calendar the calendar to add
* @param {Object} data destructuring object
* @param {Object} data.calendar calendar the calendar to add
*/
addCalendar(state, calendar) {
addCalendar(state, { calendar }) {
// extend the calendar to the default model
state.calendars.push(Object.assign({}, calendarModel, calendar))
},
@ -57,18 +122,20 @@ const mutations = {
* Delete calendar
*
* @param {Object} state the store data
* @param {Object} calendar the calendar to delete
* @param {Object} data destructuring object
* @param {Object} data.calendar the calendar to delete
*/
deleteCalendar(state, calendar) {
deleteCalendar(state, { calendar }) {
state.calendars.splice(state.calendars.indexOf(calendar), 1)
},
/**
* Toggle whether a calendar is Enabled
* @param {Object} context the store mutations
* @param {Object} calendar the calendar to toggle
* @param {Object} data destructuring object
* @param {Object} data.calendar the calendar to toggle
*/
toggleCalendarEnabled(context, calendar) {
toggleCalendarEnabled(context, { calendar }) {
calendar = state.calendars.find(search => search.id === calendar.id)
calendar.enabled = !calendar.enabled
},
@ -86,47 +153,27 @@ const mutations = {
},
/**
* Append a list of events to an calendar
* and remove duplicates
*
* @param {Object} state the store data
* Change calendar's color
* @param {Object} context the store mutations
* @param {Object} data destructuring object
* @param {Object} data.calendar the calendar to add the event to
* @param {Event[]} data.events array of events to append
* @param {Object} data.calendar the calendar to rename
* @param {String} data.newColor the new color of the calendar
*/
appendEventsToCalendar(state, { calendar, events }) {
calendar = state.calendars.find(search => search === calendar)
// convert list into an array and remove duplicate
calendar.events = events.reduce((list, event) => {
if (list[event.uid]) {
console.debug('Duplicate event overrided', list[event.uid], event)
}
Vue.set(list, event.uid, event)
return list
}, calendar.events)
changeCalendarColor(context, { calendar, newColor }) {
calendar = state.calendars.find(search => search.id === calendar.id)
calendar.color = newColor
},
/**
* Add an event to an calendar and overwrite if duplicate uid
*
* @param {Object} state the store data
* @param {Event} event the event to add
* Change calendar's order
* @param {Object} context the store mutations
* @param {Object} data destructuring object
* @param {Object} data.calendar the calendar to rename
* @param {String} data.newOrder the new order of the calendar
*/
addEventToCalendar(state, event) {
let calendar = state.calendars.find(search => search.id === event.calendar.id)
Vue.set(calendar.events, event.uid, event)
},
/**
* Delete an event in a specified calendar
*
* @param {Object} state the store data
* @param {Event} event the event to delete
*/
deleteEventFromCalendar(state, event) {
let calendar = state.calendars.find(search => search.id === event.calendar.id)
Vue.delete(calendar, event.uid)
changeCalendarOrder(context, { calendar, newOrder }) {
calendar = state.calendars.find(search => search.id === calendar.id)
calendar.order = newOrder
},
/**
@ -135,17 +182,19 @@ const mutations = {
* @param {Object} state the store data
* @param {Object} data destructuring object
* @param {Object} data.calendar the calendar
* @param {string} data.sharee the sharee
* @param {string} data.id id
* @param {Boolean} data.group group
* @param {string} data.user the userId
* @param {string} data.displayName the displayName
* @param {string} data.uri the sharing principalScheme uri
* @param {Boolean} data.isGroup is this a group ?
*/
shareCalendar(state, { calendar, sharee, id, group }) {
shareCalendar(state, { calendar, user, displayName, uri, isGroup }) {
calendar = state.calendars.find(search => search.id === calendar.id)
let newSharee = {
displayname: sharee,
id,
const newSharee = {
displayName,
id: user,
writeable: false,
group
isGroup,
uri
}
calendar.shares.push(newSharee)
},
@ -154,41 +203,75 @@ const mutations = {
* Remove Sharee from calendar shares list
*
* @param {Object} state the store data
* @param {Object} sharee the sharee
* @param {Object} data destructuring object
* @param {Object} data.calendar the calendar
* @param {string} data.uri the sharee uri
*/
removeSharee(state, sharee) {
let calendar = state.calendars.find(search => {
for (let i in search.shares) {
if (search.shares[i] === sharee) {
return true
}
}
})
calendar.shares.splice(calendar.shares.indexOf(sharee), 1)
unshareCalendar(state, { calendar, uri }) {
calendar = state.calendars.find(search => search.id === calendar.id)
let shareIndex = calendar.shares.findIndex(sharee => sharee.uri === uri)
calendar.shares.splice(shareIndex, 1)
},
/**
* Toggle sharee's writable permission
*
* @param {Object} state the store data
* @param {Object} sharee the sharee
* @param {Object} data destructuring object
* @param {Object} data.calendar the calendar
* @param {string} data.uri the sharee uri
*/
updateShareeWritable(state, sharee) {
let calendar = state.calendars.find(search => {
for (let i in search.shares) {
if (search.shares[i] === sharee) {
return true
}
}
})
sharee = calendar.shares.find(search => search === sharee)
toggleCalendarShareWritable(state, { calendar, uri }) {
calendar = state.calendars.find(search => search.id === calendar.id)
let sharee = calendar.shares.find(sharee => sharee.uri === uri)
sharee.writeable = !sharee.writeable
}
},
/**
* Publish a calendar calendar
*
* @param {Object} state the store data
* @param {Object} data destructuring object
* @param {Object} data.calendar the calendar to publish
* @param {String} data.publishURL published URL of calendar
*/
publishCalendar(state, { calendar, publishURL }) {
calendar = state.calendars.find(search => search.id === calendar.id)
calendar.publishURL = publishURL
},
/**
* Unpublish a calendar
*
* @param {Object} state the store data
* @param {Object} data destructuring object
* @param {Object} data.calendar the calendar to unpublish
*/
unpublishCalendar(state, { calendar }) {
calendar = state.calendars.find(search => search.id === calendar.id)
calendar.publishURL = null
}
}
const getters = {
getCalendars: state => state.calendar
sortedCalendars(state) {
console.debug(state.calendars)
return state.calendars
.filter(calendar => calendar.components.includes('VEVENT'))
.filter(calendar => !calendar.readOnly)
.sort((a, b) => a.order - b.order)
},
sortedSubscriptions(state) {
return state.calendars
.filter(calendar => calendar.components.includes('VEVENT'))
.filter(calendar => calendar.readOnly)
.sort((a, b) => a.order - b.order)
},
enabledCalendars(state) {
return state.calendars
.filter(calendar => calendar.components.includes('VEVENT'))
.filter(calendar => calendar.enabled)
}
}
const actions = {
@ -200,71 +283,88 @@ const actions = {
* @returns {Promise<Array>} the calendars
*/
async getCalendars(context) {
let calendars = await client.calendarHomes[0].findAllCalendars()
.then(calendars => {
return calendars.map(calendar => {
return mapDavCollectionToCalendar(calendar)
})
})
calendars.forEach(calendar => {
context.commit('addCalendar', calendar)
const calendars = await client.calendarHomes[0].findAllCalendars()
calendars.map(mapDavCollectionToCalendar).forEach(calendar => {
context.commit('addCalendar', { calendar })
})
return calendars
return [1]
},
/**
* Append a new calendar to array of existing calendars
*
* @param {Object} context the store mutations
* @param {Object} calendar The calendar to append
* @param {Object} data destructuring object
* @param {Object} data.calendar the new calendar to append
* @returns {Promise}
*/
async appendCalendar(context, calendar) {
return client.calendarHomes[0].createCalendarCollection(calendar.displayName)
async appendCalendar(context, { calendar }) {
const { displayName, color, order } = calendar
return client.calendarHomes[0].createCalendarCollection(displayName, color, ['VEVENT'], order)
.then((response) => {
calendar = mapDavCollectionToCalendar(response)
context.commit('addCalendar', calendar)
context.commit('addCalendar', { calendar })
})
.catch((error) => { throw error })
},
/**
* Delete calendar
* @param {Object} context the store mutations Current context
* @param {Object} calendar the calendar to delete
* Append a new subscription to array of existing calendars
*
* @param {Object} context the store mutations
* @param {Object} data destructuring object
* @param {Object} data.calendar the new subscription calendar to append
* @param {String} data.source source of new subscription
* @returns {Promise}
*/
async deleteCalendar(context, calendar) {
async appendSubscription(context, { calendar, source }) {
const { displayName, color, order } = calendar
return client.calendarHomes[0].createSubscribedCollection(displayName, color, source, order)
.then((response) => {
calendar = mapDavCollectionToCalendar(response)
context.commit('addCalendar', { calendar })
})
.catch((error) => { throw error })
},
/**
* Delete a calendar
*
* @param {Object} context the store mutations Current context
* @param {Object} data destructuring object
* @param {Object} data.calendar the calendar to delete
* @returns {Promise}
*/
async deleteCalendar(context, { calendar }) {
return calendar.dav.delete()
.then((response) => {
// delete all the events from the store that belong to this calendar
Object.values(calendar.events)
.forEach(event => context.commit('deleteEvent', event))
// then delete the calendar
context.commit('deleteCalendar', calendar)
context.commit('deleteCalendar', { calendar })
})
.catch((error) => { throw error })
},
/**
* Toggle whether a calendar is Enabled
* Toggle whether a calendar is enabled
*
* @param {Object} context the store mutations Current context
* @param {Object} calendar the calendar to toggle
* @param {Object} data destructuring object
* @param {Object} data.calendar the calendar to modify
* @returns {Promise}
*/
async toggleCalendarEnabled(context, calendar) {
async toggleCalendarEnabled(context, { calendar }) {
calendar.dav.enabled = !calendar.dav.enabled
return calendar.dav.update()
.then((response) => context.commit('toggleCalendarEnabled', calendar))
.then((response) => context.commit('toggleCalendarEnabled', { calendar }))
.catch((error) => { throw error })
},
/**
* Rename a calendar
*
* @param {Object} context the store mutations Current context
* @param {Object} data.calendar the calendar to rename
* @param {Object} data destructuring object
* @param {Object} data.calendar the calendar to modify
* @param {String} data.newName the new name of the calendar
* @returns {Promise}
*/
@ -275,15 +375,153 @@ const actions = {
.catch((error) => { throw error })
},
/**
* Change a calendar's color
*
* @param {Object} context the store mutations Current context
* @param {Object} data destructuring object
* @param {Object} data.calendar the calendar to modify
* @param {String} data.newColor the new color of the calendar
* @returns {Promise}
*/
async changeCalendarColor(context, { calendar, newColor }) {
calendar.dav.color = newColor
return calendar.dav.update()
.then((response) => context.commit('changeCalendarColor', { calendar, newColor }))
.catch((error) => { throw error })
},
/**
* Change a calendar's order
*
* @param {Object} context the store mutations Current context
* @param {Object} data destructuring object
* @param {Object} data.calendar the calendar to modify
* @param {String} data.newOrder the new order of the calendar
* @returns {Promise}
*/
async changeCalendarOrder(context, { calendar, newOrder }) {
calendar.dav.order = newOrder
return calendar.dav.update()
.then((response) => context.commit('changeCalendarOrder', { calendar, newOrder }))
.catch((error) => { throw error })
},
/**
* Change order of multiple calendars
*
* @param {Object} context the store mutations Current context
* @param {Array} new order of calendars
*/
async changeMultipleCalendarOrders(context, { calendars }) {
// TODO - implement me
// TODO - extract new order from order of calendars in array
// send proppatch to all calendars
// limit number of requests similar to import
},
/**
* Share calendar with User or Group
*
* @param {Object} context the store mutations Current context
* @param {Object} data destructuring object
* @param {Object} data.calendar the calendar to share
* @param {string} data.user the userId
* @param {string} data.displayName the displayName
* @param {string} data.uri the sharing principalScheme uri
* @param {Boolean} data.isGroup is this a group ?
*/
async shareCalendar(context, { calendar, user, displayName, uri, isGroup }) {
// Share calendar with entered group or user
try {
await calendar.dav.share(uri)
context.commit('shareCalendar', { calendar, user, displayName, uri, isGroup })
} catch (error) {
throw error
}
},
/**
* Toggle permissions of calendar Sharees writeable rights
*
* @param {Object} context the store mutations Current context
* @param {Object} data destructuring object
* @param {Object} data.calendar the calendar to change
* @param {string} data.uri the sharing principalScheme uri
*/
async toggleCalendarShareWritable(context, { calendar, uri }) {
try {
const sharee = calendar.shares.find(sharee => sharee.uri === uri)
await calendar.dav.share(uri, !sharee.writeable)
context.commit('toggleCalendarShareWritable', { calendar, uri })
} catch (error) {
throw error
}
},
/**
* Remove sharee from calendar
*
* @param {Object} context the store mutations Current context
* @param {Object} data destructuring object
* @param {Object} data.calendar the calendar to change
* @param {string} data.uri the sharing principalScheme uri
*/
async unshareCalendar(context, { calendar, uri }) {
try {
await calendar.dav.unshare(uri)
context.commit('unshareCalendar', { calendar, uri })
} catch (error) {
throw error
}
},
/**
* Publish a calendar
*
* @param {Object} context the store mutations Current context
* @param {Object} data destructuring object
* @param {Object} data.calendar the calendar to change
* @returns {Promise<void>}
*/
async publishCalendar(context, { calendar }) {
return calendar.dav.publish()
.then((response) => {
const publishURL = calendar.dav.publishURL
context.commit('publishCalendar', { calendar, publishURL })
})
.catch((error) => { throw error })
},
/**
* Unpublish a calendar
*
* @param {Object} context the store mutations Current context
* @param {Object} data destructuring object
* @param {Object} data.calendar the calendar to change
* @returns {Promise<void>}
*/
async unpublishCalendar(context, { calendar }) {
return calendar.dav.unpublish()
.then((response) => context.commit('unpublishCalendar', { calendar }))
.catch((error) => { throw error })
},
/**
* Retrieve the events of the specified calendar
* and commit the results
*
* @param {Object} context the store mutations
* @param {Object} importDetails = { ics, calendar }
* @param {Object} data destructuring object
* @param {Object} data.calendar the calendar to get events from
* @param {Date} data.from the date to start querying events from
* @param {Date} data.to the last date to query events from
* @returns {Promise}
*/
async getEventsFromCalendar(context, { calendar }) {
async getEventsFromCalendar(context, { calendar, from, to }) {
// TODO
return calendar.dav.findByType('VEVENT')
.then((response) => {
// We don't want to lose the url information
@ -309,7 +547,9 @@ const actions = {
/**
*
* @param {Object} context the store mutations
* @param {Object} importDetails = { ics, calendar }
* @param {Object} data destructuring object
* @param {String} data.ics The ICS data to import
* @param {Object} data.calendar the calendar to import the ics data into
*/
async importEventsIntoCalendar(context, { ics, calendar }) {
const events = parseIcs(ics, calendar)
@ -350,71 +590,6 @@ const actions = {
context.commit('changeStage', 'default')
})
},
/**
* Remove sharee from calendar
* @param {Object} context the store mutations Current context
* @param {Object} sharee calendar sharee object
*/
removeSharee(context, sharee) {
context.commit('removeSharee', sharee)
},
/**
* Toggle permissions of calendar Sharees writeable rights
* @param {Object} context the store mutations Current context
* @param {Object} sharee calendar sharee object
*/
toggleShareeWritable(context, sharee) {
context.commit('updateShareeWritable', sharee)
},
/**
* Share calendar with User or Group
* @param {Object} context the store mutations Current context
* @param {Object} data.calendar the calendar
* @param {String} data.sharee the sharee
* @param {Boolean} data.id id
* @param {Boolean} data.group group
*/
shareCalendar(context, { calendar, sharee, id, group }) {
// Share calendar with entered group or user
context.commit('shareCalendar', { calendar, sharee, id, group })
},
/**
* Move an event to the provided calendar
*
* @param {Object} context the store mutations
* @param {Object} data destructuring object
* @param {Event} data.event the event to move
* @param {Object} data.calendar the calendar to move the event to
*/
async moveEventToCalendar(context, { event, calendar }) {
// only local move if the event doesn't exists on the server
if (event.dav) {
// TODO: implement proper move
// await events.dav.move(calendar.dav)
// .catch((error) => {
// console.error(error)
// OC.Notification.showTemporary(t('calendars', 'An error occurred'))
// })
let vData = ICAL.stringify(event.vCard.jCal)
let newDav
await calendar.dav.createVCard(vData)
.then((response) => { newDav = response })
.catch((error) => { throw error })
await event.dav.delete()
.catch((error) => {
console.error(error)
OC.Notification.showTemporary(t('calendars', 'An error occurred'))
})
await Vue.set(event, 'dav', newDav)
}
await context.commit('deleteEventFromCalendar', event)
await context.commit('updateEventCalendar', { event, calendar })
await context.commit('addEventToCalendar', event)
}
}
export default { state, mutations, getters, actions }

17
src/store/editingEvent.js Normal file
View File

@ -0,0 +1,17 @@
const state = {
}
const mutations = {
}
const getters = {
}
const actions = {
}
export default { state, mutations, getters, actions }

View File

@ -1,6 +1,7 @@
import Vue from 'vue'
import Vuex from 'vuex'
import calendars from './calendars.js'
import editingEvent from './editingEvent.js'
import settings from './settings.js'
Vue.use(Vuex)
@ -10,6 +11,7 @@ const mutations = {}
export default new Vuex.Store({
modules: {
calendars,
editingEvent,
settings
},