Commit f88ac1ef authored by Natalia Tepluhina's avatar Natalia Tepluhina

Implemented basic design fetching

- project is split onto 2 queries
- removed the local qury
parent 37dbffcb
...@@ -539,7 +539,7 @@ type DesignCollection { ...@@ -539,7 +539,7 @@ type DesignCollection {
""" """
Filters designs to only those that existed at the version. If argument is Filters designs to only those that existed at the version. If argument is
omitted or nil then all designs will reflect the latest version. omitted or nil then all designs will reflect the latest version
""" """
atVersion: ID atVersion: ID
...@@ -548,13 +548,18 @@ type DesignCollection { ...@@ -548,13 +548,18 @@ type DesignCollection {
""" """
before: String before: String
"""
Filters designs by their filename
"""
filenames: [String!]
""" """
Returns the first _n_ elements from the list. Returns the first _n_ elements from the list.
""" """
first: Int first: Int
""" """
The list of IDs of designs. Filters designs by their ID
""" """
ids: [ID!] ids: [ID!]
......
...@@ -7979,7 +7979,7 @@ ...@@ -7979,7 +7979,7 @@
"args": [ "args": [
{ {
"name": "ids", "name": "ids",
"description": "The list of IDs of designs.", "description": "Filters designs by their ID",
"type": { "type": {
"kind": "LIST", "kind": "LIST",
"name": null, "name": null,
...@@ -7995,9 +7995,27 @@ ...@@ -7995,9 +7995,27 @@
}, },
"defaultValue": null "defaultValue": null
}, },
{
"name": "filenames",
"description": "Filters designs by their filename",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
},
"defaultValue": null
},
{ {
"name": "atVersion", "name": "atVersion",
"description": "Filters designs to only those that existed at the version. If argument is omitted or nil then all designs will reflect the latest version.", "description": "Filters designs to only those that existed at the version. If argument is omitted or nil then all designs will reflect the latest version",
"type": { "type": {
"kind": "SCALAR", "kind": "SCALAR",
"name": "ID", "name": "ID",
......
<script> <script>
import { __, s__, sprintf } from '~/locale';
import createFlash from '~/flash';
import { ApolloMutation } from 'vue-apollo'; import { ApolloMutation } from 'vue-apollo';
import projectQuery from '../graphql/queries/project.query.graphql'; import projectQuery from '../graphql/queries/project.query.graphql';
import destroyDesignMutation from '../graphql/mutations/destroyDesign.mutation.graphql'; import destroyDesignMutation from '../graphql/mutations/destroyDesign.mutation.graphql';
import { import { updateStoreAfterDesignsDelete } from '../utils/design_management_utils';
updateStoreAfterDesignsDelete,
onDesignDeletionError,
} from '../utils/design_management_utils';
export default { export default {
components: { components: {
...@@ -34,8 +33,13 @@ export default { ...@@ -34,8 +33,13 @@ export default {
}, },
}, },
methods: { methods: {
onError(...args) { onError() {
onDesignDeletionError(...args); const design = this.filenames.length > 1 ? __('designs') : __('a design');
createFlash(
sprintf(s__('DesignManagement|We could not delete %{design}. Please try again.'), {
design,
}),
);
}, },
updateStoreAfterDelete( updateStoreAfterDelete(
store, store,
......
...@@ -8,7 +8,7 @@ import createNoteMutation from '../../graphql/mutations/createNote.mutation.grap ...@@ -8,7 +8,7 @@ import createNoteMutation from '../../graphql/mutations/createNote.mutation.grap
import getDesignQuery from '../../graphql/queries/getDesign.query.graphql'; import getDesignQuery from '../../graphql/queries/getDesign.query.graphql';
import DesignNote from './design_note.vue'; import DesignNote from './design_note.vue';
import DesignReplyForm from './design_reply_form.vue'; import DesignReplyForm from './design_reply_form.vue';
import { extractCurrentDiscussion } from '../../utils/design_management_utils'; import { extractCurrentDiscussion, extractDesign } from '../../utils/design_management_utils';
export default { export default {
components: { components: {
...@@ -55,6 +55,14 @@ export default { ...@@ -55,6 +55,14 @@ export default {
discussionId: this.discussion.id, discussionId: this.discussion.id,
}; };
}, },
designVariables() {
return {
fullPath: this.projectPath,
iid: this.issueIid,
filenames: [this.$route.params.id],
atVersion: this.designsVersion,
};
},
}, },
methods: { methods: {
addDiscussionComment( addDiscussionComment(
...@@ -65,54 +73,29 @@ export default { ...@@ -65,54 +73,29 @@ export default {
) { ) {
const data = store.readQuery({ const data = store.readQuery({
query: getDesignQuery, query: getDesignQuery,
variables: { variables: this.designVariables,
id: this.designId,
version: this.designsVersion,
},
}); });
const currentDiscussion = extractCurrentDiscussion(
data.design.discussions,
this.discussion.id,
);
const updatedDiscussion = { const design = extractDesign(data);
...currentDiscussion, const currentDiscussion = extractCurrentDiscussion(design.discussions, this.discussion.id);
node: { currentDiscussion.node.notes.edges = [
...currentDiscussion.node, ...currentDiscussion.node.notes.edges,
notes: { {
...currentDiscussion.node.notes, __typename: 'NoteEdge',
edges: [ node: createNote.note,
...currentDiscussion.node.notes.edges,
{ __typename: 'NoteEdge', node: createNote.note },
],
},
}, },
}; ];
const currentDiscussionIndex = data.design.discussions.edges.indexOf(currentDiscussion);
const payload = {
...data,
design: {
...data.design,
discussions: {
...data.design.discussions,
edges: [
...data.design.discussions.edges.slice(0, currentDiscussionIndex),
updatedDiscussion,
...data.design.discussions.edges.slice(
currentDiscussionIndex + 1,
data.design.discussions.edges.length,
),
],
},
notesCount: data.design.notesCount + 1,
},
};
design.notesCount += 1;
store.writeQuery({ store.writeQuery({
query: getDesignQuery, query: getDesignQuery,
data: payload, variables: this.designVariables,
data: {
...data,
design: {
...design,
},
},
}); });
}, },
onDone() { onDone() {
......
#import "./designNote.fragment.graphql"
#import "./designList.fragment.graphql"
#import "./diffRefs.fragment.graphql"
fragment DesignItem on Design {
...DesignListItem
fullPath
diffRefs {
...DesignDiffRefs
}
discussions {
edges {
node {
id
replyId
notes {
edges {
node {
...DesignNote
}
}
}
}
}
}
}
#import "./designNote.fragment.graphql"
#import "./diffRefs.fragment.graphql"
fragment DesignListItem on Design { fragment DesignListItem on Design {
id id
image image
event event
filename filename
fullPath
diffRefs {
...DesignDiffRefs
}
notesCount notesCount
discussions {
edges {
node {
id
replyId
notes {
edges {
node {
...DesignNote
}
}
}
}
}
}
} }
#import "../fragments/designList.fragment.graphql" #import "../fragments/design.fragment.graphql"
mutation uploadDesign($files: [Upload!]!, $projectPath: ID!, $iid: ID!) { mutation uploadDesign($files: [Upload!]!, $projectPath: ID!, $iid: ID!) {
designManagementUpload(input: { projectPath: $projectPath, iid: $iid, files: $files }) { designManagementUpload(input: { projectPath: $projectPath, iid: $iid, files: $files }) {
designs { designs {
...DesignListItem ...DesignItem
versions { versions {
edges { edges {
node { node {
......
#import "../fragments/designList.fragment.graphql" #import "../fragments/design.fragment.graphql"
query getDesign($id: String!, $version: String) { query getDesign($fullPath: ID!, $iid: String!, $atVersion: ID, $filenames: [String!]) {
design(id: $id, version: $version) @client { project(fullPath: $fullPath) {
...DesignListItem id
issue(iid: $iid) {
designCollection {
designs(atVersion: $atVersion, filenames: $filenames) {
edges {
node {
...DesignItem
}
}
}
}
}
} }
} }
...@@ -11,8 +11,9 @@ import DesignDiscussion from '../../components/design_notes/design_discussion.vu ...@@ -11,8 +11,9 @@ import DesignDiscussion from '../../components/design_notes/design_discussion.vu
import DesignReplyForm from '../../components/design_notes/design_reply_form.vue'; import DesignReplyForm from '../../components/design_notes/design_reply_form.vue';
import DesignDestroyer from '../../components/design_destroyer.vue'; import DesignDestroyer from '../../components/design_destroyer.vue';
import getDesignQuery from '../../graphql/queries/getDesign.query.graphql'; import getDesignQuery from '../../graphql/queries/getDesign.query.graphql';
import appDataQuery from '../../graphql/queries/appData.query.graphql';
import createImageDiffNoteMutation from '../../graphql/mutations/createImageDiffNote.mutation.graphql'; import createImageDiffNoteMutation from '../../graphql/mutations/createImageDiffNote.mutation.graphql';
import { extractDiscussions } from '../../utils/design_management_utils'; import { extractDiscussions, extractDesign } from '../../utils/design_management_utils';
export default { export default {
components: { components: {
...@@ -41,18 +42,25 @@ export default { ...@@ -41,18 +42,25 @@ export default {
height: 0, height: 0,
}, },
projectPath: '', projectPath: '',
issueId: '',
isNoteSaving: false, isNoteSaving: false,
}; };
}, },
apollo: { apollo: {
appData: {
query: appDataQuery,
manual: true,
result({ data: { projectPath, issueIid } }) {
this.projectPath = projectPath;
this.issueIid = issueIid;
},
},
design: { design: {
query: getDesignQuery, query: getDesignQuery,
variables() { variables() {
return { return this.designVariables;
id: this.id,
version: this.designsVersion,
};
}, },
update: data => extractDesign(data),
result({ data }) { result({ data }) {
if (!data) { if (!data) {
createFlash(s__('DesignManagement|Could not find design, please try again.')); createFlash(s__('DesignManagement|Could not find design, please try again.'));
...@@ -84,6 +92,14 @@ export default { ...@@ -84,6 +92,14 @@ export default {
renderDiscussions() { renderDiscussions() {
return this.discussions.length || this.annotationCoordinates; return this.discussions.length || this.annotationCoordinates;
}, },
designVariables() {
return {
fullPath: this.projectPath,
iid: this.issueIid,
filenames: [this.$route.params.id],
atVersion: this.designsVersion,
};
},
}, },
mounted() { mounted() {
Mousetrap.bind('esc', this.closeDesign); Mousetrap.bind('esc', this.closeDesign);
...@@ -119,10 +135,7 @@ export default { ...@@ -119,10 +135,7 @@ export default {
update: (store, { data: { createImageDiffNote } }) => { update: (store, { data: { createImageDiffNote } }) => {
const data = store.readQuery({ const data = store.readQuery({
query: getDesignQuery, query: getDesignQuery,
variables: { variables: this.designVariables,
id: this.id,
version: this.designsVersion,
},
}); });
const newDiscussion = { const newDiscussion = {
__typename: 'DiscussionEdge', __typename: 'DiscussionEdge',
...@@ -143,9 +156,20 @@ export default { ...@@ -143,9 +156,20 @@ export default {
}, },
}, },
}; };
data.design.discussions.edges.push(newDiscussion); const design = extractDesign(data);
data.design.notesCount += 1; design.discussions.edges = [...design.discussions.edges, newDiscussion];
store.writeQuery({ query: getDesignQuery, data }); design.notesCount += 1;
store.writeQuery({
query: getDesignQuery,
variables: this.designVariables,
data: {
...data,
design: {
...design,
notesCount: design.notesCount + 1,
},
},
});
}, },
}) })
.then(() => { .then(() => {
......
...@@ -47,6 +47,13 @@ const deleteDesignsFromStore = (store, query, selectedDesigns) => { ...@@ -47,6 +47,13 @@ const deleteDesignsFromStore = (store, query, selectedDesigns) => {
}); });
}; };
/**
* Adds a new version of designs to store
*
* @param {Object} store
* @param {Object} query
* @param {Object} version
*/
const addNewVersionToStore = (store, query, version) => { const addNewVersionToStore = (store, query, version) => {
if (!version) return; if (!version) return;
...@@ -64,14 +71,19 @@ const addNewVersionToStore = (store, query, version) => { ...@@ -64,14 +71,19 @@ const addNewVersionToStore = (store, query, version) => {
}); });
}; };
export const onDesignDeletionError = e => { /**
createFlash(s__('DesignManagement|We could not delete design(s). Please try again.')); * Updates a store after design deletion
throw e; *
}; * @param {Object} store
* @param {Object} data
* @param {Object} query
* @param {Array} designs
*/
export const updateStoreAfterDesignsDelete = (store, data, query, designs) => { export const updateStoreAfterDesignsDelete = (store, data, query, designs) => {
if (data.errors) { if (data.errors) {
onDesignDeletionError(new Error(data.errors)); createFlash(s__('DesignManagement|We could not delete design(s). Please try again.'));
throw new Error(data.errors);
} else { } else {
deleteDesignsFromStore(store, query, designs); deleteDesignsFromStore(store, query, designs);
addNewVersionToStore(store, query, data.version); addNewVersionToStore(store, query, data.version);
...@@ -79,3 +91,5 @@ export const updateStoreAfterDesignsDelete = (store, data, query, designs) => { ...@@ -79,3 +91,5 @@ export const updateStoreAfterDesignsDelete = (store, data, query, designs) => {
}; };
export const findVersionId = id => id.match('::Version/(.+$)')[1]; export const findVersionId = id => id.match('::Version/(.+$)')[1];
export const extractDesign = data => data.project.issue.designCollection.designs.edges[0].node;
...@@ -17,7 +17,8 @@ module DesignManagement ...@@ -17,7 +17,8 @@ module DesignManagement
items = init_collection items = init_collection
items = by_visible_at_version(items) items = by_visible_at_version(items)
items = by_ids(items) items = by_filename(items)
items = by_id(items)
items items
end end
...@@ -37,7 +38,13 @@ module DesignManagement ...@@ -37,7 +38,13 @@ module DesignManagement
items.visible_at_version(params[:visible_at_version]) items.visible_at_version(params[:visible_at_version])
end end
def by_ids(items) def by_filename(items)
return items unless params[:filenames].present?
items.with_filename(params[:filenames])
end
def by_id(items)
return items unless params[:ids].present? return items unless params[:ids].present?
items.id_in(params[:ids]) items.id_in(params[:ids])
......
...@@ -3,12 +3,19 @@ ...@@ -3,12 +3,19 @@
module Resolvers module Resolvers
module DesignManagement module DesignManagement
class DesignResolver < BaseResolver class DesignResolver < BaseResolver
argument :ids, [GraphQL::ID_TYPE], required: false, description: 'The list of IDs of designs.' argument :ids,
[GraphQL::ID_TYPE],
required: false,
description: 'Filters designs by their ID'
argument :filenames,
[GraphQL::STRING_TYPE],
required: false,
description: 'Filters designs by their filename'
argument :at_version, argument :at_version,
GraphQL::ID_TYPE, GraphQL::ID_TYPE,
required: false, required: false,
description: 'Filters designs to only those that existed at the version. ' \ description: 'Filters designs to only those that existed at the version. ' \
'If argument is omitted or nil then all designs will reflect the latest version.' 'If argument is omitted or nil then all designs will reflect the latest version'
def resolve(**args) def resolve(**args)
find_designs(args) find_designs(args)
...@@ -27,6 +34,7 @@ module Resolvers ...@@ -27,6 +34,7 @@ module Resolvers
object.issue, object.issue,
context[:current_user], context[:current_user],
ids: design_ids(args), ids: design_ids(args),
filenames: args[:filenames],
visible_at_version: version(args) visible_at_version: version(args)
).execute ).execute
end end
......
...@@ -49,6 +49,8 @@ module DesignManagement ...@@ -49,6 +49,8 @@ module DesignManagement
joins(join.join_sources).where(actions[:event].not_eq(deletion)).order(:id) joins(join.join_sources).where(actions[:event].not_eq(deletion)).order(:id)
end end
scope :with_filename, -> (filenames) { where(filename: filenames) }
# Scope called by our REST API to avoid N+1 problems # Scope called by our REST API to avoid N+1 problems
scope :with_api_entity_associations, -> { preload(:issue) } scope :with_api_entity_associations, -> { preload(:issue) }
......
...@@ -5,11 +5,12 @@ require 'spec_helper' ...@@ -5,11 +5,12 @@ require 'spec_helper'
describe DesignManagement::DesignsFinder do describe DesignManagement::DesignsFinder do
include DesignManagementTestHelpers include DesignManagementTestHelpers
set(:user) { create(:user) } let_it_be(:user) { create(:user) }
set(:project) { create(:project, :private) } let_it_be(:project) { create(:project, :private) }
set(:issue) { create(:issue, project: project) } let_it_be(:issue) { create(:issue, project: project) }
set(:design1) { create(:design, :with_file, issue: issue, versions_count: 1) } let_it_be(:design1) { create(:design, :with_file, issue: issue, versions_count: 1) }
set(:design2) { create(:design, :with_file, issue: issue, versions_count: 1) } let_it_be(:design2) { create(:design, :with_file, issue: issue, versions_count: 1) }
let_it_be(:design3) { create(:design, :with_file, issue: issue, versions_count: 1) }
let(:params) { {} } let(:params) { {} }
subject(:designs) { described_class.new(issue, user, params).execute } subject(:designs) { described_class.new(issue, user, params).execute }
...@@ -38,13 +39,25 @@ describe DesignManagement::DesignsFinder do ...@@ -38,13 +39,25 @@ describe DesignManagement::DesignsFinder do
end end
it 'returns the designs' do it 'returns the designs' do
is_expected.to contain_exactly(design2, design1) is_expected.to contain_exactly(design1, design2, design3)
end
context 'when argument is the ids of designs' do
let(:params) { { ids: [design1.id] } }
it { is_expected.to eq([design1]) }
end
context 'when argument is the filenames of designs' do
let(:params) { { filenames: [design2.filename] } }
it { is_expected.to eq([design2]) }
end end
describe 'returning designs that existed at a particular given version' do describe 'returning designs that existed at a particular given version' do
let(:all_versions) { issue.design_collection.versions.ordered } let(:all_versions) { issue.design_collection.versions.ordered }
let(:first_version) { all_versions.last } let(:first_version) { all_versions.last }
let(:second_version) { all_versions.first } let(:second_version) { all_versions.second }
context 'when argument is the first version' do context 'when argument is the first version' do
let(:params) { { visible_at_version: first_version } } let(:params) { { visible_at_version: first_version } }
...@@ -52,12 +65,6 @@ describe DesignManagement::DesignsFinder do ...@@ -52,12 +65,6 @@ describe DesignManagement::DesignsFinder do
it { is_expected.to eq([design1]) } it { is_expected.to eq([design1]) }
end end
context 'when argument is the ids of designs' do
let(:params) { { ids: [design1.id] } }
it { is_expected.to eq([design1]) }
end
context 'when arguments are version and id' do context 'when arguments are version and id' do
context 'when id is absent at version' do context 'when id is absent at version' do
let(:params) { { visible_at_version: first_version, ids: [design2.id] } } let(:params) { { visible_at_version: first_version, ids: [design2.id] } }
...@@ -75,7 +82,7 @@ describe DesignManagement::DesignsFinder do ...@@ -75,7 +82,7 @@ describe DesignManagement::DesignsFinder do
context 'when argument is the second version' do context 'when argument is the second version' do
let(:params) { { visible_at_version: second_version } } let(:params) { { visible_at_version: second_version } }
it { is_expected.to contain_exactly(design2, design1) } it { is_expected.to contain_exactly(design1, design2) }
end end
end end
end end
......
...@@ -144,6 +144,18 @@ describe DesignManagement::Design do ...@@ -144,6 +144,18 @@ describe DesignManagement::Design do
end end
end end
describe '.with_filename' do
it 'returns correct design when passed a single filename' do
expect(described_class.with_filename(design1.filename)).to eq([design1])
end
it 'returns correct designs when passed an Array of filenames' do
expect(
described_class.with_filename([design1, design2].map(&:filename))
).to contain_exactly(design1, design2)
end
end
describe '.current' do describe '.current' do
it 'returns just the undeleted designs' do it 'returns just the undeleted designs' do
delete_designs(design3) delete_designs(design3)
......
...@@ -5800,6 +5800,9 @@ msgstr "" ...@@ -5800,6 +5800,9 @@ msgstr ""
msgid "DesignManagement|Upload and view the latest designs for this issue. Consistent and easy to find, so everyone is up to date." msgid "DesignManagement|Upload and view the latest designs for this issue. Consistent and easy to find, so everyone is up to date."
msgstr "" msgstr ""
msgid "DesignManagement|We could not delete %{design}. Please try again."
msgstr ""
msgid "DesignManagement|We could not delete design(s). Please try again." msgid "DesignManagement|We could not delete design(s). Please try again."
msgstr "" msgstr ""
...@@ -20127,6 +20130,9 @@ msgstr "" ...@@ -20127,6 +20130,9 @@ msgstr ""
msgid "a deleted user" msgid "a deleted user"
msgstr "" msgstr ""
msgid "a design"
msgstr ""
msgid "added %{created_at_timeago}" msgid "added %{created_at_timeago}"
msgstr "" msgstr ""
...@@ -20521,6 +20527,9 @@ msgstr "" ...@@ -20521,6 +20527,9 @@ msgstr ""
msgid "design" msgid "design"
msgstr "" msgstr ""
msgid "designs"
msgstr ""
msgid "detached" msgid "detached"
msgstr "" msgstr ""
......
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