diff options
Diffstat (limited to 'app')
97 files changed, 887 insertions, 371 deletions
diff --git a/app/assets/javascripts/boards/components/board_blank_state.vue b/app/assets/javascripts/boards/components/board_blank_state.vue index 47a46502bff..1cbd31729cd 100644 --- a/app/assets/javascripts/boards/components/board_blank_state.vue +++ b/app/assets/javascripts/boards/components/board_blank_state.vue @@ -1,6 +1,5 @@ <script> /* global ListLabel */ -import _ from 'underscore'; import Cookies from 'js-cookie'; import boardsStore from '../stores/boards_store'; @@ -29,8 +28,6 @@ export default { }); }); - boardsStore.state.lists = _.sortBy(boardsStore.state.lists, 'position'); - // Save the labels gl.boardService .generateDefaultLists() diff --git a/app/assets/javascripts/boards/components/sidebar/remove_issue.vue b/app/assets/javascripts/boards/components/sidebar/remove_issue.vue index a2b8a0af236..4ab2b17301f 100644 --- a/app/assets/javascripts/boards/components/sidebar/remove_issue.vue +++ b/app/assets/javascripts/boards/components/sidebar/remove_issue.vue @@ -48,7 +48,7 @@ export default Vue.extend({ list.removeIssue(issue); }); - boardsStore.detail.issue = {}; + boardsStore.clearDetailIssue(); }, /** * Build the default patch request. diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js index 4995a8d9367..bc6a3cf212e 100644 --- a/app/assets/javascripts/boards/index.js +++ b/app/assets/javascripts/boards/index.js @@ -1,5 +1,4 @@ import $ from 'jquery'; -import _ from 'underscore'; import Vue from 'vue'; import Flash from '~/flash'; @@ -106,18 +105,23 @@ export default () => { gl.boardService .all() .then(res => res.data) - .then(data => { - data.forEach(board => { - const list = boardsStore.addList(board, this.defaultAvatar); - - if (list.type === 'closed') { - list.position = Infinity; - } else if (list.type === 'backlog') { - list.position = -1; + .then(lists => { + lists.forEach(listObj => { + let { position } = listObj; + if (listObj.list_type === 'closed') { + position = Infinity; + } else if (listObj.list_type === 'backlog') { + position = -1; } - }); - this.state.lists = _.sortBy(this.state.lists, 'position'); + boardsStore.addList( + { + ...listObj, + position, + }, + this.defaultAvatar, + ); + }); boardsStore.addBlankState(); this.loading = false; @@ -167,7 +171,7 @@ export default () => { boardsStore.detail.issue = newIssue; }, clearDetailIssue() { - boardsStore.detail.issue = {}; + boardsStore.clearDetailIssue(); }, toggleSubscription(id) { const { issue } = boardsStore.detail; diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js index 70861fbf2b3..05efcbaa3cc 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js +++ b/app/assets/javascripts/boards/stores/boards_store.js @@ -45,7 +45,7 @@ const boardsStore = { }, addList(listObj, defaultAvatar) { const list = new List(listObj, defaultAvatar); - this.state.lists.push(list); + this.state.lists = _.sortBy([...this.state.lists, list], 'position'); return list; }, @@ -82,8 +82,6 @@ const boardsStore = { title: __('Welcome to your Issue Board!'), position: 0, }); - - this.state.lists = _.sortBy(this.state.lists, 'position'); }, removeBlankState() { this.removeList('blank'); @@ -188,6 +186,10 @@ const boardsStore = { updateFiltersUrl() { window.history.pushState(null, null, `?${this.filter.path}`); }, + + clearDetailIssue() { + this.detail.issue = {}; + }, }; BoardsStoreEE.initEESpecific(boardsStore); diff --git a/app/assets/javascripts/commons/polyfills.js b/app/assets/javascripts/commons/polyfills.js index 9216d4ab372..d0cc4897aeb 100644 --- a/app/assets/javascripts/commons/polyfills.js +++ b/app/assets/javascripts/commons/polyfills.js @@ -1,20 +1,21 @@ // ECMAScript polyfills -import 'core-js/fn/array/fill'; -import 'core-js/fn/array/find'; -import 'core-js/fn/array/find-index'; -import 'core-js/fn/array/from'; -import 'core-js/fn/array/includes'; -import 'core-js/fn/object/assign'; -import 'core-js/fn/object/values'; -import 'core-js/fn/object/entries'; -import 'core-js/fn/promise'; -import 'core-js/fn/promise/finally'; -import 'core-js/fn/string/code-point-at'; -import 'core-js/fn/string/from-code-point'; -import 'core-js/fn/string/includes'; -import 'core-js/fn/symbol'; -import 'core-js/es6/map'; -import 'core-js/es6/weak-map'; +import 'core-js/es/array/fill'; +import 'core-js/es/array/find'; +import 'core-js/es/array/find-index'; +import 'core-js/es/array/from'; +import 'core-js/es/array/includes'; +import 'core-js/es/object/assign'; +import 'core-js/es/object/values'; +import 'core-js/es/object/entries'; +import 'core-js/es/promise'; +import 'core-js/es/promise/finally'; +import 'core-js/es/string/code-point-at'; +import 'core-js/es/string/from-code-point'; +import 'core-js/es/string/includes'; +import 'core-js/es/symbol'; +import 'core-js/es/map'; +import 'core-js/es/weak-map'; +import 'core-js/modules/web.url'; // Browser polyfills import 'formdata-polyfill'; diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue index 392de1c9f23..d26b58d461a 100644 --- a/app/assets/javascripts/diffs/components/diff_file_header.vue +++ b/app/assets/javascripts/diffs/components/diff_file_header.vue @@ -240,7 +240,7 @@ export default { css-class="btn-default btn-transparent btn-clipboard" /> - <small v-if="isModeChanged" ref="fileMode"> + <small v-if="isModeChanged" ref="fileMode" class="mr-1"> {{ diffFile.a_mode }} → {{ diffFile.b_mode }} </small> diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index 6a4c1aab308..a143d79097b 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -335,6 +335,10 @@ GitLabDropdown = (function() { _this.fullData = data; _this.parseData(_this.fullData); _this.focusTextInput(); + + // Update dropdown position since remote data may have changed dropdown size + _this.dropdown.find('.dropdown-menu-toggle').dropdown('update'); + if ( _this.options.filterable && _this.filter && diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue index 9894ebb0624..e41b1530226 100644 --- a/app/assets/javascripts/ide/components/ide.vue +++ b/app/assets/javascripts/ide/components/ide.vue @@ -1,6 +1,7 @@ <script> import Vue from 'vue'; import { mapActions, mapState, mapGetters } from 'vuex'; +import { GlButton, GlLoadingIcon } from '@gitlab/ui'; import { __ } from '~/locale'; import FindFile from '~/vue_shared/components/file_finder/index.vue'; import NewModal from './new_dropdown/modal.vue'; @@ -22,6 +23,8 @@ export default { FindFile, ErrorMessage, CommitEditorHeader, + GlButton, + GlLoadingIcon, }, props: { rightPaneComponent: { @@ -47,13 +50,15 @@ export default { 'someUncommittedChanges', 'isCommitModeActive', 'allBlobs', + 'emptyRepo', + 'currentTree', ]), }, mounted() { window.onbeforeunload = e => this.onBeforeUnload(e); }, methods: { - ...mapActions(['toggleFileFinder']), + ...mapActions(['toggleFileFinder', 'openNewEntryModal']), onBeforeUnload(e = {}) { const returnValue = __('Are you sure you want to lose unsaved changes?'); @@ -98,17 +103,40 @@ export default { <repo-editor :file="activeFile" class="multi-file-edit-pane-content" /> </template> <template v-else> - <div v-once class="ide-empty-state"> + <div class="ide-empty-state"> <div class="row js-empty-state"> <div class="col-12"> <div class="svg-content svg-250"><img :src="emptyStateSvgPath" /></div> </div> <div class="col-12"> <div class="text-content text-center"> - <h4>Welcome to the GitLab IDE</h4> - <p> - Select a file from the left sidebar to begin editing. Afterwards, you'll be able - to commit your changes. + <h4> + {{ __('Make and review changes in the browser with the Web IDE') }} + </h4> + <template v-if="emptyRepo"> + <p> + {{ + __( + "Create a new file as there are no files yet. Afterwards, you'll be able to commit your changes.", + ) + }} + </p> + <gl-button + variant="success" + :title="__('New file')" + :aria-label="__('New file')" + @click="openNewEntryModal({ type: 'blob' })" + > + {{ __('New file') }} + </gl-button> + </template> + <gl-loading-icon v-else-if="!currentTree || currentTree.loading" size="md" /> + <p v-else> + {{ + __( + "Select a file from the left sidebar to begin editing. Afterwards, you'll be able to commit your changes.", + ) + }} </p> </div> </div> diff --git a/app/assets/javascripts/ide/components/ide_tree_list.vue b/app/assets/javascripts/ide/components/ide_tree_list.vue index 81374f26645..95782b2c88a 100644 --- a/app/assets/javascripts/ide/components/ide_tree_list.vue +++ b/app/assets/javascripts/ide/components/ide_tree_list.vue @@ -54,14 +54,17 @@ export default { <slot name="header"></slot> </header> <div class="ide-tree-body h-100"> - <file-row - v-for="file in currentTree.tree" - :key="file.key" - :file="file" - :level="0" - :extra-component="$options.FileRowExtra" - @toggleTreeOpen="toggleTreeOpen" - /> + <template v-if="currentTree.tree.length"> + <file-row + v-for="file in currentTree.tree" + :key="file.key" + :file="file" + :level="0" + :extra-component="$options.FileRowExtra" + @toggleTreeOpen="toggleTreeOpen" + /> + </template> + <div v-else class="file-row">{{ __('No files') }}</div> </div> </template> </div> diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js index fd678e6e10c..dc8ca732879 100644 --- a/app/assets/javascripts/ide/stores/actions.js +++ b/app/assets/javascripts/ide/stores/actions.js @@ -1,12 +1,15 @@ import $ from 'jquery'; import Vue from 'vue'; +import { __, sprintf } from '~/locale'; import { visitUrl } from '~/lib/utils/url_utility'; import flash from '~/flash'; +import _ from 'underscore'; import * as types from './mutation_types'; import { decorateFiles } from '../lib/files'; import { stageKeys } from '../constants'; +import service from '../services'; -export const redirectToUrl = (_, url) => visitUrl(url); +export const redirectToUrl = (self, url) => visitUrl(url); export const setInitialData = ({ commit }, data) => commit(types.SET_INITIAL_DATA, data); @@ -239,6 +242,53 @@ export const renameEntry = ( } }; +export const getBranchData = ({ commit, state }, { projectId, branchId, force = false } = {}) => + new Promise((resolve, reject) => { + const currentProject = state.projects[projectId]; + if (!currentProject || !currentProject.branches[branchId] || force) { + service + .getBranchData(projectId, branchId) + .then(({ data }) => { + const { id } = data.commit; + commit(types.SET_BRANCH, { + projectPath: projectId, + branchName: branchId, + branch: data, + }); + commit(types.SET_BRANCH_WORKING_REFERENCE, { projectId, branchId, reference: id }); + resolve(data); + }) + .catch(e => { + if (e.response.status === 404) { + reject(e); + } else { + flash( + __('Error loading branch data. Please try again.'), + 'alert', + document, + null, + false, + true, + ); + + reject( + new Error( + sprintf( + __('Branch not loaded - %{branchId}'), + { + branchId: `<strong>${_.escape(projectId)}/${_.escape(branchId)}</strong>`, + }, + false, + ), + ), + ); + } + }); + } else { + resolve(currentProject.branches[branchId]); + } + }); + export * from './actions/tree'; export * from './actions/file'; export * from './actions/project'; diff --git a/app/assets/javascripts/ide/stores/actions/project.js b/app/assets/javascripts/ide/stores/actions/project.js index 4b10d148ebf..dd8f17e4f3a 100644 --- a/app/assets/javascripts/ide/stores/actions/project.js +++ b/app/assets/javascripts/ide/stores/actions/project.js @@ -35,48 +35,6 @@ export const getProjectData = ({ commit, state }, { namespace, projectId, force } }); -export const getBranchData = ( - { commit, dispatch, state }, - { projectId, branchId, force = false } = {}, -) => - new Promise((resolve, reject) => { - if ( - typeof state.projects[`${projectId}`] === 'undefined' || - !state.projects[`${projectId}`].branches[branchId] || - force - ) { - service - .getBranchData(`${projectId}`, branchId) - .then(({ data }) => { - const { id } = data.commit; - commit(types.SET_BRANCH, { - projectPath: `${projectId}`, - branchName: branchId, - branch: data, - }); - commit(types.SET_BRANCH_WORKING_REFERENCE, { projectId, branchId, reference: id }); - resolve(data); - }) - .catch(e => { - if (e.response.status === 404) { - dispatch('showBranchNotFoundError', branchId); - } else { - flash( - __('Error loading branch data. Please try again.'), - 'alert', - document, - null, - false, - true, - ); - } - reject(new Error(`Branch not loaded - ${projectId}/${branchId}`)); - }); - } else { - resolve(state.projects[`${projectId}`].branches[branchId]); - } - }); - export const refreshLastCommitData = ({ commit }, { projectId, branchId } = {}) => service .getBranchData(projectId, branchId) @@ -125,40 +83,66 @@ export const showBranchNotFoundError = ({ dispatch }, branchId) => { }); }; -export const openBranch = ({ dispatch, state }, { projectId, branchId, basePath }) => { - dispatch('setCurrentBranchId', branchId); - - dispatch('getBranchData', { - projectId, - branchId, +export const showEmptyState = ({ commit, state }, { projectId, branchId }) => { + const treePath = `${projectId}/${branchId}`; + commit(types.CREATE_TREE, { treePath }); + commit(types.TOGGLE_LOADING, { + entry: state.trees[treePath], + forceValue: false, }); +}; - return dispatch('getFiles', { +export const openBranch = ({ dispatch, state, getters }, { projectId, branchId, basePath }) => { + dispatch('setCurrentBranchId', branchId); + + if (getters.emptyRepo) { + return dispatch('showEmptyState', { projectId, branchId }); + } + return dispatch('getBranchData', { projectId, branchId, }) .then(() => { - if (basePath) { - const path = basePath.slice(-1) === '/' ? basePath.slice(0, -1) : basePath; - const treeEntryKey = Object.keys(state.entries).find( - key => key === path && !state.entries[key].pending, - ); - const treeEntry = state.entries[treeEntryKey]; - - if (treeEntry) { - dispatch('handleTreeEntryAction', treeEntry); - } else { - dispatch('createTempEntry', { - name: path, - type: 'blob', - }); - } - } - }) - .then(() => { dispatch('getMergeRequestsForBranch', { projectId, branchId, }); + dispatch('getFiles', { + projectId, + branchId, + }) + .then(() => { + if (basePath) { + const path = basePath.slice(-1) === '/' ? basePath.slice(0, -1) : basePath; + const treeEntryKey = Object.keys(state.entries).find( + key => key === path && !state.entries[key].pending, + ); + const treeEntry = state.entries[treeEntryKey]; + + if (treeEntry) { + dispatch('handleTreeEntryAction', treeEntry); + } else { + dispatch('createTempEntry', { + name: path, + type: 'blob', + }); + } + } + }) + .catch( + () => + new Error( + sprintf( + __('An error occurred whilst getting files for - %{branchId}'), + { + branchId: `<strong>${_.escape(projectId)}/${_.escape(branchId)}</strong>`, + }, + false, + ), + ), + ); + }) + .catch(() => { + dispatch('showBranchNotFoundError', branchId); }); }; diff --git a/app/assets/javascripts/ide/stores/actions/tree.js b/app/assets/javascripts/ide/stores/actions/tree.js index 3d83e4a9ba5..75511574d3e 100644 --- a/app/assets/javascripts/ide/stores/actions/tree.js +++ b/app/assets/javascripts/ide/stores/actions/tree.js @@ -74,17 +74,13 @@ export const getFiles = ({ state, commit, dispatch }, { projectId, branchId } = resolve(); }) .catch(e => { - if (e.response.status === 404) { - dispatch('showBranchNotFoundError', branchId); - } else { - dispatch('setErrorMessage', { - text: __('An error occurred whilst loading all the files.'), - action: payload => - dispatch('getFiles', payload).then(() => dispatch('setErrorMessage', null)), - actionText: __('Please try again'), - actionPayload: { projectId, branchId }, - }); - } + dispatch('setErrorMessage', { + text: __('An error occurred whilst loading all the files.'), + action: payload => + dispatch('getFiles', payload).then(() => dispatch('setErrorMessage', null)), + actionText: __('Please try again'), + actionPayload: { projectId, branchId }, + }); reject(e); }); } else { diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js index 490658a4543..7a88ac5b116 100644 --- a/app/assets/javascripts/ide/stores/getters.js +++ b/app/assets/javascripts/ide/stores/getters.js @@ -36,6 +36,9 @@ export const currentMergeRequest = state => { export const currentProject = state => state.projects[state.currentProjectId]; +export const emptyRepo = state => + state.projects[state.currentProjectId] && state.projects[state.currentProjectId].empty_repo; + export const currentTree = state => state.trees[`${state.currentProjectId}/${state.currentBranchId}`]; diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js index c2760eb1554..f81bdb8a30e 100644 --- a/app/assets/javascripts/ide/stores/modules/commit/actions.js +++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js @@ -135,6 +135,17 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo return null; } + if (!data.parent_ids.length) { + commit( + rootTypes.TOGGLE_EMPTY_STATE, + { + projectPath: rootState.currentProjectId, + value: false, + }, + { root: true }, + ); + } + dispatch('setLastCommitMessage', data); dispatch('updateCommitMessage', ''); return dispatch('updateFilesAfterCommit', { diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js index a5f8098dc17..86ab76136df 100644 --- a/app/assets/javascripts/ide/stores/mutation_types.js +++ b/app/assets/javascripts/ide/stores/mutation_types.js @@ -12,6 +12,7 @@ export const SET_LINKS = 'SET_LINKS'; export const SET_PROJECT = 'SET_PROJECT'; export const SET_CURRENT_PROJECT = 'SET_CURRENT_PROJECT'; export const TOGGLE_PROJECT_OPEN = 'TOGGLE_PROJECT_OPEN'; +export const TOGGLE_EMPTY_STATE = 'TOGGLE_EMPTY_STATE'; // Merge Request Mutation Types export const SET_MERGE_REQUEST = 'SET_MERGE_REQUEST'; diff --git a/app/assets/javascripts/ide/stores/mutations/branch.js b/app/assets/javascripts/ide/stores/mutations/branch.js index e09f88878f4..6afd8de2aa4 100644 --- a/app/assets/javascripts/ide/stores/mutations/branch.js +++ b/app/assets/javascripts/ide/stores/mutations/branch.js @@ -19,6 +19,12 @@ export default { }); }, [types.SET_BRANCH_WORKING_REFERENCE](state, { projectId, branchId, reference }) { + if (!state.projects[projectId].branches[branchId]) { + Object.assign(state.projects[projectId].branches, { + [branchId]: {}, + }); + } + Object.assign(state.projects[projectId].branches[branchId], { workingReference: reference, }); diff --git a/app/assets/javascripts/ide/stores/mutations/project.js b/app/assets/javascripts/ide/stores/mutations/project.js index 284b39a2c72..9230f3839c1 100644 --- a/app/assets/javascripts/ide/stores/mutations/project.js +++ b/app/assets/javascripts/ide/stores/mutations/project.js @@ -21,4 +21,9 @@ export default { }), }); }, + [types.TOGGLE_EMPTY_STATE](state, { projectPath, value }) { + Object.assign(state.projects[projectPath], { + empty_repo: value, + }); + }, }; diff --git a/app/assets/javascripts/jobs/components/stages_dropdown.vue b/app/assets/javascripts/jobs/components/stages_dropdown.vue index 6e92b599b0a..cb073a9b04d 100644 --- a/app/assets/javascripts/jobs/components/stages_dropdown.vue +++ b/app/assets/javascripts/jobs/components/stages_dropdown.vue @@ -2,6 +2,7 @@ import _ from 'underscore'; import { GlLink } from '@gitlab/ui'; import CiIcon from '~/vue_shared/components/ci_icon.vue'; +import PipelineLink from '~/vue_shared/components/ci_pipeline_link.vue'; import Icon from '~/vue_shared/components/icon.vue'; export default { @@ -9,6 +10,7 @@ export default { CiIcon, Icon, GlLink, + PipelineLink, }, props: { pipeline: { @@ -48,9 +50,12 @@ export default { <ci-icon :status="pipeline.details.status" class="vertical-align-middle" /> <span class="font-weight-bold">{{ s__('Job|Pipeline') }}</span> - <gl-link :href="pipeline.path" class="js-pipeline-path link-commit qa-pipeline-path" - >#{{ pipeline.id }}</gl-link - > + <pipeline-link + :href="pipeline.path" + :pipeline-id="pipeline.id" + :pipeline-iid="pipeline.iid" + class="js-pipeline-path link-commit qa-pipeline-path" + /> <template v-if="hasRef"> {{ s__('Job|for') }} diff --git a/app/assets/javascripts/lib/graphql.js b/app/assets/javascripts/lib/graphql.js index 47e91dedd5a..5857f9e22ae 100644 --- a/app/assets/javascripts/lib/graphql.js +++ b/app/assets/javascripts/lib/graphql.js @@ -1,6 +1,8 @@ import { ApolloClient } from 'apollo-client'; import { InMemoryCache } from 'apollo-cache-inmemory'; import { createUploadLink } from 'apollo-upload-client'; +import { ApolloLink } from 'apollo-link'; +import { BatchHttpLink } from 'apollo-link-batch-http'; import csrf from '~/lib/utils/csrf'; export default (resolvers = {}, config = {}) => { @@ -11,13 +13,19 @@ export default (resolvers = {}, config = {}) => { uri = `${config.baseUrl}${uri}`.replace(/\/{3,}/g, '/'); } + const httpOptions = { + uri, + headers: { + [csrf.headerKey]: csrf.token, + }, + }; + return new ApolloClient({ - link: createUploadLink({ - uri, - headers: { - [csrf.headerKey]: csrf.token, - }, - }), + link: ApolloLink.split( + operation => operation.getContext().hasUpload, + createUploadLink(httpOptions), + new BatchHttpLink(httpOptions), + ), cache: new InMemoryCache(config.cacheConfig), resolvers, }); diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index b236daff1e0..cc5e12aa467 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -94,6 +94,8 @@ export const handleLocationHash = () => { const fixedNav = document.querySelector('.navbar-gitlab'); const performanceBar = document.querySelector('#js-peek'); const topPadding = 8; + const diffFileHeader = document.querySelector('.js-file-title'); + const versionMenusContainer = document.querySelector('.mr-version-menus-container'); let adjustment = 0; if (fixedNav) adjustment -= fixedNav.offsetHeight; @@ -114,6 +116,14 @@ export const handleLocationHash = () => { adjustment -= performanceBar.offsetHeight; } + if (diffFileHeader) { + adjustment -= diffFileHeader.offsetHeight; + } + + if (versionMenusContainer) { + adjustment -= versionMenusContainer.offsetHeight; + } + if (isInMRPage()) { adjustment -= topPadding; } diff --git a/app/assets/javascripts/notes/components/discussion_counter.vue b/app/assets/javascripts/notes/components/discussion_counter.vue index c7cfc0f0f3b..307e56708e0 100644 --- a/app/assets/javascripts/notes/components/discussion_counter.vue +++ b/app/assets/javascripts/notes/components/discussion_counter.vue @@ -57,7 +57,7 @@ export default { class="line-resolve-btn is-disabled" type="button" > - <icon name="check-circle" /> + <icon :name="allResolved ? 'check-circle-filled' : 'check-circle'" /> </span> <span class="line-resolve-text"> {{ resolvedDiscussionsCount }}/{{ resolvableDiscussionsCount }} diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue index 5a4ff15d198..78f397ccc12 100644 --- a/app/assets/javascripts/notes/components/note_actions.vue +++ b/app/assets/javascripts/notes/components/note_actions.vue @@ -135,7 +135,7 @@ export default { @click="onResolve" > <template v-if="!isResolving"> - <icon name="check-circle" /> + <icon :name="isResolved ? 'check-circle-filled' : 'check-circle'" /> </template> <gl-loading-icon v-else inline /> </button> diff --git a/app/assets/javascripts/pipelines/components/header_component.vue b/app/assets/javascripts/pipelines/components/header_component.vue index b2e365e5cde..f3a71ee434c 100644 --- a/app/assets/javascripts/pipelines/components/header_component.vue +++ b/app/assets/javascripts/pipelines/components/header_component.vue @@ -83,6 +83,8 @@ export default { v-if="shouldRenderContent" :status="status" :item-id="pipeline.id" + :item-iid="pipeline.iid" + :item-id-tooltip="__('Pipeline ID (IID)')" :time="pipeline.created_at" :user="pipeline.user" :actions="actions" diff --git a/app/assets/javascripts/pipelines/components/pipeline_url.vue b/app/assets/javascripts/pipelines/components/pipeline_url.vue index c41ecab1294..00c02e15562 100644 --- a/app/assets/javascripts/pipelines/components/pipeline_url.vue +++ b/app/assets/javascripts/pipelines/components/pipeline_url.vue @@ -2,6 +2,7 @@ import { GlLink, GlTooltipDirective } from '@gitlab/ui'; import _ from 'underscore'; import { __, sprintf } from '~/locale'; +import PipelineLink from '~/vue_shared/components/ci_pipeline_link.vue'; import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; import popover from '~/vue_shared/directives/popover'; @@ -19,6 +20,7 @@ export default { components: { UserAvatarLink, GlLink, + PipelineLink, }, directives: { GlTooltip: GlTooltipDirective, @@ -59,10 +61,13 @@ export default { }; </script> <template> - <div class="table-section section-10 d-none d-sm-none d-md-block pipeline-tags"> - <gl-link :href="pipeline.path" class="js-pipeline-url-link"> - <span class="pipeline-id">#{{ pipeline.id }}</span> - </gl-link> + <div class="table-section section-10 d-none d-sm-none d-md-block pipeline-tags section-wrap"> + <pipeline-link + :href="pipeline.path" + :pipeline-id="pipeline.id" + :pipeline-iid="pipeline.iid" + class="js-pipeline-url-link" + /> <div class="label-container"> <span v-if="pipeline.flags.latest" diff --git a/app/assets/javascripts/repository/components/breadcrumbs.vue b/app/assets/javascripts/repository/components/breadcrumbs.vue new file mode 100644 index 00000000000..6eca015036f --- /dev/null +++ b/app/assets/javascripts/repository/components/breadcrumbs.vue @@ -0,0 +1,61 @@ +<script> +import getRefMixin from '../mixins/get_ref'; +import getProjectShortPath from '../queries/getProjectShortPath.graphql'; + +export default { + apollo: { + projectShortPath: { + query: getProjectShortPath, + }, + }, + mixins: [getRefMixin], + props: { + currentPath: { + type: String, + required: false, + default: '/', + }, + }, + data() { + return { + projectShortPath: '', + }; + }, + computed: { + pathLinks() { + return this.currentPath + .split('/') + .filter(p => p !== '') + .reduce( + (acc, name, i) => { + const path = `${i > 0 ? acc[i].path : ''}/${name}`; + + return acc.concat({ + name, + path, + to: `/tree/${this.ref}${path}`, + }); + }, + [{ name: this.projectShortPath, path: '/', to: `/tree/${this.ref}` }], + ); + }, + }, + methods: { + isLast(i) { + return i === this.pathLinks.length - 1; + }, + }, +}; +</script> + +<template> + <nav :aria-label="__('Files breadcrumb')"> + <ol class="breadcrumb repo-breadcrumb"> + <li v-for="(link, i) in pathLinks" :key="i" class="breadcrumb-item"> + <router-link :to="link.to" :aria-current="isLast(i) ? 'page' : null"> + {{ link.name }} + </router-link> + </li> + </ol> + </nav> +</template> diff --git a/app/assets/javascripts/repository/components/table/parent_row.vue b/app/assets/javascripts/repository/components/table/parent_row.vue index b4433f00d8a..3c39f404226 100644 --- a/app/assets/javascripts/repository/components/table/parent_row.vue +++ b/app/assets/javascripts/repository/components/table/parent_row.vue @@ -27,8 +27,8 @@ export default { </script> <template> - <tr v-once @click="clickRow"> - <td colspan="3" class="tree-item-file-name"> + <tr class="tree-item"> + <td colspan="3" class="tree-item-file-name" @click.self="clickRow"> <router-link :to="parentRoute" :aria-label="__('Go to parent')"> .. </router-link> diff --git a/app/assets/javascripts/repository/index.js b/app/assets/javascripts/repository/index.js index f992d4b6d54..52f53be045b 100644 --- a/app/assets/javascripts/repository/index.js +++ b/app/assets/javascripts/repository/index.js @@ -1,24 +1,27 @@ import Vue from 'vue'; import createRouter from './router'; import App from './components/app.vue'; +import Breadcrumbs from './components/breadcrumbs.vue'; import apolloProvider from './graphql'; import { setTitle } from './utils/title'; export default function setupVueRepositoryList() { const el = document.getElementById('js-tree-list'); - const { projectPath, ref, fullName } = el.dataset; + const { projectPath, projectShortPath, ref, fullName } = el.dataset; const router = createRouter(projectPath, ref); apolloProvider.clients.defaultClient.cache.writeData({ data: { projectPath, + projectShortPath, ref, }, }); - router.afterEach(({ params: { pathMatch } }) => setTitle(pathMatch, ref, fullName)); - router.afterEach(to => { - const isRoot = to.params.pathMatch === undefined || to.params.pathMatch === '/'; + router.afterEach(({ params: { pathMatch } }) => { + const isRoot = pathMatch === undefined || pathMatch === '/'; + + setTitle(pathMatch, ref, fullName); if (!isRoot) { document @@ -31,6 +34,20 @@ export default function setupVueRepositoryList() { .forEach(elem => elem.classList.toggle('hidden', !isRoot)); }); + // eslint-disable-next-line no-new + new Vue({ + el: document.getElementById('js-repo-breadcrumb'), + router, + apolloProvider, + render(h) { + return h(Breadcrumbs, { + props: { + currentPath: this.$route.params.pathMatch, + }, + }); + }, + }); + return new Vue({ el, router, diff --git a/app/assets/javascripts/repository/queries/getProjectShortPath.graphql b/app/assets/javascripts/repository/queries/getProjectShortPath.graphql new file mode 100644 index 00000000000..34eb26598c2 --- /dev/null +++ b/app/assets/javascripts/repository/queries/getProjectShortPath.graphql @@ -0,0 +1,3 @@ +query getProjectShortPath { + projectShortPath @client +} diff --git a/app/assets/javascripts/repository/utils/title.js b/app/assets/javascripts/repository/utils/title.js index 3c3e918c0a8..4e194640e92 100644 --- a/app/assets/javascripts/repository/utils/title.js +++ b/app/assets/javascripts/repository/utils/title.js @@ -1,5 +1,7 @@ // eslint-disable-next-line import/prefer-default-export export const setTitle = (pathMatch, ref, project) => { + if (!pathMatch) return; + const path = pathMatch.replace(/^\//, ''); const isEmpty = path === ''; diff --git a/app/assets/javascripts/test_utils/index.js b/app/assets/javascripts/test_utils/index.js index a55a338eea8..1e75ee60671 100644 --- a/app/assets/javascripts/test_utils/index.js +++ b/app/assets/javascripts/test_utils/index.js @@ -1,5 +1,5 @@ -import 'core-js/es6/map'; -import 'core-js/es6/set'; +import 'core-js/es/map'; +import 'core-js/es/set'; import simulateDrag from './simulate_drag'; import simulateInput from './simulate_input'; diff --git a/app/assets/javascripts/visual_review_toolbar/index.js b/app/assets/javascripts/visual_review_toolbar/index.js new file mode 100644 index 00000000000..91d0382feac --- /dev/null +++ b/app/assets/javascripts/visual_review_toolbar/index.js @@ -0,0 +1,2 @@ +import './styles/toolbar.css'; +import 'vendor/visual_review_toolbar'; diff --git a/app/assets/javascripts/visual_review_toolbar/styles/toolbar.css b/app/assets/javascripts/visual_review_toolbar/styles/toolbar.css new file mode 100644 index 00000000000..342b3599a44 --- /dev/null +++ b/app/assets/javascripts/visual_review_toolbar/styles/toolbar.css @@ -0,0 +1,149 @@ +/* + As a standalone script, the toolbar has its own css + */ + +#gitlab-collapse > * { + pointer-events: none; +} + +#gitlab-form-wrapper { + display: flex; + flex-direction: column; + width: 100% +} + +#gitlab-review-container { + max-width: 22rem; + max-height: 22rem; + overflow: scroll; + position: fixed; + bottom: 1rem; + right: 1rem; + display: flex; + flex-direction: row-reverse; + padding: 1rem; + background-color: #fff; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, + 'Helvetica Neue', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', + 'Noto Color Emoji'; + font-size: .8rem; + font-weight: 400; + color: #2e2e2e; +} + +.gitlab-open-wrapper { + max-width: 22rem; + max-height: 22rem; +} + +.gitlab-closed-wrapper { + max-width: 3.4rem; + max-height: 3.4rem; +} + +.gitlab-button { + cursor: pointer; + transition: background-color 100ms linear, border-color 100ms linear, color 100ms linear, box-shadow 100ms linear; +} + +.gitlab-button-secondary { + background: none #fff; + margin: 0 .5rem; + border: 1px solid #e3e3e3; +} + +.gitlab-button-secondary:hover { + background-color: #f0f0f0; + border-color: #e3e3e3; + color: #2e2e2e; +} + +.gitlab-button-secondary:active { + color: #2e2e2e; + background-color: #e1e1e1; + border-color: #dadada; +} + +.gitlab-button-success:hover { + color: #fff; + background-color: #137e3f; + border-color: #127339; +} + +.gitlab-button-success:active { + background-color: #168f48; + border-color: #12753a; + color: #fff; +} + +.gitlab-button-success { + background-color: #1aaa55; + border: 1px solid #168f48; + color: #fff; +} + +.gitlab-button-wide { + width: 100%; +} + +.gitlab-button-wrapper { + margin-top: 1rem; + display: flex; + align-items: baseline; + justify-content: flex-end; +} + +.gitlab-collapse { + width: 2.4rem; + height: 2.2rem; + margin-left: 1rem; + padding: .5rem; +} + +.gitlab-collapse-closed { + align-self: center; +} + +.gitlab-checkbox-label { + padding: 0 .2rem; +} + +.gitlab-checkbox-wrapper { + display: flex; + align-items: baseline; +} + +.gitlab-label { + font-weight: 600; + display: inline-block; + width: 100%; +} + +.gitlab-link { + color: #1b69b6; + text-decoration: none; + background-color: transparent; + background-image: none; +} + +.gitlab-message { + padding: .25rem 0; + margin: 0; + line-height: 1.2rem; +} + +.gitlab-metadata-note { + font-size: .7rem; + line-height: 1rem; + color: #666; + margin-bottom: 0; +} + +.gitlab-input { + width: 100%; + border: 1px solid #dfdfdf; + border-radius: 4px; + padding: .1rem .2rem; + min-height: 2rem; + max-width: 17rem; +} diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue index f5fa68308bc..c377c16fb13 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue @@ -5,6 +5,7 @@ import { sprintf, __ } from '~/locale'; import PipelineStage from '~/pipelines/components/stage.vue'; import CiIcon from '~/vue_shared/components/ci_icon.vue'; import Icon from '~/vue_shared/components/icon.vue'; +import PipelineLink from '~/vue_shared/components/ci_pipeline_link.vue'; import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue'; import mrWidgetPipelineMixin from 'ee_else_ce/vue_merge_request_widget/mixins/mr_widget_pipeline'; @@ -16,6 +17,7 @@ export default { Icon, TooltipOnTruncate, GlLink, + PipelineLink, LinkedPipelinesMiniList: () => import('ee_component/vue_shared/components/linked_pipelines_mini_list.vue'), }, @@ -112,9 +114,12 @@ export default { <div class="media-body"> <div class="font-weight-bold js-pipeline-info-container"> {{ s__('Pipeline|Pipeline') }} - <gl-link :href="pipeline.path" class="pipeline-id font-weight-normal pipeline-number" - >#{{ pipeline.id }}</gl-link - > + <pipeline-link + :href="pipeline.path" + :pipeline-id="pipeline.id" + :pipeline-iid="pipeline.iid" + class="pipeline-id pipeline-iid font-weight-normal" + /> {{ pipeline.details.status.label }} <template v-if="hasCommitInfo"> {{ s__('Pipeline|for') }} diff --git a/app/assets/javascripts/vue_shared/components/ci_pipeline_link.vue b/app/assets/javascripts/vue_shared/components/ci_pipeline_link.vue new file mode 100644 index 00000000000..eae4c06467c --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/ci_pipeline_link.vue @@ -0,0 +1,32 @@ +<script> +import { GlLink, GlTooltipDirective } from '@gitlab/ui'; + +export default { + components: { + GlLink, + }, + directives: { + GlTooltip: GlTooltipDirective, + }, + props: { + href: { + type: String, + required: true, + }, + pipelineId: { + type: Number, + required: true, + }, + pipelineIid: { + type: Number, + required: true, + }, + }, +}; +</script> +<template> + <gl-link v-gl-tooltip :href="href" :title="__('Pipeline ID (IID)')"> + <span class="pipeline-id">#{{ pipelineId }}</span> + <span class="pipeline-iid">(#{{ pipelineIid }})</span> + </gl-link> +</template> diff --git a/app/assets/javascripts/vue_shared/components/header_ci_component.vue b/app/assets/javascripts/vue_shared/components/header_ci_component.vue index 3f45dc7853b..0bac63b1062 100644 --- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue +++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue @@ -37,6 +37,16 @@ export default { type: Number, required: true, }, + itemIid: { + type: Number, + required: false, + default: null, + }, + itemIdTooltip: { + type: String, + required: false, + default: '', + }, time: { type: String, required: true, @@ -85,7 +95,12 @@ export default { <section class="header-main-content"> <ci-icon-badge :status="status" /> - <strong> {{ itemName }} #{{ itemId }} </strong> + <strong v-gl-tooltip :title="itemIdTooltip"> + {{ itemName }} #{{ itemId }} + <template v-if="itemIid" + >(#{{ itemIid }})</template + > + </strong> <template v-if="shouldRenderTriggeredLabel"> triggered @@ -96,9 +111,8 @@ export default { <timeago-tooltip :time="time" /> - by - <template v-if="user"> + by <gl-link v-gl-tooltip :href="user.path" diff --git a/app/assets/stylesheets/framework/awards.scss b/app/assets/stylesheets/framework/awards.scss index a8df7e1bfad..7760c48cb92 100644 --- a/app/assets/stylesheets/framework/awards.scss +++ b/app/assets/stylesheets/framework/awards.scss @@ -232,9 +232,6 @@ height: $default-icon-size; width: $default-icon-size; border-radius: 50%; - } - - path { fill: $gray-700; } } diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index 2c720703822..b85abfd9c14 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -339,6 +339,8 @@ svg { top: auto; + width: 16px; + height: 16px; } } diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 8fb4027bf97..cd951f67293 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -570,10 +570,10 @@ } .dropdown-menu-close { - right: 5px; + top: $gl-padding-4; + right: $gl-padding-8; width: 20px; height: 20px; - top: -1px; } .dropdown-menu-close-icon { diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index 17c117188b3..8493dfff1c5 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -366,10 +366,6 @@ span.idiff { color: $gl-text-color; } - small { - margin: 0 10px 0 0; - } - .file-actions .btn { padding: 0 10px; font-size: 13px; diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss index 4a9c73a1bc9..33930871cdc 100644 --- a/app/assets/stylesheets/framework/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -27,10 +27,16 @@ input[type='text'].danger { } label { + font-weight: $gl-font-weight-bold; + &.inline-label { margin: 0; } + &.form-check-label { + font-weight: $gl-font-weight-normal; + } + &.label-bold { font-weight: $gl-font-weight-bold; } diff --git a/app/assets/stylesheets/framework/snippets.scss b/app/assets/stylesheets/framework/snippets.scss index 36ab38f1c9d..3ab83f4c8e6 100644 --- a/app/assets/stylesheets/framework/snippets.scss +++ b/app/assets/stylesheets/framework/snippets.scss @@ -22,6 +22,10 @@ .snippet-file-content { border-radius: 3px; + + .file-title-flex-parent .btn-clipboard { + line-height: 28px; + } } .snippet-header { diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss index 13288d8bad1..11e8a32389f 100644 --- a/app/assets/stylesheets/pages/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss @@ -456,8 +456,9 @@ // Don't hide the overflow in system messages .system-note-message, -.issuable-detail, +.issuable-details, .md-preview-holder, +.referenced-commands, .note-body { .scoped-label-wrapper { .badge { diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 50c87e55f56..170432a9e62 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -822,6 +822,7 @@ $note-form-margin-left: 72px; .line-resolve-btn { margin-right: 5px; + color: $gray-darkest; svg { vertical-align: middle; @@ -836,7 +837,6 @@ $note-form-margin-left: 72px; background-color: transparent; border: 0; outline: 0; - color: $gray-darkest; transition: color $general-hover-transition-duration $general-hover-transition-curve; &.is-disabled { diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb index aeff0c96b64..70db15916b9 100644 --- a/app/controllers/admin/projects_controller.rb +++ b/app/controllers/admin/projects_controller.rb @@ -3,7 +3,7 @@ class Admin::ProjectsController < Admin::ApplicationController include MembersPresentation - before_action :project, only: [:show, :transfer, :repository_check] + before_action :project, only: [:show, :transfer, :repository_check, :destroy] before_action :group, only: [:show, :transfer] def index @@ -35,6 +35,15 @@ class Admin::ProjectsController < Admin::ApplicationController end # rubocop: enable CodeReuse/ActiveRecord + def destroy + ::Projects::DestroyService.new(@project, current_user, {}).async_execute + flash[:notice] = _("Project '%{project_name}' is in the process of being deleted.") % { project_name: @project.full_name } + + redirect_to admin_projects_path, status: :found + rescue Projects::DestroyService::DestroyError => ex + redirect_to admin_projects_path, status: 302, alert: ex.message + end + # rubocop: disable CodeReuse/ActiveRecord def transfer namespace = Namespace.find_by(id: params[:new_namespace_id]) diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb index 91e875dca54..9cf25915e92 100644 --- a/app/controllers/concerns/issuable_collections.rb +++ b/app/controllers/concerns/issuable_collections.rb @@ -41,6 +41,7 @@ module IssuableCollections return if pagination_disabled? @issuables = @issuables.page(params[:page]) + @issuables = per_page_for_relative_position if params[:sort] == 'relative_position' @issuable_meta_data = issuable_meta_data(@issuables, collection_type) @total_pages = issuable_page_count end @@ -80,6 +81,11 @@ module IssuableCollections (row_count.to_f / limit).ceil end + # manual / relative_position sorting allows for 100 items on the page + def per_page_for_relative_position + @issuables.per(100) # rubocop:disable Gitlab/ModuleWithInstanceVariables + end + def issuable_finder_for(finder_class) finder_class.new(current_user, finder_options) end diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb index e8f38899647..1ce0afac83b 100644 --- a/app/controllers/graphql_controller.rb +++ b/app/controllers/graphql_controller.rb @@ -53,7 +53,8 @@ class GraphqlController < ApplicationController { query: single_query_info[:query], variables: build_variables(single_query_info[:variables]), - operation_name: single_query_info[:operationName] + operation_name: single_query_info[:operationName], + context: context } end end diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index 7e631053b54..0d6a6496993 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -188,7 +188,7 @@ module BlobHelper end def copy_file_path_button(file_path) - clipboard_button(text: file_path, gfm: "`#{file_path}`", class: 'btn-clipboard btn-transparent prepend-left-5', title: 'Copy file path to clipboard') + clipboard_button(text: file_path, gfm: "`#{file_path}`", class: 'btn-clipboard btn-transparent', title: 'Copy file path to clipboard') end def copy_blob_source_button(blob) diff --git a/app/helpers/environments_helper.rb b/app/helpers/environments_helper.rb index 365b94f5a3e..8002eb08ada 100644 --- a/app/helpers/environments_helper.rb +++ b/app/helpers/environments_helper.rb @@ -30,7 +30,8 @@ module EnvironmentsHelper "environments-endpoint": project_environments_path(project, format: :json), "project-path" => project_path(project), "tags-path" => project_tags_path(project), - "has-metrics" => "#{environment.has_metrics?}" + "has-metrics" => "#{environment.has_metrics?}", + "external-dashboard-url" => project.metrics_setting_external_dashboard_url } end end diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb index acc8aeae282..db4f29cd996 100644 --- a/app/helpers/labels_helper.rb +++ b/app/helpers/labels_helper.rb @@ -76,29 +76,39 @@ module LabelsHelper end def suggested_colors - [ - '#0033CC', - '#428BCA', - '#44AD8E', - '#A8D695', - '#5CB85C', - '#69D100', - '#004E00', - '#34495E', - '#7F8C8D', - '#A295D6', - '#5843AD', - '#8E44AD', - '#FFECDB', - '#AD4363', - '#D10069', - '#CC0033', - '#FF0000', - '#D9534F', - '#D1D100', - '#F0AD4E', - '#AD8D43' - ] + { + '#0033CC' => s_('SuggestedColors|UA blue'), + '#428BCA' => s_('SuggestedColors|Moderate blue'), + '#44AD8E' => s_('SuggestedColors|Lime green'), + '#A8D695' => s_('SuggestedColors|Feijoa'), + '#5CB85C' => s_('SuggestedColors|Slightly desaturated green'), + '#69D100' => s_('SuggestedColors|Bright green'), + '#004E00' => s_('SuggestedColors|Very dark lime green'), + '#34495E' => s_('SuggestedColors|Very dark desaturated blue'), + '#7F8C8D' => s_('SuggestedColors|Dark grayish cyan'), + '#A295D6' => s_('SuggestedColors|Slightly desaturated blue'), + '#5843AD' => s_('SuggestedColors|Dark moderate blue'), + '#8E44AD' => s_('SuggestedColors|Dark moderate violet'), + '#FFECDB' => s_('SuggestedColors|Very pale orange'), + '#AD4363' => s_('SuggestedColors|Dark moderate pink'), + '#D10069' => s_('SuggestedColors|Strong pink'), + '#CC0033' => s_('SuggestedColors|Strong red'), + '#FF0000' => s_('SuggestedColors|Pure red'), + '#D9534F' => s_('SuggestedColors|Soft red'), + '#D1D100' => s_('SuggestedColors|Strong yellow'), + '#F0AD4E' => s_('SuggestedColors|Soft orange'), + '#AD8D43' => s_('SuggestedColors|Dark moderate orange') + } + end + + def render_suggested_colors + colors_html = suggested_colors.map do |color_hex_value, color_name| + link_to('', '#', class: "has-tooltip", style: "background-color: #{color_hex_value}", data: { color: color_hex_value }, title: color_name) + end + + content_tag(:div, class: 'suggest-colors') do + colors_html.join.html_safe + end end def text_color_for_bg(bg_color) diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index f798bfbf703..e587cf4045d 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -343,6 +343,10 @@ module ProjectsHelper description.html_safe % { project_name: project.name } end + def metrics_external_dashboard_url + @project.metrics_setting_external_dashboard_url + end + private def get_project_nav_tabs(project, current_user) @@ -655,4 +659,8 @@ module ProjectsHelper project.builds_enabled? && !project.repository.gitlab_ci_yml end + + def vue_file_list_enabled? + Gitlab::Graphql.enabled? && Feature.enabled?(:vue_file_list, @project) + end end diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb index f2d814e6930..26692934456 100644 --- a/app/helpers/sorting_helper.rb +++ b/app/helpers/sorting_helper.rb @@ -3,29 +3,30 @@ module SortingHelper def sort_options_hash { - sort_value_created_date => sort_title_created_date, - sort_value_downvotes => sort_title_downvotes, - sort_value_due_date => sort_title_due_date, - sort_value_due_date_later => sort_title_due_date_later, - sort_value_due_date_soon => sort_title_due_date_soon, - sort_value_label_priority => sort_title_label_priority, - sort_value_largest_group => sort_title_largest_group, - sort_value_largest_repo => sort_title_largest_repo, - sort_value_milestone => sort_title_milestone, - sort_value_milestone_later => sort_title_milestone_later, - sort_value_milestone_soon => sort_title_milestone_soon, - sort_value_name => sort_title_name, - sort_value_name_desc => sort_title_name_desc, - sort_value_oldest_created => sort_title_oldest_created, - sort_value_oldest_signin => sort_title_oldest_signin, - sort_value_oldest_updated => sort_title_oldest_updated, - sort_value_recently_created => sort_title_recently_created, - sort_value_recently_signin => sort_title_recently_signin, - sort_value_recently_updated => sort_title_recently_updated, - sort_value_popularity => sort_title_popularity, - sort_value_priority => sort_title_priority, - sort_value_upvotes => sort_title_upvotes, - sort_value_contacted_date => sort_title_contacted_date + sort_value_created_date => sort_title_created_date, + sort_value_downvotes => sort_title_downvotes, + sort_value_due_date => sort_title_due_date, + sort_value_due_date_later => sort_title_due_date_later, + sort_value_due_date_soon => sort_title_due_date_soon, + sort_value_label_priority => sort_title_label_priority, + sort_value_largest_group => sort_title_largest_group, + sort_value_largest_repo => sort_title_largest_repo, + sort_value_milestone => sort_title_milestone, + sort_value_milestone_later => sort_title_milestone_later, + sort_value_milestone_soon => sort_title_milestone_soon, + sort_value_name => sort_title_name, + sort_value_name_desc => sort_title_name_desc, + sort_value_oldest_created => sort_title_oldest_created, + sort_value_oldest_signin => sort_title_oldest_signin, + sort_value_oldest_updated => sort_title_oldest_updated, + sort_value_recently_created => sort_title_recently_created, + sort_value_recently_signin => sort_title_recently_signin, + sort_value_recently_updated => sort_title_recently_updated, + sort_value_popularity => sort_title_popularity, + sort_value_priority => sort_title_priority, + sort_value_upvotes => sort_title_upvotes, + sort_value_contacted_date => sort_title_contacted_date, + sort_value_relative_position => sort_title_relative_position } end @@ -397,6 +398,10 @@ module SortingHelper s_('SortOptions|Recent last activity') end + def sort_title_relative_position + s_('SortOptions|Manual') + end + # Values. def sort_value_access_level_asc 'access_level_asc' @@ -545,4 +550,8 @@ module SortingHelper def sort_value_recently_last_activity 'last_activity_on_desc' end + + def sort_value_relative_position + 'relative_position' + end end diff --git a/app/helpers/storage_helper.rb b/app/helpers/storage_helper.rb index e80b3f2b54a..ecf37bae6b3 100644 --- a/app/helpers/storage_helper.rb +++ b/app/helpers/storage_helper.rb @@ -12,10 +12,11 @@ module StorageHelper def storage_counters_details(statistics) counters = { counter_repositories: storage_counter(statistics.repository_size), + counter_wikis: storage_counter(statistics.wiki_size), counter_build_artifacts: storage_counter(statistics.build_artifacts_size), counter_lfs_objects: storage_counter(statistics.lfs_objects_size) } - _("%{counter_repositories} repositories, %{counter_build_artifacts} build artifacts, %{counter_lfs_objects} LFS") % counters + _("%{counter_repositories} repositories, %{counter_wikis} wikis, %{counter_build_artifacts} build artifacts, %{counter_lfs_objects} LFS") % counters end end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 1b67a7272bc..56786fae6ea 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -765,6 +765,10 @@ module Ci end end + def report_artifacts + job_artifacts.with_reports + end + # Virtual deployment status depending on the environment status. def deployment_status return unless starts_environment? diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb index 3beb76ffc2b..0dbeab30498 100644 --- a/app/models/ci/job_artifact.rb +++ b/app/models/ci/job_artifact.rb @@ -26,10 +26,13 @@ module Ci metrics: 'metrics.txt' }.freeze - TYPE_AND_FORMAT_PAIRS = { + INTERNAL_TYPES = { archive: :zip, metadata: :gzip, - trace: :raw, + trace: :raw + }.freeze + + REPORT_TYPES = { junit: :gzip, metrics: :gzip, @@ -45,6 +48,8 @@ module Ci performance: :raw }.freeze + TYPE_AND_FORMAT_PAIRS = INTERNAL_TYPES.merge(REPORT_TYPES).freeze + belongs_to :project belongs_to :job, class_name: "Ci::Build", foreign_key: :job_id @@ -66,6 +71,10 @@ module Ci where(file_type: types) end + scope :with_reports, -> do + with_file_types(REPORT_TYPES.keys.map(&:to_s)) + end + scope :test_reports, -> do with_file_types(TEST_REPORT_FILE_TYPES) end diff --git a/app/models/issue.rb b/app/models/issue.rb index eb5544f2a12..6da6fbe55cb 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -58,6 +58,7 @@ class Issue < ApplicationRecord scope :order_due_date_asc, -> { reorder('issues.due_date IS NULL, issues.due_date ASC') } scope :order_due_date_desc, -> { reorder('issues.due_date IS NULL, issues.due_date DESC') } scope :order_closest_future_date, -> { reorder('CASE WHEN issues.due_date >= CURRENT_DATE THEN 0 ELSE 1 END ASC, ABS(CURRENT_DATE - issues.due_date) ASC') } + scope :order_relative_position_asc, -> { reorder(::Gitlab::Database.nulls_last_order('relative_position', 'ASC')) } scope :preload_associations, -> { preload(:labels, project: :namespace) } scope :with_api_entity_associations, -> { preload(:timelogs, :assignees, :author, :notes, :labels, project: [:route, { namespace: :route }] ) } @@ -130,9 +131,10 @@ class Issue < ApplicationRecord def self.sort_by_attribute(method, excluded_labels: []) case method.to_s when 'closest_future_date' then order_closest_future_date - when 'due_date' then order_due_date_asc - when 'due_date_asc' then order_due_date_asc - when 'due_date_desc' then order_due_date_desc + when 'due_date' then order_due_date_asc + when 'due_date_asc' then order_due_date_asc + when 'due_date_desc' then order_due_date_desc + when 'relative_position' then order_relative_position_asc else super end diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 7393ef4b05c..f7c31890198 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -76,6 +76,7 @@ class Namespace < ApplicationRecord 'namespaces.*', 'COALESCE(SUM(ps.storage_size), 0) AS storage_size', 'COALESCE(SUM(ps.repository_size), 0) AS repository_size', + 'COALESCE(SUM(ps.wiki_size), 0) AS wiki_size', 'COALESCE(SUM(ps.lfs_objects_size), 0) AS lfs_objects_size', 'COALESCE(SUM(ps.build_artifacts_size), 0) AS build_artifacts_size', 'COALESCE(SUM(ps.packages_size), 0) AS packages_size' diff --git a/app/models/project.rb b/app/models/project.rb index ab4da61dcf8..20895923d3b 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -309,6 +309,7 @@ class Project < ApplicationRecord delegate :group_clusters_enabled?, to: :group, allow_nil: true delegate :root_ancestor, to: :namespace, allow_nil: true delegate :last_pipeline, to: :commit, allow_nil: true + delegate :external_dashboard_url, to: :metrics_setting, allow_nil: true, prefix: true # Validations validates :creator, presence: true, on: :create diff --git a/app/models/project_statistics.rb b/app/models/project_statistics.rb index 6fe8cb40d25..832c8417b5b 100644 --- a/app/models/project_statistics.rb +++ b/app/models/project_statistics.rb @@ -4,9 +4,16 @@ class ProjectStatistics < ApplicationRecord belongs_to :project belongs_to :namespace + default_value_for :wiki_size, 0 + + # older migrations fail due to non-existent attribute without this + def wiki_size + has_attribute?(:wiki_size) ? super : 0 + end + before_save :update_storage_size - COLUMNS_TO_REFRESH = [:repository_size, :lfs_objects_size, :commit_count].freeze + COLUMNS_TO_REFRESH = [:repository_size, :wiki_size, :lfs_objects_size, :commit_count].freeze INCREMENTABLE_COLUMNS = { build_artifacts_size: %i[storage_size], packages_size: %i[storage_size] }.freeze def total_repository_size @@ -27,11 +34,14 @@ class ProjectStatistics < ApplicationRecord self.commit_count = project.repository.commit_count end - # Repository#size needs to be converted from MB to Byte. def update_repository_size self.repository_size = project.repository.size * 1.megabyte end + def update_wiki_size + self.wiki_size = project.wiki.repository.size * 1.megabyte + end + def update_lfs_objects_size self.lfs_objects_size = project.lfs_objects.sum(:size) end @@ -42,7 +52,7 @@ class ProjectStatistics < ApplicationRecord end def update_storage_size - self.storage_size = repository_size + lfs_objects_size + build_artifacts_size + packages_size + self.storage_size = repository_size + wiki_size + lfs_objects_size + build_artifacts_size + packages_size end # Since this incremental update method does not call update_storage_size above, diff --git a/app/presenters/ci/pipeline_presenter.rb b/app/presenters/ci/pipeline_presenter.rb index 944895904fe..358473d0a74 100644 --- a/app/presenters/ci/pipeline_presenter.rb +++ b/app/presenters/ci/pipeline_presenter.rb @@ -43,7 +43,7 @@ module Ci if pipeline.ref_exists? _("for %{link_to_pipeline_ref}").html_safe % { link_to_pipeline_ref: link_to_pipeline_ref } else - _("for %{ref}") % { ref: content_tag(:span, pipeline.ref, class: 'ref-name') } + _("for %{ref}").html_safe % { ref: content_tag(:span, pipeline.ref, class: 'ref-name') } end end end diff --git a/app/serializers/build_details_entity.rb b/app/serializers/build_details_entity.rb index 6928968edc0..67e44ee9d10 100644 --- a/app/serializers/build_details_entity.rb +++ b/app/serializers/build_details_entity.rb @@ -42,6 +42,11 @@ class BuildDetailsEntity < JobEntity end end + expose :report_artifacts, + as: :reports, + using: JobArtifactReportEntity, + if: -> (*) { can?(current_user, :read_build, build) } + expose :erased_by, if: -> (*) { build.erased? }, using: UserEntity expose :erase_path, if: -> (*) { build.erasable? && can?(current_user, :erase_build, build) } do |build| erase_project_job_path(project, build) diff --git a/app/serializers/job_artifact_report_entity.rb b/app/serializers/job_artifact_report_entity.rb new file mode 100644 index 00000000000..4280351a6b0 --- /dev/null +++ b/app/serializers/job_artifact_report_entity.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class JobArtifactReportEntity < Grape::Entity + include RequestAwareEntity + + expose :file_type + expose :file_format + expose :size + + expose :download_path do |artifact| + download_project_job_artifacts_path(artifact.job.project, artifact.job, file_type: artifact.file_format) + end +end diff --git a/app/serializers/pipeline_entity.rb b/app/serializers/pipeline_entity.rb index 9ef93b2387f..ec2698ecbe3 100644 --- a/app/serializers/pipeline_entity.rb +++ b/app/serializers/pipeline_entity.rb @@ -4,6 +4,7 @@ class PipelineEntity < Grape::Entity include RequestAwareEntity expose :id + expose :iid expose :user, using: UserEntity expose :active?, as: :active diff --git a/app/services/projects/update_statistics_service.rb b/app/services/projects/update_statistics_service.rb index f32a779fab0..28677a398f3 100644 --- a/app/services/projects/update_statistics_service.rb +++ b/app/services/projects/update_statistics_service.rb @@ -3,7 +3,7 @@ module Projects class UpdateStatisticsService < BaseService def execute - return unless project && project.repository.exists? + return unless project Rails.logger.info("Updating statistics for project #{project.id}") diff --git a/app/views/admin/application_settings/_external_authorization_service_form.html.haml b/app/views/admin/application_settings/_external_authorization_service_form.html.haml index 01f6c7afe61..7587ecbf9d3 100644 --- a/app/views/admin/application_settings/_external_authorization_service_form.html.haml +++ b/app/views/admin/application_settings/_external_authorization_service_form.html.haml @@ -5,7 +5,7 @@ %button.btn.js-settings-toggle{ type: 'button' } = expanded ? 'Collapse' : 'Expand' %p - = _('External Classification Policy Authorization') + = _('External Classification Policy Authorization') .settings-content = form_for @application_setting, url: admin_application_settings_path(anchor: 'js-external-auth-settings'), html: { class: 'fieldset-form' } do |f| diff --git a/app/views/admin/labels/_form.html.haml b/app/views/admin/labels/_form.html.haml index 49aa62a5408..299d0a12e6c 100644 --- a/app/views/admin/labels/_form.html.haml +++ b/app/views/admin/labels/_form.html.haml @@ -24,10 +24,7 @@ %br = _("Or you can choose one of the suggested colors below") - .suggest-colors - - suggested_colors.each do |color| - = link_to '#', style: "background-color: #{color}", data: { color: color } do - + = render_suggested_colors .form-actions = f.submit _('Save'), class: 'btn btn-success js-save-button' diff --git a/app/views/admin/projects/_projects.html.haml b/app/views/admin/projects/_projects.html.haml index 9117f63f939..2f7ad35eb3e 100644 --- a/app/views/admin/projects/_projects.html.haml +++ b/app/views/admin/projects/_projects.html.haml @@ -7,7 +7,7 @@ = link_to 'Edit', edit_project_path(project), id: "edit_#{dom_id(project)}", class: "btn" %button.delete-project-button.btn.btn-danger{ data: { toggle: 'modal', target: '#delete-project-modal', - delete_project_url: project_path(project), + delete_project_url: admin_project_path(project), project_name: project.name }, type: 'button' } = s_('AdminProjects|Delete') @@ -17,7 +17,7 @@ - if project.archived %span.badge.badge-warning archived .title - = link_to(admin_namespace_project_path(project.namespace, project)) do + = link_to(admin_project_path(project)) do .dash-project-avatar .avatar-container.rect-avatar.s40 = project_icon(project, alt: '', class: 'avatar project-avatar s40', width: 40, height: 40) diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml index c4178296e67..dcd6f7c8078 100644 --- a/app/views/admin/users/show.html.haml +++ b/app/views/admin/users/show.html.haml @@ -124,6 +124,8 @@ %strong = Gitlab::Access.human_access_with_none(@user.highest_role) + = render_if_exists 'admin/users/using_license_seat', user: @user + - if @user.ldap_user? %li %span.light LDAP uid: diff --git a/app/views/import/bitbucket_server/status.html.haml b/app/views/import/bitbucket_server/status.html.haml index 9280f12e187..40609fddbde 100644 --- a/app/views/import/bitbucket_server/status.html.haml +++ b/app/views/import/bitbucket_server/status.html.haml @@ -29,7 +29,7 @@ %tr %th= _('From Bitbucket Server') %th= _('To GitLab') - %th= _(' Status') + %th= _('Status') %tbody - @already_added_projects.each do |project| %tr{ id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}" } diff --git a/app/views/profiles/active_sessions/_active_session.html.haml b/app/views/profiles/active_sessions/_active_session.html.haml index 2bf514d72a5..bb31049111c 100644 --- a/app/views/profiles/active_sessions/_active_session.html.haml +++ b/app/views/profiles/active_sessions/_active_session.html.haml @@ -8,18 +8,19 @@ %div %strong= active_session.ip_address - if is_current_session - %div This is your current session + %div + = _('This is your current session') - else %div - Last accessed on + = _('Last accessed on') = l(active_session.updated_at, format: :short) %div %strong= active_session.browser - on + = s_('ProfileSession|on') %strong= active_session.os %div - %strong Signed in - on + %strong= _('Signed in') + = s_('ProfileSession|on') = l(active_session.created_at, format: :short) diff --git a/app/views/profiles/active_sessions/index.html.haml b/app/views/profiles/active_sessions/index.html.haml index 8688a52843d..d651319fc3f 100644 --- a/app/views/profiles/active_sessions/index.html.haml +++ b/app/views/profiles/active_sessions/index.html.haml @@ -1,4 +1,4 @@ -- page_title 'Active Sessions' +- page_title _('Active Sessions') - @content_class = "limit-container-width" unless fluid_layout .row.prepend-top-default @@ -6,7 +6,7 @@ %h4.prepend-top-0 = page_title %p - This is a list of devices that have logged into your account. Revoke any sessions that you do not recognize. + = _('This is a list of devices that have logged into your account. Revoke any sessions that you do not recognize.') .col-lg-8 .append-bottom-default diff --git a/app/views/profiles/gpg_keys/_form.html.haml b/app/views/profiles/gpg_keys/_form.html.haml index 6c4cb614a2b..225487b2638 100644 --- a/app/views/profiles/gpg_keys/_form.html.haml +++ b/app/views/profiles/gpg_keys/_form.html.haml @@ -3,8 +3,8 @@ = form_errors(@gpg_key) .form-group - = f.label :key, class: 'label-bold' - = f.text_area :key, class: "form-control", rows: 8, required: true, placeholder: "Don't paste the private part of the GPG key. Paste the public part which begins with '-----BEGIN PGP PUBLIC KEY BLOCK-----'." + = f.label :key, s_('Profiles|Key'), class: 'label-bold' + = f.text_area :key, class: "form-control", rows: 8, required: true, placeholder: _("Don't paste the private part of the GPG key. Paste the public part which begins with '-----BEGIN PGP PUBLIC KEY BLOCK-----'.") .prepend-top-default - = f.submit 'Add key', class: "btn btn-success" + = f.submit s_('Profiles|Add key'), class: "btn btn-success" diff --git a/app/views/profiles/gpg_keys/_key.html.haml b/app/views/profiles/gpg_keys/_key.html.haml index d1fd7bc8e71..f8351644df5 100644 --- a/app/views/profiles/gpg_keys/_key.html.haml +++ b/app/views/profiles/gpg_keys/_key.html.haml @@ -9,17 +9,19 @@ %code= key.fingerprint - if key.subkeys.present? .subkeys - %span.bold Subkeys: + %span.bold + = _('Subkeys') + = ':' %ul.subkeys-list - key.subkeys.each do |subkey| %li %code= subkey.fingerprint .float-right %span.key-created-at - created #{time_ago_with_tooltip(key.created_at)} - = link_to profile_gpg_key_path(key), data: { confirm: 'Are you sure? Removing this GPG key does not affect already signed commits.' }, method: :delete, class: "btn btn-danger prepend-left-10" do - %span.sr-only Remove + = s_('Profiles|Created %{time_ago}'.html_safe) % { time_ago:time_ago_with_tooltip(key.created_at)} + = link_to profile_gpg_key_path(key), data: { confirm: _('Are you sure? Removing this GPG key does not affect already signed commits.') }, method: :delete, class: "btn btn-danger prepend-left-10" do + %span.sr-only= _('Remove') = icon('trash') - = link_to revoke_profile_gpg_key_path(key), data: { confirm: 'Are you sure? All commits that were signed with this GPG key will be unverified.' }, method: :put, class: "btn btn-danger prepend-left-10" do - %span.sr-only Revoke - Revoke + = link_to revoke_profile_gpg_key_path(key), data: { confirm: _('Are you sure? All commits that were signed with this GPG key will be unverified.') }, method: :put, class: "btn btn-danger prepend-left-10" do + %span.sr-only= _('Revoke') + = _('Revoke') diff --git a/app/views/profiles/gpg_keys/_key_table.html.haml b/app/views/profiles/gpg_keys/_key_table.html.haml index b9b60c218fd..ebbd1c8f672 100644 --- a/app/views/profiles/gpg_keys/_key_table.html.haml +++ b/app/views/profiles/gpg_keys/_key_table.html.haml @@ -6,6 +6,6 @@ - else %p.settings-message.text-center - if is_admin - There are no GPG keys associated with this account. + = _('There are no GPG keys associated with this account.') - else - There are no GPG keys with access to your account. + = _('There are no GPG keys with access to your account.') diff --git a/app/views/profiles/gpg_keys/index.html.haml b/app/views/profiles/gpg_keys/index.html.haml index 1d2e41cb437..f9f898a9225 100644 --- a/app/views/profiles/gpg_keys/index.html.haml +++ b/app/views/profiles/gpg_keys/index.html.haml @@ -1,4 +1,4 @@ -- page_title "GPG Keys" +- page_title _('GPG Keys') - @content_class = "limit-container-width" unless fluid_layout .row.prepend-top-default @@ -6,16 +6,16 @@ %h4.prepend-top-0 = page_title %p - GPG keys allow you to verify signed commits. + = _('GPG keys allow you to verify signed commits.') .col-lg-8 %h5.prepend-top-0 - Add a GPG key + = _('Add a GPG key') %p.profile-settings-content - Before you can add a GPG key you need to - = link_to 'generate it.', help_page_path('user/project/repository/gpg_signed_commits/index.md') + - help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/project/repository/gpg_signed_commits/index.md') } + = _('Before you can add a GPG key you need to %{help_link_start}Generate it.%{help_link_end}'.html_safe) % {help_link_start: help_link_start, help_link_end:'</a>'.html_safe } = render 'form' %hr %h5 - Your GPG keys (#{@gpg_keys.count}) + = _('Your GPG keys (%{count})') % { count:@gpg_keys.count} .append-bottom-default = render 'key_table' diff --git a/app/views/profiles/keys/_form.html.haml b/app/views/profiles/keys/_form.html.haml index 21eef08983c..7846cdbcd52 100644 --- a/app/views/profiles/keys/_form.html.haml +++ b/app/views/profiles/keys/_form.html.haml @@ -3,11 +3,11 @@ = form_errors(@key) .form-group - = f.label :key, class: 'label-bold' + = f.label :key, s_('Profiles|Key'), class: 'label-bold' %p= _("Paste your public SSH key, which is usually contained in the file '~/.ssh/id_rsa.pub' and begins with 'ssh-rsa'. Don't use your private SSH key.") = f.text_area :key, class: "form-control js-add-ssh-key-validation-input qa-key-public-key-field", rows: 8, required: true, placeholder: s_('Profiles|Typically starts with "ssh-rsa …"') .form-group - = f.label :title, class: 'label-bold' + = f.label :title, _('Title'), class: 'label-bold' = f.text_field :title, class: "form-control input-lg qa-key-title-field", required: true, placeholder: s_('Profiles|e.g. My MacBook key') %p.form-text.text-muted= _('Name your individual key via a title') diff --git a/app/views/profiles/keys/_key.html.haml b/app/views/profiles/keys/_key.html.haml index ce20994b0f4..47494fc3f06 100644 --- a/app/views/profiles/keys/_key.html.haml +++ b/app/views/profiles/keys/_key.html.haml @@ -17,7 +17,7 @@ = key.last_used_at ? time_ago_with_tooltip(key.last_used_at) : 'n/a' .float-right %span.key-created-at - created #{time_ago_with_tooltip(key.created_at)} - = link_to path_to_key(key, is_admin), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-transparent prepend-left-10" do - %span.sr-only Remove + = s_('Profiles|Created %{time_ago}'.html_safe) % { time_ago:time_ago_with_tooltip(key.created_at)} + = link_to path_to_key(key, is_admin), data: { confirm: _('Are you sure?')}, method: :delete, class: "btn btn-transparent prepend-left-10" do + %span.sr-only= _('Remove') = icon('trash') diff --git a/app/views/profiles/keys/_key_details.html.haml b/app/views/profiles/keys/_key_details.html.haml index 88473c7f72d..dcdb7fc63b1 100644 --- a/app/views/profiles/keys/_key_details.html.haml +++ b/app/views/profiles/keys/_key_details.html.haml @@ -3,25 +3,25 @@ .col-md-4 .card .card-header - SSH Key + = _('SSH Key') %ul.content-list %li - %span.light Title: + %span.light= _('Title:') %strong= @key.title %li - %span.light Created on: + %span.light= _('Created on:') %strong= @key.created_at.to_s(:medium) %li - %span.light Last used on: + %span.light= _('Last used on:') %strong= @key.last_used_at.try(:to_s, :medium) || 'N/A' .col-md-8 = form_errors(@key, type: 'key') unless @key.valid? %p - %span.light Fingerprint: + %span.light= _('Fingerprint:') %code.key-fingerprint= @key.fingerprint %pre.well-pre = @key.key .col-md-12 .float-right - = link_to 'Remove', path_to_key(@key, is_admin), data: {confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove delete-key qa-delete-key-button" + = link_to _('Remove'), path_to_key(@key, is_admin), data: {confirm: _('Are you sure?')}, method: :delete, class: "btn btn-remove delete-key qa-delete-key-button" diff --git a/app/views/profiles/keys/_key_table.html.haml b/app/views/profiles/keys/_key_table.html.haml index e088140fdd2..4a6d8a1870d 100644 --- a/app/views/profiles/keys/_key_table.html.haml +++ b/app/views/profiles/keys/_key_table.html.haml @@ -6,6 +6,6 @@ - else %p.settings-message.text-center - if is_admin - There are no SSH keys associated with this account. + = _('There are no SSH keys associated with this account.') - else - There are no SSH keys with access to your account. + = _('There are no SSH keys with access to your account.') diff --git a/app/views/profiles/keys/index.html.haml b/app/views/profiles/keys/index.html.haml index 55ca8d0ebd4..da6aa0fce3a 100644 --- a/app/views/profiles/keys/index.html.haml +++ b/app/views/profiles/keys/index.html.haml @@ -1,4 +1,4 @@ -- page_title "SSH Keys" +- page_title _('SSH Keys') - @content_class = "limit-container-width" unless fluid_layout .row.prepend-top-default @@ -6,10 +6,10 @@ %h4.prepend-top-0 = page_title %p - SSH keys allow you to establish a secure connection between your computer and GitLab. + = _('SSH keys allow you to establish a secure connection between your computer and GitLab.') .col-lg-8 %h5.prepend-top-0 - Add an SSH key + = _('Add an SSH key') %p.profile-settings-content - generate_link_url = help_page_path("ssh/README", anchor: 'generating-a-new-ssh-key-pair') - existing_link_url = help_page_path("ssh/README", anchor: 'locating-an-existing-ssh-key-pair') @@ -19,6 +19,6 @@ = render 'form' %hr %h5 - Your SSH keys (#{@keys.count}) + = _('Your SSH keys (%{count})') % { count:@keys.count } .append-bottom-default = render 'key_table' diff --git a/app/views/profiles/keys/show.html.haml b/app/views/profiles/keys/show.html.haml index 28be6172219..360de7a0c11 100644 --- a/app/views/profiles/keys/show.html.haml +++ b/app/views/profiles/keys/show.html.haml @@ -1,5 +1,5 @@ - add_to_breadcrumbs "SSH Keys", profile_keys_path - breadcrumb_title @key.title -- page_title @key.title, "SSH Keys" +- page_title @key.title, _('SSH Keys') - @content_class = "limit-container-width" unless fluid_layout = render "key_details" diff --git a/app/views/projects/_files.html.haml b/app/views/projects/_files.html.haml index 7f50a7e4294..2b0c3985755 100644 --- a/app/views/projects/_files.html.haml +++ b/app/views/projects/_files.html.haml @@ -4,7 +4,6 @@ - project = local_assigns.fetch(:project) { @project } - content_url = local_assigns.fetch(:content_url) { @tree.readme ? project_blob_path(@project, tree_join(@ref, @tree.readme.path)) : project_tree_path(@project, @ref) } - show_auto_devops_callout = show_auto_devops_callout?(@project) -- vue_file_list = Feature.enabled?(:vue_file_list, @project) #tree-holder.tree-holder.clearfix .nav-block @@ -14,11 +13,11 @@ = render 'shared/commit_well', commit: commit, ref: ref, project: project - if is_project_overview - .project-buttons.append-bottom-default{ class: ("js-keep-hidden-on-navigation" if vue_file_list) } + .project-buttons.append-bottom-default{ class: ("js-keep-hidden-on-navigation" if vue_file_list_enabled?) } = render 'stat_anchor_list', anchors: @project.statistics_buttons(show_auto_devops_callout: show_auto_devops_callout) - - if vue_file_list - #js-tree-list{ data: { project_path: @project.full_path, full_name: @project.name_with_namespace, ref: ref } } + - if vue_file_list_enabled? + #js-tree-list{ data: { project_path: @project.full_path, project_short_path: @project.path, ref: ref, full_name: @project.name_with_namespace } } - if @tree.readme = render "projects/tree/readme", readme: @tree.readme - else diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index 6de8848d3a1..9f5241344a7 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -1,7 +1,7 @@ - empty_repo = @project.empty_repo? - show_auto_devops_callout = show_auto_devops_callout?(@project) - max_project_topic_length = 15 -.project-home-panel{ class: [("empty-project" if empty_repo), ("js-keep-hidden-on-navigation" if Feature.enabled?(:vue_file_list, @project))] } +.project-home-panel{ class: [("empty-project" if empty_repo), ("js-keep-hidden-on-navigation" if vue_file_list_enabled?)] } .row.append-bottom-8 .home-panel-title-row.col-md-12.col-lg-6.d-flex .avatar-container.rect-avatar.s64.home-panel-avatar.append-right-default.float-none diff --git a/app/views/projects/blob/_header_content.html.haml b/app/views/projects/blob/_header_content.html.haml index 88fa31a73b0..7ed71a7d43c 100644 --- a/app/views/projects/blob/_header_content.html.haml +++ b/app/views/projects/blob/_header_content.html.haml @@ -6,7 +6,7 @@ = copy_file_path_button(blob.path) - %small + %small.mr-1 = number_to_human_size(blob.raw_size) - if blob.stored_externally? && blob.external_storage == :lfs diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml index f4560404c03..bdf7b933ab8 100644 --- a/app/views/projects/ci/builds/_build.html.haml +++ b/app/views/projects/ci/builds/_build.html.haml @@ -53,9 +53,10 @@ %span.badge.badge-info= _('manual') - if pipeline_link - %td - = link_to pipeline_path(pipeline) do + %td.pipeline-link + = link_to pipeline_path(pipeline), class: 'has-tooltip', title: _('Pipeline ID (IID)') do %span.pipeline-id ##{pipeline.id} + %span.pipeline-iid (##{pipeline.iid}) %span by - if pipeline.user = user_avatar(user: pipeline.user, size: 20) diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index a0db48bf8ff..ef2777e6601 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -81,7 +81,7 @@ = link_to project_pipeline_path(@project, last_pipeline.id), class: "ci-status-icon-#{last_pipeline.status}" do = ci_icon_for_status(last_pipeline.status) #{ _('Pipeline') } - = link_to "##{last_pipeline.id}", project_pipeline_path(@project, last_pipeline.id) + = link_to "##{last_pipeline.id} (##{last_pipeline.iid})", project_pipeline_path(@project, last_pipeline.id), class: "has-tooltip", title: _('Pipeline ID (IID)') = ci_label_for_status(last_pipeline.status) - if last_pipeline.stages_count.nonzero? #{ n_(s_('Pipeline|with stage'), s_('Pipeline|with stages'), last_pipeline.stages_count) } diff --git a/app/views/projects/settings/operations/_external_dashboard.html.haml b/app/views/projects/settings/operations/_external_dashboard.html.haml index 2fbb9195a04..f049c35b38d 100644 --- a/app/views/projects/settings/operations/_external_dashboard.html.haml +++ b/app/views/projects/settings/operations/_external_dashboard.html.haml @@ -1,2 +1,2 @@ -.js-operation-settings{ data: { external_dashboard: { path: '', +.js-operation-settings{ data: { external_dashboard: { path: metrics_external_dashboard_url, help_page_path: help_page_path('user/project/operations/link_to_external_dashboard') } } } diff --git a/app/views/projects/settings/repository/_protected_branches.html.haml b/app/views/projects/settings/repository/_protected_branches.html.haml new file mode 100644 index 00000000000..31630828571 --- /dev/null +++ b/app/views/projects/settings/repository/_protected_branches.html.haml @@ -0,0 +1,2 @@ += render "projects/protected_branches/index" += render "projects/protected_tags/index" diff --git a/app/views/projects/settings/repository/show.html.haml b/app/views/projects/settings/repository/show.html.haml index cb3a035c49e..ff30cc4f6db 100644 --- a/app/views/projects/settings/repository/show.html.haml +++ b/app/views/projects/settings/repository/show.html.haml @@ -3,14 +3,17 @@ - @content_class = "limit-container-width" unless fluid_layout = render "projects/default_branch/show" += render_if_exists "projects/push_rules/index" = render "projects/mirrors/mirror_repos" -# Protected branches & tags use a lot of nested partials. -# The shared parts of the views can be found in the `shared` directory. -# Those are used throughout the actual views. These `shared` views are then -# reused in EE. -= render "projects/protected_branches/index" -= render "projects/protected_tags/index" += render "projects/settings/repository/protected_branches" + = render @deploy_keys = render "projects/deploy_tokens/index" = render "projects/cleanup/show" + += render_if_exists 'shared/promotions/promote_repository_features' diff --git a/app/views/projects/tree/_readme.html.haml b/app/views/projects/tree/_readme.html.haml index e935af23659..4f6c7e1f9a6 100644 --- a/app/views/projects/tree/_readme.html.haml +++ b/app/views/projects/tree/_readme.html.haml @@ -1,5 +1,5 @@ - if readme.rich_viewer - %article.file-holder.readme-holder{ id: 'readme', class: [("limited-width-container" unless fluid_layout), ("js-hide-on-navigation" if Feature.enabled?(:vue_file_list, @project))] } + %article.file-holder.readme-holder{ id: 'readme', class: [("limited-width-container" unless fluid_layout), ("js-hide-on-navigation" if vue_file_list_enabled?)] } .js-file-title.file-title = blob_icon readme.mode, readme.name = link_to project_blob_path(@project, tree_join(@ref, readme.path)) do diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml index ec8e5234bd4..ea6349f2f57 100644 --- a/app/views/projects/tree/_tree_header.html.haml +++ b/app/views/projects/tree/_tree_header.html.haml @@ -6,71 +6,74 @@ = render 'shared/ref_switcher', destination: 'tree', path: @path, show_create: true - if on_top_of_branch? - - addtotree_toggle_attributes = { href: '#', 'data-toggle': 'dropdown', 'data-target': '.add-to-tree-dropdown', 'data-boundary': 'window' } + - addtotree_toggle_attributes = { 'data-toggle': 'dropdown', 'data-target': '.add-to-tree-dropdown', 'data-boundary': 'window' } - else - addtotree_toggle_attributes = { title: _("You can only add files when you are on a branch"), data: { container: 'body' }, class: 'disabled has-tooltip' } - %ul.breadcrumb.repo-breadcrumb - %li.breadcrumb-item - = link_to project_tree_path(@project, @ref) do - = @project.path - - path_breadcrumbs do |title, path| + - if vue_file_list_enabled? + #js-repo-breadcrumb + - else + %ul.breadcrumb.repo-breadcrumb %li.breadcrumb-item - = link_to truncate(title, length: 40), project_tree_path(@project, tree_join(@ref, path)) + = link_to project_tree_path(@project, @ref) do + = @project.path + - path_breadcrumbs do |title, path| + %li.breadcrumb-item + = link_to truncate(title, length: 40), project_tree_path(@project, tree_join(@ref, path)) - - if can_collaborate || can_create_mr_from_fork - %li.breadcrumb-item - %a.btn.add-to-tree.qa-add-to-tree{ addtotree_toggle_attributes } - = sprite_icon('plus', size: 16, css_class: 'float-left') - = sprite_icon('arrow-down', size: 16, css_class: 'float-left') - - if on_top_of_branch? - .add-to-tree-dropdown - %ul.dropdown-menu - - if can_edit_tree? - %li.dropdown-header - #{ _('This directory') } - %li - = link_to project_new_blob_path(@project, @id), class: 'qa-new-file-option' do - #{ _('New file') } - %li - = link_to '#modal-upload-blob', { 'data-target' => '#modal-upload-blob', 'data-toggle' => 'modal' } do - #{ _('Upload file') } - %li - = link_to '#modal-create-new-dir', { 'data-target' => '#modal-create-new-dir', 'data-toggle' => 'modal' } do - #{ _('New directory') } - - elsif can?(current_user, :fork_project, @project) && can?(current_user, :create_merge_request_in, @project) - %li - - continue_params = { to: project_new_blob_path(@project, @id), - notice: edit_in_new_fork_notice, - notice_now: edit_in_new_fork_notice_now } - - fork_path = project_forks_path(@project, namespace_key: current_user.namespace.id, continue: continue_params) - = link_to fork_path, method: :post do - #{ _('New file') } - %li - - continue_params = { to: request.fullpath, - notice: edit_in_new_fork_notice + " Try to upload a file again.", - notice_now: edit_in_new_fork_notice_now } - - fork_path = project_forks_path(@project, namespace_key: current_user.namespace.id, continue: continue_params) - = link_to fork_path, method: :post do - #{ _('Upload file') } - %li - - continue_params = { to: request.fullpath, - notice: edit_in_new_fork_notice + " Try to create a new directory again.", - notice_now: edit_in_new_fork_notice_now } - - fork_path = project_forks_path(@project, namespace_key: current_user.namespace.id, continue: continue_params) - = link_to fork_path, method: :post do - #{ _('New directory') } + - if can_collaborate || can_create_mr_from_fork + %li.breadcrumb-item + %button.btn.add-to-tree.qa-add-to-tree{ addtotree_toggle_attributes, type: 'button' } + = sprite_icon('plus', size: 16, css_class: 'float-left') + = sprite_icon('arrow-down', size: 16, css_class: 'float-left') + - if on_top_of_branch? + .add-to-tree-dropdown + %ul.dropdown-menu + - if can_edit_tree? + %li.dropdown-header + #{ _('This directory') } + %li + = link_to project_new_blob_path(@project, @id), class: 'qa-new-file-option' do + #{ _('New file') } + %li + = link_to '#modal-upload-blob', { 'data-target' => '#modal-upload-blob', 'data-toggle' => 'modal' } do + #{ _('Upload file') } + %li + = link_to '#modal-create-new-dir', { 'data-target' => '#modal-create-new-dir', 'data-toggle' => 'modal' } do + #{ _('New directory') } + - elsif can?(current_user, :fork_project, @project) && can?(current_user, :create_merge_request_in, @project) + %li + - continue_params = { to: project_new_blob_path(@project, @id), + notice: edit_in_new_fork_notice, + notice_now: edit_in_new_fork_notice_now } + - fork_path = project_forks_path(@project, namespace_key: current_user.namespace.id, continue: continue_params) + = link_to fork_path, method: :post do + #{ _('New file') } + %li + - continue_params = { to: request.fullpath, + notice: edit_in_new_fork_notice + " Try to upload a file again.", + notice_now: edit_in_new_fork_notice_now } + - fork_path = project_forks_path(@project, namespace_key: current_user.namespace.id, continue: continue_params) + = link_to fork_path, method: :post do + #{ _('Upload file') } + %li + - continue_params = { to: request.fullpath, + notice: edit_in_new_fork_notice + " Try to create a new directory again.", + notice_now: edit_in_new_fork_notice_now } + - fork_path = project_forks_path(@project, namespace_key: current_user.namespace.id, continue: continue_params) + = link_to fork_path, method: :post do + #{ _('New directory') } - - if can?(current_user, :push_code, @project) - %li.divider - %li.dropdown-header - #{ _('This repository') } - %li - = link_to new_project_branch_path(@project) do - #{ _('New branch') } - %li - = link_to new_project_tag_path(@project) do - #{ _('New tag') } + - if can?(current_user, :push_code, @project) + %li.divider + %li.dropdown-header + #{ _('This repository') } + %li + = link_to new_project_branch_path(@project) do + #{ _('New branch') } + %li + = link_to new_project_tag_path(@project) do + #{ _('New tag') } .tree-controls = link_to s_('Commits|History'), project_commits_path(@project, @id), class: 'btn' diff --git a/app/views/search/_form.html.haml b/app/views/search/_form.html.haml index 4af0c6bf84a..db0dcc8adfb 100644 --- a/app/views/search/_form.html.haml +++ b/app/views/search/_form.html.haml @@ -13,3 +13,4 @@ - unless params[:snippets].eql? 'true' = render 'filter' = button_tag _("Search"), class: "btn btn-success btn-search" + = render_if_exists 'search/form_elasticsearch' diff --git a/app/views/shared/_old_visibility_level.html.haml b/app/views/shared/_old_visibility_level.html.haml index fd576e4fbea..e8f3d888cce 100644 --- a/app/views/shared/_old_visibility_level.html.haml +++ b/app/views/shared/_old_visibility_level.html.haml @@ -1,6 +1,6 @@ .form-group.row .col-sm-2.col-form-label = _('Visibility level') - = link_to icon('question-circle'), help_page_path("public_access/public_access") + = link_to icon('question-circle'), help_page_path("public_access/public_access"), target: '_blank' .col-sm-10 = render 'shared/visibility_level', f: f, visibility_level: visibility_level, can_change_visibility_level: can_change_visibility_level, form_model: form_model, with_label: with_label diff --git a/app/views/shared/issuable/_label_page_create.html.haml b/app/views/shared/issuable/_label_page_create.html.haml index d173e3c0192..a0d3bc64f1f 100644 --- a/app/views/shared/issuable/_label_page_create.html.haml +++ b/app/views/shared/issuable/_label_page_create.html.haml @@ -9,9 +9,7 @@ .dropdown-labels-error.js-label-error %input#new_label_name.default-dropdown-input{ type: "text", placeholder: _('Name new label') } .suggest-colors.suggest-colors-dropdown - - suggested_colors.each do |color| - = link_to '#', style: "background-color: #{color}", data: { color: color } do -   + = render_suggested_colors .dropdown-label-color-input .dropdown-label-color-preview.js-dropdown-label-color-preview %input#new_label_color.default-dropdown-input{ type: "text", placeholder: _('Assign custom color like #FF0000') } diff --git a/app/views/shared/issuable/_sort_dropdown.html.haml b/app/views/shared/issuable/_sort_dropdown.html.haml index 967f31c8325..1dd97bc4ed1 100644 --- a/app/views/shared/issuable/_sort_dropdown.html.haml +++ b/app/views/shared/issuable/_sort_dropdown.html.haml @@ -10,12 +10,13 @@ = icon('chevron-down') %ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable.dropdown-menu-sort %li - = sortable_item(sort_title_priority, page_filter_path(sort: sort_value_priority), sort_title) - = sortable_item(sort_title_created_date, page_filter_path(sort: sort_value_created_date), sort_title) - = sortable_item(sort_title_recently_updated, page_filter_path(sort: sort_value_recently_updated), sort_title) - = sortable_item(sort_title_milestone, page_filter_path(sort: sort_value_milestone), sort_title) - = sortable_item(sort_title_due_date, page_filter_path(sort: sort_value_due_date), sort_title) if viewing_issues - = sortable_item(sort_title_popularity, page_filter_path(sort: sort_value_popularity), sort_title) - = sortable_item(sort_title_label_priority, page_filter_path(sort: sort_value_label_priority), sort_title) + = sortable_item(sort_title_priority, page_filter_path(sort: sort_value_priority), sort_title) + = sortable_item(sort_title_created_date, page_filter_path(sort: sort_value_created_date), sort_title) + = sortable_item(sort_title_recently_updated, page_filter_path(sort: sort_value_recently_updated), sort_title) + = sortable_item(sort_title_milestone, page_filter_path(sort: sort_value_milestone), sort_title) + = sortable_item(sort_title_due_date, page_filter_path(sort: sort_value_due_date), sort_title) if viewing_issues + = sortable_item(sort_title_popularity, page_filter_path(sort: sort_value_popularity), sort_title) + = sortable_item(sort_title_label_priority, page_filter_path(sort: sort_value_label_priority), sort_title) + = sortable_item(sort_title_relative_position, page_filter_path(sort: sort_value_relative_position), sort_title) if viewing_issues && Feature.enabled?(:manual_sorting) = render_if_exists('shared/ee/issuable/sort_dropdown', viewing_issues: viewing_issues, sort_title: sort_title) = issuable_sort_direction_button(sort_value) diff --git a/app/views/shared/labels/_form.html.haml b/app/views/shared/labels/_form.html.haml index 4b88aff3313..78ff225daad 100644 --- a/app/views/shared/labels/_form.html.haml +++ b/app/views/shared/labels/_form.html.haml @@ -25,12 +25,7 @@ Choose any color. %br Or you can choose one of the suggested colors below - - .suggest-colors - - suggested_colors.each do |color| - = link_to '#', style: "background-color: #{color}", data: { color: color } do - - + = render_suggested_colors .form-actions - if @label.persisted? = f.submit 'Save changes', class: 'btn btn-success js-save-button' diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb index 9a9c0c9d803..3f1639ec2ed 100644 --- a/app/workers/post_receive.rb +++ b/app/workers/post_receive.rb @@ -74,6 +74,8 @@ class PostReceive def process_wiki_changes(post_received) post_received.project.touch(:last_activity_at, :last_repository_updated_at) + post_received.project.wiki.repository.expire_statistics_caches + ProjectCacheWorker.perform_async(post_received.project.id, [], [:wiki_size]) end def log(message) diff --git a/app/workers/project_cache_worker.rb b/app/workers/project_cache_worker.rb index b2e0701008a..4e8ea903139 100644 --- a/app/workers/project_cache_worker.rb +++ b/app/workers/project_cache_worker.rb @@ -16,10 +16,12 @@ class ProjectCacheWorker def perform(project_id, files = [], statistics = []) project = Project.find_by(id: project_id) - return unless project && project.repository.exists? + return unless project update_statistics(project, statistics) + return unless project.repository.exists? + project.repository.refresh_method_caches(files.map(&:to_sym)) project.cleanup |