feat: add release digest controls
This commit is contained in:
112
frontend/src/api/schema.d.ts
vendored
112
frontend/src/api/schema.d.ts
vendored
@@ -132,6 +132,22 @@ export interface paths {
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/developer/release-update-email-digests/force": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
post: operations["SocializeApiModulesReleaseCommunicationsHandlersForceDeveloperReleaseUpdateDigestEmailsHandler"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/developer/release-updates/{id}": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@@ -580,6 +596,22 @@ export interface paths {
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/users/preferred-language": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
post: operations["SocializeApiModulesIdentityHandlersChangePreferredLanguageHandler"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/users/confirm-email-change": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@@ -1493,6 +1525,10 @@ export interface components {
|
||||
titleFr: string;
|
||||
descriptionFr: string;
|
||||
};
|
||||
SocializeApiModulesReleaseCommunicationsContractsReleaseUpdateDigestSendResultDto: {
|
||||
/** Format: int32 */
|
||||
sentCount?: number;
|
||||
};
|
||||
SocializeApiModulesReleaseCommunicationsContractsReleaseUpdateUnreadSummaryDto: {
|
||||
/** Format: int32 */
|
||||
unreadCount?: number;
|
||||
@@ -1683,6 +1719,9 @@ export interface components {
|
||||
/** Format: binary */
|
||||
file: string;
|
||||
};
|
||||
SocializeApiModulesIdentityHandlersChangePreferredLanguageRequest: {
|
||||
preferredLanguage?: string;
|
||||
};
|
||||
SocializeApiModulesIdentityHandlersConfirmEmailChangeResponse: {
|
||||
message?: string;
|
||||
};
|
||||
@@ -1708,6 +1747,7 @@ export interface components {
|
||||
/** Format: date-time */
|
||||
birthDate?: string | null;
|
||||
address?: string | null;
|
||||
preferredLanguage?: string;
|
||||
};
|
||||
SystemIOStream: components["schemas"]["SystemMarshalByRefObject"] & {
|
||||
canTimeout?: boolean;
|
||||
@@ -2715,6 +2755,40 @@ export interface operations {
|
||||
};
|
||||
};
|
||||
};
|
||||
SocializeApiModulesReleaseCommunicationsHandlersForceDeveloperReleaseUpdateDigestEmailsHandler: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Success */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["SocializeApiModulesReleaseCommunicationsContractsReleaseUpdateDigestSendResultDto"];
|
||||
};
|
||||
};
|
||||
/** @description Unauthorized */
|
||||
401: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
/** @description Forbidden */
|
||||
403: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
SocializeApiModulesReleaseCommunicationsHandlersGetDeveloperReleaseUpdateHandler: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@@ -3789,6 +3863,44 @@ export interface operations {
|
||||
};
|
||||
};
|
||||
};
|
||||
SocializeApiModulesIdentityHandlersChangePreferredLanguageHandler: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["SocializeApiModulesIdentityHandlersChangePreferredLanguageRequest"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description No Content */
|
||||
204: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
/** @description Bad Request */
|
||||
400: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/problem+json": components["schemas"]["FastEndpointsErrorResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Unauthorized */
|
||||
401: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
SocializeApiModulesIdentityHandlersConfirmEmailChangeHandler: {
|
||||
parameters: {
|
||||
query: {
|
||||
|
||||
@@ -18,6 +18,7 @@ export const useReleaseCommunicationsStore = defineStore('release-communications
|
||||
const isLoading = ref(false);
|
||||
const isSaving = ref(false);
|
||||
const isRefreshingCommits = ref(false);
|
||||
const isForcingDigestEmails = ref(false);
|
||||
const error = ref(null);
|
||||
|
||||
const unreadCount = computed(() => unreadSummary.value?.unreadCount ?? 0);
|
||||
@@ -142,6 +143,16 @@ export const useReleaseCommunicationsStore = defineStore('release-communications
|
||||
}
|
||||
}
|
||||
|
||||
async function forceDigestEmails() {
|
||||
isForcingDigestEmails.value = true;
|
||||
try {
|
||||
const response = await client.post('/api/developer/release-update-email-digests/force');
|
||||
return response.data;
|
||||
} finally {
|
||||
isForcingDigestEmails.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function linkCommit(sha, releaseUpdateId) {
|
||||
await client.post(`/api/developer/release-commits/${sha}/link`, { releaseUpdateId });
|
||||
await loadCommits();
|
||||
@@ -202,6 +213,7 @@ export const useReleaseCommunicationsStore = defineStore('release-communications
|
||||
isLoading,
|
||||
isSaving,
|
||||
isRefreshingCommits,
|
||||
isForcingDigestEmails,
|
||||
error,
|
||||
loadUserUpdates,
|
||||
loadUnreadSummary,
|
||||
@@ -214,6 +226,7 @@ export const useReleaseCommunicationsStore = defineStore('release-communications
|
||||
archiveUpdate,
|
||||
loadCommits,
|
||||
refreshCommits,
|
||||
forceDigestEmails,
|
||||
linkCommit,
|
||||
linkCommitsToUpdate,
|
||||
linkFirstReleaseCommits,
|
||||
|
||||
@@ -397,12 +397,6 @@
|
||||
v-else
|
||||
class="updates-panel"
|
||||
>
|
||||
<div class="panel-header">
|
||||
<div>
|
||||
<h2>{{ t('releaseCommunications.developer.pastReleases') }}</h2>
|
||||
<p>{{ t('releaseCommunications.developer.pastReleasesDescription') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
v-for="update in store.developerUpdates"
|
||||
:key="update.id"
|
||||
@@ -747,7 +741,7 @@
|
||||
.update-row {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
gap: 3px;
|
||||
gap: 8px;
|
||||
border: 0;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
background: transparent;
|
||||
|
||||
@@ -3,12 +3,14 @@ import {defineStore} from 'pinia'
|
||||
import {useAuthStore} from "@/features/auth/stores/authStore.js";
|
||||
import {useClient} from "@/plugins/api.js";
|
||||
import {useSessionStorage} from "@vueuse/core";
|
||||
import {useLanguageStore} from "@/stores/languageStore.js";
|
||||
|
||||
export const useUserProfileStore = defineStore(
|
||||
'user-profile',
|
||||
() => {
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const languageStore = useLanguageStore()
|
||||
const isUpdating = ref(false)
|
||||
const isUploadingPortrait = ref(false)
|
||||
const isLoadingCalendarFeed = ref(false)
|
||||
@@ -72,6 +74,7 @@ export const useUserProfileStore = defineStore(
|
||||
const client = useClient()
|
||||
const userResponse = await client.get("/api/users/profile");
|
||||
value.value = userResponse.data
|
||||
languageStore.setLocale(userResponse.data?.preferredLanguage ?? 'en')
|
||||
} catch (fetchError) {
|
||||
console.error(fetchError)
|
||||
}
|
||||
@@ -170,6 +173,28 @@ export const useUserProfileStore = defineStore(
|
||||
}
|
||||
}
|
||||
|
||||
async function changePreferredLanguage(preferredLanguage) {
|
||||
isUpdating.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
const client = useClient()
|
||||
await client.post(
|
||||
`/api/users/preferred-language`,
|
||||
{
|
||||
preferredLanguage: preferredLanguage
|
||||
})
|
||||
value.value.preferredLanguage = preferredLanguage;
|
||||
languageStore.setLocale(preferredLanguage);
|
||||
} catch (updateError) {
|
||||
console.error(updateError)
|
||||
error.value = 'Failed to update profile.'
|
||||
throw updateError
|
||||
} finally {
|
||||
isUpdating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function changeAddress(address) {
|
||||
try {
|
||||
const client = useClient()
|
||||
@@ -278,6 +303,7 @@ export const useUserProfileStore = defineStore(
|
||||
changeBirthday,
|
||||
changePhone,
|
||||
changeEmail,
|
||||
changePreferredLanguage,
|
||||
changeAddress,
|
||||
changePortrait,
|
||||
fetchCalendarExportFeed,
|
||||
|
||||
@@ -19,12 +19,17 @@
|
||||
lastname: '',
|
||||
alias: '',
|
||||
email: '',
|
||||
preferredLanguage: 'en',
|
||||
});
|
||||
|
||||
const email = computed(() => userProfileStore.user?.email || t('userSettings.noEmail'));
|
||||
const alias = computed(() => userProfileStore.alias);
|
||||
const fullname = computed(() => userProfileStore.fullname);
|
||||
const canSave = computed(() => Boolean(form.email.trim()) && !userProfileStore.isUpdating);
|
||||
const languageOptions = computed(() => [
|
||||
{ title: t('releaseCommunications.english'), value: 'en' },
|
||||
{ title: t('releaseCommunications.french'), value: 'fr' },
|
||||
]);
|
||||
const calendarFeedUrl = computed(() => {
|
||||
const feedUrl = userProfileStore.calendarExportFeed?.feedUrl;
|
||||
|
||||
@@ -42,6 +47,7 @@
|
||||
form.lastname = user?.lastname ?? '';
|
||||
form.alias = user?.alias ?? '';
|
||||
form.email = user?.email ?? '';
|
||||
form.preferredLanguage = user?.preferredLanguage ?? 'en';
|
||||
}
|
||||
|
||||
async function submitSettings() {
|
||||
@@ -56,6 +62,7 @@
|
||||
const nextLastname = form.lastname.trim();
|
||||
const nextAlias = form.alias.trim();
|
||||
const nextEmail = form.email.trim();
|
||||
const nextPreferredLanguage = form.preferredLanguage;
|
||||
|
||||
settingsError.value = null;
|
||||
settingsStatus.value = null;
|
||||
@@ -69,6 +76,10 @@
|
||||
await userProfileStore.changeAlias(nextAlias || null);
|
||||
}
|
||||
|
||||
if (nextPreferredLanguage !== (user.preferredLanguage ?? 'en')) {
|
||||
await userProfileStore.changePreferredLanguage(nextPreferredLanguage);
|
||||
}
|
||||
|
||||
let emailChangeRequested = false;
|
||||
if (nextEmail !== (user.email ?? '')) {
|
||||
await userProfileStore.changeEmail(nextEmail);
|
||||
@@ -248,6 +259,15 @@
|
||||
variant="outlined"
|
||||
hide-details
|
||||
/>
|
||||
|
||||
<v-select
|
||||
v-model="form.preferredLanguage"
|
||||
:items="languageOptions"
|
||||
:label="t('userSettings.preferredLanguage')"
|
||||
:disabled="userProfileStore.isUpdating"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
mdiCalendar,
|
||||
mdiChevronDown,
|
||||
mdiCogOutline,
|
||||
mdiEmailOutline,
|
||||
mdiEyeOffOutline,
|
||||
mdiFlagVariantOutline,
|
||||
mdiFormatListBulleted,
|
||||
@@ -79,6 +80,17 @@
|
||||
contentViewActions.value.find(action => action.active) ?? contentViewActions.value[0]
|
||||
);
|
||||
|
||||
async function forceReleaseDigestEmails() {
|
||||
if (!window.confirm(t('releaseCommunications.developer.forceDigestConfirm'))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await releaseCommunicationsStore.forceDigestEmails();
|
||||
window.alert(t('releaseCommunications.developer.forceDigestResult', {
|
||||
count: result?.sentCount ?? 0,
|
||||
}));
|
||||
}
|
||||
|
||||
const appBarActions = computed(() => {
|
||||
if (!authStore.isAuthenticated) {
|
||||
return [];
|
||||
@@ -111,7 +123,15 @@
|
||||
}];
|
||||
case 'developer-release-notes':
|
||||
return route.query.tab === 'release-notes'
|
||||
? []
|
||||
? [
|
||||
{
|
||||
key: 'force-release-digest',
|
||||
label: t('releaseCommunications.developer.forceDigest'),
|
||||
icon: mdiEmailOutline,
|
||||
loading: releaseCommunicationsStore.isForcingDigestEmails,
|
||||
handler: forceReleaseDigestEmails,
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
key: 'refresh-release-commits',
|
||||
@@ -257,6 +277,25 @@
|
||||
<v-icon :icon="action.icon" />
|
||||
<span class="label">{{ action.label }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-else-if="action.handler"
|
||||
class="menu-item-action"
|
||||
type="button"
|
||||
:disabled="action.loading"
|
||||
@click="action.handler"
|
||||
>
|
||||
<v-progress-circular
|
||||
v-if="action.loading"
|
||||
indeterminate
|
||||
size="18"
|
||||
width="2"
|
||||
/>
|
||||
<v-icon
|
||||
v-else
|
||||
:icon="action.icon"
|
||||
/>
|
||||
<span class="label">{{ action.label }}</span>
|
||||
</button>
|
||||
<router-link
|
||||
v-else
|
||||
:to="action.route"
|
||||
|
||||
@@ -33,10 +33,15 @@
|
||||
isUserMenuOpen.value = !isUserMenuOpen.value;
|
||||
}
|
||||
|
||||
function toggleLanguage() {
|
||||
async function toggleLanguage() {
|
||||
const nextLocale = locale.value === 'en' ? 'fr' : 'en';
|
||||
languageStore.setLocale(nextLocale);
|
||||
locale.value = nextLocale;
|
||||
try {
|
||||
await userProfileStore.changePreferredLanguage(nextLocale);
|
||||
} catch (error) {
|
||||
console.error('Failed to save preferred language:', error);
|
||||
}
|
||||
isUserMenuOpen.value = false;
|
||||
}
|
||||
|
||||
|
||||
@@ -631,12 +631,13 @@
|
||||
"creationDescription": "Draft a release note from selected commits or edit an existing release.",
|
||||
"createReleaseNote": "Create Release Note",
|
||||
"createFirstRelease": "Create First Release",
|
||||
"pastReleases": "Past releases",
|
||||
"pastReleasesDescription": "Published, archived, and draft release notes.",
|
||||
"noReleaseNotes": "No release notes yet.",
|
||||
"newUpdate": "New update",
|
||||
"publish": "Publish",
|
||||
"archive": "Archive",
|
||||
"forceDigest": "Send daily email",
|
||||
"forceDigestConfirm": "Send the daily release email now to every eligible user with unread updates?",
|
||||
"forceDigestResult": "Daily release email sent to {count} users.",
|
||||
"linkedCommits": "Linked commits",
|
||||
"noLinkedCommits": "No commits linked to this update yet."
|
||||
},
|
||||
@@ -1110,6 +1111,7 @@
|
||||
"lastname": "Last name",
|
||||
"fullName": "Full name",
|
||||
"email": "Email",
|
||||
"preferredLanguage": "Preferred language",
|
||||
"noEmail": "No email set",
|
||||
"cropperTitle": "Update user portrait",
|
||||
"savePortrait": "Save portrait",
|
||||
|
||||
@@ -631,12 +631,13 @@
|
||||
"creationDescription": "Rédigez une note depuis les commits sélectionnés ou modifiez une release existante.",
|
||||
"createReleaseNote": "Créer une note de release",
|
||||
"createFirstRelease": "Créer la première release",
|
||||
"pastReleases": "Releases passées",
|
||||
"pastReleasesDescription": "Notes de release publiées, archivées et brouillons.",
|
||||
"noReleaseNotes": "Aucune note de release pour le moment.",
|
||||
"newUpdate": "Nouvelle mise à jour",
|
||||
"publish": "Publier",
|
||||
"archive": "Archiver",
|
||||
"forceDigest": "Envoyer l'email quotidien",
|
||||
"forceDigestConfirm": "Envoyer maintenant l'email quotidien des releases a chaque utilisateur eligible avec des mises a jour non lues ?",
|
||||
"forceDigestResult": "Email quotidien des releases envoye a {count} utilisateurs.",
|
||||
"linkedCommits": "Commits liés",
|
||||
"noLinkedCommits": "Aucun commit lié à cette mise à jour."
|
||||
},
|
||||
@@ -1110,6 +1111,7 @@
|
||||
"lastname": "Nom",
|
||||
"fullName": "Nom complet",
|
||||
"email": "Email",
|
||||
"preferredLanguage": "Langue préférée",
|
||||
"noEmail": "Aucun email défini",
|
||||
"cropperTitle": "Mettre à jour le portrait utilisateur",
|
||||
"savePortrait": "Enregistrer le portrait",
|
||||
|
||||
Reference in New Issue
Block a user