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 {
|
try {
|
||||||
const { data: accounts, ...response } = await api(getState).get(url || '/api/v1/admin/accounts', { params });
|
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 next = getLinks(response as AxiosResponse<any, any>).refs.find(link => link.rel === 'next')?.uri;
|
||||||
|
|
||||||
const count = next
|
|
||||||
? page * pageSize + 1
|
|
||||||
: (page - 1) * pageSize + accounts.length;
|
|
||||||
|
|
||||||
dispatch(importFetchedAccounts(accounts.map(({ account }: APIEntity) => account)));
|
dispatch(importFetchedAccounts(accounts.map(({ account }: APIEntity) => account)));
|
||||||
dispatch(fetchRelationships(accounts.map((account_1: APIEntity) => account_1.id)));
|
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 });
|
dispatch({ type: ADMIN_USERS_FETCH_SUCCESS, accounts, pageSize, filters, page, next });
|
||||||
return { users: accounts, count, pageSize, next: next?.uri || false };
|
return { accounts, next };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return dispatch({ type: ADMIN_USERS_FETCH_FAIL, error, filters, page, pageSize });
|
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({ 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) => {
|
.then((data: any) => {
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
dispatch({ type: ADMIN_USER_INDEX_FETCH_FAIL });
|
dispatch({ type: ADMIN_USER_INDEX_FETCH_FAIL });
|
||||||
} else {
|
} else {
|
||||||
const { users, count, next } = (data);
|
const { accounts, next } = data;
|
||||||
dispatch({ type: ADMIN_USER_INDEX_FETCH_SUCCESS, users, count, next });
|
dispatch({ type: ADMIN_USER_INDEX_FETCH_SUCCESS, accounts, next });
|
||||||
}
|
}
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
dispatch({ type: ADMIN_USER_INDEX_FETCH_FAIL });
|
dispatch({ type: ADMIN_USER_INDEX_FETCH_FAIL });
|
||||||
|
@ -422,13 +418,13 @@ const expandUserIndex = () =>
|
||||||
|
|
||||||
dispatch({ type: ADMIN_USER_INDEX_EXPAND_REQUEST });
|
dispatch({ type: ADMIN_USER_INDEX_EXPAND_REQUEST });
|
||||||
|
|
||||||
dispatch(fetchUsers(filters.toJS() as string[], page + 1, query, pageSize, next))
|
dispatch(fetchUsers([...filters], page + 1, query, pageSize, next))
|
||||||
.then((data: any) => {
|
.then((data) => {
|
||||||
if (data.error) {
|
if ('error' in data) {
|
||||||
dispatch({ type: ADMIN_USER_INDEX_EXPAND_FAIL });
|
dispatch({ type: ADMIN_USER_INDEX_EXPAND_FAIL });
|
||||||
} else {
|
} else {
|
||||||
const { users, count, next } = (data);
|
const { accounts, next } = data;
|
||||||
dispatch({ type: ADMIN_USER_INDEX_EXPAND_SUCCESS, users, count, next });
|
dispatch({ type: ADMIN_USER_INDEX_EXPAND_SUCCESS, accounts, next });
|
||||||
}
|
}
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
dispatch({ type: ADMIN_USER_INDEX_EXPAND_FAIL });
|
dispatch({ type: ADMIN_USER_INDEX_EXPAND_FAIL });
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { HTTPError } from './HTTPError';
|
import { HTTPError } from './HTTPError';
|
||||||
|
|
||||||
interface Opts {
|
interface Opts {
|
||||||
searchParams?: Record<string, string | number | boolean>;
|
searchParams?: URLSearchParams | Record<string, string | number | boolean>;
|
||||||
headers?: Record<string, string>;
|
headers?: Record<string, string>;
|
||||||
signal?: AbortSignal;
|
signal?: AbortSignal;
|
||||||
}
|
}
|
||||||
|
@ -51,9 +51,11 @@ export class MastodonClient {
|
||||||
const url = new URL(path, this.baseUrl);
|
const url = new URL(path, this.baseUrl);
|
||||||
|
|
||||||
if (opts.searchParams) {
|
if (opts.searchParams) {
|
||||||
const params = Object
|
const params = opts.searchParams instanceof URLSearchParams
|
||||||
.entries(opts.searchParams)
|
? opts.searchParams
|
||||||
.map(([key, value]) => ([key, String(value)]));
|
: Object
|
||||||
|
.entries(opts.searchParams)
|
||||||
|
.map(([key, value]) => ([key, String(value)]));
|
||||||
|
|
||||||
url.search = new URLSearchParams(params).toString();
|
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 from 'react';
|
||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import { defineMessages, useIntl } from 'react-intl';
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
import { useHistory } from 'react-router-dom';
|
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 { Widget } from 'soapbox/components/ui';
|
||||||
import AccountContainer from 'soapbox/containers/account-container';
|
|
||||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
title: { id: 'admin.latest_accounts_panel.title', defaultMessage: 'Latest Accounts' },
|
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 {
|
interface ILatestAccountsPanel {
|
||||||
|
@ -20,18 +17,8 @@ interface ILatestAccountsPanel {
|
||||||
const LatestAccountsPanel: React.FC<ILatestAccountsPanel> = ({ limit = 5 }) => {
|
const LatestAccountsPanel: React.FC<ILatestAccountsPanel> = ({ limit = 5 }) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const accountIds = useAppSelector<ImmutableOrderedSet<string>>((state) => state.admin.get('latestUsers').take(limit));
|
|
||||||
|
|
||||||
const [total, setTotal] = useState(accountIds.size);
|
const { accounts } = useAdminAccounts(['local', 'active'], limit);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
dispatch(fetchUsers(['local', 'active'], 1, null, limit))
|
|
||||||
.then((value) => {
|
|
||||||
setTotal((value as { count: number }).count);
|
|
||||||
})
|
|
||||||
.catch(() => {});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleAction = () => {
|
const handleAction = () => {
|
||||||
history.push('/soapbox/admin/users');
|
history.push('/soapbox/admin/users');
|
||||||
|
@ -41,10 +28,9 @@ const LatestAccountsPanel: React.FC<ILatestAccountsPanel> = ({ limit = 5 }) => {
|
||||||
<Widget
|
<Widget
|
||||||
title={intl.formatMessage(messages.title)}
|
title={intl.formatMessage(messages.title)}
|
||||||
onActionClick={handleAction}
|
onActionClick={handleAction}
|
||||||
actionTitle={intl.formatMessage(messages.expand, { count: total })}
|
|
||||||
>
|
>
|
||||||
{accountIds.take(limit).map((account) => (
|
{accounts.slice(0, limit).map(account => (
|
||||||
<AccountContainer key={account} id={account} withRelationship={false} withDate />
|
<Account key={account.id} account={account} withRelationship={false} withDate />
|
||||||
))}
|
))}
|
||||||
</Widget>
|
</Widget>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
import debounce from 'lodash/debounce';
|
import React from 'react';
|
||||||
import React, { useCallback, useEffect } from 'react';
|
|
||||||
import { defineMessages, useIntl } from 'react-intl';
|
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 ScrollableList from 'soapbox/components/scrollable-list';
|
||||||
import { Column, Input } from 'soapbox/components/ui';
|
import { Column } from 'soapbox/components/ui';
|
||||||
import AccountContainer from 'soapbox/containers/account-container';
|
|
||||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
heading: { id: 'column.admin.users', defaultMessage: 'Users' },
|
heading: { id: 'column.admin.users', defaultMessage: 'Users' },
|
||||||
|
@ -15,51 +13,30 @@ const messages = defineMessages({
|
||||||
});
|
});
|
||||||
|
|
||||||
const UserIndex: React.FC = () => {
|
const UserIndex: React.FC = () => {
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
const { isLoading, items, total, query, next } = useAppSelector((state) => state.admin_user_index);
|
const { accounts, isLoading, hasNextPage, fetchNextPage } = useAdminAccounts(['local']);
|
||||||
|
|
||||||
const handleLoadMore = () => {
|
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 (
|
return (
|
||||||
<Column label={intl.formatMessage(messages.heading)}>
|
<Column label={intl.formatMessage(messages.heading)}>
|
||||||
<Input
|
|
||||||
value={query}
|
|
||||||
onChange={handleQueryChange}
|
|
||||||
placeholder={intl.formatMessage(messages.searchPlaceholder)}
|
|
||||||
/>
|
|
||||||
<ScrollableList
|
<ScrollableList
|
||||||
scrollKey='user-index'
|
scrollKey='user-index'
|
||||||
hasMore={hasMore}
|
hasMore={hasNextPage}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
showLoading={showLoading}
|
showLoading={isLoading}
|
||||||
onLoadMore={handleLoadMore}
|
onLoadMore={handleLoadMore}
|
||||||
emptyMessage={intl.formatMessage(messages.empty)}
|
emptyMessage={intl.formatMessage(messages.empty)}
|
||||||
className='mt-4'
|
className='mt-4'
|
||||||
itemClassName='pb-4'
|
itemClassName='pb-4'
|
||||||
>
|
>
|
||||||
{items.map(id =>
|
{accounts.map((account) =>
|
||||||
<AccountContainer key={id} id={id} withDate />,
|
<Account key={account.id} account={account} withDate />,
|
||||||
)}
|
)}
|
||||||
</ScrollableList>
|
</ScrollableList>
|
||||||
</Column>
|
</Column>
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
import { Set as ImmutableSet, OrderedSet as ImmutableOrderedSet, Record as ImmutableRecord } from 'immutable';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ADMIN_USER_INDEX_EXPAND_FAIL,
|
ADMIN_USER_INDEX_EXPAND_FAIL,
|
||||||
ADMIN_USER_INDEX_EXPAND_REQUEST,
|
ADMIN_USER_INDEX_EXPAND_REQUEST,
|
||||||
|
@ -11,57 +9,67 @@ import {
|
||||||
} from 'soapbox/actions/admin';
|
} from 'soapbox/actions/admin';
|
||||||
|
|
||||||
import type { AnyAction } from 'redux';
|
import type { AnyAction } from 'redux';
|
||||||
import type { APIEntity } from 'soapbox/types/entities';
|
|
||||||
|
|
||||||
const ReducerRecord = ImmutableRecord({
|
interface State {
|
||||||
isLoading: false,
|
isLoading: boolean;
|
||||||
loaded: false,
|
loaded: boolean;
|
||||||
items: ImmutableOrderedSet<string>(),
|
items: Set<string>;
|
||||||
filters: ImmutableSet(['local', 'active']),
|
filters: Set<string>;
|
||||||
total: Infinity,
|
pageSize: number;
|
||||||
pageSize: 50,
|
page: number;
|
||||||
page: -1,
|
query: string;
|
||||||
query: '',
|
next: string | null;
|
||||||
next: null as 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) {
|
switch (action.type) {
|
||||||
case ADMIN_USER_INDEX_QUERY_SET:
|
case ADMIN_USER_INDEX_QUERY_SET:
|
||||||
return state.set('query', action.query);
|
return { ...state, query: action.query };
|
||||||
case ADMIN_USER_INDEX_FETCH_REQUEST:
|
case ADMIN_USER_INDEX_FETCH_REQUEST:
|
||||||
return state
|
return {
|
||||||
.set('isLoading', true)
|
...state,
|
||||||
.set('loaded', true)
|
isLoading: true,
|
||||||
.set('items', ImmutableOrderedSet())
|
loaded: true,
|
||||||
.set('total', action.count)
|
items: new Set(),
|
||||||
.set('page', 0)
|
page: 0,
|
||||||
.set('next', null);
|
next: null,
|
||||||
|
};
|
||||||
case ADMIN_USER_INDEX_FETCH_SUCCESS:
|
case ADMIN_USER_INDEX_FETCH_SUCCESS:
|
||||||
return state
|
return {
|
||||||
.set('isLoading', false)
|
...state,
|
||||||
.set('loaded', true)
|
isLoading: false,
|
||||||
.set('items', ImmutableOrderedSet(action.users.map((user: APIEntity) => user.id)))
|
loaded: true,
|
||||||
.set('total', action.count)
|
items: new Set(action.accounts.map((account: { id: string }) => account.id)),
|
||||||
.set('page', 1)
|
page: 1,
|
||||||
.set('next', action.next);
|
next: action.next,
|
||||||
|
};
|
||||||
case ADMIN_USER_INDEX_FETCH_FAIL:
|
case ADMIN_USER_INDEX_FETCH_FAIL:
|
||||||
case ADMIN_USER_INDEX_EXPAND_FAIL:
|
case ADMIN_USER_INDEX_EXPAND_FAIL:
|
||||||
return state
|
return { ...state, isLoading: false };
|
||||||
.set('isLoading', false);
|
|
||||||
case ADMIN_USER_INDEX_EXPAND_REQUEST:
|
case ADMIN_USER_INDEX_EXPAND_REQUEST:
|
||||||
return state
|
return { ...state, isLoading: true };
|
||||||
.set('isLoading', true);
|
|
||||||
case ADMIN_USER_INDEX_EXPAND_SUCCESS:
|
case ADMIN_USER_INDEX_EXPAND_SUCCESS:
|
||||||
return state
|
return {
|
||||||
.set('isLoading', false)
|
...state,
|
||||||
.set('loaded', true)
|
isLoading: false,
|
||||||
.set('items', state.items.union(action.users.map((user: APIEntity) => user.id)))
|
loaded: true,
|
||||||
.set('total', action.count)
|
items: new Set([...state.items, ...action.accounts.map((account: { id: string }) => account.id)]),
|
||||||
.set('page', 1)
|
page: 1,
|
||||||
.set('next', action.next);
|
next: action.next,
|
||||||
|
};
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
Ładowanie…
Reference in New Issue