mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-15 20:30:33 +00:00
feat: organization setup.
This commit is contained in:
@@ -5,10 +5,10 @@ export const getSetupWizardSteps = () => [
|
||||
label: intl.get('setup.plan.plans'),
|
||||
},
|
||||
{
|
||||
label: intl.get('setup.plan.initializing'),
|
||||
label: intl.get('setup.plan.getting_started'),
|
||||
},
|
||||
{
|
||||
label: intl.get('setup.plan.getting_started'),
|
||||
label: intl.get('setup.plan.initializing'),
|
||||
},
|
||||
{
|
||||
label: intl.get('setup.plan.congrats'),
|
||||
|
||||
@@ -2,37 +2,30 @@ import { connect } from 'react-redux';
|
||||
import {
|
||||
getOrganizationByIdFactory,
|
||||
isOrganizationReadyFactory,
|
||||
isOrganizationSeededFactory,
|
||||
isOrganizationBuiltFactory,
|
||||
isOrganizationSeedingFactory,
|
||||
isOrganizationInitializingFactory,
|
||||
isOrganizationSubscribedFactory,
|
||||
isOrganizationCongratsFactory,
|
||||
isOrganizationBuildRunningFactory
|
||||
} from 'store/organizations/organizations.selectors';
|
||||
|
||||
export default (mapState) => {
|
||||
const getOrganizationById = getOrganizationByIdFactory();
|
||||
const isOrganizationReady = isOrganizationReadyFactory();
|
||||
|
||||
const isOrganizationSeeded = isOrganizationSeededFactory();
|
||||
const isOrganizationBuilt = isOrganizationBuiltFactory();
|
||||
|
||||
const isOrganizationInitializing = isOrganizationInitializingFactory();
|
||||
const isOrganizationSeeding = isOrganizationSeedingFactory();
|
||||
|
||||
const isOrganizationSubscribed = isOrganizationSubscribedFactory();
|
||||
const isOrganizationCongrats = isOrganizationCongratsFactory();
|
||||
const isOrganizationBuildRunning = isOrganizationBuildRunningFactory();
|
||||
|
||||
const mapStateToProps = (state, props) => {
|
||||
const mapped = {
|
||||
organization: getOrganizationById(state, props),
|
||||
isOrganizationReady: isOrganizationReady(state, props),
|
||||
isOrganizationSeeded: isOrganizationSeeded(state, props),
|
||||
isOrganizationInitialized: isOrganizationBuilt(state, props),
|
||||
isOrganizationSeeding: isOrganizationInitializing(state, props),
|
||||
isOrganizationInitializing: isOrganizationSeeding(state, props),
|
||||
|
||||
isOrganizationSubscribed: isOrganizationSubscribed(state, props),
|
||||
isOrganizationSetupCompleted: isOrganizationCongrats(state, props),
|
||||
isOrganizationBuildRunning: isOrganizationBuildRunning(state, props)
|
||||
};
|
||||
return (mapState) ? mapState(mapped, state, props) : mapped;
|
||||
};
|
||||
|
||||
@@ -1,62 +1,108 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import React from 'react';
|
||||
import { ProgressBar, Intent } from '@blueprintjs/core';
|
||||
import { useBuildTenant } from 'hooks/query';
|
||||
import * as R from 'ramda';
|
||||
|
||||
import { useJob, useCurrentOrganization } from 'hooks/query';
|
||||
import { FormattedMessage as T } from 'components';
|
||||
|
||||
import withOrganizationActions from 'containers/Organization/withOrganizationActions';
|
||||
import withCurrentOrganization from 'containers/Organization/withCurrentOrganization';
|
||||
import withOrganization from '../Organization/withOrganization';
|
||||
|
||||
import 'style/pages/Setup/Initializing.scss';
|
||||
|
||||
/**
|
||||
* Setup initializing step form.
|
||||
*/
|
||||
export default function SetupInitializingForm() {
|
||||
function SetupInitializingForm({
|
||||
setOrganizationSetupCompleted,
|
||||
organization,
|
||||
}) {
|
||||
const { refetch, isSuccess } = useCurrentOrganization({ enabled: false });
|
||||
|
||||
const [isJobDone, setIsJobDone] = React.useState(false);
|
||||
|
||||
const {
|
||||
mutateAsync: buildTenantMutate,
|
||||
isLoading,
|
||||
isError,
|
||||
} = useBuildTenant();
|
||||
data: { running, queued, failed, completed },
|
||||
} = useJob(organization?.build_job_id, {
|
||||
refetchInterval: 2000,
|
||||
enabled: !!organization?.build_job_id,
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
buildTenantMutate();
|
||||
}, [buildTenantMutate]);
|
||||
if (completed) {
|
||||
refetch();
|
||||
setIsJobDone(true);
|
||||
}
|
||||
}, [refetch, completed, setOrganizationSetupCompleted]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isSuccess && isJobDone) {
|
||||
setOrganizationSetupCompleted(true);
|
||||
setIsJobDone(false);
|
||||
}
|
||||
}, [setOrganizationSetupCompleted, isJobDone, isSuccess]);
|
||||
|
||||
return (
|
||||
<div class="setup-initializing-form">
|
||||
{isLoading && <ProgressBar intent={Intent.PRIMARY} value={null} />}
|
||||
<ProgressBar intent={Intent.PRIMARY} value={null} />
|
||||
|
||||
<div className={'setup-initializing-form__title'}>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<h1>
|
||||
<T id={'setup.initializing.title'} />
|
||||
</h1>
|
||||
<p className={'paragraph'}>
|
||||
<T id={'setup.initializing.description'} />
|
||||
</p>
|
||||
</>
|
||||
) : isError ? (
|
||||
<>
|
||||
<h1>
|
||||
<T id={'setup.initializing.something_went_wrong'} />
|
||||
</h1>
|
||||
<p class="paragraph">
|
||||
<T id={'setup.initializing.please_refresh_the_page'} />
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<h1>
|
||||
<T id={'setup.initializing.waiting_to_redirect'} />
|
||||
</h1>
|
||||
<p class="paragraph">
|
||||
<T
|
||||
id={
|
||||
'setup.initializing.refresh_the_page_if_redirect_not_worked'
|
||||
}
|
||||
/>
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
{failed ? (
|
||||
<SetupInitializingFailed />
|
||||
) : running || queued ? (
|
||||
<SetupInitializingRunning />
|
||||
) : completed ? (
|
||||
<SetupInitializingCompleted />
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default R.compose(
|
||||
withOrganizationActions,
|
||||
withCurrentOrganization(({ organizationTenantId }) => ({
|
||||
organizationId: organizationTenantId,
|
||||
})),
|
||||
withOrganization(({ organization }) => ({ organization })),
|
||||
)(SetupInitializingForm);
|
||||
|
||||
function SetupInitializingFailed() {
|
||||
return (
|
||||
<div class="failed">
|
||||
<h1>
|
||||
<T id={'setup.initializing.something_went_wrong'} />
|
||||
</h1>
|
||||
<p class="paragraph">
|
||||
<T id={'setup.initializing.please_refresh_the_page'} />
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SetupInitializingRunning() {
|
||||
return (
|
||||
<div class="running">
|
||||
<h1>
|
||||
<T id={'setup.initializing.title'} />
|
||||
</h1>
|
||||
<p className={'paragraph'}>
|
||||
<T id={'setup.initializing.description'} />
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SetupInitializingCompleted() {
|
||||
return (
|
||||
<div class="completed">
|
||||
<h1>
|
||||
<T id={'setup.initializing.waiting_to_redirect'} />
|
||||
</h1>
|
||||
<p class="paragraph">
|
||||
<T id={'setup.initializing.refresh_the_page_if_redirect_not_worked'} />
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,5 +11,5 @@ export const getSetupOrganizationValidation = () =>
|
||||
baseCurrency: Yup.string().required().label(intl.get('base_currency_')),
|
||||
language: Yup.string().required().label(intl.get('language')),
|
||||
fiscalYear: Yup.string().required().label(intl.get('fiscal_year_')),
|
||||
timeZone: Yup.string().required().label(intl.get('time_zone_')),
|
||||
timezone: Yup.string().required().label(intl.get('time_zone_')),
|
||||
});
|
||||
|
||||
@@ -195,7 +195,7 @@ export default function SetupOrganizationForm({ isSubmitting, values }) {
|
||||
</FastField>
|
||||
|
||||
{/* ---------- Time zone ---------- */}
|
||||
<FastField name={'timeZone'}>
|
||||
<FastField name={'timezone'}>
|
||||
{({
|
||||
form: { setFieldValue },
|
||||
field: { value },
|
||||
@@ -209,12 +209,12 @@ export default function SetupOrganizationForm({ isSubmitting, values }) {
|
||||
Classes.FILL,
|
||||
)}
|
||||
intent={inputIntent({ error, touched })}
|
||||
helperText={<ErrorMessage name={'timeZone'} />}
|
||||
helperText={<ErrorMessage name={'timezone'} />}
|
||||
>
|
||||
<TimezonePicker
|
||||
value={value}
|
||||
onChange={(item) => {
|
||||
setFieldValue('timeZone', item);
|
||||
setFieldValue('timezone', item);
|
||||
}}
|
||||
valueDisplayFormat="composite"
|
||||
showLocalTimezone={true}
|
||||
|
||||
@@ -3,7 +3,6 @@ import { Formik } from 'formik';
|
||||
import moment from 'moment';
|
||||
import { FormattedMessage as T } from 'components';
|
||||
|
||||
|
||||
import 'style/pages/Setup/Organization.scss';
|
||||
|
||||
import SetupOrganizationForm from './SetupOrganizationForm';
|
||||
@@ -15,7 +14,6 @@ import withOrganizationActions from 'containers/Organization/withOrganizationAct
|
||||
import { compose, transfromToSnakeCase } from 'utils';
|
||||
import { getSetupOrganizationValidation } from './SetupOrganization.schema';
|
||||
|
||||
|
||||
// Initial values.
|
||||
const defaultValues = {
|
||||
organization_name: '',
|
||||
@@ -23,7 +21,7 @@ const defaultValues = {
|
||||
baseCurrency: '',
|
||||
language: 'en',
|
||||
fiscalYear: '',
|
||||
timeZone: '',
|
||||
timezone: '',
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -43,9 +41,6 @@ function SetupOrganizationPage({ wizard, setOrganizationSetupCompleted }) {
|
||||
// Handle the form submit.
|
||||
const handleSubmit = (values, { setSubmitting, setErrors }) => {
|
||||
organizationSetupMutate({ ...transfromToSnakeCase(values) })
|
||||
.then(() => {
|
||||
return setOrganizationSetupCompleted(true);
|
||||
})
|
||||
.then((response) => {
|
||||
setSubmitting(false);
|
||||
wizard.next();
|
||||
|
||||
@@ -47,11 +47,13 @@ export default compose(
|
||||
isOrganizationInitialized,
|
||||
isOrganizationSeeded,
|
||||
isOrganizationSetupCompleted,
|
||||
isOrganizationBuildRunning,
|
||||
}) => ({
|
||||
organization,
|
||||
isOrganizationInitialized,
|
||||
isOrganizationSeeded,
|
||||
isOrganizationSetupCompleted,
|
||||
isOrganizationBuildRunning,
|
||||
}),
|
||||
),
|
||||
withSubscriptions(
|
||||
|
||||
@@ -19,8 +19,8 @@ export default function SetupWizardContent({ setupStepIndex, setupStepId }) {
|
||||
<div class="setup-page-form">
|
||||
<SetupSteps step={{ id: setupStepId }}>
|
||||
<SetupSubscription id="subscription" />
|
||||
<SetupInitializingForm id={'initializing'} />
|
||||
<SetupOrganizationPage id="organization" />
|
||||
<SetupInitializingForm id={'initializing'} />
|
||||
<SetupCongratsPage id="congrats" />
|
||||
</SetupSteps>
|
||||
</div>
|
||||
|
||||
@@ -26,3 +26,4 @@ export * from './organization';
|
||||
export * from './landedCost';
|
||||
export * from './UniversalSearch/UniversalSearch';
|
||||
export * from './GenericResource';
|
||||
export * from './jobs';
|
||||
|
||||
16
client/src/hooks/query/jobs.js
Normal file
16
client/src/hooks/query/jobs.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import { useRequestQuery } from '../useQueryRequest';
|
||||
|
||||
/**
|
||||
* Retrieve the job metadata.
|
||||
*/
|
||||
export function useJob(jobId, props = {}) {
|
||||
return useRequestQuery(
|
||||
['JOB', jobId],
|
||||
{ method: 'get', url: `jobs/${jobId}` },
|
||||
{
|
||||
select: (res) => res.data.job,
|
||||
defaultData: {},
|
||||
...props,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -35,7 +35,7 @@ export function useCurrentOrganization(props) {
|
||||
|
||||
return useRequestQuery(
|
||||
[t.ORGANIZATION_CURRENT],
|
||||
{ method: 'get', url: `organization/current` },
|
||||
{ method: 'get', url: `organization` },
|
||||
{
|
||||
select: (res) => res.data.organization,
|
||||
defaultData: {},
|
||||
@@ -55,37 +55,6 @@ export function useCurrentOrganization(props) {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the current tenant.
|
||||
*/
|
||||
export function useBuildTenant(props) {
|
||||
const apiRequest = useApiRequest();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation((values) => apiRequest.post('organization/build'), {
|
||||
onSuccess: (res, values) => {
|
||||
queryClient.invalidateQueries(t.ORGANIZATION_CURRENT);
|
||||
queryClient.invalidateQueries(t.ORGANIZATIONS);
|
||||
},
|
||||
...props,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Seeds the current tenant
|
||||
*/
|
||||
export function useSeedTenant() {
|
||||
const apiRequest = useApiRequest();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation((values) => apiRequest.post('organization/seed'), {
|
||||
onSuccess: (res) => {
|
||||
queryClient.invalidateQueries(t.ORGANIZATION_CURRENT);
|
||||
queryClient.invalidateQueries(t.ORGANIZATIONS);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Organization setup.
|
||||
*/
|
||||
@@ -94,7 +63,7 @@ export function useOrganizationSetup() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation(
|
||||
(values) => apiRequest.post(`setup/organization`, values),
|
||||
(values) => apiRequest.post(`organization/build`, values),
|
||||
{
|
||||
onSuccess: (res) => {
|
||||
queryClient.invalidateQueries(t.ORGANIZATION_CURRENT);
|
||||
|
||||
@@ -8,66 +8,34 @@ export const setOrganizations = (organizations) => {
|
||||
organizations,
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchOrganizations = () => (dispatch) =>
|
||||
new Promise((resolve, reject) => {
|
||||
ApiService.get('organization/all')
|
||||
.then((response) => {
|
||||
dispatch({
|
||||
type: t.ORGANIZATIONS_LIST_SET,
|
||||
payload: {
|
||||
organizations: response.data.organizations,
|
||||
},
|
||||
});
|
||||
resolve(response);
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
|
||||
export const setOrganizationSetupCompleted =
|
||||
(congrats) => (dispatch, getState) => {
|
||||
const organizationId = getState().authentication.organizationId;
|
||||
|
||||
export const fetchOrganizations = () => (dispatch) => new Promise((resolve, reject) => {
|
||||
ApiService.get('organization/all').then((response) => {
|
||||
dispatch({
|
||||
type: t.ORGANIZATIONS_LIST_SET,
|
||||
type: t.SET_ORGANIZATION_CONGRATS,
|
||||
payload: {
|
||||
organizations: response.data.organizations,
|
||||
organizationId,
|
||||
congrats,
|
||||
},
|
||||
});
|
||||
resolve(response)
|
||||
}).catch(error => { reject(error); });
|
||||
});
|
||||
|
||||
export const buildTenant = () => (dispatch, getState) => new Promise((resolve, reject) => {
|
||||
const organizationId = getState().authentication.organizationId;
|
||||
|
||||
dispatch({
|
||||
type: t.SET_ORGANIZATION_INITIALIZING,
|
||||
payload: { organizationId }
|
||||
});
|
||||
ApiService.post(`organization/build`).then((response) => {
|
||||
resolve(response);
|
||||
dispatch({
|
||||
type: t.SET_ORGANIZATION_INITIALIZED,
|
||||
payload: { organizationId }
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error.response.data.errors || []);
|
||||
});
|
||||
});
|
||||
|
||||
export const seedTenant = () => (dispatch, getState) => new Promise((resolve, reject) => {
|
||||
const organizationId = getState().authentication.organizationId;
|
||||
|
||||
dispatch({
|
||||
type: t.SET_ORGANIZATION_SEEDING,
|
||||
payload: { organizationId }
|
||||
});
|
||||
ApiService.post(`organization/seed/`).then((response) => {
|
||||
dispatch({
|
||||
type: t.SET_ORGANIZATION_SEEDED,
|
||||
payload: { organizationId }
|
||||
});
|
||||
resolve(response);
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error.response.data.errors || []);
|
||||
});
|
||||
});
|
||||
|
||||
export const setOrganizationSetupCompleted = (congrats) => (dispatch, getState) => {
|
||||
const organizationId = getState().authentication.organizationId;
|
||||
|
||||
dispatch({
|
||||
type: t.SET_ORGANIZATION_CONGRATS,
|
||||
payload: {
|
||||
organizationId,
|
||||
congrats
|
||||
},
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
@@ -24,45 +24,6 @@ const reducer = createReducer(initialState, {
|
||||
state.byOrganizationId = _dataByOrganizationId;
|
||||
},
|
||||
|
||||
[t.SET_ORGANIZATION_SEEDING]: (state, action) => {
|
||||
const { organizationId } = action.payload;
|
||||
|
||||
state.data[organizationId] = {
|
||||
...(state.data[organizationId] || {}),
|
||||
is_seeding: true,
|
||||
};
|
||||
},
|
||||
|
||||
[t.SET_ORGANIZATION_SEEDED]: (state, action) => {
|
||||
const { organizationId } = action.payload;
|
||||
|
||||
state.data[organizationId] = {
|
||||
...(state.data[organizationId] || {}),
|
||||
is_seeding: false,
|
||||
seeded_at: new Date().toISOString(),
|
||||
is_ready: true,
|
||||
};
|
||||
},
|
||||
|
||||
[t.SET_ORGANIZATION_INITIALIZING]: (state, action) => {
|
||||
const { organizationId } = action.payload;
|
||||
|
||||
state.data[organizationId] = {
|
||||
...(state.data[organizationId] || {}),
|
||||
is_initializing: true,
|
||||
};
|
||||
},
|
||||
|
||||
[t.SET_ORGANIZATION_INITIALIZED]: (state, action) => {
|
||||
const { organizationId } = action.payload;
|
||||
|
||||
state.data[organizationId] = {
|
||||
...(state.data[organizationId] || {}),
|
||||
is_initializing: false,
|
||||
initialized_at: new Date().toISOString(),
|
||||
};
|
||||
},
|
||||
|
||||
[t.SET_ORGANIZATION_CONGRATS]: (state, action) => {
|
||||
const { organizationId, congrats } = action.payload;
|
||||
|
||||
|
||||
@@ -21,20 +21,6 @@ export const isOrganizationBuiltFactory = () => createSelector(
|
||||
},
|
||||
);
|
||||
|
||||
export const isOrganizationInitializingFactory = () => createSelector(
|
||||
organizationSelector,
|
||||
(organization) => {
|
||||
return organization?.is_initializing;
|
||||
},
|
||||
);
|
||||
|
||||
export const isOrganizationSeedingFactory = () => createSelector(
|
||||
organizationSelector,
|
||||
(organization) => {
|
||||
return organization?.is_seeding;
|
||||
},
|
||||
);
|
||||
|
||||
export const isOrganizationReadyFactory = () => createSelector(
|
||||
organizationSelector,
|
||||
(organization) => {
|
||||
@@ -55,3 +41,10 @@ export const isOrganizationCongratsFactory = () => createSelector(
|
||||
return !!organization?.is_congrats;
|
||||
}
|
||||
);
|
||||
|
||||
export const isOrganizationBuildRunningFactory = () => createSelector(
|
||||
organizationSelector,
|
||||
(organization) => {
|
||||
return !!organization?.is_build_running;
|
||||
}
|
||||
)
|
||||
@@ -1,12 +1,5 @@
|
||||
|
||||
export default {
|
||||
ORGANIZATIONS_LIST_SET: 'ORGANIZATIONS_LIST_SET',
|
||||
|
||||
SET_ORGANIZATION_SEEDING: 'SET_ORGANIZATION_SEEDING',
|
||||
SET_ORGANIZATION_SEEDED: 'SET_ORGANIZATION_SEEDED',
|
||||
|
||||
SET_ORGANIZATION_INITIALIZED: 'SET_ORGANIZATION_INITIALIZED',
|
||||
SET_ORGANIZATION_INITIALIZING: 'SET_ORGANIZATION_INITIALIZING',
|
||||
|
||||
SET_ORGANIZATION_CONGRATS: 'SET_ORGANIZATION_CONGRATS'
|
||||
};
|
||||
@@ -5,28 +5,27 @@ export default (mapState) => {
|
||||
const {
|
||||
isOrganizationSetupCompleted,
|
||||
isOrganizationInitialized,
|
||||
isOrganizationSeeded,
|
||||
isSubscriptionActive
|
||||
isSubscriptionActive,
|
||||
isOrganizationBuildRunning
|
||||
} = props;
|
||||
|
||||
const condits = {
|
||||
isCongratsStep: isOrganizationSetupCompleted,
|
||||
isSubscriptionStep: !isSubscriptionActive,
|
||||
isInitializingStep: isSubscriptionActive && !isOrganizationInitialized,
|
||||
isOrganizationStep: isOrganizationInitialized && !isOrganizationSeeded,
|
||||
isInitializingStep: isOrganizationBuildRunning,
|
||||
isOrganizationStep: !isOrganizationInitialized && !isOrganizationBuildRunning,
|
||||
};
|
||||
|
||||
const scenarios = [
|
||||
{ condition: condits.isCongratsStep, step: 'congrats' },
|
||||
{ condition: condits.isSubscriptionStep, step: 'subscription' },
|
||||
{ condition: condits.isInitializingStep, step: 'initializing' },
|
||||
{ condition: condits.isOrganizationStep, step: 'organization' },
|
||||
{ condition: condits.isInitializingStep, step: 'initializing' },
|
||||
{ condition: condits.isCongratsStep, step: 'congrats' },
|
||||
];
|
||||
const setupStep = scenarios.find((scenario) => scenario.condition);
|
||||
const mapped = {
|
||||
...condits,
|
||||
setupStepId: setupStep?.step,
|
||||
setupStepIndex: scenarios.indexOf(setupStep) ,
|
||||
setupStepIndex: scenarios.indexOf(setupStep) + 1,
|
||||
};
|
||||
return mapState ? mapState(mapped, state, props) : mapped;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user