diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-08-20 21:42:06 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-08-20 21:42:06 +0300 |
commit | 6e4e1050d9dba2b7b2523fdd1768823ab85feef4 (patch) | |
tree | 78be5963ec075d80116a932011d695dd33910b4e /app/assets/javascripts/snippets | |
parent | 1ce776de4ae122aba3f349c02c17cebeaa8ecf07 (diff) |
Add latest changes from gitlab-org/gitlab@13-3-stable-ee
Diffstat (limited to 'app/assets/javascripts/snippets')
11 files changed, 346 insertions, 168 deletions
diff --git a/app/assets/javascripts/snippets/components/edit.vue b/app/assets/javascripts/snippets/components/edit.vue index c01f9524ca8..6e3a670dc38 100644 --- a/app/assets/javascripts/snippets/components/edit.vue +++ b/app/assets/javascripts/snippets/components/edit.vue @@ -1,7 +1,7 @@ <script> import { GlButton, GlLoadingIcon } from '@gitlab/ui'; -import Flash from '~/flash'; +import { deprecatedCreateFlash as Flash } from '~/flash'; import { __, sprintf } from '~/locale'; import TitleField from '~/vue_shared/components/form/title.vue'; import { redirectTo } from '~/lib/utils/url_utility'; @@ -14,19 +14,17 @@ import { SNIPPET_VISIBILITY_PRIVATE, SNIPPET_CREATE_MUTATION_ERROR, SNIPPET_UPDATE_MUTATION_ERROR, - SNIPPET_BLOB_ACTION_CREATE, - SNIPPET_BLOB_ACTION_UPDATE, - SNIPPET_BLOB_ACTION_MOVE, } from '../constants'; -import SnippetBlobEdit from './snippet_blob_edit.vue'; +import SnippetBlobActionsEdit from './snippet_blob_actions_edit.vue'; import SnippetVisibilityEdit from './snippet_visibility_edit.vue'; import SnippetDescriptionEdit from './snippet_description_edit.vue'; +import { SNIPPET_MARK_EDIT_APP_START } from '~/performance_constants'; export default { components: { SnippetDescriptionEdit, SnippetVisibilityEdit, - SnippetBlobEdit, + SnippetBlobActionsEdit, TitleField, FormFooterActions, GlButton, @@ -55,25 +53,20 @@ export default { }, data() { return { - blobsActions: {}, isUpdating: false, newSnippet: false, + actions: [], }; }, computed: { - getActionsEntries() { - return Object.values(this.blobsActions); + hasBlobChanges() { + return this.actions.length > 0; }, - allBlobsHaveContent() { - const entries = this.getActionsEntries; - return entries.length > 0 && !entries.find(action => !action.content); - }, - allBlobChangesRegistered() { - const entries = this.getActionsEntries; - return entries.length > 0 && !entries.find(action => action.action === ''); + hasValidBlobs() { + return this.actions.every(x => x.filePath && x.content); }, updatePrevented() { - return this.snippet.title === '' || !this.allBlobsHaveContent || this.isUpdating; + return this.snippet.title === '' || !this.hasValidBlobs || this.isUpdating; }, isProjectSnippet() { return Boolean(this.projectPath); @@ -84,7 +77,7 @@ export default { title: this.snippet.title, description: this.snippet.description, visibilityLevel: this.snippet.visibilityLevel, - files: this.getActionsEntries.filter(entry => entry.action !== ''), + blobActions: this.actions, }; }, saveButtonLabel() { @@ -95,7 +88,7 @@ export default { }, cancelButtonHref() { if (this.newSnippet) { - return this.projectPath ? `/${this.projectPath}/snippets` : `/snippets`; + return this.projectPath ? `/${this.projectPath}/-/snippets` : `/-/snippets`; } return this.snippet.webUrl; }, @@ -106,6 +99,9 @@ export default { return `${this.isProjectSnippet ? 'project' : 'personal'}_snippet_description`; }, }, + beforeCreate() { + performance.mark(SNIPPET_MARK_EDIT_APP_START); + }, created() { window.addEventListener('beforeunload', this.onBeforeUnload); }, @@ -116,48 +112,11 @@ export default { onBeforeUnload(e = {}) { const returnValue = __('Are you sure you want to lose unsaved changes?'); - if (!this.allBlobChangesRegistered) return undefined; + if (!this.hasBlobChanges || this.isUpdating) return undefined; Object.assign(e, { returnValue }); return returnValue; }, - updateBlobActions(args = {}) { - // `_constants` is the internal prop that - // should not be sent to the mutation. Hence we filter it out from - // the argsToUpdateAction that is the data-basis for the mutation. - const { _constants: blobConstants, ...argsToUpdateAction } = args; - const { previousPath, filePath, content } = argsToUpdateAction; - let actionEntry = this.blobsActions[blobConstants.id] || {}; - let tunedActions = { - action: '', - previousPath, - }; - - if (this.newSnippet) { - // new snippet, hence new blob - tunedActions = { - action: SNIPPET_BLOB_ACTION_CREATE, - previousPath: '', - }; - } else if (previousPath && filePath) { - // renaming of a blob + renaming & content update - const renamedToOriginal = filePath === blobConstants.originalPath; - tunedActions = { - action: renamedToOriginal ? SNIPPET_BLOB_ACTION_UPDATE : SNIPPET_BLOB_ACTION_MOVE, - previousPath: !renamedToOriginal ? blobConstants.originalPath : '', - }; - } else if (content !== blobConstants.originalContent) { - // content update only - tunedActions = { - action: SNIPPET_BLOB_ACTION_UPDATE, - previousPath: '', - }; - } - - actionEntry = { ...actionEntry, ...argsToUpdateAction, ...tunedActions }; - - this.$set(this.blobsActions, blobConstants.id, actionEntry); - }, flashAPIFailure(err) { const defaultErrorMsg = this.newSnippet ? SNIPPET_CREATE_MUTATION_ERROR @@ -214,7 +173,6 @@ export default { if (errors.length) { this.flashAPIFailure(errors[0]); } else { - this.originalContent = this.content; redirectTo(baseObj.snippet.webUrl); } }) @@ -222,6 +180,9 @@ export default { this.flashAPIFailure(e); }); }, + updateActions(actions) { + this.actions = actions; + }, }, newSnippetSchema: { title: '', @@ -257,15 +218,7 @@ export default { :markdown-preview-path="markdownPreviewPath" :markdown-docs-path="markdownDocsPath" /> - <template v-if="blobs.length"> - <snippet-blob-edit - v-for="blob in blobs" - :key="blob.name" - :blob="blob" - @blob-updated="updateBlobActions" - /> - </template> - <snippet-blob-edit v-else @blob-updated="updateBlobActions" /> + <snippet-blob-actions-edit :init-blobs="blobs" @actions="updateActions" /> <snippet-visibility-edit v-model="snippet.visibilityLevel" diff --git a/app/assets/javascripts/snippets/components/show.vue b/app/assets/javascripts/snippets/components/show.vue index 0779e87e6b6..ca41fd0a2b1 100644 --- a/app/assets/javascripts/snippets/components/show.vue +++ b/app/assets/javascripts/snippets/components/show.vue @@ -1,13 +1,16 @@ <script> +import { GlLoadingIcon } from '@gitlab/ui'; import BlobEmbeddable from '~/blob/components/blob_embeddable.vue'; import SnippetHeader from './snippet_header.vue'; import SnippetTitle from './snippet_title.vue'; import SnippetBlob from './snippet_blob_view.vue'; -import { GlLoadingIcon } from '@gitlab/ui'; +import CloneDropdownButton from '~/vue_shared/components/clone_dropdown.vue'; import { getSnippetMixin } from '../mixins/snippets'; import { SNIPPET_VISIBILITY_PUBLIC } from '~/snippets/constants'; +import { SNIPPET_MARK_VIEW_APP_START } from '~/performance_constants'; + export default { components: { BlobEmbeddable, @@ -15,12 +18,19 @@ export default { SnippetTitle, GlLoadingIcon, SnippetBlob, + CloneDropdownButton, }, mixins: [getSnippetMixin], computed: { embeddable() { return this.snippet.visibilityLevel === SNIPPET_VISIBILITY_PUBLIC; }, + canBeCloned() { + return Boolean(this.snippet.sshUrlToRepo || this.snippet.httpUrlToRepo); + }, + }, + beforeCreate() { + performance.mark(SNIPPET_MARK_VIEW_APP_START); }, }; </script> @@ -35,10 +45,17 @@ export default { <template v-else> <snippet-header :snippet="snippet" /> <snippet-title :snippet="snippet" /> - <blob-embeddable v-if="embeddable" class="gl-mb-5" :url="snippet.webUrl" /> - <div v-for="blob in blobs" :key="blob.path"> - <snippet-blob :snippet="snippet" :blob="blob" /> + <div class="gl-display-flex gl-justify-content-end gl-mb-5"> + <blob-embeddable v-if="embeddable" class="gl-flex-fill-1" :url="snippet.webUrl" /> + <clone-dropdown-button + v-if="canBeCloned" + class="gl-ml-3" + :ssh-link="snippet.sshUrlToRepo" + :http-link="snippet.httpUrlToRepo" + data-qa-selector="clone_button" + /> </div> + <snippet-blob v-for="blob in blobs" :key="blob.path" :snippet="snippet" :blob="blob" /> </template> </div> </template> diff --git a/app/assets/javascripts/snippets/components/snippet_blob_actions_edit.vue b/app/assets/javascripts/snippets/components/snippet_blob_actions_edit.vue new file mode 100644 index 00000000000..55cd13a6930 --- /dev/null +++ b/app/assets/javascripts/snippets/components/snippet_blob_actions_edit.vue @@ -0,0 +1,156 @@ +<script> +import { GlButton } from '@gitlab/ui'; +import { cloneDeep } from 'lodash'; +import { s__, sprintf } from '~/locale'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; +import SnippetBlobEdit from './snippet_blob_edit.vue'; +import { SNIPPET_MAX_BLOBS } from '../constants'; +import { createBlob, decorateBlob, diffAll } from '../utils/blob'; + +export default { + components: { + SnippetBlobEdit, + GlButton, + }, + mixins: [glFeatureFlagsMixin()], + props: { + initBlobs: { + type: Array, + required: true, + }, + }, + data() { + return { + // This is a dictionary (by .id) of the original blobs and + // is used as the baseline for calculating diffs + // (e.g., what has been deleted, changed, renamed, etc.) + blobsOrig: {}, + // This is a dictionary (by .id) of the current blobs and + // is updated as the user makes changes. + blobs: {}, + // This is a list of blob ID's in order how they should be + // presented. + blobIds: [], + }; + }, + computed: { + actions() { + return diffAll(this.blobs, this.blobsOrig); + }, + count() { + return this.blobIds.length; + }, + addLabel() { + return sprintf(s__('Snippets|Add another file %{num}/%{total}'), { + num: this.count, + total: SNIPPET_MAX_BLOBS, + }); + }, + canDelete() { + return this.count > 1; + }, + canAdd() { + return this.count < SNIPPET_MAX_BLOBS; + }, + hasMultiFilesEnabled() { + return this.glFeatures.snippetMultipleFiles; + }, + filesLabel() { + return this.hasMultiFilesEnabled ? s__('Snippets|Files') : s__('Snippets|File'); + }, + firstInputId() { + const blobId = this.blobIds[0]; + + if (!blobId) { + return ''; + } + + return `${blobId}_file_path`; + }, + }, + watch: { + actions: { + immediate: true, + handler(val) { + this.$emit('actions', val); + }, + }, + }, + created() { + const blobs = this.initBlobs.map(decorateBlob); + const blobsById = blobs.reduce((acc, x) => Object.assign(acc, { [x.id]: x }), {}); + + this.blobsOrig = blobsById; + this.blobs = cloneDeep(blobsById); + this.blobIds = blobs.map(x => x.id); + + // Show 1 empty blob if none exist + if (!this.blobIds.length) { + this.addBlob(); + } + }, + methods: { + updateBlobContent(id, content) { + const origBlob = this.blobsOrig[id]; + const blob = this.blobs[id]; + + blob.content = content; + + // If we've received content, but we haven't loaded the content before + // then this is also the original content. + if (origBlob && !origBlob.isLoaded) { + blob.isLoaded = true; + origBlob.isLoaded = true; + origBlob.content = content; + } + }, + updateBlobFilePath(id, path) { + const blob = this.blobs[id]; + + blob.path = path; + }, + addBlob() { + const blob = createBlob(); + + this.$set(this.blobs, blob.id, blob); + this.blobIds.push(blob.id); + }, + deleteBlob(id) { + this.blobIds = this.blobIds.filter(x => x !== id); + this.$delete(this.blobs, id); + }, + updateBlob(id, args) { + if ('content' in args) { + this.updateBlobContent(id, args.content); + } + if ('path' in args) { + this.updateBlobFilePath(id, args.path); + } + }, + }, +}; +</script> +<template> + <div class="form-group file-editor"> + <label :for="firstInputId">{{ filesLabel }}</label> + <snippet-blob-edit + v-for="(blobId, index) in blobIds" + :key="blobId" + :class="{ 'gl-mt-3': index > 0 }" + :blob="blobs[blobId]" + :can-delete="canDelete" + :show-delete="hasMultiFilesEnabled" + @blob-updated="updateBlob(blobId, $event)" + @delete="deleteBlob(blobId)" + /> + <gl-button + v-if="hasMultiFilesEnabled" + :disabled="!canAdd" + data-testid="add_button" + class="gl-my-3" + variant="dashed" + @click="addBlob" + >{{ addLabel }}</gl-button + > + </div> +</template> diff --git a/app/assets/javascripts/snippets/components/snippet_blob_edit.vue b/app/assets/javascripts/snippets/components/snippet_blob_edit.vue index 3c2dbfff6e1..ff03432f942 100644 --- a/app/assets/javascripts/snippets/components/snippet_blob_edit.vue +++ b/app/assets/javascripts/snippets/components/snippet_blob_edit.vue @@ -1,19 +1,13 @@ <script> +import { GlLoadingIcon } from '@gitlab/ui'; import BlobHeaderEdit from '~/blob/components/blob_edit_header.vue'; import BlobContentEdit from '~/blob/components/blob_edit_content.vue'; -import { GlLoadingIcon } from '@gitlab/ui'; import { getBaseURL, joinPaths } from '~/lib/utils/url_utility'; import axios from '~/lib/utils/axios_utils'; import { SNIPPET_BLOB_CONTENT_FETCH_ERROR } from '~/snippets/constants'; -import Flash from '~/flash'; +import { deprecatedCreateFlash as Flash } from '~/flash'; import { sprintf } from '~/locale'; -function localId() { - return Math.floor((1 + Math.random()) * 0x10000) - .toString(16) - .substring(1); -} - export default { components: { BlobHeaderEdit, @@ -24,49 +18,35 @@ export default { props: { blob: { type: Object, + required: true, + }, + canDelete: { + type: Boolean, required: false, - default: null, - validator: ({ rawPath }) => Boolean(rawPath), + default: true, }, - }, - data() { - return { - id: localId(), - filePath: this.blob?.path || '', - previousPath: '', - originalPath: this.blob?.path || '', - content: this.blob?.content || '', - originalContent: '', - isContentLoading: this.blob, - }; - }, - watch: { - filePath(filePath, previousPath) { - this.previousPath = previousPath; - this.notifyAboutUpdates({ previousPath }); + showDelete: { + type: Boolean, + required: false, + default: false, }, - content() { - this.notifyAboutUpdates(); + }, + computed: { + inputId() { + return `${this.blob.id}_file_path`; }, }, mounted() { - if (this.blob) { + if (!this.blob.isLoaded) { this.fetchBlobContent(); } }, methods: { + onDelete() { + this.$emit('delete'); + }, notifyAboutUpdates(args = {}) { - const { filePath, previousPath } = args; - this.$emit('blob-updated', { - filePath: filePath || this.filePath, - previousPath: previousPath || this.previousPath, - content: this.content, - _constants: { - originalPath: this.originalPath, - originalContent: this.originalContent, - id: this.id, - }, - }); + this.$emit('blob-updated', args); }, fetchBlobContent() { const baseUrl = getBaseURL(); @@ -75,33 +55,39 @@ export default { axios .get(url) .then(res => { - this.originalContent = res.data; - this.content = res.data; + this.notifyAboutUpdates({ content: res.data }); }) - .catch(e => this.flashAPIFailure(e)) - .finally(() => { - this.isContentLoading = false; - }); + .catch(e => this.flashAPIFailure(e)); }, flashAPIFailure(err) { Flash(sprintf(SNIPPET_BLOB_CONTENT_FETCH_ERROR, { err })); - this.isContentLoading = false; }, }, }; </script> <template> - <div class="form-group file-editor"> - <label>{{ s__('Snippets|File') }}</label> - <div class="file-holder snippet"> - <blob-header-edit v-model="filePath" data-qa-selector="file_name_field" /> - <gl-loading-icon - v-if="isContentLoading" - :label="__('Loading snippet')" - size="lg" - class="loading-animation prepend-top-20 append-bottom-20" - /> - <blob-content-edit v-else v-model="content" :file-name="filePath" /> - </div> + <div class="file-holder snippet"> + <blob-header-edit + :id="inputId" + :value="blob.path" + data-qa-selector="file_name_field" + :can-delete="canDelete" + :show-delete="showDelete" + @input="notifyAboutUpdates({ path: $event })" + @delete="onDelete" + /> + <gl-loading-icon + v-if="!blob.isLoaded" + :label="__('Loading snippet')" + size="lg" + class="loading-animation prepend-top-20 append-bottom-20" + /> + <blob-content-edit + v-else + :value="blob.content" + :file-global-id="blob.id" + :file-name="blob.path" + @input="notifyAboutUpdates({ content: $event })" + /> </div> </template> diff --git a/app/assets/javascripts/snippets/components/snippet_blob_view.vue b/app/assets/javascripts/snippets/components/snippet_blob_view.vue index afd038eef58..b38be5bb9a4 100644 --- a/app/assets/javascripts/snippets/components/snippet_blob_view.vue +++ b/app/assets/javascripts/snippets/components/snippet_blob_view.vue @@ -1,7 +1,6 @@ <script> import BlobHeader from '~/blob/components/blob_header.vue'; import BlobContent from '~/blob/components/blob_content.vue'; -import CloneDropdownButton from '~/vue_shared/components/clone_dropdown.vue'; import GetBlobContent from '../queries/snippet.blob.content.query.graphql'; @@ -16,7 +15,6 @@ export default { components: { BlobHeader, BlobContent, - CloneDropdownButton, }, apollo: { blobContent: { @@ -27,8 +25,9 @@ export default { rich: this.activeViewerType === RICH_BLOB_VIEWER, }; }, - update: data => - data.snippets.edges[0].node.blob.richData || data.snippets.edges[0].node.blob.plainData, + update(data) { + return this.onContentUpdate(data); + }, result() { if (this.activeViewerType === RICH_BLOB_VIEWER) { this.blob.richViewer.renderError = null; @@ -66,9 +65,6 @@ export default { const { richViewer, simpleViewer } = this.blob; return this.activeViewerType === RICH_BLOB_VIEWER ? richViewer : simpleViewer; }, - canBeCloned() { - return this.snippet.sshUrlToRepo || this.snippet.httpUrlToRepo; - }, hasRenderError() { return Boolean(this.viewer.renderError); }, @@ -81,6 +77,12 @@ export default { this.$apollo.queries.blobContent.skip = false; this.$apollo.queries.blobContent.refetch(); }, + onContentUpdate(data) { + const { path: blobPath } = this.blob; + const { blobs } = data.snippets.edges[0].node; + const updatedBlobData = blobs.find(blob => blob.path === blobPath); + return updatedBlobData.richData || updatedBlobData.plainData; + }, }, BLOB_RENDER_EVENT_LOAD, BLOB_RENDER_EVENT_SHOW_SOURCE, @@ -93,17 +95,7 @@ export default { :active-viewer-type="viewer.type" :has-render-error="hasRenderError" @viewer-changed="switchViewer" - > - <template #actions> - <clone-dropdown-button - v-if="canBeCloned" - class="gl-mr-3" - :ssh-link="snippet.sshUrlToRepo" - :http-link="snippet.httpUrlToRepo" - data-qa-selector="clone_button" - /> - </template> - </blob-header> + /> <blob-content :loading="isContentLoading" :content="blobContent" diff --git a/app/assets/javascripts/snippets/components/snippet_header.vue b/app/assets/javascripts/snippets/components/snippet_header.vue index 707e2b0ea30..ed087dcfaf9 100644 --- a/app/assets/javascripts/snippets/components/snippet_header.vue +++ b/app/assets/javascripts/snippets/components/snippet_header.vue @@ -1,5 +1,4 @@ <script> -import { __ } from '~/locale'; import { GlAvatar, GlIcon, @@ -7,11 +6,12 @@ import { GlModal, GlAlert, GlLoadingIcon, - GlDropdown, - GlDropdownItem, + GlDeprecatedDropdown, + GlDeprecatedDropdownItem, GlButton, GlTooltipDirective, } from '@gitlab/ui'; +import { __ } from '~/locale'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import DeleteSnippetMutation from '../mutations/deleteSnippet.mutation.graphql'; @@ -26,8 +26,8 @@ export default { GlModal, GlAlert, GlLoadingIcon, - GlDropdown, - GlDropdownItem, + GlDeprecatedDropdown, + GlDeprecatedDropdownItem, TimeAgoTooltip, GlButton, }, @@ -68,6 +68,11 @@ export default { snippetHasBinary() { return Boolean(this.snippet.blobs.find(blob => blob.binary)); }, + authoredMessage() { + return this.snippet.author + ? __('Authored %{timeago} by %{author}') + : __('Authored %{timeago}'); + }, personalSnippetActions() { return [ { @@ -91,8 +96,8 @@ export default { condition: this.canCreateSnippet, text: __('New snippet'), href: this.snippet.project - ? `${this.snippet.project.webUrl}/snippets/new` - : '/snippets/new', + ? `${this.snippet.project.webUrl}/-/snippets/new` + : '/-/snippets/new', variant: 'success', category: 'secondary', cssClass: 'ml-2', @@ -130,7 +135,9 @@ export default { }, methods: { redirectToSnippets() { - window.location.pathname = `${this.snippet.project?.fullPath || 'dashboard'}/snippets`; + window.location.pathname = this.snippet.project + ? `${this.snippet.project.fullPath}/-/snippets` + : 'dashboard/snippets'; }, closeDeleteModal() { this.$refs.deleteModal.hide(); @@ -176,8 +183,8 @@ export default { </span> <gl-icon :name="visibilityLevelIcon" :size="14" /> </div> - <div class="creator"> - <gl-sprintf :message="__('Authored %{timeago} by %{author}')"> + <div class="creator" data-testid="authored-message"> + <gl-sprintf :message="authoredMessage"> <template #timeago> <time-ago-tooltip :time="snippet.createdAt" @@ -221,17 +228,17 @@ export default { </template> </div> <div class="d-block d-sm-none dropdown"> - <gl-dropdown :text="__('Options')" class="w-100" toggle-class="text-center"> - <gl-dropdown-item + <gl-deprecated-dropdown :text="__('Options')" class="w-100" toggle-class="text-center"> + <gl-deprecated-dropdown-item v-for="(action, index) in personalSnippetActions" :key="index" :disabled="action.disabled" :title="action.title" :href="action.href" @click="action.click ? action.click() : undefined" - >{{ action.text }}</gl-dropdown-item + >{{ action.text }}</gl-deprecated-dropdown-item > - </gl-dropdown> + </gl-deprecated-dropdown> </div> </div> diff --git a/app/assets/javascripts/snippets/constants.js b/app/assets/javascripts/snippets/constants.js index 99ee698408d..12b83525bf7 100644 --- a/app/assets/javascripts/snippets/constants.js +++ b/app/assets/javascripts/snippets/constants.js @@ -30,3 +30,6 @@ export const SNIPPET_BLOB_CONTENT_FETCH_ERROR = __("Can't fetch content for the export const SNIPPET_BLOB_ACTION_CREATE = 'create'; export const SNIPPET_BLOB_ACTION_UPDATE = 'update'; export const SNIPPET_BLOB_ACTION_MOVE = 'move'; +export const SNIPPET_BLOB_ACTION_DELETE = 'delete'; + +export const SNIPPET_MAX_BLOBS = 10; diff --git a/app/assets/javascripts/snippets/index.js b/app/assets/javascripts/snippets/index.js index 1c79492957d..bb5e7d6e3f0 100644 --- a/app/assets/javascripts/snippets/index.js +++ b/app/assets/javascripts/snippets/index.js @@ -1,6 +1,6 @@ import Vue from 'vue'; -import Translate from '~/vue_shared/translate'; import VueApollo from 'vue-apollo'; +import Translate from '~/vue_shared/translate'; import createDefaultClient from '~/lib/graphql'; import SnippetsShow from './components/show.vue'; @@ -38,5 +38,3 @@ export const SnippetShowInit = () => { export const SnippetEditInit = () => { appFactory(document.getElementById('js-snippet-edit'), SnippetsEdit); }; - -export default () => {}; diff --git a/app/assets/javascripts/snippets/mixins/snippets.js b/app/assets/javascripts/snippets/mixins/snippets.js index 91331cdf339..3f5d64a768f 100644 --- a/app/assets/javascripts/snippets/mixins/snippets.js +++ b/app/assets/javascripts/snippets/mixins/snippets.js @@ -2,6 +2,7 @@ import GetSnippetQuery from '../queries/snippet.query.graphql'; const blobsDefault = []; +// eslint-disable-next-line import/prefer-default-export export const getSnippetMixin = { apollo: { snippet: { @@ -39,5 +40,3 @@ export const getSnippetMixin = { }, }, }; - -export default () => {}; diff --git a/app/assets/javascripts/snippets/queries/snippet.blob.content.query.graphql b/app/assets/javascripts/snippets/queries/snippet.blob.content.query.graphql index 889a88dd93c..8f1f16b76c2 100644 --- a/app/assets/javascripts/snippets/queries/snippet.blob.content.query.graphql +++ b/app/assets/javascripts/snippets/queries/snippet.blob.content.query.graphql @@ -3,7 +3,8 @@ query SnippetBlobContent($ids: [ID!], $rich: Boolean!) { edges { node { id - blob { + blobs { + path richData @include(if: $rich) plainData @skip(if: $rich) } diff --git a/app/assets/javascripts/snippets/utils/blob.js b/app/assets/javascripts/snippets/utils/blob.js new file mode 100644 index 00000000000..fd5ff9a3d2e --- /dev/null +++ b/app/assets/javascripts/snippets/utils/blob.js @@ -0,0 +1,66 @@ +import { uniqueId } from 'lodash'; +import { + SNIPPET_BLOB_ACTION_CREATE, + SNIPPET_BLOB_ACTION_UPDATE, + SNIPPET_BLOB_ACTION_MOVE, + SNIPPET_BLOB_ACTION_DELETE, +} from '../constants'; + +const createLocalId = () => uniqueId('blob_local_'); + +export const decorateBlob = blob => ({ + ...blob, + id: createLocalId(), + isLoaded: false, + content: '', +}); + +export const createBlob = () => ({ + id: createLocalId(), + content: '', + path: '', + isLoaded: true, +}); + +const diff = ({ content, path }, origBlob) => { + if (!origBlob) { + return { + action: SNIPPET_BLOB_ACTION_CREATE, + previousPath: path, + content, + filePath: path, + }; + } else if (origBlob.path !== path || origBlob.content !== content) { + return { + action: origBlob.path === path ? SNIPPET_BLOB_ACTION_UPDATE : SNIPPET_BLOB_ACTION_MOVE, + previousPath: origBlob.path, + content, + filePath: path, + }; + } + + return null; +}; + +/** + * This function returns an array of diff actions (to be sent to the BE) based on the current vs. original blobs + * + * @param {Object} blobs + * @param {Object} origBlobs + */ +export const diffAll = (blobs, origBlobs) => { + const deletedEntries = Object.values(origBlobs) + .filter(x => !blobs[x.id]) + .map(({ path, content }) => ({ + action: SNIPPET_BLOB_ACTION_DELETE, + previousPath: path, + filePath: path, + content, + })); + + const newEntries = Object.values(blobs) + .map(blob => diff(blob, origBlobs[blob.id])) + .filter(x => x); + + return [...deletedEntries, ...newEntries]; +}; |