Commit 5aa2c955 authored by Luke Duncalfe's avatar Luke Duncalfe Committed by Markus Koller

Move Design Management `lib` files to FOSS

This change is part of
https://gitlab.com/gitlab-org/gitlab/-/issues/212566 to move all Design
Management code to FOSS.

This change moves all code under `lib`.
parent cd42cccf
......@@ -56,7 +56,10 @@ module Projects
end
def exporters
[version_saver, avatar_saver, project_tree_saver, uploads_saver, repo_saver, wiki_repo_saver, lfs_saver, snippets_repo_saver]
[
version_saver, avatar_saver, project_tree_saver, uploads_saver,
repo_saver, wiki_repo_saver, lfs_saver, snippets_repo_saver, design_repo_saver
]
end
def version_saver
......@@ -95,6 +98,10 @@ module Projects
Gitlab::ImportExport::SnippetsRepoSaver.new(current_user: current_user, project: project, shared: shared)
end
def design_repo_saver
Gitlab::ImportExport::DesignRepoSaver.new(project: project, shared: shared)
end
def cleanup
FileUtils.rm_rf(shared.archive_path) if shared&.archive_path
end
......@@ -117,5 +124,3 @@ module Projects
end
end
end
Projects::ImportExport::ExportService.prepend_if_ee('EE::Projects::ImportExport::ExportService')
......@@ -57,7 +57,7 @@ module DesignManagement
end
def counter
::Gitlab::UsageCounters::DesignsCounter
::Gitlab::UsageDataCounters::DesignsCounter
end
def formatted_file_list
......
......@@ -60,7 +60,7 @@ module DesignManagement
return if design_unchanged?(design, content)
action = new_file?(design) ? :create : :update
on_success { ::Gitlab::UsageCounters::DesignsCounter.count(action) }
on_success { ::Gitlab::UsageDataCounters::DesignsCounter.count(action) }
DesignManagement::DesignAction.new(design, action, content)
end
......
# frozen_string_literal: true
module EE::Projects::ImportExport::ExportService
extend ActiveSupport::Concern
extend ::Gitlab::Utils::Override
private
override :exporters
def exporters
super + Array.wrap(design_repo_saver)
end
def design_repo_saver
Gitlab::ImportExport::DesignRepoSaver.new(project: project, shared: shared)
end
end
# frozen_string_literal: true
module EE
module API
module Entities
module DesignManagement
class Design < Grape::Entity
expose :id
expose :project_id
expose :filename
expose :image_url do |design|
::Gitlab::UrlBuilder.build(design)
end
end
end
end
end
end
......@@ -15,19 +15,6 @@ module EE
# see also https://gitlab.com/gitlab-org/gitlab-foss/issues/59719
::EE::API::Entities.const_get(target_type, false)
end
override :todo_target_url
def todo_target_url(todo)
return super unless todo.target_type == ::DesignManagement::Design.name
design = todo.target
path_options = {
anchor: todo_target_anchor(todo),
vueroute: design.filename
}
::Gitlab::Routing.url_helpers.designs_project_issue_url(design.project, design.issue, path_options)
end
end
end
end
......
# frozen_string_literal: true
module EE
module Banzai
module Filter
module IssueReferenceFilter
extend ActiveSupport::Concern
prepended do
extend ::Gitlab::Utils::Override
override :object_link_text_extras
def object_link_text_extras(issue, matches)
super + design_link_extras(issue, matches.named_captures['path'])
end
private
def design_link_extras(issue, path)
if path == '/designs' && read_designs?(issue)
['designs']
else
[]
end
end
def read_designs?(issue)
Ability.allowed?(current_user, :read_design, issue)
end
end
end
end
end
end
# frozen_string_literal: true
module EE::Gitlab::ImportExport
extend ActiveSupport::Concern
prepended do
def design_repo_bundle_filename
'project.design.bundle'
end
end
end
# frozen_string_literal: true
module EE::Gitlab::ImportExport::Importer
extend ActiveSupport::Concern
extend ::Gitlab::Utils::Override
private
override :restorers
def restorers
super + Array.wrap(design_repo_restorer)
end
def design_repo_restorer
Gitlab::ImportExport::DesignRepoRestorer.new(
path_to_bundle: design_repo_path,
shared: shared,
project: project
)
end
def design_repo_path
File.join(shared.export_path, ::Gitlab::ImportExport.design_repo_bundle_filename)
end
end
# frozen_string_literal: true
module EE
module Gitlab
module ImportExport
module Project
module ObjectBuilder
extend ::Gitlab::Utils::Override
extend ActiveSupport::Concern
private
override :where_clause_for_klass
def where_clause_for_klass
return attrs_to_arel(attributes.slice('filename')).and(table[:issue_id].eq(nil)) if design?
super
end
def design?
klass == DesignManagement::Design
end
end
end
end
end
end
......@@ -8,16 +8,10 @@ module EE
extend ActiveSupport::Concern
EE_OVERRIDES = {
design: 'DesignManagement::Design',
designs: 'DesignManagement::Design',
design_versions: 'DesignManagement::Version',
actions: 'DesignManagement::Action',
deploy_access_levels: 'ProtectedEnvironment::DeployAccessLevel',
unprotect_access_levels: 'ProtectedBranch::UnprotectAccessLevel'
}.freeze
EE_EXISTING_OBJECT_RELATIONS = %i[DesignManagement::Design].freeze
class_methods do
extend ::Gitlab::Utils::Override
......@@ -25,11 +19,6 @@ module EE
def overrides
super.merge(EE_OVERRIDES)
end
override :existing_object_relations
def existing_object_relations
super + EE_EXISTING_OBJECT_RELATIONS
end
end
end
end
......
# frozen_string_literal: true
module EE
module Gitlab
module Uploads
module MigrationHelper
extend ActiveSupport::Concern
EE_CATEGORIES = [
%w(DesignManagement::DesignV432x230Uploader DesignManagement::Action :image_v432x230)
].freeze
class_methods do
extend ::Gitlab::Utils::Override
override :categories
def categories
super + EE_CATEGORIES
end
end
end
end
end
end
......@@ -11,8 +11,6 @@ module EE
override :build
def build(object, **options)
case object.itself
when ::DesignManagement::Design
design_url(object, **options)
when Epic
instance.group_epic_url(object.group, object, **options)
when Vulnerability
......@@ -45,17 +43,6 @@ module EE
super
end
end
def design_url(design, **options)
size, ref = options.values_at(:size, :ref)
options.except!(:size, :ref)
if size
instance.project_design_management_designs_resized_image_url(design.project, design, ref, size, **options)
else
instance.project_design_management_designs_raw_image_url(design.project, design, ref, **options)
end
end
end
end
end
......
......@@ -31,7 +31,7 @@ module EE
override :usage_data_counters
def usage_data_counters
super + [::Gitlab::UsageCounters::DesignsCounter, ::Gitlab::UsageDataCounters::LicensesList]
super + [::Gitlab::UsageDataCounters::LicensesList]
end
override :uncached_data
......
# frozen_string_literal: true
require 'spec_helper'
describe Banzai::Filter::IssueReferenceFilter do
include FilterSpecHelper
include DesignManagementTestHelpers
let_it_be(:issue) { create(:issue) }
let_it_be(:project) { issue.project }
let_it_be(:current_user) { project.owner }
let_it_be(:designs_tab_url) { url_for_designs(issue) }
context 'when processing a link to the designs tab' do
before do
enable_design_management
end
let(:input_text) { "See #{designs_tab_url}" }
subject(:link) { reference_filter(input_text).css('a').first }
it 'includes the word "designs" after the reference in the text content', :aggregate_failures do
expect(link.attr('title')).to eq(issue.title)
expect(link.attr('href')).to eq(designs_tab_url)
expect(link.text).to eq("#{issue.to_reference} (designs)")
end
end
describe '#object_link_text_extras' do
before do
enable_design_management(enabled)
end
let(:enabled) { true }
let(:matches) { ::Issue.link_reference_pattern.match(input_text) }
let(:extras) { subject.object_link_text_extras(issue, matches) }
subject { filter_instance }
context 'the link does not go to the designs tab' do
let(:input_text) { Gitlab::Routing.url_helpers.project_issue_url(issue.project, issue) }
it 'does not include designs' do
expect(extras).not_to include('designs')
end
end
context 'the link goes to the designs tab' do
let(:input_text) { designs_tab_url }
it 'includes designs' do
expect(extras).to include('designs')
end
context 'design management is disabled' do
let(:enabled) { false }
it 'does not include designs in the extras' do
expect(extras).not_to include('designs')
end
end
end
end
end
......@@ -3,7 +3,6 @@ require 'spec_helper'
describe Gitlab::ImportExport::Project::TreeRestorer do
include ImportExport::CommonUtil
using RSpec::Parameterized::TableSyntax
let(:shared) { project.import_export_shared }
let(:project_tree_restorer) { described_class.new(user: user, shared: shared, project: project) }
......@@ -73,7 +72,7 @@ describe Gitlab::ImportExport::Project::TreeRestorer do
end
end
describe 'restoring design management data' do
describe 'restores `protected_environments` with `deploy_access_levels`' do
let_it_be(:user) { create(:admin, email: 'user_1@gitlabexample.com') }
let_it_be(:second_user) { create(:user, email: 'user_2@gitlabexample.com') }
let_it_be(:project) do
......@@ -82,56 +81,11 @@ describe Gitlab::ImportExport::Project::TreeRestorer do
end
before do
setup_import_export_config('designs', 'ee')
setup_import_export_config('designs')
restored_project_json
end
it_behaves_like 'restores project successfully', issues: 2
it 'restores project associations correctly' do
expect(project.designs.size).to eq(7)
end
describe 'restores issue associations correctly' do
let(:issue) { project.issues.offset(index).first }
where(:index, :design_filenames, :version_shas, :events, :author_emails) do
0 | %w[chirrido3.jpg jonathan_richman.jpg mariavontrap.jpeg] | %w[27702d08f5ee021ae938737f84e8fe7c38599e85 9358d1bac8ff300d3d2597adaa2572a20f7f8703 e1a4a501bcb42f291f84e5d04c8f927821542fb6] | %w[creation creation creation modification modification deletion] | %w[user_1@gitlabexample.com user_1@gitlabexample.com user_2@gitlabexample.com]
1 | ['1 (1).jpeg', '2099743.jpg', 'a screenshot (1).jpg', 'chirrido3.jpg'] | %w[73f871b4c8c1d65c62c460635e023179fb53abc4 8587e78ab6bda3bc820a9f014c3be4a21ad4fcc8 c9b5f067f3e892122a4b12b0a25a8089192f3ac8] | %w[creation creation creation creation modification] | %w[user_1@gitlabexample.com user_2@gitlabexample.com user_2@gitlabexample.com]
end
with_them do
it do
expect(issue.designs.pluck(:filename)).to contain_exactly(*design_filenames)
expect(issue.design_versions.pluck(:sha)).to contain_exactly(*version_shas)
expect(issue.design_versions.flat_map(&:actions).map(&:event)).to contain_exactly(*events)
expect(issue.design_versions.map(&:author).map(&:email)).to contain_exactly(*author_emails)
end
end
end
describe 'restores design version associations correctly' do
let(:project_designs) { project.designs.reorder(:filename, :issue_id) }
let(:design) { project_designs.offset(index).first }
where(:index, :version_shas) do
0 | %w[73f871b4c8c1d65c62c460635e023179fb53abc4 c9b5f067f3e892122a4b12b0a25a8089192f3ac8]
1 | %w[73f871b4c8c1d65c62c460635e023179fb53abc4]
2 | %w[c9b5f067f3e892122a4b12b0a25a8089192f3ac8]
3 | %w[27702d08f5ee021ae938737f84e8fe7c38599e85 9358d1bac8ff300d3d2597adaa2572a20f7f8703 e1a4a501bcb42f291f84e5d04c8f927821542fb6]
4 | %w[8587e78ab6bda3bc820a9f014c3be4a21ad4fcc8]
5 | %w[27702d08f5ee021ae938737f84e8fe7c38599e85 e1a4a501bcb42f291f84e5d04c8f927821542fb6]
6 | %w[27702d08f5ee021ae938737f84e8fe7c38599e85]
end
with_them do
it do
expect(design.versions.pluck(:sha)).to contain_exactly(*version_shas)
end
end
end
it 'restores `protected_environments` with `deploy_access_levels`' do
specify do
aggregate_failures do
expect(project.protected_environments.count).to eq(1)
......
......@@ -8,9 +8,6 @@ describe Gitlab::ImportExport::Project::TreeSaver do
let_it_be(:project) { create(:project, group: group) }
let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:shared) { project.import_export_shared }
let_it_be(:design) { create(:design, :with_file, versions_count: 2, issue: issue) }
let_it_be(:note) { create(:diff_note_on_design, noteable: design, project: project, author: user) }
let_it_be(:note2) { create(:note, noteable: issue, project: project, author: user) }
let_it_be(:epic) { create(:epic, group: group) }
......@@ -45,25 +42,6 @@ describe Gitlab::ImportExport::Project::TreeSaver do
let_it_be(:issue_json) { get_json(full_path, exportable_path, :issues, ndjson_enabled).first }
describe 'the designs json' do
it 'saves issue.designs correctly' do
expect(issue_json['designs'].size).to eq(1)
end
it 'saves issue.design_versions correctly' do
actions = issue_json['design_versions'].flat_map { |v| v['actions'] }
expect(issue_json['design_versions'].size).to eq(2)
issue_json['design_versions'].each do |version|
expect(version['author_id']).to eq(issue.author_id)
end
expect(actions.size).to eq(2)
actions.each do |action|
expect(action['design']).to be_present
end
end
end
context 'epics' do
it 'has epic_issue' do
expect(issue_json['epic_issue']).not_to be_empty
......
......@@ -9,7 +9,6 @@ describe Gitlab::UrlBuilder do
using RSpec::Parameterized::TableSyntax
where(:factory, :path_generator) do
:design | ->(design) { "/#{design.project.full_path}/-/design_management/designs/#{design.id}/raw_image" }
:epic | ->(epic) { "/groups/#{epic.group.full_path}/-/epics/#{epic.iid}" }
:vulnerability | ->(vulnerability) { "/#{vulnerability.project.full_path}/-/security/vulnerabilities/#{vulnerability.id}" }
......@@ -31,15 +30,5 @@ describe Gitlab::UrlBuilder do
expect(subject.build(object, only_path: true)).to eq(path)
end
end
context 'when passing a DesignManagement::Design' do
let(:design) { build_stubbed(:design) }
it 'uses the given ref and size in the URL' do
url = subject.build(design, ref: 'feature', size: 'small')
expect(url).to eq "#{Settings.gitlab['url']}/#{design.project.full_path}/-/design_management/designs/#{design.id}/feature/resized_image/small"
end
end
end
end
......@@ -107,9 +107,6 @@ describe Gitlab::UsageData do
sast_jobs
status_page_projects
status_page_issues
design_management_designs_create
design_management_designs_update
design_management_designs_delete
user_preferences_group_overview_details
user_preferences_group_overview_security_dashboard
template_repositories
......
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::GlRepository::RepoType do
let_it_be(:project) { create(:project) }
let_it_be(:personal_snippet) { create(:personal_snippet, author: project.owner) }
let_it_be(:project_snippet) { create(:project_snippet, project: project, author: project.owner) }
describe Gitlab::GlRepository::DESIGN do
it_behaves_like 'a repo type' do
let(:expected_identifier) { "design-#{project.id}" }
let(:expected_id) { project.id.to_s }
let(:expected_suffix) { '.design' }
let(:expected_repository) { project.design_repository }
let(:expected_container) { project }
end
it 'knows its type' do
expect(described_class).to be_design
expect(described_class).not_to be_project
expect(described_class).not_to be_wiki
expect(described_class).not_to be_snippet
end
it 'checks if repository path is valid' do
expect(described_class.valid?(project.design_repository.full_path)).to be_truthy
expect(described_class.valid?(project.repository.full_path)).to be_falsey
expect(described_class.valid?(project.wiki.repository.full_path)).to be_falsey
expect(described_class.valid?("snippets/#{personal_snippet.id}")).to be_falsey
expect(described_class.valid?("#{project.full_path}/snippets/#{project_snippet.id}")).to be_falsey
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::ImportExport::Importer do
describe '#execute' do
let(:project) { create(:project) }
let(:test_path) { "#{Dir.tmpdir}/importer_spec" }
let(:shared) { project.import_export_shared }
let(:import_file) { fixture_file_upload('spec/features/projects/import_export/test_project_export.tar.gz') }
subject(:importer) { described_class.new(project) }
before do
allow_next_instance_of(Gitlab::ImportExport) do |instance|
allow(instance).to receive(:storage_path).and_return(test_path)
end
allow_next_instance_of(Gitlab::ImportExport::FileImporter) do |instance|
allow(instance).to receive(:remove_import_file)
end
stub_uploads_object_storage(FileUploader)
FileUtils.mkdir_p(shared.export_path)
ImportExportUpload.create(project: project, import_file: import_file)
end
after do
FileUtils.rm_rf(test_path)
end
it 'restores the design repo' do
expect(Gitlab::ImportExport::DesignRepoRestorer).to receive(:new).and_call_original
importer.execute
end
end
end
......@@ -26,16 +26,6 @@ describe API::Todos do
create(:todo, project: nil, group: new_group, author: author_1, user: user, target: new_epic)
end
def create_todo_for_mentioned_in_design
issue = create(:issue, project: project)
create(:todo, :mentioned,
user: user,
project: project,
target: create(:design, issue: issue),
author: create(:user),
note: create(:note, project: project, note: "I am note, hear me roar"))
end
shared_examples 'an endpoint that responds with success' do
specify do
expect(response).to have_gitlab_http_status(:ok)
......@@ -67,34 +57,6 @@ describe API::Todos do
)
end
end
context 'when there is a Design Todo' do
let!(:design_todo) { create_todo_for_mentioned_in_design }
def api_request
get api('/todos', personal_access_token: pat)
end
before do
api_request
end
it_behaves_like 'an endpoint that responds with success'
it 'avoids N+1 queries', :request_store do
control = ActiveRecord::QueryRecorder.new { api_request }
create_todo_for_mentioned_in_design
expect { api_request }.not_to exceed_query_limit(control)
end
it 'includes the Design Todo in the response' do
expect(json_response).to include(
a_hash_including('id' => design_todo.id)
)
end
end
end
describe 'POST :id/epics/:epic_iid/todo' do
......
......@@ -70,7 +70,7 @@ describe DesignManagement::DeleteDesignsService do
it_behaves_like "a top-level error"
it 'does not log any events' do
counter = ::Gitlab::UsageCounters::DesignsCounter
counter = ::Gitlab::UsageDataCounters::DesignsCounter
expect { run_service rescue nil }.not_to change { counter.totals }
end
end
......@@ -87,7 +87,7 @@ describe DesignManagement::DeleteDesignsService do
end
it 'logs a deletion event' do
counter = ::Gitlab::UsageCounters::DesignsCounter
counter = ::Gitlab::UsageDataCounters::DesignsCounter
expect { run_service }.to change { counter.read(:delete) }.by(1)
end
......@@ -145,7 +145,7 @@ describe DesignManagement::DeleteDesignsService do
end
it 'logs the correct number of deletion events' do
counter = ::Gitlab::UsageCounters::DesignsCounter
counter = ::Gitlab::UsageDataCounters::DesignsCounter
expect { run_service }.to change { counter.read(:delete) }.by(2)
end
......
......@@ -85,7 +85,7 @@ describe DesignManagement::SaveDesignsService do
end
it 'updates the creation count' do
counter = Gitlab::UsageCounters::DesignsCounter
counter = Gitlab::UsageDataCounters::DesignsCounter
expect { run_service }.to change { counter.read(:create) }.by(1)
end
......@@ -167,7 +167,7 @@ describe DesignManagement::SaveDesignsService do
end
it 'increments the update counter' do
counter = Gitlab::UsageCounters::DesignsCounter
counter = Gitlab::UsageDataCounters::DesignsCounter
expect { run_service }.to change { counter.read(:update) }.by 1
end
......@@ -221,7 +221,7 @@ describe DesignManagement::SaveDesignsService do
end
it 'counts one creation and one update' do
counter = Gitlab::UsageCounters::DesignsCounter
counter = Gitlab::UsageDataCounters::DesignsCounter
expect { run_service }
.to change { counter.read(:create) }.by(1)
.and change { counter.read(:update) }.by(1)
......@@ -258,7 +258,7 @@ describe DesignManagement::SaveDesignsService do
end
it 'increments the creation count by 2' do
counter = Gitlab::UsageCounters::DesignsCounter
counter = Gitlab::UsageDataCounters::DesignsCounter
expect { run_service }.to change { counter.read(:create) }.by 2
end
......
......@@ -4,29 +4,11 @@ require 'spec_helper'
describe Projects::ImportExport::ExportService do
describe '#execute' do
let_it_be(:user) { create(:user) }
context 'user can admin project' do
let_it_be(:project) { create(:project) }
let(:shared) { project.import_export_shared }
subject { described_class.new(project, user).execute }
before do
project.add_maintainer(user)
end
it 'saves the design repo' do
expect(Gitlab::ImportExport::DesignRepoSaver).to receive(:new).and_call_original
subject
end
end
context 'project templates' do
let_it_be(:group) { create(:group, :private) }
let_it_be(:subgroup) { create(:group, :private, parent: group) }
let_it_be(:project_template) { create(:project, group: subgroup) }
let_it_be(:user) { create(:user) }
let(:shared) { project_template.import_export_shared }
subject { described_class.new(project_template, user).execute }
......
# frozen_string_literal: true
module API
module Entities
module DesignManagement
class Design < Grape::Entity
expose :id
expose :project_id
expose :filename
expose :image_url do |design|
::Gitlab::UrlBuilder.build(design)
end
end
end
end
end
......@@ -31,6 +31,8 @@ module API
end
def todo_target_url(todo)
return design_todo_target_url(todo) if todo.for_design?
target_type = todo.target_type.underscore
target_url = "#{todo.resource_parent.class.to_s.underscore}_#{target_type}_url"
......@@ -42,6 +44,16 @@ module API
def todo_target_anchor(todo)
"note_#{todo.note_id}" if todo.note_id?
end
def design_todo_target_url(todo)
design = todo.target
path_options = {
anchor: todo_target_anchor(todo),
vueroute: design.filename
}
::Gitlab::Routing.url_helpers.designs_project_issue_url(design.project, design.issue, path_options)
end
end
end
end
......
......@@ -28,8 +28,24 @@ module Banzai
def parent_records(parent, ids)
parent.issues.where(iid: ids.to_a)
end
def object_link_text_extras(issue, matches)
super + design_link_extras(issue, matches.named_captures['path'])
end
private
def design_link_extras(issue, path)
if path == '/designs' && read_designs?(issue)
['designs']
else
[]
end
end
end
Banzai::Filter::IssueReferenceFilter.prepend_if_ee('EE::Banzai::Filter::IssueReferenceFilter')
def read_designs?(issue)
Ability.allowed?(current_user, :read_design, issue)
end
end
end
end
......@@ -42,6 +42,10 @@ module Gitlab
"project.wiki.bundle"
end
def design_repo_bundle_filename
'project.design.bundle'
end
def snippet_repo_bundle_dir
'snippets'
end
......
......@@ -34,7 +34,7 @@ module Gitlab
attr_accessor :archive_file, :current_user, :project, :shared
def restorers
[repo_restorer, wiki_restorer, project_tree, avatar_restorer,
[repo_restorer, wiki_restorer, project_tree, avatar_restorer, design_repo_restorer,
uploads_restorer, lfs_restorer, statistics_restorer, snippets_repo_restorer]
end
......@@ -71,6 +71,12 @@ module Gitlab
wiki_enabled: project.wiki_enabled?)
end
def design_repo_restorer
Gitlab::ImportExport::DesignRepoRestorer.new(path_to_bundle: design_repo_path,
shared: shared,
project: project)
end
def uploads_restorer
Gitlab::ImportExport::UploadsRestorer.new(project: project, shared: shared)
end
......@@ -101,6 +107,10 @@ module Gitlab
File.join(shared.export_path, Gitlab::ImportExport.wiki_repo_bundle_filename)
end
def design_repo_path
File.join(shared.export_path, Gitlab::ImportExport.design_repo_bundle_filename)
end
def remove_import_file
upload = project.import_export_upload
......@@ -141,5 +151,3 @@ module Gitlab
end
end
end
Gitlab::ImportExport::Importer.prepend_if_ee('EE::Gitlab::ImportExport::Importer')
......@@ -29,6 +29,14 @@ tree:
- resource_label_events:
- label:
- :priorities
- designs:
- notes:
- :author
- events:
- :push_event_payload
- design_versions:
- actions:
- :design # Duplicate export of issues.designs in order to link the record to both Issue and Action
- :issue_assignees
- :zoom_meetings
- :sentry_issue
......@@ -287,6 +295,7 @@ excluded_attributes:
actions:
- :design_id
- :version_id
- image_v432x230
links:
- :release_id
project_members:
......@@ -379,14 +388,6 @@ ee:
tree:
project:
- issues:
- designs:
- notes:
- :author
- events:
- :push_event_payload
- design_versions:
- actions:
- :design # Duplicate export of issues.designs in order to link the record to both Issue and Action
- epic_issue:
- :epic
- protected_branches:
......@@ -394,6 +395,3 @@ ee:
- protected_environments:
- :deploy_access_levels
- :service_desk_setting
excluded_attributes:
actions:
- image_v432x230
......@@ -57,6 +57,8 @@ module Gitlab
# Returns Arel clause for a particular model or `nil`.
def where_clause_for_klass
return attrs_to_arel(attributes.slice('filename')).and(table[:issue_id].eq(nil)) if design?
attrs_to_arel(attributes.slice('iid')) if merge_request?
end
......@@ -95,6 +97,10 @@ module Gitlab
klass == Epic
end
def design?
klass == DesignManagement::Design
end
# If an existing group milestone used the IID
# claim the IID back and set the group milestone to use one available
# This is necessary to fix situations like the following:
......@@ -115,5 +121,3 @@ module Gitlab
end
end
end
Gitlab::ImportExport::Project::ObjectBuilder.prepend_if_ee('EE::Gitlab::ImportExport::Project::ObjectBuilder')
......@@ -17,6 +17,10 @@ module Gitlab
merge_access_levels: 'ProtectedBranch::MergeAccessLevel',
push_access_levels: 'ProtectedBranch::PushAccessLevel',
create_access_levels: 'ProtectedTag::CreateAccessLevel',
design: 'DesignManagement::Design',
designs: 'DesignManagement::Design',
design_versions: 'DesignManagement::Version',
actions: 'DesignManagement::Action',
labels: :project_labels,
priorities: :label_priorities,
auto_devops: :project_auto_devops,
......@@ -51,6 +55,7 @@ module Gitlab
container_expiration_policy
external_pull_request
external_pull_requests
DesignManagement::Design
].freeze
def create
......
......@@ -15,6 +15,7 @@ module Gitlab
%w(FileUploader Project),
%w(PersonalFileUploader Snippet),
%w(NamespaceFileUploader Snippet),
%w(DesignManagement::DesignV432x230Uploader DesignManagement::Action :image_v432x230),
%w(FileUploader MergeRequest)].freeze
def initialize(args, logger)
......@@ -74,5 +75,3 @@ module Gitlab
end
end
end
Gitlab::Uploads::MigrationHelper.prepend_if_ee('EE::Gitlab::Uploads::MigrationHelper')
......@@ -11,6 +11,7 @@ module Gitlab
class << self
include ActionView::RecordIdentifier
# rubocop:disable Metrics/CyclomaticComplexity
def build(object, **options)
# Objects are sometimes wrapped in a BatchLoader instance
case object.itself
......@@ -38,10 +39,13 @@ module Gitlab
wiki_url(object, **options)
when WikiPage
instance.project_wiki_url(object.wiki.project, object.slug, **options)
when ::DesignManagement::Design
design_url(object, **options)
else
raise NotImplementedError.new("No URL builder defined for #{object.inspect}")
end
end
# rubocop:enable Metrics/CyclomaticComplexity
def commit_url(commit, **options)
return '' unless commit.project
......@@ -78,6 +82,17 @@ module Gitlab
raise NotImplementedError.new("No URL builder defined for #{object.inspect}")
end
end
def design_url(design, **options)
size, ref = options.values_at(:size, :ref)
options.except!(:size, :ref)
if size
instance.project_design_management_designs_resized_image_url(design.project, design, ref, size, **options)
else
instance.project_design_management_designs_raw_image_url(design.project, design, ref, **options)
end
end
end
end
end
......
......@@ -223,7 +223,8 @@ module Gitlab
Gitlab::UsageDataCounters::CycleAnalyticsCounter,
Gitlab::UsageDataCounters::ProductivityAnalyticsCounter,
Gitlab::UsageDataCounters::SourceCodeCounter,
Gitlab::UsageDataCounters::MergeRequestCounter
Gitlab::UsageDataCounters::MergeRequestCounter,
Gitlab::UsageDataCounters::DesignsCounter
]
end
......
# frozen_string_literal: true
module Gitlab::UsageCounters
module Gitlab::UsageDataCounters
class DesignsCounter
extend ::Gitlab::UsageDataCounters::RedisCounter
extend Gitlab::UsageDataCounters::RedisCounter
KNOWN_EVENTS = %w[create update delete].map(&:freeze).freeze
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe EE::API::Entities::DesignManagement::Design do
describe API::Entities::DesignManagement::Design do
let_it_be(:design) { create(:design) }
let(:entity) { described_class.new(design, request: double) }
......
......@@ -4,6 +4,7 @@ require 'spec_helper'
describe Banzai::Filter::IssueReferenceFilter do
include FilterSpecHelper
include DesignManagementTestHelpers
def helper
IssuesHelper
......@@ -358,6 +359,23 @@ describe Banzai::Filter::IssueReferenceFilter do
end
end
context 'when processing a link to the designs tab' do
let(:designs_tab_url) { url_for_designs(issue) }
let(:input_text) { "See #{designs_tab_url}" }
subject(:link) { reference_filter(input_text).css('a').first }
before do
enable_design_management
end
it 'includes the word "designs" after the reference in the text content', :aggregate_failures do
expect(link.attr('title')).to eq(issue.title)
expect(link.attr('href')).to eq(designs_tab_url)
expect(link.text).to eq("#{issue.to_reference} (designs)")
end
end
context 'group context' do
let(:group) { create(:group) }
let(:context) { { project: nil, group: group } }
......@@ -467,4 +485,41 @@ describe Banzai::Filter::IssueReferenceFilter do
end.not_to yield_control
end
end
describe '#object_link_text_extras' do
before do
enable_design_management(enabled)
end
let(:current_user) { project.owner }
let(:enabled) { true }
let(:matches) { Issue.link_reference_pattern.match(input_text) }
let(:extras) { subject.object_link_text_extras(issue, matches) }
subject { filter_instance }
context 'the link does not go to the designs tab' do
let(:input_text) { Gitlab::Routing.url_helpers.project_issue_url(issue.project, issue) }
it 'does not include designs' do
expect(extras).not_to include('designs')
end
end
context 'the link goes to the designs tab' do
let(:input_text) { url_for_designs(issue) }
it 'includes designs' do
expect(extras).to include('designs')
end
context 'design management is disabled' do
let(:enabled) { false }
it 'does not include designs in the extras' do
expect(extras).not_to include('designs')
end
end
end
end
end
......@@ -53,22 +53,15 @@ describe 'Test coverage of the Project Import' do
].freeze
# A list of JSON fixture files we use to test Import.
# Note that we use separate fixture to test ee-only features.
# Most of the relations are present in `complex/project.json`
# which is our main fixture.
PROJECT_JSON_FIXTURES_EE =
if Gitlab.ee?
['ee/spec/fixtures/lib/gitlab/import_export/designs/project.json'].freeze
else
[]
end
PROJECT_JSON_FIXTURES = [
'spec/fixtures/lib/gitlab/import_export/complex/project.json',
'spec/fixtures/lib/gitlab/import_export/group/project.json',
'spec/fixtures/lib/gitlab/import_export/light/project.json',
'spec/fixtures/lib/gitlab/import_export/milestone-iid/project.json'
].freeze + PROJECT_JSON_FIXTURES_EE
'spec/fixtures/lib/gitlab/import_export/milestone-iid/project.json',
'spec/fixtures/lib/gitlab/import_export/designs/project.json'
].freeze
it 'ensures that all imported/exported relations are present in test JSONs' do
not_tested_relations = (relations_from_config - tested_relations) - MUTED_RELATIONS
......
......@@ -51,7 +51,8 @@ describe Gitlab::ImportExport::Importer do
Gitlab::ImportExport::UploadsRestorer,
Gitlab::ImportExport::LfsRestorer,
Gitlab::ImportExport::StatisticsRestorer,
Gitlab::ImportExport::SnippetsRepoRestorer
Gitlab::ImportExport::SnippetsRepoRestorer,
Gitlab::ImportExport::DesignRepoRestorer
].each do |restorer|
it "calls the #{restorer}" do
fake_restorer = double(restorer.to_s)
......
......@@ -8,6 +8,7 @@ end
describe Gitlab::ImportExport::Project::TreeRestorer do
include ImportExport::CommonUtil
using RSpec::Parameterized::TableSyntax
let(:shared) { project.import_export_shared }
......@@ -987,6 +988,69 @@ describe Gitlab::ImportExport::Project::TreeRestorer do
end
end
end
context 'JSON with design management data' do
let_it_be(:user) { create(:admin, email: 'user_1@gitlabexample.com') }
let_it_be(:second_user) { create(:user, email: 'user_2@gitlabexample.com') }
let_it_be(:project) do
create(:project, :builds_disabled, :issues_disabled,
{ name: 'project', path: 'project' })
end
let(:shared) { project.import_export_shared }
let(:project_tree_restorer) { described_class.new(user: user, shared: shared, project: project) }
subject(:restored_project_json) { project_tree_restorer.restore }
before do
setup_import_export_config('designs')
restored_project_json
end
it_behaves_like 'restores project successfully', issues: 2
it 'restores project associations correctly' do
expect(project.designs.size).to eq(7)
end
describe 'restores issue associations correctly' do
let(:issue) { project.issues.offset(index).first }
where(:index, :design_filenames, :version_shas, :events, :author_emails) do
0 | %w[chirrido3.jpg jonathan_richman.jpg mariavontrap.jpeg] | %w[27702d08f5ee021ae938737f84e8fe7c38599e85 9358d1bac8ff300d3d2597adaa2572a20f7f8703 e1a4a501bcb42f291f84e5d04c8f927821542fb6] | %w[creation creation creation modification modification deletion] | %w[user_1@gitlabexample.com user_1@gitlabexample.com user_2@gitlabexample.com]
1 | ['1 (1).jpeg', '2099743.jpg', 'a screenshot (1).jpg', 'chirrido3.jpg'] | %w[73f871b4c8c1d65c62c460635e023179fb53abc4 8587e78ab6bda3bc820a9f014c3be4a21ad4fcc8 c9b5f067f3e892122a4b12b0a25a8089192f3ac8] | %w[creation creation creation creation modification] | %w[user_1@gitlabexample.com user_2@gitlabexample.com user_2@gitlabexample.com]
end
with_them do
it do
expect(issue.designs.pluck(:filename)).to contain_exactly(*design_filenames)
expect(issue.design_versions.pluck(:sha)).to contain_exactly(*version_shas)
expect(issue.design_versions.flat_map(&:actions).map(&:event)).to contain_exactly(*events)
expect(issue.design_versions.map(&:author).map(&:email)).to contain_exactly(*author_emails)
end
end
end
describe 'restores design version associations correctly' do
let(:project_designs) { project.designs.reorder(:filename, :issue_id) }
let(:design) { project_designs.offset(index).first }
where(:index, :version_shas) do
0 | %w[73f871b4c8c1d65c62c460635e023179fb53abc4 c9b5f067f3e892122a4b12b0a25a8089192f3ac8]
1 | %w[73f871b4c8c1d65c62c460635e023179fb53abc4]
2 | %w[c9b5f067f3e892122a4b12b0a25a8089192f3ac8]
3 | %w[27702d08f5ee021ae938737f84e8fe7c38599e85 9358d1bac8ff300d3d2597adaa2572a20f7f8703 e1a4a501bcb42f291f84e5d04c8f927821542fb6]
4 | %w[8587e78ab6bda3bc820a9f014c3be4a21ad4fcc8]
5 | %w[27702d08f5ee021ae938737f84e8fe7c38599e85 e1a4a501bcb42f291f84e5d04c8f927821542fb6]
6 | %w[27702d08f5ee021ae938737f84e8fe7c38599e85]
end
with_them do
it do
expect(design.versions.pluck(:sha)).to contain_exactly(*version_shas)
end
end
end
end
end
context 'enable ndjson import' do
......
......@@ -168,6 +168,28 @@ describe Gitlab::ImportExport::Project::TreeSaver do
it 'has issue resource label events' do
expect(subject.first['resource_label_events']).not_to be_empty
end
it 'saves the issue designs correctly' do
expect(subject.first['designs'].size).to eq(1)
end
it 'saves the issue design notes correctly' do
expect(subject.first['designs'].first['notes']).not_to be_empty
end
it 'saves the issue design versions correctly' do
issue_json = subject.first
actions = issue_json['design_versions'].flat_map { |v| v['actions'] }
expect(issue_json['design_versions'].size).to eq(2)
issue_json['design_versions'].each do |version|
expect(version['author_id']).to be_kind_of(Integer)
end
expect(actions.size).to eq(2)
actions.each do |action|
expect(action['design']).to be_present
end
end
end
context 'with ci_pipelines' do
......@@ -442,6 +464,9 @@ describe Gitlab::ImportExport::Project::TreeSaver do
board = create(:board, project: project, name: 'TestBoard')
create(:list, board: board, position: 0, label: project_label)
design = create(:design, :with_file, versions_count: 2, issue: issue)
create(:diff_note_on_design, noteable: design, project: project, author: user)
project
end
end
......@@ -25,6 +25,7 @@ describe Gitlab::UrlBuilder do
:project_snippet | ->(snippet) { "/#{snippet.project.full_path}/snippets/#{snippet.id}" }
:project_wiki | ->(wiki) { "/#{wiki.container.full_path}/-/wikis/home" }
:ci_build | ->(build) { "/#{build.project.full_path}/-/jobs/#{build.id}" }
:design | ->(design) { "/#{design.project.full_path}/-/design_management/designs/#{design.id}/raw_image" }
:group | ->(group) { "/groups/#{group.full_path}" }
:group_milestone | ->(milestone) { "/groups/#{milestone.group.full_path}/-/milestones/#{milestone.iid}" }
......@@ -95,6 +96,16 @@ describe Gitlab::UrlBuilder do
end
end
context 'when passing a DesignManagement::Design' do
let(:design) { build_stubbed(:design) }
it 'uses the given ref and size in the URL' do
url = subject.build(design, ref: 'feature', size: 'small')
expect(url).to eq "#{Settings.gitlab['url']}/#{design.project.full_path}/-/design_management/designs/#{design.id}/feature/resized_image/small"
end
end
context 'when passing an unsupported class' do
let(:object) { Object.new }
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe Gitlab::UsageCounters::DesignsCounter do
describe Gitlab::UsageDataCounters::DesignsCounter do
it_behaves_like 'a redis usage counter', 'Designs', :create
it_behaves_like 'a redis usage counter', 'Designs', :update
it_behaves_like 'a redis usage counter', 'Designs', :delete
......
......@@ -159,6 +159,46 @@ describe API::Todos do
expect { get api('/todos', john_doe) }.not_to exceed_query_limit(control)
expect(response).to have_gitlab_http_status(:ok)
end
context 'when there is a Design Todo' do
let!(:design_todo) { create_todo_for_mentioned_in_design }
def create_todo_for_mentioned_in_design
issue = create(:issue, project: project_1)
create(:todo, :mentioned,
user: john_doe,
project: project_1,
target: create(:design, issue: issue),
author: create(:user),
note: create(:note, project: project_1, note: "I am note, hear me roar"))
end
def api_request
get api('/todos', john_doe)
end
before do
api_request
end
specify do
expect(response).to have_gitlab_http_status(:ok)
end
it 'avoids N+1 queries', :request_store do
control = ActiveRecord::QueryRecorder.new { api_request }
create_todo_for_mentioned_in_design
expect { api_request }.not_to exceed_query_limit(control)
end
it 'includes the Design Todo in the response' do
expect(json_response).to include(
a_hash_including('id' => design_todo.id)
)
end
end
end
describe 'POST /todos/:id/mark_as_done' do
......
......@@ -46,8 +46,8 @@ describe Projects::ImportExport::ExportService do
# in the corresponding EE spec.
skip if Gitlab.ee?
# once for the normal repo, once for the wiki
expect(Gitlab::ImportExport::RepoSaver).to receive(:new).twice.and_call_original
# once for the normal repo, once for the wiki repo, and once for the design repo
expect(Gitlab::ImportExport::RepoSaver).to receive(:new).exactly(3).times.and_call_original
service.execute
end
......@@ -58,6 +58,12 @@ describe Projects::ImportExport::ExportService do
service.execute
end
it 'saves the design repo' do
expect(Gitlab::ImportExport::DesignRepoSaver).to receive(:new).and_call_original
service.execute
end
it 'saves the lfs objects' do
expect(Gitlab::ImportExport::LfsSaver).to receive(:new).and_call_original
......
......@@ -19,6 +19,9 @@ module UsageDataHelpers
cycle_analytics_views
productivity_analytics_views
source_code_pushes
design_management_designs_create
design_management_designs_update
design_management_designs_delete
).freeze
COUNTS_KEYS = %i(
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment