Extend dashboard, use hooks more See merge request soapbox-pub/soapbox!2989environments/review-main-yi2y9f/deployments/4540
commit
9b63adc23f
@ -1,6 +1,6 @@
|
|||||||
export { useCreateDomain, type CreateDomainParams } from './useCreateDomain';
|
|
||||||
export { useDeleteDomain } from './useDeleteDomain';
|
|
||||||
export { useDomains } from './useDomains';
|
export { useDomains } from './useDomains';
|
||||||
|
export { useModerationLog } from './useModerationLog';
|
||||||
|
export { useRelays } from './useRelays';
|
||||||
|
export { useRules } from './useRules';
|
||||||
export { useSuggest } from './useSuggest';
|
export { useSuggest } from './useSuggest';
|
||||||
export { useUpdateDomain } from './useUpdateDomain';
|
|
||||||
export { useVerify } from './useVerify';
|
export { useVerify } from './useVerify';
|
@ -0,0 +1,42 @@
|
|||||||
|
import { useInfiniteQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { useApi } from 'soapbox/hooks';
|
||||||
|
import { moderationLogEntrySchema, type ModerationLogEntry } from 'soapbox/schemas';
|
||||||
|
|
||||||
|
interface ModerationLogResult {
|
||||||
|
items: ModerationLogEntry[];
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const flattenPages = (pages?: ModerationLogResult[]): ModerationLogEntry[] => (pages || []).map(({ items }) => items).flat();
|
||||||
|
|
||||||
|
const useModerationLog = () => {
|
||||||
|
const api = useApi();
|
||||||
|
|
||||||
|
const getModerationLog = async (page: number): Promise<ModerationLogResult> => {
|
||||||
|
const { data } = await api.get<ModerationLogResult>('/api/v1/pleroma/admin/moderation_log', { params: { page } });
|
||||||
|
|
||||||
|
const normalizedData = data.items.map((domain) => moderationLogEntrySchema.parse(domain));
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: normalizedData,
|
||||||
|
total: data.total,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const queryInfo = useInfiniteQuery({
|
||||||
|
queryKey: ['admin', 'moderation_log'],
|
||||||
|
queryFn: ({ pageParam }) => getModerationLog(pageParam),
|
||||||
|
initialPageParam: 1,
|
||||||
|
getNextPageParam: (page, allPages) => flattenPages(allPages)!.length >= page.total ? undefined : allPages.length + 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = flattenPages(queryInfo.data?.pages);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...queryInfo,
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export { useModerationLog };
|
@ -0,0 +1,60 @@
|
|||||||
|
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { useApi } from 'soapbox/hooks';
|
||||||
|
import { queryClient } from 'soapbox/queries/client';
|
||||||
|
import { relaySchema, type Relay } from 'soapbox/schemas';
|
||||||
|
|
||||||
|
import type { AxiosResponse } from 'axios';
|
||||||
|
|
||||||
|
const useRelays = () => {
|
||||||
|
const api = useApi();
|
||||||
|
|
||||||
|
const getRelays = async () => {
|
||||||
|
const { data } = await api.get<{ relays: Relay[] }>('/api/v1/pleroma/admin/relay');
|
||||||
|
|
||||||
|
const normalizedData = data.relays?.map((relay) => relaySchema.parse(relay));
|
||||||
|
return normalizedData;
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = useQuery<ReadonlyArray<Relay>>({
|
||||||
|
queryKey: ['admin', 'relays'],
|
||||||
|
queryFn: getRelays,
|
||||||
|
placeholderData: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
mutate: followRelay,
|
||||||
|
isPending: isPendingFollow,
|
||||||
|
} = useMutation({
|
||||||
|
mutationFn: (relayUrl: string) => api.post('/api/v1/pleroma/admin/relays', { relay_url: relayUrl }),
|
||||||
|
retry: false,
|
||||||
|
onSuccess: ({ data }: AxiosResponse) =>
|
||||||
|
queryClient.setQueryData(['admin', 'relays'], (prevResult: ReadonlyArray<Relay>) =>
|
||||||
|
[...prevResult, relaySchema.parse(data)],
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
mutate: unfollowRelay,
|
||||||
|
isPending: isPendingUnfollow,
|
||||||
|
} = useMutation({
|
||||||
|
mutationFn: (relayUrl: string) => api.delete('/api/v1/pleroma/admin/relays', {
|
||||||
|
data: { relay_url: relayUrl },
|
||||||
|
}),
|
||||||
|
retry: false,
|
||||||
|
onSuccess: (_, relayUrl) =>
|
||||||
|
queryClient.setQueryData(['admin', 'relays'], (prevResult: ReadonlyArray<Relay>) =>
|
||||||
|
prevResult.filter(({ actor }) => actor !== relayUrl),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
followRelay,
|
||||||
|
isPendingFollow,
|
||||||
|
unfollowRelay,
|
||||||
|
isPendingUnfollow,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export { useRelays };
|
@ -0,0 +1,85 @@
|
|||||||
|
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { useApi } from 'soapbox/hooks';
|
||||||
|
import { queryClient } from 'soapbox/queries/client';
|
||||||
|
import { adminRuleSchema, type AdminRule } from 'soapbox/schemas';
|
||||||
|
|
||||||
|
import type { AxiosResponse } from 'axios';
|
||||||
|
|
||||||
|
interface CreateRuleParams {
|
||||||
|
priority?: number;
|
||||||
|
text: string;
|
||||||
|
hint?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UpdateRuleParams {
|
||||||
|
id: string;
|
||||||
|
priority?: number;
|
||||||
|
text?: string;
|
||||||
|
hint?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useRules = () => {
|
||||||
|
const api = useApi();
|
||||||
|
|
||||||
|
const getRules = async () => {
|
||||||
|
const { data } = await api.get<AdminRule[]>('/api/v1/pleroma/admin/rules');
|
||||||
|
|
||||||
|
const normalizedData = data.map((rule) => adminRuleSchema.parse(rule));
|
||||||
|
return normalizedData;
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = useQuery<ReadonlyArray<AdminRule>>({
|
||||||
|
queryKey: ['admin', 'rules'],
|
||||||
|
queryFn: getRules,
|
||||||
|
placeholderData: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
mutate: createRule,
|
||||||
|
isPending: isCreating,
|
||||||
|
} = useMutation({
|
||||||
|
mutationFn: (params: CreateRuleParams) => api.post('/api/v1/pleroma/admin/rules', params),
|
||||||
|
retry: false,
|
||||||
|
onSuccess: ({ data }: AxiosResponse) =>
|
||||||
|
queryClient.setQueryData(['admin', 'rules'], (prevResult: ReadonlyArray<AdminRule>) =>
|
||||||
|
[...prevResult, adminRuleSchema.parse(data)],
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
mutate: updateRule,
|
||||||
|
isPending: isUpdating,
|
||||||
|
} = useMutation({
|
||||||
|
mutationFn: ({ id, ...params }: UpdateRuleParams) => api.patch(`/api/v1/pleroma/admin/rules/${id}`, params),
|
||||||
|
retry: false,
|
||||||
|
onSuccess: ({ data }: AxiosResponse) =>
|
||||||
|
queryClient.setQueryData(['admin', 'rules'], (prevResult: ReadonlyArray<AdminRule>) =>
|
||||||
|
prevResult.map((rule) => rule.id === data.id ? adminRuleSchema.parse(data) : rule),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
mutate: deleteRule,
|
||||||
|
isPending: isDeleting,
|
||||||
|
} = useMutation({
|
||||||
|
mutationFn: (id: string) => api.delete(`/api/v1/pleroma/admin/rules/${id}`),
|
||||||
|
retry: false,
|
||||||
|
onSuccess: (_, id) =>
|
||||||
|
queryClient.setQueryData(['admin', 'rules'], (prevResult: ReadonlyArray<AdminRule>) =>
|
||||||
|
prevResult.filter(({ id: ruleId }) => ruleId !== id),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
createRule,
|
||||||
|
isCreating,
|
||||||
|
updateRule,
|
||||||
|
isUpdating,
|
||||||
|
deleteRule,
|
||||||
|
isDeleting,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export { useRules };
|
@ -0,0 +1,139 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
import { useRelays } from 'soapbox/api/hooks/admin';
|
||||||
|
import ScrollableList from 'soapbox/components/scrollable-list';
|
||||||
|
import { Button, Column, Form, HStack, Input, Stack, Text } from 'soapbox/components/ui';
|
||||||
|
import { useTextField } from 'soapbox/hooks/forms';
|
||||||
|
import toast from 'soapbox/toast';
|
||||||
|
|
||||||
|
import type { Relay as RelayEntity } from 'soapbox/schemas';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
heading: { id: 'column.admin.relays', defaultMessage: 'Instance relays' },
|
||||||
|
relayDeleteSuccess: { id: 'admin.relays.deleted', defaultMessage: 'Relay unfollowed' },
|
||||||
|
label: { id: 'admin.relays.new.url_placeholder', defaultMessage: 'Instance relay URL' },
|
||||||
|
createSuccess: { id: 'admin.relays.add.success', defaultMessage: 'Instance relay followed' },
|
||||||
|
createFail: { id: 'admin.relays.add.fail', defaultMessage: 'Failed to follow the instance relay' },
|
||||||
|
});
|
||||||
|
|
||||||
|
interface IRelay {
|
||||||
|
relay: RelayEntity;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Relay: React.FC<IRelay> = ({ relay }) => {
|
||||||
|
const { unfollowRelay } = useRelays();
|
||||||
|
|
||||||
|
const handleDeleteRelay = () => () => {
|
||||||
|
unfollowRelay(relay.actor, {
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success(messages.relayDeleteSuccess);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={relay.id} className='rounded-lg bg-gray-100 p-4 dark:bg-primary-800'>
|
||||||
|
<Stack space={2}>
|
||||||
|
<HStack alignItems='center' space={4} wrap>
|
||||||
|
<Text size='sm'>
|
||||||
|
<Text tag='span' size='sm' weight='medium'>
|
||||||
|
<FormattedMessage id='admin.relays.url' defaultMessage='Instance URL:' />
|
||||||
|
</Text>
|
||||||
|
{' '}
|
||||||
|
{relay.actor}
|
||||||
|
</Text>
|
||||||
|
{relay.followed_back && (
|
||||||
|
<Text tag='span' size='sm' weight='medium'>
|
||||||
|
<FormattedMessage id='admin.relays.followed_back' defaultMessage='Followed back' />
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</HStack>
|
||||||
|
<HStack justifyContent='end' space={2}>
|
||||||
|
<Button theme='primary' onClick={handleDeleteRelay()}>
|
||||||
|
<FormattedMessage id='admin.relays.unfollow' defaultMessage='Unfollow' />
|
||||||
|
</Button>
|
||||||
|
</HStack>
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const NewRelayForm: React.FC = () => {
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const name = useTextField();
|
||||||
|
|
||||||
|
const { followRelay, isPendingFollow } = useRelays();
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent<Element>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
followRelay(name.value, {
|
||||||
|
onSuccess() {
|
||||||
|
toast.success(messages.createSuccess);
|
||||||
|
},
|
||||||
|
onError() {
|
||||||
|
toast.error(messages.createFail);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const label = intl.formatMessage(messages.label);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form onSubmit={handleSubmit}>
|
||||||
|
<HStack space={2} alignItems='center'>
|
||||||
|
<label className='grow'>
|
||||||
|
<span style={{ display: 'none' }}>{label}</span>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
type='text'
|
||||||
|
placeholder={label}
|
||||||
|
disabled={isPendingFollow}
|
||||||
|
{...name}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
disabled={isPendingFollow}
|
||||||
|
onClick={handleSubmit}
|
||||||
|
theme='primary'
|
||||||
|
>
|
||||||
|
<FormattedMessage id='admin.relays.new.follow' defaultMessage='Follow' />
|
||||||
|
</Button>
|
||||||
|
</HStack>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Relays: React.FC = () => {
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const { data: relays, isFetching } = useRelays();
|
||||||
|
|
||||||
|
const emptyMessage = <FormattedMessage id='empty_column.admin.relays' defaultMessage='There are no relays followed yet.' />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Column label={intl.formatMessage(messages.heading)}>
|
||||||
|
<Stack className='gap-4'>
|
||||||
|
<NewRelayForm />
|
||||||
|
|
||||||
|
{relays && (
|
||||||
|
<ScrollableList
|
||||||
|
scrollKey='relays'
|
||||||
|
emptyMessage={emptyMessage}
|
||||||
|
itemClassName='py-3 first:pt-0 last:pb-0'
|
||||||
|
isLoading={isFetching}
|
||||||
|
showLoading={isFetching && !relays?.length}
|
||||||
|
>
|
||||||
|
{relays.map((relay) => (
|
||||||
|
<Relay key={relay.id} relay={relay} />
|
||||||
|
))}
|
||||||
|
</ScrollableList>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Relays;
|
@ -0,0 +1,111 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
import { openModal } from 'soapbox/actions/modals';
|
||||||
|
import { useRules } from 'soapbox/api/hooks/admin';
|
||||||
|
import ScrollableList from 'soapbox/components/scrollable-list';
|
||||||
|
import { Button, Column, HStack, Stack, Text } from 'soapbox/components/ui';
|
||||||
|
import { useAppDispatch } from 'soapbox/hooks';
|
||||||
|
import { AdminRule } from 'soapbox/schemas';
|
||||||
|
import toast from 'soapbox/toast';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
heading: { id: 'column.admin.rules', defaultMessage: 'Instance rules' },
|
||||||
|
deleteConfirm: { id: 'confirmations.admin.delete_rule.confirm', defaultMessage: 'Delete' },
|
||||||
|
deleteHeading: { id: 'confirmations.admin.delete_rule.heading', defaultMessage: 'Delete rule' },
|
||||||
|
deleteMessage: { id: 'confirmations.admin.delete_rule.message', defaultMessage: 'Are you sure you want to delete the rule?' },
|
||||||
|
ruleDeleteSuccess: { id: 'admin.edit_rule.deleted', defaultMessage: 'Rule deleted' },
|
||||||
|
});
|
||||||
|
|
||||||
|
interface IRule {
|
||||||
|
rule: AdminRule;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Rule: React.FC<IRule> = ({ rule }) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const { deleteRule } = useRules();
|
||||||
|
|
||||||
|
const handleEditRule = (rule: AdminRule) => () => {
|
||||||
|
dispatch(openModal('EDIT_RULE', { rule }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteRule = (id: string) => () => {
|
||||||
|
dispatch(openModal('CONFIRM', {
|
||||||
|
heading: intl.formatMessage(messages.deleteHeading),
|
||||||
|
message: intl.formatMessage(messages.deleteMessage),
|
||||||
|
confirm: intl.formatMessage(messages.deleteConfirm),
|
||||||
|
onConfirm: () => deleteRule(id, {
|
||||||
|
onSuccess: () => toast.success(messages.ruleDeleteSuccess),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={rule.id} className='rounded-lg bg-gray-100 p-4 dark:bg-primary-800'>
|
||||||
|
<Stack space={2}>
|
||||||
|
<Text>{rule.text}</Text>
|
||||||
|
<Text tag='span' theme='muted' size='sm'>{rule.hint}</Text>
|
||||||
|
{rule.priority !== null && (
|
||||||
|
<Text size='sm'>
|
||||||
|
<Text tag='span' size='sm' weight='medium'>
|
||||||
|
<FormattedMessage id='admin.rule.priority' defaultMessage='Priority:' />
|
||||||
|
</Text>
|
||||||
|
{' '}
|
||||||
|
{rule.priority}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
<HStack justifyContent='end' space={2}>
|
||||||
|
<Button theme='primary' onClick={handleEditRule(rule)}>
|
||||||
|
<FormattedMessage id='admin.rules.edit' defaultMessage='Edit' />
|
||||||
|
</Button>
|
||||||
|
<Button theme='primary' onClick={handleDeleteRule(rule.id)}>
|
||||||
|
<FormattedMessage id='admin.rules.delete' defaultMessage='Delete' />
|
||||||
|
</Button>
|
||||||
|
</HStack>
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Rules: React.FC = () => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
const { data, isLoading } = useRules();
|
||||||
|
|
||||||
|
const handleCreateRule = () => {
|
||||||
|
dispatch(openModal('EDIT_RULE'));
|
||||||
|
};
|
||||||
|
|
||||||
|
const emptyMessage = <FormattedMessage id='empty_column.admin.rules' defaultMessage='There are no instance rules yet.' />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Column label={intl.formatMessage(messages.heading)}>
|
||||||
|
<Stack className='gap-4'>
|
||||||
|
<Button
|
||||||
|
className='sm:w-fit sm:self-end'
|
||||||
|
icon={require('@tabler/icons/outline/plus.svg')}
|
||||||
|
onClick={handleCreateRule}
|
||||||
|
theme='secondary'
|
||||||
|
block
|
||||||
|
>
|
||||||
|
<FormattedMessage id='admin.rules.action' defaultMessage='Create rule' />
|
||||||
|
</Button>
|
||||||
|
<ScrollableList
|
||||||
|
scrollKey='rules'
|
||||||
|
emptyMessage={emptyMessage}
|
||||||
|
itemClassName='py-3 first:pt-0 last:pb-0'
|
||||||
|
isLoading={isLoading}
|
||||||
|
showLoading={isLoading}
|
||||||
|
>
|
||||||
|
{data!.map((rule) => (
|
||||||
|
<Rule key={rule.id} rule={rule} />
|
||||||
|
))}
|
||||||
|
</ScrollableList>
|
||||||
|
</Stack>
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Rules;
|
@ -0,0 +1,92 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
import { useRules } from 'soapbox/api/hooks/admin';
|
||||||
|
import { Form, FormGroup, Input, Modal } from 'soapbox/components/ui';
|
||||||
|
import { useTextField } from 'soapbox/hooks/forms';
|
||||||
|
import { type AdminRule } from 'soapbox/schemas';
|
||||||
|
import toast from 'soapbox/toast';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
save: { id: 'admin.edit_rule.save', defaultMessage: 'Save' },
|
||||||
|
ruleTextPlaceholder: { id: 'admin.edit_rule.fields.text_placeholder', defaultMessage: 'Instance rule text' },
|
||||||
|
rulePriorityPlaceholder: { id: 'admin.edit_rule.fields.priority_placeholder', defaultMessage: 'Instance rule display priority' },
|
||||||
|
ruleCreateSuccess: { id: 'admin.edit_rule.created', defaultMessage: 'Rule created' },
|
||||||
|
ruleUpdateSuccess: { id: 'admin.edit_rule.updated', defaultMessage: 'Rule edited' },
|
||||||
|
});
|
||||||
|
|
||||||
|
interface IEditRuleModal {
|
||||||
|
onClose: (type?: string) => void;
|
||||||
|
rule?: AdminRule;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EditRuleModal: React.FC<IEditRuleModal> = ({ onClose, rule }) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const { createRule, updateRule } = useRules();
|
||||||
|
|
||||||
|
const text = useTextField(rule?.text);
|
||||||
|
const priority = useTextField(rule?.priority?.toString());
|
||||||
|
|
||||||
|
const onClickClose = () => {
|
||||||
|
onClose('EDIT_RULE');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
if (rule) {
|
||||||
|
updateRule({
|
||||||
|
id: rule.id,
|
||||||
|
text: text.value,
|
||||||
|
priority: isNaN(Number(priority.value)) ? undefined : Number(priority.value),
|
||||||
|
}, {
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success(messages.ruleUpdateSuccess);
|
||||||
|
onClickClose();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
createRule({
|
||||||
|
text: text.value,
|
||||||
|
priority: isNaN(Number(priority.value)) ? undefined : Number(priority.value),
|
||||||
|
}, {
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success(messages.ruleUpdateSuccess);
|
||||||
|
onClickClose();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
onClose={onClickClose}
|
||||||
|
title={rule
|
||||||
|
? <FormattedMessage id='column.admin.edit_rule' defaultMessage='Edit rule' />
|
||||||
|
: <FormattedMessage id='column.admin.create_rule' defaultMessage='Create rule' />}
|
||||||
|
confirmationAction={handleSubmit}
|
||||||
|
confirmationText={intl.formatMessage(messages.save)}
|
||||||
|
>
|
||||||
|
<Form>
|
||||||
|
<FormGroup
|
||||||
|
labelText={<FormattedMessage id='admin.edit_rule.fields.text_label' defaultMessage='Rule text' />}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
placeholder={intl.formatMessage(messages.ruleTextPlaceholder)}
|
||||||
|
{...text}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup
|
||||||
|
labelText={<FormattedMessage id='admin.edit_rule.fields.priority_label' defaultMessage='Rule priority' />}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
placeholder={intl.formatMessage(messages.rulePriorityPlaceholder)}
|
||||||
|
type='number'
|
||||||
|
{...priority}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditRuleModal;
|
@ -1,58 +0,0 @@
|
|||||||
import {
|
|
||||||
Map as ImmutableMap,
|
|
||||||
Record as ImmutableRecord,
|
|
||||||
OrderedSet as ImmutableOrderedSet,
|
|
||||||
} from 'immutable';
|
|
||||||
|
|
||||||
import { ADMIN_LOG_FETCH_SUCCESS } from 'soapbox/actions/admin';
|
|
||||||
|
|
||||||
import type { AnyAction } from 'redux';
|
|
||||||
import type { APIEntity } from 'soapbox/types/entities';
|
|
||||||
|
|
||||||
export const LogEntryRecord = ImmutableRecord({
|
|
||||||
data: ImmutableMap<string, any>(),
|
|
||||||
id: 0,
|
|
||||||
message: '',
|
|
||||||
time: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
const ReducerRecord = ImmutableRecord({
|
|
||||||
items: ImmutableMap<string, LogEntry>(),
|
|
||||||
index: ImmutableOrderedSet<number>(),
|
|
||||||
total: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
type LogEntry = ReturnType<typeof LogEntryRecord>;
|
|
||||||
type State = ReturnType<typeof ReducerRecord>;
|
|
||||||
type APIEntities = Array<APIEntity>;
|
|
||||||
|
|
||||||
const parseItems = (items: APIEntities) => {
|
|
||||||
const ids: Array<number> = [];
|
|
||||||
const map: Record<string, LogEntry> = {};
|
|
||||||
|
|
||||||
items.forEach(item => {
|
|
||||||
ids.push(item.id);
|
|
||||||
map[item.id] = LogEntryRecord(item);
|
|
||||||
});
|
|
||||||
|
|
||||||
return { ids: ids, map: map };
|
|
||||||
};
|
|
||||||
|
|
||||||
const importItems = (state: State, items: APIEntities, total: number) => {
|
|
||||||
const { ids, map } = parseItems(items);
|
|
||||||
|
|
||||||
return state.withMutations(state => {
|
|
||||||
state.update('index', v => v.union(ids));
|
|
||||||
state.update('items', v => v.merge(map));
|
|
||||||
state.set('total', total);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function admin_log(state = ReducerRecord(), action: AnyAction) {
|
|
||||||
switch (action.type) {
|
|
||||||
case ADMIN_LOG_FETCH_SUCCESS:
|
|
||||||
return importItems(state, action.items, action.total);
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,12 @@
|
|||||||
|
import z from 'zod';
|
||||||
|
|
||||||
|
const moderationLogEntrySchema = z.object({
|
||||||
|
id: z.coerce.string(),
|
||||||
|
data: z.record(z.string(), z.any()).catch({}),
|
||||||
|
time: z.number().catch(0),
|
||||||
|
message: z.string().catch(''),
|
||||||
|
});
|
||||||
|
|
||||||
|
type ModerationLogEntry = z.infer<typeof moderationLogEntrySchema>
|
||||||
|
|
||||||
|
export { moderationLogEntrySchema, type ModerationLogEntry };
|
@ -0,0 +1,13 @@
|
|||||||
|
import z from 'zod';
|
||||||
|
|
||||||
|
const relaySchema = z.preprocess((data: any) => {
|
||||||
|
return { id: data.actor, ...data };
|
||||||
|
}, z.object({
|
||||||
|
actor: z.string().catch(''),
|
||||||
|
id: z.string(),
|
||||||
|
followed_back: z.boolean().catch(false),
|
||||||
|
}));
|
||||||
|
|
||||||
|
type Relay = z.infer<typeof relaySchema>
|
||||||
|
|
||||||
|
export { relaySchema, type Relay };
|
@ -1,19 +1,25 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { coerceObject } from './utils';
|
const baseRuleSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
text: z.string().catch(''),
|
||||||
|
hint: z.string().catch(''),
|
||||||
|
rule_type: z.enum(['account', 'content', 'group']).nullable().catch(null),
|
||||||
|
});
|
||||||
|
|
||||||
const ruleSchema = z.preprocess((data: any) => {
|
const ruleSchema = z.preprocess((data: any) => {
|
||||||
return {
|
return {
|
||||||
...data,
|
...data,
|
||||||
hint: data.hint || data.subtext,
|
hint: data.hint || data.subtext,
|
||||||
};
|
};
|
||||||
}, coerceObject({
|
}, baseRuleSchema);
|
||||||
id: z.string(),
|
|
||||||
text: z.string().catch(''),
|
|
||||||
hint: z.string().catch(''),
|
|
||||||
rule_type: z.enum(['account', 'content', 'group']).nullable().catch(null),
|
|
||||||
}));
|
|
||||||
|
|
||||||
type Rule = z.infer<typeof ruleSchema>;
|
type Rule = z.infer<typeof ruleSchema>;
|
||||||
|
|
||||||
export { ruleSchema, type Rule };
|
const adminRuleSchema = baseRuleSchema.extend({
|
||||||
|
priority: z.number().nullable().catch(null),
|
||||||
|
});
|
||||||
|
|
||||||
|
type AdminRule = z.infer<typeof adminRuleSchema>;
|
||||||
|
|
||||||
|
export { ruleSchema, adminRuleSchema, type Rule, type AdminRule };
|
Loading…
Reference in new issue