Skip to content

Commit b106f8a

Browse files
authored
feat(tokens): separate token tables by category (#7121)
# Motivation The Tokens and Neurons tables will be split into multiple tables to differentiate ICP from other assets. This PR separates the Tokens table into distinct tables for each type: ICP, CK, SNS, and imported. Tables without holdings will be hidden if the user prefers not to display zero balances. | Mobile | Desktop | |--------|--------| | <img width="419" height="882" alt="Screenshot 2025-07-17 at 11 38 36" src="https://github.com/user-attachments/assets/d8630e23-d877-4c1f-837c-d88b9cb4f23c" /> | <img width="1505" height="1316" alt="Screenshot 2025-07-17 at 11 38 56" src="https://github.com/user-attachments/assets/cf0cfe37-2aaf-4805-82bd-0a9c1b6907cf" /> | | <img width="491" height="896" alt="Screenshot 2025-07-17 at 11 39 23" src="https://github.com/user-attachments/assets/73177ffe-0bb0-45e4-8fb0-15288439d11e" /> | <img width="1482" height="1388" alt="Screenshot 2025-07-17 at 11 39 41" src="https://github.com/user-attachments/assets/b0b68341-a978-4800-86dc-e07449f79ff8" /> | | <img width="419" height="882" alt="Screenshot 2025-07-17 at 11 40 47" src="https://github.com/user-attachments/assets/f211a7c3-676c-4d6f-9264-1bf0a0e7492b" /> | <img width="1508" height="1028" alt="Screenshot 2025-07-17 at 11 41 06" src="https://github.com/user-attachments/assets/1fb82622-db51-422b-8dea-9c3550d52c13" /> | | <img width="499" height="900" alt="Screenshot 2025-07-17 at 11 40 20" src="https://github.com/user-attachments/assets/b20e2fca-9176-433a-a52d-f53e59a01fcc" /> | <img width="954" height="741" alt="Screenshot 2025-07-17 at 11 40 06" src="https://github.com/user-attachments/assets/221b0607-e1c2-486a-983c-c97ee133d377" /> | [NNS1-3966](https://dfinity.atlassian.net/browse/NNS1-3966) # Changes - Split the current Tokens table into four instances, one for each type. - Move the Import Token button to the Popover. - Update the table settings button to an IconMenu. # Tests - New unit tests for multiple tables. - [DevEnv](https://qsgjb-riaaa-aaaaa-aaaga-cai.yhabib-ingress.devenv.dfinity.network/tokens/) # Todos - [x] Accessibility (a11y) – any impact? - [x] Changelog – is it needed? [NNS1-3966]: https://dfinity.atlassian.net/browse/NNS1-3966?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
1 parent d8489e8 commit b106f8a

20 files changed

+542
-70
lines changed
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<script lang="ts">
2+
import { MAX_IMPORTED_TOKENS } from "$lib/constants/imported-tokens.constants";
3+
import { i18n } from "$lib/stores/i18n";
4+
import { importedTokensStore } from "$lib/stores/imported-tokens.store";
5+
import { replacePlaceholders } from "$lib/utils/i18n.utils";
6+
import { IconAdd, Tooltip } from "@dfinity/gix-components";
7+
import { nonNullish } from "@dfinity/utils";
8+
9+
type Props = {
10+
isModalVisible: boolean;
11+
};
12+
let { isModalVisible = $bindable() }: Props = $props();
13+
14+
const importedTokens = $derived($importedTokensStore.importedTokens);
15+
const maximumImportedTokensReached = $derived(
16+
(importedTokens?.length ?? 0) >= MAX_IMPORTED_TOKENS
17+
);
18+
19+
const showModal = () => (isModalVisible = true);
20+
</script>
21+
22+
{#if nonNullish(importedTokens)}
23+
<Tooltip
24+
top
25+
testId="maximum-imported-tokens-tooltip"
26+
text={maximumImportedTokensReached
27+
? replacePlaceholders($i18n.import_token.maximum_reached_tooltip, {
28+
$max: `${MAX_IMPORTED_TOKENS}`,
29+
})
30+
: undefined}
31+
>
32+
<button
33+
data-tid="import-token-button"
34+
class="button"
35+
onclick={showModal}
36+
disabled={maximumImportedTokensReached}
37+
>
38+
{$i18n.import_token.import_token}<span class="icon">
39+
<IconAdd />
40+
</span>
41+
</button>
42+
</Tooltip>
43+
{/if}
44+
45+
<style lang="scss">
46+
.button {
47+
display: flex;
48+
justify-content: space-between;
49+
align-items: center;
50+
width: 100%;
51+
padding: 0;
52+
color: var(--gix-color-text);
53+
54+
.icon {
55+
display: flex;
56+
color: var(--primary);
57+
}
58+
}
59+
</style>

frontend/src/lib/components/tokens/TokensTable/TokenActionsCell.svelte

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import ReceiveButton from "$lib/components/tokens/TokensTable/actions/ReceiveButton.svelte";
55
import RemoveButton from "$lib/components/tokens/TokensTable/actions/RemoveButton.svelte";
66
import SendButton from "$lib/components/tokens/TokensTable/actions/SendButton.svelte";
7+
import VoidAction from "$lib/components/tokens/TokensTable/actions/VoidAction.svelte";
78
import {
89
UserTokenAction,
910
type UserTokenData,
@@ -27,6 +28,7 @@
2728
[UserTokenAction.Send]: SendButton,
2829
[UserTokenAction.GoToDashboard]: GoToDashboardButton,
2930
[UserTokenAction.Remove]: RemoveButton,
31+
[UserTokenAction.Blank]: VoidAction,
3032
};
3133
3234
let userToken: UserTokenData | UserTokenFailed | undefined;

frontend/src/lib/components/tokens/TokensTable/TokensTable.svelte

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
export let firstColumnHeader: string;
2020
export let order: TokensTableOrder = [];
2121
export let displayTableSettings = false;
22+
export let testId: string = "tokens-table-component";
2223
2324
let enableSorting: boolean;
2425
$: enableSorting = order.length > 0;
@@ -61,7 +62,7 @@
6162
</script>
6263

6364
<ResponsiveTable
64-
testId="tokens-table-component"
65+
{testId}
6566
tableData={userTokensData}
6667
{columns}
6768
bind:order
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<script lang="ts">
2+
import type { UserTokenData, UserTokenFailed } from "$lib/types/tokens-page";
3+
4+
// svelte-ignore unused-export-let
5+
export let userToken: UserTokenData | UserTokenFailed;
6+
</script>
7+
8+
<span class="blank"></span>
9+
10+
<style lang="scss">
11+
.blank {
12+
width: 36px;
13+
}
14+
</style>

frontend/src/lib/components/ui/ResponsiveTable.svelte

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
sortTableData,
1919
} from "$lib/utils/responsive-table.utils";
2020
import { heightTransition } from "$lib/utils/transition.utils";
21-
import { IconSettings, IconSouth, Popover } from "@dfinity/gix-components";
21+
import { IconMenu, IconSouth, Popover } from "@dfinity/gix-components";
2222
import { assertNonNullish, isNullish, nonNullish } from "@dfinity/utils";
2323
2424
export let testId = "responsive-table-component";
@@ -132,7 +132,7 @@
132132
class="settings-button icon-only"
133133
aria-label={$i18n.tokens.settings_button}
134134
bind:this={settingsButton}
135-
on:click={openSettings}><IconSettings /></button
135+
on:click={openSettings}><IconMenu /></button
136136
>{/if}
137137
</span>
138138
{/if}

frontend/src/lib/derived/tokens-list-user.derived.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ const convertToUserTokenData = ({
117117
}),
118118
actions: [
119119
...(baseTokenData.universeId.toText() === OWN_CANISTER_ID_TEXT
120-
? [UserTokenAction.GoToDetail]
120+
? [UserTokenAction.Blank, UserTokenAction.GoToDetail]
121121
: [UserTokenAction.Receive, UserTokenAction.Send]),
122122
],
123123
accountIdentifier,

frontend/src/lib/i18n/en.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1242,6 +1242,10 @@
12421242
},
12431243
"tokens": {
12441244
"projects_header": "Projects",
1245+
"projects_header_icp": "ICP Token",
1246+
"projects_header_ck": "ckTokens",
1247+
"projects_header_sns": "SNS Tokens",
1248+
"projects_header_imported": "Imported Tokens",
12451249
"balance_header": "Balance",
12461250
"accounts_header": "Accounts",
12471251
"settings_button": "Open tokens settings",

frontend/src/lib/pages/SignInTokens.svelte

Lines changed: 45 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,24 @@
55
import { authSignedInStore } from "$lib/derived/auth.derived";
66
import { pageStore } from "$lib/derived/page.derived";
77
import ImportTokenModal from "$lib/modals/accounts/ImportTokenModal.svelte";
8+
import { ENABLE_NEW_TABLES } from "$lib/stores/feature-flags.store";
89
import { i18n } from "$lib/stores/i18n";
910
import type { UserToken } from "$lib/types/tokens-page";
11+
import { filterTokensByType } from "$lib/utils/tokens-table.utils";
1012
import { IconAccountsPage, PageBanner } from "@dfinity/gix-components";
1113
import { nonNullish } from "@dfinity/utils";
1214
13-
export let userTokensData: UserToken[];
14-
let showImportTokenModal = false;
15-
$: showImportTokenModal =
16-
// Since there are two ImportTokenModals on both Tokens and SignInTokens pages,
17-
// we need to hide this modal after a successful sign-in to
18-
// prevent it from blocking this component’s destruction.
19-
!$authSignedInStore && nonNullish($pageStore.importTokenLedgerId);
15+
type Props = {
16+
userTokensData: UserToken[];
17+
};
18+
const { userTokensData = [] }: Props = $props();
19+
20+
// Since there are two ImportTokenModals on both Tokens and SignInTokens pages,
21+
// we need to hide this modal after a successful sign-in to
22+
// prevent it from blocking this component’s destruction.
23+
let showImportTokenModal = $state(
24+
!$authSignedInStore && nonNullish($pageStore.importTokenLedgerId)
25+
);
2026
</script>
2127

2228
<TestIdWrapper testId="sign-in-tokens-page-component">
@@ -28,14 +34,39 @@
2834
<p class="description" slot="description">{$i18n.auth_accounts.text}</p>
2935
<SignIn slot="actions" />
3036
</PageBanner>
31-
32-
<TokensTable
33-
on:nnsAction
34-
{userTokensData}
35-
firstColumnHeader={$i18n.tokens.projects_header}
36-
/>
37+
{#if $ENABLE_NEW_TABLES}
38+
<TokensTable
39+
on:nnsAction
40+
userTokensData={filterTokensByType({
41+
tokens: userTokensData,
42+
type: "icp",
43+
})}
44+
firstColumnHeader={$i18n.tokens.projects_header_icp}
45+
/>
46+
<TokensTable
47+
on:nnsAction
48+
userTokensData={filterTokensByType({
49+
tokens: userTokensData,
50+
type: "ck",
51+
})}
52+
firstColumnHeader={$i18n.tokens.projects_header_ck}
53+
/>
54+
<TokensTable
55+
on:nnsAction
56+
userTokensData={filterTokensByType({
57+
tokens: userTokensData,
58+
type: "sns",
59+
})}
60+
firstColumnHeader={$i18n.tokens.projects_header_sns}
61+
/>
62+
{:else}
63+
<TokensTable
64+
on:nnsAction
65+
{userTokensData}
66+
firstColumnHeader={$i18n.tokens.projects_header}
67+
/>
68+
{/if}
3769
</div>
38-
3970
{#if showImportTokenModal}
4071
<ImportTokenModal on:nnsClose={() => (showImportTokenModal = false)} />
4172
{/if}

0 commit comments

Comments
 (0)