From fe883edf6d711cb9dc282b9df326ab06af4eb674 Mon Sep 17 00:00:00 2001 From: Aigamo <51428094+ycanardeau@users.noreply.github.com> Date: Sat, 3 Jul 2021 16:19:01 +1000 Subject: [PATCH 1/4] Install mobx --- VocaDbWeb/package-lock.json | 43 +++++++++++++++++++++++++++++++++++++ VocaDbWeb/package.json | 2 ++ VocaDbWeb/tsconfig.json | 5 ++++- VocaDbWeb/webpack.mix.js | 1 + 4 files changed, 50 insertions(+), 1 deletion(-) diff --git a/VocaDbWeb/package-lock.json b/VocaDbWeb/package-lock.json index 9caf957941..012be95985 100644 --- a/VocaDbWeb/package-lock.json +++ b/VocaDbWeb/package-lock.json @@ -19,6 +19,8 @@ "knockout-punches": "^0.5.1", "lodash": "^4.17.19", "marked": "^0.3.18", + "mobx": "^6.3.2", + "mobx-react-lite": "^3.2.0", "moment": "^2.17.0", "qtip2": "^3.0.3", "react": "^17.0.2", @@ -13392,6 +13394,36 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/mobx": { + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/mobx/-/mobx-6.3.2.tgz", + "integrity": "sha512-xGPM9dIE1qkK9Nrhevp0gzpsmELKU4MFUJRORW/jqxVFIHHWIoQrjDjL8vkwoJYY3C2CeVJqgvl38hgKTalTWg==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mobx" + } + }, + "node_modules/mobx-react-lite": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mobx-react-lite/-/mobx-react-lite-3.2.0.tgz", + "integrity": "sha512-q5+UHIqYCOpBoFm/PElDuOhbcatvTllgRp3M1s+Hp5j0Z6XNgDbgqxawJ0ZAUEyKM8X1zs70PCuhAIzX1f4Q/g==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mobx" + }, + "peerDependencies": { + "mobx": "^6.1.0", + "react": "^16.8.0 || ^17" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, "node_modules/moment": { "version": "2.17.0", "resolved": "https://registry.npmjs.org/moment/-/moment-2.17.0.tgz", @@ -31244,6 +31276,17 @@ "minimist": "^1.2.5" } }, + "mobx": { + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/mobx/-/mobx-6.3.2.tgz", + "integrity": "sha512-xGPM9dIE1qkK9Nrhevp0gzpsmELKU4MFUJRORW/jqxVFIHHWIoQrjDjL8vkwoJYY3C2CeVJqgvl38hgKTalTWg==" + }, + "mobx-react-lite": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mobx-react-lite/-/mobx-react-lite-3.2.0.tgz", + "integrity": "sha512-q5+UHIqYCOpBoFm/PElDuOhbcatvTllgRp3M1s+Hp5j0Z6XNgDbgqxawJ0ZAUEyKM8X1zs70PCuhAIzX1f4Q/g==", + "requires": {} + }, "moment": { "version": "2.17.0", "resolved": "https://registry.npmjs.org/moment/-/moment-2.17.0.tgz", diff --git a/VocaDbWeb/package.json b/VocaDbWeb/package.json index 10b025140b..fcf4a65397 100644 --- a/VocaDbWeb/package.json +++ b/VocaDbWeb/package.json @@ -64,6 +64,8 @@ "knockout-punches": "^0.5.1", "lodash": "^4.17.19", "marked": "^0.3.18", + "mobx": "^6.3.2", + "mobx-react-lite": "^3.2.0", "moment": "^2.17.0", "qtip2": "^3.0.3", "react": "^17.0.2", diff --git a/VocaDbWeb/tsconfig.json b/VocaDbWeb/tsconfig.json index 9248270f41..c9b6efffac 100644 --- a/VocaDbWeb/tsconfig.json +++ b/VocaDbWeb/tsconfig.json @@ -20,9 +20,12 @@ "@Models/*": ["Scripts/Models/*"], "@Repositories/*": ["Scripts/Repositories/*"], "@Shared/*": ["Scripts/Shared/*"], + "@Stores/*": ["Scripts/Stores/*"], "@ViewModels/*": ["Scripts/ViewModels/*"] }, - "resolveJsonModule": true + "resolveJsonModule": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true }, "include": ["Scripts/**/*"] } diff --git a/VocaDbWeb/webpack.mix.js b/VocaDbWeb/webpack.mix.js index 27f80a6c28..bf3a038deb 100644 --- a/VocaDbWeb/webpack.mix.js +++ b/VocaDbWeb/webpack.mix.js @@ -34,6 +34,7 @@ mix '@Models': path.join(__dirname, 'Scripts/Models'), '@Repositories': path.join(__dirname, 'Scripts/Repositories'), '@Shared': path.join(__dirname, 'Scripts/Shared'), + '@Stores': path.join(__dirname, 'Scripts/Stores'), '@ViewModels': path.join(__dirname, 'Scripts/ViewModels'), }) .eslint({ From e55d20b563fadb9c390cf8bc943fc6f1c0671965 Mon Sep 17 00:00:00 2001 From: Aigamo <51428094+ycanardeau@users.noreply.github.com> Date: Tue, 6 Jul 2021 21:06:52 +1000 Subject: [PATCH 2/4] Create ServerSidePaging --- .../Shared/Partials/EntryCountBox.tsx | 25 ++++++ .../Shared/Partials/Knockout/EntryCount.tsx | 43 ++++++++++ .../Partials/Knockout/ServerSidePaging.tsx | 61 +++++++++++++ .../Scripts/Stores/ServerSidePagingStore.ts | 86 +++++++++++++++++++ 4 files changed, 215 insertions(+) create mode 100644 VocaDbWeb/Scripts/Components/Shared/Partials/EntryCountBox.tsx create mode 100644 VocaDbWeb/Scripts/Components/Shared/Partials/Knockout/EntryCount.tsx create mode 100644 VocaDbWeb/Scripts/Components/Shared/Partials/Knockout/ServerSidePaging.tsx create mode 100644 VocaDbWeb/Scripts/Stores/ServerSidePagingStore.ts diff --git a/VocaDbWeb/Scripts/Components/Shared/Partials/EntryCountBox.tsx b/VocaDbWeb/Scripts/Components/Shared/Partials/EntryCountBox.tsx new file mode 100644 index 0000000000..5f8e319004 --- /dev/null +++ b/VocaDbWeb/Scripts/Components/Shared/Partials/EntryCountBox.tsx @@ -0,0 +1,25 @@ +import ServerSidePagingStore from '@Stores/ServerSidePagingStore'; +import { observer } from 'mobx-react-lite'; +import React from 'react'; + +import EntryCount from './Knockout/EntryCount'; + +interface EntryCountBoxProps { + pagingStore: ServerSidePagingStore; + selections?: number[]; +} + +const EntryCountBox = observer( + ({ + pagingStore, + selections = [10, 20, 40, 100], + }: EntryCountBoxProps): React.ReactElement => { + return ( +
+ +
+ ); + }, +); + +export default EntryCountBox; diff --git a/VocaDbWeb/Scripts/Components/Shared/Partials/Knockout/EntryCount.tsx b/VocaDbWeb/Scripts/Components/Shared/Partials/Knockout/EntryCount.tsx new file mode 100644 index 0000000000..bd4be7ad8b --- /dev/null +++ b/VocaDbWeb/Scripts/Components/Shared/Partials/Knockout/EntryCount.tsx @@ -0,0 +1,43 @@ +import Dropdown from '@Bootstrap/Dropdown'; +import SafeAnchor from '@Bootstrap/SafeAnchor'; +import ServerSidePagingStore from '@Stores/ServerSidePagingStore'; +import { observer } from 'mobx-react-lite'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +interface EntryCountProps { + pagingStore: ServerSidePagingStore; + selections?: number[]; +} + +const EntryCount = observer( + ({ + pagingStore, + selections = [10, 20, 40, 100], + }: EntryCountProps): React.ReactElement => { + const { t } = useTranslation(['ViewRes.Search']); + + return ( + + + {t('ViewRes.Search:Index.ShowingItemsOf', { + 0: pagingStore.pageSize, + 1: pagingStore.totalItems, + })} + + + {selections.map((selection) => ( + pagingStore.setPageSize(selection)} + key={selection} + > + {t('ViewRes.Search:Index.ItemsPerPage', { 0: selection })} + + ))} + + + ); + }, +); + +export default EntryCount; diff --git a/VocaDbWeb/Scripts/Components/Shared/Partials/Knockout/ServerSidePaging.tsx b/VocaDbWeb/Scripts/Components/Shared/Partials/Knockout/ServerSidePaging.tsx new file mode 100644 index 0000000000..1819fedd2a --- /dev/null +++ b/VocaDbWeb/Scripts/Components/Shared/Partials/Knockout/ServerSidePaging.tsx @@ -0,0 +1,61 @@ +import Pagination from '@Bootstrap/Pagination'; +import ServerSidePagingStore from '@Stores/ServerSidePagingStore'; +import { observer } from 'mobx-react-lite'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +interface ServerSidePagingProps { + pagingStore: ServerSidePagingStore; +} + +const ServerSidePaging = observer( + ({ pagingStore }: ServerSidePagingProps): React.ReactElement => { + const { t } = useTranslation(['VocaDb.Web.Resources.Other']); + + return ( + + + «« {t('VocaDb.Web.Resources.Other:PagedList.First')} + + + « {t('VocaDb.Web.Resources.Other:PagedList.Previous')} + + + {pagingStore.showMoreBegin && } + + {pagingStore.pages.map((page) => ( + pagingStore.setPage(page)} + key={page} + > + {page} + + ))} + + {pagingStore.showMoreEnd && } + + + {t('VocaDb.Web.Resources.Other:PagedList.Next')} » + + + {t('VocaDb.Web.Resources.Other:PagedList.Last')} »» + + + ); + }, +); + +export default ServerSidePaging; diff --git a/VocaDbWeb/Scripts/Stores/ServerSidePagingStore.ts b/VocaDbWeb/Scripts/Stores/ServerSidePagingStore.ts new file mode 100644 index 0000000000..e9569268fc --- /dev/null +++ b/VocaDbWeb/Scripts/Stores/ServerSidePagingStore.ts @@ -0,0 +1,86 @@ +import _ from 'lodash'; +import { action, computed, makeObservable, observable } from 'mobx'; + +export default class ServerSidePagingStore { + @observable public page = 1; + @action public setPage = (value: number): void => { + this.page = value; + }; + + @observable public totalItems = 0; + @action public setTotalItems = (value: number): void => { + this.totalItems = value; + }; + + @observable public pageSize = 10; + @action public setPageSize = (value: number): void => { + this.pageSize = value; + }; + + public constructor(pageSize: number = 10) { + makeObservable(this); + + this.pageSize = pageSize; + } + + @computed public get firstItem(): number { + return (this.page - 1) * this.pageSize; + } + + @computed public get totalPages(): number { + return Math.ceil(this.totalItems / this.pageSize); + } + + @computed public get hasMultiplePages(): boolean { + return this.totalPages > 1; + } + + @computed public get isFirstPage(): boolean { + return this.page <= 1; + } + + @computed public get isLastPage(): boolean { + return this.page >= this.totalPages; + } + + @computed public get pages(): number[] { + const start = Math.max(this.page - 4, 1); + const end = Math.min(this.page + 4, this.totalPages); + + return _.range(start, end + 1); + } + + @computed public get showMoreBegin(): boolean { + return this.page > 5; + } + + @computed public get showMoreEnd(): boolean { + return this.page < this.totalPages - 4; + } + + public getPagingProperties = ( + clearResults: boolean = false, + ): { start: number; maxEntries: number; getTotalCount: boolean } => { + return { + start: this.firstItem, + maxEntries: this.pageSize, + getTotalCount: clearResults || this.totalItems === 0, + }; + }; + + @action public goToFirstPage = (): void => { + this.page = 1; + }; + + @action public goToLastPage = (): void => { + this.page = this.totalPages; + }; + + @action public nextPage = (): void => { + if (!this.isLastPage) this.page = this.page + 1; + }; + + @action public previousPage = (): void => { + if (!this.isFirstPage) this.page = this.page - 1; + }; +} From 45c6a561694f2f6fa891b6a4bf1b472a9261532f Mon Sep 17 00:00:00 2001 From: Aigamo <51428094+ycanardeau@users.noreply.github.com> Date: Fri, 9 Jul 2021 21:31:39 +1000 Subject: [PATCH 3/4] Use SafeAnchor regardless of whether or not PageItem is active --- VocaDbWeb/Scripts/Bootstrap/PageItem.tsx | 2 +- .../Components/Shared/Partials/Knockout/ServerSidePaging.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/VocaDbWeb/Scripts/Bootstrap/PageItem.tsx b/VocaDbWeb/Scripts/Bootstrap/PageItem.tsx index fc64f93246..6455a4f0d3 100644 --- a/VocaDbWeb/Scripts/Bootstrap/PageItem.tsx +++ b/VocaDbWeb/Scripts/Bootstrap/PageItem.tsx @@ -33,7 +33,7 @@ const PageItem: BsPrefixRefForwardingComponent< }: PageItemProps, ref, ) => { - const Component = active || disabled ? 'span' : SafeAnchor; + const Component = SafeAnchor; return (
  • + Date: Fri, 9 Jul 2021 21:47:18 +1000 Subject: [PATCH 4/4] Use PagingProperties --- VocaDbWeb/Scripts/Stores/ServerSidePagingStore.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/VocaDbWeb/Scripts/Stores/ServerSidePagingStore.ts b/VocaDbWeb/Scripts/Stores/ServerSidePagingStore.ts index e9569268fc..7b78a52680 100644 --- a/VocaDbWeb/Scripts/Stores/ServerSidePagingStore.ts +++ b/VocaDbWeb/Scripts/Stores/ServerSidePagingStore.ts @@ -1,3 +1,4 @@ +import PagingProperties from '@DataContracts/PagingPropertiesContract'; import _ from 'lodash'; import { action, computed, makeObservable, observable } from 'mobx'; @@ -60,7 +61,7 @@ export default class ServerSidePagingStore { public getPagingProperties = ( clearResults: boolean = false, - ): { start: number; maxEntries: number; getTotalCount: boolean } => { + ): PagingProperties => { return { start: this.firstItem, maxEntries: this.pageSize,