gitlab-ce/app/assets/javascripts/todos/components/todos_app.vue

265 lines
7.3 KiB
Vue

<script>
import { computed } from 'vue';
import { GlLoadingIcon, GlKeysetPagination, GlLink, GlBadge, GlTab, GlTabs } from '@gitlab/ui';
import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { createAlert } from '~/alert';
import { s__ } from '~/locale';
import Tracking from '~/tracking';
import {
INSTRUMENT_TAB_LABELS,
INSTRUMENT_TODO_FILTER_CHANGE,
STATUS_BY_TAB,
} from '~/todos/constants';
import getTodosQuery from './queries/get_todos.query.graphql';
import getPendingTodosCount from './queries/get_pending_todos_count.query.graphql';
import markAsDoneMutation from './mutations/mark_as_done.mutation.graphql';
import markAsPendingMutation from './mutations/mark_as_pending.mutation.graphql';
import TodoItem from './todo_item.vue';
import TodosEmptyState from './todos_empty_state.vue';
import TodosFilterBar, { SORT_OPTIONS } from './todos_filter_bar.vue';
import TodosMarkAllDoneButton from './todos_mark_all_done_button.vue';
const ENTRIES_PER_PAGE = 20;
export default {
components: {
GlLink,
GlLoadingIcon,
GlKeysetPagination,
GlBadge,
GlTabs,
GlTab,
TodosEmptyState,
TodosFilterBar,
TodoItem,
TodosMarkAllDoneButton,
},
mixins: [Tracking.mixin()],
provide() {
return {
currentTab: computed(() => this.currentTab),
};
},
data() {
return {
cursor: {
first: ENTRIES_PER_PAGE,
after: null,
last: null,
before: null,
},
currentUserId: null,
pageInfo: {},
todos: [],
currentTab: 0,
pendingTodosCount: '-',
queryFilterValues: {
groupId: [],
projectId: [],
authorId: [],
type: [],
action: [],
sort: `${SORT_OPTIONS[0].value}_DESC`,
},
alert: null,
showSpinnerWhileLoading: true,
};
},
apollo: {
todos: {
query: getTodosQuery,
fetchPolicy: 'cache-and-network',
variables() {
return {
state: this.statusByTab,
...this.queryFilterValues,
...this.cursor,
};
},
update({ currentUser: { id, todos: { nodes = [], pageInfo = {} } } = {} }) {
this.pageInfo = pageInfo;
this.currentUserId = id;
return nodes;
},
error(error) {
this.alert = createAlert({ message: s__('Todos|Something went wrong. Please try again.') });
Sentry.captureException(error);
},
},
pendingTodosCount: {
query: getPendingTodosCount,
variables() {
return this.queryFilterValues;
},
update({ currentUser: { todos: { count } } = {} }) {
return count;
},
},
},
computed: {
statusByTab() {
return STATUS_BY_TAB[this.currentTab];
},
isLoading() {
return this.$apollo.queries.todos.loading;
},
isFiltered() {
// Ignore sort value. It is always present and not really a filter.
const { sort: _, ...filters } = this.queryFilterValues;
return Object.values(filters).some((value) => value.length > 0);
},
showPagination() {
return !this.isLoading && (this.pageInfo?.hasPreviousPage || this.pageInfo?.hasNextPage);
},
showEmptyState() {
return !this.isLoading && this.todos.length === 0;
},
showMarkAllAsDone() {
return this.currentTab === 0 && !this.showEmptyState;
},
},
methods: {
nextPage(item) {
this.cursor = {
first: ENTRIES_PER_PAGE,
after: item,
last: null,
before: null,
};
},
prevPage(item) {
this.cursor = {
first: null,
after: null,
last: ENTRIES_PER_PAGE,
before: item,
};
},
tabChanged(tabIndex) {
this.track(INSTRUMENT_TODO_FILTER_CHANGE, {
label: INSTRUMENT_TAB_LABELS[tabIndex],
});
this.currentTab = tabIndex;
this.cursor = {
first: ENTRIES_PER_PAGE,
after: null,
last: null,
before: null,
};
},
handleFiltersChanged(data) {
this.alert?.dismiss();
this.queryFilterValues = { ...data };
},
async handleItemChanged(id, markedAsDone) {
await this.updateAllQueries(false);
this.showUndoToast(id, markedAsDone);
},
showUndoToast(todoId, markedAsDone) {
const message = markedAsDone ? s__('Todos|Marked as done') : s__('Todos|Marked as undone');
const mutation = markedAsDone ? markAsPendingMutation : markAsDoneMutation;
const { hide } = this.$toast.show(message, {
action: {
text: s__('Todos|Undo'),
onClick: async () => {
hide();
await this.$apollo.mutate({ mutation, variables: { todoId } });
this.updateAllQueries(false);
},
},
});
},
updateCounts() {
this.$apollo.queries.pendingTodosCount.refetch();
},
async updateAllQueries(showLoading = true) {
this.showSpinnerWhileLoading = showLoading;
this.updateCounts();
await this.$apollo.queries.todos.refetch();
this.showSpinnerWhileLoading = true;
},
},
};
</script>
<template>
<div>
<div class="gl-flex gl-justify-between gl-border-b-1 gl-border-gray-100 gl-border-b-solid">
<gl-tabs content-class="gl-p-0" nav-class="gl-border-0" @input="tabChanged">
<gl-tab>
<template #title>
<span>{{ s__('Todos|To Do') }}</span>
<gl-badge pill size="sm" class="gl-tab-counter-badge" data-testid="pending-todos-count">
{{ pendingTodosCount }}
</gl-badge>
</template>
</gl-tab>
<gl-tab>
<template #title>
<span>{{ s__('Todos|Done') }}</span>
</template>
</gl-tab>
<gl-tab>
<template #title>
<span>{{ s__('Todos|All') }}</span>
</template>
</gl-tab>
</gl-tabs>
<div v-if="showMarkAllAsDone" class="gl-my-3 gl-mr-5 gl-flex gl-items-center gl-justify-end">
<todos-mark-all-done-button :filters="queryFilterValues" @change="updateCounts" />
</div>
</div>
<todos-filter-bar :todos-status="statusByTab" @filters-changed="handleFiltersChanged" />
<div>
<div class="gl-flex gl-flex-col">
<gl-loading-icon v-if="isLoading && showSpinnerWhileLoading" size="lg" class="gl-mt-5" />
<ul v-else class="gl-m-0 gl-border-collapse gl-list-none gl-p-0">
<transition-group name="todos">
<todo-item
v-for="todo in todos"
:key="todo.id"
:todo="todo"
:current-user-id="currentUserId"
@change="handleItemChanged"
/>
</transition-group>
</ul>
<todos-empty-state v-if="showEmptyState" :is-filtered="isFiltered" />
<gl-keyset-pagination
v-if="showPagination"
v-bind="pageInfo"
class="gl-mt-3 gl-self-center"
@prev="prevPage"
@next="nextPage"
/>
<div class="gl-mt-5 gl-text-center">
<gl-link href="https://gitlab.com/gitlab-org/gitlab/-/issues/498315" target="_blank">{{
s__('Todos|Leave feedback')
}}</gl-link>
</div>
</div>
</div>
</div>
</template>
<style>
.todos-leave-active {
transition: transform 0.15s ease-out;
position: absolute;
}
.todos-leave-to {
opacity: 0;
transform: translateY(-100px);
}
.todos-move {
transition: transform 0.15s ease-out;
}
</style>