feat(editors): redesign editors
Signed-off-by: Richard Steinmetz <richard@steinmetz.cloud>
|
@ -37,11 +37,14 @@
|
|||
}
|
||||
}
|
||||
|
||||
/** Hide the submit button for the title, because it does not trigger a save */
|
||||
.app-sidebar-header__mainname-form {
|
||||
button {
|
||||
display: none;
|
||||
// We use our custom header layout for the sidebar editor
|
||||
.app-sidebar-header__info {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.app-sidebar-header__description {
|
||||
// Close button should be aligned with calendar picker (header)
|
||||
padding-top: 5px;
|
||||
}
|
||||
|
||||
.editor-invitee-list-empty-message,
|
||||
|
@ -228,6 +231,18 @@
|
|||
.property-title-time-picker {
|
||||
width: 100%;
|
||||
|
||||
&--readonly {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
margin-left: -5px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
&__time-pickers,
|
||||
&__all-day {
|
||||
display: flex;
|
||||
|
@ -235,10 +250,12 @@
|
|||
}
|
||||
|
||||
&__time-pickers {
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
gap: 5px;
|
||||
|
||||
.mx-datepicker {
|
||||
width: 49%;
|
||||
flex: 1 auto;
|
||||
|
||||
.mx-input-append {
|
||||
background-color: transparent !important;
|
||||
|
@ -246,16 +263,24 @@
|
|||
}
|
||||
|
||||
&--readonly {
|
||||
justify-content: start;
|
||||
|
||||
.property-title-time-picker-read-only-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 50%;
|
||||
margin: 3px 3px 3px 0;
|
||||
padding: 8px 7px;
|
||||
background-color: var(--color-main-background);
|
||||
color: var(--color-main-text);
|
||||
outline: none;
|
||||
|
||||
&--start-date {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
&--end-date {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
margin-left: 8px;
|
||||
height: 16px;
|
||||
|
@ -275,22 +300,14 @@
|
|||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1500px) {
|
||||
&__time-pickers {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mx-datepicker {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.property-title-time-picker-read-only-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&__all-day {
|
||||
justify-content: flex-start;
|
||||
padding-left: 3px;
|
||||
margin-top: 5px;
|
||||
|
||||
// Reduce the height just a little bit (from 44px) to save some space
|
||||
.checkbox-radio-switch__label {
|
||||
min-height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.datetime-picker-inline-icon {
|
||||
|
@ -421,7 +438,6 @@
|
|||
&__summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 5px;
|
||||
|
||||
&__icon {
|
||||
width: 34px;
|
||||
|
@ -432,7 +448,7 @@
|
|||
|
||||
&__content {
|
||||
flex: 1 auto;
|
||||
padding: 0 7px;
|
||||
padding: 8px 7px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
@ -519,7 +535,6 @@
|
|||
display: flex;
|
||||
width: 100%;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 5px;
|
||||
|
||||
&__icon,
|
||||
&__info {
|
||||
|
@ -536,6 +551,7 @@
|
|||
&__info {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
|
@ -568,7 +584,6 @@
|
|||
div {
|
||||
width: calc(100% - 8px); /* for typical (thin) scrollbar size */
|
||||
white-space: pre-line;
|
||||
margin: 3px 3px 3px 0;
|
||||
padding: 8px 7px;
|
||||
background-color: var(--color-main-background);
|
||||
color: var(--color-main-text);
|
||||
|
@ -577,26 +592,31 @@
|
|||
word-break: break-word; /* allows breaking on long URLs */
|
||||
max-height: 30vh;
|
||||
}
|
||||
|
||||
a.linkified {
|
||||
text-decoration: underline;
|
||||
|
||||
&::after {
|
||||
content: ' ↗';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&--readonly-calendar-picker {
|
||||
|
||||
div.calendar-picker-option {
|
||||
margin: 3px 3px 3px 0;
|
||||
padding: 8px 7px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.property-text,
|
||||
.property-select,
|
||||
.property-color,
|
||||
.property-select-multiple,
|
||||
.property-title,
|
||||
.property-repeat,
|
||||
.resource-capacity,
|
||||
.resource-room-type {
|
||||
margin-bottom: 5px;
|
||||
|
||||
&--readonly {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.property-select,
|
||||
.property-select-multiple {
|
||||
align-items: center;
|
||||
|
@ -611,21 +631,46 @@
|
|||
}
|
||||
|
||||
.property-color {
|
||||
|
||||
&__input {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
margin-bottom: 5px;
|
||||
|
||||
&--readonly {
|
||||
// Align with other (text based) fields
|
||||
margin: 3px 0 3px 7px;
|
||||
}
|
||||
}
|
||||
|
||||
&__color-preview {
|
||||
border-radius: var(--border-radius);
|
||||
height: 34px !important;
|
||||
width: 34px !important;
|
||||
margin: 0;
|
||||
$size: 44px;
|
||||
width: $size !important;
|
||||
height: $size !important;
|
||||
border-radius: $size;
|
||||
}
|
||||
}
|
||||
|
||||
.property-text {
|
||||
&__icon {
|
||||
// Prevent icon misalignment on vertically growing inputs
|
||||
height: unset;
|
||||
align-self: flex-start;
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
&--readonly {
|
||||
.property-text__icon {
|
||||
padding-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
&__input {
|
||||
&--readonly {
|
||||
// Reduce line height but still keep first row aligned to the icon
|
||||
line-height: 1;
|
||||
padding-top: calc(var(--default-line-height) / 2 - 0.5lh);
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: none;
|
||||
}
|
||||
|
@ -655,12 +700,29 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fix weird height
|
||||
&__input {
|
||||
max-height: 44px;
|
||||
}
|
||||
}
|
||||
|
||||
.property-title {
|
||||
&__input,
|
||||
&__input input {
|
||||
font-size: 20px;
|
||||
&__input, input {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&__input--readonly {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize gaps between all properties. We use outer margins between each row so a padding
|
||||
// around inputs (from core) is not required.
|
||||
.property-title,
|
||||
.property-title-time-picker {
|
||||
input {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -668,16 +730,19 @@
|
|||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.illustration-header {
|
||||
max-height: 150px;
|
||||
height: 150px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.event-popover .event-popover__inner {
|
||||
.event-popover__response-buttons {
|
||||
margin-top: 8px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.illustration-header svg {
|
||||
width: 100%;
|
||||
height: 150px;
|
||||
padding: 8px 8px 0 8px;
|
||||
.property-text,
|
||||
.property-title-time-picker {
|
||||
&__icon {
|
||||
margin: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -705,27 +770,21 @@
|
|||
text-align: left;
|
||||
max-width: 480px;
|
||||
width: 480px;
|
||||
padding: 5px 8px;
|
||||
padding: 5px 10px 10px 10px;
|
||||
|
||||
.empty-content {
|
||||
margin-top: 0 !important;
|
||||
padding: 50px 0;
|
||||
}
|
||||
|
||||
.illustration-header {
|
||||
height: 100px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 5px;
|
||||
background-color: var(--color-background-dark);
|
||||
// There is probably a more elegant solution for this
|
||||
margin: -5px 0 5px -8px;
|
||||
width: 496px;
|
||||
border-top-left-radius: var(--border-radius);
|
||||
border-top-right-radius: var(--border-radius);
|
||||
.property-title-time-picker:not(.property-title-time-picker--readonly) {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.property-title-time-picker {
|
||||
margin-bottom: 12px;
|
||||
.event-popover__invitees {
|
||||
.avatar-participation-status__text {
|
||||
bottom: 22px;
|
||||
}
|
||||
}
|
||||
|
||||
.event-popover__buttons {
|
||||
|
@ -824,7 +883,11 @@
|
|||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
// Only apply the margin if at least one button is being rendered
|
||||
&:not(:empty) {
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.resource-search-list-item,
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
cursor: text;
|
||||
width: 100% !important;
|
||||
box-sizing: border-box;
|
||||
margin-top: 1px !important;
|
||||
padding: 12px;
|
||||
white-space: pre-line;
|
||||
overflow: auto;
|
||||
|
@ -19,7 +18,14 @@
|
|||
max-height: 16em;
|
||||
max-height: calc(100vh - 500px);
|
||||
|
||||
a.linkified::after {
|
||||
a.linkified {
|
||||
text-decoration: underline;
|
||||
|
||||
// Prevent misalignment when a linkified line starts with a link, e.g. in the location field
|
||||
margin: 0;
|
||||
|
||||
&::after {
|
||||
content: ' ↗';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Before Width: | Height: | Size: 90 KiB |
Before Width: | Height: | Size: 9.8 KiB |
Before Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 9.7 KiB |
Before Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 7.4 KiB |
Before Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 6.3 KiB |
Before Width: | Height: | Size: 72 KiB |
Before Width: | Height: | Size: 8.5 KiB |
Before Width: | Height: | Size: 45 KiB |
Before Width: | Height: | Size: 5.6 KiB |
Before Width: | Height: | Size: 8.6 KiB |
Before Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 68 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 27 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 7.3 KiB |
Before Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 8.9 KiB |
Before Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 6.4 KiB |
Before Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 79 KiB |
Before Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 6.4 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 9.4 KiB |
Before Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 45 KiB |
Before Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 5.9 KiB |
Before Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 7.4 KiB |
Before Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 6.7 KiB |
Before Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 6.2 KiB |
Before Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 9.7 KiB |
Before Width: | Height: | Size: 7.8 KiB |
Before Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 5.7 KiB |
Before Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 27 KiB |
Before Width: | Height: | Size: 27 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 57 KiB |
Before Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 9.3 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 14 KiB |
|
@ -8,15 +8,15 @@
|
|||
<div class="attachments-summary">
|
||||
<div class="attachments-summary-inner">
|
||||
<Paperclip :size="20" />
|
||||
<div v-if="attachments.length > 0">
|
||||
<div v-if="attachments.length > 0" class="attachments-summary-inner-label">
|
||||
{{ n('calendar', '{count} attachment', '{count} attachments', attachments.length, { count: attachments.length }) }}
|
||||
</div>
|
||||
<div v-else>
|
||||
<div v-else class="attachments-summary-inner-label">
|
||||
{{ t('calendar', 'No attachments') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<NcActions>
|
||||
<NcActions v-if="!isReadOnly">
|
||||
<template #icon>
|
||||
<Plus :size="20" />
|
||||
</template>
|
||||
|
@ -35,9 +35,10 @@
|
|||
</NcActions>
|
||||
</div>
|
||||
<div v-if="attachments.length > 0">
|
||||
<ul class="attachments-list-item">
|
||||
<ul class="attachments-list">
|
||||
<NcListItem v-for="attachment in attachments"
|
||||
:key="attachment.path"
|
||||
class="attachments-list-item"
|
||||
:force-display-actions="true"
|
||||
:name="getBaseName(attachment.fileName)"
|
||||
@click="openFile(attachment.uri)">
|
||||
|
@ -45,7 +46,8 @@
|
|||
<img :src="getPreview(attachment)" class="attachment-icon">
|
||||
</template>
|
||||
<template #actions>
|
||||
<NcActionButton @click="deleteAttachmentFromEvent(attachment)">
|
||||
<NcActionButton v-if="!isReadOnly"
|
||||
@click="deleteAttachmentFromEvent(attachment)">
|
||||
<template #icon>
|
||||
<Close :size="20" />
|
||||
</template>
|
||||
|
@ -217,13 +219,37 @@ export default {
|
|||
width: 34px;
|
||||
height: 34px;
|
||||
margin-left: -10px;
|
||||
margin-right: 10px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.attachments-summary-inner-label {
|
||||
padding: 0 7px;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.attachments-list-item {
|
||||
.attachments-list {
|
||||
margin: 0 -8px;
|
||||
|
||||
.attachments-list-item {
|
||||
// Reduce height to 44px
|
||||
:deep(.list-item) {
|
||||
padding: 0 8px;
|
||||
}
|
||||
:deep(.list-item-content__wrapper) {
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
:deep(.list-item-content) {
|
||||
// Align text with other properties
|
||||
padding-left: 18px;
|
||||
}
|
||||
|
||||
:deep(.line-one__title) {
|
||||
font-weight: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#attachments .empty-content {
|
||||
|
@ -240,8 +266,8 @@ export default {
|
|||
}
|
||||
}
|
||||
.attachment-icon {
|
||||
width: 40px;
|
||||
height: auto;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,167 @@
|
|||
<!--
|
||||
- @copyright Copyright (c) 2024 Richard Steinmetz <richard@steinmetz.cloud>
|
||||
-
|
||||
- @author Richard Steinmetz <richard@steinmetz.cloud>
|
||||
-
|
||||
- @license AGPL-3.0-or-later
|
||||
-
|
||||
- This program is free software: you can redistribute it and/or modify
|
||||
- it under the terms of the GNU 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 General Public License for more details.
|
||||
-
|
||||
- You should have received a copy of the GNU General Public License
|
||||
- along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="calendar-picker-header"
|
||||
:class="{ 'calendar-picker-header--readonly': isReadOnly }">
|
||||
<NcActions type="tertiary"
|
||||
class="calendar-picker-header__picker"
|
||||
:class="{ 'calendar-picker-header__picker--fix-width': isReadOnly && value && value.isSharedWithMe }"
|
||||
:menu-name="value.displayName"
|
||||
:force-name="true"
|
||||
:disabled="isDisabled">
|
||||
<template #icon>
|
||||
<div class="calendar-picker-header__icon">
|
||||
<div class="calendar-picker-header__icon__dot"
|
||||
:style="{ 'background-color': value.color }" />
|
||||
</div>
|
||||
</template>
|
||||
<NcActionButton v-for="calendar in calendars"
|
||||
:key="calendar.id"
|
||||
:close-after-click="true"
|
||||
@click="$emit('update:value', calendar)">
|
||||
<template #icon>
|
||||
<div class="calendar-picker-header__icon">
|
||||
<div class="calendar-picker-header__icon__dot"
|
||||
:style="{ 'background-color': calendar.color }" />
|
||||
</div>
|
||||
</template>
|
||||
{{ calendar.displayName }}
|
||||
</NcActionButton>
|
||||
</NcActions>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { NcActions, NcActionButton } from '@nextcloud/vue'
|
||||
|
||||
export default {
|
||||
name: 'CalendarPickerHeader',
|
||||
components: {
|
||||
NcActions,
|
||||
NcActionButton,
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
calendars: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
isReadOnly: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
isDisabled() {
|
||||
return this.isReadOnly || this.calendars.length < 2
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.event-popover {
|
||||
.calendar-picker-header {
|
||||
width: calc(100% - 79px);
|
||||
|
||||
button {
|
||||
margin-left: -9px;
|
||||
|
||||
.button-vue__text {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.calendar-picker-header--readonly button .button-vue__text {
|
||||
margin-left: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.app-sidebar {
|
||||
.calendar-picker-header {
|
||||
width: calc(100% - 30px);
|
||||
|
||||
button {
|
||||
margin-left: -14px;
|
||||
|
||||
.button-vue__text {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.calendar-picker-header--readonly button .button-vue__text {
|
||||
margin-left: 6px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.calendar-picker-header {
|
||||
align-self: flex-start;
|
||||
margin-bottom: 5px;
|
||||
|
||||
&__picker {
|
||||
width: 100%;
|
||||
|
||||
// For some reason the NcActions component behaves weirdly when a calendar is shared
|
||||
// read-only with the user. This is an ugly workaround to fix the width of the button.
|
||||
&--fix-width {
|
||||
width: unset;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
:deep(button.button-vue) {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
// Keep full opacity for disabled buttons
|
||||
&:disabled, :deep(:disabled) {
|
||||
opacity: 1 !important;
|
||||
filter: unset !important;
|
||||
}
|
||||
}
|
||||
|
||||
&__icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
|
||||
&__dot {
|
||||
$dot-size: 16px;
|
||||
width: $dot-size;
|
||||
height: $dot-size;
|
||||
border-radius: $dot-size;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -21,7 +21,8 @@
|
|||
-->
|
||||
|
||||
<template>
|
||||
<div class="invitation-response-buttons">
|
||||
<div class="invitation-response-buttons"
|
||||
:class="{ 'invitation-response-buttons--grow': growHorizontally }">
|
||||
<NcButton v-if="!isAccepted"
|
||||
type="primary"
|
||||
class="invitation-response-buttons__button"
|
||||
|
@ -87,6 +88,10 @@ export default {
|
|||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
growHorizontally: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -165,11 +170,16 @@ export default {
|
|||
<style lang="scss" scoped>
|
||||
.invitation-response-buttons {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
gap: 5px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
&__button {
|
||||
&--grow {
|
||||
width: 100%;
|
||||
|
||||
.invitation-response-buttons__button {
|
||||
flex: 1 auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -24,7 +24,13 @@
|
|||
-->
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="!hideIfEmpty || !isListEmpty" class="invitees-list">
|
||||
<div v-if="showHeader" class="invitees-list__header">
|
||||
<AccountMultipleIcon :size="20" />
|
||||
<b>{{ t('calendar', 'Attendees') }}</b>
|
||||
{{ statusHeader }}
|
||||
</div>
|
||||
|
||||
<InviteesListSearch v-if="!isReadOnly && !isSharedWithMe && hasUserEmailAddress"
|
||||
:already-invited-emails="alreadyInvitedEmails"
|
||||
:organizer="calendarObjectInstance.organizer"
|
||||
|
@ -32,22 +38,24 @@
|
|||
<OrganizerListItem v-if="hasOrganizer"
|
||||
:is-read-only="isReadOnly || isSharedWithMe"
|
||||
:organizer="calendarObjectInstance.organizer" />
|
||||
<InviteesListItem v-for="invitee in inviteesWithoutOrganizer"
|
||||
<InviteesListItem v-for="invitee in limitedInviteesWithoutOrganizer"
|
||||
:key="invitee.email"
|
||||
:attendee="invitee"
|
||||
:is-read-only="isReadOnly || isSharedWithMe"
|
||||
:organizer-display-name="organizerDisplayName"
|
||||
:members="invitee.members"
|
||||
@remove-attendee="removeAttendee" />
|
||||
<NoAttendeesView v-if="isReadOnly && isListEmpty"
|
||||
:message="noInviteesMessage" />
|
||||
<NoAttendeesView v-if="!isReadOnly && isListEmpty && hasUserEmailAddress"
|
||||
:message="noInviteesMessage" />
|
||||
<NoAttendeesView v-if="isSharedWithMe"
|
||||
<div v-if="limit > 0 && inviteesWithoutOrganizer.length > limit"
|
||||
class="invitees-list__more">
|
||||
{{ n('calendar', '%n more guest', '%n more guests', inviteesWithoutOrganizer.length - limit) }}
|
||||
</div>
|
||||
<NoAttendeesView v-if="isReadOnly && isSharedWithMe && !hideErrors"
|
||||
:message="noOwnerMessage" />
|
||||
<OrganizerNoEmailError v-if="!isReadOnly && isListEmpty && !hasUserEmailAddress" />
|
||||
<NoAttendeesView v-else-if="isReadOnly && isListEmpty && hasUserEmailAddress"
|
||||
:message="noInviteesMessage" />
|
||||
<OrganizerNoEmailError v-else-if="!isReadOnly && isListEmpty && !hasUserEmailAddress && !hideErrors" />
|
||||
|
||||
<div class="invitees-list-button-group">
|
||||
<div v-if="!hideButtons" class="invitees-list-button-group">
|
||||
<NcButton v-if="isCreateTalkRoomButtonVisible"
|
||||
class="invitees-list-button-group__button"
|
||||
:disabled="isCreateTalkRoomButtonDisabled"
|
||||
|
@ -86,6 +94,7 @@ import {
|
|||
showError,
|
||||
} from '@nextcloud/dialogs'
|
||||
import { organizerDisplayName, removeMailtoPrefix } from '../../../utils/attendee.js'
|
||||
import AccountMultipleIcon from 'vue-material-design-icons/AccountMultiple.vue'
|
||||
|
||||
export default {
|
||||
name: 'InviteesList',
|
||||
|
@ -97,6 +106,7 @@ export default {
|
|||
InviteesListItem,
|
||||
InviteesListSearch,
|
||||
OrganizerListItem,
|
||||
AccountMultipleIcon,
|
||||
},
|
||||
props: {
|
||||
isReadOnly: {
|
||||
|
@ -111,11 +121,32 @@ export default {
|
|||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
showHeader: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
hideIfEmpty: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
hideButtons: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
hideErrors: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
limit: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
creatingTalkRoom: false,
|
||||
showFreeBusyModel: false,
|
||||
recentAttendees: [],
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -146,8 +177,12 @@ export default {
|
|||
return false
|
||||
})
|
||||
},
|
||||
/**
|
||||
* All invitees except the organizer.
|
||||
*
|
||||
* @return {object[]}
|
||||
*/
|
||||
inviteesWithoutOrganizer() {
|
||||
|
||||
if (!this.calendarObjectInstance.organizer) {
|
||||
return this.invitees
|
||||
}
|
||||
|
@ -171,6 +206,25 @@ export default {
|
|||
return attendee.uri !== this.calendarObjectInstance.organizer.uri
|
||||
})
|
||||
},
|
||||
/**
|
||||
* All invitees except the organizer limited by the limit prop.
|
||||
* If the limit prop is 0 all invitees except the organizer are returned.
|
||||
*
|
||||
* @return {object[]}
|
||||
*/
|
||||
limitedInviteesWithoutOrganizer() {
|
||||
const filteredInvitees = this.inviteesWithoutOrganizer
|
||||
|
||||
if (this.limit) {
|
||||
const limit = this.hasOrganizer ? this.limit - 1 : this.limit
|
||||
return filteredInvitees
|
||||
// Push newly added attendees to the top of the list
|
||||
.toSorted((a, b) => this.recentAttendees.indexOf(b.uri) - this.recentAttendees.indexOf(a.uri))
|
||||
.slice(0, limit)
|
||||
}
|
||||
|
||||
return filteredInvitees
|
||||
},
|
||||
isOrganizer() {
|
||||
return this.calendarObjectInstance.organizer !== null
|
||||
&& this.$store.getters.getCurrentUserPrincipal !== null
|
||||
|
@ -221,6 +275,18 @@ export default {
|
|||
|
||||
return false
|
||||
},
|
||||
statusHeader() {
|
||||
if (!this.isReadOnly) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return this.t('calendar', '{invitedCount} invited, {confirmedCount} confirmed', {
|
||||
invitedCount: this.inviteesWithoutOrganizer.length,
|
||||
confirmedCount: this.inviteesWithoutOrganizer
|
||||
.filter((attendee) => attendee.participationStatus === 'ACCEPTED')
|
||||
.length,
|
||||
})
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
addAttendee({ commonName, email, calendarUserType, language, timezoneId, member }) {
|
||||
|
@ -237,6 +303,7 @@ export default {
|
|||
organizer: this.$store.getters.getCurrentUserPrincipal,
|
||||
member,
|
||||
})
|
||||
this.recentAttendees.push(email)
|
||||
},
|
||||
removeAttendee(attendee) {
|
||||
// Remove attendee from participating group
|
||||
|
@ -256,6 +323,7 @@ export default {
|
|||
calendarObjectInstance: this.calendarObjectInstance,
|
||||
attendee,
|
||||
})
|
||||
this.recentAttendees = this.recentAttendees.filter((a) => a.uri !== attendee.email)
|
||||
},
|
||||
openFreeBusy() {
|
||||
this.showFreeBusyModel = true
|
||||
|
@ -304,19 +372,35 @@ export default {
|
|||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.invitees-list-button-group {
|
||||
.invitees-list {
|
||||
margin-top: 12px;
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
padding: 5px 5px 5px 6px;
|
||||
}
|
||||
|
||||
&__more {
|
||||
padding: 15px 0 0 46px;
|
||||
font-weight: bold;
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.invitees-list-button-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.invitees-list-button-group__button {
|
||||
&__button {
|
||||
flex: 1 0 200px;
|
||||
|
||||
::v-deep .button-vue__text {
|
||||
:deep(.button-vue__text) {
|
||||
white-space: unset !important;
|
||||
overflow: unset !important;
|
||||
text-overflow: unset !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -21,7 +21,9 @@
|
|||
-->
|
||||
|
||||
<template>
|
||||
<div v-if="display" class="property-select">
|
||||
<div v-if="display"
|
||||
class="property-select"
|
||||
:class="{ 'property-select--readonly': isReadOnly }">
|
||||
<div class="property-select__input"
|
||||
:class="{ 'property-select__input--readonly-calendar-picker': isReadOnly }">
|
||||
<CalendarPicker v-if="!isReadOnly"
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
-->
|
||||
|
||||
<template>
|
||||
<div class="property-color">
|
||||
<div class="property-color" :class="{ 'property-color--readonly': isReadOnly }">
|
||||
<component :is="icon"
|
||||
:size="20"
|
||||
:name="readableName"
|
||||
|
|
|
@ -22,7 +22,9 @@
|
|||
-->
|
||||
|
||||
<template>
|
||||
<div v-if="display" class="property-select">
|
||||
<div v-if="display"
|
||||
class="property-select"
|
||||
:class="{ 'property-select--readonly': isReadOnly }">
|
||||
<component :is="icon"
|
||||
:size="20"
|
||||
:name="readableName"
|
||||
|
|
|
@ -22,7 +22,9 @@
|
|||
-->
|
||||
|
||||
<template>
|
||||
<div v-if="display" class="property-text">
|
||||
<div v-if="display"
|
||||
class="property-text"
|
||||
:class="{ 'property-text--readonly': isReadOnly }">
|
||||
<component :is="icon"
|
||||
:size="20"
|
||||
:name="readableName"
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
-->
|
||||
|
||||
<template>
|
||||
<div class="property-title">
|
||||
<div class="property-title" :class="{ 'property-title--readonly': isReadOnly }">
|
||||
<div class="property-title__input"
|
||||
:class="{ 'property-title__input--readonly': isReadOnly }">
|
||||
<input v-if="!isReadOnly"
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
-
|
||||
- @author Georg Ehrke <oc.list@georgehrke.com>
|
||||
- @author Jakob Röhrl <jakob.roehrl@web.de>
|
||||
- @author Richard Steinmetz <richard@steinmetz.cloud>
|
||||
-
|
||||
- @license AGPL-3.0-or-later
|
||||
-
|
||||
|
@ -23,7 +24,12 @@
|
|||
-->
|
||||
|
||||
<template>
|
||||
<div class="property-title-time-picker">
|
||||
<div class="property-title-time-picker"
|
||||
:class="{ 'property-title-time-picker--readonly': isReadOnly }">
|
||||
<CalendarIcon v-if="isReadOnly"
|
||||
class="property-title-time-picker__icon"
|
||||
:size="20" />
|
||||
|
||||
<div v-if="!isReadOnly"
|
||||
class="property-title-time-picker__time-pickers">
|
||||
<DatePicker :date="startDate"
|
||||
|
@ -46,7 +52,7 @@
|
|||
</div>
|
||||
<div v-if="isReadOnly"
|
||||
class="property-title-time-picker__time-pickers property-title-time-picker__time-pickers--readonly">
|
||||
<div class="property-title-time-picker-read-only-wrapper">
|
||||
<div class="property-title-time-picker-read-only-wrapper property-title-time-picker-read-only-wrapper--start-date">
|
||||
<div class="property-title-time-picker-read-only-wrapper__label">
|
||||
{{ formattedStart }}
|
||||
</div>
|
||||
|
@ -55,28 +61,26 @@
|
|||
:class="{ 'highlighted-timezone-icon': highlightStartTimezone }"
|
||||
:size="20" />
|
||||
</div>
|
||||
<div class="property-title-time-picker-read-only-wrapper">
|
||||
<template v-if="!isAllDayOneDayEvent">
|
||||
<div>-</div>
|
||||
<div class="property-title-time-picker-read-only-wrapper property-title-time-picker-read-only-wrapper--end-date">
|
||||
<div class="property-title-time-picker-read-only-wrapper__label">
|
||||
{{ formattedEnd }}
|
||||
</div>
|
||||
<IconTimezone v-if="!isAllDay"
|
||||
:name="endTimezone"
|
||||
:title="endTimezone"
|
||||
:class="{ 'highlighted-timezone-icon': highlightStartTimezone }"
|
||||
:size="20" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div v-if="!isReadOnly" class="property-title-time-picker__all-day">
|
||||
<input id="allDay"
|
||||
:checked="isAllDay"
|
||||
type="checkbox"
|
||||
class="checkbox"
|
||||
<NcCheckboxRadioSwitch :checked="isAllDay"
|
||||
:disabled="!canModifyAllDay"
|
||||
@change="toggleAllDay">
|
||||
<label v-tooltip="allDayTooltip"
|
||||
for="allDay">
|
||||
@update:checked="toggleAllDay">
|
||||
{{ $t('calendar', 'All day') }}
|
||||
</label>
|
||||
</NcCheckboxRadioSwitch>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -85,13 +89,17 @@
|
|||
import moment from '@nextcloud/moment'
|
||||
import DatePicker from '../../Shared/DatePicker.vue'
|
||||
import IconTimezone from 'vue-material-design-icons/Web.vue'
|
||||
import CalendarIcon from 'vue-material-design-icons/Calendar.vue'
|
||||
import { mapState } from 'vuex'
|
||||
import { NcCheckboxRadioSwitch } from '@nextcloud/vue'
|
||||
|
||||
export default {
|
||||
name: 'PropertyTitleTimePicker',
|
||||
components: {
|
||||
DatePicker,
|
||||
IconTimezone,
|
||||
CalendarIcon,
|
||||
NcCheckboxRadioSwitch,
|
||||
},
|
||||
props: {
|
||||
/**
|
||||
|
@ -195,16 +203,10 @@ export default {
|
|||
*/
|
||||
formattedStart() {
|
||||
if (this.isAllDay) {
|
||||
return this.$t('calendar', 'from {startDate}', {
|
||||
startDate: moment(this.startDate).locale(this.locale).format('L'),
|
||||
endDate: moment(this.endDate).locale(this.locale).format('L'),
|
||||
})
|
||||
return moment(this.startDate).locale(this.locale).format('ll')
|
||||
}
|
||||
|
||||
return this.$t('calendar', 'from {startDate} at {startTime}', {
|
||||
startDate: moment(this.startDate).locale(this.locale).format('L'),
|
||||
startTime: moment(this.startDate).locale(this.locale).format('LT'),
|
||||
})
|
||||
return moment(this.startDate).locale(this.locale).format('lll')
|
||||
},
|
||||
/**
|
||||
*
|
||||
|
@ -212,15 +214,10 @@ export default {
|
|||
*/
|
||||
formattedEnd() {
|
||||
if (this.isAllDay) {
|
||||
return this.$t('calendar', 'to {endDate}', {
|
||||
endDate: moment(this.endDate).locale(this.locale).format('L'),
|
||||
})
|
||||
return moment(this.endDate).locale(this.locale).format('ll')
|
||||
}
|
||||
|
||||
return this.$t('calendar', 'to {endDate} at {endTime}', {
|
||||
endDate: moment(this.endDate).locale(this.locale).format('L'),
|
||||
endTime: moment(this.endDate).locale(this.locale).format('LT'),
|
||||
})
|
||||
return moment(this.endDate).locale(this.locale).format('lll')
|
||||
},
|
||||
/**
|
||||
* @return {boolean}
|
||||
|
@ -234,6 +231,17 @@ export default {
|
|||
highlightEndTimezone() {
|
||||
return this.endTimezone !== this.userTimezone
|
||||
},
|
||||
/**
|
||||
* True if the event is an all day event, starts and ends on the same date
|
||||
*
|
||||
* @return {boolean}
|
||||
*/
|
||||
isAllDayOneDayEvent() {
|
||||
return this.isAllDay
|
||||
&& this.startDate.getDate() === this.endDate.getDate()
|
||||
&& this.startDate.getMonth() === this.endDate.getMonth()
|
||||
&& this.startDate.getFullYear() === this.endDate.getFullYear()
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
-->
|
||||
|
||||
<template>
|
||||
<div class="property-repeat">
|
||||
<div class="property-repeat" :class="{ 'property-repeat--readonly': isReadOnly }">
|
||||
<div class="property-repeat__summary">
|
||||
<RepeatIcon class="property-repeat__summary__icon"
|
||||
:name="$t('calendar', 'Repeat')"
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
{{ recurrenceRule | formatRecurrenceRule(locale) }}
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ $t('calendar', 'No recurrence') }}
|
||||
{{ $t('calendar', 'Does not repeat') }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
|
|