diff options
Diffstat (limited to 'app/assets/javascripts/pages')
58 files changed, 1482 insertions, 17 deletions
diff --git a/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue b/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue new file mode 100644 index 00000000000..555725cbe12 --- /dev/null +++ b/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue @@ -0,0 +1,47 @@ +<script> + import axios from '~/lib/utils/axios_utils'; + import Flash from '~/flash'; + import modal from '~/vue_shared/components/modal.vue'; + import { s__ } from '~/locale'; + import { redirectTo } from '~/lib/utils/url_utility'; + + export default { + components: { + modal, + }, + props: { + url: { + type: String, + required: true, + }, + }, + computed: { + text() { + return s__('AdminArea|You’re about to stop all jobs. This will halt all current jobs that are running.'); + }, + }, + methods: { + onSubmit() { + return axios.post(this.url) + .then((response) => { + // follow the rediect to refresh the page + redirectTo(response.request.responseURL); + }) + .catch((error) => { + Flash(s__('AdminArea|Stopping jobs failed')); + throw error; + }); + }, + }, + }; +</script> + +<template> + <modal + id="stop-jobs-modal" + :title="s__('AdminArea|Stop all jobs?')" + :text="text" + kind="danger" + :primary-button-label="s__('AdminArea|Stop jobs')" + @submit="onSubmit" /> +</template> diff --git a/app/assets/javascripts/pages/admin/jobs/index/index.js b/app/assets/javascripts/pages/admin/jobs/index/index.js new file mode 100644 index 00000000000..0e004bd9174 --- /dev/null +++ b/app/assets/javascripts/pages/admin/jobs/index/index.js @@ -0,0 +1,29 @@ +import Vue from 'vue'; + +import Translate from '~/vue_shared/translate'; + +import stopJobsModal from './components/stop_jobs_modal.vue'; + +Vue.use(Translate); + +export default () => { + const stopJobsButton = document.getElementById('stop-jobs-button'); + + // eslint-disable-next-line no-new + new Vue({ + el: '#stop-jobs-modal', + components: { + stopJobsModal, + }, + mounted() { + stopJobsButton.classList.remove('disabled'); + }, + render(createElement) { + return createElement('stop-jobs-modal', { + props: { + url: stopJobsButton.dataset.url, + }, + }); + }, + }); +}; diff --git a/app/assets/javascripts/pages/constants.js b/app/assets/javascripts/pages/constants.js new file mode 100644 index 00000000000..328b6541636 --- /dev/null +++ b/app/assets/javascripts/pages/constants.js @@ -0,0 +1,6 @@ +/* eslint-disable import/prefer-default-export */ + +export const FILTERED_SEARCH = { + MERGE_REQUESTS: 'merge_requests', + ISSUES: 'issues', +}; diff --git a/app/assets/javascripts/pages/dashboard/groups/index/index.js b/app/assets/javascripts/pages/dashboard/groups/index/index.js new file mode 100644 index 00000000000..8a2aae706c0 --- /dev/null +++ b/app/assets/javascripts/pages/dashboard/groups/index/index.js @@ -0,0 +1,5 @@ +import initGroupsList from '../../../../groups'; + +export default () => { + initGroupsList(); +}; diff --git a/app/assets/javascripts/pages/explore/groups/index.js b/app/assets/javascripts/pages/explore/groups/index.js index 859b073f1cb..e59c38b8bc4 100644 --- a/app/assets/javascripts/pages/explore/groups/index.js +++ b/app/assets/javascripts/pages/explore/groups/index.js @@ -1,8 +1,10 @@ import GroupsList from '~/groups_list'; import Landing from '~/landing'; +import initGroupsList from '../../../groups'; export default function () { new GroupsList(); // eslint-disable-line no-new + initGroupsList(); const landingElement = document.querySelector('.js-explore-groups-landing'); if (!landingElement) return; const exploreGroupsLanding = new Landing( diff --git a/app/assets/javascripts/pages/groups/activity/index.js b/app/assets/javascripts/pages/groups/activity/index.js new file mode 100644 index 00000000000..95faf1f1e98 --- /dev/null +++ b/app/assets/javascripts/pages/groups/activity/index.js @@ -0,0 +1,3 @@ +import Activities from '~/activities'; + +export default () => new Activities(); diff --git a/app/assets/javascripts/pages/groups/edit/index.js b/app/assets/javascripts/pages/groups/edit/index.js new file mode 100644 index 00000000000..48e8c9550bf --- /dev/null +++ b/app/assets/javascripts/pages/groups/edit/index.js @@ -0,0 +1,3 @@ +import groupAvatar from '~/group_avatar'; + +export default groupAvatar; diff --git a/app/assets/javascripts/pages/groups/group_members/index/index.js b/app/assets/javascripts/pages/groups/group_members/index/index.js new file mode 100644 index 00000000000..29319b97ae2 --- /dev/null +++ b/app/assets/javascripts/pages/groups/group_members/index/index.js @@ -0,0 +1,11 @@ +/* eslint-disable no-new */ + +import memberExpirationDate from '~/member_expiration_date'; +import Members from '~/members'; +import UsersSelect from '~/users_select'; + +export default () => { + memberExpirationDate(); + new Members(); + new UsersSelect(); +}; diff --git a/app/assets/javascripts/pages/groups/issues/index.js b/app/assets/javascripts/pages/groups/issues/index.js new file mode 100644 index 00000000000..78db543a64d --- /dev/null +++ b/app/assets/javascripts/pages/groups/issues/index.js @@ -0,0 +1,8 @@ +import projectSelect from '~/project_select'; +import initFilteredSearch from '~/pages/search/init_filtered_search'; +import { FILTERED_SEARCH } from '~/pages/constants'; + +export default () => { + initFilteredSearch(FILTERED_SEARCH.ISSUES); + projectSelect(); +}; diff --git a/app/assets/javascripts/pages/groups/labels/edit/index.js b/app/assets/javascripts/pages/groups/labels/edit/index.js new file mode 100644 index 00000000000..72c5e4744ac --- /dev/null +++ b/app/assets/javascripts/pages/groups/labels/edit/index.js @@ -0,0 +1,3 @@ +import Labels from '~/labels'; + +export default () => new Labels(); diff --git a/app/assets/javascripts/pages/groups/labels/index/index.js b/app/assets/javascripts/pages/groups/labels/index/index.js new file mode 100644 index 00000000000..018345fa112 --- /dev/null +++ b/app/assets/javascripts/pages/groups/labels/index/index.js @@ -0,0 +1,3 @@ +import initLabels from '~/init_labels'; + +export default initLabels; diff --git a/app/assets/javascripts/pages/groups/labels/new/index.js b/app/assets/javascripts/pages/groups/labels/new/index.js new file mode 100644 index 00000000000..72c5e4744ac --- /dev/null +++ b/app/assets/javascripts/pages/groups/labels/new/index.js @@ -0,0 +1,3 @@ +import Labels from '~/labels'; + +export default () => new Labels(); diff --git a/app/assets/javascripts/pages/groups/merge_requests/index.js b/app/assets/javascripts/pages/groups/merge_requests/index.js new file mode 100644 index 00000000000..9b3af4537e7 --- /dev/null +++ b/app/assets/javascripts/pages/groups/merge_requests/index.js @@ -0,0 +1,8 @@ +import projectSelect from '~/project_select'; +import initFilteredSearch from '~/pages/search/init_filtered_search'; +import { FILTERED_SEARCH } from '~/pages/constants'; + +export default () => { + initFilteredSearch(FILTERED_SEARCH.MERGE_REQUESTS); + projectSelect(); +}; diff --git a/app/assets/javascripts/pages/groups/milestones/show/index.js b/app/assets/javascripts/pages/groups/milestones/show/index.js new file mode 100644 index 00000000000..c9a18353f2e --- /dev/null +++ b/app/assets/javascripts/pages/groups/milestones/show/index.js @@ -0,0 +1,3 @@ +import initMilestonesShow from '~/pages/milestones/shared/init_milestones_show'; + +export default initMilestonesShow; diff --git a/app/assets/javascripts/pages/groups/new/index.js b/app/assets/javascripts/pages/groups/new/index.js new file mode 100644 index 00000000000..7850b90d3d2 --- /dev/null +++ b/app/assets/javascripts/pages/groups/new/index.js @@ -0,0 +1,9 @@ +import BindInOut from '~/behaviors/bind_in_out'; +import Group from '~/group'; +import groupAvatar from '~/group_avatar'; + +export default () => { + BindInOut.initAll(); + new Group(); // eslint-disable-line no-new + groupAvatar(); +}; diff --git a/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js b/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js new file mode 100644 index 00000000000..f26c7360fbe --- /dev/null +++ b/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js @@ -0,0 +1,11 @@ +import SecretValues from '~/behaviors/secret_values'; + +export default () => { + const secretVariableTable = document.querySelector('.js-secret-variable-table'); + if (secretVariableTable) { + const secretVariableTableValues = new SecretValues({ + container: secretVariableTable, + }); + secretVariableTableValues.init(); + } +}; diff --git a/app/assets/javascripts/pages/groups/show/index.js b/app/assets/javascripts/pages/groups/show/index.js new file mode 100644 index 00000000000..6ed0f010f15 --- /dev/null +++ b/app/assets/javascripts/pages/groups/show/index.js @@ -0,0 +1,22 @@ +/* eslint-disable no-new */ + +import NewGroupChild from '~/groups/new_group_child'; +import notificationsDropdown from '~/notifications_dropdown'; +import NotificationsForm from '~/notifications_form'; +import ProjectsList from '~/projects_list'; +import ShortcutsNavigation from '~/shortcuts_navigation'; +import initGroupsList from '../../../groups'; + +export default () => { + const newGroupChildWrapper = document.querySelector('.js-new-project-subgroup'); + new ShortcutsNavigation(); + new NotificationsForm(); + notificationsDropdown(); + new ProjectsList(); + + if (newGroupChildWrapper) { + new NewGroupChild(newGroupChildWrapper); + } + + initGroupsList(); +}; diff --git a/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue b/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue new file mode 100644 index 00000000000..c43e0a0490f --- /dev/null +++ b/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue @@ -0,0 +1,110 @@ +<script> + import axios from '~/lib/utils/axios_utils'; + + import Flash from '~/flash'; + import modal from '~/vue_shared/components/modal.vue'; + import { n__, s__, sprintf } from '~/locale'; + import { redirectTo } from '~/lib/utils/url_utility'; + import eventHub from '../event_hub'; + + export default { + components: { + modal, + }, + props: { + issueCount: { + type: Number, + required: true, + }, + mergeRequestCount: { + type: Number, + required: true, + }, + milestoneId: { + type: Number, + required: true, + }, + milestoneTitle: { + type: String, + required: true, + }, + milestoneUrl: { + type: String, + required: true, + }, + }, + computed: { + text() { + const milestoneTitle = sprintf('<strong>%{milestoneTitle}</strong>', { milestoneTitle: this.milestoneTitle }); + + if (this.issueCount === 0 && this.mergeRequestCount === 0) { + return sprintf( + s__(`Milestones| +You’re about to permanently delete the milestone %{milestoneTitle} from this project. +%{milestoneTitle} is not currently used in any issues or merge requests.`), + { + milestoneTitle, + }, + false, + ); + } + + return sprintf( + s__(`Milestones| +You’re about to permanently delete the milestone %{milestoneTitle} from this project and remove it from %{issuesWithCount} and %{mergeRequestsWithCount}. +Once deleted, it cannot be undone or recovered.`), + { + milestoneTitle, + issuesWithCount: n__('%d issue', '%d issues', this.issueCount), + mergeRequestsWithCount: n__('%d merge request', '%d merge requests', this.mergeRequestCount), + }, + false, + ); + }, + title() { + return sprintf(s__('Milestones|Delete milestone %{milestoneTitle}?'), { milestoneTitle: this.milestoneTitle }); + }, + }, + methods: { + onSubmit() { + eventHub.$emit('deleteMilestoneModal.requestStarted', this.milestoneUrl); + + return axios.delete(this.milestoneUrl) + .then((response) => { + eventHub.$emit('deleteMilestoneModal.requestFinished', { milestoneUrl: this.milestoneUrl, successful: true }); + + // follow the rediect to milestones overview page + redirectTo(response.request.responseURL); + }) + .catch((error) => { + eventHub.$emit('deleteMilestoneModal.requestFinished', { milestoneUrl: this.milestoneUrl, successful: false }); + + if (error.response && error.response.status === 404) { + Flash(sprintf(s__('Milestones|Milestone %{milestoneTitle} was not found'), { milestoneTitle: this.milestoneTitle })); + } else { + Flash(sprintf(s__('Milestones|Failed to delete milestone %{milestoneTitle}'), { milestoneTitle: this.milestoneTitle })); + } + throw error; + }); + }, + }, + }; +</script> + +<template> + <modal + id="delete-milestone-modal" + :title="title" + :text="text" + kind="danger" + :primary-button-label="s__('Milestones|Delete milestone')" + @submit="onSubmit"> + + <template + slot="body" + slot-scope="props"> + <p v-html="props.text"></p> + </template> + + </modal> +</template> diff --git a/app/assets/javascripts/pages/milestones/shared/event_hub.js b/app/assets/javascripts/pages/milestones/shared/event_hub.js new file mode 100644 index 00000000000..0948c2e5352 --- /dev/null +++ b/app/assets/javascripts/pages/milestones/shared/event_hub.js @@ -0,0 +1,3 @@ +import Vue from 'vue'; + +export default new Vue(); diff --git a/app/assets/javascripts/pages/milestones/shared/index.js b/app/assets/javascripts/pages/milestones/shared/index.js new file mode 100644 index 00000000000..327e2cf569c --- /dev/null +++ b/app/assets/javascripts/pages/milestones/shared/index.js @@ -0,0 +1,88 @@ +import Vue from 'vue'; + +import Translate from '~/vue_shared/translate'; + +import deleteMilestoneModal from './components/delete_milestone_modal.vue'; +import eventHub from './event_hub'; + +export default () => { + Vue.use(Translate); + + const onRequestFinished = ({ milestoneUrl, successful }) => { + const button = document.querySelector(`.js-delete-milestone-button[data-milestone-url="${milestoneUrl}"]`); + + if (!successful) { + button.removeAttribute('disabled'); + } + + button.querySelector('.js-loading-icon').classList.add('hidden'); + }; + + const onRequestStarted = (milestoneUrl) => { + const button = document.querySelector(`.js-delete-milestone-button[data-milestone-url="${milestoneUrl}"]`); + button.setAttribute('disabled', ''); + button.querySelector('.js-loading-icon').classList.remove('hidden'); + eventHub.$once('deleteMilestoneModal.requestFinished', onRequestFinished); + }; + + const onDeleteButtonClick = (event) => { + const button = event.currentTarget; + const modalProps = { + milestoneId: parseInt(button.dataset.milestoneId, 10), + milestoneTitle: button.dataset.milestoneTitle, + milestoneUrl: button.dataset.milestoneUrl, + issueCount: parseInt(button.dataset.milestoneIssueCount, 10), + mergeRequestCount: parseInt(button.dataset.milestoneMergeRequestCount, 10), + }; + eventHub.$once('deleteMilestoneModal.requestStarted', onRequestStarted); + eventHub.$emit('deleteMilestoneModal.props', modalProps); + }; + + const deleteMilestoneButtons = document.querySelectorAll('.js-delete-milestone-button'); + for (let i = 0; i < deleteMilestoneButtons.length; i += 1) { + const button = deleteMilestoneButtons[i]; + button.addEventListener('click', onDeleteButtonClick); + } + + eventHub.$once('deleteMilestoneModal.mounted', () => { + for (let i = 0; i < deleteMilestoneButtons.length; i += 1) { + const button = deleteMilestoneButtons[i]; + button.removeAttribute('disabled'); + } + }); + + return new Vue({ + el: '#delete-milestone-modal', + components: { + deleteMilestoneModal, + }, + data() { + return { + modalProps: { + milestoneId: -1, + milestoneTitle: '', + milestoneUrl: '', + issueCount: -1, + mergeRequestCount: -1, + }, + }; + }, + mounted() { + eventHub.$on('deleteMilestoneModal.props', this.setModalProps); + eventHub.$emit('deleteMilestoneModal.mounted'); + }, + beforeDestroy() { + eventHub.$off('deleteMilestoneModal.props', this.setModalProps); + }, + methods: { + setModalProps(modalProps) { + this.modalProps = modalProps; + }, + }, + render(createElement) { + return createElement(deleteMilestoneModal, { + props: this.modalProps, + }); + }, + }); +}; diff --git a/app/assets/javascripts/pages/milestones/shared/init_milestones_show.js b/app/assets/javascripts/pages/milestones/shared/init_milestones_show.js new file mode 100644 index 00000000000..7aa5be0d5b9 --- /dev/null +++ b/app/assets/javascripts/pages/milestones/shared/init_milestones_show.js @@ -0,0 +1,9 @@ +/* eslint-disable no-new */ + +import Milestone from '~/milestone'; +import Sidebar from '~/right_sidebar'; + +export default () => { + new Milestone(); + new Sidebar(); +}; diff --git a/app/assets/javascripts/pages/omniauth_callbacks/index.js b/app/assets/javascripts/pages/omniauth_callbacks/index.js new file mode 100644 index 00000000000..54f4e56359a --- /dev/null +++ b/app/assets/javascripts/pages/omniauth_callbacks/index.js @@ -0,0 +1,5 @@ +import initU2F from '../../shared/sessions/u2f'; + +export default () => { + initU2F(); +}; diff --git a/app/assets/javascripts/pages/projects/constants.js b/app/assets/javascripts/pages/projects/constants.js new file mode 100644 index 00000000000..9efbf7cd36e --- /dev/null +++ b/app/assets/javascripts/pages/projects/constants.js @@ -0,0 +1,6 @@ +/* eslint-disable import/prefer-default-export */ + +export const ISSUABLE_INDEX = { + MERGE_REQUEST: 'merge_request_', + ISSUE: 'issue_', +}; diff --git a/app/assets/javascripts/pages/projects/edit/index.js b/app/assets/javascripts/pages/projects/edit/index.js index 7f662ef6b6a..9edf36d66b1 100644 --- a/app/assets/javascripts/pages/projects/edit/index.js +++ b/app/assets/javascripts/pages/projects/edit/index.js @@ -1,8 +1,14 @@ import initSettingsPanels from '~/settings_panels'; import setupProjectEdit from '~/project_edit'; +import ProjectNew from '../shared/project_new'; +import projectAvatar from '../shared/project_avatar'; +import initProjectPermissionsSettings from '../shared/permissions'; export default () => { + new ProjectNew(); // eslint-disable-line no-new setupProjectEdit(); // Initialize expandable settings panels initSettingsPanels(); + projectAvatar(); + initProjectPermissionsSettings(); }; diff --git a/app/assets/javascripts/pages/projects/environments/metrics/index.js b/app/assets/javascripts/pages/projects/environments/metrics/index.js new file mode 100644 index 00000000000..f4760cb2720 --- /dev/null +++ b/app/assets/javascripts/pages/projects/environments/metrics/index.js @@ -0,0 +1,3 @@ +import monitoringBundle from '~/monitoring/monitoring_bundle'; + +export default monitoringBundle; diff --git a/app/assets/javascripts/pages/projects/index.js b/app/assets/javascripts/pages/projects/index.js new file mode 100644 index 00000000000..9b1d52692a3 --- /dev/null +++ b/app/assets/javascripts/pages/projects/index.js @@ -0,0 +1,7 @@ +import Project from './project'; +import ShortcutsNavigation from '../../shortcuts_navigation'; + +export default () => { + new Project(); // eslint-disable-line no-new + new ShortcutsNavigation(); // eslint-disable-line no-new +}; diff --git a/app/assets/javascripts/pages/projects/init_form.js b/app/assets/javascripts/pages/projects/init_form.js new file mode 100644 index 00000000000..0b6c5c1d30b --- /dev/null +++ b/app/assets/javascripts/pages/projects/init_form.js @@ -0,0 +1,7 @@ +import ZenMode from '~/zen_mode'; +import GLForm from '~/gl_form'; + +export default function ($formEl) { + new ZenMode(); // eslint-disable-line no-new + new GLForm($formEl, true); // eslint-disable-line no-new +} diff --git a/app/assets/javascripts/pages/projects/issues/index/index.js b/app/assets/javascripts/pages/projects/issues/index/index.js index fd395a45f00..0d3f35f044d 100644 --- a/app/assets/javascripts/pages/projects/issues/index/index.js +++ b/app/assets/javascripts/pages/projects/issues/index/index.js @@ -1,17 +1,15 @@ - /* eslint-disable no-new */ import IssuableIndex from '~/issuable_index'; import ShortcutsNavigation from '~/shortcuts_navigation'; import UsersSelect from '~/users_select'; +import initFilteredSearch from '~/pages/search/init_filtered_search'; +import { FILTERED_SEARCH } from '~/pages/constants'; +import { ISSUABLE_INDEX } from '~/pages/projects/constants'; export default () => { - const filteredSearchEnabled = gl.FilteredSearchManager && document.querySelector('.filtered-search'); - if (filteredSearchEnabled) { - const filteredSearchManager = new gl.FilteredSearchManager('issues'); - filteredSearchManager.setup(); - } - new IssuableIndex('issue_'); + initFilteredSearch(FILTERED_SEARCH.ISSUES); + new IssuableIndex(ISSUABLE_INDEX.ISSUE); new ShortcutsNavigation(); new UsersSelect(); diff --git a/app/assets/javascripts/pages/projects/merge_requests/creations/diffs/index.js b/app/assets/javascripts/pages/projects/merge_requests/creations/diffs/index.js new file mode 100644 index 00000000000..734d01ae6f2 --- /dev/null +++ b/app/assets/javascripts/pages/projects/merge_requests/creations/diffs/index.js @@ -0,0 +1,3 @@ +import initMergeRequest from '~/pages/projects/merge_requests/init_merge_request'; + +export default initMergeRequest; diff --git a/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js b/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js new file mode 100644 index 00000000000..ccd0b54c5ed --- /dev/null +++ b/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js @@ -0,0 +1,18 @@ +import Compare from '~/compare'; +import MergeRequest from '~/merge_request'; + +export default () => { + const mrNewCompareNode = document.querySelector('.js-merge-request-new-compare'); + if (mrNewCompareNode) { + new Compare({ // eslint-disable-line no-new + targetProjectUrl: mrNewCompareNode.dataset.targetProjectUrl, + sourceBranchUrl: mrNewCompareNode.dataset.sourceBranchUrl, + targetBranchUrl: mrNewCompareNode.dataset.targetBranchUrl, + }); + } else { + const mrNewSubmitNode = document.querySelector('.js-merge-request-new-submit'); + new MergeRequest({ // eslint-disable-line no-new + action: mrNewSubmitNode.dataset.mrSubmitAction, + }); + } +}; diff --git a/app/assets/javascripts/pages/projects/merge_requests/edit/index.js b/app/assets/javascripts/pages/projects/merge_requests/edit/index.js new file mode 100644 index 00000000000..734d01ae6f2 --- /dev/null +++ b/app/assets/javascripts/pages/projects/merge_requests/edit/index.js @@ -0,0 +1,3 @@ +import initMergeRequest from '~/pages/projects/merge_requests/init_merge_request'; + +export default initMergeRequest; diff --git a/app/assets/javascripts/pages/projects/merge_requests/index/index.js b/app/assets/javascripts/pages/projects/merge_requests/index/index.js index a52bea03aa2..b386e8fb48d 100644 --- a/app/assets/javascripts/pages/projects/merge_requests/index/index.js +++ b/app/assets/javascripts/pages/projects/merge_requests/index/index.js @@ -1,16 +1,13 @@ import IssuableIndex from '~/issuable_index'; import ShortcutsNavigation from '~/shortcuts_navigation'; import UsersSelect from '~/users_select'; +import initFilteredSearch from '~/pages/search/init_filtered_search'; +import { FILTERED_SEARCH } from '~/pages/constants'; +import { ISSUABLE_INDEX } from '~/pages/projects/constants'; export default () => { - const filteredSearchEnabled = gl.FilteredSearchManager && document.querySelector('.filtered-search'); - - if (filteredSearchEnabled) { - const filteredSearchManager = new gl.FilteredSearchManager('merge_requests'); - filteredSearchManager.setup(); - } - - new IssuableIndex('merge_request_'); // eslint-disable-line no-new + initFilteredSearch(FILTERED_SEARCH.MERGE_REQUESTS); + new IssuableIndex(ISSUABLE_INDEX.MERGE_REQUEST); // eslint-disable-line no-new new ShortcutsNavigation(); // eslint-disable-line no-new new UsersSelect(); // eslint-disable-line no-new }; diff --git a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js new file mode 100644 index 00000000000..8bfac606aab --- /dev/null +++ b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js @@ -0,0 +1,19 @@ +/* eslint-disable no-new */ + +import Diff from '~/diff'; +import ShortcutsNavigation from '~/shortcuts_navigation'; +import GLForm from '~/gl_form'; +import IssuableForm from '~/issuable_form'; +import LabelsSelect from '~/labels_select'; +import MilestoneSelect from '~/milestone_select'; +import IssuableTemplateSelectors from '~/templates/issuable_template_selectors'; + +export default () => { + new Diff(); + new ShortcutsNavigation(); + new GLForm($('.merge-request-form'), true); + new IssuableForm($('.merge-request-form')); + new LabelsSelect(); + new MilestoneSelect(); + new IssuableTemplateSelectors(); +}; diff --git a/app/assets/javascripts/pages/projects/milestones/index/index.js b/app/assets/javascripts/pages/projects/milestones/index/index.js new file mode 100644 index 00000000000..8fb4d83d8a3 --- /dev/null +++ b/app/assets/javascripts/pages/projects/milestones/index/index.js @@ -0,0 +1,3 @@ +import milestones from '~/pages/milestones/shared'; + +export default milestones; diff --git a/app/assets/javascripts/pages/projects/milestones/show/index.js b/app/assets/javascripts/pages/projects/milestones/show/index.js new file mode 100644 index 00000000000..35b5c9c2ced --- /dev/null +++ b/app/assets/javascripts/pages/projects/milestones/show/index.js @@ -0,0 +1,7 @@ +import initMilestonesShow from '~/pages/milestones/shared/init_milestones_show'; +import milestones from '~/pages/milestones/shared'; + +export default () => { + initMilestonesShow(); + milestones(); +}; diff --git a/app/assets/javascripts/pages/projects/new/index.js b/app/assets/javascripts/pages/projects/new/index.js new file mode 100644 index 00000000000..71c49deb9d0 --- /dev/null +++ b/app/assets/javascripts/pages/projects/new/index.js @@ -0,0 +1,9 @@ +import ProjectNew from '../shared/project_new'; +import initProjectVisibilitySelector from '../../../project_visibility'; +import initProjectNew from '../../../projects/project_new'; + +export default () => { + new ProjectNew(); // eslint-disable-line no-new + initProjectVisibilitySelector(); + initProjectNew.bindEvents(); +}; diff --git a/app/assets/javascripts/pages/projects/project.js b/app/assets/javascripts/pages/projects/project.js new file mode 100644 index 00000000000..e30d558726b --- /dev/null +++ b/app/assets/javascripts/pages/projects/project.js @@ -0,0 +1,133 @@ +/* eslint-disable func-names, space-before-function-paren, no-var, consistent-return, no-new, prefer-arrow-callback, no-return-assign, one-var, one-var-declaration-per-line, object-shorthand, no-else-return, newline-per-chained-call, no-shadow, vars-on-top, prefer-template, max-len */ + +import Cookies from 'js-cookie'; +import { visitUrl } from '../../lib/utils/url_utility'; +import projectSelect from '../../project_select'; + +export default class Project { + constructor() { + const $cloneOptions = $('ul.clone-options-dropdown'); + const $projectCloneField = $('#project_clone'); + const $cloneBtnText = $('a.clone-dropdown-btn span'); + + const selectedCloneOption = $cloneBtnText.text().trim(); + if (selectedCloneOption.length > 0) { + $(`a:contains('${selectedCloneOption}')`, $cloneOptions).addClass('is-active'); + } + + $('a', $cloneOptions).on('click', (e) => { + const $this = $(e.currentTarget); + const url = $this.attr('href'); + const activeText = $this.find('.dropdown-menu-inner-title').text(); + + e.preventDefault(); + + $('.is-active', $cloneOptions).not($this).removeClass('is-active'); + $this.toggleClass('is-active'); + $projectCloneField.val(url); + $cloneBtnText.text(activeText); + + return $('.clone').text(url); + }); + // Ref switcher + Project.initRefSwitcher(); + $('.project-refs-select').on('change', function() { + return $(this).parents('form').submit(); + }); + $('.hide-no-ssh-message').on('click', function(e) { + Cookies.set('hide_no_ssh_message', 'false'); + $(this).parents('.no-ssh-key-message').remove(); + return e.preventDefault(); + }); + $('.hide-no-password-message').on('click', function(e) { + Cookies.set('hide_no_password_message', 'false'); + $(this).parents('.no-password-message').remove(); + return e.preventDefault(); + }); + Project.projectSelectDropdown(); + } + + static projectSelectDropdown () { + projectSelect(); + $('.project-item-select').on('click', e => Project.changeProject($(e.currentTarget).val())); + } + + static changeProject(url) { + return window.location = url; + } + + static initRefSwitcher() { + var refListItem = document.createElement('li'); + var refLink = document.createElement('a'); + + refLink.href = '#'; + + return $('.js-project-refs-dropdown').each(function() { + var $dropdown, selected; + $dropdown = $(this); + selected = $dropdown.data('selected'); + return $dropdown.glDropdown({ + data: function(term, callback) { + return $.ajax({ + url: $dropdown.data('refs-url'), + data: { + ref: $dropdown.data('ref'), + search: term, + }, + dataType: 'json', + }).done(function(refs) { + return callback(refs); + }); + }, + selectable: true, + filterable: true, + filterRemote: true, + filterByText: true, + inputFieldName: $dropdown.data('input-field-name'), + fieldName: $dropdown.data('field-name'), + renderRow: function(ref) { + var li = refListItem.cloneNode(false); + + if (ref.header != null) { + li.className = 'dropdown-header'; + li.textContent = ref.header; + } else { + var link = refLink.cloneNode(false); + + if (ref === selected) { + link.className = 'is-active'; + } + + link.textContent = ref; + link.dataset.ref = ref; + + li.appendChild(link); + } + + return li; + }, + id: function(obj, $el) { + return $el.attr('data-ref'); + }, + toggleLabel: function(obj, $el) { + return $el.text().trim(); + }, + clicked: function(options) { + const { e } = options; + e.preventDefault(); + if ($('input[name="ref"]').length) { + var $form = $dropdown.closest('form'); + + var $visit = $dropdown.data('visit'); + var shouldVisit = $visit ? true : $visit; + var action = $form.attr('action'); + var divider = action.indexOf('?') === -1 ? '?' : '&'; + if (shouldVisit) { + visitUrl(`${action}${divider}${$form.serialize()}`); + } + } + }, + }); + }); + } +} diff --git a/app/assets/javascripts/pages/projects/releases/edit/index.js b/app/assets/javascripts/pages/projects/releases/edit/index.js new file mode 100644 index 00000000000..3d997cdfff0 --- /dev/null +++ b/app/assets/javascripts/pages/projects/releases/edit/index.js @@ -0,0 +1,3 @@ +import initForm from '~/pages/projects/init_form'; + +export default initForm($('.release-form')); diff --git a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js new file mode 100644 index 00000000000..18dc1dc03a5 --- /dev/null +++ b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js @@ -0,0 +1,22 @@ +import initSettingsPanels from '~/settings_panels'; +import SecretValues from '~/behaviors/secret_values'; + +export default function () { + // Initialize expandable settings panels + initSettingsPanels(); + const runnerToken = document.querySelector('.js-secret-runner-token'); + if (runnerToken) { + const runnerTokenSecretValue = new SecretValues({ + container: runnerToken, + }); + runnerTokenSecretValue.init(); + } + + const secretVariableTable = document.querySelector('.js-secret-variable-table'); + if (secretVariableTable) { + const secretVariableTableValues = new SecretValues({ + container: secretVariableTable, + }); + secretVariableTableValues.init(); + } +} diff --git a/app/assets/javascripts/pages/projects/settings/repository/show/index.js b/app/assets/javascripts/pages/projects/settings/repository/show/index.js new file mode 100644 index 00000000000..83b5467fbc0 --- /dev/null +++ b/app/assets/javascripts/pages/projects/settings/repository/show/index.js @@ -0,0 +1,3 @@ +import initSettingsPanels from '~/settings_panels'; + +export default initSettingsPanels; diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue new file mode 100644 index 00000000000..9b13b2a524f --- /dev/null +++ b/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue @@ -0,0 +1,111 @@ +<script> + import projectFeatureToggle from '../../../../../vue_shared/components/toggle_button.vue'; + + export default { + components: { + projectFeatureToggle, + }, + + model: { + prop: 'value', + event: 'change', + }, + + props: { + name: { + type: String, + required: false, + default: '', + }, + options: { + type: Array, + required: false, + default: () => [], + }, + value: { + type: Number, + required: false, + default: 0, + }, + disabledInput: { + type: Boolean, + required: false, + default: false, + }, + }, + + computed: { + featureEnabled() { + return this.value !== 0; + }, + + displayOptions() { + if (this.featureEnabled) { + return this.options; + } + return [ + [0, 'Enable feature to choose access level'], + ]; + }, + + displaySelectInput() { + return this.disabledInput || !this.featureEnabled || this.displayOptions.length < 2; + }, + }, + + methods: { + toggleFeature(featureEnabled) { + if (featureEnabled === false || this.options.length < 1) { + this.$emit('change', 0); + } else { + const [firstOptionValue] = this.options[this.options.length - 1]; + this.$emit('change', firstOptionValue); + } + }, + + selectOption(e) { + this.$emit('change', Number(e.target.value)); + }, + }, + }; +</script> + +<template> + <div + class="project-feature-controls" + :data-for="name" + > + <input + v-if="name" + type="hidden" + :name="name" + :value="value" + /> + <project-feature-toggle + :value="featureEnabled" + @change="toggleFeature" + :disabled-input="disabledInput" + /> + <div class="select-wrapper"> + <select + class="form-control project-repo-select select-control" + @change="selectOption" + :disabled="displaySelectInput" + > + <option + v-for="[optionValue, optionName] in displayOptions" + :key="optionValue" + :value="optionValue" + :selected="optionValue === value" + > + {{ optionName }} + </option> + </select> + <i + aria-hidden="true" + class="fa fa-chevron-down" + > + </i> + </div> + </div> +</template> diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue new file mode 100644 index 00000000000..25a88f846eb --- /dev/null +++ b/app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue @@ -0,0 +1,51 @@ +<script> + export default { + props: { + label: { + type: String, + required: false, + default: null, + }, + helpPath: { + type: String, + required: false, + default: null, + }, + helpText: { + type: String, + required: false, + default: null, + }, + }, + }; +</script> + +<template> + <div class="project-feature-row"> + <label + v-if="label" + class="label-light" + > + {{ label }} + <a + v-if="helpPath" + :href="helpPath" + target="_blank" + > + <i + aria-hidden="true" + data-hidden="true" + class="fa fa-question-circle" + > + </i> + </a> + </label> + <span + v-if="helpText" + class="help-block" + > + {{ helpText }} + </span> + <slot></slot> + </div> +</template> diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue new file mode 100644 index 00000000000..755a34b7348 --- /dev/null +++ b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue @@ -0,0 +1,328 @@ +<script> + import projectFeatureSetting from './project_feature_setting.vue'; + import projectFeatureToggle from '../../../../../vue_shared/components/toggle_button.vue'; + import projectSettingRow from './project_setting_row.vue'; + import { visibilityOptions, visibilityLevelDescriptions } from '../constants'; + import { toggleHiddenClassBySelector } from '../external'; + + export default { + components: { + projectFeatureSetting, + projectFeatureToggle, + projectSettingRow, + }, + + props: { + currentSettings: { + type: Object, + required: true, + }, + canChangeVisibilityLevel: { + type: Boolean, + required: false, + default: false, + }, + allowedVisibilityOptions: { + type: Array, + required: false, + default: () => [0, 10, 20], + }, + lfsAvailable: { + type: Boolean, + required: false, + default: false, + }, + registryAvailable: { + type: Boolean, + required: false, + default: false, + }, + visibilityHelpPath: { + type: String, + required: false, + default: '', + }, + lfsHelpPath: { + type: String, + required: false, + default: '', + }, + registryHelpPath: { + type: String, + required: false, + default: '', + }, + }, + + data() { + const defaults = { + visibilityOptions, + visibilityLevel: visibilityOptions.PUBLIC, + issuesAccessLevel: 20, + repositoryAccessLevel: 20, + mergeRequestsAccessLevel: 20, + buildsAccessLevel: 20, + wikiAccessLevel: 20, + snippetsAccessLevel: 20, + containerRegistryEnabled: true, + lfsEnabled: true, + requestAccessEnabled: true, + highlightChangesClass: false, + }; + + return { ...defaults, ...this.currentSettings }; + }, + + computed: { + featureAccessLevelOptions() { + const options = [ + [10, 'Only Project Members'], + ]; + if (this.visibilityLevel !== visibilityOptions.PRIVATE) { + options.push([20, 'Everyone With Access']); + } + return options; + }, + + repoFeatureAccessLevelOptions() { + return this.featureAccessLevelOptions.filter( + ([value]) => value <= this.repositoryAccessLevel, + ); + }, + + repositoryEnabled() { + return this.repositoryAccessLevel > 0; + }, + + visibilityLevelDescription() { + return visibilityLevelDescriptions[this.visibilityLevel]; + }, + }, + + watch: { + visibilityLevel(value, oldValue) { + if (value === visibilityOptions.PRIVATE) { + // when private, features are restricted to "only team members" + this.issuesAccessLevel = Math.min(10, this.issuesAccessLevel); + this.repositoryAccessLevel = Math.min(10, this.repositoryAccessLevel); + this.mergeRequestsAccessLevel = Math.min(10, this.mergeRequestsAccessLevel); + this.buildsAccessLevel = Math.min(10, this.buildsAccessLevel); + this.wikiAccessLevel = Math.min(10, this.wikiAccessLevel); + this.snippetsAccessLevel = Math.min(10, this.snippetsAccessLevel); + this.highlightChanges(); + } else if (oldValue === visibilityOptions.PRIVATE) { + // if changing away from private, make enabled features more permissive + if (this.issuesAccessLevel > 0) this.issuesAccessLevel = 20; + if (this.repositoryAccessLevel > 0) this.repositoryAccessLevel = 20; + if (this.mergeRequestsAccessLevel > 0) this.mergeRequestsAccessLevel = 20; + if (this.buildsAccessLevel > 0) this.buildsAccessLevel = 20; + if (this.wikiAccessLevel > 0) this.wikiAccessLevel = 20; + if (this.snippetsAccessLevel > 0) this.snippetsAccessLevel = 20; + this.highlightChanges(); + } + }, + + repositoryAccessLevel(value, oldValue) { + if (value < oldValue) { + // sub-features cannot have more premissive access level + this.mergeRequestsAccessLevel = Math.min(this.mergeRequestsAccessLevel, value); + this.buildsAccessLevel = Math.min(this.buildsAccessLevel, value); + + if (value === 0) { + this.containerRegistryEnabled = false; + this.lfsEnabled = false; + } + } else if (oldValue === 0) { + this.mergeRequestsAccessLevel = value; + this.buildsAccessLevel = value; + this.containerRegistryEnabled = true; + this.lfsEnabled = true; + } + }, + + issuesAccessLevel(value, oldValue) { + if (value === 0) toggleHiddenClassBySelector('.issues-feature', true); + else if (oldValue === 0) toggleHiddenClassBySelector('.issues-feature', false); + }, + + mergeRequestsAccessLevel(value, oldValue) { + if (value === 0) toggleHiddenClassBySelector('.merge-requests-feature', true); + else if (oldValue === 0) toggleHiddenClassBySelector('.merge-requests-feature', false); + }, + + buildsAccessLevel(value, oldValue) { + if (value === 0) toggleHiddenClassBySelector('.builds-feature', true); + else if (oldValue === 0) toggleHiddenClassBySelector('.builds-feature', false); + }, + }, + + methods: { + highlightChanges() { + this.highlightChangesClass = true; + this.$nextTick(() => { + this.highlightChangesClass = false; + }); + }, + + visibilityAllowed(option) { + return this.allowedVisibilityOptions.includes(option); + }, + }, + }; +</script> + +<template> + <div> + <div class="project-visibility-setting"> + <project-setting-row + label="Project visibility" + :help-path="visibilityHelpPath" + > + <div class="project-feature-controls"> + <div class="select-wrapper"> + <select + name="project[visibility_level]" + v-model="visibilityLevel" + class="form-control select-control" + :disabled="!canChangeVisibilityLevel" + > + <option + :value="visibilityOptions.PRIVATE" + :disabled="!visibilityAllowed(visibilityOptions.PRIVATE)" + > + Private + </option> + <option + :value="visibilityOptions.INTERNAL" + :disabled="!visibilityAllowed(visibilityOptions.INTERNAL)" + > + Internal + </option> + <option + :value="visibilityOptions.PUBLIC" + :disabled="!visibilityAllowed(visibilityOptions.PUBLIC)" + > + Public + </option> + </select> + <i + aria-hidden="true" + data-hidden="true" + class="fa fa-chevron-down" + > + </i> + </div> + </div> + <span class="help-block">{{ visibilityLevelDescription }}</span> + <label + v-if="visibilityLevel !== visibilityOptions.PRIVATE" + class="request-access" + > + <input + type="hidden" + name="project[request_access_enabled]" + :value="requestAccessEnabled" + /> + <input + type="checkbox" + v-model="requestAccessEnabled" + /> + Allow users to request access + </label> + </project-setting-row> + </div> + <div + class="project-feature-settings" + :class="{ 'highlight-changes': highlightChangesClass }" + > + <project-setting-row + label="Issues" + help-text="Lightweight issue tracking system for this project" + > + <project-feature-setting + name="project[project_feature_attributes][issues_access_level]" + :options="featureAccessLevelOptions" + v-model="issuesAccessLevel" + /> + </project-setting-row> + <project-setting-row + label="Repository" + help-text="View and edit files in this project" + > + <project-feature-setting + name="project[project_feature_attributes][repository_access_level]" + :options="featureAccessLevelOptions" + v-model="repositoryAccessLevel" + /> + </project-setting-row> + <div class="project-feature-setting-group"> + <project-setting-row + label="Merge requests" + help-text="Submit changes to be merged upstream" + > + <project-feature-setting + name="project[project_feature_attributes][merge_requests_access_level]" + :options="repoFeatureAccessLevelOptions" + v-model="mergeRequestsAccessLevel" + :disabled-input="!repositoryEnabled" + /> + </project-setting-row> + <project-setting-row + label="Pipelines" + help-text="Build, test, and deploy your changes" + > + <project-feature-setting + name="project[project_feature_attributes][builds_access_level]" + :options="repoFeatureAccessLevelOptions" + v-model="buildsAccessLevel" + :disabled-input="!repositoryEnabled" + /> + </project-setting-row> + <project-setting-row + v-if="registryAvailable" + label="Container registry" + :help-path="registryHelpPath" + help-text="Every project can have its own space to store its Docker images" + > + <project-feature-toggle + name="project[container_registry_enabled]" + v-model="containerRegistryEnabled" + :disabled-input="!repositoryEnabled" + /> + </project-setting-row> + <project-setting-row + v-if="lfsAvailable" + label="Git Large File Storage" + :help-path="lfsHelpPath" + help-text="Manages large files such as audio, video, and graphics files" + > + <project-feature-toggle + name="project[lfs_enabled]" + v-model="lfsEnabled" + :disabled-input="!repositoryEnabled" + /> + </project-setting-row> + </div> + <project-setting-row + label="Wiki" + help-text="Pages for project documentation" + > + <project-feature-setting + name="project[project_feature_attributes][wiki_access_level]" + :options="featureAccessLevelOptions" + v-model="wikiAccessLevel" + /> + </project-setting-row> + <project-setting-row + label="Snippets" + help-text="Share code pastes with others out of Git repository" + > + <project-feature-setting + name="project[project_feature_attributes][snippets_access_level]" + :options="featureAccessLevelOptions" + v-model="snippetsAccessLevel" + /> + </project-setting-row> + </div> + </div> +</template> diff --git a/app/assets/javascripts/pages/projects/shared/permissions/constants.js b/app/assets/javascripts/pages/projects/shared/permissions/constants.js new file mode 100644 index 00000000000..ce47562f259 --- /dev/null +++ b/app/assets/javascripts/pages/projects/shared/permissions/constants.js @@ -0,0 +1,11 @@ +export const visibilityOptions = { + PRIVATE: 0, + INTERNAL: 10, + PUBLIC: 20, +}; + +export const visibilityLevelDescriptions = { + [visibilityOptions.PRIVATE]: 'The project is accessible only by members of the project. Access must be granted explicitly to each user.', + [visibilityOptions.INTERNAL]: 'The project can be accessed by any user who is logged in.', + [visibilityOptions.PUBLIC]: 'The project can be accessed by anyone, regardless of authentication.', +}; diff --git a/app/assets/javascripts/pages/projects/shared/permissions/external.js b/app/assets/javascripts/pages/projects/shared/permissions/external.js new file mode 100644 index 00000000000..460af4a2111 --- /dev/null +++ b/app/assets/javascripts/pages/projects/shared/permissions/external.js @@ -0,0 +1,18 @@ +const selectorCache = []; + +// workaround since we don't have a polyfill for classList.toggle 2nd parameter +export function toggleHiddenClass(element, hidden) { + if (hidden) { + element.classList.add('hidden'); + } else { + element.classList.remove('hidden'); + } +} + +// hide external feature-specific settings when a given feature is disabled +export function toggleHiddenClassBySelector(selector, hidden) { + if (!selectorCache[selector]) { + selectorCache[selector] = document.querySelectorAll(selector); + } + selectorCache[selector].forEach(elm => toggleHiddenClass(elm, hidden)); +} diff --git a/app/assets/javascripts/pages/projects/shared/permissions/index.js b/app/assets/javascripts/pages/projects/shared/permissions/index.js new file mode 100644 index 00000000000..dbde8dda634 --- /dev/null +++ b/app/assets/javascripts/pages/projects/shared/permissions/index.js @@ -0,0 +1,13 @@ +import Vue from 'vue'; +import settingsPanel from './components/settings_panel.vue'; + +export default function initProjectPermissionsSettings() { + const mountPoint = document.querySelector('.js-project-permissions-form'); + const componentPropsEl = document.querySelector('.js-project-permissions-form-data'); + const componentProps = JSON.parse(componentPropsEl.innerHTML); + + return new Vue({ + el: mountPoint, + render: createElement => createElement(settingsPanel, { props: { ...componentProps } }), + }); +} diff --git a/app/assets/javascripts/pages/projects/shared/project_avatar.js b/app/assets/javascripts/pages/projects/shared/project_avatar.js new file mode 100644 index 00000000000..56627aa155c --- /dev/null +++ b/app/assets/javascripts/pages/projects/shared/project_avatar.js @@ -0,0 +1,13 @@ +export default function projectAvatar() { + $('.js-choose-project-avatar-button').bind('click', function onClickAvatar() { + const form = $(this).closest('form'); + return form.find('.js-project-avatar-input').click(); + }); + + $('.js-project-avatar-input').bind('change', function onClickAvatarInput() { + const form = $(this).closest('form'); + // eslint-disable-next-line no-useless-escape + const filename = $(this).val().replace(/^.*[\\\/]/, ''); + return form.find('.js-avatar-filename').text(filename); + }); +} diff --git a/app/assets/javascripts/pages/projects/shared/project_new.js b/app/assets/javascripts/pages/projects/shared/project_new.js new file mode 100644 index 00000000000..86faba0b910 --- /dev/null +++ b/app/assets/javascripts/pages/projects/shared/project_new.js @@ -0,0 +1,151 @@ +/* eslint-disable func-names, no-var, no-underscore-dangle, prefer-template, prefer-arrow-callback*/ + +import VisibilitySelect from '../../../visibility_select'; + +function highlightChanges($elm) { + $elm.addClass('highlight-changes'); + setTimeout(() => $elm.removeClass('highlight-changes'), 10); +} + +export default class ProjectNew { + constructor() { + this.toggleSettings = this.toggleSettings.bind(this); + this.$selects = $('.features select'); + this.$repoSelects = this.$selects.filter('.js-repo-select'); + this.$projectSelects = this.$selects.not('.js-repo-select'); + + $('.project-edit-container').on('ajax:before', () => { + $('.project-edit-container').hide(); + return $('.save-project-loader').show(); + }); + + this.initVisibilitySelect(); + + this.toggleSettings(); + this.toggleSettingsOnclick(); + this.toggleRepoVisibility(); + } + + initVisibilitySelect() { + const visibilityContainer = document.querySelector('.js-visibility-select'); + if (!visibilityContainer) return; + const visibilitySelect = new VisibilitySelect(visibilityContainer); + visibilitySelect.init(); + + const $visibilitySelect = $(visibilityContainer).find('select'); + let projectVisibility = $visibilitySelect.val(); + const PROJECT_VISIBILITY_PRIVATE = '0'; + + $visibilitySelect.on('change', () => { + const newProjectVisibility = $visibilitySelect.val(); + + if (projectVisibility !== newProjectVisibility) { + this.$projectSelects.each((idx, select) => { + const $select = $(select); + const $options = $select.find('option'); + const values = $.map($options, e => e.value); + + // if switched to "private", limit visibility options + if (newProjectVisibility === PROJECT_VISIBILITY_PRIVATE) { + if ($select.val() !== values[0] && $select.val() !== values[1]) { + $select.val(values[1]).trigger('change'); + highlightChanges($select); + } + $options.slice(2).disable(); + } + + // if switched from "private", increase visibility for non-disabled options + if (projectVisibility === PROJECT_VISIBILITY_PRIVATE) { + $options.enable(); + if ($select.val() !== values[0] && $select.val() !== values[values.length - 1]) { + $select.val(values[values.length - 1]).trigger('change'); + highlightChanges($select); + } + } + }); + + projectVisibility = newProjectVisibility; + } + }); + } + + toggleSettings() { + this.$selects.each(function () { + var $select = $(this); + var className = $select.data('field') + .replace(/_/g, '-') + .replace('access-level', 'feature'); + ProjectNew._showOrHide($select, '.' + className); + }); + } + + toggleSettingsOnclick() { + this.$selects.on('change', this.toggleSettings); + } + + static _showOrHide(checkElement, container) { + const $container = $(container); + + if ($(checkElement).val() !== '0') { + return $container.show(); + } + return $container.hide(); + } + + toggleRepoVisibility() { + var $repoAccessLevel = $('.js-repo-access-level select'); + var $lfsEnabledOption = $('.js-lfs-enabled select'); + var containerRegistry = document.querySelectorAll('.js-container-registry')[0]; + var containerRegistryCheckbox = document.getElementById('project_container_registry_enabled'); + var prevSelectedVal = parseInt($repoAccessLevel.val(), 10); + + this.$repoSelects.find("option[value='" + $repoAccessLevel.val() + "']") + .nextAll() + .hide(); + + $repoAccessLevel + .off('change') + .on('change', function () { + var selectedVal = parseInt($repoAccessLevel.val(), 10); + + this.$repoSelects.each(function () { + var $this = $(this); + var repoSelectVal = parseInt($this.val(), 10); + + $this.find('option').enable(); + + if (selectedVal < repoSelectVal || repoSelectVal === prevSelectedVal) { + $this.val(selectedVal).trigger('change'); + highlightChanges($this); + } + + $this.find("option[value='" + selectedVal + "']").nextAll().disable(); + }); + + if (selectedVal) { + this.$repoSelects.removeClass('disabled'); + + if ($lfsEnabledOption.length) { + $lfsEnabledOption.removeClass('disabled'); + highlightChanges($lfsEnabledOption); + } + if (containerRegistry) { + containerRegistry.style.display = ''; + } + } else { + this.$repoSelects.addClass('disabled'); + + if ($lfsEnabledOption.length) { + $lfsEnabledOption.val('false').addClass('disabled'); + highlightChanges($lfsEnabledOption); + } + if (containerRegistry) { + containerRegistry.style.display = 'none'; + containerRegistryCheckbox.checked = false; + } + } + + prevSelectedVal = selectedVal; + }.bind(this)); + } +} diff --git a/app/assets/javascripts/pages/projects/show/index.js b/app/assets/javascripts/pages/projects/show/index.js new file mode 100644 index 00000000000..55154cdddcb --- /dev/null +++ b/app/assets/javascripts/pages/projects/show/index.js @@ -0,0 +1,27 @@ +import ShortcutsNavigation from '~/shortcuts_navigation'; +import NotificationsForm from '~/notifications_form'; +import UserCallout from '~/user_callout'; +import TreeView from '~/tree'; +import BlobViewer from '~/blob/viewer/index'; +import Activities from '~/activities'; +import { ajaxGet } from '~/lib/utils/common_utils'; +import Star from '../../../star'; +import notificationsDropdown from '../../../notifications_dropdown'; + +export default () => { + new Star(); // eslint-disable-line no-new + notificationsDropdown(); + new ShortcutsNavigation(); // eslint-disable-line no-new + new NotificationsForm(); // eslint-disable-line no-new + new UserCallout({ // eslint-disable-line no-new + setCalloutPerProject: true, + className: 'js-autodevops-banner', + }); + + if ($('#tree-slider').length) new TreeView(); // eslint-disable-line no-new + if ($('.blob-viewer').length) new BlobViewer(); // eslint-disable-line no-new + if ($('.project-show-activity').length) new Activities(); // eslint-disable-line no-new + $('#tree-slider').waitForImages(() => { + ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath); + }); +}; diff --git a/app/assets/javascripts/pages/projects/snippets/edit/index.js b/app/assets/javascripts/pages/projects/snippets/edit/index.js new file mode 100644 index 00000000000..9edb16dc73b --- /dev/null +++ b/app/assets/javascripts/pages/projects/snippets/edit/index.js @@ -0,0 +1,3 @@ +import initForm from '~/pages/projects/init_form'; + +export default initForm($('.snippet-form')); diff --git a/app/assets/javascripts/pages/projects/snippets/new/index.js b/app/assets/javascripts/pages/projects/snippets/new/index.js new file mode 100644 index 00000000000..9edb16dc73b --- /dev/null +++ b/app/assets/javascripts/pages/projects/snippets/new/index.js @@ -0,0 +1,3 @@ +import initForm from '~/pages/projects/init_form'; + +export default initForm($('.snippet-form')); diff --git a/app/assets/javascripts/pages/projects/snippets/show/index.js b/app/assets/javascripts/pages/projects/snippets/show/index.js new file mode 100644 index 00000000000..a3cf75c385b --- /dev/null +++ b/app/assets/javascripts/pages/projects/snippets/show/index.js @@ -0,0 +1,11 @@ +import initNotes from '~/init_notes'; +import ZenMode from '~/zen_mode'; +import LineHighlighter from '../../../../line_highlighter'; +import BlobViewer from '../../../../blob/viewer'; + +export default function () { + new LineHighlighter(); // eslint-disable-line no-new + new BlobViewer(); // eslint-disable-line no-new + initNotes(); + new ZenMode(); // eslint-disable-line no-new +} diff --git a/app/assets/javascripts/pages/projects/wikis/index.js b/app/assets/javascripts/pages/projects/wikis/index.js new file mode 100644 index 00000000000..eb14c7a0e78 --- /dev/null +++ b/app/assets/javascripts/pages/projects/wikis/index.js @@ -0,0 +1,11 @@ +import Wikis from './wikis'; +import ShortcutsWiki from '../../../shortcuts_wiki'; +import ZenMode from '../../../zen_mode'; +import GLForm from '../../../gl_form'; + +export default () => { + new Wikis(); // eslint-disable-line no-new + new ShortcutsWiki(); // eslint-disable-line no-new + new ZenMode(); // eslint-disable-line no-new + new GLForm($('.wiki-form'), true); // eslint-disable-line no-new +}; diff --git a/app/assets/javascripts/pages/projects/wikis/wikis.js b/app/assets/javascripts/pages/projects/wikis/wikis.js new file mode 100644 index 00000000000..34a12ef76a1 --- /dev/null +++ b/app/assets/javascripts/pages/projects/wikis/wikis.js @@ -0,0 +1,60 @@ +import bp from '../../../breakpoints'; +import { slugify } from '../../../lib/utils/text_utility'; + +export default class Wikis { + constructor() { + this.sidebarEl = document.querySelector('.js-wiki-sidebar'); + this.sidebarExpanded = false; + + const sidebarToggles = document.querySelectorAll('.js-sidebar-wiki-toggle'); + for (let i = 0; i < sidebarToggles.length; i += 1) { + sidebarToggles[i].addEventListener('click', e => this.handleToggleSidebar(e)); + } + + this.newWikiForm = document.querySelector('form.new-wiki-page'); + if (this.newWikiForm) { + this.newWikiForm.addEventListener('submit', e => this.handleNewWikiSubmit(e)); + } + + window.addEventListener('resize', () => this.renderSidebar()); + this.renderSidebar(); + } + + handleNewWikiSubmit(e) { + if (!this.newWikiForm) return; + + const slugInput = this.newWikiForm.querySelector('#new_wiki_path'); + const slug = slugify(slugInput.value); + + if (slug.length > 0) { + const wikisPath = slugInput.getAttribute('data-wikis-path'); + window.location.href = `${wikisPath}/${slug}`; + e.preventDefault(); + } + } + + handleToggleSidebar(e) { + e.preventDefault(); + this.sidebarExpanded = !this.sidebarExpanded; + this.renderSidebar(); + } + + static sidebarCanCollapse() { + const bootstrapBreakpoint = bp.getBreakpointSize(); + return bootstrapBreakpoint === 'xs' || bootstrapBreakpoint === 'sm'; + } + + renderSidebar() { + if (!this.sidebarEl) return; + const { classList } = this.sidebarEl; + if (this.sidebarExpanded || !Wikis.sidebarCanCollapse()) { + if (!classList.contains('right-sidebar-expanded')) { + classList.remove('right-sidebar-collapsed'); + classList.add('right-sidebar-expanded'); + } + } else if (classList.contains('right-sidebar-expanded')) { + classList.add('right-sidebar-collapsed'); + classList.remove('right-sidebar-expanded'); + } + } +} diff --git a/app/assets/javascripts/pages/search/init_filtered_search.js b/app/assets/javascripts/pages/search/init_filtered_search.js new file mode 100644 index 00000000000..44853636aea --- /dev/null +++ b/app/assets/javascripts/pages/search/init_filtered_search.js @@ -0,0 +1,7 @@ +export default (page) => { + const filteredSearchEnabled = gl.FilteredSearchManager && document.querySelector('.filtered-search'); + if (filteredSearchEnabled) { + const filteredSearchManager = new gl.FilteredSearchManager(page); + filteredSearchManager.setup(); + } +}; diff --git a/app/assets/javascripts/pages/search/show/search.js b/app/assets/javascripts/pages/search/show/search.js index d44195f6b72..dc621bc87c0 100644 --- a/app/assets/javascripts/pages/search/show/search.js +++ b/app/assets/javascripts/pages/search/show/search.js @@ -15,6 +15,7 @@ export default class Search { $groupDropdown.glDropdown({ selectable: true, filterable: true, + filterRemote: true, fieldName: 'group_id', search: { fields: ['full_name'], @@ -43,6 +44,7 @@ export default class Search { $projectDropdown.glDropdown({ selectable: true, filterable: true, + filterRemote: true, fieldName: 'project_id', search: { fields: ['name'], diff --git a/app/assets/javascripts/pages/sessions/index.js b/app/assets/javascripts/pages/sessions/index.js new file mode 100644 index 00000000000..54f4e56359a --- /dev/null +++ b/app/assets/javascripts/pages/sessions/index.js @@ -0,0 +1,5 @@ +import initU2F from '../../shared/sessions/u2f'; + +export default () => { + initU2F(); +}; diff --git a/app/assets/javascripts/pages/sessions/new/signin_tabs_memoizer.js b/app/assets/javascripts/pages/sessions/new/signin_tabs_memoizer.js index f99573e5c74..08f0afdcce3 100644 --- a/app/assets/javascripts/pages/sessions/new/signin_tabs_memoizer.js +++ b/app/assets/javascripts/pages/sessions/new/signin_tabs_memoizer.js @@ -1,5 +1,3 @@ -/* eslint no-param-reassign: ["error", { "props": false }]*/ -/* eslint no-new: "off" */ import AccessorUtilities from '~/lib/utils/accessor'; /** @@ -11,6 +9,10 @@ export default class SigninTabsMemoizer { this.currentTabKey = currentTabKey; this.tabSelector = tabSelector; this.isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe(); + // sets selected tab if given as hash tag + if (window.location.hash) { + this.saveData(window.location.hash); + } this.bootstrap(); } |