diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-08-18 21:10:10 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-08-18 21:10:10 +0300 |
commit | 85f7fa54f404f28b0f351c2be0f7a6e9d74fe65f (patch) | |
tree | b0f4a7578f374185fb649be904641cd79baa2ca0 /app/assets/javascripts/snippets | |
parent | a8a9c520128bffc1157db4dc1beaa215fc731c80 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts/snippets')
4 files changed, 186 insertions, 111 deletions
diff --git a/app/assets/javascripts/snippets/components/edit.vue b/app/assets/javascripts/snippets/components/edit.vue index 81be39bda04..e1b57f35134 100644 --- a/app/assets/javascripts/snippets/components/edit.vue +++ b/app/assets/javascripts/snippets/components/edit.vue @@ -14,9 +14,6 @@ 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 SnippetBlobActionsEdit from './snippet_blob_actions_edit.vue'; import SnippetVisibilityEdit from './snippet_visibility_edit.vue'; @@ -56,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); @@ -85,7 +77,7 @@ export default { title: this.snippet.title, description: this.snippet.description, visibilityLevel: this.snippet.visibilityLevel, - blobActions: this.getActionsEntries.filter(entry => entry.action !== ''), + blobActions: this.actions, }; }, saveButtonLabel() { @@ -120,48 +112,11 @@ export default { onBeforeUnload(e = {}) { const returnValue = __('Are you sure you want to lose unsaved changes?'); - if (!this.allBlobChangesRegistered || this.isUpdating) 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 @@ -218,7 +173,6 @@ export default { if (errors.length) { this.flashAPIFailure(errors[0]); } else { - this.originalContent = this.content; redirectTo(baseObj.snippet.webUrl); } }) @@ -226,6 +180,9 @@ export default { this.flashAPIFailure(e); }); }, + updateActions(actions) { + this.actions = actions; + }, }, newSnippetSchema: { title: '', @@ -261,7 +218,7 @@ export default { :markdown-preview-path="markdownPreviewPath" :markdown-docs-path="markdownDocsPath" /> - <snippet-blob-actions-edit :blobs="blobs" @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/snippet_blob_actions_edit.vue b/app/assets/javascripts/snippets/components/snippet_blob_actions_edit.vue index fd81a5fa69c..55cd13a6930 100644 --- a/app/assets/javascripts/snippets/components/snippet_blob_actions_edit.vue +++ b/app/assets/javascripts/snippets/components/snippet_blob_actions_edit.vue @@ -1,25 +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: { - blobs: { + 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="snippet_file_path">{{ s__('Snippets|File') }}</label> - <template v-if="blobs.length"> - <snippet-blob-edit v-for="blob in blobs" :key="blob.name" :blob="blob" v-on="$listeners" /> - </template> - <snippet-blob-edit v-else v-on="$listeners" /> + <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 b349069d73a..5066b7f3ed6 100644 --- a/app/assets/javascripts/snippets/components/snippet_blob_edit.vue +++ b/app/assets/javascripts/snippets/components/snippet_blob_edit.vue @@ -8,12 +8,6 @@ import { SNIPPET_BLOB_CONTENT_FETCH_ERROR } from '~/snippets/constants'; import 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,17 +55,12 @@ 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; }, }, }; @@ -93,16 +68,26 @@ export default { <template> <div class="file-holder snippet"> <blob-header-edit - id="snippet_file_path" - v-model="filePath" + :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="isContentLoading" + v-if="!blob.isLoaded" :label="__('Loading snippet')" size="lg" class="loading-animation prepend-top-20 append-bottom-20" /> - <blob-content-edit v-else v-model="content" :file-global-id="id" :file-name="filePath" /> + <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/constants.js b/app/assets/javascripts/snippets/constants.js index a59d7aa84eb..12b83525bf7 100644 --- a/app/assets/javascripts/snippets/constants.js +++ b/app/assets/javascripts/snippets/constants.js @@ -31,3 +31,5 @@ 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; |