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-20 21:42:06 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-08-20 21:42:06 +0300
commit6e4e1050d9dba2b7b2523fdd1768823ab85feef4 (patch)
tree78be5963ec075d80116a932011d695dd33910b4e /app/assets/javascripts/snippets
parent1ce776de4ae122aba3f349c02c17cebeaa8ecf07 (diff)
Add latest changes from gitlab-org/gitlab@13-3-stable-ee
Diffstat (limited to 'app/assets/javascripts/snippets')
-rw-r--r--app/assets/javascripts/snippets/components/edit.vue87
-rw-r--r--app/assets/javascripts/snippets/components/show.vue25
-rw-r--r--app/assets/javascripts/snippets/components/snippet_blob_actions_edit.vue156
-rw-r--r--app/assets/javascripts/snippets/components/snippet_blob_edit.vue104
-rw-r--r--app/assets/javascripts/snippets/components/snippet_blob_view.vue28
-rw-r--r--app/assets/javascripts/snippets/components/snippet_header.vue35
-rw-r--r--app/assets/javascripts/snippets/constants.js3
-rw-r--r--app/assets/javascripts/snippets/index.js4
-rw-r--r--app/assets/javascripts/snippets/mixins/snippets.js3
-rw-r--r--app/assets/javascripts/snippets/queries/snippet.blob.content.query.graphql3
-rw-r--r--app/assets/javascripts/snippets/utils/blob.js66
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];
+};