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 {
);
this.monacoInstance.setModel(newModel);
this.monacoInstance.updateOptions({
readOnly: !!this.activeFile.file_lock,
});
},
addMonacoEvents() {
this.monacoInstance.onKeyUp(() => {
......@@ -87,7 +90,7 @@ export default {
'activeFileExtension',
]),
shouldHideEditor() {
return this.activeFile.binary && !this.activeFile.raw;
return this.activeFile && this.activeFile.binary && !this.activeFile.raw;
},
},
};
......
......@@ -2,6 +2,7 @@
import { mapActions, mapGetters } from 'vuex';
import timeAgoMixin from '../../vue_shared/mixins/timeago';
import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue';
import fileStatusIcon from './repo_file_status_icon.vue';
export default {
mixins: [
......@@ -9,6 +10,7 @@
],
components: {
skeletonLoadingContainer,
fileStatusIcon,
},
props: {
file: {
......@@ -70,6 +72,9 @@
class="repo-file-name"
>
{{ file.name }}
<fileStatusIcon
:file="file">
</fileStatusIcon>
</a>
<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>
import { mapActions } from 'vuex';
import fileStatusIcon from './repo_file_status_icon.vue';
export default {
props: {
......@@ -9,6 +10,10 @@ export default {
},
},
components: {
fileStatusIcon,
},
computed: {
closeLabel() {
if (this.tab.changed || this.tab.tempFile) {
......@@ -57,6 +62,9 @@ export default {
:title="tab.url"
@click.prevent.stop="setFileActive(tab)">
{{tab.name}}
<fileStatusIcon
:file="tab">
</fileStatusIcon>
</a>
</li>
</template>
......@@ -51,6 +51,9 @@ export const decorateData = (entity) => {
parentTreeUrl = '',
level = 0,
base64 = false,
file_lock,
} = entity;
return {
......@@ -72,6 +75,9 @@ export const decorateData = (entity) => {
renderError,
content,
base64,
file_lock,
};
};
......
......@@ -289,6 +289,13 @@
color: $almost-black;
}
}
.file-status-icon {
width: 10px;
height: 10px;
margin-left: 3px;
}
}
.render-error {
......
......@@ -52,15 +52,13 @@ class Projects::RefsController < Projects::ApplicationController
contents.push(*tree.blobs)
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
@logs = Gitlab::GitalyClient.allow_n_plus_1_calls do
contents[@offset, @limit].to_a.map do |content|
file = @path ? File.join(@path, content.name) : content.name
last_commit = @repo.last_commit_for_path(@commit.id, file)
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,
......
module EE
module LockHelper
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?
path_lock = project.find_path_lock(path, downstream: true)
......@@ -67,7 +67,6 @@ module EE
end
def render_lock_icon(path)
return unless @project.feature_available?(:file_locks)
return unless @project.root_ref?(@ref)
if file_lock = @project.find_path_lock(path, exact_match: true)
......
......@@ -10,4 +10,10 @@ class BlobEntity < Grape::Entity
expose :url do |blob|
project_blob_path(request.project, File.join(request.ref, blob.path))
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
class FileLockEntity < Grape::Entity
expose :user, using: API::Entities::UserSafe
end
......@@ -17,6 +17,8 @@ class Gitlab::PathLocksFinder
end
def find(path, exact_match: false, downstream: false)
return unless @project.feature_available?(:file_locks)
if exact_match
find_by_token(path)
else
......
......@@ -53,4 +53,48 @@ describe('RepoEditor', () => {
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', () => {
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', () => {
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('closeTab', () => {
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