Skip to content

Commit

Permalink
Merge pull request #6502 from nextcloud/feat/js-hook-avatar-action
Browse files Browse the repository at this point in the history
feat: add availability action to the contacts menu
  • Loading branch information
st3iny authored Jan 10, 2025
2 parents 35916a5 + 266a345 commit 4ebf8d3
Show file tree
Hide file tree
Showing 9 changed files with 245 additions and 9 deletions.
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 @@ -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 @@ export default {
},
eventTitle: {
type: String,
required: false,
default: '',
},
alreadyInvitedEmails: {
type: Array,
required: true,
default: () => [],
},
calendarObjectInstance: {
type: Object,
required: true,
dialogName: {
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())

// 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/')

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

registerContactsMenuAction({
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')

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

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

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

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

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

const View = Vue.extend(ContactsMenuAvailability)
const vm = new View({
propsData: {
userId: args.uid,
userDisplayName: args.fullName,
userEmail: args.emailAddresses[0],
},
pinia,
})
vm.$mount(el)
},
})
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'

export default {
name: 'ContactsMenuAvailability',
components: {
FreeBusy,
},
props: {
userId: {
type: String,
required: true,
},
userDisplayName: {
type: String,
required: true,
},
userEmail: {
type: String,
required: true,
},
},
data() {
const initialAttendee = AttendeeProperty.fromNameAndEMail(this.userId, this.userEmail)
const attendees = [mapAttendeePropertyToAttendeeObject(initialAttendee)]

return {
initialized: false,
attendees,
}
},
computed: {
...mapStores(usePrincipalsStore, useSettingsStore),
dialogName() {
return t('calendar', 'Availability of {displayName}', {
displayName: this.userDisplayName,
})
},
startDate() {
return new Date()
},
endDate() {
// Let's assign a slot of one hour as a default for now
const date = new Date(this.startDate)
date.setHours(date.getHours() + 1)
return date
},
organizer() {
if (!this.principalsStore.getCurrentUserPrincipal) {
throw new Error('No principal available for current user')
}

return mapPrincipalObjectToAttendeeObject(
this.principalsStore.getCurrentUserPrincipal,
true,
)
},
},
async created() {
this.initSettings()
await initializeClientForUserView()
await this.principalsStore.fetchCurrentUserPrincipal()
getTimezoneManager()
await this.loadMomentLocale()
this.initialized = true
},
methods: {
initSettings() {
this.settingsStore.loadSettingsFromServer({
timezone: 'automatic',
})
this.settingsStore.initializeCalendarJsConfig()
},
async loadMomentLocale() {
const locale = await loadMomentLocalization()
this.settingsStore.setMomentLocale({ locale })
},
addAttendee({ commonName, email }) {
this.attendees.push(mapAttendeePropertyToAttendeeObject(
AttendeeProperty.fromNameAndEMail(commonName, email)
))
},
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

0 comments on commit 4ebf8d3

Please sign in to comment.