mirror of
https://github.com/twentyhq/twenty.git
synced 2024-12-26 13:31:45 +03:00
Prevent remote object updates (#4804)
Backend: Adding a new util function that throw an error if the objectMetadata is remote Frontend: hiding the save button when remote Also renaming `useObjectMetadataItemForSettings` since this hook is used in other places than settings and is not in the settings repo. Name can definitely be challenged! --------- Co-authored-by: Thomas Trompette <thomast@twenty.com>
This commit is contained in:
parent
c5349291c8
commit
2e419091cc
@ -1,6 +1,6 @@
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
|
||||
import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings';
|
||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||
import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData';
|
||||
import { PrefetchKey } from '@/prefetch/types/PrefetchKey';
|
||||
import { useIcons } from '@/ui/display/icon/hooks/useIcons';
|
||||
@ -9,7 +9,7 @@ import { GraphQLView } from '@/views/types/GraphQLView';
|
||||
import { getObjectMetadataItemViews } from '@/views/utils/getObjectMetadataItemViews';
|
||||
|
||||
export const ObjectMetadataNavItems = () => {
|
||||
const { activeObjectMetadataItems } = useObjectMetadataItemForSettings();
|
||||
const { activeObjectMetadataItems } = useFilteredObjectMetadataItems();
|
||||
const navigate = useNavigate();
|
||||
const { getIcon } = useIcons();
|
||||
const currentPath = useLocation().pathname;
|
||||
|
@ -7,8 +7,8 @@ import {
|
||||
query,
|
||||
responseData,
|
||||
variables,
|
||||
} from '@/object-metadata/hooks/__mocks__/useObjectMetadataItemForSettings';
|
||||
import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings';
|
||||
} from '@/object-metadata/hooks/__mocks__/useFilteredObjectMetadataItems';
|
||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock';
|
||||
|
||||
@ -36,14 +36,14 @@ const Wrapper = ({ children }: { children: ReactNode }) => (
|
||||
|
||||
const mockObjectMetadataItems = getObjectMetadataItemsMock();
|
||||
|
||||
describe('useObjectMetadataItemForSettings', () => {
|
||||
describe('useFilteredObjectMetadataItems', () => {
|
||||
it('should findActiveObjectMetadataItemBySlug', async () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const setMetadataItems = useSetRecoilState(objectMetadataItemsState);
|
||||
setMetadataItems(mockObjectMetadataItems);
|
||||
|
||||
return useObjectMetadataItemForSettings();
|
||||
return useFilteredObjectMetadataItems();
|
||||
},
|
||||
{
|
||||
wrapper: Wrapper,
|
||||
@ -63,7 +63,7 @@ describe('useObjectMetadataItemForSettings', () => {
|
||||
const setMetadataItems = useSetRecoilState(objectMetadataItemsState);
|
||||
setMetadataItems(mockObjectMetadataItems);
|
||||
|
||||
return useObjectMetadataItemForSettings();
|
||||
return useFilteredObjectMetadataItems();
|
||||
},
|
||||
{
|
||||
wrapper: Wrapper,
|
||||
@ -85,7 +85,7 @@ describe('useObjectMetadataItemForSettings', () => {
|
||||
const setMetadataItems = useSetRecoilState(objectMetadataItemsState);
|
||||
setMetadataItems(mockObjectMetadataItems);
|
||||
|
||||
return useObjectMetadataItemForSettings();
|
||||
return useFilteredObjectMetadataItems();
|
||||
},
|
||||
{
|
||||
wrapper: Wrapper,
|
@ -4,7 +4,7 @@ import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadat
|
||||
|
||||
import { getObjectSlug } from '../utils/getObjectSlug';
|
||||
|
||||
export const useObjectMetadataItemForSettings = () => {
|
||||
export const useFilteredObjectMetadataItems = () => {
|
||||
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
|
||||
|
||||
const activeObjectMetadataItems = objectMetadataItems.filter(
|
@ -1,6 +1,6 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings';
|
||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||
import { isObjectMetadataAvailableForRelation } from '@/object-metadata/utils/isObjectMetadataAvailableForRelation';
|
||||
import { validateMetadataLabel } from '@/object-metadata/utils/validateMetadataLabel';
|
||||
import { useIcons } from '@/ui/display/icon/hooks/useIcons';
|
||||
@ -58,7 +58,7 @@ export const SettingsObjectFieldRelationForm = ({
|
||||
}: SettingsObjectFieldRelationFormProps) => {
|
||||
const { getIcon } = useIcons();
|
||||
const { objectMetadataItems, findObjectMetadataItemById } =
|
||||
useObjectMetadataItemForSettings();
|
||||
useFilteredObjectMetadataItems();
|
||||
|
||||
const selectedObjectMetadataItem =
|
||||
(values.objectMetadataId
|
||||
|
@ -1,6 +1,6 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings';
|
||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { SettingsDataModelDefaultValueForm } from '@/settings/data-model/components/SettingsDataModelDefaultValue';
|
||||
import { SettingsDataModelPreviewFormCard } from '@/settings/data-model/components/SettingsDataModelPreviewFormCard';
|
||||
@ -79,7 +79,7 @@ export const SettingsDataModelFieldSettingsFormCard = ({
|
||||
relationFieldMetadataItem,
|
||||
values,
|
||||
}: SettingsDataModelFieldSettingsFormCardProps) => {
|
||||
const { findObjectMetadataItemById } = useObjectMetadataItemForSettings();
|
||||
const { findObjectMetadataItemById } = useFilteredObjectMetadataItems();
|
||||
|
||||
if (!previewableTypes.includes(fieldMetadataItem.type)) return null;
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings';
|
||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||
import { recordIndexViewTypeState } from '@/object-record/record-index/states/recordIndexViewTypeState';
|
||||
import { useIcons } from '@/ui/display/icon/hooks/useIcons';
|
||||
import { PageAddButton } from '@/ui/layout/page/PageAddButton';
|
||||
@ -20,7 +20,10 @@ export const RecordIndexPageHeader = ({
|
||||
const objectNamePlural = useParams().objectNamePlural ?? '';
|
||||
|
||||
const { findObjectMetadataItemByNamePlural } =
|
||||
useObjectMetadataItemForSettings();
|
||||
useFilteredObjectMetadataItems();
|
||||
|
||||
const objectMetadataItem =
|
||||
findObjectMetadataItemByNamePlural(objectNamePlural);
|
||||
|
||||
const { getIcon } = useIcons();
|
||||
const Icon = getIcon(
|
||||
@ -29,12 +32,13 @@ export const RecordIndexPageHeader = ({
|
||||
|
||||
const recordIndexViewType = useRecoilValue(recordIndexViewTypeState);
|
||||
|
||||
const canAddRecord =
|
||||
recordIndexViewType === ViewType.Table && !objectMetadataItem?.isRemote;
|
||||
|
||||
return (
|
||||
<PageHeader title={capitalize(objectNamePlural)} Icon={Icon}>
|
||||
<PageHotkeysEffect onAddButtonClick={createRecord} />
|
||||
{recordIndexViewType === ViewType.Table && (
|
||||
<PageAddButton onClick={createRecord} />
|
||||
)}
|
||||
{canAddRecord && <PageAddButton onClick={createRecord} />}
|
||||
</PageHeader>
|
||||
);
|
||||
};
|
||||
|
@ -5,7 +5,7 @@ import { IconPlus, IconSettings } from 'twenty-ui';
|
||||
|
||||
import { LABEL_IDENTIFIER_FIELD_METADATA_TYPES } from '@/object-metadata/constants/LabelIdentifierFieldMetadataTypes';
|
||||
import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem';
|
||||
import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings';
|
||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||
import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem';
|
||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { getActiveFieldMetadataItems } from '@/object-metadata/utils/getActiveFieldMetadataItems';
|
||||
@ -45,7 +45,7 @@ export const SettingsObjectDetail = () => {
|
||||
|
||||
const { objectSlug = '' } = useParams();
|
||||
const { findActiveObjectMetadataItemBySlug } =
|
||||
useObjectMetadataItemForSettings();
|
||||
useFilteredObjectMetadataItems();
|
||||
const { updateOneObjectMetadataItem } = useUpdateOneObjectMetadataItem();
|
||||
|
||||
const activeObjectMetadataItem =
|
||||
|
@ -6,7 +6,7 @@ import pick from 'lodash.pick';
|
||||
import { IconArchive, IconSettings } from 'twenty-ui';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings';
|
||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||
import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem';
|
||||
import { getObjectSlug } from '@/object-metadata/utils/getObjectSlug';
|
||||
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
|
||||
@ -44,7 +44,7 @@ export const SettingsObjectEdit = () => {
|
||||
|
||||
const { objectSlug = '' } = useParams();
|
||||
const { findActiveObjectMetadataItemBySlug } =
|
||||
useObjectMetadataItemForSettings();
|
||||
useFilteredObjectMetadataItems();
|
||||
const { updateOneObjectMetadataItem } = useUpdateOneObjectMetadataItem();
|
||||
|
||||
const activeObjectMetadataItem =
|
||||
|
@ -5,8 +5,8 @@ import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { IconArchive, IconSettings } from 'twenty-ui';
|
||||
|
||||
import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem';
|
||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||
import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata';
|
||||
import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings';
|
||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { getFieldSlug } from '@/object-metadata/utils/getFieldSlug';
|
||||
import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField';
|
||||
@ -52,7 +52,7 @@ export const SettingsObjectFieldEdit = () => {
|
||||
|
||||
const { objectSlug = '', fieldSlug = '' } = useParams();
|
||||
const { findActiveObjectMetadataItemBySlug } =
|
||||
useObjectMetadataItemForSettings();
|
||||
useFilteredObjectMetadataItems();
|
||||
|
||||
const activeObjectMetadataItem =
|
||||
findActiveObjectMetadataItemBySlug(objectSlug);
|
||||
|
@ -4,7 +4,7 @@ import styled from '@emotion/styled';
|
||||
import { IconMinus, IconPlus, IconSettings } from 'twenty-ui';
|
||||
|
||||
import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem';
|
||||
import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings';
|
||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||
import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField';
|
||||
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
|
||||
import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer';
|
||||
@ -39,7 +39,7 @@ export const SettingsObjectNewFieldStep1 = () => {
|
||||
|
||||
const { objectSlug = '' } = useParams();
|
||||
const { findActiveObjectMetadataItemBySlug } =
|
||||
useObjectMetadataItemForSettings();
|
||||
useFilteredObjectMetadataItems();
|
||||
|
||||
const activeObjectMetadataItem =
|
||||
findActiveObjectMetadataItemBySlug(objectSlug);
|
||||
@ -114,11 +114,13 @@ export const SettingsObjectNewFieldStep1 = () => {
|
||||
{ children: 'New Field' },
|
||||
]}
|
||||
/>
|
||||
<SaveAndCancelButtons
|
||||
isSaveDisabled={!canSave}
|
||||
onCancel={() => navigate(`/settings/objects/${objectSlug}`)}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
{!activeObjectMetadataItem.isRemote && (
|
||||
<SaveAndCancelButtons
|
||||
isSaveDisabled={!canSave}
|
||||
onCancel={() => navigate(`/settings/objects/${objectSlug}`)}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
)}
|
||||
</SettingsHeaderContainer>
|
||||
<StyledSection>
|
||||
<H2Title
|
||||
|
@ -7,7 +7,7 @@ import { IconSettings } from 'twenty-ui';
|
||||
import { CachedObjectRecordEdge } from '@/apollo/types/CachedObjectRecordEdge';
|
||||
import { useCreateOneRelationMetadataItem } from '@/object-metadata/hooks/useCreateOneRelationMetadataItem';
|
||||
import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem';
|
||||
import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings';
|
||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||
import { useObjectMetadataItemOnly } from '@/object-metadata/hooks/useObjectMetadataItemOnly';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { modifyRecordFromCache } from '@/object-record/cache/utils/modifyRecordFromCache';
|
||||
@ -46,7 +46,7 @@ export const SettingsObjectNewFieldStep2 = () => {
|
||||
findActiveObjectMetadataItemBySlug,
|
||||
findObjectMetadataItemById,
|
||||
findObjectMetadataItemByNamePlural,
|
||||
} = useObjectMetadataItemForSettings();
|
||||
} = useFilteredObjectMetadataItems();
|
||||
|
||||
const activeObjectMetadataItem =
|
||||
findActiveObjectMetadataItemBySlug(objectSlug);
|
||||
@ -300,11 +300,13 @@ export const SettingsObjectNewFieldStep2 = () => {
|
||||
{ children: 'New Field' },
|
||||
]}
|
||||
/>
|
||||
<SaveAndCancelButtons
|
||||
isSaveDisabled={!canSave}
|
||||
onCancel={() => navigate(`/settings/objects/${objectSlug}`)}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
{!activeObjectMetadataItem.isRemote && (
|
||||
<SaveAndCancelButtons
|
||||
isSaveDisabled={!canSave}
|
||||
onCancel={() => navigate(`/settings/objects/${objectSlug}`)}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
)}
|
||||
</SettingsHeaderContainer>
|
||||
<SettingsObjectFieldFormSection
|
||||
iconKey={formValues.icon}
|
||||
|
@ -4,7 +4,7 @@ import styled from '@emotion/styled';
|
||||
import { IconChevronRight, IconPlus, IconSettings } from 'twenty-ui';
|
||||
|
||||
import { useDeleteOneObjectMetadataItem } from '@/object-metadata/hooks/useDeleteOneObjectMetadataItem';
|
||||
import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings';
|
||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||
import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem';
|
||||
import { getObjectSlug } from '@/object-metadata/utils/getObjectSlug';
|
||||
import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer';
|
||||
@ -39,7 +39,7 @@ export const SettingsObjects = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { activeObjectMetadataItems, inactiveObjectMetadataItems } =
|
||||
useObjectMetadataItemForSettings();
|
||||
useFilteredObjectMetadataItems();
|
||||
const { deleteOneObjectMetadataItem } = useDeleteOneObjectMetadataItem();
|
||||
const { updateOneObjectMetadataItem } = useUpdateOneObjectMetadataItem();
|
||||
|
||||
|
@ -46,6 +46,7 @@ import { EnvironmentService } from 'src/engine/integrations/environment/environm
|
||||
import { NotFoundError } from 'src/engine/utils/graphql-errors.util';
|
||||
import { QueryRunnerArgsFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory';
|
||||
import { QueryResultGettersFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters.factory';
|
||||
import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util';
|
||||
|
||||
import { WorkspaceQueryRunnerOptions } from './interfaces/query-runner-option.interface';
|
||||
import {
|
||||
@ -217,6 +218,9 @@ export class WorkspaceQueryRunnerService {
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<Record[] | undefined> {
|
||||
const { workspaceId, userId, objectMetadataItem } = options;
|
||||
|
||||
assertMutationNotOnRemoteObject(objectMetadataItem);
|
||||
|
||||
const computedArgs = await this.queryRunnerArgsFactory.create(
|
||||
args,
|
||||
options,
|
||||
@ -273,6 +277,8 @@ export class WorkspaceQueryRunnerService {
|
||||
): Promise<Record | undefined> {
|
||||
const { workspaceId, userId, objectMetadataItem } = options;
|
||||
|
||||
assertMutationNotOnRemoteObject(objectMetadataItem);
|
||||
|
||||
const existingRecord = await this.findOne(
|
||||
{ filter: { id: { eq: args.id } } } as FindOneResolverArgs,
|
||||
options,
|
||||
@ -318,6 +324,9 @@ export class WorkspaceQueryRunnerService {
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<Record[] | undefined> {
|
||||
const { workspaceId, objectMetadataItem } = options;
|
||||
|
||||
assertMutationNotOnRemoteObject(objectMetadataItem);
|
||||
|
||||
const maximumRecordAffected = this.environmentService.get(
|
||||
'MUTATION_MAXIMUM_RECORD_AFFECTED',
|
||||
);
|
||||
@ -359,6 +368,9 @@ export class WorkspaceQueryRunnerService {
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<Record[] | undefined> {
|
||||
const { workspaceId, userId, objectMetadataItem } = options;
|
||||
|
||||
assertMutationNotOnRemoteObject(objectMetadataItem);
|
||||
|
||||
const maximumRecordAffected = this.environmentService.get(
|
||||
'MUTATION_MAXIMUM_RECORD_AFFECTED',
|
||||
);
|
||||
@ -403,6 +415,9 @@ export class WorkspaceQueryRunnerService {
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<Record | undefined> {
|
||||
const { workspaceId, userId, objectMetadataItem } = options;
|
||||
|
||||
assertMutationNotOnRemoteObject(objectMetadataItem);
|
||||
|
||||
const query = await this.workspaceQueryBuilderFactory.deleteOne(
|
||||
args,
|
||||
options,
|
||||
|
@ -38,6 +38,7 @@ import {
|
||||
} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
|
||||
import { DeleteOneFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/delete-field.input';
|
||||
import { computeCustomName } from 'src/engine/utils/compute-custom-name.util';
|
||||
import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util';
|
||||
|
||||
import {
|
||||
FieldMetadataEntity,
|
||||
@ -94,6 +95,10 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
throw new NotFoundException('Object does not exist');
|
||||
}
|
||||
|
||||
if (!fieldMetadataInput.isRemoteCreation) {
|
||||
assertMutationNotOnRemoteObject(objectMetadata);
|
||||
}
|
||||
|
||||
// Double check in case the service is directly called
|
||||
if (isEnumFieldMetadataType(fieldMetadataInput.type)) {
|
||||
if (
|
||||
@ -273,6 +278,8 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
throw new NotFoundException('Object does not exist');
|
||||
}
|
||||
|
||||
assertMutationNotOnRemoteObject(objectMetadata);
|
||||
|
||||
if (
|
||||
objectMetadata.labelIdentifierFieldMetadataId ===
|
||||
existingFieldMetadata.id &&
|
||||
|
@ -0,0 +1,9 @@
|
||||
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
|
||||
|
||||
export const assertMutationNotOnRemoteObject = (
|
||||
objectMetadataItem: ObjectMetadataInterface,
|
||||
) => {
|
||||
if (objectMetadataItem.isRemote) {
|
||||
throw new Error('Remote objects are read-only');
|
||||
}
|
||||
};
|
Loading…
Reference in New Issue
Block a user