Skip to content

Commit 48451b7

Browse files
authored
sns topics: Topic item component (#6624)
# Motivation Migrating from following by type to following by topic to improve neuron management. In the new “Follow” modal, all topics will be displayed with the ability to select them for following in the next step. This PR adds the component that will be used in the topic list. The full component structure can be found in the draft pr - #6610. Currently **DEMO NOT WORKING** because of the missing aggregator update. [Demo](https://qsgjb-riaaa-aaaaa-aaaga-cai.mstr-ingress.devenv.dfinity.network/neuron/?u=7tjcv-pp777-77776-qaaaa-cai&neuron=a12652a79755ee2158248250b9038a35a87adbd53674d789b318bf024e1d2989) 1. `__featureFlags.ENABLE_SNS_TOPICS.overrideWith(true)` 2. Click `Follow Neurons` button on the sns neuron detail page. https://github.com/user-attachments/assets/1de40535-f9d2-43c5-b87b-07c5add8ef5c # Changes - New FollowSnsNeuronsByTopicItem component # Tests - Added. # Todos - [ ] Add entry to changelog (if necessary). Not necessary.
1 parent 6f4b0cd commit 48451b7

File tree

3 files changed

+251
-0
lines changed

3 files changed

+251
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
<script lang="ts">
2+
import {
3+
Checkbox,
4+
Collapsible,
5+
IconErrorOutline,
6+
IconExpandMore,
7+
} from "@dfinity/gix-components";
8+
import type { TopicInfoWithUnknown } from "$lib/types/sns-aggregator";
9+
import { fromDefinedNullable } from "@dfinity/utils";
10+
import type { SnsTopicKey } from "$lib/types/sns";
11+
import { getSnsTopicInfoKey } from "$lib/utils/sns-topics.utils";
12+
13+
export let topicInfo: TopicInfoWithUnknown;
14+
export let checked: boolean = false;
15+
export let onNnsChange: (args: {
16+
topicKey: SnsTopicKey;
17+
checked: boolean;
18+
}) => void;
19+
20+
let topicKey: SnsTopicKey;
21+
$: topicKey = getSnsTopicInfoKey(topicInfo);
22+
let name: string;
23+
$: name = fromDefinedNullable(topicInfo.name);
24+
let description: string;
25+
$: description = fromDefinedNullable(topicInfo.description);
26+
27+
const onChange = () => {
28+
// Checkbox doesn't support two-way binding
29+
checked = !checked;
30+
onNnsChange({ topicKey, checked });
31+
};
32+
33+
let toggleContent: () => void;
34+
let expanded: boolean;
35+
36+
// TODO(sns-topics): Add "stopPropagation" prop to the gix/Checkbox component
37+
// to avoid collapsable toggling
38+
</script>
39+
40+
<div class="topic-item" data-tid="follow-sns-neurons-by-topic-item-component">
41+
<Collapsible
42+
testId="topic-collapsible"
43+
expandButton={false}
44+
externalToggle={true}
45+
bind:toggleContent
46+
bind:expanded
47+
wrapHeight
48+
>
49+
<div slot="header" class="header" class:expanded>
50+
<Checkbox
51+
inputId={topicKey}
52+
text="block"
53+
{checked}
54+
on:nnsChange={onChange}
55+
preventDefault
56+
--checkbox-label-order="1"
57+
--checkbox-padding="var(--padding) 0"
58+
>
59+
<span data-tid="topic-name">{name}</span>
60+
</Checkbox>
61+
62+
<!-- TODO: display following status -->
63+
<div class="icon" data-tid="topic-following-status">
64+
<IconErrorOutline />
65+
</div>
66+
67+
<button
68+
data-tid="expand-button"
69+
class="expand-button"
70+
class:expanded
71+
on:click={toggleContent}
72+
>
73+
<IconExpandMore />
74+
</button>
75+
</div>
76+
<div class="expandable-content">
77+
<p class="description" data-tid="topic-description">
78+
{description}
79+
</p>
80+
</div>
81+
</Collapsible>
82+
</div>
83+
84+
<style lang="scss">
85+
.header {
86+
display: grid;
87+
grid-template-columns: auto min-content min-content;
88+
gap: var(--padding);
89+
align-items: center;
90+
91+
// stretching to the full Collapsible header width
92+
flex: 1 1 100%;
93+
}
94+
95+
.expand-button {
96+
padding: 0;
97+
display: flex;
98+
align-items: center;
99+
justify-content: center;
100+
color: var(--primary);
101+
102+
transition: transform ease-out var(--animation-time-normal);
103+
&.expanded {
104+
transform: rotate(-180deg);
105+
}
106+
}
107+
108+
.expandable-content {
109+
// Aligning with the checkbox label
110+
margin-left: calc(20px + var(--padding));
111+
112+
.description {
113+
margin: 0 0 var(--padding-3x);
114+
}
115+
}
116+
</style>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import FollowSnsNeuronsByTopicItem from "$lib/modals/sns/neurons/FollowSnsNeuronsByTopicItem.svelte";
2+
import type { TopicInfoWithUnknown } from "$lib/types/sns-aggregator";
3+
import { FollowSnsNeuronsByTopicItemPo } from "$tests/page-objects/FollowSnsNeuronsByTopicItem.page-object";
4+
import { JestPageObjectElement } from "$tests/page-objects/jest.page-object";
5+
import { render } from "$tests/utils/svelte.test-utils";
6+
import type { SnsNervousSystemFunction } from "@dfinity/sns";
7+
8+
describe("FollowSnsNeuronsByTopicItem", () => {
9+
const nativeNsFunction: SnsNervousSystemFunction = {
10+
id: 1n,
11+
name: "Native Function",
12+
description: ["Description 1"],
13+
function_type: [{ NativeNervousSystemFunction: {} }],
14+
};
15+
const topicKey = "DaoCommunitySettings";
16+
const topicInfo: TopicInfoWithUnknown = {
17+
native_functions: [[nativeNsFunction]],
18+
topic: [
19+
{
20+
[topicKey]: null,
21+
},
22+
],
23+
is_critical: [true],
24+
name: ["Known topic name"],
25+
description: ["Known topic description"],
26+
custom_functions: [[]],
27+
};
28+
29+
const renderComponent = (props: {
30+
topicInfo: TopicInfoWithUnknown;
31+
checked: boolean;
32+
onNnsChange: () => void;
33+
}) => {
34+
const { container } = render(FollowSnsNeuronsByTopicItem, {
35+
props,
36+
});
37+
38+
return FollowSnsNeuronsByTopicItemPo.under(
39+
new JestPageObjectElement(container)
40+
);
41+
};
42+
const defaultProps = {
43+
topicInfo,
44+
checked: false,
45+
onNnsChange: vi.fn(),
46+
};
47+
48+
it("should expand and collapse", async () => {
49+
const po = renderComponent({
50+
...defaultProps,
51+
});
52+
53+
expect(await po.getCollapsiblePo().isExpanded()).toBe(false);
54+
await po.clickExpandButton();
55+
expect(await po.getCollapsiblePo().isExpanded()).toBe(true);
56+
await po.clickExpandButton();
57+
expect(await po.getCollapsiblePo().isExpanded()).toBe(false);
58+
});
59+
60+
it("should dispatch on nnsChange on check", async () => {
61+
const onNnsChange = vi.fn();
62+
const po = renderComponent({
63+
...defaultProps,
64+
onNnsChange,
65+
});
66+
67+
expect(await po.getCheckboxPo().isChecked()).toBe(false);
68+
69+
await po.getCheckboxPo().click();
70+
expect(await po.getCheckboxPo().isChecked()).toBe(true);
71+
expect(onNnsChange).toBeCalledTimes(1);
72+
expect(onNnsChange).toBeCalledWith({
73+
checked: true,
74+
topicKey,
75+
});
76+
77+
await po.getCheckboxPo().click();
78+
expect(await po.getCheckboxPo().isChecked()).toBe(false);
79+
expect(onNnsChange).toBeCalledTimes(2);
80+
expect(onNnsChange).toBeCalledWith({ checked: false, topicKey });
81+
});
82+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { ButtonPo } from "$tests/page-objects/Button.page-object";
2+
import { CheckboxPo } from "$tests/page-objects/Checkbox.page-object";
3+
import { CollapsiblePo } from "$tests/page-objects/Collapsible.page-object";
4+
import { BasePageObject } from "$tests/page-objects/base.page-object";
5+
import type { PageObjectElement } from "$tests/types/page-object.types";
6+
7+
export class FollowSnsNeuronsByTopicItemPo extends BasePageObject {
8+
private static readonly TID = "follow-sns-neurons-by-topic-item-component";
9+
10+
static under(element: PageObjectElement): FollowSnsNeuronsByTopicItemPo {
11+
return new FollowSnsNeuronsByTopicItemPo(
12+
element.byTestId(FollowSnsNeuronsByTopicItemPo.TID)
13+
);
14+
}
15+
16+
static async allUnder(
17+
element: PageObjectElement
18+
): Promise<FollowSnsNeuronsByTopicItemPo[]> {
19+
return (await element.allByTestId(FollowSnsNeuronsByTopicItemPo.TID)).map(
20+
(element) => FollowSnsNeuronsByTopicItemPo.under(element)
21+
);
22+
}
23+
24+
getCollapsiblePo(): CollapsiblePo {
25+
return CollapsiblePo.under({
26+
element: this.root,
27+
testId: "topic-collapsible",
28+
});
29+
}
30+
31+
getExpandButtonPo(): ButtonPo {
32+
return ButtonPo.under({
33+
element: this.root,
34+
testId: "expand-button",
35+
});
36+
}
37+
38+
clickExpandButton(): Promise<void> {
39+
return this.getExpandButtonPo().click();
40+
}
41+
42+
getCheckboxPo(): CheckboxPo {
43+
return CheckboxPo.under({ element: this.root });
44+
}
45+
46+
getTopicName(): Promise<string> {
47+
return this.root.byTestId("topic-name").getText();
48+
}
49+
50+
getTopicDescription(): Promise<string> {
51+
return this.root.byTestId("topic-description").getText();
52+
}
53+
}

0 commit comments

Comments
 (0)