Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-08-18 21:10:10 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-08-18 21:10:10 +0300
commit85f7fa54f404f28b0f351c2be0f7a6e9d74fe65f (patch)
treeb0f4a7578f374185fb649be904641cd79baa2ca0 /app/assets/javascripts/snippets
parenta8a9c520128bffc1157db4dc1beaa215fc731c80 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts/snippets')
-rw-r--r--app/assets/javascripts/snippets/components/edit.vue67
-rw-r--r--app/assets/javascripts/snippets/components/snippet_blob_actions_edit.vue145
-rw-r--r--app/assets/javascripts/snippets/components/snippet_blob_edit.vue83
-rw-r--r--app/assets/javascripts/snippets/constants.js2
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;