...@@ -174,7 +174,38 @@ review-qa-all: ...@@ -174,7 +174,38 @@ review-qa-all:
script: script:
- export KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/review-qa-all_master_report.json - export KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/review-qa-all_master_report.json
- export KNAPSACK_TEST_FILE_PATTERN=qa/specs/features/**/*_spec.rb - export KNAPSACK_TEST_FILE_PATTERN=qa/specs/features/**/*_spec.rb
- gitlab-qa Test::Instance::Any "${QA_IMAGE}" "${CI_ENVIRONMENT_URL}" - gitlab-qa Test::Instance::Any "${QA_IMAGE}" "${CI_ENVIRONMENT_URL}" -- --format RspecJunitFormatter --out tmp/rspec-${CI_JOB_ID}.xml --format html --out tmp/rspec.htm --color --format documentation
parallel-spec-reports:
extends: .dedicated-runner
dependencies:
- review-qa-all
image: ruby:2.6-alpine
services: []
before_script: []
variables:
SETUP_DB: "false"
NEW_PARALLEL_SPECS_REPORT: qa/report-new.html
BASE_ARTIFACT_URL: "${CI_PROJECT_URL}/-/jobs/${CI_JOB_ID}/artifacts/file/qa/"
stage: post-test
allow_failure: true
when: manual
retry: 0
artifacts:
when: always
paths:
- qa/report-new.html
- qa/gitlab-qa-run-*
reports:
junit: qa/gitlab-qa-run-*/**/rspec-*.xml
script:
- apk add --update build-base libxml2-dev libxslt-dev && rm -rf /var/cache/apk/*
- gem install nokogiri
- cd qa/gitlab-qa-run-*/gitlab-*
- ARTIFACT_DIRS=$(pwd |rev| awk -F / '{print $1,$2}' | rev | sed s_\ _/_)
- cd ../../..
- '[[ -f $NEW_PARALLEL_SPECS_REPORT ]] || echo "{}" > ${NEW_PARALLEL_SPECS_REPORT}'
- scripts/merge-html-reports ${NEW_PARALLEL_SPECS_REPORT} ${BASE_ARTIFACT_URL}${ARTIFACT_DIRS} qa/gitlab-qa-run-*/**/rspec.htm
.review-performance-base: &review-performance-base .review-performance-base: &review-performance-base
<<: *review-qa-base <<: *review-qa-base
... ...
......
...@@ -22,7 +22,7 @@ https://docs.gitlab.com/ce/development/documentation/index.html#changing-documen ...@@ -22,7 +22,7 @@ https://docs.gitlab.com/ce/development/documentation/index.html#changing-documen
- [ ] Make sure internal links pointing to the document in question are not broken. - [ ] Make sure internal links pointing to the document in question are not broken.
- [ ] Search and replace any links referring to old docs in GitLab Rails app, - [ ] Search and replace any links referring to old docs in GitLab Rails app,
specifically under the `app/views/` and `ee/app/views` (for GitLab EE) directories. specifically under the `app/views/` and `ee/app/views` (for GitLab EE) directories.
- [ ] Make sure to add [`redirect_from`](https://docs.gitlab.com/ce/development/writing_documentation.html#redirections-for-pages-with-disqus-comments) - [ ] Make sure to add [`redirect_from`](https://docs.gitlab.com/ce/development/documentation/index.html#redirections-for-pages-with-disqus-comments)
to the new document if there are any Disqus comments on the old document thread. to the new document if there are any Disqus comments on the old document thread.
- [ ] Update the link in `features.yml` (if applicable) - [ ] Update the link in `features.yml` (if applicable)
- [ ] If working on CE and the `ee-compat-check` jobs fails, submit an MR to EE - [ ] If working on CE and the `ee-compat-check` jobs fails, submit an MR to EE
... ...
......
# This is the options file for mdl, configured in .gitlab/ci/docs.gitlab-ci.yml,
# and related to the style file ./mdlrc.style
# See https://github.com/markdownlint/markdownlint/blob/master/docs/configuration.md # See https://github.com/markdownlint/markdownlint/blob/master/docs/configuration.md
ignore_front_matter true ignore_front_matter true
... ...
......
# This is the style file for mdl, configured in .gitlab/ci/docs.gitlab-ci.yml,
# and related to the options file ./mdlrc
# See https://github.com/markdownlint/markdownlint/blob/master/docs/RULES.md # See https://github.com/markdownlint/markdownlint/blob/master/docs/RULES.md
# for more detailed information on the rules and styles.
rule "MD001"
rule "MD003", :style => :atx
rule "MD011"
rule "MD023"
rule "MD032"
rule "MD034"
rule "MD037"
# Should not be used currently:
rule 'MD001' # rule "MD004", :style => :dash # unordered list style - dash
# False positives, see https://github.com/markdownlint/markdownlint/issues/261 # False positives, see https://github.com/markdownlint/markdownlint/issues/261
# rule 'MD004', style: :dash
rule 'MD032' # rule "MD039" # Spaces inside link text
rule 'MD034' # Crashes when link text has certain punctuation
...@@ -100,7 +100,7 @@ gem 'carrierwave', '~> 1.3' ...@@ -100,7 +100,7 @@ gem 'carrierwave', '~> 1.3'
gem 'mini_magick' gem 'mini_magick'
# for backups # for backups
gem 'fog-aws', '~> 3.3' gem 'fog-aws', '~> 3.5'
# Locked until fog-google resolves https://github.com/fog/fog-google/issues/421. # Locked until fog-google resolves https://github.com/fog/fog-google/issues/421.
# Also see config/initializers/fog_core_patch.rb. # Also see config/initializers/fog_core_patch.rb.
gem 'fog-core', '= 2.1.0' gem 'fog-core', '= 2.1.0'
...@@ -431,7 +431,7 @@ group :ed25519 do ...@@ -431,7 +431,7 @@ group :ed25519 do
end end
# Gitaly GRPC client # Gitaly GRPC client
gem 'gitaly-proto', '~> 1.36.0', require: 'gitaly' gem 'gitaly-proto', '~> 1.37.0', require: 'gitaly'
gem 'grpc', '~> 1.19.0' gem 'grpc', '~> 1.19.0'
... ...
......
...@@ -253,7 +253,7 @@ GEM ...@@ -253,7 +253,7 @@ GEM
fog-json fog-json
ipaddress (~> 0.8) ipaddress (~> 0.8)
xml-simple (~> 1.1) xml-simple (~> 1.1)
fog-aws (3.3.0) fog-aws (3.5.2)
fog-core (~> 2.1) fog-core (~> 2.1)
fog-json (~> 1.1) fog-json (~> 1.1)
fog-xml (~> 0.1) fog-xml (~> 0.1)
...@@ -310,7 +310,7 @@ GEM ...@@ -310,7 +310,7 @@ GEM
gettext_i18n_rails (>= 0.7.1) gettext_i18n_rails (>= 0.7.1)
po_to_json (>= 1.0.0) po_to_json (>= 1.0.0)
rails (>= 3.2.0) rails (>= 3.2.0)
gitaly-proto (1.36.0) gitaly-proto (1.37.0)
grpc (~> 1.0) grpc (~> 1.0)
github-markup (1.7.0) github-markup (1.7.0)
gitlab-labkit (0.3.0) gitlab-labkit (0.3.0)
...@@ -1105,7 +1105,7 @@ DEPENDENCIES ...@@ -1105,7 +1105,7 @@ DEPENDENCIES
flipper-active_support_cache_store (~> 0.13.0) flipper-active_support_cache_store (~> 0.13.0)
flowdock (~> 0.7) flowdock (~> 0.7)
fog-aliyun (~> 0.3) fog-aliyun (~> 0.3)
fog-aws (~> 3.3) fog-aws (~> 3.5)
fog-core (= 2.1.0) fog-core (= 2.1.0)
fog-google (~> 1.8) fog-google (~> 1.8)
fog-local (~> 0.6) fog-local (~> 0.6)
...@@ -1119,7 +1119,7 @@ DEPENDENCIES ...@@ -1119,7 +1119,7 @@ DEPENDENCIES
gettext (~> 3.2.2) gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3) gettext_i18n_rails_js (~> 1.3)
gitaly-proto (~> 1.36.0) gitaly-proto (~> 1.37.0)
github-markup (~> 1.7.0) github-markup (~> 1.7.0)
gitlab-labkit (~> 0.3.0) gitlab-labkit (~> 0.3.0)
gitlab-markup (~> 1.7.0) gitlab-markup (~> 1.7.0)
... ...
......
...@@ -15,7 +15,7 @@ To see how GitLab looks please see the [features page on our website](https://ab ...@@ -15,7 +15,7 @@ To see how GitLab looks please see the [features page on our website](https://ab
- Manage Git repositories with fine grained access controls that keep your code secure - Manage Git repositories with fine grained access controls that keep your code secure
- Perform code reviews and enhance collaboration with merge requests - Perform code reviews and enhance collaboration with merge requests
- Complete continuous integration (CI) and CD pipelines to builds, test, and deploy your applications - Complete continuous integration (CI) and continuous deployment/delivery (CD) pipelines to build, test, and deploy your applications
- Each project can also have an issue tracker, issue board, and a wiki - Each project can also have an issue tracker, issue board, and a wiki
- Used by more than 100,000 organizations, GitLab is the most popular solution to manage Git repositories on-premises - Used by more than 100,000 organizations, GitLab is the most popular solution to manage Git repositories on-premises
- Completely free and open source (MIT Expat license) - Completely free and open source (MIT Expat license)
... ...
......
...@@ -36,7 +36,8 @@ export default function renderMermaid($els) { ...@@ -36,7 +36,8 @@ export default function renderMermaid($els) {
}); });
$els.each((i, el) => { $els.each((i, el) => {
const source = el.textContent; // Mermaid doesn't like `<br />` tags, so collapse all like tags into `<br>`, which is parsed correctly.
const source = el.textContent.replace(/<br\s*\/>/g, '<br>');
/** /**
* Restrict the rendering to a certain amount of character to * Restrict the rendering to a certain amount of character to
... ...
......
<script>
import Flash from '~/flash';
import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
import { visitUrl } from '~/lib/utils/url_utility';
import boardsStore from '~/boards/stores/boards_store';
const boardDefaults = {
id: false,
name: '',
labels: [],
milestone_id: undefined,
assignee: {},
assignee_id: undefined,
weight: null,
};
export default {
components: {
BoardScope: () => import('ee_component/boards/components/board_scope.vue'),
DeprecatedModal,
},
props: {
canAdminBoard: {
type: Boolean,
required: true,
},
milestonePath: {
type: String,
required: true,
},
labelsPath: {
type: String,
required: true,
},
scopedIssueBoardFeatureEnabled: {
type: Boolean,
required: false,
default: false,
},
projectId: {
type: Number,
required: false,
default: 0,
},
groupId: {
type: Number,
required: false,
default: 0,
},
weights: {
type: Array,
required: false,
default: () => [],
},
enableScopedLabels: {
type: Boolean,
required: false,
default: false,
},
scopedLabelsDocumentationLink: {
type: String,
required: false,
default: '#',
},
},
data() {
return {
board: { ...boardDefaults, ...this.currentBoard },
currentBoard: boardsStore.state.currentBoard,
currentPage: boardsStore.state.currentPage,
isLoading: false,
};
},
computed: {
isNewForm() {
return this.currentPage === 'new';
},
isDeleteForm() {
return this.currentPage === 'delete';
},
isEditForm() {
return this.currentPage === 'edit';
},
isVisible() {
return this.currentPage !== '';
},
buttonText() {
if (this.isNewForm) {
return 'Create board';
}
if (this.isDeleteForm) {
return 'Delete';
}
return 'Save changes';
},
buttonKind() {
if (this.isNewForm) {
return 'success';
}
if (this.isDeleteForm) {
return 'danger';
}
return 'info';
},
title() {
if (this.isNewForm) {
return 'Create new board';
}
if (this.isDeleteForm) {
return 'Delete board';
}
if (this.readonly) {
return 'Board scope';
}
return 'Edit board';
},
readonly() {
return !this.canAdminBoard;
},
submitDisabled() {
return this.isLoading || this.board.name.length === 0;
},
},
mounted() {
this.resetFormState();
if (this.$refs.name) {
this.$refs.name.focus();
}
},
methods: {
submit() {
if (this.board.name.length === 0) return;
this.isLoading = true;
if (this.isDeleteForm) {
gl.boardService
.deleteBoard(this.currentBoard)
.then(() => {
visitUrl(boardsStore.rootPath);
})
.catch(() => {
Flash('Failed to delete board. Please try again.');
this.isLoading = false;
});
} else {
gl.boardService
.createBoard(this.board)
.then(resp => resp.data)
.then(data => {
visitUrl(data.board_path);
})
.catch(() => {
Flash('Unable to save your changes. Please try again.');
this.isLoading = false;
});
}
},
cancel() {
boardsStore.showPage('');
},
resetFormState() {
if (this.isNewForm) {
// Clear the form when we open the "New board" modal
this.board = { ...boardDefaults };
} else if (this.currentBoard && Object.keys(this.currentBoard).length) {
this.board = { ...boardDefaults, ...this.currentBoard };
}
},
},
};
</script>
<template>
<deprecated-modal
v-show="isVisible"
:hide-footer="readonly"
:title="title"
:primary-button-label="buttonText"
:kind="buttonKind"
:submit-disabled="submitDisabled"
modal-dialog-class="board-config-modal"
@cancel="cancel"
@submit="submit"
>
<template slot="body">
<p v-if="isDeleteForm">Are you sure you want to delete this board?</p>
<form v-else class="js-board-config-modal" @submit.prevent>
<div v-if="!readonly" class="append-bottom-20">
<label class="form-section-title label-bold" for="board-new-name"> Board name </label>
<input
id="board-new-name"
ref="name"
v-model="board.name"
class="form-control"
type="text"
placeholder="Enter board name"
@keyup.enter="submit"
/>
</div>
<board-scope
v-if="scopedIssueBoardFeatureEnabled"
:collapse-scope="isNewForm"
:board="board"
:can-admin-board="canAdminBoard"
:milestone-path="milestonePath"
:labels-path="labelsPath"
:scoped-labels-documentation-link="scopedLabelsDocumentationLink"
:enable-scoped-labels="enableScopedLabels"
:project-id="projectId"
:group-id="groupId"
:weights="weights"
/>
</form>
</template>
</deprecated-modal>
</template>
<script>
import { throttle } from 'underscore';
import {
GlLoadingIcon,
GlSearchBoxByType,
GlDropdown,
GlDropdownDivider,
GlDropdownHeader,
GlDropdownItem,
} from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
import httpStatusCodes from '~/lib/utils/http_status';
import boardsStore from '../stores/boards_store';
import BoardForm from './board_form.vue';
const MIN_BOARDS_TO_VIEW_RECENT = 10;
export default {
name: 'BoardsSelector',
components: {
Icon,
BoardForm,
GlLoadingIcon,
GlSearchBoxByType,
GlDropdown,
GlDropdownDivider,
GlDropdownHeader,
GlDropdownItem,
},
props: {
currentBoard: {
type: Object,
required: true,
},
milestonePath: {
type: String,
required: true,
},
throttleDuration: {
type: Number,
default: 200,
},
boardBaseUrl: {
type: String,
required: true,
},
hasMissingBoards: {
type: Boolean,
required: true,
},
canAdminBoard: {
type: Boolean,
required: true,
},
multipleIssueBoardsAvailable: {
type: Boolean,
required: true,
},
labelsPath: {
type: String,
required: true,
},
projectId: {
type: Number,
required: true,
},
groupId: {
type: Number,
required: true,
},
scopedIssueBoardFeatureEnabled: {
type: Boolean,
required: true,
},
weights: {
type: Array,
required: true,
},
enabledScopedLabels: {
type: Boolean,
required: false,
default: false,
},
scopedLabelsDocumentationLink: {
type: String,
required: false,
default: '#',
},
},
data() {
return {
loading: true,
hasScrollFade: false,
scrollFadeInitialized: false,
boards: [],
recentBoards: [],
state: boardsStore.state,
throttledSetScrollFade: throttle(this.setScrollFade, this.throttleDuration),
contentClientHeight: 0,
maxPosition: 0,
store: boardsStore,
filterTerm: '',
};
},
computed: {
currentPage() {
return this.state.currentPage;
},
filteredBoards() {
return this.boards.filter(board =>
board.name.toLowerCase().includes(this.filterTerm.toLowerCase()),
);
},
reload: {
get() {
return this.state.reload;
},
set(newValue) {
this.state.reload = newValue;
},
},
board() {
return this.state.currentBoard;
},
showDelete() {
return this.boards.length > 1;
},
scrollFadeClass() {
return {
'fade-out': !this.hasScrollFade,
};
},
showRecentSection() {
return (
this.recentBoards.length &&
this.boards.length > MIN_BOARDS_TO_VIEW_RECENT &&
!this.filterTerm.length
);
},
},
watch: {
filteredBoards() {
this.scrollFadeInitialized = false;
this.$nextTick(this.setScrollFade);
},
reload() {
if (this.reload) {
this.boards = [];
this.recentBoards = [];
this.loading = true;
this.reload = false;
this.loadBoards(false);
}
},
},
created() {
boardsStore.setCurrentBoard(this.currentBoard);
},
methods: {
showPage(page) {
boardsStore.showPage(page);
},
loadBoards(toggleDropdown = true) {
if (toggleDropdown && this.boards.length > 0) {
return;
}
const recentBoardsPromise = new Promise((resolve, reject) =>
gl.boardService
.recentBoards()
.then(resolve)
.catch(err => {
/**
* If user is unauthorized we'd still want to resolve the
* request to display all boards.
*/
if (err.response.status === httpStatusCodes.UNAUTHORIZED) {
resolve({ data: [] }); // recent boards are empty
return;
}
reject(err);
}),
);
Promise.all([gl.boardService.allBoards(), recentBoardsPromise])
.then(([allBoards, recentBoards]) => [allBoards.data, recentBoards.data])
.then(([allBoardsJson, recentBoardsJson]) => {
this.loading = false;
this.boards = allBoardsJson;
this.recentBoards = recentBoardsJson;
})
.then(() => this.$nextTick()) // Wait for boards list in DOM
.then(() => {
this.setScrollFade();
})
.catch(() => {
this.loading = false;
});
},
isScrolledUp() {
const { content } = this.$refs;
const currentPosition = this.contentClientHeight + content.scrollTop;
return content && currentPosition < this.maxPosition;
},
initScrollFade() {
this.scrollFadeInitialized = true;
const { content } = this.$refs;
this.contentClientHeight = content.clientHeight;
this.maxPosition = content.scrollHeight;
},
setScrollFade() {
if (!this.scrollFadeInitialized) this.initScrollFade();
this.hasScrollFade = this.isScrolledUp();
},
},
};
</script>
<template>
<div class="boards-switcher js-boards-selector append-right-10">
<span class="boards-selector-wrapper js-boards-selector-wrapper">
<gl-dropdown
toggle-class="dropdown-menu-toggle js-dropdown-toggle"
menu-class="flex-column dropdown-extended-height"
:text="board.name"
@show="loadBoards"
>
<div>
<div class="dropdown-title mb-0" @mousedown.prevent>
{{ s__('IssueBoards|Switch board') }}
</div>
</div>
<gl-dropdown-header class="mt-0">
<gl-search-box-by-type ref="searchBox" v-model="filterTerm" />
</gl-dropdown-header>
<div
v-if="!loading"
ref="content"
class="dropdown-content flex-fill"
@scroll.passive="throttledSetScrollFade"
>
<gl-dropdown-item
v-show="filteredBoards.length === 0"
class="no-pointer-events text-secondary"
>
{{ s__('IssueBoards|No matching boards found') }}
</gl-dropdown-item>
<h6 v-if="showRecentSection" class="dropdown-bold-header my-0">
{{ __('Recent') }}
</h6>
<template v-if="showRecentSection">
<gl-dropdown-item
v-for="recentBoard in recentBoards"
:key="`recent-${recentBoard.id}`"
class="js-dropdown-item"
:href="`${boardBaseUrl}/${recentBoard.id}`"
>
{{ recentBoard.name }}
</gl-dropdown-item>
</template>
<hr v-if="showRecentSection" class="my-1" />
<h6 v-if="showRecentSection" class="dropdown-bold-header my-0">
{{ __('All') }}
</h6>
<gl-dropdown-item
v-for="otherBoard in filteredBoards"
:key="otherBoard.id"
class="js-dropdown-item"
:href="`${boardBaseUrl}/${otherBoard.id}`"
>
{{ otherBoard.name }}
</gl-dropdown-item>
<gl-dropdown-item v-if="hasMissingBoards" class="small unclickable">
{{
s__(
'IssueBoards|Some of your boards are hidden, activate a license to see them again.',
)
}}
</gl-dropdown-item>
</div>
<div
v-show="filteredBoards.length > 0"
class="dropdown-content-faded-mask"
:class="scrollFadeClass"
></div>
<gl-loading-icon v-if="loading" />
<div v-if="canAdminBoard">
<gl-dropdown-divider />
<gl-dropdown-item v-if="multipleIssueBoardsAvailable" @click.prevent="showPage('new')">
{{ s__('IssueBoards|Create new board') }}
</gl-dropdown-item>
<gl-dropdown-item
v-if="showDelete"
class="text-danger"
@click.prevent="showPage('delete')"
>
{{ s__('IssueBoards|Delete board') }}
</gl-dropdown-item>
</div>
</gl-dropdown>
<board-form
v-if="currentPage"
:milestone-path="milestonePath"
:labels-path="labelsPath"
:project-id="projectId"
:group-id="groupId"
:can-admin-board="canAdminBoard"
:scoped-issue-board-feature-enabled="scopedIssueBoardFeatureEnabled"
:weights="weights"
:enable-scoped-labels="enabledScopedLabels"
:scoped-labels-documentation-link="scopedLabelsDocumentationLink"
/>
</span>
</div>
</template>
<script> <script>
import footerEEMixin from 'ee_else_ce/boards/mixins/modal_footer';
import Flash from '../../../flash'; import Flash from '../../../flash';
import { __, n__ } from '../../../locale'; import { __, n__ } from '../../../locale';
import ListsDropdown from './lists_dropdown.vue'; import ListsDropdown from './lists_dropdown.vue';
...@@ -10,7 +11,7 @@ export default { ...@@ -10,7 +11,7 @@ export default {
components: { components: {
ListsDropdown, ListsDropdown,
}, },
mixins: [modalMixin], mixins: [modalMixin, footerEEMixin],
data() { data() {
return { return {
modal: ModalStore.store, modal: ModalStore.store,
... ...
......
import $ from 'jquery'; import $ from 'jquery';
import Vue from 'vue'; import Vue from 'vue';
import mountMultipleBoardsSwitcher from 'ee_else_ce/boards/mount_multiple_boards_switcher';
import Flash from '~/flash'; import Flash from '~/flash';
import { __ } from '~/locale'; import { __ } from '~/locale';
import './models/label'; import './models/label';
...@@ -31,6 +30,7 @@ import { ...@@ -31,6 +30,7 @@ import {
} from '~/lib/utils/common_utils'; } from '~/lib/utils/common_utils';
import boardConfigToggle from 'ee_else_ce/boards/config_toggle'; import boardConfigToggle from 'ee_else_ce/boards/config_toggle';
import toggleFocusMode from 'ee_else_ce/boards/toggle_focus'; import toggleFocusMode from 'ee_else_ce/boards/toggle_focus';
import mountMultipleBoardsSwitcher from './mount_multiple_boards_switcher';
let issueBoardsApp; let issueBoardsApp;
... ...
......
export default {};
// this will be moved from EE to CE as part of https://gitlab.com/gitlab-org/gitlab-ce/issues/53811 import Vue from 'vue';
export default () => {}; import { parseBoolean } from '~/lib/utils/common_utils';
import BoardsSelector from '~/boards/components/boards_selector.vue';
export default () => {
const boardsSwitcherElement = document.getElementById('js-multiple-boards-switcher');
return new Vue({
el: boardsSwitcherElement,
components: {
BoardsSelector,
},
data() {
const { dataset } = boardsSwitcherElement;
const boardsSelectorProps = {
...dataset,
currentBoard: JSON.parse(dataset.currentBoard),
hasMissingBoards: parseBoolean(dataset.hasMissingBoards),
canAdminBoard: parseBoolean(dataset.canAdminBoard),
multipleIssueBoardsAvailable: parseBoolean(dataset.multipleIssueBoardsAvailable),
projectId: Number(dataset.projectId),
groupId: Number(dataset.groupId),
scopedIssueBoardFeatureEnabled: parseBoolean(dataset.scopedIssueBoardFeatureEnabled),
weights: JSON.parse(dataset.weights),
};
return { boardsSelectorProps };
},
render(createElement) {
return createElement(BoardsSelector, {
props: this.boardsSelectorProps,
});
},
});
};
...@@ -374,6 +374,10 @@ const boardsStore = { ...@@ -374,6 +374,10 @@ const boardsStore = {
deleteBoard({ id }) { deleteBoard({ id }) {
return axios.delete(this.generateBoardsPath(id)); return axios.delete(this.generateBoardsPath(id));
}, },
setCurrentBoard(board) {
this.state.currentBoard = board;
},
}; };
BoardsStoreEE.initEESpecific(boardsStore); BoardsStoreEE.initEESpecific(boardsStore);
... ...
......
...@@ -182,7 +182,7 @@ export default class CreateMergeRequestDropdown { ...@@ -182,7 +182,7 @@ export default class CreateMergeRequestDropdown {
} }
enable() { enable() {
if (!canCreateConfidentialMergeRequest()) return; if (isConfidentialIssue() && !canCreateConfidentialMergeRequest()) return;
this.createMergeRequestButton.classList.remove('disabled'); this.createMergeRequestButton.classList.remove('disabled');
this.createMergeRequestButton.removeAttribute('disabled'); this.createMergeRequestButton.removeAttribute('disabled');
... ...
......
...@@ -67,6 +67,18 @@ export default { ...@@ -67,6 +67,18 @@ export default {
errorMessage() { errorMessage() {
return this.file.viewer.error_message; return this.file.viewer.error_message;
}, },
forkMessage() {
return sprintf(
__(
"You're not allowed to %{tag_start}edit%{tag_end} files in this project directly. Please fork this project, make your changes there, and submit a merge request.",
),
{
tag_start: '<span class="js-file-fork-suggestion-section-action">',
tag_end: '</span>',
},
false,
);
},
}, },
watch: { watch: {
isCollapsed: function fileCollapsedWatch(newVal, oldVal) { isCollapsed: function fileCollapsedWatch(newVal, oldVal) {
...@@ -150,12 +162,7 @@ export default { ...@@ -150,12 +162,7 @@ export default {
/> />
<div v-if="forkMessageVisible" class="js-file-fork-suggestion-section file-fork-suggestion"> <div v-if="forkMessageVisible" class="js-file-fork-suggestion-section file-fork-suggestion">
<span class="file-fork-suggestion-note"> <span class="file-fork-suggestion-note" v-html="forkMessage"></span>
{{ sprintf(__("You're not allowed to %{tag_start}edit%{tag_end} files in this project
directly. Please fork this project, make your changes there, and submit a merge request."),
{ tag_start: '<span class="js-file-fork-suggestion-section-action">', tag_end: '</span>' })
}}
</span>
<a <a
:href="file.fork_path" :href="file.fork_path"
class="js-fork-suggestion-button btn btn-grouped btn-inverted btn-success" class="js-fork-suggestion-button btn btn-grouped btn-inverted btn-success"
... ...
......
...@@ -55,6 +55,11 @@ export default { ...@@ -55,6 +55,11 @@ export default {
required: false, required: false,
default: true, default: true,
}, },
zoomMeetingUrl: {
type: String,
required: false,
default: null,
},
issuableRef: { issuableRef: {
type: String, type: String,
required: true, required: true,
...@@ -342,7 +347,7 @@ export default { ...@@ -342,7 +347,7 @@ export default {
:title-text="state.titleText" :title-text="state.titleText"
:show-inline-edit-button="showInlineEditButton" :show-inline-edit-button="showInlineEditButton"
/> />
<pinned-links :description-html="state.descriptionHtml" /> <pinned-links :zoom-meeting-url="zoomMeetingUrl" />
<description-component <description-component
v-if="state.descriptionHtml" v-if="state.descriptionHtml"
:can-update="canUpdate" :can-update="canUpdate"
... ...
......
<script> <script>
import { __, sprintf } from '~/locale';
export default { export default {
computed: { computed: {
currentPath() { currentPath() {
return window.location.pathname; return window.location.pathname;
}, },
alertMessage() {
return sprintf(
__(
'Someone edited the issue at the same time you did. Please check out %{linkStart}the issue%{linkEnd} and make sure your changes will not unintentionally remove theirs.',
),
{
linkStart: `<a href="${this.currentPath}" target="_blank" rel="nofollow">`,
linkEnd: `</a>`,
},
false,
);
},
}, },
}; };
</script> </script>
<template> <template>
<div class="alert alert-danger"> <div class="alert alert-danger" v-html="alertMessage"></div>
{{ sprintf(__("Someone edited the issue at the same time you did. Please check out
%{linkStart}%the issue%{linkEnd} and make sure your changes will not unintentionally remove
theirs."), { linkStart: `<a href="${currentPath}" target="_blank" rel="nofollow">` linkEnd: '</a
>', }) }}
</div>
</template> </template>
...@@ -8,40 +8,19 @@ export default { ...@@ -8,40 +8,19 @@ export default {
GlLink, GlLink,
}, },
props: { props: {
descriptionHtml: { zoomMeetingUrl: {
type: String, type: String,
required: true, required: false,
}, default: null,
},
computed: {
linksInDescription() {
const el = document.createElement('div');
el.innerHTML = this.descriptionHtml;
return [...el.querySelectorAll('a')].map(a => a.href);
},
// Detect links matching the following formats:
// Zoom Start links: https://zoom.us/s/<meeting-id>
// Zoom Join links: https://zoom.us/j/<meeting-id>
// Personal Zoom links: https://zoom.us/my/<meeting-id>
// Vanity Zoom links: https://gitlab.zoom.us/j/<meeting-id> (also /s and /my)
zoomHref() {
const zoomRegex = /^https:\/\/([\w\d-]+\.)?zoom\.us\/(s|j|my)\/.+/;
return this.linksInDescription.reduce((acc, currentLink) => {
let lastLink = acc;
if (zoomRegex.test(currentLink)) {
lastLink = currentLink;
}
return lastLink;
}, '');
}, },
}, },
}; };
</script> </script>
<template> <template>
<div v-if="zoomHref" class="border-bottom mb-3 mt-n2"> <div v-if="zoomMeetingUrl" class="border-bottom mb-3 mt-n2">
<gl-link <gl-link
:href="zoomHref" :href="zoomMeetingUrl"
target="_blank" target="_blank"
class="btn btn-inverted btn-secondary btn-sm text-dark mb-3" class="btn btn-inverted btn-secondary btn-sm text-dark mb-3"
> >
... ...
......