Skip to content

Commit

Permalink
Actionable universe navigation (#4905)
Browse files Browse the repository at this point in the history
# Motivation

The `Actionable Proposals` page should be shown when the user clicks
`Voting` in the main navigation or when the `Actionable Proposals` card
in the universe navigation is clicked. With this PR, we add the card to
the universe navigation and update the main navigation link to navigate
to “Actionable Proposals” when the user is logged-in.
There should be no actionable proposals visible for logged-out users.

# Changes

- Update main menu proposals link to navigate to "Actionable proposals".
- Add empty component for "Actionable proposals" page.
- Add "Actionable proposals" card to `SelectUniverseList` (Desktop. The
card is always visible with the separator).
- Add "Actionable proposals" card to `SelectUniverseDropdown` (Mobile.
The card is shown only when selected, w/o the separator).

# Tests

- Add `testId` param to Separator component.
- Add `ENABLE_ACTIONABLE_TAB` to `vites.setup` to rewrite it in the unit
tests w/o a error.
- Update mock page store to support boolean parameters values (for
`actionable`).
- Add test id to the `Separator` component.
- Add PO for `ActionableProposals` empty page.
- Tested manually that the actionable page is reachable.
- Should display "Actionable proposals" card in 

# Todos

- [ ] Add entry to changelog (if necessary).
Not yet

# Screenshot

| Mobile | Desktop |
|--------|--------|
| <img width="269" alt="image"
src="https://github.com/dfinity/nns-dapp/assets/98811342/fd98fbcb-3d53-4da5-98c5-bebfb32c492d">
| <img width="616" alt="image"
src="https://github.com/dfinity/nns-dapp/assets/98811342/3f3b9444-66db-42b7-bebd-c3e163ac3e89">
|
  • Loading branch information
mstrasinskis authored May 24, 2024
1 parent ec81b0a commit a415872
Show file tree
Hide file tree
Showing 20 changed files with 308 additions and 25 deletions.
2 changes: 1 addition & 1 deletion frontend/__mocks__/$app/stores.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const initPageStoreMock = () => {
data = { universe: OWN_CANISTER_ID_TEXT },
}: {
routeId?: string;
data?: { universe: string | null } & Record<string, string>;
data?: { universe: string | null } & Record<string, string | boolean>;
}) =>
set({
data,
Expand Down
14 changes: 12 additions & 2 deletions frontend/src/lib/components/common/MenuItems.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,14 @@
proposalsPathStore,
} from "$lib/derived/paths.derived";
import { pageStore } from "$lib/derived/page.derived";
import { isSelectedPath } from "$lib/utils/navigation.utils";
import {
ACTIONABLE_PROPOSALS_URL,
isSelectedPath,
} from "$lib/utils/navigation.utils";
import MenuMetrics from "$lib/components/common/MenuMetrics.svelte";
import ActionableProposalTotalCountBadge from "$lib/components/proposals/ActionableProposalTotalCountBadge.svelte";
import { ENABLE_ACTIONABLE_TAB } from "$lib/stores/feature-flags.store";
import { authSignedInStore } from "$lib/derived/auth.derived";
let routes: {
context: string;
Expand Down Expand Up @@ -59,7 +64,12 @@
},
{
context: "proposals",
href: $proposalsPathStore,
href:
// Switch to the actionable proposals page only when users are signed in.
// When users are signed out, we preserve the universe in the URL.
$ENABLE_ACTIONABLE_TAB && $authSignedInStore
? ACTIONABLE_PROPOSALS_URL
: $proposalsPathStore,
selected: isSelectedPath({
currentPath: $pageStore.path,
paths: [AppPath.Proposals, AppPath.Proposal],
Expand Down
7 changes: 6 additions & 1 deletion frontend/src/lib/components/ui/Separator.svelte
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
<script lang="ts">
export let spacing: "medium" | "large" | "none" = "large";
export let testId: string | undefined = undefined;
</script>

<hr class:medium={spacing === "medium"} class:no-margin={spacing === "none"} />
<hr
data-tid={testId}
class:medium={spacing === "medium"}
class:no-margin={spacing === "none"}
/>

<style lang="scss">
hr {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,9 @@
>
<span class="name">
{#if universe === "all-actionable"}
{$i18n.voting.actionable_proposals}
<TestIdWrapper testId="universe-name"
>{$i18n.voting.actionable_proposals}</TestIdWrapper
>
{#if $actionableProposalIndicationEnabledStore}
{#if $actionableProposalTotalCountStore > 0 && mounted}
<div
Expand All @@ -95,7 +97,7 @@
{/if}
{/if}
{:else}
{universe.title}
<TestIdWrapper testId="universe-name">{universe.title}</TestIdWrapper>
{#if $actionableProposalIndicationEnabledStore}
{#if nonNullish(actionableProposalCount) && actionableProposalCount > 0 && mounted}
<ActionableProposalCountBadge
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
import SelectUniverseCard from "$lib/components/universe/SelectUniverseCard.svelte";
import TestIdWrapper from "$lib/components/common/TestIdWrapper.svelte";
import SelectUniverseModal from "$lib/modals/universe/SelectUniverseModal.svelte";
import { pageStore } from "$lib/derived/page.derived";
import { ENABLE_ACTIONABLE_TAB } from "$lib/stores/feature-flags.store";
import { authSignedInStore } from "$lib/derived/auth.derived";
import { AppPath } from "$lib/constants/routes.constants";
let showProjectPicker = false;
Expand All @@ -17,13 +21,20 @@
};
$: onWindowSizeChange(innerWidth);
let isActionableSelected = false;
$: isActionableSelected =
$ENABLE_ACTIONABLE_TAB &&
$authSignedInStore &&
$pageStore.path === AppPath.Proposals &&
$pageStore.actionable;
</script>

<svelte:window bind:innerWidth />

<TestIdWrapper testId="select-universe-dropdown-component">
<SelectUniverseCard
universe={$selectedUniverseStore}
universe={isActionableSelected ? "all-actionable" : $selectedUniverseStore}
selected={true}
role="dropdown"
on:click={() => (showProjectPicker = true)}
Expand Down
24 changes: 20 additions & 4 deletions frontend/src/lib/components/universe/SelectUniverseList.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,37 @@
import SelectUniverseCard from "$lib/components/universe/SelectUniverseCard.svelte";
import { createEventDispatcher } from "svelte";
import { selectedUniverseIdStore } from "$lib/derived/selected-universe.derived";
import { ENABLE_ACTIONABLE_TAB } from "$lib/stores/feature-flags.store";
import { pageStore } from "$lib/derived/page.derived";
import Separator from "$lib/components/ui/Separator.svelte";
import { authSignedInStore } from "$lib/derived/auth.derived";
import { AppPath } from "$lib/constants/routes.constants";
export let role: "link" | "button" = "link";
let selectedCanisterId: string;
$: selectedCanisterId = $selectedUniverseIdStore.toText();
const dispatch = createEventDispatcher();
$: selectedUniverse =
$ENABLE_ACTIONABLE_TAB && $authSignedInStore && $pageStore.actionable
? "all-actionable"
: $selectedUniverseIdStore.toText();
</script>

<TestIdWrapper testId="select-universe-list-component">
{#if $ENABLE_ACTIONABLE_TAB && $authSignedInStore && $pageStore.path === AppPath.Proposals}
<SelectUniverseCard
on:click={() => dispatch("nnsSelectActionable")}
selected={"all-actionable" === selectedUniverse}
universe="all-actionable"
/>
<Separator spacing="medium" testId="all-actionable-separator" />
{/if}

{#each $selectableUniversesStore as universe (universe.canisterId)}
<SelectUniverseCard
{universe}
{role}
selected={universe.canisterId === selectedCanisterId}
selected={universe.canisterId === selectedUniverse}
on:click={() => dispatch("nnsSelectUniverse", universe.canisterId)}
/>
{/each}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
<script lang="ts">
import SelectUniverseList from "$lib/components/universe/SelectUniverseList.svelte";
import { goto } from "$app/navigation";
import { buildSwitchUniverseUrl } from "$lib/utils/navigation.utils";
import {
ACTIONABLE_PROPOSALS_URL,
buildSwitchUniverseUrl,
} from "$lib/utils/navigation.utils";
</script>

<SelectUniverseList
on:nnsSelectUniverse={async ({ detail }) =>
await goto(buildSwitchUniverseUrl(detail))}
on:nnsSelectActionable={async () => await goto(ACTIONABLE_PROPOSALS_URL)}
/>
11 changes: 10 additions & 1 deletion frontend/src/lib/modals/universe/SelectUniverseModal.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
import SelectUniverseList from "$lib/components/universe/SelectUniverseList.svelte";
import { createEventDispatcher } from "svelte";
import { goto } from "$app/navigation";
import { buildSwitchUniverseUrl } from "$lib/utils/navigation.utils";
import {
ACTIONABLE_PROPOSALS_URL,
buildSwitchUniverseUrl,
} from "$lib/utils/navigation.utils";
import { titleTokenSelectorStore } from "$lib/derived/title-token-selector.derived";
const dispatcher = createEventDispatcher();
Expand All @@ -13,6 +16,11 @@
await goto(buildSwitchUniverseUrl(canisterId));
close();
};
const selectActionable = async () => {
await goto(ACTIONABLE_PROPOSALS_URL);
close();
};
</script>

<div class="container">
Expand All @@ -24,6 +32,7 @@
<SelectUniverseList
role="button"
on:nnsSelectUniverse={({ detail }) => select(detail)}
on:nnsSelectActionable={() => selectActionable()}
/>
</Modal>
</div>
Expand Down
7 changes: 7 additions & 0 deletions frontend/src/lib/pages/ActionableProposals.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<script lang="ts">
import TestIdWrapper from "$lib/components/common/TestIdWrapper.svelte";
</script>

<TestIdWrapper testId="actionable-proposals-component">
<h1>Under construction</h1>
</TestIdWrapper>
18 changes: 13 additions & 5 deletions frontend/src/lib/routes/Proposals.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,22 @@
import SummaryUniverse from "$lib/components/summary/SummaryUniverse.svelte";
import { snsProjectSelectedStore } from "$lib/derived/sns/sns-selected-project.derived";
import { nonNullish } from "@dfinity/utils";
import { pageStore } from "$lib/derived/page.derived";
import { authSignedInStore } from "$lib/derived/auth.derived";
import { ENABLE_ACTIONABLE_TAB } from "$lib/stores/feature-flags.store";
import ActionableProposals from "$lib/pages/ActionableProposals.svelte";
</script>

<main data-tid="proposals-component">
<SummaryUniverse />
{#if $ENABLE_ACTIONABLE_TAB && $authSignedInStore && $pageStore.actionable}
<ActionableProposals />
{:else}
<SummaryUniverse />

{#if $isNnsUniverseStore}
<Proposals />
{:else if nonNullish($snsProjectSelectedStore)}
<SnsProposals />
{#if $isNnsUniverseStore}
<Proposals />
{:else if nonNullish($snsProjectSelectedStore)}
<SnsProposals />
{/if}
{/if}
</main>
54 changes: 53 additions & 1 deletion frontend/src/tests/lib/components/common/MenuItems.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { actionableNnsProposalsStore } from "$lib/stores/actionable-nns-proposal
import { actionableSnsProposalsStore } from "$lib/stores/actionable-sns-proposals.store";
import { overrideFeatureFlagsStore } from "$lib/stores/feature-flags.store";
import { page } from "$mocks/$app/stores";
import { resetIdentity } from "$tests/mocks/auth.store.mock";
import { resetIdentity, setNoIdentity } from "$tests/mocks/auth.store.mock";
import en from "$tests/mocks/i18n.mock";
import { mockProposalInfo } from "$tests/mocks/proposal.mock";
import { principal } from "$tests/mocks/sns-projects.mock";
Expand Down Expand Up @@ -36,15 +36,20 @@ describe("MenuItems", () => {
const shouldRenderMenuItem = ({
context,
labelKey,
href,
}: {
context: string;
labelKey: string;
href?: string;
}) => {
const { getByTestId } = render(MenuItems);
const link = getByTestId(`menuitem-${context}`) as HTMLElement;
expect(link).not.toBeNull();
expect(link).toBeVisible();
expect(link.textContent?.trim()).toEqual(en.navigation[labelKey]);
if (href) {
expect(link.getAttribute("href")).toEqual(href);
}
};

beforeEach(() => {
Expand Down Expand Up @@ -89,6 +94,53 @@ describe("MenuItems", () => {
});
});

describe("actionable proposal link", () => {
it("should have actionable proposal link", async () => {
resetIdentity();
overrideFeatureFlagsStore.setFlag("ENABLE_ACTIONABLE_TAB", true);
page.mock({
data: { universe: OWN_CANISTER_ID_TEXT },
routeId: AppPath.Proposals,
});

shouldRenderMenuItem({
context: "proposals",
labelKey: "voting",
href: "/proposals/?actionable",
});
});

it("should have default proposal link when signedOut", async () => {
overrideFeatureFlagsStore.setFlag("ENABLE_ACTIONABLE_TAB", true);
setNoIdentity();
page.mock({
data: { universe: OWN_CANISTER_ID_TEXT },
routeId: AppPath.Proposals,
});

shouldRenderMenuItem({
context: "proposals",
labelKey: "voting",
href: "/proposals/?u=qhbym-qaaaa-aaaaa-aaafq-cai",
});
});

it("should have default proposal link when no feature flag set", async () => {
overrideFeatureFlagsStore.setFlag("ENABLE_ACTIONABLE_TAB", false);
resetIdentity();
page.mock({
data: { universe: OWN_CANISTER_ID_TEXT },
routeId: AppPath.Proposals,
});

shouldRenderMenuItem({
context: "proposals",
labelKey: "voting",
href: "/proposals/?u=qhbym-qaaaa-aaaaa-aaafq-cai",
});
});
});

describe("actionable proposal count badge", () => {
const nnsProposals: ProposalInfo[] = [
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import SelectUniverseDropdown from "$lib/components/universe/SelectUniverseDropdown.svelte";
import { OWN_CANISTER_ID_TEXT } from "$lib/constants/canister-ids.constants";
import { AppPath } from "$lib/constants/routes.constants";
import { snsProjectsCommittedStore } from "$lib/derived/sns/sns-projects.derived";
import { snsProjectSelectedStore } from "$lib/derived/sns/sns-selected-project.derived";
import { overrideFeatureFlagsStore } from "$lib/stores/feature-flags.store";
import { icrcAccountsStore } from "$lib/stores/icrc-accounts.store";
import { page } from "$mocks/$app/stores";
import { resetIdentity, setNoIdentity } from "$tests/mocks/auth.store.mock";
Expand Down Expand Up @@ -34,6 +36,7 @@ describe("SelectUniverseDropdown", () => {
icrcAccountsStore.reset();
resetSnsProjects();
resetIdentity();
overrideFeatureFlagsStore.reset();

page.mock({
data: { universe: mockSnsFullProject.rootCanisterId.toText() },
Expand Down Expand Up @@ -78,6 +81,25 @@ describe("SelectUniverseDropdown", () => {
});
});

describe('"all actionable" card', () => {
beforeEach(() => {
page.mock({
data: { universe: OWN_CANISTER_ID_TEXT, actionable: true },
routeId: AppPath.Proposals,
});
});

it('should render "Actionable proposals" card', async () => {
overrideFeatureFlagsStore.setFlag("ENABLE_ACTIONABLE_TAB", true);
resetIdentity();
const po = renderComponent();
expect(await po.getSelectUniverseCardPo().getName()).toEqual(
"Actionable Proposals"
);
expect(await po.getSelectUniverseCardPo().isSelected()).toEqual(true);
});
});

describe("balance", () => {
beforeEach(() => {
const accounts = [
Expand Down
Loading

0 comments on commit a415872

Please sign in to comment.