diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js index 0b920ba8e7a..cc2cf787a8f 100644 --- a/app/assets/javascripts/lib/utils/url_utility.js +++ b/app/assets/javascripts/lib/utils/url_utility.js @@ -16,6 +16,50 @@ function decodeUrlParameter(val) { return decodeURIComponent(val.replace(/\+/g, '%20')); } +/** + * Safely encodes a string to be used as a path + * + * Note: This function DOES encode typical URL parts like ?, =, &, #, and + + * If you need to use search parameters or URL fragments, they should be + * added AFTER calling this function, not before. + * + * Usecase: An image filename is stored verbatim, and you need to load it in + * the browser. + * + * Example: /some_path/file #1.jpg ==> /some_path/file%20%231.jpg + * Example: /some-path/file! Final!.jpg ==> /some-path/file%21%20Final%21.jpg + * + * Essentially, if a character *could* present a problem in a URL, it's escaped + * to the hexadecimal representation instead. This means it's a bit more + * aggressive than encodeURIComponent: that built-in function doesn't + * encode some characters that *could* be problematic, so this function + * adds them (#, !, ~, *, ', (, and )). + * Additionally, encodeURIComponent *does* encode `/`, but we want safer + * URLs, not non-functional URLs, so this function DEcodes slashes ('%2F'). + * + * @param {String} potentiallyUnsafePath + * @returns {String} + */ +export function encodeSaferUrl(potentiallyUnsafePath) { + const unencode = ['%2F']; + const encode = ['#', '!', '~', '\\*', "'", '\\(', '\\)']; + let saferPath = encodeURIComponent(potentiallyUnsafePath); + + unencode.forEach((code) => { + saferPath = saferPath.replace(new RegExp(code, 'g'), decodeURIComponent(code)); + }); + encode.forEach((code) => { + const encodedValue = code + .codePointAt(code.length - 1) + .toString(16) + .toUpperCase(); + + saferPath = saferPath.replace(new RegExp(code, 'g'), `%${encodedValue}`); + }); + + return saferPath; +} + export function cleanLeadingSeparator(path) { return path.replace(PATH_SEPARATOR_LEADING_REGEX, ''); } diff --git a/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_a.vue b/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_a.vue new file mode 100644 index 00000000000..0393793bfe1 --- /dev/null +++ b/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_a.vue @@ -0,0 +1,27 @@ + + diff --git a/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_b.vue b/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_b.vue new file mode 100644 index 00000000000..0393793bfe1 --- /dev/null +++ b/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_b.vue @@ -0,0 +1,27 @@ + + diff --git a/app/assets/javascripts/pages/projects/learn_gitlab/constants/index.js b/app/assets/javascripts/pages/projects/learn_gitlab/constants/index.js new file mode 100644 index 00000000000..8606af29785 --- /dev/null +++ b/app/assets/javascripts/pages/projects/learn_gitlab/constants/index.js @@ -0,0 +1,12 @@ +import { s__ } from '~/locale'; + +export const ACTION_TEXT = { + gitWrite: s__('LearnGitLab|Create a repository'), + userAdded: s__('LearnGitLab|Invite your colleagues'), + pipelineCreated: s__('LearnGitLab|Set-up CI/CD'), + trialStarted: s__('LearnGitLab|Start a free trial of GitLab Gold'), + codeOwnersEnabled: s__('LearnGitLab|Add code owners'), + requiredMrApprovalsEnabled: s__('LearnGitLab|Enable require merge approvals'), + mergeRequestCreated: s__('LearnGitLab|Submit a merge request (MR)'), + securityScanEnabled: s__('LearnGitLab|Run a Security scan using CI/CD'), +}; diff --git a/app/assets/javascripts/pages/projects/learn_gitlab/index/index.js b/app/assets/javascripts/pages/projects/learn_gitlab/index/index.js new file mode 100644 index 00000000000..c4dec89b984 --- /dev/null +++ b/app/assets/javascripts/pages/projects/learn_gitlab/index/index.js @@ -0,0 +1,25 @@ +import Vue from 'vue'; +import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; +import LearnGitlabA from '../components/learn_gitlab_a.vue'; +import LearnGitlabB from '../components/learn_gitlab_b.vue'; + +function initLearnGitlab() { + const el = document.getElementById('js-learn-gitlab-app'); + + if (!el) { + return false; + } + + const actions = convertObjectPropsToCamelCase(JSON.parse(el.dataset.actions)); + + const { learnGitlabA } = gon.experiments; + + return new Vue({ + el, + render(createElement) { + return createElement(learnGitlabA ? LearnGitlabA : LearnGitlabB, { props: { actions } }); + }, + }); +} + +initLearnGitlab(); diff --git a/app/assets/javascripts/projects/settings/access_dropdown.js b/app/assets/javascripts/projects/settings/access_dropdown.js index 209ae9927cf..a5e53ee3927 100644 --- a/app/assets/javascripts/projects/settings/access_dropdown.js +++ b/app/assets/javascripts/projects/settings/access_dropdown.js @@ -11,7 +11,6 @@ export default class AccessDropdown { const { $dropdown, accessLevel, accessLevelsData, hasLicense = true } = options; this.options = options; this.hasLicense = hasLicense; - this.deployKeysOnProtectedBranchesEnabled = gon.features.deployKeysOnProtectedBranches; this.groups = []; this.accessLevel = accessLevel; this.accessLevelsData = accessLevelsData.roles; @@ -330,11 +329,7 @@ export default class AccessDropdown { ); }) .catch(() => { - if (this.deployKeysOnProtectedBranchesEnabled) { - createFlash({ message: __('Failed to load groups, users and deploy keys.') }); - } else { - createFlash({ message: __('Failed to load groups & users.') }); - } + createFlash({ message: __('Failed to load groups, users and deploy keys.') }); }); } else { this.getDeployKeys(query) @@ -445,35 +440,33 @@ export default class AccessDropdown { } } - if (this.deployKeysOnProtectedBranchesEnabled) { - const deployKeys = deployKeysResponse.map((response) => { - const { - id, - fingerprint, - title, - owner: { avatar_url, name, username }, - } = response; + const deployKeys = deployKeysResponse.map((response) => { + const { + id, + fingerprint, + title, + owner: { avatar_url, name, username }, + } = response; - const shortFingerprint = `(${fingerprint.substring(0, 14)}...)`; + const shortFingerprint = `(${fingerprint.substring(0, 14)}...)`; - return { - id, - title: title.concat(' ', shortFingerprint), - avatar_url, - fullname: name, - username, - type: LEVEL_TYPES.DEPLOY_KEY, - }; - }); + return { + id, + title: title.concat(' ', shortFingerprint), + avatar_url, + fullname: name, + username, + type: LEVEL_TYPES.DEPLOY_KEY, + }; + }); - if (this.accessLevel === ACCESS_LEVELS.PUSH) { - if (deployKeys.length) { - consolidatedData = consolidatedData.concat( - [{ type: 'divider' }], - [{ type: 'header', content: s__('AccessDropdown|Deploy Keys') }], - deployKeys, - ); - } + if (this.accessLevel === ACCESS_LEVELS.PUSH) { + if (deployKeys.length) { + consolidatedData = consolidatedData.concat( + [{ type: 'divider' }], + [{ type: 'header', content: s__('AccessDropdown|Deploy Keys') }], + deployKeys, + ); } } @@ -501,19 +494,15 @@ export default class AccessDropdown { } getDeployKeys(query) { - if (this.deployKeysOnProtectedBranchesEnabled) { - return axios.get(this.buildUrl(gon.relative_url_root, this.deployKeysPath), { - params: { - search: query, - per_page: 20, - active: true, - project_id: gon.current_project_id, - push_code: true, - }, - }); - } - - return Promise.resolve({ data: [] }); + return axios.get(this.buildUrl(gon.relative_url_root, this.deployKeysPath), { + params: { + search: query, + per_page: 20, + active: true, + project_id: gon.current_project_id, + push_code: true, + }, + }); } buildUrl(urlRoot, url) { diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue index 9ece6a52805..a49eb7fd611 100644 --- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue +++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue @@ -1,6 +1,7 @@