diff --git a/app/assets/javascripts/custom_emoji/components/list.vue b/app/assets/javascripts/custom_emoji/components/list.vue new file mode 100644 index 0000000000000000000000000000000000000000..82d5f3320b4320d74e9a30010468f906c031c001 --- /dev/null +++ b/app/assets/javascripts/custom_emoji/components/list.vue @@ -0,0 +1,135 @@ +<script> +import { GlLoadingIcon, GlTableLite, GlTabs, GlTab, GlBadge, GlKeysetPagination } from '@gitlab/ui'; +import { __ } from '~/locale'; +import { formatDate } from '~/lib/utils/datetime/date_format_utility'; + +export default { + components: { + GlTableLite, + GlLoadingIcon, + GlTabs, + GlTab, + GlBadge, + GlKeysetPagination, + }, + props: { + loading: { + type: Boolean, + required: false, + default: false, + }, + customEmojis: { + type: Array, + required: true, + }, + pageInfo: { + type: Object, + required: true, + }, + count: { + type: Number, + required: true, + }, + }, + methods: { + prevPage() { + this.$emit('input', { + before: this.pageInfo.startCursor, + }); + }, + nextPage() { + this.$emit('input', { + after: this.pageInfo.endCursor, + }); + }, + formatDate(date) { + return formatDate(date, 'mmmm d, yyyy'); + }, + }, + primaryAction: { + text: __('New custom emoji'), + attributes: { + variant: 'info', + to: '/new', + }, + }, + fields: [ + { + key: 'emoji', + label: __('Image'), + thClass: 'gl-border-t-0!', + tdClass: 'gl-vertical-align-middle!', + columnWidth: '70px', + }, + { + key: 'name', + label: __('Name'), + thClass: 'gl-border-t-0!', + tdClass: 'gl-vertical-align-middle! gl-font-monospace', + }, + { + key: 'created_at', + label: __('Created date'), + thClass: 'gl-border-t-0!', + tdClass: 'gl-vertical-align-middle!', + columnWidth: '25%', + }, + { + key: 'action', + label: '', + thClass: 'gl-border-t-0!', + columnWidth: '64px', + }, + ], +}; +</script> + +<template> + <div> + <gl-loading-icon v-if="loading" size="lg" /> + <template v-else> + <gl-tabs content-class="gl-pt-0" :action-primary="$options.primaryAction"> + <gl-tab> + <template #title> + {{ __('Emoji') }} + <gl-badge size="sm" class="gl-tab-counter-badge">{{ count }}</gl-badge> + </template> + <gl-table-lite + :items="customEmojis" + :fields="$options.fields" + table-class="gl-table-layout-fixed" + > + <template #table-colgroup="scope"> + <col + v-for="field in scope.fields" + :key="field.key" + :style="{ width: field.columnWidth }" + /> + </template> + <template #cell(emoji)="data"> + <gl-emoji + :data-name="data.item.name" + :data-fallback-src="data.item.url" + data-unicode-version="custom" + /> + </template> + <template #cell(action)> </template> + <template #cell(created_at)="data"> + {{ formatDate(data.item.createdAt) }} + </template> + <template #cell(name)="data"> + <strong class="gl-str-truncated">:{{ data.item.name }}:</strong> + </template> + </gl-table-lite> + <gl-keyset-pagination + v-if="pageInfo.hasPreviousPage || pageInfo.hasNextPage" + v-bind="pageInfo" + class="gl-mt-4" + @prev="prevPage" + @next="nextPage" + /> + </gl-tab> + </gl-tabs> + </template> + </div> +</template> diff --git a/app/assets/javascripts/custom_emoji/pages/index.vue b/app/assets/javascripts/custom_emoji/pages/index.vue index 6d32ba41eaeeabe09ad6fdad0a62eab41c894cde..b8e9f0a62d7000e4d8f832514a32f86c1f40e945 100644 --- a/app/assets/javascripts/custom_emoji/pages/index.vue +++ b/app/assets/javascripts/custom_emoji/pages/index.vue @@ -1,7 +1,63 @@ <script> -export default {}; +import { fetchPolicies } from '~/lib/graphql'; +import customEmojisQuery from '../queries/custom_emojis.query.graphql'; +import List from '../components/list.vue'; + +export default { + apollo: { + customEmojis: { + fetchPolicy: fetchPolicies.NETWORK_ONLY, + query: customEmojisQuery, + update: (r) => r.group?.customEmoji?.nodes, + variables() { + return { + groupPath: this.groupPath, + ...this.pagination, + }; + }, + result({ data }) { + const pageInfo = data.group?.customEmoji?.pageInfo; + this.count = data.group?.customEmoji?.count; + + if (pageInfo) { + this.pageInfo = pageInfo; + } + }, + }, + }, + components: { + List, + }, + inject: { + groupPath: { + default: '', + }, + }, + data() { + return { + customEmojis: [], + count: 0, + pageInfo: {}, + pagination: {}, + }; + }, + methods: { + refetchCustomEmojis() { + this.$apollo.queries.customEmojis.refetch(); + }, + changePage(pageInfo) { + this.pagination = pageInfo; + }, + }, +}; </script> <template> - <div></div> + <list + :count="count" + :loading="$apollo.queries.customEmojis.loading" + :page-info="pageInfo" + :custom-emojis="customEmojis" + @input="changePage" + /> </template> diff --git a/app/assets/javascripts/custom_emoji/queries/custom_emojis.query.graphql b/app/assets/javascripts/custom_emoji/queries/custom_emojis.query.graphql new file mode 100644 index 0000000000000000000000000000000000000000..5f8af6cf42d86c37cadab72c3da9c1f655fb9c31 --- /dev/null +++ b/app/assets/javascripts/custom_emoji/queries/custom_emojis.query.graphql @@ -0,0 +1,19 @@ +#import "~/graphql_shared/fragments/page_info.fragment.graphql" + +query getCustomEmojis($groupPath: ID!, $after: String = "", $before: String = "") { + group(fullPath: $groupPath) { + id + customEmoji(after: $after, before: $before) { + count + pageInfo { + ...PageInfo + } + nodes { + id + name + url + createdAt + } + } + } +} diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 834e89c6ee0b7a2ce5b1ef5ac90aa81f6cacb308..3adec4a9ea8e6d0728840a9f22c10bd6289c07b5 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -17391,6 +17391,9 @@ msgstr "" msgid "Embed" msgstr "" +msgid "Emoji" +msgstr "" + msgid "Empty file" msgstr "" @@ -23561,6 +23564,9 @@ msgstr "" msgid "Ignored" msgstr "" +msgid "Image" +msgstr "" + msgid "ImageDiffViewer|2-up" msgstr "" @@ -30669,6 +30675,9 @@ msgstr "" msgid "New confidential issue title" msgstr "" +msgid "New custom emoji" +msgstr "" + msgid "New deploy key" msgstr "" diff --git a/spec/frontend/custom_emoji/components/__snapshots__/list_spec.js.snap b/spec/frontend/custom_emoji/components/__snapshots__/list_spec.js.snap new file mode 100644 index 0000000000000000000000000000000000000000..4e87d4d81924b325da764d820e17d060046abb31 --- /dev/null +++ b/spec/frontend/custom_emoji/components/__snapshots__/list_spec.js.snap @@ -0,0 +1,220 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Custom emoji settings list component renders table of custom emoji 1`] = ` +<div> + <div + class="tabs gl-tabs" + > + <!----> + <div + class="" + > + <ul + class="nav gl-tabs-nav" + role="tablist" + > + <div + class="gl-actions-tabs-start" + data-testid="actions-tabs-start" + > + <a + class="btn btn-info btn-md gl-button" + data-testid="action-primary" + href="/new" + to="/new" + > + <!----> + + <!----> + + <span + class="gl-button-text" + > + + New custom emoji + + </span> + </a> + + <!----> + + <!----> + </div> + <div + class="gl-actions-tabs-end" + data-testid="actions-tabs-end" + > + <a + class="btn btn-info btn-md gl-button" + data-testid="action-primary" + href="/new" + to="/new" + > + <!----> + + <!----> + + <span + class="gl-button-text" + > + + New custom emoji + + </span> + </a> + + <!----> + + <!----> + </div> + </ul> + </div> + <div + class="tab-content gl-pt-0 gl-tab-content" + > + <transition-stub + css="true" + enteractiveclass="" + enterclass="" + entertoclass="show" + leaveactiveclass="" + leaveclass="show" + leavetoclass="" + mode="out-in" + name="" + > + <div + aria-hidden="true" + class="tab-pane" + role="tabpanel" + style="display: none;" + > + + <table + aria-busy="" + aria-colcount="4" + class="table b-table gl-table gl-table-layout-fixed" + role="table" + > + <!----> + <colgroup> + <col + style="width: 70px;" + /> + <col /> + <col + style="width: 25%;" + /> + <col + style="width: 64px;" + /> + </colgroup> + <thead + class="" + role="rowgroup" + > + <!----> + <tr + class="" + role="row" + > + <th + aria-colindex="1" + class="gl-border-t-0!" + role="columnheader" + scope="col" + > + <div> + Image + </div> + </th> + <th + aria-colindex="2" + class="gl-border-t-0!" + role="columnheader" + scope="col" + > + <div> + Name + </div> + </th> + <th + aria-colindex="3" + class="gl-border-t-0!" + role="columnheader" + scope="col" + > + <div> + Created date + </div> + </th> + <th + aria-colindex="4" + aria-label="Action" + class="gl-border-t-0!" + role="columnheader" + scope="col" + > + <div /> + </th> + </tr> + </thead> + <tbody + role="rowgroup" + > + <!----> + <tr + class="" + role="row" + > + <td + aria-colindex="1" + class="gl-vertical-align-middle!" + role="cell" + > + <gl-emoji + data-fallback-src="https://gitlab.com/custom_emoji/custom_emoji/-/raw/main/img/confused_husky.gif" + data-name="confused_husky" + data-unicode-version="custom" + /> + </td> + <td + aria-colindex="2" + class="gl-vertical-align-middle! gl-font-monospace" + role="cell" + > + <strong + class="gl-str-truncated" + > + :confused_husky: + </strong> + </td> + <td + aria-colindex="3" + class="gl-vertical-align-middle!" + role="cell" + > + + created-at + + </td> + <td + aria-colindex="4" + class="" + role="cell" + /> + </tr> + <!----> + <!----> + </tbody> + <!----> + </table> + + <!----> + </div> + </transition-stub> + <!----> + </div> + </div> +</div> +`; diff --git a/spec/frontend/custom_emoji/components/list_spec.js b/spec/frontend/custom_emoji/components/list_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..ac5219e375b0f1b70b55c3217f571584a6ee2d18 --- /dev/null +++ b/spec/frontend/custom_emoji/components/list_spec.js @@ -0,0 +1,45 @@ +import Vue from 'vue'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; +import List from '~/custom_emoji/components/list.vue'; +import { CUSTOM_EMOJI } from '../mock_data'; + +jest.mock('~/lib/utils/datetime/date_format_utility', () => ({ + formatDate: (date) => date, +})); + +Vue.config.ignoredElements = ['gl-emoji']; + +let wrapper; + +function createComponent(propsData = {}) { + wrapper = mountExtended(List, { + propsData: { + customEmojis: CUSTOM_EMOJI, + pageInfo: {}, + count: CUSTOM_EMOJI.length, + ...propsData, + }, + }); +} + +describe('Custom emoji settings list component', () => { + it('renders table of custom emoji', () => { + createComponent(); + + expect(wrapper.element).toMatchSnapshot(); + }); + + describe('pagination', () => { + it.each` + emits | button | pageInfo + ${{ before: 'startCursor' }} | ${'prevButton'} | ${{ hasPreviousPage: true, startCursor: 'startCursor' }} + ${{ after: 'endCursor' }} | ${'nextButton'} | ${{ hasNextPage: true, endCursor: 'endCursor' }} + `('emits $emits when $button is clicked', async ({ emits, button, pageInfo }) => { + createComponent({ pageInfo }); + + await wrapper.findByTestId(button).vm.$emit('click'); + + expect(wrapper.emitted('input')[0]).toEqual([emits]); + }); + }); +}); diff --git a/spec/frontend/custom_emoji/mock_data.js b/spec/frontend/custom_emoji/mock_data.js new file mode 100644 index 0000000000000000000000000000000000000000..19ba7022bf14b1f9e552103600b45ac608b1ca24 --- /dev/null +++ b/spec/frontend/custom_emoji/mock_data.js @@ -0,0 +1,8 @@ +export const CUSTOM_EMOJI = [ + { + id: 'gid://gitlab/CustomEmoji/1', + name: 'confused_husky', + url: 'https://gitlab.com/custom_emoji/custom_emoji/-/raw/main/img/confused_husky.gif', + createdAt: 'created-at', + }, +];