Browse Source

Merge pull request #415 from Lezek123/set-empty-lead

Allow setting empty lead + members dropdown optimalizations
Francesco 4 years ago
parent
commit
7764ce1578

+ 12 - 3
packages/joy-proposals/src/Proposal/Body.tsx

@@ -26,16 +26,25 @@ type BodyProps = {
   cancellationFee: number;
 };
 
-function ProposedAddress(props: { address: string }) {
+function ProposedAddress(props: { address?: string | null }) {
+  if (props.address === null || props.address === undefined) {
+    return <>NONE</>;
+  }
+
   return (
     <AddressMini value={props.address} isShort={false} isPadded={false} withAddress={true} style={{ padding: 0 }} />
   );
 }
 
-function ProposedMember(props: { memberId: MemberId | number }) {
+function ProposedMember(props: { memberId?: MemberId | number | null }) {
+  if (props.memberId === null || props.memberId === undefined) {
+    return <>NONE</>;
+  }
+  const memberId: MemberId | number = props.memberId;
+
   const transport = useTransport();
   const [ member, error, loading ] = usePromise<Option<Profile> | null>(
-    () => transport.memberProfile(props.memberId),
+    () => transport.memberProfile(memberId),
     null
   );
 

+ 85 - 22
packages/joy-proposals/src/forms/SetContentWorkingGroupLeadForm.tsx

@@ -1,5 +1,5 @@
-import React, { useEffect } from "react";
-import { Dropdown, Label, Loader, Message, Icon } from "semantic-ui-react";
+import React, { useEffect, useState } from "react";
+import { Dropdown, Label, Loader, Message, Icon, DropdownItemProps, DropdownOnSearchChangeData, DropdownProps } from "semantic-ui-react";
 import { getFormErrorLabelsProps } from "./errorHandling";
 import * as Yup from "yup";
 import {
@@ -19,6 +19,7 @@ import { useTransport } from "../runtime";
 import { usePromise } from "../utils";
 import { Profile } from "@joystream/types/members";
 import PromiseComponent from "../Proposal/PromiseComponent";
+import _ from 'lodash';
 import "./forms.css";
 
 type FormValues = GenericFormValues & {
@@ -39,40 +40,76 @@ function memberOptionKey(id: number, profile: Profile) {
   return `${id}:${profile.root_account.toString()}`;
 }
 
+const MEMBERS_QUERY_MIN_LENGTH = 4;
+const MEMBERS_NONE_OPTION: DropdownItemProps = {
+  key: '- NONE -',
+  text: '- NONE -',
+  value: 'none'
+}
+
 function membersToOptions(members: { id: number, profile: Profile }[]) {
-  return members.map(({ id, profile }) => ({
-    key: profile.handle,
-    text: profile.handle,
-    value: memberOptionKey(id, profile),
-    image: profile.avatar_uri.toString() ? { avatar: true, src: profile.avatar_uri } : null
-  }));
+  return [MEMBERS_NONE_OPTION].concat(
+    members
+      .map(({ id, profile }) => ({
+        key: profile.handle,
+        text: `${ profile.handle } (id:${ id })`,
+        value: memberOptionKey(id, profile),
+        image: profile.avatar_uri.toString() ? { avatar: true, src: profile.avatar_uri } : null
+      }))
+  );
+}
+
+function filterMembers(options: DropdownItemProps[], query: string) {
+  if (query.length < MEMBERS_QUERY_MIN_LENGTH) {
+    return [MEMBERS_NONE_OPTION];
+  }
+  const regexp = new RegExp(_.escapeRegExp(query));
+  return options.filter((opt) => regexp.test((opt.text || '').toString()))
 }
 
+type MemberWithId = { id: number; profile: Profile };
+
 const SetContentWorkingGroupsLeadForm: React.FunctionComponent<FormInnerProps> = props => {
-  const { handleChange, errors, touched, values, setFieldValue } = props;
+  const { handleChange, errors, touched, values } = props;
   const errorLabelsProps = getFormErrorLabelsProps<FormValues>(errors, touched);
+  // State
+  const [ membersOptions, setMembersOptions ] = useState([] as DropdownItemProps[]);
+  const [ filteredOptions, setFilteredOptions ] = useState([] as DropdownItemProps[]);
+  const [ membersSearchQuery, setMembersSearchQuery ] = useState("");
+  // Transport
   const transport = useTransport();
-  const [members, /* error */, loading] = usePromise<{ id: number; profile: Profile }[]>(
+  const [members, /* error */, loading] = usePromise<MemberWithId[]>(
     () => transport.membersExceptCouncil(),
     []
   );
-  const membersOptions = membersToOptions(members);
-  const [currentLead, clError, clLoading] = usePromise<{ id: number; profile: Profile } | null>(
+  const [currentLead, clError, clLoading] = usePromise<MemberWithId | null>(
     () => transport.WGLead(),
     null
   );
+  // Generate members options array on load
   useEffect(() => {
-    if (currentLead) {
-      setFieldValue('workingGroupLead', memberOptionKey(currentLead.id, currentLead.profile));
+    if (members.length) {
+      setMembersOptions(membersToOptions(members));
     }
-  }, [currentLead])
+  }, [members]);
+  // Filter options on search query change (we "pulled-out" this logic here to avoid lags)
+  useEffect(() => {
+    setFilteredOptions(filterMembers(membersOptions, membersSearchQuery));
+  }, [membersSearchQuery]);
+
   return (
     <PromiseComponent error={clError} loading={clLoading} message="Fetching current lead...">
       <GenericProposalForm
         {...props}
         txMethod="createSetLeadProposal"
         proposalType="SetLead"
-        submitParams={[props.myMemberId, values.title, values.rationale, "{STAKE}", values.workingGroupLead.split(":")]}
+        submitParams={[
+          props.myMemberId,
+          values.title,
+          values.rationale,
+          "{STAKE}",
+          values.workingGroupLead !== MEMBERS_NONE_OPTION.value ? values.workingGroupLead.split(":") : undefined
+        ]}
       >
         {loading ? (
           <>
@@ -82,22 +119,48 @@ const SetContentWorkingGroupsLeadForm: React.FunctionComponent<FormInnerProps> =
           <FormField
             error={errorLabelsProps.workingGroupLead}
             label="New Content Working Group Lead"
-            help="The member you propose to set as a new Content Working Group Lead"
+            help={
+              'The member you propose to set as a new Content Working Group Lead. ' +
+              'Start typing handle or use "id:[ID]" query.'
+            }
           >
+            {
+              (!values.workingGroupLead || membersSearchQuery.length > 0) &&
+              (MEMBERS_QUERY_MIN_LENGTH - membersSearchQuery.length) > 0 && (
+                <Label>
+                  Type at least { MEMBERS_QUERY_MIN_LENGTH - membersSearchQuery.length } more characters
+                </Label>
+              )
+            }
             <Dropdown
               clearable
-              search
+              // Here we just ignore search query and return all options, since we pulled-out this logic
+              // to our component to avoid lags
+              search={ (options: DropdownItemProps[], query:string ) => options }
+              // On search change we update it in our state
+              onSearchChange={ (e: React.SyntheticEvent, data: DropdownOnSearchChangeData) => {
+                setMembersSearchQuery(data.searchQuery);
+              } }
               name="workingGroupLead"
-              placeholder="Select a member"
+              placeholder={ "Start typing member handle or \"id:[ID]\" query..." }
               fluid
               selection
-              options={membersOptions}
-              onChange={handleChange}
+              options={filteredOptions}
+              onChange={
+                (e: React.ChangeEvent<any>, data: DropdownProps) => {
+                  // Fix TypeScript issue
+                  const originalHandler = handleChange as (e: React.ChangeEvent<any>, data: DropdownProps) => void;
+                  originalHandler(e, data);
+                  if (!data.value) {
+                    setMembersSearchQuery('');
+                  }
+                }
+              }
               value={values.workingGroupLead}
             />
             {errorLabelsProps.workingGroupLead && <Label {...errorLabelsProps.workingGroupLead} prompt />}
           </FormField>
-          <Message info active>
+          <Message info active={1}>
             <Message.Content>
               <Icon name="info circle"/>
               Current Content Working Group lead: <b>{ (currentLead && currentLead.profile.handle) || 'NONE' }</b>