Compare commits

...

42 Commits

Author SHA1 Message Date
Grigory Vodyanov 4837945888 Add email to sharing dialogs
Signed-off-by: Grigory Vodyanov <scratchx@gmx.com>
2024-04-25 18:06:16 +02:00
Anna 4711c64baa
Merge pull request #5953 from nextcloud/fix/cleanup-appointments
fix(appointments): simplify booking response
2024-04-25 17:03:36 +02:00
Anna e2554308c7
Merge pull request #5952 from nextcloud/chore/adjust-renovate-tags
chore: adjust renovate tags and base branches
2024-04-25 14:13:39 +02:00
Anna Larch dad5191d6f chore: adjust renovate tags and base branches
Signed-off-by: Anna Larch <anna@nextcloud.com>
2024-04-25 14:09:15 +02:00
Nextcloud bot 6c930f2be0
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2024-04-25 00:30:17 +00:00
Richard Steinmetz dea21127a1
Merge pull request #5936 from nextcloud/fix/5864-Align_heights_and_widths_for_inputs
fix(files): replace input fields with `NcTextField`
2024-04-24 08:37:12 +02:00
Richard Steinmetz f4be9d57d0
Merge pull request #5943 from nextcloud/feat/team-rebrand
feat: rebrand circles to teams in calendar sharing modal
2024-04-24 08:33:24 +02:00
renovate[bot] 3cb4d04baa
fix(deps): bump @nextcloud/event-bus from 3.2.0 to ^3.2.0 (main) (#5947)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-24 00:32:42 +00:00
renovate[bot] 9f1f3878ed
fix(deps): bump @nextcloud/auth from 2.2.1 to ^2.3.0 (main) (#5946)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-23 22:19:59 +00:00
renovate[bot] 2f8515d0fe
fix(deps): bump @nextcloud/router from 3.0.0 to ^3.0.1 (main) (#5945)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-23 22:18:54 +00:00
Richard Steinmetz 27f98ba8ae
feat: rebrand circles to teams in calendar sharing modal
Signed-off-by: Richard Steinmetz <richard@steinmetz.cloud>
2024-04-23 17:51:35 +02:00
julia.kirschenheuter 31dd0e9bc4
fix(files): replace input fields with `NcTextField`
Signed-off-by: julia.kirschenheuter <julia.kirschenheuter@nextcloud.com>
Signed-off-by: Richard Steinmetz <richard@steinmetz.cloud>
2024-04-23 14:54:05 +02:00
Nextcloud bot 96ae4cee81
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2024-04-23 00:30:47 +00:00
Richard Steinmetz 25e264a3c8
Merge pull request #5935 from nextcloud/fix/5839-fix-misplaced-empty-content
Fix misplaced empty content
2024-04-22 18:02:15 +02:00
julia.kirschenheuter 80e131a090 fix(files): fix misplaced empty content
Signed-off-by: julia.kirschenheuter <julia.kirschenheuter@nextcloud.com>
2024-04-22 16:03:34 +02:00
Anna df6aeef797
Merge pull request #5893 from nextcloud/fix/scheduling/email-attendee-search
fix(scheduling): Find attendee via email
2024-04-22 15:45:00 +02:00
Christoph Wurst 3c0ec2ade0 fix(scheduling): Find attendee via email
Signed-off-by: Christoph Wurst <christoph@winzerhof-wurst.at>

Decode avatar identifier

Signed-off-by: Grigory Vodyanov <scratchx@gmx.com>

Display common name instead of drop down name

Signed-off-by: Grigory Vodyanov <scratchx@gmx.com>
2024-04-22 15:29:39 +02:00
Richard Steinmetz 72302840aa
Merge pull request #5828 from nextcloud/renovate/main-nextcloud-vue-8.x
fix(deps): bump @nextcloud/vue from 8.11.2 to ^8.11.2 (main)
2024-04-20 20:17:03 +02:00
renovate[bot] e15a1a2663
fix(deps): bump @nextcloud/vue from 8.11.2 to ^8.11.2
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-20 18:08:26 +00:00
Richard Steinmetz acf19b92b1
Merge pull request #5829 from nextcloud/renovate/main-nextcloud-dialogs-5.x
fix(deps): bump @nextcloud/dialogs from 4.2.6 to v5 (main)
2024-04-20 20:06:30 +02:00
renovate[bot] e93fb34070
fix(deps): bump @nextcloud/dialogs from 4.2.6 to v5
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Richard Steinmetz <richard@steinmetz.cloud>
2024-04-20 20:02:01 +02:00
Nextcloud bot 6d1d2cb5ec
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2024-04-20 00:30:26 +00:00
Anna Larch 3acd93027c fix(appointments): simplify booking response
Signed-off-by: Anna Larch <anna@nextcloud.com>
2024-04-19 18:21:57 +02:00
Nextcloud bot 5a7cfc1bf2
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2024-04-19 00:31:57 +00:00
Grigory Vodyanov e959465999
Merge pull request #5852 from nextcloud/enh/automatic-free-slot
Enh/automatic free slot feature and add tests
2024-04-18 17:30:15 +02:00
Grigory Vodyanov 6b4845ac8d Enh/automatic free slot finding and add tests
Signed-off-by: Grigory Vodyanov <scratchx@gmx.com>
2024-04-18 16:10:18 +02:00
Nextcloud bot 6ea485f3fc
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2024-04-18 00:30:35 +00:00
Hamza d2f3d1953f
Merge pull request #5929 from nextcloud/Fix/remove-widget-header
Fix: Hide widget header for non-existent calendars
2024-04-17 18:01:54 +02:00
Hamza Mahjoubi 110f94805e Fix: Hide widget header for non-existant calendars
Signed-off-by: Hamza Mahjoubi <hamzamahjoubi221@gmail.com>
2024-04-17 11:21:31 +02:00
Hamza 97fce2ac52
Merge pull request #5891 from nextcloud/feat/calendar-widget-private-calendar
Feat: calendar widget for private calendars
2024-04-17 11:12:38 +02:00
Hamza Mahjoubi 4d624e7569 Feat: calendar widget for private calendars
Signed-off-by: Hamza Mahjoubi <hamzamahjoubi221@gmail.com>
2024-04-17 10:35:39 +02:00
Richard Steinmetz ddf7a4e95a
Merge pull request #5928 from nextcloud/test/fix-date-time-immutable
test: replace \Safe\DateTimeImmutable with \DateTimeImmutable
2024-04-17 08:45:01 +02:00
Richard Steinmetz 5ad0b84810
test: replace \Safe\DateTimeImmutable with \DateTimeImmutable
Signed-off-by: Richard Steinmetz <richard@steinmetz.cloud>
2024-04-17 08:31:01 +02:00
Nextcloud bot 7a17cce4e6
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2024-04-17 00:30:45 +00:00
Richard Steinmetz 7e07384dc1
Merge pull request #5914 from nextcloud/feat/dashboard/reload-widget
feat(dashboard): reload widget once every 10 minutes
2024-04-15 17:39:30 +02:00
Richard Steinmetz af94950cbd
feat(dashboard): reload widget once every 10 minutes
Signed-off-by: Richard Steinmetz <richard@steinmetz.cloud>
2024-04-15 15:59:58 +02:00
Nextcloud bot 279088248d
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2024-04-14 00:29:24 +00:00
Nextcloud bot 48912fd899
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2024-04-13 00:33:02 +00:00
Nextcloud bot a405475d40
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2024-04-12 00:29:55 +00:00
Nextcloud bot 62717de62e
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2024-04-11 00:30:20 +00:00
Nextcloud bot a9f70025fd
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2024-04-09 00:29:47 +00:00
Nextcloud bot 8f389355ba
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2024-04-08 00:29:31 +00:00
40 changed files with 1180 additions and 1345 deletions

23
__mocks__/css.js Normal file
View File

@ -0,0 +1,23 @@
/**
* @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/>.
*
*/
export default {}

View File

@ -163,8 +163,7 @@
display: flex;
align-items: center;
.multiselect,
input[type=number] {
.multiselect {
min-width: 100px;
width: 25%;
}

View File

@ -1,62 +1,153 @@
OC.L10N.register(
"calendar",
{
"Provided email-address is too long" : "La direición de corréu electrónicu apurrida ye mui llonga",
"User-Session unexpectedly expired" : "La sesión del usuariu caducó inesperadamente",
"Provided email-address is not valid" : "La direición de corréu electrónicu apurrida nun ye válida",
"%s has published the calendar »%s«" : "%s espublizó'l calendariu «%s»",
"Unexpected error sending email. Please contact your administrator." : "Prodúxose un error inesperáu al unviar el mensaxe. Ponte en contautu col alministrador.",
"Successfully sent email to %1$s" : "El mensaxe unvióse a %1$s correutamente",
"Hello," : "Hola,",
"We wanted to inform you that %s has published the calendar »%s«." : "Queremos informate que %s espublizó'l calendariu «%s».",
"Open »%s«" : "Abrir «%s»",
"Cheers!" : "¡Saludos!",
"Upcoming events" : "Eventos próximos",
"More events" : "Más eventos",
"No more events today" : "Nun hai más eventos pa güei",
"No upcoming events" : "Nun hai eventos próximos",
"%1$s with %2$s" : "%1$s con %2$s",
"Calendar" : "Calendariu",
"New booking {booking}" : "Reserva nueva «{booking}»",
"Appointments" : "Cites",
"%1$s - %2$s" : "%1$s - %2$s",
"Confirm" : "Confirmar",
"Date:" : "Data:",
"You will receive a link with the confirmation email" : "Vas recibir un enllaz col mensaxe de confirmación",
"Where:" : "Ónde:",
"Comment:" : "Comentariu:",
"Previous day" : "Día anterior",
"Previous week" : "Selmana pasada",
"Previous year" : "Añu pasáu",
"Previous month" : "Mes pasáu",
"Next day" : "Día siguiente",
"Next week" : "La selmana que vien",
"Next year" : "Añu siguiente",
"Next month" : "Mes siguiente",
"Event" : "Eventu",
"Create new event" : "Crear un eventu",
"Today" : "Güei",
"Day" : "Día",
"Week" : "Selmana",
"Month" : "Mes",
"Year" : "Añu",
"List" : "Llista",
"Preview" : "Previsualizar",
"Copy link" : "Copiar l'enllaz",
"Edit" : "Editar",
"Delete" : "Desaniciar",
"Untitled calendar" : "Calendariu ensin títulu",
"Edit and share calendar" : "Editar y comaprtir el calendariu",
"Edit calendar" : "Editar el calendariu",
"Disable calendar \"{calendar}\"" : "Desactivar el calendariu «{calendar}»",
"Disable untitled calendar" : "Desactivar el calendariu ensin títulu",
"Enable calendar \"{calendar}\"" : "¿Quies activar el calendariu «{calendar}»?",
"Enable untitled calendar" : "Activar el calendariu ensin títulu",
"An error occurred, unable to change visibility of the calendar." : "Prodúxose un error, nun ye posible camudar la visibilidá del calendariu.",
"_Unsharing the calendar in {countdown} second_::_Unsharing the calendar in {countdown} seconds_" : ["Va dexar de compartise'l calendariu en {countdown} segundu","Va dexar de compartise'l calendariu en {countdown} segundos"],
"_Deleting the calendar in {countdown} second_::_Deleting the calendar in {countdown} seconds_" : ["Va desaniciase'l calendariu en {countdown} segundu","Va desaniciase'l calendariu en {countdown} segundos"],
"New calendar" : "Calendariu nuevu",
"Name for new calendar" : "Nome del calendariu nuevu",
"Creating calendar …" : "Creando'l calendariu…",
"Creating subscription …" : "Creando la soscripción…",
"An error occurred, unable to create the calendar." : "Prodúxose un error, nun ye posible crear el calendariu.",
"Copy subscription link" : "Copiar l'enllaz de soscripción",
"Copying link …" : "Copiando l'enllaz …",
"Copied link" : "L'enllaz copióse",
"Could not copy link" : "Nun se pudo copiar l'enllaz",
"Export" : "Esportar",
"Calendar link copied to clipboard." : "L'enllaz del calendariu copióse nel cartafueyu.",
"Calendar link could not be copied to clipboard." : "L'enllaz del calendariu nun se pudo copiar nel cartafueyu.",
"Trash bin" : "Papelera",
"Loading deleted items." : "Cargando los elementos desaniciaos.",
"You do not have any deleted items." : "Nun tienes nengún elementu desaniciáu.",
"Name" : "Nome",
"Deleted" : "Desanicióse",
"Restore" : "Restaurar",
"Delete permanently" : "Desaniciar permanentemente",
"Empty trash bin" : "Balerar la papelera",
"Untitled item" : "Elementu ensin nome",
"Unknown calendar" : "Calendariu desconocíu",
"Could not load deleted calendars and objects" : "Nun se pudieron desaniciar los calendarios y los oxetos",
"Could not restore calendar or event" : "Nun se pudo restaurar el calendariu o l'eventu",
"Do you really want to empty the trash bin?" : "¿De xuru que quies balerar la papelera de reciclaxe?",
"Could not update calendar order." : "Nun se pudo anovar l'orde del calendariu.",
"Internal link" : "Enllaz internu",
"A private link that can be used with external clients" : "Un enllaz priváu que se pue usar con veceros esternos",
"Copy internal link" : "Copiar l'enllaz internu",
"Share link" : "Compartir l'enllaz",
"Copy public link" : "Copiar l'enllaz públicu",
"Send link to calendar via email" : "Unviar l'enllaz del calendariu per corréu electrónicu",
"Enter one address" : "Introduz una direción",
"Sending email …" : "Unviando'l mensaxe…",
"Copy embedding code" : "Copiar el códigu pa empotrar",
"Copying code …" : "Copiando'l códigu …",
"Copied code" : "Copióse'l códigu",
"Could not copy code" : "Nun se pudo copiar el códigu",
"Delete share link" : "Desaniciar esti enllaz d'usu compartíu",
"Deleting share link …" : "Desaniciando l'enllaz d'usu compartíu…",
"An error occurred, unable to publish calendar." : "Prodúxose un error, nun ye posible espublizar el calendariu.",
"An error occurred, unable to send email." : "Prodúxose un error, nun ye posible unviar el mensaxe.",
"Embed code copied to clipboard." : "El códigu incrustáu copióse nel cartafueyu.",
"Embed code could not be copied to clipboard." : "El códigu incrustáu nun se pudo copiar nel cartafueyu.",
"Unshare with {displayName}" : "Dexar de compartir con {displayName}",
"An error occurred while unsharing the calendar." : "Prodúxose un error mentanto se dexaba de compartir el calendariu.",
"An error occurred, unable to change the permission of the share." : "Prodúxose un error, nun ye posible camudar el permisu del elementu compartíu.",
"Share with users or groups" : "Compartir con usuarios o grupos",
"No users or groups" : "Nun hai nengún usuariu nin grupu",
"Calendar name …" : "Nome del calendariu…",
"Share calendar" : "Compartir el calendariu",
"Save" : "Guardar",
"Failed to save calendar name and color" : "Nun se pue guardar el nome y el color del calendariu",
"Import calendars" : "Importar calendarios",
"Please select a calendar to import into …" : "Seleiciona un calendariu al qu'importar …",
"Filename" : "Nome del ficheru",
"Cancel" : "Encaboxar",
"Invalid location selected" : "Seleicionóse una llocalización inválida",
"Automatic" : "Automáticu",
"or" : "o",
"Navigation" : "Navegación",
"Previous period" : "Periodu anterior",
"Next period" : "Periodu siguiente",
"Views" : "Vistes",
"Day view" : "Vista de díes",
"Week view" : "Vista de selmanes",
"Month view" : "Vista de meses",
"Year view" : "Vista d'años",
"List view" : "Vista de llista",
"Actions" : "Aiciones",
"Create event" : "Crear un eventu",
"Show shortcuts" : "Amosar los atayos",
"Editor" : "Editor",
"Close editor" : "Zarrar l'editor",
"Save edited event" : "Guardar l'eventu editáu",
"Delete edited event" : "Desaniciar l'eventu desaniciáu",
"Duplicate event" : "Duplicar l'eventu",
"Enable birthday calendar" : "Activar el calendariu de los cumpleaños",
"Show tasks in calendar" : "Amosar les xeres nel calendariu",
"Enable simplified editor" : "Activar l'editor simplificáu",
"Limit the number of events displayed in the monthly view" : "Llendar el númberu d'eventos amosaos na vista de meses",
"Show weekends" : "Amosar les fines de selmana",
"Show week numbers" : "Amosar los númberos de selmana",
"Default reminder" : "Recordatoriu predetermináu",
"Copy primary CalDAV address" : "Copiar la direición CalDAV primaria",
"Copy iOS/macOS CalDAV address" : "Copiar la direición CalDAV d'iOS/macOS",
"Personal availability settings" : "Configuración de la disponibilidá personal",
"Show keyboard shortcuts" : "Amosar los atayos del tecáu",
"Calendar settings" : "Configuración del calendariu",
"No reminder" : "Nun hai nengún recordatoriu",
"Failed to save default calendar" : "Nun se pue guardar el calendariu predetermináu",
"CalDAV link copied to clipboard." : "L'enllaz CalDAV copióse nel cartafueyu.",
"CalDAV link could not be copied to clipboard." : "L'enllaz CalDAV nun se pudo copiar nel cartafueyu.",
"_{duration} minute_::_{duration} minutes_" : ["{duration} minutu","{duration} minutos"],
"0 minutes" : "0 minutos",
"_{duration} hour_::_{duration} hours_" : ["{duration} hora","{duration} hores"],
@ -65,9 +156,12 @@ OC.L10N.register(
"_{duration} month_::_{duration} months_" : ["{duration} mes","{duration} meses"],
"_{duration} year_::_{duration} years_" : ["{duration} añu","{duration} años"],
"Location" : "Llocalización",
"Create a Talk room" : "Crear una sala de Talk",
"Description" : "Descripción",
"Visibility" : "Visibilidá",
"Duration" : "Duración",
"to" : "pa",
"Delete slot" : "Desaniciar la ralura",
"Add" : "Amestar",
"Monday" : "Llunes",
"Tuesday" : "Martes",
@ -81,25 +175,57 @@ OC.L10N.register(
"Your email address" : "La to direición de corréu electrónicu",
"Notification" : "Avisu",
"Email" : "Corréu electrónicu",
"Audio notification" : "Avisu d'audiu",
"Other notification" : "Otru avisu",
"Edit time" : "Editar la hora",
"Save time" : "Guardar la hora",
"Remove reminder" : "Quitar el recordatoriu",
"_second_::_seconds_" : ["segundu","segundos"],
"_minute_::_minutes_" : ["minutu","minutos"],
"_hour_::_hours_" : ["hora","hores"],
"_day_::_days_" : ["día","díes"],
"_week_::_weeks_" : ["selmana","selmanes"],
"Delete file" : "Desaniciar el ficheru",
"Invitation accepted" : "Invitación aceptada",
"Available" : "Disponible",
"Not available" : "Nun ta disponible",
"Invitation declined" : "Invitación refugada",
"Checking availability" : "Comprobando la disponibilidá",
"Awaiting response" : "Esperando pola rempuesta",
"Has not responded to {organizerName}'s invitation yet" : "Nun respondió a la invitación de: {organizerName}",
"Find a time" : "Atopar una hora",
"with" : "con",
"Available times:" : "Hores disponibles:",
"Suggestion accepted" : "Suxerencia aceptada",
"Done" : "Fecho",
"Busy" : "Ocupáu",
"Out of office" : "Fuera de la oficina",
"Accept" : "Aceptar",
"Decline" : "Refugar",
"Tentative" : "Provisional",
"The invitation has been accepted successfully." : "La invitación aceptóse correutamente.",
"Failed to accept the invitation." : "Nun se pue aceptar la invitación",
"The invitation has been declined successfully." : "La invitación refugóse correutamente.",
"Failed to decline the invitation." : "Nun se pue refugar la invitación",
"Your participation has been marked as tentative." : "La to participación marcóse como provisional.",
"Failed to set the participation status to tentative." : "Nun se pudo afitar l'estáu de la participación a provisional",
"Attendees" : "Asistentes",
"No attendees yet" : "Nun hai nengún asistente",
"You do not own this calendar, so you cannot add attendees to this event" : "Esti calendariu nun te pertenez, polo que nun pues amestar asistentes a esti eventu",
"Error creating Talk room" : "Hebo un error al crear la sala de Talk",
"_%n more guest_::_%n more guests_" : ["%n convidáu más","%n convidaos más"],
"Remove group" : "Quitar el grupu",
"_%n member_::_%n members_" : ["%n miembru","%n miembros"],
"No match found" : "Nun s'atopó nenguna coincidencia",
"Remove color" : "Quitar el color",
"Event title" : "Títulu del eventu",
"All day" : "Tol día",
"Repeat" : "Repitir",
"never" : "enxamás",
"first" : "primer",
"third" : "tercer",
"fourth" : "cuartu",
"fifth" : "quintu",
"_month_::_months_" : ["mes","meses"],
"_year_::_years_" : ["añu","años"],
"Suggestions" : "Suxerencies",
@ -108,6 +234,7 @@ OC.L10N.register(
"_{seatingCapacity} seat_::_{seatingCapacity} seats_" : ["{seatingCapacity} asientu","{seatingCapacity} asientos"],
"Projector" : "Proyeutor",
"Whiteboard" : "Pizarra",
"More details" : "Mas detalles",
"Pick a date" : "Escueyi una data",
"Please enter a valid date" : "Introduz una data válida",
"Global" : "Global",
@ -132,15 +259,27 @@ OC.L10N.register(
"_Every %n week_::_Every %n weeks_" : ["Cada %n selmana","Cada %n selmanes"],
"_Every %n month_::_Every %n months_" : ["Cada %n mes","Cada %n meses"],
"_Every %n year_::_Every %n years_" : ["Cada %n añu","Cada %n años"],
"_%n time_::_%n times_" : ["%n vegada","%n vegaes"],
"Untitled task" : "Xera ensin títulu",
"Please ask your administrator to enable the Tasks App." : "Pidi al alministrador qu'active l'aplicación Xeres.",
"W" : "S",
"%n more" : "%n más",
"_+%n more_::_+%n more_" : ["+%n más","+%n más"],
"No events" : "Nun hai nengún eventu",
"Failed to save event" : "Nun se pue guardar l'eventu",
"When shared show full event" : "Cuando se comparta amosar l'eventu completu",
"When shared show only busy" : "Cuando se comparta amosar namás si ta ocupáu",
"When shared hide this event" : "Cuando se comparta anubrir l'eventu",
"Status" : "Estáu",
"Canceled" : "Anulóse",
"Categories" : "Categories",
"Error while sharing file" : "Hebo un error mentanto se compartía'l ficheru",
"Error while sharing file with user" : "Hebo un error mentanto se compartía'l ficheru col usuariu",
"An error occurred during getting file information" : "Prodúxose un error demientres se consiguía la información del ficheru",
"Chat room for event" : "Sala de charra pal eventu",
"An error occurred, unable to delete the calendar." : "Prodúxose un error, nun ye posible desaniciar el calendariu",
"Imported {filename}" : "Importóse «{filename}»",
"This is an event reminder." : "Esto ye un recordatoriu del eventu.",
"User not found" : "Nun s'atopó l'usuariu"
},
"nplurals=2; plural=(n != 1);");

View File

@ -1,60 +1,151 @@
{ "translations": {
"Provided email-address is too long" : "La direición de corréu electrónicu apurrida ye mui llonga",
"User-Session unexpectedly expired" : "La sesión del usuariu caducó inesperadamente",
"Provided email-address is not valid" : "La direición de corréu electrónicu apurrida nun ye válida",
"%s has published the calendar »%s«" : "%s espublizó'l calendariu «%s»",
"Unexpected error sending email. Please contact your administrator." : "Prodúxose un error inesperáu al unviar el mensaxe. Ponte en contautu col alministrador.",
"Successfully sent email to %1$s" : "El mensaxe unvióse a %1$s correutamente",
"Hello," : "Hola,",
"We wanted to inform you that %s has published the calendar »%s«." : "Queremos informate que %s espublizó'l calendariu «%s».",
"Open »%s«" : "Abrir «%s»",
"Cheers!" : "¡Saludos!",
"Upcoming events" : "Eventos próximos",
"More events" : "Más eventos",
"No more events today" : "Nun hai más eventos pa güei",
"No upcoming events" : "Nun hai eventos próximos",
"%1$s with %2$s" : "%1$s con %2$s",
"Calendar" : "Calendariu",
"New booking {booking}" : "Reserva nueva «{booking}»",
"Appointments" : "Cites",
"%1$s - %2$s" : "%1$s - %2$s",
"Confirm" : "Confirmar",
"Date:" : "Data:",
"You will receive a link with the confirmation email" : "Vas recibir un enllaz col mensaxe de confirmación",
"Where:" : "Ónde:",
"Comment:" : "Comentariu:",
"Previous day" : "Día anterior",
"Previous week" : "Selmana pasada",
"Previous year" : "Añu pasáu",
"Previous month" : "Mes pasáu",
"Next day" : "Día siguiente",
"Next week" : "La selmana que vien",
"Next year" : "Añu siguiente",
"Next month" : "Mes siguiente",
"Event" : "Eventu",
"Create new event" : "Crear un eventu",
"Today" : "Güei",
"Day" : "Día",
"Week" : "Selmana",
"Month" : "Mes",
"Year" : "Añu",
"List" : "Llista",
"Preview" : "Previsualizar",
"Copy link" : "Copiar l'enllaz",
"Edit" : "Editar",
"Delete" : "Desaniciar",
"Untitled calendar" : "Calendariu ensin títulu",
"Edit and share calendar" : "Editar y comaprtir el calendariu",
"Edit calendar" : "Editar el calendariu",
"Disable calendar \"{calendar}\"" : "Desactivar el calendariu «{calendar}»",
"Disable untitled calendar" : "Desactivar el calendariu ensin títulu",
"Enable calendar \"{calendar}\"" : "¿Quies activar el calendariu «{calendar}»?",
"Enable untitled calendar" : "Activar el calendariu ensin títulu",
"An error occurred, unable to change visibility of the calendar." : "Prodúxose un error, nun ye posible camudar la visibilidá del calendariu.",
"_Unsharing the calendar in {countdown} second_::_Unsharing the calendar in {countdown} seconds_" : ["Va dexar de compartise'l calendariu en {countdown} segundu","Va dexar de compartise'l calendariu en {countdown} segundos"],
"_Deleting the calendar in {countdown} second_::_Deleting the calendar in {countdown} seconds_" : ["Va desaniciase'l calendariu en {countdown} segundu","Va desaniciase'l calendariu en {countdown} segundos"],
"New calendar" : "Calendariu nuevu",
"Name for new calendar" : "Nome del calendariu nuevu",
"Creating calendar …" : "Creando'l calendariu…",
"Creating subscription …" : "Creando la soscripción…",
"An error occurred, unable to create the calendar." : "Prodúxose un error, nun ye posible crear el calendariu.",
"Copy subscription link" : "Copiar l'enllaz de soscripción",
"Copying link …" : "Copiando l'enllaz …",
"Copied link" : "L'enllaz copióse",
"Could not copy link" : "Nun se pudo copiar l'enllaz",
"Export" : "Esportar",
"Calendar link copied to clipboard." : "L'enllaz del calendariu copióse nel cartafueyu.",
"Calendar link could not be copied to clipboard." : "L'enllaz del calendariu nun se pudo copiar nel cartafueyu.",
"Trash bin" : "Papelera",
"Loading deleted items." : "Cargando los elementos desaniciaos.",
"You do not have any deleted items." : "Nun tienes nengún elementu desaniciáu.",
"Name" : "Nome",
"Deleted" : "Desanicióse",
"Restore" : "Restaurar",
"Delete permanently" : "Desaniciar permanentemente",
"Empty trash bin" : "Balerar la papelera",
"Untitled item" : "Elementu ensin nome",
"Unknown calendar" : "Calendariu desconocíu",
"Could not load deleted calendars and objects" : "Nun se pudieron desaniciar los calendarios y los oxetos",
"Could not restore calendar or event" : "Nun se pudo restaurar el calendariu o l'eventu",
"Do you really want to empty the trash bin?" : "¿De xuru que quies balerar la papelera de reciclaxe?",
"Could not update calendar order." : "Nun se pudo anovar l'orde del calendariu.",
"Internal link" : "Enllaz internu",
"A private link that can be used with external clients" : "Un enllaz priváu que se pue usar con veceros esternos",
"Copy internal link" : "Copiar l'enllaz internu",
"Share link" : "Compartir l'enllaz",
"Copy public link" : "Copiar l'enllaz públicu",
"Send link to calendar via email" : "Unviar l'enllaz del calendariu per corréu electrónicu",
"Enter one address" : "Introduz una direción",
"Sending email …" : "Unviando'l mensaxe…",
"Copy embedding code" : "Copiar el códigu pa empotrar",
"Copying code …" : "Copiando'l códigu …",
"Copied code" : "Copióse'l códigu",
"Could not copy code" : "Nun se pudo copiar el códigu",
"Delete share link" : "Desaniciar esti enllaz d'usu compartíu",
"Deleting share link …" : "Desaniciando l'enllaz d'usu compartíu…",
"An error occurred, unable to publish calendar." : "Prodúxose un error, nun ye posible espublizar el calendariu.",
"An error occurred, unable to send email." : "Prodúxose un error, nun ye posible unviar el mensaxe.",
"Embed code copied to clipboard." : "El códigu incrustáu copióse nel cartafueyu.",
"Embed code could not be copied to clipboard." : "El códigu incrustáu nun se pudo copiar nel cartafueyu.",
"Unshare with {displayName}" : "Dexar de compartir con {displayName}",
"An error occurred while unsharing the calendar." : "Prodúxose un error mentanto se dexaba de compartir el calendariu.",
"An error occurred, unable to change the permission of the share." : "Prodúxose un error, nun ye posible camudar el permisu del elementu compartíu.",
"Share with users or groups" : "Compartir con usuarios o grupos",
"No users or groups" : "Nun hai nengún usuariu nin grupu",
"Calendar name …" : "Nome del calendariu…",
"Share calendar" : "Compartir el calendariu",
"Save" : "Guardar",
"Failed to save calendar name and color" : "Nun se pue guardar el nome y el color del calendariu",
"Import calendars" : "Importar calendarios",
"Please select a calendar to import into …" : "Seleiciona un calendariu al qu'importar …",
"Filename" : "Nome del ficheru",
"Cancel" : "Encaboxar",
"Invalid location selected" : "Seleicionóse una llocalización inválida",
"Automatic" : "Automáticu",
"or" : "o",
"Navigation" : "Navegación",
"Previous period" : "Periodu anterior",
"Next period" : "Periodu siguiente",
"Views" : "Vistes",
"Day view" : "Vista de díes",
"Week view" : "Vista de selmanes",
"Month view" : "Vista de meses",
"Year view" : "Vista d'años",
"List view" : "Vista de llista",
"Actions" : "Aiciones",
"Create event" : "Crear un eventu",
"Show shortcuts" : "Amosar los atayos",
"Editor" : "Editor",
"Close editor" : "Zarrar l'editor",
"Save edited event" : "Guardar l'eventu editáu",
"Delete edited event" : "Desaniciar l'eventu desaniciáu",
"Duplicate event" : "Duplicar l'eventu",
"Enable birthday calendar" : "Activar el calendariu de los cumpleaños",
"Show tasks in calendar" : "Amosar les xeres nel calendariu",
"Enable simplified editor" : "Activar l'editor simplificáu",
"Limit the number of events displayed in the monthly view" : "Llendar el númberu d'eventos amosaos na vista de meses",
"Show weekends" : "Amosar les fines de selmana",
"Show week numbers" : "Amosar los númberos de selmana",
"Default reminder" : "Recordatoriu predetermináu",
"Copy primary CalDAV address" : "Copiar la direición CalDAV primaria",
"Copy iOS/macOS CalDAV address" : "Copiar la direición CalDAV d'iOS/macOS",
"Personal availability settings" : "Configuración de la disponibilidá personal",
"Show keyboard shortcuts" : "Amosar los atayos del tecáu",
"Calendar settings" : "Configuración del calendariu",
"No reminder" : "Nun hai nengún recordatoriu",
"Failed to save default calendar" : "Nun se pue guardar el calendariu predetermináu",
"CalDAV link copied to clipboard." : "L'enllaz CalDAV copióse nel cartafueyu.",
"CalDAV link could not be copied to clipboard." : "L'enllaz CalDAV nun se pudo copiar nel cartafueyu.",
"_{duration} minute_::_{duration} minutes_" : ["{duration} minutu","{duration} minutos"],
"0 minutes" : "0 minutos",
"_{duration} hour_::_{duration} hours_" : ["{duration} hora","{duration} hores"],
@ -63,9 +154,12 @@
"_{duration} month_::_{duration} months_" : ["{duration} mes","{duration} meses"],
"_{duration} year_::_{duration} years_" : ["{duration} añu","{duration} años"],
"Location" : "Llocalización",
"Create a Talk room" : "Crear una sala de Talk",
"Description" : "Descripción",
"Visibility" : "Visibilidá",
"Duration" : "Duración",
"to" : "pa",
"Delete slot" : "Desaniciar la ralura",
"Add" : "Amestar",
"Monday" : "Llunes",
"Tuesday" : "Martes",
@ -79,25 +173,57 @@
"Your email address" : "La to direición de corréu electrónicu",
"Notification" : "Avisu",
"Email" : "Corréu electrónicu",
"Audio notification" : "Avisu d'audiu",
"Other notification" : "Otru avisu",
"Edit time" : "Editar la hora",
"Save time" : "Guardar la hora",
"Remove reminder" : "Quitar el recordatoriu",
"_second_::_seconds_" : ["segundu","segundos"],
"_minute_::_minutes_" : ["minutu","minutos"],
"_hour_::_hours_" : ["hora","hores"],
"_day_::_days_" : ["día","díes"],
"_week_::_weeks_" : ["selmana","selmanes"],
"Delete file" : "Desaniciar el ficheru",
"Invitation accepted" : "Invitación aceptada",
"Available" : "Disponible",
"Not available" : "Nun ta disponible",
"Invitation declined" : "Invitación refugada",
"Checking availability" : "Comprobando la disponibilidá",
"Awaiting response" : "Esperando pola rempuesta",
"Has not responded to {organizerName}'s invitation yet" : "Nun respondió a la invitación de: {organizerName}",
"Find a time" : "Atopar una hora",
"with" : "con",
"Available times:" : "Hores disponibles:",
"Suggestion accepted" : "Suxerencia aceptada",
"Done" : "Fecho",
"Busy" : "Ocupáu",
"Out of office" : "Fuera de la oficina",
"Accept" : "Aceptar",
"Decline" : "Refugar",
"Tentative" : "Provisional",
"The invitation has been accepted successfully." : "La invitación aceptóse correutamente.",
"Failed to accept the invitation." : "Nun se pue aceptar la invitación",
"The invitation has been declined successfully." : "La invitación refugóse correutamente.",
"Failed to decline the invitation." : "Nun se pue refugar la invitación",
"Your participation has been marked as tentative." : "La to participación marcóse como provisional.",
"Failed to set the participation status to tentative." : "Nun se pudo afitar l'estáu de la participación a provisional",
"Attendees" : "Asistentes",
"No attendees yet" : "Nun hai nengún asistente",
"You do not own this calendar, so you cannot add attendees to this event" : "Esti calendariu nun te pertenez, polo que nun pues amestar asistentes a esti eventu",
"Error creating Talk room" : "Hebo un error al crear la sala de Talk",
"_%n more guest_::_%n more guests_" : ["%n convidáu más","%n convidaos más"],
"Remove group" : "Quitar el grupu",
"_%n member_::_%n members_" : ["%n miembru","%n miembros"],
"No match found" : "Nun s'atopó nenguna coincidencia",
"Remove color" : "Quitar el color",
"Event title" : "Títulu del eventu",
"All day" : "Tol día",
"Repeat" : "Repitir",
"never" : "enxamás",
"first" : "primer",
"third" : "tercer",
"fourth" : "cuartu",
"fifth" : "quintu",
"_month_::_months_" : ["mes","meses"],
"_year_::_years_" : ["añu","años"],
"Suggestions" : "Suxerencies",
@ -106,6 +232,7 @@
"_{seatingCapacity} seat_::_{seatingCapacity} seats_" : ["{seatingCapacity} asientu","{seatingCapacity} asientos"],
"Projector" : "Proyeutor",
"Whiteboard" : "Pizarra",
"More details" : "Mas detalles",
"Pick a date" : "Escueyi una data",
"Please enter a valid date" : "Introduz una data válida",
"Global" : "Global",
@ -130,15 +257,27 @@
"_Every %n week_::_Every %n weeks_" : ["Cada %n selmana","Cada %n selmanes"],
"_Every %n month_::_Every %n months_" : ["Cada %n mes","Cada %n meses"],
"_Every %n year_::_Every %n years_" : ["Cada %n añu","Cada %n años"],
"_%n time_::_%n times_" : ["%n vegada","%n vegaes"],
"Untitled task" : "Xera ensin títulu",
"Please ask your administrator to enable the Tasks App." : "Pidi al alministrador qu'active l'aplicación Xeres.",
"W" : "S",
"%n more" : "%n más",
"_+%n more_::_+%n more_" : ["+%n más","+%n más"],
"No events" : "Nun hai nengún eventu",
"Failed to save event" : "Nun se pue guardar l'eventu",
"When shared show full event" : "Cuando se comparta amosar l'eventu completu",
"When shared show only busy" : "Cuando se comparta amosar namás si ta ocupáu",
"When shared hide this event" : "Cuando se comparta anubrir l'eventu",
"Status" : "Estáu",
"Canceled" : "Anulóse",
"Categories" : "Categories",
"Error while sharing file" : "Hebo un error mentanto se compartía'l ficheru",
"Error while sharing file with user" : "Hebo un error mentanto se compartía'l ficheru col usuariu",
"An error occurred during getting file information" : "Prodúxose un error demientres se consiguía la información del ficheru",
"Chat room for event" : "Sala de charra pal eventu",
"An error occurred, unable to delete the calendar." : "Prodúxose un error, nun ye posible desaniciar el calendariu",
"Imported {filename}" : "Importóse «{filename}»",
"This is an event reminder." : "Esto ye un recordatoriu del eventu.",
"User not found" : "Nun s'atopó l'usuariu"
},"pluralForm" :"nplurals=2; plural=(n != 1);"
}

View File

@ -11,6 +11,7 @@ OC.L10N.register(
"Calendar" : "Calendario",
"Appointments" : "Citas",
"Confirm" : "Confirmar",
"Date:" : "Fecha:",
"Where:" : "Dónde:",
"A Calendar app for Nextcloud" : "Una aplicación de Calendario para Nextcloud",
"Today" : "Hoy",
@ -23,6 +24,7 @@ OC.L10N.register(
"Copy link" : "Copiar liga",
"Edit" : "Editar",
"Delete" : "Borrar",
"Untitled calendar" : "Calendario sin título",
"New calendar" : "Nuevo calendario",
"Export" : "Exportar",
"Name" : "Nombre",
@ -53,6 +55,7 @@ OC.L10N.register(
"Visibility" : "Visibilidad",
"Duration" : "Duración",
"to" : "para",
"Delete slot" : "Eliminar ranura",
"Add" : "Agregar",
"Monday" : "Lunes",
"Tuesday" : "Martes",
@ -61,6 +64,7 @@ OC.L10N.register(
"Friday" : "Viernes",
"Saturday" : "Sábado",
"Sunday" : "Domingo",
"Weekdays" : "Días laborales",
"Update" : "Actualizar",
"Your email address" : "Tu dirección de correo electrónico",
"Notification" : "Notificación",
@ -74,6 +78,8 @@ OC.L10N.register(
"Checking availability" : "Comprobando disponibilidad",
"Availability of attendees, resources and rooms" : "Disponibilidad de asistentes, recursos y salas",
"Done" : "Terminado",
"Busy" : "Ocupado",
"Out of office" : "Fuera de la oficina",
"Unknown" : "Desconocido",
"Accept" : "Aceptar",
"Decline" : "Rechazar",
@ -90,15 +96,19 @@ OC.L10N.register(
"Pick a date" : "Elige una fecha",
"Global" : "Global",
"Subscribe" : "Suscríbete",
"Time:" : "Hora:",
"Personal" : "Personal",
"Create a new event" : "Crear un nuevo evento",
"Details" : "Detalles",
"Invite" : "Invitar",
"Resources" : "Recursos",
"Close" : "Cerrar",
"Untitled event" : "Evento sin título",
"Anniversary" : "Aniversario",
"Week {number} of {year}" : "Semana {number} de {year}",
"Daily" : "Diariamente",
"Weekly" : "Semanalmente",
"Untitled task" : "Tarea sin título",
"Other" : "Otro",
"When shared show full event" : "Al compartir, mostrar el evento completo",
"When shared show only busy" : "Al compartir, mostrar sólo como ocupado ",

View File

@ -9,6 +9,7 @@
"Calendar" : "Calendario",
"Appointments" : "Citas",
"Confirm" : "Confirmar",
"Date:" : "Fecha:",
"Where:" : "Dónde:",
"A Calendar app for Nextcloud" : "Una aplicación de Calendario para Nextcloud",
"Today" : "Hoy",
@ -21,6 +22,7 @@
"Copy link" : "Copiar liga",
"Edit" : "Editar",
"Delete" : "Borrar",
"Untitled calendar" : "Calendario sin título",
"New calendar" : "Nuevo calendario",
"Export" : "Exportar",
"Name" : "Nombre",
@ -51,6 +53,7 @@
"Visibility" : "Visibilidad",
"Duration" : "Duración",
"to" : "para",
"Delete slot" : "Eliminar ranura",
"Add" : "Agregar",
"Monday" : "Lunes",
"Tuesday" : "Martes",
@ -59,6 +62,7 @@
"Friday" : "Viernes",
"Saturday" : "Sábado",
"Sunday" : "Domingo",
"Weekdays" : "Días laborales",
"Update" : "Actualizar",
"Your email address" : "Tu dirección de correo electrónico",
"Notification" : "Notificación",
@ -72,6 +76,8 @@
"Checking availability" : "Comprobando disponibilidad",
"Availability of attendees, resources and rooms" : "Disponibilidad de asistentes, recursos y salas",
"Done" : "Terminado",
"Busy" : "Ocupado",
"Out of office" : "Fuera de la oficina",
"Unknown" : "Desconocido",
"Accept" : "Aceptar",
"Decline" : "Rechazar",
@ -88,15 +94,19 @@
"Pick a date" : "Elige una fecha",
"Global" : "Global",
"Subscribe" : "Suscríbete",
"Time:" : "Hora:",
"Personal" : "Personal",
"Create a new event" : "Crear un nuevo evento",
"Details" : "Detalles",
"Invite" : "Invitar",
"Resources" : "Recursos",
"Close" : "Cerrar",
"Untitled event" : "Evento sin título",
"Anniversary" : "Aniversario",
"Week {number} of {year}" : "Semana {number} de {year}",
"Daily" : "Diariamente",
"Weekly" : "Semanalmente",
"Untitled task" : "Tarea sin título",
"Other" : "Otro",
"When shared show full event" : "Al compartir, mostrar el evento completo",
"When shared show only busy" : "Al compartir, mostrar sólo como ocupado ",

View File

@ -222,6 +222,7 @@ OC.L10N.register(
"Friday" : "venres",
"Saturday" : "sábado",
"Sunday" : "domingo",
"Weekdays" : "Días laborables",
"Add time before and after the event" : "Engadir tempo antes e após o evento",
"Before the event" : "Antes do evento",
"After the event" : "Após o evento",

View File

@ -220,6 +220,7 @@
"Friday" : "venres",
"Saturday" : "sábado",
"Sunday" : "domingo",
"Weekdays" : "Días laborables",
"Add time before and after the event" : "Engadir tempo antes e após o evento",
"Before the event" : "Antes do evento",
"After the event" : "Após o evento",

View File

@ -3,6 +3,7 @@ OC.L10N.register(
{
"Cheers!" : "Prost!",
"Calendar" : "Kalenner",
"Confirm" : "Konfirméieren",
"Today" : "Haut",
"Day" : "Dag",
"Week" : "Woch",
@ -16,12 +17,14 @@ OC.L10N.register(
"Deleted" : "Geläscht",
"Restore" : "Zrécksetzen",
"Delete permanently" : "Permanent läschen",
"Empty trash bin" : "Eidel Dreckskëscht",
"Share link" : "Link deelen",
"can edit" : "kann änneren",
"Share with users or groups" : "Mat Benotzer oder Gruppen deelen",
"Save" : "Späicheren",
"Cancel" : "Ofbriechen",
"Automatic" : "Automatesch",
"List view" : "Lëscht Vue",
"Actions" : "Aktiounen",
"Location" : "Uert",
"Description" : "Beschreiwung",

View File

@ -1,6 +1,7 @@
{ "translations": {
"Cheers!" : "Prost!",
"Calendar" : "Kalenner",
"Confirm" : "Konfirméieren",
"Today" : "Haut",
"Day" : "Dag",
"Week" : "Woch",
@ -14,12 +15,14 @@
"Deleted" : "Geläscht",
"Restore" : "Zrécksetzen",
"Delete permanently" : "Permanent läschen",
"Empty trash bin" : "Eidel Dreckskëscht",
"Share link" : "Link deelen",
"can edit" : "kann änneren",
"Share with users or groups" : "Mat Benotzer oder Gruppen deelen",
"Save" : "Späicheren",
"Cancel" : "Ofbriechen",
"Automatic" : "Automatesch",
"List view" : "Lëscht Vue",
"Actions" : "Aktiounen",
"Location" : "Uert",
"Description" : "Beschreiwung",

View File

@ -25,12 +25,12 @@ OC.L10N.register(
"%1$s - %2$s" : "%1$s - %2$s",
"Prepare for %s" : "Voorbereiden op %s",
"Follow up for %s" : "Follow-up voor %s",
"Your appointment \"%s\" with %s needs confirmation" : "Je afspraak \"%s\" met %smoet nog bevestigd worden ",
"Your appointment \"%s\" with %s needs confirmation" : "Je afspraak \"%s\" met %s moet nog bevestigd worden",
"Dear %s, please confirm your booking" : "Beste %s, bevestig alsjeblieft je boeking",
"Confirm" : "Bevestigen",
"This confirmation link expires in %s hours." : "Deze bevestigingslink verloopt over %s uur.",
"If you wish to cancel the appointment after all, please contact your organizer by replying to this email or by visiting their profile page." : "Als je de afspraak toch wilt annuleren, neem dan contact op met de organisator door te antwoorden op deze e-mail of door hun profielpagina te bezoeken.",
"Your appointment \"%s\" with %s has been accepted" : "Ja afspraak \"%s\" met %sis geaccepteerd",
"Your appointment \"%s\" with %s has been accepted" : "Je afspraak \"%s\" met %sis geaccepteerd",
"Dear %s, your booking has been accepted." : "Beste %s, je boeking is aanvaard.",
"Appointment for:" : "Afspraak voor:",
"Date:" : "Datum:",

View File

@ -23,12 +23,12 @@
"%1$s - %2$s" : "%1$s - %2$s",
"Prepare for %s" : "Voorbereiden op %s",
"Follow up for %s" : "Follow-up voor %s",
"Your appointment \"%s\" with %s needs confirmation" : "Je afspraak \"%s\" met %smoet nog bevestigd worden ",
"Your appointment \"%s\" with %s needs confirmation" : "Je afspraak \"%s\" met %s moet nog bevestigd worden",
"Dear %s, please confirm your booking" : "Beste %s, bevestig alsjeblieft je boeking",
"Confirm" : "Bevestigen",
"This confirmation link expires in %s hours." : "Deze bevestigingslink verloopt over %s uur.",
"If you wish to cancel the appointment after all, please contact your organizer by replying to this email or by visiting their profile page." : "Als je de afspraak toch wilt annuleren, neem dan contact op met de organisator door te antwoorden op deze e-mail of door hun profielpagina te bezoeken.",
"Your appointment \"%s\" with %s has been accepted" : "Ja afspraak \"%s\" met %sis geaccepteerd",
"Your appointment \"%s\" with %s has been accepted" : "Je afspraak \"%s\" met %sis geaccepteerd",
"Dear %s, your booking has been accepted." : "Beste %s, je boeking is aanvaard.",
"Appointment for:" : "Afspraak voor:",
"Date:" : "Datum:",

View File

@ -29,6 +29,7 @@ OC.L10N.register(
"Update" : "Ažuriraj",
"Email" : "email",
"Done" : "Gotovo",
"Out of office" : "Van kancelarije",
"Unknown" : "Nepoznato",
"never" : "never",
"Details" : "Detalji",

View File

@ -27,6 +27,7 @@
"Update" : "Ažuriraj",
"Email" : "email",
"Done" : "Gotovo",
"Out of office" : "Van kancelarije",
"Unknown" : "Nepoznato",
"never" : "never",
"Details" : "Detalji",

View File

@ -74,7 +74,7 @@ OC.L10N.register(
"Enable untitled calendar" : "Увімкнути календар без назви",
"An error occurred, unable to change visibility of the calendar." : "Помилка: неможливо змінити подання календаря.",
"_Unsharing the calendar in {countdown} second_::_Unsharing the calendar in {countdown} seconds_" : ["Поширення календаря буде відмінено через {countdown} секунду","Поширення календаря буде відмінено через {countdown} секунд","Поширення календаря буде відмінено через {countdown} секунд","Календар буде від'єднано за {countdown} секунд"],
"_Deleting the calendar in {countdown} second_::_Deleting the calendar in {countdown} seconds_" : ["Календар буде видалено через {countdown} секунду","Календар буде видалено через {countdown} секунд","Календар буде видалено через {countdown} секунд","Календар буде вилучено через {countdown} секунд"],
"_Deleting the calendar in {countdown} second_::_Deleting the calendar in {countdown} seconds_" : ["Календар буде вилучено через {countdown} секунду","Календар буде вилучено через {countdown} секунди","Календар буде вилучено через {countdown} секунд","Календар буде вилучено через {countdown} секунд"],
"New calendar" : "Додати календар",
"Name for new calendar" : "Назва нового календаря",
"Creating calendar …" : "Створення календаря...",
@ -255,7 +255,7 @@ OC.L10N.register(
"Edit time" : "Редагувати час",
"Save time" : "Зберегти час",
"Remove reminder" : "Прибрати нагадування",
"on" : "у",
"on" : "о",
"at" : "о",
"+ Add reminder" : "+ Додати нагадування",
"Add reminder" : "Додати нагадування",
@ -440,7 +440,7 @@ OC.L10N.register(
"Invite" : "Запросити",
"Resources" : "Ресурси",
"_User requires access to your file_::_Users require access to your file_" : ["Потрібно надати доступ до вашого файлу.","Потрібно надати доступ до вашого файлу.","Потрібно надати доступ до вашого файлу.","Потрібно надати доступ до вашого файлу."],
"_Attachment requires shared access_::_Attachments requiring shared access_" : ["Для доступу до додатку потрібний спільний доступ.","Для доступу до додатків потрібний спільний доступ.","Для доступу до додатків потрібний спільний доступ.","Для доступу до додатків потрібний спільний доступ."],
"_Attachment requires shared access_::_Attachments requiring shared access_" : ["Для доступу до застосунку потрібний спільний доступ.","Для доступу до застосунків потрібний спільний доступ.","Для доступу до застосунків потрібний спільний доступ.","Для доступу до застосунків потрібний спільний доступ."],
"Close" : "Закрити",
"Untitled event" : "Подія без назви",
"Subscribe to {name}" : "Підписатися на {name}",

View File

@ -72,7 +72,7 @@
"Enable untitled calendar" : "Увімкнути календар без назви",
"An error occurred, unable to change visibility of the calendar." : "Помилка: неможливо змінити подання календаря.",
"_Unsharing the calendar in {countdown} second_::_Unsharing the calendar in {countdown} seconds_" : ["Поширення календаря буде відмінено через {countdown} секунду","Поширення календаря буде відмінено через {countdown} секунд","Поширення календаря буде відмінено через {countdown} секунд","Календар буде від'єднано за {countdown} секунд"],
"_Deleting the calendar in {countdown} second_::_Deleting the calendar in {countdown} seconds_" : ["Календар буде видалено через {countdown} секунду","Календар буде видалено через {countdown} секунд","Календар буде видалено через {countdown} секунд","Календар буде вилучено через {countdown} секунд"],
"_Deleting the calendar in {countdown} second_::_Deleting the calendar in {countdown} seconds_" : ["Календар буде вилучено через {countdown} секунду","Календар буде вилучено через {countdown} секунди","Календар буде вилучено через {countdown} секунд","Календар буде вилучено через {countdown} секунд"],
"New calendar" : "Додати календар",
"Name for new calendar" : "Назва нового календаря",
"Creating calendar …" : "Створення календаря...",
@ -253,7 +253,7 @@
"Edit time" : "Редагувати час",
"Save time" : "Зберегти час",
"Remove reminder" : "Прибрати нагадування",
"on" : "у",
"on" : "о",
"at" : "о",
"+ Add reminder" : "+ Додати нагадування",
"Add reminder" : "Додати нагадування",
@ -438,7 +438,7 @@
"Invite" : "Запросити",
"Resources" : "Ресурси",
"_User requires access to your file_::_Users require access to your file_" : ["Потрібно надати доступ до вашого файлу.","Потрібно надати доступ до вашого файлу.","Потрібно надати доступ до вашого файлу.","Потрібно надати доступ до вашого файлу."],
"_Attachment requires shared access_::_Attachments requiring shared access_" : ["Для доступу до додатку потрібний спільний доступ.","Для доступу до додатків потрібний спільний доступ.","Для доступу до додатків потрібний спільний доступ.","Для доступу до додатків потрібний спільний доступ."],
"_Attachment requires shared access_::_Attachments requiring shared access_" : ["Для доступу до застосунку потрібний спільний доступ.","Для доступу до застосунків потрібний спільний доступ.","Для доступу до застосунків потрібний спільний доступ.","Для доступу до застосунків потрібний спільний доступ."],
"Close" : "Закрити",
"Untitled event" : "Подія без назви",
"Subscribe to {name}" : "Підписатися на {name}",

View File

@ -214,7 +214,7 @@ class BookingController extends Controller {
);
}
return JsonResponse::success($booking);
return JsonResponse::success();
}
/**

View File

@ -27,12 +27,13 @@ declare(strict_types=1);
namespace OCA\Calendar\Dashboard;
use OCP\Dashboard\IAPIWidgetV2;
use OCP\Dashboard\IReloadableWidget;
use OCP\Dashboard\Model\WidgetItems;
/**
* Requires Nextcloud >= 27.1.0
*/
class CalendarWidgetV2 extends CalendarWidget implements IAPIWidgetV2 {
class CalendarWidgetV2 extends CalendarWidget implements IAPIWidgetV2, IReloadableWidget {
/**
* @inheritDoc
@ -68,4 +69,11 @@ class CalendarWidgetV2 extends CalendarWidget implements IAPIWidgetV2 {
public function getIconClass(): string {
return 'icon-calendar-dark';
}
/**
* @inheritDoc
*/
public function getReloadInterval(): int {
return 600;
}
}

View File

@ -73,24 +73,55 @@ class ReferenceProvider extends ADiscoverableReferenceProvider {
public function matchReference(string $referenceText): bool {
$start = $this->urlGenerator->getAbsoluteURL('/apps/' . Application::APP_ID);
$startIndex = $this->urlGenerator->getAbsoluteURL('/index.php/apps/' . Application::APP_ID);
if (preg_match('/^' . preg_quote($start, '/') . '\/p\/[a-zA-Z0-9]+$/i', $referenceText) === 1 || preg_match('/^' . preg_quote($startIndex, '/') . '\/p\/[a-zA-Z0-9]+$/i', $referenceText) === 1) {
return true;
}
$start = $this->urlGenerator->getAbsoluteURL('/remote.php/dav/calendars');
if (preg_match('/^' . preg_quote($start, '/') . '\/[a-zA-Z0-9-]+\/[a-zA-Z0-9-]+\/$/i', $referenceText) === 1) {
return true;
}
return preg_match('/^' . preg_quote($start, '/') . '\/p\/[a-zA-Z0-9]+$/i', $referenceText) === 1 || preg_match('/^' . preg_quote($startIndex, '/') . '\/p\/[a-zA-Z0-9]+$/i', $referenceText) === 1;
return false;
}
public function resolveReference(string $referenceText): ?IReference {
if ($this->matchReference($referenceText)) {
$token = $this->getCalendarTokenFromLink($referenceText);
$type = $this->getType($referenceText);
$reference = new Reference($referenceText);
$reference->setTitle('calendar');
$reference->setDescription($token);
$reference->setRichObject(
'calendar_widget',
[
'title' => 'calendar',
'token' => $token,
'url' => $referenceText,]
);
$reference->setDescription('calendar widget');
switch ($type) {
case 'public':
$token = $this->getCalendarTokenFromLink($referenceText);
$url = $this->getUrlFromLink($token, 'public');
$reference->setRichObject(
'calendar_widget',
[
'title' => 'calendar',
'token' => $token,
'isPublic' => true,
'url' => $url,
]
);
break;
case 'private':
$url = $this->getUrlFromLink($referenceText, 'private');
$reference->setRichObject(
'calendar_widget',
[
'title' => 'calendar',
'isPublic' => false,
'url' => $url,
]
);
break;
default:
return null;
}
return $reference;
}
@ -99,14 +130,31 @@ class ReferenceProvider extends ADiscoverableReferenceProvider {
}
private function getCalendarTokenFromLink(string $url): ?string {
if (preg_match('/\/p\/([a-zA-Z0-9]+)/', $url, $output_array)) {
return $output_array[1];
}
return $url;
return null;
}
private function getUrlFromLink(string $data, string $type): ?string {
if ($type === 'public') {
return "{$this->urlGenerator->getWebroot()}/remote.php/dav/public-calendars/{$data}/";
} elseif ($type === 'private' && preg_match('/\/remote.php\/dav\/calendars\/([a-zA-Z0-9-]+)\/([a-zA-Z0-9-]+)\//', $data, $output_array)) {
return $this->urlGenerator->getWebroot().$output_array[0];
}
return null;
}
private function getType(string $url): string {
if (preg_match('/\/p\/([a-zA-Z0-9]+)/', $url) === 1) {
return 'public';
}
if (preg_match('/\/dav\/calendars\/([^\/]+)\/([^\/]+)/', $url) === 1) {
return 'private';
}
return 'unknown';
}
public function getCachePrefix(string $referenceId): string {
return '';

1551
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -45,19 +45,19 @@
"@fullcalendar/resource-timeline": "6.1.11",
"@fullcalendar/timegrid": "6.1.11",
"@fullcalendar/vue": "6.1.11",
"@nextcloud/auth": "^2.2.1",
"@nextcloud/auth": "^2.3.0",
"@nextcloud/axios": "^2.4.0",
"@nextcloud/calendar-availability-vue": "^2.2.0",
"@nextcloud/calendar-js": "^6.1.0",
"@nextcloud/cdav-library": "^1.3.0",
"@nextcloud/dialogs": "^4.2.6",
"@nextcloud/event-bus": "^3.1.0",
"@nextcloud/dialogs": "^5.3.1",
"@nextcloud/event-bus": "^3.2.0",
"@nextcloud/initial-state": "^2.1.0",
"@nextcloud/l10n": "^2.2.0",
"@nextcloud/logger": "^2.7.0",
"@nextcloud/moment": "^1.3.1",
"@nextcloud/router": "^3.0.0",
"@nextcloud/vue": "^8.7.1",
"@nextcloud/router": "^3.0.1",
"@nextcloud/vue": "^8.11.2",
"@nextcloud/vue-dashboard": "^2.0.1",
"autosize": "^6.0.1",
"color-convert": "^2.0.1",
@ -118,7 +118,8 @@
],
"moduleNameMapper": {
"^@/(.*)$": "<rootDir>/src/$1",
"^ical.js": "<rootDir>/node_modules/ical.js"
"^ical.js": "<rootDir>/node_modules/ical.js",
"\\.(css|less|scss|sass)$": "<rootDir>/__mocks__/css.js"
},
"transform": {
".*\\.js$": "<rootDir>/node_modules/babel-jest",

View File

@ -13,7 +13,7 @@
],
"labels": [
"dependencies",
"3 - to review"
"3. to review"
],
"commitMessageAction": "Bump",
"commitMessageTopic": "{{depName}}",
@ -23,8 +23,7 @@
"ignoreUnstable": false,
"baseBranches": [
"main",
"stable4.4",
"stable4.3"
"stable4.7"
],
"enabledManagers": [
"composer",
@ -71,7 +70,7 @@
"platformAutomerge": true,
"labels": [
"dependencies",
"4 - to release"
"4. to release"
],
"reviewers": []
},
@ -81,7 +80,7 @@
"automerge": false,
"labels": [
"dependencies",
"3 - to review"
"3. to review"
],
"reviewers": [
"GretaD",

View File

@ -24,12 +24,15 @@
<template>
<div class="share-item">
<AccountMultiple v-if="sharee.isGroup" :size="20" class="share-item__group-icon" />
<IconCircle v-else-if="sharee.isCircle" />
<AccountGroupIcon v-else-if="sharee.isCircle" :size="20" class="share-item__team-icon" />
<NcAvatar v-else :user="sharee.userId" :display-name="sharee.displayName" />
<p class="share-item__label">
<div class="share-item__label">
{{ sharee.displayName }}
</p>
<p>
{{ shareeEmail }}
</p>
</div>
<input :id="`${id}-can-edit`"
:disabled="updatingSharee"
@ -54,7 +57,7 @@
<script>
import { NcActions, NcActionButton, NcAvatar } from '@nextcloud/vue'
import AccountMultiple from 'vue-material-design-icons/AccountMultiple.vue'
import IconCircle from '../../Icons/IconCircles.vue'
import AccountGroupIcon from 'vue-material-design-icons/AccountGroup.vue'
import Delete from 'vue-material-design-icons/Delete.vue'
import {
showInfo,
@ -67,7 +70,7 @@ export default {
NcActions,
NcActionButton,
NcAvatar,
IconCircle,
AccountGroupIcon,
AccountMultiple,
Delete,
},
@ -85,12 +88,28 @@ export default {
return {
id: randomId(),
updatingSharee: false,
shareeEmail: '',
}
},
computed: {
uid() {
return this._uid
},
/**
* @return {string}
*/
displayName() {
if (this.sharee.isCircle) {
return t('calendar', '{teamDisplayName} (Team)', {
teamDisplayName: this.sharee.displayName
})
}
return this.sharee.displayName
}
},
mounted() {
this.updateShareeEmail()
},
methods: {
/**
@ -133,6 +152,20 @@ export default {
this.updatingSharee = false
}
},
async updateShareeEmail() {
if (this.sharee.isGroup || this.sharee.isCircle) {
return
}
const shareeUrl = this.sharee.uri.replace('principal:', '/remote.php/dav/') + '/'
await this.$store.dispatch('fetchPrincipalByUrl', { url: shareeUrl })
const principal = this.$store.getters.getPrincipalByUrl(shareeUrl)
this.shareeEmail = principal.emailAddress
},
},
}
</script>
@ -143,7 +176,8 @@ export default {
align-items: center;
gap: 10px;
&__group-icon {
&__group-icon,
&__team-icon {
width: 32px;
height: 32px;
border-radius: 16px;
@ -151,8 +185,21 @@ export default {
background-color: var(--color-text-maxcontrast);
}
&__team-icon {
// Upstream icon is slightly misaligned when centered using flex
:deep(svg) {
margin-bottom: 3px;
}
}
&__label {
flex: 1 auto;
flex-direction: column;
p {
color: var(--color-text-lighter);
line-height: 1;
}
}
}
</style>

View File

@ -43,21 +43,40 @@
<template #no-options>
<span>{{ $t('calendar', 'No users or groups') }}</span>
</template>
<template #option="sharee">
<div class="share-item">
<AccountMultiple v-if="sharee.isGroup" :size="20" class="share-item__group-icon" />
<AccountGroupIcon v-else-if="sharee.isCircle" :size="20" class="share-item__team-icon" />
<NcAvatar v-else :user="sharee.userId" :display-name="sharee.displayName" />
<div class="share-item__label">
{{ sharee.displayName }}
<p>
{{ sharee.email }}
</p>
</div>
</div>
</template>
</NcSelect>
</div>
</template>
<script>
import { NcSelect } from '@nextcloud/vue'
import { NcAvatar, NcSelect } from '@nextcloud/vue'
import { principalPropertySearchByDisplaynameOrEmail } from '../../../services/caldavService.js'
import HttpClient from '@nextcloud/axios'
import debounce from 'debounce'
import { generateOcsUrl } from '@nextcloud/router'
import { urldecode } from '../../../utils/url.js'
import AccountMultiple from 'vue-material-design-icons/AccountMultiple.vue'
import AccountGroupIcon from 'vue-material-design-icons/AccountGroup.vue'
export default {
name: 'SharingSearch',
components: {
NcAvatar,
AccountGroupIcon,
AccountMultiple,
NcSelect,
},
props: {
@ -141,6 +160,7 @@ export default {
this.inputGiven = false
this.isLoading = false
}
}, 500),
/**
*
@ -188,6 +208,7 @@ export default {
isCircle: false,
isNoUser: isGroup,
search: query,
email: result.email,
})
return list
}, [])
@ -255,4 +276,30 @@ export default {
flex: 1 auto;
}
}
.share-item {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
&__group-icon,
&__team-icon {
width: 32px;
height: 32px;
border-radius: 16px;
color: white;
background-color: var(--color-text-maxcontrast);
}
&__label {
flex: 1 auto;
flex-direction: column;
p {
color: var(--color-text-lighter);
line-height: 1;
}
}
}
</style>

View File

@ -22,9 +22,10 @@
-->
<template>
<FullCalendar ref="fullCalendar"
<FullCalendar v-if="calendarOptions"
ref="fullCalendar"
:class="isWidget? 'fullcalendar-widget': ''"
:options="options" />
:options="calendarOptions" />
</template>
<script>
@ -77,6 +78,10 @@ export default {
type: Boolean,
default: false,
},
url: {
type: String,
required: false,
},
/**
* Whether or not the user is authenticated
*/
@ -89,6 +94,7 @@ export default {
return {
updateTodayJob: null,
updateTodayJobPreviousDate: null,
calendarOptions: null,
}
},
computed: {
@ -154,6 +160,13 @@ export default {
}
},
eventSources() {
if (this.isWidget) {
const calendar = this.$store.getters.getCalendarByUrl(this.url)
if (!calendar) {
return []
}
return [calendar].map(eventSource(this.$store))
}
return this.$store.getters.enabledCalendars.map(eventSource(this.$store))
},
widgetView() {
@ -194,6 +207,22 @@ export default {
const calendarApi = this.$refs.fullCalendar.getApi()
calendarApi.gotoDate(getYYYYMMDDFromFirstdayParam(newDate))
},
eventSources(sources, oldSources) {
const newSources = sources.filter(source => !oldSources.map(oldSource => oldSource.id).includes(source.id))
const removedSources = oldSources.filter(oldSource => !sources.map(source => source.id).includes(oldSource.id))
// Hackity hack! Unfortunately, calendarOptions.eventSources is not reactive ...
// Ref https://fullcalendar.io/docs/Calendar-addEventSource
// TODO: Find a better/safer way to prevent duplicated event sources
const calendarApi = this.$refs.fullCalendar.getApi()
for (const source of newSources) {
calendarApi.addEventSource(source)
}
const eventSources = calendarApi.getEventSources()
for (const source of removedSources) {
eventSources.find(x => x.id === source.id)?.remove()
}
},
modificationCount: debounce(function() {
const calendarApi = this.$refs.fullCalendar.getApi()
calendarApi.refetchEvents()
@ -213,7 +242,7 @@ export default {
* we have to register a resize-observer here, that will automatically
* update the fullCalendar size, when the available space changes.
*/
mounted() {
mounted() {
if (window.ResizeObserver) {
const resizeObserver = new ResizeObserver(debounce(() => {
this.$refs.fullCalendar
@ -225,6 +254,8 @@ export default {
}
},
async created() {
this.calendarOptions = await this.options
this.updateTodayJob = setInterval(() => {
const newDate = getYYYYMMDDFromFirstdayParam('now')

View File

@ -168,7 +168,7 @@ import HelpCircleIcon from 'vue-material-design-icons/HelpCircle.vue'
import InviteesListSearch from '../Invitees/InviteesListSearch.vue'
import { getColorForFBType } from '../../../utils/freebusy.js'
import { getFirstFreeSlot } from '../../../services/freeBusySlotService.js'
import { getFirstFreeSlot, getBusySlots } from '../../../services/freeBusySlotService.js'
import dateFormat from '../../../filters/dateFormat.js'
export default {
@ -275,7 +275,7 @@ export default {
]
},
formattedCurrentStart() {
return this.currentStart.toLocaleDateString(this.lang, this.formattingOptions)
return this.currentDate.toLocaleDateString(this.lang, this.formattingOptions)
},
formattedCurrentTime() {
const options = { hour: '2-digit', minute: '2-digit', hour12: true }
@ -477,12 +477,21 @@ export default {
endSearch.setYear(this.currentDate.getFullYear())
try {
const freeSlots = await getFirstFreeSlot(
// for now search slots only in the first week days
const endSearchDate = new Date(startSearch)
endSearchDate.setDate(startSearch.getDate() + 7)
const eventResults = await getBusySlots(
this.organizer.attendeeProperty,
this.attendees.map((a) => a.attendeeProperty),
startSearch,
endSearchDate,
this.timeZoneId
)
const freeSlots = getFirstFreeSlot(
startSearch,
endSearch,
this.timezoneId,
eventResults.events,
)
freeSlots.forEach((slot) => {
@ -506,6 +515,8 @@ export default {
// have to make these "selected" version of the props seeing as they can't be modified directly, and they aren't updated reactively when vuex is
this.currentStart = slot.start
this.currentEnd = slot.end
const clonedDate = new Date(slot.start) // so as not to modify slot.start
this.currentDate = new Date(clonedDate.setHours(0, 0, 0, 0))
},
},
}

View File

@ -51,13 +51,13 @@
<Avatar v-if="!option.isUser && option.type !== 'circle'"
:key="option.uid"
:url="option.avatar"
:display-name="option.dropdownName" />
:display-name="option.commonName" />
<div class="invitees-search-list-item__label">
<div>
{{ option.dropdownName }}
{{ option.commonName }}
</div>
<div v-if="option.email !== option.dropdownName && option.type !== 'circle'">
<div v-if="option.email !== option.commonName && option.type !== 'circle'">
{{ option.email }}
</div>
<div v-if="option.type === 'circle'">
@ -282,9 +282,9 @@ export default {
email: principal.email,
language: principal.language,
isUser: principal.calendarUserType === 'INDIVIDUAL',
avatar: principal.userId,
avatar: decodeURIComponent(principal.userId),
hasMultipleEMails: false,
dropdownName: principal.displayname || principal.email,
dropdownName: principal.displayname ? [principal.displayname, principal.email].join(' ') : principal.email,
}
})
},

View File

@ -2,6 +2,7 @@
- @copyright Copyright (c) 2019 Georg Ehrke <oc.list@georgehrke.com>
-
- @author Georg Ehrke <oc.list@georgehrke.com>
- @author Richard Steinmetz <richard@steinmetz.cloud>
-
- @license AGPL-3.0-or-later
-
@ -22,17 +23,16 @@
<template>
<div class="repeat-option-set repeat-option-set--interval-freq">
<span class="repeat-option-set__label">
{{ repeatEveryLabel }}
</span>
<input v-if="!isIntervalDisabled"
class="intervalInput"
<NcTextField v-if="!isIntervalDisabled"
:label="repeatEveryLabel"
type="number"
class="repeat-option-set__interval"
min="1"
max="366"
:value="interval"
@input="changeInterval">
<RepeatFreqSelect :freq="frequency"
@input="changeInterval" />
<RepeatFreqSelect class="repeat-option-set__frequency"
:freq="frequency"
:count="interval"
@change="changeFrequency" />
</div>
@ -40,11 +40,13 @@
<script>
import RepeatFreqSelect from './RepeatFreqSelect.vue'
import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js'
export default {
name: 'RepeatFreqInterval',
components: {
RepeatFreqSelect,
NcTextField,
},
props: {
frequency: {
@ -89,3 +91,16 @@ export default {
},
}
</script>
<style lang="scss" scoped>
.repeat-option-set {
&__interval {
margin: 0 5px 0 0;
}
&__frequency {
min-width: unset !important;
padding-top: 7px;
}
}
</style>

View File

@ -250,9 +250,3 @@ export default {
},
}
</script>
<style lang="scss" scoped>
.resource-search__multiselect {
padding-bottom: 5px !important;
}
</style>

View File

@ -22,19 +22,22 @@
<template>
<div class="resource-capacity">
<div class="resource-capacity__input">
<input type="number"
min="0"
:placeholder="placeholder"
:value="value"
@input.prevent.stop="changeValue">
</div>
<NcTextField :label="placeholder"
type="number"
min="0"
:placeholder="placeholder"
:value="value"
@input="changeValue" />
</div>
</template>
<script>
import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js'
export default {
name: 'ResourceSeatingCapacity',
components: {
NcTextField,
},
props: {
value: {
type: Number,
@ -53,3 +56,9 @@ export default {
},
}
</script>
<style lang="scss" scoped>
.resource-capacity {
padding-bottom: 2px;
}
</style>

View File

@ -1,38 +0,0 @@
<template functional>
<span :aria-hidden="!props.title"
:aria-label="props.title"
:class="[data.class, data.staticClass]"
class="material-design-icon icon-circle"
role="img"
v-bind="data.attrs"
v-on="listeners">
<svg :fill="props.fillColor"
class="material-design-icon__svg"
:width="props.size"
:height="props.size"
viewBox="0 0 21.33 21.33">
<path d="M0 0h24v24H0V0z" fill="none" />
<path d="M10.67 1.33a9.34 9.34 0 100 18.68 9.34 9.34 0 000-18.68zM6.93 15.8a2.33 2.33 0 110-4.67 2.33 2.33 0 010 4.67zm1.4-8.87a2.33 2.33 0 114.67 0 2.33 2.33 0 01-4.67 0zm6.07 8.87a2.33 2.33 0 110-4.67 2.33 2.33 0 010 4.67z" />
</svg>
</span>
</template>
<script>
export default {
name: 'IconCircles',
props: {
title: {
type: String,
default: '',
},
size: {
type: Number,
default: 20,
},
fillColor: {
type: String,
default: 'currentColor',
},
},
}
</script>

View File

@ -20,7 +20,9 @@ registerWidget('calendar_widget', async (el, { richObjectType, richObject, acces
store,
propsData: {
isWidget: true,
referenceToken: richObject.token,
isPublic: richObject.isPublic,
referenceToken: richObject?.token,
url: richObject.url,
},
}).$mount(el)
return new NcCustomPickerRenderResult(vueElement.$el, vueElement)

View File

@ -33,7 +33,6 @@ import logger from '../utils/logger.js'
* @param {AttendeeProperty[]} attendees Array of the event's attendees
* @param {Date} start The start date and time of the event
* @param {Date} end The end date and time of the event
* @param timeZone Timezone of the user
* @param timeZoneId
* @return {Promise<>}
*/
@ -74,66 +73,72 @@ export async function getBusySlots(organizer, attendees, start, end, timeZoneId)
}
/**
* Get the first available slot for an event using freebusy API
* Get the first available slot for an event using the freebusy API
*
* @param {AttendeeProperty} organizer The organizer of the event
* @param {AttendeeProperty[]} attendees Array of the event's attendees
* @param {Date} start The start date and time of the event
* @param {Date} end The end date and time of the event
* @param timeZoneId TimezoneId of the user
* @return {Promise<[]>}
* @param retrievedEvents Events found by the freebusy API
* @return []
*/
export async function getFirstFreeSlot(organizer, attendees, start, end, timeZoneId) {
export function getFirstFreeSlot(start, end, retrievedEvents) {
let duration = getDurationInSeconds(start, end)
if (duration === 0) {
duration = 86400 // one day
}
// for now search slots only in the first five days
const endSearchDate = new Date(start)
endSearchDate.setDate(start.getDate() + 5)
const eventResults = await getBusySlots(organizer, attendees, start, endSearchDate, timeZoneId)
endSearchDate.setDate(start.getDate() + 7)
if (eventResults.error) {
return [{ error: eventResults.error }]
if (retrievedEvents.error) {
return [{ error: retrievedEvents.error }]
}
const events = eventResults.events
const events = sortEvents(retrievedEvents)
let currentCheckedTime = start
const currentCheckedTimeEnd = new Date(currentCheckedTime)
currentCheckedTimeEnd.setSeconds(currentCheckedTime.getSeconds() + duration)
const foundSlots = []
let offset = 1
// more than 1 suggestions is too much
// todo: make it 5
for (let i = 0; (i < events.length + 1 && i < 1); i++) {
if (new Date(events[0]?.start) < currentCheckedTime) {
offset = 0
}
for (let i = 0; i < events.length + offset && i < 5; i++) {
foundSlots[i] = checkTimes(currentCheckedTime, duration, events)
if (foundSlots[i].nextEvent !== undefined && foundSlots[i].nextEvent !== null) currentCheckedTime = new Date(foundSlots[i].nextEvent.end)
if (foundSlots[i].nextEvent !== undefined && foundSlots[i].nextEvent !== null) {
currentCheckedTime = new Date(foundSlots[i].nextEvent.end)
}
// avoid repetitions caused by events blocking at first iteration of currentCheckedTime
if (foundSlots[i]?.start === foundSlots[i - 1]?.start) {
foundSlots.pop()
if (foundSlots[i]?.start === foundSlots[i - 1]?.start && foundSlots[i] !== undefined) {
foundSlots[i] = {}
break
}
}
foundSlots.forEach((slot, index) => {
const roundedTime = roundTime(slot.start, slot.end, slot.blockingEvent, duration)
const roundedSlots = []
foundSlots[index].start = roundedTime.start
foundSlots[index].end = roundedTime.end
// not needed anymore
foundSlots[index].nextEvent = undefined
foundSlots.forEach((slot) => {
const roundedTime = roundTime(slot.start, slot.end, slot.blockingEvent, slot.nextEvent, duration)
if (roundedTime !== null && roundedTime.start < endSearchDate) {
roundedSlots.push({
start: roundedTime.start,
end: roundedTime.end,
})
}
})
return foundSlots
return roundedSlots
}
/**
*
* @param start
* @param end
* @return {number}
*/
function getDurationInSeconds(start, end) {
// convert dates to UTC to account for daylight saving time
@ -150,9 +155,11 @@ function getDurationInSeconds(start, end) {
* @param currentCheckedTime
* @param currentCheckedTimeEnd
* @param blockingEvent
* @param nextEvent
* @param duration
*/
function roundTime(currentCheckedTime, currentCheckedTimeEnd, blockingEvent, duration) {
function roundTime(currentCheckedTime, currentCheckedTimeEnd, blockingEvent, nextEvent, duration) {
if (currentCheckedTime === null) return null
if (!blockingEvent) return { start: currentCheckedTime, end: currentCheckedTimeEnd }
// make sure that difference between currentCheckedTime and blockingEvent.end is at least 15 minutes
@ -177,6 +184,11 @@ function roundTime(currentCheckedTime, currentCheckedTimeEnd, blockingEvent, dur
currentCheckedTimeEnd = new Date(currentCheckedTime)
currentCheckedTimeEnd.setSeconds(currentCheckedTime.getSeconds() + duration)
// if the rounding of the event doesn't conflict with the start of the next one
if (currentCheckedTimeEnd > new Date(nextEvent?.start)) {
return null
}
return { start: currentCheckedTime, end: currentCheckedTimeEnd }
}
@ -245,3 +257,19 @@ function checkTimes(currentCheckedTime, duration, events) {
return { start: currentCheckedTime, end: currentCheckedTimeEnd, nextEvent, blockingEvent }
}
// make a function that sorts a list of objects by the "start" property
function sortEvents(events) {
// remove events that have the same start and end time, if not done causes problems
const mappedEvents = new Map()
for (const obj of events) {
const key = obj.start.toString() + obj.end.toString()
if (!mappedEvents.has(key)) {
mappedEvents.set(key, obj)
}
}
return Array.from(mappedEvents.values()).sort((a, b) => new Date(a.start) - new Date(b.start))
}

View File

@ -88,7 +88,7 @@
</template>
<script>
import '@nextcloud/dialogs/dist/index.css'
import '@nextcloud/dialogs/style.css'
import {
NcAvatar as Avatar,

View File

@ -22,11 +22,12 @@
<template>
<div v-if="isWidget" class="calendar-Widget">
<EmbedTopNavigation :is-widget="true" />
<EmbedTopNavigation v-if="!showEmptyCalendarScreen" :is-widget="true" />
<CalendarGrid v-if="!showEmptyCalendarScreen"
ref="calendarGridWidget"
:is-widget="isWidget"
:url="url"
:is-authenticated-user="isAuthenticatedUser" />
<EmptyCalendar v-else />
@ -115,7 +116,7 @@ import { loadState } from '@nextcloud/initial-state'
import {
showWarning,
} from '@nextcloud/dialogs'
import '@nextcloud/dialogs/dist/index.css'
import '@nextcloud/dialogs/style.css'
import Trashbin from '../components/AppNavigation/CalendarList/Trashbin.vue'
import AppointmentConfigList from '../components/AppNavigation/AppointmentConfigList.vue'
@ -139,14 +140,26 @@ export default {
EditSimple,
},
props: {
// Is the calendar in a widget ?
isWidget: {
type: Boolean,
default: false,
},
// The reference token for the widget for public share calendars
referenceToken: {
type: String,
required: false,
},
// Is public share ?
isPublic: {
type: Boolean,
required: false,
},
// Url of private calendar
url: {
type: String,
required: false,
},
},
data() {
return {
@ -263,7 +276,7 @@ export default {
})
this.$store.dispatch('initializeCalendarJsConfig')
if (this.$route?.name.startsWith('Public') || this.$route?.name.startsWith('Embed') || this.isWidget) {
if (this.$route?.name.startsWith('Public') || this.$route?.name.startsWith('Embed') || this.isPublic) {
await initializeClientForPublicView()
const tokens = this.isWidget ? [this.referenceToken] : this.$route.params.tokens.split('-')
const calendars = await this.$store.dispatch('getPublicCalendars', { tokens })

View File

@ -719,5 +719,10 @@ export default {
// Close button should be aligned with calendar picker (header)
padding-top: 5px;
}
.empty-content {
height: 100vh;
padding: 0 20px;
}
}
</style>

View File

@ -0,0 +1,120 @@
/**
* @copyright 2024 Grigory Vodyanov <scratchx@gmx.com>
*
* @author 2024 Grigory Vodyanov <scratchx@gmx.com>
*
* @license AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import { getFirstFreeSlot } from "../../../../src/services/freeBusySlotService.js";
describe('services/freeBusySlotService test suite', () => {
it('should return the first rounded slot after blocking event end', () => {
const events = [
{
start: '2024-01-01T09:00:00Z',
end: '2024-01-01T10:00:00Z',
},
]
let start = new Date('2024-01-01T08:30:00Z')
let end = new Date('2024-01-01T09:30:00Z')
const result = getFirstFreeSlot(start, end, events)
expect(result[0].start).toEqual(new Date('2024-01-01T10:30:00Z'))
expect(result[0].end).toEqual(new Date('2024-01-01T11:30:00Z'))
})
it('should return the same amount of suggested slots as events plus one if first blocking event starts after searched time', () => {
// First blocking event starts after the searched time
const events = [
{
start: '2024-01-01T09:00:00Z',
end: '2024-01-01T10:00:00Z',
},
{
start: '2024-01-01T12:00:00Z',
end: '2024-01-01T14:00:00Z',
},
{
start: '2024-01-02T18:00:00Z',
end: '2024-01-02T19:00:00Z',
},
]
let start = new Date('2024-01-01T08:00:00Z')
let end = new Date('2024-01-01T08:45:00Z')
const result = getFirstFreeSlot(start, end, events)
expect(result.length).toEqual(events.length + 1)
expect(result[3].start).toEqual(new Date('2024-01-02T19:30:00Z'))
expect(result[3].end).toEqual(new Date('2024-01-02T20:15:00Z'))
})
it('should return the same amount of suggested slots as events if first blocking event conflicts with searched time', () => {
// First blocking event starts before the searched time
const events = [
{
start: '2023-12-31T09:00:00Z',
end: '2024-01-01T10:00:00Z',
},
{
start: '2024-01-01T12:00:00Z',
end: '2024-01-01T14:00:00Z',
},
{
start: '2024-01-02T18:00:00Z',
end: '2024-01-02T19:00:00Z',
},
]
let start = new Date('2024-01-01T08:00:00Z')
let end = new Date('2024-01-01T08:45:00Z')
const result = getFirstFreeSlot(start, end, events)
expect(result.length).toEqual(events.length)
expect(result[2].start).toEqual(new Date('2024-01-02T19:30:00Z'))
expect(result[2].end).toEqual(new Date('2024-01-02T20:15:00Z'))
})
it('should not give slots between events if the difference is smaller than the searched time duration', () => {
// First blocking event starts before the searched time
const events = [
{
start: '2024-01-01T12:00:00Z',
end: '2024-01-01T14:00:00Z',
},
{
start: '2024-01-01T15:30:00Z',
end: '2024-01-01T16:00:00Z',
},
]
let start = new Date('2024-01-01T11:00:00Z')
let end = new Date('2024-01-01T12:45:00Z')
const result = getFirstFreeSlot(start, end, events)
expect(result[0].start).toEqual(new Date('2024-01-01T16:30:00Z'))
})
})

View File

@ -25,6 +25,7 @@ declare(strict_types=1);
namespace OCA\Calendar\Controller;
use ChristophWurst\Nextcloud\Testing\TestCase;
use DateTimeImmutable;
use DateTimeZone;
use Exception;
use InvalidArgumentException;
@ -46,7 +47,6 @@ use OCP\IUser;
use OCP\Mail\IMailer;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Log\LoggerInterface;
use Safe\DateTimeImmutable;
class BookingControllerTest extends TestCase {
/** @var string */

View File

@ -23,6 +23,7 @@ declare(strict_types=1);
*/
namespace OCA\Calendar\Dashboard;
use DateTimeImmutable;
use OCA\Calendar\Service\JSDataService;
use OCA\DAV\CalDAV\CalendarImpl;
use OCP\AppFramework\Services\IInitialState;
@ -34,7 +35,6 @@ use OCP\IDateTimeFormatter;
use OCP\IL10N;
use OCP\IURLGenerator;
use PHPUnit\Framework\MockObject\MockObject;
use Safe\DateTimeImmutable;
use Test\TestCase;
class CalendarWidgetTest extends TestCase {

View File

@ -27,6 +27,7 @@ declare(strict_types=1);
namespace OCA\Calendar\Tests\Unit\Service\Appointments;
use ChristophWurst\Nextcloud\Testing\TestCase;
use DateTimeImmutable;
use Exception;
use OCA\Calendar\Db\AppointmentConfig;
use OCA\Calendar\Db\Booking;
@ -47,7 +48,6 @@ use OCP\IUser;
use OCP\Security\ISecureRandom;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Log\LoggerInterface;
use Safe\DateTimeImmutable;
class BookingServiceTest extends TestCase {
/** @var AvailabilityGenerator|MockObject */