From 394d37d65e05a393a060815261452a652484ebaf Mon Sep 17 00:00:00 2001 From: Max Kovalenko Date: Thu, 26 Dec 2019 20:20:35 -0500 Subject: [PATCH 1/3] edit products page: products list --- src/Routes.tsx | 4 +- src/client/bindings.ts | 1 + .../pricing/edit/EditProjectPricingLayout.tsx | 144 ++++++++++++++++++ .../{ => view}/ProjectPricingLayout.tsx | 23 ++- .../entity/project/view/ProjectPage.tsx | 5 +- 5 files changed, 169 insertions(+), 8 deletions(-) create mode 100644 src/layouts/entity/project/pricing/edit/EditProjectPricingLayout.tsx rename src/layouts/entity/project/pricing/{ => view}/ProjectPricingLayout.tsx (75%) diff --git a/src/Routes.tsx b/src/Routes.tsx index d357a20..de61b57 100644 --- a/src/Routes.tsx +++ b/src/Routes.tsx @@ -16,11 +16,12 @@ import BoardPage from "./layouts/entity/board/page/BoardPage"; import SubscriptionLayout from "./layouts/account/subscription/SubscriptionLayout"; import LibraryLayout from "./layouts/account/library/LibraryLayout"; import EditProjectLayout from "./layouts/entity/project/edit/EditProjectLayout"; -import ProjectPricingLayout from "./layouts/entity/project/pricing/ProjectPricingLayout"; +import ProjectPricingLayout from "./layouts/entity/project/pricing/view/ProjectPricingLayout"; import RegisterLayout from "./layouts/auth/register/RegisterLayout"; import ConfirmEmailLayout from "./layouts/auth/confirmEmail/ConfirmEmailLayout"; import NotFoundLayout from "./layouts/404/NotFoundLayout"; import HelpLayout from "./layouts/help/HelpLayout"; +import EditProjectPricingLayout from "./layouts/entity/project/pricing/edit/EditProjectPricingLayout"; class Routes extends React.Component { render() { @@ -64,6 +65,7 @@ class Routes extends React.Component { + diff --git a/src/client/bindings.ts b/src/client/bindings.ts index 5d18744..8f40534 100644 --- a/src/client/bindings.ts +++ b/src/client/bindings.ts @@ -143,6 +143,7 @@ export class ProjectProduct { project_guid?: string; usd_price?: number; duration_hours?: number; + users_count?: number; created_at?: string; updated_at?: string; } diff --git a/src/layouts/entity/project/pricing/edit/EditProjectPricingLayout.tsx b/src/layouts/entity/project/pricing/edit/EditProjectPricingLayout.tsx new file mode 100644 index 0000000..b4c9935 --- /dev/null +++ b/src/layouts/entity/project/pricing/edit/EditProjectPricingLayout.tsx @@ -0,0 +1,144 @@ +import React from "react"; +import FullPageWithSideBar from "../../../../../components/layout/simple/fullpagewithsidebar/FullPageWithSidebar"; +import {Button, Col, Divider, Icon, Row} from "antd"; +import PricingBlock from "../../../../../components/entity/product/pricing_block/single/PricingBlock"; +import {ProjectModel, ProjectProduct} from "../../../../../client/bindings"; +import {handleApiError} from "../../../../../classes/notification/errorHandler/errorHandler"; + +interface IProps { + match: { + params: { + owner: string, + alias: string + } + } +} + +interface IState { + isLoaded: boolean, + project: ProjectModel|null, + products: ProjectProduct[]|null +} + +class EditProjectPricingLayout extends React.Component { + constructor(props: IProps) { + super(props); + this.state = { + isLoaded: false, + project: null, + products: null + }; + } + + componentDidMount(): void { + this.getProjectInfo(); + } + + getProjectInfo(): void { + window.App.apiClient.getProjectByAlias(this.props.match.params.owner, this.props.match.params.alias) + .then((result) => + this.processGetProjectInfo(result._response)) + .catch((error) => handleApiError(error.response)); + } + + processGetProjectInfo(response: any) { + let json = JSON.parse(response.bodyAsText); + + this.setState({ + isLoaded: true, + project: json.data.project + }); + + this.getProducts(); + } + + getProducts() { + window.App.apiClient.getProjectProducts(this.state.project!.guid!) + .then((result) => + this.processGetProducts(result._response)) + .catch((error) => handleApiError(error.response)); + } + + processGetProducts(response: any) { + let json = JSON.parse(response.bodyAsText); + this.setState({ + products: json.data.products + }); + } + + render() { + const infoBlock = + +

+ There are multiple ways how you can sell your open source product, a few popular business models are:
+

+

+ 1. Open core model (when you have open source product and additional features built on top of it which + you're selling)
+ example: Community and Professional edition - it can be single purchase or subscription
+ 2. Sell support (your product can be 100% open source and you can earn money by offering technical support) +

+ There are much more ways to do that - you can do your own research if it's harder to decide +

+
; + + if (!this.state.isLoaded || !this.state.project) { + return +

Loading project info

+ +
; + } + + let project: ProjectModel = this.state.project; + + return +

{project.name}

+

{project.description}

+ {infoBlock} + + + + + + + {this.state.products ? this.state.products.map((product, i) => { + return
+ + + 0} + description={product.description!} + /> + + + Stats:

+ +

Users count: {product.users_count}

+
+
+ Possible actions:
+ + + + +
+ +
; + }) : null} + + + +
; + } +} + +export default EditProjectPricingLayout; diff --git a/src/layouts/entity/project/pricing/ProjectPricingLayout.tsx b/src/layouts/entity/project/pricing/view/ProjectPricingLayout.tsx similarity index 75% rename from src/layouts/entity/project/pricing/ProjectPricingLayout.tsx rename to src/layouts/entity/project/pricing/view/ProjectPricingLayout.tsx index 2ad742f..2df9308 100644 --- a/src/layouts/entity/project/pricing/ProjectPricingLayout.tsx +++ b/src/layouts/entity/project/pricing/view/ProjectPricingLayout.tsx @@ -1,9 +1,11 @@ import React from "react"; -import FullPageWithSideBar from "../../../../components/layout/simple/fullpagewithsidebar/FullPageWithSidebar"; -import {handleApiError} from "../../../../classes/notification/errorHandler/errorHandler"; -import {ProjectModel, ProjectProduct} from "../../../../client/bindings"; -import {Col, Icon, Row} from "antd"; -import PricingBlock from "../../../../components/entity/product/pricing_block/single/PricingBlock"; +import FullPageWithSideBar from "../../../../../components/layout/simple/fullpagewithsidebar/FullPageWithSidebar"; +import {handleApiError} from "../../../../../classes/notification/errorHandler/errorHandler"; +import {ProjectModel, ProjectProduct} from "../../../../../client/bindings"; +import {Button, Col, Icon, Row} from "antd"; +import PricingBlock from "../../../../../components/entity/product/pricing_block/single/PricingBlock"; +import PermissionCheck from "../../../../../components/check/permission_check/single/PermissionCheck"; +import {Link} from "react-router-dom"; interface IProps { match: { @@ -79,6 +81,17 @@ class ProjectPricingLayout extends React.Component { return

{project.name}

{project.description}

+ + + + + + + { {this.state.boards === null ? : null} {this.state.boards != null && this.state.boards.length === 0 ?
No boards for this project -

- {/* TODO: check permissions */} + {/* TODO: add board button */}
: null} {this.state.boards != null && this.state.boards.map((board: BoardModel, i: number) => { return From 66b98f3fc421560272487ae2ac1144ef42da8e12 Mon Sep 17 00:00:00 2001 From: Max Kovalenko Date: Sat, 28 Dec 2019 16:05:08 -0500 Subject: [PATCH 2/3] new project product button & logic --- src/client/models/index.ts | 34 ++++ src/client/models/mappers.ts | 34 ++++ src/client/models/parameters.ts | 32 +++- src/client/supportHubApi.ts | 73 +++++++- .../invoice/single/create/NewInvoice.tsx | 4 +- .../product/action/new/NewProductButton.tsx | 165 ++++++++++++++++++ .../pricing_block/single/PricingBlock.tsx | 13 +- .../pricing/edit/EditProjectPricingLayout.tsx | 23 +-- src/layouts/help/HelpLayout.tsx | 4 + swagger.json | 71 +++++++- 10 files changed, 430 insertions(+), 23 deletions(-) create mode 100644 src/components/entity/product/action/new/NewProductButton.tsx diff --git a/src/client/models/index.ts b/src/client/models/index.ts index c966f64..fbc6123 100644 --- a/src/client/models/index.ts +++ b/src/client/models/index.ts @@ -913,6 +913,20 @@ export interface GetProjectProductsOKResponse { data?: GetProjectProductsOKResponseData; } +/** + * An interface representing PostProjectProductCreatedResponseData. + */ +export interface PostProjectProductCreatedResponseData { + product?: ProjectProduct; +} + +/** + * An interface representing PostProjectProductCreatedResponse. + */ +export interface PostProjectProductCreatedResponse { + data?: PostProjectProductCreatedResponseData; +} + /** * An interface representing SupportHubApiOptions. */ @@ -1988,3 +2002,23 @@ export type GetProjectProductsResponse = GetProjectProductsOKResponse & { parsedBody: GetProjectProductsOKResponse; }; }; + +/** + * Contains response data for the postProjectProduct operation. + */ +export type PostProjectProductResponse = PostProjectProductCreatedResponse & { + /** + * The underlying HTTP response. + */ + _response: msRest.HttpResponse & { + /** + * The response body as text (string format) + */ + bodyAsText: string; + + /** + * The response body as parsed JSON or XML + */ + parsedBody: PostProjectProductCreatedResponse; + }; +}; diff --git a/src/client/models/mappers.ts b/src/client/models/mappers.ts index 028436b..7cf83b1 100644 --- a/src/client/models/mappers.ts +++ b/src/client/models/mappers.ts @@ -2498,3 +2498,37 @@ export const GetProjectProductsOKResponse: msRest.CompositeMapper = { } } }; + +export const PostProjectProductCreatedResponseData: msRest.CompositeMapper = { + serializedName: "PostProjectProductCreatedResponse_data", + type: { + name: "Composite", + className: "PostProjectProductCreatedResponseData", + modelProperties: { + product: { + serializedName: "product", + type: { + name: "Composite", + className: "ProjectProduct" + } + } + } + } +}; + +export const PostProjectProductCreatedResponse: msRest.CompositeMapper = { + serializedName: "PostProjectProductCreatedResponse", + type: { + name: "Composite", + className: "PostProjectProductCreatedResponse", + modelProperties: { + data: { + serializedName: "data", + type: { + name: "Composite", + className: "PostProjectProductCreatedResponseData" + } + } + } + } +}; diff --git a/src/client/models/parameters.ts b/src/client/models/parameters.ts index 3284a3a..871d674 100644 --- a/src/client/models/parameters.ts +++ b/src/client/models/parameters.ts @@ -134,7 +134,7 @@ export const currencyType: msRest.OperationQueryParameter = { } } }; -export const description: msRest.OperationQueryParameter = { +export const description0: msRest.OperationQueryParameter = { parameterPath: [ "options", "description" @@ -146,6 +146,16 @@ export const description: msRest.OperationQueryParameter = { } } }; +export const description1: msRest.OperationQueryParameter = { + parameterPath: "description", + mapper: { + required: true, + serializedName: "description", + type: { + name: "String" + } + } +}; export const email0: msRest.OperationQueryParameter = { parameterPath: [ "options", @@ -364,6 +374,26 @@ export const status: msRest.OperationQueryParameter = { } } }; +export const url: msRest.OperationQueryParameter = { + parameterPath: "url", + mapper: { + required: true, + serializedName: "url", + type: { + name: "String" + } + } +}; +export const useUrl: msRest.OperationQueryParameter = { + parameterPath: "useUrl", + mapper: { + required: true, + serializedName: "use_url", + type: { + name: "String" + } + } +}; export const value: msRest.OperationQueryParameter = { parameterPath: "value", mapper: { diff --git a/src/client/supportHubApi.ts b/src/client/supportHubApi.ts index 9c89a4b..2100c4f 100644 --- a/src/client/supportHubApi.ts +++ b/src/client/supportHubApi.ts @@ -1310,6 +1310,53 @@ class SupportHubApi extends SupportHubApiContext { getProjectProductsOperationSpec, callback) as Promise; } + + /** + * @param apiToken JWT token + * @param projectGuid + * @param name + * @param description + * @param url + * @param useUrl + * @param [options] The optional parameters + * @returns Promise + */ + postProjectProduct(apiToken: string, projectGuid: string, name: string, description: string, url: string, useUrl: string, options?: msRest.RequestOptionsBase): Promise; + /** + * @param apiToken JWT token + * @param projectGuid + * @param name + * @param description + * @param url + * @param useUrl + * @param callback The callback + */ + postProjectProduct(apiToken: string, projectGuid: string, name: string, description: string, url: string, useUrl: string, callback: msRest.ServiceCallback): void; + /** + * @param apiToken JWT token + * @param projectGuid + * @param name + * @param description + * @param url + * @param useUrl + * @param options The optional parameters + * @param callback The callback + */ + postProjectProduct(apiToken: string, projectGuid: string, name: string, description: string, url: string, useUrl: string, options: msRest.RequestOptionsBase, callback: msRest.ServiceCallback): void; + postProjectProduct(apiToken: string, projectGuid: string, name: string, description: string, url: string, useUrl: string, options?: msRest.RequestOptionsBase | msRest.ServiceCallback, callback?: msRest.ServiceCallback): Promise { + return this.sendOperationRequest( + { + apiToken, + projectGuid, + name, + description, + url, + useUrl, + options + }, + postProjectProductOperationSpec, + callback) as Promise; + } } // Operation Specifications @@ -1500,7 +1547,7 @@ const editProjectOperationSpec: msRest.OperationSpec = { queryParameters: [ Parameters.apiToken, Parameters.projectGuid, - Parameters.description + Parameters.description0 ], responses: { 200: { @@ -1594,7 +1641,7 @@ const createCardOperationSpec: msRest.OperationSpec = { Parameters.apiToken, Parameters.columnGuid0, Parameters.name0, - Parameters.description, + Parameters.description0, Parameters.columnOrder ], responses: { @@ -1613,7 +1660,7 @@ const editCardOperationSpec: msRest.OperationSpec = { Parameters.apiToken, Parameters.cardGuid, Parameters.name1, - Parameters.description, + Parameters.description0, Parameters.columnOrder, Parameters.columnGuid1 ], @@ -2011,6 +2058,26 @@ const getProjectProductsOperationSpec: msRest.OperationSpec = { serializer }; +const postProjectProductOperationSpec: msRest.OperationSpec = { + httpMethod: "POST", + path: "api/v1/project/product/new", + queryParameters: [ + Parameters.apiToken, + Parameters.projectGuid, + Parameters.name0, + Parameters.description1, + Parameters.url, + Parameters.useUrl + ], + responses: { + 201: { + bodyMapper: Mappers.PostProjectProductCreatedResponse + }, + default: {} + }, + serializer +}; + export { SupportHubApi, SupportHubApiContext, diff --git a/src/components/entity/invoice/single/create/NewInvoice.tsx b/src/components/entity/invoice/single/create/NewInvoice.tsx index b915176..4f38cf9 100644 --- a/src/components/entity/invoice/single/create/NewInvoice.tsx +++ b/src/components/entity/invoice/single/create/NewInvoice.tsx @@ -172,8 +172,6 @@ class NewInvoice extends React.Component { } render() { - const currencyTypes = this.getCurrencyTypes(); - return
@@ -211,7 +209,7 @@ class NewInvoice extends React.Component { }} onSelect={(value) => this.onCurrencySelected(value)} > - {currencyTypes.map((option: any, i: number) => { + {this.getCurrencyTypes().map((option: any, i: number) => { return ; })} diff --git a/src/components/entity/product/action/new/NewProductButton.tsx b/src/components/entity/product/action/new/NewProductButton.tsx new file mode 100644 index 0000000..2b3781f --- /dev/null +++ b/src/components/entity/product/action/new/NewProductButton.tsx @@ -0,0 +1,165 @@ +import React, {SyntheticEvent} from "react"; +import {Button, Col, Input, Modal, notification, Row} from "antd"; +import {handleApiError} from "../../../../../classes/notification/errorHandler/errorHandler"; + +const { TextArea } = Input; + +interface IProps { + projectGuid: string +} + +interface IState { + showModal: boolean, + isLoading: boolean, + form: { + name: string, + description: string, + amount: number, + url: string, + use_url: string + } +} + +class NewProductButton extends React.Component { + constructor(props: IProps) { + super(props); + this.state = { + showModal: false, + isLoading: false, + form: { + name: 'Pro edition', + description: 'product description', + url: '', + use_url: '', + amount: 0 + } + } + } + + updateForm(field: string, val: string) { + let form: any = this.state.form; + form[field] = val; + this.setState({form}); + } + + updatedAmount(e: SyntheticEvent) { + const target: any = e.target; + + const floatVal = parseFloat(target.value); + + if (floatVal <= 0) return; + + let form = this.state.form; + form.amount = floatVal; + this.setState({form}); + } + + createProduct() { + let form = this.state.form; + window.App.apiClient.postProjectProduct( + window.App.apiToken!, this.props.projectGuid, form.name, form.description, form.url, form.use_url + ) + .then(() => { + notification['success']({ + message: 'Product was created successfully!' + }); + setTimeout(() => { + window.location.reload(); + }, 1500); + }) + .catch((error) => handleApiError(error.response)); + } + + render() { + return
+ + Create new product + } + visible={this.state.showModal} + width={window.innerWidth < 1000 ? "90%" : "40%"} + onCancel={() => { + this.setState({showModal: false}) + }} + footer={null} + > + + + Product name + + + {this.updateForm('name', e.target.value)}} + defaultValue={'Professional edition'} + /> + + + + + Description + + +