diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-02-09 06:09:18 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-02-09 06:09:18 +0300 |
commit | 9c8d620e48c59fe3d10f9c4b50f91124d7c09182 (patch) | |
tree | c629ebcedd29c2ca756af2367218f6723ac3d58d /app | |
parent | 1c0289261b8d67e983b5d3ed1ef23fd800deab98 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
21 files changed, 739 insertions, 154 deletions
diff --git a/app/assets/javascripts/boards/components/board_form.vue b/app/assets/javascripts/boards/components/board_form.vue index 879f62ee6ff..b5f6d03d6d3 100644 --- a/app/assets/javascripts/boards/components/board_form.vue +++ b/app/assets/javascripts/boards/components/board_form.vue @@ -5,8 +5,8 @@ import { deprecatedCreateFlash as Flash } from '~/flash'; import { visitUrl } from '~/lib/utils/url_utility'; import { getParameterByName } from '~/lib/utils/common_utils'; import { convertToGraphQLId } from '~/graphql_shared/utils'; -import boardsStore from '~/boards/stores/boards_store'; import { fullLabelId, fullBoardId } from '../boards_util'; +import { formType } from '../constants'; import updateBoardMutation from '../graphql/board_update.mutation.graphql'; import createBoardMutation from '../graphql/board_create.mutation.graphql'; @@ -26,12 +26,6 @@ const boardDefaults = { hide_closed_list: false, }; -const formType = { - new: 'new', - delete: 'delete', - edit: 'edit', -}; - export default { i18n: { [formType.new]: { title: s__('Board|Create new board'), btnText: s__('Board|Create board') }, @@ -100,11 +94,14 @@ export default { type: Object, required: true, }, + currentPage: { + type: String, + required: true, + }, }, data() { return { board: { ...boardDefaults, ...this.currentBoard }, - currentPage: boardsStore.state.currentPage, isLoading: false, }; }, @@ -256,7 +253,7 @@ export default { } }, cancel() { - boardsStore.showPage(''); + this.$emit('cancel'); }, resetFormState() { if (this.isNewForm) { diff --git a/app/assets/javascripts/boards/components/boards_selector.vue b/app/assets/javascripts/boards/components/boards_selector.vue index fcd1c3fdceb..da76d21fe39 100644 --- a/app/assets/javascripts/boards/components/boards_selector.vue +++ b/app/assets/javascripts/boards/components/boards_selector.vue @@ -12,11 +12,12 @@ import { import httpStatusCodes from '~/lib/utils/http_status'; +import axios from '~/lib/utils/axios_utils'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import projectQuery from '../graphql/project_boards.query.graphql'; import groupQuery from '../graphql/group_boards.query.graphql'; -import boardsStore from '../stores/boards_store'; +import eventHub from '../eventhub'; import BoardForm from './board_form.vue'; const MIN_BOARDS_TO_VIEW_RECENT = 10; @@ -35,6 +36,7 @@ export default { directives: { GlModalDirective, }, + inject: ['fullPath', 'recentBoardsEndpoint'], props: { currentBoard: { type: Object, @@ -99,12 +101,11 @@ export default { scrollFadeInitialized: false, boards: [], recentBoards: [], - state: boardsStore.state, throttledSetScrollFade: throttle(this.setScrollFade, this.throttleDuration), contentClientHeight: 0, maxPosition: 0, - store: boardsStore, filterTerm: '', + currentPage: '', }; }, computed: { @@ -114,16 +115,13 @@ export default { loading() { return this.loadingRecentBoards || Boolean(this.loadingBoards); }, - currentPage() { - return this.state.currentPage; - }, filteredBoards() { return this.boards.filter((board) => board.name.toLowerCase().includes(this.filterTerm.toLowerCase()), ); }, board() { - return this.state.currentBoard; + return this.currentBoard; }, showDelete() { return this.boards.length > 1; @@ -148,11 +146,17 @@ export default { }, }, created() { - boardsStore.setCurrentBoard(this.currentBoard); + eventHub.$on('showBoardModal', this.showPage); + }, + beforeDestroy() { + eventHub.$off('showBoardModal', this.showPage); }, methods: { showPage(page) { - boardsStore.showPage(page); + this.currentPage = page; + }, + cancel() { + this.showPage(''); }, loadBoards(toggleDropdown = true) { if (toggleDropdown && this.boards.length > 0) { @@ -161,7 +165,7 @@ export default { this.$apollo.addSmartQuery('boards', { variables() { - return { fullPath: this.state.endpoints.fullPath }; + return { fullPath: this.fullPath }; }, query() { return this.groupId ? groupQuery : projectQuery; @@ -179,8 +183,10 @@ export default { }); this.loadingRecentBoards = true; - boardsStore - .recentBoards() + // Follow up to fetch recent boards using GraphQL + // https://gitlab.com/gitlab-org/gitlab/-/issues/300985 + axios + .get(this.recentBoardsEndpoint) .then((res) => { this.recentBoards = res.data; }) @@ -346,6 +352,8 @@ export default { :weights="weights" :enable-scoped-labels="enabledScopedLabels" :current-board="currentBoard" + :current-page="currentPage" + @cancel="cancel" /> </span> </div> diff --git a/app/assets/javascripts/boards/components/boards_selector_deprecated.vue b/app/assets/javascripts/boards/components/boards_selector_deprecated.vue new file mode 100644 index 00000000000..3483f5c1281 --- /dev/null +++ b/app/assets/javascripts/boards/components/boards_selector_deprecated.vue @@ -0,0 +1,357 @@ +<script> +import { throttle } from 'lodash'; +import { + GlLoadingIcon, + GlSearchBoxByType, + GlDropdown, + GlDropdownDivider, + GlDropdownSectionHeader, + GlDropdownItem, + GlModalDirective, +} from '@gitlab/ui'; + +import httpStatusCodes from '~/lib/utils/http_status'; + +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; +import projectQuery from '../graphql/project_boards.query.graphql'; +import groupQuery from '../graphql/group_boards.query.graphql'; + +import boardsStore from '../stores/boards_store'; +import BoardForm from './board_form.vue'; + +const MIN_BOARDS_TO_VIEW_RECENT = 10; + +export default { + name: 'BoardsSelector', + components: { + BoardForm, + GlLoadingIcon, + GlSearchBoxByType, + GlDropdown, + GlDropdownDivider, + GlDropdownSectionHeader, + GlDropdownItem, + }, + directives: { + GlModalDirective, + }, + props: { + currentBoard: { + type: Object, + required: true, + }, + throttleDuration: { + type: Number, + default: 200, + required: false, + }, + 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, + }, + labelsWebUrl: { + 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, + }, + }, + data() { + return { + hasScrollFade: false, + loadingBoards: 0, + loadingRecentBoards: false, + scrollFadeInitialized: false, + boards: [], + recentBoards: [], + state: boardsStore.state, + throttledSetScrollFade: throttle(this.setScrollFade, this.throttleDuration), + contentClientHeight: 0, + maxPosition: 0, + store: boardsStore, + filterTerm: '', + }; + }, + computed: { + parentType() { + return this.groupId ? 'group' : 'project'; + }, + loading() { + return this.loadingRecentBoards || Boolean(this.loadingBoards); + }, + currentPage() { + return this.state.currentPage; + }, + filteredBoards() { + return this.boards.filter((board) => + board.name.toLowerCase().includes(this.filterTerm.toLowerCase()), + ); + }, + 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); + }, + }, + created() { + boardsStore.setCurrentBoard(this.currentBoard); + }, + methods: { + showPage(page) { + boardsStore.showPage(page); + }, + cancel() { + this.showPage(''); + }, + loadBoards(toggleDropdown = true) { + if (toggleDropdown && this.boards.length > 0) { + return; + } + + this.$apollo.addSmartQuery('boards', { + variables() { + return { fullPath: this.state.endpoints.fullPath }; + }, + query() { + return this.groupId ? groupQuery : projectQuery; + }, + loadingKey: 'loadingBoards', + update(data) { + if (!data?.[this.parentType]) { + return []; + } + return data[this.parentType].boards.edges.map(({ node }) => ({ + id: getIdFromGraphQLId(node.id), + name: node.name, + })); + }, + }); + + this.loadingRecentBoards = true; + boardsStore + .recentBoards() + .then((res) => { + this.recentBoards = res.data; + }) + .catch((err) => { + /** + * If user is unauthorized we'd still want to resolve the + * request to display all boards. + */ + if (err?.response?.status === httpStatusCodes.UNAUTHORIZED) { + this.recentBoards = []; // recent boards are empty + return; + } + throw err; + }) + .then(() => this.$nextTick()) // Wait for boards list in DOM + .then(() => { + this.setScrollFade(); + }) + .catch(() => {}) + .finally(() => { + this.loadingRecentBoards = false; + }); + }, + isScrolledUp() { + const { content } = this.$refs; + + if (!content) { + return false; + } + + const currentPosition = this.contentClientHeight + content.scrollTop; + + return currentPosition < this.maxPosition; + }, + initScrollFade() { + const { content } = this.$refs; + + if (!content) { + return; + } + + this.scrollFadeInitialized = true; + + 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 gl-mr-3"> + <span class="boards-selector-wrapper js-boards-selector-wrapper"> + <gl-dropdown + data-qa-selector="boards_dropdown" + toggle-class="dropdown-menu-toggle js-dropdown-toggle" + menu-class="flex-column dropdown-extended-height" + :text="board.name" + @show="loadBoards" + > + <p class="gl-new-dropdown-header-top" @mousedown.prevent> + {{ s__('IssueBoards|Switch board') }} + </p> + <gl-search-box-by-type ref="searchBox" v-model="filterTerm" class="m-2" /> + + <div + v-if="!loading" + ref="content" + data-qa-selector="boards_dropdown_content" + class="dropdown-content flex-fill" + @scroll.passive="throttledSetScrollFade" + > + <gl-dropdown-item + v-show="filteredBoards.length === 0" + class="gl-pointer-events-none text-secondary" + > + {{ s__('IssueBoards|No matching boards found') }} + </gl-dropdown-item> + + <gl-dropdown-section-header v-if="showRecentSection"> + {{ __('Recent') }} + </gl-dropdown-section-header> + + <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> + + <gl-dropdown-divider v-if="showRecentSection" /> + + <gl-dropdown-section-header v-if="showRecentSection"> + {{ __('All') }} + </gl-dropdown-section-header> + + <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="no-pointer-events"> + {{ + 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" + v-gl-modal-directive="'board-config-modal'" + data-qa-selector="create_new_board_button" + @click.prevent="showPage('new')" + > + {{ s__('IssueBoards|Create new board') }} + </gl-dropdown-item> + + <gl-dropdown-item + v-if="showDelete" + v-gl-modal-directive="'board-config-modal'" + class="text-danger js-delete-board" + @click.prevent="showPage('delete')" + > + {{ s__('IssueBoards|Delete board') }} + </gl-dropdown-item> + </div> + </gl-dropdown> + + <board-form + v-if="currentPage" + :labels-path="labelsPath" + :labels-web-url="labelsWebUrl" + :project-id="projectId" + :group-id="groupId" + :can-admin-board="canAdminBoard" + :scoped-issue-board-feature-enabled="scopedIssueBoardFeatureEnabled" + :weights="weights" + :enable-scoped-labels="enabledScopedLabels" + :current-board="currentBoard" + :current-page="state.currentPage" + @cancel="cancel" + /> + </span> + </div> +</template> diff --git a/app/assets/javascripts/boards/constants.js b/app/assets/javascripts/boards/constants.js index 723aef4875d..f45c5d8fbcd 100644 --- a/app/assets/javascripts/boards/constants.js +++ b/app/assets/javascripts/boards/constants.js @@ -21,6 +21,12 @@ export const ListTypeTitles = { label: __('Label'), }; +export const formType = { + new: 'new', + delete: 'delete', + edit: 'edit', +}; + export const inactiveId = 0; export const ISSUABLE = 'issuable'; diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js index 5e8dd81438b..9a86e4ae3eb 100644 --- a/app/assets/javascripts/boards/index.js +++ b/app/assets/javascripts/boards/index.js @@ -358,5 +358,6 @@ export default () => { mountMultipleBoardsSwitcher({ fullPath: $boardApp.dataset.fullPath, rootPath: $boardApp.dataset.boardsEndpoint, + recentBoardsEndpoint: $boardApp.dataset.recentBoardsEndpoint, }); }; diff --git a/app/assets/javascripts/boards/mount_multiple_boards_switcher.js b/app/assets/javascripts/boards/mount_multiple_boards_switcher.js index 738c8fb927e..fda56546697 100644 --- a/app/assets/javascripts/boards/mount_multiple_boards_switcher.js +++ b/app/assets/javascripts/boards/mount_multiple_boards_switcher.js @@ -1,8 +1,12 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; +import { mapGetters } from 'vuex'; +import store from '~/boards/stores'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import createDefaultClient from '~/lib/graphql'; import { parseBoolean } from '~/lib/utils/common_utils'; import BoardsSelector from '~/boards/components/boards_selector.vue'; +import BoardsSelectorDeprecated from '~/boards/components/boards_selector_deprecated.vue'; Vue.use(VueApollo); @@ -16,11 +20,15 @@ export default (params = {}) => { el: boardsSwitcherElement, components: { BoardsSelector, + BoardsSelectorDeprecated, }, + mixins: [glFeatureFlagMixin()], apolloProvider, + store, provide: { fullPath: params.fullPath, rootPath: params.rootPath, + recentBoardsEndpoint: params.recentBoardsEndpoint, }, data() { const { dataset } = boardsSwitcherElement; @@ -39,8 +47,16 @@ export default (params = {}) => { return { boardsSelectorProps }; }, + computed: { + ...mapGetters(['shouldUseGraphQL']), + }, render(createElement) { - return createElement(BoardsSelector, { + if (this.shouldUseGraphQL) { + return createElement(BoardsSelector, { + props: this.boardsSelectorProps, + }); + } + return createElement(BoardsSelectorDeprecated, { props: this.boardsSelectorProps, }); }, diff --git a/app/graphql/mutations/boards/lists/base.rb b/app/graphql/mutations/boards/lists/base.rb deleted file mode 100644 index 34c138bddc9..00000000000 --- a/app/graphql/mutations/boards/lists/base.rb +++ /dev/null @@ -1,28 +0,0 @@ -# frozen_string_literal: true - -module Mutations - module Boards - module Lists - class Base < BaseMutation - include Mutations::ResolvesIssuable - - argument :board_id, ::Types::GlobalIDType[::Board], - required: true, - description: 'Global ID of the issue board to mutate.' - - field :list, - Types::BoardListType, - null: true, - description: 'List of the issue board.' - - authorize :admin_list - - private - - def find_object(id:) - GitlabSchema.object_from_id(id, expected_type: ::Board) - end - end - end - end -end diff --git a/app/graphql/mutations/boards/lists/base_create.rb b/app/graphql/mutations/boards/lists/base_create.rb new file mode 100644 index 00000000000..a21c7feece3 --- /dev/null +++ b/app/graphql/mutations/boards/lists/base_create.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +module Mutations + module Boards + module Lists + class BaseCreate < BaseMutation + argument :backlog, GraphQL::BOOLEAN_TYPE, + required: false, + description: 'Create the backlog list.' + + argument :label_id, ::Types::GlobalIDType[::Label], + required: false, + description: 'Global ID of an existing label.' + + def ready?(**args) + if args.slice(*mutually_exclusive_args).size != 1 + arg_str = mutually_exclusive_args.map { |x| x.to_s.camelize(:lower) }.join(' or ') + raise Gitlab::Graphql::Errors::ArgumentError, "one and only one of #{arg_str} is required" + end + + super + end + + def resolve(**args) + board = authorized_find!(id: args[:board_id]) + params = create_list_params(args) + + response = create_list(board, params) + + { + list: response.success? ? response.payload[:list] : nil, + errors: response.errors + } + end + + private + + def create_list(board, params) + raise NotImplementedError + end + + def create_list_params(args) + params = args.slice(*mutually_exclusive_args).with_indifferent_access + params[:label_id] &&= ::GitlabSchema.parse_gid(params[:label_id], expected_type: ::Label).model_id + + params + end + + def mutually_exclusive_args + [:backlog, :label_id] + end + end + end + end +end diff --git a/app/graphql/mutations/boards/lists/create.rb b/app/graphql/mutations/boards/lists/create.rb index 9eb9a4d4b87..f3aae9ac9c8 100644 --- a/app/graphql/mutations/boards/lists/create.rb +++ b/app/graphql/mutations/boards/lists/create.rb @@ -3,59 +3,32 @@ module Mutations module Boards module Lists - class Create < Base + class Create < BaseCreate graphql_name 'BoardListCreate' - argument :backlog, GraphQL::BOOLEAN_TYPE, - required: false, - description: 'Create the backlog list.' + argument :board_id, ::Types::GlobalIDType[::Board], + required: true, + description: 'Global ID of the issue board to mutate.' - argument :label_id, ::Types::GlobalIDType[::Label], - required: false, - description: 'Global ID of an existing label.' + field :list, + Types::BoardListType, + null: true, + description: 'Issue list in the issue board.' - def ready?(**args) - if args.slice(*mutually_exclusive_args).size != 1 - arg_str = mutually_exclusive_args.map { |x| x.to_s.camelize(:lower) }.join(' or ') - raise Gitlab::Graphql::Errors::ArgumentError, "one and only one of #{arg_str} is required" - end + authorize :admin_list - super - end - - def resolve(**args) - board = authorized_find!(id: args[:board_id]) - params = create_list_params(args) - - response = create_list(board, params) + private - { - list: response.success? ? response.payload[:list] : nil, - errors: response.errors - } + def find_object(id:) + GitlabSchema.object_from_id(id, expected_type: ::Board) end - private - def create_list(board, params) create_list_service = ::Boards::Lists::CreateService.new(board.resource_parent, current_user, params) create_list_service.execute(board) end - - # Overridden in EE - def create_list_params(args) - params = args.slice(*mutually_exclusive_args).with_indifferent_access - params[:label_id] &&= ::GitlabSchema.parse_gid(params[:label_id], expected_type: ::Label).model_id - - params - end - - # Overridden in EE - def mutually_exclusive_args - [:backlog, :label_id] - end end end end diff --git a/app/models/concerns/packages/debian/architecture.rb b/app/models/concerns/packages/debian/architecture.rb index 4aa633e0357..760ebb49980 100644 --- a/app/models/concerns/packages/debian/architecture.rb +++ b/app/models/concerns/packages/debian/architecture.rb @@ -7,6 +7,12 @@ module Packages included do belongs_to :distribution, class_name: "Packages::Debian::#{container_type.capitalize}Distribution", inverse_of: :architectures + # files must be destroyed by ruby code in order to properly remove carrierwave uploads + has_many :files, + class_name: "Packages::Debian::#{container_type.capitalize}ComponentFile", + foreign_key: :architecture_id, + inverse_of: :architecture, + dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent validates :distribution, presence: true diff --git a/app/models/concerns/packages/debian/component.rb b/app/models/concerns/packages/debian/component.rb index e37110231ce..7b342c7b684 100644 --- a/app/models/concerns/packages/debian/component.rb +++ b/app/models/concerns/packages/debian/component.rb @@ -7,6 +7,12 @@ module Packages included do belongs_to :distribution, class_name: "Packages::Debian::#{container_type.capitalize}Distribution", inverse_of: :components + # files must be destroyed by ruby code in order to properly remove carrierwave uploads + has_many :files, + class_name: "Packages::Debian::#{container_type.capitalize}ComponentFile", + foreign_key: :component_id, + inverse_of: :component, + dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent validates :distribution, presence: true diff --git a/app/models/concerns/packages/debian/component_file.rb b/app/models/concerns/packages/debian/component_file.rb new file mode 100644 index 00000000000..3cc2c291e96 --- /dev/null +++ b/app/models/concerns/packages/debian/component_file.rb @@ -0,0 +1,101 @@ +# frozen_string_literal: true + +module Packages + module Debian + module ComponentFile + extend ActiveSupport::Concern + + included do + include Sortable + include FileStoreMounter + + def self.container_foreign_key + "#{container_type}_id".to_sym + end + + def self.distribution_class + "::Packages::Debian::#{container_type.capitalize}Distribution".constantize + end + + belongs_to :component, class_name: "Packages::Debian::#{container_type.capitalize}Component", inverse_of: :files + belongs_to :architecture, class_name: "Packages::Debian::#{container_type.capitalize}Architecture", inverse_of: :files, optional: true + + enum file_type: { packages: 1, source: 2, di_packages: 3 } + enum compression_type: { gz: 1, bz2: 2, xz: 3 } + + validates :component, presence: true + validates :file_type, presence: true + validates :architecture, presence: true, unless: :source? + validates :architecture, absence: true, if: :source? + validates :file, length: { minimum: 0, allow_nil: false } + validates :size, presence: true + validates :file_store, presence: true + validates :file_md5, presence: true + validates :file_sha256, presence: true + + scope :with_container, ->(container) do + joins(component: :distribution) + .where("packages_debian_#{container_type}_distributions" => { container_foreign_key => container.id }) + end + + scope :with_codename_or_suite, ->(codename_or_suite) do + joins(component: :distribution) + .merge(distribution_class.with_codename_or_suite(codename_or_suite)) + end + + scope :with_component_name, ->(component_name) do + joins(:component) + .where("packages_debian_#{container_type}_components" => { name: component_name }) + end + + scope :with_file_type, ->(file_type) { where(file_type: file_type) } + + scope :with_architecture_name, ->(architecture_name) do + left_outer_joins(:architecture) + .where("packages_debian_#{container_type}_architectures" => { name: architecture_name }) + end + + scope :with_compression_type, ->(compression_type) { where(compression_type: compression_type) } + scope :with_file_sha256, ->(file_sha256) { where(file_sha256: file_sha256) } + + scope :preload_distribution, -> { includes(component: :distribution) } + + mount_file_store_uploader Packages::Debian::ComponentFileUploader + + before_validation :update_size_from_file + + def file_name + case file_type + when 'di_packages' + 'Packages' + else + file_type.capitalize + end + end + + def relative_path + case file_type + when 'packages' + "#{component.name}/binary-#{architecture.name}/#{file_name}#{extension}" + when 'source' + "#{component.name}/source/#{file_name}#{extension}" + when 'di_packages' + "#{component.name}/debian-installer/binary-#{architecture.name}/#{file_name}#{extension}" + end + end + + private + + def extension + return '' unless compression_type + + ".#{compression_type}" + end + + def update_size_from_file + self.size ||= file.size + end + end + end + end +end diff --git a/app/models/concerns/packages/debian/distribution.rb b/app/models/concerns/packages/debian/distribution.rb index 546d866d670..08fb9ccf3ea 100644 --- a/app/models/concerns/packages/debian/distribution.rb +++ b/app/models/concerns/packages/debian/distribution.rb @@ -18,10 +18,16 @@ module Packages belongs_to container_type belongs_to :creator, class_name: 'User' + # component_files must be destroyed by ruby code in order to properly remove carrierwave uploads has_many :components, class_name: "Packages::Debian::#{container_type.capitalize}Component", foreign_key: :distribution_id, - inverse_of: :distribution + inverse_of: :distribution, + dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent + has_many :component_files, + through: :components, + source: :files, + class_name: "Packages::Debian::#{container_type.capitalize}ComponentFile" has_many :architectures, class_name: "Packages::Debian::#{container_type.capitalize}Architecture", foreign_key: :distribution_id, diff --git a/app/models/group.rb b/app/models/group.rb index aa79d379fac..ed8ce67015b 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -75,7 +75,7 @@ class Group < Namespace has_many :dependency_proxy_blobs, class_name: 'DependencyProxy::Blob' has_many :dependency_proxy_manifests, class_name: 'DependencyProxy::Manifest' - # debian_distributions must be destroyed by ruby code in order to properly remove carrierwave uploads + # debian_distributions and associated component_files must be destroyed by ruby code in order to properly remove carrierwave uploads has_many :debian_distributions, class_name: 'Packages::Debian::GroupDistribution', dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent accepts_nested_attributes_for :variables, allow_destroy: true diff --git a/app/models/operations/feature_flag.rb b/app/models/operations/feature_flag.rb index 442f9d36c43..be3f719ddb3 100644 --- a/app/models/operations/feature_flag.rb +++ b/app/models/operations/feature_flag.rb @@ -6,6 +6,7 @@ module Operations include AtomicInternalId include IidRoutes include Limitable + include Referable self.table_name = 'operations_feature_flags' self.limit_scope = :project @@ -65,6 +66,31 @@ module Operations .reorder(:id) .references(:operations_scopes) end + + def reference_prefix + '[feature_flag:' + end + + def reference_pattern + @reference_pattern ||= %r{ + #{Regexp.escape(reference_prefix)}(#{::Project.reference_pattern}\/)?(?<feature_flag>\d+)#{Regexp.escape(reference_postfix)} + }x + end + + def link_reference_pattern + @link_reference_pattern ||= super("feature_flags", /(?<feature_flag>\d+)\/edit/) + end + + def reference_postfix + ']' + end + end + + def to_reference(from = nil, full: false) + project + .to_reference_base(from, full: full) + .then { |reference_base| reference_base.present? ? "#{reference_base}/" : nil } + .then { |reference_base| "#{self.class.reference_prefix}#{reference_base}#{iid}#{self.class.reference_postfix}" } end def related_issues(current_user, preload:) diff --git a/app/models/packages/debian/group_component_file.rb b/app/models/packages/debian/group_component_file.rb new file mode 100644 index 00000000000..333aab044a4 --- /dev/null +++ b/app/models/packages/debian/group_component_file.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class Packages::Debian::GroupComponentFile < ApplicationRecord + def self.container_type + :group + end + + include Packages::Debian::ComponentFile +end diff --git a/app/models/packages/debian/project_component_file.rb b/app/models/packages/debian/project_component_file.rb new file mode 100644 index 00000000000..60ac29f91c2 --- /dev/null +++ b/app/models/packages/debian/project_component_file.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class Packages::Debian::ProjectComponentFile < ApplicationRecord + def self.container_type + :project + end + + include Packages::Debian::ComponentFile +end diff --git a/app/models/project.rb b/app/models/project.rb index ab70ed56913..d2b996b6911 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -200,7 +200,7 @@ class Project < ApplicationRecord # Packages has_many :packages, class_name: 'Packages::Package' has_many :package_files, through: :packages, class_name: 'Packages::PackageFile' - # debian_distributions must be destroyed by ruby code in order to properly remove carrierwave uploads + # debian_distributions and associated component_files must be destroyed by ruby code in order to properly remove carrierwave uploads has_many :debian_distributions, class_name: 'Packages::Debian::ProjectDistribution', dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_one :import_state, autosave: true, class_name: 'ProjectImportState', inverse_of: :project diff --git a/app/services/boards/lists/base_create_service.rb b/app/services/boards/lists/base_create_service.rb new file mode 100644 index 00000000000..8399b1cc149 --- /dev/null +++ b/app/services/boards/lists/base_create_service.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +module Boards + module Lists + # This class is used by issue and epic board lists + # for creating new list + class BaseCreateService < Boards::BaseService + include Gitlab::Utils::StrongMemoize + + def execute(board) + list = case type + when :backlog + create_backlog(board) + else + target = target(board) + position = next_position(board) + + return ServiceResponse.error(message: _('%{board_target} not found') % { board_target: type.to_s.capitalize }) if target.blank? + + create_list(board, type, target, position) + end + + return ServiceResponse.error(message: list.errors.full_messages) unless list.persisted? + + ServiceResponse.success(payload: { list: list }) + end + + private + + def type + # We don't ever expect to have more than one list + # type param at once. + if params.key?('backlog') + :backlog + else + :label + end + end + + def target(board) + strong_memoize(:target) do + available_labels.find_by(id: params[:label_id]) # rubocop: disable CodeReuse/ActiveRecord + end + end + + def available_labels + ::Labels::AvailableLabelsService.new(current_user, parent, {}) + .available_labels + end + + def next_position(board) + max_position = board.lists.movable.maximum(:position) + max_position.nil? ? 0 : max_position.succ + end + + def create_list(board, type, target, position) + board.lists.create(create_list_attributes(type, target, position)) + end + + def create_list_attributes(type, target, position) + { type => target, list_type: type, position: position } + end + + def create_backlog(board) + return board.lists.backlog.first if board.lists.backlog.exists? + + board.lists.create(list_type: :backlog, position: nil) + end + end + end +end diff --git a/app/services/boards/lists/create_service.rb b/app/services/boards/lists/create_service.rb index a21ceee083f..37fe0a815bd 100644 --- a/app/services/boards/lists/create_service.rb +++ b/app/services/boards/lists/create_service.rb @@ -2,68 +2,7 @@ module Boards module Lists - class CreateService < Boards::BaseService - include Gitlab::Utils::StrongMemoize - - def execute(board) - list = case type - when :backlog - create_backlog(board) - else - target = target(board) - position = next_position(board) - - return ServiceResponse.error(message: _('%{board_target} not found') % { board_target: type.to_s.capitalize }) if target.blank? - - create_list(board, type, target, position) - end - - return ServiceResponse.error(message: list.errors.full_messages) unless list.persisted? - - ServiceResponse.success(payload: { list: list }) - end - - private - - def type - # We don't ever expect to have more than one list - # type param at once. - if params.key?('backlog') - :backlog - else - :label - end - end - - def target(board) - strong_memoize(:target) do - available_labels.find_by(id: params[:label_id]) # rubocop: disable CodeReuse/ActiveRecord - end - end - - def available_labels - ::Labels::AvailableLabelsService.new(current_user, parent, {}) - .available_labels - end - - def next_position(board) - max_position = board.lists.movable.maximum(:position) - max_position.nil? ? 0 : max_position.succ - end - - def create_list(board, type, target, position) - board.lists.create(create_list_attributes(type, target, position)) - end - - def create_list_attributes(type, target, position) - { type => target, list_type: type, position: position } - end - - def create_backlog(board) - return board.lists.backlog.first if board.lists.backlog.exists? - - board.lists.create(list_type: :backlog, position: nil) - end + class CreateService < Boards::Lists::BaseCreateService end end end diff --git a/app/uploaders/packages/debian/component_file_uploader.rb b/app/uploaders/packages/debian/component_file_uploader.rb new file mode 100644 index 00000000000..e4d637fecac --- /dev/null +++ b/app/uploaders/packages/debian/component_file_uploader.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true +class Packages::Debian::ComponentFileUploader < GitlabUploader + extend Workhorse::UploadPath + include ObjectStorage::Concern + + storage_options Gitlab.config.packages + + after :store, :schedule_background_upload + + alias_method :upload, :model + + def filename + model.file_name + end + + def store_dir + dynamic_segment + end + + private + + def dynamic_segment + raise ObjectNotReadyError, 'Package model not ready' unless model.id && model.component.distribution.container_id + + Gitlab::HashedPath.new("debian_#{model.class.container_type}_component_file", model.id, root_hash: model.component.distribution.container_id) + end +end |