Browse Source

Storage working group opportunities listing

Leszek Wiesner 4 years ago
parent
commit
3d7c529a34

+ 4 - 0
pioneer/packages/joy-roles/src/flows/apply.controller.tsx

@@ -17,6 +17,7 @@ import { keyPairDetails, FlowModal, ProgressSteps } from './apply';
 
 import { OpeningStakeAndApplicationStatus } from '../tabs/Opportunities';
 import { Min, Step, Sum } from '../balances';
+import { WorkingGroups } from '../working_groups';
 
 type State = {
   // Input data from state
@@ -196,6 +197,9 @@ export class ApplyController extends Controller<State, ITransport> {
 
 export const ApplyView = View<ApplyController, State>(
   (state, controller, params) => {
+    if (params.get('group') !== WorkingGroups.ContentCurators) {
+      return <h1>Applying not yet implemented for this group!</h1>;
+    }
     controller.findOpening(params.get('id'));
     return (
       <Container className="apply-flow">

+ 1 - 0
pioneer/packages/joy-roles/src/index.tsx

@@ -88,6 +88,7 @@ export const App: React.FC<Props> = (props: Props) => {
       <Switch>
         <Route path={`${basePath}/opportunities/:group/:id/apply`} render={(props) => renderViewComponent(ApplyView(applyCtrl), props)} />
         <Route path={`${basePath}/opportunities/:group/:id`} render={(props) => renderViewComponent(OpportunityView(oppCtrl), props)} />
+        <Route path={`${basePath}/opportunities/:group`} render={(props) => renderViewComponent(OpportunitiesView(oppsCtrl), props)} />
         <Route path={`${basePath}/opportunities`} render={() => renderViewComponent(OpportunitiesView(oppsCtrl))} />
         <Route path={`${basePath}/my-roles`} render={() => renderViewComponent(MyRolesView(myRolesCtrl))} />
         <Route path={`${basePath}/admin`} render={() => renderViewComponent(AdminView(adminCtrl))} />

+ 4 - 1
pioneer/packages/joy-roles/src/tabs/Opportunities.controller.tsx

@@ -11,6 +11,8 @@ import {
   OpeningsView
 } from './Opportunities';
 
+import { AvailableGroups, WorkingGroups } from '../working_groups';
+
 type State = {
   blockTime?: number;
   opportunities?: Array<WorkingGroupOpening>;
@@ -37,8 +39,9 @@ export class OpportunitiesController extends Controller<State, ITransport> {
 }
 
 export const OpportunitiesView = View<OpportunitiesController, State>(
-  (state) => (
+  (state, controller, params) => (
     <OpeningsView
+      group={AvailableGroups.includes(params.get('group') as any) ? params.get('group') as WorkingGroups : undefined}
       openings={state.opportunities}
       block_time_in_seconds={state.blockTime}
       member_id={state.memberId}

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

@@ -4,7 +4,7 @@ import NumberFormat from 'react-number-format';
 import marked from 'marked';
 import CopyToClipboard from 'react-copy-to-clipboard';
 
-import { Link } from 'react-router-dom';
+import { Link, useHistory } from 'react-router-dom';
 import {
   Button,
   Card,
@@ -14,7 +14,9 @@ import {
   Label,
   List,
   Message,
-  Statistic
+  Statistic,
+  Dropdown,
+  DropdownProps
 } from 'semantic-ui-react';
 
 import { formatBalance } from '@polkadot/util';
@@ -37,6 +39,9 @@ import {
 } from '../openingStateMarkup';
 
 import { Loadable } from '@polkadot/joy-utils/index';
+import styled from 'styled-components';
+import _ from 'lodash';
+import { WorkingGroups, AvailableGroups } from '../working_groups';
 
 type OpeningStage = OpeningMetadataProps & {
   stage: OpeningStageClassification;
@@ -458,6 +463,14 @@ export type WorkingGroupOpening = OpeningStage & DefactoMinimumStake & OpeningMe
   applications: OpeningStakeAndApplicationStatus;
 }
 
+const OpeningTitle = styled.h2`
+  display: flex;
+  align-items: flex-end;
+`;
+const OpeningLabel = styled(Label)`
+  margin-left: auto !important;
+`;
+
 type OpeningViewProps = WorkingGroupOpening & BlockTimeProps & MemberIdProps
 
 export const OpeningView = Loadable<OpeningViewProps>(
@@ -473,7 +486,10 @@ export const OpeningView = Loadable<OpeningViewProps>(
 
     return (
       <Container className={'opening ' + openingClass(props.stage.state)}>
-        <h2>{text.job.title}</h2>
+        <OpeningTitle>
+          {text.job.title}
+          <OpeningLabel>{ _.startCase(props.meta.group) }</OpeningLabel>
+        </OpeningTitle>
         <Card fluid className="container">
           <Card.Content className="header">
             <OpeningHeader stage={props.stage} meta={props.meta} />
@@ -497,17 +513,47 @@ export const OpeningView = Loadable<OpeningViewProps>(
   }
 );
 
+const FilterOpportunities = styled.div`
+  display: flex;
+  width: 100%;
+  margin-bottom: 1rem;
+`;
+const FilterOpportunitiesDropdown = styled(Dropdown)`
+  margin-left: auto !important;
+  width: 250px !important;
+`;
+
 export type OpeningsViewProps = MemberIdProps & {
   openings?: Array<WorkingGroupOpening>;
   block_time_in_seconds?: number;
+  group?: WorkingGroups;
 }
 
 export const OpeningsView = Loadable<OpeningsViewProps>(
   ['openings', 'block_time_in_seconds'],
   props => {
+    const history = useHistory();
+    const { group = '' } = props;
+    const onFilterChange: DropdownProps['onChange'] = (e, data) => (
+      data.value !== group && history.push(`/working-groups/opportunities/${data.value}`)
+    );
+
     return (
       <Container>
-        {props.openings && props.openings.map((opening, key) => (
+        <FilterOpportunities>
+          <FilterOpportunitiesDropdown
+            placeholder="All opportunities"
+            options={
+              [{ value: '', text: 'All opportunities' }]
+                .concat(AvailableGroups.map(g => ({ value: g, text: _.startCase(g) })))
+            }
+            defaultValue={''}
+            value={group}
+            onChange={onFilterChange}
+            selection
+          />
+        </FilterOpportunities>
+        {props.openings && props.openings.filter(o => !group || o.meta.group === group).map((opening, key) => (
           <OpeningView key={key} {...opening} block_time_in_seconds={props.block_time_in_seconds as number} member_id={props.member_id} />
         ))}
       </Container>

+ 1 - 1
pioneer/packages/joy-roles/src/tabs/WorkingGroup.tsx

@@ -33,7 +33,7 @@ export const ContentCurators = Loadable<WorkingGroupMembership>(
           <p>
             There are openings for new content curators. This is a great way to support Joystream!
           </p>
-          <Link to="/working-groups/opportunities">
+          <Link to="/working-groups/opportunities/curators">
             <Button icon labelPosition="right" color="green" positive>
               Find out more
               <Icon name={'right arrow' as SemanticICONS} />

+ 87 - 21
pioneer/packages/joy-roles/src/transport.substrate.ts

@@ -3,7 +3,7 @@ import { map, switchMap } from 'rxjs/operators';
 
 import ApiPromise from '@polkadot/api/promise';
 import { Balance } from '@polkadot/types/interfaces';
-import { GenericAccountId, Option, u32, u64, u128, Vec } from '@polkadot/types';
+import { GenericAccountId, Option, u32, u128, Vec } from '@polkadot/types';
 import { Moment } from '@polkadot/types/interfaces/runtime';
 import { QueueTxExtrinsicAdd } from '@polkadot/react-components/Status/types';
 import { SubmittableExtrinsic } from '@polkadot/api/promise/types';
@@ -24,7 +24,11 @@ import {
   Lead, LeadId
 } from '@joystream/types/content-working-group';
 
-import { Application, Opening, OpeningId } from '@joystream/types/hiring';
+import {
+  WorkerApplication, WorkerApplicationId, WorkerOpening, WorkerOpeningId
+} from '@joystream/types/bureaucracy';
+
+import { Application, Opening } from '@joystream/types/hiring';
 import { Stake, StakeId } from '@joystream/types/stake';
 import { Recipient, RewardRelationship, RewardRelationshipId } from '@joystream/types/recurring-rewards';
 import { ActorInRole, Profile, MemberId, Role as MemberRole, RoleKeys, ActorId } from '@joystream/types/members';
@@ -42,7 +46,7 @@ import {
   classifyOpeningStakes,
   isApplicationHired
 } from './classifiers';
-import { WorkingGroups } from './working_groups';
+import { WorkingGroups, AvailableGroups } from './working_groups';
 import { Sort, Sum, Zero } from './balances';
 
 type WorkingGroupPair<HiringModuleType, WorkingGroupType> = {
@@ -62,6 +66,36 @@ interface IRoleAccounter {
   reward_relationship: Option<RewardRelationshipId>;
 }
 
+type WGApiMethodType = 'nextOpeningId' | 'openingById' | 'nextApplicationId' | 'applicationById';
+type WGApiMethodsMapping = { [key in WGApiMethodType]: string };
+type WGToApiMethodsMapping = { [key in WorkingGroups]: { module: string; methods: WGApiMethodsMapping } };
+
+type GroupApplication = CuratorApplication | WorkerApplication;
+type GroupApplicationId = CuratorApplicationId | WorkerApplicationId;
+type GroupOpening = CuratorOpening | WorkerOpening;
+type GroupOpeningId = CuratorOpeningId | WorkerOpeningId;
+
+const wgApiMethodsMapping: WGToApiMethodsMapping = {
+  [WorkingGroups.StorageProviders]: {
+    module: 'storageBureaucracy',
+    methods: {
+      nextOpeningId: 'nextWorkerOpeningId',
+      openingById: 'workerOpeningById',
+      nextApplicationId: 'nextWorkerApplicationId',
+      applicationById: 'workerApplicationById'
+    }
+  },
+  [WorkingGroups.ContentCurators]: {
+    module: 'contentWorkingGroup',
+    methods: {
+      nextOpeningId: 'nextCuratorOpeningId',
+      openingById: 'curatorOpeningById',
+      nextApplicationId: 'nextCuratorApplicationId',
+      applicationById: 'curatorApplicationById'
+    }
+  }
+};
+
 export class Transport extends TransportBase implements ITransport {
   protected api: ApiPromise
   protected cachedApi: APIQueryCache
@@ -74,6 +108,13 @@ export class Transport extends TransportBase implements ITransport {
     this.queueExtrinsic = queueExtrinsic;
   }
 
+  cachedApiMethodByGroup (group: WorkingGroups, method: WGApiMethodType) {
+    const apiModule = wgApiMethodsMapping[group].module;
+    const apiMethod = wgApiMethodsMapping[group].methods[method];
+
+    return this.cachedApi.query[apiModule][apiMethod];
+  }
+
   unsubscribe () {
     this.cachedApi.unsubscribe();
   }
@@ -263,22 +304,32 @@ export class Transport extends TransportBase implements ITransport {
     );
   }
 
-  async currentOpportunities (): Promise<Array<WorkingGroupOpening>> {
+  async opportunitiesByGroup (group: WorkingGroups): Promise<WorkingGroupOpening[]> {
     const output = new Array<WorkingGroupOpening>();
-    const nextId = await this.cachedApi.query.contentWorkingGroup.nextCuratorOpeningId() as CuratorOpeningId;
+    const nextId = (await this.cachedApiMethodByGroup(group, 'nextOpeningId')()) as GroupOpeningId;
 
     // This is chain specfic, but if next id is still 0, it means no curator openings have been added yet
     if (!nextId.eq(0)) {
       const highestId = nextId.toNumber() - 1;
 
       for (let i = highestId; i >= 0; i--) {
-        output.push(await this.curationGroupOpening(i));
+        output.push(await this.groupOpening(group, i));
       }
     }
 
     return output;
   }
 
+  async currentOpportunities (): Promise<WorkingGroupOpening[]> {
+    let opportunities: WorkingGroupOpening[] = [];
+
+    for (const group of AvailableGroups) {
+      opportunities = opportunities.concat(await this.opportunitiesByGroup(group));
+    }
+
+    return opportunities.sort((a, b) => b.stage.starting_block - a.stage.starting_block);
+  }
+
   protected async opening (id: number): Promise<Opening> {
     const opening = new SingleLinkedMapEntry<Opening>(
       Opening,
@@ -288,17 +339,21 @@ export class Transport extends TransportBase implements ITransport {
     return opening.value;
   }
 
-  protected async curatorOpeningApplications (curatorOpeningId: number): Promise<Array<WorkingGroupPair<Application, CuratorApplication>>> {
-    const output = new Array<WorkingGroupPair<Application, CuratorApplication>>();
+  protected async groupOpeningApplications (group: WorkingGroups, groupOpeningId: number): Promise<WorkingGroupPair<Application, GroupApplication>[]> {
+    const output = new Array<WorkingGroupPair<Application, GroupApplication>>();
 
-    const nextAppid = await this.cachedApi.query.contentWorkingGroup.nextCuratorApplicationId() as u64;
+    const nextAppid = (await this.cachedApiMethodByGroup(group, 'nextApplicationId')()) as GroupApplicationId;
     for (let i = 0; i < nextAppid.toNumber(); i++) {
-      const cApplication = new SingleLinkedMapEntry<CuratorApplication>(
-        CuratorApplication,
-        await this.cachedApi.query.contentWorkingGroup.curatorApplicationById(i)
+      const cApplication = new SingleLinkedMapEntry<GroupApplication>(
+        group === WorkingGroups.ContentCurators ? CuratorApplication : WorkerApplication,
+        await this.cachedApiMethodByGroup(group, 'applicationById')(i)
       );
 
-      if (cApplication.value.curator_opening_id.toNumber() !== curatorOpeningId) {
+      if (
+        group === WorkingGroups.ContentCurators
+          ? (cApplication.value as CuratorApplication).curator_opening_id.toNumber() !== groupOpeningId
+          : (cApplication.value as WorkerApplication).worker_opening_id.toNumber() !== groupOpeningId
+      ) {
         continue;
       }
 
@@ -319,29 +374,35 @@ export class Transport extends TransportBase implements ITransport {
     return output;
   }
 
-  async curationGroupOpening (id: number): Promise<WorkingGroupOpening> {
-    const nextId = (await this.cachedApi.query.contentWorkingGroup.nextCuratorOpeningId() as u32).toNumber();
+  protected async curatorOpeningApplications (curatorOpeningId: number): Promise<WorkingGroupPair<Application, CuratorApplication>[]> {
+    // Backwards compatibility
+    const applications = await this.groupOpeningApplications(WorkingGroups.ContentCurators, curatorOpeningId);
+    return applications as WorkingGroupPair<Application, CuratorApplication>[];
+  }
+
+  async groupOpening (group: WorkingGroups, id: number): Promise<WorkingGroupOpening> {
+    const nextId = (await this.cachedApiMethodByGroup(group, 'nextOpeningId')() as u32).toNumber();
     if (id < 0 || id >= nextId) {
       throw new Error('invalid id');
     }
 
-    const curatorOpening = new SingleLinkedMapEntry<CuratorOpening>(
-      CuratorOpening,
-      await this.cachedApi.query.contentWorkingGroup.curatorOpeningById(id)
+    const groupOpening = new SingleLinkedMapEntry<GroupOpening>(
+      group === WorkingGroups.ContentCurators ? CuratorOpening : WorkerOpening,
+      await this.cachedApiMethodByGroup(group, 'openingById')(id)
     );
 
     const opening = await this.opening(
-      curatorOpening.value.getField<OpeningId>('opening_id').toNumber()
+      groupOpening.value.opening_id.toNumber()
     );
 
-    const applications = await this.curatorOpeningApplications(id);
+    const applications = await this.groupOpeningApplications(group, id);
     const stakes = classifyOpeningStakes(opening);
 
     return ({
       opening: opening,
       meta: {
         id: id.toString(),
-        group: WorkingGroups.ContentCurators
+        group
       },
       stage: await classifyOpeningStage(this, opening),
       applications: {
@@ -355,6 +416,11 @@ export class Transport extends TransportBase implements ITransport {
     });
   }
 
+  async curationGroupOpening (id: number): Promise<WorkingGroupOpening> {
+    // Backwards compatibility
+    return this.groupOpening(WorkingGroups.ContentCurators, id);
+  }
+
   protected async openingApplicationTotalStake (application: Application): Promise<Balance> {
     const promises = new Array<Promise<Balance>>();
 

+ 6 - 0
pioneer/packages/joy-roles/src/working_groups.ts

@@ -1,3 +1,9 @@
 export enum WorkingGroups {
   ContentCurators = 'curators',
+  StorageProviders = 'storageProviders'
 }
+
+export const AvailableGroups: readonly WorkingGroups[] = [
+  WorkingGroups.ContentCurators,
+  WorkingGroups.StorageProviders
+] as const;