summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/snippets
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/snippets')
-rw-r--r--app/assets/javascripts/snippets/components/edit.vue112
-rw-r--r--app/assets/javascripts/snippets/components/show.vue13
-rw-r--r--app/assets/javascripts/snippets/components/snippet_blob_edit.vue98
-rw-r--r--app/assets/javascripts/snippets/components/snippet_blob_view.vue70
-rw-r--r--app/assets/javascripts/snippets/components/snippet_header.vue9
-rw-r--r--app/assets/javascripts/snippets/constants.js5
-rw-r--r--app/assets/javascripts/snippets/fragments/project.fragment.graphql4
-rw-r--r--app/assets/javascripts/snippets/fragments/snippetBase.fragment.graphql2
-rw-r--r--app/assets/javascripts/snippets/mixins/snippets.js4
-rw-r--r--app/assets/javascripts/snippets/mutations/deleteSnippet.mutation.graphql4
-rw-r--r--app/assets/javascripts/snippets/queries/projectPermissions.query.graphql2
-rw-r--r--app/assets/javascripts/snippets/queries/snippet.query.graphql2
-rw-r--r--app/assets/javascripts/snippets/queries/userPermissions.query.graphql2
13 files changed, 220 insertions, 107 deletions
diff --git a/app/assets/javascripts/snippets/components/edit.vue b/app/assets/javascripts/snippets/components/edit.vue
index a6651515e47..c01f9524ca8 100644
--- a/app/assets/javascripts/snippets/components/edit.vue
+++ b/app/assets/javascripts/snippets/components/edit.vue
@@ -3,9 +3,8 @@ import { GlButton, GlLoadingIcon } from '@gitlab/ui';
import Flash from '~/flash';
import { __, sprintf } from '~/locale';
-import axios from '~/lib/utils/axios_utils';
import TitleField from '~/vue_shared/components/form/title.vue';
-import { getBaseURL, joinPaths, redirectTo } from '~/lib/utils/url_utility';
+import { redirectTo } from '~/lib/utils/url_utility';
import FormFooterActions from '~/vue_shared/components/form/form_footer_actions.vue';
import UpdateSnippetMutation from '../mutations/updateSnippet.mutation.graphql';
@@ -15,6 +14,9 @@ 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 SnippetVisibilityEdit from './snippet_visibility_edit.vue';
@@ -53,17 +55,25 @@ export default {
},
data() {
return {
- blob: {},
- fileName: '',
- content: '',
- isContentLoading: true,
+ blobsActions: {},
isUpdating: false,
newSnippet: false,
};
},
computed: {
+ getActionsEntries() {
+ return Object.values(this.blobsActions);
+ },
+ 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 === '');
+ },
updatePrevented() {
- return this.snippet.title === '' || this.content === '' || this.isUpdating;
+ return this.snippet.title === '' || !this.allBlobsHaveContent || this.isUpdating;
},
isProjectSnippet() {
return Boolean(this.projectPath);
@@ -74,8 +84,7 @@ export default {
title: this.snippet.title,
description: this.snippet.description,
visibilityLevel: this.snippet.visibilityLevel,
- fileName: this.fileName,
- content: this.content,
+ files: this.getActionsEntries.filter(entry => entry.action !== ''),
};
},
saveButtonLabel() {
@@ -97,9 +106,57 @@ export default {
return `${this.isProjectSnippet ? 'project' : 'personal'}_snippet_description`;
},
},
+ created() {
+ window.addEventListener('beforeunload', this.onBeforeUnload);
+ },
+ destroyed() {
+ window.removeEventListener('beforeunload', this.onBeforeUnload);
+ },
methods: {
- updateFileName(newName) {
- this.fileName = newName;
+ onBeforeUnload(e = {}) {
+ const returnValue = __('Are you sure you want to lose unsaved changes?');
+
+ if (!this.allBlobChangesRegistered) 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
@@ -111,24 +168,9 @@ export default {
onNewSnippetFetched() {
this.newSnippet = true;
this.snippet = this.$options.newSnippetSchema;
- this.blob = this.snippet.blob;
- this.isContentLoading = false;
},
onExistingSnippetFetched() {
this.newSnippet = false;
- const { blob } = this.snippet;
- this.blob = blob;
- this.fileName = blob.name;
- const baseUrl = getBaseURL();
- const url = joinPaths(baseUrl, blob.rawPath);
-
- axios
- .get(url)
- .then(res => {
- this.content = res.data;
- this.isContentLoading = false;
- })
- .catch(e => this.flashAPIFailure(e));
},
onSnippetFetch(snippetRes) {
if (snippetRes.data.snippets.edges.length === 0) {
@@ -172,6 +214,7 @@ export default {
if (errors.length) {
this.flashAPIFailure(errors[0]);
} else {
+ this.originalContent = this.content;
redirectTo(baseObj.snippet.webUrl);
}
})
@@ -184,7 +227,6 @@ export default {
title: '',
description: '',
visibilityLevel: SNIPPET_VISIBILITY_PRIVATE,
- blob: {},
},
};
</script>
@@ -215,12 +257,16 @@ export default {
:markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath"
/>
- <snippet-blob-edit
- v-model="content"
- :file-name="fileName"
- :is-loading="isContentLoading"
- @name-change="updateFileName"
- />
+ <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-visibility-edit
v-model="snippet.visibilityLevel"
:help-link="visibilityHelpLink"
diff --git a/app/assets/javascripts/snippets/components/show.vue b/app/assets/javascripts/snippets/components/show.vue
index bc0034d397e..0779e87e6b6 100644
--- a/app/assets/javascripts/snippets/components/show.vue
+++ b/app/assets/javascripts/snippets/components/show.vue
@@ -1,19 +1,27 @@
<script>
+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 { getSnippetMixin } from '../mixins/snippets';
+import { SNIPPET_VISIBILITY_PUBLIC } from '~/snippets/constants';
export default {
components: {
+ BlobEmbeddable,
SnippetHeader,
SnippetTitle,
GlLoadingIcon,
SnippetBlob,
},
mixins: [getSnippetMixin],
+ computed: {
+ embeddable() {
+ return this.snippet.visibilityLevel === SNIPPET_VISIBILITY_PUBLIC;
+ },
+ },
};
</script>
<template>
@@ -27,7 +35,10 @@ export default {
<template v-else>
<snippet-header :snippet="snippet" />
<snippet-title :snippet="snippet" />
- <snippet-blob :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>
</template>
</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 62c29b0c7cd..3c2dbfff6e1 100644
--- a/app/assets/javascripts/snippets/components/snippet_blob_edit.vue
+++ b/app/assets/javascripts/snippets/components/snippet_blob_edit.vue
@@ -2,6 +2,17 @@
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 { sprintf } from '~/locale';
+
+function localId() {
+ return Math.floor((1 + Math.random()) * 0x10000)
+ .toString(16)
+ .substring(1);
+}
export default {
components: {
@@ -11,20 +22,70 @@ export default {
},
inheritAttrs: false,
props: {
- value: {
- type: String,
+ blob: {
+ type: Object,
required: false,
- default: '',
+ default: null,
+ validator: ({ rawPath }) => Boolean(rawPath),
},
- fileName: {
- type: String,
- required: false,
- default: '',
+ },
+ 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 });
},
- isLoading: {
- type: Boolean,
- required: false,
- default: true,
+ content() {
+ this.notifyAboutUpdates();
+ },
+ },
+ mounted() {
+ if (this.blob) {
+ this.fetchBlobContent();
+ }
+ },
+ methods: {
+ 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,
+ },
+ });
+ },
+ fetchBlobContent() {
+ const baseUrl = getBaseURL();
+ const url = joinPaths(baseUrl, this.blob.rawPath);
+
+ axios
+ .get(url)
+ .then(res => {
+ this.originalContent = res.data;
+ this.content = res.data;
+ })
+ .catch(e => this.flashAPIFailure(e))
+ .finally(() => {
+ this.isContentLoading = false;
+ });
+ },
+ flashAPIFailure(err) {
+ Flash(sprintf(SNIPPET_BLOB_CONTENT_FETCH_ERROR, { err }));
+ this.isContentLoading = false;
},
},
};
@@ -33,23 +94,14 @@ export default {
<div class="form-group file-editor">
<label>{{ s__('Snippets|File') }}</label>
<div class="file-holder snippet">
- <blob-header-edit
- :value="fileName"
- data-qa-selector="file_name_field"
- @input="$emit('name-change', $event)"
- />
+ <blob-header-edit v-model="filePath" data-qa-selector="file_name_field" />
<gl-loading-icon
- v-if="isLoading"
+ v-if="isContentLoading"
:label="__('Loading snippet')"
size="lg"
class="loading-animation prepend-top-20 append-bottom-20"
/>
- <blob-content-edit
- v-else
- :value="value"
- :file-name="fileName"
- @input="$emit('input', $event)"
- />
+ <blob-content-edit v-else v-model="content" :file-name="filePath" />
</div>
</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 7472aff3318..afd038eef58 100644
--- a/app/assets/javascripts/snippets/components/snippet_blob_view.vue
+++ b/app/assets/javascripts/snippets/components/snippet_blob_view.vue
@@ -1,6 +1,4 @@
<script>
-import BlobEmbeddable from '~/blob/components/blob_embeddable.vue';
-import { SNIPPET_VISIBILITY_PUBLIC } from '../constants';
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';
@@ -16,7 +14,6 @@ import {
export default {
components: {
- BlobEmbeddable,
BlobHeader,
BlobContent,
CloneDropdownButton,
@@ -49,21 +46,19 @@ export default {
type: Object,
required: true,
},
+ blob: {
+ type: Object,
+ required: true,
+ },
},
data() {
return {
- blob: this.snippet.blob,
blobContent: '',
activeViewerType:
- this.snippet.blob?.richViewer && !window.location.hash
- ? RICH_BLOB_VIEWER
- : SIMPLE_BLOB_VIEWER,
+ this.blob?.richViewer && !window.location.hash ? RICH_BLOB_VIEWER : SIMPLE_BLOB_VIEWER,
};
},
computed: {
- embeddable() {
- return this.snippet.visibilityLevel === SNIPPET_VISIBILITY_PUBLIC;
- },
isContentLoading() {
return this.$apollo.queries.blobContent.loading;
},
@@ -92,33 +87,30 @@ export default {
};
</script>
<template>
- <div>
- <blob-embeddable v-if="embeddable" class="mb-3" :url="snippet.webUrl" />
- <article class="file-holder snippet-file-content">
- <blob-header
- :blob="blob"
- :active-viewer-type="viewer.type"
- :has-render-error="hasRenderError"
- @viewer-changed="switchViewer"
- >
- <template #actions>
- <clone-dropdown-button
- v-if="canBeCloned"
- class="mr-2"
- :ssh-link="snippet.sshUrlToRepo"
- :http-link="snippet.httpUrlToRepo"
- data-qa-selector="clone_button"
- />
- </template>
- </blob-header>
- <blob-content
- :loading="isContentLoading"
- :content="blobContent"
- :active-viewer="viewer"
- :blob="blob"
- @[$options.BLOB_RENDER_EVENT_LOAD]="forceQuery"
- @[$options.BLOB_RENDER_EVENT_SHOW_SOURCE]="switchViewer"
- />
- </article>
- </div>
+ <article class="file-holder snippet-file-content">
+ <blob-header
+ :blob="blob"
+ :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"
+ :active-viewer="viewer"
+ :blob="blob"
+ @[$options.BLOB_RENDER_EVENT_LOAD]="forceQuery"
+ @[$options.BLOB_RENDER_EVENT_SHOW_SOURCE]="switchViewer"
+ />
+ </article>
</template>
diff --git a/app/assets/javascripts/snippets/components/snippet_header.vue b/app/assets/javascripts/snippets/components/snippet_header.vue
index 2a06296cb15..707e2b0ea30 100644
--- a/app/assets/javascripts/snippets/components/snippet_header.vue
+++ b/app/assets/javascripts/snippets/components/snippet_header.vue
@@ -65,14 +65,17 @@ export default {
};
},
computed: {
+ snippetHasBinary() {
+ return Boolean(this.snippet.blobs.find(blob => blob.binary));
+ },
personalSnippetActions() {
return [
{
condition: this.snippet.userPermissions.updateSnippet,
text: __('Edit'),
href: this.editLink,
- disabled: this.snippet.blob.binary,
- title: this.snippet.blob.binary
+ disabled: this.snippetHasBinary,
+ title: this.snippetHasBinary
? __('Snippets with non-text files can only be edited via Git.')
: undefined,
},
@@ -163,7 +166,7 @@ export default {
<div class="detail-page-header">
<div class="detail-page-header-body">
<div
- class="snippet-box has-tooltip d-flex align-items-center append-right-5 mb-1"
+ class="snippet-box has-tooltip d-flex align-items-center gl-mr-2 mb-1"
data-qa-selector="snippet_container"
:title="snippetVisibilityLevelDescription"
data-container="body"
diff --git a/app/assets/javascripts/snippets/constants.js b/app/assets/javascripts/snippets/constants.js
index b3abc73557c..99ee698408d 100644
--- a/app/assets/javascripts/snippets/constants.js
+++ b/app/assets/javascripts/snippets/constants.js
@@ -25,3 +25,8 @@ export const SNIPPET_VISIBILITY = {
export const SNIPPET_CREATE_MUTATION_ERROR = __("Can't create snippet: %{err}");
export const SNIPPET_UPDATE_MUTATION_ERROR = __("Can't update snippet: %{err}");
+export const SNIPPET_BLOB_CONTENT_FETCH_ERROR = __("Can't fetch content for the blob: %{err}");
+
+export const SNIPPET_BLOB_ACTION_CREATE = 'create';
+export const SNIPPET_BLOB_ACTION_UPDATE = 'update';
+export const SNIPPET_BLOB_ACTION_MOVE = 'move';
diff --git a/app/assets/javascripts/snippets/fragments/project.fragment.graphql b/app/assets/javascripts/snippets/fragments/project.fragment.graphql
index 7d65789c67b..64bb2315c1b 100644
--- a/app/assets/javascripts/snippets/fragments/project.fragment.graphql
+++ b/app/assets/javascripts/snippets/fragments/project.fragment.graphql
@@ -1,6 +1,6 @@
-fragment Project on Snippet {
+fragment SnippetProject on Snippet {
project {
fullPath
webUrl
}
-} \ No newline at end of file
+}
diff --git a/app/assets/javascripts/snippets/fragments/snippetBase.fragment.graphql b/app/assets/javascripts/snippets/fragments/snippetBase.fragment.graphql
index e7765dfd8ba..2cca71708ca 100644
--- a/app/assets/javascripts/snippets/fragments/snippetBase.fragment.graphql
+++ b/app/assets/javascripts/snippets/fragments/snippetBase.fragment.graphql
@@ -11,7 +11,7 @@ fragment SnippetBase on Snippet {
webUrl
httpUrlToRepo
sshUrlToRepo
- blob {
+ blobs {
binary
name
path
diff --git a/app/assets/javascripts/snippets/mixins/snippets.js b/app/assets/javascripts/snippets/mixins/snippets.js
index 837c41cdf6b..91331cdf339 100644
--- a/app/assets/javascripts/snippets/mixins/snippets.js
+++ b/app/assets/javascripts/snippets/mixins/snippets.js
@@ -1,5 +1,7 @@
import GetSnippetQuery from '../queries/snippet.query.graphql';
+const blobsDefault = [];
+
export const getSnippetMixin = {
apollo: {
snippet: {
@@ -11,6 +13,7 @@ export const getSnippetMixin = {
},
update: data => data.snippets.edges[0]?.node,
result(res) {
+ this.blobs = res.data.snippets.edges[0]?.node?.blobs || blobsDefault;
if (this.onSnippetFetch) {
this.onSnippetFetch(res);
}
@@ -27,6 +30,7 @@ export const getSnippetMixin = {
return {
snippet: {},
newSnippet: false,
+ blobs: blobsDefault,
};
},
computed: {
diff --git a/app/assets/javascripts/snippets/mutations/deleteSnippet.mutation.graphql b/app/assets/javascripts/snippets/mutations/deleteSnippet.mutation.graphql
index 0c829cbdee6..f43d53661f4 100644
--- a/app/assets/javascripts/snippets/mutations/deleteSnippet.mutation.graphql
+++ b/app/assets/javascripts/snippets/mutations/deleteSnippet.mutation.graphql
@@ -1,5 +1,5 @@
mutation DeleteSnippet($id: ID!) {
- destroySnippet(input: {id: $id}) {
+ destroySnippet(input: { id: $id }) {
errors
}
-} \ No newline at end of file
+}
diff --git a/app/assets/javascripts/snippets/queries/projectPermissions.query.graphql b/app/assets/javascripts/snippets/queries/projectPermissions.query.graphql
index 288bd0889bf..03c81460fb5 100644
--- a/app/assets/javascripts/snippets/queries/projectPermissions.query.graphql
+++ b/app/assets/javascripts/snippets/queries/projectPermissions.query.graphql
@@ -4,4 +4,4 @@ query CanCreateProjectSnippet($fullPath: ID!) {
createSnippet
}
}
-} \ No newline at end of file
+}
diff --git a/app/assets/javascripts/snippets/queries/snippet.query.graphql b/app/assets/javascripts/snippets/queries/snippet.query.graphql
index c58a5168ba3..b23ab862439 100644
--- a/app/assets/javascripts/snippets/queries/snippet.query.graphql
+++ b/app/assets/javascripts/snippets/queries/snippet.query.graphql
@@ -7,7 +7,7 @@ query GetSnippetQuery($ids: [ID!]) {
edges {
node {
...SnippetBase
- ...Project
+ ...SnippetProject
author {
...Author
}
diff --git a/app/assets/javascripts/snippets/queries/userPermissions.query.graphql b/app/assets/javascripts/snippets/queries/userPermissions.query.graphql
index f5b97b3d0f0..c3e5519e266 100644
--- a/app/assets/javascripts/snippets/queries/userPermissions.query.graphql
+++ b/app/assets/javascripts/snippets/queries/userPermissions.query.graphql
@@ -4,4 +4,4 @@ query CanCreatePersonalSnippet {
createSnippet
}
}
-} \ No newline at end of file
+}