AddWorkingGroupOpeningForm.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361
  1. import React, { useEffect } from 'react';
  2. import { getFormErrorLabelsProps, FormErrorLabelsProps } from './errorHandling';
  3. import * as Yup from 'yup';
  4. import {
  5. withProposalFormData,
  6. ProposalFormExportProps,
  7. ProposalFormContainerProps,
  8. ProposalFormInnerProps,
  9. genericFormDefaultOptions
  10. } from './GenericProposalForm';
  11. import {
  12. GenericWorkingGroupProposalForm,
  13. FormValues as WGFormValues,
  14. defaultValues as wgFromDefaultValues
  15. } from './GenericWorkingGroupProposalForm';
  16. import { FormField, InputFormField, TextareaFormField } from './FormFields';
  17. import { withFormContainer } from './FormContainer';
  18. import { ActivateOpeningAtKey, ActivateOpeningAtDef, StakingAmountLimitModeKeys, IApplicationRationingPolicy, IStakingPolicy } from '@joystream/types/hiring';
  19. import { GenericJoyStreamRoleSchema } from '@joystream/types/hiring/schemas/role.schema.typings';
  20. import { Dropdown, Grid, Message, Checkbox } from 'semantic-ui-react';
  21. import { formatBalance } from '@polkadot/util';
  22. import _ from 'lodash';
  23. import { IWorkingGroupOpeningPolicyCommitment } from '@joystream/types/working-group';
  24. import { IAddOpeningParameters } from '@joystream/types/proposals';
  25. import { WorkingGroupKey, InputValidationLengthConstraint } from '@joystream/types/common';
  26. import { BlockNumber } from '@polkadot/types/interfaces';
  27. import { withCalls } from '@polkadot/react-api';
  28. import { SimplifiedTypeInterface } from '@polkadot/joy-utils/types/common';
  29. import Validation from '../validationSchema';
  30. export type FormValues = WGFormValues & {
  31. activateAt: ActivateOpeningAtKey;
  32. activateAtBlock: string;
  33. maxReviewPeriodLength: string;
  34. applicationsLimited: boolean;
  35. maxApplications: string;
  36. applicationStakeRequired: boolean;
  37. applicationStakeMode: StakingAmountLimitModeKeys;
  38. applicationStakeValue: string;
  39. roleStakeRequired: boolean;
  40. roleStakeMode: StakingAmountLimitModeKeys;
  41. roleStakeValue: string;
  42. terminateRoleUnstakingPeriod: string;
  43. leaveRoleUnstakingPeriod: string;
  44. humanReadableText: string;
  45. };
  46. const defaultValues: FormValues = {
  47. ...wgFromDefaultValues,
  48. activateAt: 'CurrentBlock',
  49. activateAtBlock: '',
  50. maxReviewPeriodLength: (14400 * 30).toString(), // 30 days
  51. applicationsLimited: false,
  52. maxApplications: '',
  53. applicationStakeRequired: false,
  54. applicationStakeMode: StakingAmountLimitModeKeys.Exact,
  55. applicationStakeValue: '',
  56. roleStakeRequired: false,
  57. roleStakeMode: StakingAmountLimitModeKeys.Exact,
  58. roleStakeValue: '',
  59. terminateRoleUnstakingPeriod: (14400 * 7).toString(), // 7 days
  60. leaveRoleUnstakingPeriod: (14400 * 7).toString(), // 7 days
  61. humanReadableText: ''
  62. };
  63. const HRTDefault: (memberHandle: string, group: WorkingGroupKey) => GenericJoyStreamRoleSchema =
  64. (memberHandle, group) => ({
  65. version: 1,
  66. headline: `Looking for ${group} Working Group Leader!`,
  67. job: {
  68. title: `${group} Working Group Leader`,
  69. description: `Become ${group} Working Group Leader! This is a great opportunity to support Joystream!`
  70. },
  71. application: {
  72. sections: [
  73. {
  74. title: 'About you',
  75. questions: [
  76. {
  77. title: 'Your name',
  78. type: 'text'
  79. },
  80. {
  81. title: 'What makes you a good fit for the job?',
  82. type: 'text area'
  83. }
  84. ]
  85. }
  86. ]
  87. },
  88. reward: '100 JOY per block',
  89. creator: {
  90. membership: {
  91. handle: memberHandle
  92. }
  93. }
  94. });
  95. type FormAdditionalProps = {}; // Aditional props coming all the way from export component into the inner form.
  96. type ExportComponentProps = ProposalFormExportProps<FormAdditionalProps, FormValues>;
  97. type FormContainerProps = ProposalFormContainerProps<ExportComponentProps> & {
  98. currentBlock?: BlockNumber;
  99. HRTConstraint?: InputValidationLengthConstraint;
  100. };
  101. type FormInnerProps = ProposalFormInnerProps<FormContainerProps, FormValues>;
  102. type StakeFieldsProps = Pick<FormInnerProps, 'values' | 'handleChange' | 'setFieldValue'> & {
  103. errorLabelsProps: FormErrorLabelsProps<FormValues>;
  104. stakeType: 'role' | 'application';
  105. };
  106. const StakeFields: React.FunctionComponent<StakeFieldsProps> = ({
  107. values,
  108. errorLabelsProps,
  109. handleChange,
  110. stakeType,
  111. setFieldValue
  112. }) => {
  113. return (
  114. <>
  115. <FormField label={`${_.startCase(stakeType)} stake` }>
  116. <Checkbox
  117. toggle
  118. onChange={(e, data) => { setFieldValue(`${stakeType}StakeRequired`, data.checked); }}
  119. label={ `Require ${stakeType} stake` }
  120. checked={ stakeType === 'role' ? values.roleStakeRequired : values.applicationStakeRequired }/>
  121. </FormField>
  122. { (stakeType === 'role' ? values.roleStakeRequired : values.applicationStakeRequired) && (<>
  123. <FormField label="Stake mode">
  124. <Dropdown
  125. onChange={handleChange}
  126. name={ `${stakeType}StakeMode` }
  127. selection
  128. options={[StakingAmountLimitModeKeys.Exact, StakingAmountLimitModeKeys.AtLeast].map(mode => ({ text: mode, value: mode }))}
  129. value={ stakeType === 'role' ? values.roleStakeMode : values.applicationStakeMode }
  130. />
  131. </FormField>
  132. <InputFormField
  133. label="Stake value"
  134. unit={formatBalance.getDefaults().unit}
  135. onChange={handleChange}
  136. name={ `${stakeType}StakeValue` }
  137. error={ stakeType === 'role' ? errorLabelsProps.roleStakeValue : errorLabelsProps.applicationStakeValue}
  138. value={ stakeType === 'role' ? values.roleStakeValue : values.applicationStakeValue}
  139. placeholder={'ie. 100'}
  140. />
  141. </>) }
  142. </>
  143. );
  144. };
  145. const valuesToAddOpeningParams = (values: FormValues): SimplifiedTypeInterface<IAddOpeningParameters> => {
  146. const commitment: SimplifiedTypeInterface<IWorkingGroupOpeningPolicyCommitment> = {
  147. max_review_period_length: parseInt(values.maxReviewPeriodLength)
  148. };
  149. if (parseInt(values.terminateRoleUnstakingPeriod) > 0) {
  150. commitment.terminate_role_stake_unstaking_period = parseInt(values.terminateRoleUnstakingPeriod);
  151. }
  152. if (parseInt(values.leaveRoleUnstakingPeriod) > 0) {
  153. commitment.exit_role_stake_unstaking_period = parseInt(values.leaveRoleUnstakingPeriod);
  154. }
  155. if (values.applicationsLimited) {
  156. const rationingPolicy: SimplifiedTypeInterface<IApplicationRationingPolicy> = {
  157. max_active_applicants: parseInt(values.maxApplications)
  158. };
  159. commitment.application_rationing_policy = rationingPolicy;
  160. }
  161. if (values.applicationStakeRequired) {
  162. const applicationStakingPolicy: SimplifiedTypeInterface<IStakingPolicy> = {
  163. amount: parseInt(values.applicationStakeValue),
  164. amount_mode: values.applicationStakeMode
  165. };
  166. commitment.application_staking_policy = applicationStakingPolicy;
  167. }
  168. if (values.roleStakeRequired) {
  169. const roleStakingPolicy: SimplifiedTypeInterface<IStakingPolicy> = {
  170. amount: parseInt(values.roleStakeValue),
  171. amount_mode: values.roleStakeMode
  172. };
  173. commitment.role_staking_policy = roleStakingPolicy;
  174. }
  175. return {
  176. activate_at: { [values.activateAt]: values.activateAt === 'ExactBlock' ? parseInt(values.activateAtBlock) : null },
  177. commitment: commitment,
  178. human_readable_text: values.humanReadableText,
  179. working_group: values.workingGroup
  180. };
  181. };
  182. const AddWorkingGroupOpeningForm: React.FunctionComponent<FormInnerProps> = props => {
  183. const { handleChange, errors, touched, values, setFieldValue, myMemberId, myMembership } = props;
  184. useEffect(() => {
  185. if (myMembership && !touched.humanReadableText) {
  186. setFieldValue(
  187. 'humanReadableText',
  188. JSON.stringify(HRTDefault(myMembership.handle.toString(), values.workingGroup), undefined, 4)
  189. );
  190. }
  191. }, [values.workingGroup, myMembership]);
  192. const errorLabelsProps = getFormErrorLabelsProps<FormValues>(errors, touched);
  193. return (
  194. <GenericWorkingGroupProposalForm
  195. {...props}
  196. txMethod="createAddWorkingGroupLeaderOpeningProposal"
  197. proposalType="AddWorkingGroupLeaderOpening"
  198. submitParams={[
  199. myMemberId,
  200. values.title,
  201. values.rationale,
  202. '{STAKE}',
  203. valuesToAddOpeningParams(values)
  204. ]}
  205. >
  206. <Grid columns="4" doubling stackable verticalAlign="bottom">
  207. <Grid.Row>
  208. <Grid.Column>
  209. <FormField label="Activate opening at">
  210. <Dropdown
  211. onChange={handleChange}
  212. name="activateAt"
  213. selection
  214. options={Object.keys(ActivateOpeningAtDef).map(wgKey => ({ text: wgKey, value: wgKey }))}
  215. value={values.activateAt}
  216. />
  217. </FormField>
  218. </Grid.Column>
  219. <Grid.Column>
  220. { values.activateAt === 'ExactBlock' && (
  221. <InputFormField
  222. onChange={handleChange}
  223. name="activateAtBlock"
  224. error={errorLabelsProps.activateAtBlock}
  225. value={values.activateAtBlock}
  226. placeholder={'Provide the block number'}
  227. />
  228. ) }
  229. </Grid.Column>
  230. </Grid.Row>
  231. </Grid>
  232. { values.activateAt === 'ExactBlock' && (
  233. <Message info>
  234. In case <b>ExactBlock</b> is specified, the opening will remain in <i>Waiting to Begin</i> stage (which means it will be visible,
  235. but no applicants will be able to apply yet) until current block number will equal the specified number.
  236. </Message>
  237. ) }
  238. <Grid columns="4" doubling stackable verticalAlign="bottom">
  239. <Grid.Row>
  240. <Grid.Column>
  241. <InputFormField
  242. label="Max. review period"
  243. onChange={handleChange}
  244. name="maxReviewPeriodLength"
  245. error={errorLabelsProps.maxReviewPeriodLength}
  246. value={values.maxReviewPeriodLength}
  247. placeholder={'ie. 72000'}
  248. unit="blocks"
  249. />
  250. </Grid.Column>
  251. </Grid.Row>
  252. </Grid>
  253. <Grid columns="4" doubling stackable verticalAlign="bottom">
  254. <Grid.Row>
  255. <Grid.Column>
  256. <FormField label="Applications limit">
  257. <Checkbox
  258. toggle
  259. onChange={(e, data) => { setFieldValue('applicationsLimited', data.checked); }}
  260. label="Limit applications"
  261. checked={values.applicationsLimited}/>
  262. </FormField>
  263. { values.applicationsLimited && (
  264. <InputFormField
  265. onChange={handleChange}
  266. name="maxApplications"
  267. error={errorLabelsProps.maxApplications}
  268. value={values.maxApplications}
  269. placeholder={'Max. number of applications'}
  270. />
  271. ) }
  272. </Grid.Column>
  273. </Grid.Row>
  274. </Grid>
  275. <Grid columns="2" stackable style={{ marginBottom: 0 }}>
  276. <Grid.Row>
  277. <Grid.Column>
  278. <StakeFields stakeType="application" {...{ errorLabelsProps, values, handleChange, setFieldValue }}/>
  279. </Grid.Column>
  280. <Grid.Column>
  281. <StakeFields stakeType="role" {...{ errorLabelsProps, values, handleChange, setFieldValue }}/>
  282. </Grid.Column>
  283. </Grid.Row>
  284. </Grid>
  285. <Grid columns="2" stackable style={{ marginBottom: 0 }}>
  286. <Grid.Row>
  287. <Grid.Column>
  288. <InputFormField
  289. onChange={handleChange}
  290. name="terminateRoleUnstakingPeriod"
  291. error={errorLabelsProps.terminateRoleUnstakingPeriod}
  292. value={values.terminateRoleUnstakingPeriod}
  293. label={'Terminate role unstaking period'}
  294. placeholder={'ie. 14400'}
  295. unit="blocks"
  296. help={
  297. 'In case leader role or application is terminated - this will be the unstaking period for the role stake (in blocks).'
  298. }
  299. />
  300. </Grid.Column>
  301. <Grid.Column>
  302. <InputFormField
  303. onChange={handleChange}
  304. name="leaveRoleUnstakingPeriod"
  305. error={errorLabelsProps.leaveRoleUnstakingPeriod}
  306. value={values.leaveRoleUnstakingPeriod}
  307. label={'Leave role unstaking period'}
  308. placeholder={'ie. 14400'}
  309. unit="blocks"
  310. help={
  311. 'In case leader leaves/exits his role - this will be the unstaking period for his role stake (in blocks). ' +
  312. 'It also applies when user is withdrawing an active leader application.'
  313. }
  314. />
  315. </Grid.Column>
  316. </Grid.Row>
  317. </Grid>
  318. <TextareaFormField
  319. label="Opening schema (human_readable_text)"
  320. help="JSON schema that describes some characteristics of the opening presented in the UI (headers, content, application form etc.)"
  321. onChange={handleChange}
  322. name="humanReadableText"
  323. placeholder="Paste the JSON schema here..."
  324. error={errorLabelsProps.humanReadableText}
  325. value={values.humanReadableText}
  326. rows={20}
  327. />
  328. </GenericWorkingGroupProposalForm>
  329. );
  330. };
  331. const FormContainer = withFormContainer<FormContainerProps, FormValues>({
  332. mapPropsToValues: (props: FormContainerProps) => ({
  333. ...defaultValues,
  334. ...(props.initialData || {})
  335. }),
  336. validationSchema: (props: FormContainerProps) => Yup.object().shape({
  337. ...genericFormDefaultOptions.validationSchema,
  338. ...Validation.AddWorkingGroupLeaderOpening(
  339. props.currentBlock?.toNumber() || 0,
  340. props.HRTConstraint
  341. )
  342. }),
  343. handleSubmit: genericFormDefaultOptions.handleSubmit,
  344. displayName: 'AddWorkingGroupOpeningForm'
  345. })(AddWorkingGroupOpeningForm);
  346. export default withCalls<ExportComponentProps>(
  347. ['derive.chain.bestNumber', { propName: 'currentBlock' }],
  348. ['query.storageWorkingGroup.openingHumanReadableText', { propName: 'HRTConstraint' }]
  349. )(
  350. withProposalFormData<FormContainerProps, ExportComponentProps>(FormContainer)
  351. );