Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 42 additions & 2 deletions src/Helpers/sources.php
Original file line number Diff line number Diff line change
Expand Up @@ -243,9 +243,12 @@ function get_modularous_impersonation_config()

$defaultInput = modularousConfig('default_input');

$isActive = $activeUser ? $activeUser->is_superadmin || $activeUser->isImpersonating() : false;
$isImpersonating = $activeUser ? $activeUser->isImpersonating() : false;

return [
'active' => $activeUser ? $activeUser->is_superadmin || $activeUser->isImpersonating() : false,
'impersonated' => $activeUser ? $activeUser->isImpersonating() : false,
'active' => $isActive,
'impersonated' => $isImpersonating,
'stopRoute' => route(Route::hasAdmin('impersonate.stop')),
'route' => route(Route::hasAdmin('impersonate'), ['id' => ':id']),

Expand All @@ -258,10 +261,47 @@ function get_modularous_impersonation_config()
'variant' => $defaultInput['variant'] ?? 'outlined',
'itemTitle' => 'email_with_company',
'searchKeys' => ['name', 'email', 'company.name'],
'recent' => ($isActive && ! $isImpersonating)
? get_modularous_recent_impersonations($userRepository)
: [],
];
}
}

if (! function_exists('get_modularous_recent_impersonations')) {
/**
* Hydrate the recently impersonated user ids stored in the session into a
* lightweight collection matching the shape returned by the impersonate
* search endpoint, preserving the stack order (newest first).
*
* @return array<int, array<string, mixed>>
*/
function get_modularity_recent_impersonations(UserRepository $userRepository): array
{
$ids = \array_values(\array_filter(
(array) session('impersonate_recent', []),
fn ($value) => \is_numeric($value)
));

if (empty($ids)) {
return [];
}

$users = $userRepository->getModel()
->whereIn('id', $ids)
->select(['id', 'name', 'email', 'company_id'])
->get()
->keyBy('id');

return \array_values(\array_filter(\array_map(
fn ($id) => $users->get((int) $id)?->only([
'id', 'name', 'email', 'company_id', 'company_name', 'email_with_company',
]),
$ids
)));
}
}

if (! function_exists('get_modularous_localization_config')) {
function get_modularous_localization_config()
{
Expand Down
31 changes: 31 additions & 0 deletions src/Http/Controllers/Utility/ImpersonateController.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,23 @@

use Illuminate\Auth\AuthManager;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Session;
use Modules\SystemUser\Repositories\UserRepository;
use Unusualify\Modularous\Facades\Modularous;
use Unusualify\Modularous\Http\Controllers\Controller;

class ImpersonateController extends Controller
{
/**
* Max number of recently impersonated users tracked in the session.
*/
protected const RECENT_IMPERSONATIONS_LIMIT = 5;

/**
* Session key holding the recent impersonation id stack (newest first).
*/
protected const RECENT_IMPERSONATIONS_KEY = 'impersonate_recent';

// /**
// * @var AuthManager
// */
Expand All @@ -31,11 +42,31 @@ public function impersonate($id, UserRepository $users)
if ($this->authManager->guard(Modularous::getAuthGuardName())->user()->can('impersonate')) {
$user = $users->getById($id);
$this->authManager->guard(Modularous::getAuthGuardName())->user()->setImpersonating($user->id);

$this->pushRecentImpersonation((int) $user->id);
}

return back();
}

/**
* Prepend the just-impersonated user id onto the recent stack, dedupe, cap.
*/
protected function pushRecentImpersonation(int $id): void
{
$recent = \array_values(\array_filter(
(array) Session::get(self::RECENT_IMPERSONATIONS_KEY, []),
fn ($value) => \is_numeric($value) && (int) $value !== $id
));

\array_unshift($recent, $id);

Session::put(
self::RECENT_IMPERSONATIONS_KEY,
\array_slice($recent, 0, self::RECENT_IMPERSONATIONS_LIMIT)
);
}

/**
* @return RedirectResponse
*/
Expand Down
7 changes: 7 additions & 0 deletions vue/src/js/components/ImpersonateToolbar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@
type: String,
default: null
},
recent: {
type: Array,
default () {
return []
}
},
itemTitle: {
type: String,
default: 'name'
Expand Down Expand Up @@ -96,6 +102,7 @@
:density="density"
:item-title="itemTitle"
:item-value="itemValue"
:recent-items="recent"
>
<template v-slot:activator="{ props }">
<v-list-item
Expand Down
50 changes: 50 additions & 0 deletions vue/src/js/components/inputs/Browser.vue
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@
preserveInitialValues: {
type: Boolean,
default: true
},
recentItems: {
type: Array,
default () {
return []
}
}
})

Expand Down Expand Up @@ -292,6 +298,19 @@
if (!elements.value.length) {
fetchInitialItems()
}

// Seed the list with "recent" shortcuts when nothing else is showing.
// Skipped if the consumer has a current modelValue (fetchInitialItems
// will populate the list) or if results are already loaded.
if (!input.value && !elements.value.length && props.recentItems.length) {
setElements([...props.recentItems])
}
}

const isRecentItem = (item) => {
const latest = props.recentItems[0]
if (!latest) return false
return latest[props.itemValue] === item[props.itemValue]
}

const itemIsSelected = (item) => {
Expand Down Expand Up @@ -414,6 +433,27 @@
}
})

// When the search box is cleared after a query, the previous results
// (or a "No items found" empty list) would otherwise stay visible until
// the modal was closed and reopened. Restore the dialog's default view
// — recent items, initial selection, or empty — as soon as the input
// goes back to blank. Scoped to consumers that opt into recentItems so
// existing usages keep their prior behavior untouched.
watch(searchModel, (newVal, oldVal) => {
if (!dialog.value) return
if (!props.recentItems.length) return
if (!oldVal || (newVal && newVal.trim())) return

setActivePage(1)
setActiveLastPage(-1)

if (input.value) {
fetchInitialItems()
} else {
setElements([...props.recentItems])
}
})

onMounted(() => {
fetchInitialItems()
})
Expand Down Expand Up @@ -500,6 +540,8 @@
v-bind="$lodash.pick(boundProps, ['color'])"
:variant="variant"
:density="density"
clearable
clear-icon="mdi-close"
@keyup.enter="performSearch"
@click:append-inner="performSearch"
append-inner-icon="mdi-magnify"
Expand Down Expand Up @@ -566,6 +608,14 @@
>
New
</v-chip>
<v-chip
v-if="isRecentItem(item) && !isInitialItem(item)"
color="blue-lighten-3"
size="x-small"
class="ml-2"
>
Recent
</v-chip>
</v-list-item-title>
</v-list-item>
</v-list>
Expand Down
Loading