Merge remote-tracking branch 'origin/main'

This commit is contained in:
778a69cd 2023-12-14 15:06:06 +01:00
commit 7d7abd0dda
19 changed files with 1220 additions and 843 deletions

View file

@ -105,3 +105,8 @@ config :tz_world,
data_dir: System.get_env("MOBILIZON_TIMEZONES_DIR", "/var/lib/mobilizon/timezones") data_dir: System.get_env("MOBILIZON_TIMEZONES_DIR", "/var/lib/mobilizon/timezones")
config :tzdata, :data_dir, System.get_env("MOBILIZON_TZDATA_DIR", "/var/lib/mobilizon/tzdata") config :tzdata, :data_dir, System.get_env("MOBILIZON_TZDATA_DIR", "/var/lib/mobilizon/tzdata")
config :web_push_encryption, :vapid_details,
subject: System.get_env("MOBILIZON_WEB_PUSH_ENCRYPTION_SUBJECT", nil),
public_key: System.get_env("MOBILIZON_WEB_PUSH_ENCRYPTION_PUBLIC_KEY", nil),
private_key: System.get_env("MOBILIZON_WEB_PUSH_ENCRYPTION_PRIVATE_KEY", nil)

View file

@ -85,6 +85,9 @@ defmodule Mobilizon.GraphQL.Resolvers.Config do
@spec build_config_cache :: map() @spec build_config_cache :: map()
defp build_config_cache do defp build_config_cache do
webpush_public_key =
get_in(Application.get_env(:web_push_encryption, :vapid_details), [:public_key])
%{ %{
name: Config.instance_name(), name: Config.instance_name(),
registrations_open: Config.instance_registrations_open?(), registrations_open: Config.instance_registrations_open?(),
@ -170,9 +173,9 @@ defmodule Mobilizon.GraphQL.Resolvers.Config do
enabled: Config.get([:instance, :enable_instance_feeds]) enabled: Config.get([:instance, :enable_instance_feeds])
}, },
web_push: %{ web_push: %{
enabled: !is_nil(Application.get_env(:web_push_encryption, :vapid_details)), enabled: is_binary(webpush_public_key) && String.trim(webpush_public_key) != "",
public_key: public_key:
get_in(Application.get_env(:web_push_encryption, :vapid_details), [:public_key]) if(is_binary(webpush_public_key), do: String.trim(webpush_public_key), else: nil)
}, },
export_formats: Config.instance_export_formats(), export_formats: Config.instance_export_formats(),
analytics: FrontEndAnalytics.config(), analytics: FrontEndAnalytics.config(),

View file

@ -5,7 +5,6 @@ defmodule Mix.Tasks.Mobilizon.WebPush.Gen.Keypair do
Taken from https://github.com/danhper/elixir-web-push-encryption/blob/8fd0f71f3222b466d389f559be9800c49f9bb641/lib/mix/tasks/web_push_gen_keypair.ex Taken from https://github.com/danhper/elixir-web-push-encryption/blob/8fd0f71f3222b466d389f559be9800c49f9bb641/lib/mix/tasks/web_push_gen_keypair.ex
""" """
use Mix.Task use Mix.Task
import Mix.Tasks.Mobilizon.Common, only: [mix_shell?: 0]
@shortdoc "Manages Mobilizon users" @shortdoc "Manages Mobilizon users"
@ -13,20 +12,28 @@ defmodule Mix.Tasks.Mobilizon.WebPush.Gen.Keypair do
def run(_) do def run(_) do
{public, private} = :crypto.generate_key(:ecdh, :prime256v1) {public, private} = :crypto.generate_key(:ecdh, :prime256v1)
IO.puts("# Put the following in your #{file_name()} config file:") IO.puts("Public and private VAPID keys have been generated.")
IO.puts("")
if is_nil(System.get_env("MOBILIZON_DOCKER")) do
IO.puts("# Put the following in your runtime.exs config file:")
IO.puts("") IO.puts("")
IO.puts("config :web_push_encryption, :vapid_details,") IO.puts("config :web_push_encryption, :vapid_details,")
IO.puts(" subject: \"mailto:administrator@example.com\",") IO.puts(" subject: \"mailto:administrator@example.com\",")
IO.puts(" public_key: \"#{ub64(public)}\",") IO.puts(" public_key: \"#{ub64(public)}\",")
IO.puts(" private_key: \"#{ub64(private)}\"") IO.puts(" private_key: \"#{ub64(private)}\"")
IO.puts("") IO.puts("")
else
IO.puts("# Set the following environment variables in your .env file:")
IO.puts("")
IO.puts("MOBILIZON_WEB_PUSH_ENCRYPTION_SUBJECT=\"mailto:administrator@example.com\"")
IO.puts("MOBILIZON_WEB_PUSH_ENCRYPTION_PUBLIC_KEY=\"#{ub64(public)}\"")
IO.puts("MOBILIZON_WEB_PUSH_ENCRYPTION_PRIVATE_KEY=\"#{ub64(private)}\"")
IO.puts("")
end
end end
defp ub64(value) do defp ub64(value) do
Base.url_encode64(value, padding: false) Base.url_encode64(value, padding: false)
end end
defp file_name do
if mix_shell?(), do: "runtime.exs", else: "config.exs"
end
end end

1760
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -70,6 +70,7 @@ import { CONFIG } from "@/graphql/config";
import { IConfig } from "@/types/config.model"; import { IConfig } from "@/types/config.model";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import RouteName from "@/router/name"; import RouteName from "@/router/name";
import { useLazyCurrentUserIdentities } from "./composition/apollo/actor";
const { result: configResult } = useQuery<{ config: IConfig }>( const { result: configResult } = useQuery<{ config: IConfig }>(
CONFIG, CONFIG,
@ -138,11 +139,15 @@ interval.value = window.setInterval(async () => {
} }
}, 60000) as unknown as number; }, 60000) as unknown as number;
const { load: loadIdentities } = useLazyCurrentUserIdentities();
onBeforeMount(async () => { onBeforeMount(async () => {
console.debug("Before mount App"); console.debug("Before mount App");
if (initializeCurrentUser()) { if (initializeCurrentUser()) {
try { try {
await initializeCurrentActor(); const result = await loadIdentities();
if (!result) return;
await initializeCurrentActor(result.loggedUser.actors);
} catch (err) { } catch (err) {
if (err instanceof NoIdentitiesException) { if (err instanceof NoIdentitiesException) {
await router.push({ await router.push({
@ -223,7 +228,7 @@ const initializeCurrentUser = () => {
console.debug("Initialized current user", userData); console.debug("Initialized current user", userData);
return true; return true;
} }
console.debug("Failed to initialize current user"); console.debug("We don't seem to have a currently logged-in user");
return false; return false;
}; };

View file

@ -9,7 +9,12 @@ let isRefreshing = false;
let pendingRequests: any[] = []; let pendingRequests: any[] = [];
const resolvePendingRequests = () => { const resolvePendingRequests = () => {
pendingRequests.map((callback) => callback()); console.debug("resolving pending requests");
pendingRequests.map((callback) => {
console.debug("calling callback", callback);
return callback();
});
console.debug("emptying pending requests after resolving them all");
pendingRequests = []; pendingRequests = [];
}; };
@ -21,7 +26,23 @@ const isAuthError = (graphQLError: GraphQLError | undefined) => {
const errorLink = onError( const errorLink = onError(
({ graphQLErrors, networkError, forward, operation }) => { ({ graphQLErrors, networkError, forward, operation }) => {
console.debug("We have an apollo error", [graphQLErrors, networkError]); if (graphQLErrors) {
graphQLErrors.map(
(graphQLError: GraphQLError & { status_code?: number }) => {
if (graphQLError?.status_code !== 401) {
console.debug(
`[GraphQL error]: Message: ${graphQLError.message}, Location: ${graphQLError.locations}, Path: ${graphQLError.path}`
);
}
}
);
}
if (networkError) {
console.error(`[Network error]: ${networkError}`);
console.debug(JSON.stringify(networkError));
}
if ( if (
graphQLErrors?.some((graphQLError) => isAuthError(graphQLError)) || graphQLErrors?.some((graphQLError) => isAuthError(graphQLError)) ||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
@ -67,6 +88,9 @@ const errorLink = onError(
}) })
).filter((value) => Boolean(value)); ).filter((value) => Boolean(value));
} else { } else {
console.debug(
"Skipping refreshing as isRefreshing is already to true, adding requests to pending"
);
forwardOperation = fromPromise( forwardOperation = fromPromise(
new Promise((resolve) => { new Promise((resolve) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
@ -78,23 +102,6 @@ const errorLink = onError(
return forwardOperation.flatMap(() => forward(operation)); return forwardOperation.flatMap(() => forward(operation));
} }
if (graphQLErrors) {
graphQLErrors.map(
(graphQLError: GraphQLError & { status_code?: number }) => {
if (graphQLError?.status_code !== 401) {
console.debug(
`[GraphQL error]: Message: ${graphQLError.message}, Location: ${graphQLError.locations}, Path: ${graphQLError.path}`
);
}
}
);
}
if (networkError) {
console.error(`[Network error]: ${networkError}`);
console.debug(JSON.stringify(networkError));
}
} }
); );

View file

@ -34,7 +34,7 @@ if (!import.meta.env.VITE_HISTOIRE_ENV) {
const retryLink = new RetryLink(); const retryLink = new RetryLink();
export const fullLink = authMiddleware export const fullLink = retryLink
.concat(retryLink)
.concat(errorLink) .concat(errorLink)
.concat(authMiddleware)
.concat(link ?? uploadLink); .concat(link ?? uploadLink);

View file

@ -26,13 +26,13 @@ body {
@apply opacity-50 cursor-not-allowed; @apply opacity-50 cursor-not-allowed;
} }
.btn-danger { .btn-danger {
@apply bg-mbz-danger hover:bg-mbz-danger/90; @apply border-2 bg-mbz-danger hover:bg-mbz-danger/90 text-white;
} }
.btn-success { .btn-success {
@apply bg-mbz-success; @apply border-2 bg-mbz-success text-white;
} }
.btn-warning { .btn-warning {
@apply bg-mbz-warning text-black hover:bg-mbz-warning/90 hover:text-slate-800; @apply border-2 bg-mbz-warning text-black hover:bg-mbz-warning/90 hover:text-slate-800;
} }
.btn-text { .btn-text {
@apply bg-transparent border-transparent text-black dark:text-white font-normal underline hover:bg-zinc-200 hover:text-black; @apply bg-transparent border-transparent text-black dark:text-white font-normal underline hover:bg-zinc-200 hover:text-black;
@ -42,13 +42,13 @@ body {
@apply bg-transparent text-black dark:text-white font-semibold py-2 px-4 border border-mbz-bluegreen dark:border-violet-3; @apply bg-transparent text-black dark:text-white font-semibold py-2 px-4 border border-mbz-bluegreen dark:border-violet-3;
} }
.btn-outlined-success { .btn-outlined-success {
@apply border-2 border-mbz-success bg-transparent text-mbz-success hover:bg-mbz-success; @apply border-2 border-mbz-success bg-transparent text-mbz-success hover:bg-mbz-success hover:text-white;
} }
.btn-outlined-warning { .btn-outlined-warning {
@apply bg-transparent border dark:text-white hover:dark:text-slate-900 hover:bg-mbz-warning border-mbz-warning; @apply bg-transparent border dark:text-white hover:dark:text-slate-900 hover:bg-mbz-warning border-mbz-warning;
} }
.btn-outlined-danger { .btn-outlined-danger {
@apply border-2 bg-transparent border-mbz-danger text-mbz-danger hover:bg-mbz-danger; @apply border-2 bg-transparent border-mbz-danger text-mbz-danger hover:bg-mbz-danger hover:text-white;
} }
.btn-outlined-text { .btn-outlined-text {
@apply bg-transparent hover:text-slate-900; @apply bg-transparent hover:text-slate-900;
@ -97,6 +97,15 @@ body {
.input { .input {
@apply appearance-none box-border rounded border w-full py-2 px-3 text-black leading-tight dark:bg-zinc-600 dark:placeholder:text-zinc-400 dark:text-zinc-50; @apply appearance-none box-border rounded border w-full py-2 px-3 text-black leading-tight dark:bg-zinc-600 dark:placeholder:text-zinc-400 dark:text-zinc-50;
} }
.input-size-small {
@apply text-sm;
}
.input-size-medium {
@apply text-base;
}
.input-size-large {
@apply text-xl;
}
.input-danger { .input-danger {
@apply border-red-500; @apply border-red-500;
} }
@ -205,7 +214,7 @@ body {
/* Select */ /* Select */
.select { .select {
@apply dark:bg-zinc-600 dark:placeholder:text-zinc-400 dark:text-zinc-50 rounded pl-2 pr-8 border-2 border-transparent h-10 shadow-none border rounded w-full; @apply dark:bg-zinc-600 dark:placeholder:text-zinc-400 dark:text-zinc-50 pl-2 pr-8 border-2 border-transparent h-10 shadow-none rounded w-full;
} }
/* Radio */ /* Radio */

View file

@ -36,7 +36,7 @@
keypath="{instanceName} is an instance of {mobilizon_link}, a free software built with the community." keypath="{instanceName} is an instance of {mobilizon_link}, a free software built with the community."
> >
<template #instanceName> <template #instanceName>
<b>{{ config?.name }}</b> <b>{{ instanceName }}</b>
</template> </template>
<template #mobilizon_link> <template #mobilizon_link>
<a href="https://joinmobilizon.org">{{ t("Mobilizon") }}</a> <a href="https://joinmobilizon.org">{{ t("Mobilizon") }}</a>
@ -57,7 +57,10 @@
}} }}
</span> </span>
</p> </p>
<SentryFeedback /> <SentryFeedback
v-if="sentryProvider"
:providerConfig="sentryProvider"
/>
<p class="prose dark:prose-invert" v-if="!sentryEnabled"> <p class="prose dark:prose-invert" v-if="!sentryEnabled">
{{ {{
@ -84,7 +87,7 @@
<div class="buttons" v-if="!sentryEnabled"> <div class="buttons" v-if="!sentryEnabled">
<o-tooltip <o-tooltip
:label="tooltipConfig.label" :label="tooltipConfig.label"
:type="tooltipConfig.type" :variant="tooltipConfig.variant"
:active="copied !== false" :active="copied !== false"
always always
> >
@ -101,12 +104,13 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { checkProviderConfig } from "@/services/statistics"; import { checkProviderConfig } from "@/services/statistics";
import { IAnalyticsConfig } from "@/types/config.model"; import { IAnalyticsConfig, IConfig } from "@/types/config.model";
import { computed, defineAsyncComponent, ref } from "vue"; import { computed, defineAsyncComponent, ref } from "vue";
import { useQueryLoading } from "@vue/apollo-composable"; import { useQuery, useQueryLoading } from "@vue/apollo-composable";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { useHead } from "@unhead/vue"; import { useHead } from "@unhead/vue";
import { useAnalytics } from "@/composition/apollo/config"; import { useAnalytics } from "@/composition/apollo/config";
import { INSTANCE_NAME } from "@/graphql/config";
const SentryFeedback = defineAsyncComponent( const SentryFeedback = defineAsyncComponent(
() => import("./Feedback/SentryFeedback.vue") () => import("./Feedback/SentryFeedback.vue")
); );
@ -126,6 +130,12 @@ useHead({
title: computed(() => t("Error")), title: computed(() => t("Error")),
}); });
const { result: instanceConfig } = useQuery<{ config: Pick<IConfig, "name"> }>(
INSTANCE_NAME
);
const instanceName = computed(() => instanceConfig.value?.config.name);
const copyErrorToClipboard = async (): Promise<void> => { const copyErrorToClipboard = async (): Promise<void> => {
try { try {
if (window.isSecureContext && navigator.clipboard) { if (window.isSecureContext && navigator.clipboard) {

View file

@ -100,13 +100,12 @@
<span v-if="event.participantStats.notApproved > 0"> <span v-if="event.participantStats.notApproved > 0">
<o-button <o-button
variant="text" variant="text"
@click=" tag="router-link"
gotToWithCheck(participation, { :to="{
name: RouteName.PARTICIPATIONS, name: RouteName.PARTICIPATIONS,
query: { role: ParticipantRole.NOT_APPROVED }, query: { role: ParticipantRole.NOT_APPROVED },
params: { eventId: event.uuid }, params: { eventId: event.uuid },
}) }"
"
> >
{{ {{
$t( $t(

View file

@ -273,6 +273,28 @@
</div> </div>
</o-dropdown-item> </o-dropdown-item>
<o-dropdown-item
aria-role="listitem"
has-link
v-if="
![
ParticipantRole.PARTICIPANT,
ParticipantRole.NOT_APPROVED,
].includes(participation.role)
"
>
<router-link
class="flex gap-1"
:to="{
name: RouteName.ANNOUNCEMENTS,
params: { eventId: participation.event?.uuid },
}"
>
<Bullhorn />
{{ t("Announcements") }}
</router-link>
</o-dropdown-item>
<o-dropdown-item aria-role="listitem"> <o-dropdown-item aria-role="listitem">
<router-link <router-link
class="flex gap-1" class="flex gap-1"
@ -302,7 +324,6 @@ import {
organizerDisplayName, organizerDisplayName,
} from "@/types/event.model"; } from "@/types/event.model";
import { displayNameAndUsername, IPerson } from "@/types/actor"; import { displayNameAndUsername, IPerson } from "@/types/actor";
import { CURRENT_ACTOR_CLIENT } from "@/graphql/actor";
import RouteName from "@/router/name"; import RouteName from "@/router/name";
import { changeIdentity } from "@/utils/identity"; import { changeIdentity } from "@/utils/identity";
import LazyImageWrapper from "@/components/Image/LazyImageWrapper.vue"; import LazyImageWrapper from "@/components/Image/LazyImageWrapper.vue";
@ -318,12 +339,14 @@ import AccountGroup from "vue-material-design-icons/AccountGroup.vue";
import Video from "vue-material-design-icons/Video.vue"; import Video from "vue-material-design-icons/Video.vue";
import { useProgrammatic } from "@oruga-ui/oruga-next"; import { useProgrammatic } from "@oruga-ui/oruga-next";
import { computed, inject } from "vue"; import { computed, inject } from "vue";
import { useQuery } from "@vue/apollo-composable";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { Dialog } from "@/plugins/dialog"; import { Dialog } from "@/plugins/dialog";
import { Snackbar } from "@/plugins/snackbar"; import { Snackbar } from "@/plugins/snackbar";
import { useDeleteEvent } from "@/composition/apollo/event"; import { useDeleteEvent } from "@/composition/apollo/event";
import Tag from "@/components/TagElement.vue"; import Tag from "@/components/TagElement.vue";
import { escapeHtml } from "@/utils/html";
import Bullhorn from "vue-material-design-icons/Bullhorn.vue";
import { useCurrentActorClient } from "@/composition/apollo/actor";
const props = defineProps<{ const props = defineProps<{
participation: IParticipant; participation: IParticipant;
@ -332,8 +355,7 @@ const props = defineProps<{
const emit = defineEmits(["eventDeleted"]); const emit = defineEmits(["eventDeleted"]);
const { result: currentActorResult } = useQuery(CURRENT_ACTOR_CLIENT); const { currentActor } = useCurrentActorClient();
const currentActor = computed(() => currentActorResult.value?.currentActor);
const { t } = useI18n({ useScope: "global" }); const { t } = useI18n({ useScope: "global" });
const dialog = inject<Dialog>("dialog"); const dialog = inject<Dialog>("dialog");
@ -365,7 +387,7 @@ const openDeleteEventModal = (
)} )}
<br><br> <br><br>
${t('To confirm, type your event title "{eventTitle}"', { ${t('To confirm, type your event title "{eventTitle}"', {
eventTitle: event.title, eventTitle: escapeHtml(event.title),
})}`, })}`,
confirmText: t("Delete {eventTitle}", { confirmText: t("Delete {eventTitle}", {
eventTitle: event.title, eventTitle: event.title,
@ -374,6 +396,7 @@ const openDeleteEventModal = (
placeholder: event.title, placeholder: event.title,
pattern: escapeRegExp(event.title), pattern: escapeRegExp(event.title),
}, },
hasInput: true,
onConfirm: () => callback(event), onConfirm: () => callback(event),
}); });
}; };
@ -432,7 +455,7 @@ const gotToWithCheck = async (
route: RouteLocationRaw route: RouteLocationRaw
): Promise<any> => { ): Promise<any> => {
if ( if (
participation.actor.id !== currentActor.value.id && participation.actor.id !== currentActor.value?.id &&
participation.event.organizerActor participation.event.organizerActor
) { ) {
const organizerActor = participation.event.organizerActor as IPerson; const organizerActor = participation.event.organizerActor as IPerson;

View file

@ -8,7 +8,7 @@
drag-drop drag-drop
> >
<div <div
class="w-100 rounded text-center p-4 rounded-xl border-dashed border-2 border-gray-600" class="w-100 text-center p-4 rounded-xl border-dashed border-2 border-gray-600"
> >
<span class="mx-auto flex w-fit"> <span class="mx-auto flex w-fit">
<Upload /> <Upload />

View file

@ -6,7 +6,7 @@ import {
} from "@/graphql/actor"; } from "@/graphql/actor";
import { IPerson } from "@/types/actor"; import { IPerson } from "@/types/actor";
import { ICurrentUser } from "@/types/current-user.model"; import { ICurrentUser } from "@/types/current-user.model";
import { useQuery } from "@vue/apollo-composable"; import { useLazyQuery, useQuery } from "@vue/apollo-composable";
import { computed, Ref, unref } from "vue"; import { computed, Ref, unref } from "vue";
import { useCurrentUserClient } from "./user"; import { useCurrentUserClient } from "./user";
@ -22,6 +22,12 @@ export function useCurrentActorClient() {
return { currentActor, error, loading }; return { currentActor, error, loading };
} }
export function useLazyCurrentUserIdentities() {
return useLazyQuery<{
loggedUser: Pick<ICurrentUser, "actors">;
}>(IDENTITIES);
}
export function useCurrentUserIdentities() { export function useCurrentUserIdentities() {
const { currentUser } = useCurrentUserClient(); const { currentUser } = useCurrentUserClient();

View file

@ -453,7 +453,7 @@ export const DELETE_PERSON = gql`
* Prefer CREATE_PERSON when creating another identity * Prefer CREATE_PERSON when creating another identity
*/ */
export const REGISTER_PERSON = gql` export const REGISTER_PERSON = gql`
mutation ( mutation RegisterPerson(
$preferredUsername: String! $preferredUsername: String!
$name: String! $name: String!
$summary: String! $summary: String!

View file

@ -1,4 +1,4 @@
import { provide, createApp, h, computed, ref } from "vue"; import { provide, createApp, h, ref } from "vue";
import VueScrollTo from "vue-scrollto"; import VueScrollTo from "vue-scrollto";
// import VueAnnouncer from "@vue-a11y/announcer"; // import VueAnnouncer from "@vue-a11y/announcer";
// import VueSkipTo from "@vue-a11y/skip-to"; // import VueSkipTo from "@vue-a11y/skip-to";
@ -59,11 +59,7 @@ apolloClient
instanceName.value = configData.config?.name; instanceName.value = configData.config?.name;
}); });
const head = createHead({ const head = createHead();
titleTemplate: computed(() =>
instanceName.value ? `%s | ${instanceName.value}` : "%s"
).value,
});
app.use(head); app.use(head);
app.mount("#app"); app.mount("#app");

View file

@ -1,14 +1,8 @@
import { AUTH_USER_ACTOR_ID } from "@/constants"; import { AUTH_USER_ACTOR_ID } from "@/constants";
import { UPDATE_CURRENT_ACTOR_CLIENT, IDENTITIES } from "@/graphql/actor"; import { UPDATE_CURRENT_ACTOR_CLIENT } from "@/graphql/actor";
import { IPerson } from "@/types/actor"; import { IPerson } from "@/types/actor";
import { ICurrentUser } from "@/types/current-user.model";
import { apolloClient } from "@/vue-apollo"; import { apolloClient } from "@/vue-apollo";
import { import { provideApolloClient, useMutation } from "@vue/apollo-composable";
provideApolloClient,
useLazyQuery,
useMutation,
} from "@vue/apollo-composable";
import { computed } from "vue";
export class NoIdentitiesException extends Error {} export class NoIdentitiesException extends Error {}
@ -38,38 +32,31 @@ export async function changeIdentity(identity: IPerson): Promise<void> {
}); });
} }
const { load: loadIdentities } = provideApolloClient(apolloClient)(() =>
useLazyQuery<{ loggedUser: Pick<ICurrentUser, "actors"> }>(IDENTITIES)
);
/** /**
* We fetch from localStorage the latest actor ID used, * We fetch from localStorage the latest actor ID used,
* then fetch the current identities to set in cache * then fetch the current identities to set in cache
* the current identity used * the current identity used
*/ */
export async function initializeCurrentActor(): Promise<void> { export async function initializeCurrentActor(
identities: IPerson[] | undefined
): Promise<void> {
const actorId = localStorage.getItem(AUTH_USER_ACTOR_ID); const actorId = localStorage.getItem(AUTH_USER_ACTOR_ID);
console.debug("Initializing current actor", actorId); console.debug("Initializing current actor", actorId);
try { try {
const result = await loadIdentities(); if (!identities) {
if (!result) return; console.debug("Failed to load user's identities", identities);
return;
}
console.debug("got identities", result); if (identities && identities.length < 1) {
const identities = computed(() => result.loggedUser?.actors);
console.debug(
"initializing current actor based on identities",
identities.value
);
if (identities.value && identities.value.length < 1) {
console.warn("Logged user has no identities!"); console.warn("Logged user has no identities!");
throw new NoIdentitiesException(); throw new NoIdentitiesException();
} }
const activeIdentity = const activeIdentity =
(identities.value || []).find( (identities || []).find(
(identity: IPerson | undefined) => identity?.id === actorId (identity: IPerson | undefined) => identity?.id === actorId
) || ((identities.value || [])[0] as IPerson); ) || ((identities || [])[0] as IPerson);
if (activeIdentity) { if (activeIdentity) {
await changeIdentity(activeIdentity); await changeIdentity(activeIdentity);

View file

@ -33,6 +33,7 @@
</li> </li>
</breadcrumbs-nav> </breadcrumbs-nav>
<DraggableList <DraggableList
v-if="resource.actor"
:resources="resource.children.elements" :resources="resource.children.elements"
:isRoot="resource.path === '/'" :isRoot="resource.path === '/'"
:group="resource.actor" :group="resource.actor"
@ -98,20 +99,34 @@
v-model:active="createResourceModal" v-model:active="createResourceModal"
has-modal-card has-modal-card
:close-button-aria-label="t('Close')" :close-button-aria-label="t('Close')"
trap-focus :autoFocus="false"
> >
<section class="w-full md:w-[640px]"> <section class="w-full md:w-[640px]">
<o-notification variant="danger" v-if="modalError"> <o-notification variant="danger" v-if="modalError">
{{ modalError }} {{ modalError }}
</o-notification> </o-notification>
<form @submit.prevent="createResource"> <form @submit.prevent="createResource">
<p v-if="newResource.type !== 'folder'"> <p v-if="newResource.type === 'pad'">
{{ {{
t("The pad will be created on {service}", { t("The pad will be created on {service}", {
service: newResourceHost, service: newResourceHost,
}) })
}} }}
</p> </p>
<p v-else-if="newResource.type === 'calc'">
{{
t("The calc will be created on {service}", {
service: newResourceHost,
})
}}
</p>
<p v-else-if="newResource.type === 'visio'">
{{
t("The videoconference will be created on {service}", {
service: newResourceHost,
})
}}
</p>
<o-field :label="t('Title')" label-for="new-resource-title"> <o-field :label="t('Title')" label-for="new-resource-title">
<o-input <o-input
ref="modalNewResourceInput" ref="modalNewResourceInput"
@ -132,7 +147,7 @@
has-modal-card has-modal-card
aria-modal aria-modal
:close-button-aria-label="t('Close')" :close-button-aria-label="t('Close')"
trap-focus :autoFocus="false"
:width="640" :width="640"
> >
<div class="w-full md:w-[640px]"> <div class="w-full md:w-[640px]">
@ -298,8 +313,12 @@ const modalError = ref("");
const modalFieldErrors: Record<string, string> = reactive({}); const modalFieldErrors: Record<string, string> = reactive({});
const resourceRenameInput = ref<any>(); const resourceRenameInput = ref<any>();
const modalNewResourceInput = ref<HTMLElement>(); const modalNewResourceInput = ref<{
const modalNewResourceLinkInput = ref<HTMLElement>(); $refs: { inputRef: HTMLInputElement };
} | null>();
const modalNewResourceLinkInput = ref<{
$refs: { inputRef: HTMLInputElement };
} | null>();
const actualPath = computed((): string => { const actualPath = computed((): string => {
const path = Array.isArray(props.path) ? props.path.join("/") : props.path; const path = Array.isArray(props.path) ? props.path.join("/") : props.path;
@ -419,14 +438,14 @@ const createSentenceForType = (type: string): string => {
const createLinkModal = async (): Promise<void> => { const createLinkModal = async (): Promise<void> => {
createLinkResourceModal.value = true; createLinkResourceModal.value = true;
await nextTick(); await nextTick();
modalNewResourceLinkInput.value?.focus(); modalNewResourceLinkInput.value?.$refs.inputRef?.focus();
}; };
const createFolderModal = async (): Promise<void> => { const createFolderModal = async (): Promise<void> => {
newResource.type = "folder"; newResource.type = "folder";
createResourceModal.value = true; createResourceModal.value = true;
await nextTick(); await nextTick();
modalNewResourceInput.value?.focus(); modalNewResourceInput.value?.$refs.inputRef?.focus();
}; };
const createResourceFromProvider = async ( const createResourceFromProvider = async (
@ -436,7 +455,7 @@ const createResourceFromProvider = async (
newResource.type = provider.software; newResource.type = provider.software;
createResourceModal.value = true; createResourceModal.value = true;
await nextTick(); await nextTick();
modalNewResourceInput.value?.focus(); modalNewResourceInput.value?.$refs.inputRef?.focus();
}; };
const generateFullResourceUrl = (provider: IProvider): string => { const generateFullResourceUrl = (provider: IProvider): string => {

View file

@ -143,6 +143,7 @@ import { LoginError, LoginErrorCode } from "@/types/enums";
import { useCurrentUserClient } from "@/composition/apollo/user"; import { useCurrentUserClient } from "@/composition/apollo/user";
import { useHead } from "@unhead/vue"; import { useHead } from "@unhead/vue";
import { enumTransformer, useRouteQuery } from "vue-use-route-query"; import { enumTransformer, useRouteQuery } from "vue-use-route-query";
import { useLazyCurrentUserIdentities } from "@/composition/apollo/actor";
const { t } = useI18n({ useScope: "global" }); const { t } = useI18n({ useScope: "global" });
const router = useRouter(); const router = useRouter();
@ -235,12 +236,17 @@ const loginAction = (e: Event) => {
}); });
}; };
const { load: loadIdentities } = useLazyCurrentUserIdentities();
const { onDone: onCurrentUserMutationDone, mutate: updateCurrentUserMutation } = const { onDone: onCurrentUserMutationDone, mutate: updateCurrentUserMutation } =
useMutation(UPDATE_CURRENT_USER_CLIENT); useMutation(UPDATE_CURRENT_USER_CLIENT);
onCurrentUserMutationDone(async () => { onCurrentUserMutationDone(async () => {
console.debug("Current user mutation done, now setuping actors…");
try { try {
await initializeCurrentActor(); const result = await loadIdentities();
if (!result) return;
await initializeCurrentActor(result.loggedUser.actors);
} catch (err: any) { } catch (err: any) {
if (err instanceof NoIdentitiesException && currentUser.value) { if (err instanceof NoIdentitiesException && currentUser.value) {
await router.push({ await router.push({
@ -257,6 +263,7 @@ onCurrentUserMutationDone(async () => {
}); });
const setupClientUserAndActors = async (login: ILogin): Promise<void> => { const setupClientUserAndActors = async (login: ILogin): Promise<void> => {
console.debug("Setuping client user and actors");
updateCurrentUserMutation({ updateCurrentUserMutation({
id: login.user.id, id: login.user.id,
email: credentials.email, email: credentials.email,
@ -298,7 +305,7 @@ onMounted(() => {
if (currentUser.value?.isLoggedIn) { if (currentUser.value?.isLoggedIn) {
console.debug( console.debug(
"Current user is already logged-in, redirecting to Homepage", "Current user is already logged-in, redirecting to Homepage",
currentUser currentUser.value
); );
router.push("/"); router.push("/");
} }

View file

@ -26,15 +26,19 @@
required required
type="email" type="email"
v-model="emailValue" v-model="emailValue"
expanded
/> />
</o-field> </o-field>
<p class="control"> <p class="my-4 flex gap-2">
<o-button variant="primary" native-type="submit"> <o-button variant="primary" native-type="submit">
{{ t("Submit") }} {{ t("Submit") }}
</o-button> </o-button>
<router-link :to="{ name: RouteName.LOGIN }" class="button is-text">{{ <o-button
t("Cancel") tag="router-link"
}}</router-link> :to="{ name: RouteName.LOGIN }"
variant="text"
>{{ t("Cancel") }}</o-button
>
</p> </p>
</form> </form>
<div v-else> <div v-else>