From 02fefd1aa0774d61551908e47291dbd4b9f27017 Mon Sep 17 00:00:00 2001
From: eniolam1000752
Date: Mon, 27 Jun 2022 16:23:03 +0100
Subject: [PATCH 01/25] feature: created expore domain module
---
.../BlockchainApplicationDetails/BlockchainApplicationDetails.css | 0
.../BlockchainApplicationDetails/BlockchainApplicationDetails.js | 0
.../BlockchainApplicationDetails.test.js | 0
.../explore/components/BlockchainApplicationDetails/index.js | 0
src/modules/explore/const/index.js | 0
src/modules/explore/context/.gitkeep | 0
src/modules/explore/hooks/index.js | 0
src/modules/explore/manager/.gitkeep | 0
src/modules/explore/store/.gitkeep | 0
src/modules/explore/utils/index.js | 0
10 files changed, 0 insertions(+), 0 deletions(-)
create mode 100644 src/modules/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.css
create mode 100644 src/modules/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.js
create mode 100644 src/modules/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.test.js
create mode 100644 src/modules/explore/components/BlockchainApplicationDetails/index.js
create mode 100644 src/modules/explore/const/index.js
create mode 100644 src/modules/explore/context/.gitkeep
create mode 100644 src/modules/explore/hooks/index.js
create mode 100644 src/modules/explore/manager/.gitkeep
create mode 100644 src/modules/explore/store/.gitkeep
create mode 100644 src/modules/explore/utils/index.js
diff --git a/src/modules/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.css b/src/modules/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.css
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/modules/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.js b/src/modules/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.js
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/modules/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.test.js b/src/modules/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.test.js
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/modules/explore/components/BlockchainApplicationDetails/index.js b/src/modules/explore/components/BlockchainApplicationDetails/index.js
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/modules/explore/const/index.js b/src/modules/explore/const/index.js
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/modules/explore/context/.gitkeep b/src/modules/explore/context/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/modules/explore/hooks/index.js b/src/modules/explore/hooks/index.js
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/modules/explore/manager/.gitkeep b/src/modules/explore/manager/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/modules/explore/store/.gitkeep b/src/modules/explore/store/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/modules/explore/utils/index.js b/src/modules/explore/utils/index.js
new file mode 100644
index 0000000000..e69de29bb2
From fe626b2618eaae48921bc9db189af8776ffd4b5c Mon Sep 17 00:00:00 2001
From: eniolam1000752
Date: Mon, 27 Jun 2022 17:16:21 +0100
Subject: [PATCH 02/25] removed empty index.js files
---
src/modules/explore/const/{index.js => .gitkeep} | 0
src/modules/explore/hooks/{index.js => .gitkeep} | 0
src/modules/explore/utils/{index.js => .gitkeep} | 0
3 files changed, 0 insertions(+), 0 deletions(-)
rename src/modules/explore/const/{index.js => .gitkeep} (100%)
rename src/modules/explore/hooks/{index.js => .gitkeep} (100%)
rename src/modules/explore/utils/{index.js => .gitkeep} (100%)
diff --git a/src/modules/explore/const/index.js b/src/modules/explore/const/.gitkeep
similarity index 100%
rename from src/modules/explore/const/index.js
rename to src/modules/explore/const/.gitkeep
diff --git a/src/modules/explore/hooks/index.js b/src/modules/explore/hooks/.gitkeep
similarity index 100%
rename from src/modules/explore/hooks/index.js
rename to src/modules/explore/hooks/.gitkeep
diff --git a/src/modules/explore/utils/index.js b/src/modules/explore/utils/.gitkeep
similarity index 100%
rename from src/modules/explore/utils/index.js
rename to src/modules/explore/utils/.gitkeep
From 4adb21fbd3d6b0987dbd8d56670ebe8a589bd346 Mon Sep 17 00:00:00 2001
From: eniolam1000752
Date: Tue, 28 Jun 2022 08:57:07 +0100
Subject: [PATCH 03/25] fixed directory structure
---
src/modules/{ => blockchainApplication}/explore/const/.gitkeep | 0
src/modules/{ => blockchainApplication}/explore/context/.gitkeep | 0
src/modules/{ => blockchainApplication}/explore/hooks/.gitkeep | 0
src/modules/{ => blockchainApplication}/explore/manager/.gitkeep | 0
src/modules/{ => blockchainApplication}/explore/store/.gitkeep | 0
src/modules/{ => blockchainApplication}/explore/utils/.gitkeep | 0
.../manage/components/.gitkeep} | 0
.../manage/const/.gitkeep} | 0
.../manage/context/.gitkeep} | 0
.../index.js => blockchainApplication/manage/hooks/.gitkeep} | 0
src/modules/blockchainApplication/manage/manager/.gitkeep | 0
src/modules/blockchainApplication/manage/store/.gitkeep | 0
src/modules/blockchainApplication/manage/utils/.gitkeep | 0
13 files changed, 0 insertions(+), 0 deletions(-)
rename src/modules/{ => blockchainApplication}/explore/const/.gitkeep (100%)
rename src/modules/{ => blockchainApplication}/explore/context/.gitkeep (100%)
rename src/modules/{ => blockchainApplication}/explore/hooks/.gitkeep (100%)
rename src/modules/{ => blockchainApplication}/explore/manager/.gitkeep (100%)
rename src/modules/{ => blockchainApplication}/explore/store/.gitkeep (100%)
rename src/modules/{ => blockchainApplication}/explore/utils/.gitkeep (100%)
rename src/modules/{explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.css => blockchainApplication/manage/components/.gitkeep} (100%)
rename src/modules/{explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.js => blockchainApplication/manage/const/.gitkeep} (100%)
rename src/modules/{explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.test.js => blockchainApplication/manage/context/.gitkeep} (100%)
rename src/modules/{explore/components/BlockchainApplicationDetails/index.js => blockchainApplication/manage/hooks/.gitkeep} (100%)
create mode 100644 src/modules/blockchainApplication/manage/manager/.gitkeep
create mode 100644 src/modules/blockchainApplication/manage/store/.gitkeep
create mode 100644 src/modules/blockchainApplication/manage/utils/.gitkeep
diff --git a/src/modules/explore/const/.gitkeep b/src/modules/blockchainApplication/explore/const/.gitkeep
similarity index 100%
rename from src/modules/explore/const/.gitkeep
rename to src/modules/blockchainApplication/explore/const/.gitkeep
diff --git a/src/modules/explore/context/.gitkeep b/src/modules/blockchainApplication/explore/context/.gitkeep
similarity index 100%
rename from src/modules/explore/context/.gitkeep
rename to src/modules/blockchainApplication/explore/context/.gitkeep
diff --git a/src/modules/explore/hooks/.gitkeep b/src/modules/blockchainApplication/explore/hooks/.gitkeep
similarity index 100%
rename from src/modules/explore/hooks/.gitkeep
rename to src/modules/blockchainApplication/explore/hooks/.gitkeep
diff --git a/src/modules/explore/manager/.gitkeep b/src/modules/blockchainApplication/explore/manager/.gitkeep
similarity index 100%
rename from src/modules/explore/manager/.gitkeep
rename to src/modules/blockchainApplication/explore/manager/.gitkeep
diff --git a/src/modules/explore/store/.gitkeep b/src/modules/blockchainApplication/explore/store/.gitkeep
similarity index 100%
rename from src/modules/explore/store/.gitkeep
rename to src/modules/blockchainApplication/explore/store/.gitkeep
diff --git a/src/modules/explore/utils/.gitkeep b/src/modules/blockchainApplication/explore/utils/.gitkeep
similarity index 100%
rename from src/modules/explore/utils/.gitkeep
rename to src/modules/blockchainApplication/explore/utils/.gitkeep
diff --git a/src/modules/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.css b/src/modules/blockchainApplication/manage/components/.gitkeep
similarity index 100%
rename from src/modules/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.css
rename to src/modules/blockchainApplication/manage/components/.gitkeep
diff --git a/src/modules/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.js b/src/modules/blockchainApplication/manage/const/.gitkeep
similarity index 100%
rename from src/modules/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.js
rename to src/modules/blockchainApplication/manage/const/.gitkeep
diff --git a/src/modules/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.test.js b/src/modules/blockchainApplication/manage/context/.gitkeep
similarity index 100%
rename from src/modules/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.test.js
rename to src/modules/blockchainApplication/manage/context/.gitkeep
diff --git a/src/modules/explore/components/BlockchainApplicationDetails/index.js b/src/modules/blockchainApplication/manage/hooks/.gitkeep
similarity index 100%
rename from src/modules/explore/components/BlockchainApplicationDetails/index.js
rename to src/modules/blockchainApplication/manage/hooks/.gitkeep
diff --git a/src/modules/blockchainApplication/manage/manager/.gitkeep b/src/modules/blockchainApplication/manage/manager/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/modules/blockchainApplication/manage/store/.gitkeep b/src/modules/blockchainApplication/manage/store/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/modules/blockchainApplication/manage/utils/.gitkeep b/src/modules/blockchainApplication/manage/utils/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
From 9f515d5966c05346fc636dc8609e250f46485ff7 Mon Sep 17 00:00:00 2001
From: eniolam1000752
Date: Tue, 28 Jun 2022 08:57:51 +0100
Subject: [PATCH 04/25] added keep to components folder
---
src/modules/blockchainApplication/explore/components/.gitkeep | 0
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 src/modules/blockchainApplication/explore/components/.gitkeep
diff --git a/src/modules/blockchainApplication/explore/components/.gitkeep b/src/modules/blockchainApplication/explore/components/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
From 34bb50f1559cfc0c628b4e14ced6f6c436f272aa Mon Sep 17 00:00:00 2001
From: eniolam1000752
Date: Tue, 28 Jun 2022 20:02:19 +0100
Subject: [PATCH 05/25] feature: implemented app details component
---
packages/views/screens/router/routes.js | 4 +
packages/views/screens/router/routesMap.js | 2 +
.../react/assets/images/icons/chain-link.svg | 3 +
setup/react/assets/images/icons/pinned.svg | 3 +
setup/react/assets/images/icons/unpinned.svg | 3 +
src/locales/en/common.json | 4 +
.../BlockchainApplicationDetails.css | 121 ++++++++++++++++++
.../BlockchainApplicationDetails.js | 115 +++++++++++++++++
.../BlockchainApplicationDetails.test.js | 0
.../BlockchainApplicationDetails/index.js | 0
src/theme/Icon/index.js | 6 +
11 files changed, 261 insertions(+)
create mode 100644 setup/react/assets/images/icons/chain-link.svg
create mode 100644 setup/react/assets/images/icons/pinned.svg
create mode 100644 setup/react/assets/images/icons/unpinned.svg
create mode 100644 src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.css
create mode 100644 src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.js
create mode 100644 src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.test.js
create mode 100644 src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/index.js
diff --git a/packages/views/screens/router/routes.js b/packages/views/screens/router/routes.js
index b0b5f4ec12..8461dfaf7d 100644
--- a/packages/views/screens/router/routes.js
+++ b/packages/views/screens/router/routes.js
@@ -214,4 +214,8 @@ export const modals = {
isPrivate: true,
forbiddenTokens: [],
},
+ blockChainApplicationDetails: {
+ isPrivate: false,
+ forbiddenTokens: [],
+ },
};
diff --git a/packages/views/screens/router/routesMap.js b/packages/views/screens/router/routesMap.js
index f1e059f6c2..45fb13fe16 100644
--- a/packages/views/screens/router/routesMap.js
+++ b/packages/views/screens/router/routesMap.js
@@ -41,6 +41,7 @@ import SwitchAccount from '@account/components/SwitchAccount';
import BackupRecoveryPhraseFlow from '@account/components/BackupRecoveryPhraseFlow';
import RemoveCurrentAccountFlow from 'src/modules/account/components/RemoveCurrentAccountFlow';
import RemoveSelectedAccountFlow from 'src/modules/account/components/RemoveSelectedAccountFlow';
+import BlockchainApplicationDetails from 'src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails';
export default {
wallet: AccountDetails,
@@ -87,4 +88,5 @@ export default {
removeCurrentAccountFlow: RemoveCurrentAccountFlow,
removeSelectedAccount: RemoveSelectedAccountFlow,
addAccountByFile: AddAccountByFile,
+ blockChainApplicationDetails: BlockchainApplicationDetails,
};
diff --git a/setup/react/assets/images/icons/chain-link.svg b/setup/react/assets/images/icons/chain-link.svg
new file mode 100644
index 0000000000..726f561d34
--- /dev/null
+++ b/setup/react/assets/images/icons/chain-link.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/setup/react/assets/images/icons/pinned.svg b/setup/react/assets/images/icons/pinned.svg
new file mode 100644
index 0000000000..467c2937d7
--- /dev/null
+++ b/setup/react/assets/images/icons/pinned.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/setup/react/assets/images/icons/unpinned.svg b/setup/react/assets/images/icons/unpinned.svg
new file mode 100644
index 0000000000..c8b1afac44
--- /dev/null
+++ b/setup/react/assets/images/icons/unpinned.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/locales/en/common.json b/src/locales/en/common.json
index 02b7b995df..1253ed4011 100644
--- a/src/locales/en/common.json
+++ b/src/locales/en/common.json
@@ -216,6 +216,8 @@
"Label": "Label",
"Label is too long, Max. 20 characters": "Label is too long, Max. 20 characters",
"Label is too long.": "Label is too long.",
+ "Last Certificate Height": "Last Certificate Height",
+ "Last Update": "Last Update",
"Last {{num}} blocks": "Last {{num}} blocks",
"Latest votes": "Latest votes",
"Less filters": "Less filters",
@@ -541,7 +543,9 @@
"from": "from",
"here": "here",
"hh:mm A": "hh:mm A",
+ "https://enevti.com/": "https://enevti.com/",
"in {{unlockTime}} hours.": "in {{unlockTime}} hours.",
+ "ksdjfkjsdfjsdf": "ksdjfkjsdfjsdf",
"locked": "locked",
"more": "more",
"removed": "removed",
diff --git a/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.css b/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.css
new file mode 100644
index 0000000000..9db26e9acf
--- /dev/null
+++ b/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.css
@@ -0,0 +1,121 @@
+@import '../../../../../../setup/react/app/mixins.css';
+
+.wrapper {
+ width: 100%;
+ box-sizing: border-box;
+ max-width: 757px;
+ min-width: 450px;
+ border-radius: 16px;
+ overflow: hidden;
+ background-color: var(--color-white);
+
+ & > div:first-child {
+ width: 100%;
+ height: 130px;
+ background: linear-gradient(268.31deg, #0c152e 0%, #254898 100%);
+ }
+}
+
+.avatarContainer {
+ width: 100%;
+ height: 51px;
+ display: flex;
+ position: relative;
+ flex-direction: column;
+ align-items: center;
+ justify-content: flex-start;
+
+ & > div:first-child {
+ position: absolute;
+ top: -39px;
+ width: 77px;
+ height: 77px;
+ border-radius: 40px;
+ }
+}
+
+.detailsWrapper {
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+}
+
+.chainNameWrapper {
+ & > span {
+ font-size: 28px;
+ font-weight: 700;
+ color: var(--color-maastricht-blue);
+ }
+
+ & > span ~ button {
+ padding: 0px 0px !important;
+ min-width: fit-content;
+ margin-left: 16px;
+ }
+}
+
+.addressRow {
+ & span {
+ font-size: 14px;
+ }
+}
+
+.appLink {
+ margin-top: 14px;
+ display: flex;
+ font-size: 14px;
+ font-weight: 400;
+
+ & > img {
+ width: 16px;
+ height: 16px;
+ margin-right: 8px;
+ }
+}
+
+.balanceRow {
+ display: flex;
+ align-items: center;
+ margin-top: 22px;
+ column-gap: 8px;
+
+ & > span:first-child {
+ font-size: 14px;
+ color: var(--color-blue-gray);
+ }
+
+ & > span:nth-child(2) {
+ font-size: 20px;
+ font-weight: 600;
+ color: var(--color-link-active);
+ }
+}
+
+.footerDetailsRow {
+ width: 100%;
+ padding: 32px;
+ box-sizing: border-box;
+ display: flex;
+ flex-direction: row;
+ column-gap: 10px;
+
+ & .headerText {
+ font-size: 13px;
+ font-weight: 400;
+ color: var(--color-blue-gray);
+ }
+
+ & .detail {
+ width: 25%;
+ }
+
+ & .detail:last-of-type {
+ width: 30%;
+ }
+
+ & .detail:nth-last-of-type(2) {
+ width: 20%;
+ }
+}
diff --git a/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.js b/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.js
new file mode 100644
index 0000000000..49fc317115
--- /dev/null
+++ b/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.js
@@ -0,0 +1,115 @@
+import React from 'react';
+import { useTranslation } from 'react-i18next';
+import ValueAndLabel from 'src/modules/transaction/components/TransactionDetails/valueAndLabel';
+import CopyToClipboard from 'src/modules/common/components/copyToClipboard';
+import { TertiaryButton } from 'src/theme/buttons';
+import grid from 'flexboxgrid/dist/flexboxgrid.css';
+import Dialog from '@theme/dialog/dialog';
+import Icon from 'src/theme/Icon';
+import Tooltip from 'src/theme/Tooltip';
+import { Link } from 'react-router-dom';
+import styles from './BlockchainApplicationDetails.css';
+
+const BlockchainApplicationDetails = () => {
+ const { t } = useTranslation();
+ console.log('>>>', t);
+
+ const footerDetails = [
+ {
+ header:
+ {t('Confirmations')}
+
+
+ {t('ksdjfkjsdfjsdf')}
+
+
+ ,
+ content: 10 ,
+ },
+ {
+ header:
+ {t('Status')}
+ ,
+ content: dskjfsdf ,
+ },
+ {
+ header:
+ {t('Last Update')}
+ ,
+ content: 26 Jan 2022 ,
+ },
+ {
+ header:
+ {t('Last Certificate Height')}
+ ,
+ content: 156785 ,
+ },
+ ];
+
+ return (
+
+
+
+
+
+
+
+ Enevti
+
+
+
+
+
+
+
+
+
+
+
+
+ {t('https://enevti.com/')}
+
+
+
+ Deposited:
+ 5,351.859 LSK
+
+
+ {footerDetails.map(({ header, content }, index) => (
+
+ {content}
+
+ ))}
+
+
+
+
+
+ );
+};
+
+export default BlockchainApplicationDetails;
diff --git a/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.test.js b/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.test.js
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/index.js b/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/index.js
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/theme/Icon/index.js b/src/theme/Icon/index.js
index 1be57c648b..626e371230 100644
--- a/src/theme/Icon/index.js
+++ b/src/theme/Icon/index.js
@@ -209,6 +209,9 @@ import switchIcon from '@setup/react/assets/images/icons/switch.svg';
import secretPassphrase from '@setup/react/assets/images/icons/secret-passphrase.svg';
import accountUpload from '@setup/react/assets/images/icons/account-upload.svg';
import accountRemoved from '@setup/react/assets/images/icons/account-removed.svg';
+import unpinnedIcon from '@setup/react/assets/images/icons/unpinned.svg';
+import pinnedIcon from '@setup/react/assets/images/icons/pinned.svg';
+import chainLinkIcon from '@setup/react/assets/images/icons/chain-link.svg';
export const icons = {
academy,
@@ -418,6 +421,9 @@ export const icons = {
secretPassphrase,
accountUpload,
accountRemoved,
+ unpinnedIcon,
+ pinnedIcon,
+ chainLinkIcon,
};
const Icon = ({ name, noTheme, ...props }) => {
From b94ba6c6c6487f10e9a029b3568a24fa0f407643 Mon Sep 17 00:00:00 2001
From: eniolam1000752
Date: Tue, 28 Jun 2022 23:51:52 +0100
Subject: [PATCH 06/25] feature: fixed status text css
---
src/locales/en/common.json | 1 +
.../BlockchainApplicationDetails.css | 40 ++++++++++++++++---
.../BlockchainApplicationDetails.js | 16 ++++----
3 files changed, 43 insertions(+), 14 deletions(-)
diff --git a/src/locales/en/common.json b/src/locales/en/common.json
index 1253ed4011..bfb7694a07 100644
--- a/src/locales/en/common.json
+++ b/src/locales/en/common.json
@@ -548,6 +548,7 @@
"ksdjfkjsdfjsdf": "ksdjfkjsdfjsdf",
"locked": "locked",
"more": "more",
+ "registered": "registered",
"removed": "removed",
"to": "to",
"will be available to unlock in": "will be available to unlock in",
diff --git a/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.css b/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.css
index 9db26e9acf..3f8ae6b73a 100644
--- a/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.css
+++ b/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.css
@@ -27,6 +27,9 @@
& > div:first-child {
position: absolute;
+ display: flex;
+ align-items: center;
+ justify-content: center;
top: -39px;
width: 77px;
height: 77px;
@@ -107,15 +110,40 @@
color: var(--color-blue-gray);
}
- & .detail {
- width: 25%;
+ & .detailContentText {
+ font-size: 15px;
}
- & .detail:last-of-type {
- width: 30%;
+ & .statusChip {
+ font-size: 14px !important;
+ padding: 10px 16px;
+ border-radius: 40px;
+ }
+
+ & .active {
+ background-color: #00d5631a;
+ color: var(--color-jade-green);
}
- & .detail:nth-last-of-type(2) {
- width: 20%;
+ & .reigstered {
+ background-color: #4070f41a;
+ color: var(--color-ultramarine-blue);
}
+
+ & .terminated {
+ background-color: var(--color-maastricht-blue);
+ color: var(--color-ghost-white);
+ }
+}
+
+.detail {
+ width: 25%;
+}
+
+.detail:last-of-type {
+ width: 30%;
+}
+
+.detail:nth-last-of-type(3) {
+ width: 20%;
}
diff --git a/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.js b/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.js
index 49fc317115..288e6a266f 100644
--- a/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.js
+++ b/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.js
@@ -24,25 +24,27 @@ const BlockchainApplicationDetails = () => {
,
- content: 10 ,
+ content: 10 ,
},
{
header:
{t('Status')}
,
- content: dskjfsdf ,
+ content:
+ {t('registered')}
+ ,
},
{
header:
{t('Last Update')}
,
- content: 26 Jan 2022 ,
+ content: 26 Jan 2022 ,
},
{
header:
{t('Last Certificate Height')}
,
- content: 156785 ,
+ content: 156785 ,
},
];
@@ -52,10 +54,8 @@ const BlockchainApplicationDetails = () => {
-
+ {/* just a place holder */}
+
sdf
From 43020efed4634a9d5e4b8a68be560e7e69d496c8 Mon Sep 17 00:00:00 2001
From: eniolam1000752
Date: Thu, 30 Jun 2022 03:07:58 +0100
Subject: [PATCH 07/25] feature: implemented usePinBlockChainApplication hook
and written unit test for it;
---
.eslintrc | 1 +
packages/common/store/reducers/index.js | 1 +
packages/views/screens/router/routesMap.js | 2 +-
setup/config/webpack.config.js | 1 +
setup/jest.config.js | 3 +-
src/locales/en/common.json | 1 -
.../blockchainApplication/api/index.js | 29 +++++++
.../hooks/.gitkeep => api/index.test.js} | 0
.../BlockchainApplicationDetails.css | 7 +-
.../BlockchainApplicationDetails.js | 59 +++++++++----
.../BlockchainApplicationDetails/index.js | 25 ++++++
.../hooks/usePinBlockchainApplication.js | 21 +++++
.../hooks/usePinBlockchainApplication.test.js | 84 +++++++++++++++++++
.../explore/store/.gitkeep | 0
.../explore/store/action.js | 16 ++++
.../explore/store/action.test.js | 32 +++++++
.../explore/store/actionTypes.js | 6 ++
.../explore/store/reducer.js | 32 +++++++
.../explore/store/reducer.test.js | 22 +++++
.../explore/store/selectors.js | 2 +
.../explore/store/selectors.test.js | 9 ++
tests/fixtures/blockchainApplications.js | 12 +++
22 files changed, 346 insertions(+), 19 deletions(-)
create mode 100644 src/modules/blockchainApplication/api/index.js
rename src/modules/blockchainApplication/{explore/hooks/.gitkeep => api/index.test.js} (100%)
create mode 100644 src/modules/blockchainApplication/explore/hooks/usePinBlockchainApplication.js
create mode 100644 src/modules/blockchainApplication/explore/hooks/usePinBlockchainApplication.test.js
delete mode 100644 src/modules/blockchainApplication/explore/store/.gitkeep
create mode 100644 src/modules/blockchainApplication/explore/store/action.js
create mode 100644 src/modules/blockchainApplication/explore/store/action.test.js
create mode 100644 src/modules/blockchainApplication/explore/store/actionTypes.js
create mode 100644 src/modules/blockchainApplication/explore/store/reducer.js
create mode 100644 src/modules/blockchainApplication/explore/store/reducer.test.js
create mode 100644 src/modules/blockchainApplication/explore/store/selectors.js
create mode 100644 src/modules/blockchainApplication/explore/store/selectors.test.js
create mode 100644 tests/fixtures/blockchainApplications.js
diff --git a/.eslintrc b/.eslintrc
index 61250ffdd5..9504b08608 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -43,6 +43,7 @@
["@settings", "./packages/settings/"],
["@token", "./src/modules/token/"],
["@transaction", "./src/modules/transaction"],
+ ["@blockchainApplication", "./src/modules/blockchainApplication"],
["@update", "./src/modules/update"],
["@wallet", "./src/modules/wallet/"],
["@account", "./src/modules/account/"],
diff --git a/packages/common/store/reducers/index.js b/packages/common/store/reducers/index.js
index 235deb51e5..82c4312f33 100644
--- a/packages/common/store/reducers/index.js
+++ b/packages/common/store/reducers/index.js
@@ -11,3 +11,4 @@ export { default as watchList } from '@dpos/validator/store/reducers/watchList';
export { default as service } from './service';
export { default as loading } from './loading';
export { account } from 'src/modules/account/store/reducer';
+export { blockChainApplications } from 'src/modules/blockchainApplication/explore/store/reducer';
diff --git a/packages/views/screens/router/routesMap.js b/packages/views/screens/router/routesMap.js
index 45fb13fe16..a401497da0 100644
--- a/packages/views/screens/router/routesMap.js
+++ b/packages/views/screens/router/routesMap.js
@@ -41,7 +41,7 @@ import SwitchAccount from '@account/components/SwitchAccount';
import BackupRecoveryPhraseFlow from '@account/components/BackupRecoveryPhraseFlow';
import RemoveCurrentAccountFlow from 'src/modules/account/components/RemoveCurrentAccountFlow';
import RemoveSelectedAccountFlow from 'src/modules/account/components/RemoveSelectedAccountFlow';
-import BlockchainApplicationDetails from 'src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails';
+import BlockchainApplicationDetails from 'src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails';
export default {
wallet: AccountDetails,
diff --git a/setup/config/webpack.config.js b/setup/config/webpack.config.js
index 02086cfd42..d7699d4da5 100644
--- a/setup/config/webpack.config.js
+++ b/setup/config/webpack.config.js
@@ -25,6 +25,7 @@ const config = {
'@settings': resolve('./packages/settings'),
'@token': resolve('./src/modules/token'),
'@transaction': resolve('./src/modules/transaction'),
+ '@blockchainApplication': resolve('./src/modules/blockchainApplication'),
'@update': resolve('./src/modules/update'),
'@views': resolve('./packages/views'),
'@packages': resolve('./packages'),
diff --git a/setup/jest.config.js b/setup/jest.config.js
index b33110ddf9..aa855a086d 100644
--- a/setup/jest.config.js
+++ b/setup/jest.config.js
@@ -29,6 +29,7 @@ module.exports = {
'^@fixtures(.*)$': resolve(__dirname, '../tests/fixtures/$1'),
'^@theme(.*)$': resolve(__dirname, '../src/theme/$1'),
'^@account(.*)$': resolve(__dirname, '../src/modules/account/$1'),
+ '^@blockchainApplication(.*)$': resolve(__dirname, '../src/modules/blockchainApplication/$1'),
'^@block(.*)$': resolve(__dirname, '../src/modules/block/$1'),
'^@bookmark(.*)$': resolve(__dirname, '../src/modules/bookmark/$1'),
'^@search(.*)$': resolve(__dirname, '../src/modules/search/$1'),
@@ -52,7 +53,7 @@ module.exports = {
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
'/tests/__mocks__/fileMock.js',
},
- collectCoverage: true,
+ collectCoverage: false,
coverageDirectory: '/coverage/jest',
collectCoverageFrom: ['packages/**/*.js', 'src/**/*.js', 'setup/**/*.js', 'app/src/**/*.js'],
coveragePathIgnorePatterns: [
diff --git a/src/locales/en/common.json b/src/locales/en/common.json
index bfb7694a07..1253ed4011 100644
--- a/src/locales/en/common.json
+++ b/src/locales/en/common.json
@@ -548,7 +548,6 @@
"ksdjfkjsdfjsdf": "ksdjfkjsdfjsdf",
"locked": "locked",
"more": "more",
- "registered": "registered",
"removed": "removed",
"to": "to",
"will be available to unlock in": "will be available to unlock in",
diff --git a/src/modules/blockchainApplication/api/index.js b/src/modules/blockchainApplication/api/index.js
new file mode 100644
index 0000000000..5c3fe2522c
--- /dev/null
+++ b/src/modules/blockchainApplication/api/index.js
@@ -0,0 +1,29 @@
+import http from '@common/utilities/api/http';
+import { HTTP_PREFIX } from 'src/const/httpCodes';
+
+const httpPaths = {
+ application: `${HTTP_PREFIX}/application`,
+};
+
+/**
+ * Retrieves the details of a single transaction
+ *
+ * @param {Object} data
+ * @param {String} data.params
+ * @param {String} data.params.id - Id of the transaction
+ * @param {String?} data.baseUrl - Lisk Service API url to override the
+ * existing ServiceUrl on the network param. We may use this to retrieve
+ * the details of an archived transaction.
+ * @param {Object} data.network - Network setting from Redux store
+ * @returns {Promise} Transaction details API call
+ */
+export const getApplication = ({
+ params, network, baseUrl,
+}) => http({
+ path: httpPaths.transaction,
+ params,
+ network,
+ baseUrl,
+});
+
+export const getApplications = () => {};
diff --git a/src/modules/blockchainApplication/explore/hooks/.gitkeep b/src/modules/blockchainApplication/api/index.test.js
similarity index 100%
rename from src/modules/blockchainApplication/explore/hooks/.gitkeep
rename to src/modules/blockchainApplication/api/index.test.js
diff --git a/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.css b/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.css
index 3f8ae6b73a..5d1bfb278e 100644
--- a/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.css
+++ b/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.css
@@ -33,6 +33,7 @@
top: -39px;
width: 77px;
height: 77px;
+ background-color: white;
border-radius: 40px;
}
}
@@ -57,6 +58,10 @@
min-width: fit-content;
margin-left: 16px;
}
+
+ & img {
+ width: 17px;
+ }
}
.addressRow {
@@ -125,7 +130,7 @@
color: var(--color-jade-green);
}
- & .reigstered {
+ & .registered {
background-color: #4070f41a;
color: var(--color-ultramarine-blue);
}
diff --git a/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.js b/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.js
index 288e6a266f..f3e32ae7ef 100644
--- a/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.js
+++ b/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.js
@@ -1,5 +1,6 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
+import moment from 'moment';
import ValueAndLabel from 'src/modules/transaction/components/TransactionDetails/valueAndLabel';
import CopyToClipboard from 'src/modules/common/components/copyToClipboard';
import { TertiaryButton } from 'src/theme/buttons';
@@ -8,11 +9,38 @@ import Dialog from '@theme/dialog/dialog';
import Icon from 'src/theme/Icon';
import Tooltip from 'src/theme/Tooltip';
import { Link } from 'react-router-dom';
+import { parseSearchParams } from 'src/utils/searchParams';
import styles from './BlockchainApplicationDetails.css';
+import { usePinBlockchainApplication } from '../../hooks/usePinBlockchainApplication';
-const BlockchainApplicationDetails = () => {
+const application = {
+ data: {
+ name: 'Test app',
+ chainID: 'aq02qkbb35u4jdq8szo3pnsq',
+ state: 'active',
+ address: 'lsk24cd35u4jdq8szo3pnsqe5dsxwrnazyqqqg5eu',
+ lastCertificateHeight: 1000,
+ lastUpdated: 123456789,
+ },
+};
+
+const BlockchainApplicationDetails = ({ location }) => {
const { t } = useTranslation();
- console.log('>>>', t);
+ const chainId = parseSearchParams(location.search).chainId;
+ const { checkPinByChainId, deletePin, setPin } = usePinBlockchainApplication();
+ const {
+ name, state, address, lastCertificateHeight, lastUpdated,
+ } = application.data;
+
+ const isPinned = checkPinByChainId(chainId);
+
+ const toggleApplicationPin = () => {
+ if (!isPinned) {
+ setPin(chainId);
+ } else {
+ deletePin(chainId);
+ }
+ };
const footerDetails = [
{
@@ -24,27 +52,27 @@ const BlockchainApplicationDetails = () => {
,
- content: 10 ,
+ content: {chainId} ,
},
{
header:
{t('Status')}
,
- content:
- {t('registered')}
+ content:
+ {t(state)}
,
},
{
header:
{t('Last Update')}
,
- content: 26 Jan 2022 ,
+ content: {moment(lastUpdated).format('DD MMM YYYY')} ,
},
{
header:
{t('Last Certificate Height')}
,
- content: 156785 ,
+ content: {lastCertificateHeight} ,
},
];
@@ -55,34 +83,35 @@ const BlockchainApplicationDetails = () => {
{/* just a place holder */}
-
- Enevti
-
+ {name}
+
+
+
diff --git a/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/index.js b/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/index.js
index e69de29bb2..1966b7ee16 100644
--- a/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/index.js
+++ b/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/index.js
@@ -0,0 +1,25 @@
+/* istanbul ignore file */
+import { compose } from 'redux';
+import { withRouter } from 'react-router-dom';
+import { getApplication } from '@blockchainApplication/api';
+import withData from 'src/utils/withData';
+import { parseSearchParams } from 'src/utils/searchParams';
+import BlockchainApplicationDetails from './BlockchainApplicationDetails';
+
+const apis = {
+ application: {
+ apiUtil: (network, { token, chainId }) =>
+ getApplication({ network, params: { chainId } }, token),
+ getApiParams: (state, ownProps) => ({
+ chainId: parseSearchParams(ownProps.location.search).chainId,
+ network: state.network,
+ }),
+ transformResponse: response => response.data[0] || {},
+ autoload: true,
+ },
+};
+
+export default compose(
+ withRouter,
+ withData(apis),
+)(BlockchainApplicationDetails);
diff --git a/src/modules/blockchainApplication/explore/hooks/usePinBlockchainApplication.js b/src/modules/blockchainApplication/explore/hooks/usePinBlockchainApplication.js
new file mode 100644
index 0000000000..8fba281a75
--- /dev/null
+++ b/src/modules/blockchainApplication/explore/hooks/usePinBlockchainApplication.js
@@ -0,0 +1,21 @@
+import { useCallback } from 'react';
+import { useSelector, useDispatch } from 'react-redux';
+import { selectPinnedApplications } from '@blockchainApplication/explore/store/selectors';
+import { pinApplication, removePinnedApplication } from '../store/action';
+
+// eslint-disable-next-line
+export function usePinBlockchainApplication() {
+ const dispatch = useDispatch();
+ const pins = useSelector(selectPinnedApplications);
+
+ const setPin = useCallback((chainId) => dispatch(pinApplication(chainId)), []);
+ const deletePin = useCallback(
+ (chainId) => dispatch(removePinnedApplication(chainId)),
+ [],
+ );
+ const checkPinByChainId = useCallback((chainId) => pins.includes(chainId));
+
+ return {
+ pins, setPin, deletePin, checkPinByChainId,
+ };
+}
diff --git a/src/modules/blockchainApplication/explore/hooks/usePinBlockchainApplication.test.js b/src/modules/blockchainApplication/explore/hooks/usePinBlockchainApplication.test.js
new file mode 100644
index 0000000000..79d46c0483
--- /dev/null
+++ b/src/modules/blockchainApplication/explore/hooks/usePinBlockchainApplication.test.js
@@ -0,0 +1,84 @@
+import { renderHook, act } from '@testing-library/react-hooks';
+import mockBlockchainApplications from '@tests/fixtures/blockchainApplications';
+import actionTypes from '@blockchainApplication/explore/store/actionTypes';
+import { usePinBlockchainApplication } from './usePinBlockchainApplication';
+import { pinApplication } from '../store/action';
+
+const mockDispatch = jest.fn();
+const mockState = {
+ blockChainApplications: {
+ pins: mockBlockchainApplications.map(({ chainID }) => chainID),
+ },
+};
+jest.mock('react-redux', () => ({
+ useSelector: jest.fn().mockImplementation((fn) => fn(mockState)),
+ useDispatch: () => mockDispatch,
+}));
+
+describe('usePinBlockchainApplication hook', () => {
+ beforeEach(() => {
+ mockDispatch.mockClear();
+ });
+ const { result } = renderHook(() => usePinBlockchainApplication());
+
+ it('setPin and deletePin Should not be triggered on mounting', async () => {
+ expect(mockDispatch).toHaveBeenCalledTimes(0);
+ });
+
+ it('setPin should dispatch an action', async () => {
+ const { setPin } = result.current;
+ const chainId = mockBlockchainApplications[0].chainID;
+ act(() => setPin(chainId));
+
+ expect(mockDispatch).toHaveBeenCalledTimes(1);
+ expect(mockDispatch).toHaveBeenCalledWith(
+ pinApplication(chainId),
+ );
+ });
+
+ it('should return pins as an arrray', async () => {
+ const { pins, setPin } = result.current;
+ const chainId = mockBlockchainApplications[0].chainID;
+
+ act(() => setPin(chainId));
+ const expectPins = mockBlockchainApplications.map(({ chainID }) => chainID);
+ expect(pins).toEqual(expect.arrayContaining(expectPins));
+ });
+
+ it('should flag chain as a pinned application', async () => {
+ const { checkPinByChainId, setPin } = result.current;
+ const chainId = mockBlockchainApplications[0].chainID;
+
+ act(() => setPin(chainId));
+ expect(checkPinByChainId(chainId)).toBeTruthy();
+ });
+
+ it('should not flag chain as a pinned application', async () => {
+ mockState.blockChainApplications.pins = [];
+ jest.mock('react-redux', () => ({
+ useSelector: jest.fn().mockImplementation((fn) => fn(mockState)),
+ useDispatch: () => mockDispatch,
+ }));
+
+ const {
+ result:
+ { current: { checkPinByChainId } },
+ } = renderHook(() => usePinBlockchainApplication());
+
+ const chainId = mockBlockchainApplications[0].chainID;
+ expect(checkPinByChainId(chainId)).not.toBeTruthy();
+ });
+
+ it('deletePin should dispatch an action', async () => {
+ const { deletePin } = result.current;
+ const chainId = mockBlockchainApplications[0].chainID;
+ const expectedAction = {
+ type: actionTypes.removeApplicationPin,
+ chainId,
+ };
+
+ act(() => deletePin(chainId));
+ expect(mockDispatch).toHaveBeenCalledTimes(1);
+ expect(mockDispatch).toHaveBeenCalledWith(expectedAction);
+ });
+});
diff --git a/src/modules/blockchainApplication/explore/store/.gitkeep b/src/modules/blockchainApplication/explore/store/.gitkeep
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/src/modules/blockchainApplication/explore/store/action.js b/src/modules/blockchainApplication/explore/store/action.js
new file mode 100644
index 0000000000..dd0ac3429f
--- /dev/null
+++ b/src/modules/blockchainApplication/explore/store/action.js
@@ -0,0 +1,16 @@
+import actionTypes from './actionTypes';
+
+/**
+ * Trigger this action to set blockchain application pin
+ *
+ * @returns {Object} - Action object
+ */
+export const pinApplication = (chainId) => ({
+ type: actionTypes.setApplicationPin,
+ chainId,
+});
+
+export const removePinnedApplication = (chainId) => ({
+ type: actionTypes.removeApplicationPin,
+ chainId,
+});
diff --git a/src/modules/blockchainApplication/explore/store/action.test.js b/src/modules/blockchainApplication/explore/store/action.test.js
new file mode 100644
index 0000000000..c6b736f8de
--- /dev/null
+++ b/src/modules/blockchainApplication/explore/store/action.test.js
@@ -0,0 +1,32 @@
+/* eslint-disable max-lines */
+import mockBlockchainApplications from '@tests/fixtures/blockchainApplications';
+import actionTypes from './actionTypes';
+import {
+ removePinnedApplication,
+ pinApplication,
+} from './action';
+
+const chainId = mockBlockchainApplications[0].chainID;
+
+describe('actions: blockchainApplication', () => {
+ beforeEach(() => {
+ jest.resetAllMocks();
+ });
+
+ it('should create an action to pin blockchain application', () => {
+ const expectedAction = {
+ type: actionTypes.setApplicationPin,
+ chainId,
+ };
+
+ expect(pinApplication(chainId)).toEqual(expectedAction);
+ });
+ it('should create an action to remove blockchain application', () => {
+ const expectedAction = {
+ type: actionTypes.removeApplicationPin,
+ chainId,
+ };
+
+ expect(removePinnedApplication(chainId)).toEqual(expectedAction);
+ });
+});
diff --git a/src/modules/blockchainApplication/explore/store/actionTypes.js b/src/modules/blockchainApplication/explore/store/actionTypes.js
new file mode 100644
index 0000000000..2c80ea70ad
--- /dev/null
+++ b/src/modules/blockchainApplication/explore/store/actionTypes.js
@@ -0,0 +1,6 @@
+const actionTypes = {
+ setApplicationPin: 'SET_APPLICATION_PIN',
+ removeApplicationPin: 'REMOVE_APPLICATION_PIN',
+};
+
+export default actionTypes;
diff --git a/src/modules/blockchainApplication/explore/store/reducer.js b/src/modules/blockchainApplication/explore/store/reducer.js
new file mode 100644
index 0000000000..d17e26ce65
--- /dev/null
+++ b/src/modules/blockchainApplication/explore/store/reducer.js
@@ -0,0 +1,32 @@
+import { combineReducers } from 'redux';
+import { persistReducer } from 'redux-persist';
+import { storage } from '../../../../../packages/common/store';
+import actionTypes from './actionTypes';
+
+/**
+ *
+ * @param {Object} state
+ * @param {type: String, chainId: string} action
+ */
+export const pins = (state = [], { type, chainId }) => {
+ switch (type) {
+ case actionTypes.setApplicationPin:
+ return chainId ? [...state, chainId] : [...state];
+ case actionTypes.removeApplicationPin:
+ return state.filter((pinnedChainId) => pinnedChainId !== chainId);
+ default:
+ return state;
+ }
+};
+
+const persistConfig = {
+ storage,
+ key: 'blockChainApplications',
+ whitelist: ['pinnedList'],
+ blacklist: [],
+};
+
+const blockChainApplicationsReducer = combineReducers({ pins });
+
+// eslint-disable-next-line import/prefer-default-export
+export const blockChainApplications = persistReducer(persistConfig, blockChainApplicationsReducer);
diff --git a/src/modules/blockchainApplication/explore/store/reducer.test.js b/src/modules/blockchainApplication/explore/store/reducer.test.js
new file mode 100644
index 0000000000..9d5a3b6f69
--- /dev/null
+++ b/src/modules/blockchainApplication/explore/store/reducer.test.js
@@ -0,0 +1,22 @@
+import mockBlockchainApplications from '@tests/fixtures/blockchainApplications';
+import actionTypes from './actionTypes';
+import { pins } from './reducer';
+
+describe('BlockchainApplication reducer', () => {
+ it('Should return list of chainIds', async () => {
+ const actionData = {
+ type: actionTypes.setApplicationPin,
+ chainId: mockBlockchainApplications[0].chainID,
+ };
+ expect(pins([], actionData)).toContain(actionData.chainId);
+ });
+
+ it('Should return list of chainIds without the removed one', async () => {
+ const actionData = {
+ type: actionTypes.removeApplicationPin,
+ chainId: mockBlockchainApplications[0].chainID,
+ };
+ expect(pins([mockBlockchainApplications[0].chainID], actionData)).not
+ .toContain(actionData.chainId);
+ });
+});
diff --git a/src/modules/blockchainApplication/explore/store/selectors.js b/src/modules/blockchainApplication/explore/store/selectors.js
new file mode 100644
index 0000000000..12e1b3fd0a
--- /dev/null
+++ b/src/modules/blockchainApplication/explore/store/selectors.js
@@ -0,0 +1,2 @@
+// eslint-disable-next-line import/prefer-default-export
+export const selectPinnedApplications = state => state.blockChainApplications.pins;
diff --git a/src/modules/blockchainApplication/explore/store/selectors.test.js b/src/modules/blockchainApplication/explore/store/selectors.test.js
new file mode 100644
index 0000000000..e5c51b6b33
--- /dev/null
+++ b/src/modules/blockchainApplication/explore/store/selectors.test.js
@@ -0,0 +1,9 @@
+import mockPinnedApplications from '@tests/fixtures/blockchainApplications';
+import { selectPinnedApplications } from './selectors';
+
+describe('Application Explorer selector', () => {
+ it('Should return list of pinned applications action type is tiggered', async () => {
+ const state = { blockChainApplications: { pins: mockPinnedApplications } };
+ expect(selectPinnedApplications(state)).toEqual(mockPinnedApplications);
+ });
+});
diff --git a/tests/fixtures/blockchainApplications.js b/tests/fixtures/blockchainApplications.js
new file mode 100644
index 0000000000..f9fe91e2ba
--- /dev/null
+++ b/tests/fixtures/blockchainApplications.js
@@ -0,0 +1,12 @@
+const blockchainApplications = [
+ {
+ name: 'Test app',
+ chainID: 'aq02qkbb35u4jdq8szo3pnsq',
+ state: 'active',
+ address: 'lsk24cd35u4jdq8szo3pnsqe5dsxwrnazyqqqg5eu',
+ lastCertificateHeight: 1000,
+ lastUpdated: 123456789,
+ },
+];
+
+export default blockchainApplications;
From 93dc29af8fcd365743a771480dc0a7d6a30d2eb5 Mon Sep 17 00:00:00 2001
From: eniolam1000752
Date: Thu, 30 Jun 2022 03:19:20 +0100
Subject: [PATCH 08/25] feature: added test to api implementation
---
.../blockchainApplication/api/index.js | 4 +--
.../blockchainApplication/api/index.test.js | 27 +++++++++++++++++++
2 files changed, 29 insertions(+), 2 deletions(-)
diff --git a/src/modules/blockchainApplication/api/index.js b/src/modules/blockchainApplication/api/index.js
index 5c3fe2522c..24340e5b43 100644
--- a/src/modules/blockchainApplication/api/index.js
+++ b/src/modules/blockchainApplication/api/index.js
@@ -2,7 +2,7 @@ import http from '@common/utilities/api/http';
import { HTTP_PREFIX } from 'src/const/httpCodes';
const httpPaths = {
- application: `${HTTP_PREFIX}/application`,
+ application: `${HTTP_PREFIX}/blockchain/apps`,
};
/**
@@ -20,7 +20,7 @@ const httpPaths = {
export const getApplication = ({
params, network, baseUrl,
}) => http({
- path: httpPaths.transaction,
+ path: httpPaths.application,
params,
network,
baseUrl,
diff --git a/src/modules/blockchainApplication/api/index.test.js b/src/modules/blockchainApplication/api/index.test.js
index e69de29bb2..60ceb6ff1d 100644
--- a/src/modules/blockchainApplication/api/index.test.js
+++ b/src/modules/blockchainApplication/api/index.test.js
@@ -0,0 +1,27 @@
+import { getState } from '@fixtures/transactions';
+import http from '@common/utilities/api/http';
+import { getApplication } from '.';
+
+jest.mock('@common/utilities/api/http', () =>
+ jest.fn().mockImplementation(() => Promise.resolve({ data: [{ type: 0 }] })));
+
+const baseUrl = 'http://custom-base-url.com/';
+const sampleId = 'sample_id';
+const { network } = getState();
+
+describe('get blockchain application detail', () => {
+ it('Should call http with given params', () => {
+ getApplication({
+ network,
+ baseUrl,
+ params: { chainId: sampleId },
+ });
+
+ expect(http).toHaveBeenCalledWith({
+ path: '/api/v2/blockchain/apps',
+ params: { chainId: sampleId },
+ network,
+ baseUrl,
+ });
+ });
+});
From 05a82de8a59d081ea58fdf127db3810c2b2c011b Mon Sep 17 00:00:00 2001
From: eniolam1000752
Date: Thu, 30 Jun 2022 04:59:15 +0100
Subject: [PATCH 09/25] feature: extended unit test on hook and component
---
src/locales/en/common.json | 2 +-
.../BlockchainApplicationDetails.js | 7 +--
.../BlockchainApplicationDetails.test.js | 62 +++++++++++++++++++
.../explore/store/reducer.js | 2 +-
4 files changed, 67 insertions(+), 6 deletions(-)
diff --git a/src/locales/en/common.json b/src/locales/en/common.json
index 1253ed4011..7500bded22 100644
--- a/src/locales/en/common.json
+++ b/src/locales/en/common.json
@@ -71,6 +71,7 @@
"Caution! This delegate was punished on {{punishmentStartDate}}. There is approximately {{daysLeft}} days remaining before the punishment ends.": "Caution! This delegate was punished on {{punishmentStartDate}}. There is approximately {{daysLeft}} days remaining before the punishment ends.",
"Caution! You are about to send the majority of your balance": "Caution! You are about to send the majority of your balance",
"Caution! You are about to vote for the punished delegate, this will result in your LSK tokens being locked for a period of {{daysLeft}} days.": "Caution! You are about to vote for the punished delegate, this will result in your LSK tokens being locked for a period of {{daysLeft}} days.",
+ "Chain ID": "Chain ID",
"Changed votes": "Changed votes",
"Check for Updates...": "Check for Updates...",
"Choose account": "Choose account",
@@ -545,7 +546,6 @@
"hh:mm A": "hh:mm A",
"https://enevti.com/": "https://enevti.com/",
"in {{unlockTime}} hours.": "in {{unlockTime}} hours.",
- "ksdjfkjsdfjsdf": "ksdjfkjsdfjsdf",
"locked": "locked",
"more": "more",
"removed": "removed",
diff --git a/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.js b/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.js
index f3e32ae7ef..b5fc79898b 100644
--- a/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.js
+++ b/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.js
@@ -33,7 +33,6 @@ const BlockchainApplicationDetails = ({ location }) => {
} = application.data;
const isPinned = checkPinByChainId(chainId);
-
const toggleApplicationPin = () => {
if (!isPinned) {
setPin(chainId);
@@ -45,10 +44,10 @@ const BlockchainApplicationDetails = ({ location }) => {
const footerDetails = [
{
header:
- {t('Confirmations')}
+ {t('Chain ID')}
- {t('ksdjfkjsdfjsdf')}
+ {t('')}
,
@@ -91,7 +90,7 @@ const BlockchainApplicationDetails = ({ location }) => {
{name}
-
+
diff --git a/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.test.js b/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.test.js
index e69de29bb2..8a819cf5a0 100644
--- a/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.test.js
+++ b/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.test.js
@@ -0,0 +1,62 @@
+import moment from 'moment';
+import { fireEvent, screen } from '@testing-library/react';
+import mockBlockchainApplications from '@tests/fixtures/blockchainApplications';
+import { renderWithRouter } from 'src/utils/testHelpers';
+import BlockchainApplicationDetails from '.';
+
+const mockedPins = ['1111'];
+const mockSetPin = jest.fn();
+const mockDeletePin = jest.fn();
+jest.mock('../../hooks/usePinBlockchainApplication', () => ({
+ usePinBlockchainApplication: () =>
+ ({
+ setPin: mockSetPin,
+ deletePin: mockDeletePin,
+ pins: mockedPins,
+ checkPinByChainId: jest.fn(),
+ }),
+}));
+
+describe('BlockchainApplicationDetails', () => {
+ const props = {
+ location: {
+ search: 'chainId=test-chain-id',
+ },
+ application: {
+ data: mockBlockchainApplications[0],
+ isLoading: true,
+ loadData: jest.fn(),
+ error: false,
+ },
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ renderWithRouter(BlockchainApplicationDetails, props);
+ });
+
+ it('should display properly', () => {
+ const {
+ name, address, state, lastCertificateHeight, lastUpdated,
+ } = mockBlockchainApplications[0];
+
+ expect(screen.getByText(name)).toBeTruthy();
+ expect(screen.getByText(address)).toBeTruthy();
+ expect(screen.getByText(state)).toBeTruthy();
+ expect(screen.getByText(lastCertificateHeight)).toBeTruthy();
+ expect(screen.getByText(moment(lastUpdated).format('DD MMM YYYY'))).toBeTruthy();
+
+ expect(screen.getByText('Chain ID')).toBeTruthy();
+ expect(screen.getByText('Status')).toBeTruthy();
+ expect(screen.getByText('Last Update')).toBeTruthy();
+ expect(screen.getByText('Last Certificate Height')).toBeTruthy();
+ expect(screen.getByText('Deposited:')).toBeTruthy();
+ });
+
+ it('should pin blockchain application', () => {
+ const pinButton = screen.queryByTestId('pin-button');
+ fireEvent.click(pinButton);
+
+ expect(mockSetPin).toHaveBeenCalled();
+ });
+});
diff --git a/src/modules/blockchainApplication/explore/store/reducer.js b/src/modules/blockchainApplication/explore/store/reducer.js
index d17e26ce65..46844131fa 100644
--- a/src/modules/blockchainApplication/explore/store/reducer.js
+++ b/src/modules/blockchainApplication/explore/store/reducer.js
@@ -22,7 +22,7 @@ export const pins = (state = [], { type, chainId }) => {
const persistConfig = {
storage,
key: 'blockChainApplications',
- whitelist: ['pinnedList'],
+ whitelist: ['pins'],
blacklist: [],
};
From b0fce3842ae2210861f1601cbb5b255d72941445 Mon Sep 17 00:00:00 2001
From: eniolam1000752
Date: Thu, 30 Jun 2022 14:11:28 +0100
Subject: [PATCH 10/25] added e2e test for blochainExplorer
---
.../BlockchainApplicationDetails.js | 12 ++++----
tests/constants/selectors.js | 8 ++++++
.../features/blockchainExplorer.feature | 8 ++++++
.../blockchainExplorer/blockchainExplorer.js | 28 +++++++++++++++++++
4 files changed, 51 insertions(+), 5 deletions(-)
create mode 100644 tests/cypress/features/blockchainExplorer.feature
create mode 100644 tests/cypress/features/blockchainExplorer/blockchainExplorer.js
diff --git a/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.js b/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.js
index b5fc79898b..3b2c2ecc65 100644
--- a/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.js
+++ b/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.js
@@ -51,13 +51,13 @@ const BlockchainApplicationDetails = ({ location }) => {
,
- content: {chainId} ,
+ content: {chainId} ,
},
{
header:
{t('Status')}
,
- content:
+ content:
{t(state)}
,
},
@@ -65,13 +65,13 @@ const BlockchainApplicationDetails = ({ location }) => {
header:
{t('Last Update')}
,
- content: {moment(lastUpdated).format('DD MMM YYYY')} ,
+ content: {moment(lastUpdated).format('DD MMM YYYY')} ,
},
{
header:
{t('Last Certificate Height')}
,
- content: {lastCertificateHeight} ,
+ content: {lastCertificateHeight} ,
},
];
@@ -88,7 +88,7 @@ const BlockchainApplicationDetails = ({ location }) => {
-
{name}
+
{name}
@@ -112,6 +112,8 @@ const BlockchainApplicationDetails = ({ location }) => {
diff --git a/tests/constants/selectors.js b/tests/constants/selectors.js
index 83a65d6062..50ca0153da 100644
--- a/tests/constants/selectors.js
+++ b/tests/constants/selectors.js
@@ -1,3 +1,4 @@
+/* eslint-disable max-lines */
const delegatesPage = {
totalVotingNumber: '.total-voting-number',
startVotingButton: '.start-voting-button',
@@ -294,6 +295,13 @@ const ss = {
verifyPublicKeyInput: '.publicKey',
verifySignatureInput: '.signature',
acceptTermsButton: '.accept-terms',
+
+ blockchainName: '.chain-name-text',
+ chainOwnerAddress: '.copy-address-wrapper .copy-title',
+ chainIdDisplay: '.chain-id',
+ chainStatusDisplay: '.chain-status',
+ lastChainUpdateDisplay: '.last-update',
+ lastCertHeightDisplay: '.last-certificate-height',
};
export default ss;
diff --git a/tests/cypress/features/blockchainExplorer.feature b/tests/cypress/features/blockchainExplorer.feature
new file mode 100644
index 0000000000..ed3da69fe9
--- /dev/null
+++ b/tests/cypress/features/blockchainExplorer.feature
@@ -0,0 +1,8 @@
+Feature: BlockchainExplore
+
+ @basic
+ Scenario: visit blockchain application details
+ Given I visit blockchain application details link
+ Then blockchain details should be accuratly displayed
+ Given I click on closeDialog
+ Given blockchain details should be displayed
diff --git a/tests/cypress/features/blockchainExplorer/blockchainExplorer.js b/tests/cypress/features/blockchainExplorer/blockchainExplorer.js
new file mode 100644
index 0000000000..23c83209b7
--- /dev/null
+++ b/tests/cypress/features/blockchainExplorer/blockchainExplorer.js
@@ -0,0 +1,28 @@
+/* eslint-disable */
+import { ss, urls } from '@tests/constants';
+import mockBlockchainApplications from '@tests/fixtures/blockchainApplications';
+import { Given, Then } from 'cypress-cucumber-preprocessor/steps';
+import moment from 'moment';
+
+const chainDetails = mockBlockchainApplications[0]
+
+Given(/^I visit blockchain application details link$/, function () {
+ cy.visit(`${urls.login}?modal=blockChainApplicationDetails&chainId=${chainDetails.chainID}`);
+});
+
+Then(/^blockchain details should be accuratly displayed$/, function () {
+ cy.get(`${ss.blockchainName}`).eq(0).should('have.text', chainDetails.name);
+ cy.get(`${ss.lastCertHeightDisplay}`).eq(0).should('have.text', chainDetails.lastCertificateHeight);
+ cy.get(`${ss.chainStatusDisplay}`).eq(0).should('have.text', chainDetails.state);
+ cy.get(`${ss.chainOwnerAddress}`).eq(0).should('have.text', chainDetails.address);
+ cy.get(`${ss.lastChainUpdateDisplay}`).eq(0).should('have.text', moment(chainDetails.lastUpdated).format('DD MMM YYYY'));
+});
+
+Then(/^blockchain details should not displayed$/, function () {
+ cy.get(`${ss.blockchainName}`).eq(0).should('not.have.text', chainDetails.name);
+ cy.get(`${ss.lastCertHeightDisplay}`).eq(0).should('not.have.text', chainDetails.lastCertificateHeight);
+ cy.get(`${ss.chainStatusDisplay}`).eq(0).should('not.have.text', chainDetails.state);
+ cy.get(`${ss.chainOwnerAddress}`).eq(0).should('not.have.text', chainDetails.address);
+ cy.get(`${ss.lastChainUpdateDisplay}`).eq(0).should('not.have.text', moment(chainDetails.lastUpdated).format('DD MMM YYYY'));
+});
+
From 93ca6d34bbb396f50336ddc959204bb40fe71f3e Mon Sep 17 00:00:00 2001
From: eniolam1000752
Date: Thu, 30 Jun 2022 15:16:51 +0100
Subject: [PATCH 11/25] fix: fixed deepscan issue and tested e2e test
---
.../BlockchainApplicationDetails.css | 10 +++----
.../hooks/usePinBlockchainApplication.js | 2 +-
.../features/blockchainExplorer.feature | 2 +-
.../blockchainExplorer/blockchainExplorer.js | 26 +++++++++----------
4 files changed, 20 insertions(+), 20 deletions(-)
diff --git a/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.css b/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.css
index 5d1bfb278e..9e9bd94710 100644
--- a/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.css
+++ b/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.css
@@ -142,13 +142,13 @@
}
.detail {
- width: 25%;
+ width: 20%;
}
-.detail:last-of-type {
- width: 30%;
+.detail:first-of-type {
+ width: 35%;
}
-.detail:nth-last-of-type(3) {
- width: 20%;
+.detail:last-of-type {
+ width: 25%;
}
diff --git a/src/modules/blockchainApplication/explore/hooks/usePinBlockchainApplication.js b/src/modules/blockchainApplication/explore/hooks/usePinBlockchainApplication.js
index 8fba281a75..4cc0524645 100644
--- a/src/modules/blockchainApplication/explore/hooks/usePinBlockchainApplication.js
+++ b/src/modules/blockchainApplication/explore/hooks/usePinBlockchainApplication.js
@@ -13,7 +13,7 @@ export function usePinBlockchainApplication() {
(chainId) => dispatch(removePinnedApplication(chainId)),
[],
);
- const checkPinByChainId = useCallback((chainId) => pins.includes(chainId));
+ const checkPinByChainId = useCallback((chainId) => pins.includes(chainId), []);
return {
pins, setPin, deletePin, checkPinByChainId,
diff --git a/tests/cypress/features/blockchainExplorer.feature b/tests/cypress/features/blockchainExplorer.feature
index ed3da69fe9..85d6b36b36 100644
--- a/tests/cypress/features/blockchainExplorer.feature
+++ b/tests/cypress/features/blockchainExplorer.feature
@@ -5,4 +5,4 @@ Feature: BlockchainExplore
Given I visit blockchain application details link
Then blockchain details should be accuratly displayed
Given I click on closeDialog
- Given blockchain details should be displayed
+ Then blockchain details should not be displayed
diff --git a/tests/cypress/features/blockchainExplorer/blockchainExplorer.js b/tests/cypress/features/blockchainExplorer/blockchainExplorer.js
index 23c83209b7..4a1378accf 100644
--- a/tests/cypress/features/blockchainExplorer/blockchainExplorer.js
+++ b/tests/cypress/features/blockchainExplorer/blockchainExplorer.js
@@ -1,6 +1,6 @@
/* eslint-disable */
-import { ss, urls } from '@tests/constants';
-import mockBlockchainApplications from '@tests/fixtures/blockchainApplications';
+import { ss, urls } from '../../../constants';
+import mockBlockchainApplications from '../../../fixtures/blockchainApplications';
import { Given, Then } from 'cypress-cucumber-preprocessor/steps';
import moment from 'moment';
@@ -11,18 +11,18 @@ Given(/^I visit blockchain application details link$/, function () {
});
Then(/^blockchain details should be accuratly displayed$/, function () {
- cy.get(`${ss.blockchainName}`).eq(0).should('have.text', chainDetails.name);
- cy.get(`${ss.lastCertHeightDisplay}`).eq(0).should('have.text', chainDetails.lastCertificateHeight);
- cy.get(`${ss.chainStatusDisplay}`).eq(0).should('have.text', chainDetails.state);
- cy.get(`${ss.chainOwnerAddress}`).eq(0).should('have.text', chainDetails.address);
- cy.get(`${ss.lastChainUpdateDisplay}`).eq(0).should('have.text', moment(chainDetails.lastUpdated).format('DD MMM YYYY'));
+ cy.get(ss.blockchainName).eq(0).should('have.text', chainDetails.name);
+ cy.get(ss.lastCertHeightDisplay).eq(0).should('have.text', chainDetails.lastCertificateHeight);
+ cy.get(ss.chainStatusDisplay).eq(0).should('have.text', chainDetails.state);
+ cy.get(ss.chainOwnerAddress).eq(0).should('have.text', chainDetails.address);
+ cy.get(ss.lastChainUpdateDisplay).eq(0).should('have.text', moment(chainDetails.lastUpdated).format('DD MMM YYYY'));
});
-Then(/^blockchain details should not displayed$/, function () {
- cy.get(`${ss.blockchainName}`).eq(0).should('not.have.text', chainDetails.name);
- cy.get(`${ss.lastCertHeightDisplay}`).eq(0).should('not.have.text', chainDetails.lastCertificateHeight);
- cy.get(`${ss.chainStatusDisplay}`).eq(0).should('not.have.text', chainDetails.state);
- cy.get(`${ss.chainOwnerAddress}`).eq(0).should('not.have.text', chainDetails.address);
- cy.get(`${ss.lastChainUpdateDisplay}`).eq(0).should('not.have.text', moment(chainDetails.lastUpdated).format('DD MMM YYYY'));
+Then(/^blockchain details should not be displayed$/, function () {
+ cy.get(ss.blockchainName).should('not.exist');
+ cy.get(ss.lastCertHeightDisplay).should('not.exist');
+ cy.get(ss.chainStatusDisplay).should('not.exist');
+ cy.get(ss.chainOwnerAddress).should('not.exist');
+ cy.get(ss.lastChainUpdateDisplay).should('not.exist');
});
From 86208a360e34a0638ac64020df70564aba8efa2b Mon Sep 17 00:00:00 2001
From: eniolam1000752
Date: Thu, 30 Jun 2022 15:22:57 +0100
Subject: [PATCH 12/25] chore: added todos
---
.../BlockchainApplicationDetails.js | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.js b/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.js
index 3b2c2ecc65..d0e7e7a53a 100644
--- a/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.js
+++ b/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.js
@@ -13,6 +13,7 @@ import { parseSearchParams } from 'src/utils/searchParams';
import styles from './BlockchainApplicationDetails.css';
import { usePinBlockchainApplication } from '../../hooks/usePinBlockchainApplication';
+// TODO: this is a mock response of an application's details
const application = {
data: {
name: 'Test app',
@@ -81,9 +82,8 @@ const BlockchainApplicationDetails = ({ location }) => {
- {/* just a place holder */}
- sdf
+ {/* TODO: chain logo goes here when its available from service's response */}
@@ -122,6 +122,7 @@ const BlockchainApplicationDetails = ({ location }) => {
Deposited:
+ {/* TODO: this is a placeholder value pending when its part of service response */}
5,351.859 LSK
From 2a0d9ac3c453380c2c7a37caf446b3a05ea66542 Mon Sep 17 00:00:00 2001
From: eniolam1000752
Date: Fri, 1 Jul 2022 10:10:29 +0100
Subject: [PATCH 13/25] fix: fixed pr comments
---
setup/jest.config.js | 2 +-
src/locales/en/common.json | 2 +-
.../BlockchainApplicationDetails.js | 73 +++++++++++--------
.../hooks/usePinBlockchainApplication.js | 3 +-
4 files changed, 45 insertions(+), 35 deletions(-)
diff --git a/setup/jest.config.js b/setup/jest.config.js
index aa855a086d..1b477aedf9 100644
--- a/setup/jest.config.js
+++ b/setup/jest.config.js
@@ -53,7 +53,7 @@ module.exports = {
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
'/tests/__mocks__/fileMock.js',
},
- collectCoverage: false,
+ collectCoverage: true,
coverageDirectory: '/coverage/jest',
collectCoverageFrom: ['packages/**/*.js', 'src/**/*.js', 'setup/**/*.js', 'app/src/**/*.js'],
coveragePathIgnorePatterns: [
diff --git a/src/locales/en/common.json b/src/locales/en/common.json
index 7500bded22..7e375dc3b7 100644
--- a/src/locales/en/common.json
+++ b/src/locales/en/common.json
@@ -130,6 +130,7 @@
"Delegation status": "Delegation status",
"Depending on the number of votes locked to your account (delegate weight), your account can become eligible to forge new blocks on the Lisk blockchain. With every new round (103 blocks), the top 101 active delegates and 2 randomly selected standby delegates each become eligible to forge a new block. For each block forged and accepted by the Lisk network, a delegate receives a new block reward and the transaction fees collected from each sender. The minimum required delegate weight to become eligible is 1000 LSK.": "Depending on the number of votes locked to your account (delegate weight), your account can become eligible to forge new blocks on the Lisk blockchain. With every new round (103 blocks), the top 101 active delegates and 2 randomly selected standby delegates each become eligible to forge a new block. For each block forged and accepted by the Lisk network, a delegate receives a new block reward and the transaction fees collected from each sender. The minimum required delegate weight to become eligible is 1000 LSK.",
"Deposit at least {{amount}} LSK to your new account": "Deposit at least {{amount}} LSK to your new account",
+ "Deposited:": "Deposited:",
"Details": "Details",
"Disable dark mode": "Disable dark mode",
"Disable discreet mode": "Disable discreet mode",
@@ -544,7 +545,6 @@
"from": "from",
"here": "here",
"hh:mm A": "hh:mm A",
- "https://enevti.com/": "https://enevti.com/",
"in {{unlockTime}} hours.": "in {{unlockTime}} hours.",
"locked": "locked",
"more": "more",
diff --git a/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.js b/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.js
index d0e7e7a53a..da80f6c1fa 100644
--- a/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.js
+++ b/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.js
@@ -1,6 +1,7 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import moment from 'moment';
+import TokenAmount from '@token/fungible/components/tokenAmount';
import ValueAndLabel from 'src/modules/transaction/components/TransactionDetails/valueAndLabel';
import CopyToClipboard from 'src/modules/common/components/copyToClipboard';
import { TertiaryButton } from 'src/theme/buttons';
@@ -22,6 +23,8 @@ const application = {
address: 'lsk24cd35u4jdq8szo3pnsqe5dsxwrnazyqqqg5eu',
lastCertificateHeight: 1000,
lastUpdated: 123456789,
+ deposit: 5e10,
+ serviceUrl: 'https://enevti.com/',
},
};
@@ -30,7 +33,7 @@ const BlockchainApplicationDetails = ({ location }) => {
const chainId = parseSearchParams(location.search).chainId;
const { checkPinByChainId, deletePin, setPin } = usePinBlockchainApplication();
const {
- name, state, address, lastCertificateHeight, lastUpdated,
+ name, state, address, lastCertificateHeight, lastUpdated, deposit, serviceUrl,
} = application.data;
const isPinned = checkPinByChainId(chainId);
@@ -44,35 +47,33 @@ const BlockchainApplicationDetails = ({ location }) => {
const footerDetails = [
{
- header:
- {t('Chain ID')}
-
-
- {t('')}
-
-
- ,
- content: {chainId} ,
+ header: (
+ <>
+ {t('Chain ID')}
+
+
+ {t('')}
+
+
+ >
+ ),
+ className: `${styles.detailContentText} chain-id`,
+ content: chainId,
},
{
- header:
- {t('Status')}
- ,
- content:
- {t(state)}
- ,
+ header: t('Status'),
+ className: `${styles.detailContentText} ${styles.statusChip} ${styles[state]}`,
+ content: t(state),
},
{
- header:
- {t('Last Update')}
- ,
- content: {moment(lastUpdated).format('DD MMM YYYY')} ,
+ header: t('Last Update'),
+ className: `${styles.detailContentText} last-update`,
+ content: moment(lastUpdated).format('DD MMM YYYY'),
},
{
- header:
- {t('Last Certificate Height')}
- ,
- content: {lastCertificateHeight} ,
+ header: t('Last Certificate Height'),
+ className: `${styles.detailContentText} last-certificate-height`,
+ content: lastCertificateHeight,
},
];
@@ -114,25 +115,35 @@ const BlockchainApplicationDetails = ({ location }) => {
target="_blank"
// eslint-disable-next-line
// TODO: this is just a place holder link pending when its part of the response payload from service
- to="https://enevti.com/"
+ to={serviceUrl}
>
- {t('https://enevti.com/')}
+ {t(serviceUrl)}
- Deposited:
+ {t('Deposited:')}
{/* TODO: this is a placeholder value pending when its part of service response */}
- 5,351.859 LSK
+
+
+ {' '}
+ LSK
+
- {footerDetails.map(({ header, content }, index) => (
+ {footerDetails.map(({ header, content, className }, index) => (
+ {header}
+
+ )}
>
- {content}
+
+ {content}
+
))}
diff --git a/src/modules/blockchainApplication/explore/hooks/usePinBlockchainApplication.js b/src/modules/blockchainApplication/explore/hooks/usePinBlockchainApplication.js
index 4cc0524645..f5b162f1a0 100644
--- a/src/modules/blockchainApplication/explore/hooks/usePinBlockchainApplication.js
+++ b/src/modules/blockchainApplication/explore/hooks/usePinBlockchainApplication.js
@@ -13,8 +13,7 @@ export function usePinBlockchainApplication() {
(chainId) => dispatch(removePinnedApplication(chainId)),
[],
);
- const checkPinByChainId = useCallback((chainId) => pins.includes(chainId), []);
-
+ const checkPinByChainId = useCallback((chainId) => pins.includes(chainId), [pins]);
return {
pins, setPin, deletePin, checkPinByChainId,
};
From 508e661787f43cdce24066fe2c8d77b141fe5f64 Mon Sep 17 00:00:00 2001
From: eniolam1000752
Date: Fri, 1 Jul 2022 12:51:53 +0100
Subject: [PATCH 14/25] fix: fixed failing unit test
---
.../blockchainApplication/api/index.js | 4 +--
.../BlockchainApplicationDetails.test.js | 36 ++++++++++++++-----
2 files changed, 29 insertions(+), 11 deletions(-)
diff --git a/src/modules/blockchainApplication/api/index.js b/src/modules/blockchainApplication/api/index.js
index 24340e5b43..34d8d41e4c 100644
--- a/src/modules/blockchainApplication/api/index.js
+++ b/src/modules/blockchainApplication/api/index.js
@@ -17,6 +17,8 @@ const httpPaths = {
* @param {Object} data.network - Network setting from Redux store
* @returns {Promise} Transaction details API call
*/
+
+// eslint-disable-next-line import/prefer-default-export
export const getApplication = ({
params, network, baseUrl,
}) => http({
@@ -25,5 +27,3 @@ export const getApplication = ({
network,
baseUrl,
});
-
-export const getApplications = () => {};
diff --git a/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.test.js b/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.test.js
index 8a819cf5a0..9187912cff 100644
--- a/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.test.js
+++ b/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.test.js
@@ -3,19 +3,20 @@ import { fireEvent, screen } from '@testing-library/react';
import mockBlockchainApplications from '@tests/fixtures/blockchainApplications';
import { renderWithRouter } from 'src/utils/testHelpers';
import BlockchainApplicationDetails from '.';
+import { usePinBlockchainApplication } from '../../hooks/usePinBlockchainApplication';
const mockedPins = ['1111'];
const mockSetPin = jest.fn();
const mockDeletePin = jest.fn();
-jest.mock('../../hooks/usePinBlockchainApplication', () => ({
- usePinBlockchainApplication: () =>
- ({
- setPin: mockSetPin,
- deletePin: mockDeletePin,
- pins: mockedPins,
- checkPinByChainId: jest.fn(),
- }),
-}));
+
+jest.mock('../../hooks/usePinBlockchainApplication');
+
+usePinBlockchainApplication.mockReturnValue({
+ setPin: mockSetPin,
+ deletePin: mockDeletePin,
+ pins: mockedPins,
+ checkPinByChainId: jest.fn(),
+});
describe('BlockchainApplicationDetails', () => {
const props = {
@@ -59,4 +60,21 @@ describe('BlockchainApplicationDetails', () => {
expect(mockSetPin).toHaveBeenCalled();
});
+
+ it('should unpin blockchain application', () => {
+ usePinBlockchainApplication.mockReturnValue(
+ {
+ setPin: mockSetPin,
+ deletePin: mockDeletePin,
+ pins: mockedPins,
+ checkPinByChainId: jest.fn().mockReturnValue(true),
+ },
+ );
+
+ renderWithRouter(BlockchainApplicationDetails, props);
+ const pinButton = screen.queryAllByTestId('pin-button')[1];
+ fireEvent.click(pinButton);
+
+ expect(mockDeletePin).toHaveBeenCalled();
+ });
});
From b619168bb4c19f3c980b095620ef0795fbed9f38 Mon Sep 17 00:00:00 2001
From: eniolam1000752
Date: Fri, 1 Jul 2022 13:45:50 +0100
Subject: [PATCH 15/25] fix: fixed issues raised by ikem
---
packages/views/screens/router/routesMap.js | 6 +++---
src/modules/blockchainApplication/api/index.js | 9 ++++-----
.../blockchainApplication/explore/store/action.js | 8 ++++----
.../blockchainApplication/explore/store/reducer.js | 6 +++---
4 files changed, 14 insertions(+), 15 deletions(-)
diff --git a/packages/views/screens/router/routesMap.js b/packages/views/screens/router/routesMap.js
index a401497da0..a59d3993f0 100644
--- a/packages/views/screens/router/routesMap.js
+++ b/packages/views/screens/router/routesMap.js
@@ -39,9 +39,9 @@ import AddAccountByFile from '@account/components/AddAccountByFile';
import AddAccountForm from '@account/components/AddAccountForm';
import SwitchAccount from '@account/components/SwitchAccount';
import BackupRecoveryPhraseFlow from '@account/components/BackupRecoveryPhraseFlow';
-import RemoveCurrentAccountFlow from 'src/modules/account/components/RemoveCurrentAccountFlow';
-import RemoveSelectedAccountFlow from 'src/modules/account/components/RemoveSelectedAccountFlow';
-import BlockchainApplicationDetails from 'src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails';
+import RemoveCurrentAccountFlow from '@account/components/RemoveCurrentAccountFlow';
+import RemoveSelectedAccountFlow from '@account/components/RemoveSelectedAccountFlow';
+import BlockchainApplicationDetails from '@blockchainApplication/explore/components/BlockchainApplicationDetails';
export default {
wallet: AccountDetails,
diff --git a/src/modules/blockchainApplication/api/index.js b/src/modules/blockchainApplication/api/index.js
index 34d8d41e4c..0e0521dddd 100644
--- a/src/modules/blockchainApplication/api/index.js
+++ b/src/modules/blockchainApplication/api/index.js
@@ -6,16 +6,15 @@ const httpPaths = {
};
/**
- * Retrieves the details of a single transaction
+ * Retrieves the details of a single blockchain application
*
* @param {Object} data
* @param {String} data.params
- * @param {String} data.params.id - Id of the transaction
+ * @param {String} data.params.chainId - Id of the chain
* @param {String?} data.baseUrl - Lisk Service API url to override the
- * existing ServiceUrl on the network param. We may use this to retrieve
- * the details of an archived transaction.
+ * existing ServiceUrl on the network param.
* @param {Object} data.network - Network setting from Redux store
- * @returns {Promise} Transaction details API call
+ * @returns {Promise} Blockchain application details API call
*/
// eslint-disable-next-line import/prefer-default-export
diff --git a/src/modules/blockchainApplication/explore/store/action.js b/src/modules/blockchainApplication/explore/store/action.js
index dd0ac3429f..62de4e136e 100644
--- a/src/modules/blockchainApplication/explore/store/action.js
+++ b/src/modules/blockchainApplication/explore/store/action.js
@@ -5,12 +5,12 @@ import actionTypes from './actionTypes';
*
* @returns {Object} - Action object
*/
-export const pinApplication = (chainId) => ({
+export const pinApplication = (data) => ({
type: actionTypes.setApplicationPin,
- chainId,
+ data,
});
-export const removePinnedApplication = (chainId) => ({
+export const removePinnedApplication = (data) => ({
type: actionTypes.removeApplicationPin,
- chainId,
+ data,
});
diff --git a/src/modules/blockchainApplication/explore/store/reducer.js b/src/modules/blockchainApplication/explore/store/reducer.js
index 46844131fa..df31ac333d 100644
--- a/src/modules/blockchainApplication/explore/store/reducer.js
+++ b/src/modules/blockchainApplication/explore/store/reducer.js
@@ -8,12 +8,12 @@ import actionTypes from './actionTypes';
* @param {Object} state
* @param {type: String, chainId: string} action
*/
-export const pins = (state = [], { type, chainId }) => {
+export const pins = (state = [], { type, data }) => {
switch (type) {
case actionTypes.setApplicationPin:
- return chainId ? [...state, chainId] : [...state];
+ return data ? [...state, data] : [...state];
case actionTypes.removeApplicationPin:
- return state.filter((pinnedChainId) => pinnedChainId !== chainId);
+ return state.filter((pinnedChainId) => pinnedChainId !== data);
default:
return state;
}
From 16dc5208cabd9f0aa02b081b94c5a3228d6fe553 Mon Sep 17 00:00:00 2001
From: eniolam1000752
Date: Fri, 1 Jul 2022 13:53:21 +0100
Subject: [PATCH 16/25] fix: fixed pr comment
---
packages/common/store/reducers/index.js | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/packages/common/store/reducers/index.js b/packages/common/store/reducers/index.js
index 82c4312f33..c66cbd7a8b 100644
--- a/packages/common/store/reducers/index.js
+++ b/packages/common/store/reducers/index.js
@@ -8,7 +8,7 @@ export { default as transactions } from '@transaction/store/reducer';
export { default as appUpdates } from '@update/store/reducers/appUpdates';
export { default as voting } from '@dpos/validator/store/reducers/voting';
export { default as watchList } from '@dpos/validator/store/reducers/watchList';
+export { account } from '@account/store/reducer';
+export { blockChainApplications } from '@blockchainApplication/explore/store/reducer';
export { default as service } from './service';
export { default as loading } from './loading';
-export { account } from 'src/modules/account/store/reducer';
-export { blockChainApplications } from 'src/modules/blockchainApplication/explore/store/reducer';
From 4e770b9f4c154d89358b1a816daf3acf4c067461 Mon Sep 17 00:00:00 2001
From: eniolam1000752
Date: Fri, 1 Jul 2022 15:16:36 +0100
Subject: [PATCH 17/25] fix: fixed unit test
---
.../explore/hooks/usePinBlockchainApplication.test.js | 2 +-
.../blockchainApplication/explore/store/action.test.js | 4 ++--
.../blockchainApplication/explore/store/reducer.test.js | 8 ++++----
3 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/src/modules/blockchainApplication/explore/hooks/usePinBlockchainApplication.test.js b/src/modules/blockchainApplication/explore/hooks/usePinBlockchainApplication.test.js
index 79d46c0483..11cdc24474 100644
--- a/src/modules/blockchainApplication/explore/hooks/usePinBlockchainApplication.test.js
+++ b/src/modules/blockchainApplication/explore/hooks/usePinBlockchainApplication.test.js
@@ -74,7 +74,7 @@ describe('usePinBlockchainApplication hook', () => {
const chainId = mockBlockchainApplications[0].chainID;
const expectedAction = {
type: actionTypes.removeApplicationPin,
- chainId,
+ data: chainId,
};
act(() => deletePin(chainId));
diff --git a/src/modules/blockchainApplication/explore/store/action.test.js b/src/modules/blockchainApplication/explore/store/action.test.js
index c6b736f8de..1a4a26b228 100644
--- a/src/modules/blockchainApplication/explore/store/action.test.js
+++ b/src/modules/blockchainApplication/explore/store/action.test.js
@@ -16,7 +16,7 @@ describe('actions: blockchainApplication', () => {
it('should create an action to pin blockchain application', () => {
const expectedAction = {
type: actionTypes.setApplicationPin,
- chainId,
+ data: chainId,
};
expect(pinApplication(chainId)).toEqual(expectedAction);
@@ -24,7 +24,7 @@ describe('actions: blockchainApplication', () => {
it('should create an action to remove blockchain application', () => {
const expectedAction = {
type: actionTypes.removeApplicationPin,
- chainId,
+ data: chainId,
};
expect(removePinnedApplication(chainId)).toEqual(expectedAction);
diff --git a/src/modules/blockchainApplication/explore/store/reducer.test.js b/src/modules/blockchainApplication/explore/store/reducer.test.js
index 9d5a3b6f69..837ae5f107 100644
--- a/src/modules/blockchainApplication/explore/store/reducer.test.js
+++ b/src/modules/blockchainApplication/explore/store/reducer.test.js
@@ -6,17 +6,17 @@ describe('BlockchainApplication reducer', () => {
it('Should return list of chainIds', async () => {
const actionData = {
type: actionTypes.setApplicationPin,
- chainId: mockBlockchainApplications[0].chainID,
+ data: mockBlockchainApplications[0].chainID,
};
- expect(pins([], actionData)).toContain(actionData.chainId);
+ expect(pins([], actionData)).toContain(actionData.data);
});
it('Should return list of chainIds without the removed one', async () => {
const actionData = {
type: actionTypes.removeApplicationPin,
- chainId: mockBlockchainApplications[0].chainID,
+ data: mockBlockchainApplications[0].chainID,
};
expect(pins([mockBlockchainApplications[0].chainID], actionData)).not
- .toContain(actionData.chainId);
+ .toContain(actionData.data);
});
});
From 842fe34c62e2c1459aae36062e1d4fd3a2a2363d Mon Sep 17 00:00:00 2001
From: eniolam1000752
Date: Fri, 1 Jul 2022 15:25:56 +0100
Subject: [PATCH 18/25] fix:fixed failing e2e test
---
.../BlockchainApplicationDetails.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.js b/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.js
index da80f6c1fa..0ed67888a9 100644
--- a/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.js
+++ b/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.js
@@ -62,7 +62,7 @@ const BlockchainApplicationDetails = ({ location }) => {
},
{
header: t('Status'),
- className: `${styles.detailContentText} ${styles.statusChip} ${styles[state]}`,
+ className: `${styles.detailContentText} ${styles.statusChip} ${styles[state]} chain-status`,
content: t(state),
},
{
From 6a3b5700a7394bdb8d7933b03b87b552d67e516e Mon Sep 17 00:00:00 2001
From: eniolam1000752
Date: Sun, 3 Jul 2022 17:17:39 +0100
Subject: [PATCH 19/25] fix: fixed comments from masoud
---
packages/common/store/reducers/index.js | 2 +-
.../BlockchainApplicationDetails.js | 136 +++++++++---------
.../BlockchainApplicationDetails.test.js | 15 +-
.../hooks/usePinBlockchainApplication.js | 12 +-
.../hooks/usePinBlockchainApplication.test.js | 34 ++---
.../explore/store/action.test.js | 32 -----
.../explore/store/actionTypes.js | 6 -
.../{explore => manage}/store/action.js | 13 +-
.../manage/store/action.test.js | 23 +++
.../manage/store/actionTypes.js | 5 +
.../{explore => manage}/store/reducer.js | 12 +-
.../{explore => manage}/store/reducer.test.js | 12 +-
.../{explore => manage}/store/selectors.js | 0
.../store/selectors.test.js | 0
14 files changed, 145 insertions(+), 157 deletions(-)
delete mode 100644 src/modules/blockchainApplication/explore/store/action.test.js
delete mode 100644 src/modules/blockchainApplication/explore/store/actionTypes.js
rename src/modules/blockchainApplication/{explore => manage}/store/action.js (50%)
create mode 100644 src/modules/blockchainApplication/manage/store/action.test.js
create mode 100644 src/modules/blockchainApplication/manage/store/actionTypes.js
rename src/modules/blockchainApplication/{explore => manage}/store/reducer.js (69%)
rename src/modules/blockchainApplication/{explore => manage}/store/reducer.test.js (66%)
rename src/modules/blockchainApplication/{explore => manage}/store/selectors.js (100%)
rename src/modules/blockchainApplication/{explore => manage}/store/selectors.test.js (100%)
diff --git a/packages/common/store/reducers/index.js b/packages/common/store/reducers/index.js
index c66cbd7a8b..52d65d6faf 100644
--- a/packages/common/store/reducers/index.js
+++ b/packages/common/store/reducers/index.js
@@ -9,6 +9,6 @@ export { default as appUpdates } from '@update/store/reducers/appUpdates';
export { default as voting } from '@dpos/validator/store/reducers/voting';
export { default as watchList } from '@dpos/validator/store/reducers/watchList';
export { account } from '@account/store/reducer';
-export { blockChainApplications } from '@blockchainApplication/explore/store/reducer';
+export { blockChainApplications } from '@blockchainApplication/manage/store/reducer';
export { default as service } from './service';
export { default as loading } from './loading';
diff --git a/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.js b/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.js
index 0ed67888a9..42b96020ad 100644
--- a/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.js
+++ b/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.js
@@ -28,21 +28,20 @@ const application = {
},
};
+const deposit = 5e10;
+const serviceUrl = 'https://enevti.com/';
+
const BlockchainApplicationDetails = ({ location }) => {
const { t } = useTranslation();
const chainId = parseSearchParams(location.search).chainId;
- const { checkPinByChainId, deletePin, setPin } = usePinBlockchainApplication();
+ const { checkPinByChainId, togglePin } = usePinBlockchainApplication();
const {
- name, state, address, lastCertificateHeight, lastUpdated, deposit, serviceUrl,
+ name, state, address, lastCertificateHeight, lastUpdated,
} = application.data;
const isPinned = checkPinByChainId(chainId);
const toggleApplicationPin = () => {
- if (!isPinned) {
- setPin(chainId);
- } else {
- deletePin(chainId);
- }
+ togglePin(chainId);
};
const footerDetails = [
@@ -80,73 +79,70 @@ const BlockchainApplicationDetails = ({ location }) => {
return (
-
-
-
-
- {/* TODO: chain logo goes here when its available from service's response */}
-
+
+
+ {/* TODO: chain logo goes here when its available from service's response */}
-
-
- {name}
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+ {name}
+
+
+
+
+
+
+
+
+
+
+
+
+
-
- {t(serviceUrl)}
-
-
-
- {t('Deposited:')}
- {/* TODO: this is a placeholder value pending when its part of service response */}
-
-
- {' '}
- LSK
-
-
-
- {footerDetails.map(({ header, content, className }, index) => (
-
- {header}
-
- )}
- >
-
- {content}
+ to={serviceUrl}
+ >
+
+ {t(serviceUrl)}
+
+
+
+ {t('Deposited:')}
+ {/* TODO: this is a placeholder value pending when its part of service response */}
+
+
+ {' '}
+ LSK
+
+
+
+ {footerDetails.map(({ header, content, className }, index) => (
+
+ {header}
-
- ))}
-
+ )}
+ >
+
+ {content}
+
+
+ ))}
diff --git a/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.test.js b/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.test.js
index 9187912cff..1bdfae4d7c 100644
--- a/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.test.js
+++ b/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.test.js
@@ -6,14 +6,12 @@ import BlockchainApplicationDetails from '.';
import { usePinBlockchainApplication } from '../../hooks/usePinBlockchainApplication';
const mockedPins = ['1111'];
-const mockSetPin = jest.fn();
-const mockDeletePin = jest.fn();
+const mockTogglePin = jest.fn();
jest.mock('../../hooks/usePinBlockchainApplication');
usePinBlockchainApplication.mockReturnValue({
- setPin: mockSetPin,
- deletePin: mockDeletePin,
+ togglePin: mockTogglePin,
pins: mockedPins,
checkPinByChainId: jest.fn(),
});
@@ -58,16 +56,15 @@ describe('BlockchainApplicationDetails', () => {
const pinButton = screen.queryByTestId('pin-button');
fireEvent.click(pinButton);
- expect(mockSetPin).toHaveBeenCalled();
+ expect(mockTogglePin).toHaveBeenCalled();
});
it('should unpin blockchain application', () => {
usePinBlockchainApplication.mockReturnValue(
{
- setPin: mockSetPin,
- deletePin: mockDeletePin,
+ togglePin: mockTogglePin,
pins: mockedPins,
- checkPinByChainId: jest.fn().mockReturnValue(true),
+ checkPinByChainId: jest.fn(),
},
);
@@ -75,6 +72,6 @@ describe('BlockchainApplicationDetails', () => {
const pinButton = screen.queryAllByTestId('pin-button')[1];
fireEvent.click(pinButton);
- expect(mockDeletePin).toHaveBeenCalled();
+ expect(mockTogglePin).toHaveBeenCalled();
});
});
diff --git a/src/modules/blockchainApplication/explore/hooks/usePinBlockchainApplication.js b/src/modules/blockchainApplication/explore/hooks/usePinBlockchainApplication.js
index f5b162f1a0..12f50b86c3 100644
--- a/src/modules/blockchainApplication/explore/hooks/usePinBlockchainApplication.js
+++ b/src/modules/blockchainApplication/explore/hooks/usePinBlockchainApplication.js
@@ -1,20 +1,16 @@
import { useCallback } from 'react';
import { useSelector, useDispatch } from 'react-redux';
-import { selectPinnedApplications } from '@blockchainApplication/explore/store/selectors';
-import { pinApplication, removePinnedApplication } from '../store/action';
+import { selectPinnedApplications } from '@blockchainApplication/manage/store/selectors';
+import { toggleApplicationPin } from '../../manage/store/action';
// eslint-disable-next-line
export function usePinBlockchainApplication() {
const dispatch = useDispatch();
const pins = useSelector(selectPinnedApplications);
- const setPin = useCallback((chainId) => dispatch(pinApplication(chainId)), []);
- const deletePin = useCallback(
- (chainId) => dispatch(removePinnedApplication(chainId)),
- [],
- );
+ const togglePin = useCallback((chainId) => dispatch(toggleApplicationPin(chainId)), []);
const checkPinByChainId = useCallback((chainId) => pins.includes(chainId), [pins]);
return {
- pins, setPin, deletePin, checkPinByChainId,
+ pins, togglePin, checkPinByChainId,
};
}
diff --git a/src/modules/blockchainApplication/explore/hooks/usePinBlockchainApplication.test.js b/src/modules/blockchainApplication/explore/hooks/usePinBlockchainApplication.test.js
index 11cdc24474..76aca45fa3 100644
--- a/src/modules/blockchainApplication/explore/hooks/usePinBlockchainApplication.test.js
+++ b/src/modules/blockchainApplication/explore/hooks/usePinBlockchainApplication.test.js
@@ -1,8 +1,8 @@
import { renderHook, act } from '@testing-library/react-hooks';
import mockBlockchainApplications from '@tests/fixtures/blockchainApplications';
-import actionTypes from '@blockchainApplication/explore/store/actionTypes';
+import actionTypes from '@blockchainApplication/manage/store/actionTypes';
import { usePinBlockchainApplication } from './usePinBlockchainApplication';
-import { pinApplication } from '../store/action';
+import { toggleApplicationPin } from '../../manage/store/action';
const mockDispatch = jest.fn();
const mockState = {
@@ -21,35 +21,35 @@ describe('usePinBlockchainApplication hook', () => {
});
const { result } = renderHook(() => usePinBlockchainApplication());
- it('setPin and deletePin Should not be triggered on mounting', async () => {
+ it('togglePin Should not be triggered on mounting', async () => {
expect(mockDispatch).toHaveBeenCalledTimes(0);
});
- it('setPin should dispatch an action', async () => {
- const { setPin } = result.current;
+ it('togglePin should dispatch an action', async () => {
+ const { togglePin } = result.current;
const chainId = mockBlockchainApplications[0].chainID;
- act(() => setPin(chainId));
+ act(() => togglePin(chainId));
expect(mockDispatch).toHaveBeenCalledTimes(1);
expect(mockDispatch).toHaveBeenCalledWith(
- pinApplication(chainId),
+ toggleApplicationPin(chainId),
);
});
- it('should return pins as an arrray', async () => {
- const { pins, setPin } = result.current;
+ it('should return pins as an array', async () => {
+ const { pins, togglePin } = result.current;
const chainId = mockBlockchainApplications[0].chainID;
- act(() => setPin(chainId));
+ act(() => togglePin(chainId));
const expectPins = mockBlockchainApplications.map(({ chainID }) => chainID);
expect(pins).toEqual(expect.arrayContaining(expectPins));
});
it('should flag chain as a pinned application', async () => {
- const { checkPinByChainId, setPin } = result.current;
+ const { checkPinByChainId, togglePin } = result.current;
const chainId = mockBlockchainApplications[0].chainID;
- act(() => setPin(chainId));
+ act(() => togglePin(chainId));
expect(checkPinByChainId(chainId)).toBeTruthy();
});
@@ -69,15 +69,15 @@ describe('usePinBlockchainApplication hook', () => {
expect(checkPinByChainId(chainId)).not.toBeTruthy();
});
- it('deletePin should dispatch an action', async () => {
- const { deletePin } = result.current;
+ it('togglePin should dispatch an action', async () => {
+ const { togglePin } = result.current;
const chainId = mockBlockchainApplications[0].chainID;
const expectedAction = {
- type: actionTypes.removeApplicationPin,
- data: chainId,
+ type: actionTypes.toggleApplicationPin,
+ chainId,
};
- act(() => deletePin(chainId));
+ act(() => togglePin(chainId));
expect(mockDispatch).toHaveBeenCalledTimes(1);
expect(mockDispatch).toHaveBeenCalledWith(expectedAction);
});
diff --git a/src/modules/blockchainApplication/explore/store/action.test.js b/src/modules/blockchainApplication/explore/store/action.test.js
deleted file mode 100644
index 1a4a26b228..0000000000
--- a/src/modules/blockchainApplication/explore/store/action.test.js
+++ /dev/null
@@ -1,32 +0,0 @@
-/* eslint-disable max-lines */
-import mockBlockchainApplications from '@tests/fixtures/blockchainApplications';
-import actionTypes from './actionTypes';
-import {
- removePinnedApplication,
- pinApplication,
-} from './action';
-
-const chainId = mockBlockchainApplications[0].chainID;
-
-describe('actions: blockchainApplication', () => {
- beforeEach(() => {
- jest.resetAllMocks();
- });
-
- it('should create an action to pin blockchain application', () => {
- const expectedAction = {
- type: actionTypes.setApplicationPin,
- data: chainId,
- };
-
- expect(pinApplication(chainId)).toEqual(expectedAction);
- });
- it('should create an action to remove blockchain application', () => {
- const expectedAction = {
- type: actionTypes.removeApplicationPin,
- data: chainId,
- };
-
- expect(removePinnedApplication(chainId)).toEqual(expectedAction);
- });
-});
diff --git a/src/modules/blockchainApplication/explore/store/actionTypes.js b/src/modules/blockchainApplication/explore/store/actionTypes.js
deleted file mode 100644
index 2c80ea70ad..0000000000
--- a/src/modules/blockchainApplication/explore/store/actionTypes.js
+++ /dev/null
@@ -1,6 +0,0 @@
-const actionTypes = {
- setApplicationPin: 'SET_APPLICATION_PIN',
- removeApplicationPin: 'REMOVE_APPLICATION_PIN',
-};
-
-export default actionTypes;
diff --git a/src/modules/blockchainApplication/explore/store/action.js b/src/modules/blockchainApplication/manage/store/action.js
similarity index 50%
rename from src/modules/blockchainApplication/explore/store/action.js
rename to src/modules/blockchainApplication/manage/store/action.js
index 62de4e136e..5de1883168 100644
--- a/src/modules/blockchainApplication/explore/store/action.js
+++ b/src/modules/blockchainApplication/manage/store/action.js
@@ -5,12 +5,17 @@ import actionTypes from './actionTypes';
*
* @returns {Object} - Action object
*/
-export const pinApplication = (data) => ({
+export const pinApplication = (chainId) => ({
type: actionTypes.setApplicationPin,
- data,
+ chainId,
});
-export const removePinnedApplication = (data) => ({
+export const toggleApplicationPin = (chainId) => ({
+ type: actionTypes.toggleApplicationPin,
+ chainId,
+});
+
+export const removePinnedApplication = (chainId) => ({
type: actionTypes.removeApplicationPin,
- data,
+ chainId,
});
diff --git a/src/modules/blockchainApplication/manage/store/action.test.js b/src/modules/blockchainApplication/manage/store/action.test.js
new file mode 100644
index 0000000000..e11219485b
--- /dev/null
+++ b/src/modules/blockchainApplication/manage/store/action.test.js
@@ -0,0 +1,23 @@
+/* eslint-disable max-lines */
+import mockBlockchainApplications from '@tests/fixtures/blockchainApplications';
+import actionTypes from './actionTypes';
+import {
+ toggleApplicationPin,
+} from './action';
+
+const chainId = mockBlockchainApplications[0].chainID;
+
+describe('actions: blockchainApplication', () => {
+ beforeEach(() => {
+ jest.resetAllMocks();
+ });
+
+ it('should create an action to toggle blockchain application', () => {
+ const expectedAction = {
+ type: actionTypes.toggleApplicationPin,
+ chainId,
+ };
+
+ expect(toggleApplicationPin(chainId)).toEqual(expectedAction);
+ });
+});
diff --git a/src/modules/blockchainApplication/manage/store/actionTypes.js b/src/modules/blockchainApplication/manage/store/actionTypes.js
new file mode 100644
index 0000000000..8d65e239be
--- /dev/null
+++ b/src/modules/blockchainApplication/manage/store/actionTypes.js
@@ -0,0 +1,5 @@
+const actionTypes = {
+ toggleApplicationPin: 'TOGGLE_APPLICATION_PIN',
+};
+
+export default actionTypes;
diff --git a/src/modules/blockchainApplication/explore/store/reducer.js b/src/modules/blockchainApplication/manage/store/reducer.js
similarity index 69%
rename from src/modules/blockchainApplication/explore/store/reducer.js
rename to src/modules/blockchainApplication/manage/store/reducer.js
index df31ac333d..ce68d19a32 100644
--- a/src/modules/blockchainApplication/explore/store/reducer.js
+++ b/src/modules/blockchainApplication/manage/store/reducer.js
@@ -8,12 +8,14 @@ import actionTypes from './actionTypes';
* @param {Object} state
* @param {type: String, chainId: string} action
*/
-export const pins = (state = [], { type, data }) => {
+export const pins = (state = [], { type, chainId }) => {
switch (type) {
- case actionTypes.setApplicationPin:
- return data ? [...state, data] : [...state];
- case actionTypes.removeApplicationPin:
- return state.filter((pinnedChainId) => pinnedChainId !== data);
+ case actionTypes.toggleApplicationPin:
+ if (state.includes(chainId) && chainId) {
+ return state.filter((pinnedChainId) => pinnedChainId !== chainId);
+ }
+ return chainId ? [...state, chainId] : [...state];
+
default:
return state;
}
diff --git a/src/modules/blockchainApplication/explore/store/reducer.test.js b/src/modules/blockchainApplication/manage/store/reducer.test.js
similarity index 66%
rename from src/modules/blockchainApplication/explore/store/reducer.test.js
rename to src/modules/blockchainApplication/manage/store/reducer.test.js
index 837ae5f107..3813366308 100644
--- a/src/modules/blockchainApplication/explore/store/reducer.test.js
+++ b/src/modules/blockchainApplication/manage/store/reducer.test.js
@@ -5,17 +5,19 @@ import { pins } from './reducer';
describe('BlockchainApplication reducer', () => {
it('Should return list of chainIds', async () => {
const actionData = {
- type: actionTypes.setApplicationPin,
- data: mockBlockchainApplications[0].chainID,
+ type: actionTypes.toggleApplicationPin,
+ chainId: mockBlockchainApplications[0].chainID,
};
- expect(pins([], actionData)).toContain(actionData.data);
+
+ expect(pins([], actionData)).toContain(actionData.chainId);
});
it('Should return list of chainIds without the removed one', async () => {
const actionData = {
- type: actionTypes.removeApplicationPin,
- data: mockBlockchainApplications[0].chainID,
+ type: actionTypes.toggleApplicationPin,
+ chainId: mockBlockchainApplications[0].chainID,
};
+
expect(pins([mockBlockchainApplications[0].chainID], actionData)).not
.toContain(actionData.data);
});
diff --git a/src/modules/blockchainApplication/explore/store/selectors.js b/src/modules/blockchainApplication/manage/store/selectors.js
similarity index 100%
rename from src/modules/blockchainApplication/explore/store/selectors.js
rename to src/modules/blockchainApplication/manage/store/selectors.js
diff --git a/src/modules/blockchainApplication/explore/store/selectors.test.js b/src/modules/blockchainApplication/manage/store/selectors.test.js
similarity index 100%
rename from src/modules/blockchainApplication/explore/store/selectors.test.js
rename to src/modules/blockchainApplication/manage/store/selectors.test.js
From f5c25fe1fa99f821ec6655a625c0e2607d0e0996 Mon Sep 17 00:00:00 2001
From: eniolam1000752
Date: Sun, 3 Jul 2022 18:44:39 +0100
Subject: [PATCH 20/25] fix: resolved issues from ikem
---
.../BlockchainApplicationDetails.js | 4 +---
.../BlockchainApplicationDetails.test.js | 2 +-
tests/constants/selectors.js | 1 -
3 files changed, 2 insertions(+), 5 deletions(-)
diff --git a/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.js b/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.js
index 42b96020ad..d3b5f0ffd5 100644
--- a/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.js
+++ b/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.js
@@ -11,8 +11,8 @@ import Icon from 'src/theme/Icon';
import Tooltip from 'src/theme/Tooltip';
import { Link } from 'react-router-dom';
import { parseSearchParams } from 'src/utils/searchParams';
-import styles from './BlockchainApplicationDetails.css';
import { usePinBlockchainApplication } from '../../hooks/usePinBlockchainApplication';
+import styles from './BlockchainApplicationDetails.css';
// TODO: this is a mock response of an application's details
const application = {
@@ -23,8 +23,6 @@ const application = {
address: 'lsk24cd35u4jdq8szo3pnsqe5dsxwrnazyqqqg5eu',
lastCertificateHeight: 1000,
lastUpdated: 123456789,
- deposit: 5e10,
- serviceUrl: 'https://enevti.com/',
},
};
diff --git a/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.test.js b/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.test.js
index 1bdfae4d7c..9affc7db6e 100644
--- a/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.test.js
+++ b/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.test.js
@@ -2,8 +2,8 @@ import moment from 'moment';
import { fireEvent, screen } from '@testing-library/react';
import mockBlockchainApplications from '@tests/fixtures/blockchainApplications';
import { renderWithRouter } from 'src/utils/testHelpers';
-import BlockchainApplicationDetails from '.';
import { usePinBlockchainApplication } from '../../hooks/usePinBlockchainApplication';
+import BlockchainApplicationDetails from '.';
const mockedPins = ['1111'];
const mockTogglePin = jest.fn();
diff --git a/tests/constants/selectors.js b/tests/constants/selectors.js
index 88efa2b2ab..9e70bc4e4c 100644
--- a/tests/constants/selectors.js
+++ b/tests/constants/selectors.js
@@ -294,7 +294,6 @@ const ss = {
verifyPublicKeyInput: '.publicKey',
verifySignatureInput: '.signature',
acceptTermsButton: '.accept-terms',
-
blockchainName: '.chain-name-text',
chainOwnerAddress: '.copy-address-wrapper .copy-title',
chainIdDisplay: '.chain-id',
From 36af2bf467f33ee9b20b0fd2d21fa6fc302fec90 Mon Sep 17 00:00:00 2001
From: eniolam1000752
Date: Sun, 3 Jul 2022 19:26:03 +0100
Subject: [PATCH 21/25] fixed failing unit tests
---
.../BlockchainApplicationDetails.test.js | 18 ++++++------------
.../manage/store/action.js | 14 +++-----------
2 files changed, 9 insertions(+), 23 deletions(-)
diff --git a/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.test.js b/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.test.js
index 9affc7db6e..ae2593b29c 100644
--- a/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.test.js
+++ b/src/modules/blockchainApplication/explore/components/BlockchainApplicationDetails/BlockchainApplicationDetails.test.js
@@ -13,7 +13,7 @@ jest.mock('../../hooks/usePinBlockchainApplication');
usePinBlockchainApplication.mockReturnValue({
togglePin: mockTogglePin,
pins: mockedPins,
- checkPinByChainId: jest.fn(),
+ checkPinByChainId: jest.fn().mockReturnValue(true),
});
describe('BlockchainApplicationDetails', () => {
@@ -52,26 +52,20 @@ describe('BlockchainApplicationDetails', () => {
expect(screen.getByText('Deposited:')).toBeTruthy();
});
- it('should pin blockchain application', () => {
- const pinButton = screen.queryByTestId('pin-button');
- fireEvent.click(pinButton);
-
- expect(mockTogglePin).toHaveBeenCalled();
+ it('should show application as pinned', () => {
+ expect(screen.getByAltText('pinnedIcon')).toBeTruthy();
});
- it('should unpin blockchain application', () => {
+ it('should show application as unpinned', () => {
usePinBlockchainApplication.mockReturnValue(
{
togglePin: mockTogglePin,
pins: mockedPins,
- checkPinByChainId: jest.fn(),
+ checkPinByChainId: jest.fn().mockReturnValue(false),
},
);
renderWithRouter(BlockchainApplicationDetails, props);
- const pinButton = screen.queryAllByTestId('pin-button')[1];
- fireEvent.click(pinButton);
-
- expect(mockTogglePin).toHaveBeenCalled();
+ expect(screen.getByAltText('unpinnedIcon')).toBeTruthy();
});
});
diff --git a/src/modules/blockchainApplication/manage/store/action.js b/src/modules/blockchainApplication/manage/store/action.js
index 5de1883168..f41ce380d2 100644
--- a/src/modules/blockchainApplication/manage/store/action.js
+++ b/src/modules/blockchainApplication/manage/store/action.js
@@ -1,21 +1,13 @@
import actionTypes from './actionTypes';
/**
- * Trigger this action to set blockchain application pin
+ * Trigger this action to toggle blockchain application pin
*
* @returns {Object} - Action object
*/
-export const pinApplication = (chainId) => ({
- type: actionTypes.setApplicationPin,
- chainId,
-});
-
+//
+// eslint-disable-next-line import/prefer-default-export
export const toggleApplicationPin = (chainId) => ({
type: actionTypes.toggleApplicationPin,
chainId,
});
-
-export const removePinnedApplication = (chainId) => ({
- type: actionTypes.removeApplicationPin,
- chainId,
-});
From 12e57e16b408a118d79782e91ffb933a124389b4 Mon Sep 17 00:00:00 2001
From: eniolam1000752
Date: Sun, 3 Jul 2022 23:55:37 +0100
Subject: [PATCH 22/25] fixed broken tests and minor CSS changes
---
.../images/default-chain-background.png | Bin 0 -> 73887 bytes
.../BlockchainApplicationDetails.css | 19 +++++++++++--
.../BlockchainApplicationDetails.js | 26 ++++++++++--------
.../BlockchainApplicationDetails.test.js | 2 +-
4 files changed, 32 insertions(+), 15 deletions(-)
create mode 100644 setup/react/assets/images/default-chain-background.png
diff --git a/setup/react/assets/images/default-chain-background.png b/setup/react/assets/images/default-chain-background.png
new file mode 100644
index 0000000000000000000000000000000000000000..f80314122d78c324c9d69372b1a2ed8b4de91450
GIT binary patch
literal 73887
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%0Dnz?QNCGf%KEE!gN
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