Merge branch 'fixes' into 'main'

Little features

Closes #1082, #1102, #1154 et #540

See merge request framasoft/mobilizon!1305
This commit is contained in:
Thomas Citharel 2022-10-28 19:00:49 +00:00
commit 720c11c43f
27 changed files with 244 additions and 58 deletions

View file

@ -169,7 +169,8 @@ e2e:
artifacts: artifacts:
expire_in: 2 days expire_in: 2 days
paths: paths:
- js/playwright-report - js/playwright-report/
- js/test-results/
pages: pages:
stage: deploy stage: deploy

View file

@ -19,6 +19,7 @@ config :mobilizon, :instance,
registrations_open: false, registrations_open: false,
registration_email_allowlist: [], registration_email_allowlist: [],
registration_email_denylist: [], registration_email_denylist: [],
disable_database_login: false,
languages: [], languages: [],
default_language: "en", default_language: "en",
demo: false, demo: false,

View file

@ -54,6 +54,7 @@ import {
defineAsyncComponent, defineAsyncComponent,
computed, computed,
watch, watch,
onBeforeUnmount,
} from "vue"; } from "vue";
import { LocationType } from "@/types/user-location.model"; import { LocationType } from "@/types/user-location.model";
import { useMutation, useQuery } from "@vue/apollo-composable"; import { useMutation, useQuery } from "@vue/apollo-composable";
@ -123,7 +124,7 @@ const interval = ref<number>(0);
const notifier = inject<Notifier>("notifier"); const notifier = inject<Notifier>("notifier");
interval.value = setInterval(async () => { interval.value = window.setInterval(async () => {
const accessToken = localStorage.getItem(AUTH_ACCESS_TOKEN); const accessToken = localStorage.getItem(AUTH_ACCESS_TOKEN);
if (accessToken) { if (accessToken) {
const token = jwt_decode<JwtPayload>(accessToken); const token = jwt_decode<JwtPayload>(accessToken);
@ -155,6 +156,7 @@ onBeforeMount(async () => {
}); });
const snackbar = inject<Snackbar>("snackbar"); const snackbar = inject<Snackbar>("snackbar");
const darkModePreference = window.matchMedia("(prefers-color-scheme: dark)");
onMounted(() => { onMounted(() => {
online.value = window.navigator.onLine; online.value = window.navigator.onLine;
@ -187,6 +189,7 @@ onMounted(() => {
}, },
}); });
}); });
darkModePreference.addEventListener("change", changeTheme);
}); });
onUnmounted(() => { onUnmounted(() => {
@ -289,6 +292,23 @@ watch(config, async (configWatched: IConfig | undefined) => {
}); });
const isDemoMode = computed(() => config.value?.demoMode); const isDemoMode = computed(() => config.value?.demoMode);
const changeTheme = () => {
console.debug("changing theme");
if (
localStorage.getItem("theme") === "dark" ||
(!("theme" in localStorage) &&
window.matchMedia("(prefers-color-scheme: dark)").matches)
) {
document.documentElement.classList.add("dark");
} else {
document.documentElement.classList.remove("dark");
}
};
onBeforeUnmount(() => {
darkModePreference.removeEventListener("change", changeTheme);
});
</script> </script>
<style lang="scss"> <style lang="scss">

View file

@ -2,6 +2,10 @@ body {
@apply bg-body-background-color dark:bg-zinc-800 dark:text-white; @apply bg-body-background-color dark:bg-zinc-800 dark:text-white;
} }
.out {
@apply underline hover:decoration-2 hover:decoration-mbz-yellow-alt-600;
}
/* Button */ /* Button */
.btn { .btn {
@apply font-bold py-2 px-4 bg-mbz-bluegreen hover:bg-mbz-bluegreen-600 text-white rounded h-10 outline-none focus:ring ring-offset-1 ring-offset-slate-50 ring-blue-300; @apply font-bold py-2 px-4 bg-mbz-bluegreen hover:bg-mbz-bluegreen-600 text-white rounded h-10 outline-none focus:ring ring-offset-1 ring-offset-slate-50 ring-blue-300;
@ -194,6 +198,10 @@ body {
@apply pl-2; @apply pl-2;
} }
.o-field--addons .o-radio:not(:only-child) input {
@apply rounded-full;
}
/* Editor */ /* Editor */
button.menubar__button { button.menubar__button {
@apply dark:text-white; @apply dark:text-white;

View file

@ -3,9 +3,8 @@
<div class=""> <div class="">
<o-field <o-field
:label-for="id" :label-for="id"
expanded
:message="fieldErrors" :message="fieldErrors"
:type="{ 'is-danger': fieldErrors }" :variant="{ danger: fieldErrors }"
class="!-mt-2" class="!-mt-2"
:labelClass="labelClass" :labelClass="labelClass"
> >
@ -32,7 +31,6 @@
v-model="queryText" v-model="queryText"
:placeholder="placeholderWithDefault" :placeholder="placeholderWithDefault"
:customFormatter="(elem: IAddress) => addressFullName(elem)" :customFormatter="(elem: IAddress) => addressFullName(elem)"
:loading="isFetching"
:debounceTyping="debounceDelay" :debounceTyping="debounceDelay"
@typing="asyncData" @typing="asyncData"
:icon="canShowLocateMeButton ? null : 'map-marker'" :icon="canShowLocateMeButton ? null : 'map-marker'"

View file

@ -20,7 +20,6 @@
/> />
<full-address-auto-complete <full-address-auto-complete
:resultType="AddressSearchType.ADMINISTRATIVE" :resultType="AddressSearchType.ADMINISTRATIVE"
:doGeoLocation="false"
v-model="location" v-model="location"
:hide-map="true" :hide-map="true"
:hide-selected="true" :hide-selected="true"

View file

@ -24,7 +24,7 @@
@click="scrollLeft" @click="scrollLeft"
class="absolute inset-y-0 my-auto z-10 rounded-full bg-white dark:bg-transparent w-10 h-10 border border-shadowColor -left-5" class="absolute inset-y-0 my-auto z-10 rounded-full bg-white dark:bg-transparent w-10 h-10 border border-shadowColor -left-5"
> >
<div class="">&lt;</div> <span class="">&lt;</span>
</button> </button>
</div> </div>
<div class="overflow-hidden"> <div class="overflow-hidden">
@ -41,7 +41,7 @@
@click="scrollRight" @click="scrollRight"
class="absolute inset-y-0 my-auto z-10 rounded-full bg-white dark:bg-transparent w-10 h-10 border border-shadowColor -right-5" class="absolute inset-y-0 my-auto z-10 rounded-full bg-white dark:bg-transparent w-10 h-10 border border-shadowColor -right-5"
> >
<div class="">&gt;</div> <span class="">&gt;</span>
</button> </button>
</div> </div>
</div> </div>

View file

@ -29,9 +29,7 @@
v-for="group in selectedGroups" v-for="group in selectedGroups"
:key="group.id" :key="group.id"
:group="group" :group="group"
:view-mode="'column'" :mode="'column'"
:minimal="true"
:has-border="true"
:showSummary="false" :showSummary="false"
/> />

View file

@ -19,9 +19,7 @@
v-for="event in events?.elements" v-for="event in events?.elements"
:key="event.id" :key="event.id"
:event="event" :event="event"
view-mode="column" mode="column"
:has-border="true"
:minimal="true"
/> />
<more-content <more-content
:to="{ :to="{

View file

@ -185,7 +185,7 @@
>{{ t("Login") }}</router-link >{{ t("Login") }}</router-link
> >
</li> </li>
<li v-if="!currentActor?.id"> <li v-if="!currentActor?.id && canRegister">
<router-link <router-link
:to="{ name: RouteName.REGISTER }" :to="{ name: RouteName.REGISTER }"
class="block py-2 pr-4 pl-3 text-zinc-700 border-b border-gray-100 hover:bg-zinc-50 md:hover:bg-transparent md:border-0 md:hover:text-mbz-purple-700 md:p-0 dark:text-zinc-400 md:dark:hover:text-white dark:hover:bg-zinc-700 dark:hover:text-white md:dark:hover:bg-transparent dark:border-gray-700" class="block py-2 pr-4 pl-3 text-zinc-700 border-b border-gray-100 hover:bg-zinc-50 md:hover:bg-transparent md:border-0 md:hover:text-mbz-purple-700 md:p-0 dark:text-zinc-400 md:dark:hover:text-white dark:hover:bg-zinc-700 dark:hover:text-white md:dark:hover:bg-transparent dark:border-gray-700"
@ -374,7 +374,7 @@ import { ICurrentUserRole } from "@/types/enums";
import { logout } from "../utils/auth"; import { logout } from "../utils/auth";
import { displayName } from "../types/actor"; import { displayName } from "../types/actor";
import RouteName from "../router/name"; import RouteName from "../router/name";
import { ref, watch } from "vue"; import { computed, ref, watch } from "vue";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import AccountCircle from "vue-material-design-icons/AccountCircle.vue"; import AccountCircle from "vue-material-design-icons/AccountCircle.vue";
@ -387,6 +387,7 @@ import {
import { useMutation } from "@vue/apollo-composable"; import { useMutation } from "@vue/apollo-composable";
import { UPDATE_DEFAULT_ACTOR } from "@/graphql/actor"; import { UPDATE_DEFAULT_ACTOR } from "@/graphql/actor";
import { changeIdentity } from "@/utils/identity"; import { changeIdentity } from "@/utils/identity";
import { useRegistrationConfig } from "@/composition/apollo/config";
// import { useRestrictions } from "@/composition/apollo/config"; // import { useRestrictions } from "@/composition/apollo/config";
const { currentUser } = useCurrentUserClient(); const { currentUser } = useCurrentUserClient();
@ -399,6 +400,15 @@ const router = useRouter();
// const route = useRoute(); // const route = useRoute();
const { identities } = useCurrentUserIdentities(); const { identities } = useCurrentUserIdentities();
const { registrationsOpen, registrationsAllowlist, databaseLogin } =
useRegistrationConfig();
const canRegister = computed(() => {
return (
(registrationsOpen.value || registrationsAllowlist.value) &&
databaseLogin.value
);
});
// const mobileNavbarActive = ref(false); // const mobileNavbarActive = ref(false);

View file

@ -11,8 +11,7 @@
<event-card <event-card
v-if="instanceOfIEvent(activeElement)" v-if="instanceOfIEvent(activeElement)"
:event="(activeElement as IEvent)" :event="(activeElement as IEvent)"
:has-border="false" mode="column"
view-mode="column"
:options="{ :options="{
isRemoteEvent: activeElement.__typename === 'EventResult', isRemoteEvent: activeElement.__typename === 'EventResult',
isLoggedIn, isLoggedIn,
@ -21,8 +20,7 @@
<group-card <group-card
v-else v-else
:group="(activeElement as IGroup)" :group="(activeElement as IGroup)"
:has-border="false" mode="column"
view-mode="column"
:isRemoteGroup="activeElement.__typename === 'GroupResult'" :isRemoteGroup="activeElement.__typename === 'GroupResult'"
:isLoggedIn="isLoggedIn" :isLoggedIn="isLoggedIn"
/> />
@ -34,8 +32,7 @@
<event-card <event-card
v-if="instanceOfIEvent(activeElement)" v-if="instanceOfIEvent(activeElement)"
:event="(activeElement as IEvent)" :event="(activeElement as IEvent)"
view-mode="column" mode="column"
:has-border="false"
:options="{ :options="{
isRemoteEvent: activeElement.__typename === 'EventResult', isRemoteEvent: activeElement.__typename === 'EventResult',
isLoggedIn, isLoggedIn,
@ -44,8 +41,7 @@
<group-card <group-card
v-else v-else
:group="(activeElement as IGroup)" :group="(activeElement as IGroup)"
:has-border="false" mode="column"
view-mode="column"
:isRemoteGroup="activeElement.__typename === 'GroupResult'" :isRemoteGroup="activeElement.__typename === 'GroupResult'"
:isLoggedIn="isLoggedIn" :isLoggedIn="isLoggedIn"
/> />

View file

@ -11,6 +11,7 @@ import {
GEOCODING_AUTOCOMPLETE, GEOCODING_AUTOCOMPLETE,
LOCATION, LOCATION,
MAPS_TILES, MAPS_TILES,
REGISTRATIONS,
RESOURCE_PROVIDERS, RESOURCE_PROVIDERS,
RESTRICTIONS, RESTRICTIONS,
ROUTING_TYPE, ROUTING_TYPE,
@ -204,3 +205,28 @@ export function useSearchConfig() {
const searchConfig = computed(() => result.value?.config.search); const searchConfig = computed(() => result.value?.config.search);
return { searchConfig, error, loading, onResult }; return { searchConfig, error, loading, onResult };
} }
export function useRegistrationConfig() {
const { result, error, loading, onResult } = useQuery<{
config: Pick<
IConfig,
"registrationsOpen" | "registrationsAllowlist" | "auth"
>;
}>(REGISTRATIONS, undefined, { fetchPolicy: "cache-only" });
const registrationsOpen = computed(
() => result.value?.config.registrationsOpen
);
const registrationsAllowlist = computed(
() => result.value?.config.registrationsAllowlist
);
const databaseLogin = computed(() => result.value?.config.auth.databaseLogin);
return {
registrationsOpen,
registrationsAllowlist,
databaseLogin,
error,
loading,
onResult,
};
}

View file

@ -79,6 +79,7 @@ export const CONFIG = gql`
} }
auth { auth {
ldap ldap
databaseLogin
oauthProviders { oauthProviders {
id id
label label
@ -386,6 +387,7 @@ export const LOGIN_CONFIG = gql`
query LoginConfig { query LoginConfig {
config { config {
auth { auth {
databaseLogin
oauthProviders { oauthProviders {
id id
label label
@ -444,3 +446,15 @@ export const SEARCH_CONFIG = gql`
} }
} }
`; `;
export const REGISTRATIONS = gql`
query Registrations {
config {
registrationsOpen
registrationsAllowlist
auth {
databaseLogin
}
}
}
`;

View file

@ -1408,5 +1408,14 @@
"Most recently published": "Most recently published", "Most recently published": "Most recently published",
"Least recently published": "Least recently published", "Least recently published": "Least recently published",
"With the most participants": "With the most participants", "With the most participants": "With the most participants",
"Number of members": "Number of members" "Number of members": "Number of members",
"More options": "More options",
"Reported by someone anonymously": "Reported by someone anonymously",
"Back to homepage": "Back to homepage",
"Category list": "Category list",
"No categories with public upcoming events on this instance were found.": "No categories with public upcoming events on this instance were found.",
"Theme": "Theme",
"Adapt to system theme": "Adapt to system theme",
"Light": "Light",
"Dark": "Dark"
} }

View file

@ -1406,5 +1406,14 @@
"{timezoneLongName} ({timezoneShortName})": "{timezoneLongName} ({timezoneShortName})", "{timezoneLongName} ({timezoneShortName})": "{timezoneLongName} ({timezoneShortName})",
"{title} ({count} todos)": "{title} ({count} todos)", "{title} ({count} todos)": "{title} ({count} todos)",
"{username} was invited to {group}": "{username} a été invité à {group}", "{username} was invited to {group}": "{username} a été invité à {group}",
"© The OpenStreetMap Contributors": "© Les Contributeur⋅ices OpenStreetMap" "© The OpenStreetMap Contributors": "© Les Contributeur⋅ices OpenStreetMap",
"More options": "Plus d'options",
"Reported by someone anonymously": "Signalé par quelqu'un anonymement",
"Back to homepage": "Retour à la page d'accueil",
"Category list": "Liste des catégories",
"No categories with public upcoming events on this instance were found.": "Aucune catégorie avec des événements publics à venir n'a été trouvée.",
"Theme": "Thème",
"Adapt to system theme": "Sadapter au thème du système",
"Light": "Clair",
"Dark": "Sombre"
} }

View file

@ -106,6 +106,7 @@ export interface IConfig {
version: string; version: string;
auth: { auth: {
ldap: boolean; ldap: boolean;
databaseLogin: boolean;
oauthProviders: IOAuthProvider[]; oauthProviders: IOAuthProvider[];
}; };
uploadLimits: { uploadLimits: {

View file

@ -33,7 +33,7 @@
<div class="flex flex-wrap gap-4"> <div class="flex flex-wrap gap-4">
<o-field <o-field
v-if="eventCategories" v-if="orderedCategories"
:label="t('Category')" :label="t('Category')"
label-for="categoryField" label-for="categoryField"
class="w-full md:max-w-fit" class="w-full md:max-w-fit"
@ -45,7 +45,7 @@
expanded expanded
> >
<option <option
v-for="category in eventCategories" v-for="category in orderedCategories"
:value="category.id" :value="category.id"
:key="category.id" :key="category.id"
> >
@ -595,6 +595,7 @@ import { Notifier } from "@/plugins/notifier";
import { useHead } from "@vueuse/head"; import { useHead } from "@vueuse/head";
import { useProgrammatic } from "@oruga-ui/oruga-next"; import { useProgrammatic } from "@oruga-ui/oruga-next";
import type { Locale } from "date-fns"; import type { Locale } from "date-fns";
import sortBy from "lodash/sortBy";
const DEFAULT_LIMIT_NUMBER_OF_PLACES = 10; const DEFAULT_LIMIT_NUMBER_OF_PLACES = 10;
@ -1331,6 +1332,11 @@ watch(group, () => {
event.value.visibility = EventVisibility.PUBLIC; event.value.visibility = EventVisibility.PUBLIC;
} }
}); });
const orderedCategories = computed(() => {
if (!eventCategories.value) return undefined;
return sortBy(eventCategories.value, ["label"]);
});
</script> </script>
<style lang="scss"> <style lang="scss">

View file

@ -193,7 +193,7 @@
<template #options> <template #options>
<fieldset class="flex flex-col"> <fieldset class="flex flex-col">
<legend class="sr-only">{{ t("Categories") }}</legend> <legend class="sr-only">{{ t("Categories") }}</legend>
<div v-for="category in eventCategories" :key="category.id"> <div v-for="category in orderedCategories" :key="category.id">
<input <input
:id="category.id" :id="category.id"
v-model="categoryOneOf" v-model="categoryOneOf"
@ -692,6 +692,7 @@ import { IAddress } from "@/types/address.model";
import { IConfig } from "@/types/config.model"; import { IConfig } from "@/types/config.model";
import { TypeNamed } from "@/types/apollo"; import { TypeNamed } from "@/types/apollo";
import { LatLngBounds } from "leaflet"; import { LatLngBounds } from "leaflet";
import lodashSortBy from "lodash/sortBy";
const EventMarkerMap = defineAsyncComponent( const EventMarkerMap = defineAsyncComponent(
() => import("@/components/Search/EventMarkerMap.vue") () => import("@/components/Search/EventMarkerMap.vue")
@ -825,6 +826,11 @@ const props = defineProps<{
const { features } = useFeatures(); const { features } = useFeatures();
const { eventCategories } = useEventCategories(); const { eventCategories } = useEventCategories();
const orderedCategories = computed(() => {
if (!eventCategories.value) return [];
return lodashSortBy(eventCategories.value, ["label"]);
});
const searchEvents = computed(() => searchElementsResult.value?.searchEvents); const searchEvents = computed(() => searchElementsResult.value?.searchEvents);
const searchGroups = computed(() => searchElementsResult.value?.searchGroups); const searchGroups = computed(() => searchElementsResult.value?.searchGroups);

View file

@ -13,6 +13,36 @@
]" ]"
/> />
<div> <div>
<o-field :label="t('Theme')" addonsClass="flex flex-col">
<o-field>
<o-checkbox v-model="systemTheme">{{
t("Adapt to system theme")
}}</o-checkbox>
</o-field>
<o-field>
<fieldset>
<legend class="sr-only">{{ t("Theme") }}</legend>
<o-radio
:class="{ 'border-mbz-bluegreen border-2': theme === 'light' }"
class="p-4 bg-white text-zinc-800 rounded-md mt-2 mr-2"
:disabled="systemTheme"
v-model="theme"
name="theme"
native-value="light"
>{{ t("Light") }}</o-radio
>
<o-radio
:class="{ 'border-mbz-bluegreen border-2': theme === 'dark' }"
class="p-4 bg-zinc-800 rounded-md text-white mt-2 ml-2"
:disabled="systemTheme"
v-model="theme"
name="theme"
native-value="dark"
>{{ t("Dark") }}</o-radio
>
</fieldset>
</o-field>
</o-field>
<o-field :label="t('Language')" label-for="setting-language"> <o-field :label="t('Language')" label-for="setting-language">
<o-select <o-select
:loading="loadingTimezones || loadingUserSettings" :loading="loadingTimezones || loadingUserSettings"
@ -65,7 +95,6 @@
<full-address-auto-complete <full-address-auto-complete
v-if="loggedUser?.settings" v-if="loggedUser?.settings"
:resultType="AddressSearchType.ADMINISTRATIVE" :resultType="AddressSearchType.ADMINISTRATIVE"
:doGeoLocation="false"
v-model="address" v-model="address"
:default-text="address?.description" :default-text="address?.description"
id="setting-city" id="setting-city"
@ -120,7 +149,7 @@ import { Address, IAddress } from "@/types/address.model";
import { useTimezones } from "@/composition/apollo/config"; import { useTimezones } from "@/composition/apollo/config";
import { useUserSettings } from "@/composition/apollo/user"; import { useUserSettings } from "@/composition/apollo/user";
import { useHead } from "@vueuse/head"; import { useHead } from "@vueuse/head";
import { computed, defineAsyncComponent } from "vue"; import { computed, defineAsyncComponent, ref, watch } from "vue";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { useMutation } from "@vue/apollo-composable"; import { useMutation } from "@vue/apollo-composable";
@ -140,6 +169,44 @@ useHead({
// langs: Record<string, string> = langs; // langs: Record<string, string> = langs;
const theme = ref(localStorage.getItem("theme"));
const systemTheme = ref(!("theme" in localStorage));
watch(systemTheme, (newSystemTheme) => {
console.debug("changing system theme", newSystemTheme);
if (newSystemTheme) {
theme.value = null;
localStorage.removeItem("theme");
} else {
theme.value = "light";
localStorage.setItem("theme", theme.value);
}
changeTheme();
});
watch(theme, (newTheme) => {
console.debug("changing theme value", newTheme);
if (newTheme) {
localStorage.setItem("theme", newTheme);
}
changeTheme();
});
const changeTheme = () => {
console.debug("changing theme to apply");
if (
localStorage.getItem("theme") === "dark" ||
(!("theme" in localStorage) &&
window.matchMedia("(prefers-color-scheme: dark)").matches)
) {
console.debug("applying dark theme");
document.documentElement.classList.add("dark");
} else {
console.debug("removing dark theme");
document.documentElement.classList.remove("dark");
}
};
const selectedTimezone = computed({ const selectedTimezone = computed({
get() { get() {
if (loggedUser.value?.settings?.timezone) { if (loggedUser.value?.settings?.timezone) {

View file

@ -42,7 +42,7 @@
> >
{{ error }} {{ error }}
</o-notification> </o-notification>
<form @submit="loginAction"> <form @submit="loginAction" v-if="config?.auth.databaseLogin">
<o-field <o-field
:label="t('Email')" :label="t('Email')"
label-for="email" label-for="email"
@ -81,13 +81,6 @@
</p> </p>
<!-- <o-loading :is-full-page="false" v-model="submitted" /> --> <!-- <o-loading :is-full-page="false" v-model="submitted" /> -->
<div
class="control"
v-if="config && config?.auth.oauthProviders.length > 0"
>
<auth-providers :oauthProviders="config.auth.oauthProviders" />
</div>
<div class="flex flex-wrap gap-2 mt-3"> <div class="flex flex-wrap gap-2 mt-3">
<o-button <o-button
tag="router-link" tag="router-link"
@ -107,7 +100,7 @@
}" }"
>{{ t("Didn't receive the instructions?") }}</o-button >{{ t("Didn't receive the instructions?") }}</o-button
> >
<p class="control" v-if="config && config.registrationsOpen"> <p class="control" v-if="canRegister">
<o-button <o-button
tag="router-link" tag="router-link"
variant="text" variant="text"
@ -123,6 +116,9 @@
</p> </p>
</div> </div>
</form> </form>
<div v-if="config && config?.auth.oauthProviders.length > 0">
<auth-providers :oauthProviders="config.auth.oauthProviders" />
</div>
</section> </section>
</template> </template>
@ -162,11 +158,21 @@ const route = useRoute();
const { currentUser } = useCurrentUserClient(); const { currentUser } = useCurrentUserClient();
const { result: configResult } = useQuery<{ const { result: configResult } = useQuery<{
config: Pick<IConfig, "auth" | "registrationsOpen">; config: Pick<
IConfig,
"auth" | "registrationsOpen" | "registrationsAllowlist"
>;
}>(LOGIN_CONFIG); }>(LOGIN_CONFIG);
const config = computed(() => configResult.value?.config); const config = computed(() => configResult.value?.config);
const canRegister = computed(() => {
return (
(config.value?.registrationsOpen || config.value?.registrationsAllowlist) &&
config.value?.auth.databaseLogin
);
});
const errors = ref<string[]>([]); const errors = ref<string[]>([]);
const submitted = ref(false); const submitted = ref(false);

View file

@ -1,5 +1,5 @@
<template> <template>
<div class="container mx-auto pt-6"> <div class="container mx-auto py-6">
<section class=""> <section class="">
<h1> <h1>
{{ {{
@ -123,7 +123,7 @@
/> />
</o-field> </o-field>
<div class="flex items-start mb-6"> <div class="flex items-start mb-6 mt-2">
<div class="flex items-center h-5"> <div class="flex items-center h-5">
<input <input
type="checkbox" type="checkbox"
@ -155,7 +155,7 @@
</label> </label>
</div> </div>
<p class="create-account control has-text-centered"> <p>
<o-button <o-button
variant="primary" variant="primary"
size="large" size="large"
@ -166,19 +166,19 @@
</o-button> </o-button>
</p> </p>
<p class="control has-text-centered"> <p class="my-6">
<router-link <o-button
class="button is-text" tag="router-link"
variant="text"
:to="{ :to="{
name: RouteName.RESEND_CONFIRMATION, name: RouteName.RESEND_CONFIRMATION,
params: { email: credentials.email }, params: { email: credentials.email },
}" }"
>{{ t("Didn't receive the instructions?") }}</router-link >{{ t("Didn't receive the instructions?") }}</o-button
> >
</p> <o-button
<p class="control has-text-centered"> tag="router-link"
<router-link variant="text"
class="button is-text"
:to="{ :to="{
name: RouteName.LOGIN, name: RouteName.LOGIN,
params: { params: {
@ -186,7 +186,7 @@
password: credentials.password, password: credentials.password,
}, },
}" }"
>{{ t("Login") }}</router-link >{{ t("Login") }}</o-button
> >
</p> </p>
@ -252,13 +252,13 @@ const title = computed((): string => {
if (config.value) { if (config.value) {
return t("Register an account on {instanceName}!", { return t("Register an account on {instanceName}!", {
instanceName: config.value?.name, instanceName: config.value?.name,
}) as string; });
} }
return ""; return "";
}); });
useHead({ useHead({
title: title.value, title: () => title.value,
}); });
const { onDone, onError, mutate } = useMutation(CREATE_USER); const { onDone, onError, mutate } = useMutation(CREATE_USER);

View file

@ -1,5 +1,6 @@
module.exports = { module.exports = {
content: ["./public/**/*.html", "./src/**/*.{vue,js,ts,jsx,tsx}"], content: ["./public/**/*.html", "./src/**/*.{vue,js,ts,jsx,tsx}"],
darkMode: "class",
theme: { theme: {
extend: { extend: {
colors: { colors: {

View file

@ -14,7 +14,9 @@ test("Login has everything we need", async ({ page }) => {
hasText: "Didn't receive the instructions?", hasText: "Didn't receive the instructions?",
}); });
const registerLink = page.locator("a", { hasText: "Create an account" }); const registerLink = page.locator("a > span > span", {
hasText: "Create an account",
});
await expect(forgotPasswordLink).toBeVisible(); await expect(forgotPasswordLink).toBeVisible();
await expect(reAskInstructionsLink).toBeVisible(); await expect(reAskInstructionsLink).toBeVisible();

View file

@ -24,7 +24,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Address do
} }
res = res =
if is_nil(object["address"]) do if is_nil(object["address"]) or not is_map(object["address"]) do
res res
else else
Map.merge(res, %{ Map.merge(res, %{

View file

@ -156,6 +156,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Config do
federating: Config.instance_federating(), federating: Config.instance_federating(),
auth: %{ auth: %{
ldap: Config.ldap_enabled?(), ldap: Config.ldap_enabled?(),
database_login:
Application.get_env(:mobilizon, :instance) |> get_in([:disable_database_login]) == false,
oauth_providers: Config.oauth_consumer_strategies() oauth_providers: Config.oauth_consumer_strategies()
}, },
upload_limits: %{ upload_limits: %{

View file

@ -305,6 +305,7 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do
""" """
object :auth do object :auth do
field(:ldap, :boolean, description: "Whether or not LDAP auth is enabled") field(:ldap, :boolean, description: "Whether or not LDAP auth is enabled")
field(:database_login, :boolean, description: "Whether or not database login is enabled")
field(:oauth_providers, list_of(:oauth_provider), description: "List of oauth providers") field(:oauth_providers, list_of(:oauth_provider), description: "List of oauth providers")
end end

View file

@ -7,6 +7,13 @@
<link rel="apple-touch-icon" href="/img/icons/apple-touch-icon-152x152.png" sizes="152x152" /> <link rel="apple-touch-icon" href="/img/icons/apple-touch-icon-152x152.png" sizes="152x152" />
<link rel="mask-icon" href="/img/icons/safari-pinned-tab.svg" color={theme_color()} /> <link rel="mask-icon" href="/img/icons/safari-pinned-tab.svg" color={theme_color()} />
<meta name="theme-color" content={theme_color()} /> <meta name="theme-color" content={theme_color()} />
<script>
if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark')
} else {
document.documentElement.classList.remove('dark')
}
</script>
<%= if is_root(assigns) do %> <%= if is_root(assigns) do %>
<link rel="preload" href="/img/shape-1.svg" as="image" /> <link rel="preload" href="/img/shape-1.svg" as="image" />
<link rel="preload" href="/img/shape-2.svg" as="image" /> <link rel="preload" href="/img/shape-2.svg" as="image" />