diff --git a/src/data/cacheUpdates.ts b/src/data/cacheUpdates.ts index 2aa2e4d6396..b8baa702672 100644 --- a/src/data/cacheUpdates.ts +++ b/src/data/cacheUpdates.ts @@ -22,11 +22,50 @@ import { UserQuery, UserQueryVariables, UserDocument, + CreateProgramMutationResult, + ColonyProgramsQuery, + ColonyProgramsQueryVariables, + ColonyProgramsDocument, } from './generated'; type Cache = typeof apolloCache; const cacheUpdates = { + createProgram(colonyAddress: Address) { + return (cache: Cache, { data }: CreateProgramMutationResult) => { + try { + const cacheData = cache.readQuery< + ColonyProgramsQuery, + ColonyProgramsQueryVariables + >({ + query: ColonyProgramsDocument, + variables: { + address: colonyAddress, + }, + }); + const createProgramData = data && data.createProgram; + if (cacheData && createProgramData) { + const programs = cacheData.colony.programs || []; + programs.push(createProgramData); + cache.writeQuery({ + data: { + colony: { + ...cacheData.colony, + programs, + }, + }, + query: ColonyProgramsDocument, + variables: { + address: colonyAddress, + }, + }); + } + } catch (e) { + log.verbose(e); + log.verbose('Not updating store - colony programs not loaded yet'); + } + }; + }, createTask(colonyAddress: Address) { return (cache: Cache, { data }: CreateTaskMutationResult) => { try { diff --git a/src/data/generated.ts b/src/data/generated.ts index 91c8d11e4c7..b0d34d11db9 100644 --- a/src/data/generated.ts +++ b/src/data/generated.ts @@ -157,6 +157,10 @@ export type Scalars = { }; +export type AcceptSubmissionInput = { + id: Scalars['String'], +}; + export type AddUpvoteToSuggestionInput = { id: Scalars['String'], }; @@ -199,6 +203,7 @@ export type Colony = { founder?: Maybe, isNativeTokenExternal: Scalars['Boolean'], nativeTokenAddress: Scalars['String'], + programs: Array, subscribedUsers: Array, suggestions: Array, tokenAddresses: Array, @@ -246,6 +251,24 @@ export type CreateDomainInput = { name: Scalars['String'], }; +export type CreateLevelInput = { + programId: Scalars['String'], +}; + +export type CreateLevelTaskInput = { + levelId: Scalars['String'], +}; + +export type CreateLevelTaskSubmissionInput = { + levelId: Scalars['String'], + persistentTaskId: Scalars['String'], + submission: Scalars['String'], +}; + +export type CreateProgramInput = { + colonyAddress: Scalars['String'], +}; + export type CreateSuggestionInput = { colonyAddress: Scalars['String'], ethDomainId: Scalars['Int'], @@ -315,6 +338,33 @@ export type EditDomainNameInput = { name: Scalars['String'], }; +export type EditLevelInput = { + id: Scalars['String'], + title?: Maybe, + description?: Maybe, + achievement?: Maybe, + numRequiredSteps?: Maybe, +}; + +export type EditPersistentTaskInput = { + id: Scalars['String'], + ethDomainId?: Maybe, + ethSkillId?: Maybe, + title?: Maybe, + description?: Maybe, +}; + +export type EditProgramInput = { + id: Scalars['String'], + title?: Maybe, + description?: Maybe, +}; + +export type EditSubmissionInput = { + id: Scalars['String'], + submission: Scalars['String'], +}; + export type EditUserInput = { avatarHash?: Maybe, bio?: Maybe, @@ -323,6 +373,10 @@ export type EditUserInput = { website?: Maybe, }; +export type EnrollInProgramInput = { + id: Scalars['String'], +}; + export type Event = { id: Scalars['String'], type: EventType, @@ -366,6 +420,25 @@ export type FinalizeTaskInput = { ethPotId: Scalars['Int'], }; +export type Level = { + id: Scalars['String'], + createdAt: Scalars['DateTime'], + creatorAddress: Scalars['String'], + programId: Scalars['String'], + title?: Maybe, + description?: Maybe, + achievement?: Maybe, + numRequiredSteps?: Maybe, + stepIds: Array, + steps: Array, + status: LevelStatus, +}; + +export enum LevelStatus { + Active = 'Active', + Deleted = 'Deleted' +} + export type LoggedInUser = { id: Scalars['String'], balance: Scalars['String'], @@ -416,6 +489,25 @@ export type Mutation = { subscribeToColony?: Maybe, unsubscribeFromColony?: Maybe, setUserTokens?: Maybe, + createLevelTaskSubmission?: Maybe, + editSubmission?: Maybe, + acceptSubmission?: Maybe, + createLevelTask?: Maybe, + removeLevelTask?: Maybe, + editPersistentTask?: Maybe, + setPersistentTaskPayout?: Maybe, + removePersistentTaskPayout?: Maybe, + removePersistentTask?: Maybe, + createLevel?: Maybe, + editLevel?: Maybe, + reorderLevelSteps?: Maybe, + removeLevel?: Maybe, + createProgram?: Maybe, + enrollInProgram?: Maybe, + editProgram?: Maybe, + reorderProgramLevels?: Maybe, + publishProgram?: Maybe, + removeProgram?: Maybe, setLoggedInUser: LoggedInUser, clearLoggedInUser: LoggedInUser, }; @@ -576,6 +668,101 @@ export type MutationSetUserTokensArgs = { }; +export type MutationCreateLevelTaskSubmissionArgs = { + input: CreateLevelTaskSubmissionInput +}; + + +export type MutationEditSubmissionArgs = { + input: EditSubmissionInput +}; + + +export type MutationAcceptSubmissionArgs = { + input: AcceptSubmissionInput +}; + + +export type MutationCreateLevelTaskArgs = { + input: CreateLevelTaskInput +}; + + +export type MutationRemoveLevelTaskArgs = { + input: RemoveLevelTaskInput +}; + + +export type MutationEditPersistentTaskArgs = { + input: EditPersistentTaskInput +}; + + +export type MutationSetPersistentTaskPayoutArgs = { + input: SetTaskPayoutInput +}; + + +export type MutationRemovePersistentTaskPayoutArgs = { + input: RemoveTaskPayoutInput +}; + + +export type MutationRemovePersistentTaskArgs = { + input: RemovePersistentTaskInput +}; + + +export type MutationCreateLevelArgs = { + input: CreateLevelInput +}; + + +export type MutationEditLevelArgs = { + input: EditLevelInput +}; + + +export type MutationReorderLevelStepsArgs = { + input: ReorderLevelStepsInput +}; + + +export type MutationRemoveLevelArgs = { + input: RemoveLevelInput +}; + + +export type MutationCreateProgramArgs = { + input: CreateProgramInput +}; + + +export type MutationEnrollInProgramArgs = { + input: EnrollInProgramInput +}; + + +export type MutationEditProgramArgs = { + input: EditProgramInput +}; + + +export type MutationReorderProgramLevelsArgs = { + input: ReorderProgramLevelsInput +}; + + +export type MutationPublishProgramArgs = { + input: PublishProgramInput +}; + + +export type MutationRemoveProgramArgs = { + input: RemoveProgramInput +}; + + export type MutationSetLoggedInUserArgs = { input?: Maybe }; @@ -590,6 +777,50 @@ export type Notification = { read: Scalars['Boolean'], }; +export type PersistentTask = { + id: Scalars['String'], + createdAt: Scalars['DateTime'], + colonyAddress: Scalars['String'], + creatorAddress: Scalars['String'], + ethDomainId?: Maybe, + ethSkillId?: Maybe, + title?: Maybe, + description?: Maybe, + payouts: Array, + submissions: Array, + status: PersistentTaskStatus, +}; + +export enum PersistentTaskStatus { + Active = 'Active', + Closed = 'Closed', + Deleted = 'Deleted' +} + +export type Program = { + id: Scalars['String'], + createdAt: Scalars['DateTime'], + creatorAddress: Scalars['String'], + colonyAddress: Scalars['String'], + title?: Maybe, + description?: Maybe, + levelIds: Array, + levels: Array, + enrolledUserAddresses: Array, + status: ProgramStatus, + submissions: Array, +}; + +export enum ProgramStatus { + Draft = 'Draft', + Active = 'Active', + Deleted = 'Deleted' +} + +export type PublishProgramInput = { + id: Scalars['String'], +}; + export type Query = { user: User, colony: Colony, @@ -662,6 +893,23 @@ export type QueryUsernameArgs = { address: Scalars['String'] }; +export type RemoveLevelInput = { + id: Scalars['String'], +}; + +export type RemoveLevelTaskInput = { + id: Scalars['String'], + levelId: Scalars['String'], +}; + +export type RemovePersistentTaskInput = { + id: Scalars['String'], +}; + +export type RemoveProgramInput = { + id: Scalars['String'], +}; + export type RemoveTaskPayoutEvent = TaskEvent & { type: EventType, taskId: Scalars['String'], @@ -679,6 +927,16 @@ export type RemoveUpvoteFromSuggestionInput = { id: Scalars['String'], }; +export type ReorderLevelStepsInput = { + id: Scalars['String'], + stepIds: Array, +}; + +export type ReorderProgramLevelsInput = { + id: Scalars['String'], + levelIds: Array, +}; + export type SendTaskMessageInput = { id: Scalars['String'], message: Scalars['String'], @@ -777,6 +1035,24 @@ export type SetUserTokensInput = { tokenAddresses: Array, }; +export type Submission = { + id: Scalars['String'], + createdAt: Scalars['DateTime'], + creatorAddress: Scalars['String'], + creator: User, + persistentTaskId: Scalars['String'], + submission: Scalars['String'], + status: SubmissionStatus, + statusChangedAt?: Maybe, +}; + +export enum SubmissionStatus { + Open = 'Open', + Accepted = 'Accepted', + Rejected = 'Rejected', + Deleted = 'Deleted' +} + export type SubscribeToColonyInput = { colonyAddress: Scalars['String'], }; @@ -1271,6 +1547,13 @@ export type CreateTaskFromSuggestionMutationVariables = { export type CreateTaskFromSuggestionMutation = { createTaskFromSuggestion: Maybe }; +export type CreateProgramMutationVariables = { + input: CreateProgramInput +}; + + +export type CreateProgramMutation = { createProgram: Maybe> }; + export type TaskQueryVariables = { id: Scalars['String'] }; @@ -1469,6 +1752,16 @@ export type ColonyTasksQuery = { colony: ( )> } ) }; +export type ColonyProgramsQueryVariables = { + address: Scalars['String'] +}; + + +export type ColonyProgramsQuery = { colony: ( + Pick + & { programs: Array> } + ) }; + export type ColonySubscribedUsersQueryVariables = { colonyAddress: Scalars['String'] }; @@ -2957,6 +3250,46 @@ export function useCreateTaskFromSuggestionMutation(baseOptions?: ApolloReactHoo export type CreateTaskFromSuggestionMutationHookResult = ReturnType; export type CreateTaskFromSuggestionMutationResult = ApolloReactCommon.MutationResult; export type CreateTaskFromSuggestionMutationOptions = ApolloReactCommon.BaseMutationOptions; +export const CreateProgramDocument = gql` + mutation CreateProgram($input: CreateProgramInput!) { + createProgram(input: $input) { + id + createdAt + creatorAddress + colonyAddress + title + description + levelIds + enrolledUserAddresses + status + } +} + `; +export type CreateProgramMutationFn = ApolloReactCommon.MutationFunction; + +/** + * __useCreateProgramMutation__ + * + * To run a mutation, you first call `useCreateProgramMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useCreateProgramMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [createProgramMutation, { data, loading, error }] = useCreateProgramMutation({ + * variables: { + * input: // value for 'input' + * }, + * }); + */ +export function useCreateProgramMutation(baseOptions?: ApolloReactHooks.MutationHookOptions) { + return ApolloReactHooks.useMutation(CreateProgramDocument, baseOptions); + } +export type CreateProgramMutationHookResult = ReturnType; +export type CreateProgramMutationResult = ApolloReactCommon.MutationResult; +export type CreateProgramMutationOptions = ApolloReactCommon.BaseMutationOptions; export const TaskDocument = gql` query Task($id: String!) { task(id: $id) { @@ -3745,6 +4078,50 @@ export function useColonyTasksLazyQuery(baseOptions?: ApolloReactHooks.LazyQuery export type ColonyTasksQueryHookResult = ReturnType; export type ColonyTasksLazyQueryHookResult = ReturnType; export type ColonyTasksQueryResult = ApolloReactCommon.QueryResult; +export const ColonyProgramsDocument = gql` + query ColonyPrograms($address: String!) { + colony(address: $address) { + id + programs { + id + createdAt + creatorAddress + colonyAddress + title + description + levelIds + enrolledUserAddresses + status + } + } +} + `; + +/** + * __useColonyProgramsQuery__ + * + * To run a query within a React component, call `useColonyProgramsQuery` and pass it any options that fit your needs. + * When your component renders, `useColonyProgramsQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useColonyProgramsQuery({ + * variables: { + * address: // value for 'address' + * }, + * }); + */ +export function useColonyProgramsQuery(baseOptions?: ApolloReactHooks.QueryHookOptions) { + return ApolloReactHooks.useQuery(ColonyProgramsDocument, baseOptions); + } +export function useColonyProgramsLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHookOptions) { + return ApolloReactHooks.useLazyQuery(ColonyProgramsDocument, baseOptions); + } +export type ColonyProgramsQueryHookResult = ReturnType; +export type ColonyProgramsLazyQueryHookResult = ReturnType; +export type ColonyProgramsQueryResult = ApolloReactCommon.QueryResult; export const ColonySubscribedUsersDocument = gql` query ColonySubscribedUsers($colonyAddress: String!) { colony(address: $colonyAddress) { diff --git a/src/data/index.ts b/src/data/index.ts index 8441385ae0c..276c2f7f48c 100644 --- a/src/data/index.ts +++ b/src/data/index.ts @@ -16,6 +16,7 @@ import { UserTasksQuery, UserTokensQuery, ColonySuggestionsQuery, + ColonyProgramsQuery, } from './generated'; import { loggedInUserResolvers, @@ -66,6 +67,8 @@ export type AnyColonyProfile = FullColonyFragment | ColonyProfileFragment; export type OneSuggestion = ColonySuggestionsQuery['colony']['suggestions'][number]; +export type ColonyPrograms = ColonyProgramsQuery['colony']['programs']; + export type OneToken = TokenQuery['token']; export type ColonyTokens = ColonyQuery['colony']['tokens']; export type UserTokens = UserTokensQuery['user']['tokens']; diff --git a/src/data/mutations.graphql b/src/data/mutations.graphql index b2791e61c80..a0e4fbcdf48 100644 --- a/src/data/mutations.graphql +++ b/src/data/mutations.graphql @@ -323,3 +323,17 @@ mutation CreateTaskFromSuggestion($input: CreateTaskFromSuggestionInput!) { ...CreateTaskFields } } + +mutation CreateProgram($input: CreateProgramInput!) { + createProgram(input: $input) { + id + createdAt + creatorAddress + colonyAddress + title + description + levelIds + enrolledUserAddresses + status + } +} diff --git a/src/data/queries.graphql b/src/data/queries.graphql index e56b61d3ee9..dc6ca4c4f3a 100644 --- a/src/data/queries.graphql +++ b/src/data/queries.graphql @@ -299,6 +299,23 @@ query ColonyTasks($address: String!) { } } +query ColonyPrograms($address: String!) { + colony(address: $address) { + id + programs { + id + createdAt + creatorAddress + colonyAddress + title + description + levelIds + enrolledUserAddresses + status + } + } +} + query ColonySubscribedUsers($colonyAddress: String!) { colony(address: $colonyAddress) { id diff --git a/src/modules/core/components/NavLink/NavLink.tsx b/src/modules/core/components/NavLink/NavLink.tsx index 16bcef98a1f..0d1c47beea8 100644 --- a/src/modules/core/components/NavLink/NavLink.tsx +++ b/src/modules/core/components/NavLink/NavLink.tsx @@ -26,6 +26,12 @@ interface Props { /** Values for text (react-intl interpolation) */ textValues?: MessageValues; + /** A string or a `messageDescriptor` that make up the nav link's title */ + title?: MessageDescriptor | string; + + /** Values for title (react-intl interpolation) */ + titleValues?: MessageValues; + /** @ignore injected by `react-intl` */ intl: IntlShape; } @@ -36,14 +42,26 @@ const NavLink = ({ intl: { formatMessage }, text, textValues, + title, + titleValues, to, ...linkProps }: Props) => { const linkText = typeof text === 'string' ? text : text && formatMessage(text, textValues); + const titleText = + typeof title === 'string' + ? title + : title && formatMessage(title, titleValues); + return ( - + {linkText || children} ); diff --git a/src/modules/dashboard/checks.ts b/src/modules/dashboard/checks.ts index df6364ef807..fadcffd93f7 100644 --- a/src/modules/dashboard/checks.ts +++ b/src/modules/dashboard/checks.ts @@ -131,3 +131,10 @@ export const hasUpvotedSuggestion = ( upvotes: OneSuggestion['upvotes'], userAddress: Address, ) => upvotes.includes(userAddress); + +/* + * Programs + */ + +export const canCreateProgram = (userRoles: ROLES[]) => + canAdminister(userRoles); diff --git a/src/modules/dashboard/components/ColonyHome/ColonyMeta/ColonyMeta.tsx b/src/modules/dashboard/components/ColonyHome/ColonyMeta/ColonyMeta.tsx index f3d193a1c42..60ca86a6aeb 100644 --- a/src/modules/dashboard/components/ColonyHome/ColonyMeta/ColonyMeta.tsx +++ b/src/modules/dashboard/components/ColonyHome/ColonyMeta/ColonyMeta.tsx @@ -2,20 +2,21 @@ import React, { useMemo } from 'react'; import { defineMessages, FormattedMessage } from 'react-intl'; import { COLONY_TOTAL_BALANCE_DOMAIN_ID } from '~constants'; -import { DomainsMapType } from '~types/index'; -import { AnyColonyProfile } from '~data/index'; -import { stripProtocol, multiLineTextEllipsis } from '~utils/strings'; +import Button from '~core/Button'; +import CopyableAddress from '~core/CopyableAddress'; import ExpandedParagraph from '~core/ExpandedParagraph'; +import ExternalLink from '~core/ExternalLink'; import Heading from '~core/Heading'; import Icon from '~core/Icon'; -import Button from '~core/Button'; import Link from '~core/Link'; -import ExternalLink from '~core/ExternalLink'; import HookedColonyAvatar from '~dashboard/HookedColonyAvatar'; -import CopyableAddress from '~core/CopyableAddress'; +import { AnyColonyProfile } from '~data/index'; +import { DomainsMapType } from '~types/index'; +import { multiLineTextEllipsis, stripProtocol } from '~utils/strings'; -import ColonySubscribe from './ColonySubscribe'; import ColonyInvite from './ColonyInvite'; +import ColonyPrograms from './ColonyPrograms'; +import ColonySubscribe from './ColonySubscribe'; import styles from './ColonyMeta.css'; @@ -116,6 +117,7 @@ const ColonyMeta = ({ ); + return (
@@ -164,6 +166,7 @@ const ColonyMeta = ({ {renderExpandedElements} )} +
  • diff --git a/src/modules/dashboard/components/ColonyHome/ColonyMeta/ColonyPrograms.css b/src/modules/dashboard/components/ColonyHome/ColonyMeta/ColonyPrograms.css new file mode 100644 index 00000000000..516f6f4d512 --- /dev/null +++ b/src/modules/dashboard/components/ColonyHome/ColonyMeta/ColonyPrograms.css @@ -0,0 +1,50 @@ + +.main { + padding: 25px 0; + border-bottom: 1px solid var(--temp-grey-5); +} + +.programsNav { + margin-bottom: 10px; +} + +.baseStyles { + display: flex; + align-items: center; +} + +.baseStyles + .baseStyles { + margin-top: 10px; +} + +.draft { + composes: baseStyles; +} + +.draft::after { + display: inline-block; + margin-bottom: 1px; + margin-left: 10px; + height: 8px; + width: 8px; + border-radius: 50%; + background-color: var(--golden); + content: ''; +} + +.navLink { + margin-right: 10px; + overflow: hidden; + font-weight: var(--weight-medium); + text-overflow: ellipsis; + white-space: nowrap; +} + +.draft .navLink { + /* Leave room for "pending dot" */ + max-width: 87%; +} + +.navLink:hover { + opacity: 0.8; +} diff --git a/src/modules/dashboard/components/ColonyHome/ColonyMeta/ColonyPrograms.css.d.ts b/src/modules/dashboard/components/ColonyHome/ColonyMeta/ColonyPrograms.css.d.ts new file mode 100644 index 00000000000..203e57ccc8c --- /dev/null +++ b/src/modules/dashboard/components/ColonyHome/ColonyMeta/ColonyPrograms.css.d.ts @@ -0,0 +1,5 @@ +export const main: string; +export const programsNav: string; +export const baseStyles: string; +export const draft: string; +export const navLink: string; diff --git a/src/modules/dashboard/components/ColonyHome/ColonyMeta/ColonyPrograms.tsx b/src/modules/dashboard/components/ColonyHome/ColonyMeta/ColonyPrograms.tsx new file mode 100644 index 00000000000..a62a114e20e --- /dev/null +++ b/src/modules/dashboard/components/ColonyHome/ColonyMeta/ColonyPrograms.tsx @@ -0,0 +1,155 @@ +import React, { useCallback, useState, FC } from 'react'; +import throttle from 'lodash/throttle'; +import { defineMessages, injectIntl, IntlShape } from 'react-intl'; +import { useHistory } from 'react-router'; + +import { ROOT_DOMAIN } from '~constants'; +import Button from '~core/Button'; +import NavLink from '~core/NavLink'; +import { + cacheUpdates, + ProgramStatus, + useColonyProgramsQuery, + useCreateProgramMutation, + useLoggedInUser, +} from '~data/index'; +import { Address } from '~types/index'; +import { useDataFetcher, useTransformer } from '~utils/hooks'; + +import { canCreateProgram } from '../../../checks'; +import { domainsAndRolesFetcher } from '../../../fetchers'; +import { getUserRoles } from '../../../../transformers'; + +import styles from './ColonyPrograms.css'; + +const MSG = defineMessages({ + linkUntitledProgramText: { + id: + 'dashboard.ColonyHome.ColonyMeta.ColonyPrograms.linkUntitledProgramText', + defaultMessage: 'Untitled Program', + }, + linkProgramTitleText: { + id: 'dashboard.ColonyHome.ColonyMeta.ColonyPrograms.linkProgramTitleText', + defaultMessage: '{isDraft, select, true {Draft - } false {}}{title}', + }, + buttonCreateProgram: { + id: 'dashboard.ColonyHome.ColonyMeta.ColonyPrograms.buttonCreateProgram', + defaultMessage: `Create {hasPrograms, select, + true {new} + false {a} + } program`, + }, +}); + +interface Props { + colonyAddress: Address; + colonyName: string; +} + +interface EnhancedProps extends Props { + intl: IntlShape; +} + +const displayName = 'dashboard.ColonyHome.ColonyMeta.ColonyPrograms'; + +const ColonyPrograms = ({ + colonyAddress, + colonyName, + intl: { formatMessage }, +}: EnhancedProps) => { + const [isCreatingProgram, setIsCreatingProgram] = useState(false); + + const history = useHistory(); + + const { walletAddress } = useLoggedInUser(); + + const { data: domainsAndRolesData } = useDataFetcher( + domainsAndRolesFetcher, + [colonyAddress], + [colonyAddress], + ); + const userRoles = useTransformer(getUserRoles, [ + domainsAndRolesData, + ROOT_DOMAIN, + walletAddress, + ]); + + const { data: programsData } = useColonyProgramsQuery({ + variables: { address: colonyAddress }, + }); + + const canCreate = canCreateProgram(userRoles); + + const unfilteredPrograms = + (programsData && programsData.colony.programs) || []; + + const programs = unfilteredPrograms.filter( + ({ status }) => + status === ProgramStatus.Active || + (status === ProgramStatus.Draft && canCreate), + ); + + const [createProgramFn, { error }] = useCreateProgramMutation({ + variables: { input: { colonyAddress } }, + update: cacheUpdates.createProgram(colonyAddress), + }); + const handleCreateProgram = useCallback( + throttle(async () => { + setIsCreatingProgram(true); + const mutationResult = await createProgramFn(); + const id = + mutationResult && + mutationResult.data && + mutationResult.data.createProgram && + mutationResult.data.createProgram.id; + history.replace(`/colony/${colonyName}/program/${id}`); + }, 2000), + [colonyName, createProgramFn, history], + ); + + if (!programsData || (!canCreate && programs.length === 0)) { + return null; + } + + return ( +
    + {programs.length > 0 && ( + + )} + {canCreate && ( +
    + ); +}; + +ColonyPrograms.displayName = displayName; + +export default injectIntl(ColonyPrograms) as FC;