Commit d992333a authored by Tim Zallmann's avatar Tim Zallmann Committed by Douwe Maan

Ide lock files

parent 16eb31b1
...@@ -64,6 +64,9 @@ export default { ...@@ -64,6 +64,9 @@ export default {
); );
this.monacoInstance.setModel(newModel); this.monacoInstance.setModel(newModel);
this.monacoInstance.updateOptions({
readOnly: !!this.activeFile.file_lock,
});
}, },
addMonacoEvents() { addMonacoEvents() {
this.monacoInstance.onKeyUp(() => { this.monacoInstance.onKeyUp(() => {
...@@ -87,7 +90,7 @@ export default { ...@@ -87,7 +90,7 @@ export default {
'activeFileExtension', 'activeFileExtension',
]), ]),
shouldHideEditor() { shouldHideEditor() {
return this.activeFile.binary && !this.activeFile.raw; return this.activeFile && this.activeFile.binary && !this.activeFile.raw;
}, },
}, },
}; };
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import { mapActions, mapGetters } from 'vuex'; import { mapActions, mapGetters } from 'vuex';
import timeAgoMixin from '../../vue_shared/mixins/timeago'; import timeAgoMixin from '../../vue_shared/mixins/timeago';
import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue'; import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue';
import fileStatusIcon from './repo_file_status_icon.vue';
export default { export default {
mixins: [ mixins: [
...@@ -9,6 +10,7 @@ ...@@ -9,6 +10,7 @@
], ],
components: { components: {
skeletonLoadingContainer, skeletonLoadingContainer,
fileStatusIcon,
}, },
props: { props: {
file: { file: {
...@@ -70,6 +72,9 @@ ...@@ -70,6 +72,9 @@
class="repo-file-name" class="repo-file-name"
> >
{{ file.name }} {{ file.name }}
<fileStatusIcon
:file="file">
</fileStatusIcon>
</a> </a>
<template v-if="isSubmodule && file.id"> <template v-if="isSubmodule && file.id">
@ @
......
<script>
import icon from '../../vue_shared/components/icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
import '../../lib/utils/datetime_utility';
export default {
components: {
icon,
},
directives: {
tooltip,
},
props: {
file: {
type: Object,
required: true,
},
},
computed: {
lockTooltip() {
return `Locked by ${this.file.file_lock.user.name}`;
},
},
};
</script>
<template>
<span
v-if="file.file_lock"
v-tooltip
:title="lockTooltip"
data-container="body"
>
<icon
name="lock"
css-classes="file-status-icon"
>
</icon>
</span>
</template>
<script> <script>
import { mapActions } from 'vuex'; import { mapActions } from 'vuex';
import fileStatusIcon from './repo_file_status_icon.vue';
export default { export default {
props: { props: {
...@@ -9,6 +10,10 @@ export default { ...@@ -9,6 +10,10 @@ export default {
}, },
}, },
components: {
fileStatusIcon,
},
computed: { computed: {
closeLabel() { closeLabel() {
if (this.tab.changed || this.tab.tempFile) { if (this.tab.changed || this.tab.tempFile) {
...@@ -57,6 +62,9 @@ export default { ...@@ -57,6 +62,9 @@ export default {
:title="tab.url" :title="tab.url"
@click.prevent.stop="setFileActive(tab)"> @click.prevent.stop="setFileActive(tab)">
{{tab.name}} {{tab.name}}
<fileStatusIcon
:file="tab">
</fileStatusIcon>
</a> </a>
</li> </li>
</template> </template>
...@@ -51,6 +51,9 @@ export const decorateData = (entity) => { ...@@ -51,6 +51,9 @@ export const decorateData = (entity) => {
parentTreeUrl = '', parentTreeUrl = '',
level = 0, level = 0,
base64 = false, base64 = false,
file_lock,
} = entity; } = entity;
return { return {
...@@ -72,6 +75,9 @@ export const decorateData = (entity) => { ...@@ -72,6 +75,9 @@ export const decorateData = (entity) => {
renderError, renderError,
content, content,
base64, base64,
file_lock,
}; };
}; };
......
...@@ -289,6 +289,13 @@ ...@@ -289,6 +289,13 @@
color: $almost-black; color: $almost-black;
} }
} }
.file-status-icon {
width: 10px;
height: 10px;
margin-left: 3px;
}
} }
.render-error { .render-error {
......
...@@ -52,15 +52,13 @@ class Projects::RefsController < Projects::ApplicationController ...@@ -52,15 +52,13 @@ class Projects::RefsController < Projects::ApplicationController
contents.push(*tree.blobs) contents.push(*tree.blobs)
contents.push(*tree.submodules) contents.push(*tree.submodules)
show_path_locks = @project.feature_available?(:file_locks) && @project.path_locks.any?
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37433 # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37433
@logs = Gitlab::GitalyClient.allow_n_plus_1_calls do @logs = Gitlab::GitalyClient.allow_n_plus_1_calls do
contents[@offset, @limit].to_a.map do |content| contents[@offset, @limit].to_a.map do |content|
file = @path ? File.join(@path, content.name) : content.name file = @path ? File.join(@path, content.name) : content.name
last_commit = @repo.last_commit_for_path(@commit.id, file) last_commit = @repo.last_commit_for_path(@commit.id, file)
commit_path = project_commit_path(@project, last_commit) if last_commit commit_path = project_commit_path(@project, last_commit) if last_commit
path_lock = show_path_locks && @project.find_path_lock(file) path_lock = @project.find_path_lock(file)
{ {
file_name: content.name, file_name: content.name,
......
module EE module EE
module LockHelper module LockHelper
def lock_file_link(project = @project, path = @path, html_options: {}) def lock_file_link(project = @project, path = @path, html_options: {})
return unless project.feature_available?(:file_locks) && current_user return unless current_user
return if path.blank? return if path.blank?
path_lock = project.find_path_lock(path, downstream: true) path_lock = project.find_path_lock(path, downstream: true)
...@@ -67,7 +67,6 @@ module EE ...@@ -67,7 +67,6 @@ module EE
end end
def render_lock_icon(path) def render_lock_icon(path)
return unless @project.feature_available?(:file_locks)
return unless @project.root_ref?(@ref) return unless @project.root_ref?(@ref)
if file_lock = @project.find_path_lock(path, exact_match: true) if file_lock = @project.find_path_lock(path, exact_match: true)
......
...@@ -10,4 +10,10 @@ class BlobEntity < Grape::Entity ...@@ -10,4 +10,10 @@ class BlobEntity < Grape::Entity
expose :url do |blob| expose :url do |blob|
project_blob_path(request.project, File.join(request.ref, blob.path)) project_blob_path(request.project, File.join(request.ref, blob.path))
end end
expose :file_lock, using: FileLockEntity do |blob|
if request.project.root_ref?(request.ref)
request.project.find_path_lock(blob.path, exact_match: true)
end
end
end end
class FileLockEntity < Grape::Entity
expose :user, using: API::Entities::UserSafe
end
...@@ -17,6 +17,8 @@ class Gitlab::PathLocksFinder ...@@ -17,6 +17,8 @@ class Gitlab::PathLocksFinder
end end
def find(path, exact_match: false, downstream: false) def find(path, exact_match: false, downstream: false)
return unless @project.feature_available?(:file_locks)
if exact_match if exact_match
find_by_token(path) find_by_token(path)
else else
......
...@@ -53,4 +53,48 @@ describe('RepoEditor', () => { ...@@ -53,4 +53,48 @@ describe('RepoEditor', () => {
expect(vm.$el.textContent.trim()).toBe('testing'); expect(vm.$el.textContent.trim()).toBe('testing');
}); });
}); });
describe('when open file is locked', () => {
beforeEach((done) => {
const f = file('test', '123', 'plaintext');
f.active = true;
f.tempFile = true;
const RepoEditor = Vue.extend(repoEditor);
vm = new RepoEditor({
store,
});
// Stubbing the getRawFileData Method to return a plain content
spyOn(vm, 'getRawFileData').and.callFake(() => Promise.resolve('testing'));
// Spying on setupEditor so we know when the async process executed
vm.oldSetupEditor = vm.setupEditor;
spyOn(vm, 'setupEditor').and.callFake(() => {
spyOn(vm.monacoInstance, 'updateOptions');
vm.oldSetupEditor();
Vue.nextTick(() => {
done();
});
});
vm.$store.state.openFiles.push(f);
vm.$store.getters.activeFile.html = 'testing';
vm.$store.getters.activeFile.file_lock = {
user: {
name: 'testuser',
updated_at: new Date(),
},
};
vm.$mount();
});
it('Monaco should be in read-only mode', () => {
expect(vm.monacoInstance.updateOptions).toHaveBeenCalledWith({
readOnly: true,
});
});
});
}); });
...@@ -111,4 +111,34 @@ describe('RepoFile', () => { ...@@ -111,4 +111,34 @@ describe('RepoFile', () => {
expect(vm.$el.querySelector('td').textContent.replace(/\s+/g, ' ')).toContain('submodule name @ 12345678'); expect(vm.$el.querySelector('td').textContent.replace(/\s+/g, ' ')).toContain('submodule name @ 12345678');
}); });
}); });
describe('locked file', () => {
let f;
beforeEach(() => {
f = file('locked file');
f.file_lock = {
user: {
name: 'testuser',
updated_at: new Date(),
},
};
vm = createComponent({
file: f,
});
});
afterEach(() => {
vm.$destroy();
});
it('renders lock icon', () => {
expect(vm.$el.querySelector('.file-status-icon')).not.toBeNull();
});
it('renders a tooltip', () => {
expect(vm.$el.querySelector('.repo-file-name span').dataset.originalTitle).toContain('Locked by testuser');
});
});
}); });
...@@ -65,6 +65,36 @@ describe('RepoTab', () => { ...@@ -65,6 +65,36 @@ describe('RepoTab', () => {
expect(vm.$el.querySelector('.close-btn .fa-circle')).toBeTruthy(); expect(vm.$el.querySelector('.close-btn .fa-circle')).toBeTruthy();
}); });
describe('locked file', () => {
let f;
beforeEach(() => {
f = file('locked file');
f.file_lock = {
user: {
name: 'testuser',
updated_at: new Date(),
},
};
vm = createComponent({
tab: f,
});
});
afterEach(() => {
vm.$destroy();
});
it('renders lock icon', () => {
expect(vm.$el.querySelector('.file-status-icon')).not.toBeNull();
});
it('renders a tooltip', () => {
expect(vm.$el.querySelector('.repo-tab span').dataset.originalTitle).toContain('Locked by testuser');
});
});
describe('methods', () => { describe('methods', () => {
describe('closeTab', () => { describe('closeTab', () => {
it('does not close tab if is changed', (done) => { it('does not close tab if is changed', (done) => {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment