Administration of users with pending - #877
This commit is contained in:
@@ -4,7 +4,7 @@ defmodule Mobilizon.GraphQL.Authorization do
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
use Rajska,
|
use Rajska,
|
||||||
valid_roles: [:user, :moderator, :administrator],
|
valid_roles: [:user, :moderator, :administrator, :pending],
|
||||||
super_role: :administrator,
|
super_role: :administrator,
|
||||||
default_rule: :default
|
default_rule: :default
|
||||||
|
|
||||||
|
|||||||
@@ -279,15 +279,24 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
|
|||||||
{:ok, %User{} = user} ->
|
{:ok, %User{} = user} ->
|
||||||
actor = Users.get_actor_for_user(user)
|
actor = Users.get_actor_for_user(user)
|
||||||
|
|
||||||
{:ok, %{access_token: access_token, refresh_token: refresh_token}} =
|
if Config.instance_registrations_moderation?() do
|
||||||
Authenticator.generate_tokens(user)
|
{:ok,
|
||||||
|
%{
|
||||||
|
access_token: "",
|
||||||
|
refresh_token: "",
|
||||||
|
user: Map.put(user, :default_actor, actor)
|
||||||
|
}}
|
||||||
|
else
|
||||||
|
{:ok, %{access_token: access_token, refresh_token: refresh_token}} =
|
||||||
|
Authenticator.generate_tokens(user)
|
||||||
|
|
||||||
{:ok,
|
{:ok,
|
||||||
%{
|
%{
|
||||||
access_token: access_token,
|
access_token: access_token,
|
||||||
refresh_token: refresh_token,
|
refresh_token: refresh_token,
|
||||||
user: Map.put(user, :default_actor, actor)
|
user: Map.put(user, :default_actor, actor)
|
||||||
}}
|
}}
|
||||||
|
end
|
||||||
|
|
||||||
{:error, :invalid_token} ->
|
{:error, :invalid_token} ->
|
||||||
Logger.info("Invalid token #{token} to validate user")
|
Logger.info("Invalid token #{token} to validate user")
|
||||||
|
|||||||
@@ -310,6 +310,7 @@ export const GET_USER = gql`
|
|||||||
user(id: $id) {
|
user(id: $id) {
|
||||||
id
|
id
|
||||||
email
|
email
|
||||||
|
moderation
|
||||||
confirmedAt
|
confirmedAt
|
||||||
confirmationSentAt
|
confirmationSentAt
|
||||||
lastSignInAt
|
lastSignInAt
|
||||||
|
|||||||
@@ -52,6 +52,8 @@
|
|||||||
user.role == ICurrentUserRole.MODERATOR,
|
user.role == ICurrentUserRole.MODERATOR,
|
||||||
'bg-blue-100 text-blue-800':
|
'bg-blue-100 text-blue-800':
|
||||||
user.role == ICurrentUserRole.USER,
|
user.role == ICurrentUserRole.USER,
|
||||||
|
'bg-orange-100 text-orange-800':
|
||||||
|
user.role == ICurrentUserRole.PENDING,
|
||||||
}"
|
}"
|
||||||
class="text-sm font-medium mr-2 px-2.5 py-0.5 rounded"
|
class="text-sm font-medium mr-2 px-2.5 py-0.5 rounded"
|
||||||
>
|
>
|
||||||
@@ -67,7 +69,10 @@
|
|||||||
>
|
>
|
||||||
<o-button
|
<o-button
|
||||||
size="small"
|
size="small"
|
||||||
v-if="!user.disabled"
|
v-if="
|
||||||
|
!user.disabled &&
|
||||||
|
user.role != ICurrentUserRole.PENDING
|
||||||
|
"
|
||||||
@click="isEmailChangeModalActive = true"
|
@click="isEmailChangeModalActive = true"
|
||||||
variant="text"
|
variant="text"
|
||||||
icon-left="pencil"
|
icon-left="pencil"
|
||||||
@@ -106,7 +111,10 @@
|
|||||||
>
|
>
|
||||||
<o-button
|
<o-button
|
||||||
size="small"
|
size="small"
|
||||||
v-if="!user.disabled"
|
v-if="
|
||||||
|
!user.disabled &&
|
||||||
|
user.role != ICurrentUserRole.PENDING
|
||||||
|
"
|
||||||
@click="isRoleChangeModalActive = true"
|
@click="isRoleChangeModalActive = true"
|
||||||
variant="text"
|
variant="text"
|
||||||
icon-left="chevron-double-up"
|
icon-left="chevron-double-up"
|
||||||
@@ -140,7 +148,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section class="my-4">
|
<section v-if="user.role != ICurrentUserRole.PENDING" class="my-4">
|
||||||
<h2 class="text-lg font-bold mb-3">{{ t("Profiles") }}</h2>
|
<h2 class="text-lg font-bold mb-3">{{ t("Profiles") }}</h2>
|
||||||
<div
|
<div
|
||||||
class="flex flex-wrap justify-center sm:justify-start gap-4"
|
class="flex flex-wrap justify-center sm:justify-start gap-4"
|
||||||
@@ -165,18 +173,31 @@
|
|||||||
</section>
|
</section>
|
||||||
<section class="my-4">
|
<section class="my-4">
|
||||||
<h2 class="text-lg font-bold mb-3">{{ t("Actions") }}</h2>
|
<h2 class="text-lg font-bold mb-3">{{ t("Actions") }}</h2>
|
||||||
<div class="buttons" v-if="!user.disabled">
|
<table>
|
||||||
<o-button @click="suspendAccount" variant="danger">{{
|
<tr>
|
||||||
t("Suspend")
|
<td v-if="user.role == ICurrentUserRole.PENDING">
|
||||||
}}</o-button>
|
<div class="buttons">
|
||||||
</div>
|
<o-button @click="acceptAccount" variant="success">{{
|
||||||
<div
|
t("Accept")
|
||||||
v-else
|
}}</o-button>
|
||||||
class="p-4 mb-4 text-sm text-red-700 bg-red-100 rounded-lg"
|
</div>
|
||||||
role="alert"
|
</td>
|
||||||
>
|
<td>
|
||||||
{{ t("The user has been disabled") }}
|
<div class="buttons" v-if="!user.disabled">
|
||||||
</div>
|
<o-button @click="suspendAccount" variant="danger">{{
|
||||||
|
t("Suspend")
|
||||||
|
}}</o-button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="p-4 mb-4 text-sm text-red-700 bg-red-100 rounded-lg"
|
||||||
|
role="alert"
|
||||||
|
>
|
||||||
|
{{ t("The user has been disabled") }}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
</section>
|
</section>
|
||||||
<o-modal
|
<o-modal
|
||||||
v-model:active="isEmailChangeModalActive"
|
v-model:active="isEmailChangeModalActive"
|
||||||
@@ -379,7 +400,7 @@ const newUser = reactive({
|
|||||||
const metadata = computed(
|
const metadata = computed(
|
||||||
(): Array<{ key: string; value: string; type?: string }> => {
|
(): Array<{ key: string; value: string; type?: string }> => {
|
||||||
if (!user.value) return [];
|
if (!user.value) return [];
|
||||||
return [
|
let fields = [
|
||||||
{
|
{
|
||||||
key: t("Email"),
|
key: t("Email"),
|
||||||
value: user.value.email,
|
value: user.value.email,
|
||||||
@@ -394,10 +415,19 @@ const metadata = computed(
|
|||||||
value: roleName(user.value.role),
|
value: roleName(user.value.role),
|
||||||
type: "role",
|
type: "role",
|
||||||
},
|
},
|
||||||
{
|
];
|
||||||
|
if (user.value.role == ICurrentUserRole.PENDING) {
|
||||||
|
fields.push({
|
||||||
|
key: t("Moderation"),
|
||||||
|
value: user.value.moderation,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
fields.push({
|
||||||
key: t("Login status"),
|
key: t("Login status"),
|
||||||
value: user.value.disabled ? t("Disabled") : t("Activated"),
|
value: user.value.disabled ? t("Disabled") : t("Activated"),
|
||||||
},
|
});
|
||||||
|
}
|
||||||
|
fields = fields.concat([
|
||||||
{
|
{
|
||||||
key: t("Confirmed"),
|
key: t("Confirmed"),
|
||||||
value: user.value.confirmedAt
|
value: user.value.confirmedAt
|
||||||
@@ -416,15 +446,20 @@ const metadata = computed(
|
|||||||
value: user.value.currentSignInIp || t("Unknown"),
|
value: user.value.currentSignInIp || t("Unknown"),
|
||||||
type: user.value.currentSignInIp ? "ip" : undefined,
|
type: user.value.currentSignInIp ? "ip" : undefined,
|
||||||
},
|
},
|
||||||
{
|
]);
|
||||||
key: t("Total number of participations"),
|
if (user.value.role != ICurrentUserRole.PENDING) {
|
||||||
value: user.value.participations.total.toString(),
|
fields = fields.concat([
|
||||||
},
|
{
|
||||||
{
|
key: t("Total number of participations"),
|
||||||
key: t("Uploaded media total size"),
|
value: user.value.participations.total.toString(),
|
||||||
value: formatBytes(user.value.mediaSize),
|
},
|
||||||
},
|
{
|
||||||
];
|
key: t("Uploaded media total size"),
|
||||||
|
value: formatBytes(user.value.mediaSize),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
return fields;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -434,6 +469,8 @@ const roleName = (role: ICurrentUserRole): string => {
|
|||||||
return t("Administrator");
|
return t("Administrator");
|
||||||
case ICurrentUserRole.MODERATOR:
|
case ICurrentUserRole.MODERATOR:
|
||||||
return t("Moderator");
|
return t("Moderator");
|
||||||
|
case ICurrentUserRole.PENDING:
|
||||||
|
return t("Pending");
|
||||||
case ICurrentUserRole.USER:
|
case ICurrentUserRole.USER:
|
||||||
default:
|
default:
|
||||||
return t("User");
|
return t("User");
|
||||||
@@ -467,6 +504,16 @@ const suspendAccount = async (): Promise<void> => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const acceptAccount = async () => {
|
||||||
|
isRoleChangeModalActive.value = false;
|
||||||
|
await updateUser({
|
||||||
|
id: props.id,
|
||||||
|
role: ICurrentUserRole.USER,
|
||||||
|
notify: true,
|
||||||
|
});
|
||||||
|
router.push({ name: RouteName.ADMIN_USER_PROFILE, id: props.id });
|
||||||
|
};
|
||||||
|
|
||||||
const profiles = computed((): IPerson[] | undefined => {
|
const profiles = computed((): IPerson[] | undefined => {
|
||||||
return user.value?.actors;
|
return user.value?.actors;
|
||||||
});
|
});
|
||||||
@@ -478,6 +525,7 @@ const confirmUser = async () => {
|
|||||||
confirmed: true,
|
confirmed: true,
|
||||||
notify: newUser.notify,
|
notify: newUser.notify,
|
||||||
});
|
});
|
||||||
|
router.push({ name: RouteName.ADMIN_USER_PROFILE, id: props.id });
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateUserRole = async () => {
|
const updateUserRole = async () => {
|
||||||
@@ -487,6 +535,7 @@ const updateUserRole = async () => {
|
|||||||
role: newUser.role,
|
role: newUser.role,
|
||||||
notify: newUser.notify,
|
notify: newUser.notify,
|
||||||
});
|
});
|
||||||
|
router.push({ name: RouteName.ADMIN_USER_PROFILE, id: props.id });
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateUserEmail = async () => {
|
const updateUserEmail = async () => {
|
||||||
@@ -496,6 +545,7 @@ const updateUserEmail = async () => {
|
|||||||
email: newUser.email,
|
email: newUser.email,
|
||||||
notify: newUser.notify,
|
notify: newUser.notify,
|
||||||
});
|
});
|
||||||
|
router.push({ name: RouteName.ADMIN_USER_PROFILE, id: props.id });
|
||||||
};
|
};
|
||||||
|
|
||||||
const { mutate: updateUser } = useMutation<
|
const { mutate: updateUser } = useMutation<
|
||||||
|
|||||||
@@ -679,6 +679,7 @@ defmodule Mobilizon.GraphQL.Resolvers.UserTest do
|
|||||||
|
|
||||||
assert json_response(res, 200)["data"]["validateUser"]["user"]["id"] == to_string(user.id)
|
assert json_response(res, 200)["data"]["validateUser"]["user"]["id"] == to_string(user.id)
|
||||||
assert json_response(res, 200)["data"]["validateUser"]["user"]["role"] == "USER"
|
assert json_response(res, 200)["data"]["validateUser"]["user"]["role"] == "USER"
|
||||||
|
assert json_response(res, 200)["data"]["validateUser"]["accessToken"] != ""
|
||||||
Config.put([:instance, :registrations_open], true)
|
Config.put([:instance, :registrations_open], true)
|
||||||
Config.put([:instance, :registrations_moderation], false)
|
Config.put([:instance, :registrations_moderation], false)
|
||||||
end
|
end
|
||||||
@@ -715,6 +716,7 @@ defmodule Mobilizon.GraphQL.Resolvers.UserTest do
|
|||||||
|
|
||||||
assert json_response(res, 200)["data"]["validateUser"]["user"]["id"] == to_string(user.id)
|
assert json_response(res, 200)["data"]["validateUser"]["user"]["id"] == to_string(user.id)
|
||||||
assert json_response(res, 200)["data"]["validateUser"]["user"]["role"] == "PENDING"
|
assert json_response(res, 200)["data"]["validateUser"]["user"]["role"] == "PENDING"
|
||||||
|
assert json_response(res, 200)["data"]["validateUser"]["accessToken"] == ""
|
||||||
Config.put([:instance, :registrations_open], true)
|
Config.put([:instance, :registrations_open], true)
|
||||||
Config.put([:instance, :registrations_moderation], false)
|
Config.put([:instance, :registrations_moderation], false)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -68,6 +68,12 @@ export function defaultResolvers(
|
|||||||
} satisfies Resolvers;
|
} satisfies Resolvers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function htmlRemoveId(htmlText: string) {
|
||||||
|
return htmlText
|
||||||
|
.replaceAll(/ id="[a-z0-9]+" /gi, ' id="" ')
|
||||||
|
.replaceAll(/ for="[a-z0-9]+"/gi, ' for=""');
|
||||||
|
}
|
||||||
|
|
||||||
export const nullMock = {
|
export const nullMock = {
|
||||||
data: {},
|
data: {},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,89 @@
|
|||||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
|
exports[`UsersView > Show moderate list 1`] = `
|
||||||
|
"<div>
|
||||||
|
<breadcrumbs-nav links="[object Object],[object Object],[object Object]"></breadcrumbs-nav>
|
||||||
|
<section>
|
||||||
|
<h2 class="text-lg font-bold mb-3">Details</h2>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<div class="inline-block py-2 min-w-full sm:px-2">
|
||||||
|
<div class="overflow-hidden shadow-md sm:rounded-lg">
|
||||||
|
<table class="table w-full">
|
||||||
|
<tbody>
|
||||||
|
<tr class="border-b">
|
||||||
|
<td class="py-4 px-2 whitespace-nowrap align-middle">Email</td>
|
||||||
|
<td class="py-4 px-2 align-middle">truc@mobilizon.test</td>
|
||||||
|
<td class="py-4 px-2 whitespace-nowrap flex flex flex-col items-start gap-2">
|
||||||
|
<!--v-if-->
|
||||||
|
<o-button-stub tag="router-link" variant="text" size="small" iconleft="magnify" rounded="false" expanded="false" disabled="false" outlined="false" loading="false" inverted="false" nativetype="button" role="button" iconboth="false" to="[object Object]"></o-button-stub>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="border-b">
|
||||||
|
<td class="py-4 px-2 whitespace-nowrap align-middle">Language</td>
|
||||||
|
<td class="py-4 px-2 align-middle">French</td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
<tr class="border-b">
|
||||||
|
<td class="py-4 px-2 whitespace-nowrap align-middle">Role</td>
|
||||||
|
<td class="py-4 px-2 whitespace-nowrap"><span class="bg-orange-100 text-orange-800 text-sm font-medium mr-2 px-2.5 py-0.5 rounded">Pending</span></td>
|
||||||
|
<td class="py-4 px-2 whitespace-nowrap flex items-center">
|
||||||
|
<!--v-if-->
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="border-b">
|
||||||
|
<td class="py-4 px-2 whitespace-nowrap align-middle">Moderation</td>
|
||||||
|
<td class="py-4 px-2 align-middle">moderation text</td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
<tr class="border-b">
|
||||||
|
<td class="py-4 px-2 whitespace-nowrap align-middle">Confirmed</td>
|
||||||
|
<td class="py-4 px-2 align-middle">Saturday, August 30, 2025 at 11:56 AM</td>
|
||||||
|
<td class="py-4 px-2 whitespace-nowrap flex items-center">
|
||||||
|
<!--v-if-->
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="border-b">
|
||||||
|
<td class="py-4 px-2 whitespace-nowrap align-middle">Last sign-in</td>
|
||||||
|
<td class="py-4 px-2 align-middle">Unknown</td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
<tr class="border-b">
|
||||||
|
<td class="py-4 px-2 whitespace-nowrap align-middle">Last IP adress</td>
|
||||||
|
<td class="py-4 px-2 align-middle">Unknown</td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<!--v-if-->
|
||||||
|
<section class="my-4">
|
||||||
|
<h2 class="text-lg font-bold mb-3">Actions</h2>
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div class="buttons">
|
||||||
|
<o-button-stub tag="button" variant="success" rounded="false" expanded="false" disabled="false" outlined="false" loading="false" inverted="false" nativetype="button" role="button" iconboth="false"></o-button-stub>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="buttons">
|
||||||
|
<o-button-stub tag="button" variant="danger" rounded="false" expanded="false" disabled="false" outlined="false" loading="false" inverted="false" nativetype="button" role="button" iconboth="false"></o-button-stub>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
<o-modal-stub active="false" fullscreen="false" width="960" animation="zoom-out" cancelable="escape,x,outside,button" scroll="keep" trapfocus="true" ariarole="dialog" aria-label="Edit user email" destroyonhide="false" autofocus="true" closeicon="close" closeiconsize="medium" teleport="false" events="[object Object]" container="body" close-button-aria-label="Close" aria-modal=""></o-modal-stub>
|
||||||
|
<o-modal-stub active="false" fullscreen="false" width="960" animation="zoom-out" cancelable="escape,x,outside,button" scroll="keep" trapfocus="true" ariarole="dialog" aria-label="Edit user email" destroyonhide="false" autofocus="true" closeicon="close" closeiconsize="medium" teleport="false" events="[object Object]" container="body" has-modal-card="" close-button-aria-label="Close" aria-modal=""></o-modal-stub>
|
||||||
|
<o-modal-stub active="false" fullscreen="false" width="960" animation="zoom-out" cancelable="escape,x,outside,button" scroll="keep" trapfocus="true" ariarole="dialog" aria-label="Edit user email" destroyonhide="false" autofocus="true" closeicon="close" closeiconsize="medium" teleport="false" events="[object Object]" container="body" has-modal-card="" close-button-aria-label="Close" aria-modal=""></o-modal-stub>
|
||||||
|
</div>"
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`UsersView > Show simple list 1`] = `
|
exports[`UsersView > Show simple list 1`] = `
|
||||||
"<div>
|
"<div>
|
||||||
<breadcrumbs-nav links="[object Object],[object Object],[object Object]"></breadcrumbs-nav>
|
<breadcrumbs-nav links="[object Object],[object Object],[object Object]"></breadcrumbs-nav>
|
||||||
@@ -78,9 +162,16 @@ exports[`UsersView > Show simple list 1`] = `
|
|||||||
</section>
|
</section>
|
||||||
<section class="my-4">
|
<section class="my-4">
|
||||||
<h2 class="text-lg font-bold mb-3">Actions</h2>
|
<h2 class="text-lg font-bold mb-3">Actions</h2>
|
||||||
<div class="buttons">
|
<table>
|
||||||
<o-button-stub tag="button" variant="danger" rounded="false" expanded="false" disabled="false" outlined="false" loading="false" inverted="false" nativetype="button" role="button" iconboth="false"></o-button-stub>
|
<tr>
|
||||||
</div>
|
<!--v-if-->
|
||||||
|
<td>
|
||||||
|
<div class="buttons">
|
||||||
|
<o-button-stub tag="button" variant="danger" rounded="false" expanded="false" disabled="false" outlined="false" loading="false" inverted="false" nativetype="button" role="button" iconboth="false"></o-button-stub>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
</section>
|
</section>
|
||||||
<o-modal-stub active="false" fullscreen="false" width="960" animation="zoom-out" cancelable="escape,x,outside,button" scroll="keep" trapfocus="true" ariarole="dialog" aria-label="Edit user email" destroyonhide="false" autofocus="true" closeicon="close" closeiconsize="medium" teleport="false" events="[object Object]" container="body" close-button-aria-label="Close" aria-modal=""></o-modal-stub>
|
<o-modal-stub active="false" fullscreen="false" width="960" animation="zoom-out" cancelable="escape,x,outside,button" scroll="keep" trapfocus="true" ariarole="dialog" aria-label="Edit user email" destroyonhide="false" autofocus="true" closeicon="close" closeiconsize="medium" teleport="false" events="[object Object]" container="body" close-button-aria-label="Close" aria-modal=""></o-modal-stub>
|
||||||
<o-modal-stub active="false" fullscreen="false" width="960" animation="zoom-out" cancelable="escape,x,outside,button" scroll="keep" trapfocus="true" ariarole="dialog" aria-label="Edit user email" destroyonhide="false" autofocus="true" closeicon="close" closeiconsize="medium" teleport="false" events="[object Object]" container="body" has-modal-card="" close-button-aria-label="Close" aria-modal=""></o-modal-stub>
|
<o-modal-stub active="false" fullscreen="false" width="960" animation="zoom-out" cancelable="escape,x,outside,button" scroll="keep" trapfocus="true" ariarole="dialog" aria-label="Edit user email" destroyonhide="false" autofocus="true" closeicon="close" closeiconsize="medium" teleport="false" events="[object Object]" container="body" has-modal-card="" close-button-aria-label="Close" aria-modal=""></o-modal-stub>
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
|
exports[`UsersView > Show simple list 1`] = `
|
||||||
|
"<div data-v-01123d3b="">
|
||||||
|
<breadcrumbs-nav data-v-01123d3b="" links="[object Object],[object Object]"></breadcrumbs-nav>
|
||||||
|
<div data-v-01123d3b="">
|
||||||
|
<form data-v-01123d3b="">
|
||||||
|
<div data-v-01123d3b="" data-oruga="field" class="o-field mb-5">
|
||||||
|
<!---->
|
||||||
|
<div class="o-field__body">
|
||||||
|
<div class="o-field o-field--grouped-multiline o-field--grouped">
|
||||||
|
<div data-v-01123d3b="" data-oruga="field" class="o-field" expanded=""><label class="o-field__label" for="">Email</label>
|
||||||
|
<div class="o-field__body">
|
||||||
|
<div class="o-field o-field--addons">
|
||||||
|
<div data-v-01123d3b="" data-oruga="input" class="o-input__wrapper"><input trap-focus="" id="" data-oruga-input="text" type="text" class="o-input o-input--iconspace-left" autocomplete="off"><span class="o-icon o-input__icon-left" data-oruga="icon"><i class="mdi mdi-email mdi-24px"></i></span>
|
||||||
|
<!---->
|
||||||
|
<!---->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!---->
|
||||||
|
</div>
|
||||||
|
<div data-v-01123d3b="" data-oruga="field" class="o-field" expanded=""><label class="o-field__label" for="">IP Address</label>
|
||||||
|
<div class="o-field__body">
|
||||||
|
<div class="o-field o-field--addons">
|
||||||
|
<div data-v-01123d3b="" data-oruga="input" class="o-input__wrapper"><input id="" data-oruga-input="text" type="text" class="o-input o-input--iconspace-left" autocomplete="off"><span class="o-icon o-input__icon-left" data-oruga="icon"><i class="mdi mdi-web mdi-24px"></i></span>
|
||||||
|
<!---->
|
||||||
|
<!---->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!---->
|
||||||
|
</div>
|
||||||
|
<p data-v-01123d3b="" class="control self-end mb-0"><button data-v-01123d3b="" type="submit" class="o-btn o-btn--primary" role="button" data-oruga="button"><span class="o-btn__wrapper"><!----><span class="o-btn__label">Filter</span>
|
||||||
|
<!----></span>
|
||||||
|
</button></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!---->
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div data-v-01123d3b="" class="o-table__root" data-oruga="table">
|
||||||
|
<div style="display: none;"><span data-v-01123d3b="" data-id="1" data-oruga="table-column"> <!----></span><span data-v-01123d3b="" data-id="2" data-oruga="table-column">Email <!----></span><span data-v-01123d3b="" data-id="3" data-oruga="table-column" centered="true">Last seen on <!----></span><span data-v-01123d3b="" data-id="4" data-oruga="table-column" centered="true">Language <!----></span></div>
|
||||||
|
<!---->
|
||||||
|
<!---->
|
||||||
|
<div class="o-table__wrapper">
|
||||||
|
<table class="o-table">
|
||||||
|
<!---->
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<!---->
|
||||||
|
<!---->
|
||||||
|
<th class="o-table__th" style="width: 40px;" draggable="false"><span> <span class="o-table__th__sort-icon" style="display: none;"><span class="o-icon o-icon--small" data-oruga="icon"><i class="mdi mdi-arrow-up"></i></span></span></span></th>
|
||||||
|
<th class="o-table__th" draggable="false"><span>Email <span class="o-table__th__sort-icon" style="display: none;"><span class="o-icon o-icon--small" data-oruga="icon"><i class="mdi mdi-arrow-up"></i></span></span></span></th>
|
||||||
|
<th class="o-table__th" draggable="false"><span>Last seen on <span class="o-table__th__sort-icon" style="display: none;"><span class="o-icon o-icon--small" data-oruga="icon"><i class="mdi mdi-arrow-up"></i></span></span></span></th>
|
||||||
|
<th class="o-table__th" draggable="false"><span>Language <span class="o-table__th__sort-icon" style="display: none;"><span class="o-icon o-icon--small" data-oruga="icon"><i class="mdi mdi-arrow-up"></i></span></span></span></th>
|
||||||
|
<!---->
|
||||||
|
</tr>
|
||||||
|
<!---->
|
||||||
|
<!---->
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr class="" draggable="false">
|
||||||
|
<!---->
|
||||||
|
<!---->
|
||||||
|
<td class="o-table__td" style="width: 40px;">6</td>
|
||||||
|
<td class="o-table__td" data-label="Email"><a data-v-01123d3b="" href="/settings/admin/users/6" class="">truc@mobilizon.test</a></td>
|
||||||
|
<td class="o-table__td" data-label="Last seen on"><time data-v-01123d3b="">Thursday, January 1, 1970 at 1:00 AM</time></td>
|
||||||
|
<td class="o-table__td" data-label="Language">English</td>
|
||||||
|
<!---->
|
||||||
|
</tr>
|
||||||
|
<transition-stub name="slide" appear="false" persisted="false" css="true">
|
||||||
|
<!---->
|
||||||
|
</transition-stub>
|
||||||
|
<!---->
|
||||||
|
<tr class="" draggable="false">
|
||||||
|
<!---->
|
||||||
|
<!---->
|
||||||
|
<td class="o-table__td" style="width: 40px;">1</td>
|
||||||
|
<td class="o-table__td" data-label="Email"><a data-v-01123d3b="" href="/settings/admin/users/1" class="">admin@mobilizon.test</a></td>
|
||||||
|
<td class="o-table__td" data-label="Last seen on"><time data-v-01123d3b="" datetime="2025-09-11T16:10:03Z">Thursday, September 11, 2025 at 6:10 PM</time></td>
|
||||||
|
<td class="o-table__td" data-label="Language">English</td>
|
||||||
|
<!---->
|
||||||
|
</tr>
|
||||||
|
<transition-stub name="slide" appear="false" persisted="false" css="true">
|
||||||
|
<!---->
|
||||||
|
</transition-stub>
|
||||||
|
<!---->
|
||||||
|
<!---->
|
||||||
|
</tbody>
|
||||||
|
<!---->
|
||||||
|
</table>
|
||||||
|
<transition-stub name="fade" appear="false" persisted="false" css="true">
|
||||||
|
<!---->
|
||||||
|
</transition-stub>
|
||||||
|
</div>
|
||||||
|
<div class="o-table__pagination" per-page="10" total="2" rounded="false" size="small" simple="false" aria-next-label="Next page" aria-previous-label="Previous page" aria-page-label="Page" aria-current-label="Current page">
|
||||||
|
<div></div>
|
||||||
|
<div>
|
||||||
|
<nav class="o-pag o-pag--right o-pag--small" data-oruga="pagination"><button role="button" tabindex="0" class="o-pag__link o-pag__previous o-pag__link--disabled" aria-label="Previous page" aria-current="false"><span class="o-icon" data-oruga="icon" aria-hidden="true"><i class="mdi mdi-chevron-left mdi-24px"></i></span></button><button role="button" tabindex="0" class="o-pag__link o-pag__next o-pag__link--disabled" aria-label="Next page" aria-current="false"><span class="o-icon" data-oruga="icon" aria-hidden="true"><i class="mdi mdi-chevron-right mdi-24px"></i></span></button>
|
||||||
|
<ul class="o-pag__list">
|
||||||
|
<!---->
|
||||||
|
<!---->
|
||||||
|
<li class="o-pag__item"><button role="button" tabindex="0" class="o-pag__link o-pag__link--current" aria-label="Current page, Page 1." aria-current="true">1</button></li>
|
||||||
|
<!---->
|
||||||
|
<!---->
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>"
|
||||||
|
`;
|
||||||
@@ -11,12 +11,15 @@ import {
|
|||||||
} from "mock-apollo-client";
|
} from "mock-apollo-client";
|
||||||
import AdminUserProfile from "@/views/Admin/AdminUserProfile.vue";
|
import AdminUserProfile from "@/views/Admin/AdminUserProfile.vue";
|
||||||
import { GET_USER } from "@/graphql/user";
|
import { GET_USER } from "@/graphql/user";
|
||||||
import { LANGUAGES_CODES } from "@/graphql/admin";
|
import { ADMIN_UPDATE_USER, LANGUAGES_CODES } from "@/graphql/admin";
|
||||||
import { createRouter, createWebHistory, Router } from "vue-router";
|
|
||||||
import { routes } from "@/router";
|
|
||||||
import { Oruga } from "@oruga-ui/oruga-next";
|
import { Oruga } from "@oruga-ui/oruga-next";
|
||||||
|
import { nullMock } from "../../common";
|
||||||
let router: Router;
|
import {
|
||||||
|
createRouterMock,
|
||||||
|
injectRouterMock,
|
||||||
|
VueRouterMock,
|
||||||
|
} from "vue-router-mock";
|
||||||
|
import { SettingsRouteName } from "@/router/settings";
|
||||||
|
|
||||||
let mockClient: MockApolloClient | null;
|
let mockClient: MockApolloClient | null;
|
||||||
let requestHandlers: Record<string, RequestHandler>;
|
let requestHandlers: Record<string, RequestHandler>;
|
||||||
@@ -38,7 +41,7 @@ const languageCodeMock = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const getUsersMock = {
|
const getUserMock = {
|
||||||
data: {
|
data: {
|
||||||
user: {
|
user: {
|
||||||
__typename: "User",
|
__typename: "User",
|
||||||
@@ -55,6 +58,7 @@ const getUsersMock = {
|
|||||||
url: "http://mobilizon.test/@truc",
|
url: "http://mobilizon.test/@truc",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
moderation: "moderation text",
|
||||||
confirmedAt: "2025-08-30T09:56:59Z",
|
confirmedAt: "2025-08-30T09:56:59Z",
|
||||||
currentSignInAt: null,
|
currentSignInAt: null,
|
||||||
currentSignInIp: null,
|
currentSignInIp: null,
|
||||||
@@ -74,50 +78,126 @@ const getUsersMock = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
config.global.plugins.push(Oruga);
|
const getModerateMock = {
|
||||||
|
data: {
|
||||||
const generateWrapper = () => {
|
user: {
|
||||||
mockClient = createMockClient({
|
__typename: "User",
|
||||||
cache,
|
actors: [
|
||||||
resolvers: buildCurrentUserResolver(cache),
|
{
|
||||||
});
|
__typename: "Person",
|
||||||
requestHandlers = {
|
avatar: null,
|
||||||
languagecode: vi.fn().mockResolvedValue(languageCodeMock),
|
domain: null,
|
||||||
get_users: vi.fn().mockResolvedValue(getUsersMock),
|
id: "11371",
|
||||||
};
|
name: "Truc",
|
||||||
mockClient.setRequestHandler(LANGUAGES_CODES, requestHandlers.languagecode);
|
preferredUsername: "truc",
|
||||||
mockClient.setRequestHandler(GET_USER, requestHandlers.get_users);
|
summary: null,
|
||||||
|
type: "PERSON",
|
||||||
const wrapper = shallowMount(AdminUserProfile, {
|
url: "http://mobilizon.test/@truc",
|
||||||
props: { id: "1234" },
|
},
|
||||||
stubs: ["router-link", "router-view"],
|
],
|
||||||
global: {
|
moderation: "moderation text",
|
||||||
provide: {
|
confirmedAt: "2025-08-30T09:56:59Z",
|
||||||
[DefaultApolloClient]: mockClient,
|
currentSignInAt: null,
|
||||||
|
currentSignInIp: null,
|
||||||
|
disabled: false,
|
||||||
|
email: "truc@mobilizon.test",
|
||||||
|
id: "1234",
|
||||||
|
lastSignInAt: "2025-08-28T12:33:03Z",
|
||||||
|
lastSignInIp: "176.171.166.30",
|
||||||
|
locale: "fr",
|
||||||
|
mediaSize: 7093555,
|
||||||
|
participations: {
|
||||||
|
__typename: "PaginatedParticipantList",
|
||||||
|
total: 14,
|
||||||
},
|
},
|
||||||
plugins: [router],
|
role: "PENDING",
|
||||||
},
|
},
|
||||||
});
|
},
|
||||||
return wrapper;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
describe("UsersView", () => {
|
config.global.plugins.push(Oruga);
|
||||||
beforeEach(async () => {
|
config.plugins.VueWrapper.install(VueRouterMock);
|
||||||
router = createRouter({
|
|
||||||
history: createWebHistory(),
|
|
||||||
routes: routes,
|
|
||||||
});
|
|
||||||
|
|
||||||
// await router.isReady();
|
describe("UsersView", () => {
|
||||||
|
const router = createRouterMock({
|
||||||
|
spy: {
|
||||||
|
create: (fn) => vi.fn(fn),
|
||||||
|
reset: (spy) => spy.mockReset(),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
beforeEach(async () => {
|
||||||
|
// await router.isReady();
|
||||||
|
injectRouterMock(router);
|
||||||
|
});
|
||||||
|
|
||||||
|
const generateWrapper = (currentUserMock = getUserMock) => {
|
||||||
|
mockClient = createMockClient({
|
||||||
|
cache,
|
||||||
|
resolvers: buildCurrentUserResolver(cache),
|
||||||
|
});
|
||||||
|
requestHandlers = {
|
||||||
|
languagecode: vi.fn().mockResolvedValue(languageCodeMock),
|
||||||
|
get_users: vi.fn().mockResolvedValue(currentUserMock),
|
||||||
|
update_user: vi.fn().mockResolvedValue(nullMock),
|
||||||
|
};
|
||||||
|
mockClient.setRequestHandler(LANGUAGES_CODES, requestHandlers.languagecode);
|
||||||
|
mockClient.setRequestHandler(GET_USER, requestHandlers.get_users);
|
||||||
|
mockClient.setRequestHandler(
|
||||||
|
ADMIN_UPDATE_USER,
|
||||||
|
requestHandlers.update_user
|
||||||
|
);
|
||||||
|
|
||||||
|
const wrapper = shallowMount(AdminUserProfile, {
|
||||||
|
props: { id: "1234" },
|
||||||
|
stubs: ["router-link", "router-view"],
|
||||||
|
global: {
|
||||||
|
provide: {
|
||||||
|
[DefaultApolloClient]: mockClient,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return wrapper;
|
||||||
|
};
|
||||||
|
|
||||||
it("Show simple list", async () => {
|
it("Show simple list", async () => {
|
||||||
const wrapper = generateWrapper();
|
const wrapper = generateWrapper();
|
||||||
await wrapper.vm.$nextTick();
|
await wrapper.vm.$nextTick();
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
expect(wrapper.exists()).toBe(true);
|
expect(wrapper.exists()).toBe(true);
|
||||||
expect(requestHandlers.languagecode).toHaveBeenCalled();
|
|
||||||
expect(requestHandlers.get_users).toHaveBeenCalled();
|
|
||||||
expect(wrapper.html()).toMatchSnapshot();
|
expect(wrapper.html()).toMatchSnapshot();
|
||||||
|
expect(requestHandlers.languagecode).toHaveBeenCalledTimes(1);
|
||||||
|
expect(requestHandlers.get_users).toHaveBeenCalledTimes(1);
|
||||||
|
expect(requestHandlers.update_user).toHaveBeenCalledTimes(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Show moderate list", async () => {
|
||||||
|
const wrapper = generateWrapper(getModerateMock);
|
||||||
|
expect(wrapper.router).toBe(router);
|
||||||
|
wrapper.router.push.mockReset();
|
||||||
|
await wrapper.vm.$nextTick();
|
||||||
|
await flushPromises();
|
||||||
|
expect(wrapper.exists()).toBe(true);
|
||||||
|
expect(wrapper.html()).toMatchSnapshot();
|
||||||
|
expect(requestHandlers.languagecode).toHaveBeenCalledTimes(0);
|
||||||
|
expect(requestHandlers.get_users).toHaveBeenCalledTimes(1);
|
||||||
|
expect(requestHandlers.update_user).toHaveBeenCalledTimes(0);
|
||||||
|
const btn = wrapper.find('o-button-stub[variant="success"]');
|
||||||
|
expect(btn.exists()).toBe(true);
|
||||||
|
btn.trigger("click");
|
||||||
|
await flushPromises();
|
||||||
|
expect(requestHandlers.languagecode).toHaveBeenCalledTimes(0);
|
||||||
|
expect(requestHandlers.get_users).toHaveBeenCalledTimes(1);
|
||||||
|
expect(requestHandlers.update_user).toHaveBeenCalledTimes(1);
|
||||||
|
expect(requestHandlers.update_user).toHaveBeenCalledWith({
|
||||||
|
id: "1234",
|
||||||
|
notify: true,
|
||||||
|
role: "USER",
|
||||||
|
});
|
||||||
|
await flushPromises();
|
||||||
|
await flushPromises();
|
||||||
|
expect(wrapper.router.push).toHaveBeenCalledWith({
|
||||||
|
name: SettingsRouteName.ADMIN_USER_PROFILE,
|
||||||
|
id: "1234",
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { LANGUAGES_CODES } from "@/graphql/admin";
|
|||||||
import { createRouter, createWebHistory, Router } from "vue-router";
|
import { createRouter, createWebHistory, Router } from "vue-router";
|
||||||
import { routes } from "@/router";
|
import { routes } from "@/router";
|
||||||
import { Oruga } from "@oruga-ui/oruga-next";
|
import { Oruga } from "@oruga-ui/oruga-next";
|
||||||
|
import { htmlRemoveId } from "../../common";
|
||||||
|
|
||||||
let router: Router;
|
let router: Router;
|
||||||
|
|
||||||
@@ -102,14 +103,14 @@ const listUsersMock = {
|
|||||||
|
|
||||||
config.global.plugins.push(Oruga);
|
config.global.plugins.push(Oruga);
|
||||||
|
|
||||||
const generateWrapper = () => {
|
const generateWrapper = (currentUsersMock = listUsersMock) => {
|
||||||
mockClient = createMockClient({
|
mockClient = createMockClient({
|
||||||
cache,
|
cache,
|
||||||
resolvers: buildCurrentUserResolver(cache),
|
resolvers: buildCurrentUserResolver(cache),
|
||||||
});
|
});
|
||||||
requestHandlers = {
|
requestHandlers = {
|
||||||
languagecode: vi.fn().mockResolvedValue(languageCodeMock),
|
languagecode: vi.fn().mockResolvedValue(languageCodeMock),
|
||||||
list_users: vi.fn().mockResolvedValue(listUsersMock),
|
list_users: vi.fn().mockResolvedValue(currentUsersMock),
|
||||||
};
|
};
|
||||||
mockClient.setRequestHandler(LANGUAGES_CODES, requestHandlers.languagecode);
|
mockClient.setRequestHandler(LANGUAGES_CODES, requestHandlers.languagecode);
|
||||||
mockClient.setRequestHandler(LIST_USERS, requestHandlers.list_users);
|
mockClient.setRequestHandler(LIST_USERS, requestHandlers.list_users);
|
||||||
@@ -144,5 +145,6 @@ describe("UsersView", () => {
|
|||||||
expect(wrapper.exists()).toBe(true);
|
expect(wrapper.exists()).toBe(true);
|
||||||
expect(requestHandlers.languagecode).toHaveBeenCalled();
|
expect(requestHandlers.languagecode).toHaveBeenCalled();
|
||||||
expect(requestHandlers.list_users).toHaveBeenCalled();
|
expect(requestHandlers.list_users).toHaveBeenCalled();
|
||||||
|
expect(htmlRemoveId(wrapper.html())).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user