Commit ef1444ec authored by Kushal Pandya's avatar Kushal Pandya

Epic Vuex app

parent e4e31028
<script>
import EpicHeader from './epic_header.vue';
import EpicBody from './epic_body.vue';
export default {
components: {
EpicHeader,
EpicBody,
},
};
</script>
<template>
<div class="epic-page-container">
<epic-header/>
<epic-body/>
</div>
</template>
<script>
import { mapState } from 'vuex';
import IssuableBody from '~/issue_show/components/app.vue';
import RelatedIssues from 'ee/related_issues/components/related_issues_root.vue';
export default {
components: {
IssuableBody,
RelatedIssues,
},
computed: {
...mapState([
'endpoint',
'updateEndpoint',
'issueLinksEndpoint',
'groupPath',
'markdownPreviewPath',
'markdownDocsPath',
'canUpdate',
'canDestroy',
'canAdmin',
'initialTitleHtml',
'initialTitleText',
'initialDescriptionHtml',
'initialDescriptionText',
]),
},
};
</script>
<template>
<div class="issuable-details content-block">
<div class="detail-page-description">
<issuable-body
:endpoint="endpoint"
:update-endpoint="updateEndpoint"
:project-path="groupPath"
:markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath"
:can-update="canUpdate"
:can-destroy="canDestroy"
:show-delete-button="canDestroy"
:initial-title-html="initialTitleHtml"
:initial-title-text="initialTitleText"
:initial-description-html="initialDescriptionHtml"
:initial-description-text="initialDescriptionText"
:show-inline-edit-button="true"
:enable-autocomplete="true"
project-namespace=""
issuable-ref=""
issuable-type="epic"
/>
</div>
<related-issues
:endpoint="issueLinksEndpoint"
:can-admin="canAdmin"
:can-reorder="canAdmin"
:allow-auto-complete="false"
title="Issues"
/>
</div>
</template>
<script>
import { mapState, mapGetters, mapActions } from 'vuex';
import { __ } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
import Icon from '~/vue_shared/components/icon.vue';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import epicUtils from '../utils/epic_utils';
import { statusType } from '../constants';
export default {
directives: {
tooltip,
},
components: {
Icon,
LoadingButton,
UserAvatarLink,
TimeagoTooltip,
},
computed: {
...mapState([
'epicDeleteInProgress',
'epicStatusChangeInProgress',
'author',
'created',
'canUpdate',
]),
...mapGetters(['isEpicOpen']),
statusIcon() {
return this.isEpicOpen ? 'issue-open-m' : 'mobile-issue-close';
},
statusText() {
return this.isEpicOpen ? __('Open') : __('Closed');
},
actionButtonClass() {
return `btn btn-grouped js-btn-epic-action qa-close-reopen-epic-button ${
this.isEpicOpen ? 'btn-close' : 'btn-open'
}`;
},
actionButtonText() {
return this.isEpicOpen ? __('Close epic') : __('Reopen epic');
},
},
mounted() {
/**
* This event is triggered from Notes app
* when user clicks on `Close` button below
* comment form.
*
* When event is triggered, we want to reflect Epic status change
* across the UI so we directly call `requestEpicStatusChangeSuccess` action
* to update store state.
*/
epicUtils.bindDocumentEvent('issuable_vue_app:change', (e, isClosed) => {
const isEpicOpen = e.detail ? !e.detail.isClosed : !isClosed;
this.requestEpicStatusChangeSuccess({
state: isEpicOpen ? statusType.open : statusType.close,
});
});
},
methods: {
...mapActions(['requestEpicStatusChangeSuccess', 'toggleEpicStatus']),
},
};
</script>
<template>
<div class="detail-page-header">
<div class="detail-page-header-body">
<div
:class="{ 'status-box-open': isEpicOpen, 'status-box-issue-closed': !isEpicOpen }"
class="issuable-status-box status-box"
>
<icon
:name="statusIcon"
css-classes="d-block d-sm-none"
/>
<span class="d-none d-sm-block">{{ statusText }}</span>
</div>
<div class="issuable-meta">
{{ __('Opened') }}
<timeago-tooltip :time="created" />
{{ __('by') }}
<strong>
<user-avatar-link
:link-href="author.url"
:img-src="author.src"
:img-size="24"
:tooltip-text="author.username"
:username="author.name"
img-css-classes="avatar-inline"
/>
</strong>
</div>
</div>
<div
v-if="canUpdate"
class="detail-page-header-actions js-issuable-actions"
>
<loading-button
:label="actionButtonText"
:loading="epicStatusChangeInProgress"
:container-class="actionButtonClass"
@click="toggleEpicStatus(isEpicOpen)"
/>
</div>
</div>
</template>
export const statusType = {
open: 'opened',
close: 'closed',
};
export const statusEvent = {
close: 'close',
reopen: 'reopen',
};
import Vue from 'vue';
import { mapActions } from 'vuex';
import Cookies from 'js-cookie';
import bp from '~/breakpoints';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import createStore from './store';
import EpicApp from './components/epic_app.vue';
export default () => {
const el = document.getElementById('epic-app-root');
const epicMeta = convertObjectPropsToCamelCase(JSON.parse(el.dataset.meta), { deep: true });
const epicData = JSON.parse(el.dataset.initial);
const store = createStore();
// Collapse the sidebar on mobile screens by default
const bpBreakpoint = bp.getBreakpointSize();
if (bpBreakpoint === 'xs' || bpBreakpoint === 'sm') {
Cookies.set('collapsed_gutter', true);
}
return new Vue({
el,
store,
components: { EpicApp },
created() {
this.setEpicMeta(epicMeta);
this.setEpicData(epicData);
},
methods: {
...mapActions(['setEpicMeta', 'setEpicData']),
},
render: createElement => createElement('epic-app'),
});
};
import flash from '~/flash';
import { __ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
import epicUtils from '../utils/epic_utils';
import { statusType, statusEvent } from '../constants';
import * as types from './mutation_types';
export const setEpicMeta = ({ commit }, meta) => commit(types.SET_EPIC_META, meta);
export const setEpicData = ({ commit }, data) => commit(types.SET_EPIC_DATA, data);
export const requestEpicStatusChange = ({ commit }) => commit(types.REQUEST_EPIC_STATUS_CHANGE);
export const requestEpicStatusChangeSuccess = ({ commit }, data) =>
commit(types.REQUEST_EPIC_STATUS_CHANGE_SUCCESS, data);
export const requestEpicStatusChangeFailure = ({ commit }) => {
commit(types.REQUEST_EPIC_STATUS_CHANGE_FAILURE);
flash(__('Unable to update this epic at this time.'));
};
export const triggerIssuableEvent = (_, { isEpicOpen }) => {
// Ensure that status change is reflected across the page.
// As `Close`/`Reopen` button is also present under
// comment form (part of Notes app) We've wrapped
// call to `$(document).trigger` within `triggerDocumentEvent`
// for ease of testing
epicUtils.triggerDocumentEvent('issuable_vue_app:change', isEpicOpen);
epicUtils.triggerDocumentEvent('issuable:change', isEpicOpen);
};
export const toggleEpicStatus = ({ state, dispatch }, isEpicOpen) => {
dispatch('requestEpicStatusChange');
const statusEventType = isEpicOpen ? statusEvent.close : statusEvent.reopen;
const queryParam = `epic[state_event]=${statusEventType}`;
axios
.put(`${state.endpoint}.json?${encodeURI(queryParam)}`)
.then(({ data }) => {
dispatch('requestEpicStatusChangeSuccess', data);
dispatch('triggerIssuableEvent', { isEpicOpen: data.state === statusType.close });
})
.catch(() => {
dispatch('requestEpicStatusChangeFailure');
dispatch('triggerIssuableEvent', { isEpicOpen: !isEpicOpen });
});
};
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
import { statusType } from '../constants';
export const isEpicOpen = state => state.state === statusType.open;
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
import Vue from 'vue';
import Vuex from 'vuex';
import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations';
import state from './state';
Vue.use(Vuex);
const createStore = () =>
new Vuex.Store({
actions,
getters,
mutations,
state,
});
export default createStore;
export const SET_EPIC_META = 'SET_EPIC_META';
export const SET_EPIC_DATA = 'SET_EPIC_DATA';
export const REQUEST_EPIC_STATUS_CHANGE = 'REQUEST_EPIC_STATUS_CHANGE';
export const REQUEST_EPIC_STATUS_CHANGE_SUCCESS = 'REQUEST_EPIC_STATUS_CHANGE_SUCCESS';
export const REQUEST_EPIC_STATUS_CHANGE_FAILURE = 'REQUEST_EPIC_STATUS_CHANGE_FAILURE';
export const TRIGGER_ISSUABLE_EVENTS = 'TRIGGER_ISSUABLE_EVENTS';
import * as types from './mutation_types';
export default {
[types.SET_EPIC_META](state, meta) {
Object.assign(state, { ...meta });
},
[types.SET_EPIC_DATA](state, data) {
Object.assign(state, { ...data });
},
[types.REQUEST_EPIC_STATUS_CHANGE](state) {
state.epicStatusChangeInProgress = true;
},
[types.REQUEST_EPIC_STATUS_CHANGE_SUCCESS](state, data) {
state.state = data.state;
state.epicStatusChangeInProgress = false;
},
[types.REQUEST_EPIC_STATUS_CHANGE_FAILURE](state) {
state.epicStatusChangeInProgress = false;
},
};
export default {
// API Paths to Send/Receive Data
endpoint: '',
updateEndpoint: '',
issueLinksEndpoint: '',
groupPath: '',
markdownPreviewPath: '',
labelsPath: '',
todoPath: '',
todoDeletePath: '',
toggleSubscriptionPath: '',
// URLs to use with links
epicsWebUrl: '',
labelsWebUrl: '',
markdownDocsPath: '',
// Flags
canUpdate: false,
canDestroy: false,
canAdmin: false,
// Epic Information
epicId: 0,
state: '',
created: '',
author: null,
initialTitleHtml: '',
initialTitleText: '',
initialDescriptionHtml: '',
initialDescriptionText: '',
todoExists: false,
startDateSourcingMilestoneTitle: '',
startDateIsFixed: false,
startDateFixed: '',
startDateFromMilestones: '',
startDate: '',
dueDateSourcingMilestoneTitle: '',
dueDateIsFixed: '',
dueDateFixed: '',
dueDateFromMilestones: '',
dueDate: '',
labels: [],
participants: [],
subscribed: false,
// UI status flags
epicStatusChangeInProgress: false,
epicDeleteInProgress: false,
};
import $ from 'jquery';
const triggerDocumentEvent = (eventName, eventParam) => {
$(document).trigger(eventName, eventParam);
};
const bindDocumentEvent = (eventName, callback) => {
$(document).on(eventName, callback);
};
// This is for mocking methods from this
// file within tests using `spyOnDependency`
// which requires first param to always
// be default export of dependency as per
// https://gitlab.com/help/development/testing_guide/frontend_testing.md#stubbing-and-mocking
const epicUtils = {
triggerDocumentEvent,
bindDocumentEvent,
};
export default epicUtils;
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment