diff --git a/package.json b/package.json index ee24adaf31..d9905c6fc6 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,10 @@ "pioneer/packages/joy-utils", "pioneer/packages/joy-members", "pioneer/packages/joy-pages", + "pioneer/packages/joy-election", + "pioneer/packages/joy-proposals", + "pioneer/packages/joy-roles", + "pioneer/packages/joy-media", "utils/api-examples" ], "resolutions": { @@ -37,7 +41,8 @@ "@polkadot/util-crypto": "3.0.1", "@polkadot/wasm-crypto": "1.2.1", "babel-core": "^7.0.0-bridge.0", - "typescript": "^3.9.7" + "typescript": "^3.9.7", + "bn.js": "^5.1.2" }, "devDependencies": { "husky": "^4.2.5", diff --git a/pioneer/.eslintignore b/pioneer/.eslintignore index 42a2471efd..e975aac9b9 100644 --- a/pioneer/.eslintignore +++ b/pioneer/.eslintignore @@ -2,12 +2,8 @@ **/coverage/* **/node_modules/* packages/old-apps/* -packages/joy-election/* packages/joy-forum/* packages/joy-help/* -packages/joy-media/* -packages/joy-proposals/* -packages/joy-roles/* packages/joy-settings/* packages/joy-utils-old/* .eslintrc.js diff --git a/pioneer/.eslintrc.js b/pioneer/.eslintrc.js index a909f51bfc..4b3de541bd 100644 --- a/pioneer/.eslintrc.js +++ b/pioneer/.eslintrc.js @@ -27,6 +27,8 @@ module.exports = { 'react/jsx-max-props-per-line': 'off', 'sort-destructure-keys/sort-destructure-keys': 'off', '@typescript-eslint/unbound-method': 'warn', // Doesn't work well with our version of Formik, see: https://github.com/formium/formik/issues/2589 + 'react-hooks/exhaustive-deps': 'warn', // Causes more issues than it solves currently + 'no-void': 'off' // Otherwise we cannot mark unhandles promises }, // isolate pioneer from monorepo eslint rules root: true diff --git a/pioneer/.storybook/webpack.config.js b/pioneer/.storybook/webpack.config.js index 6c02952773..f32af8608d 100644 --- a/pioneer/.storybook/webpack.config.js +++ b/pioneer/.storybook/webpack.config.js @@ -37,7 +37,7 @@ config.module.rules.push({ use: [ { loader: require.resolve('babel-loader'), - options: require('@polkadot/dev-react/config/babel') + options: require('@polkadot/dev/config/babel') }, ], }); diff --git a/pioneer/package.json b/pioneer/package.json index b050147ce3..91a97d68d7 100644 --- a/pioneer/package.json +++ b/pioneer/package.json @@ -25,7 +25,6 @@ "test": "echo \"skipping tests\"", "vanitygen": "node packages/app-accounts/scripts/vanitygen.js", "start": "yarn clean && cd packages/apps && webpack --config webpack.config.js", - "generate-schemas": "json2ts -i packages/joy-types/src/schemas/role.schema.json -o packages/joy-types/src/schemas/role.schema.ts", "build-storybook": "build-storybook -c .storybook", "storybook": "start-storybook -s ./packages/apps/public -p 3001" }, @@ -40,7 +39,7 @@ "@types/chart.js": "^2.9.23", "@types/file-saver": "^2.0.1", "@types/i18next": "^13.0.0", - "@types/jest": "^26.0.7", + "@types/jest": "^26.0.10", "@types/react-beautiful-dnd": "^13.0.0", "@types/react-copy-to-clipboard": "^4.3.0", "@types/react-dom": "^16.9.8", @@ -74,11 +73,13 @@ "@storybook/addon-actions": "^5.2.5", "@storybook/addon-console": "^1.2.1", "@storybook/react": "^5.2.5", - "json-schema-to-typescript": "^7.1.0", "storybook-react-router": "^1.0.8", "typescript": "^3.9.7", "eslint-plugin-header": "^3.0.0", - "eslint-plugin-sort-destructure-keys": "^1.3.5" + "eslint-plugin-sort-destructure-keys": "^1.3.5", + "jest": "^26.4.1", + "ts-jest": "^26.2.0", + "tsconfig-paths-webpack-plugin": "^3.2.0" }, "dependencies": { "@types/lodash": "^4.14.138", diff --git a/pioneer/packages/apps-routing/src/index.ts b/pioneer/packages/apps-routing/src/index.ts index 47899c3931..28c7964d5b 100644 --- a/pioneer/packages/apps-routing/src/index.ts +++ b/pioneer/packages/apps-routing/src/index.ts @@ -21,11 +21,19 @@ import transfer from './transfer'; // Joy packages import members from './joy-members'; import { terms, privacyPolicy } from './joy-pages'; +import election from './joy-election'; +import proposals from './joy-proposals'; +import roles from './joy-roles'; +import media from './joy-media'; export default function create (t: (key: string, text: string, options: { ns: string }) => T): Routes { return appSettings.uiMode === 'light' ? [ + media(t), members(t), + roles(t), + election(t), + proposals(t), staking(t), null, transfer(t), @@ -36,7 +44,11 @@ export default function create (t: (key: string, text: string, opti privacyPolicy(t) ] : [ + media(t), members(t), + roles(t), + election(t), + proposals(t), staking(t), null, transfer(t), diff --git a/pioneer/packages/apps-routing/src/joy-election.ts b/pioneer/packages/apps-routing/src/joy-election.ts new file mode 100644 index 0000000000..cbff554d99 --- /dev/null +++ b/pioneer/packages/apps-routing/src/joy-election.ts @@ -0,0 +1,17 @@ +import { Route } from './types'; + +import Election from '@polkadot/joy-election/index'; +import SidebarSubtitle from '@polkadot/joy-election/SidebarSubtitle'; + +export default function create (t: (key: string, text: string, options: { ns: string }) => T): Route { + return { + Component: Election, + display: { + needsApi: ['query.council.activeCouncil', 'query.councilElection.stage'] + }, + text: t('nav.election', 'Council', { ns: 'apps-routing' }), + icon: 'university', + name: 'council', + SubtitleComponent: SidebarSubtitle + }; +} diff --git a/pioneer/packages/apps-routing/src/joy-media.ts b/pioneer/packages/apps-routing/src/joy-media.ts new file mode 100644 index 0000000000..befff152ad --- /dev/null +++ b/pioneer/packages/apps-routing/src/joy-media.ts @@ -0,0 +1,15 @@ +import Media from '@polkadot/joy-media/index'; + +import { Route } from './types'; + +export default function create (t: (key: string, text: string, options: { ns: string }) => T): Route { + return { + Component: Media, + display: { + needsApi: ['query.storageWorkingGroup.workerById', 'query.dataObjectStorageRegistry.relationshipsByContentId'] + }, + text: t('nav.media', 'Media', { ns: 'apps-routing' }), + icon: 'play-circle', + name: 'media' + }; +} diff --git a/pioneer/packages/apps-routing/src/joy-proposals.ts b/pioneer/packages/apps-routing/src/joy-proposals.ts new file mode 100644 index 0000000000..4c76e21eb8 --- /dev/null +++ b/pioneer/packages/apps-routing/src/joy-proposals.ts @@ -0,0 +1,16 @@ +import { Route } from './types'; + +import Proposals from '@polkadot/joy-proposals/index'; + +export default function create (t: (key: string, text: string, options: { ns: string }) => T): Route { + return { + Component: Proposals, + display: { + needsApi: ['query.proposalsEngine.proposalCount'] + }, + text: t('nav.proposals', 'Proposals', { ns: 'apps-routing' }), + icon: 'tasks', + name: 'proposals' + // TODO: useCounter with active proposals count? (could be a nice addition) + }; +} diff --git a/pioneer/packages/old-apps/apps-routing/src/joy-roles.ts b/pioneer/packages/apps-routing/src/joy-roles.ts similarity index 50% rename from pioneer/packages/old-apps/apps-routing/src/joy-roles.ts rename to pioneer/packages/apps-routing/src/joy-roles.ts index 7131a2fce8..8664da1e52 100644 --- a/pioneer/packages/old-apps/apps-routing/src/joy-roles.ts +++ b/pioneer/packages/apps-routing/src/joy-roles.ts @@ -1,9 +1,9 @@ -import { Routes } from './types'; +import { Route } from './types'; import Roles from '@polkadot/joy-roles/index'; -export default ([ - { +export default function create (t: (key: string, text: string, options: { ns: string }) => T): Route { + return { Component: Roles, display: { needsApi: [ @@ -11,10 +11,8 @@ export default ([ 'query.storageWorkingGroup.mint' ] }, - i18n: { - defaultValue: 'Working groups' - }, + text: t('nav.roles', 'Working groups', { ns: 'apps-routing' }), icon: 'users', name: 'working-groups' - } -] as Routes); + }; +} diff --git a/pioneer/packages/apps-routing/src/types.ts b/pioneer/packages/apps-routing/src/types.ts index 6dce432d8c..4a6695fef2 100644 --- a/pioneer/packages/apps-routing/src/types.ts +++ b/pioneer/packages/apps-routing/src/types.ts @@ -24,6 +24,8 @@ export interface Route { name: string; text: string; useCounter?: () => number | string | null; + // Joystream-specific + SubtitleComponent?: React.ComponentType; } export type Routes = (Route | null)[]; diff --git a/pioneer/packages/old-apps/apps/public/images/default-thumbnail.png b/pioneer/packages/apps/public/images/default-thumbnail.png similarity index 100% rename from pioneer/packages/old-apps/apps/public/images/default-thumbnail.png rename to pioneer/packages/apps/public/images/default-thumbnail.png diff --git a/pioneer/packages/apps/public/locales/en/index.json b/pioneer/packages/apps/public/locales/en/index.json index 962b58bcc3..eec63e3079 100644 --- a/pioneer/packages/apps/public/locales/en/index.json +++ b/pioneer/packages/apps/public/locales/en/index.json @@ -25,6 +25,7 @@ "joy-media.json", "joy-members.json", "joy-roles.json", + "joy-utils.json", "react-components.json", "react-params.json", "react-query.json", diff --git a/pioneer/packages/apps/public/locales/en/joy-utils.json b/pioneer/packages/apps/public/locales/en/joy-utils.json index 9e26dfeeb6..d094e35b91 100644 --- a/pioneer/packages/apps/public/locales/en/joy-utils.json +++ b/pioneer/packages/apps/public/locales/en/joy-utils.json @@ -1 +1,4 @@ -{} \ No newline at end of file +{ + "click to select or drag and drop the file here": "click to select or drag and drop the file here", + "{{name}} ({{size}} bytes)": "{{name}} ({{size}} bytes)" +} \ No newline at end of file diff --git a/pioneer/packages/apps/src/Content/index.tsx b/pioneer/packages/apps/src/Content/index.tsx index 63f0238535..a375b8b91c 100644 --- a/pioneer/packages/apps/src/Content/index.tsx +++ b/pioneer/packages/apps/src/Content/index.tsx @@ -16,6 +16,11 @@ import { useTranslation } from '../translate'; import NotFound from './NotFound'; import Status from './Status'; +// Joystream-specific +// We must use transport provider here instead of /apps/src/index +// to avoid "Cannot create Transport: The Substrate API is not ready yet." error +import { TransportProvider } from '@polkadot/joy-utils/react/context'; + interface Props { className?: string; } @@ -60,11 +65,25 @@ function Content ({ className }: Props): React.ReactElement { ? : ( - + { needsApi + // Add transport provider for routes that need the api + // (the above condition makes sure it's aleady initialized at this point) + ? ( + + + + ) + : ( + + ) } ) } @@ -78,7 +97,6 @@ function Content ({ className }: Props): React.ReactElement { } export default React.memo(styled(Content)` - background: rgba(250, 250, 250); padding: 0 1.5rem; position: relative; width: 100%; diff --git a/pioneer/packages/apps/src/SideBar/Item.tsx b/pioneer/packages/apps/src/SideBar/Item.tsx index abcd801144..629f9c9c19 100644 --- a/pioneer/packages/apps/src/SideBar/Item.tsx +++ b/pioneer/packages/apps/src/SideBar/Item.tsx @@ -79,12 +79,15 @@ function Item ({ isCollapsed, onClick, route }: Props): React.ReactElement - {text} + + {text} + { SubtitleComponent && } + {!!count && ( { render () { - const { index, accountId, stake } = this.props; + const { index, accountId, stake, isVotingStage } = this.props; const voteUrl = `/council/votes?applicantId=${accountId.toString()}`; return ( @@ -33,9 +34,11 @@ class Applicant extends React.PureComponent { {formatBalance(calcTotalStake(stake))} - - Vote - + { isVotingStage && ( + + Vote + + ) } ); } diff --git a/pioneer/packages/joy-election/src/Applicants.tsx b/pioneer/packages/joy-election/src/Applicants.tsx index e84c85f3d5..3ee84db4d7 100644 --- a/pioneer/packages/joy-election/src/Applicants.tsx +++ b/pioneer/packages/joy-election/src/Applicants.tsx @@ -1,49 +1,68 @@ import React from 'react'; -import { Table } from 'semantic-ui-react'; +import { Table, Message } from 'semantic-ui-react'; import BN from 'bn.js'; import { I18nProps } from '@polkadot/react-components/types'; import { ApiProps } from '@polkadot/react-api/types'; -import { withCalls } from '@polkadot/react-api/with'; +import { withCalls } from '@polkadot/react-api/hoc'; import { AccountId } from '@polkadot/types/interfaces'; +import { Option } from '@polkadot/types'; import { formatNumber } from '@polkadot/util'; import translate from './translate'; import Applicant from './Applicant'; import ApplyForm from './ApplyForm'; -import Section from '@polkadot/joy-utils/Section'; -import { queryToProp } from '@polkadot/joy-utils/index'; -import { withMyAccount, MyAccountProps } from '@polkadot/joy-utils/MyAccount'; +import Section from '@polkadot/joy-utils/react/components/Section'; +import { queryToProp } from '@polkadot/joy-utils/functions/misc'; +import { withMyAccount, MyAccountProps } from '@polkadot/joy-utils/react/hocs/accounts'; +import { ElectionStage } from '@joystream/types/src/council'; +import { RouteProps } from 'react-router-dom'; -type Props = ApiProps & I18nProps & MyAccountProps & { +type Props = RouteProps & ApiProps & I18nProps & MyAccountProps & { candidacyLimit?: BN; applicants?: Array; + stage?: Option; }; class Applicants extends React.PureComponent { - private renderTable = (applicants: Array) => ( - - - - # - Applicant - Total stake - Actions - - - {applicants.map((accountId, index) => ( - - ))} -
- ) + private renderTable = (applicants: Array) => { + const isVotingStage = this.props.stage?.unwrapOr(undefined)?.isOfType('Voting') || false; + + return ( + + + + # + Applicant + Total stake + { isVotingStage && ( + Actions + ) } + + + {applicants.map((accountId, index) => ( + + ))} +
+ ); + } render () { - const { myAddress, applicants = [], candidacyLimit = new BN(0) } = this.props; + const { myAddress, applicants = [], candidacyLimit = new BN(0), stage } = this.props; const title = Applicants {applicants.length}/{formatNumber(candidacyLimit)}; return <>
- + { stage?.unwrapOr(undefined)?.isOfType('Announcing') + ? ( + + ) + : ( + + Applying to council is only possible during Announcing stage. + + ) + }
{!applicants.length @@ -59,6 +78,7 @@ class Applicants extends React.PureComponent { export default translate( withCalls( queryToProp('query.councilElection.candidacyLimit'), - queryToProp('query.councilElection.applicants') + queryToProp('query.councilElection.applicants'), + queryToProp('query.councilElection.stage') )(withMyAccount(Applicants)) ); diff --git a/pioneer/packages/joy-election/src/ApplyForm.tsx b/pioneer/packages/joy-election/src/ApplyForm.tsx index f058fc992c..518d95ce51 100644 --- a/pioneer/packages/joy-election/src/ApplyForm.tsx +++ b/pioneer/packages/joy-election/src/ApplyForm.tsx @@ -3,21 +3,21 @@ import React from 'react'; import { I18nProps } from '@polkadot/react-components/types'; import { ApiProps } from '@polkadot/react-api/types'; -import { withCalls, withMulti } from '@polkadot/react-api/with'; +import { withCalls, withMulti } from '@polkadot/react-api/hoc'; import { Labelled } from '@polkadot/react-components/index'; import { Balance } from '@polkadot/types/interfaces'; import translate from './translate'; -import TxButton from '@polkadot/joy-utils/TxButton'; -import InputStake from '@polkadot/joy-utils/InputStake'; +import TxButton from '@polkadot/joy-utils/react/components/TxButton'; +import InputStake from '@polkadot/joy-utils/react/components/InputStake'; import { ElectionStake } from '@joystream/types/council'; -import { calcTotalStake, ZERO } from '@polkadot/joy-utils/index'; -import { MyAddressProps, withOnlyMembers } from '@polkadot/joy-utils/MyAccount'; +import { calcTotalStake, ZERO } from '@polkadot/joy-utils/functions/misc'; +import { MyAddressProps } from '@polkadot/joy-utils/react/hocs/accounts'; +import { withOnlyMembers } from '@polkadot/joy-utils/react/hocs/guards'; type Props = ApiProps & I18nProps & MyAddressProps & { minStake?: Balance; alreadyStaked?: ElectionStake; - myBalance?: Balance; }; type State = { @@ -48,15 +48,16 @@ class ApplyForm extends React.PureComponent { isValid={isStakeValid} onChange={this.onChangeStake} /> - - - +
+ + + +
); } @@ -71,10 +72,9 @@ class ApplyForm extends React.PureComponent { private onChangeStake = (stake?: BN): void => { stake = stake || ZERO; - const { myBalance = ZERO } = this.props; - const isStakeLteBalance = stake.lte(myBalance); const isStakeGteMinStake = stake.add(this.alreadyStaked()).gte(this.minStake()); - const isStakeValid = !stake.isZero() && isStakeGteMinStake && isStakeLteBalance; + const isStakeValid = !stake.isZero() && isStakeGteMinStake; + this.setState({ stake, isStakeValid }); } } @@ -88,8 +88,6 @@ export default withMulti( ['query.councilElection.minCouncilStake', { propName: 'minStake' }], ['query.councilElection.applicantStakes', - { paramName: 'myAddress', propName: 'alreadyStaked' }], - ['query.balances.freeBalance', - { paramName: 'myAddress', propName: 'myBalance' }] + { paramName: 'myAddress', propName: 'alreadyStaked' }] ) ); diff --git a/pioneer/packages/joy-election/src/CandidatePreview.tsx b/pioneer/packages/joy-election/src/CandidatePreview.tsx index 9e6571efb6..bab07182e7 100644 --- a/pioneer/packages/joy-election/src/CandidatePreview.tsx +++ b/pioneer/packages/joy-election/src/CandidatePreview.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import AddressMini from '@polkadot/react-components/AddressMiniJoy'; -import MemberByAccount from '@polkadot/joy-utils/MemberByAccountPreview'; +import AddressMini from '@polkadot/react-components/AddressMini'; +import MemberByAccount from '@polkadot/joy-utils/react/components/MemberByAccountPreview'; import { AccountId } from '@polkadot/types/interfaces'; import styled from 'styled-components'; diff --git a/pioneer/packages/joy-election/src/Council.tsx b/pioneer/packages/joy-election/src/Council.tsx index b81c144883..f4e9e9597d 100644 --- a/pioneer/packages/joy-election/src/Council.tsx +++ b/pioneer/packages/joy-election/src/Council.tsx @@ -2,22 +2,22 @@ import React from 'react'; import { ApiProps } from '@polkadot/react-api/types'; import { I18nProps } from '@polkadot/react-components/types'; -import { withCalls } from '@polkadot/react-api/with'; +import { withCalls } from '@polkadot/react-api/hoc'; import { Table } from 'semantic-ui-react'; import { formatBalance } from '@polkadot/util'; import CouncilCandidate from './CandidatePreview'; -import { calcBackersStake } from '@polkadot/joy-utils/index'; +import { calcBackersStake } from '@polkadot/joy-utils/functions/misc'; import { Seat } from '@joystream/types/council'; import translate from './translate'; -import Section from '@polkadot/joy-utils/Section'; +import Section from '@polkadot/joy-utils/react/components/Section'; +import { RouteProps } from 'react-router-dom'; -type Props = ApiProps & -I18nProps & { +type Props = RouteProps & ApiProps & I18nProps & { council?: Seat[]; }; -type State = {}; +type State = Record; class Council extends React.PureComponent { state: State = {}; @@ -53,9 +53,10 @@ class Council extends React.PureComponent { render () { const { council = [] } = this.props; + // console.log({ council }); return ( -
+
{!council.length ? Council is not elected yet : this.renderTable(council)}
); diff --git a/pioneer/packages/joy-election/src/Dashboard.tsx b/pioneer/packages/joy-election/src/Dashboard.tsx index c7f64ef811..da6fffbd9c 100644 --- a/pioneer/packages/joy-election/src/Dashboard.tsx +++ b/pioneer/packages/joy-election/src/Dashboard.tsx @@ -3,18 +3,19 @@ import React from 'react'; import { ApiProps } from '@polkadot/react-api/types'; import { I18nProps } from '@polkadot/react-components/types'; -import { withCalls } from '@polkadot/react-api/with'; +import { withCalls } from '@polkadot/react-api/hoc'; import { Option } from '@polkadot/types'; import { BlockNumber, Balance } from '@polkadot/types/interfaces'; -import { Bubble } from '@polkadot/react-components/index'; +import { Label, Icon } from 'semantic-ui-react'; import { formatNumber, formatBalance } from '@polkadot/util'; -import Section from '@polkadot/joy-utils/Section'; -import { queryToProp } from '@polkadot/joy-utils/index'; +import Section from '@polkadot/joy-utils/react/components/Section'; +import { queryToProp } from '@polkadot/joy-utils/functions/misc'; import { ElectionStage, Seat } from '@joystream/types/council'; import translate from './translate'; +import { RouteProps } from 'react-router-dom'; -type Props = ApiProps & I18nProps & { +type Props = RouteProps & ApiProps & I18nProps & { bestNumber?: BN; activeCouncil?: Seat[]; @@ -34,7 +35,7 @@ type Props = ApiProps & I18nProps & { stage?: Option; }; -type State = {}; +type State = Record; class Dashboard extends React.PureComponent { state: State = {}; @@ -45,12 +46,17 @@ class Dashboard extends React.PureComponent { const title = `Council ${activeCouncil.length > 0 ? '' : '(not elected)'}`; return
- - {activeCouncil.length} - - - {formatNumber(p.termEndsAt)} - + + + +
; } @@ -59,13 +65,16 @@ class Dashboard extends React.PureComponent { let stageName: string | undefined; let stageEndsAt: BlockNumber | undefined; + if (stage && stage.isSome) { const stageValue = stage.value as ElectionStage; + stageEndsAt = stageValue.value as BlockNumber; // contained value stageName = stageValue.type; // name of Enum variant } let leftBlocks: BN | undefined; + if (stageEndsAt && bestNumber) { leftBlocks = stageEndsAt.sub(bestNumber); } @@ -76,20 +85,28 @@ class Dashboard extends React.PureComponent { const title = <>Election ({stateText}); return
- - {formatNumber(round)} - - {isRunning && <> - - {stageName} - - - {formatNumber(leftBlocks)} - - - {formatNumber(stageEndsAt)} - - } + + + {isRunning && <> + + + + } +
; } @@ -98,33 +115,46 @@ class Dashboard extends React.PureComponent { const isAutoStart = (p.autoStart || false).valueOf(); return
- - {isAutoStart ? 'Yes' : 'No'} - - - {formatNumber(p.newTermDuration)} - - - {formatNumber(p.candidacyLimit)} - - - {formatNumber(p.councilSize)} - - - {formatBalance(p.minCouncilStake)} - - - {formatBalance(p.minVotingStake)} - - - {formatNumber(p.announcingPeriod)} blocks - - - {formatNumber(p.votingPeriod)} blocks - - - {formatNumber(p.revealingPeriod)} blocks - + + + + + + + + + + + + +
; } diff --git a/pioneer/packages/joy-election/src/Reveals.tsx b/pioneer/packages/joy-election/src/Reveals.tsx index 6a20dad6f9..7b51d2cb0c 100644 --- a/pioneer/packages/joy-election/src/Reveals.tsx +++ b/pioneer/packages/joy-election/src/Reveals.tsx @@ -1,23 +1,23 @@ import React from 'react'; -import { AppProps, I18nProps } from '@polkadot/react-components/types'; +import { I18nProps } from '@polkadot/react-components/types'; import { ApiProps } from '@polkadot/react-api/types'; -import { withCalls, withMulti } from '@polkadot/react-api/with'; +import { withCalls, withMulti } from '@polkadot/react-api/hoc'; import { AccountId } from '@polkadot/types/interfaces'; import { Input, Labelled, InputAddress } from '@polkadot/react-components/index'; import translate from './translate'; -import { nonEmptyStr, queryToProp, getUrlParam } from '@polkadot/joy-utils/index'; +import { nonEmptyStr, queryToProp, getUrlParam } from '@polkadot/joy-utils/functions/misc'; import { accountIdsToOptions, hashVote } from './utils'; -import TxButton from '@polkadot/joy-utils/TxButton'; +import TxButton from '@polkadot/joy-utils/react/components/TxButton'; import { findVoteByHash } from './myVotesStore'; -import { withOnlyMembers } from '@polkadot/joy-utils/MyAccount'; +import { withOnlyMembers } from '@polkadot/joy-utils/react/hocs/guards'; +import { RouteProps } from 'react-router-dom'; // AppsProps is needed to get a location from the route. -type Props = AppProps & ApiProps & I18nProps & { +type Props = RouteProps & ApiProps & I18nProps & { applicantId?: string | null; applicants?: AccountId[]; - location: any; }; type State = { @@ -30,8 +30,9 @@ class RevealVoteForm extends React.PureComponent { constructor (props: Props) { super(props); let { applicantId, location } = this.props; - applicantId = applicantId || getUrlParam(location, 'applicantId'); - const hashedVote = getUrlParam(location, 'hashedVote'); + + applicantId = applicantId || (location && getUrlParam(location, 'applicantId')); + const hashedVote = location && getUrlParam(location, 'hashedVote'); this.state = { applicantId, @@ -45,6 +46,7 @@ class RevealVoteForm extends React.PureComponent { const applicantOpts = accountIdsToOptions(this.props.applicants || []); const myVote = hashedVote ? findVoteByHash(hashedVote) : undefined; + if (myVote) { // Try to substitue applicantId and salt from local sotre: if (!applicantId) applicantId = myVote.applicantId; @@ -81,15 +83,16 @@ class RevealVoteForm extends React.PureComponent { onChange={this.onChangeSalt} /> } - - - +
+ + + +
); } diff --git a/pioneer/packages/joy-election/src/SealedVote.tsx b/pioneer/packages/joy-election/src/SealedVote.tsx index 54650328c1..769a884e4a 100644 --- a/pioneer/packages/joy-election/src/SealedVote.tsx +++ b/pioneer/packages/joy-election/src/SealedVote.tsx @@ -1,38 +1,47 @@ import React from 'react'; import { Link } from 'react-router-dom'; -import { Table } from 'semantic-ui-react'; +import { Table, Message } from 'semantic-ui-react'; import { I18nProps } from '@polkadot/react-components/types'; import { ApiProps } from '@polkadot/react-api/types'; -import { withCalls } from '@polkadot/react-api/with'; +import { withCalls } from '@polkadot/react-api/hoc'; import { Hash } from '@polkadot/types/interfaces'; import { formatBalance } from '@polkadot/util'; import translate from './translate'; -import { calcTotalStake } from '@polkadot/joy-utils/index'; +import { calcTotalStake } from '@polkadot/joy-utils/functions/misc'; import { SealedVote } from '@joystream/types/council'; -import AddressMini from '@polkadot/react-components/AddressMiniJoy'; +import AddressMini from '@polkadot/react-components/AddressMini'; import CandidatePreview from './CandidatePreview'; import { findVoteByHash } from './myVotesStore'; type Props = ApiProps & I18nProps & { hash: Hash; sealedVote?: SealedVote; + isStageRevealing: boolean; + isMyVote: boolean; }; class Comp extends React.PureComponent { renderCandidateOrAction () { - const { hash, sealedVote } = this.props; + const { hash, sealedVote, isStageRevealing, isMyVote } = this.props; + if (!sealedVote) { return Unknown hashed vote: {hash.toHex()}; } if (sealedVote.vote.isSome) { const candidateId = sealedVote.vote.unwrap(); + return ; - } else { + } else if (isStageRevealing && isMyVote) { const revealUrl = `/council/reveals?hashedVote=${hash.toHex()}`; + return Reveal this vote; + } else if (isMyVote) { + return Wait until Revealing stage in order to reveal this vote.; + } else { + return This vote has not been revealed yet.; } } diff --git a/pioneer/packages/joy-election/src/SealedVotes.tsx b/pioneer/packages/joy-election/src/SealedVotes.tsx index 1f03b66ac0..5effd2b117 100644 --- a/pioneer/packages/joy-election/src/SealedVotes.tsx +++ b/pioneer/packages/joy-election/src/SealedVotes.tsx @@ -1,36 +1,43 @@ import React from 'react'; import { Link } from 'react-router-dom'; -import { Button } from 'semantic-ui-react'; +import { Button, Message } from 'semantic-ui-react'; import { I18nProps } from '@polkadot/react-components/types'; import { ApiProps } from '@polkadot/react-api/types'; -import { withCalls } from '@polkadot/react-api/with'; +import { withCalls } from '@polkadot/react-api/hoc'; import { Hash } from '@polkadot/types/interfaces'; import translate from './translate'; import SealedVote from './SealedVote'; -import { queryToProp } from '@polkadot/joy-utils/index'; -import { MyAddressProps } from '@polkadot/joy-utils/MyAccount'; +import { queryToProp } from '@polkadot/joy-utils/functions/misc'; +import { MyAddressProps } from '@polkadot/joy-utils/react/hocs/accounts'; import { SavedVote } from './myVotesStore'; -import Section from '@polkadot/joy-utils/Section'; +import Section from '@polkadot/joy-utils/react/components/Section'; type Props = ApiProps & I18nProps & MyAddressProps & { myVotes?: SavedVote[]; commitments?: Hash[]; + isStageRevealing: boolean; }; class Comp extends React.PureComponent { private filterVotes = (myVotesOnly: boolean): Hash[] => { const { myVotes = [], commitments = [] } = this.props; + const isMyVote = (hash: string): boolean => { - return myVotes.find(x => x.hash === hash) !== undefined; + return myVotes.find((x) => x.hash === hash) !== undefined; }; - return commitments.filter(x => myVotesOnly === isMyVote(x.toHex())); + + return commitments.filter((x) => myVotesOnly === isMyVote(x.toHex())); } - private renderVotes = (votes: Hash[]) => { + private renderVotes = (votes: Hash[], areVotesMine: boolean) => { return votes.map((hash, index) => - + ); } @@ -39,17 +46,19 @@ class Comp extends React.PureComponent { const otherVotes = this.filterVotes(false); return <> -
{ - !myVotes.length - ? No votes by the current account found on the current browser. - : this.renderVotes(myVotes) - }
+
+ { + !myVotes.length + ? No votes by the current account found on the current browser. + : this.renderVotes(myVotes, true) + } + { this.props.isStageRevealing && } +
- { !otherVotes.length ? No votes submitted by other accounts yet. - : this.renderVotes(otherVotes) + : this.renderVotes(otherVotes, false) }
; diff --git a/pioneer/packages/joy-election/src/SidebarSubtitle.tsx b/pioneer/packages/joy-election/src/SidebarSubtitle.tsx new file mode 100644 index 0000000000..82e63d39bf --- /dev/null +++ b/pioneer/packages/joy-election/src/SidebarSubtitle.tsx @@ -0,0 +1,35 @@ +/** Component providing election stage subtitle for SideBar menu **/ +import React from 'react'; +import { ElectionStage } from '@joystream/types/council'; +import { Option } from '@polkadot/types/codec'; +import { useApi, useCall } from '@polkadot/react-hooks'; +import styled from 'styled-components'; + +const colorByStage = { + Announcing: '#4caf50', + Voting: '#2196f3', + Revealing: '#ff5722' +} as const; + +type StyledSubtitleProps = { + stage?: keyof typeof colorByStage; +} +const StyledSubtitle = styled.div` + display: block; + font-size: 0.85rem; + color: ${(props: StyledSubtitleProps) => props.stage ? colorByStage[props.stage] : 'grey'}; +`; + +export default function SidebarSubtitle () { + const apiProps = useApi(); + const electionStage = useCall>(apiProps.isApiReady && apiProps.api.query.councilElection.stage, []); + + if (electionStage) { + const stageName = electionStage.unwrapOr(undefined)?.type; + const text = stageName ? `${stageName} stage` : 'No active election'; + + return {text}; + } + + return null; +} diff --git a/pioneer/packages/joy-election/src/VoteForm.tsx b/pioneer/packages/joy-election/src/VoteForm.tsx index 63fbb78b17..418c4bd6d0 100644 --- a/pioneer/packages/joy-election/src/VoteForm.tsx +++ b/pioneer/packages/joy-election/src/VoteForm.tsx @@ -4,9 +4,9 @@ import uuid from 'uuid/v4'; import React from 'react'; import { Message, Table } from 'semantic-ui-react'; -import { AppProps, I18nProps } from '@polkadot/react-components/types'; +import { I18nProps } from '@polkadot/react-components/types'; import { ApiProps } from '@polkadot/react-api/types'; -import { withCalls, withMulti } from '@polkadot/react-api/with'; +import { withCalls, withMulti } from '@polkadot/react-api/hoc'; import { AccountId, Balance } from '@polkadot/types/interfaces'; import { Button, Input, Labelled } from '@polkadot/react-components/index'; import { SubmittableResult } from '@polkadot/api'; @@ -14,14 +14,16 @@ import { formatBalance } from '@polkadot/util'; import translate from './translate'; import { hashVote } from './utils'; -import { queryToProp, ZERO, getUrlParam, nonEmptyStr } from '@polkadot/joy-utils/index'; -import TxButton from '@polkadot/joy-utils/TxButton'; -import InputStake from '@polkadot/joy-utils/InputStake'; +import { queryToProp, ZERO, getUrlParam, nonEmptyStr } from '@polkadot/joy-utils/functions/misc'; +import TxButton from '@polkadot/joy-utils/react/components/TxButton'; +import InputStake from '@polkadot/joy-utils/react/components/InputStake'; import CandidatePreview from './CandidatePreview'; -import { MyAccountProps, withOnlyMembers } from '@polkadot/joy-utils/MyAccount'; -import MembersDropdown from '@polkadot/joy-utils/MembersDropdown'; +import { MyAccountProps } from '@polkadot/joy-utils/react/hocs/accounts'; +import { withOnlyMembers } from '@polkadot/joy-utils/react/hocs/guards'; +import MembersDropdown from '@polkadot/joy-utils/react/components/MembersDropdown'; import { saveVote, NewVote } from './myVotesStore'; import { TxFailedCallback } from '@polkadot/react-components/Status/types'; +import { RouteProps } from 'react-router-dom'; // TODO use a crypto-prooven generator instead of UUID 4. function randomSalt () { @@ -29,11 +31,10 @@ function randomSalt () { } // AppsProps is needed to get a location from the route. -type Props = AppProps & ApiProps & I18nProps & MyAccountProps & { +type Props = RouteProps & ApiProps & I18nProps & MyAccountProps & { applicantId?: string | null; minVotingStake?: Balance; applicants?: AccountId[]; - location?: any; }; type State = { @@ -49,7 +50,8 @@ class Component extends React.PureComponent { super(props); let { applicantId, location } = this.props; - applicantId = applicantId || getUrlParam(location, 'applicantId'); + + applicantId = applicantId || (location && getUrlParam(location, 'applicantId')); this.state = { applicantId, @@ -103,14 +105,11 @@ class Component extends React.PureComponent { - - + ); @@ -106,6 +112,7 @@ class Upload extends React.PureComponent { private resetForm = () => { const { cancelSource } = this.state; + this.setState({ ...defaultState(), cancelSource @@ -114,16 +121,17 @@ class Upload extends React.PureComponent { private renderUploading () { const { file, newContentId, progress, error } = this.state; + if (!file || !file.name) return ; const success = !error && progress >= 100; - const { history, match: { params: { channelId } } } = this.props; + const { history, match: { params: { channelId } }, api } = this.props; return
{this.renderProgress()} {success && {
; } + private renderSendingTx () { + return ; + } + private renderDiscovering () { return Contacting storage provider.; } @@ -142,6 +154,7 @@ class Upload extends React.PureComponent { const success = !error && progress >= 100; let label = ''; + if (active) { label = 'Your file is uploading. Please keep this page open until it\'s done.'; } else if (success) { @@ -165,7 +178,7 @@ class Upload extends React.PureComponent { return
{ /> {file_name &&
{ + this.setState({ sendingTx: true }); + sendTx(); + }} + txSuccessCb={ this.onDataObjectCreated } + txFailedCb={() => { this.setState({ sendingTx: false }); }} />
}
; @@ -205,7 +222,7 @@ class Upload extends React.PureComponent { }); } else { this.setState({ file, computingHash: true }); - this.startComputingHash(); + void this.startComputingHash(); } } @@ -218,7 +235,8 @@ class Upload extends React.PureComponent { try { const iterableFile = new IterableFile(file, { chunkSize: 65535 }); - const ipfs_cid = await IpfsHash.of(iterableFile); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-call + const ipfs_cid = (await IpfsHash.of(iterableFile)) as string; this.hashComputationComplete(ipfs_cid); } catch (err) { @@ -244,27 +262,31 @@ class Upload extends React.PureComponent { private buildTxParams = () => { const { file, newContentId, ipfs_cid } = this.state; + if (!file || !ipfs_cid) return []; // TODO get corresponding data type id based on file content const dataObjectTypeId = new BN(1); const { myMemberId } = this.props; + return [myMemberId, newContentId, dataObjectTypeId, new BN(file.size), ipfs_cid]; } private onDataObjectCreated = async (_txResult: SubmittableResult) => { - this.setState({ discovering: true }); + this.setState({ sendingTx: false, discovering: true }); const { api } = this.props; const { newContentId } = this.state; let dataObject: Option; + try { dataObject = await api.query.dataDirectory.dataObjectByContentId(newContentId) as Option; } catch (err) { this.setState({ - error: err, + error: normalizeError(err), discovering: false }); + return; } @@ -276,10 +298,11 @@ class Upload extends React.PureComponent { if (dataObject.isSome) { const storageProvider = dataObject.unwrap().liaison; - this.uploadFileTo(storageProvider); + + void this.uploadFileTo(storageProvider); } else { this.setState({ - error: new Error('No Storage Provider assigned to process upload'), + error: 'No Storage Provider assigned to process upload', discovering: false }); } @@ -287,16 +310,18 @@ class Upload extends React.PureComponent { private uploadFileTo = async (storageProvider: StorageProviderId) => { const { file, newContentId, cancelSource } = this.state; + if (!file || !file.size) { this.setState({ - error: new Error('No file to upload!'), + error: 'No file to upload!', discovering: false }); + return; } const contentId = newContentId.encode(); - const config = { + const config: AxiosRequestConfig = { headers: { // TODO uncomment this once the issue fixed: // https://github.com/Joystream/storage-node-joystream/issues/16 @@ -304,8 +329,17 @@ class Upload extends React.PureComponent { 'Content-Type': '' // <-- this is a temporary hack }, cancelToken: cancelSource.token, - onUploadProgress: (progressEvent: any) => { + onUploadProgress: (progressEvent: unknown) => { + if ( + !isObjectWithProperties(progressEvent, 'loaded', 'total') || + typeof progressEvent.loaded !== 'number' || + typeof progressEvent.total !== 'number' + ) { + return; + } + const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total); + this.setState({ progress: percentCompleted }); @@ -314,11 +348,12 @@ class Upload extends React.PureComponent { const { discoveryProvider } = this.props; let url: string; + try { url = await discoveryProvider.resolveAssetEndpoint(storageProvider, contentId, cancelSource.token); } catch (err) { return this.setState({ - error: new Error(`Failed to contact storage provider: ${err.message}`), + error: `Failed to contact storage provider: ${normalizeError(err)}`, discovering: false }); } @@ -335,12 +370,20 @@ class Upload extends React.PureComponent { try { await axios.put<{ message: string }>(url, file, config); - } catch (err) { - this.setState({ progress: 0, error: err, uploading: false }); + } catch (e) { + const err = e as unknown; + + this.setState({ progress: 0, error: normalizeError(err), uploading: false }); + if (axios.isCancel(err)) { return; } - if (!err.response || (err.response.status >= 500 && err.response.status <= 504)) { + + const response = isObjectWithProperties(err, 'response') + ? (err as AxiosError).response + : undefined; + + if (!response || (response.status >= 500 && response.status <= 504)) { // network connection error discoveryProvider.reportUnreachable(storageProvider); } diff --git a/pioneer/packages/joy-media/src/channels/ChannelAvatar.tsx b/pioneer/packages/joy-media/src/channels/ChannelAvatar.tsx index f1416df962..48d3d59f35 100644 --- a/pioneer/packages/joy-media/src/channels/ChannelAvatar.tsx +++ b/pioneer/packages/joy-media/src/channels/ChannelAvatar.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { Link } from 'react-router-dom'; import { ChannelEntity } from '../entities/ChannelEntity'; import { BgImg } from '../common/BgImg'; -import { DEFAULT_THUMBNAIL_URL } from '@polkadot/joy-utils/images'; +import { DEFAULT_THUMBNAIL_URL } from '../common/images'; const defaultSizePx = 75; diff --git a/pioneer/packages/joy-media/src/channels/ChannelAvatarAndName.tsx b/pioneer/packages/joy-media/src/channels/ChannelAvatarAndName.tsx index ca553c6b1f..16f9f06aac 100644 --- a/pioneer/packages/joy-media/src/channels/ChannelAvatarAndName.tsx +++ b/pioneer/packages/joy-media/src/channels/ChannelAvatarAndName.tsx @@ -9,6 +9,7 @@ type Props = { export const ChannelAvatarAndName = (props: Props) => { const { channel } = props; + return (
diff --git a/pioneer/packages/joy-media/src/channels/ChannelHelpers.ts b/pioneer/packages/joy-media/src/channels/ChannelHelpers.ts index 9e3558e5bf..6ea47c8253 100644 --- a/pioneer/packages/joy-media/src/channels/ChannelHelpers.ts +++ b/pioneer/packages/joy-media/src/channels/ChannelHelpers.ts @@ -4,7 +4,7 @@ import { ChannelPublicationStatusAllValues } from '@joystream/types/content-work export const ChannelPublicationStatusDropdownOptions = ChannelPublicationStatusAllValues - .map(x => ({ key: x, value: x, text: x })); + .map((x) => ({ key: x, value: x, text: x })); export const isVideoChannel = (channel: ChannelType) => { return channel.content === 'Video'; diff --git a/pioneer/packages/joy-media/src/channels/ChannelNameAsLink.tsx b/pioneer/packages/joy-media/src/channels/ChannelNameAsLink.tsx index d794b57602..884516a00d 100644 --- a/pioneer/packages/joy-media/src/channels/ChannelNameAsLink.tsx +++ b/pioneer/packages/joy-media/src/channels/ChannelNameAsLink.tsx @@ -10,6 +10,7 @@ type Props = { export const ChannelNameAsLink = (props: Props) => { const { channel, className, style } = props; + return ( {channel.title || channel.handle} diff --git a/pioneer/packages/joy-media/src/channels/ChannelPreview.tsx b/pioneer/packages/joy-media/src/channels/ChannelPreview.tsx index 7c0e19372a..98e0369e37 100644 --- a/pioneer/packages/joy-media/src/channels/ChannelPreview.tsx +++ b/pioneer/packages/joy-media/src/channels/ChannelPreview.tsx @@ -6,8 +6,8 @@ import { ChannelEntity } from '../entities/ChannelEntity'; import { ChannelAvatar, ChannelAvatarSize } from './ChannelAvatar'; import { isPublicChannel, isMusicChannel, isVideoChannel, isAccountAChannelOwner, isVerifiedChannel } from './ChannelHelpers'; -import { useMyMembership } from '@polkadot/joy-utils/MyMembershipContext'; -import { nonEmptyStr } from '@polkadot/joy-utils/index'; +import { useMyMembership } from '@polkadot/joy-utils/react/hooks'; +import { nonEmptyStr } from '@polkadot/joy-utils/functions/misc'; import { CurationPanel } from './CurationPanel'; import { ChannelNameAsLink } from './ChannelNameAsLink'; @@ -52,8 +52,6 @@ export const ChannelPreview = (props: ChannelPreviewProps) => {

- - {isAccountAChannelOwner(channel, myAccountId) &&
@@ -101,6 +99,8 @@ export const ChannelPreview = (props: ChannelPreviewProps) => { }
+ + {withDescription && nonEmptyStr(channel.description) && } diff --git a/pioneer/packages/joy-media/src/channels/ChannelPreviewStats.tsx b/pioneer/packages/joy-media/src/channels/ChannelPreviewStats.tsx index cee1016479..12835cd3b8 100644 --- a/pioneer/packages/joy-media/src/channels/ChannelPreviewStats.tsx +++ b/pioneer/packages/joy-media/src/channels/ChannelPreviewStats.tsx @@ -13,6 +13,7 @@ export const ChannelPreviewStats = (props: Props) => { const statSize = 'tiny'; let itemsPublishedLabel = ''; + if (channel.content === 'Video') { itemsPublishedLabel = 'Videos'; } else if (channel.content === 'Music') { diff --git a/pioneer/packages/joy-media/src/channels/ChannelsByOwner.tsx b/pioneer/packages/joy-media/src/channels/ChannelsByOwner.tsx index 4e29d3420f..08d1d60a0d 100644 --- a/pioneer/packages/joy-media/src/channels/ChannelsByOwner.tsx +++ b/pioneer/packages/joy-media/src/channels/ChannelsByOwner.tsx @@ -19,7 +19,8 @@ const TabsAndChannels = (props: ChannelsByOwnerProps) => { let videoChannelsCount = 0; let musicChannelsCount = 0; - allChannels.forEach(x => { + + allChannels.forEach((x) => { if (x.content === 'Video') { videoChannelsCount++; } else if (x.content === 'Music') { @@ -38,6 +39,7 @@ const TabsAndChannels = (props: ChannelsByOwnerProps) => { const switchTab = (activeIndex: number) => { const activeContentType = contentTypeByTabIndex[activeIndex]; + if (activeContentType === undefined) { setChannels(allChannels); } else { diff --git a/pioneer/packages/joy-media/src/channels/ChannelsByOwner.view.tsx b/pioneer/packages/joy-media/src/channels/ChannelsByOwner.view.tsx index 9200d6d778..a60863e7ef 100644 --- a/pioneer/packages/joy-media/src/channels/ChannelsByOwner.view.tsx +++ b/pioneer/packages/joy-media/src/channels/ChannelsByOwner.view.tsx @@ -1,10 +1,10 @@ import React from 'react'; import { RouteComponentProps } from 'react-router'; -import { GenericAccountId } from '@polkadot/types'; import { MediaView } from '../MediaView'; import { ChannelsByOwnerProps, ChannelsByOwner } from './ChannelsByOwner'; -import { JoyError } from '@polkadot/joy-utils/JoyStatus'; +import { JoyError } from '@polkadot/joy-utils/react/components'; +import { useApi } from '@polkadot/react-hooks'; type Props = ChannelsByOwnerProps; @@ -13,16 +13,18 @@ export const ChannelsByOwnerView = MediaView({ resolveProps: async (props) => { const { transport, accountId } = props; const channels = await transport.channelsByAccount(accountId); + return { channels }; } }); -export const ChannelsByOwnerWithRouter = (props: Props & RouteComponentProps) => { +export const ChannelsByOwnerWithRouter = (props: Props & RouteComponentProps>) => { const { match: { params: { account } } } = props; + const { api } = useApi(); if (account) { try { - return ; + return ; } catch (err) { console.log('ChannelsByOwnerWithRouter failed:', err); } diff --git a/pioneer/packages/joy-media/src/channels/CurationPanel.tsx b/pioneer/packages/joy-media/src/channels/CurationPanel.tsx index ce2279a537..145341b799 100644 --- a/pioneer/packages/joy-media/src/channels/CurationPanel.tsx +++ b/pioneer/packages/joy-media/src/channels/CurationPanel.tsx @@ -1,16 +1,18 @@ import React from 'react'; import { ChannelEntity } from '../entities/ChannelEntity'; import { isVerifiedChannel, isCensoredChannel } from './ChannelHelpers'; -import { useMyMembership } from '@polkadot/joy-utils/MyMembershipContext'; -import TxButton from '@polkadot/joy-utils/TxButton'; -import { ChannelCurationStatus } from '@joystream/types/content-working-group'; +import { useMyMembership } from '@polkadot/joy-utils/react/hooks'; +import { SemanticTxButton } from '@polkadot/joy-utils/react/components/TxButton'; import { AccountId } from '@polkadot/types/interfaces'; +import { useApi } from '@polkadot/react-hooks'; +import { Icon } from 'semantic-ui-react'; type ChannelCurationPanelProps = { channel: ChannelEntity; }; export const CurationPanel = (props: ChannelCurationPanelProps) => { + const { api } = useApi(); const { curationActor, allAccounts } = useMyMembership(); const { channel } = props; @@ -34,17 +36,16 @@ export const CurationPanel = (props: ChannelCurationPanelProps) => { const isCensored = isCensoredChannel(channel); - const new_curation_status = new ChannelCurationStatus( + const new_curation_status = api.createType('ChannelCurationStatus', isCensored ? 'Normal' : 'Censored' ); - return { new_curation_status // toggled curation status ]} tx={'contentWorkingGroup.updateChannelAsCurationActor'} - />; + > + + { isCensored ? 'Un-Censor' : 'Censor' } + ; }; const renderToggleVerifiedButton = () => { @@ -62,13 +66,12 @@ export const CurationPanel = (props: ChannelCurationPanelProps) => { const accountAvailable = canUseAccount(role_account); const isVerified = isVerifiedChannel(channel); - return { null // not changing curation status ]} tx={'contentWorkingGroup.updateChannelAsCurationActor'} - />; + > + + { isVerified ? 'Remove Verification' : 'Verify' } + ; }; return <> -
+
{renderToggleCensorshipButton()} {renderToggleVerifiedButton()}
diff --git a/pioneer/packages/joy-media/src/channels/EditChannel.tsx b/pioneer/packages/joy-media/src/channels/EditChannel.tsx index b0f1b4f068..d7c8eed135 100644 --- a/pioneer/packages/joy-media/src/channels/EditChannel.tsx +++ b/pioneer/packages/joy-media/src/channels/EditChannel.tsx @@ -3,21 +3,21 @@ import { Button } from 'semantic-ui-react'; import { Form, withFormik } from 'formik'; import { History } from 'history'; -import { Text, Option } from '@polkadot/types'; -import TxButton from '@polkadot/joy-utils/TxButton'; -import { onImageError } from '@polkadot/joy-utils/images'; +import { Option } from '@polkadot/types'; +import { TxButton, JoyError, Section } from '@polkadot/joy-utils/react/components'; +import { onImageError } from '../common/images'; import { withMediaForm, MediaFormProps } from '../common/MediaForms'; import { ChannelType, ChannelClass as Fields, buildChannelValidationSchema, ChannelFormValues, ChannelToFormValues, ChannelGenericProp } from '../schemas/channel/Channel'; import { MediaDropdownOptions } from '../common/MediaDropdownOptions'; -import { ChannelId, ChannelContentType, ChannelPublicationStatus, OptionalText } from '@joystream/types/content-working-group'; -import { newOptionalText, findFirstParamOfSubstrateEvent } from '@polkadot/joy-utils/index'; -import { useMyMembership } from '@polkadot/joy-utils/MyMembershipContext'; +import { ChannelId, OptionalText } from '@joystream/types/content-working-group'; +import { findFirstParamOfSubstrateEvent } from '@polkadot/joy-utils/functions/misc'; +import { useMyMembership } from '@polkadot/joy-utils/react/hooks'; import { ChannelPublicationStatusDropdownOptions, isAccountAChannelOwner } from './ChannelHelpers'; import { TxCallback } from '@polkadot/react-components/Status/types'; import { SubmittableResult } from '@polkadot/api'; import { ChannelValidationConstraints } from '../transport'; -import { JoyError } from '@polkadot/joy-utils/JoyStatus'; -import Section from '@polkadot/joy-utils/Section'; + +import { useApi } from '@polkadot/react-hooks'; export type OuterProps = { history?: History; @@ -56,6 +56,7 @@ const InnerForm = (props: MediaFormProps) => { } = props; const { myAccountId, myMemberId } = useMyMembership(); + const { api } = useApi(); if (entity && !isAccountAChannelOwner(entity, myAccountId)) { return ; @@ -83,52 +84,49 @@ const InnerForm = (props: MediaFormProps) => { const buildTxParams = () => { if (!isValid) return []; - // TODO get value from the form: - const publicationStatus = new ChannelPublicationStatus('Public'); - if (!entity) { // Create a new channel const channelOwner = myMemberId; const roleAccount = myAccountId; - const contentType = new ChannelContentType(values.content); + const contentType = api.createType('ChannelContentType', values.content); return [ channelOwner, roleAccount, contentType, - new Text(values.handle), - newOptionalText(values.title), - newOptionalText(values.description), - newOptionalText(values.avatar), - newOptionalText(values.banner), - publicationStatus + values.handle, + values.title || null, + values.description || null, + values.avatar || null, + values.banner || null, + values.publicationStatus ]; } else { // Update an existing channel const updOptText = (field: ChannelGenericProp): Option => { - return new Option(OptionalText, + return api.createType('Option', isFieldChanged(field) - ? newOptionalText(values[field.id]) + ? api.createType('Option', values[field.id]) : null ); }; - const updHandle = new Option(Text, + const updHandle = api.createType('Option', isFieldChanged(Fields.handle) ? values[Fields.handle.id] : null ); - const updPublicationStatus = new Option(ChannelPublicationStatus, + const updPublicationStatus = api.createType('Option', isFieldChanged(Fields.publicationStatus) - ? new ChannelPublicationStatus(values[Fields.publicationStatus.id] as any) + ? api.createType('ChannelPublicationStatus', values[Fields.publicationStatus.id]) : null ); return [ - new ChannelId(entity.id), + entity.id, updHandle, updOptText(Fields.title), updOptText(Fields.description), @@ -156,7 +154,6 @@ const InnerForm = (props: MediaFormProps) => { const renderMainButton = () => ({ // Transform outer props into form values mapPropsToValues: (props): FormValues => { const { entity } = props; + return ChannelToFormValues(entity); }, validationSchema: (props: OuterProps): any => { const { constraints } = props; + if (!constraints) return null; return buildChannelValidationSchema(constraints); diff --git a/pioneer/packages/joy-media/src/channels/EditChannel.view.tsx b/pioneer/packages/joy-media/src/channels/EditChannel.view.tsx index b05582a965..3776a0155a 100644 --- a/pioneer/packages/joy-media/src/channels/EditChannel.view.tsx +++ b/pioneer/packages/joy-media/src/channels/EditChannel.view.tsx @@ -2,8 +2,8 @@ import React from 'react'; import { RouteComponentProps } from 'react-router'; import { MediaView } from '../MediaView'; import { OuterProps, EditForm } from './EditChannel'; -import { ChannelId } from '@joystream/types/content-working-group'; -import { JoyError } from '@polkadot/joy-utils/JoyStatus'; +import { JoyError } from '@polkadot/joy-utils/react/components'; +import { useApi } from '@polkadot/react-hooks'; type Props = OuterProps; @@ -15,18 +15,20 @@ export const EditChannelView = MediaView({ const { transport, id } = props; const entity = id && await transport.channelById(id); const constraints = await transport.channelValidationConstraints(); + return { entity, constraints }; } }); -type WithRouterProps = Props & RouteComponentProps +type WithRouterProps = Props & RouteComponentProps> export const EditChannelWithRouter = (props: WithRouterProps) => { const { match: { params: { id } } } = props; + const { api } = useApi(); if (id) { try { - return ; + return ; } catch (err) { console.log('EditChannelWithRouter failed:', err); } diff --git a/pioneer/packages/joy-media/src/channels/ViewChannel.tsx b/pioneer/packages/joy-media/src/channels/ViewChannel.tsx index f55af16abf..7f35b05331 100644 --- a/pioneer/packages/joy-media/src/channels/ViewChannel.tsx +++ b/pioneer/packages/joy-media/src/channels/ViewChannel.tsx @@ -8,7 +8,7 @@ import { ViewVideoChannel } from './ViewVideoChannel'; import { ViewMusicChannel } from './ViewMusicChannel'; import { toVideoPreviews } from '../video/VideoPreview'; import { isVideoChannel, isMusicChannel } from './ChannelHelpers'; -import { JoyError } from '@polkadot/joy-utils/JoyStatus'; +import { JoyError } from '@polkadot/joy-utils/react/components'; export type ViewChannelProps = { id: ChannelId; @@ -31,6 +31,7 @@ export function ViewChannel (props: ViewChannelProps) { if (isVideoChannel(channel)) { const previews = toVideoPreviews(videos); + return ; } else if (isMusicChannel(channel)) { return ; diff --git a/pioneer/packages/joy-media/src/channels/ViewChannel.view.tsx b/pioneer/packages/joy-media/src/channels/ViewChannel.view.tsx index d44432c73a..8a05504706 100644 --- a/pioneer/packages/joy-media/src/channels/ViewChannel.view.tsx +++ b/pioneer/packages/joy-media/src/channels/ViewChannel.view.tsx @@ -2,8 +2,8 @@ import React from 'react'; import { RouteComponentProps } from 'react-router'; import { MediaView } from '../MediaView'; import { ViewChannelProps, ViewChannel } from './ViewChannel'; -import { ChannelId } from '@joystream/types/content-working-group'; -import { JoyError } from '@polkadot/joy-utils/JoyStatus'; +import { JoyError } from '@polkadot/joy-utils/react/components'; +import { useApi } from '@polkadot/react-hooks'; type Props = ViewChannelProps; @@ -14,16 +14,18 @@ export const ViewChannelView = MediaView({ const { transport, id } = props; const channel = await transport.channelById(id); const videos = await transport.videosByChannelId(id); + return { channel, videos }; } }); -export const ViewChannelWithRouter = (props: Props & RouteComponentProps) => { +export const ViewChannelWithRouter = (props: Props & RouteComponentProps>) => { const { match: { params: { id } } } = props; + const { api } = useApi(); if (id) { try { - return ; + return ; } catch (err) { console.log('ViewChannelWithRouter failed:', err); } diff --git a/pioneer/packages/joy-media/src/channels/ViewMusicChannel.tsx b/pioneer/packages/joy-media/src/channels/ViewMusicChannel.tsx index f3526e7b49..3fd24d1c1c 100644 --- a/pioneer/packages/joy-media/src/channels/ViewMusicChannel.tsx +++ b/pioneer/packages/joy-media/src/channels/ViewMusicChannel.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { ChannelEntity } from '../entities/ChannelEntity'; -import Section from '@polkadot/joy-utils/Section'; +import { Section } from '@polkadot/joy-utils/react/components'; import { ChannelHeader } from './ChannelHeader'; import { MusicAlbumPreviewProps, MusicAlbumPreview } from '../music/MusicAlbumPreview'; import { MusicTrackReaderPreview, MusicTrackReaderPreviewProps } from '../music/MusicTrackReaderPreview'; @@ -27,7 +27,7 @@ export function ViewMusicChannel (props: Props) { !albums.length ? :
- {albums.map(x => )} + {albums.map((x) => )}
); @@ -35,7 +35,7 @@ export function ViewMusicChannel (props: Props) { !tracks.length ? :
- {tracks.map(x => )} + {tracks.map((x) => )}
); diff --git a/pioneer/packages/joy-media/src/channels/ViewVideoChannel.tsx b/pioneer/packages/joy-media/src/channels/ViewVideoChannel.tsx index 0d330f7810..ad089ed4c0 100644 --- a/pioneer/packages/joy-media/src/channels/ViewVideoChannel.tsx +++ b/pioneer/packages/joy-media/src/channels/ViewVideoChannel.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import Section from '@polkadot/joy-utils/Section'; +import { Section } from '@polkadot/joy-utils/react/components'; import { ChannelEntity } from '../entities/ChannelEntity'; import { ChannelHeader } from './ChannelHeader'; import { VideoPreview, VideoPreviewProps } from '../video/VideoPreview'; diff --git a/pioneer/packages/joy-media/src/common/BgImg.tsx b/pioneer/packages/joy-media/src/common/BgImg.tsx index 6913c7e2ec..9b73866f22 100644 --- a/pioneer/packages/joy-media/src/common/BgImg.tsx +++ b/pioneer/packages/joy-media/src/common/BgImg.tsx @@ -13,7 +13,7 @@ type Props = { export function BgImg (props: Props) { let { url, width, height, size, circle, className, style } = props; - const fullClass = 'JoyBgImg ' + className; + const fullClass = `JoyBgImg ${className || ''}`; let fullStyle: CSSProperties = { backgroundImage: `url(${url})` diff --git a/pioneer/packages/joy-media/src/common/FormTabs.tsx b/pioneer/packages/joy-media/src/common/FormTabs.tsx index 1d1b32c1df..01610afb20 100644 --- a/pioneer/packages/joy-media/src/common/FormTabs.tsx +++ b/pioneer/packages/joy-media/src/common/FormTabs.tsx @@ -20,7 +20,7 @@ export function FormTabs (props: FormTabsProps) { return { + panes={panes.map((tab) => { const { id, fields = [], @@ -29,8 +29,10 @@ export function FormTabs (props: FormTabsProps) { } = tab; const tabErrors: any[] = []; - fields.forEach(f => { + + fields.forEach((f) => { const err = errors[f.id]; + if (err) { tabErrors.push(err); } diff --git a/pioneer/packages/joy-media/src/common/MediaDropdownOptions.tsx b/pioneer/packages/joy-media/src/common/MediaDropdownOptions.tsx index 0fd9b77b0b..a9ec671ba0 100644 --- a/pioneer/packages/joy-media/src/common/MediaDropdownOptions.tsx +++ b/pioneer/packages/joy-media/src/common/MediaDropdownOptions.tsx @@ -5,10 +5,10 @@ import { TextValueEntity } from '@joystream/types/versioned-store/EntityCodec'; import { InternalEntities } from '../transport'; const buildOptions = (entities: TextValueEntity[]): DropdownItemProps[] => - entities.map(x => ({ key: x.id, value: x.id, text: x.value })); + entities.map((x) => ({ key: x.id, value: x.id, text: x.value })); const buildLanguageOptions = (entities: LanguageType[]): DropdownItemProps[] => - entities.map(x => ({ key: x.id, value: x.id, text: ISO6391.getName(x.value) })); + entities.map((x) => ({ key: x.id, value: x.id, text: ISO6391.getName(x.value) })); export class MediaDropdownOptions { public languageOptions: DropdownItemProps[] diff --git a/pioneer/packages/joy-media/src/common/MediaForms.tsx b/pioneer/packages/joy-media/src/common/MediaForms.tsx index 0607e27095..c61cd94c20 100644 --- a/pioneer/packages/joy-media/src/common/MediaForms.tsx +++ b/pioneer/packages/joy-media/src/common/MediaForms.tsx @@ -1,11 +1,11 @@ import React from 'react'; import { Dropdown, DropdownItemProps, DropdownProps } from 'semantic-ui-react'; import { FormikProps, Field } from 'formik'; -import * as JoyForms from '@polkadot/joy-utils/forms'; +import * as JoyForms from '@polkadot/joy-utils/react/components/forms'; import { SubmittableResult } from '@polkadot/api'; import { TxFailedCallback, TxCallback } from '@polkadot/react-components/Status/types'; import { MediaDropdownOptions } from './MediaDropdownOptions'; -import { OnTxButtonClick } from '@polkadot/joy-utils/TxButton'; +import { OnTxButtonClick } from '@polkadot/joy-utils/react/components/TxButton'; import isEqual from 'lodash/isEqual'; import { componentName } from '@polkadot/joy-utils/react/helpers'; @@ -40,7 +40,7 @@ type MediaTextProps = type MediaFieldProps = BaseFieldProps & JoyForms.LabelledProps & { - fieldProps: any; + fieldProps: Record; } type MediaDropdownProps = @@ -78,6 +78,7 @@ export function withMediaForm function MediaText (props: MediaTextProps) { const { field: f } = props; + return !f ? null : ; } @@ -105,8 +106,8 @@ export function withMediaForm const MediaDropdown = (props: MediaDropdownProps) => { const { field: f, options = [] } = props; - const id = f.id as string; - const value = (props.values as any)[id] || ''; + const id = f.id; + const value = props.values[id] || ''; return options, value, onBlur: (_event: any, _data: DropdownProps) => { - props.setFieldTouched(id, true); + props.setFieldTouched(id as string, true); }, onChange: (_event: any, data: DropdownProps) => { - props.setFieldValue(id, data.value); + props.setFieldValue(id as string, data.value); } }} />; }; @@ -138,6 +139,7 @@ export function withMediaForm const isFieldChanged = (field: FieldName | FieldObject): boolean => { const fieldName = typeof field === 'string' ? field : (field as FieldObject).id; + return ( dirty && touched[fieldName] === true && @@ -159,6 +161,7 @@ export function withMediaForm const onTxFailed: TxFailedCallback = (txResult: SubmittableResult | null) => { setSubmitting(false); + if (txResult === null) { // Tx cancelled @@ -187,6 +190,8 @@ export function withMediaForm return ; }; + ResultComponent.displayName = `withMediaForm(${componentName(Component)})`; + return ResultComponent; } diff --git a/pioneer/packages/joy-media/src/common/MediaPlayerView.tsx b/pioneer/packages/joy-media/src/common/MediaPlayerView.tsx index ff0d7a2193..959b18f1fc 100644 --- a/pioneer/packages/joy-media/src/common/MediaPlayerView.tsx +++ b/pioneer/packages/joy-media/src/common/MediaPlayerView.tsx @@ -5,7 +5,7 @@ import APlayer from 'react-aplayer'; import { ApiProps } from '@polkadot/react-api/types'; import { I18nProps } from '@polkadot/react-components/types'; -import { withCalls, withMulti } from '@polkadot/react-api/with'; +import { withCalls, withMulti } from '@polkadot/react-api/hoc'; import { Option } from '@polkadot/types/codec'; import translate from '../translate'; @@ -14,8 +14,8 @@ import { DataObject, ContentId } from '@joystream/types/media'; import { VideoType } from '../schemas/video/Video'; import { isAccountAChannelOwner } from '../channels/ChannelHelpers'; import { ChannelEntity } from '../entities/ChannelEntity'; -import { useMyMembership } from '@polkadot/joy-utils/MyMembershipContext'; -import { JoyError } from '@polkadot/joy-utils/JoyStatus'; +import { useMyMembership } from '@polkadot/joy-utils/react/hooks'; +import { JoyError } from '@polkadot/joy-utils/react/components'; const PLAYER_COMMON_PARAMS = { lang: 'en', @@ -41,7 +41,7 @@ export type RequiredMediaPlayerProps = { type ContentProps = { contentType?: string; dataObjectOpt?: Option; - resolvedAssetUrl?: string; + resolvedAssetUrl: string; } type MediaPlayerViewProps = ApiProps & I18nProps & @@ -78,6 +78,7 @@ function Player (props: PlayerProps) { if (prefix === 'video') { const video = { url, name, pic: cover }; + return ; } else if (prefix === 'audio') { const audio = { url, name, cover }; + return (); const [resolvedAssetUrl, setResolvedAssetUrl] = useState(); const [contentType, setContentType] = useState(); const [cancelSource, setCancelSource] = useState(newCancelSource()); - const getActiveStorageProviderIds = async (): Promise => { - const nextId = await api.query.storageWorkingGroup.nextWorkerId() as StorageProviderId; - // This is chain specfic, but if next id is still 0, it means no workers have been added, - // so the workerById is empty - if (nextId.eq(0)) { - return []; - } - - const workers = new MultipleLinkedMapEntry( - StorageProviderId, - Worker, - await api.query.storageWorkingGroup.workerById() - ); - - return workers.linked_keys; - }; - const resolveAsset = async () => { setError(undefined); setCancelSource(newCancelSource()); - const rids: DataObjectStorageRelationshipId[] = await api.query.dataObjectStorageRegistry.relationshipsByContentId(contentId) as any; + const rids = await api.query.dataObjectStorageRegistry.relationshipsByContentId>(contentId); - const allRelationships: Option[] = await Promise.all(rids.map((id) => api.query.dataObjectStorageRegistry.relationships(id))) as any; + const allRelationships = await Promise.all( + rids.map((id) => + api.query.dataObjectStorageRegistry.relationships>(id) + ) + ); // Providers that have signalled onchain that they have the asset - let readyProviders = allRelationships.filter(r => r.isSome).map(r => r.unwrap()) - .filter(r => r.ready) - .map(r => r.storage_provider); + let readyProviders = allRelationships.filter((r) => r.isSome).map((r) => r.unwrap()) + .filter((r) => r.ready) + .map((r) => r.storage_provider); // runtime doesn't currently guarantee unique set - readyProviders = _.uniqBy(readyProviders, provider => provider.toString()); + readyProviders = _.uniqBy(readyProviders, (provider) => provider.toString()); if (!readyProviders.length) { setError(new Error('No Storage Providers found storing this content')); + return; } + const activeProviders = (await transport.workingGroups.allWorkers('Storage')).map(([id]) => id); + // filter out providers no longer active - relationships of providers that have left // are not pruned onchain. - const activeProviders = await getActiveStorageProviderIds(); - readyProviders = _.intersectionBy(activeProviders, readyProviders, provider => provider.toString()); + readyProviders = _.intersectionBy(activeProviders, readyProviders, (provider) => provider.toString()); - console.log(`Found ${readyProviders.length} providers ready to serve content: ${readyProviders}`); + console.log(`Found ${readyProviders.length} providers ready to serve content: ${readyProviders.join(', ')}`); // shuffle to spread the load readyProviders = _.shuffle(readyProviders); @@ -84,9 +74,11 @@ function InnerComponent (props: Props) { // loop over providers until we find one that responds while (readyProviders.length) { const provider = readyProviders.shift(); + if (!provider) continue; let assetUrl: string | undefined; + try { assetUrl = await discoveryProvider.resolveAssetEndpoint(provider, contentId.encode(), cancelSource.token); } catch (err) { @@ -100,16 +92,23 @@ function InnerComponent (props: Props) { try { console.log('Check URL of resolved asset:', assetUrl); const response = await axios.head(assetUrl, { cancelToken: cancelSource.token }); + const headers = response.headers as Record; - setContentType(response.headers['content-type'] || 'video/video'); + setContentType(headers['content-type'] || 'video/video'); setResolvedAssetUrl(assetUrl); return; - } catch (err) { + } catch (e) { + const err = e as unknown; + if (axios.isCancel(err)) { return; } else { - if (!err.response || (err.response.status >= 500 && err.response.status <= 504)) { + const response = isObjectWithProperties(err, 'response') + ? (err as AxiosError).response + : undefined; + + if (!response || (response.status >= 500 && response.status <= 504)) { // network connection error discoveryProvider.reportUnreachable(provider); } @@ -124,7 +123,7 @@ function InnerComponent (props: Props) { }; useEffect(() => { - resolveAsset(); + void resolveAsset(); return () => { cancelSource.cancel(); @@ -149,6 +148,7 @@ function InnerComponent (props: Props) { } const playerProps = { ...props, contentType, resolvedAssetUrl }; + return ; } diff --git a/pioneer/packages/joy-media/src/common/TypeHelpers.ts b/pioneer/packages/joy-media/src/common/TypeHelpers.ts index 4feaa33d92..893c2e08b4 100644 --- a/pioneer/packages/joy-media/src/common/TypeHelpers.ts +++ b/pioneer/packages/joy-media/src/common/TypeHelpers.ts @@ -1,4 +1,5 @@ import BN from 'bn.js'; +import { createMock } from '@joystream/types'; import { ChannelId } from '@joystream/types/content-working-group'; import { EntityId, ClassId } from '@joystream/types/versioned-store'; @@ -16,9 +17,9 @@ export function asChannelId (id: AnyChannelId): ChannelId { if (id instanceof ChannelId) { return id; } else if (canBeId(id)) { - return new ChannelId(id); + return createMock('ChannelId', id); } else { - throw new Error(`Not supported format for Channel id: ${id}`); + throw new Error(`Not supported format for Channel id: ${typeof id === 'object' ? id.constructor.name : id}`); } } @@ -26,9 +27,9 @@ export function asEntityId (id: AnyEntityId): EntityId { if (id instanceof EntityId) { return id; } else if (canBeId(id)) { - return new EntityId(id); + return createMock('EntityId', id); } else { - throw new Error(`Not supported format for Entity id: ${id}`); + throw new Error(`Not supported format for Entity id: ${typeof id === 'object' ? id.constructor.name : id}`); } } @@ -36,8 +37,8 @@ export function asClassId (id: AnyClassId): ClassId { if (id instanceof ClassId) { return id; } else if (canBeId(id)) { - return new ClassId(id); + return createMock('ClassId', id); } else { - throw new Error(`Not supported format for Class id: ${id}`); + throw new Error(`Not supported format for Class id: ${typeof id === 'object' ? id.constructor.name : id}`); } } diff --git a/pioneer/packages/joy-utils-old/src/images.ts b/pioneer/packages/joy-media/src/common/images.tsx similarity index 94% rename from pioneer/packages/joy-utils-old/src/images.ts rename to pioneer/packages/joy-media/src/common/images.tsx index c246b7bbd5..859dedbe78 100644 --- a/pioneer/packages/joy-utils-old/src/images.ts +++ b/pioneer/packages/joy-media/src/common/images.tsx @@ -1,3 +1,5 @@ +import React from 'react'; + export const DEFAULT_THUMBNAIL_URL = 'images/default-thumbnail.png'; // This is a hack to just satisfy TypeScript compiler. @@ -8,6 +10,7 @@ type ImageOnErrorEvent = EventTarget & { export function onImageError (event: React.SyntheticEvent) { const target = event.target as ImageOnErrorEvent; + // Set onerror callback to undefined to prevent infinite callbacks when image src path fails: target.onerror = undefined; target.src = DEFAULT_THUMBNAIL_URL; diff --git a/pioneer/packages/joy-media/src/common/index.css b/pioneer/packages/joy-media/src/common/index.scss similarity index 100% rename from pioneer/packages/joy-media/src/common/index.css rename to pioneer/packages/joy-media/src/common/index.scss diff --git a/pioneer/packages/joy-media/src/explore/AllChannels.tsx b/pioneer/packages/joy-media/src/explore/AllChannels.tsx index 86a3cf5046..dffbf7d14d 100644 --- a/pioneer/packages/joy-media/src/explore/AllChannels.tsx +++ b/pioneer/packages/joy-media/src/explore/AllChannels.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import Section from '@polkadot/joy-utils/Section'; +import { Section } from '@polkadot/joy-utils/react/components'; import { MediaView } from '../MediaView'; import { ChannelEntity } from '../entities/ChannelEntity'; import { ChannelPreview } from '../channels/ChannelPreview'; diff --git a/pioneer/packages/joy-media/src/explore/AllVideos.tsx b/pioneer/packages/joy-media/src/explore/AllVideos.tsx index 588981f2d5..83fdda698d 100644 --- a/pioneer/packages/joy-media/src/explore/AllVideos.tsx +++ b/pioneer/packages/joy-media/src/explore/AllVideos.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import Section from '@polkadot/joy-utils/Section'; +import { Section } from '@polkadot/joy-utils/react/components'; import { VideoPreviewProps, VideoPreview } from '../video/VideoPreview'; import { MediaView } from '../MediaView'; diff --git a/pioneer/packages/joy-media/src/explore/ExploreContent.tsx b/pioneer/packages/joy-media/src/explore/ExploreContent.tsx index b8a1aba49e..286d92e828 100644 --- a/pioneer/packages/joy-media/src/explore/ExploreContent.tsx +++ b/pioneer/packages/joy-media/src/explore/ExploreContent.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { Link } from 'react-router-dom'; -import Section from '@polkadot/joy-utils/Section'; +import { Section } from '@polkadot/joy-utils/react/components'; import { VideoPreviewProps, VideoPreview } from '../video/VideoPreview'; import { ChannelEntity } from '../entities/ChannelEntity'; import { ChannelPreview } from '../channels/ChannelPreview'; diff --git a/pioneer/packages/joy-media/src/explore/PlayContent.tsx b/pioneer/packages/joy-media/src/explore/PlayContent.tsx index 96f28b7cf3..dd5e86225b 100644 --- a/pioneer/packages/joy-media/src/explore/PlayContent.tsx +++ b/pioneer/packages/joy-media/src/explore/PlayContent.tsx @@ -1,7 +1,7 @@ import React, { useState } from 'react'; import { MusicAlbumPreviewProps, MusicAlbumPreview } from '../music/MusicAlbumPreview'; import { MusicTrackReaderPreviewProps, MusicTrackReaderPreview } from '../music/MusicTrackReaderPreview'; -import { Pluralize } from '@polkadot/joy-utils/Pluralize'; +import { Pluralize } from '@polkadot/joy-utils/react/components'; import { Table } from 'semantic-ui-react'; import { ChannelEntity } from '../entities/ChannelEntity'; import { ChannelPreview } from '../channels/ChannelPreview'; @@ -85,7 +85,7 @@ export function PlayContent (props: Props) { {featuredAlbums.length > 0 &&

Featured albums

- {featuredAlbums.map(x => )} + {featuredAlbums.map((x) => )}
}
; diff --git a/pioneer/packages/joy-media/src/index.css b/pioneer/packages/joy-media/src/index.scss similarity index 96% rename from pioneer/packages/joy-media/src/index.css rename to pioneer/packages/joy-media/src/index.scss index 2b7395c214..6b4a55e972 100644 --- a/pioneer/packages/joy-media/src/index.css +++ b/pioneer/packages/joy-media/src/index.scss @@ -41,6 +41,11 @@ width: 100%; max-width: 600px; } + + .EditMetaForm { + flex-grow: 1; + max-width: 600px; + } } .PlayBox { @@ -72,9 +77,6 @@ .MediaCell { width: 25%; - &.MyContent { - /* background-color: #fff8e1; */ - } &:hover { background-color: #deeffc; } diff --git a/pioneer/packages/joy-media/src/index.tsx b/pioneer/packages/joy-media/src/index.tsx index c499fa2529..5a2fd2b17e 100644 --- a/pioneer/packages/joy-media/src/index.tsx +++ b/pioneer/packages/joy-media/src/index.tsx @@ -3,15 +3,16 @@ import React from 'react'; import { Route, Switch } from 'react-router'; import { AppProps, I18nProps } from '@polkadot/react-components/types'; -import Tabs, { TabItem } from '@polkadot/react-components/Tabs'; +import Tabs from '@polkadot/react-components/Tabs'; +import { TabItem } from '@polkadot/react-components/Tabs/types'; import { ApiProps } from '@polkadot/react-api/types'; -import { withMulti } from '@polkadot/react-api/with'; +import { withMulti } from '@polkadot/react-api/hoc'; -import './index.css'; -import './common/index.css'; +import './index.scss'; +import './common/index.scss'; import translate from './translate'; -import { useMyAccount } from '@polkadot/joy-utils/MyAccountContext'; +import { useMyAccount } from '@polkadot/joy-utils/react/hooks'; import { UploadWithRouter } from './Upload'; import { DiscoveryProviderProps, DiscoveryProviderProvider } from './DiscoveryProvider'; import { SubstrateTransportProvider } from './TransportContext'; @@ -25,7 +26,7 @@ import { AllVideosView } from './explore/AllVideos'; import { AllChannelsView } from './explore/AllChannels'; // import { VideosByOwner } from './video/VideosByOwner'; -type Props = AppProps & I18nProps & ApiProps & DiscoveryProviderProps & {}; +type Props = AppProps & I18nProps & ApiProps & DiscoveryProviderProps; function App (props: Props) { const { t, basePath } = props; @@ -45,7 +46,7 @@ function App (props: Props) { // name: `account/${myAddress}/videos`, // text: t('My videos') // } - ].filter(x => x !== undefined) as TabItem[]; + ].filter((x) => x !== undefined) as TabItem[]; return ( diff --git a/pioneer/packages/joy-media/src/mocks/ContentLicense.mock.ts b/pioneer/packages/joy-media/src/mocks/ContentLicense.mock.ts index e9c5ffc99d..09eb0f4525 100644 --- a/pioneer/packages/joy-media/src/mocks/ContentLicense.mock.ts +++ b/pioneer/packages/joy-media/src/mocks/ContentLicense.mock.ts @@ -9,6 +9,6 @@ const values = [ ]; export const AllContentLicenses: ContentLicenseType[] = - values.map(value => ({ id: newEntityId(), value })) as unknown as ContentLicenseType[]; // A hack to fix TS compilation. + values.map((value) => ({ id: newEntityId(), value })) as unknown as ContentLicenseType[]; // A hack to fix TS compilation. export const ContentLicense = AllContentLicenses[0]; diff --git a/pioneer/packages/joy-media/src/mocks/Language.mock.ts b/pioneer/packages/joy-media/src/mocks/Language.mock.ts index 70ebd051fe..c4ecfc380f 100644 --- a/pioneer/packages/joy-media/src/mocks/Language.mock.ts +++ b/pioneer/packages/joy-media/src/mocks/Language.mock.ts @@ -6,6 +6,6 @@ const values = [ ]; export const AllLanguages: LanguageType[] = - values.map(value => ({ id: newEntityId(), value })) as unknown as LanguageType[]; // A hack to fix TS compilation. + values.map((value) => ({ id: newEntityId(), value })) as unknown as LanguageType[]; // A hack to fix TS compilation. export const Language = AllLanguages[0]; diff --git a/pioneer/packages/joy-media/src/mocks/MediaObject.mock.ts b/pioneer/packages/joy-media/src/mocks/MediaObject.mock.ts index 7fce1ed0ee..bfd0cac211 100644 --- a/pioneer/packages/joy-media/src/mocks/MediaObject.mock.ts +++ b/pioneer/packages/joy-media/src/mocks/MediaObject.mock.ts @@ -12,6 +12,6 @@ const values = [ ]; export const AllMediaObjects: MediaObjectType[] = - values.map(value => ({ id: newEntityId(), value })) as unknown as MediaObjectType[]; // A hack to fix TS compilation. + values.map((value) => ({ id: newEntityId(), value })) as unknown as MediaObjectType[]; // A hack to fix TS compilation. export const MediaObject = AllMediaObjects[0]; diff --git a/pioneer/packages/joy-media/src/mocks/MusicGenre.mock.ts b/pioneer/packages/joy-media/src/mocks/MusicGenre.mock.ts index 9c005ed01d..c412d95ed9 100644 --- a/pioneer/packages/joy-media/src/mocks/MusicGenre.mock.ts +++ b/pioneer/packages/joy-media/src/mocks/MusicGenre.mock.ts @@ -26,6 +26,6 @@ const values = [ ]; export const AllMusicGenres: MusicGenreType[] = - values.map(value => ({ id: newEntityId(), value })) as unknown as MusicGenreType[]; // A hack to fix TS compilation. + values.map((value) => ({ id: newEntityId(), value })) as unknown as MusicGenreType[]; // A hack to fix TS compilation. export const MusicGenre = AllMusicGenres[0]; diff --git a/pioneer/packages/joy-media/src/mocks/MusicMood.mock.ts b/pioneer/packages/joy-media/src/mocks/MusicMood.mock.ts index b2624c0176..b7c871ad61 100644 --- a/pioneer/packages/joy-media/src/mocks/MusicMood.mock.ts +++ b/pioneer/packages/joy-media/src/mocks/MusicMood.mock.ts @@ -294,6 +294,6 @@ const values = [ ]; export const AllMusicMoods: MusicMoodType[] = - values.map(value => ({ id: newEntityId(), value })) as unknown as MusicMoodType[]; // A hack to fix TS compilation. + values.map((value) => ({ id: newEntityId(), value })) as unknown as MusicMoodType[]; // A hack to fix TS compilation. export const MusicMood = AllMusicMoods[0]; diff --git a/pioneer/packages/joy-media/src/mocks/MusicTheme.mock.ts b/pioneer/packages/joy-media/src/mocks/MusicTheme.mock.ts index 20373fb812..af1db48ad6 100644 --- a/pioneer/packages/joy-media/src/mocks/MusicTheme.mock.ts +++ b/pioneer/packages/joy-media/src/mocks/MusicTheme.mock.ts @@ -187,6 +187,6 @@ const values = [ ]; export const AllMusicThemes: MusicThemeType[] = - values.map(value => ({ id: newEntityId(), value })) as unknown as MusicThemeType[]; // A hack to fix TS compilation. + values.map((value) => ({ id: newEntityId(), value })) as unknown as MusicThemeType[]; // A hack to fix TS compilation. export const MusicTheme = AllMusicThemes[0]; diff --git a/pioneer/packages/joy-media/src/mocks/VideoCategory.mock.ts b/pioneer/packages/joy-media/src/mocks/VideoCategory.mock.ts index d235004232..8eada98733 100644 --- a/pioneer/packages/joy-media/src/mocks/VideoCategory.mock.ts +++ b/pioneer/packages/joy-media/src/mocks/VideoCategory.mock.ts @@ -20,6 +20,6 @@ const values = [ ]; export const AllVideoCategories: VideoCategoryType[] = - values.map(value => ({ id: newEntityId(), value })) as unknown as VideoCategoryType[]; // A hack to fix TS compilation. + values.map((value) => ({ id: newEntityId(), value })) as unknown as VideoCategoryType[]; // A hack to fix TS compilation. export const VideoCategory = AllVideoCategories[0]; diff --git a/pioneer/packages/joy-media/src/music/EditMusicAlbum.tsx b/pioneer/packages/joy-media/src/music/EditMusicAlbum.tsx index 4d8cd00f27..1ed8210af1 100644 --- a/pioneer/packages/joy-media/src/music/EditMusicAlbum.tsx +++ b/pioneer/packages/joy-media/src/music/EditMusicAlbum.tsx @@ -3,8 +3,8 @@ import { Button, Tab } from 'semantic-ui-react'; import { Form, withFormik } from 'formik'; import { History } from 'history'; -import TxButton from '@polkadot/joy-utils/TxButton'; -import { onImageError } from '@polkadot/joy-utils/images'; +import { TxButton } from '@polkadot/joy-utils/react/components'; +import { onImageError } from '../common/images'; import { ReorderableTracks } from './ReorderableTracks'; import { MusicAlbumValidationSchema, MusicAlbumType, MusicAlbumClass as Fields, MusicAlbumFormValues, MusicAlbumToFormValues } from '../schemas/music/MusicAlbum'; import { withMediaForm, MediaFormProps, datePlaceholder } from '../common/MediaForms'; @@ -123,7 +123,6 @@ const InnerForm = (props: MediaFormProps) => { const renderMainButton = () => ({ // Transform outer props into form values mapPropsToValues: (props): FormValues => { const { entity } = props; + return MusicAlbumToFormValues(entity); }, diff --git a/pioneer/packages/joy-media/src/music/EditMusicAlbum.view.tsx b/pioneer/packages/joy-media/src/music/EditMusicAlbum.view.tsx index 4e52b8657c..7ba8eb9154 100644 --- a/pioneer/packages/joy-media/src/music/EditMusicAlbum.view.tsx +++ b/pioneer/packages/joy-media/src/music/EditMusicAlbum.view.tsx @@ -8,6 +8,7 @@ export const EditMusicAlbumView = MediaView({ const { transport, id } = props; const entity = id ? await transport.musicAlbumById(id) : undefined; const opts = await transport.dropdownOptions(); + return { entity, opts }; } }); diff --git a/pioneer/packages/joy-media/src/music/MusicAlbumPreview.tsx b/pioneer/packages/joy-media/src/music/MusicAlbumPreview.tsx index 3281bdf322..0acc51fc0f 100644 --- a/pioneer/packages/joy-media/src/music/MusicAlbumPreview.tsx +++ b/pioneer/packages/joy-media/src/music/MusicAlbumPreview.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { Button } from 'semantic-ui-react'; -import { Pluralize } from '@polkadot/joy-utils/Pluralize'; +import { Pluralize } from '@polkadot/joy-utils/react/components'; import { BgImg } from '../common/BgImg'; import { ChannelEntity } from '../entities/ChannelEntity'; diff --git a/pioneer/packages/joy-media/src/music/MusicAlbumTracks.tsx b/pioneer/packages/joy-media/src/music/MusicAlbumTracks.tsx index ff48a86e2e..9fa63c7a27 100644 --- a/pioneer/packages/joy-media/src/music/MusicAlbumTracks.tsx +++ b/pioneer/packages/joy-media/src/music/MusicAlbumTracks.tsx @@ -1,6 +1,6 @@ import React, { useState } from 'react'; import { Button, CheckboxProps } from 'semantic-ui-react'; -import { Pluralize } from '@polkadot/joy-utils/Pluralize'; +import { Pluralize } from '@polkadot/joy-utils/react/components'; import { EditableMusicTrackPreviewProps, MusicTrackPreview } from './MusicTrackPreview'; import { MusicAlbumPreviewProps, MusicAlbumPreview } from './MusicAlbumPreview'; @@ -21,6 +21,7 @@ export function TracksOfMyMusicAlbum (props: TracksOfMyMusicAlbumProps) { data: CheckboxProps ) => { const set = new Set(idxsOfSelectedTracks); + data.checked ? set.add(trackIdx) : set.delete(trackIdx) diff --git a/pioneer/packages/joy-media/src/music/MusicTrackPreview.tsx b/pioneer/packages/joy-media/src/music/MusicTrackPreview.tsx index 09b980fe71..200374860e 100644 --- a/pioneer/packages/joy-media/src/music/MusicTrackPreview.tsx +++ b/pioneer/packages/joy-media/src/music/MusicTrackPreview.tsx @@ -35,10 +35,11 @@ export function MusicTrackPreview (props: EditableMusicTrackPreviewProps) { } catch (err) { console.log('Error during checkbox change:', err); } + setChecked(d.checked || false); }; - return
+ return
{props.onSelect &&
} diff --git a/pioneer/packages/joy-media/src/music/MusicTrackReaderPreview.tsx b/pioneer/packages/joy-media/src/music/MusicTrackReaderPreview.tsx index caccabf532..9a834a5423 100644 --- a/pioneer/packages/joy-media/src/music/MusicTrackReaderPreview.tsx +++ b/pioneer/packages/joy-media/src/music/MusicTrackReaderPreview.tsx @@ -14,6 +14,7 @@ export function MusicTrackReaderPreview (props: MusicTrackReaderPreviewProps) { const { size = 200, orientation = 'vertical' } = props; const descStyle: CSSProperties = {}; + if (orientation === 'vertical') { descStyle.maxWidth = size; } diff --git a/pioneer/packages/joy-media/src/music/MyMusicTracks.tsx b/pioneer/packages/joy-media/src/music/MyMusicTracks.tsx index 2f5a964617..ecd6f1b1ac 100644 --- a/pioneer/packages/joy-media/src/music/MyMusicTracks.tsx +++ b/pioneer/packages/joy-media/src/music/MyMusicTracks.tsx @@ -1,8 +1,8 @@ import React, { useState } from 'react'; import { Button, CheckboxProps, Dropdown, Message } from 'semantic-ui-react'; -import { Pluralize } from '@polkadot/joy-utils/Pluralize'; -import Section from '@polkadot/joy-utils/Section'; +import { Pluralize, Section } from '@polkadot/joy-utils/react/components'; + import { EditableMusicTrackPreviewProps, MusicTrackPreview } from './MusicTrackPreview'; import { ReorderableTracks } from './ReorderableTracks'; import { MusicAlbumPreviewProps } from './MusicAlbumPreview'; @@ -36,14 +36,16 @@ export function MyMusicTracks (props: MyMusicTracksProps) { const selectedCount = idsOfSelectedTracks.size; let longestAlbumName = ''; - albums.forEach(x => { + + albums.forEach((x) => { if (longestAlbumName.length < x.title.length) { longestAlbumName = x.title; } }); - const albumsDropdownOptions = albums.map(x => { + const albumsDropdownOptions = albums.map((x) => { const { id } = x; + return { key: id, value: id, @@ -67,7 +69,8 @@ export function MyMusicTracks (props: MyMusicTracksProps) { return
{ - const selectedAlbum = albums.find(x => x.id === id); + const selectedAlbum = albums.find((x) => x.id === id); + if (selectedAlbum) { setAlbumName(selectedAlbum.title); setShowSecondScreen(true); @@ -125,10 +128,10 @@ export function MyMusicTracks (props: MyMusicTracksProps) {

; }; - const selectedTracks = tracks.filter(track => idsOfSelectedTracks.has(track.id)); + const selectedTracks = tracks.filter((track) => idsOfSelectedTracks.has(track.id)); const renderReorderTracks = () => { - return
+ return
{ + onRemove={(track) => { const set = new Set(idsOfSelectedTracks); + set.delete(track.id); setIdsOfSelectedTracks(set); }} diff --git a/pioneer/packages/joy-media/src/music/ReorderableTracks.tsx b/pioneer/packages/joy-media/src/music/ReorderableTracks.tsx index 258885b955..717b2765d6 100644 --- a/pioneer/packages/joy-media/src/music/ReorderableTracks.tsx +++ b/pioneer/packages/joy-media/src/music/ReorderableTracks.tsx @@ -6,6 +6,7 @@ import { EditableMusicTrackPreviewProps, MusicTrackPreview } from './MusicTrackP const reorder = (list: OrderableItem[], startIndex: number, endIndex: number) => { const result = Array.from(list); const [removed] = result.splice(startIndex, 1); + result.splice(endIndex, 0, removed); return result; @@ -72,7 +73,8 @@ export const ReorderableTracks = (props: Props) => { withRemoveButton onRemove={() => { onRemove(item); - const lessItems = items.filter(x => x.id !== item.id); + const lessItems = items.filter((x) => x.id !== item.id); + setItems(lessItems); }} /> diff --git a/pioneer/packages/joy-media/src/schemas/channel/Channel.ts b/pioneer/packages/joy-media/src/schemas/channel/Channel.ts index 26820320fa..d66922de8c 100644 --- a/pioneer/packages/joy-media/src/schemas/channel/Channel.ts +++ b/pioneer/packages/joy-media/src/schemas/channel/Channel.ts @@ -4,7 +4,7 @@ import { BlockNumber, AccountId } from '@polkadot/types/interfaces'; import { ChannelContentTypeValue, PrincipalId, Channel, ChannelId, ChannelPublicationStatusValue, ChannelCurationStatusValue } from '@joystream/types/content-working-group'; import { MemberId } from '@joystream/types/members'; import { ChannelValidationConstraints } from '@polkadot/joy-media/transport'; -import { ValidationConstraint } from '@polkadot/joy-utils/ValidationConstraint'; +import { ValidationConstraint } from '@polkadot/joy-utils/types/ValidationConstraint'; function textValidation (constraint?: ValidationConstraint) { if (!constraint) { @@ -12,10 +12,12 @@ function textValidation (constraint?: ValidationConstraint) { } const { min, max } = constraint; + return Yup.string() .min(min, `Text is too short. Minimum length is ${min} chars.`) .max(max, `Text is too long. Maximum length is ${max} chars.`); } + export const buildChannelValidationSchema = (constraints?: ChannelValidationConstraints) => Yup.object().shape({ handle: textValidation(constraints?.handle).required('This field is required'), @@ -56,19 +58,19 @@ export class ChannelCodec { static fromSubstrate (id: ChannelId, sub: Channel): ChannelType { return { id: id.toNumber(), - verified: sub.getBoolean('verified'), - handle: sub.getString('handle'), - title: sub.getOptionalString('title'), - description: sub.getOptionalString('description'), - avatar: sub.getOptionalString('avatar'), - banner: sub.getOptionalString('banner'), - content: sub.getEnumAsString('content'), - owner: sub.getField('owner'), - roleAccount: sub.getField('role_account'), - publicationStatus: sub.getEnumAsString('publication_status'), - curationStatus: sub.getEnumAsString('curation_status'), - created: sub.getField('created'), - principalId: sub.getField('principal_id') + verified: sub.verified.valueOf(), + handle: sub.handle.toString(), + title: sub.title.unwrapOr(undefined)?.toString(), + description: sub.description.unwrapOr(undefined)?.toString(), + avatar: sub.avatar.unwrapOr(undefined)?.toString(), + banner: sub.banner.unwrapOr(undefined)?.toString(), + content: sub.content.type, + owner: sub.owner, + roleAccount: sub.role_account, + publicationStatus: sub.publication_status.type, + curationStatus: sub.curation_status.type, + created: sub.created, + principalId: sub.principal_id }; } } diff --git a/pioneer/packages/joy-media/src/schemas/general/FeaturedContent.ts b/pioneer/packages/joy-media/src/schemas/general/FeaturedContent.ts index a6e55ca28c..0712605dc0 100644 --- a/pioneer/packages/joy-media/src/schemas/general/FeaturedContent.ts +++ b/pioneer/packages/joy-media/src/schemas/general/FeaturedContent.ts @@ -30,8 +30,8 @@ export class FeaturedContentCodec extends EntityCodec { } export function FeaturedContentToFormValues (entity?: FeaturedContentType): FeaturedContentFormValues { return { topVideo: (entity && entity.topVideo?.id) || 0, - featuredVideos: (entity && entity.featuredVideos?.map(x => x.id)) || [], - featuredAlbums: (entity && entity.featuredAlbums?.map(x => x.id)) || [] + featuredVideos: (entity && entity.featuredVideos?.map((x) => x.id)) || [], + featuredAlbums: (entity && entity.featuredAlbums?.map((x) => x.id)) || [] }; } diff --git a/pioneer/packages/joy-media/src/schemas/music/MusicAlbum.ts b/pioneer/packages/joy-media/src/schemas/music/MusicAlbum.ts index 78f8881a18..8780a6937e 100644 --- a/pioneer/packages/joy-media/src/schemas/music/MusicAlbum.ts +++ b/pioneer/packages/joy-media/src/schemas/music/MusicAlbum.ts @@ -102,7 +102,7 @@ export function MusicAlbumToFormValues (entity?: MusicAlbumType): MusicAlbumForm genre: (entity && entity.genre?.id) || 0, mood: (entity && entity.mood?.id) || 0, theme: (entity && entity.theme?.id) || 0, - tracks: (entity && entity.tracks?.map(x => x.id)) || [], + tracks: (entity && entity.tracks?.map((x) => x.id)) || [], language: (entity && entity.language?.id) || 0, links: (entity && entity.links) || [], lyrics: (entity && entity.lyrics) || '', diff --git a/pioneer/packages/joy-media/src/stories/ExploreContent.stories.tsx b/pioneer/packages/joy-media/src/stories/ExploreContent.stories.tsx index f446771f75..6a7cf9bc6a 100644 --- a/pioneer/packages/joy-media/src/stories/ExploreContent.stories.tsx +++ b/pioneer/packages/joy-media/src/stories/ExploreContent.stories.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import '../common/index.css'; +import '../common/index.scss'; import { ExploreContent } from '../explore/ExploreContent'; import { withMockTransport } from './withMockTransport'; diff --git a/pioneer/packages/joy-media/src/stories/MusicAlbumTracks.stories.tsx b/pioneer/packages/joy-media/src/stories/MusicAlbumTracks.stories.tsx index f45d00a1c6..f50cdbd617 100644 --- a/pioneer/packages/joy-media/src/stories/MusicAlbumTracks.stories.tsx +++ b/pioneer/packages/joy-media/src/stories/MusicAlbumTracks.stories.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import '../common/index.css'; +import '../common/index.scss'; import { EditForm } from '../music/EditMusicAlbum'; import { MyMusicTracks } from '../music/MyMusicTracks'; @@ -7,7 +7,7 @@ import { MusicAlbumSamples } from './data/MusicAlbumSamples'; import { albumTracks, AllMusicTrackSamples } from './data/MusicTrackSamples'; import { withMockTransport } from './withMockTransport'; import { EditMusicAlbumView } from '../music/EditMusicAlbum.view'; -import EntityId from '@joystream/types/versioned-store/EntityId'; +import { createMock } from '@joystream/types'; export default { title: 'Media | My music tracks', @@ -19,7 +19,7 @@ export const DefaultState = () => export const MockEditAlbumView = () => ; diff --git a/pioneer/packages/joy-media/src/stories/MusicChannel.stories.tsx b/pioneer/packages/joy-media/src/stories/MusicChannel.stories.tsx index eff4f344e7..d7004e7879 100644 --- a/pioneer/packages/joy-media/src/stories/MusicChannel.stories.tsx +++ b/pioneer/packages/joy-media/src/stories/MusicChannel.stories.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import '../common/index.css'; +import '../common/index.scss'; import { MockMusicChannel } from './data/ChannelSamples'; import { ViewMusicChannel } from '../channels/ViewMusicChannel'; diff --git a/pioneer/packages/joy-media/src/stories/MyChannels.stories.tsx b/pioneer/packages/joy-media/src/stories/MyChannels.stories.tsx index d3ab3fe063..f5a15228bb 100644 --- a/pioneer/packages/joy-media/src/stories/MyChannels.stories.tsx +++ b/pioneer/packages/joy-media/src/stories/MyChannels.stories.tsx @@ -1,14 +1,13 @@ import React from 'react'; -import '../common/index.css'; +import '../common/index.scss'; -import { GenericAccountId } from '@polkadot/types'; import { ChannelsByOwner } from '../channels/ChannelsByOwner'; import { AllMockChannels } from './data/ChannelSamples'; import { withMockTransport } from './withMockTransport'; import EditForm from '../channels/EditChannel'; import { EditChannelView } from '../channels/EditChannel.view'; -import { ChannelId } from '@joystream/types/content-working-group'; import { AccountIdSamples } from './data/AccountIdSamples'; +import { createMock } from '@joystream/types'; export default { title: 'Media | My channels', @@ -16,7 +15,7 @@ export default { }; // TODO pass to mocked MyMembershipContext provider via Stories decorators: -const accountId = new GenericAccountId(AccountIdSamples.Alice); +const accountId = createMock('AccountId', AccountIdSamples.Alice); export const DefaultState = () => ; @@ -31,4 +30,4 @@ export const DefaultEditForm = () => ; export const MockEditFormView = () => - ; + ; diff --git a/pioneer/packages/joy-media/src/stories/MyMusicAlbums.stories.tsx b/pioneer/packages/joy-media/src/stories/MyMusicAlbums.stories.tsx index 90e711f10e..1c8dee2751 100644 --- a/pioneer/packages/joy-media/src/stories/MyMusicAlbums.stories.tsx +++ b/pioneer/packages/joy-media/src/stories/MyMusicAlbums.stories.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import '../common/index.css'; +import '../common/index.scss'; import { MyMusicAlbums } from '../music/MyMusicAlbums'; import { MusicAlbumSamples } from './data/MusicAlbumSamples'; diff --git a/pioneer/packages/joy-media/src/stories/Playback.stories.tsx b/pioneer/packages/joy-media/src/stories/Playback.stories.tsx index d6f2efe5bd..75c4353da2 100644 --- a/pioneer/packages/joy-media/src/stories/Playback.stories.tsx +++ b/pioneer/packages/joy-media/src/stories/Playback.stories.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import '../common/index.css'; +import '../common/index.scss'; import { PlayContent } from '../explore/PlayContent'; import { PlayVideo } from '../video/PlayVideo'; @@ -8,7 +8,7 @@ import { Album1TrackSamples } from './data/MusicTrackSamples'; import { MockMusicChannel, MockVideoChannel } from './data/ChannelSamples'; import { withMockTransport } from './withMockTransport'; import { Video } from '../mocks'; -import { EntityId } from '@joystream/types/versioned-store'; +import { createMock } from '@joystream/types'; export default { title: 'Media | Playback', @@ -17,7 +17,7 @@ export default { export const PlayVideoStory = () => ; diff --git a/pioneer/packages/joy-media/src/stories/UploadAudio.stories.tsx b/pioneer/packages/joy-media/src/stories/UploadAudio.stories.tsx index a770d2d711..736773758a 100644 --- a/pioneer/packages/joy-media/src/stories/UploadAudio.stories.tsx +++ b/pioneer/packages/joy-media/src/stories/UploadAudio.stories.tsx @@ -3,16 +3,16 @@ import { EditForm } from '../upload/UploadAudio'; import '../index.css'; import { ContentId } from '@joystream/types/media'; -import EntityId from '@joystream/types/versioned-store/EntityId'; import { UploadAudioView } from '../upload/UploadAudio.view'; import { withMockTransport } from './withMockTransport'; +import { mockRegistry, createMock } from '@joystream/types'; export default { title: 'Media | Upload audio', decorators: [withMockTransport] }; -const contentId = ContentId.generate(); +const contentId = ContentId.generate(mockRegistry); export const DefaultState = () => export const MockEditFormView = () => ; diff --git a/pioneer/packages/joy-media/src/stories/UploadVideo.stories.tsx b/pioneer/packages/joy-media/src/stories/UploadVideo.stories.tsx index 5c2913b575..e15e2be3d3 100644 --- a/pioneer/packages/joy-media/src/stories/UploadVideo.stories.tsx +++ b/pioneer/packages/joy-media/src/stories/UploadVideo.stories.tsx @@ -5,14 +5,14 @@ import '../index.css'; import { ContentId } from '@joystream/types/media'; import { withMockTransport } from './withMockTransport'; import EditVideoView from '../upload/EditVideo.view'; -import EntityId from '@joystream/types/versioned-store/EntityId'; +import { createMock, mockRegistry } from '@joystream/types'; export default { title: 'Media | Upload video', decorators: [withMockTransport] }; -const contentId = ContentId.generate(); +const contentId = ContentId.generate(mockRegistry); export const DefaultState = () => export const MockEditFormView = () => ; diff --git a/pioneer/packages/joy-media/src/stories/data/AccountIdSamples.ts b/pioneer/packages/joy-media/src/stories/data/AccountIdSamples.ts index d10e2a4fdd..03d78c4062 100644 --- a/pioneer/packages/joy-media/src/stories/data/AccountIdSamples.ts +++ b/pioneer/packages/joy-media/src/stories/data/AccountIdSamples.ts @@ -1,7 +1,7 @@ -import { GenericAccountId } from '@polkadot/types'; +import { createMock } from '@joystream/types'; export const AccountIdSamples = { - Alice: new GenericAccountId('5GNJqTPyNqANBkUVMN1LPPrxXnFouWXoe2wNSmmEoLctxiZY'), - Bob: new GenericAccountId('5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty'), - Charlie: new GenericAccountId('5FLSigC9HGRKVhB9FiEo4Y3koPsNmBmLJbpXg2mp1hXcS59Y') + Alice: createMock('AccountId', '5GNJqTPyNqANBkUVMN1LPPrxXnFouWXoe2wNSmmEoLctxiZY'), + Bob: createMock('AccountId', '5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty'), + Charlie: createMock('AccountId', '5FLSigC9HGRKVhB9FiEo4Y3koPsNmBmLJbpXg2mp1hXcS59Y') }; diff --git a/pioneer/packages/joy-media/src/stories/data/ChannelSamples.ts b/pioneer/packages/joy-media/src/stories/data/ChannelSamples.ts index 8afa50661d..b2373eeaa8 100644 --- a/pioneer/packages/joy-media/src/stories/data/ChannelSamples.ts +++ b/pioneer/packages/joy-media/src/stories/data/ChannelSamples.ts @@ -1,9 +1,7 @@ import BN from 'bn.js'; import { ChannelEntity } from '@polkadot/joy-media/entities/ChannelEntity'; -import { u32 } from '@polkadot/types'; import { AccountIdSamples } from './AccountIdSamples'; -import { MemberId } from '@joystream/types/members'; -import { PrincipalId } from '@joystream/types/content-working-group'; +import { createMock } from '@joystream/types'; let id = 0; const nextId = () => ++id; @@ -22,10 +20,10 @@ export const MockMusicChannel: ChannelEntity = publicationStatus: 'Unlisted', curationStatus: 'Censored', - owner: new MemberId(1), + owner: createMock('MemberId', 1), roleAccount: AccountIdSamples.Alice, - principalId: new PrincipalId(1), - created: new u32(123456), + principalId: createMock('PrincipalId', 1), + created: createMock('u32', 123456), rewardEarned: new BN('4587'), contentItemsCount: 57 @@ -45,10 +43,10 @@ export const MockVideoChannel: ChannelEntity = publicationStatus: 'Public', curationStatus: 'Normal', - owner: new MemberId(1), + owner: createMock('MemberId', 1), roleAccount: AccountIdSamples.Alice, - principalId: new PrincipalId(1), - created: new u32(123456), + principalId: createMock('PrincipalId', 1), + created: createMock('u32', 123456), rewardEarned: new BN('1820021'), contentItemsCount: 1529 diff --git a/pioneer/packages/joy-media/src/transport.mock.ts b/pioneer/packages/joy-media/src/transport.mock.ts index a79f776ea5..febefef6e9 100644 --- a/pioneer/packages/joy-media/src/transport.mock.ts +++ b/pioneer/packages/joy-media/src/transport.mock.ts @@ -17,6 +17,7 @@ import { PublicationStatusType } from './schemas/general/PublicationStatus'; import { VideoCategoryType } from './schemas/video/VideoCategory'; import { ChannelEntity } from './entities/ChannelEntity'; import { AllMockChannels } from './stories/data/ChannelSamples'; +import { mockPromise } from '@polkadot/joy-utils/transport/mock/base'; export class MockTransport extends MediaTransport { constructor () { @@ -29,7 +30,7 @@ export class MockTransport extends MediaTransport { } allChannels (): Promise { - return this.promise(AllMockChannels); + return mockPromise(AllMockChannels); } channelValidationConstraints (): Promise { @@ -45,54 +46,54 @@ export class MockTransport extends MediaTransport { } allVideos (): Promise { - return this.promise(mocks.AllVideos); + return mockPromise(mocks.AllVideos); } allMusicTracks (): Promise { - return this.promise(mocks.AllMusicTracks); + return mockPromise(mocks.AllMusicTracks); } allMusicAlbums (): Promise { - return this.promise(mocks.AllMusicAlbums); + return mockPromise(mocks.AllMusicAlbums); } featuredContent (): Promise { - return this.promise(mocks.FeaturedContent); + return mockPromise(mocks.FeaturedContent); } allContentLicenses (): Promise { - return this.promise(mocks.AllContentLicenses); + return mockPromise(mocks.AllContentLicenses); } allCurationStatuses (): Promise { - return this.promise(mocks.AllCurationStatuses); + return mockPromise(mocks.AllCurationStatuses); } allLanguages (): Promise { - return this.promise(mocks.AllLanguages); + return mockPromise(mocks.AllLanguages); } allMediaObjects (): Promise { - return this.promise(mocks.AllMediaObjects); + return mockPromise(mocks.AllMediaObjects); } allMusicGenres (): Promise { - return this.promise(mocks.AllMusicGenres); + return mockPromise(mocks.AllMusicGenres); } allMusicMoods (): Promise { - return this.promise(mocks.AllMusicMoods); + return mockPromise(mocks.AllMusicMoods); } allMusicThemes (): Promise { - return this.promise(mocks.AllMusicThemes); + return mockPromise(mocks.AllMusicThemes); } allPublicationStatuses (): Promise { - return this.promise(mocks.AllPublicationStatuses); + return mockPromise(mocks.AllPublicationStatuses); } allVideoCategories (): Promise { - return this.promise(mocks.AllVideoCategories); + return mockPromise(mocks.AllVideoCategories); } } diff --git a/pioneer/packages/joy-media/src/transport.substrate.ts b/pioneer/packages/joy-media/src/transport.substrate.ts index 64396c35f6..16e695c205 100644 --- a/pioneer/packages/joy-media/src/transport.substrate.ts +++ b/pioneer/packages/joy-media/src/transport.substrate.ts @@ -19,13 +19,11 @@ import { ChannelEntity } from './entities/ChannelEntity'; import { ChannelId, Channel } from '@joystream/types/content-working-group'; import { ApiPromise } from '@polkadot/api/index'; import { ApiProps } from '@polkadot/react-api/types'; -import { Vec } from '@polkadot/types'; -import { LinkageResult } from '@polkadot/types/codec/Linkage'; import { ChannelCodec } from './schemas/channel/Channel'; import { FeaturedContentType } from './schemas/general/FeaturedContent'; -import { AnyChannelId, asChannelId, AnyClassId, AnyEntityId } from './common/TypeHelpers'; -import { SimpleCache } from '@polkadot/joy-utils/SimpleCache'; -import { ValidationConstraint } from '@polkadot/joy-utils/ValidationConstraint'; +import { AnyChannelId, AnyClassId, AnyEntityId, asChannelId } from './common/TypeHelpers'; +import { SimpleCache } from '@polkadot/joy-utils/transport/SimpleCache'; +import { ValidationConstraint } from '@polkadot/joy-utils/types/ValidationConstraint'; const FIRST_CHANNEL_ID = 1; const FIRST_CLASS_ID = 1; @@ -126,21 +124,22 @@ export class SubstrateTransport extends MediaTransport { async allChannelIds (): Promise { let nextId = (await this.nextChannelId()).toNumber(); + if (nextId < 1) nextId = 1; const allIds: ChannelId[] = []; + for (let id = FIRST_CHANNEL_ID; id < nextId; id++) { - allIds.push(new ChannelId(id)); + allIds.push(this.api.createType('ChannelId', id)); } return allIds; } async loadChannelsByIds (ids: AnyChannelId[]): Promise { - const channelTuples = await this.cwgQuery().channelById.multi(ids); + const channels = await this.cwgQuery().channelById.multi(ids); - return channelTuples.map((tuple, i) => { - const channel = tuple[0] as Channel; + return channels.map((channel, i) => { const id = asChannelId(ids[i]); const plain = ChannelCodec.fromSubstrate(id, channel); @@ -154,11 +153,13 @@ export class SubstrateTransport extends MediaTransport { async allChannels (): Promise { const ids = await this.allChannelIds(); + return await this.channelCache.getOrLoadByIds(ids); } protected async getValidationConstraint (constraintName: string): Promise { const constraint = await this.cwgQuery()[constraintName](); + return { min: constraint.min.toNumber(), max: constraint.max.toNumber() @@ -179,6 +180,7 @@ export class SubstrateTransport extends MediaTransport { this.getValidationConstraint('channelAvatarConstraint'), this.getValidationConstraint('channelBannerConstraint') ]); + return { handle, title, @@ -199,38 +201,44 @@ export class SubstrateTransport extends MediaTransport { const nextId = (await this.nextClassId()).toNumber(); const allIds: ClassId[] = []; + for (let id = FIRST_CLASS_ID; id < nextId; id++) { - allIds.push(new ClassId(id)); + allIds.push(this.api.createType('ClassId', id)); } return allIds; } async loadClassesByIds (ids: AnyClassId[]): Promise { - return await this.vsQuery().classById.multi>(ids) as unknown as Class[]; + return await this.vsQuery().classById.multi(ids); } async allClasses (): Promise { const ids = await this.allClassIds(); + return await this.classCache.getOrLoadByIds(ids); } async getEntityCodecResolver (): Promise { if (!this.entityCodecResolver) { const classes = await this.allClasses(); + this.entityCodecResolver = new EntityCodecResolver(classes); } + return this.entityCodecResolver; } async classNamesToIdSet (classNames: ClassName[]): Promise> { const classNameToIdMap = await this.classIdByNameMap(); + return new Set(classNames - .map(name => { + .map((name) => { const classId = classNameToIdMap[name]; + return classId ? classId.toString() : undefined; }) - .filter(classId => typeof classId !== 'undefined') as string[] + .filter((classId) => typeof classId !== 'undefined') as string[] ); } @@ -245,8 +253,9 @@ export class SubstrateTransport extends MediaTransport { const nextId = (await this.nextEntityId()).toNumber(); const allIds: EntityId[] = []; + for (let id = FIRST_ENTITY_ID; id < nextId; id++) { - allIds.push(new EntityId(id)); + allIds.push(this.api.createType('EntityId', id)); } return allIds; @@ -255,14 +264,15 @@ export class SubstrateTransport extends MediaTransport { private async loadEntitiesByIds (ids: AnyEntityId[]): Promise { if (!ids || ids.length === 0) return []; - return await this.vsQuery().entityById.multi>(ids) as unknown as Entity[]; + return await this.vsQuery().entityById.multi(ids); } // TODO try to cache this func private async loadPlainEntitiesByIds (ids: AnyEntityId[]): Promise { const entities = await this.loadEntitiesByIds(ids); const cacheClassIds = await this.classNamesToIdSet(ClassNamesThatCanBeCached); - entities.forEach(e => { + + entities.forEach((e) => { if (cacheClassIds.has(e.class_id.toString())) { this.idsOfEntitiesToKeepInCache.add(e.id.toString()); } @@ -277,20 +287,23 @@ export class SubstrateTransport extends MediaTransport { async allPlainEntities (): Promise { const ids = await this.allEntityIds(); + return await this.entityCache.getOrLoadByIds(ids); } async findPlainEntitiesByClassName (className: ClassName): Promise { const res: T[] = []; const clazz = await this.classByName(className); + if (!clazz) { console.warn(`No class found by name '${className}'`); + return res; } const allIds = await this.allEntityIds(); const filteredEntities = (await this.entityCache.getOrLoadByIds(allIds)) - .filter(entity => clazz.id.eq(entity.classId)) as T[]; + .filter((entity) => clazz.id.eq(entity.classId)) as T[]; console.log(`Found ${filteredEntities.length} plain entities by class name '${className}'`); @@ -305,6 +318,7 @@ export class SubstrateTransport extends MediaTransport { const loadableClassIds = await this.classNamesToIdSet(ClassNamesThatRequireLoadingInternals); const converted: PlainEntity[] = []; + for (const entity of entities) { const classIdStr = entity.class_id.toString(); const codec = entityCodecResolver.getCodecByClassId(entity.class_id); @@ -337,6 +351,7 @@ export class SubstrateTransport extends MediaTransport { async featuredContent (): Promise { const arr = await this.findPlainEntitiesByClassName('FeaturedContent'); + return arr && arr.length ? arr[0] : undefined; } diff --git a/pioneer/packages/joy-media/src/transport.ts b/pioneer/packages/joy-media/src/transport.ts index 788a4c7562..faefba1cc5 100644 --- a/pioneer/packages/joy-media/src/transport.ts +++ b/pioneer/packages/joy-media/src/transport.ts @@ -1,4 +1,3 @@ -import { Transport } from '@polkadot/joy-utils/index'; import { AccountId } from '@polkadot/types/interfaces'; import { EntityId, Class, ClassName, unifyClassName, ClassIdByNameMap } from '@joystream/types/versioned-store'; import { MusicTrackType, MusicTrackCodec } from './schemas/music/MusicTrack'; @@ -18,7 +17,8 @@ import { MediaDropdownOptions } from './common/MediaDropdownOptions'; import { ChannelEntity } from './entities/ChannelEntity'; import { ChannelId } from '@joystream/types/content-working-group'; import { isVideoChannel, isPublicChannel } from './channels/ChannelHelpers'; -import { ValidationConstraint } from '@polkadot/joy-utils/ValidationConstraint'; +import { ValidationConstraint } from '@polkadot/joy-utils/types/ValidationConstraint'; +import { createMock } from '@joystream/types'; export interface ChannelValidationConstraints { handle: ValidationConstraint; @@ -57,10 +57,11 @@ export const EntityCodecByClassNameMap = { function insensitiveEq (text1: string, text2: string): boolean { const prepare = (txt: string) => txt.replace(/[\s]+/mg, '').toLowerCase(); + return prepare(text1) === prepare(text2); } -export abstract class MediaTransport extends Transport { +export abstract class MediaTransport { protected cachedClassIdByNameMap: ClassIdByNameMap | undefined protected sessionId = 0 @@ -83,9 +84,12 @@ export abstract class MediaTransport extends Transport { if (typeof operation !== 'function') { throw new Error('Operation is not a function'); } + this.openSession(); const res = await operation(); + this.closeSession(); + return res; } @@ -93,12 +97,12 @@ export abstract class MediaTransport extends Transport { async channelById (id: ChannelId): Promise { return (await this.allChannels()) - .find(x => id && id.eq(x.id)); + .find((x) => id && id.eq(x.id)); } async channelsByAccount (accountId: AccountId): Promise { return (await this.allChannels()) - .filter(x => accountId && accountId.eq(x.roleAccount)); + .filter((x) => accountId && accountId.eq(x.roleAccount)); } abstract channelValidationConstraints(): Promise @@ -114,12 +118,15 @@ export abstract class MediaTransport extends Transport { if (!this.cachedClassIdByNameMap) { const map: ClassIdByNameMap = {}; const classes = await this.allClasses(); + classes.forEach((c) => { const className = unifyClassName(c.name); + map[className] = c.id; }); this.cachedClassIdByNameMap = map; } + return this.cachedClassIdByNameMap; } @@ -128,23 +135,26 @@ export abstract class MediaTransport extends Transport { async topVideo (): Promise { const content = await this.featuredContent(); const topVideoId = content?.topVideo as unknown as number | undefined; - return !topVideoId ? undefined : await this.videoById(new EntityId(topVideoId)); + + return !topVideoId ? undefined : await this.videoById(createMock('EntityId', topVideoId)); } async featuredVideos (): Promise { const content = await this.featuredContent(); const videoIds = (content?.featuredVideos || []) as unknown as number[]; const videos = await Promise.all(videoIds.map((id) => - this.videoById(new EntityId(id)))); - return videos.filter(x => x !== undefined) as VideoType[]; + this.videoById(createMock('EntityId', id)))); + + return videos.filter((x) => x !== undefined) as VideoType[]; } async featuredAlbums (): Promise { const content = await this.featuredContent(); const albumIds = (content?.featuredAlbums || []) as unknown as EntityId[]; const albums = await Promise.all(albumIds.map((id) => - this.musicAlbumById(new EntityId(id)))); - return albums.filter(x => x !== undefined) as MusicAlbumType[]; + this.musicAlbumById(createMock('EntityId', id)))); + + return albums.filter((x) => x !== undefined) as MusicAlbumType[]; } abstract allMediaObjects(): Promise @@ -157,8 +167,8 @@ export abstract class MediaTransport extends Transport { async videosByChannelId (channelId: ChannelId, limit?: number, additionalFilter?: (x: VideoType) => boolean): Promise { let videos = (await this.allVideos()) - .filter(x => channelId && channelId.eq(x.channelId) && (additionalFilter || (() => true))(x)) - .sort(x => -1 * x.id); + .filter((x) => channelId && channelId.eq(x.channelId) && (additionalFilter || (() => true))(x)) + .sort((x) => -1 * x.id); if (limit && limit > 0) { videos = videos.slice(0, limit); @@ -169,30 +179,30 @@ export abstract class MediaTransport extends Transport { async videosByAccount (accountId: AccountId): Promise { const accountChannels = await this.channelsByAccount(accountId); - const accountChannelIds = new Set(accountChannels.map(x => x.id)); + const accountChannelIds = new Set(accountChannels.map((x) => x.id)); return (await this.allVideos()) - .filter(x => x.channelId && accountChannelIds.has(x.channelId)); + .filter((x) => x.channelId && accountChannelIds.has(x.channelId)); } async mediaObjectById (id: EntityId): Promise { return (await this.allMediaObjects()) - .find(x => id && id.eq(x.id)); + .find((x) => id && id.eq(x.id)); } async videoById (id: EntityId): Promise { return (await this.allVideos()) - .find(x => id && id.eq(x.id)); + .find((x) => id && id.eq(x.id)); } async musicTrackById (id: EntityId): Promise { return (await this.allMusicTracks()) - .find(x => id && id.eq(x.id)); + .find((x) => id && id.eq(x.id)); } async musicAlbumById (id: EntityId): Promise { return (await this.allMusicAlbums()) - .find(x => id && id.eq(x.id)); + .find((x) => id && id.eq(x.id)); } async allPublicChannels (): Promise { @@ -208,7 +218,7 @@ export abstract class MediaTransport extends Transport { async allPublicVideoChannels (): Promise { return (await this.allVideoChannels()) .filter(isPublicChannel) - .sort(x => -1 * x.id); + .sort((x) => -1 * x.id); } async latestPublicVideoChannels (limit = 6): Promise { @@ -217,26 +227,27 @@ export abstract class MediaTransport extends Transport { async allPublicVideos (): Promise { const idOfPublicPS = (await this.allPublicationStatuses()) - .find(x => + .find((x) => insensitiveEq(x.value, 'Public') )?.id; const idsOfCuratedCS = (await this.allCurationStatuses()) - .filter(x => + .filter((x) => insensitiveEq(x.value, 'Under review') || insensitiveEq(x.value, 'Removed') - ).map(x => x.id); + ).map((x) => x.id); const isPublicAndNotCurated = (video: VideoType) => { const isPublic = video.publicationStatus.id === idOfPublicPS; const isNotCurated = !idsOfCuratedCS.includes(video.curationStatus?.id || -1); const isPubChannel = video.channel ? isPublicChannel(video.channel) : true; + return isPublic && isNotCurated && isPubChannel; }; return (await this.allVideos()) .filter(isPublicAndNotCurated) - .sort(x => -1 * x.id); + .sort((x) => -1 * x.id); } async latestPublicVideos (limit = 12): Promise { @@ -285,6 +296,7 @@ export abstract class MediaTransport extends Transport { const res = new MediaDropdownOptions( await this.allInternalEntities() ); + // console.log('Transport.dropdownOptions', res) return res; } diff --git a/pioneer/packages/joy-media/src/upload/EditVideo.view.tsx b/pioneer/packages/joy-media/src/upload/EditVideo.view.tsx index 4bcba0a122..f58c793402 100644 --- a/pioneer/packages/joy-media/src/upload/EditVideo.view.tsx +++ b/pioneer/packages/joy-media/src/upload/EditVideo.view.tsx @@ -2,9 +2,8 @@ import React from 'react'; import { RouteComponentProps } from 'react-router'; import { MediaView } from '../MediaView'; import { OuterProps, EditForm } from './UploadVideo'; -import EntityId from '@joystream/types/versioned-store/EntityId'; -import { ChannelId } from '@joystream/types/content-working-group'; -import { JoyError } from '@polkadot/joy-utils/JoyStatus'; +import { JoyError } from '@polkadot/joy-utils/react/components'; +import { useApi } from '@polkadot/react-hooks'; type Props = OuterProps; @@ -19,18 +18,20 @@ export const EditVideoView = MediaView({ const entityClass = await transport.videoClass(); const entity = id && await transport.videoById(id); const opts = await transport.dropdownOptions(); + return { channel, mediaObjectClass, entityClass, entity, opts }; } }); -type WithRouterProps = Props & RouteComponentProps +type WithRouterProps = Props & RouteComponentProps> export const UploadVideoWithRouter = (props: WithRouterProps) => { const { match: { params: { channelId } } } = props; + const { api } = useApi(); if (channelId) { try { - return ; + return ; } catch (err) { console.log('UploadVideoWithRouter failed:', err); } @@ -41,10 +42,11 @@ export const UploadVideoWithRouter = (props: WithRouterProps) => { export const EditVideoWithRouter = (props: WithRouterProps) => { const { match: { params: { id } } } = props; + const { api } = useApi(); if (id) { try { - return ; + return ; } catch (err) { console.log('EditVideoWithRouter failed:', err); } diff --git a/pioneer/packages/joy-media/src/upload/UploadAudio.tsx b/pioneer/packages/joy-media/src/upload/UploadAudio.tsx index 331ad77f5f..f74ab1a28f 100644 --- a/pioneer/packages/joy-media/src/upload/UploadAudio.tsx +++ b/pioneer/packages/joy-media/src/upload/UploadAudio.tsx @@ -3,9 +3,9 @@ import { Button, Tab } from 'semantic-ui-react'; import { Form, withFormik } from 'formik'; import { History } from 'history'; -import TxButton from '@polkadot/joy-utils/TxButton'; +import { TxButton } from '@polkadot/joy-utils/react/components'; import { ContentId } from '@joystream/types/media'; -import { onImageError } from '@polkadot/joy-utils/images'; +import { onImageError } from '../common/images'; import { MusicTrackValidationSchema, MusicTrackType, MusicTrackClass as Fields, MusicTrackFormValues, MusicTrackToFormValues } from '../schemas/music/MusicTrack'; import { withMediaForm, MediaFormProps, datePlaceholder } from '../common/MediaForms'; import EntityId from '@joystream/types/versioned-store/EntityId'; @@ -113,7 +113,6 @@ const InnerForm = (props: MediaFormProps) => { const renderMainButton = () => ({ mapPropsToValues: (props): FormValues => { const { entity, fileName } = props; const res = MusicTrackToFormValues(entity); + if (!res.title && fileName) { res.title = fileName; } + return res; }, diff --git a/pioneer/packages/joy-media/src/upload/UploadAudio.view.tsx b/pioneer/packages/joy-media/src/upload/UploadAudio.view.tsx index b83a3e4a76..592c1186a3 100644 --- a/pioneer/packages/joy-media/src/upload/UploadAudio.view.tsx +++ b/pioneer/packages/joy-media/src/upload/UploadAudio.view.tsx @@ -7,6 +7,7 @@ export const UploadAudioView = MediaView({ const { transport, id } = props; const entity = id ? await transport.musicTrackById(id) : undefined; const opts = await transport.dropdownOptions(); + return { entity, opts }; } }); diff --git a/pioneer/packages/joy-media/src/upload/UploadVideo.tsx b/pioneer/packages/joy-media/src/upload/UploadVideo.tsx index ad0bfceb58..3aae159461 100644 --- a/pioneer/packages/joy-media/src/upload/UploadVideo.tsx +++ b/pioneer/packages/joy-media/src/upload/UploadVideo.tsx @@ -4,9 +4,10 @@ import { Form, withFormik } from 'formik'; import { History } from 'history'; import moment from 'moment'; -import TxButton, { OnTxButtonClick } from '@polkadot/joy-utils/TxButton'; +import TxButton, { OnTxButtonClick } from '@polkadot/joy-utils/react/components/TxButton'; import { ContentId } from '@joystream/types/media'; -import { onImageError } from '@polkadot/joy-utils/images'; +import { onImageError } from '../common/images'; +import { nonEmptyStr, filterSubstrateEventsAndExtractData } from '@polkadot/joy-utils/functions/misc'; import { VideoValidationSchema, VideoType, VideoClass as Fields, VideoFormValues, VideoToFormValues, VideoCodec, VideoPropId } from '../schemas/video/Video'; import { MediaFormProps, withMediaForm, datePlaceholder } from '../common/MediaForms'; import EntityId from '@joystream/types/versioned-store/EntityId'; @@ -14,23 +15,20 @@ import { MediaDropdownOptions } from '../common/MediaDropdownOptions'; import { FormTabs } from '../common/FormTabs'; import { ChannelId } from '@joystream/types/content-working-group'; import { ChannelEntity } from '../entities/ChannelEntity'; -import { Credential } from '@joystream/types/common'; import { Class, VecClassPropertyValue } from '@joystream/types/versioned-store'; import { TxCallback } from '@polkadot/react-components/Status/types'; import { SubmittableResult } from '@polkadot/api'; -import { nonEmptyStr, filterSubstrateEventsAndExtractData } from '@polkadot/joy-utils/index'; -import { u16, u32, bool, Option, Vec } from '@polkadot/types'; import { isInternalProp } from '@joystream/types/versioned-store/EntityCodec'; import { MediaObjectCodec } from '../schemas/general/MediaObject'; -import { Operation } from '@joystream/types/versioned-store/permissions/batching'; import { OperationType } from '@joystream/types/versioned-store/permissions/batching/operation-types'; import { ParametrizedEntity } from '@joystream/types/versioned-store/permissions/batching/parametrized-entity'; import ParametrizedClassPropertyValue from '@joystream/types/versioned-store/permissions/batching/ParametrizedClassPropertyValue'; import { ParametrizedPropertyValue } from '@joystream/types/versioned-store/permissions/batching/parametrized-property-value'; import { ParameterizedClassPropertyValues } from '@joystream/types/versioned-store/permissions/batching/operations'; -import { useMyMembership } from '@polkadot/joy-utils/MyMembershipContext'; +import { useMyMembership } from '@polkadot/joy-utils/react/hooks'; import { isAccountAChannelOwner } from '../channels/ChannelHelpers'; -import { JoyError } from '@polkadot/joy-utils/JoyStatus'; +import { JoyError } from '@polkadot/joy-utils/react/components'; +import { useApi } from '@polkadot/react-hooks'; /** Example: "2019-01-23" -> 1548201600 */ function humanDateToUnixTs (humanFriendlyDate: string): number | undefined { @@ -87,6 +85,7 @@ const InnerForm = (props: MediaFormProps) => { } = props; const { myAccountId } = useMyMembership(); + const { api } = useApi(); const { thumbnail } = values; @@ -103,9 +102,9 @@ const InnerForm = (props: MediaFormProps) => { } // Next consts are used in tx params: - const with_credential = new Option(Credential, new Credential(2)); - const as_entity_maintainer = new bool(false); - const schema_id = new u16(0); + const with_credential = api.createType('Option', 2); + const as_entity_maintainer = api.createType('bool', false); + const schema_id = api.createType('u16', 0); const entityCodec = new VideoCodec(entityClass); const mediaObjectCodec = new MediaObjectCodec(mediaObjectClass); @@ -116,9 +115,10 @@ const InnerForm = (props: MediaFormProps) => { Object.keys(values).forEach((prop) => { const fieldName = prop as VideoPropId; const field = Fields[fieldName]; - let fieldValue = values[fieldName] as any; + let fieldValue = values[fieldName]; let shouldIncludeValue = true; + if (entity) { // If we updating existing entity, then update only changed props: shouldIncludeValue = isFieldChanged(fieldName); @@ -145,35 +145,41 @@ const InnerForm = (props: MediaFormProps) => { if (typeof fieldValue === 'string') { fieldValue = fieldValue.trim(); } + if (isDateField(fieldName)) { - fieldValue = humanDateToUnixTs(fieldValue); + fieldValue = typeof fieldValue === 'string' ? (humanDateToUnixTs(fieldValue) || '') : ''; } - res[fieldName] = fieldValue; + + // FIXME: Temporary solution, since fixing this would require a bigger refactorization + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + res[fieldName] = fieldValue as any; } }); return res; }; - const indexOfCreateMediaObjectOperation = new u32(0); + const indexOfCreateMediaObjectOperation = api.createType('u32', 0); - const indexOfCreateVideoEntityOperation = new u32(2); + const indexOfCreateVideoEntityOperation = api.createType('u32', 2); const referToIdOfCreatedMediaObjectEntity = () => - ParametrizedEntity.InternalEntityJustAdded(indexOfCreateMediaObjectOperation); + ParametrizedEntity.InternalEntityJustAdded(api.registry, indexOfCreateMediaObjectOperation); const referToIdOfCreatedVideoEntity = () => - ParametrizedEntity.InternalEntityJustAdded(indexOfCreateVideoEntityOperation); + ParametrizedEntity.InternalEntityJustAdded(api.registry, indexOfCreateVideoEntityOperation); const newlyCreatedMediaObjectProp = () => { const inClassIndexOfMediaObject = entityCodec.inClassIndexOfProp(Fields.object.id); + if (!inClassIndexOfMediaObject) { throw new Error('Cannot not find an in-class index of "object" prop on Video entity.'); } - return new ParametrizedClassPropertyValue({ - in_class_index: new u16(inClassIndexOfMediaObject), + return api.createType('ParametrizedClassPropertyValue', { + in_class_index: inClassIndexOfMediaObject, value: ParametrizedPropertyValue.InternalEntityJustAdded( + api.registry, indexOfCreateMediaObjectOperation ) }); @@ -183,23 +189,24 @@ const InnerForm = (props: MediaFormProps) => { props: VecClassPropertyValue, extra: ParametrizedClassPropertyValue[] = [] ): ParameterizedClassPropertyValues => { - const parametrizedProps = props.map(p => { + const parametrizedProps = props.map((p) => { const { in_class_index, value } = p; - return new ParametrizedClassPropertyValue({ + + return api.createType('ParametrizedClassPropertyValue', { in_class_index, - value: new ParametrizedPropertyValue({ PropertyValue: value }) + value: api.createType('ParametrizedPropertyValue', { PropertyValue: value }) }); }); if (extra && extra.length) { - extra.forEach(x => parametrizedProps.push(x)); + extra.forEach((x) => parametrizedProps.push(x)); } - return new ParameterizedClassPropertyValues(parametrizedProps); + return api.createType('Vec', parametrizedProps); }; const newEntityOperation = (operation_type: OperationType) => { - return new Operation({ + return api.createType('Operation', { with_credential, as_entity_maintainer, operation_type @@ -209,6 +216,7 @@ const InnerForm = (props: MediaFormProps) => { const prepareTxParamsForCreateMediaObject = () => { return newEntityOperation( OperationType.CreateEntity( + api.registry, mediaObjectClass.id ) ); @@ -224,6 +232,7 @@ const InnerForm = (props: MediaFormProps) => { return newEntityOperation( OperationType.AddSchemaSupportToEntity( + api.registry, referToIdOfCreatedMediaObjectEntity(), schema_id, propValues @@ -234,6 +243,7 @@ const InnerForm = (props: MediaFormProps) => { const prepareTxParamsForCreateEntity = () => { return newEntityOperation( OperationType.CreateEntity( + api.registry, entityClass.id ) ); @@ -249,6 +259,7 @@ const InnerForm = (props: MediaFormProps) => { return newEntityOperation( OperationType.AddSchemaSupportToEntity( + api.registry, referToIdOfCreatedVideoEntity(), schema_id, propValues @@ -272,7 +283,7 @@ const InnerForm = (props: MediaFormProps) => { // Use for debug: // console.log('Batch entity operations:', ops) - return [new Vec(Operation, ops)]; + return [api.createType('Vec', ops)]; }; const buildUpdateEntityTxParams = () => { @@ -292,6 +303,7 @@ const InnerForm = (props: MediaFormProps) => { const redirectToPlaybackPage = (newEntityId?: EntityId) => { const entityId = newEntityId || id; + if (history && entityId) { history.push('/media/videos/' + entityId.toString()); } @@ -312,6 +324,7 @@ const InnerForm = (props: MediaFormProps) => { // Extract id from from event: const newId = videoEntityCreatedEvent[0] as EntityId; + console.log('New video entity id:', newId && newId.toString()); redirectToPlaybackPage(newId); @@ -376,7 +389,6 @@ const InnerForm = (props: MediaFormProps) => { const renderTransactionButton = () => ) => { const renderUpdateEntityButton = () => ({ mapPropsToValues: (props): FormValues => { const { entity, channelId, fileName } = props; const res = VideoToFormValues(entity); + if (!res.title && fileName) { res.title = fileName; } + if (channelId) { res.channelId = channelId.toNumber(); } + return res; }, diff --git a/pioneer/packages/joy-media/src/utils.ts b/pioneer/packages/joy-media/src/utils.ts index 91d876568f..c2d4935742 100644 --- a/pioneer/packages/joy-media/src/utils.ts +++ b/pioneer/packages/joy-media/src/utils.ts @@ -1,4 +1,5 @@ export function fileNameWoExt (fileName: string): string { const lastDotIdx = fileName.lastIndexOf('.'); + return fileName.substring(0, lastDotIdx); } diff --git a/pioneer/packages/joy-media/src/video/PlayVideo.tsx b/pioneer/packages/joy-media/src/video/PlayVideo.tsx index 757b2b6dd7..d1dd34357f 100644 --- a/pioneer/packages/joy-media/src/video/PlayVideo.tsx +++ b/pioneer/packages/joy-media/src/video/PlayVideo.tsx @@ -12,7 +12,8 @@ import { printExplicit, printReleaseDate, printLanguage } from '../entities/Enti import { MediaObjectType } from '../schemas/general/MediaObject'; import { MediaPlayerWithResolver } from '../common/MediaPlayerWithResolver'; import { ContentId } from '@joystream/types/media'; -import { JoyError } from '@polkadot/joy-utils/JoyStatus'; +import { JoyError } from '@polkadot/joy-utils/react/components'; +import { useApi } from '@polkadot/react-hooks'; export type PlayVideoProps = { channel?: ChannelEntity; @@ -29,6 +30,7 @@ type ListOfVideoPreviewProps = { function VertialListOfVideoPreviews (props: ListOfVideoPreviewProps) { const { videos = [] } = props; + return <>{videos.map((video) => )}; @@ -36,6 +38,7 @@ function VertialListOfVideoPreviews (props: ListOfVideoPreviewProps) { export function PlayVideo (props: PlayVideoProps) { const { channel, mediaObject, video, moreChannelVideos = [], featuredVideos = [] } = props; + const { api } = useApi(); if (!mediaObject || !video) { return ; @@ -80,7 +83,7 @@ export function PlayVideo (props: PlayVideoProps) { // TODO show video only to its owner, if the video is not public. // see isPublicVideo() function. - const contentId = ContentId.decode(mediaObject.value); + const contentId = ContentId.decode(api.registry, mediaObject.value); // console.log('PlayVideo: props', props) diff --git a/pioneer/packages/joy-media/src/video/PlayVideo.view.tsx b/pioneer/packages/joy-media/src/video/PlayVideo.view.tsx index 501abcd8ae..84ba65408d 100644 --- a/pioneer/packages/joy-media/src/video/PlayVideo.view.tsx +++ b/pioneer/packages/joy-media/src/video/PlayVideo.view.tsx @@ -2,9 +2,8 @@ import React from 'react'; import { RouteComponentProps } from 'react-router'; import { MediaView } from '../MediaView'; import { PlayVideoProps, PlayVideo } from './PlayVideo'; -import { ChannelId } from '@joystream/types/content-working-group'; -import { EntityId } from '@joystream/types/versioned-store'; -import { JoyError } from '@polkadot/joy-utils/JoyStatus'; +import { JoyError } from '@polkadot/joy-utils/react/components'; +import { useApi } from '@polkadot/react-hooks'; type Props = PlayVideoProps; @@ -12,14 +11,15 @@ export const PlayVideoView = MediaView({ component: PlayVideo, triggers: ['id'], resolveProps: async (props) => { - const { transport, id } = props; + const { transport, api, id } = props; const video = await transport.videoById(id); + if (!video) return {}; - const channelId = new ChannelId(video.channelId); + const channelId = api.createType('ChannelId', video.channelId); const channel = await transport.channelById(channelId); - const moreChannelVideos = (await transport.videosByChannelId(channelId, 5, x => x.id !== video.id)); + const moreChannelVideos = (await transport.videosByChannelId(channelId, 5, (x) => x.id !== video.id)); const featuredVideos = await transport.featuredVideos(); const mediaObject = video.object; @@ -27,12 +27,13 @@ export const PlayVideoView = MediaView({ } }); -export const PlayVideoWithRouter = (props: Props & RouteComponentProps) => { +export const PlayVideoWithRouter = (props: Props & RouteComponentProps>) => { const { match: { params: { id } } } = props; + const { api } = useApi(); if (id) { try { - return ; + return ; } catch (err) { console.log('PlayVideoWithRouter failed:', err); } diff --git a/pioneer/packages/joy-media/src/video/VideoPreview.tsx b/pioneer/packages/joy-media/src/video/VideoPreview.tsx index f07a559696..53f723c115 100644 --- a/pioneer/packages/joy-media/src/video/VideoPreview.tsx +++ b/pioneer/packages/joy-media/src/video/VideoPreview.tsx @@ -2,7 +2,7 @@ import React, { CSSProperties } from 'react'; import { Link } from 'react-router-dom'; import { BgImg } from '../common/BgImg'; import { VideoType } from '../schemas/video/Video'; -import { useMyMembership } from '@polkadot/joy-utils/MyMembershipContext'; +import { useMyMembership } from '@polkadot/joy-utils/react/hooks'; import { ChannelEntity } from '../entities/ChannelEntity'; import { isAccountAChannelOwner } from '../channels/ChannelHelpers'; import { ChannelAvatarAndName } from '../channels/ChannelAvatarAndName'; @@ -64,7 +64,7 @@ export function VideoPreview (props: VideoPreviewProps) { } {iAmOwner && -
+
Edit @@ -77,7 +77,7 @@ export function VideoPreview (props: VideoPreviewProps) { } export function toVideoPreviews (items: VideoType[]): VideoPreviewProps[] { - return items.map(x => ({ + return items.map((x) => ({ id: x.id, title: x.title, thumbnail: x.thumbnail diff --git a/pioneer/packages/joy-members/src/EditForm.tsx b/pioneer/packages/joy-members/src/EditForm.tsx index 9d0c229173..58525d9ba4 100644 --- a/pioneer/packages/joy-members/src/EditForm.tsx +++ b/pioneer/packages/joy-members/src/EditForm.tsx @@ -172,7 +172,6 @@ const InnerForm = (props: FormProps) => {
NONE; } return ( - + ); } -function ProposedMember (props: { memberId?: MemberId | number | null }) { - if (props.memberId === null || props.memberId === undefined) { - return <>NONE; - } - const memberId: MemberId | number = props.memberId; - +function ProposedMember (props: { memberId: MemberId | number }) { const transport = useTransport(); + const [member, error, loading] = usePromise( - () => transport.members.membershipById(memberId), + () => transport.members.membershipById(props.memberId), null ); return ( - - { (member && !member.handle.isEmpty) ? ( + + { (member && !member.isEmpty) ? ( ParsedParam[]} = { - Text: ([content]) => [ +const paramParsers: { [k in ProposalType]: (params: SpecificProposalDetails) => ParsedParam[]} = { + Text: (content) => [ new ParsedParam( 'Content', - , + , true ) ], RuntimeUpgrade: ([hash, filesize]) => [ new ParsedParam('Blake2b256 hash of WASM code', hash, true), - new ParsedParam('File size', filesize + ' bytes') + new ParsedParam('File size', `${filesize} bytes`) ], - SetElectionParameters: ([params]) => [ - new ParsedParam('Announcing period', params.announcing_period + ' blocks'), - new ParsedParam('Voting period', params.voting_period + ' blocks'), - new ParsedParam('Revealing period', params.revealing_period + ' blocks'), - new ParsedParam('Council size', params.council_size + ' members'), - new ParsedParam('Candidacy limit', params.candidacy_limit + ' members'), - new ParsedParam('New term duration', params.new_term_duration + ' blocks'), + SetElectionParameters: (params) => [ + new ParsedParam('Announcing period', `${params.announcing_period.toString()} blocks`), + new ParsedParam('Voting period', `${params.voting_period.toString()} blocks`), + new ParsedParam('Revealing period', `${params.revealing_period.toString()} blocks`), + new ParsedParam('Council size', `${params.council_size.toString()} members`), + new ParsedParam('Candidacy limit', `${params.candidacy_limit.toString()} members`), + new ParsedParam('New term duration', `${params.new_term_duration.toString()} blocks`), new ParsedParam('Min. council stake', formatBalance(params.min_council_stake)), new ParsedParam('Min. voting stake', formatBalance(params.min_voting_stake)) ], Spending: ([amount, account]) => [ - new ParsedParam('Amount', formatBalance(amount)), - new ParsedParam('Account', ) + new ParsedParam('Amount', formatBalance(amount as Balance)), + new ParsedParam('Account', ) ], - SetLead: ([memberId, accountId]) => [ - new ParsedParam('Member', ), - new ParsedParam('Account id', ) + SetLead: (params) => [ + new ParsedParam( + 'Member', + params.isSome ? : 'NONE' + ), + new ParsedParam('Account id', ) ], - SetContentWorkingGroupMintCapacity: ([capacity]) => [ + SetContentWorkingGroupMintCapacity: (capacity) => [ new ParsedParam('Mint capacity', formatBalance(capacity)) ], - EvictStorageProvider: ([accountId]) => [ - new ParsedParam('Storage provider account', ) + EvictStorageProvider: (accountId) => [ + new ParsedParam('Storage provider account', ) ], - SetValidatorCount: ([count]) => [ - new ParsedParam('Validator count', count) + SetValidatorCount: (count) => [ + new ParsedParam('Validator count', count.toString()) ], - SetStorageRoleParameters: ([params]) => [ + SetStorageRoleParameters: (params) => [ new ParsedParam('Min. stake', formatBalance(params.min_stake)), // "Min. actors": params.min_actors, - new ParsedParam('Max. actors', params.max_actors), + new ParsedParam('Max. actors', params.max_actors.toString()), new ParsedParam('Reward', formatBalance(params.reward)), - new ParsedParam('Reward period', params.reward_period + ' blocks'), + new ParsedParam('Reward period', `${params.reward_period.toString()} blocks`), // "Bonding period": params.bonding_period + " blocks", - new ParsedParam('Unbonding period', params.unbonding_period + ' blocks'), + new ParsedParam('Unbonding period', `${params.unbonding_period.toString()} blocks`), // "Min. service period": params.min_service_period + " blocks", // "Startup grace period": params.startup_grace_period + " blocks", new ParsedParam('Entry request fee', formatBalance(params.entry_request_fee)) ], - AddWorkingGroupLeaderOpening: ([{ activate_at, commitment, human_readable_text, working_group }]) => { - const workingGroup = new WorkingGroup(working_group); - const activateAt = new ActivateOpeningAt(activate_at); - const activateAtBlock = activateAt.type === ActivateOpeningAtKeys.ExactBlock ? activateAt.value : null; - const OPCommitment = new WorkingGroupOpeningPolicyCommitment(commitment); + AddWorkingGroupLeaderOpening: ({ + activate_at: activateAt, + commitment, + human_readable_text: humanReadableText, + working_group: workingGroup + }) => { + const activateAtBlock = activateAt.isOfType('ExactBlock') ? activateAt.asType('ExactBlock') : null; const { application_staking_policy: applicationSP, role_staking_policy: roleSP, application_rationing_policy: rationingPolicy - } = OPCommitment; - let HRT = bytesToString(new Bytes(human_readable_text)); + } = commitment; + let HRT = bytesToString(humanReadableText); + try { HRT = JSON.stringify(JSON.parse(HRT), undefined, 4); } catch (e) { /* Do nothing */ } + const formatStake = (stake: Option) => ( - stake.isSome ? stake.unwrap().amount_mode.type + `(${stake.unwrap().amount})` : 'NONE' + stake.isSome ? stake.unwrap().amount_mode.type + `(${stake.unwrap().amount.toString()})` : 'NONE' ); const formatPeriod = (unstakingPeriod: Option) => ( - unstakingPeriod.unwrapOr(0) + ' blocks' + `${unstakingPeriod.unwrapOr(new BN(0)).toString()} blocks` ); + return [ new ParsedParam('Working group', workingGroup.type), new ParsedParam('Activate at', `${activateAt.type}${activateAtBlock ? `(${activateAtBlock.toString()})` : ''}`), @@ -177,34 +183,35 @@ const paramParsers: { [x in ProposalType]: (params: any[]) => ParsedParam[]} = { 'Max. applications', rationingPolicy.isSome ? rationingPolicy.unwrap().max_active_applicants.toNumber() : 'UNLIMITED' ), + new ParsedParam('Max. review period length', `${commitment.max_review_period_length.toString()} blocks`), new ParsedParam( 'Terminate unstaking period (role stake)', - formatPeriod(OPCommitment.terminate_role_stake_unstaking_period) + formatPeriod(commitment.terminate_role_stake_unstaking_period) ), new ParsedParam( 'Exit unstaking period (role stake)', - formatPeriod(OPCommitment.exit_role_stake_unstaking_period) + formatPeriod(commitment.exit_role_stake_unstaking_period) ), // new ParsedParam( 'Terminate unstaking period (appl. stake)', - formatPeriod(OPCommitment.terminate_application_stake_unstaking_period) + formatPeriod(commitment.terminate_application_stake_unstaking_period) ), new ParsedParam( 'Exit unstaking period (appl. stake)', - formatPeriod(OPCommitment.exit_role_application_stake_unstaking_period) + formatPeriod(commitment.exit_role_application_stake_unstaking_period) ), new ParsedParam( 'Appl. accepted unstaking period (appl. stake)', - formatPeriod(OPCommitment.fill_opening_successful_applicant_application_stake_unstaking_period) + formatPeriod(commitment.fill_opening_successful_applicant_application_stake_unstaking_period) ), new ParsedParam( 'Appl. failed unstaking period (role stake)', - formatPeriod(OPCommitment.fill_opening_failed_applicant_role_stake_unstaking_period) + formatPeriod(commitment.fill_opening_failed_applicant_role_stake_unstaking_period) ), new ParsedParam( 'Appl. failed unstaking period (appl. stake)', - formatPeriod(OPCommitment.fill_opening_failed_applicant_application_stake_unstaking_period) + formatPeriod(commitment.fill_opening_failed_applicant_application_stake_unstaking_period) ), new ParsedParam( 'Crowded out unstaking period (role stake)', @@ -227,55 +234,76 @@ const paramParsers: { [x in ProposalType]: (params: any[]) => ParsedParam[]} = { ]; }, SetWorkingGroupMintCapacity: ([capacity, group]) => [ - new ParsedParam('Working group', (new WorkingGroup(group)).type), - new ParsedParam('Mint capacity', formatBalance(capacity)) + new ParsedParam('Working group', (group as WorkingGroup).type), + new ParsedParam('Mint capacity', formatBalance((capacity as Balance))) ], BeginReviewWorkingGroupLeaderApplication: ([id, group]) => [ - new ParsedParam('Working group', (new WorkingGroup(group)).type), + new ParsedParam('Working group', (group as WorkingGroup).type), // TODO: Adjust the link to work with multiple groups after working-groups are normalized! - new ParsedParam('Opening id', #{id}) + new ParsedParam( + 'Opening id', + #{id.toString()} + ) + ], + FillWorkingGroupLeaderOpening: ({ + opening_id: openingId, + successful_application_id: succesfulApplicationId, + reward_policy: rewardPolicy, + working_group: workingGroup + }) => [ + new ParsedParam('Working group', workingGroup.type), + // TODO: Adjust the link to work with multiple groups after working-groups are normalized! + new ParsedParam( + 'Opening id', + #{openingId.toString()}), + new ParsedParam('Reward policy', rewardPolicy.isSome ? formatReward(rewardPolicy.unwrap(), true) : 'NONE'), + new ParsedParam( + 'Result', + , + true + ) ], - FillWorkingGroupLeaderOpening: ([params]) => { - const { opening_id, successful_application_id, reward_policy, working_group } = params; - const rewardPolicy = reward_policy && new RewardPolicy(reward_policy); - return [ - new ParsedParam('Working group', (new WorkingGroup(working_group)).type), - // TODO: Adjust the link to work with multiple groups after working-groups are normalized! - new ParsedParam('Opening id', #{opening_id}), - new ParsedParam('Reward policy', rewardPolicy ? formatReward(rewardPolicy, true) : 'NONE'), - new ParsedParam( - 'Result', - , - true - ) - ]; - }, SlashWorkingGroupLeaderStake: ([leadId, amount, group]) => [ - new ParsedParam('Working group', (new WorkingGroup(group)).type), - new ParsedParam('Slash amount', formatBalance(amount)), - new ParsedParam('Lead', , true) + new ParsedParam('Working group', (group as WorkingGroup).type), + new ParsedParam('Slash amount', formatBalance(amount as Balance)), + new ParsedParam( + 'Lead', + , + true + ) ], DecreaseWorkingGroupLeaderStake: ([leadId, amount, group]) => [ - new ParsedParam('Working group', (new WorkingGroup(group)).type), - new ParsedParam('Decrease amount', formatBalance(amount)), - new ParsedParam('Lead', , true) + new ParsedParam('Working group', (group as WorkingGroup).type), + new ParsedParam('Decrease amount', formatBalance(amount as Balance)), + new ParsedParam( + 'Lead', + , + true + ) ], SetWorkingGroupLeaderReward: ([leadId, amount, group]) => [ - new ParsedParam('Working group', (new WorkingGroup(group)).type), - new ParsedParam('New reward amount', formatBalance(amount)), - new ParsedParam('Lead', , true) + new ParsedParam('Working group', (group as WorkingGroup).type), + new ParsedParam('New reward amount', formatBalance(amount as Balance)), + new ParsedParam( + 'Lead', + , + true + ) ], - TerminateWorkingGroupLeaderRole: ([params]) => { - const paramsObj = new TerminateRoleParameters(params); - const { working_group: workingGroup, rationale, worker_id: leadId, slash } = paramsObj; + TerminateWorkingGroupLeaderRole: ({ + working_group: workingGroup, + rationale, + worker_id: leadId, + slash + }) => { return [ new ParsedParam('Working group', workingGroup.type), new ParsedParam('Rationale', bytesToString(rationale), true), new ParsedParam('Slash stake', slash.isTrue ? 'YES' : 'NO'), - new ParsedParam('Lead', , true) + new ParsedParam('Lead', , true) ]; } }; @@ -305,6 +333,7 @@ const ParamsHeader = styled.h4` padding: 0.3rem; left: 0.5rem; `; + type ProposalParamProps = { fullWidth?: boolean }; const ProposalParam = ({ fullWidth, children }: React.PropsWithChildren) => (
@@ -330,20 +359,26 @@ export default function Body ({ type, title, description, - params = [], + params, iAmProposer, proposalId, proposerId, isCancellable, cancellationFee }: BodyProps) { - const parseParams = paramParsers[type]; - const parsedParams = parseParams(params); + // Assert more generic type (since TypeScript cannot possibly know the value of "type" here yet) + const parseParams = paramParsers[type] as (params: SpecificProposalDetails) => ParsedParam[]; + const parsedParams = parseParams( + type === 'RuntimeUpgrade' + ? params as RuntimeUpgradeProposalDetails + : (params as ProposalDetails).asType(type) + ); + return ( -
{title}
+
{title}
@@ -368,20 +403,20 @@ export default function Body ({ The cancellation fee for this type of proposal is:  { cancellationFee ? formatBalance(cancellationFee) : 'NONE' }

- - { sendTx(); } } - className={'icon left labeled'} - > - - Withdraw proposal - - + { sendTx(); } } + icon + color={ 'red' } + labelPosition={ 'left' } + > + + Withdraw proposal + - ) } + ) }
); diff --git a/pioneer/packages/joy-proposals/src/Proposal/ChooseProposalType.tsx b/pioneer/packages/joy-proposals/src/Proposal/ChooseProposalType.tsx index fbd8194ab6..09645da010 100644 --- a/pioneer/packages/joy-proposals/src/Proposal/ChooseProposalType.tsx +++ b/pioneer/packages/joy-proposals/src/Proposal/ChooseProposalType.tsx @@ -4,7 +4,7 @@ import { Item, Dropdown } from 'semantic-ui-react'; import { useTransport, usePromise } from '@polkadot/joy-utils/react/hooks'; import { Categories } from '@polkadot/joy-utils/types/proposals'; -import { PromiseComponent } from '@polkadot/joy-utils/react/components'; +import PromiseComponent from '@polkadot/joy-utils/react/components/PromiseComponent'; import './ChooseProposalType.css'; import { RouteComponentProps } from 'react-router-dom'; @@ -15,12 +15,12 @@ export default function ChooseProposalType (props: RouteComponentProps) { const [category, setCategory] = useState(''); return ( -
+
-
+
({ value: category, text: category }))} + placeholder='Category' + options={Object.values(Categories).map((category) => ({ value: category, text: category }))} value={category} onChange={(e, data) => setCategory((data.value || '').toString())} clearable @@ -29,9 +29,9 @@ export default function ChooseProposalType (props: RouteComponentProps) {
{proposalTypes - .filter(typeInfo => (!category || typeInfo.category === category) && !typeInfo.outdated) - .map((typeInfo, idx) => ( - + .filter((typeInfo) => (!category || typeInfo.category === category) && !typeInfo.outdated) + .map((typeInfo) => ( + ))} diff --git a/pioneer/packages/joy-proposals/src/Proposal/Details.tsx b/pioneer/packages/joy-proposals/src/Proposal/Details.tsx index 3289f0037b..328431064d 100644 --- a/pioneer/packages/joy-proposals/src/Proposal/Details.tsx +++ b/pioneer/packages/joy-proposals/src/Proposal/Details.tsx @@ -5,7 +5,7 @@ import { metadata as proposalConsts } from '@polkadot/joy-utils/consts/proposals import { ExtendedProposalStatus } from './ProposalDetails'; import styled from 'styled-components'; -import ProfilePreview from '@polkadot/joy-utils/MemberProfilePreview'; +import ProfilePreview from '@polkadot/joy-utils/react/components/MemberProfilePreview'; const DetailsContainer = styled(Item.Group)` display: grid; @@ -57,7 +57,7 @@ const Detail: React.FunctionComponent = ({ name, value, children }) { name }: - { value &&
{value}
} + { value &&
{value}
} { children }
@@ -72,9 +72,10 @@ type DetailsProps = { export default function Details ({ proposal, extendedStatus, proposerLink = false }: DetailsProps) { const { type, createdAt, createdAtBlock, proposer } = proposal; const { displayStatus, periodStatus, expiresIn, finalizedAtBlock, executedAtBlock, executionFailReason } = extendedStatus; + return ( - + { `${createdAt.toLocaleString()}` } - - { proposalConsts[type].outdated && } + + { proposalConsts[type].outdated && } - + { createdAtBlock && Created at block #{ createdAtBlock } } { finalizedAtBlock && Finalized at block #{ finalizedAtBlock } } @@ -98,13 +99,13 @@ export default function Details ({ proposal, extendedStatus, proposerLink = fals ) } - { (periodStatus !== null) && } + { (periodStatus !== null) && } {expiresIn !== null && ( ) } - {executionFailReason && } + {executionFailReason && } ); } diff --git a/pioneer/packages/joy-proposals/src/Proposal/ProposalDetails.tsx b/pioneer/packages/joy-proposals/src/Proposal/ProposalDetails.tsx index 075f87843a..bb419b680f 100644 --- a/pioneer/packages/joy-proposals/src/Proposal/ProposalDetails.tsx +++ b/pioneer/packages/joy-proposals/src/Proposal/ProposalDetails.tsx @@ -4,13 +4,11 @@ import Details from './Details'; import Body from './Body'; import VotingSection from './VotingSection'; import Votes from './Votes'; -import { MyAccountProps, withMyAccount } from '@polkadot/joy-utils/MyAccount'; +import { MyAccountProps, withMyAccount } from '@polkadot/joy-utils/react/hocs/accounts'; import { ParsedProposal } from '@polkadot/joy-utils/types/proposals'; import { withCalls } from '@polkadot/react-api'; -import { withMulti } from '@polkadot/react-api/with'; - -import './Proposal.css'; -import { ProposalId, ProposalDecisionStatuses, ApprovedProposalStatuses, ExecutionFailedStatus } from '@joystream/types/proposals'; +import { withMulti } from '@polkadot/react-api/hoc'; +import { ProposalId, ProposalDecisionStatuses, ApprovedProposalStatuses } from '@joystream/types/proposals'; import { BlockNumber } from '@polkadot/types/interfaces'; import { MemberId } from '@joystream/types/members'; import { Seat } from '@joystream/types/council'; @@ -59,7 +57,7 @@ export type ExtendedProposalStatus = { } export function getExtendedStatus (proposal: ParsedProposal, bestNumber: BlockNumber | undefined): ExtendedProposalStatus { - const basicStatus = Object.keys(proposal.status)[0] as BasicProposalStatus; + const basicStatus: BasicProposalStatus = proposal.status.type; let expiresIn: number | null = null; let displayStatus: ProposalDisplayStatus = basicStatus; @@ -75,26 +73,32 @@ export function getExtendedStatus (proposal: ParsedProposal, bestNumber: BlockNu if (basicStatus === 'Active') { periodStatus = 'Voting period'; - expiresIn = Math.max(votingPeriod - blockAge, 0) || null; + expiresIn = Math.max(votingPeriod.toNumber() - blockAge, 0) || null; } if (basicStatus === 'Finalized') { - const { finalizedAt, proposalStatus } = proposal.status.Finalized; - const decisionStatus: ProposalDecisionStatuses = Object.keys(proposalStatus)[0] as ProposalDecisionStatuses; + const { finalizedAt, proposalStatus } = proposal.status.asType('Finalized'); + const decisionStatus: ProposalDecisionStatuses = proposalStatus.type; + displayStatus = decisionStatus; - finalizedAtBlock = finalizedAt as number; + finalizedAtBlock = finalizedAt.toNumber(); + if (decisionStatus === 'Approved') { - const approvedStatus: ApprovedProposalStatuses = Object.keys(proposalStatus.Approved)[0] as ApprovedProposalStatuses; + const approvedStatus: ApprovedProposalStatuses = proposalStatus.asType('Approved').type; + if (approvedStatus === 'PendingExecution') { - const finalizedAge = best - finalizedAt; + const finalizedAge = best - finalizedAt.toNumber(); + periodStatus = 'Grace period'; - expiresIn = Math.max(gracePeriod - finalizedAge, 0) || null; + expiresIn = Math.max(gracePeriod.toNumber() - finalizedAge, 0) || null; } else { // Executed / ExecutionFailed displayStatus = approvedStatus; - executedAtBlock = finalizedAtBlock + gracePeriod; + executedAtBlock = finalizedAtBlock + gracePeriod.toNumber(); + if (approvedStatus === 'ExecutionFailed') { - const executionFailedStatus = proposalStatus.Approved.ExecutionFailed as ExecutionFailedStatus; + const executionFailedStatus = proposalStatus.asType('Approved').asType('ExecutionFailed'); + executionFailReason = Buffer.from(executionFailedStatus.error.toString().replace('0x', ''), 'hex').toString(); } } @@ -127,12 +131,13 @@ function ProposalDetails ({ council, bestNumber }: ProposalDetailsProps) { - const iAmCouncilMember = Boolean(iAmMember && council && council.some(seat => seat.member.toString() === myAddress)); + const iAmCouncilMember = Boolean(iAmMember && council && council.some((seat) => seat.member.toString() === myAddress)); const iAmProposer = Boolean(iAmMember && myMemberId !== undefined && proposal.proposerId === myMemberId.toNumber()); const extendedStatus = getExtendedStatus(proposal, bestNumber); const isVotingPeriod = extendedStatus.periodStatus === 'Voting period'; + return ( -
+
) { +type RouteParams = { id?: string | undefined }; + +export default function ProposalFromId (props: RouteComponentProps) { const { match: { params: { id } } } = props; + const { api } = useApi(); - const proposalState = useProposalSubscription(new ProposalId(id)); + const proposalState = useProposalSubscription(api.createType('ProposalId', id)); return ( + href={`#/proposals/${proposal.id.toString()}`}> { `#${proposal.id.toString()}` } -
{proposal.title}
+
{proposal.title}
diff --git a/pioneer/packages/joy-proposals/src/Proposal/ProposalPreviewList.tsx b/pioneer/packages/joy-proposals/src/Proposal/ProposalPreviewList.tsx index 22df3b8803..e160532267 100644 --- a/pioneer/packages/joy-proposals/src/Proposal/ProposalPreviewList.tsx +++ b/pioneer/packages/joy-proposals/src/Proposal/ProposalPreviewList.tsx @@ -6,7 +6,7 @@ import { Link, useLocation } from 'react-router-dom'; import ProposalPreview from './ProposalPreview'; import { ParsedProposal, proposalStatusFilters, ProposalStatusFilter, ProposalsBatch } from '@polkadot/joy-utils/types/proposals'; import { useTransport, usePromise } from '@polkadot/joy-utils/react/hooks'; -import { PromiseComponent } from '@polkadot/joy-utils/react/components'; +import PromiseComponent from '@polkadot/joy-utils/react/components/PromiseComponent'; import { withCalls } from '@polkadot/react-api'; import { BlockNumber } from '@polkadot/types/interfaces'; import { Dropdown } from '@polkadot/react-components'; @@ -41,7 +41,7 @@ function ProposalPreviewList ({ bestNumber }: ProposalPreviewListProps) { [activeFilter, currentPage] ); - const filterOptions = proposalStatusFilters.map(filter => ({ + const filterOptions = proposalStatusFilters.map((filter) => ({ text: filter, value: filter })); @@ -52,20 +52,20 @@ function ProposalPreviewList ({ bestNumber }: ProposalPreviewListProps) { }; return ( - + - + { proposalsBatch && (<> { proposalsBatch.totalBatches > 1 && ( @@ -81,16 +81,16 @@ function ProposalPreviewList ({ bestNumber }: ProposalPreviewListProps) { /> ) } - { proposalsBatch.proposals.length - ? ( - - {proposalsBatch.proposals.map((prop: ParsedProposal, idx: number) => ( - - ))} - - ) - : `There are currently no ${activeFilter !== 'All' ? activeFilter.toLocaleLowerCase() : 'submitted'} proposals.` - } + { proposalsBatch.proposals.length + ? ( + + {proposalsBatch.proposals.map((prop: ParsedProposal, idx: number) => ( + + ))} + + ) + : `There are currently no ${activeFilter !== 'All' ? activeFilter.toLocaleLowerCase() : 'submitted'} proposals.` + } ) } diff --git a/pioneer/packages/joy-proposals/src/Proposal/ProposalTypePreview.tsx b/pioneer/packages/joy-proposals/src/Proposal/ProposalTypePreview.tsx index 092848a77b..994faf34bf 100644 --- a/pioneer/packages/joy-proposals/src/Proposal/ProposalTypePreview.tsx +++ b/pioneer/packages/joy-proposals/src/Proposal/ProposalTypePreview.tsx @@ -6,7 +6,7 @@ import { Item, Icon, Button, Label } from 'semantic-ui-react'; import { ProposalType, Category } from '@polkadot/joy-utils/types/proposals'; import _ from 'lodash'; import styled from 'styled-components'; -import useVoteStyles from './useVoteStyles'; +import getVoteStyles from './getVoteStyles'; import { formatBalance } from '@polkadot/util'; import './ProposalType.css'; @@ -62,9 +62,9 @@ type ProposalTypePreviewProps = { }; const ProposalTypeDetail = (props: { title: string; value: string }) => ( -
-
{ `${props.title}:` }
-
{ props.value }
+
+
{ `${props.title}:` }
+
{ props.value }
); @@ -90,7 +90,7 @@ export default function ProposalTypePreview (props: ProposalTypePreviewProps) { }; return ( - + {/* TODO: We can add it once we have the actual assets @@ -98,51 +98,51 @@ export default function ProposalTypePreview (props: ProposalTypePreviewProps) { {_.startCase(type)} {description} -
+
1 ? 's' : ''}` : 'NONE' } /> 1 ? 's' : ''}` : 'NONE' } />
{ approvalQuorum && ( - - + + Approval Quorum: { approvalQuorum }% ) } { approvalThreshold && ( - - + + Approval Threshold: { approvalThreshold }% ) } { slashingQuorum && ( - - + + Slashing Quorum: { slashingQuorum }% ) } { slashingThreshold && ( - - + + Slashing Threshold: { slashingThreshold }% ) } -
- +
+ Create - +
diff --git a/pioneer/packages/joy-proposals/src/Proposal/Votes.tsx b/pioneer/packages/joy-proposals/src/Proposal/Votes.tsx index 6eec0b42c8..20cea5011d 100644 --- a/pioneer/packages/joy-proposals/src/Proposal/Votes.tsx +++ b/pioneer/packages/joy-proposals/src/Proposal/Votes.tsx @@ -1,12 +1,12 @@ import React from 'react'; import { Header, Divider, Table, Icon } from 'semantic-ui-react'; -import useVoteStyles from './useVoteStyles'; +import getVoteStyles from './getVoteStyles'; import { VoteKind } from '@joystream/types/proposals'; import { VoteKindStr } from './VotingSection'; -import ProfilePreview from '@polkadot/joy-utils/MemberProfilePreview'; +import ProfilePreview from '@polkadot/joy-utils/react/components/MemberProfilePreview'; import { useTransport, usePromise } from '@polkadot/joy-utils/react/hooks'; import { ParsedProposal, ProposalVotes } from '@polkadot/joy-utils/types/proposals'; -import { PromiseComponent } from '@polkadot/joy-utils/react/components'; +import PromiseComponent from '@polkadot/joy-utils/react/components/PromiseComponent'; type VotesProps = { proposal: ParsedProposal; @@ -24,20 +24,21 @@ export default function Votes ({ proposal: { id, votingResults } }: VotesProps) + message='Fetching the votes...'> { (votes && votes.votes.length > 0) ? ( <> -
+
All Votes: ({votes.votes.length}/{votes.councilMembersLength})
- +
{votes.votes.map((proposalVote, idx) => { const { vote, member } = proposalVote; const voteStr = (vote as VoteKind).type.toString() as VoteKindStr; - const { icon, textColor } = useVoteStyles(voteStr); + const { icon, textColor } = getVoteStyles(voteStr); + return ( @@ -59,7 +60,7 @@ export default function Votes ({ proposal: { id, votingResults } }: VotesProps) ) : ( -
No votes have been submitted!
+
No votes have been submitted!
) } diff --git a/pioneer/packages/joy-proposals/src/Proposal/VotingSection.tsx b/pioneer/packages/joy-proposals/src/Proposal/VotingSection.tsx index 7e10d4b1fb..2908792788 100644 --- a/pioneer/packages/joy-proposals/src/Proposal/VotingSection.tsx +++ b/pioneer/packages/joy-proposals/src/Proposal/VotingSection.tsx @@ -1,8 +1,8 @@ import React, { useState } from 'react'; -import { Icon, Button, Message, Divider, Header } from 'semantic-ui-react'; -import useVoteStyles from './useVoteStyles'; -import TxButton from '@polkadot/joy-utils/TxButton'; +import { Icon, Message, Divider, Header } from 'semantic-ui-react'; +import getVoteStyles from './getVoteStyles'; +import { SemanticTxButton } from '@polkadot/joy-utils/react/components/TxButton'; import { MemberId } from '@joystream/types/members'; import { ProposalId, VoteKind, VoteKinds } from '@joystream/types/proposals'; import { useTransport, usePromise } from '@polkadot/joy-utils/react/hooks'; @@ -32,27 +32,28 @@ type VoteButtonProps = { proposalId: ProposalId; onSuccess: () => void; } + function VoteButton ({ voteKind, proposalId, memberId, onSuccess }: VoteButtonProps) { - const { icon, color } = useVoteStyles(voteKind); + const { icon, color } = getVoteStyles(voteKind); + return ( - // Button.Group "cheat" to force TxButton color - - sendTx() } - txFailedCb={ () => null } - txSuccessCb={ onSuccess } - className={'icon left labeled'}> - - { voteKind } - - + sendTx() } + txFailedCb={ () => null } + txSuccessCb={ onSuccess } + color={color} + style={{ marginRight: '5px' }} + icon + labelPosition={ 'left' }> + + { voteKind } + ); } @@ -82,13 +83,13 @@ export default function VotingSection ({ const voteStr: VoteKindStr | null = voted || (vote && vote.type.toString() as VoteKindStr); if (voteStr) { - const { icon, color } = useVoteStyles(voteStr); + const { icon, color } = getVoteStyles(voteStr); return ( - You voted {`"${voteStr}"`} + You voted {`"${voteStr}"`} ); @@ -98,7 +99,7 @@ export default function VotingSection ({ return ( <> -
Sumbit your vote
+
Sumbit your vote
{ VoteKinds.map((voteKind) => diff --git a/pioneer/packages/joy-proposals/src/Proposal/discussion/DiscussionPost.tsx b/pioneer/packages/joy-proposals/src/Proposal/discussion/DiscussionPost.tsx index 40d3125e22..b1c26fdea5 100644 --- a/pioneer/packages/joy-proposals/src/Proposal/discussion/DiscussionPost.tsx +++ b/pioneer/packages/joy-proposals/src/Proposal/discussion/DiscussionPost.tsx @@ -1,7 +1,7 @@ import React, { useState } from 'react'; import { Button, Icon } from 'semantic-ui-react'; import { ParsedPost } from '@polkadot/joy-utils/types/proposals'; -import MemberProfilePreview from '@polkadot/joy-utils/MemberProfilePreview'; +import MemberProfilePreview from '@polkadot/joy-utils/react/components/MemberProfilePreview'; import DiscussionPostForm from './DiscussionPostForm'; import { MemberId } from '@joystream/types/members'; import { useTransport } from '@polkadot/joy-utils/react/hooks'; @@ -61,6 +61,7 @@ export default function DiscussionPost ({ authorId.toNumber() === memberId.toNumber() && editsCount < constraints.maxPostEdits ); + const onEditSuccess = () => { setEditing(false); refreshDiscussion(); @@ -98,9 +99,9 @@ export default function DiscussionPost ({ setEditing(true)} primary - size="tiny" + size='tiny' icon> - + ) } diff --git a/pioneer/packages/joy-proposals/src/Proposal/discussion/DiscussionPostForm.tsx b/pioneer/packages/joy-proposals/src/Proposal/discussion/DiscussionPostForm.tsx index 5b7ff204fd..b9161990bb 100644 --- a/pioneer/packages/joy-proposals/src/Proposal/discussion/DiscussionPostForm.tsx +++ b/pioneer/packages/joy-proposals/src/Proposal/discussion/DiscussionPostForm.tsx @@ -2,8 +2,8 @@ import React from 'react'; import { Form, Field, withFormik, FormikProps } from 'formik'; import * as Yup from 'yup'; -import TxButton from '@polkadot/joy-utils/TxButton'; -import * as JoyForms from '@polkadot/joy-utils/forms'; +import TxButton from '@polkadot/joy-utils/react/components/TxButton'; +import * as JoyForms from '@polkadot/joy-utils/react/components/forms'; import { SubmittableResult } from '@polkadot/api'; import { Button } from 'semantic-ui-react'; import { TxFailedCallback, TxCallback } from '@polkadot/react-components/Status/types'; @@ -76,7 +76,7 @@ const DiscussionPostFormInner = (props: InnerProps) => { }; return ( -
+ { { /> { isEditForm ? ( )} - - + ); }; diff --git a/pioneer/packages/joy-proposals/src/forms/GenericWorkingGroupProposalForm.tsx b/pioneer/packages/joy-proposals/src/forms/GenericWorkingGroupProposalForm.tsx index 4c228e15dd..98bc9dfaa3 100644 --- a/pioneer/packages/joy-proposals/src/forms/GenericWorkingGroupProposalForm.tsx +++ b/pioneer/packages/joy-proposals/src/forms/GenericWorkingGroupProposalForm.tsx @@ -1,20 +1,17 @@ import React from 'react'; import { getFormErrorLabelsProps } from './errorHandling'; -import { - GenericProposalForm, +import { GenericProposalForm, GenericFormValues, genericFormDefaultValues, ProposalFormExportProps, ProposalFormContainerProps, - ProposalFormInnerProps -} from './GenericProposalForm'; + ProposalFormInnerProps } from './GenericProposalForm'; import { FormField } from './FormFields'; import { ProposalType } from '@polkadot/joy-utils/types/proposals'; import { WorkingGroupKey, WorkingGroupDef } from '@joystream/types/common'; -import './forms.css'; import { Dropdown, Message } from 'semantic-ui-react'; import { usePromise, useTransport } from '@polkadot/joy-utils/react/hooks'; -import { PromiseComponent } from '@polkadot/joy-utils/react/components'; +import PromiseComponent from '@polkadot/joy-utils/react/components/PromiseComponent'; import { WorkerData } from '@polkadot/joy-utils/types/workingGroups'; import { LeadInfo } from '@polkadot/joy-utils/react/components/working-groups/LeadInfo'; @@ -46,7 +43,7 @@ type ExportComponentProps = ProposalFormExportProps; export type FormInnerProps = ProposalFormInnerProps; -export const GenericWorkingGroupProposalForm: React.FunctionComponent = props => { +export const GenericWorkingGroupProposalForm: React.FunctionComponent = (props) => { const { handleChange, errors, @@ -70,20 +67,21 @@ export const GenericWorkingGroupProposalForm: React.FunctionComponent(errors, touched); + return ( ({ text: wgKey + ' Working Group', value: wgKey }))} + options={Object.keys(WorkingGroupDef).map((wgKey) => ({ text: wgKey + ' Working Group', value: wgKey }))} value={values.workingGroup} onChange={ handleChange } /> diff --git a/pioneer/packages/joy-proposals/src/forms/LabelWithHelp.tsx b/pioneer/packages/joy-proposals/src/forms/LabelWithHelp.tsx index d8880ba5bc..deb9be8c0d 100644 --- a/pioneer/packages/joy-proposals/src/forms/LabelWithHelp.tsx +++ b/pioneer/packages/joy-proposals/src/forms/LabelWithHelp.tsx @@ -5,6 +5,7 @@ type LabelWithHelpProps = { text: string; help: string }; export default function LabelWithHelp (props: LabelWithHelpProps) { const [open, setOpen] = useState(false); + return (
{[...props.transactionDetails].map((v, k) => ( @@ -919,6 +942,7 @@ export const SubmitApplicationStage = (props: SubmitApplicationStageProps) => {

Please select the account that will be used as the source of funds.

@@ -942,7 +966,7 @@ export type DoneStageProps = { export function DoneStage (props: DoneStageProps) { return ( - +

Application submitted!

Your application is #. @@ -950,8 +974,8 @@ export function DoneStage (props: DoneStageProps) {

You can track the progress of your - application in the My roles and applications section. Note that your application is attached - to your role key (see below). If you have any issues, you can message the group lead in in the Forum or contact them directly. + application in the My roles and applications section. Note that your application is attached + to your role key (see below). If you have any issues, you can message the group lead in in the Forum or contact them directly.

Your new role key

@@ -961,7 +985,7 @@ export function DoneStage (props: DoneStageProps) {

{'We\'ve generated a new role key, '}{props.roleKeyName}, automatically. A copy of the backup file should have been downloaded, or you can - get a backup from the My account section. + get a backup from the My account section.

You can also switch your role key using the Accounts selector in the top right of the screen. It works like @@ -977,7 +1001,7 @@ export function DoneStage (props: DoneStageProps) { This role key has been generated with no password! - We strongly recommend that you set a password for it in the My account section. + We strongly recommend that you set a password for it in the My account section. @@ -1003,8 +1027,8 @@ export type FlowModalProps = Pick & Fu setApplicationStake: (b: Balance) => void; roleStake: Balance; setRoleStake: (b: Balance) => void; - appDetails: any; - setAppDetails: (v: any) => void; + appDetails: ApplicationDetailsData; + setAppDetails: (v: ApplicationDetailsData) => void; txKeyAddress: AccountId; setTxKeyAddress: (v: AccountId) => void; activeStep: ProgressSteps; @@ -1022,7 +1046,7 @@ export const FlowModal = Loadable( 'keypairs', 'slots' ], - props => { + (props) => { const { applicationStake, setApplicationStake, roleStake, setRoleStake, @@ -1033,17 +1057,23 @@ export const FlowModal = Loadable( complete, setComplete } = props; - const accCtx = useMyAccount(); - if (txKeyAddress.isEmpty) { - setTxKeyAddress(new AccountId(accCtx.state.address)); - } + const accContext = useMyAccount(); + + useEffect(() => { + if (txKeyAddress.isEmpty) { + setTxKeyAddress(createMock('AccountId', accContext.state.address)); + } + }, [txKeyAddress]); const history = useHistory(); + const cancel = () => { if (history.length > 1) { history.goBack(); + return; } + history.push('/working-groups/'); }; @@ -1095,63 +1125,59 @@ export const FlowModal = Loadable( setSelectedRoleStake: setRoleStake }; - const stages: { [k in ProgressSteps]: JSX.Element } = { - [ProgressSteps.ConfirmStakes]: (), - - [ProgressSteps.ApplicationDetails]: ( { props.hasConfirmStep ? enterConfirmStakeState() : cancel(); }} - />), - - [ProgressSteps.SubmitApplication]: (), - - [ProgressSteps.Done]: () - }; - const cancelText = complete ? 'Close' : 'Cancel application'; return ( - -

- - - + +
+ + + - + cancel()}> {cancelText} - - + + - {stages[activeStep]} + { activeStep === ProgressSteps.ConfirmStakes && ( + ) } + { activeStep === ProgressSteps.ApplicationDetails && ( { props.hasConfirmStep ? enterConfirmStakeState() : cancel(); }} + /> + ) } + { activeStep === ProgressSteps.SubmitApplication && ( + ) } + { activeStep === ProgressSteps.Done && () } - +
{props.role.headline}
-
{txInProgress && -
-
+
+
} diff --git a/pioneer/packages/joy-roles/src/index.tsx b/pioneer/packages/joy-roles/src/index.tsx index aeabe4a4df..e231d2d5eb 100644 --- a/pioneer/packages/joy-roles/src/index.tsx +++ b/pioneer/packages/joy-roles/src/index.tsx @@ -8,9 +8,7 @@ import { Route, Switch, RouteComponentProps } from 'react-router'; import Tabs from '@polkadot/react-components/Tabs'; import { withMulti } from '@polkadot/react-api/index'; import QueueContext from '@polkadot/react-components/Status/Context'; -import { withMyAccount, MyAccountProps } from '@polkadot/joy-utils/MyAccount'; - -import { ViewComponent } from '@polkadot/joy-utils/index'; +import { withMyAccount, MyAccountProps } from '@polkadot/joy-utils/react/hocs/accounts'; import { Transport } from './transport.substrate'; @@ -25,17 +23,10 @@ import './index.sass'; import translate from './translate'; -const renderViewComponent = (Component: ViewComponent, props?: RouteComponentProps) => { - let params = new Map(); - if (props && props.match.params) { - params = new Map(Object.entries(props.match.params)); - } - - return ; -}; - type Props = AppProps & ApiProps & I18nProps & MyAccountProps +type DefaultRouteProps = RouteComponentProps>; + export const App: React.FC = (props: Props) => { const { t } = props; const tabs: Array = [ @@ -53,20 +44,26 @@ export const App: React.FC = (props: Props) => { const { api } = useContext(ApiContext); const { queueExtrinsic } = useContext(QueueContext); - const transport = new Transport(api, queueExtrinsic); + const [transport] = useState(() => new Transport(api, queueExtrinsic)); - const [wgCtrl] = useState(new WorkingGroupsController(transport)); - const oppCtrl = new OpportunityController(transport, props.myMemberId); - const oppsCtrl = new OpportunitiesController(transport, props.myMemberId); - const [applyCtrl] = useState(new ApplyController(transport)); - const myRolesCtrl = new MyRolesController(transport, props.myAddress); - const [adminCtrl] = useState(new AdminController(transport, api, queueExtrinsic)); + const [wgCtrl] = useState(() => new WorkingGroupsController(transport)); + const [oppCtrl] = useState(() => new OpportunityController(transport)); + const [oppsCtrl] = useState(() => new OpportunitiesController(transport)); + const [applyCtrl] = useState(() => new ApplyController(transport)); + const [myRolesCtrl] = useState(() => new MyRolesController(transport)); + const [adminCtrl] = useState(() => new AdminController(transport, api, queueExtrinsic)); useEffect(() => { return () => { transport.unsubscribe(); }; - }); + }, []); + + useEffect(() => { + oppCtrl.setMemberId(props.myMemberId); + oppsCtrl.setMemberId(props.myMemberId); + myRolesCtrl.setMyAddress(props.myAddress); + }, [props.myMemberId, props.myAddress]); const { basePath } = props; @@ -86,13 +83,26 @@ export const App: React.FC = (props: Props) => { /> - renderViewComponent(ApplyView(applyCtrl), props)} /> - renderViewComponent(OpportunityView(oppCtrl), props)} /> - renderViewComponent(OpportunitiesView(oppsCtrl), props)} /> - renderViewComponent(OpportunitiesView(oppsCtrl))} /> - renderViewComponent(MyRolesView(myRolesCtrl))} /> - renderViewComponent(AdminView(adminCtrl))} /> - renderViewComponent(WorkingGroupsView(wgCtrl))} /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> ); diff --git a/pioneer/packages/joy-roles/src/mocks.ts b/pioneer/packages/joy-roles/src/mocks.ts index 1ec3ac28da..b106c93dc4 100644 --- a/pioneer/packages/joy-roles/src/mocks.ts +++ b/pioneer/packages/joy-roles/src/mocks.ts @@ -1,41 +1,22 @@ -import { bool, Option, Text, u32, u64, Vec } from '@polkadot/types'; -import AccountId from '@polkadot/types/primitive/Generic/AccountId'; +import { IMembership } from '@joystream/types/members'; -import { IMembership, EntryMethod } from '@joystream/types/members'; - -import { - AcceptingApplications, - ActiveOpeningStage, - OpeningStage, - ActiveOpeningStageVariant, - ApplicationId -} from '@joystream/types/hiring'; +import { createMock } from '@joystream/types'; export function mockProfile (name: string, avatar_uri = ''): IMembership { - return { - handle: new Text(name), - avatar_uri: new Text(avatar_uri), - about: new Text(''), - registered_at_block: new u32(0), - registered_at_time: new u64(0), - entry: new EntryMethod(), - suspended: new bool(false), - subscription: new Option(u64), - root_account: new AccountId('5HZ6GtaeyxagLynPryM7ZnmLzoWFePKuDrkb4AT8rT4pU1fp'), - controller_account: new AccountId('5HZ6GtaeyxagLynPryM7ZnmLzoWFePKuDrkb4AT8rT4pU1fp') - }; + return createMock('Membership', { + handle: name, + avatar_uri: avatar_uri, + root_account: '5HZ6GtaeyxagLynPryM7ZnmLzoWFePKuDrkb4AT8rT4pU1fp', + controller_account: '5HZ6GtaeyxagLynPryM7ZnmLzoWFePKuDrkb4AT8rT4pU1fp' + }); } -export const mockStage = new OpeningStage({ - Active: new ActiveOpeningStageVariant({ - applications_added: new (Vec.with(ApplicationId))([]), - active_application_count: new u32(0), - unstaking_application_count: new u32(0), - deactivated_application_count: new u32(0), - stage: new ActiveOpeningStage({ - AcceptingApplications: new AcceptingApplications({ - started_accepting_applicants_at_block: new u32(100) - }) +export const mockStage = createMock('OpeningStage', { + Active: { + stage: createMock('ActiveOpeningStage', { + AcceptingApplications: { + started_accepting_applicants_at_block: 100 + } }) - }) + } }); diff --git a/pioneer/packages/joy-roles/src/openingStateMarkup.tsx b/pioneer/packages/joy-roles/src/openingStateMarkup.tsx index 9a2fb9912e..3136049179 100644 --- a/pioneer/packages/joy-roles/src/openingStateMarkup.tsx +++ b/pioneer/packages/joy-roles/src/openingStateMarkup.tsx @@ -10,56 +10,52 @@ export type headerMarkup = { iconSpin?: boolean; } -export const stateMarkup = new Map([ - [OpeningState.WaitingToBegin, { +export const stateMarkup: Record = { + [OpeningState.WaitingToBegin]: { class: 'waiting-to-begin', description: 'Waiting to begin', icon: 'spinner', iconSpin: true - }], - [OpeningState.AcceptingApplications, { + }, + [OpeningState.AcceptingApplications]: { class: 'active', description: 'Accepting applications', icon: 'heart' - }], - [OpeningState.InReview, { + }, + [OpeningState.InReview]: { class: 'in-review', description: 'Applications in review', icon: 'hourglass half' - }], - [OpeningState.Complete, { + }, + [OpeningState.Complete]: { class: 'complete', description: 'Hiring complete', icon: 'thumbs up' - }], - [OpeningState.Cancelled, { + }, + [OpeningState.Cancelled]: { class: 'cancelled', description: 'Cancelled', icon: 'ban' - }] -]); - -export function openingStateMarkup (state: OpeningState, key: string): T { - const markup = stateMarkup.get(state); - - if (typeof markup === 'undefined') { - return null as unknown as T; } +}; + +export function openingStateMarkup (state: OpeningState, key: K): headerMarkup[K] { + const markup = stateMarkup[state]; - return (markup as any)[key]; + return markup[key]; } export function openingClass (state: OpeningState): string { - return 'status-' + openingStateMarkup(state, 'class'); + return `status-${openingStateMarkup(state, 'class') || ''}`; } export function openingDescription (state: OpeningState): string { - return openingStateMarkup(state, 'description'); + return openingStateMarkup(state, 'description') || ''; } export function openingIcon (state: OpeningState) { - const icon = openingStateMarkup(state, 'icon'); - const spin = openingStateMarkup(state, 'iconSpin'); + const icon = openingStateMarkup(state, 'icon'); + const spin = openingStateMarkup(state, 'iconSpin'); return ; } diff --git a/pioneer/packages/joy-roles/src/tabs.stories.tsx b/pioneer/packages/joy-roles/src/tabs.stories.tsx index 03c5640628..fa6a546449 100644 --- a/pioneer/packages/joy-roles/src/tabs.stories.tsx +++ b/pioneer/packages/joy-roles/src/tabs.stories.tsx @@ -14,7 +14,7 @@ export default { export const RolesPage = () => { const tab = ( - + diff --git a/pioneer/packages/joy-roles/src/tabs/Admin.controller.tsx b/pioneer/packages/joy-roles/src/tabs/Admin.controller.tsx index 5a5d40a5cd..b1b73d8a17 100644 --- a/pioneer/packages/joy-roles/src/tabs/Admin.controller.tsx +++ b/pioneer/packages/joy-roles/src/tabs/Admin.controller.tsx @@ -3,16 +3,15 @@ import { Link } from 'react-router-dom'; import { formatBalance } from '@polkadot/util'; import { ApiPromise } from '@polkadot/api'; -import { GenericAccountId, Option, Text, Vec, u32, u128 } from '@polkadot/types'; +import { Option, Text, Vec } from '@polkadot/types'; import { Balance } from '@polkadot/types/interfaces'; -import { SingleLinkedMapEntry, Controller, View } from '@polkadot/joy-utils/index'; -import { useMyAccount } from '@polkadot/joy-utils/MyAccountContext'; -import { SubmittableExtrinsic } from '@polkadot/api/promise/types'; +import { Controller } from '@polkadot/joy-utils/react/helpers'; +import { View } from '@polkadot/joy-utils/react/hocs'; +import { useMyAccount } from '@polkadot/joy-utils/react/hooks'; import { QueueTxExtrinsicAdd } from '@polkadot/react-components/Status/types'; -import { - Accordion, +import { Accordion, Button, Card, Checkbox, @@ -26,48 +25,37 @@ import { Message, Modal, Table, - TextArea -} from 'semantic-ui-react'; + TextArea, + InputOnChangeData } from 'semantic-ui-react'; import { ITransport } from '../transport'; -import { - Application, +import { Application, ApplicationStage, ActivateOpeningAt, - ApplicationRationingPolicy, - CurrentBlock, Opening, OpeningStage, StakingPolicy, StakingAmountLimitModeKeys, - StakingAmountLimitMode -} from '@joystream/types/hiring'; + StakingAmountLimitMode } from '@joystream/types/hiring'; -import { - Membership, - MemberId -} from '@joystream/types/members'; +import { Membership, + MemberId } from '@joystream/types/members'; import { Stake, StakeId } from '@joystream/types/stake'; -import { - CuratorApplication, CuratorApplicationId, +import { CuratorApplication, CuratorApplicationId, CuratorOpening, - IOpeningPolicyCommitment, CuratorOpeningId -} from '@joystream/types/content-working-group'; + IOpeningPolicyCommitment, CuratorOpeningId } from '@joystream/types/content-working-group'; -import { - classifyOpeningStage, +import { classifyOpeningStage, OpeningStageClassification, - OpeningState -} from '../classifiers'; + OpeningState } from '../classifiers'; -import { - openingDescription -} from '../openingStateMarkup'; +import { openingDescription } from '../openingStateMarkup'; import { Add, Zero } from '../balances'; +import { createMock } from '@joystream/types'; type ids = { curatorId: number; @@ -110,7 +98,7 @@ type State = { } function newHRT (title: string): Text { - return new Text(JSON.stringify({ + return createMock('Text', JSON.stringify({ version: 1, headline: 'some headline', job: { @@ -155,57 +143,49 @@ function newHRT (title: string): Text { } const createRationingPolicyOpt = (maxApplicants: number) => - new Option( - ApplicationRationingPolicy, - new ApplicationRationingPolicy({ - max_active_applicants: new u32(maxApplicants) - }) - ); + createMock('Option', { + max_active_applicants: maxApplicants + }); const createStakingPolicyOpt = (amount: number, amount_mode: StakingAmountLimitMode): Option => - new Option( - StakingPolicy, - new StakingPolicy({ - amount: new u128(amount), - amount_mode, - crowded_out_unstaking_period_length: new Option('BlockNumber', null), - review_period_expired_unstaking_period_length: new Option('BlockNumber', null) - }) - ); + createMock('Option', { + amount, + amount_mode + }); -const STAKING_MODE_EXACT = new StakingAmountLimitMode(StakingAmountLimitModeKeys.Exact); -const STAKING_MODE_AT_LEAST = new StakingAmountLimitMode(StakingAmountLimitModeKeys.AtLeast); +const STAKING_MODE_EXACT = createMock('StakingAmountLimitMode', StakingAmountLimitModeKeys.Exact); +const STAKING_MODE_AT_LEAST = createMock('StakingAmountLimitMode', StakingAmountLimitModeKeys.AtLeast); const stockOpenings: openingDescriptor[] = [ { title: 'Test config A: no application stake, no role stake, no applicant limit', - start: new ActivateOpeningAt(CurrentBlock), + start: createMock('ActivateOpeningAt', 'CurrentBlock'), policy: { - max_review_period_length: new u32(99999) + max_review_period_length: createMock('u32', 99999) }, text: newHRT('Test configuration A') }, { title: 'Test config B: no application stake, no role stake, 10 applicant limit', - start: new ActivateOpeningAt(CurrentBlock), + start: createMock('ActivateOpeningAt', 'CurrentBlock'), policy: { - max_review_period_length: new u32(99999) + max_review_period_length: createMock('u32', 99999) }, text: newHRT('Test configuration B') }, { title: 'Test config C: fixed application stake (100), no role stake, no applicant limit', - start: new ActivateOpeningAt(CurrentBlock), + start: createMock('ActivateOpeningAt', 'CurrentBlock'), policy: { - max_review_period_length: new u32(99999), + max_review_period_length: createMock('u32', 99999), application_staking_policy: createStakingPolicyOpt(100, STAKING_MODE_EXACT) }, text: newHRT('Test configuration C') }, { title: 'Test config D: fixed application stake (100), no role stake, 10 applicant limit', - start: new ActivateOpeningAt(CurrentBlock), + start: createMock('ActivateOpeningAt', 'CurrentBlock'), policy: { - max_review_period_length: new u32(99999), + max_review_period_length: createMock('u32', 99999), application_rationing_policy: createRationingPolicyOpt(10), application_staking_policy: createStakingPolicyOpt(100, STAKING_MODE_EXACT) }, @@ -213,18 +193,18 @@ const stockOpenings: openingDescriptor[] = [ }, { title: 'Test config E: no application stake, fixed role stake (100), no applicant limit', - start: new ActivateOpeningAt(CurrentBlock), + start: createMock('ActivateOpeningAt', 'CurrentBlock'), policy: { - max_review_period_length: new u32(99999), + max_review_period_length: createMock('u32', 99999), role_staking_policy: createStakingPolicyOpt(100, STAKING_MODE_EXACT) }, text: newHRT('Test configuration E') }, { title: 'Test config F: no application stake, fixed role stake (100), 10 applicant limit', - start: new ActivateOpeningAt(CurrentBlock), + start: createMock('ActivateOpeningAt', 'CurrentBlock'), policy: { - max_review_period_length: new u32(99999), + max_review_period_length: createMock('u32', 99999), application_rationing_policy: createRationingPolicyOpt(10), role_staking_policy: createStakingPolicyOpt(100, STAKING_MODE_EXACT) }, @@ -232,18 +212,18 @@ const stockOpenings: openingDescriptor[] = [ }, { title: 'Test config G: minimum application stake (100), no role stake, no applicant limit', - start: new ActivateOpeningAt(CurrentBlock), + start: createMock('ActivateOpeningAt', 'CurrentBlock'), policy: { - max_review_period_length: new u32(99999), + max_review_period_length: createMock('u32', 99999), application_staking_policy: createStakingPolicyOpt(100, STAKING_MODE_AT_LEAST) }, text: newHRT('Test configuration G') }, { title: 'Test config H: minimum application stake (100), no role stake, 10 applicant limit', - start: new ActivateOpeningAt(CurrentBlock), + start: createMock('ActivateOpeningAt', 'CurrentBlock'), policy: { - max_review_period_length: new u32(99999), + max_review_period_length: createMock('u32', 99999), application_rationing_policy: createRationingPolicyOpt(10), application_staking_policy: createStakingPolicyOpt(100, STAKING_MODE_AT_LEAST) }, @@ -251,18 +231,18 @@ const stockOpenings: openingDescriptor[] = [ }, { title: 'Test config I: no application stake, minimum role stake (100), no applicant limit', - start: new ActivateOpeningAt(CurrentBlock), + start: createMock('ActivateOpeningAt', 'CurrentBlock'), policy: { - max_review_period_length: new u32(99999), + max_review_period_length: createMock('u32', 99999), role_staking_policy: createStakingPolicyOpt(100, STAKING_MODE_AT_LEAST) }, text: newHRT('Test configuration I') }, { title: 'Test config J: no application stake, minimum role stake (100), 10 applicant limit', - start: new ActivateOpeningAt(CurrentBlock), + start: createMock('ActivateOpeningAt', 'CurrentBlock'), policy: { - max_review_period_length: new u32(99999), + max_review_period_length: createMock('u32', 99999), application_rationing_policy: createRationingPolicyOpt(10), role_staking_policy: createStakingPolicyOpt(100, STAKING_MODE_AT_LEAST) }, @@ -270,9 +250,9 @@ const stockOpenings: openingDescriptor[] = [ }, { title: 'Test config K: fixed application stake (100), fixed role stake (200), no applicant limit', - start: new ActivateOpeningAt(CurrentBlock), + start: createMock('ActivateOpeningAt', 'CurrentBlock'), policy: { - max_review_period_length: new u32(99999), + max_review_period_length: createMock('u32', 99999), application_staking_policy: createStakingPolicyOpt(100, STAKING_MODE_EXACT), role_staking_policy: createStakingPolicyOpt(200, STAKING_MODE_EXACT) }, @@ -280,9 +260,9 @@ const stockOpenings: openingDescriptor[] = [ }, { title: 'Test config L: fixed application stake (100), fixed role stake (200), 10 applicant limit', - start: new ActivateOpeningAt(CurrentBlock), + start: createMock('ActivateOpeningAt', 'CurrentBlock'), policy: { - max_review_period_length: new u32(99999), + max_review_period_length: createMock('u32', 99999), application_rationing_policy: createRationingPolicyOpt(10), application_staking_policy: createStakingPolicyOpt(100, STAKING_MODE_EXACT), role_staking_policy: createStakingPolicyOpt(200, STAKING_MODE_EXACT) @@ -291,9 +271,9 @@ const stockOpenings: openingDescriptor[] = [ }, { title: 'Test config M: Minimum application stake (100), minimum role stake (200), no applicant limit', - start: new ActivateOpeningAt(CurrentBlock), + start: createMock('ActivateOpeningAt', 'CurrentBlock'), policy: { - max_review_period_length: new u32(99999), + max_review_period_length: createMock('u32', 99999), application_staking_policy: createStakingPolicyOpt(100, STAKING_MODE_AT_LEAST), role_staking_policy: createStakingPolicyOpt(200, STAKING_MODE_AT_LEAST) }, @@ -301,9 +281,9 @@ const stockOpenings: openingDescriptor[] = [ }, { title: 'Test config N: Minimum application stake (100), minimum role stake (200), 10 applicant limit', - start: new ActivateOpeningAt(CurrentBlock), + start: createMock('ActivateOpeningAt', 'CurrentBlock'), policy: { - max_review_period_length: new u32(99999), + max_review_period_length: createMock('u32', 99999), application_rationing_policy: createRationingPolicyOpt(10), application_staking_policy: createStakingPolicyOpt(100, STAKING_MODE_AT_LEAST), role_staking_policy: createStakingPolicyOpt(200, STAKING_MODE_AT_LEAST) @@ -312,9 +292,9 @@ const stockOpenings: openingDescriptor[] = [ }, { title: 'Test config O: Fixed application stake (100), minimum role stake (200), no applicant limit', - start: new ActivateOpeningAt(CurrentBlock), + start: createMock('ActivateOpeningAt', 'CurrentBlock'), policy: { - max_review_period_length: new u32(99999), + max_review_period_length: createMock('u32', 99999), application_staking_policy: createStakingPolicyOpt(100, STAKING_MODE_EXACT), role_staking_policy: createStakingPolicyOpt(200, STAKING_MODE_AT_LEAST) }, @@ -322,15 +302,10 @@ const stockOpenings: openingDescriptor[] = [ }, { title: 'Test config P: Fixed application stake (100), minimum role stake (200), 10 applicant limit', - start: new ActivateOpeningAt(CurrentBlock), + start: createMock('ActivateOpeningAt', 'CurrentBlock'), policy: { - max_review_period_length: new u32(99999), - application_rationing_policy: new Option( - ApplicationRationingPolicy, - new ApplicationRationingPolicy({ - max_active_applicants: new u32(10) - }) - ), + max_review_period_length: createMock('u32', 99999), + application_rationing_policy: createRationingPolicyOpt(10), application_staking_policy: createStakingPolicyOpt(100, STAKING_MODE_EXACT), role_staking_policy: createStakingPolicyOpt(200, STAKING_MODE_AT_LEAST) }, @@ -338,9 +313,9 @@ const stockOpenings: openingDescriptor[] = [ }, { title: 'Test config Q: Minimum application stake (100), fixed role stake (200), no applicant limit', - start: new ActivateOpeningAt(CurrentBlock), + start: createMock('ActivateOpeningAt', 'CurrentBlock'), policy: { - max_review_period_length: new u32(99999), + max_review_period_length: createMock('u32', 99999), application_staking_policy: createStakingPolicyOpt(100, STAKING_MODE_AT_LEAST), role_staking_policy: createStakingPolicyOpt(200, STAKING_MODE_EXACT) }, @@ -348,9 +323,9 @@ const stockOpenings: openingDescriptor[] = [ }, { title: 'Test config R: Minimum application stake (100), fixed role stake (200), 10 applicant limit', - start: new ActivateOpeningAt(CurrentBlock), + start: createMock('ActivateOpeningAt', 'CurrentBlock'), policy: { - max_review_period_length: new u32(99999), + max_review_period_length: createMock('u32', 99999), application_rationing_policy: createRationingPolicyOpt(10), application_staking_policy: createStakingPolicyOpt(100, STAKING_MODE_AT_LEAST), role_staking_policy: createStakingPolicyOpt(200, STAKING_MODE_EXACT) @@ -376,48 +351,51 @@ export class AdminController extends Controller { this.api = api; this.queueExtrinsic = queueExtrinsic; this.state.currentDescriptor = stockOpenings[0]; - this.updateState(); + void this.refreshState(); } - onTxSuccess = () => { this.updateState(); } + onTxSuccess = () => { this.closeModal(); void this.refreshState(); } newOpening (accountId: string, desc: openingDescriptor) { const tx = this.api.tx.contentWorkingGroup.addCuratorOpening( desc.start, desc.policy, desc.text - ) as unknown as SubmittableExtrinsic; + ); - // FIXME: Normally we would keep it open in case of errror, but due to bad design - // the values in the form are reset at this point anyway, so there is no point - this.closeModal(); this.queueExtrinsic({ extrinsic: tx, txSuccessCb: this.onTxSuccess, accountId }); } startAcceptingApplications (accountId: string, id = 0) { const tx = this.api.tx.contentWorkingGroup.acceptCuratorApplications(id); + this.queueExtrinsic({ extrinsic: tx, txSuccessCb: this.onTxSuccess, accountId }); } async applyAsACurator (creatorAddress: string, openingId: number) { const membershipIds = (await this.api.query.members.memberIdsByControllerAccountId(creatorAddress)) as Vec; + if (membershipIds.length === 0) { console.error('No membship ID associated with this address'); + return; } + const tx = this.api.tx.contentWorkingGroup.applyOnCuratorOpening( membershipIds[0], openingId, - new GenericAccountId(creatorAddress), - new Option(u128, 400), - new Option(u128, 400), - new Text('This is my application') - ) as unknown as SubmittableExtrinsic; + creatorAddress, + 400, + 400, + 'This is my application' + ); + this.queueExtrinsic({ extrinsic: tx, txSuccessCb: this.onTxSuccess, accountId: creatorAddress }); } beginApplicantReview (accountId: string, openingId: number) { const tx = this.api.tx.contentWorkingGroup.beginCuratorApplicantReview(openingId); + this.queueExtrinsic({ extrinsic: tx, txSuccessCb: this.onTxSuccess, accountId }); } @@ -426,26 +404,25 @@ export class AdminController extends Controller { openingId, applications, null - ) as unknown as SubmittableExtrinsic; + ); + this.queueExtrinsic({ extrinsic: tx, txSuccessCb: this.onTxSuccess, accountId }); } protected async profile (id: MemberId): Promise { const member = (await this.api.query.members.membershipById(id)) as Membership; - if (member.handle.isEmpty) { + + if (member.isEmpty) { throw new Error(`Expected member profile not found! (id: ${id.toString()}`); } + return member; } protected async stakeValue (stakeId: StakeId): Promise { - const stake = new SingleLinkedMapEntry( - Stake, - await this.api.query.stake.stakes( - stakeId - ) - ); - return stake.value.value; + const stake = await this.api.query.stake.stakes(stakeId) as Stake; + + return stake.value; } protected async roleStake (application: Application): Promise { @@ -464,67 +441,51 @@ export class AdminController extends Controller { return this.stakeValue(application.active_application_staking_id.unwrap()); } - async updateState () { + async refreshState () { this.state.openings = new Map(); const nextOpeningId = await this.api.query.contentWorkingGroup.nextCuratorOpeningId() as CuratorOpeningId; + for (let i = nextOpeningId.toNumber() - 1; i >= 0; i--) { - const curatorOpening = new SingleLinkedMapEntry( - CuratorOpening, - await this.api.query.contentWorkingGroup.curatorOpeningById(i) - ); + const curatorOpening = await this.api.query.contentWorkingGroup.curatorOpeningById(i) as CuratorOpening; - const openingId = curatorOpening.value.opening_id; + const openingId = curatorOpening.opening_id; - const baseOpening = new SingleLinkedMapEntry( - Opening, - await this.api.query.hiring.openingById( - openingId - ) - ); + const baseOpening = await this.api.query.hiring.openingById(openingId) as Opening; - const hrt = baseOpening.value.parse_human_readable_text_with_fallback(); + const hrt = baseOpening.parse_human_readable_text_with_fallback(); const title = hrt.job.title; this.state.openings.set(i, { openingId: openingId.toNumber(), curatorId: i, applications: new Array(), - state: baseOpening.value.stage, + state: baseOpening.stage, title: title, - classification: await classifyOpeningStage(this.transport, baseOpening.value) + classification: await classifyOpeningStage(this.transport, baseOpening) }); } const nextAppid = await this.api.query.contentWorkingGroup.nextCuratorApplicationId() as CuratorApplicationId; + for (let i = 0; i < nextAppid.toNumber(); i++) { - const cApplication = new SingleLinkedMapEntry( - CuratorApplication, - await this.api.query.contentWorkingGroup.curatorApplicationById(i) - ); + const cApplication = await this.api.query.contentWorkingGroup.curatorApplicationById(i) as CuratorApplication; - const appId = cApplication.value.application_id; - const baseApplications = new SingleLinkedMapEntry( - Application, - await this.api.query.hiring.applicationById( - appId - ) - ); + const appId = cApplication.application_id; + const baseApplications = await this.api.query.hiring.applicationById(appId) as Application; - const curatorOpening = this.state.openings.get( - cApplication.value.curator_opening_id.toNumber() - ) as opening; + const curatorOpening = this.state.openings.get(cApplication.curator_opening_id.toNumber()) as opening; curatorOpening.applications.push({ openingId: appId.toNumber(), curatorId: i, - stage: baseApplications.value.stage, - account: cApplication.value.role_account_id.toString(), - memberId: cApplication.value.member_id.toNumber(), - profile: (await this.profile(cApplication.value.member_id)), - applicationStake: await this.applicationStake(baseApplications.value), - roleStake: await this.roleStake(baseApplications.value), - application: baseApplications.value + stage: baseApplications.stage, + account: cApplication.role_account_id.toString(), + memberId: cApplication.member_id.toNumber(), + profile: (await this.profile(cApplication.member_id)), + applicationStake: await this.applicationStake(baseApplications), + roleStake: await this.roleStake(baseApplications), + application: baseApplications }); } @@ -547,12 +508,14 @@ type AdminContainerProps = { state: State; controller: AdminController; } + const AdminContainer = ({ state, controller }: AdminContainerProps) => { const address = useMyAccount().state.address; const containerRef = useRef(null); + return (
- + @@ -573,7 +536,7 @@ const AdminContainer = ({ state, controller }: AdminContainerProps) => { controller.closeModal()} - mountNode={containerRef.current} // Prevent conflicts with tx-modal (after form values reset issue is fixed, see FIXME: above) + mountNode={containerRef.current} // Prevent conflicts with tx-modal > @@ -584,7 +547,7 @@ const AdminContainer = ({ state, controller }: AdminContainerProps) => { { - [...state.openings.keys()].map(key => ) + [...state.openings.keys()].map((key) => ) }
@@ -593,7 +556,7 @@ const AdminContainer = ({ state, controller }: AdminContainerProps) => { }; export const AdminView = View( - (state, controller) => { + ({ state, controller }) => { return ( ); @@ -627,25 +590,26 @@ const NewOpening = (props: NewOpeningProps) => { switch (value) { case 'CurrentBlock': setShowExactBlock(false); - setStart(new ActivateOpeningAt(CurrentBlock)); + setStart(createMock('ActivateOpeningAt', 'CurrentBlock')); break; case 'ExactBlock': - setStart(new ActivateOpeningAt({ ExactBlock: exactBlock })); + setStart(createMock('ActivateOpeningAt', { ExactBlock: exactBlock })); setShowExactBlock(true); break; } }; - const onChangeExactBlock = (e: any, { value }: any) => { - setExactBlock(value); - setStart(new ActivateOpeningAt({ ExactBlock: value })); + const onChangeExactBlock = (e: any, { value }: InputOnChangeData) => { + setExactBlock(typeof value === 'number' ? value : (parseInt(value) || 0)); + setStart(createMock('ActivateOpeningAt', { ExactBlock: value })); }; const [policy, setPolicy] = useState(props.desc.policy); const onChangePolicyField = (fieldName: PolicyKey, value: policyDescriptor[PolicyKey]) => { const newState = { ...policy }; + newState[fieldName] = value; setPolicy(newState); }; @@ -682,14 +646,17 @@ const NewOpening = (props: NewOpeningProps) => { ) => { if (mode === '') { const policyField = policy[fieldName]; + mode = policyField && policyField.isSome ? (policyField.unwrap().amount_mode.type as StakingAmountLimitModeKeys) : StakingAmountLimitModeKeys.Exact; // Default } + const value = createStakingPolicyOpt( stakeValue, mode === StakingAmountLimitModeKeys.Exact ? STAKING_MODE_EXACT : STAKING_MODE_AT_LEAST ); + onChangePolicyField(fieldName, value); }; @@ -709,7 +676,7 @@ const NewOpening = (props: NewOpeningProps) => { props.fn({ start: start, policy: policy, - text: new Text(text), + text: createMock('Text', text), title: '' }); }; @@ -726,7 +693,7 @@ const NewOpening = (props: NewOpeningProps) => { /> {showExactBlock === true && @@ -736,15 +703,15 @@ const NewOpening = (props: NewOpeningProps) => { onChangePolicyField('max_review_period_length', new u32(value))} + onChange={(e: any, { value }: any) => onChangePolicyField('max_review_period_length', createMock('u32', value))} /> - onStakeModeCheckboxChange(setRequireAppStakingPolicy, 'application_staking_policy', checked, 0)} /> + onStakeModeCheckboxChange(setRequireAppStakingPolicy, 'application_staking_policy', checked, 0)} /> {requireAppStakingPolicy && ( @@ -757,7 +724,7 @@ const NewOpening = (props: NewOpeningProps) => { changeStakingMode('application_staking_policy', '', value)} /> @@ -767,7 +734,7 @@ const NewOpening = (props: NewOpeningProps) => { - onStakeModeCheckboxChange(setRequireRoleStakingPolicy, 'role_staking_policy', checked, 0)} /> + onStakeModeCheckboxChange(setRequireRoleStakingPolicy, 'role_staking_policy', checked, 0)} /> {requireRoleStakingPolicy && ( @@ -780,7 +747,7 @@ const NewOpening = (props: NewOpeningProps) => { changeStakingMode('role_staking_policy', '', value)} /> @@ -792,7 +759,7 @@ const NewOpening = (props: NewOpeningProps) => {