mirror of https://github.com/nextcloud/calendar
add Calendar list
Signed-off-by: Georg Ehrke <developer@georgehrke.com>
This commit is contained in:
parent
c43116302b
commit
3f8874a64f
|
@ -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'],
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
<template>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "SubscriptionList"
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -1,13 +0,0 @@
|
|||
<template>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "SubscriptionListItem"
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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'
|
||||
)
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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 }
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
const state = {
|
||||
|
||||
}
|
||||
|
||||
const mutations = {
|
||||
|
||||
}
|
||||
|
||||
const getters = {
|
||||
|
||||
}
|
||||
|
||||
const actions = {
|
||||
|
||||
}
|
||||
|
||||
export default { state, mutations, getters, actions }
|
|
@ -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
|
||||
},
|
||||
|
||||
|
|
Loading…
Reference in New Issue