kopia lustrzana https://gitlab.com/soapbox-pub/soapbox
Add useAdminAccount hook, refactor admin accounts in dashboard, refactor admin actions
rodzic
dde5e9154b
commit
c0f8c9d5e7
|
@ -173,16 +173,12 @@ function fetchUsers(filters: string[] = [], page = 1, query?: string | null, pag
|
|||
|
||||
try {
|
||||
const { data: accounts, ...response } = await api(getState).get(url || '/api/v1/admin/accounts', { params });
|
||||
const next = getLinks(response as AxiosResponse<any, any>).refs.find(link => link.rel === 'next');
|
||||
|
||||
const count = next
|
||||
? page * pageSize + 1
|
||||
: (page - 1) * pageSize + accounts.length;
|
||||
const next = getLinks(response as AxiosResponse<any, any>).refs.find(link => link.rel === 'next')?.uri;
|
||||
|
||||
dispatch(importFetchedAccounts(accounts.map(({ account }: APIEntity) => account)));
|
||||
dispatch(fetchRelationships(accounts.map((account_1: APIEntity) => account_1.id)));
|
||||
dispatch({ type: ADMIN_USERS_FETCH_SUCCESS, users: accounts, count, pageSize, filters, page, next: next?.uri || false });
|
||||
return { users: accounts, count, pageSize, next: next?.uri || false };
|
||||
dispatch({ type: ADMIN_USERS_FETCH_SUCCESS, accounts, pageSize, filters, page, next });
|
||||
return { accounts, next };
|
||||
} catch (error) {
|
||||
return dispatch({ type: ADMIN_USERS_FETCH_FAIL, error, filters, page, pageSize });
|
||||
}
|
||||
|
@ -401,13 +397,13 @@ const fetchUserIndex = () =>
|
|||
|
||||
dispatch({ type: ADMIN_USER_INDEX_FETCH_REQUEST });
|
||||
|
||||
dispatch(fetchUsers(filters.toJS() as string[], page + 1, query, pageSize))
|
||||
dispatch(fetchUsers([...filters], page + 1, query, pageSize))
|
||||
.then((data: any) => {
|
||||
if (data.error) {
|
||||
dispatch({ type: ADMIN_USER_INDEX_FETCH_FAIL });
|
||||
} else {
|
||||
const { users, count, next } = (data);
|
||||
dispatch({ type: ADMIN_USER_INDEX_FETCH_SUCCESS, users, count, next });
|
||||
const { accounts, next } = data;
|
||||
dispatch({ type: ADMIN_USER_INDEX_FETCH_SUCCESS, accounts, next });
|
||||
}
|
||||
}).catch(() => {
|
||||
dispatch({ type: ADMIN_USER_INDEX_FETCH_FAIL });
|
||||
|
@ -422,13 +418,13 @@ const expandUserIndex = () =>
|
|||
|
||||
dispatch({ type: ADMIN_USER_INDEX_EXPAND_REQUEST });
|
||||
|
||||
dispatch(fetchUsers(filters.toJS() as string[], page + 1, query, pageSize, next))
|
||||
.then((data: any) => {
|
||||
if (data.error) {
|
||||
dispatch(fetchUsers([...filters], page + 1, query, pageSize, next))
|
||||
.then((data) => {
|
||||
if ('error' in data) {
|
||||
dispatch({ type: ADMIN_USER_INDEX_EXPAND_FAIL });
|
||||
} else {
|
||||
const { users, count, next } = (data);
|
||||
dispatch({ type: ADMIN_USER_INDEX_EXPAND_SUCCESS, users, count, next });
|
||||
const { accounts, next } = data;
|
||||
dispatch({ type: ADMIN_USER_INDEX_EXPAND_SUCCESS, accounts, next });
|
||||
}
|
||||
}).catch(() => {
|
||||
dispatch({ type: ADMIN_USER_INDEX_EXPAND_FAIL });
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { HTTPError } from './HTTPError';
|
||||
|
||||
interface Opts {
|
||||
searchParams?: Record<string, string | number | boolean>;
|
||||
searchParams?: URLSearchParams | Record<string, string | number | boolean>;
|
||||
headers?: Record<string, string>;
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
|
@ -51,9 +51,11 @@ export class MastodonClient {
|
|||
const url = new URL(path, this.baseUrl);
|
||||
|
||||
if (opts.searchParams) {
|
||||
const params = Object
|
||||
.entries(opts.searchParams)
|
||||
.map(([key, value]) => ([key, String(value)]));
|
||||
const params = opts.searchParams instanceof URLSearchParams
|
||||
? opts.searchParams
|
||||
: Object
|
||||
.entries(opts.searchParams)
|
||||
.map(([key, value]) => ([key, String(value)]));
|
||||
|
||||
url.search = new URLSearchParams(params).toString();
|
||||
}
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { useEntities } from 'soapbox/entity-store/hooks';
|
||||
import { useApi } from 'soapbox/hooks';
|
||||
import { adminAccountSchema } from 'soapbox/schemas/admin-account';
|
||||
|
||||
type Filter = 'local' | 'remote' | 'active' | 'pending' | 'disabled' | 'silenced' | 'suspended' | 'sensitized';
|
||||
|
||||
/** https://docs.joinmastodon.org/methods/admin/accounts/#v1 */
|
||||
export function useAdminAccounts(filters: Filter[] = [], limit?: number) {
|
||||
const api = useApi();
|
||||
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
for (const filter of filters) {
|
||||
searchParams.append(filter, 'true');
|
||||
}
|
||||
|
||||
if (typeof limit === 'number') {
|
||||
searchParams.append('limit', limit.toString());
|
||||
}
|
||||
|
||||
const { entities, ...rest } = useEntities(
|
||||
[Entities.ACCOUNTS, searchParams.toString()],
|
||||
() => api.get('/api/v1/admin/accounts', { searchParams }),
|
||||
{ schema: adminAccountSchema.transform(({ account }) => account) },
|
||||
);
|
||||
|
||||
return { accounts: entities, ...rest };
|
||||
}
|
|
@ -1,16 +1,13 @@
|
|||
import { OrderedSet as ImmutableOrderedSet } from 'immutable';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
import { fetchUsers } from 'soapbox/actions/admin';
|
||||
import { useAdminAccounts } from 'soapbox/api/hooks/admin/useAdminAccounts';
|
||||
import Account from 'soapbox/components/account';
|
||||
import { Widget } from 'soapbox/components/ui';
|
||||
import AccountContainer from 'soapbox/containers/account-container';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'admin.latest_accounts_panel.title', defaultMessage: 'Latest Accounts' },
|
||||
expand: { id: 'admin.latest_accounts_panel.more', defaultMessage: 'Click to see {count, plural, one {# account} other {# accounts}}' },
|
||||
});
|
||||
|
||||
interface ILatestAccountsPanel {
|
||||
|
@ -20,18 +17,8 @@ interface ILatestAccountsPanel {
|
|||
const LatestAccountsPanel: React.FC<ILatestAccountsPanel> = ({ limit = 5 }) => {
|
||||
const intl = useIntl();
|
||||
const history = useHistory();
|
||||
const dispatch = useAppDispatch();
|
||||
const accountIds = useAppSelector<ImmutableOrderedSet<string>>((state) => state.admin.get('latestUsers').take(limit));
|
||||
|
||||
const [total, setTotal] = useState(accountIds.size);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchUsers(['local', 'active'], 1, null, limit))
|
||||
.then((value) => {
|
||||
setTotal((value as { count: number }).count);
|
||||
})
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
const { accounts } = useAdminAccounts(['local', 'active'], limit);
|
||||
|
||||
const handleAction = () => {
|
||||
history.push('/soapbox/admin/users');
|
||||
|
@ -41,10 +28,9 @@ const LatestAccountsPanel: React.FC<ILatestAccountsPanel> = ({ limit = 5 }) => {
|
|||
<Widget
|
||||
title={intl.formatMessage(messages.title)}
|
||||
onActionClick={handleAction}
|
||||
actionTitle={intl.formatMessage(messages.expand, { count: total })}
|
||||
>
|
||||
{accountIds.take(limit).map((account) => (
|
||||
<AccountContainer key={account} id={account} withRelationship={false} withDate />
|
||||
{accounts.slice(0, limit).map(account => (
|
||||
<Account key={account.id} account={account} withRelationship={false} withDate />
|
||||
))}
|
||||
</Widget>
|
||||
);
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
import debounce from 'lodash/debounce';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { expandUserIndex, fetchUserIndex, setUserIndexQuery } from 'soapbox/actions/admin';
|
||||
import { useAdminAccounts } from 'soapbox/api/hooks/admin/useAdminAccounts';
|
||||
import Account from 'soapbox/components/account';
|
||||
import ScrollableList from 'soapbox/components/scrollable-list';
|
||||
import { Column, Input } from 'soapbox/components/ui';
|
||||
import AccountContainer from 'soapbox/containers/account-container';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
import { Column } from 'soapbox/components/ui';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.admin.users', defaultMessage: 'Users' },
|
||||
|
@ -15,51 +13,30 @@ const messages = defineMessages({
|
|||
});
|
||||
|
||||
const UserIndex: React.FC = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const intl = useIntl();
|
||||
|
||||
const { isLoading, items, total, query, next } = useAppSelector((state) => state.admin_user_index);
|
||||
const { accounts, isLoading, hasNextPage, fetchNextPage } = useAdminAccounts(['local']);
|
||||
|
||||
const handleLoadMore = () => {
|
||||
if (!isLoading) dispatch(expandUserIndex());
|
||||
if (!isLoading) {
|
||||
fetchNextPage();
|
||||
}
|
||||
};
|
||||
|
||||
const updateQuery = useCallback(debounce(() => {
|
||||
dispatch(fetchUserIndex());
|
||||
}, 900, { leading: true }), []);
|
||||
|
||||
const handleQueryChange: React.ChangeEventHandler<HTMLInputElement> = e => {
|
||||
dispatch(setUserIndexQuery(e.target.value));
|
||||
updateQuery();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
updateQuery();
|
||||
}, []);
|
||||
|
||||
const hasMore = items.count() < total && !!next;
|
||||
|
||||
const showLoading = isLoading && items.isEmpty();
|
||||
|
||||
return (
|
||||
<Column label={intl.formatMessage(messages.heading)}>
|
||||
<Input
|
||||
value={query}
|
||||
onChange={handleQueryChange}
|
||||
placeholder={intl.formatMessage(messages.searchPlaceholder)}
|
||||
/>
|
||||
<ScrollableList
|
||||
scrollKey='user-index'
|
||||
hasMore={hasMore}
|
||||
hasMore={hasNextPage}
|
||||
isLoading={isLoading}
|
||||
showLoading={showLoading}
|
||||
showLoading={isLoading}
|
||||
onLoadMore={handleLoadMore}
|
||||
emptyMessage={intl.formatMessage(messages.empty)}
|
||||
className='mt-4'
|
||||
itemClassName='pb-4'
|
||||
>
|
||||
{items.map(id =>
|
||||
<AccountContainer key={id} id={id} withDate />,
|
||||
{accounts.map((account) =>
|
||||
<Account key={account.id} account={account} withDate />,
|
||||
)}
|
||||
</ScrollableList>
|
||||
</Column>
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
import { Set as ImmutableSet, OrderedSet as ImmutableOrderedSet, Record as ImmutableRecord } from 'immutable';
|
||||
|
||||
import {
|
||||
ADMIN_USER_INDEX_EXPAND_FAIL,
|
||||
ADMIN_USER_INDEX_EXPAND_REQUEST,
|
||||
|
@ -11,57 +9,67 @@ import {
|
|||
} from 'soapbox/actions/admin';
|
||||
|
||||
import type { AnyAction } from 'redux';
|
||||
import type { APIEntity } from 'soapbox/types/entities';
|
||||
|
||||
const ReducerRecord = ImmutableRecord({
|
||||
isLoading: false,
|
||||
loaded: false,
|
||||
items: ImmutableOrderedSet<string>(),
|
||||
filters: ImmutableSet(['local', 'active']),
|
||||
total: Infinity,
|
||||
pageSize: 50,
|
||||
page: -1,
|
||||
query: '',
|
||||
next: null as string | null,
|
||||
});
|
||||
interface State {
|
||||
isLoading: boolean;
|
||||
loaded: boolean;
|
||||
items: Set<string>;
|
||||
filters: Set<string>;
|
||||
pageSize: number;
|
||||
page: number;
|
||||
query: string;
|
||||
next: string | null;
|
||||
}
|
||||
|
||||
type State = ReturnType<typeof ReducerRecord>;
|
||||
function createState(): State {
|
||||
return {
|
||||
isLoading: false,
|
||||
loaded: false,
|
||||
items: new Set(),
|
||||
filters: new Set(['local', 'active']),
|
||||
pageSize: 50,
|
||||
page: -1,
|
||||
query: '',
|
||||
next: null,
|
||||
};
|
||||
}
|
||||
|
||||
export default function admin_user_index(state: State = ReducerRecord(), action: AnyAction): State {
|
||||
export default function admin_user_index(state: State = createState(), action: AnyAction): State {
|
||||
switch (action.type) {
|
||||
case ADMIN_USER_INDEX_QUERY_SET:
|
||||
return state.set('query', action.query);
|
||||
return { ...state, query: action.query };
|
||||
case ADMIN_USER_INDEX_FETCH_REQUEST:
|
||||
return state
|
||||
.set('isLoading', true)
|
||||
.set('loaded', true)
|
||||
.set('items', ImmutableOrderedSet())
|
||||
.set('total', action.count)
|
||||
.set('page', 0)
|
||||
.set('next', null);
|
||||
return {
|
||||
...state,
|
||||
isLoading: true,
|
||||
loaded: true,
|
||||
items: new Set(),
|
||||
page: 0,
|
||||
next: null,
|
||||
};
|
||||
case ADMIN_USER_INDEX_FETCH_SUCCESS:
|
||||
return state
|
||||
.set('isLoading', false)
|
||||
.set('loaded', true)
|
||||
.set('items', ImmutableOrderedSet(action.users.map((user: APIEntity) => user.id)))
|
||||
.set('total', action.count)
|
||||
.set('page', 1)
|
||||
.set('next', action.next);
|
||||
return {
|
||||
...state,
|
||||
isLoading: false,
|
||||
loaded: true,
|
||||
items: new Set(action.accounts.map((account: { id: string }) => account.id)),
|
||||
page: 1,
|
||||
next: action.next,
|
||||
};
|
||||
case ADMIN_USER_INDEX_FETCH_FAIL:
|
||||
case ADMIN_USER_INDEX_EXPAND_FAIL:
|
||||
return state
|
||||
.set('isLoading', false);
|
||||
return { ...state, isLoading: false };
|
||||
case ADMIN_USER_INDEX_EXPAND_REQUEST:
|
||||
return state
|
||||
.set('isLoading', true);
|
||||
return { ...state, isLoading: true };
|
||||
case ADMIN_USER_INDEX_EXPAND_SUCCESS:
|
||||
return state
|
||||
.set('isLoading', false)
|
||||
.set('loaded', true)
|
||||
.set('items', state.items.union(action.users.map((user: APIEntity) => user.id)))
|
||||
.set('total', action.count)
|
||||
.set('page', 1)
|
||||
.set('next', action.next);
|
||||
return {
|
||||
...state,
|
||||
isLoading: false,
|
||||
loaded: true,
|
||||
items: new Set([...state.items, ...action.accounts.map((account: { id: string }) => account.id)]),
|
||||
page: 1,
|
||||
next: action.next,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
|
Ładowanie…
Reference in New Issue