Эх сурвалжийг харах

typescript-eslint/no-use-before-define fix

Leszek Wiesner 4 жил өмнө
parent
commit
9f3cb135bc

+ 5 - 5
pioneer/packages/apps/src/SideBar/index.tsx

@@ -44,6 +44,11 @@ export function OuterLink ({ url, title, icon = 'external alternate' }: OuterLin
   );
 }
 
+function JoystreamLogo (isCollapsed: boolean) {
+  const logo = isCollapsed ? 'images/logo-j.svg' : 'images/logo-joytream.svg';
+  return <img alt="Joystream" className="apps--SideBar-logo" src={logo} />;
+}
+
 function SideBar ({
   className,
   collapse,
@@ -149,8 +154,3 @@ export default translate(
     }
   `
 );
-
-function JoystreamLogo (isCollapsed: boolean) {
-  const logo = isCollapsed ? 'images/logo-j.svg' : 'images/logo-joytream.svg';
-  return <img alt="Joystream" className="apps--SideBar-logo" src={logo} />;
-}

+ 20 - 20
pioneer/packages/joy-forum/src/CategoryList.tsx

@@ -108,10 +108,6 @@ type ViewCategoryProps = InnerViewCategoryProps & {
   id: CategoryId;
 };
 
-const ViewCategory = withForumCalls<ViewCategoryProps>(
-  ['categoryById', { propName: 'category', paramName: 'id' }]
-)(InnerViewCategory);
-
 function InnerViewCategory (props: InnerViewCategoryProps) {
   const { history, category, page = 1, preview = false } = props;
 
@@ -201,6 +197,10 @@ function InnerViewCategory (props: InnerViewCategoryProps) {
   </>);
 }
 
+const ViewCategory = withForumCalls<ViewCategoryProps>(
+  ['categoryById', { propName: 'category', paramName: 'id' }]
+)(InnerViewCategory);
+
 type InnerCategoryThreadsProps = {
   category: Category;
   page: number;
@@ -211,14 +211,6 @@ type CategoryThreadsProps = ApiProps & InnerCategoryThreadsProps & {
   nextThreadId?: ThreadId;
 };
 
-export const CategoryThreads = withMulti(
-  InnerCategoryThreads,
-  withApi,
-  withForumCalls<CategoryThreadsProps>(
-    ['nextThreadId', { propName: 'nextThreadId' }]
-  )
-);
-
 function InnerCategoryThreads (props: CategoryThreadsProps) {
   const { api, category, nextThreadId, page, history } = props;
 
@@ -317,6 +309,14 @@ function InnerCategoryThreads (props: CategoryThreadsProps) {
   </>;
 }
 
+export const CategoryThreads = withMulti(
+  InnerCategoryThreads,
+  withApi,
+  withForumCalls<CategoryThreadsProps>(
+    ['nextThreadId', { propName: 'nextThreadId' }]
+  )
+);
+
 type ViewCategoryByIdProps = UrlHasIdProps & {
   history: History;
   match: {
@@ -343,14 +343,6 @@ type CategoryListProps = ApiProps & {
   parentId?: CategoryId;
 };
 
-export const CategoryList = withMulti(
-  InnerCategoryList,
-  withApi,
-  withForumCalls<CategoryListProps>(
-    ['nextCategoryId', { propName: 'nextCategoryId' }]
-  )
-);
-
 function InnerCategoryList (props: CategoryListProps) {
   const { api, parentId, nextCategoryId } = props;
   const [loaded, setLoaded] = useState(false);
@@ -409,3 +401,11 @@ function InnerCategoryList (props: CategoryListProps) {
     </Table>
   );
 }
+
+export const CategoryList = withMulti(
+  InnerCategoryList,
+  withApi,
+  withForumCalls<CategoryListProps>(
+    ['nextCategoryId', { propName: 'nextCategoryId' }]
+  )
+);

+ 6 - 6
pioneer/packages/joy-forum/src/EditReply.tsx

@@ -72,6 +72,12 @@ const InnerForm = (props: FormProps) => {
     text
   } = values;
 
+  const goToThreadView = () => {
+    if (history) {
+      history.push('/forum/threads/' + threadId.toString());
+    }
+  };
+
   const onSubmit = (sendTx: () => void) => {
     if (isValid) sendTx();
   };
@@ -102,12 +108,6 @@ const InnerForm = (props: FormProps) => {
     }
   };
 
-  const goToThreadView = () => {
-    if (history) {
-      history.push('/forum/threads/' + threadId.toString());
-    }
-  };
-
   const form =
     <Form className='ui form JoyForm EditEntityForm'>
 

+ 13 - 13
pioneer/packages/joy-forum/src/ForumSudo.tsx

@@ -189,14 +189,6 @@ export const EditForumSudo = withMulti(
   withLoadForumSudo
 );
 
-export function withOnlyForumSudo<P extends {}> (Component: React.ComponentType<P>) {
-  return withMulti(
-    Component,
-    withLoadForumSudo,
-    innerWithOnlyForumSudo
-  );
-}
-
 function innerWithOnlyForumSudo<P extends LoadStructProps> (Component: React.ComponentType<P>) {
   return function (props: P) {
     const { structOpt } = props;
@@ -221,17 +213,20 @@ function innerWithOnlyForumSudo<P extends LoadStructProps> (Component: React.Com
   };
 }
 
+export function withOnlyForumSudo<P extends {}> (Component: React.ComponentType<P>) {
+  return withMulti(
+    Component,
+    withLoadForumSudo,
+    innerWithOnlyForumSudo
+  );
+}
+
 type ForumSudoContextProps = {
   forumSudo?: AccountId;
 };
 
 export const ForumSudoContext = createContext<ForumSudoContextProps>({});
 
-export const ForumSudoProvider = withMulti(
-  InnerForumSudoProvider,
-  withLoadForumSudo
-);
-
 export function InnerForumSudoProvider (props: React.PropsWithChildren<LoadStructProps>) {
   const { structOpt } = props;
   const forumSudo = structOpt ? structOpt.unwrapOr(undefined) : undefined;
@@ -242,6 +237,11 @@ export function InnerForumSudoProvider (props: React.PropsWithChildren<LoadStruc
   );
 }
 
+export const ForumSudoProvider = withMulti(
+  InnerForumSudoProvider,
+  withLoadForumSudo
+);
+
 export function useForumSudo () {
   return useContext(ForumSudoContext);
 }

+ 10 - 10
pioneer/packages/joy-forum/src/ViewThread.tsx

@@ -48,14 +48,6 @@ type ViewThreadProps = ApiProps & InnerViewThreadProps & {
   nextPostId?: ThreadId;
 };
 
-export const ViewThread = withMulti(
-  InnerViewThread,
-  withApi,
-  withForumCalls<ViewThreadProps>(
-    ['nextPostId', { propName: 'nextPostId' }]
-  )
-);
-
 function InnerViewThread (props: ViewThreadProps) {
   const [showModerateForm, setShowModerateForm] = useState(false);
   const { history, category, thread, page = 1, preview = false } = props;
@@ -238,6 +230,14 @@ function InnerViewThread (props: ViewThreadProps) {
   </div>;
 }
 
+export const ViewThread = withMulti(
+  InnerViewThread,
+  withApi,
+  withForumCalls<ViewThreadProps>(
+    ['nextPostId', { propName: 'nextPostId' }]
+  )
+);
+
 type ViewThreadByIdProps = ApiProps & {
   history: History;
   match: {
@@ -248,8 +248,6 @@ type ViewThreadByIdProps = ApiProps & {
   };
 };
 
-export const ViewThreadById = withApi(InnerViewThreadById);
-
 function InnerViewThreadById (props: ViewThreadByIdProps) {
   const { api, history, match: { params: { id, page: pageStr } } } = props;
 
@@ -306,3 +304,5 @@ function InnerViewThreadById (props: ViewThreadByIdProps) {
 
   return <ViewThread id={threadId} category={category} thread={thread} page={page} history={history} />;
 }
+
+export const ViewThreadById = withApi(InnerViewThreadById);

+ 10 - 10
pioneer/packages/joy-forum/src/utils.tsx

@@ -38,13 +38,6 @@ type CategoryCrumbsProps = {
   thread?: Thread;
 };
 
-const CategoryCrumb = withMulti(
-  InnerCategoryCrumb,
-  withForumCalls<CategoryCrumbsProps>(
-    ['categoryById', { propName: 'category', paramName: 'categoryId' }]
-  )
-);
-
 function InnerCategoryCrumb (p: CategoryCrumbsProps) {
   const { category } = p;
 
@@ -64,10 +57,10 @@ function InnerCategoryCrumb (p: CategoryCrumbsProps) {
   return null;
 }
 
-const ThreadCrumb = withMulti(
-  InnerThreadCrumb,
+const CategoryCrumb = withMulti(
+  InnerCategoryCrumb,
   withForumCalls<CategoryCrumbsProps>(
-    ['threadById', { propName: 'thread', paramName: 'threadId' }]
+    ['categoryById', { propName: 'category', paramName: 'categoryId' }]
   )
 );
 
@@ -90,6 +83,13 @@ function InnerThreadCrumb (p: CategoryCrumbsProps) {
   return null;
 }
 
+const ThreadCrumb = withMulti(
+  InnerThreadCrumb,
+  withForumCalls<CategoryCrumbsProps>(
+    ['threadById', { propName: 'thread', paramName: 'threadId' }]
+  )
+);
+
 export const CategoryCrumbs = (p: CategoryCrumbsProps) => {
   return (
     <div className='ui breadcrumb'>

+ 8 - 8
pioneer/packages/joy-media/src/common/MediaPlayerWithResolver.tsx

@@ -29,14 +29,6 @@ function InnerComponent (props: Props) {
   const [contentType, setContentType] = useState<string>();
   const [cancelSource, setCancelSource] = useState<CancelTokenSource>(newCancelSource());
 
-  useEffect(() => {
-    resolveAsset();
-
-    return () => {
-      cancelSource.cancel();
-    };
-  }, [contentId.encode()]);
-
   const resolveAsset = async () => {
     setError(undefined);
     setCancelSource(newCancelSource());
@@ -111,6 +103,14 @@ function InnerComponent (props: Props) {
     setError(new Error('Unable to reach any provider serving this content'));
   };
 
+  useEffect(() => {
+    resolveAsset();
+
+    return () => {
+      cancelSource.cancel();
+    };
+  }, [contentId.encode()]);
+
   console.log('Content id:', contentId.encode());
   console.log('Resolved asset URL:', resolvedAssetUrl);
 

+ 98 - 98
pioneer/packages/joy-roles/src/classifiers.ts

@@ -56,69 +56,6 @@ export interface IBlockQueryer {
   expectedBlockTime: () => Promise<number>;
 }
 
-export async function classifyOpeningStage (queryer: IBlockQueryer, opening: Opening): Promise<OpeningStageClassification> {
-  switch (opening.stage.type) {
-    case OpeningStageKeys.WaitingToBegin:
-      return classifyWaitingToBeginStage(
-        opening,
-        queryer,
-        opening.stage.value as WaitingToBeingOpeningStageVariant
-      );
-
-    case OpeningStageKeys.Active:
-      return classifyActiveOpeningStage(
-        opening,
-        queryer,
-        opening.stage.value as ActiveOpeningStageVariant
-      );
-  }
-
-  throw new Error('Unknown stage type: ' + opening.stage.type);
-}
-
-async function classifyActiveOpeningStage (
-  opening: Opening,
-  queryer: IBlockQueryer,
-  stage: ActiveOpeningStageVariant
-): Promise<OpeningStageClassification> {
-  switch (stage.stage.type) {
-    case ActiveOpeningStageKeys.AcceptingApplications:
-      return classifyActiveOpeningStageAcceptingApplications(
-        queryer,
-        stage.stage.value as AcceptingApplications
-      );
-
-    case ActiveOpeningStageKeys.ReviewPeriod:
-      return classifyActiveOpeningStageReviewPeriod(
-        opening,
-        queryer,
-        stage.stage.value as ReviewPeriod
-      );
-
-    case ActiveOpeningStageKeys.Deactivated:
-      return classifyActiveOpeningStageDeactivated(
-        queryer,
-        stage.stage.value as Deactivated
-      );
-  }
-
-  throw new Error('Unknown active opening stage: ' + stage.stage.type);
-}
-
-async function classifyWaitingToBeginStage (
-  opening: Opening,
-  queryer: IBlockQueryer,
-  stage: WaitingToBeingOpeningStageVariant
-): Promise<OpeningStageClassification> {
-  const blockNumber = opening.created.toNumber();
-  return {
-    state: OpeningState.WaitingToBegin,
-    starting_block: blockNumber,
-    starting_block_hash: await queryer.blockHash(blockNumber),
-    starting_time: await queryer.blockTimestamp(blockNumber)
-  };
-}
-
 async function classifyActiveOpeningStageAcceptingApplications (
   queryer: IBlockQueryer,
   stage: AcceptingApplications
@@ -191,6 +128,69 @@ async function classifyActiveOpeningStageDeactivated (
   };
 }
 
+async function classifyActiveOpeningStage (
+  opening: Opening,
+  queryer: IBlockQueryer,
+  stage: ActiveOpeningStageVariant
+): Promise<OpeningStageClassification> {
+  switch (stage.stage.type) {
+    case ActiveOpeningStageKeys.AcceptingApplications:
+      return classifyActiveOpeningStageAcceptingApplications(
+        queryer,
+        stage.stage.value as AcceptingApplications
+      );
+
+    case ActiveOpeningStageKeys.ReviewPeriod:
+      return classifyActiveOpeningStageReviewPeriod(
+        opening,
+        queryer,
+        stage.stage.value as ReviewPeriod
+      );
+
+    case ActiveOpeningStageKeys.Deactivated:
+      return classifyActiveOpeningStageDeactivated(
+        queryer,
+        stage.stage.value as Deactivated
+      );
+  }
+
+  throw new Error('Unknown active opening stage: ' + stage.stage.type);
+}
+
+async function classifyWaitingToBeginStage (
+  opening: Opening,
+  queryer: IBlockQueryer,
+  stage: WaitingToBeingOpeningStageVariant
+): Promise<OpeningStageClassification> {
+  const blockNumber = opening.created.toNumber();
+  return {
+    state: OpeningState.WaitingToBegin,
+    starting_block: blockNumber,
+    starting_block_hash: await queryer.blockHash(blockNumber),
+    starting_time: await queryer.blockTimestamp(blockNumber)
+  };
+}
+
+export async function classifyOpeningStage (queryer: IBlockQueryer, opening: Opening): Promise<OpeningStageClassification> {
+  switch (opening.stage.type) {
+    case OpeningStageKeys.WaitingToBegin:
+      return classifyWaitingToBeginStage(
+        opening,
+        queryer,
+        opening.stage.value as WaitingToBeingOpeningStageVariant
+      );
+
+    case OpeningStageKeys.Active:
+      return classifyActiveOpeningStage(
+        opening,
+        queryer,
+        opening.stage.value as ActiveOpeningStageVariant
+      );
+  }
+
+  throw new Error('Unknown stage type: ' + opening.stage.type);
+}
+
 export type StakeRequirementSetClassification = {
   application: ApplicationStakeRequirement;
   role: RoleStakeRequirement;
@@ -200,17 +200,16 @@ interface StakeRequirementConstructor<T extends StakeRequirement> {
   new(hard: Balance, stakeType?: StakeType): T;
 }
 
-export function classifyOpeningStakes (opening: Opening): StakeRequirementSetClassification {
-  return {
-    application: classifyStakeRequirement<ApplicationStakeRequirement>(
-      ApplicationStakeRequirement,
-      opening.application_staking_policy
-    ),
-    role: classifyStakeRequirement<RoleStakeRequirement>(
-      RoleStakeRequirement,
-      opening.role_staking_policy
-    )
-  };
+function classifyStakeType (mode: StakingAmountLimitMode): StakeType {
+  switch (mode.type) {
+    case StakingAmountLimitModeKeys.AtLeast:
+      return StakeType.AtLeast;
+
+    case StakingAmountLimitModeKeys.Exact:
+      return StakeType.Fixed;
+  }
+
+  throw new Error('Unknown stake type: ' + mode.type);
 }
 
 function classifyStakeRequirement<T extends StakeRequirement> (
@@ -229,16 +228,34 @@ function classifyStakeRequirement<T extends StakeRequirement> (
   );
 }
 
-function classifyStakeType (mode: StakingAmountLimitMode): StakeType {
-  switch (mode.type) {
-    case StakingAmountLimitModeKeys.AtLeast:
-      return StakeType.AtLeast;
+export function classifyOpeningStakes (opening: Opening): StakeRequirementSetClassification {
+  return {
+    application: classifyStakeRequirement<ApplicationStakeRequirement>(
+      ApplicationStakeRequirement,
+      opening.application_staking_policy
+    ),
+    role: classifyStakeRequirement<RoleStakeRequirement>(
+      RoleStakeRequirement,
+      opening.role_staking_policy
+    )
+  };
+}
 
-    case StakingAmountLimitModeKeys.Exact:
-      return StakeType.Fixed;
+function classifyApplicationCancellationFromCause (cause: ApplicationDeactivationCause): CancelledReason | undefined {
+  console.log(cause.type);
+  switch (cause.type) {
+    case ApplicationDeactivationCauseKeys.External:
+      return CancelledReason.ApplicantCancelled;
+
+    case ApplicationDeactivationCauseKeys.OpeningCancelled:
+    case ApplicationDeactivationCauseKeys.OpeningFilled:
+      return CancelledReason.HirerCancelledOpening;
+
+    case ApplicationDeactivationCauseKeys.ReviewPeriodExpired:
+      return CancelledReason.NoOneHired;
   }
 
-  throw new Error('Unknown stake type: ' + mode.type);
+  return undefined;
 }
 
 export function classifyApplicationCancellation (a: Application): CancelledReason | undefined {
@@ -257,23 +274,6 @@ export function classifyApplicationCancellation (a: Application): CancelledReaso
   return undefined;
 }
 
-function classifyApplicationCancellationFromCause (cause: ApplicationDeactivationCause): CancelledReason | undefined {
-  console.log(cause.type);
-  switch (cause.type) {
-    case ApplicationDeactivationCauseKeys.External:
-      return CancelledReason.ApplicantCancelled;
-
-    case ApplicationDeactivationCauseKeys.OpeningCancelled:
-    case ApplicationDeactivationCauseKeys.OpeningFilled:
-      return CancelledReason.HirerCancelledOpening;
-
-    case ApplicationDeactivationCauseKeys.ReviewPeriodExpired:
-      return CancelledReason.NoOneHired;
-  }
-
-  return undefined;
-}
-
 export function isApplicationHired (a: Application): boolean {
   switch (a.stage.type) {
     case ApplicationStageKeys.Unstaking:

+ 32 - 32
pioneer/packages/joy-roles/src/flows/apply.tsx

@@ -360,6 +360,21 @@ function CTA (props: CTAProps) {
   );
 }
 
+function stakeCount (props: StakeRequirementProps): number {
+  return (props.requiredApplicationStake.anyRequirement() ? 1 : 0) +
+    (props.requiredRoleStake.anyRequirement() ? 1 : 0);
+}
+
+function zeroOrTwoStakes (props: StakeRequirementProps): boolean {
+  const count = stakeCount(props);
+  return (count == 0 || count == 2);
+}
+
+function bothStakesVariable (props: StakeRequirementProps): boolean {
+  return props.requiredApplicationStake.anyRequirement() && props.requiredRoleStake.anyRequirement() &&
+    props.requiredApplicationStake.atLeast() && props.requiredRoleStake.atLeast();
+}
+
 export type StageTransitionProps = {
   nextTransition: () => void;
   prevTransition: () => void;
@@ -414,21 +429,6 @@ export function ConfirmStakesStage (props: ConfirmStakesStageProps & StageTransi
   );
 }
 
-function stakeCount (props: StakeRequirementProps): number {
-  return (props.requiredApplicationStake.anyRequirement() ? 1 : 0) +
-    (props.requiredRoleStake.anyRequirement() ? 1 : 0);
-}
-
-function zeroOrTwoStakes (props: StakeRequirementProps): boolean {
-  const count = stakeCount(props);
-  return (count == 0 || count == 2);
-}
-
-function bothStakesVariable (props: StakeRequirementProps): boolean {
-  return props.requiredApplicationStake.anyRequirement() && props.requiredRoleStake.anyRequirement() &&
-    props.requiredApplicationStake.atLeast() && props.requiredRoleStake.atLeast();
-}
-
 type StakeSelectorProps = ConfirmStakesStageProps & ApplicationStatusProps
 
 function ConfirmStakes (props: StakeSelectorProps) {
@@ -497,23 +497,6 @@ export function ConfirmStakes2Up (props: ConfirmStakes2UpProps) {
   const minStake = props.slots[0];
   const [combined, setCombined] = useState(new u128(0));
 
-  // Watch stake values
-  useEffect(() => {
-    const newCombined = Add(props.selectedApplicationStake, props.selectedRoleStake);
-    setCombined(newCombined);
-  },
-  [props.selectedApplicationStake, props.selectedRoleStake]
-  );
-
-  useEffect(() => {
-    setRank(findRankValue(combined));
-    if (slotCount > 0) {
-      setValid(combined.gte(minStake));
-    }
-  },
-  [combined]
-  );
-
   const findRankValue = (newStake: Balance): number => {
     if (slotCount == 0) {
       return 0;
@@ -532,6 +515,23 @@ export function ConfirmStakes2Up (props: ConfirmStakes2UpProps) {
     return 0;
   };
 
+  // Watch stake values
+  useEffect(() => {
+    const newCombined = Add(props.selectedApplicationStake, props.selectedRoleStake);
+    setCombined(newCombined);
+  },
+  [props.selectedApplicationStake, props.selectedRoleStake]
+  );
+
+  useEffect(() => {
+    setRank(findRankValue(combined));
+    if (slotCount > 0) {
+      setValid(combined.gte(minStake));
+    }
+  },
+  [combined]
+  );
+
   const ticks = [];
   for (let i = 0; i < slotCount; i++) {
     ticks.push(<div key={i} className="tick" style={{ width: (100 / slotCount) + '%' }}>{slotCount - i}</div>);

+ 9 - 9
pioneer/packages/joy-roles/src/index.tsx

@@ -25,6 +25,15 @@ import './index.sass';
 
 import translate from './translate';
 
+const renderViewComponent = (Component: ViewComponent<any>, props?: RouteComponentProps) => {
+  let params = new Map<string, string>();
+  if (props && props.match.params) {
+    params = new Map<string, string>(Object.entries(props.match.params));
+  }
+
+  return <Component params={params} />;
+};
+
 type Props = AppProps & ApiProps & I18nProps & MyAccountProps
 
 export const App: React.FC<Props> = (props: Props) => {
@@ -88,15 +97,6 @@ export const App: React.FC<Props> = (props: Props) => {
   );
 };
 
-const renderViewComponent = (Component: ViewComponent<any>, props?: RouteComponentProps) => {
-  let params = new Map<string, string>();
-  if (props && props.match.params) {
-    params = new Map<string, string>(Object.entries(props.match.params));
-  }
-
-  return <Component params={params} />;
-};
-
 export default withMulti(
   App,
   translate,

+ 26 - 26
pioneer/packages/joy-roles/src/tabs/Admin.controller.tsx

@@ -98,22 +98,6 @@ type State = {
   modalOpen: boolean;
 }
 
-const newEmptyState = (): State => {
-  return {
-    openings: new Map<number, opening>(),
-    openingDescriptor: stockOpenings[0],
-    modalOpen: false
-  };
-};
-
-// TODO: Make a list of stock openings
-type openingDescriptor = {
-  title: string;
-  start: ActivateOpeningAt;
-  policy: IOpeningPolicyCommitment;
-  text: Text;
-}
-
 function newHRT (title: string): Text {
   return new Text(JSON.stringify({
     version: 1,
@@ -522,6 +506,22 @@ const stockOpenings: openingDescriptor[] = [
   }
 ];
 
+const newEmptyState = (): State => {
+  return {
+    openings: new Map<number, opening>(),
+    openingDescriptor: stockOpenings[0],
+    modalOpen: false
+  };
+};
+
+// TODO: Make a list of stock openings
+type openingDescriptor = {
+  title: string;
+  start: ActivateOpeningAt;
+  policy: IOpeningPolicyCommitment;
+  text: Text;
+}
+
 export class AdminController extends Controller<State, ITransport> {
   api: ApiPromise
   constructor (transport: ITransport, api: ApiPromise, initialState: State = newEmptyState()) {
@@ -862,16 +862,6 @@ const NewOpening = (props: NewOpeningProps) => {
     }
   ];
 
-  const onStakeModeCheckboxChange = (fn: (v: boolean) => void, fieldName: string, checked: boolean, stakeValue: number) => {
-    fn(checked);
-
-    if (checked) {
-      changeStakingMode(fieldName, StakingAmountLimitModeKeys.AtLeast, stakeValue);
-    } else {
-      onChangePolicyField(fieldName, null);
-    }
-  };
-
   const changeStakingMode = (fieldName: string, mode: string, stakeValue: number) => {
     const value = new Option<StakingPolic>(
       StakingPolicy,
@@ -883,6 +873,16 @@ const NewOpening = (props: NewOpeningProps) => {
     onChangePolicyField(fieldName, value);
   };
 
+  const onStakeModeCheckboxChange = (fn: (v: boolean) => void, fieldName: string, checked: boolean, stakeValue: number) => {
+    fn(checked);
+
+    if (checked) {
+      changeStakingMode(fieldName, StakingAmountLimitModeKeys.AtLeast, stakeValue);
+    } else {
+      onChangePolicyField(fieldName, null);
+    }
+  };
+
   const [text, setText] = useState(JSON.stringify(JSON.parse(props.desc.text.toString()), null, 2));
 
   const submit = () => {

+ 6 - 6
pioneer/packages/joy-roles/src/tabs/MyRoles.tsx

@@ -209,12 +209,6 @@ function ApplicationCancelledStatus (props: ApplicationStatusProps) {
 }
 type statusRenderer = (p: ApplicationStatusProps) => any
 
-const applicationStatusRenderers = new Map<OpeningState, statusRenderer>([
-  [OpeningState.AcceptingApplications, ApplicationStatusAcceptingApplications],
-  [OpeningState.InReview, ApplicationStatusInReview],
-  [OpeningState.Complete, ApplicationStatusComplete]
-]);
-
 function ApplicationStatusAcceptingApplications (props: ApplicationStatusProps): any {
   let positive = true;
   let message = (
@@ -283,6 +277,12 @@ function ApplicationStatusHired (props: ApplicationStatusProps) {
   );
 }
 
+const applicationStatusRenderers = new Map<OpeningState, statusRenderer>([
+  [OpeningState.AcceptingApplications, ApplicationStatusAcceptingApplications],
+  [OpeningState.InReview, ApplicationStatusInReview],
+  [OpeningState.Complete, ApplicationStatusComplete]
+]);
+
 export function ApplicationStatus (props: ApplicationStatusProps) {
   if (typeof props.hired !== 'undefined' && props.hired) {
     return ApplicationStatusHired(props);

+ 66 - 66
pioneer/packages/joy-roles/src/tabs/Opportunities.tsx

@@ -105,72 +105,6 @@ type MemberIdProps = {
   member_id?: MemberId;
 }
 
-type OpeningBodyCTAProps = OpeningStakeAndApplicationStatus & OpeningStage & OpeningBodyProps & MemberIdProps
-
-function OpeningBodyCTAView (props: OpeningBodyCTAProps) {
-  if (props.stage.state != OpeningState.AcceptingApplications || applicationImpossible(props.applications)) {
-    return null;
-  }
-
-  let message = (
-    <Message positive>
-      <Icon name="check circle" /> No stake required
-    </Message>
-  );
-
-  if (hasAnyStake(props)) {
-    const balance = !props.defactoMinimumStake.isZero() ? props.defactoMinimumStake : props.requiredApplicationStake.hard.add(props.requiredRoleStake.hard);
-    const plural = (props.requiredApplicationStake.anyRequirement() && props.requiredRoleStake.anyRequirement()) ? 's totalling' : ' of';
-    message = (
-      <Message warning icon>
-        <Icon name="warning sign" />
-        <Message.Content>
-          Stake{plural} at least <strong>{formatBalance(balance)}</strong> required!
-        </Message.Content>
-      </Message>
-    );
-  }
-
-  let applyButton = (
-    <Link to={'/working-groups/opportunities/' + props.meta.group + '/' + props.meta.id + '/apply'}>
-      <Button icon fluid positive size="huge">
-        APPLY NOW
-        <Icon name="angle right" />
-      </Button>
-    </Link>
-  );
-
-  const accountCtx = useMyAccount();
-  if (!accountCtx.state.address) {
-    applyButton = (
-      <Message error icon>
-        <Icon name="info circle" />
-        <Message.Content>
-          You will need an account to apply for this role. You can generate one in the <Link to="/accounts">Accounts</Link> section.
-        </Message.Content>
-      </Message>
-    );
-    message = <p></p>;
-  } else if (!props.member_id) {
-    applyButton = (
-      <Message error icon>
-        <Icon name="info circle" />
-        <Message.Content>
-          You will need a membership to apply for this role. You can sign up in the <Link to="/members">Membership</Link> section.
-        </Message.Content>
-      </Message>
-    );
-    message = <p></p>;
-  }
-
-  return (
-    <Container>
-      {applyButton}
-      {message}
-    </Container>
-  );
-}
-
 export type StakeRequirementProps = DefactoMinimumStake & {
   requiredApplicationStake: ApplicationStakeRequirement;
   requiredRoleStake: RoleStakeRequirement;
@@ -278,6 +212,72 @@ export function ApplicationCount (props: ApplicationCountProps) {
   );
 }
 
+type OpeningBodyCTAProps = OpeningStakeAndApplicationStatus & OpeningStage & OpeningBodyProps & MemberIdProps
+
+function OpeningBodyCTAView (props: OpeningBodyCTAProps) {
+  if (props.stage.state != OpeningState.AcceptingApplications || applicationImpossible(props.applications)) {
+    return null;
+  }
+
+  let message = (
+    <Message positive>
+      <Icon name="check circle" /> No stake required
+    </Message>
+  );
+
+  if (hasAnyStake(props)) {
+    const balance = !props.defactoMinimumStake.isZero() ? props.defactoMinimumStake : props.requiredApplicationStake.hard.add(props.requiredRoleStake.hard);
+    const plural = (props.requiredApplicationStake.anyRequirement() && props.requiredRoleStake.anyRequirement()) ? 's totalling' : ' of';
+    message = (
+      <Message warning icon>
+        <Icon name="warning sign" />
+        <Message.Content>
+          Stake{plural} at least <strong>{formatBalance(balance)}</strong> required!
+        </Message.Content>
+      </Message>
+    );
+  }
+
+  let applyButton = (
+    <Link to={'/working-groups/opportunities/' + props.meta.group + '/' + props.meta.id + '/apply'}>
+      <Button icon fluid positive size="huge">
+        APPLY NOW
+        <Icon name="angle right" />
+      </Button>
+    </Link>
+  );
+
+  const accountCtx = useMyAccount();
+  if (!accountCtx.state.address) {
+    applyButton = (
+      <Message error icon>
+        <Icon name="info circle" />
+        <Message.Content>
+          You will need an account to apply for this role. You can generate one in the <Link to="/accounts">Accounts</Link> section.
+        </Message.Content>
+      </Message>
+    );
+    message = <p></p>;
+  } else if (!props.member_id) {
+    applyButton = (
+      <Message error icon>
+        <Icon name="info circle" />
+        <Message.Content>
+          You will need a membership to apply for this role. You can sign up in the <Link to="/members">Membership</Link> section.
+        </Message.Content>
+      </Message>
+    );
+    message = <p></p>;
+  }
+
+  return (
+    <Container>
+      {applyButton}
+      {message}
+    </Container>
+  );
+}
+
 export function OpeningBodyApplicationsStatus (props: OpeningStakeAndApplicationStatus) {
   const impossible = applicationImpossible(props);
   const msg = new messageState();

+ 3 - 3
pioneer/packages/joy-utils/src/index.ts

@@ -55,14 +55,14 @@ export function bnToStr (bn?: BN, dflt = ''): string {
 // String, Numbers, Object
 // --------------------------------------
 
+export const notDefined = (x: any): boolean =>
+  x === null || typeof x === 'undefined';
+
 export const isDefined = (x: any): boolean =>
   !notDefined(x);
 
 export const isDef = isDefined;
 
-export const notDefined = (x: any): boolean =>
-  x === null || typeof x === 'undefined';
-
 export const notDef = notDefined;
 
 export const isObj = (x: any): boolean =>

+ 8 - 6
pioneer/packages/joy-utils/src/memoize.ts

@@ -1,10 +1,5 @@
-export function memoize () {
-  return (target: Record<string, any>, propertyKey: string, descriptor: TypedPropertyDescriptor<any>) => {
-    descriptor.value = getNewFunction(descriptor.value);
-  };
-}
-
 let counter = 0;
+
 function getNewFunction (originalMethod: () => void) {
   const identifier = ++counter;
 
@@ -49,3 +44,10 @@ function getNewFunction (originalMethod: () => void) {
     return returnedValue;
   };
 }
+
+
+export function memoize () {
+  return (target: Record<string, any>, propertyKey: string, descriptor: TypedPropertyDescriptor<any>) => {
+    descriptor.value = getNewFunction(descriptor.value);
+  };
+}