diff options
Diffstat (limited to 'app/assets/javascripts/projects/commit')
13 files changed, 478 insertions, 0 deletions
diff --git a/app/assets/javascripts/projects/commit/components/branches_dropdown.vue b/app/assets/javascripts/projects/commit/components/branches_dropdown.vue new file mode 100644 index 00000000000..3ecc3f1d1d3 --- /dev/null +++ b/app/assets/javascripts/projects/commit/components/branches_dropdown.vue @@ -0,0 +1,94 @@ +<script> +import { + GlDropdown, + GlSearchBoxByType, + GlDropdownItem, + GlDropdownText, + GlLoadingIcon, +} from '@gitlab/ui'; +import { mapActions, mapGetters, mapState } from 'vuex'; +import { I18N_DROPDOWN } from '../constants'; + +export default { + name: 'BranchesDropdown', + components: { + GlDropdown, + GlSearchBoxByType, + GlDropdownItem, + GlDropdownText, + GlLoadingIcon, + }, + props: { + value: { + type: String, + required: false, + default: '', + }, + }, + i18n: I18N_DROPDOWN, + data() { + return { + searchTerm: this.value, + }; + }, + computed: { + ...mapGetters(['joinedBranches']), + ...mapState(['isFetching', 'branch', 'branches']), + filteredResults() { + const lowerCasedSearchTerm = this.searchTerm.toLowerCase(); + return this.joinedBranches.filter((resultString) => + resultString.toLowerCase().includes(lowerCasedSearchTerm), + ); + }, + }, + mounted() { + this.fetchBranches(this.searchTerm); + }, + methods: { + ...mapActions(['fetchBranches']), + selectBranch(branch) { + this.$emit('selectBranch', branch); + this.searchTerm = branch; // enables isSelected to work as expected + }, + isSelected(selectedBranch) { + return selectedBranch === this.branch; + }, + searchTermChanged(value) { + this.searchTerm = value; + this.fetchBranches(value); + }, + }, +}; +</script> +<template> + <gl-dropdown :text="value" :header-text="$options.i18n.headerTitle"> + <gl-search-box-by-type + :value="searchTerm" + trim + autocomplete="off" + :debounce="250" + :placeholder="$options.i18n.searchPlaceholder" + @input="searchTermChanged" + /> + <gl-dropdown-item + v-for="branch in filteredResults" + v-show="!isFetching" + :key="branch" + :name="branch" + :is-checked="isSelected(branch)" + is-check-item + @click="selectBranch(branch)" + > + {{ branch }} + </gl-dropdown-item> + <gl-dropdown-text v-show="isFetching" data-testid="dropdown-text-loading-icon"> + <gl-loading-icon class="gl-mx-auto" /> + </gl-dropdown-text> + <gl-dropdown-text + v-if="!filteredResults.length && !isFetching" + data-testid="empty-result-message" + > + <span class="gl-text-gray-500">{{ $options.i18n.noResultsMessage }}</span> + </gl-dropdown-text> + </gl-dropdown> +</template> diff --git a/app/assets/javascripts/projects/commit/components/form_modal.vue b/app/assets/javascripts/projects/commit/components/form_modal.vue new file mode 100644 index 00000000000..6411b1ca921 --- /dev/null +++ b/app/assets/javascripts/projects/commit/components/form_modal.vue @@ -0,0 +1,137 @@ +<script> +import { GlModal, GlForm, GlFormCheckbox, GlSprintf, GlFormGroup } from '@gitlab/ui'; +import { mapActions, mapState } from 'vuex'; +import eventHub from '../event_hub'; +import csrf from '~/lib/utils/csrf'; +import BranchesDropdown from './branches_dropdown.vue'; + +export default { + components: { + BranchesDropdown, + GlModal, + GlForm, + GlFormCheckbox, + GlSprintf, + GlFormGroup, + }, + inject: { + prependedText: { + default: '', + }, + }, + props: { + i18n: { + type: Object, + required: true, + }, + openModal: { + type: String, + required: true, + }, + modalId: { + type: String, + required: true, + }, + }, + data() { + return { + checked: true, + actionPrimary: { + text: this.i18n.actionPrimaryText, + attributes: [ + { variant: 'success' }, + { category: 'primary' }, + { 'data-testid': 'submit-commit' }, + ], + }, + actionCancel: { + text: this.i18n.actionCancelText, + attributes: [{ 'data-testid': 'cancel-commit' }], + }, + }; + }, + computed: { + ...mapState([ + 'branch', + 'endpoint', + 'pushCode', + 'branchCollaboration', + 'modalTitle', + 'existingBranch', + 'prependedText', + ]), + }, + mounted() { + eventHub.$on(this.openModal, this.show); + }, + methods: { + ...mapActions(['clearModal', 'setBranch', 'setSelectedBranch']), + show() { + this.$root.$emit('bv::show::modal', this.modalId); + }, + handlePrimary() { + this.$refs.form.$el.submit(); + }, + resetModalHandler() { + this.clearModal(); + this.setSelectedBranch(''); + this.checked = true; + }, + }, + csrf, +}; +</script> +<template> + <gl-modal + v-bind="$attrs" + data-testid="modal-commit" + :modal-id="modalId" + size="sm" + :title="modalTitle" + :action-cancel="actionCancel" + :action-primary="actionPrimary" + @hidden="resetModalHandler" + @primary="handlePrimary" + > + <p v-if="prependedText.length" data-testid="prepended-text"> + <gl-sprintf :message="prependedText" /> + </p> + + <gl-form ref="form" :action="endpoint" method="post"> + <input type="hidden" name="authenticity_token" :value="$options.csrf.token" /> + + <gl-form-group + :label="i18n.branchLabel" + label-for="start_branch" + data-testid="dropdown-group" + > + <input id="start_branch" type="hidden" name="start_branch" :value="branch" /> + + <branches-dropdown class="gl-w-half" :value="branch" @selectBranch="setBranch" /> + </gl-form-group> + + <gl-form-checkbox + v-if="pushCode" + v-model="checked" + name="create_merge_request" + class="gl-mt-3" + > + <gl-sprintf :message="i18n.startMergeRequest"> + <template #newMergeRequest> + <strong>{{ i18n.newMergeRequest }}</strong> + </template> + </gl-sprintf> + </gl-form-checkbox> + <input v-else type="hidden" name="create_merge_request" value="1" /> + </gl-form> + + <p v-if="!pushCode" class="gl-mb-0 gl-mt-5" data-testid="appended-text"> + <gl-sprintf v-if="branchCollaboration" :message="i18n.existingBranch"> + <template #branchName> + <strong>{{ existingBranch }}</strong> + </template> + </gl-sprintf> + <gl-sprintf v-else :message="i18n.branchInFork" /> + </p> + </gl-modal> +</template> diff --git a/app/assets/javascripts/projects/commit/components/form_trigger.vue b/app/assets/javascripts/projects/commit/components/form_trigger.vue new file mode 100644 index 00000000000..e92854c1ac3 --- /dev/null +++ b/app/assets/javascripts/projects/commit/components/form_trigger.vue @@ -0,0 +1,32 @@ +<script> +import { GlLink } from '@gitlab/ui'; +import eventHub from '../event_hub'; + +export default { + components: { + GlLink, + }, + inject: { + displayText: { + default: '', + }, + }, + props: { + openModal: { + type: String, + required: true, + }, + }, + methods: { + showModal() { + eventHub.$emit(this.openModal); + }, + }, +}; +</script> + +<template> + <gl-link data-is-link="true" data-testid="revert-commit-link" @click="showModal"> + {{ displayText }} + </gl-link> +</template> diff --git a/app/assets/javascripts/projects/commit/constants.js b/app/assets/javascripts/projects/commit/constants.js new file mode 100644 index 00000000000..233f43d56b9 --- /dev/null +++ b/app/assets/javascripts/projects/commit/constants.js @@ -0,0 +1,33 @@ +import { s__, __ } from '~/locale'; + +export const OPEN_REVERT_MODAL = 'openRevertModal'; +export const REVERT_MODAL_ID = 'revert-commit-modal'; + +export const I18N_MODAL = { + startMergeRequest: s__('ChangeTypeAction|Start a %{newMergeRequest} with these changes'), + existingBranch: s__( + 'ChangeTypeAction|Your changes will be committed to %{branchName} because a merge request is open.', + ), + branchInFork: s__( + 'ChangeTypeAction|A new branch will be created in your fork and a new merge request will be started.', + ), + newMergeRequest: __('new merge request'), + actionCancelText: __('Cancel'), +}; + +export const I18N_REVERT_MODAL = { + branchLabel: s__('ChangeTypeAction|Revert in branch'), + actionPrimaryText: s__('ChangeTypeAction|Revert'), +}; + +export const PREPENDED_MODAL_TEXT = s__( + 'ChangeTypeAction|This will create a new commit in order to revert the existing changes.', +); + +export const I18N_DROPDOWN = { + noResultsMessage: __('No matching results'), + headerTitle: s__('ChangeTypeAction|Switch branch'), + searchPlaceholder: s__('ChangeTypeAction|Search branches'), +}; + +export const PROJECT_BRANCHES_ERROR = __('Something went wrong while fetching branches'); diff --git a/app/assets/javascripts/projects/commit/event_hub.js b/app/assets/javascripts/projects/commit/event_hub.js new file mode 100644 index 00000000000..e31806ad199 --- /dev/null +++ b/app/assets/javascripts/projects/commit/event_hub.js @@ -0,0 +1,3 @@ +import createEventHub from '~/helpers/event_hub_factory'; + +export default createEventHub(); diff --git a/app/assets/javascripts/projects/commit/init_revert_commit_modal.js b/app/assets/javascripts/projects/commit/init_revert_commit_modal.js new file mode 100644 index 00000000000..ec0600cd25a --- /dev/null +++ b/app/assets/javascripts/projects/commit/init_revert_commit_modal.js @@ -0,0 +1,55 @@ +import Vue from 'vue'; +import CommitFormModal from './components/form_modal.vue'; +import { parseBoolean } from '~/lib/utils/common_utils'; +import createStore from './store'; +import { + I18N_MODAL, + I18N_REVERT_MODAL, + PREPENDED_MODAL_TEXT, + OPEN_REVERT_MODAL, + REVERT_MODAL_ID, +} from './constants'; + +export default function initInviteMembersModal() { + const el = document.querySelector('.js-revert-commit-modal'); + if (!el) { + return false; + } + + const { + title, + endpoint, + branch, + pushCode, + branchCollaboration, + existingBranch, + branchesEndpoint, + } = el.dataset; + + const store = createStore({ + endpoint, + branchesEndpoint, + branch, + pushCode: parseBoolean(pushCode), + branchCollaboration: parseBoolean(branchCollaboration), + defaultBranch: branch, + modalTitle: title, + existingBranch, + }); + + return new Vue({ + el, + store, + provide: { + prependedText: PREPENDED_MODAL_TEXT, + }, + render: (createElement) => + createElement(CommitFormModal, { + props: { + i18n: { ...I18N_REVERT_MODAL, ...I18N_MODAL }, + openModal: OPEN_REVERT_MODAL, + modalId: REVERT_MODAL_ID, + }, + }), + }); +} diff --git a/app/assets/javascripts/projects/commit/init_revert_commit_trigger.js b/app/assets/javascripts/projects/commit/init_revert_commit_trigger.js new file mode 100644 index 00000000000..0bb57f22663 --- /dev/null +++ b/app/assets/javascripts/projects/commit/init_revert_commit_trigger.js @@ -0,0 +1,20 @@ +import Vue from 'vue'; +import RevertCommitTrigger from './components/form_trigger.vue'; +import { OPEN_REVERT_MODAL } from './constants'; + +export default function initInviteMembersTrigger() { + const el = document.querySelector('.js-revert-commit-trigger'); + + if (!el) { + return false; + } + + const { displayText } = el.dataset; + + return new Vue({ + el, + provide: { displayText }, + render: (createElement) => + createElement(RevertCommitTrigger, { props: { openModal: OPEN_REVERT_MODAL } }), + }); +} diff --git a/app/assets/javascripts/projects/commit/store/actions.js b/app/assets/javascripts/projects/commit/store/actions.js new file mode 100644 index 00000000000..2ae0370d579 --- /dev/null +++ b/app/assets/javascripts/projects/commit/store/actions.js @@ -0,0 +1,36 @@ +import * as types from './mutation_types'; +import axios from '~/lib/utils/axios_utils'; +import createFlash from '~/flash'; +import { PROJECT_BRANCHES_ERROR } from '../constants'; + +export const clearModal = ({ commit }) => { + commit(types.CLEAR_MODAL); +}; + +export const requestBranches = ({ commit }) => { + commit(types.REQUEST_BRANCHES); +}; + +export const fetchBranches = ({ commit, dispatch, state }, query) => { + dispatch('requestBranches'); + + return axios + .get(state.branchesEndpoint, { + params: { search: query }, + }) + .then((res) => { + commit(types.RECEIVE_BRANCHES_SUCCESS, res.data); + }) + .catch(() => { + createFlash({ message: PROJECT_BRANCHES_ERROR }); + }); +}; + +export const setBranch = ({ commit, dispatch }, branch) => { + commit(types.SET_BRANCH, branch); + dispatch('setSelectedBranch', branch); +}; + +export const setSelectedBranch = ({ commit }, branch) => { + commit(types.SET_SELECTED_BRANCH, branch); +}; diff --git a/app/assets/javascripts/projects/commit/store/getters.js b/app/assets/javascripts/projects/commit/store/getters.js new file mode 100644 index 00000000000..664eaca32cf --- /dev/null +++ b/app/assets/javascripts/projects/commit/store/getters.js @@ -0,0 +1,5 @@ +import { uniq } from 'lodash'; + +export const joinedBranches = (state) => { + return uniq(state.branches).sort(); +}; diff --git a/app/assets/javascripts/projects/commit/store/index.js b/app/assets/javascripts/projects/commit/store/index.js new file mode 100644 index 00000000000..83802f6a36f --- /dev/null +++ b/app/assets/javascripts/projects/commit/store/index.js @@ -0,0 +1,19 @@ +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); + +export default (initialState = {}) => + new Vuex.Store({ + actions, + mutations, + getters, + state: { + ...state(), + ...initialState, + }, + }); diff --git a/app/assets/javascripts/projects/commit/store/mutation_types.js b/app/assets/javascripts/projects/commit/store/mutation_types.js new file mode 100644 index 00000000000..de0bb47e18d --- /dev/null +++ b/app/assets/javascripts/projects/commit/store/mutation_types.js @@ -0,0 +1,6 @@ +export const CLEAR_MODAL = 'CLEAR_MODAL'; + +export const REQUEST_BRANCHES = 'REQUEST_BRANCHES'; +export const RECEIVE_BRANCHES_SUCCESS = 'RECEIVE_BRANCHES_SUCCESS'; +export const SET_BRANCH = 'SET_BRANCH'; +export const SET_SELECTED_BRANCH = 'SET_SELECTED_BRANCH'; diff --git a/app/assets/javascripts/projects/commit/store/mutations.js b/app/assets/javascripts/projects/commit/store/mutations.js new file mode 100644 index 00000000000..6add00deadb --- /dev/null +++ b/app/assets/javascripts/projects/commit/store/mutations.js @@ -0,0 +1,25 @@ +import * as types from './mutation_types'; + +export default { + [types.REQUEST_BRANCHES](state) { + state.isFetching = true; + }, + + [types.RECEIVE_BRANCHES_SUCCESS](state, branches) { + state.isFetching = false; + state.branches = branches; + state.branches.unshift(state.branch); + }, + + [types.CLEAR_MODAL](state) { + state.branch = state.defaultBranch; + }, + + [types.SET_BRANCH](state, branch) { + state.branch = branch; + }, + + [types.SET_SELECTED_BRANCH](state, branch) { + state.selectedBranch = branch; + }, +}; diff --git a/app/assets/javascripts/projects/commit/store/state.js b/app/assets/javascripts/projects/commit/store/state.js new file mode 100644 index 00000000000..78c294324df --- /dev/null +++ b/app/assets/javascripts/projects/commit/store/state.js @@ -0,0 +1,13 @@ +export default () => ({ + endpoint: null, + branchesEndpoint: null, + isFetching: false, + branches: [], + selectedBranch: '', + pushCode: false, + branchCollaboration: false, + modalTitle: '', + existingBranch: '', + defaultBranch: '', + branch: '', +}); |