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:
expire_in: 2 days
paths:
- js/playwright-report
- js/playwright-report/
- js/test-results/
pages:
stage: deploy

View file

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

View file

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

View file

@ -2,6 +2,10 @@ body {
@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 */
.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;
@ -194,6 +198,10 @@ body {
@apply pl-2;
}
.o-field--addons .o-radio:not(:only-child) input {
@apply rounded-full;
}
/* Editor */
button.menubar__button {
@apply dark:text-white;

View file

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

View file

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

View file

@ -24,7 +24,7 @@
@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"
>
<div class="">&lt;</div>
<span class="">&lt;</span>
</button>
</div>
<div class="overflow-hidden">
@ -41,7 +41,7 @@
@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"
>
<div class="">&gt;</div>
<span class="">&gt;</span>
</button>
</div>
</div>

View file

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

View file

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

View file

@ -185,7 +185,7 @@
>{{ t("Login") }}</router-link
>
</li>
<li v-if="!currentActor?.id">
<li v-if="!currentActor?.id && canRegister">
<router-link
: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"
@ -374,7 +374,7 @@ import { ICurrentUserRole } from "@/types/enums";
import { logout } from "../utils/auth";
import { displayName } from "../types/actor";
import RouteName from "../router/name";
import { ref, watch } from "vue";
import { computed, ref, watch } from "vue";
import { useRouter } from "vue-router";
import { useI18n } from "vue-i18n";
import AccountCircle from "vue-material-design-icons/AccountCircle.vue";
@ -387,6 +387,7 @@ import {
import { useMutation } from "@vue/apollo-composable";
import { UPDATE_DEFAULT_ACTOR } from "@/graphql/actor";
import { changeIdentity } from "@/utils/identity";
import { useRegistrationConfig } from "@/composition/apollo/config";
// import { useRestrictions } from "@/composition/apollo/config";
const { currentUser } = useCurrentUserClient();
@ -399,6 +400,15 @@ const router = useRouter();
// const route = useRoute();
const { identities } = useCurrentUserIdentities();
const { registrationsOpen, registrationsAllowlist, databaseLogin } =
useRegistrationConfig();
const canRegister = computed(() => {
return (
(registrationsOpen.value || registrationsAllowlist.value) &&
databaseLogin.value
);
});
// const mobileNavbarActive = ref(false);

View file

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

View file

@ -11,6 +11,7 @@ import {
GEOCODING_AUTOCOMPLETE,
LOCATION,
MAPS_TILES,
REGISTRATIONS,
RESOURCE_PROVIDERS,
RESTRICTIONS,
ROUTING_TYPE,
@ -204,3 +205,28 @@ export function useSearchConfig() {
const searchConfig = computed(() => result.value?.config.search);
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 {
ldap
databaseLogin
oauthProviders {
id
label
@ -386,6 +387,7 @@ export const LOGIN_CONFIG = gql`
query LoginConfig {
config {
auth {
databaseLogin
oauthProviders {
id
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",
"Least recently published": "Least recently published",
"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})",
"{title} ({count} todos)": "{title} ({count} todos)",
"{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;
auth: {
ldap: boolean;
databaseLogin: boolean;
oauthProviders: IOAuthProvider[];
};
uploadLimits: {

View file

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

View file

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

View file

@ -13,6 +13,36 @@
]"
/>
<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-select
:loading="loadingTimezones || loadingUserSettings"
@ -65,7 +95,6 @@
<full-address-auto-complete
v-if="loggedUser?.settings"
:resultType="AddressSearchType.ADMINISTRATIVE"
:doGeoLocation="false"
v-model="address"
:default-text="address?.description"
id="setting-city"
@ -120,7 +149,7 @@ import { Address, IAddress } from "@/types/address.model";
import { useTimezones } from "@/composition/apollo/config";
import { useUserSettings } from "@/composition/apollo/user";
import { useHead } from "@vueuse/head";
import { computed, defineAsyncComponent } from "vue";
import { computed, defineAsyncComponent, ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import { useMutation } from "@vue/apollo-composable";
@ -140,6 +169,44 @@ useHead({
// 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({
get() {
if (loggedUser.value?.settings?.timezone) {

View file

@ -42,7 +42,7 @@
>
{{ error }}
</o-notification>
<form @submit="loginAction">
<form @submit="loginAction" v-if="config?.auth.databaseLogin">
<o-field
:label="t('Email')"
label-for="email"
@ -81,13 +81,6 @@
</p>
<!-- <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">
<o-button
tag="router-link"
@ -107,7 +100,7 @@
}"
>{{ t("Didn't receive the instructions?") }}</o-button
>
<p class="control" v-if="config && config.registrationsOpen">
<p class="control" v-if="canRegister">
<o-button
tag="router-link"
variant="text"
@ -123,6 +116,9 @@
</p>
</div>
</form>
<div v-if="config && config?.auth.oauthProviders.length > 0">
<auth-providers :oauthProviders="config.auth.oauthProviders" />
</div>
</section>
</template>
@ -162,11 +158,21 @@ const route = useRoute();
const { currentUser } = useCurrentUserClient();
const { result: configResult } = useQuery<{
config: Pick<IConfig, "auth" | "registrationsOpen">;
config: Pick<
IConfig,
"auth" | "registrationsOpen" | "registrationsAllowlist"
>;
}>(LOGIN_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 submitted = ref(false);

View file

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

View file

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

View file

@ -14,7 +14,9 @@ test("Login has everything we need", async ({ page }) => {
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(reAskInstructionsLink).toBeVisible();

View file

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

View file

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

View file

@ -305,6 +305,7 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do
"""
object :auth do
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")
end

View file

@ -7,6 +7,13 @@
<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()} />
<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 %>
<link rel="preload" href="/img/shape-1.svg" as="image" />
<link rel="preload" href="/img/shape-2.svg" as="image" />