mirror of
				https://github.com/strapi/strapi.git
				synced 2025-11-04 03:43:34 +00:00 
			
		
		
		
	feat: make widgets api stable (#23470)
This commit is contained in:
		
							parent
							
								
									7ae14fd44d
								
							
						
					
					
						commit
						8c28a74d12
					
				@ -1,5 +1,3 @@
 | 
				
			|||||||
module.exports = ({ env }) => ({
 | 
					module.exports = ({ env }) => ({
 | 
				
			||||||
  future: {
 | 
					  future: {},
 | 
				
			||||||
    unstableWidgetsApi: true,
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
				
			|||||||
@ -1,7 +1,7 @@
 | 
				
			|||||||
import * as React from 'react';
 | 
					import * as React from 'react';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { Box, Flex, Grid, Main, Typography } from '@strapi/design-system';
 | 
					import { Box, Flex, Grid, Main, Typography } from '@strapi/design-system';
 | 
				
			||||||
import { CheckCircle, Pencil, PuzzlePiece } from '@strapi/icons';
 | 
					import { PuzzlePiece } from '@strapi/icons';
 | 
				
			||||||
import { useIntl } from 'react-intl';
 | 
					import { useIntl } from 'react-intl';
 | 
				
			||||||
import { Link as ReactRouterLink } from 'react-router-dom';
 | 
					import { Link as ReactRouterLink } from 'react-router-dom';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -12,7 +12,6 @@ import { useEnterprise } from '../../ee';
 | 
				
			|||||||
import { useAuth } from '../../features/Auth';
 | 
					import { useAuth } from '../../features/Auth';
 | 
				
			||||||
import { useStrapiApp } from '../../features/StrapiApp';
 | 
					import { useStrapiApp } from '../../features/StrapiApp';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { LastEditedWidget, LastPublishedWidget } from './components/ContentManagerWidgets';
 | 
					 | 
				
			||||||
import { GuidedTour } from './components/GuidedTour';
 | 
					import { GuidedTour } from './components/GuidedTour';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import type { WidgetType } from '@strapi/admin/strapi-admin';
 | 
					import type { WidgetType } from '@strapi/admin/strapi-admin';
 | 
				
			||||||
@ -47,12 +46,7 @@ export const WidgetRoot = ({
 | 
				
			|||||||
      setPermissionStatus(shouldGrant ? 'granted' : 'forbidden');
 | 
					      setPermissionStatus(shouldGrant ? 'granted' : 'forbidden');
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (
 | 
					    if (!permissions || permissions.length === 0) {
 | 
				
			||||||
      // TODO: remove unstable check once widgets API is stable
 | 
					 | 
				
			||||||
      !window.strapi.future.isEnabled('unstableWidgetsApi') ||
 | 
					 | 
				
			||||||
      !permissions ||
 | 
					 | 
				
			||||||
      permissions.length === 0
 | 
					 | 
				
			||||||
    ) {
 | 
					 | 
				
			||||||
      setPermissionStatus('granted');
 | 
					      setPermissionStatus('granted');
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      checkPermissions();
 | 
					      checkPermissions();
 | 
				
			||||||
@ -127,7 +121,11 @@ const WidgetComponent = ({ component }: { component: () => Promise<React.Compone
 | 
				
			|||||||
  return <Component />;
 | 
					  return <Component />;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const UnstableHomePageCe = () => {
 | 
					/* -------------------------------------------------------------------------------------------------
 | 
				
			||||||
 | 
					 * HomePageCE
 | 
				
			||||||
 | 
					 * -----------------------------------------------------------------------------------------------*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const HomePageCE = () => {
 | 
				
			||||||
  const { formatMessage } = useIntl();
 | 
					  const { formatMessage } = useIntl();
 | 
				
			||||||
  const user = useAuth('HomePageCE', (state) => state.user);
 | 
					  const user = useAuth('HomePageCE', (state) => state.user);
 | 
				
			||||||
  const displayName = user?.firstname ?? user?.username ?? user?.email;
 | 
					  const displayName = user?.firstname ?? user?.username ?? user?.email;
 | 
				
			||||||
@ -174,67 +172,6 @@ const UnstableHomePageCe = () => {
 | 
				
			|||||||
  );
 | 
					  );
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/* -------------------------------------------------------------------------------------------------
 | 
					 | 
				
			||||||
 * HomePageCE
 | 
					 | 
				
			||||||
 * -----------------------------------------------------------------------------------------------*/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const HomePageCE = () => {
 | 
					 | 
				
			||||||
  const { formatMessage } = useIntl();
 | 
					 | 
				
			||||||
  const user = useAuth('HomePageCE', (state) => state.user);
 | 
					 | 
				
			||||||
  const displayName = user?.firstname ?? user?.username ?? user?.email;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (window.strapi.future.isEnabled('unstableWidgetsApi')) {
 | 
					 | 
				
			||||||
    return <UnstableHomePageCe />;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return (
 | 
					 | 
				
			||||||
    <Main>
 | 
					 | 
				
			||||||
      <Page.Title>
 | 
					 | 
				
			||||||
        {formatMessage({ id: 'HomePage.head.title', defaultMessage: 'Homepage' })}
 | 
					 | 
				
			||||||
      </Page.Title>
 | 
					 | 
				
			||||||
      <Layouts.Header
 | 
					 | 
				
			||||||
        title={formatMessage(
 | 
					 | 
				
			||||||
          { id: 'HomePage.header.title', defaultMessage: 'Hello {name}' },
 | 
					 | 
				
			||||||
          { name: displayName }
 | 
					 | 
				
			||||||
        )}
 | 
					 | 
				
			||||||
        subtitle={formatMessage({
 | 
					 | 
				
			||||||
          id: 'HomePage.header.subtitle',
 | 
					 | 
				
			||||||
          defaultMessage: 'Welcome to your administration panel',
 | 
					 | 
				
			||||||
        })}
 | 
					 | 
				
			||||||
      />
 | 
					 | 
				
			||||||
      <Layouts.Content>
 | 
					 | 
				
			||||||
        <Flex direction="column" alignItems="stretch" gap={8} paddingBottom={10}>
 | 
					 | 
				
			||||||
          <GuidedTour />
 | 
					 | 
				
			||||||
          <Grid.Root gap={5}>
 | 
					 | 
				
			||||||
            <Grid.Item col={6} s={12}>
 | 
					 | 
				
			||||||
              <WidgetRoot
 | 
					 | 
				
			||||||
                title={{
 | 
					 | 
				
			||||||
                  id: 'content-manager.widget.last-edited.title',
 | 
					 | 
				
			||||||
                  defaultMessage: 'Last edited entries',
 | 
					 | 
				
			||||||
                }}
 | 
					 | 
				
			||||||
                icon={Pencil}
 | 
					 | 
				
			||||||
              >
 | 
					 | 
				
			||||||
                <LastEditedWidget />
 | 
					 | 
				
			||||||
              </WidgetRoot>
 | 
					 | 
				
			||||||
            </Grid.Item>
 | 
					 | 
				
			||||||
            <Grid.Item col={6} s={12}>
 | 
					 | 
				
			||||||
              <WidgetRoot
 | 
					 | 
				
			||||||
                title={{
 | 
					 | 
				
			||||||
                  id: 'content-manager.widget.last-published.title',
 | 
					 | 
				
			||||||
                  defaultMessage: 'Last published entries',
 | 
					 | 
				
			||||||
                }}
 | 
					 | 
				
			||||||
                icon={CheckCircle}
 | 
					 | 
				
			||||||
              >
 | 
					 | 
				
			||||||
                <LastPublishedWidget />
 | 
					 | 
				
			||||||
              </WidgetRoot>
 | 
					 | 
				
			||||||
            </Grid.Item>
 | 
					 | 
				
			||||||
          </Grid.Root>
 | 
					 | 
				
			||||||
        </Flex>
 | 
					 | 
				
			||||||
      </Layouts.Content>
 | 
					 | 
				
			||||||
    </Main>
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/* -------------------------------------------------------------------------------------------------
 | 
					/* -------------------------------------------------------------------------------------------------
 | 
				
			||||||
 * HomePage
 | 
					 * HomePage
 | 
				
			||||||
 * -----------------------------------------------------------------------------------------------*/
 | 
					 * -----------------------------------------------------------------------------------------------*/
 | 
				
			||||||
 | 
				
			|||||||
@ -1,184 +0,0 @@
 | 
				
			|||||||
import { Box, IconButton, Status, Table, Tbody, Td, Tr, Typography } from '@strapi/design-system';
 | 
					 | 
				
			||||||
import { Pencil } from '@strapi/icons';
 | 
					 | 
				
			||||||
import { useIntl } from 'react-intl';
 | 
					 | 
				
			||||||
import { Link, useNavigate } from 'react-router-dom';
 | 
					 | 
				
			||||||
import { styled } from 'styled-components';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import { RelativeTime } from '../../../components/RelativeTime';
 | 
					 | 
				
			||||||
import { Widget } from '../../../components/WidgetHelpers';
 | 
					 | 
				
			||||||
import { useTracking } from '../../../features/Tracking';
 | 
					 | 
				
			||||||
import { useGetRecentDocumentsQuery } from '../../../services/homepage';
 | 
					 | 
				
			||||||
import { capitalise } from '../../../utils/strings';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import type { RecentDocument } from '../../../../../shared/contracts/homepage';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const CellTypography = styled(Typography).attrs({ maxWidth: '14.4rem', display: 'block' })`
 | 
					 | 
				
			||||||
  overflow: hidden;
 | 
					 | 
				
			||||||
  text-overflow: ellipsis;
 | 
					 | 
				
			||||||
  white-space: nowrap;
 | 
					 | 
				
			||||||
`;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
interface DocumentStatusProps {
 | 
					 | 
				
			||||||
  status: RecentDocument['status'];
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const DocumentStatus = ({ status = 'draft' }: DocumentStatusProps) => {
 | 
					 | 
				
			||||||
  const statusVariant =
 | 
					 | 
				
			||||||
    status === 'draft' ? 'secondary' : status === 'published' ? 'success' : 'alternative';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const { formatMessage } = useIntl();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return (
 | 
					 | 
				
			||||||
    <Status variant={statusVariant} size="XS">
 | 
					 | 
				
			||||||
      <Typography tag="span" variant="omega" fontWeight="bold">
 | 
					 | 
				
			||||||
        {formatMessage({
 | 
					 | 
				
			||||||
          id: `content-manager.containers.List.${status}`,
 | 
					 | 
				
			||||||
          defaultMessage: capitalise(status),
 | 
					 | 
				
			||||||
        })}
 | 
					 | 
				
			||||||
      </Typography>
 | 
					 | 
				
			||||||
    </Status>
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const RecentDocumentsTable = ({ documents }: { documents: RecentDocument[] }) => {
 | 
					 | 
				
			||||||
  const { formatMessage } = useIntl();
 | 
					 | 
				
			||||||
  const { trackUsage } = useTracking();
 | 
					 | 
				
			||||||
  const navigate = useNavigate();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const getEditViewLink = (document: RecentDocument): string => {
 | 
					 | 
				
			||||||
    const isSingleType = document.kind === 'singleType';
 | 
					 | 
				
			||||||
    const kindPath = isSingleType ? 'single-types' : 'collection-types';
 | 
					 | 
				
			||||||
    const queryParams = document.locale ? `?plugins[i18n][locale]=${document.locale}` : '';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return `/content-manager/${kindPath}/${document.contentTypeUid}${isSingleType ? '' : '/' + document.documentId}${queryParams}`;
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const handleRowClick = (document: RecentDocument) => () => {
 | 
					 | 
				
			||||||
    trackUsage('willEditEntryFromHome');
 | 
					 | 
				
			||||||
    const link = getEditViewLink(document);
 | 
					 | 
				
			||||||
    navigate(link);
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return (
 | 
					 | 
				
			||||||
    <Table colCount={5} rowCount={documents?.length ?? 0}>
 | 
					 | 
				
			||||||
      <Tbody>
 | 
					 | 
				
			||||||
        {documents?.map((document) => (
 | 
					 | 
				
			||||||
          <Tr onClick={handleRowClick(document)} cursor="pointer" key={document.documentId}>
 | 
					 | 
				
			||||||
            <Td>
 | 
					 | 
				
			||||||
              <CellTypography title={document.title} variant="omega" textColor="neutral800">
 | 
					 | 
				
			||||||
                {document.title}
 | 
					 | 
				
			||||||
              </CellTypography>
 | 
					 | 
				
			||||||
            </Td>
 | 
					 | 
				
			||||||
            <Td>
 | 
					 | 
				
			||||||
              <CellTypography variant="omega" textColor="neutral600">
 | 
					 | 
				
			||||||
                {document.kind === 'singleType'
 | 
					 | 
				
			||||||
                  ? formatMessage({
 | 
					 | 
				
			||||||
                      id: 'content-manager.widget.last-edited.single-type',
 | 
					 | 
				
			||||||
                      defaultMessage: 'Single-Type',
 | 
					 | 
				
			||||||
                    })
 | 
					 | 
				
			||||||
                  : formatMessage({
 | 
					 | 
				
			||||||
                      id: document.contentTypeDisplayName,
 | 
					 | 
				
			||||||
                      defaultMessage: document.contentTypeDisplayName,
 | 
					 | 
				
			||||||
                    })}
 | 
					 | 
				
			||||||
              </CellTypography>
 | 
					 | 
				
			||||||
            </Td>
 | 
					 | 
				
			||||||
            <Td>
 | 
					 | 
				
			||||||
              <Box display="inline-block">
 | 
					 | 
				
			||||||
                {document.status ? (
 | 
					 | 
				
			||||||
                  <DocumentStatus status={document.status} />
 | 
					 | 
				
			||||||
                ) : (
 | 
					 | 
				
			||||||
                  <Typography textColor="neutral600" aria-hidden>
 | 
					 | 
				
			||||||
                    -
 | 
					 | 
				
			||||||
                  </Typography>
 | 
					 | 
				
			||||||
                )}
 | 
					 | 
				
			||||||
              </Box>
 | 
					 | 
				
			||||||
            </Td>
 | 
					 | 
				
			||||||
            <Td>
 | 
					 | 
				
			||||||
              <Typography textColor="neutral600">
 | 
					 | 
				
			||||||
                <RelativeTime timestamp={new Date(document.updatedAt)} />
 | 
					 | 
				
			||||||
              </Typography>
 | 
					 | 
				
			||||||
            </Td>
 | 
					 | 
				
			||||||
            <Td onClick={(e) => e.stopPropagation()}>
 | 
					 | 
				
			||||||
              <Box display="inline-block">
 | 
					 | 
				
			||||||
                <IconButton
 | 
					 | 
				
			||||||
                  tag={Link}
 | 
					 | 
				
			||||||
                  to={getEditViewLink(document)}
 | 
					 | 
				
			||||||
                  onClick={() => trackUsage('willEditEntryFromHome')}
 | 
					 | 
				
			||||||
                  label={formatMessage({
 | 
					 | 
				
			||||||
                    id: 'content-manager.actions.edit.label',
 | 
					 | 
				
			||||||
                    defaultMessage: 'Edit',
 | 
					 | 
				
			||||||
                  })}
 | 
					 | 
				
			||||||
                  variant="ghost"
 | 
					 | 
				
			||||||
                >
 | 
					 | 
				
			||||||
                  <Pencil />
 | 
					 | 
				
			||||||
                </IconButton>
 | 
					 | 
				
			||||||
              </Box>
 | 
					 | 
				
			||||||
            </Td>
 | 
					 | 
				
			||||||
          </Tr>
 | 
					 | 
				
			||||||
        ))}
 | 
					 | 
				
			||||||
      </Tbody>
 | 
					 | 
				
			||||||
    </Table>
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/* -------------------------------------------------------------------------------------------------
 | 
					 | 
				
			||||||
 * LastEditedWidget
 | 
					 | 
				
			||||||
 * -----------------------------------------------------------------------------------------------*/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const LastEditedWidget = () => {
 | 
					 | 
				
			||||||
  const { formatMessage } = useIntl();
 | 
					 | 
				
			||||||
  const { data, isLoading, error } = useGetRecentDocumentsQuery({ action: 'update' });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (isLoading) {
 | 
					 | 
				
			||||||
    return <Widget.Loading />;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (error || !data) {
 | 
					 | 
				
			||||||
    return <Widget.Error />;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (data.length === 0) {
 | 
					 | 
				
			||||||
    return (
 | 
					 | 
				
			||||||
      <Widget.NoData>
 | 
					 | 
				
			||||||
        {formatMessage({
 | 
					 | 
				
			||||||
          id: 'content-manager.widget.last-edited.no-data',
 | 
					 | 
				
			||||||
          defaultMessage: 'No edited entries',
 | 
					 | 
				
			||||||
        })}
 | 
					 | 
				
			||||||
      </Widget.NoData>
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return <RecentDocumentsTable documents={data} />;
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/* -------------------------------------------------------------------------------------------------
 | 
					 | 
				
			||||||
 * LastPublishedWidget
 | 
					 | 
				
			||||||
 * -----------------------------------------------------------------------------------------------*/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const LastPublishedWidget = () => {
 | 
					 | 
				
			||||||
  const { formatMessage } = useIntl();
 | 
					 | 
				
			||||||
  const { data, isLoading, error } = useGetRecentDocumentsQuery({ action: 'publish' });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (isLoading) {
 | 
					 | 
				
			||||||
    return <Widget.Loading />;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (error || !data) {
 | 
					 | 
				
			||||||
    return <Widget.Error />;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (data.length === 0) {
 | 
					 | 
				
			||||||
    return (
 | 
					 | 
				
			||||||
      <Widget.NoData>
 | 
					 | 
				
			||||||
        {formatMessage({
 | 
					 | 
				
			||||||
          id: 'content-manager.widget.last-published.no-data',
 | 
					 | 
				
			||||||
          defaultMessage: 'No published entries',
 | 
					 | 
				
			||||||
        })}
 | 
					 | 
				
			||||||
      </Widget.NoData>
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return <RecentDocumentsTable documents={data} />;
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export { LastEditedWidget, LastPublishedWidget };
 | 
					 | 
				
			||||||
@ -1,29 +0,0 @@
 | 
				
			|||||||
import * as Homepage from '../../../shared/contracts/homepage';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import { adminApi } from './api';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * TODO: Remove this service when the future flag for the widget api is removed
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
const homepageService = adminApi
 | 
					 | 
				
			||||||
  .enhanceEndpoints({
 | 
					 | 
				
			||||||
    addTagTypes: ['RecentDocumentList'],
 | 
					 | 
				
			||||||
  })
 | 
					 | 
				
			||||||
  .injectEndpoints({
 | 
					 | 
				
			||||||
    endpoints: (builder) => ({
 | 
					 | 
				
			||||||
      getRecentDocuments: builder.query<
 | 
					 | 
				
			||||||
        Homepage.GetRecentDocuments.Response['data'],
 | 
					 | 
				
			||||||
        Homepage.GetRecentDocuments.Request['query']
 | 
					 | 
				
			||||||
      >({
 | 
					 | 
				
			||||||
        query: (params) => `/content-manager/homepage/recent-documents?action=${params.action}`,
 | 
					 | 
				
			||||||
        transformResponse: (response: Homepage.GetRecentDocuments.Response) => response.data,
 | 
					 | 
				
			||||||
        providesTags: (res, _err, { action }) => [
 | 
					 | 
				
			||||||
          { type: 'RecentDocumentList' as const, id: action },
 | 
					 | 
				
			||||||
        ],
 | 
					 | 
				
			||||||
      }),
 | 
					 | 
				
			||||||
    }),
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const { useGetRecentDocumentsQuery } = homepageService;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export { useGetRecentDocumentsQuery };
 | 
					 | 
				
			||||||
@ -1,7 +1,5 @@
 | 
				
			|||||||
export interface FeaturesConfig {
 | 
					export interface FeaturesConfig {
 | 
				
			||||||
  future?: {
 | 
					  future?: object;
 | 
				
			||||||
    unstableWidgetsApi?: boolean;
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface FeaturesService {
 | 
					export interface FeaturesService {
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user