Skip to content

Commit

Permalink
Support sorting in TokensTable (#6273)
Browse files Browse the repository at this point in the history
# Motivation

We want to allow people to sort the tokens table based on name or
balance.
The tokens table is used on the tokens page but also on the ICP accounts
page.
On the accounts page we don't want to enable sorting.

This PR supports sorting in the tokens table be specifying the `order`
prop on the `TokensTable` component.
We should not specify this prop on the accounts page.
On the tokens page we would use the `$tokensTableOrderStore` as the
`order prop to persist the order between navigation.

BUT! We can not enable this on the tokens page yet either because the
sort button on mobile interferes with the settings button for "hide zero
balances".

So this PR only supports the functionality in the component but does not
yet enable it on the page.

# Changes

1. Support sorting based on either balance or title in the `TokensTable`
component, if the `order` prop is specified to indicate the order that
should be used.

# Tests

1. Unit tests added.
2. Tested manually after passing `$tokensTableOrderStore` as the `order`
prop.

# Todos

- [ ] Add entry to changelog (if necessary).
not yet
  • Loading branch information
dskloetd authored Jan 28, 2025
1 parent 7ef1819 commit 0b8be87
Show file tree
Hide file tree
Showing 2 changed files with 214 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,48 @@
import TokenTitleCell from "$lib/components/tokens/TokensTable/TokenTitleCell.svelte";
import ResponsiveTable from "$lib/components/ui/ResponsiveTable.svelte";
import { i18n } from "$lib/stores/i18n";
import { importedTokensStore } from "$lib/stores/imported-tokens.store";
import type { TokensTableColumn, UserToken } from "$lib/types/tokens-page";
import type { TokensTableOrder } from "$lib/types/tokens-page";
import {
compareTokensAlphabetically,
compareTokensByBalance,
} from "$lib/utils/tokens-table.utils";
export let userTokensData: Array<UserToken>;
export let firstColumnHeader: string;
export let order: TokensTableOrder = [];
const columns: TokensTableColumn[] = [
let enableSorting: boolean;
$: enableSorting = order.length > 0;
let importedTokenIds: Set<string> = new Set();
$: importedTokenIds = new Set(
($importedTokensStore.importedTokens ?? []).map(({ ledgerCanisterId }) =>
ledgerCanisterId.toText()
)
);
let columns: TokensTableColumn[];
$: columns = [
{
id: "title",
title: firstColumnHeader,
cellComponent: TokenTitleCell,
alignment: "left",
templateColumns: ["1fr"],
comparator: enableSorting ? compareTokensAlphabetically : undefined,
},
{
id: "balance",
title: $i18n.tokens.balance_header,
cellComponent: TokenBalanceCell,
alignment: "right",
templateColumns: ["max-content"],
comparator: enableSorting
? compareTokensByBalance({ importedTokenIds })
: undefined,
},
{
title: "",
Expand All @@ -35,6 +60,7 @@
testId="tokens-table-component"
tableData={userTokensData}
{columns}
bind:order
on:nnsAction
>
<slot name="last-row" slot="last-row" />
Expand Down
189 changes: 187 additions & 2 deletions frontend/src/tests/lib/components/tokens/TokensTable.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { AppPath } from "$lib/constants/routes.constants";
import { overrideFeatureFlagsStore } from "$lib/stores/feature-flags.store";
import { importedTokensStore } from "$lib/stores/imported-tokens.store";
import { ActionType } from "$lib/types/actions";
import type { TokensTableOrder } from "$lib/types/tokens-page";
import {
UserTokenAction,
type UserTokenData,
Expand All @@ -20,27 +21,47 @@ import { TokensTablePo } from "$tests/page-objects/TokensTable.page-object";
import { JestPageObjectElement } from "$tests/page-objects/jest.page-object";
import { createActionEvent } from "$tests/utils/actions.test-utils";
import { render } from "$tests/utils/svelte.test-utils";
import { runResolvedPromises } from "$tests/utils/timers.test-utils";
import { ICPToken, TokenAmount } from "@dfinity/utils";
import { waitFor } from "@testing-library/svelte";
import { get, writable, type Writable } from "svelte/store";
import type { Mock } from "vitest";

describe("TokensTable", () => {
const renderTable = ({
userTokensData,
firstColumnHeader,
onAction,
orderStore,
order,
}: {
userTokensData: Array<UserTokenData | UserTokenLoading>;
firstColumnHeader?: string;
onAction?: Mock;
orderStore?: Writable<TokensTableOrder>;
order?: TokensTableOrder;
}) => {
const { container } = render(TokensTable, {
props: { userTokensData, firstColumnHeader },
const { container, component } = render(TokensTable, {
props: {
userTokensData,
firstColumnHeader,
order: order ?? get(orderStore),
},
events: {
nnsAction: onAction,
},
});

if (orderStore) {
component.$$.update = () => {
orderStore.set(component.$$.ctx[component.$$.props["order"]]);
};
}

orderStore?.subscribe((order) => {
component.$set({ order });
});

return TokensTablePo.under(new JestPageObjectElement(container));
};

Expand Down Expand Up @@ -445,4 +466,168 @@ describe("TokensTable", () => {
expect(await row1Po.hasImportedTokenTag()).toBe(false);
expect(await row2Po.hasImportedTokenTag()).toBe(true);
});

describe("Sorting", () => {
const tokenIcp = createUserToken({
universeId: OWN_CANISTER_ID,
title: "Internet Computer",
});
const tokenA = createUserToken({
universeId: principal(0),
title: "A",
});
const tokenB = createUserToken({
universeId: principal(1),
title: "B",
});

const getProjectNames = async (po) =>
Promise.all((await po.getRows()).map((row) => row.getProjectName()));

it("should not allow sorting without order", async () => {
const po = renderTable({
userTokensData: [tokenIcp, tokenA],
});

expect(await po.getColumnHeaderWithArrow()).toBe(undefined);
});

it("should allow sorting when order is specified", async () => {
const po = renderTable({
userTokensData: [tokenIcp, tokenA],
order: [
{
columnId: "balance",
},
],
});

expect(await po.getColumnHeaderWithArrow()).toBe("Balance");
});

it("should change order based on order prop", async () => {
const tokensTableOrderStore: Writable<TokensTableOrder> = writable([
{
columnId: "balance",
},
]);
const po = renderTable({
userTokensData: [tokenIcp, tokenA],
orderStore: tokensTableOrderStore,
});

expect(await getProjectNames(po)).toEqual(["Internet Computer", "A"]);

tokensTableOrderStore.set([
{
columnId: "title",
},
]);
await runResolvedPromises();

expect(await getProjectNames(po)).toEqual(["A", "Internet Computer"]);
});

it("should change order store based on clicked header", async () => {
const tokensTableOrderStore: Writable<TokensTableOrder> = writable([
{
columnId: "balance",
},
{
columnId: "title",
},
]);
const firstColumnHeader = "Projects";
const po = renderTable({
firstColumnHeader,
userTokensData: [tokenIcp, tokenA],
orderStore: tokensTableOrderStore,
});

expect(get(tokensTableOrderStore)).toEqual([
{
columnId: "balance",
},
{
columnId: "title",
},
]);

await po.clickColumnHeader(firstColumnHeader);

expect(get(tokensTableOrderStore)).toEqual([
{
columnId: "title",
},
{
columnId: "balance",
},
]);

await po.clickColumnHeader("Balance");

expect(get(tokensTableOrderStore)).toEqual([
{
columnId: "balance",
},
{
columnId: "title",
},
]);

await po.clickColumnHeader("Balance");

expect(get(tokensTableOrderStore)).toEqual([
{
columnId: "balance",
reversed: true,
},
{
columnId: "title",
},
]);
});

it("should order imported tokens without balance before other tokens without balance", async () => {
const po = renderTable({
userTokensData: [tokenIcp, tokenA, tokenB],
order: [
{
columnId: "balance",
},
{
columnId: "title",
},
],
});

// If B is not an imported token, it comes after A.
expect(await getProjectNames(po)).toEqual([
"Internet Computer",
"A",
"B",
]);

// Make B an imported token.
importedTokensStore.set({
importedTokens: [
{
ledgerCanisterId: tokenB.universeId,
indexCanisterId: undefined,
},
],
certified: true,
});

await runResolvedPromises();
await new Promise((resolve) => setTimeout(resolve, 100));

// If B is an imported token, it comes before A.
expect(await getProjectNames(po)).toEqual([
"Internet Computer",
"B",
"A",
]);
});
});
});

0 comments on commit 0b8be87

Please sign in to comment.