Introduce ability to add custom features
Also, add more metadata to success callbacks Signed-off-by: Erik Engervall <erik.engervall@gmail.com>
This commit is contained in:
@@ -16,7 +16,7 @@
|
||||
|
||||
import React from 'react';
|
||||
import { createDevApp } from '@backstage/dev-utils';
|
||||
import { Box, Typography } from '@material-ui/core';
|
||||
import { Box, Button, Typography } from '@material-ui/core';
|
||||
|
||||
import { gitReleaseManagerPlugin, GitReleaseManagerPage } from '../src/plugin';
|
||||
import { InfoCardPlus } from '../src/components/InfoCardPlus';
|
||||
@@ -44,10 +44,16 @@ createDevApp()
|
||||
<Box padding={4}>
|
||||
<InfoCardPlus>
|
||||
<Typography variant="h4">Dev notes</Typography>
|
||||
|
||||
<Typography>
|
||||
Configure plugin statically by passing props to the
|
||||
`GitHubReleaseManagerPage` component
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body2">
|
||||
Note that the static configuration points towards private
|
||||
repositories and will thus not work for everyone.
|
||||
</Typography>
|
||||
</InfoCardPlus>
|
||||
|
||||
<GitReleaseManagerPage
|
||||
@@ -67,8 +73,10 @@ createDevApp()
|
||||
<Box padding={4}>
|
||||
<InfoCardPlus>
|
||||
<Typography variant="h4">Dev notes</Typography>
|
||||
<Typography>Each feature can be omitted</Typography>
|
||||
<Typography>Success callbacks can also be added</Typography>
|
||||
<Typography>
|
||||
Each feature can be individually omitted as well as have success
|
||||
callback attached to them
|
||||
</Typography>
|
||||
</InfoCardPlus>
|
||||
|
||||
<GitReleaseManagerPage
|
||||
@@ -79,22 +87,12 @@ createDevApp()
|
||||
}}
|
||||
features={{
|
||||
createRc: {
|
||||
onSuccess: ({
|
||||
comparisonUrl,
|
||||
createdTag,
|
||||
gitReleaseName,
|
||||
gitReleaseUrl,
|
||||
previousTag,
|
||||
}) => {
|
||||
onSuccess: args => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(
|
||||
'Custom success callback for Create RC',
|
||||
comparisonUrl,
|
||||
createdTag,
|
||||
gitReleaseName,
|
||||
gitReleaseUrl,
|
||||
previousTag,
|
||||
'Custom success callback for Create RC with the following args',
|
||||
);
|
||||
console.log(JSON.stringify(args, null, 2)); // eslint-disable-line no-console
|
||||
},
|
||||
},
|
||||
promoteRc: {
|
||||
@@ -108,4 +106,50 @@ createDevApp()
|
||||
</Box>
|
||||
),
|
||||
})
|
||||
.addPage({
|
||||
title: 'Custom',
|
||||
path: '/custom',
|
||||
element: (
|
||||
<Box padding={4}>
|
||||
<InfoCardPlus>
|
||||
<Typography variant="h4">Dev notes</Typography>
|
||||
<Typography>
|
||||
The custom feature's return value can either be a React Element or
|
||||
an array of React Elements.
|
||||
</Typography>
|
||||
</InfoCardPlus>
|
||||
|
||||
<GitReleaseManagerPage
|
||||
project={{
|
||||
owner: 'eengervall-playground',
|
||||
repo: 'playground-semver',
|
||||
versioningStrategy: 'semver',
|
||||
}}
|
||||
features={{
|
||||
custom: {
|
||||
factory: args => {
|
||||
return (
|
||||
<InfoCardPlus>
|
||||
<Typography variant="h4">I'm a custom feature</Typography>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
console.log(`Here's my args 🚀`); // eslint-disable-line no-console
|
||||
console.log(JSON.stringify(args, null, 2)); // eslint-disable-line no-console
|
||||
}}
|
||||
>
|
||||
View the arguments for this feature in the console by
|
||||
pressing this button
|
||||
</Button>
|
||||
</InfoCardPlus>
|
||||
);
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
),
|
||||
})
|
||||
.render();
|
||||
|
||||
@@ -18,12 +18,14 @@ import React from 'react';
|
||||
import { useAsync } from 'react-use';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
import { Box } from '@material-ui/core';
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
import { ContentHeader, Progress } from '@backstage/core-components';
|
||||
|
||||
import {
|
||||
ComponentConfig,
|
||||
ComponentConfigCreateRc,
|
||||
ComponentConfigPatch,
|
||||
ComponentConfigPromoteRc,
|
||||
CreateRcOnSuccessArgs,
|
||||
PatchOnSuccessArgs,
|
||||
PromoteRcOnSuccessArgs,
|
||||
} from './types/types';
|
||||
import { Features } from './features/Features';
|
||||
import { gitReleaseManagerApiRef } from './api/serviceApiRef';
|
||||
@@ -33,18 +35,33 @@ import { ProjectContext, Project } from './contexts/ProjectContext';
|
||||
import { RepoDetailsForm } from './features/RepoDetailsForm/RepoDetailsForm';
|
||||
import { useQueryHandler } from './hooks/useQueryHandler';
|
||||
import { UserContext } from './contexts/UserContext';
|
||||
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
import { ContentHeader, Progress } from '@backstage/core-components';
|
||||
import {
|
||||
GetBranchResult,
|
||||
GetLatestReleaseResult,
|
||||
GetRepositoryResult,
|
||||
} from './api/GitReleaseClient';
|
||||
|
||||
interface GitReleaseManagerProps {
|
||||
project?: Omit<Project, 'isProvidedViaProps'>;
|
||||
features?: {
|
||||
info?: Pick<ComponentConfig<void>, 'omit'>;
|
||||
stats?: Pick<ComponentConfig<void>, 'omit'>;
|
||||
createRc?: ComponentConfigCreateRc;
|
||||
promoteRc?: ComponentConfigPromoteRc;
|
||||
patch?: ComponentConfigPatch;
|
||||
createRc?: ComponentConfig<CreateRcOnSuccessArgs>;
|
||||
promoteRc?: ComponentConfig<PromoteRcOnSuccessArgs>;
|
||||
patch?: ComponentConfig<PatchOnSuccessArgs>;
|
||||
custom?: {
|
||||
factory: ({
|
||||
latestRelease,
|
||||
project,
|
||||
releaseBranch,
|
||||
repository,
|
||||
}: {
|
||||
latestRelease: GetLatestReleaseResult['latestRelease'] | null;
|
||||
project: Project;
|
||||
releaseBranch: GetBranchResult['branch'] | null;
|
||||
repository: GetRepositoryResult['repository'];
|
||||
}) => React.ReactElement | React.ReactElement[];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -31,7 +31,7 @@ import {
|
||||
GetLatestReleaseResult,
|
||||
GetRepositoryResult,
|
||||
} from '../../api/GitReleaseClient';
|
||||
import { ComponentConfigCreateRc } from '../../types/types';
|
||||
import { ComponentConfig, CreateRcOnSuccessArgs } from '../../types/types';
|
||||
import { Differ } from '../../components/Differ';
|
||||
import { getReleaseCandidateGitInfo } from '../../helpers/getReleaseCandidateGitInfo';
|
||||
import { InfoCardPlus } from '../../components/InfoCardPlus';
|
||||
@@ -45,7 +45,7 @@ interface CreateReleaseCandidateProps {
|
||||
defaultBranch: GetRepositoryResult['repository']['defaultBranch'];
|
||||
latestRelease: GetLatestReleaseResult['latestRelease'];
|
||||
releaseBranch: GetBranchResult['branch'] | null;
|
||||
onSuccess?: ComponentConfigCreateRc['onSuccess'];
|
||||
onSuccess?: ComponentConfig<CreateRcOnSuccessArgs>['onSuccess'];
|
||||
}
|
||||
|
||||
const InfoCardPlusWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
|
||||
+13
-3
@@ -21,7 +21,11 @@ import {
|
||||
GetRepositoryResult,
|
||||
} from '../../../api/GitReleaseClient';
|
||||
|
||||
import { CardHook, ComponentConfigCreateRc } from '../../../types/types';
|
||||
import {
|
||||
CardHook,
|
||||
ComponentConfig,
|
||||
CreateRcOnSuccessArgs,
|
||||
} from '../../../types/types';
|
||||
import { getReleaseCandidateGitInfo } from '../../../helpers/getReleaseCandidateGitInfo';
|
||||
import { gitReleaseManagerApiRef } from '../../../api/serviceApiRef';
|
||||
import { GitReleaseManagerError } from '../../../errors/GitReleaseManagerError';
|
||||
@@ -31,12 +35,12 @@ import { useResponseSteps } from '../../../hooks/useResponseSteps';
|
||||
import { useUserContext } from '../../../contexts/UserContext';
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
|
||||
interface UseCreateReleaseCandidate {
|
||||
export interface UseCreateReleaseCandidate {
|
||||
defaultBranch: GetRepositoryResult['repository']['defaultBranch'];
|
||||
latestRelease: GetLatestReleaseResult['latestRelease'];
|
||||
releaseCandidateGitInfo: ReturnType<typeof getReleaseCandidateGitInfo>;
|
||||
project: Project;
|
||||
onSuccess?: ComponentConfigCreateRc['onSuccess'];
|
||||
onSuccess?: ComponentConfig<CreateRcOnSuccessArgs>['onSuccess'];
|
||||
}
|
||||
|
||||
export function useCreateReleaseCandidate({
|
||||
@@ -266,6 +270,12 @@ export function useCreateReleaseCandidate({
|
||||
|
||||
try {
|
||||
await onSuccess({
|
||||
input: {
|
||||
defaultBranch,
|
||||
latestRelease,
|
||||
releaseCandidateGitInfo,
|
||||
project,
|
||||
},
|
||||
comparisonUrl: getComparisonRes.value.htmlUrl,
|
||||
createdTag: createReleaseRes.value.tagName,
|
||||
gitReleaseName: createReleaseRes.value.name,
|
||||
|
||||
@@ -142,6 +142,14 @@ export function Features({
|
||||
onSuccess={features?.patch?.onSuccess}
|
||||
/>
|
||||
)}
|
||||
|
||||
{features?.custom?.factory &&
|
||||
features.custom.factory({
|
||||
latestRelease: gitBatchInfo.value.latestRelease,
|
||||
project,
|
||||
releaseBranch: gitBatchInfo.value.releaseBranch,
|
||||
repository: gitBatchInfo.value.repository,
|
||||
})}
|
||||
</ErrorBoundary>
|
||||
</RefetchContext.Provider>
|
||||
);
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
GetBranchResult,
|
||||
GetLatestReleaseResult,
|
||||
} from '../../api/GitReleaseClient';
|
||||
import { ComponentConfigPatch } from '../../types/types';
|
||||
import { ComponentConfig, PatchOnSuccessArgs } from '../../types/types';
|
||||
import { getBumpedTag } from '../../helpers/getBumpedTag';
|
||||
import { InfoCardPlus } from '../../components/InfoCardPlus';
|
||||
import { NoLatestRelease } from '../../components/NoLatestRelease';
|
||||
@@ -32,7 +32,7 @@ import { useProjectContext } from '../../contexts/ProjectContext';
|
||||
interface PatchProps {
|
||||
latestRelease: GetLatestReleaseResult['latestRelease'];
|
||||
releaseBranch: GetBranchResult['branch'] | null;
|
||||
onSuccess?: ComponentConfigPatch['onSuccess'];
|
||||
onSuccess?: ComponentConfig<PatchOnSuccessArgs>['onSuccess'];
|
||||
}
|
||||
|
||||
export const Patch = ({
|
||||
|
||||
@@ -38,7 +38,7 @@ import {
|
||||
GetLatestReleaseResult,
|
||||
} from '../../api/GitReleaseClient';
|
||||
import { CalverTagParts } from '../../helpers/tagParts/getCalverTagParts';
|
||||
import { ComponentConfigPatch } from '../../types/types';
|
||||
import { ComponentConfig, PatchOnSuccessArgs } from '../../types/types';
|
||||
import { Differ } from '../../components/Differ';
|
||||
import { getPatchCommitSuffix } from './helpers/getPatchCommitSuffix';
|
||||
import { gitReleaseManagerApiRef } from '../../api/serviceApiRef';
|
||||
@@ -56,7 +56,7 @@ interface PatchBodyProps {
|
||||
bumpedTag: string;
|
||||
latestRelease: NonNullable<GetLatestReleaseResult['latestRelease']>;
|
||||
releaseBranch: GetBranchResult['branch'];
|
||||
onSuccess?: ComponentConfigPatch['onSuccess'];
|
||||
onSuccess?: ComponentConfig<PatchOnSuccessArgs>['onSuccess'];
|
||||
tagParts: NonNullable<CalverTagParts | SemverTagParts>;
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,11 @@ import {
|
||||
} from '../../../api/GitReleaseClient';
|
||||
|
||||
import { CalverTagParts } from '../../../helpers/tagParts/getCalverTagParts';
|
||||
import { ComponentConfigPatch, CardHook } from '../../../types/types';
|
||||
import {
|
||||
CardHook,
|
||||
ComponentConfig,
|
||||
PatchOnSuccessArgs,
|
||||
} from '../../../types/types';
|
||||
import { getPatchCommitSuffix } from '../helpers/getPatchCommitSuffix';
|
||||
import { gitReleaseManagerApiRef } from '../../../api/serviceApiRef';
|
||||
import { Project } from '../../../contexts/ProjectContext';
|
||||
@@ -32,12 +36,12 @@ import { useResponseSteps } from '../../../hooks/useResponseSteps';
|
||||
import { useUserContext } from '../../../contexts/UserContext';
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
|
||||
interface Patch {
|
||||
export interface UsePatch {
|
||||
bumpedTag: string;
|
||||
latestRelease: NonNullable<GetLatestReleaseResult['latestRelease']>;
|
||||
project: Project;
|
||||
tagParts: NonNullable<CalverTagParts | SemverTagParts>;
|
||||
onSuccess?: ComponentConfigPatch['onSuccess'];
|
||||
onSuccess?: ComponentConfig<PatchOnSuccessArgs>['onSuccess'];
|
||||
}
|
||||
|
||||
// Inspiration: https://stackoverflow.com/questions/53859199/how-to-cherry-pick-through-githubs-api
|
||||
@@ -47,7 +51,7 @@ export function usePatch({
|
||||
project,
|
||||
tagParts,
|
||||
onSuccess,
|
||||
}: Patch): CardHook<GetRecentCommitsResultSingle> {
|
||||
}: UsePatch): CardHook<GetRecentCommitsResultSingle> {
|
||||
const pluginApiClient = useApi(gitReleaseManagerApiRef);
|
||||
const { user } = useUserContext();
|
||||
const {
|
||||
@@ -337,13 +341,19 @@ ${selectedPatchCommit.commit.message}`,
|
||||
|
||||
try {
|
||||
await onSuccess?.({
|
||||
updatedReleaseUrl: updatedReleaseRes.value.htmlUrl,
|
||||
updatedReleaseName: updatedReleaseRes.value.name,
|
||||
previousTag: latestRelease.tagName,
|
||||
patchedTag: updatedReleaseRes.value.tagName,
|
||||
patchCommitUrl: releaseBranchRes.value.selectedPatchCommit.htmlUrl,
|
||||
input: {
|
||||
bumpedTag,
|
||||
latestRelease,
|
||||
project,
|
||||
tagParts,
|
||||
},
|
||||
patchCommitMessage:
|
||||
releaseBranchRes.value.selectedPatchCommit.commit.message,
|
||||
patchCommitUrl: releaseBranchRes.value.selectedPatchCommit.htmlUrl,
|
||||
patchedTag: updatedReleaseRes.value.tagName,
|
||||
previousTag: latestRelease.tagName,
|
||||
updatedReleaseName: updatedReleaseRes.value.name,
|
||||
updatedReleaseUrl: updatedReleaseRes.value.htmlUrl,
|
||||
});
|
||||
} catch (error) {
|
||||
asyncCatcher(error);
|
||||
|
||||
@@ -18,7 +18,7 @@ import React from 'react';
|
||||
import { Alert, AlertTitle } from '@material-ui/lab';
|
||||
import { Box, Typography } from '@material-ui/core';
|
||||
|
||||
import { ComponentConfigPromoteRc } from '../../types/types';
|
||||
import { ComponentConfig, PromoteRcOnSuccessArgs } from '../../types/types';
|
||||
import { GetLatestReleaseResult } from '../../api/GitReleaseClient';
|
||||
import { InfoCardPlus } from '../../components/InfoCardPlus';
|
||||
import { NoLatestRelease } from '../../components/NoLatestRelease';
|
||||
@@ -27,7 +27,7 @@ import { TEST_IDS } from '../../test-helpers/test-ids';
|
||||
|
||||
interface PromoteRcProps {
|
||||
latestRelease: GetLatestReleaseResult['latestRelease'];
|
||||
onSuccess?: ComponentConfigPromoteRc['onSuccess'];
|
||||
onSuccess?: ComponentConfig<PromoteRcOnSuccessArgs>['onSuccess'];
|
||||
}
|
||||
|
||||
export const PromoteRc = ({ latestRelease, onSuccess }: PromoteRcProps) => {
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
import React from 'react';
|
||||
import { Button, Typography, Box } from '@material-ui/core';
|
||||
|
||||
import { ComponentConfigPromoteRc } from '../../types/types';
|
||||
import { ComponentConfig, PromoteRcOnSuccessArgs } from '../../types/types';
|
||||
import { Differ } from '../../components/Differ';
|
||||
import { GetLatestReleaseResult } from '../../api/GitReleaseClient';
|
||||
import { ResponseStepDialog } from '../../components/ResponseStepDialog/ResponseStepDialog';
|
||||
@@ -26,7 +26,7 @@ import { usePromoteRc } from './hooks/usePromoteRc';
|
||||
|
||||
interface PromoteRcBodyProps {
|
||||
rcRelease: NonNullable<GetLatestReleaseResult['latestRelease']>;
|
||||
onSuccess?: ComponentConfigPromoteRc['onSuccess'];
|
||||
onSuccess?: ComponentConfig<PromoteRcOnSuccessArgs>['onSuccess'];
|
||||
}
|
||||
|
||||
export const PromoteRcBody = ({ rcRelease, onSuccess }: PromoteRcBodyProps) => {
|
||||
|
||||
@@ -16,7 +16,11 @@
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useAsync, useAsyncFn } from 'react-use';
|
||||
import { CardHook, ComponentConfigPromoteRc } from '../../../types/types';
|
||||
import {
|
||||
CardHook,
|
||||
ComponentConfig,
|
||||
PromoteRcOnSuccessArgs,
|
||||
} from '../../../types/types';
|
||||
|
||||
import { GetLatestReleaseResult } from '../../../api/GitReleaseClient';
|
||||
import { gitReleaseManagerApiRef } from '../../../api/serviceApiRef';
|
||||
@@ -27,17 +31,17 @@ import { useResponseSteps } from '../../../hooks/useResponseSteps';
|
||||
import { useUserContext } from '../../../contexts/UserContext';
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
|
||||
interface PromoteRc {
|
||||
export interface UsePromoteRc {
|
||||
rcRelease: NonNullable<GetLatestReleaseResult['latestRelease']>;
|
||||
releaseVersion: string;
|
||||
onSuccess?: ComponentConfigPromoteRc['onSuccess'];
|
||||
onSuccess?: ComponentConfig<PromoteRcOnSuccessArgs>['onSuccess'];
|
||||
}
|
||||
|
||||
export function usePromoteRc({
|
||||
rcRelease,
|
||||
releaseVersion,
|
||||
onSuccess,
|
||||
}: PromoteRc): CardHook<void> {
|
||||
}: UsePromoteRc): CardHook<void> {
|
||||
const pluginApiClient = useApi(gitReleaseManagerApiRef);
|
||||
const { user } = useUserContext();
|
||||
const { project } = useProjectContext();
|
||||
@@ -170,12 +174,16 @@ export function usePromoteRc({
|
||||
|
||||
try {
|
||||
await onSuccess?.({
|
||||
gitReleaseUrl: promotedReleaseRes.value.htmlUrl,
|
||||
input: {
|
||||
rcRelease,
|
||||
releaseVersion,
|
||||
},
|
||||
gitReleaseName: promotedReleaseRes.value.name,
|
||||
previousTagUrl: rcRelease.htmlUrl,
|
||||
gitReleaseUrl: promotedReleaseRes.value.htmlUrl,
|
||||
previousTag: rcRelease.tagName,
|
||||
updatedTagUrl: promotedReleaseRes.value.htmlUrl,
|
||||
previousTagUrl: rcRelease.htmlUrl,
|
||||
updatedTag: promotedReleaseRes.value.tagName,
|
||||
updatedTagUrl: promotedReleaseRes.value.htmlUrl,
|
||||
});
|
||||
} catch (error) {
|
||||
asyncCatcher(error);
|
||||
|
||||
@@ -14,21 +14,26 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export type ComponentConfig<Args> = {
|
||||
import { UseCreateReleaseCandidate } from '../features/CreateReleaseCandidate/hooks/useCreateReleaseCandidate';
|
||||
import { UsePatch } from '../features/Patch/hooks/usePatch';
|
||||
import { UsePromoteRc } from '../features/PromoteRc/hooks/usePromoteRc';
|
||||
|
||||
export type ComponentConfig<OnSuccessArgs> = {
|
||||
omit?: boolean;
|
||||
onSuccess?: (args: Args) => Promise<void> | void;
|
||||
onSuccess?: (args: OnSuccessArgs) => Promise<void> | void;
|
||||
};
|
||||
|
||||
interface CreateRcOnSuccessArgs {
|
||||
gitReleaseUrl: string;
|
||||
gitReleaseName: string | null;
|
||||
export interface CreateRcOnSuccessArgs {
|
||||
input: Omit<UseCreateReleaseCandidate, 'onSuccess'>;
|
||||
comparisonUrl: string;
|
||||
previousTag?: string;
|
||||
createdTag: string;
|
||||
gitReleaseName: string | null;
|
||||
gitReleaseUrl: string;
|
||||
previousTag?: string;
|
||||
}
|
||||
export type ComponentConfigCreateRc = ComponentConfig<CreateRcOnSuccessArgs>;
|
||||
|
||||
interface PromoteRcOnSuccessArgs {
|
||||
export interface PromoteRcOnSuccessArgs {
|
||||
input: Omit<UsePromoteRc, 'onSuccess'>;
|
||||
gitReleaseUrl: string;
|
||||
gitReleaseName: string | null;
|
||||
previousTagUrl: string;
|
||||
@@ -36,9 +41,9 @@ interface PromoteRcOnSuccessArgs {
|
||||
updatedTagUrl: string;
|
||||
updatedTag: string;
|
||||
}
|
||||
export type ComponentConfigPromoteRc = ComponentConfig<PromoteRcOnSuccessArgs>;
|
||||
|
||||
interface PatchOnSuccessArgs {
|
||||
export interface PatchOnSuccessArgs {
|
||||
input: Omit<UsePatch, 'onSuccess'>;
|
||||
updatedReleaseUrl: string;
|
||||
updatedReleaseName: string | null;
|
||||
previousTag: string;
|
||||
@@ -46,7 +51,6 @@ interface PatchOnSuccessArgs {
|
||||
patchCommitUrl: string;
|
||||
patchCommitMessage: string;
|
||||
}
|
||||
export type ComponentConfigPatch = ComponentConfig<PatchOnSuccessArgs>;
|
||||
|
||||
export interface ResponseStep {
|
||||
message: string | React.ReactNode;
|
||||
|
||||
Reference in New Issue
Block a user