summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2019-11-07 18:06:21 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2019-11-07 18:06:21 +0000
commitd8ccc7a00b7a1ea954263170a2044257424a2cfe (patch)
tree0a29cb558aae61795da47c82ce7e87983c5dc4af /app
parent90a06a20be61bb6d48d77746091492831153e075 (diff)
downloadgitlab-ce-d8ccc7a00b7a1ea954263170a2044257424a2cfe.tar.gz
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/repository/components/preview/index.vue49
-rw-r--r--app/assets/javascripts/repository/components/table/index.vue84
-rw-r--r--app/assets/javascripts/repository/components/tree_content.vue115
-rw-r--r--app/assets/javascripts/repository/graphql.js6
-rw-r--r--app/assets/javascripts/repository/pages/index.vue11
-rw-r--r--app/assets/javascripts/repository/pages/tree.vue6
-rw-r--r--app/assets/javascripts/repository/queries/getReadme.query.graphql5
-rw-r--r--app/assets/javascripts/repository/utils/readme.js17
-rw-r--r--app/assets/stylesheets/framework/snippets.scss7
-rw-r--r--app/controllers/application_controller.rb22
-rw-r--r--app/controllers/concerns/confirm_email_warning.rb7
-rw-r--r--app/controllers/concerns/uploads_actions.rb17
-rw-r--r--app/controllers/uploads_controller.rb2
-rw-r--r--app/models/concerns/issuable.rb20
-rw-r--r--app/views/import/manifest/_form.html.haml2
-rw-r--r--app/views/projects/_files.html.haml2
-rw-r--r--app/views/shared/snippets/_snippet.html.haml5
17 files changed, 273 insertions, 104 deletions
diff --git a/app/assets/javascripts/repository/components/preview/index.vue b/app/assets/javascripts/repository/components/preview/index.vue
new file mode 100644
index 00000000000..564be211c46
--- /dev/null
+++ b/app/assets/javascripts/repository/components/preview/index.vue
@@ -0,0 +1,49 @@
+<script>
+import { GlLink, GlLoadingIcon } from '@gitlab/ui';
+import getReadmeQuery from '../../queries/getReadme.query.graphql';
+
+export default {
+ apollo: {
+ readme: {
+ query: getReadmeQuery,
+ variables() {
+ return {
+ url: this.blob.webUrl,
+ };
+ },
+ loadingKey: 'loading',
+ },
+ },
+ components: {
+ GlLink,
+ GlLoadingIcon,
+ },
+ props: {
+ blob: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ readme: null,
+ loading: 0,
+ };
+ },
+};
+</script>
+
+<template>
+ <article class="file-holder js-hide-on-navigation limited-width-container readme-holder">
+ <div class="file-title">
+ <i aria-hidden="true" class="fa fa-file-text-o fa-fw"></i>
+ <gl-link :href="blob.webUrl">
+ <strong>{{ blob.name }}</strong>
+ </gl-link>
+ </div>
+ <div class="blob-viewer">
+ <gl-loading-icon v-if="loading > 0" size="md" class="my-4 mx-auto" />
+ <div v-else-if="readme" v-html="readme.html"></div>
+ </div>
+ </article>
+</template>
diff --git a/app/assets/javascripts/repository/components/table/index.vue b/app/assets/javascripts/repository/components/table/index.vue
index 98923c79c7a..ac20549acb8 100644
--- a/app/assets/javascripts/repository/components/table/index.vue
+++ b/app/assets/javascripts/repository/components/table/index.vue
@@ -1,16 +1,12 @@
<script>
import { GlSkeletonLoading } from '@gitlab/ui';
-import createFlash from '~/flash';
import { sprintf, __ } from '../../../locale';
import getRefMixin from '../../mixins/get_ref';
-import getFiles from '../../queries/getFiles.query.graphql';
import getProjectPath from '../../queries/getProjectPath.query.graphql';
import TableHeader from './header.vue';
import TableRow from './row.vue';
import ParentRow from './parent_row.vue';
-const PAGE_SIZE = 100;
-
export default {
components: {
GlSkeletonLoading,
@@ -29,22 +25,24 @@ export default {
type: String,
required: true,
},
+ entries: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ isLoading: {
+ type: Boolean,
+ required: true,
+ },
},
data() {
return {
projectPath: '',
- nextPageCursor: '',
- entries: {
- trees: [],
- submodules: [],
- blobs: [],
- },
- isLoadingFiles: false,
};
},
computed: {
tableCaption() {
- if (this.isLoadingFiles) {
+ if (this.isLoading) {
return sprintf(
__(
'Loading files, directories, and submodules in the path %{path} for commit reference %{ref}',
@@ -59,65 +57,7 @@ export default {
);
},
showParentRow() {
- return !this.isLoadingFiles && ['', '/'].indexOf(this.path) === -1;
- },
- },
- watch: {
- $route: function routeChange() {
- this.entries.trees = [];
- this.entries.submodules = [];
- this.entries.blobs = [];
- this.nextPageCursor = '';
- this.fetchFiles();
- },
- },
- mounted() {
- // We need to wait for `ref` and `projectPath` to be set
- this.$nextTick(() => this.fetchFiles());
- },
- methods: {
- fetchFiles() {
- this.isLoadingFiles = true;
-
- return this.$apollo
- .query({
- query: getFiles,
- variables: {
- projectPath: this.projectPath,
- ref: this.ref,
- path: this.path || '/',
- nextPageCursor: this.nextPageCursor,
- pageSize: PAGE_SIZE,
- },
- })
- .then(({ data }) => {
- if (!data) return;
-
- const pageInfo = this.hasNextPage(data.project.repository.tree);
-
- this.isLoadingFiles = false;
- this.entries = Object.keys(this.entries).reduce(
- (acc, key) => ({
- ...acc,
- [key]: this.normalizeData(key, data.project.repository.tree[key].edges),
- }),
- {},
- );
-
- if (pageInfo && pageInfo.hasNextPage) {
- this.nextPageCursor = pageInfo.endCursor;
- this.fetchFiles();
- }
- })
- .catch(() => createFlash(__('An error occurred while fetching folder content.')));
- },
- normalizeData(key, data) {
- return this.entries[key].concat(data.map(({ node }) => node));
- },
- hasNextPage(data) {
- return []
- .concat(data.trees.pageInfo, data.submodules.pageInfo, data.blobs.pageInfo)
- .find(({ hasNextPage }) => hasNextPage);
+ return !this.isLoading && ['', '/'].indexOf(this.path) === -1;
},
},
};
@@ -145,7 +85,7 @@ export default {
:lfs-oid="entry.lfsOid"
/>
</template>
- <template v-if="isLoadingFiles">
+ <template v-if="isLoading">
<tr v-for="i in 5" :key="i" aria-hidden="true">
<td><gl-skeleton-loading :lines="1" class="h-auto" /></td>
<td><gl-skeleton-loading :lines="1" class="h-auto" /></td>
diff --git a/app/assets/javascripts/repository/components/tree_content.vue b/app/assets/javascripts/repository/components/tree_content.vue
new file mode 100644
index 00000000000..949e653fc8f
--- /dev/null
+++ b/app/assets/javascripts/repository/components/tree_content.vue
@@ -0,0 +1,115 @@
+<script>
+import createFlash from '~/flash';
+import { __ } from '../../locale';
+import FileTable from './table/index.vue';
+import getRefMixin from '../mixins/get_ref';
+import getFiles from '../queries/getFiles.query.graphql';
+import getProjectPath from '../queries/getProjectPath.query.graphql';
+import FilePreview from './preview/index.vue';
+import { readmeFile } from '../utils/readme';
+
+const PAGE_SIZE = 100;
+
+export default {
+ components: {
+ FileTable,
+ FilePreview,
+ },
+ mixins: [getRefMixin],
+ apollo: {
+ projectPath: {
+ query: getProjectPath,
+ },
+ },
+ props: {
+ path: {
+ type: String,
+ required: false,
+ default: '/',
+ },
+ },
+ data() {
+ return {
+ projectPath: '',
+ nextPageCursor: '',
+ entries: {
+ trees: [],
+ submodules: [],
+ blobs: [],
+ },
+ isLoadingFiles: false,
+ };
+ },
+ computed: {
+ readme() {
+ return readmeFile(this.entries.blobs);
+ },
+ },
+
+ watch: {
+ $route: function routeChange() {
+ this.entries.trees = [];
+ this.entries.submodules = [];
+ this.entries.blobs = [];
+ this.nextPageCursor = '';
+ this.fetchFiles();
+ },
+ },
+ mounted() {
+ // We need to wait for `ref` and `projectPath` to be set
+ this.$nextTick(() => this.fetchFiles());
+ },
+ methods: {
+ fetchFiles() {
+ this.isLoadingFiles = true;
+
+ return this.$apollo
+ .query({
+ query: getFiles,
+ variables: {
+ projectPath: this.projectPath,
+ ref: this.ref,
+ path: this.path || '/',
+ nextPageCursor: this.nextPageCursor,
+ pageSize: PAGE_SIZE,
+ },
+ })
+ .then(({ data }) => {
+ if (!data) return;
+
+ const pageInfo = this.hasNextPage(data.project.repository.tree);
+
+ this.isLoadingFiles = false;
+ this.entries = Object.keys(this.entries).reduce(
+ (acc, key) => ({
+ ...acc,
+ [key]: this.normalizeData(key, data.project.repository.tree[key].edges),
+ }),
+ {},
+ );
+
+ if (pageInfo && pageInfo.hasNextPage) {
+ this.nextPageCursor = pageInfo.endCursor;
+ this.fetchFiles();
+ }
+ })
+ .catch(() => createFlash(__('An error occurred while fetching folder content.')));
+ },
+ normalizeData(key, data) {
+ return this.entries[key].concat(data.map(({ node }) => node));
+ },
+ hasNextPage(data) {
+ return []
+ .concat(data.trees.pageInfo, data.submodules.pageInfo, data.blobs.pageInfo)
+ .find(({ hasNextPage }) => hasNextPage);
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <file-table :path="path" :entries="entries" :is-loading="isLoadingFiles" />
+ <file-preview v-if="readme" :blob="readme" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/repository/graphql.js b/app/assets/javascripts/repository/graphql.js
index 6cb253c8169..6936c08d852 100644
--- a/app/assets/javascripts/repository/graphql.js
+++ b/app/assets/javascripts/repository/graphql.js
@@ -1,6 +1,7 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { IntrospectionFragmentMatcher } from 'apollo-cache-inmemory';
+import axios from '~/lib/utils/axios_utils';
import createDefaultClient from '~/lib/graphql';
import introspectionQueryResultData from './fragmentTypes.json';
import { fetchLogsTree } from './log_tree';
@@ -27,6 +28,11 @@ const defaultClient = createDefaultClient(
});
});
},
+ readme(_, { url }) {
+ return axios
+ .get(url, { params: { viewer: 'rich', format: 'json' } })
+ .then(({ data }) => ({ ...data, __typename: 'ReadmeFile' }));
+ },
},
},
{
diff --git a/app/assets/javascripts/repository/pages/index.vue b/app/assets/javascripts/repository/pages/index.vue
index 2d92e9174ca..967f4a99281 100644
--- a/app/assets/javascripts/repository/pages/index.vue
+++ b/app/assets/javascripts/repository/pages/index.vue
@@ -1,18 +1,13 @@
<script>
-import FileTable from '../components/table/index.vue';
+import TreeContent from '../components/tree_content.vue';
export default {
components: {
- FileTable,
- },
- data() {
- return {
- ref: '',
- };
+ TreeContent,
},
};
</script>
<template>
- <file-table path="/" />
+ <tree-content />
</template>
diff --git a/app/assets/javascripts/repository/pages/tree.vue b/app/assets/javascripts/repository/pages/tree.vue
index 3b898d1aa91..19300099449 100644
--- a/app/assets/javascripts/repository/pages/tree.vue
+++ b/app/assets/javascripts/repository/pages/tree.vue
@@ -1,9 +1,9 @@
<script>
-import FileTable from '../components/table/index.vue';
+import TreeContent from '../components/tree_content.vue';
export default {
components: {
- FileTable,
+ TreeContent,
},
props: {
path: {
@@ -16,5 +16,5 @@ export default {
</script>
<template>
- <file-table :path="path" />
+ <tree-content :path="path" />
</template>
diff --git a/app/assets/javascripts/repository/queries/getReadme.query.graphql b/app/assets/javascripts/repository/queries/getReadme.query.graphql
new file mode 100644
index 00000000000..cf056330133
--- /dev/null
+++ b/app/assets/javascripts/repository/queries/getReadme.query.graphql
@@ -0,0 +1,5 @@
+query getReadme($url: String!) {
+ readme(url: $url) @client {
+ html
+ }
+}
diff --git a/app/assets/javascripts/repository/utils/readme.js b/app/assets/javascripts/repository/utils/readme.js
new file mode 100644
index 00000000000..b219b857c66
--- /dev/null
+++ b/app/assets/javascripts/repository/utils/readme.js
@@ -0,0 +1,17 @@
+const MARKDOWN_EXTENSIONS = ['mdown', 'mkd', 'mkdn', 'md', 'markdown'];
+const ASCIIDOC_EXTENSIONS = ['adoc', 'ad', 'asciidoc'];
+const OTHER_EXTENSIONS = ['textile', 'rdoc', 'org', 'creole', 'wiki', 'mediawiki', 'rst'];
+const EXTENSIONS = [...MARKDOWN_EXTENSIONS, ...ASCIIDOC_EXTENSIONS, ...OTHER_EXTENSIONS];
+const PLAIN_FILENAMES = ['readme', 'index'];
+const FILE_REGEXP = new RegExp(`^(${PLAIN_FILENAMES.join('|')})`, 'i');
+const EXTENSIONS_REGEXP = new RegExp(`.(${EXTENSIONS.join('|')})$`, 'i');
+
+// eslint-disable-next-line import/prefer-default-export
+export const readmeFile = blobs => {
+ const readMeFiles = blobs.filter(f => f.name.search(FILE_REGEXP) !== -1);
+
+ const previewableReadme = readMeFiles.find(f => f.name.search(EXTENSIONS_REGEXP) !== -1);
+ const plainReadme = readMeFiles.find(f => f.name.search(FILE_REGEXP) !== -1);
+
+ return previewableReadme || plainReadme;
+};
diff --git a/app/assets/stylesheets/framework/snippets.scss b/app/assets/stylesheets/framework/snippets.scss
index f57b1d9f351..404f60f17ee 100644
--- a/app/assets/stylesheets/framework/snippets.scss
+++ b/app/assets/stylesheets/framework/snippets.scss
@@ -4,7 +4,12 @@
}
.snippet-filename {
- padding: 0 2px;
+ color: $gl-text-color-secondary;
+ font-weight: normal;
+ }
+
+ .snippet-info {
+ color: $gl-text-color-secondary;
}
}
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index af2869ddba7..1311c745da3 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -17,14 +17,14 @@ class ApplicationController < ActionController::Base
include Gitlab::Tracking::ControllerConcern
include Gitlab::Experimentation::ControllerConcern
- before_action :authenticate_user!, except: [:route_not_found]
+ before_action :authenticate_user!
before_action :enforce_terms!, if: :should_enforce_terms?
before_action :validate_user_service_ticket!
- before_action :check_password_expiration
+ before_action :check_password_expiration, if: :html_request?
before_action :ldap_security_check
before_action :sentry_context
before_action :default_headers
- before_action :add_gon_variables, unless: [:peek_request?, :json_request?]
+ before_action :add_gon_variables, if: :html_request?
before_action :configure_permitted_parameters, if: :devise_controller?
before_action :require_email, unless: :devise_controller?
before_action :active_user_check, unless: :devise_controller?
@@ -95,13 +95,11 @@ class ApplicationController < ActionController::Base
end
def route_not_found
- if current_user
- not_found
- else
- store_location_for(:user, request.fullpath) unless request.xhr?
+ # We need to call #authenticate_user! here because sometimes this is called from another action
+ # and not from our wildcard fallback route
+ authenticate_user!
- redirect_to new_user_session_path, alert: I18n.t('devise.failure.unauthenticated')
- end
+ not_found
end
def render(*args)
@@ -451,8 +449,8 @@ class ApplicationController < ActionController::Base
response.headers['Page-Title'] = URI.escape(page_title('GitLab'))
end
- def peek_request?
- request.path.start_with?('/-/peek')
+ def html_request?
+ request.format.html?
end
def json_request?
@@ -462,7 +460,7 @@ class ApplicationController < ActionController::Base
def should_enforce_terms?
return false unless Gitlab::CurrentSettings.current_application_settings.enforce_terms
- !(peek_request? || devise_controller?)
+ html_request? && !devise_controller?
end
def set_usage_stats_consent_flag
diff --git a/app/controllers/concerns/confirm_email_warning.rb b/app/controllers/concerns/confirm_email_warning.rb
index 5a4b5897a4f..874deeb4702 100644
--- a/app/controllers/concerns/confirm_email_warning.rb
+++ b/app/controllers/concerns/confirm_email_warning.rb
@@ -4,15 +4,18 @@ module ConfirmEmailWarning
extend ActiveSupport::Concern
included do
- before_action :set_confirm_warning, if: -> { Feature.enabled?(:soft_email_confirmation) }
+ before_action :set_confirm_warning, if: :show_confirm_warning?
end
protected
+ def show_confirm_warning?
+ html_request? && request.get? && Feature.enabled?(:soft_email_confirmation)
+ end
+
def set_confirm_warning
return unless current_user
return if current_user.confirmed?
- return if peek_request? || json_request? || !request.get?
email = current_user.unconfirmed_email || current_user.email
diff --git a/app/controllers/concerns/uploads_actions.rb b/app/controllers/concerns/uploads_actions.rb
index b87779c22d3..023c41821da 100644
--- a/app/controllers/concerns/uploads_actions.rb
+++ b/app/controllers/concerns/uploads_actions.rb
@@ -1,11 +1,16 @@
# frozen_string_literal: true
module UploadsActions
+ extend ActiveSupport::Concern
include Gitlab::Utils::StrongMemoize
include SendFileUpload
UPLOAD_MOUNTS = %w(avatar attachment file logo header_logo favicon).freeze
+ included do
+ prepend_before_action :set_request_format_from_path_extension
+ end
+
def create
uploader = UploadService.new(model, params[:file], uploader_class).execute
@@ -64,6 +69,18 @@ module UploadsActions
private
+ # From ActionDispatch::Http::MimeNegotiation. We have an initializer that
+ # monkey-patches this method out (so that repository paths don't guess a
+ # format based on extension), but we do want this behaviour when serving
+ # uploads.
+ def set_request_format_from_path_extension
+ path = request.headers['action_dispatch.original_path'] || request.headers['PATH_INFO']
+
+ if match = path&.match(/\.(\w+)\z/)
+ request.format = match.captures.first
+ end
+ end
+
def uploader_class
raise NotImplementedError
end
diff --git a/app/controllers/uploads_controller.rb b/app/controllers/uploads_controller.rb
index 635db386792..f39a2b81b54 100644
--- a/app/controllers/uploads_controller.rb
+++ b/app/controllers/uploads_controller.rb
@@ -20,7 +20,7 @@ class UploadsController < ApplicationController
skip_before_action :authenticate_user!
before_action :upload_mount_satisfied?
- before_action :find_model
+ before_action :model
before_action :authorize_access!, only: [:show]
before_action :authorize_create_access!, only: [:create, :authorize]
before_action :verify_workhorse_api!, only: [:authorize]
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 852576dbbc2..796e6438a2c 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -137,6 +137,26 @@ module Issuable
strip_attributes :title
+ # The state_machine gem will reset the value of state_id unless it
+ # is a raw attribute passed in here:
+ # https://gitlab.com/gitlab-org/gitlab/issues/35746#note_241148787
+ #
+ # This assumes another initialize isn't defined. Otherwise this
+ # method may need to be prepended.
+ def initialize(attributes = nil)
+ if attributes.is_a?(Hash)
+ attr = attributes.symbolize_keys
+
+ if attr.key?(:state) && !attr.key?(:state_id)
+ value = attr.delete(:state)
+ state_id = self.class.available_states[value]
+ attributes[:state_id] = state_id if state_id
+ end
+ end
+
+ super(attributes)
+ end
+
# We want to use optimistic lock for cases when only title or description are involved
# http://api.rubyonrails.org/classes/ActiveRecord/Locking/Optimistic.html
def locking_enabled?
diff --git a/app/views/import/manifest/_form.html.haml b/app/views/import/manifest/_form.html.haml
index 78c7fadb019..b515ce084e4 100644
--- a/app/views/import/manifest/_form.html.haml
+++ b/app/views/import/manifest/_form.html.haml
@@ -13,7 +13,7 @@
.form-group
= label_tag :manifest, class: 'label-bold' do
= _('Manifest')
- = file_field_tag :manifest, class: 'form-control-file', required: true
+ = file_field_tag :manifest, class: 'form-control-file w-auto', required: true
.form-text.text-muted
= _('Import multiple repositories by uploading a manifest file.')
= link_to icon('question-circle'), help_page_path('user/project/import/manifest')
diff --git a/app/views/projects/_files.html.haml b/app/views/projects/_files.html.haml
index 3c0dfd4c029..6681bb4d094 100644
--- a/app/views/projects/_files.html.haml
+++ b/app/views/projects/_files.html.haml
@@ -23,7 +23,5 @@
- if can_edit_tree?
= render 'projects/blob/upload', title: _('Upload New File'), placeholder: _('Upload New File'), button_title: _('Upload file'), form_path: project_create_blob_path(@project, @id), method: :post
= render 'projects/blob/new_dir'
- - if @tree.readme
- = render "projects/tree/readme", readme: @tree.readme
- else
= render 'projects/tree/tree_content', tree: @tree, content_url: content_url
diff --git a/app/views/shared/snippets/_snippet.html.haml b/app/views/shared/snippets/_snippet.html.haml
index 0ef626868a2..5602ea37b5c 100644
--- a/app/views/shared/snippets/_snippet.html.haml
+++ b/app/views/shared/snippets/_snippet.html.haml
@@ -7,8 +7,9 @@
.title
= link_to reliable_snippet_path(snippet) do
= snippet.title
- - if snippet.file_name
- %span.snippet-filename.monospace.d-none.d-sm-inline-block
+ - if snippet.file_name.present?
+ %span.snippet-filename.d-none.d-sm-inline-block.ml-2
+ = sprite_icon('doc-code', size: 16, css_class: 'file-icon align-text-bottom')
= snippet.file_name
%ul.controls