SetContentWorkingGroupLeadForm.tsx 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. import React, { useEffect, useState } from "react";
  2. import { Dropdown, Label, Loader, Message, Icon, DropdownItemProps, DropdownOnSearchChangeData, DropdownProps } from "semantic-ui-react";
  3. import { getFormErrorLabelsProps } from "./errorHandling";
  4. import * as Yup from "yup";
  5. import {
  6. GenericProposalForm,
  7. GenericFormValues,
  8. genericFormDefaultOptions,
  9. genericFormDefaultValues,
  10. withProposalFormData,
  11. ProposalFormExportProps,
  12. ProposalFormContainerProps,
  13. ProposalFormInnerProps
  14. } from "./GenericProposalForm";
  15. import Validation from "../validationSchema";
  16. import { FormField } from "./FormFields";
  17. import { withFormContainer } from "./FormContainer";
  18. import { useTransport } from "../runtime";
  19. import { usePromise } from "../utils";
  20. import { Profile } from "@joystream/types/members";
  21. import PromiseComponent from "../Proposal/PromiseComponent";
  22. import _ from 'lodash';
  23. import "./forms.css";
  24. type FormValues = GenericFormValues & {
  25. workingGroupLead: any;
  26. };
  27. const defaultValues: FormValues = {
  28. ...genericFormDefaultValues,
  29. workingGroupLead: ""
  30. };
  31. type FormAdditionalProps = {}; // Aditional props coming all the way from export comonent into the inner form.
  32. type ExportComponentProps = ProposalFormExportProps<FormAdditionalProps, FormValues>;
  33. type FormContainerProps = ProposalFormContainerProps<ExportComponentProps>;
  34. type FormInnerProps = ProposalFormInnerProps<FormContainerProps, FormValues>;
  35. function memberOptionKey(id: number, profile: Profile) {
  36. return `${id}:${profile.root_account.toString()}`;
  37. }
  38. const MEMBERS_QUERY_MIN_LENGTH = 4;
  39. const MEMBERS_NONE_OPTION: DropdownItemProps = {
  40. key: '- NONE -',
  41. text: '- NONE -',
  42. value: 'none'
  43. }
  44. function membersToOptions(members: { id: number, profile: Profile }[]) {
  45. return [MEMBERS_NONE_OPTION].concat(
  46. members
  47. .map(({ id, profile }) => ({
  48. key: profile.handle,
  49. text: `${ profile.handle } (id:${ id })`,
  50. value: memberOptionKey(id, profile),
  51. image: profile.avatar_uri.toString() ? { avatar: true, src: profile.avatar_uri } : null
  52. }))
  53. );
  54. }
  55. function filterMembers(options: DropdownItemProps[], query: string) {
  56. if (query.length < MEMBERS_QUERY_MIN_LENGTH) {
  57. return [MEMBERS_NONE_OPTION];
  58. }
  59. const regexp = new RegExp(_.escapeRegExp(query));
  60. return options.filter((opt) => regexp.test((opt.text || '').toString()))
  61. }
  62. type MemberWithId = { id: number; profile: Profile };
  63. const SetContentWorkingGroupsLeadForm: React.FunctionComponent<FormInnerProps> = props => {
  64. const { handleChange, errors, touched, values } = props;
  65. const errorLabelsProps = getFormErrorLabelsProps<FormValues>(errors, touched);
  66. // State
  67. const [ membersOptions, setMembersOptions ] = useState([] as DropdownItemProps[]);
  68. const [ filteredOptions, setFilteredOptions ] = useState([] as DropdownItemProps[]);
  69. const [ membersSearchQuery, setMembersSearchQuery ] = useState("");
  70. // Transport
  71. const transport = useTransport();
  72. const [members, /* error */, loading] = usePromise<MemberWithId[]>(
  73. () => transport.membersExceptCouncil(),
  74. []
  75. );
  76. const [currentLead, clError, clLoading] = usePromise<MemberWithId | null>(
  77. () => transport.WGLead(),
  78. null
  79. );
  80. // Generate members options array on load
  81. useEffect(() => {
  82. if (members.length) {
  83. setMembersOptions(membersToOptions(members));
  84. }
  85. }, [members]);
  86. // Filter options on search query change (we "pulled-out" this logic here to avoid lags)
  87. useEffect(() => {
  88. setFilteredOptions(filterMembers(membersOptions, membersSearchQuery));
  89. }, [membersSearchQuery]);
  90. return (
  91. <PromiseComponent error={clError} loading={clLoading} message="Fetching current lead...">
  92. <GenericProposalForm
  93. {...props}
  94. txMethod="createSetLeadProposal"
  95. proposalType="SetLead"
  96. submitParams={[
  97. props.myMemberId,
  98. values.title,
  99. values.rationale,
  100. "{STAKE}",
  101. values.workingGroupLead !== MEMBERS_NONE_OPTION.value ? values.workingGroupLead.split(":") : undefined
  102. ]}
  103. >
  104. {loading ? (
  105. <>
  106. <Loader active inline style={{ marginRight: "5px" }} /> Fetching members...
  107. </>
  108. ) : (<>
  109. <FormField
  110. error={errorLabelsProps.workingGroupLead}
  111. label="New Content Working Group Lead"
  112. help={
  113. 'The member you propose to set as a new Content Working Group Lead. ' +
  114. 'Start typing handle or use "id:[ID]" query.'
  115. }
  116. >
  117. {
  118. (!values.workingGroupLead || membersSearchQuery.length > 0) &&
  119. (MEMBERS_QUERY_MIN_LENGTH - membersSearchQuery.length) > 0 && (
  120. <Label>
  121. Type at least { MEMBERS_QUERY_MIN_LENGTH - membersSearchQuery.length } more characters
  122. </Label>
  123. )
  124. }
  125. <Dropdown
  126. clearable
  127. // Here we just ignore search query and return all options, since we pulled-out this logic
  128. // to our component to avoid lags
  129. search={ (options: DropdownItemProps[], query:string ) => options }
  130. // On search change we update it in our state
  131. onSearchChange={ (e: React.SyntheticEvent, data: DropdownOnSearchChangeData) => {
  132. setMembersSearchQuery(data.searchQuery);
  133. } }
  134. name="workingGroupLead"
  135. placeholder={ "Start typing member handle or \"id:[ID]\" query..." }
  136. fluid
  137. selection
  138. options={filteredOptions}
  139. onChange={
  140. (e: React.ChangeEvent<any>, data: DropdownProps) => {
  141. // Fix TypeScript issue
  142. const originalHandler = handleChange as (e: React.ChangeEvent<any>, data: DropdownProps) => void;
  143. originalHandler(e, data);
  144. if (!data.value) {
  145. setMembersSearchQuery('');
  146. }
  147. }
  148. }
  149. value={values.workingGroupLead}
  150. />
  151. {errorLabelsProps.workingGroupLead && <Label {...errorLabelsProps.workingGroupLead} prompt />}
  152. </FormField>
  153. <Message info active={1}>
  154. <Message.Content>
  155. <Icon name="info circle"/>
  156. Current Content Working Group lead: <b>{ (currentLead && currentLead.profile.handle) || 'NONE' }</b>
  157. </Message.Content>
  158. </Message>
  159. </>)}
  160. </GenericProposalForm>
  161. </PromiseComponent>
  162. );
  163. };
  164. const FormContainer = withFormContainer<FormContainerProps, FormValues>({
  165. mapPropsToValues: (props: FormContainerProps) => ({
  166. ...defaultValues,
  167. ...(props.initialData || {})
  168. }),
  169. validationSchema: Yup.object().shape({
  170. ...genericFormDefaultOptions.validationSchema,
  171. workingGroupLead: Validation.SetLead.workingGroupLead
  172. }),
  173. handleSubmit: genericFormDefaultOptions.handleSubmit,
  174. displayName: "SetContentWorkingGroupLeadForm"
  175. })(SetContentWorkingGroupsLeadForm);
  176. export default withProposalFormData<FormContainerProps, ExportComponentProps>(FormContainer);