Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add availability action to the contacts menu #6502

Merged
merged 1 commit into from
Jan 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@
use OCP\AppFramework\Bootstrap\IBootstrap;
use OCP\AppFramework\Bootstrap\IRegistrationContext;
use OCP\Collaboration\Reference\RenderReferenceEvent;
use OCP\ServerVersion;
use OCP\User\Events\UserDeletedEvent;
use OCP\Util;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\ContainerInterface;
use function method_exists;

class Application extends App implements IBootstrap {
Expand Down Expand Up @@ -57,5 +61,23 @@ public function register(IRegistrationContext $context): void {
* @inheritDoc
*/
public function boot(IBootContext $context): void {
$this->addContactsMenuScript($context->getServerContainer());
}

private function addContactsMenuScript(ContainerInterface $container): void {
// ServerVersion was added in 31, but we don't care about older versions anyway
try {
/** @var ServerVersion $serverVersion */
$serverVersion = $container->get(ServerVersion::class);
} catch (ContainerExceptionInterface $e) {
return;
}

// TODO: drop condition once we only support Nextcloud >= 31
if ($serverVersion->getMajorVersion() >= 31) {
// The contacts menu/avatar is potentially shown everywhere so an event based loading
// mechanism doesn't make sense here
Util::addScript(self::APP_ID, 'calendar-contacts-menu');
}
}
}
7 changes: 7 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"@fullcalendar/resource-timeline": "6.1.15",
"@fullcalendar/timegrid": "6.1.15",
"@fullcalendar/vue": "6.1.15",
"@mdi/svg": "^7.4.47",
"@nextcloud/auth": "^2.4.0",
"@nextcloud/axios": "^2.5.1",
"@nextcloud/calendar-availability-vue": "^2.2.4",
Expand Down
18 changes: 11 additions & 7 deletions src/components/Editor/FreeBusy/FreeBusy.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

<template>
<NcDialog size="large"
:name="$t('calendar', 'Availability of attendees, resources and rooms')"
:name="dialogName || $t('calendar', 'Availability of attendees, resources and rooms')"
@closing="$emit('close')">
<div class="modal__content modal--scheduler">
<div v-if="loadingIndicator" class="loading-indicator">
Expand Down Expand Up @@ -65,7 +65,7 @@
</template>
</NcButton>
</template>
<template>

Check warning on line 68 in src/components/Editor/FreeBusy/FreeBusy.vue

View workflow job for this annotation

GitHub Actions / NPM lint

`<template>` require directive
<div class="freebusy-caption">
<div class="freebusy-caption__calendar-user-types" />
<div class="freebusy-caption__colors">
Expand All @@ -83,7 +83,7 @@
</div>
<FullCalendar ref="freeBusyFullCalendar"
:options="options" />
<div class="modal__content__footer">
<div v-if="!disableFindTime" class="modal__content__footer">
<div class="modal__content__footer__title">
<p v-if="freeSlots">
{{ $t('calendar', 'Available times:') }}
Expand Down Expand Up @@ -201,16 +201,20 @@
},
eventTitle: {
type: String,
required: false,
default: '',
},
alreadyInvitedEmails: {
type: Array,
required: true,
default: () => [],
},
calendarObjectInstance: {
type: Object,
required: true,
dialogName: {

Check warning on line 210 in src/components/Editor/FreeBusy/FreeBusy.vue

View workflow job for this annotation

GitHub Actions / NPM lint

Prop 'dialogName' requires default value to be set
type: String,
required: false,
},
disableFindTime: {
type: Boolean,
default: false,
}
},
data() {
return {
Expand Down
2 changes: 1 addition & 1 deletion src/components/Editor/Invitees/InviteesList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
:end-date="calendarObjectInstance.endDate"
:event-title="calendarObjectInstance.title"
:already-invited-emails="alreadyInvitedEmails"
:calendar-object-instance="calendarObjectInstance"
:show-done-button="true"
@remove-attendee="removeAttendee"
@add-attendee="addAttendee"
@update-dates="saveNewDate"
Expand Down
74 changes: 74 additions & 0 deletions src/contactsMenu.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import 'core-js/stable/index.js'

import '../css/calendar.scss'

import { getRequestToken } from '@nextcloud/auth'
import { linkTo } from '@nextcloud/router'
import { translate as t } from '@nextcloud/l10n'
import { registerContactsMenuAction } from '@nextcloud/vue'
import CalendarBlankSvg from '@mdi/svg/svg/calendar-blank.svg'

// CSP config for webpack dynamic chunk loading
// eslint-disable-next-line
__webpack_nonce__ = btoa(getRequestToken())

Check warning on line 18 in src/contactsMenu.js

View check run for this annotation

Codecov / codecov/patch

src/contactsMenu.js#L18

Added line #L18 was not covered by tests

// Correct the root of the app for chunk loading
// OC.linkTo matches the apps folders
// OC.generateUrl ensure the index.php (or not)
// We do not want the index.php since we're loading files
// eslint-disable-next-line
__webpack_public_path__ = linkTo('calendar', 'js/')

Check warning on line 25 in src/contactsMenu.js

View check run for this annotation

Codecov / codecov/patch

src/contactsMenu.js#L25

Added line #L25 was not covered by tests

// Decode calendar icon (inline data url -> raw svg)
const CalendarBlankSvgRaw = atob(CalendarBlankSvg.split(',')[1])

Check warning on line 28 in src/contactsMenu.js

View check run for this annotation

Codecov / codecov/patch

src/contactsMenu.js#L28

Added line #L28 was not covered by tests

registerContactsMenuAction({

Check warning on line 30 in src/contactsMenu.js

View check run for this annotation

Codecov / codecov/patch

src/contactsMenu.js#L30

Added line #L30 was not covered by tests
id: 'calendar-availability',
displayName: () => t('calendar', 'Show availability'),
iconSvg: () => CalendarBlankSvgRaw,
enabled: (entry) => entry.isUser,
callback: async (args) => {
const { default: Vue } = await import('vue')
const { default: ContactsMenuAvailability } = await import('./views/ContactsMenuAvailability.vue')
const { default: ClickOutside } = await import('vue-click-outside')
const { default: VTooltip } = await import('v-tooltip')
const { default: VueShortKey } = await import('vue-shortkey')
const { createPinia, PiniaVuePlugin } = await import('pinia')
const { translatePlural } = await import('@nextcloud/l10n')

Check warning on line 42 in src/contactsMenu.js

View check run for this annotation

Codecov / codecov/patch

src/contactsMenu.js#L32-L42

Added lines #L32 - L42 were not covered by tests

Vue.use(PiniaVuePlugin)
const pinia = createPinia()

Check warning on line 45 in src/contactsMenu.js

View check run for this annotation

Codecov / codecov/patch

src/contactsMenu.js#L44-L45

Added lines #L44 - L45 were not covered by tests

// Register global components
Vue.directive('ClickOutside', ClickOutside)
Vue.use(VTooltip)
Vue.use(VueShortKey, { prevent: ['input', 'textarea'] })

Check warning on line 50 in src/contactsMenu.js

View check run for this annotation

Codecov / codecov/patch

src/contactsMenu.js#L48-L50

Added lines #L48 - L50 were not covered by tests

Vue.prototype.$t = t
Vue.prototype.$n = translatePlural

Check warning on line 53 in src/contactsMenu.js

View check run for this annotation

Codecov / codecov/patch

src/contactsMenu.js#L52-L53

Added lines #L52 - L53 were not covered by tests

// The nextcloud-vue package does currently rely on t and n
Vue.prototype.t = t
Vue.prototype.n = translatePlural

Check warning on line 57 in src/contactsMenu.js

View check run for this annotation

Codecov / codecov/patch

src/contactsMenu.js#L56-L57

Added lines #L56 - L57 were not covered by tests

// Append container element to the body to mount the vm at
const el = document.createElement('div')
document.body.appendChild(el)

Check warning on line 61 in src/contactsMenu.js

View check run for this annotation

Codecov / codecov/patch

src/contactsMenu.js#L60-L61

Added lines #L60 - L61 were not covered by tests

const View = Vue.extend(ContactsMenuAvailability)
const vm = new View({

Check warning on line 64 in src/contactsMenu.js

View check run for this annotation

Codecov / codecov/patch

src/contactsMenu.js#L63-L64

Added lines #L63 - L64 were not covered by tests
propsData: {
userId: args.uid,
userDisplayName: args.fullName,
userEmail: args.emailAddresses[0],
},
pinia,
})
vm.$mount(el)

Check warning on line 72 in src/contactsMenu.js

View check run for this annotation

Codecov / codecov/patch

src/contactsMenu.js#L72

Added line #L72 was not covered by tests
},
})
123 changes: 123 additions & 0 deletions src/views/ContactsMenuAvailability.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
<!--
- SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->

<template>
<FreeBusy v-if="initialized"
:dialog-name="dialogName"
:start-date="startDate"
:end-date="endDate"
:organizer="organizer"
:attendees="attendees"
:disable-find-time="true"
@add-attendee="addAttendee"
@remove-attendee="removeAttendee"
@close="close" />
</template>

<script>
import { mapStores } from 'pinia'
import usePrincipalsStore from '../store/principals.js'
import useSettingsStore from '../store/settings.js'
import {
mapAttendeePropertyToAttendeeObject,
mapPrincipalObjectToAttendeeObject,
} from '../models/attendee.js'
import loadMomentLocalization from '../utils/moment.js'
import { initializeClientForUserView } from '../services/caldavService.js'
import getTimezoneManager from '../services/timezoneDataProviderService.js'
import FreeBusy from '../components/Editor/FreeBusy/FreeBusy.vue'
import { AttendeeProperty } from '@nextcloud/calendar-js'

Check warning on line 32 in src/views/ContactsMenuAvailability.vue

View check run for this annotation

Codecov / codecov/patch

src/views/ContactsMenuAvailability.vue#L29-L32

Added lines #L29 - L32 were not covered by tests
export default {
name: 'ContactsMenuAvailability',
components: {
FreeBusy,
},
props: {
userId: {
type: String,

Check warning on line 40 in src/views/ContactsMenuAvailability.vue

View check run for this annotation

Codecov / codecov/patch

src/views/ContactsMenuAvailability.vue#L39-L40

Added lines #L39 - L40 were not covered by tests
required: true,
},
userDisplayName: {
type: String,
required: true,

Check warning on line 45 in src/views/ContactsMenuAvailability.vue

View check run for this annotation

Codecov / codecov/patch

src/views/ContactsMenuAvailability.vue#L44-L45

Added lines #L44 - L45 were not covered by tests
},
userEmail: {

Check warning on line 47 in src/views/ContactsMenuAvailability.vue

View check run for this annotation

Codecov / codecov/patch

src/views/ContactsMenuAvailability.vue#L47

Added line #L47 was not covered by tests
type: String,
required: true,
},
},

Check warning on line 51 in src/views/ContactsMenuAvailability.vue

View check run for this annotation

Codecov / codecov/patch

src/views/ContactsMenuAvailability.vue#L49-L51

Added lines #L49 - L51 were not covered by tests
data() {
const initialAttendee = AttendeeProperty.fromNameAndEMail(this.userId, this.userEmail)

Check warning on line 53 in src/views/ContactsMenuAvailability.vue

View check run for this annotation

Codecov / codecov/patch

src/views/ContactsMenuAvailability.vue#L53

Added line #L53 was not covered by tests
const attendees = [mapAttendeePropertyToAttendeeObject(initialAttendee)]

Check warning on line 55 in src/views/ContactsMenuAvailability.vue

View check run for this annotation

Codecov / codecov/patch

src/views/ContactsMenuAvailability.vue#L55

Added line #L55 was not covered by tests
return {
initialized: false,

Check warning on line 57 in src/views/ContactsMenuAvailability.vue

View check run for this annotation

Codecov / codecov/patch

src/views/ContactsMenuAvailability.vue#L57

Added line #L57 was not covered by tests
attendees,
}
},
computed: {
...mapStores(usePrincipalsStore, useSettingsStore),
dialogName() {
return t('calendar', 'Availability of {displayName}', {
displayName: this.userDisplayName,
})

Check warning on line 66 in src/views/ContactsMenuAvailability.vue

View check run for this annotation

Codecov / codecov/patch

src/views/ContactsMenuAvailability.vue#L60-L66

Added lines #L60 - L66 were not covered by tests
},
startDate() {
return new Date()
},

Check warning on line 70 in src/views/ContactsMenuAvailability.vue

View check run for this annotation

Codecov / codecov/patch

src/views/ContactsMenuAvailability.vue#L69-L70

Added lines #L69 - L70 were not covered by tests
endDate() {
// Let's assign a slot of one hour as a default for now
const date = new Date(this.startDate)

Check warning on line 73 in src/views/ContactsMenuAvailability.vue

View check run for this annotation

Codecov / codecov/patch

src/views/ContactsMenuAvailability.vue#L73

Added line #L73 was not covered by tests
date.setHours(date.getHours() + 1)
return date
},
organizer() {

Check warning on line 77 in src/views/ContactsMenuAvailability.vue

View check run for this annotation

Codecov / codecov/patch

src/views/ContactsMenuAvailability.vue#L75-L77

Added lines #L75 - L77 were not covered by tests
if (!this.principalsStore.getCurrentUserPrincipal) {
throw new Error('No principal available for current user')
}

return mapPrincipalObjectToAttendeeObject(
this.principalsStore.getCurrentUserPrincipal,

Check warning on line 83 in src/views/ContactsMenuAvailability.vue

View check run for this annotation

Codecov / codecov/patch

src/views/ContactsMenuAvailability.vue#L83

Added line #L83 was not covered by tests
true,
)
},
},
async created() {
this.initSettings()

Check warning on line 89 in src/views/ContactsMenuAvailability.vue

View check run for this annotation

Codecov / codecov/patch

src/views/ContactsMenuAvailability.vue#L89

Added line #L89 was not covered by tests
await initializeClientForUserView()
await this.principalsStore.fetchCurrentUserPrincipal()
getTimezoneManager()
await this.loadMomentLocale()

Check warning on line 93 in src/views/ContactsMenuAvailability.vue

View check run for this annotation

Codecov / codecov/patch

src/views/ContactsMenuAvailability.vue#L93

Added line #L93 was not covered by tests
this.initialized = true
},
methods: {
initSettings() {
this.settingsStore.loadSettingsFromServer({

Check warning on line 98 in src/views/ContactsMenuAvailability.vue

View check run for this annotation

Codecov / codecov/patch

src/views/ContactsMenuAvailability.vue#L98

Added line #L98 was not covered by tests
timezone: 'automatic',
})
this.settingsStore.initializeCalendarJsConfig()

Check warning on line 101 in src/views/ContactsMenuAvailability.vue

View check run for this annotation

Codecov / codecov/patch

src/views/ContactsMenuAvailability.vue#L100-L101

Added lines #L100 - L101 were not covered by tests
},
async loadMomentLocale() {
const locale = await loadMomentLocalization()
this.settingsStore.setMomentLocale({ locale })
},
addAttendee({ commonName, email }) {
this.attendees.push(mapAttendeePropertyToAttendeeObject(

Check warning on line 108 in src/views/ContactsMenuAvailability.vue

View check run for this annotation

Codecov / codecov/patch

src/views/ContactsMenuAvailability.vue#L106-L108

Added lines #L106 - L108 were not covered by tests
AttendeeProperty.fromNameAndEMail(commonName, email)
))

Check warning on line 110 in src/views/ContactsMenuAvailability.vue

View check run for this annotation

Codecov / codecov/patch

src/views/ContactsMenuAvailability.vue#L110

Added line #L110 was not covered by tests
},
removeAttendee({ email }) {
this.attendees = this.attendees.filter((att) => att.uri !== email)
},
close() {
this.$destroy()
},
},
}
</script>

<style lang="scss" scoped>
</style>
5 changes: 5 additions & 0 deletions tests/psalm-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@
</MissingDependency>
<UndefinedClass>
<code>IAPIWidgetV2</code>
<code><![CDATA[ServerVersion]]></code>
</UndefinedClass>
<UndefinedDocblockClass>
<code><![CDATA[$serverVersion]]></code>
<code><![CDATA[$serverVersion]]></code>
</UndefinedDocblockClass>
</file>
<file src="lib/Controller/AppointmentConfigController.php">
<RedundantCondition>
Expand Down
2 changes: 1 addition & 1 deletion webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ const webpackConfig = require('@nextcloud/webpack-vue-config')
const webpackRules = require('@nextcloud/webpack-vue-config/rules')
const BabelLoaderExcludeNodeModulesExcept = require('babel-loader-exclude-node-modules-except')

//Add reference entry
webpackConfig.entry['reference'] = path.join(__dirname, 'src', 'reference.js')
webpackConfig.entry['contacts-menu'] = path.join(__dirname, 'src', 'contactsMenu.js')

// Add appointments entries
webpackConfig.entry['appointments-booking'] = path.join(__dirname, 'src', 'appointments/main-booking.js')
Expand Down
Loading