feat: organization setup.

This commit is contained in:
a.bouhuolia
2021-09-04 10:04:07 +02:00
parent f2c51c6023
commit 8141674da8
18 changed files with 188 additions and 252 deletions

View File

@@ -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'),

View File

@@ -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;
};

View File

@@ -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>
);
}

View File

@@ -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_')),
});

View File

@@ -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}

View File

@@ -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();

View File

@@ -47,11 +47,13 @@ export default compose(
isOrganizationInitialized,
isOrganizationSeeded,
isOrganizationSetupCompleted,
isOrganizationBuildRunning,
}) => ({
organization,
isOrganizationInitialized,
isOrganizationSeeded,
isOrganizationSetupCompleted,
isOrganizationBuildRunning,
}),
),
withSubscriptions(

View File

@@ -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>

View File

@@ -26,3 +26,4 @@ export * from './organization';
export * from './landedCost';
export * from './UniversalSearch/UniversalSearch';
export * from './GenericResource';
export * from './jobs';

View 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,
},
);
}

View File

@@ -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);

View File

@@ -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
},
});
};
};

View File

@@ -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;

View File

@@ -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;
}
)

View File

@@ -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'
};

View File

@@ -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;
};