Browse Source

working-groups:createOpening rework

Leszek Wiesner 4 years ago
parent
commit
0166976559

+ 4 - 2
cli/package.json

@@ -53,7 +53,8 @@
     "mocha": "^5.2.0",
     "nyc": "^14.1.1",
     "ts-node": "^8.8.2",
-    "typescript": "^3.8.3"
+    "typescript": "^3.8.3",
+    "json-schema-to-typescript": "^9.1.1"
   },
   "engines": {
     "node": ">=12.18.0",
@@ -120,7 +121,8 @@
     "version": "oclif-dev readme && git add README.md",
     "lint": "eslint ./src --ext .ts",
     "checks": "tsc --noEmit --pretty && prettier ./ --check && yarn lint",
-    "format": "prettier ./ --write"
+    "format": "prettier ./ --write",
+    "generate:schema-typings": "rm -rf ./src/json-schemas/typings && json2ts -i ./src/json-schemas/ -o ./src/json-schemas/typings/"
   },
   "types": "lib/index.d.ts"
 }

+ 6 - 4
cli/src/base/ApiCommandBase.ts

@@ -18,7 +18,7 @@ import { DistinctQuestion } from 'inquirer'
 import { BOOL_PROMPT_OPTIONS } from '../helpers/prompting'
 import { DispatchError } from '@polkadot/types/interfaces/system'
 
-class ExtrinsicFailedError extends Error {}
+export class ExtrinsicFailedError extends Error {}
 
 /**
  * Abstract base class for commands that require access to the API.
@@ -454,13 +454,15 @@ export default abstract class ApiCommandBase extends StateAwareCommandBase {
     account: KeyringPair,
     tx: SubmittableExtrinsic<'promise'>,
     warnOnly = false // If specified - only warning will be displayed in case of failure (instead of error beeing thrown)
-  ): Promise<void> {
+  ): Promise<boolean> {
     try {
       await this.sendExtrinsic(account, tx)
       this.log(chalk.green(`Extrinsic successful!`))
+      return true
     } catch (e) {
       if (e instanceof ExtrinsicFailedError && warnOnly) {
         this.warn(`Extrinsic failed! ${e.message}`)
+        return false
       } else if (e instanceof ExtrinsicFailedError) {
         throw new CLIError(`Extrinsic failed! ${e.message}`, { exit: ExitCodes.ApiError })
       } else {
@@ -475,10 +477,10 @@ export default abstract class ApiCommandBase extends StateAwareCommandBase {
     method: string,
     params: CodecArg[],
     warnOnly = false
-  ): Promise<void> {
+  ): Promise<boolean> {
     this.log(chalk.white(`\nSending ${module}.${method} extrinsic...`))
     const tx = await this.getOriginalApi().tx[module][method](...params)
-    await this.sendAndFollowTx(account, tx, warnOnly)
+    return await this.sendAndFollowTx(account, tx, warnOnly)
   }
 
   async buildAndSendExtrinsic(

+ 2 - 2
cli/src/commands/media/createChannel.ts

@@ -27,12 +27,12 @@ export default class CreateChannelCommand extends ContentDirectoryCommandBase {
     if (!inputJson) {
       const customPrompts: JsonSchemaCustomPrompts = [
         ['language', () => this.promptForEntityId('Choose channel language', 'Language', 'name')],
-        ['isCensored', async () => undefined],
+        ['isCensored', 'skip'],
       ]
 
       const prompter = new JsonSchemaPrompter<ChannelEntity>(channelJsonSchema, undefined, customPrompts)
 
-      inputJson = await prompter.promptAll()
+      inputJson = await prompter.promptAll(true)
     }
 
     this.jsonPrettyPrint(JSON.stringify(inputJson))

+ 2 - 2
cli/src/commands/media/updateChannel.ts

@@ -75,12 +75,12 @@ export default class UpdateChannelCommand extends ContentDirectoryCommandBase {
 
       if (!asCurator) {
         // Skip isCensored is it's not updated by the curator
-        customPrompts.push(['isCensored', async () => undefined])
+        customPrompts.push(['isCensored', 'skip'])
       }
 
       const prompter = new JsonSchemaPrompter<ChannelEntity>(channelJsonSchema, currentValues, customPrompts)
 
-      inputJson = await prompter.promptAll()
+      inputJson = await prompter.promptAll(true)
     }
 
     this.jsonPrettyPrint(JSON.stringify(inputJson))

+ 197 - 56
cli/src/commands/working-groups/createOpening.ts

@@ -1,87 +1,228 @@
 import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase'
-import { ApiMethodArg, ApiMethodNamedArgs } from '../../Types'
+import { GroupMember } from '../../Types'
 import chalk from 'chalk'
-import { flags } from '@oclif/command'
 import { apiModuleByGroup } from '../../Api'
-import WorkerOpeningOptions from '../../promptOptions/addWorkerOpening'
-import { setDefaults } from '../../helpers/promptOptions'
+import HRTSchema from '@joystream/types/hiring/schemas/role.schema.json'
+import { GenericJoyStreamRoleSchema as HRTJson } from '@joystream/types/hiring/schemas/role.schema.typings'
+import { JsonSchemaPrompter } from '../../helpers/JsonSchemaPrompt'
+import { JSONSchema } from '@apidevtools/json-schema-ref-parser'
+import WGOpeningSchema from '../../json-schemas/WorkingGroupOpening.schema.json'
+import { WorkingGroupOpening as WGOpeningJson } from '../../json-schemas/typings/WorkingGroupOpening.schema'
+import _ from 'lodash'
+import { IOFlags, getInputJson, ensureOutputFileIsWriteable, saveOutputJsonToFile } from '../../helpers/InputOutput'
+import Ajv from 'ajv'
+import ExitCodes from '../../ExitCodes'
+import { flags } from '@oclif/command'
+import { createType } from '@joystream/types'
 
 export default class WorkingGroupsCreateOpening extends WorkingGroupsCommandBase {
   static description = 'Create working group opening (requires lead access)'
   static flags = {
     ...WorkingGroupsCommandBase.flags,
-    useDraft: flags.boolean({
-      char: 'd',
-      description:
-        'Whether to create the opening from existing draft.\n' +
-        'If provided without --draftName - the list of choices will be displayed.',
-    }),
-    draftName: flags.string({
-      char: 'n',
-      description: 'Name of the draft to create the opening from.',
-      dependsOn: ['useDraft'],
+    input: IOFlags.input,
+    output: flags.string({
+      char: 'o',
+      required: false,
+      description: 'Path to the file where the output JSON should be saved (this output can be then reused as input)',
     }),
-    createDraftOnly: flags.boolean({
-      char: 'c',
+    edit: flags.boolean({
+      char: 'e',
+      required: false,
       description:
-        'If provided - the extrinsic will not be executed. Use this flag if you only want to create a draft.',
+        'If provided along with --input - launches in edit mode allowing to modify the input before sending the exstinsic',
+      dependsOn: ['input'],
     }),
-    skipPrompts: flags.boolean({
-      char: 's',
-      description: 'Whether to skip all prompts when adding from draft (will use all default values)',
-      dependsOn: ['useDraft'],
-      exclusive: ['createDraftOnly'],
+    dryRun: flags.boolean({
+      required: false,
+      description:
+        'If provided along with --output - skips sending the actual extrinsic' +
+        '(can be used to generate a "draft" which can be provided as input later)',
+      dependsOn: ['output'],
     }),
   }
 
+  getHRTDefaults(memberHandle: string): HRTJson {
+    const groupName = _.startCase(this.group)
+    return {
+      version: 1,
+      headline: `Looking for ${groupName}!`,
+      job: {
+        title: groupName,
+        description: `Become part of the ${groupName} Group! This is a great opportunity to support Joystream!`,
+      },
+      application: {
+        sections: [
+          {
+            title: 'About you',
+            questions: [
+              {
+                title: 'Your name',
+                type: 'text',
+              },
+              {
+                title: 'What makes you a good fit for the job?',
+                type: 'text area',
+              },
+            ],
+          },
+        ],
+      },
+      reward: '10k JOY per 3600 blocks',
+      creator: {
+        membership: {
+          handle: memberHandle,
+        },
+      },
+    }
+  }
+
+  createTxParams(wgOpeningJson: WGOpeningJson, hrtJson: HRTJson) {
+    return [
+      wgOpeningJson.activateAt,
+      createType('WorkingGroupOpeningPolicyCommitment', {
+        max_review_period_length: wgOpeningJson.maxReviewPeriodLength,
+        application_rationing_policy: wgOpeningJson.maxActiveApplicants
+          ? { max_active_applicants: wgOpeningJson.maxActiveApplicants }
+          : null,
+        application_staking_policy: wgOpeningJson.applicationStake
+          ? {
+              amount: wgOpeningJson.applicationStake.value,
+              amount_mode: wgOpeningJson.applicationStake.mode,
+            }
+          : null,
+        role_staking_policy: wgOpeningJson.roleStake
+          ? {
+              amount: wgOpeningJson.roleStake.value,
+              amount_mode: wgOpeningJson.roleStake.mode,
+            }
+          : null,
+        terminate_role_stake_unstaking_period: wgOpeningJson.terminateRoleUnstakingPeriod,
+        exit_role_stake_unstaking_period: wgOpeningJson.leaveRoleUnstakingPeriod,
+      }),
+      JSON.stringify(hrtJson),
+      createType('OpeningType', 'Worker'),
+    ]
+  }
+
+  async promptForData(
+    lead: GroupMember,
+    rememberedInput?: [WGOpeningJson, HRTJson]
+  ): Promise<[WGOpeningJson, HRTJson]> {
+    const openingDefaults = rememberedInput?.[0]
+    const openingPrompt = new JsonSchemaPrompter<WGOpeningJson>(
+      (WGOpeningSchema as unknown) as JSONSchema,
+      openingDefaults
+    )
+    const wgOpeningJson = await openingPrompt.promptAll()
+
+    const hrtDefaults = rememberedInput?.[1] || this.getHRTDefaults(lead.profile.handle.toString())
+    this.log(`Values for ${chalk.greenBright('human_readable_text')} json:`)
+    const hrtPropmpt = new JsonSchemaPrompter<HRTJson>((HRTSchema as unknown) as JSONSchema, hrtDefaults)
+    // Prompt only for 'headline', 'job', 'application', 'reward' and 'process', leave the rest default
+    const headline = await hrtPropmpt.promptSingleProp('headline')
+    this.log('General information about the job:')
+    const job = await hrtPropmpt.promptSingleProp('job')
+    this.log('Application form sections and questions:')
+    const application = await hrtPropmpt.promptSingleProp('application')
+    this.log('Reward displayed in the opening box:')
+    const reward = await hrtPropmpt.promptSingleProp('reward')
+    this.log('Hiring process details (additional information)')
+    const process = await hrtPropmpt.promptSingleProp('process')
+
+    const hrtJson = { ...hrtDefaults, job, headline, application, reward, process }
+
+    return [wgOpeningJson, hrtJson]
+  }
+
+  async getInputFromFile(filePath: string): Promise<[WGOpeningJson, HRTJson]> {
+    const ajv = new Ajv({ allErrors: true })
+    const inputParams = await getInputJson<[WGOpeningJson, HRTJson]>(filePath)
+    if (!Array.isArray(inputParams) || inputParams.length !== 2) {
+      this.error('Invalid input file', { exit: ExitCodes.InvalidInput })
+    }
+    const [openingJson, hrtJson] = inputParams
+    if (!ajv.validate(WGOpeningSchema, openingJson)) {
+      this.error(`Invalid input file:\n${ajv.errorsText(undefined, { dataVar: 'openingJson', separator: '\n' })}`, {
+        exit: ExitCodes.InvalidInput,
+      })
+    }
+    if (!ajv.validate(HRTSchema, hrtJson)) {
+      this.error(`Invalid input file:\n${ajv.errorsText(undefined, { dataVar: 'hrtJson', separator: '\n' })}`, {
+        exit: ExitCodes.InvalidInput,
+      })
+    }
+
+    return [openingJson, hrtJson]
+  }
+
   async run() {
     const account = await this.getRequiredSelectedAccount()
     // lead-only gate
-    await this.getRequiredLead()
+    const lead = await this.getRequiredLead()
+    await this.requestAccountDecoding(account) // Prompt for password
 
-    const { flags } = this.parse(WorkingGroupsCreateOpening)
+    const {
+      flags: { input, output, edit, dryRun },
+    } = this.parse(WorkingGroupsCreateOpening)
 
-    const promptOptions = new WorkerOpeningOptions()
-    let defaultValues: ApiMethodNamedArgs | undefined
-    if (flags.useDraft) {
-      const draftName = flags.draftName || (await this.promptForOpeningDraft())
-      defaultValues = await this.loadOpeningDraftParams(draftName)
-      setDefaults(promptOptions, defaultValues)
-    }
+    ensureOutputFileIsWriteable(output)
 
-    if (!flags.skipPrompts) {
-      const module = apiModuleByGroup[this.group]
-      const method = 'addOpening'
+    let tryAgain = false
+    let rememberedInput: [WGOpeningJson, HRTJson] | undefined
+    do {
+      if (edit) {
+        rememberedInput = await this.getInputFromFile(input as string)
+      }
+      // Either prompt for the data or get it from input file
+      const [openingJson, hrtJson] =
+        !input || edit || tryAgain
+          ? await this.promptForData(lead, rememberedInput)
+          : await this.getInputFromFile(input)
 
-      let saveDraft = false
-      let params: ApiMethodArg[]
-      if (flags.createDraftOnly) {
-        params = await this.promptForExtrinsicParams(module, method, promptOptions)
-        saveDraft = true
-      } else {
-        await this.requestAccountDecoding(account) // Prompt for password
-        params = await this.buildAndSendExtrinsic(account, module, method, promptOptions, true)
+      // Remember the provided/fetched data in a variable
+      rememberedInput = [openingJson, hrtJson]
 
-        saveDraft = await this.simplePrompt({
-          message: 'Do you wish to save this opening as draft?',
-          type: 'confirm',
-        })
+      // Generate and ask to confirm tx params
+      const txParams = this.createTxParams(openingJson, hrtJson)
+      this.jsonPrettyPrint(JSON.stringify(txParams))
+      const confirmed = await this.simplePrompt({
+        type: 'confirm',
+        message: 'Do you confirm these extrinsic parameters?',
+      })
+      if (!confirmed) {
+        tryAgain = await this.simplePrompt({ type: 'confirm', message: 'Try again with remembered input?' })
+        continue
       }
 
-      if (saveDraft) {
-        const draftName = await this.promptForNewOpeningDraftName()
-        this.saveOpeningDraft(draftName, params)
+      // Save output to file
+      if (output) {
+        try {
+          saveOutputJsonToFile(output, rememberedInput)
+          this.log(chalk.green(`Output succesfully saved in: ${chalk.white(output)}!`))
+        } catch (e) {
+          this.warn(`Could not save output to ${output}!`)
+        }
+      }
 
-        this.log(chalk.green(`Opening draft ${chalk.white(draftName)} succesfully saved!`))
+      if (dryRun) {
+        this.exit(ExitCodes.OK)
       }
-    } else {
-      await this.requestAccountDecoding(account) // Prompt for password
+
+      // Send the tx
       this.log(chalk.white('Sending the extrinsic...'))
-      await this.sendExtrinsic(
+      const txSuccess = await this.sendAndFollowTx(
         account,
-        this.getOriginalApi().tx[apiModuleByGroup[this.group]].addOpening(...defaultValues!.map((v) => v.value))
+        this.getOriginalApi().tx[apiModuleByGroup[this.group]].addOpening(...txParams),
+        true // warnOnly
       )
-      this.log(chalk.green('Opening succesfully created!'))
-    }
+
+      // Display a success message on success or ask to try again on error
+      if (txSuccess) {
+        this.log(chalk.green('Opening succesfully created!'))
+        tryAgain = false
+      } else {
+        tryAgain = await this.simplePrompt({ type: 'confirm', message: 'Try again with remembered input?' })
+      }
+    } while (tryAgain)
   }
 }

+ 40 - 5
cli/src/helpers/InputOutput.ts

@@ -61,14 +61,49 @@ export function saveOutputJson(outputPath: string | undefined, fileName: string,
       fileName = fileName.replace(/(_[0-9]+)?\.json/, `_${++postfix}.json`)
       outputFilePath = path.join(outputPath, fileName)
     }
+    saveOutputJsonToFile(outputFilePath, data)
+
+    console.log(`${chalk.green('Output succesfully saved to:')} ${chalk.white(outputFilePath)}`)
+  }
+}
+
+// Output as file:
+
+export function saveOutputJsonToFile(outputFilePath: string, data: any): void {
+  try {
+    fs.writeFileSync(outputFilePath, JSON.stringify(data, null, 4))
+  } catch (e) {
+    throw new CLIError(`Could not save the output to: ${outputFilePath}. Check permissions...`, {
+      exit: ExitCodes.FsOperationFailed,
+    })
+  }
+}
+
+export function ensureOutputFileIsWriteable(outputFilePath: string | undefined): void {
+  if (outputFilePath === undefined) {
+    return
+  }
+
+  if (path.extname(outputFilePath) !== '.json') {
+    throw new CLIError(`Output path ${outputFilePath} is not a JSON file!`, { exit: ExitCodes.InvalidInput })
+  }
+
+  if (fs.existsSync(outputFilePath)) {
+    // File already exists - warn the user and check it it's writeable
+    console.warn(`WARNING: ${outputFilePath} already exists and it will get overriden!`)
     try {
-      fs.writeFileSync(outputFilePath, JSON.stringify(data, null, 4))
+      fs.accessSync(`${outputFilePath}`, fs.constants.W_OK)
     } catch (e) {
-      throw new CLIError(`Could not save the output to: ${outputFilePath}. Check directory permissions`, {
-        exit: ExitCodes.FsOperationFailed,
+      throw new CLIError(`Output path ${outputFilePath} is not writeable!`, { exit: ExitCodes.InvalidInput })
+    }
+  } else {
+    // File does not exist yet - check if the directory is writeable
+    try {
+      fs.accessSync(`${path.dirname(outputFilePath)}`, fs.constants.W_OK)
+    } catch (e) {
+      throw new CLIError(`Output directory ${path.dirname(outputFilePath)} is not writeable!`, {
+        exit: ExitCodes.InvalidInput,
       })
     }
-
-    console.log(`${chalk.green('Output succesfully saved to:')} ${chalk.white(outputFilePath)}`)
   }
 }

+ 63 - 14
cli/src/helpers/JsonSchemaPrompt.ts

@@ -8,7 +8,7 @@ import { getSchemasLocation } from 'cd-schemas'
 import path from 'path'
 
 type CustomPromptMethod = () => Promise<any>
-type CustomPrompt = DistinctQuestion | CustomPromptMethod | { $item: CustomPrompt }
+type CustomPrompt = DistinctQuestion | CustomPromptMethod | { $item: CustomPrompt } | 'skip'
 
 // For the explaination of "string & { x: never }", see: https://github.com/microsoft/TypeScript/issues/29729
 // eslint-disable-next-line @typescript-eslint/ban-types
@@ -39,20 +39,29 @@ export class JsonSchemaPrompter<JsonResult> {
     this.filledObject = defaults || {}
   }
 
-  private oneOfToChoices(oneOf: JSONSchema[]) {
+  private oneOfToOptions(oneOf: JSONSchema[], currentValue: any) {
+    let defaultValue: any
     const choices: { name: string; value: number | string }[] = []
 
     oneOf.forEach((pSchema, index) => {
       if (pSchema.description) {
-        choices.push({ name: pSchema.description, value: index })
+        choices.push({ name: pSchema.description, value: index.toString() })
       } else if (pSchema.type === 'object' && pSchema.properties) {
-        choices.push({ name: `{ ${Object.keys(pSchema.properties).join(', ')} }`, value: index })
+        choices.push({ name: `{ ${Object.keys(pSchema.properties).join(', ')} }`, value: index.toString() })
+        // Supports defaults for enum variants:
+        if (
+          typeof currentValue === 'object' &&
+          currentValue !== null &&
+          Object.keys(currentValue).join(',') === Object.keys(pSchema.properties).join(',')
+        ) {
+          defaultValue = index.toString()
+        }
       } else {
-        choices.push({ name: index.toString(), value: index })
+        choices.push({ name: index.toString(), value: index.toString() })
       }
     })
 
-    return choices
+    return { choices, default: defaultValue }
   }
 
   private getCustomPrompt(propertyPath: string): CustomPrompt | undefined {
@@ -67,9 +76,25 @@ export class JsonSchemaPrompter<JsonResult> {
     return chalk.green(propertyPath)
   }
 
-  private async prompt(schema: JSONSchema, propertyPath = '', custom?: CustomPrompt): Promise<any> {
+  private async prompt(
+    schema: JSONSchema,
+    propertyPath = '',
+    custom?: CustomPrompt,
+    allPropsRequired = false
+  ): Promise<any> {
     const customPrompt: CustomPrompt | undefined = custom || this.getCustomPrompt(propertyPath)
     const propDisplayName = this.propertyDisplayName(propertyPath)
+    const currentValue = _.get(this.filledObject, propertyPath)
+
+    if (customPrompt === 'skip') {
+      return
+    }
+
+    // Automatically handle "null" values (useful for enum variants)
+    if (schema.type === 'null') {
+      _.set(this.filledObject, propertyPath, null)
+      return null
+    }
 
     // Custom prompt
     if (typeof customPrompt === 'function') {
@@ -79,16 +104,40 @@ export class JsonSchemaPrompter<JsonResult> {
     // oneOf
     if (schema.oneOf) {
       const oneOf = schema.oneOf as JSONSchema[]
-      const choices = this.oneOfToChoices(oneOf)
-      const { choosen } = await inquirer.prompt({ name: 'choosen', message: propDisplayName, type: 'list', choices })
-      return await this.prompt(oneOf[choosen], propertyPath)
+      const options = this.oneOfToOptions(oneOf, currentValue)
+      const { choosen } = await inquirer.prompt({ name: 'choosen', message: propDisplayName, type: 'list', ...options })
+      return await this.prompt(oneOf[parseInt(choosen)], propertyPath)
     }
 
     // object
     if (schema.type === 'object' && schema.properties) {
       const value: Record<string, any> = {}
       for (const [pName, pSchema] of Object.entries(schema.properties)) {
-        value[pName] = await this.prompt(pSchema, propertyPath ? `${propertyPath}.${pName}` : pName)
+        const objectPropertyPath = propertyPath ? `${propertyPath}.${pName}` : pName
+        const propertyCustomPrompt = this.getCustomPrompt(objectPropertyPath)
+
+        if (propertyCustomPrompt === 'skip') {
+          continue
+        }
+
+        let confirmed = true
+        const required = allPropsRequired || (Array.isArray(schema.required) && schema.required.includes(pName))
+
+        if (!required) {
+          confirmed = (
+            await inquirer.prompt([
+              {
+                message: `Do you want to provide optional ${chalk.greenBright(objectPropertyPath)}?`,
+                type: 'confirm',
+                name: 'confirmed',
+                default: _.get(this.filledObject, objectPropertyPath) !== undefined,
+              },
+            ])
+          ).confirmed
+        }
+        if (confirmed) {
+          value[pName] = await this.prompt(pSchema, objectPropertyPath)
+        }
       }
       return value
     }
@@ -99,7 +148,6 @@ export class JsonSchemaPrompter<JsonResult> {
     }
 
     // "primitive" values:
-    const currentValue = _.get(this.filledObject, propertyPath)
     const basicPromptOptions: DistinctQuestion = {
       message: propDisplayName,
       default: currentValue !== undefined ? currentValue : schema.default,
@@ -156,6 +204,7 @@ export class JsonSchemaPrompter<JsonResult> {
           ...BOOL_PROMPT_OPTIONS,
           name: 'next',
           message: `Do you want to add another item to ${this.propertyDisplayName(propertyPath)} array?`,
+          default: _.get(this.filledObject, `${propertyPath}[${currItem}]`) !== undefined,
         },
       ])
       if (!next) {
@@ -210,8 +259,8 @@ export class JsonSchemaPrompter<JsonResult> {
     return await RefParser.dereference(this.schemaPath, this.schema, {})
   }
 
-  async promptAll() {
-    await this.prompt(await this.getMainSchema())
+  async promptAll(allPropsRequired = false) {
+    await this.prompt(await this.getMainSchema(), '', undefined, allPropsRequired)
     return this.filledObject as JsonResult
   }
 

+ 73 - 0
cli/src/json-schemas/WorkingGroupOpening.schema.json

@@ -0,0 +1,73 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema",
+  "$id": "https://joystream.org/WorkingGroupOpening.schema.json",
+  "title": "WorkingGroupOpening",
+  "description": "JSON schema to describe Joystream working group opening",
+  "type": "object",
+  "additionalProperties": false,
+  "required": ["activateAt", "maxReviewPeriodLength"],
+  "properties": {
+    "activateAt": {
+      "oneOf": [
+        {
+          "type": "object",
+          "additionalProperties": false,
+          "required": ["ExactBlock"],
+          "properties": {
+            "ExactBlock": {
+              "type": "integer",
+              "minimum": 1,
+              "description": "Exact block number"
+            }
+          }
+        },
+        {
+          "type": "object",
+          "additionalProperties": false,
+          "required": ["CurrentBlock"],
+          "properties": { "CurrentBlock": { "type": "null" } }
+        }
+      ]
+    },
+    "maxActiveApplicants": {
+      "type": "integer",
+      "description": "Max. number of active applicants",
+      "minimum": 1,
+      "default": 10
+    },
+    "maxReviewPeriodLength": {
+      "type": "integer",
+      "description": "Max. review period length in blocks",
+      "minimum": 1,
+      "default": 432000
+    },
+    "applicationStake": { "$ref": "#/definitions/StakingPolicy", "description": "Application stake properties" },
+    "roleStake": { "$ref": "#/definitions/StakingPolicy", "description": "Role stake properties" },
+    "terminateRoleUnstakingPeriod": { "$ref": "#/definitions/UnstakingPeriod" },
+    "leaveRoleUnstakingPeriod": { "$ref": "#/definitions/UnstakingPeriod" }
+  },
+  "definitions": {
+    "UnstakingPeriod": {
+      "type": "integer",
+      "minimum": 1,
+      "default": 100800
+    },
+    "StakingPolicy": {
+      "type": "object",
+      "additionalProperties": false,
+      "required": ["value", "mode"],
+      "properties": {
+        "mode": {
+          "type": "string",
+          "description": "Application stake mode (Exact/AtLeast)",
+          "enum": ["Exact", "AtLeast"]
+        },
+        "value": {
+          "type": "integer",
+          "description": "Required stake value in JOY",
+          "minimum": 1
+        }
+      }
+    }
+  }
+}

+ 60 - 0
cli/src/json-schemas/typings/WorkingGroupOpening.schema.d.ts

@@ -0,0 +1,60 @@
+/* tslint:disable */
+/**
+ * This file was automatically generated by json-schema-to-typescript.
+ * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,
+ * and run json-schema-to-typescript to regenerate this file.
+ */
+
+export type UnstakingPeriod = number
+
+/**
+ * JSON schema to describe Joystream working group opening
+ */
+export interface WorkingGroupOpening {
+  activateAt:
+    | {
+        /**
+         * Exact block number
+         */
+        ExactBlock: number
+      }
+    | {
+        CurrentBlock: null
+      }
+  /**
+   * Max. number of active applicants
+   */
+  maxActiveApplicants?: number
+  /**
+   * Max. review period length in blocks
+   */
+  maxReviewPeriodLength: number
+  /**
+   * Application stake properties
+   */
+  applicationStake?: {
+    /**
+     * Application stake mode (Exact/AtLeast)
+     */
+    mode: 'Exact' | 'AtLeast'
+    /**
+     * Required stake value in JOY
+     */
+    value: number
+  }
+  /**
+   * Role stake properties
+   */
+  roleStake?: {
+    /**
+     * Application stake mode (Exact/AtLeast)
+     */
+    mode: 'Exact' | 'AtLeast'
+    /**
+     * Required stake value in JOY
+     */
+    value: number
+  }
+  terminateRoleUnstakingPeriod?: UnstakingPeriod
+  leaveRoleUnstakingPeriod?: UnstakingPeriod
+}

+ 0 - 59
cli/src/promptOptions/addWorkerOpening.ts

@@ -1,59 +0,0 @@
-import { ApiParamsOptions, ApiParamOptions, HRTStruct } from '../Types'
-import { OpeningType, WorkingGroupOpeningPolicyCommitment } from '@joystream/types/working-group'
-import { SlashingTerms } from '@joystream/types/common'
-import { Bytes } from '@polkadot/types'
-import { schemaValidator } from '@joystream/types/hiring'
-import { createType } from '@joystream/types'
-
-class OpeningPolicyCommitmentOptions implements ApiParamsOptions {
-  [paramName: string]: ApiParamOptions
-  public role_slashing_terms: ApiParamOptions<SlashingTerms> = {
-    value: {
-      default: createType('SlashingTerms', { Unslashable: null }),
-      locked: true,
-    },
-  }
-
-  // Rename fields containing "curator" (solivg minor UI issue related to flat namespace)
-  public terminate_curator_application_stake_unstaking_period: ApiParamOptions = {
-    forcedName: 'terminate_application_stake_unstaking_period',
-  }
-
-  public terminate_curator_role_stake_unstaking_period: ApiParamOptions = {
-    forcedName: 'terminate_role_stake_unstaking_period',
-  }
-
-  public exit_curator_role_application_stake_unstaking_period: ApiParamOptions = {
-    forcedName: 'exit_role_application_stake_unstaking_period',
-  }
-
-  public exit_curator_role_stake_unstaking_period: ApiParamOptions = {
-    forcedName: 'exit_role_stake_unstaking_period',
-  }
-}
-
-class AddWrokerOpeningOptions implements ApiParamsOptions {
-  [paramName: string]: ApiParamOptions
-  // Lock value for opening_type
-  public opening_type: ApiParamOptions<OpeningType> = {
-    value: {
-      default: createType('OpeningType', { Worker: null }),
-      locked: true,
-    },
-  }
-
-  // Json schema for human_readable_text
-  public human_readable_text: ApiParamOptions<Bytes> = {
-    jsonSchema: {
-      schemaValidator,
-      struct: HRTStruct,
-    },
-  }
-
-  // Lock value for role_slashing_terms
-  public commitment: ApiParamOptions<WorkingGroupOpeningPolicyCommitment> = {
-    nestedOptions: new OpeningPolicyCommitmentOptions(),
-  }
-}
-
-export default AddWrokerOpeningOptions

+ 2 - 0
content-directory-schemas/types/extrinsics/index.d.ts

@@ -0,0 +1,2 @@
+export { AddClassSchema } from './AddClassSchema'
+export { CreateClass } from './CreateClass'