@kiva/kv-components 3.52.1 → 3.54.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,36 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # [3.54.0](https://github.com/kiva/kv-ui-elements/compare/@kiva/kv-components@3.53.0...@kiva/kv-components@3.54.0) (2024-02-28)
7
+
8
+
9
+ ### Features
10
+
11
+ * kvcommentsheartbutton added for comments stuff ([#346](https://github.com/kiva/kv-ui-elements/issues/346)) ([57303da](https://github.com/kiva/kv-ui-elements/commit/57303daec80222c199556b296d720462265e53ac))
12
+
13
+
14
+
15
+
16
+
17
+ # [3.53.0](https://github.com/kiva/kv-ui-elements/compare/@kiva/kv-components@3.52.1...@kiva/kv-components@3.53.0) (2024-02-26)
18
+
19
+
20
+ ### Bug Fixes
21
+
22
+ * indentation in item component ([30aee02](https://github.com/kiva/kv-ui-elements/commit/30aee02d3c2e58c7c98fe6a58689a9824f56ef60))
23
+ * optional chaining for comment object ([bac9ba1](https://github.com/kiva/kv-ui-elements/commit/bac9ba199970966564bfab7d6a2e64bb92fea286))
24
+ * revert profile image and small fixes ([fc8cf74](https://github.com/kiva/kv-ui-elements/commit/fc8cf7440fb355fbfee0e8acfb2a849dd6513a11))
25
+ * using more accurate data for components ([043f1c2](https://github.com/kiva/kv-ui-elements/commit/043f1c2d47a5d22157785b9d972246823572e4fd))
26
+
27
+
28
+ ### Features
29
+
30
+ * provisional comment list and item components ([642745c](https://github.com/kiva/kv-ui-elements/commit/642745ce1359e5cc9e3d98d122f26728b3dffda0))
31
+
32
+
33
+
34
+
35
+
6
36
  ## [3.52.1](https://github.com/kiva/kv-ui-elements/compare/@kiva/kv-components@3.52.0...@kiva/kv-components@3.52.1) (2024-02-01)
7
37
 
8
38
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kiva/kv-components",
3
- "version": "3.52.1",
3
+ "version": "3.54.0",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -75,5 +75,5 @@
75
75
  "optional": true
76
76
  }
77
77
  },
78
- "gitHead": "a1d537c8b354bbba06204c6500440183420eebcb"
78
+ "gitHead": "b29d4c7e40f229ac09c7f68c377741af7d068158"
79
79
  }
@@ -0,0 +1,80 @@
1
+ const comments = {
2
+ duration: '6.42ms',
3
+ results: [
4
+ {
5
+ actor: {
6
+ created_at: '2024-02-01T19:48:26.890734Z',
7
+ updated_at: '2024-02-01T19:48:26.890734Z',
8
+ id: '123',
9
+ data: {},
10
+ },
11
+ foreign_id: 'CHALLENGE#test-challenge-id',
12
+ id: 'efb2dbfe-c12a-11ee-9fbc-065afce7d41d',
13
+ latest_reactions: {
14
+ comment: [
15
+ {
16
+ created_at: '2024-02-01T20:12:31.398932Z',
17
+ updated_at: '2024-02-01T20:12:31.398932Z',
18
+ id: 'dade3812-6aa0-4c32-90ea-2366dab178a1',
19
+ user_id: '1de650a5-b12f-4d20-b46c-6e15b0722141',
20
+ kind: 'comment',
21
+ activity_id: 'efb2dbfe-c12a-11ee-9fbc-065afce7d41d',
22
+ data: {
23
+ text: 'another guest comment',
24
+ },
25
+ parent: '',
26
+ latest_children: {},
27
+ children_counts: {},
28
+ },
29
+ {
30
+ created_at: '2024-02-01T20:11:23.931133Z',
31
+ updated_at: '2024-02-01T20:11:23.931133Z',
32
+ id: '3de615bb-0d48-4a69-9004-141ee26985eb',
33
+ user_id: '1de650a5-b12f-4d20-b46c-6e15b0722141',
34
+ kind: 'comment',
35
+ activity_id: 'efb2dbfe-c12a-11ee-9fbc-065afce7d41d',
36
+ data: {
37
+ text: 'test as visitor',
38
+ },
39
+ parent: '',
40
+ latest_children: {},
41
+ children_counts: {},
42
+ is_liked: true,
43
+ },
44
+ {
45
+ created_at: '2024-02-01T20:06:46.651764Z',
46
+ updated_at: '2024-02-01T20:06:46.651764Z',
47
+ id: 'e1db4420-159e-4ba2-aab9-704d1cc56dae',
48
+ user_id: '123',
49
+ user: {
50
+ created_at: '2024-02-01T19:48:26.890734Z',
51
+ updated_at: '2024-02-01T19:48:26.890734Z',
52
+ id: '123',
53
+ data: {},
54
+ },
55
+ kind: 'comment',
56
+ activity_id: 'efb2dbfe-c12a-11ee-9fbc-065afce7d41d',
57
+ data: {
58
+ text: 'asd',
59
+ },
60
+ parent: '',
61
+ latest_children: {},
62
+ children_counts: {},
63
+ },
64
+ ],
65
+ },
66
+ latest_reactions_extra: {
67
+ comment: {
68
+ next: '',
69
+ },
70
+ },
71
+ object: 'test-challenge-id',
72
+ origin: null,
73
+ target: '',
74
+ time: '2024-02-01T17:54:24.932762',
75
+ verb: 'CHALLENGE',
76
+ },
77
+ ],
78
+ };
79
+
80
+ export default comments;
@@ -0,0 +1,32 @@
1
+ import { render } from '@testing-library/vue';
2
+ import userEvent from '@testing-library/user-event';
3
+ import KvCommentsHeartButton from '../../../../vue/KvCommentsHeartButton.vue';
4
+
5
+ const CLICK_EVENT = 'click';
6
+
7
+ describe('KvCommentsListItem', () => {
8
+ it('should render defaults', () => {
9
+ const { getByRole } = render(KvCommentsHeartButton);
10
+ const likeButton = getByRole('button', { name: 'Like' });
11
+
12
+ expect(likeButton).toBeDefined();
13
+ });
14
+
15
+ it('should emit true value when clicked as not liked', async () => {
16
+ const { getByRole, emitted } = render(KvCommentsHeartButton);
17
+ const likeButton = getByRole('button', { name: 'Like' });
18
+
19
+ await userEvent.click(likeButton);
20
+
21
+ expect(emitted()[CLICK_EVENT]).toEqual([[true]]);
22
+ });
23
+
24
+ it('should emit false value when clicked as liked', async () => {
25
+ const { getByRole, emitted } = render(KvCommentsHeartButton, { props: { isLiked: true } });
26
+ const likeButton = getByRole('button', { name: 'Like' });
27
+
28
+ await userEvent.click(likeButton);
29
+
30
+ expect(emitted()[CLICK_EVENT]).toEqual([[false]]);
31
+ });
32
+ });
@@ -0,0 +1,39 @@
1
+ import { render } from '@testing-library/vue';
2
+ import userEvent from '@testing-library/user-event';
3
+ import ListComponent from '../../../../vue/KvCommentsList.vue';
4
+ import activityFeed from '../../../fixtures/mockFeedActivityData';
5
+ import { LIKE_COMMENT_EVENT, REPLY_COMMENT_EVENT } from '../../../../vue/KvCommentsListItem.vue';
6
+
7
+ const renderList = (props = {}) => {
8
+ return render(ListComponent, { props });
9
+ };
10
+
11
+ const comments = activityFeed.results[0].latest_reactions;
12
+
13
+ describe('KvCommentsList', () => {
14
+ it('should render comments component', async () => {
15
+ const { container } = renderList({ comments });
16
+
17
+ const { id } = comments.comment[0];
18
+ expect(container.querySelectorAll(`#${id}`).length).toBe(1);
19
+ });
20
+
21
+ it('should emit comment events', async () => {
22
+ const { getAllByRole, emitted } = renderList({ comments });
23
+ const replyButton = getAllByRole('button', { name: 'Reply' })[0];
24
+ const likeButton = getAllByRole('button', { name: 'Like' })[0];
25
+ const firstComment = comments.comment[0];
26
+
27
+ const TEST_OBJ = {
28
+ userId: firstComment.user_id,
29
+ id: firstComment.id,
30
+ isChild: true,
31
+ };
32
+
33
+ await userEvent.click(replyButton);
34
+ expect(emitted()[REPLY_COMMENT_EVENT]).toEqual([[{ ...TEST_OBJ, reaction: REPLY_COMMENT_EVENT }]]);
35
+
36
+ await userEvent.click(likeButton);
37
+ expect(emitted()[LIKE_COMMENT_EVENT]).toEqual([[{ ...TEST_OBJ, reaction: LIKE_COMMENT_EVENT, value: true }]]);
38
+ });
39
+ });
@@ -0,0 +1,43 @@
1
+ import { render } from '@testing-library/vue';
2
+ import userEvent from '@testing-library/user-event';
3
+ import KvCommentsListItem from '../../../../vue/KvCommentsListItem.vue';
4
+ import activityFeed from '../../../fixtures/mockFeedActivityData';
5
+
6
+ const comment = activityFeed.results[0].latest_reactions.comment[0];
7
+
8
+ const handleClick = jest.fn();
9
+
10
+ const renderComment = (props = {}) => {
11
+ return render(KvCommentsListItem, { props });
12
+ };
13
+
14
+ describe('KvCommentsListItem', () => {
15
+ it('should render defaults', () => {
16
+ const { getByRole } = renderComment({ comment });
17
+ getByRole('button', { name: 'Reply' });
18
+ getByRole('button', { name: 'Like' });
19
+ });
20
+
21
+ it('should render comment text', () => {
22
+ const { getByText } = renderComment({ comment });
23
+ getByText(comment.data.text);
24
+ });
25
+
26
+ it('should handle like button click', async () => {
27
+ const { getByRole } = renderComment({ comment, handleClick });
28
+ const likeButton = getByRole('button', { name: 'Like' });
29
+
30
+ await userEvent.click(likeButton);
31
+
32
+ expect(handleClick).toHaveBeenCalledTimes(1);
33
+ });
34
+
35
+ it('should handle reply button click', async () => {
36
+ const { getByRole } = renderComment({ comment, handleClick });
37
+ const replyButton = getByRole('button', { name: 'Reply' });
38
+
39
+ await userEvent.click(replyButton);
40
+
41
+ expect(handleClick).toHaveBeenCalledTimes(1);
42
+ });
43
+ });
package/vue/KvButton.vue CHANGED
@@ -53,14 +53,14 @@ export default {
53
53
  props: {
54
54
  /**
55
55
  * Use if linking to a Vue route
56
- * */
56
+ */
57
57
  to: {
58
58
  type: [String, Object],
59
59
  default: null,
60
60
  },
61
61
  /**
62
62
  * Use if linking to an external link or old-stack page
63
- * */
63
+ */
64
64
  href: {
65
65
  type: String,
66
66
  default: null,
@@ -68,7 +68,7 @@ export default {
68
68
  /**
69
69
  * The behavior of the button when used in an HTML form.
70
70
  * `button (default), submit, reset`
71
- * */
71
+ */
72
72
  type: {
73
73
  type: String,
74
74
  default: 'button',
@@ -79,7 +79,7 @@ export default {
79
79
  /**
80
80
  * Appearance of the button
81
81
  * `primary (default), secondary, danger, link, ghost`
82
- * */
82
+ */
83
83
  variant: {
84
84
  type: String,
85
85
  default: 'primary',
@@ -90,7 +90,7 @@ export default {
90
90
  /**
91
91
  * State of the button
92
92
  * `'' (default), active, disabled, loading`
93
- * */
93
+ */
94
94
  state: {
95
95
  type: String,
96
96
  default: '',
@@ -5,17 +5,26 @@
5
5
  :user-display-name="userDisplayName"
6
6
  @add-comment="comment"
7
7
  />
8
+ <kv-comments-list
9
+ v-if="comments"
10
+ :comments="comments"
11
+ @[REPLY_COMMENT_EVENT]="comment"
12
+ @[LIKE_COMMENT_EVENT]="comment"
13
+ />
8
14
  </div>
9
15
  </template>
10
16
 
11
17
  <script>
12
18
  import KvCommentsAdd from './KvCommentsAdd.vue';
19
+ import KvCommentsList from './KvCommentsList.vue';
20
+ import { REPLY_COMMENT_EVENT, LIKE_COMMENT_EVENT } from './KvCommentsListItem.vue';
13
21
 
14
22
  export const ADD_COMMENT_EVENT = 'add-comment';
15
23
 
16
24
  export default {
17
25
  components: {
18
26
  KvCommentsAdd,
27
+ KvCommentsList,
19
28
  },
20
29
  props: {
21
30
  /**
@@ -32,13 +41,24 @@ export default {
32
41
  type: String,
33
42
  default: '',
34
43
  },
44
+ /**
45
+ * Activity comments
46
+ */
47
+ comments: {
48
+ type: Object,
49
+ default: () => {},
50
+ },
35
51
  },
36
52
  setup(_props, { emit }) {
37
53
  const comment = (commentValue) => {
38
54
  emit(ADD_COMMENT_EVENT, commentValue);
55
+ emit(REPLY_COMMENT_EVENT, commentValue);
56
+ emit(LIKE_COMMENT_EVENT, commentValue);
39
57
  };
40
58
  return {
41
59
  comment,
60
+ REPLY_COMMENT_EVENT,
61
+ LIKE_COMMENT_EVENT,
42
62
  };
43
63
  },
44
64
  };
@@ -0,0 +1,74 @@
1
+ <template>
2
+ <button
3
+ aria-label="Like"
4
+ @click="changeState"
5
+ >
6
+ <kv-material-icon
7
+ :icon="icon"
8
+ :class="classState"
9
+ />
10
+ </button>
11
+ </template>
12
+
13
+ <script>
14
+ import { computed, toRefs } from 'vue-demi';
15
+ import { mdiHeart, mdiHeartOutline } from '@mdi/js';
16
+ import KvMaterialIcon from './KvMaterialIcon.vue';
17
+
18
+ export default {
19
+ name: 'KvCommentsHeartButton',
20
+ components: {
21
+ KvMaterialIcon,
22
+ },
23
+ props: {
24
+ /**
25
+ * Use if small icon is needed
26
+ */
27
+ isSmall: {
28
+ type: Boolean,
29
+ default: false,
30
+ },
31
+ /**
32
+ * Use if icon is liked
33
+ */
34
+ isLiked: {
35
+ type: Boolean,
36
+ default: false,
37
+ },
38
+ },
39
+ emits: [
40
+ 'click',
41
+ ],
42
+ setup(props, { emit }) {
43
+ const {
44
+ isSmall,
45
+ isLiked,
46
+ } = toRefs(props);
47
+
48
+ const icon = computed(() => (isLiked.value ? mdiHeart : mdiHeartOutline));
49
+
50
+ const classState = computed(() => {
51
+ let className = isSmall.value ? 'tw-w-2.5 tw-h-2.5' : 'tw-w-3 tw-h-3';
52
+ className += isLiked.value ? ' filled' : '';
53
+
54
+ return className;
55
+ });
56
+
57
+ const changeState = () => {
58
+ emit('click', !isLiked.value);
59
+ };
60
+
61
+ return {
62
+ icon,
63
+ classState,
64
+ changeState,
65
+ };
66
+ },
67
+ };
68
+ </script>
69
+
70
+ <style scoped>
71
+ .filled >>> svg {
72
+ fill: #F60059;
73
+ }
74
+ </style>
@@ -0,0 +1,38 @@
1
+ <template>
2
+ <div>
3
+ <KvCommentsListItem
4
+ v-for="comment in comments.comment"
5
+ :id="comment.id"
6
+ :key="comment.id"
7
+ :nest-level="1"
8
+ :comment="comment"
9
+ :is-liked="comment.is_liked"
10
+ :handle-click="handleClick"
11
+ />
12
+ </div>
13
+ </template>
14
+
15
+ <script>
16
+
17
+ import KvCommentsListItem, { REPLY_COMMENT_EVENT, LIKE_COMMENT_EVENT } from './KvCommentsListItem.vue';
18
+
19
+ export default {
20
+ components: { KvCommentsListItem },
21
+ props: {
22
+ /**
23
+ * Activity comments
24
+ */
25
+ comments: {
26
+ type: Object,
27
+ default: () => {},
28
+ },
29
+ },
30
+ methods: {
31
+ handleClick(payload) {
32
+ if ([REPLY_COMMENT_EVENT, LIKE_COMMENT_EVENT].includes(payload.reaction)) {
33
+ this.$emit(payload.reaction, { ...payload });
34
+ }
35
+ },
36
+ },
37
+ };
38
+ </script>
@@ -0,0 +1,133 @@
1
+ <template>
2
+ <div>
3
+ <div
4
+ class="tw-flex tw-items-center tw-gap-1"
5
+ >
6
+ <img
7
+ v-if="profileImage"
8
+ class="
9
+ data-hj-suppress
10
+ tw-inline-block
11
+ tw-w-3.5
12
+ tw-h-3.5
13
+ tw-rounded-full
14
+ tw-overflow-hidden
15
+ tw-object-fill
16
+ "
17
+ :src="profileImage"
18
+ alt="picture"
19
+ >
20
+ <p>
21
+ {{ text }}
22
+ </p>
23
+ </div>
24
+ <div class="tw-flex tw-items-center tw-gap-x-0.5">
25
+ <kv-comments-heart-button
26
+ :is-small="true"
27
+ :is-liked="isLiked"
28
+ @click="onClick(LIKE_COMMENT_EVENT, $event)"
29
+ />
30
+ <kv-button
31
+ variant="ghost"
32
+ class="tw-font-medium"
33
+ @click="onClick(REPLY_COMMENT_EVENT)"
34
+ >
35
+ Reply
36
+ </kv-button>
37
+ </div>
38
+ <div
39
+ v-if="latestChildren"
40
+ class="tw-my-1"
41
+ >
42
+ <p
43
+ v-for="nested_comment in latestChildren"
44
+ :key="nested_comment.id"
45
+ class="tw-ml-3"
46
+ >
47
+ <kv-comments-list-item
48
+ :comment="nested_comment"
49
+ :is-liked="nested_comment.is_liked"
50
+ :nest-level="nestLevel + 1"
51
+ :handle-click="handleClick"
52
+ />
53
+ </p>
54
+ </div>
55
+ </div>
56
+ </template>
57
+
58
+ <script>
59
+ import KvButton from './KvButton.vue';
60
+ import KvCommentsHeartButton from './KvCommentsHeartButton.vue';
61
+
62
+ export const REPLY_COMMENT_EVENT = 'reply-comment';
63
+ export const LIKE_COMMENT_EVENT = 'like-comment';
64
+
65
+ export default {
66
+ name: 'KvCommentsListItem',
67
+ components: {
68
+ KvButton,
69
+ KvCommentsHeartButton,
70
+ },
71
+ props: {
72
+ /**
73
+ * Activity comment
74
+ */
75
+ comment: {
76
+ type: Object,
77
+ default: () => ({}),
78
+ },
79
+ /**
80
+ * The nest level of the comment
81
+ */
82
+ nestLevel: {
83
+ type: Number,
84
+ default: 0,
85
+ },
86
+ /**
87
+ * Comment is liked by current user
88
+ */
89
+ isLiked: {
90
+ type: Boolean,
91
+ default: false,
92
+ },
93
+ /**
94
+ * The function to call when a reaction is clicked
95
+ */
96
+ handleClick: {
97
+ type: Function,
98
+ default: () => ({}),
99
+ },
100
+ },
101
+ setup(props) {
102
+ const onClick = (reaction, value) => {
103
+ props.handleClick({
104
+ reaction,
105
+ id: props.comment?.id ?? null,
106
+ userId: props.comment?.user_id ?? null,
107
+ isChild: true,
108
+ value,
109
+ });
110
+ };
111
+
112
+ return {
113
+ onClick,
114
+ REPLY_COMMENT_EVENT,
115
+ LIKE_COMMENT_EVENT,
116
+ };
117
+ },
118
+ computed: {
119
+ text() {
120
+ return this.comment?.data?.text ?? '';
121
+ },
122
+ userId() {
123
+ return this.comment?.user_id ?? null;
124
+ },
125
+ latestChildren() {
126
+ return this.comment?.latest_children ?? null;
127
+ },
128
+ profileImage() {
129
+ return this.comment?.user_picture ?? '';
130
+ },
131
+ },
132
+ };
133
+ </script>
@@ -1,4 +1,5 @@
1
1
  import KvCommentsContainer from '../KvCommentsContainer.vue';
2
+ import comments from '../../tests/fixtures/mockFeedActivityData';
2
3
 
3
4
  export default {
4
5
  title: 'KvCommentsContainer',
@@ -20,4 +21,4 @@ const story = (args) => {
20
21
  return template;
21
22
  };
22
23
 
23
- export const Default = story({});
24
+ export const Default = story({ comments });
@@ -0,0 +1,25 @@
1
+ import KvCommentsHeartButton from '../KvCommentsHeartButton.vue';
2
+
3
+ export default {
4
+ title: 'KvCommentsHeartButton',
5
+ component: KvCommentsHeartButton,
6
+ };
7
+
8
+ const story = (args) => {
9
+ const template = (templateArgs, { argTypes }) => ({
10
+ props: Object.keys(argTypes),
11
+ components: { KvCommentsHeartButton },
12
+ setup() { return { args: templateArgs }; },
13
+ template: `
14
+ <KvCommentsHeartButton v-bind="args" />
15
+ `,
16
+ });
17
+ template.args = args;
18
+ return template;
19
+ };
20
+
21
+ export const Default = story({});
22
+
23
+ export const IsSmall = story({ isSmall: true });
24
+
25
+ export const IsLiked = story({ isLiked: true });
@@ -0,0 +1,24 @@
1
+ import KvCommentsList from '../KvCommentsList.vue';
2
+ import activityFeed from '../../tests/fixtures/mockFeedActivityData';
3
+
4
+ export default {
5
+ title: 'KvCommentsList',
6
+ component: KvCommentsList,
7
+ };
8
+
9
+ const story = (args) => {
10
+ const template = (templateArgs, { argTypes }) => ({
11
+ props: Object.keys(argTypes),
12
+ components: { KvCommentsList },
13
+ setup() { return { args: templateArgs }; },
14
+ template: `
15
+ <KvCommentsList v-bind="args" />
16
+ `,
17
+ });
18
+ template.args = args;
19
+ return template;
20
+ };
21
+
22
+ const comments = activityFeed.results[0].latest_reactions;
23
+
24
+ export const Default = story({ comments });
@@ -0,0 +1,24 @@
1
+ import KvCommentsListItem from '../KvCommentsListItem.vue';
2
+ import activityFeed from '../../tests/fixtures/mockFeedActivityData';
3
+
4
+ export default {
5
+ title: 'KvCommentsListItem',
6
+ component: KvCommentsListItem,
7
+ };
8
+
9
+ const story = (args) => {
10
+ const template = (templateArgs, { argTypes }) => ({
11
+ props: Object.keys(argTypes),
12
+ components: { KvCommentsListItem },
13
+ setup() { return { args: templateArgs }; },
14
+ template: `
15
+ <KvCommentsListItem v-bind="args" />
16
+ `,
17
+ });
18
+ template.args = args;
19
+ return template;
20
+ };
21
+
22
+ const comment = activityFeed.results[0].latest_reactions.comment[0];
23
+
24
+ export const Default = story({ comment });