Add calendar integrations and collaboration updates
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
import AppAvatar from '@/components/AppAvatar.vue';
|
||||
import ImageCropperDialog from '@/components/ImageCropperDialog.vue';
|
||||
import { useUserProfileStore } from '@/features/user-profile/stores/userProfileStore.js';
|
||||
import config from '@/config.js';
|
||||
|
||||
const userProfileStore = useUserProfileStore();
|
||||
const { t } = useI18n();
|
||||
@@ -11,6 +12,8 @@
|
||||
const isSavingPortrait = ref(false);
|
||||
const settingsError = ref(null);
|
||||
const settingsStatus = ref(null);
|
||||
const calendarFeedStatus = ref(null);
|
||||
const calendarFeedError = ref(null);
|
||||
const form = reactive({
|
||||
firstname: '',
|
||||
lastname: '',
|
||||
@@ -22,6 +25,17 @@
|
||||
const alias = computed(() => userProfileStore.alias);
|
||||
const fullname = computed(() => userProfileStore.fullname);
|
||||
const canSave = computed(() => Boolean(form.email.trim()) && !userProfileStore.isUpdating);
|
||||
const calendarFeedUrl = computed(() => {
|
||||
const feedUrl = userProfileStore.calendarExportFeed?.feedUrl;
|
||||
|
||||
if (!feedUrl) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return feedUrl.startsWith('http')
|
||||
? feedUrl
|
||||
: `${config.apiUrl.replace(/\/$/, '')}${feedUrl}`;
|
||||
});
|
||||
|
||||
function syncFormFromUser(user) {
|
||||
form.firstname = user?.firstname ?? '';
|
||||
@@ -84,11 +98,54 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function enableCalendarFeed() {
|
||||
await updateCalendarFeed(() => userProfileStore.enableCalendarExportFeed(), t('userSettings.calendarFeed.enabled'));
|
||||
}
|
||||
|
||||
async function regenerateCalendarFeed() {
|
||||
await updateCalendarFeed(() => userProfileStore.regenerateCalendarExportFeed(), t('userSettings.calendarFeed.regenerated'));
|
||||
}
|
||||
|
||||
async function revokeCalendarFeed() {
|
||||
await updateCalendarFeed(() => userProfileStore.revokeCalendarExportFeed(), t('userSettings.calendarFeed.revoked'));
|
||||
}
|
||||
|
||||
async function copyCalendarFeedUrl() {
|
||||
if (!calendarFeedUrl.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(calendarFeedUrl.value);
|
||||
calendarFeedStatus.value = t('userSettings.calendarFeed.copied');
|
||||
calendarFeedError.value = null;
|
||||
} catch (error) {
|
||||
console.error('Failed to copy calendar feed URL:', error);
|
||||
calendarFeedStatus.value = null;
|
||||
calendarFeedError.value = t('userSettings.calendarFeed.errors.copyFailed');
|
||||
}
|
||||
}
|
||||
|
||||
async function updateCalendarFeed(action, successMessage) {
|
||||
calendarFeedStatus.value = null;
|
||||
calendarFeedError.value = null;
|
||||
|
||||
try {
|
||||
await action();
|
||||
calendarFeedStatus.value = successMessage;
|
||||
} catch (error) {
|
||||
console.error('Failed to update calendar feed:', error);
|
||||
calendarFeedError.value = t('userSettings.calendarFeed.errors.updateFailed');
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => userProfileStore.user,
|
||||
syncFormFromUser,
|
||||
{ immediate: true, deep: true }
|
||||
);
|
||||
|
||||
userProfileStore.fetchCalendarExportFeed();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -201,6 +258,81 @@
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<div class="panel-heading">
|
||||
<strong>{{ t('userSettings.calendarFeed.title') }}</strong>
|
||||
<span>{{ t('userSettings.calendarFeed.description') }}</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="calendarFeedError"
|
||||
class="page-message error"
|
||||
>
|
||||
{{ calendarFeedError }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="calendarFeedStatus"
|
||||
class="page-message success"
|
||||
>
|
||||
{{ calendarFeedStatus }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="userProfileStore.calendarExportFeed?.isEnabled && calendarFeedUrl"
|
||||
class="calendar-feed-box"
|
||||
>
|
||||
<span>{{ t('userSettings.calendarFeed.feedUrl') }}</span>
|
||||
<code>{{ calendarFeedUrl }}</code>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="calendar-feed-empty"
|
||||
>
|
||||
{{ t('userSettings.calendarFeed.empty') }}
|
||||
</div>
|
||||
|
||||
<div class="calendar-feed-actions">
|
||||
<button
|
||||
v-if="!userProfileStore.calendarExportFeed?.isEnabled"
|
||||
class="primary-button"
|
||||
type="button"
|
||||
:disabled="userProfileStore.isUpdatingCalendarFeed"
|
||||
@click="enableCalendarFeed"
|
||||
>
|
||||
{{ t('userSettings.calendarFeed.enable') }}
|
||||
</button>
|
||||
|
||||
<template v-else>
|
||||
<button
|
||||
class="secondary-button"
|
||||
type="button"
|
||||
:disabled="!calendarFeedUrl"
|
||||
@click="copyCalendarFeedUrl"
|
||||
>
|
||||
{{ t('userSettings.calendarFeed.copy') }}
|
||||
</button>
|
||||
<button
|
||||
class="secondary-button"
|
||||
type="button"
|
||||
:disabled="userProfileStore.isUpdatingCalendarFeed"
|
||||
@click="regenerateCalendarFeed"
|
||||
>
|
||||
{{ t('userSettings.calendarFeed.regenerate') }}
|
||||
</button>
|
||||
<button
|
||||
class="danger-button"
|
||||
type="button"
|
||||
:disabled="userProfileStore.isUpdatingCalendarFeed"
|
||||
@click="revokeCalendarFeed"
|
||||
>
|
||||
{{ t('userSettings.calendarFeed.revoke') }}
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ImageCropperDialog
|
||||
v-model="isPortraitDialogOpen"
|
||||
:title="t('userSettings.cropperTitle')"
|
||||
@@ -318,8 +450,47 @@
|
||||
color: #fffaf2;
|
||||
}
|
||||
|
||||
.primary-button:disabled {
|
||||
.secondary-button,
|
||||
.danger-button {
|
||||
@apply inline-flex items-center justify-center gap-2 rounded-full px-5 py-3 text-sm font-bold transition;
|
||||
}
|
||||
|
||||
.secondary-button {
|
||||
background: rgba(23, 32, 51, 0.06);
|
||||
color: #172033;
|
||||
}
|
||||
|
||||
.danger-button {
|
||||
background: rgba(185, 28, 28, 0.08);
|
||||
color: #b91c1c;
|
||||
}
|
||||
|
||||
.primary-button:disabled,
|
||||
.secondary-button:disabled,
|
||||
.danger-button:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.calendar-feed-box {
|
||||
@apply flex flex-col gap-2 rounded-[1rem] border p-4;
|
||||
background: #fffaf2;
|
||||
border-color: rgba(23, 32, 51, 0.08);
|
||||
}
|
||||
|
||||
.calendar-feed-box span,
|
||||
.calendar-feed-empty {
|
||||
@apply text-sm leading-6;
|
||||
color: #526178;
|
||||
}
|
||||
|
||||
.calendar-feed-box code {
|
||||
@apply overflow-x-auto rounded-[0.75rem] px-3 py-2 text-sm;
|
||||
background: rgba(23, 32, 51, 0.06);
|
||||
color: #172033;
|
||||
}
|
||||
|
||||
.calendar-feed-actions {
|
||||
@apply flex flex-wrap gap-3;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user