Merge remote-tracking branch 'origin/main'
This commit is contained in:
commit
7d7abd0dda
|
@ -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)
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -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
1760
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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 */
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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 />
|
||||||
|
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
@ -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!
|
||||||
|
|
|
@ -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");
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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 => {
|
||||||
|
|
|
@ -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("/");
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue