Convert polygerrit to es6-modules

This change replace all HTML imports with es6-modules. The only exceptions are:
* gr-app.html file, which can be deleted only after updating the
  gerrit/httpd/raw/PolyGerritIndexHtml.soy file.
* dark-theme.html which is loaded via importHref. Must be updated manually
  later in a separate change.

This change was produced automatically by ./es6-modules-converter.sh script.
No manual changes were made.

Change-Id: I0c447dd8c05757741e2c940720652d01d9fb7d67
This commit is contained in:
Dmitrii Filippov 2020-03-17 11:27:28 +01:00
parent dbe954539d
commit daf0ec9543
600 changed files with 99000 additions and 99742 deletions

View File

@ -1,21 +1,19 @@
<!--
@license
Copyright (C) 2017 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<script>
/**
* @license
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function(window) {
'use strict';
@ -63,4 +61,3 @@ limitations under the License.
};
}
})(window);
</script>

View File

@ -19,40 +19,42 @@ limitations under the License.
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>async-foreach-behavior</title>
<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/bower_components/web-component-tester/browser.js"></script>
<script src="../../test/test-pre-setup.js"></script>
<link rel="import" href="../../test/common-test-setup.html"/>
<link rel="import" href="async-foreach-behavior.html">
<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/components/wct-browser-legacy/browser.js"></script>
<script type="module" src="../../test/test-pre-setup.js"></script>
<script type="module" src="../../test/common-test-setup.js"></script>
<script type="module" src="./async-foreach-behavior.js"></script>
<script>
suite('async-foreach-behavior tests', async () => {
await readyToTest();
test('loops over each item', () => {
const fn = sinon.stub().returns(Promise.resolve());
return Gerrit.AsyncForeachBehavior.asyncForeach([1, 2, 3], fn)
.then(() => {
assert.isTrue(fn.calledThrice);
assert.equal(fn.getCall(0).args[0], 1);
assert.equal(fn.getCall(1).args[0], 2);
assert.equal(fn.getCall(2).args[0], 3);
});
});
test('halts on stop condition', () => {
const stub = sinon.stub();
const fn = (e, stop) => {
stub(e);
stop();
return Promise.resolve();
};
return Gerrit.AsyncForeachBehavior.asyncForeach([1, 2, 3], fn)
.then(() => {
assert.isTrue(stub.calledOnce);
assert.equal(stub.lastCall.args[0], 1);
});
});
<script type="module">
import '../../test/test-pre-setup.js';
import '../../test/common-test-setup.js';
import './async-foreach-behavior.js';
suite('async-foreach-behavior tests', () => {
test('loops over each item', () => {
const fn = sinon.stub().returns(Promise.resolve());
return Gerrit.AsyncForeachBehavior.asyncForeach([1, 2, 3], fn)
.then(() => {
assert.isTrue(fn.calledThrice);
assert.equal(fn.getCall(0).args[0], 1);
assert.equal(fn.getCall(1).args[0], 2);
assert.equal(fn.getCall(2).args[0], 3);
});
});
test('halts on stop condition', () => {
const stub = sinon.stub();
const fn = (e, stop) => {
stub(e);
stop();
return Promise.resolve();
};
return Gerrit.AsyncForeachBehavior.asyncForeach([1, 2, 3], fn)
.then(() => {
assert.isTrue(stub.calledOnce);
assert.equal(stub.lastCall.args[0], 1);
});
});
});
</script>

View File

@ -1,21 +1,19 @@
<!--
@license
Copyright (C) 2017 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<script>
/**
* @license
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function(window) {
'use strict';
@ -46,4 +44,3 @@ limitations under the License.
};
}
})(window);
</script>

View File

@ -19,17 +19,20 @@ limitations under the License.
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>base-url-behavior</title>
<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/bower_components/web-component-tester/browser.js"></script>
<script src="../../test/test-pre-setup.js"></script>
<link rel="import" href="../../test/common-test-setup.html"/>
<script>
/** @type {string} */
window.CANONICAL_PATH = '/r';
<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/components/wct-browser-legacy/browser.js"></script>
<script type="module" src="../../test/test-pre-setup.js"></script>
<script type="module" src="../../test/common-test-setup.js"></script>
<script type="module">
import '../../test/test-pre-setup.js';
import '../../test/common-test-setup.js';
import './base-url-behavior.js';
/** @type {string} */
window.CANONICAL_PATH = '/r';
</script>
<link rel="import" href="base-url-behavior.html">
<script type="module" src="./base-url-behavior.js"></script>
<test-fixture id="basic">
<template>
@ -45,30 +48,33 @@ limitations under the License.
</template>
</test-fixture>
<script>
suite('base-url-behavior tests', async () => {
await readyToTest();
let element;
// eslint-disable-next-line no-unused-vars
let overlay;
<script type="module">
import '../../test/test-pre-setup.js';
import '../../test/common-test-setup.js';
import './base-url-behavior.js';
import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
suite('base-url-behavior tests', () => {
let element;
// eslint-disable-next-line no-unused-vars
let overlay;
suiteSetup(() => {
// Define a Polymer element that uses this behavior.
Polymer({
is: 'test-element',
behaviors: [
Gerrit.BaseUrlBehavior,
],
});
});
setup(() => {
element = fixture('basic');
overlay = fixture('within-overlay');
});
test('getBaseUrl', () => {
assert.deepEqual(element.getBaseUrl(), '/r');
suiteSetup(() => {
// Define a Polymer element that uses this behavior.
Polymer({
is: 'test-element',
behaviors: [
Gerrit.BaseUrlBehavior,
],
});
});
setup(() => {
element = fixture('basic');
overlay = fixture('within-overlay');
});
test('getBaseUrl', () => {
assert.deepEqual(element.getBaseUrl(), '/r');
});
});
</script>

View File

@ -1,21 +1,21 @@
<!--
@license
Copyright (C) 2017 The Android Open Source Project
/**
* @license
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import '../base-url-behavior/base-url-behavior.js';
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<link rel="import" href="../base-url-behavior/base-url-behavior.html">
<script>
(function(window) {
'use strict';
@ -77,4 +77,3 @@ limitations under the License.
};
}
})(window);
</script>

View File

@ -15,17 +15,22 @@ See the License for the specific language governing permissions and
limitations under the License.
-->
<!-- Polymer included for the html import polyfill. -->
<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/bower_components/web-component-tester/browser.js"></script>
<script src="../../test/test-pre-setup.js"></script>
<link rel="import" href="../../test/common-test-setup.html"/>
<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/components/wct-browser-legacy/browser.js"></script>
<script type="module" src="../../test/test-pre-setup.js"></script>
<script type="module" src="../../test/common-test-setup.js"></script>
<title>docs-url-behavior</title>
<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<link rel="import" href="docs-url-behavior.html">
<script type="module" src="./docs-url-behavior.js"></script>
<script>void(0);</script>
<script type="module">
import '../../test/test-pre-setup.js';
import '../../test/common-test-setup.js';
import './docs-url-behavior.js';
void(0);
</script>
<test-fixture id="basic">
<template>
@ -33,71 +38,74 @@ limitations under the License.
</template>
</test-fixture>
<script>
suite('docs-url-behavior tests', async () => {
await readyToTest();
let element;
<script type="module">
import '../../test/test-pre-setup.js';
import '../../test/common-test-setup.js';
import './docs-url-behavior.js';
import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
suite('docs-url-behavior tests', () => {
let element;
suiteSetup(() => {
// Define a Polymer element that uses this behavior.
Polymer({
is: 'docs-url-behavior-element',
behaviors: [Gerrit.DocsUrlBehavior],
});
});
setup(() => {
element = fixture('basic');
element._clearDocsBaseUrlCache();
});
test('null config', () => {
const mockRestApi = {
probePath: sinon.stub().returns(Promise.resolve(true)),
};
return element.getDocsBaseUrl(null, mockRestApi)
.then(docsBaseUrl => {
assert.isTrue(
mockRestApi.probePath.calledWith('/Documentation/index.html'));
assert.equal(docsBaseUrl, '/Documentation');
});
});
test('no doc config', () => {
const mockRestApi = {
probePath: sinon.stub().returns(Promise.resolve(true)),
};
const config = {gerrit: {}};
return element.getDocsBaseUrl(config, mockRestApi)
.then(docsBaseUrl => {
assert.isTrue(
mockRestApi.probePath.calledWith('/Documentation/index.html'));
assert.equal(docsBaseUrl, '/Documentation');
});
});
test('has doc config', () => {
const mockRestApi = {
probePath: sinon.stub().returns(Promise.resolve(true)),
};
const config = {gerrit: {doc_url: 'foobar'}};
return element.getDocsBaseUrl(config, mockRestApi)
.then(docsBaseUrl => {
assert.isFalse(mockRestApi.probePath.called);
assert.equal(docsBaseUrl, 'foobar');
});
});
test('no probe', () => {
const mockRestApi = {
probePath: sinon.stub().returns(Promise.resolve(false)),
};
return element.getDocsBaseUrl(null, mockRestApi)
.then(docsBaseUrl => {
assert.isTrue(
mockRestApi.probePath.calledWith('/Documentation/index.html'));
assert.isNotOk(docsBaseUrl);
});
suiteSetup(() => {
// Define a Polymer element that uses this behavior.
Polymer({
is: 'docs-url-behavior-element',
behaviors: [Gerrit.DocsUrlBehavior],
});
});
setup(() => {
element = fixture('basic');
element._clearDocsBaseUrlCache();
});
test('null config', () => {
const mockRestApi = {
probePath: sinon.stub().returns(Promise.resolve(true)),
};
return element.getDocsBaseUrl(null, mockRestApi)
.then(docsBaseUrl => {
assert.isTrue(
mockRestApi.probePath.calledWith('/Documentation/index.html'));
assert.equal(docsBaseUrl, '/Documentation');
});
});
test('no doc config', () => {
const mockRestApi = {
probePath: sinon.stub().returns(Promise.resolve(true)),
};
const config = {gerrit: {}};
return element.getDocsBaseUrl(config, mockRestApi)
.then(docsBaseUrl => {
assert.isTrue(
mockRestApi.probePath.calledWith('/Documentation/index.html'));
assert.equal(docsBaseUrl, '/Documentation');
});
});
test('has doc config', () => {
const mockRestApi = {
probePath: sinon.stub().returns(Promise.resolve(true)),
};
const config = {gerrit: {doc_url: 'foobar'}};
return element.getDocsBaseUrl(config, mockRestApi)
.then(docsBaseUrl => {
assert.isFalse(mockRestApi.probePath.called);
assert.equal(docsBaseUrl, 'foobar');
});
});
test('no probe', () => {
const mockRestApi = {
probePath: sinon.stub().returns(Promise.resolve(false)),
};
return element.getDocsBaseUrl(null, mockRestApi)
.then(docsBaseUrl => {
assert.isTrue(
mockRestApi.probePath.calledWith('/Documentation/index.html'));
assert.isNotOk(docsBaseUrl);
});
});
});
</script>

View File

@ -1,20 +1,19 @@
<!--
@license
Copyright (C) 2018 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<script>
/**
* @license
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function(window) {
'use strict';
@ -61,4 +60,3 @@ limitations under the License.
};
}
})(window);
</script>

View File

@ -19,13 +19,13 @@ limitations under the License.
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>dom-util-behavior</title>
<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/bower_components/web-component-tester/browser.js"></script>
<script src="../../test/test-pre-setup.js"></script>
<link rel="import" href="../../test/common-test-setup.html"/>
<link rel="import" href="dom-util-behavior.html">
<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/components/wct-browser-legacy/browser.js"></script>
<script type="module" src="../../test/test-pre-setup.js"></script>
<script type="module" src="../../test/common-test-setup.js"></script>
<script type="module" src="./dom-util-behavior.js"></script>
<test-fixture id="nested-structure">
<template>
@ -40,34 +40,37 @@ limitations under the License.
</template>
</test-fixture>
<script>
suite('dom-util-behavior tests', async () => {
await readyToTest();
let element;
let divs;
<script type="module">
import '../../test/test-pre-setup.js';
import '../../test/common-test-setup.js';
import './dom-util-behavior.js';
import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
suite('dom-util-behavior tests', () => {
let element;
let divs;
suiteSetup(() => {
// Define a Polymer element that uses this behavior.
Polymer({
is: 'test-element',
behaviors: [Gerrit.DomUtilBehavior],
});
});
setup(() => {
const testDom = fixture('nested-structure');
element = testDom[0];
divs = testDom[1];
});
test('descendedFromClass', () => {
// .c is a child of .a and not vice versa.
assert.isTrue(element.descendedFromClass(divs.querySelector('.c'), 'a'));
assert.isFalse(element.descendedFromClass(divs.querySelector('.a'), 'c'));
// Stops at stop element.
assert.isFalse(element.descendedFromClass(divs.querySelector('.c'), 'a',
divs.querySelector('.b')));
suiteSetup(() => {
// Define a Polymer element that uses this behavior.
Polymer({
is: 'test-element',
behaviors: [Gerrit.DomUtilBehavior],
});
});
setup(() => {
const testDom = fixture('nested-structure');
element = testDom[0];
divs = testDom[1];
});
test('descendedFromClass', () => {
// .c is a child of .a and not vice versa.
assert.isTrue(element.descendedFromClass(divs.querySelector('.c'), 'a'));
assert.isFalse(element.descendedFromClass(divs.querySelector('.a'), 'c'));
// Stops at stop element.
assert.isFalse(element.descendedFromClass(divs.querySelector('.c'), 'a',
divs.querySelector('.b')));
});
});
</script>

View File

@ -1,21 +1,19 @@
<!--
@license
Copyright (C) 2019 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<script>
/**
* @license
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function(window) {
'use strict';
@ -69,4 +67,3 @@ limitations under the License.
};
}
})(window);
</script>

View File

@ -1,21 +1,19 @@
<!--
@license
Copyright (C) 2017 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<script>
/**
* @license
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function(window) {
'use strict';
@ -177,4 +175,3 @@ limitations under the License.
};
}
})(window);
</script>

View File

@ -19,13 +19,13 @@ limitations under the License.
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>keyboard-shortcut-behavior</title>
<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/bower_components/web-component-tester/browser.js"></script>
<script src="../../test/test-pre-setup.js"></script>
<link rel="import" href="../../test/common-test-setup.html"/>
<link rel="import" href="gr-access-behavior.html">
<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/components/wct-browser-legacy/browser.js"></script>
<script type="module" src="../../test/test-pre-setup.js"></script>
<script type="module" src="../../test/common-test-setup.js"></script>
<script type="module" src="./gr-access-behavior.js"></script>
<test-fixture id="basic">
<template>
@ -33,41 +33,44 @@ limitations under the License.
</template>
</test-fixture>
<script>
suite('gr-access-behavior tests', async () => {
await readyToTest();
let element;
<script type="module">
import '../../test/test-pre-setup.js';
import '../../test/common-test-setup.js';
import './gr-access-behavior.js';
import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
suite('gr-access-behavior tests', () => {
let element;
suiteSetup(() => {
// Define a Polymer element that uses this behavior.
Polymer({
is: 'test-element',
behaviors: [Gerrit.AccessBehavior],
});
});
setup(() => {
element = fixture('basic');
});
test('toSortedArray', () => {
const rules = {
'global:Project-Owners': {
action: 'ALLOW', force: false,
},
'4c97682e6ce6b7247f3381b6f1789356666de7f': {
action: 'ALLOW', force: false,
},
};
const expectedResult = [
{id: '4c97682e6ce6b7247f3381b6f1789356666de7f', value: {
action: 'ALLOW', force: false,
}},
{id: 'global:Project-Owners', value: {
action: 'ALLOW', force: false,
}},
];
assert.deepEqual(element.toSortedArray(rules), expectedResult);
suiteSetup(() => {
// Define a Polymer element that uses this behavior.
Polymer({
is: 'test-element',
behaviors: [Gerrit.AccessBehavior],
});
});
setup(() => {
element = fixture('basic');
});
test('toSortedArray', () => {
const rules = {
'global:Project-Owners': {
action: 'ALLOW', force: false,
},
'4c97682e6ce6b7247f3381b6f1789356666de7f': {
action: 'ALLOW', force: false,
},
};
const expectedResult = [
{id: '4c97682e6ce6b7247f3381b6f1789356666de7f', value: {
action: 'ALLOW', force: false,
}},
{id: 'global:Project-Owners', value: {
action: 'ALLOW', force: false,
}},
];
assert.deepEqual(element.toSortedArray(rules), expectedResult);
});
});
</script>

View File

@ -1,20 +1,19 @@
<!--
@license
Copyright (C) 2018 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<script>
/**
* @license
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function(window) {
'use strict';
@ -220,4 +219,3 @@ limitations under the License.
};
}
})(window);
</script>

View File

@ -19,13 +19,13 @@ limitations under the License.
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>keyboard-shortcut-behavior</title>
<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/bower_components/web-component-tester/browser.js"></script>
<script src="../../test/test-pre-setup.js"></script>
<link rel="import" href="../../test/common-test-setup.html"/>
<link rel="import" href="gr-admin-nav-behavior.html">
<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/components/wct-browser-legacy/browser.js"></script>
<script type="module" src="../../test/test-pre-setup.js"></script>
<script type="module" src="../../test/common-test-setup.js"></script>
<script type="module" src="./gr-admin-nav-behavior.js"></script>
<test-fixture id="basic">
<template>
@ -33,338 +33,341 @@ limitations under the License.
</template>
</test-fixture>
<script>
suite('gr-admin-nav-behavior tests', async () => {
await readyToTest();
let element;
let sandbox;
let capabilityStub;
let menuLinkStub;
<script type="module">
import '../../test/test-pre-setup.js';
import '../../test/common-test-setup.js';
import './gr-admin-nav-behavior.js';
import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
suite('gr-admin-nav-behavior tests', () => {
let element;
let sandbox;
let capabilityStub;
let menuLinkStub;
suiteSetup(() => {
// Define a Polymer element that uses this behavior.
Polymer({
is: 'test-element',
behaviors: [
Gerrit.AdminNavBehavior,
],
});
});
setup(() => {
element = fixture('basic');
sandbox = sinon.sandbox.create();
capabilityStub = sinon.stub();
menuLinkStub = sinon.stub().returns([]);
});
teardown(() => {
sandbox.restore();
});
const testAdminLinks = (account, options, expected, done) => {
element.getAdminLinks(account,
capabilityStub,
menuLinkStub,
options)
.then(res => {
assert.equal(expected.totalLength, res.links.length);
assert.equal(res.links[0].name, 'Repositories');
// Repos
if (expected.groupListShown) {
assert.equal(res.links[1].name, 'Groups');
}
if (expected.pluginListShown) {
assert.equal(res.links[2].name, 'Plugins');
assert.isNotOk(res.links[2].subsection);
}
if (expected.projectPageShown) {
assert.isOk(res.links[0].subsection);
assert.equal(res.links[0].subsection.children.length, 5);
} else {
assert.isNotOk(res.links[0].subsection);
}
// Groups
if (expected.groupPageShown) {
assert.isOk(res.links[1].subsection);
assert.equal(res.links[1].subsection.children.length,
expected.groupSubpageLength);
} else if ( expected.totalLength > 1) {
assert.isNotOk(res.links[1].subsection);
}
if (expected.pluginGeneratedLinks) {
for (const link of expected.pluginGeneratedLinks) {
const linkMatch = res.links
.find(l => (l.url === link.url && l.name === link.text));
assert.isTrue(!!linkMatch);
// External links should open in new tab.
if (link.url[0] !== '/') {
assert.equal(linkMatch.target, '_blank');
} else {
assert.isNotOk(linkMatch.target);
}
}
}
// Current section
if (expected.projectPageShown || expected.groupPageShown) {
assert.isOk(res.expandedSection);
assert.isOk(res.expandedSection.children);
} else {
assert.isNotOk(res.expandedSection);
}
if (expected.projectPageShown) {
assert.equal(res.expandedSection.name, 'my-repo');
assert.equal(res.expandedSection.children.length, 5);
} else if (expected.groupPageShown) {
assert.equal(res.expandedSection.name, 'my-group');
assert.equal(res.expandedSection.children.length,
expected.groupSubpageLength);
}
done();
});
};
suite('logged out', () => {
let account;
let expected;
setup(() => {
expected = {
groupListShown: false,
groupPageShown: false,
pluginListShown: false,
};
});
test('without a specific repo or group', done => {
let options;
expected = Object.assign(expected, {
totalLength: 1,
projectPageShown: false,
});
testAdminLinks(account, options, expected, done);
});
test('with a repo', done => {
const options = {repoName: 'my-repo'};
expected = Object.assign(expected, {
totalLength: 1,
projectPageShown: true,
});
testAdminLinks(account, options, expected, done);
});
test('with plugin generated links', done => {
let options;
const generatedLinks = [
{text: 'internal link text', url: '/internal/link/url'},
{text: 'external link text', url: 'http://external/link/url'},
];
menuLinkStub.returns(generatedLinks);
expected = Object.assign(expected, {
totalLength: 3,
projectPageShown: false,
pluginGeneratedLinks: generatedLinks,
});
testAdminLinks(account, options, expected, done);
});
});
suite('no plugin capability logged in', () => {
const account = {
name: 'test-user',
};
let expected;
setup(() => {
expected = {
totalLength: 2,
pluginListShown: false,
};
capabilityStub.returns(Promise.resolve({}));
});
test('without a specific project or group', done => {
let options;
expected = Object.assign(expected, {
projectPageShown: false,
groupListShown: true,
groupPageShown: false,
});
testAdminLinks(account, options, expected, done);
});
test('with a repo', done => {
const account = {
name: 'test-user',
};
const options = {repoName: 'my-repo'};
expected = Object.assign(expected, {
projectPageShown: true,
groupListShown: true,
groupPageShown: false,
});
testAdminLinks(account, options, expected, done);
});
});
suite('view plugin capability logged in', () => {
const account = {
name: 'test-user',
};
let expected;
setup(() => {
capabilityStub.returns(Promise.resolve({viewPlugins: true}));
expected = {
totalLength: 3,
groupListShown: true,
pluginListShown: true,
};
});
test('without a specific repo or group', done => {
let options;
expected = Object.assign(expected, {
projectPageShown: false,
groupPageShown: false,
});
testAdminLinks(account, options, expected, done);
});
test('with a repo', done => {
const options = {repoName: 'my-repo'};
expected = Object.assign(expected, {
projectPageShown: true,
groupPageShown: false,
});
testAdminLinks(account, options, expected, done);
});
test('admin with internal group', done => {
const options = {
groupId: 'a15262',
groupName: 'my-group',
groupIsInternal: true,
isAdmin: true,
groupOwner: false,
};
expected = Object.assign(expected, {
projectPageShown: false,
groupPageShown: true,
groupSubpageLength: 2,
});
testAdminLinks(account, options, expected, done);
});
test('group owner with internal group', done => {
const options = {
groupId: 'a15262',
groupName: 'my-group',
groupIsInternal: true,
isAdmin: false,
groupOwner: true,
};
expected = Object.assign(expected, {
projectPageShown: false,
groupPageShown: true,
groupSubpageLength: 2,
});
testAdminLinks(account, options, expected, done);
});
test('non owner or admin with internal group', done => {
const options = {
groupId: 'a15262',
groupName: 'my-group',
groupIsInternal: true,
isAdmin: false,
groupOwner: false,
};
expected = Object.assign(expected, {
projectPageShown: false,
groupPageShown: true,
groupSubpageLength: 1,
});
testAdminLinks(account, options, expected, done);
});
test('admin with external group', done => {
const options = {
groupId: 'a15262',
groupName: 'my-group',
groupIsInternal: false,
isAdmin: true,
groupOwner: true,
};
expected = Object.assign(expected, {
projectPageShown: false,
groupPageShown: true,
groupSubpageLength: 0,
});
testAdminLinks(account, options, expected, done);
});
});
suite('view plugin screen with plugin capability', () => {
const account = {
name: 'test-user',
};
let expected;
setup(() => {
capabilityStub.returns(Promise.resolve({pluginCapability: true}));
expected = {};
});
test('with plugin with capabilities', done => {
let options;
const generatedLinks = [
{text: 'without capability', url: '/without'},
{text: 'with capability',
url: '/with',
capability: 'pluginCapability'},
];
menuLinkStub.returns(generatedLinks);
expected = Object.assign(expected, {
totalLength: 4,
pluginGeneratedLinks: generatedLinks,
});
testAdminLinks(account, options, expected, done);
});
});
suite('view plugin screen without plugin capability', () => {
const account = {
name: 'test-user',
};
let expected;
setup(() => {
capabilityStub.returns(Promise.resolve({}));
expected = {};
});
test('with plugin with capabilities', done => {
let options;
const generatedLinks = [
{text: 'without capability', url: '/without'},
{text: 'with capability',
url: '/with',
capability: 'pluginCapability'},
];
menuLinkStub.returns(generatedLinks);
expected = Object.assign(expected, {
totalLength: 3,
pluginGeneratedLinks: [generatedLinks[0]],
});
testAdminLinks(account, options, expected, done);
});
suiteSetup(() => {
// Define a Polymer element that uses this behavior.
Polymer({
is: 'test-element',
behaviors: [
Gerrit.AdminNavBehavior,
],
});
});
setup(() => {
element = fixture('basic');
sandbox = sinon.sandbox.create();
capabilityStub = sinon.stub();
menuLinkStub = sinon.stub().returns([]);
});
teardown(() => {
sandbox.restore();
});
const testAdminLinks = (account, options, expected, done) => {
element.getAdminLinks(account,
capabilityStub,
menuLinkStub,
options)
.then(res => {
assert.equal(expected.totalLength, res.links.length);
assert.equal(res.links[0].name, 'Repositories');
// Repos
if (expected.groupListShown) {
assert.equal(res.links[1].name, 'Groups');
}
if (expected.pluginListShown) {
assert.equal(res.links[2].name, 'Plugins');
assert.isNotOk(res.links[2].subsection);
}
if (expected.projectPageShown) {
assert.isOk(res.links[0].subsection);
assert.equal(res.links[0].subsection.children.length, 5);
} else {
assert.isNotOk(res.links[0].subsection);
}
// Groups
if (expected.groupPageShown) {
assert.isOk(res.links[1].subsection);
assert.equal(res.links[1].subsection.children.length,
expected.groupSubpageLength);
} else if ( expected.totalLength > 1) {
assert.isNotOk(res.links[1].subsection);
}
if (expected.pluginGeneratedLinks) {
for (const link of expected.pluginGeneratedLinks) {
const linkMatch = res.links
.find(l => (l.url === link.url && l.name === link.text));
assert.isTrue(!!linkMatch);
// External links should open in new tab.
if (link.url[0] !== '/') {
assert.equal(linkMatch.target, '_blank');
} else {
assert.isNotOk(linkMatch.target);
}
}
}
// Current section
if (expected.projectPageShown || expected.groupPageShown) {
assert.isOk(res.expandedSection);
assert.isOk(res.expandedSection.children);
} else {
assert.isNotOk(res.expandedSection);
}
if (expected.projectPageShown) {
assert.equal(res.expandedSection.name, 'my-repo');
assert.equal(res.expandedSection.children.length, 5);
} else if (expected.groupPageShown) {
assert.equal(res.expandedSection.name, 'my-group');
assert.equal(res.expandedSection.children.length,
expected.groupSubpageLength);
}
done();
});
};
suite('logged out', () => {
let account;
let expected;
setup(() => {
expected = {
groupListShown: false,
groupPageShown: false,
pluginListShown: false,
};
});
test('without a specific repo or group', done => {
let options;
expected = Object.assign(expected, {
totalLength: 1,
projectPageShown: false,
});
testAdminLinks(account, options, expected, done);
});
test('with a repo', done => {
const options = {repoName: 'my-repo'};
expected = Object.assign(expected, {
totalLength: 1,
projectPageShown: true,
});
testAdminLinks(account, options, expected, done);
});
test('with plugin generated links', done => {
let options;
const generatedLinks = [
{text: 'internal link text', url: '/internal/link/url'},
{text: 'external link text', url: 'http://external/link/url'},
];
menuLinkStub.returns(generatedLinks);
expected = Object.assign(expected, {
totalLength: 3,
projectPageShown: false,
pluginGeneratedLinks: generatedLinks,
});
testAdminLinks(account, options, expected, done);
});
});
suite('no plugin capability logged in', () => {
const account = {
name: 'test-user',
};
let expected;
setup(() => {
expected = {
totalLength: 2,
pluginListShown: false,
};
capabilityStub.returns(Promise.resolve({}));
});
test('without a specific project or group', done => {
let options;
expected = Object.assign(expected, {
projectPageShown: false,
groupListShown: true,
groupPageShown: false,
});
testAdminLinks(account, options, expected, done);
});
test('with a repo', done => {
const account = {
name: 'test-user',
};
const options = {repoName: 'my-repo'};
expected = Object.assign(expected, {
projectPageShown: true,
groupListShown: true,
groupPageShown: false,
});
testAdminLinks(account, options, expected, done);
});
});
suite('view plugin capability logged in', () => {
const account = {
name: 'test-user',
};
let expected;
setup(() => {
capabilityStub.returns(Promise.resolve({viewPlugins: true}));
expected = {
totalLength: 3,
groupListShown: true,
pluginListShown: true,
};
});
test('without a specific repo or group', done => {
let options;
expected = Object.assign(expected, {
projectPageShown: false,
groupPageShown: false,
});
testAdminLinks(account, options, expected, done);
});
test('with a repo', done => {
const options = {repoName: 'my-repo'};
expected = Object.assign(expected, {
projectPageShown: true,
groupPageShown: false,
});
testAdminLinks(account, options, expected, done);
});
test('admin with internal group', done => {
const options = {
groupId: 'a15262',
groupName: 'my-group',
groupIsInternal: true,
isAdmin: true,
groupOwner: false,
};
expected = Object.assign(expected, {
projectPageShown: false,
groupPageShown: true,
groupSubpageLength: 2,
});
testAdminLinks(account, options, expected, done);
});
test('group owner with internal group', done => {
const options = {
groupId: 'a15262',
groupName: 'my-group',
groupIsInternal: true,
isAdmin: false,
groupOwner: true,
};
expected = Object.assign(expected, {
projectPageShown: false,
groupPageShown: true,
groupSubpageLength: 2,
});
testAdminLinks(account, options, expected, done);
});
test('non owner or admin with internal group', done => {
const options = {
groupId: 'a15262',
groupName: 'my-group',
groupIsInternal: true,
isAdmin: false,
groupOwner: false,
};
expected = Object.assign(expected, {
projectPageShown: false,
groupPageShown: true,
groupSubpageLength: 1,
});
testAdminLinks(account, options, expected, done);
});
test('admin with external group', done => {
const options = {
groupId: 'a15262',
groupName: 'my-group',
groupIsInternal: false,
isAdmin: true,
groupOwner: true,
};
expected = Object.assign(expected, {
projectPageShown: false,
groupPageShown: true,
groupSubpageLength: 0,
});
testAdminLinks(account, options, expected, done);
});
});
suite('view plugin screen with plugin capability', () => {
const account = {
name: 'test-user',
};
let expected;
setup(() => {
capabilityStub.returns(Promise.resolve({pluginCapability: true}));
expected = {};
});
test('with plugin with capabilities', done => {
let options;
const generatedLinks = [
{text: 'without capability', url: '/without'},
{text: 'with capability',
url: '/with',
capability: 'pluginCapability'},
];
menuLinkStub.returns(generatedLinks);
expected = Object.assign(expected, {
totalLength: 4,
pluginGeneratedLinks: generatedLinks,
});
testAdminLinks(account, options, expected, done);
});
});
suite('view plugin screen without plugin capability', () => {
const account = {
name: 'test-user',
};
let expected;
setup(() => {
capabilityStub.returns(Promise.resolve({}));
expected = {};
});
test('with plugin with capabilities', done => {
let options;
const generatedLinks = [
{text: 'without capability', url: '/without'},
{text: 'with capability',
url: '/with',
capability: 'pluginCapability'},
];
menuLinkStub.returns(generatedLinks);
expected = Object.assign(expected, {
totalLength: 3,
pluginGeneratedLinks: [generatedLinks[0]],
});
testAdminLinks(account, options, expected, done);
});
});
});
</script>

View File

@ -1,20 +1,19 @@
<!--
@license
Copyright (C) 2016 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<script>
/**
* @license
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function(window) {
'use strict';
@ -105,4 +104,3 @@ limitations under the License.
};
}
})(window);
</script>

View File

@ -19,13 +19,13 @@ limitations under the License.
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>keyboard-shortcut-behavior</title>
<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/bower_components/web-component-tester/browser.js"></script>
<script src="../../test/test-pre-setup.js"></script>
<link rel="import" href="../../test/common-test-setup.html"/>
<link rel="import" href="gr-change-table-behavior.html">
<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/components/wct-browser-legacy/browser.js"></script>
<script type="module" src="../../test/test-pre-setup.js"></script>
<script type="module" src="../../test/common-test-setup.js"></script>
<script type="module" src="./gr-change-table-behavior.js"></script>
<test-fixture id="basic">
<template>
@ -41,87 +41,90 @@ limitations under the License.
</template>
</test-fixture>
<script>
suite('gr-change-table-behavior tests', async () => {
await readyToTest();
let element;
// eslint-disable-next-line no-unused-vars
let overlay;
<script type="module">
import '../../test/test-pre-setup.js';
import '../../test/common-test-setup.js';
import './gr-change-table-behavior.js';
import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
suite('gr-change-table-behavior tests', () => {
let element;
// eslint-disable-next-line no-unused-vars
let overlay;
suiteSetup(() => {
// Define a Polymer element that uses this behavior.
Polymer({
is: 'test-element',
behaviors: [Gerrit.ChangeTableBehavior],
});
});
setup(() => {
element = fixture('basic');
overlay = fixture('within-overlay');
});
test('getComplementColumns', () => {
let columns = [
'Subject',
'Status',
'Owner',
'Assignee',
'Repo',
'Branch',
'Updated',
'Size',
];
assert.deepEqual(element.getComplementColumns(columns), []);
columns = [
'Subject',
'Status',
'Assignee',
'Repo',
'Branch',
'Size',
];
assert.deepEqual(element.getComplementColumns(columns),
['Owner', 'Updated']);
});
test('isColumnHidden', () => {
const columnToCheck = 'Repo';
let columnsToDisplay = [
'Subject',
'Status',
'Owner',
'Assignee',
'Repo',
'Branch',
'Updated',
'Size',
];
assert.isFalse(element.isColumnHidden(columnToCheck, columnsToDisplay));
columnsToDisplay = [
'Subject',
'Status',
'Owner',
'Assignee',
'Branch',
'Updated',
'Size',
];
assert.isTrue(element.isColumnHidden(columnToCheck, columnsToDisplay));
});
test('getVisibleColumns maps Project to Repo', () => {
const columns = [
'Subject',
'Status',
'Owner',
];
assert.deepEqual(element.getVisibleColumns(columns), columns.slice(0));
assert.deepEqual(
element.getVisibleColumns(columns.concat(['Project'])),
columns.slice(0).concat(['Repo']));
suiteSetup(() => {
// Define a Polymer element that uses this behavior.
Polymer({
is: 'test-element',
behaviors: [Gerrit.ChangeTableBehavior],
});
});
setup(() => {
element = fixture('basic');
overlay = fixture('within-overlay');
});
test('getComplementColumns', () => {
let columns = [
'Subject',
'Status',
'Owner',
'Assignee',
'Repo',
'Branch',
'Updated',
'Size',
];
assert.deepEqual(element.getComplementColumns(columns), []);
columns = [
'Subject',
'Status',
'Assignee',
'Repo',
'Branch',
'Size',
];
assert.deepEqual(element.getComplementColumns(columns),
['Owner', 'Updated']);
});
test('isColumnHidden', () => {
const columnToCheck = 'Repo';
let columnsToDisplay = [
'Subject',
'Status',
'Owner',
'Assignee',
'Repo',
'Branch',
'Updated',
'Size',
];
assert.isFalse(element.isColumnHidden(columnToCheck, columnsToDisplay));
columnsToDisplay = [
'Subject',
'Status',
'Owner',
'Assignee',
'Branch',
'Updated',
'Size',
];
assert.isTrue(element.isColumnHidden(columnToCheck, columnsToDisplay));
});
test('getVisibleColumns maps Project to Repo', () => {
const columns = [
'Subject',
'Status',
'Owner',
];
assert.deepEqual(element.getVisibleColumns(columns), columns.slice(0));
assert.deepEqual(
element.getVisibleColumns(columns.concat(['Project'])),
columns.slice(0).concat(['Repo']));
});
});
</script>

View File

@ -1,23 +1,21 @@
<!--
@license
Copyright (C) 2017 The Android Open Source Project
/**
* @license
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import '../../scripts/gr-display-name-utils/gr-display-name-utils.js';
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<script src="../../scripts/gr-display-name-utils/gr-display-name-utils.js"></script>
<script>
(function(window) {
'use strict';
@ -57,4 +55,3 @@ limitations under the License.
};
}
})(window);
</script>

View File

@ -19,13 +19,13 @@ limitations under the License.
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-display-name-behavior</title>
<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/bower_components/web-component-tester/browser.js"></script>
<script src="../../test/test-pre-setup.js"></script>
<link rel="import" href="../../test/common-test-setup.html"/>
<link rel="import" href="gr-display-name-behavior.html">
<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/components/wct-browser-legacy/browser.js"></script>
<script type="module" src="../../test/test-pre-setup.js"></script>
<script type="module" src="../../test/common-test-setup.js"></script>
<script type="module" src="./gr-display-name-behavior.js"></script>
<test-fixture id="basic">
<template>
@ -33,69 +33,72 @@ limitations under the License.
</template>
</test-fixture>
<script>
suite('gr-display-name-behavior tests', async () => {
await readyToTest();
let element;
// eslint-disable-next-line no-unused-vars
const config = {
user: {
anonymous_coward_name: 'Anonymous Coward',
},
};
<script type="module">
import '../../test/test-pre-setup.js';
import '../../test/common-test-setup.js';
import './gr-display-name-behavior.js';
import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
suite('gr-display-name-behavior tests', () => {
let element;
// eslint-disable-next-line no-unused-vars
const config = {
user: {
anonymous_coward_name: 'Anonymous Coward',
},
};
suiteSetup(() => {
// Define a Polymer element that uses this behavior.
Polymer({
is: 'test-element-anon',
behaviors: [
Gerrit.DisplayNameBehavior,
],
});
});
setup(() => {
element = fixture('basic');
});
test('getUserName name only', () => {
const account = {
name: 'test-name',
};
assert.deepEqual(element.getUserName(config, account, true), 'test-name');
});
test('getUserName username only', () => {
const account = {
username: 'test-user',
};
assert.deepEqual(element.getUserName(config, account, true), 'test-user');
});
test('getUserName email only', () => {
const account = {
email: 'test-user@test-url.com',
};
assert.deepEqual(element.getUserName(config, account, true),
'test-user@test-url.com');
});
test('getUserName returns not Anonymous Coward as the anon name', () => {
assert.deepEqual(element.getUserName(config, null, true), 'Anonymous');
});
test('getUserName for the config returning the anon name', () => {
const config = {
user: {
anonymous_coward_name: 'Test Anon',
},
};
assert.deepEqual(element.getUserName(config, null, true), 'Test Anon');
});
test('getGroupDisplayName', () => {
assert.equal(element.getGroupDisplayName({name: 'Some user name'}),
'Some user name (group)');
suiteSetup(() => {
// Define a Polymer element that uses this behavior.
Polymer({
is: 'test-element-anon',
behaviors: [
Gerrit.DisplayNameBehavior,
],
});
});
setup(() => {
element = fixture('basic');
});
test('getUserName name only', () => {
const account = {
name: 'test-name',
};
assert.deepEqual(element.getUserName(config, account, true), 'test-name');
});
test('getUserName username only', () => {
const account = {
username: 'test-user',
};
assert.deepEqual(element.getUserName(config, account, true), 'test-user');
});
test('getUserName email only', () => {
const account = {
email: 'test-user@test-url.com',
};
assert.deepEqual(element.getUserName(config, account, true),
'test-user@test-url.com');
});
test('getUserName returns not Anonymous Coward as the anon name', () => {
assert.deepEqual(element.getUserName(config, null, true), 'Anonymous');
});
test('getUserName for the config returning the anon name', () => {
const config = {
user: {
anonymous_coward_name: 'Test Anon',
},
};
assert.deepEqual(element.getUserName(config, null, true), 'Test Anon');
});
test('getGroupDisplayName', () => {
assert.equal(element.getGroupDisplayName({name: 'Some user name'}),
'Some user name (group)');
});
});
</script>

View File

@ -1,22 +1,22 @@
<!--
@license
Copyright (C) 2017 The Android Open Source Project
/**
* @license
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import '../base-url-behavior/base-url-behavior.js';
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<link rel="import" href="../base-url-behavior/base-url-behavior.html">
<link rel="import" href="../gr-url-encoding-behavior/gr-url-encoding-behavior.html">
<script>
import '../gr-url-encoding-behavior/gr-url-encoding-behavior.js';
(function(window) {
'use strict';
@ -80,5 +80,3 @@ limitations under the License.
};
}
})(window);
</script>

View File

@ -19,13 +19,13 @@ limitations under the License.
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>keyboard-shortcut-behavior</title>
<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/bower_components/web-component-tester/browser.js"></script>
<script src="../../test/test-pre-setup.js"></script>
<link rel="import" href="../../test/common-test-setup.html"/>
<link rel="import" href="gr-list-view-behavior.html">
<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/components/wct-browser-legacy/browser.js"></script>
<script type="module" src="../../test/test-pre-setup.js"></script>
<script type="module" src="../../test/common-test-setup.js"></script>
<script type="module" src="./gr-list-view-behavior.js"></script>
<test-fixture id="basic">
<template>
@ -33,62 +33,65 @@ limitations under the License.
</template>
</test-fixture>
<script>
suite('gr-list-view-behavior tests', async () => {
await readyToTest();
let element;
// eslint-disable-next-line no-unused-vars
let overlay;
<script type="module">
import '../../test/test-pre-setup.js';
import '../../test/common-test-setup.js';
import './gr-list-view-behavior.js';
import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
suite('gr-list-view-behavior tests', () => {
let element;
// eslint-disable-next-line no-unused-vars
let overlay;
suiteSetup(() => {
// Define a Polymer element that uses this behavior.
Polymer({
is: 'test-element',
behaviors: [Gerrit.ListViewBehavior],
});
});
setup(() => {
element = fixture('basic');
});
test('computeLoadingClass', () => {
assert.equal(element.computeLoadingClass(true), 'loading');
assert.equal(element.computeLoadingClass(false), '');
});
test('computeShownItems', () => {
const myArr = new Array(26);
assert.equal(element.computeShownItems(myArr).length, 25);
});
test('getUrl', () => {
assert.equal(element.getUrl('/path/to/something/', 'item'),
'/path/to/something/item');
assert.equal(element.getUrl('/path/to/something/', 'item%test'),
'/path/to/something/item%2525test');
});
test('getFilterValue', () => {
let params;
assert.equal(element.getFilterValue(params), '');
params = {filter: null};
assert.equal(element.getFilterValue(params), '');
params = {filter: 'test'};
assert.equal(element.getFilterValue(params), 'test');
});
test('getOffsetValue', () => {
let params;
assert.equal(element.getOffsetValue(params), 0);
params = {offset: null};
assert.equal(element.getOffsetValue(params), 0);
params = {offset: 1};
assert.equal(element.getOffsetValue(params), 1);
suiteSetup(() => {
// Define a Polymer element that uses this behavior.
Polymer({
is: 'test-element',
behaviors: [Gerrit.ListViewBehavior],
});
});
setup(() => {
element = fixture('basic');
});
test('computeLoadingClass', () => {
assert.equal(element.computeLoadingClass(true), 'loading');
assert.equal(element.computeLoadingClass(false), '');
});
test('computeShownItems', () => {
const myArr = new Array(26);
assert.equal(element.computeShownItems(myArr).length, 25);
});
test('getUrl', () => {
assert.equal(element.getUrl('/path/to/something/', 'item'),
'/path/to/something/item');
assert.equal(element.getUrl('/path/to/something/', 'item%test'),
'/path/to/something/item%2525test');
});
test('getFilterValue', () => {
let params;
assert.equal(element.getFilterValue(params), '');
params = {filter: null};
assert.equal(element.getFilterValue(params), '');
params = {filter: 'test'};
assert.equal(element.getFilterValue(params), 'test');
});
test('getOffsetValue', () => {
let params;
assert.equal(element.getOffsetValue(params), 0);
params = {offset: null};
assert.equal(element.getOffsetValue(params), 0);
params = {offset: 1};
assert.equal(element.getOffsetValue(params), 1);
});
});
</script>

View File

@ -1,20 +1,19 @@
<!--
@license
Copyright (C) 2016 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<script>
/**
* @license
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function(window) {
'use strict';
@ -298,4 +297,3 @@ limitations under the License.
};
}
})(window);
</script>

View File

@ -15,313 +15,315 @@ See the License for the specific language governing permissions and
limitations under the License.
-->
<!-- Polymer included for the html import polyfill. -->
<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/bower_components/web-component-tester/browser.js"></script>
<script src="../../test/test-pre-setup.js"></script>
<link rel="import" href="../../test/common-test-setup.html"/>
<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/components/wct-browser-legacy/browser.js"></script>
<script type="module" src="../../test/test-pre-setup.js"></script>
<script type="module" src="../../test/common-test-setup.js"></script>
<title>gr-patch-set-behavior</title>
<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<link rel="import" href="gr-patch-set-behavior.html">
<script type="module" src="./gr-patch-set-behavior.js"></script>
<script>
suite('gr-patch-set-behavior tests', async () => {
await readyToTest();
test('getRevisionByPatchNum', () => {
const get = Gerrit.PatchSetBehavior.getRevisionByPatchNum;
const revisions = [
{_number: 0},
{_number: 1},
{_number: 2},
];
assert.deepEqual(get(revisions, '1'), revisions[1]);
assert.deepEqual(get(revisions, 2), revisions[2]);
assert.equal(get(revisions, '3'), undefined);
});
test('fetchChangeUpdates on latest', done => {
const knownChange = {
revisions: {
sha1: {description: 'patch 1', _number: 1},
sha2: {description: 'patch 2', _number: 2},
},
status: 'NEW',
messages: [],
};
const mockRestApi = {
getChangeDetail() {
return Promise.resolve(knownChange);
},
};
Gerrit.PatchSetBehavior.fetchChangeUpdates(knownChange, mockRestApi)
.then(result => {
assert.isTrue(result.isLatest);
assert.isNotOk(result.newStatus);
assert.isFalse(result.newMessages);
done();
});
});
test('fetchChangeUpdates not on latest', done => {
const knownChange = {
revisions: {
sha1: {description: 'patch 1', _number: 1},
sha2: {description: 'patch 2', _number: 2},
},
status: 'NEW',
messages: [],
};
const actualChange = {
revisions: {
sha1: {description: 'patch 1', _number: 1},
sha2: {description: 'patch 2', _number: 2},
sha3: {description: 'patch 3', _number: 3},
},
status: 'NEW',
messages: [],
};
const mockRestApi = {
getChangeDetail() {
return Promise.resolve(actualChange);
},
};
Gerrit.PatchSetBehavior.fetchChangeUpdates(knownChange, mockRestApi)
.then(result => {
assert.isFalse(result.isLatest);
assert.isNotOk(result.newStatus);
assert.isFalse(result.newMessages);
done();
});
});
test('fetchChangeUpdates new status', done => {
const knownChange = {
revisions: {
sha1: {description: 'patch 1', _number: 1},
sha2: {description: 'patch 2', _number: 2},
},
status: 'NEW',
messages: [],
};
const actualChange = {
revisions: {
sha1: {description: 'patch 1', _number: 1},
sha2: {description: 'patch 2', _number: 2},
},
status: 'MERGED',
messages: [],
};
const mockRestApi = {
getChangeDetail() {
return Promise.resolve(actualChange);
},
};
Gerrit.PatchSetBehavior.fetchChangeUpdates(knownChange, mockRestApi)
.then(result => {
assert.isTrue(result.isLatest);
assert.equal(result.newStatus, 'MERGED');
assert.isFalse(result.newMessages);
done();
});
});
test('fetchChangeUpdates new messages', done => {
const knownChange = {
revisions: {
sha1: {description: 'patch 1', _number: 1},
sha2: {description: 'patch 2', _number: 2},
},
status: 'NEW',
messages: [],
};
const actualChange = {
revisions: {
sha1: {description: 'patch 1', _number: 1},
sha2: {description: 'patch 2', _number: 2},
},
status: 'NEW',
messages: [{message: 'blah blah'}],
};
const mockRestApi = {
getChangeDetail() {
return Promise.resolve(actualChange);
},
};
Gerrit.PatchSetBehavior.fetchChangeUpdates(knownChange, mockRestApi)
.then(result => {
assert.isTrue(result.isLatest);
assert.isNotOk(result.newStatus);
assert.isTrue(result.newMessages);
done();
});
});
test('_computeWipForPatchSets', () => {
// Compute patch sets for a given timeline on a change. The initial WIP
// property of the change can be true or false. The map of tags by
// revision is keyed by patch set number. Each value is a list of change
// message tags in the order that they occurred in the timeline. These
// indicate actions that modify the WIP property of the change and/or
// create new patch sets.
//
// Returns the actual results with an assertWip method that can be used
// to compare against an expected value for a particular patch set.
const compute = (initialWip, tagsByRevision) => {
const change = {
messages: [],
work_in_progress: initialWip,
};
const revs = Object.keys(tagsByRevision).sort((a, b) => a - b);
for (const rev of revs) {
for (const tag of tagsByRevision[rev]) {
change.messages.push({
tag,
_revision_number: rev,
});
}
}
let patchNums = revs.map(rev => { return {num: rev}; });
patchNums = Gerrit.PatchSetBehavior._computeWipForPatchSets(
change, patchNums);
const actualWipsByRevision = {};
for (const patchNum of patchNums) {
actualWipsByRevision[patchNum.num] = patchNum.wip;
}
const verifier = {
assertWip(revision, expectedWip) {
const patchNum = patchNums.find(patchNum => patchNum.num == revision);
if (!patchNum) {
assert.fail('revision ' + revision + ' not found');
}
assert.equal(patchNum.wip, expectedWip,
'wip state for ' + revision + ' is ' +
patchNum.wip + '; expected ' + expectedWip);
return verifier;
},
};
return verifier;
};
compute(false, {1: ['upload']}).assertWip(1, false);
compute(true, {1: ['upload']}).assertWip(1, true);
const setWip = 'autogenerated:gerrit:setWorkInProgress';
const uploadInWip = 'autogenerated:gerrit:newWipPatchSet';
const clearWip = 'autogenerated:gerrit:setReadyForReview';
compute(false, {
1: ['upload', setWip],
2: ['upload'],
3: ['upload', clearWip],
4: ['upload', setWip],
}).assertWip(1, false) // Change was created with PS1 ready for review
.assertWip(2, true) // PS2 was uploaded during WIP
.assertWip(3, false) // PS3 was marked ready for review after upload
.assertWip(4, false); // PS4 was uploaded ready for review
compute(false, {
1: [uploadInWip, null, 'addReviewer'],
2: ['upload'],
3: ['upload', clearWip, setWip],
4: ['upload'],
5: ['upload', clearWip],
6: [uploadInWip],
}).assertWip(1, true) // Change was created in WIP
.assertWip(2, true) // PS2 was uploaded during WIP
.assertWip(3, false) // PS3 was marked ready for review
.assertWip(4, true) // PS4 was uploaded during WIP
.assertWip(5, false) // PS5 was marked ready for review
.assertWip(6, true); // PS6 was uploaded with WIP option
});
test('patchNumEquals', () => {
const equals = Gerrit.PatchSetBehavior.patchNumEquals;
assert.isFalse(equals('edit', 'PARENT'));
assert.isFalse(equals('edit', NaN));
assert.isFalse(equals(1, '2'));
assert.isTrue(equals(1, '1'));
assert.isTrue(equals(1, 1));
assert.isTrue(equals('edit', 'edit'));
assert.isTrue(equals('PARENT', 'PARENT'));
});
test('isMergeParent', () => {
const isParent = Gerrit.PatchSetBehavior.isMergeParent;
assert.isFalse(isParent(1));
assert.isFalse(isParent(4321));
assert.isFalse(isParent('52'));
assert.isFalse(isParent('edit'));
assert.isFalse(isParent('PARENT'));
assert.isFalse(isParent(0));
assert.isTrue(isParent(-23));
assert.isTrue(isParent(-1));
assert.isTrue(isParent('-42'));
});
test('findEditParentRevision', () => {
const findParent = Gerrit.PatchSetBehavior.findEditParentRevision;
let revisions = [
{_number: 0},
{_number: 1},
{_number: 2},
];
assert.strictEqual(findParent(revisions), null);
revisions = [...revisions, {_number: 'edit', basePatchNum: 3}];
assert.strictEqual(findParent(revisions), null);
revisions = [...revisions, {_number: 3}];
assert.deepEqual(findParent(revisions), {_number: 3});
});
test('findEditParentPatchNum', () => {
const findNum = Gerrit.PatchSetBehavior.findEditParentPatchNum;
let revisions = [
{_number: 0},
{_number: 1},
{_number: 2},
];
assert.equal(findNum(revisions), -1);
revisions =
[...revisions, {_number: 'edit', basePatchNum: 3}, {_number: 3}];
assert.deepEqual(findNum(revisions), 3);
});
test('sortRevisions', () => {
const sort = Gerrit.PatchSetBehavior.sortRevisions;
const revisions = [
{_number: 0},
{_number: 2},
{_number: 1},
];
const sorted = [
{_number: 2},
{_number: 1},
{_number: 0},
];
assert.deepEqual(sort(revisions), sorted);
// Edit patchset should follow directly after its basePatchNum.
revisions.push({_number: 'edit', basePatchNum: 2});
sorted.unshift({_number: 'edit', basePatchNum: 2});
assert.deepEqual(sort(revisions), sorted);
revisions[0].basePatchNum = 0;
const edit = sorted.shift();
edit.basePatchNum = 0;
// Edit patchset should be at index 2.
sorted.splice(2, 0, edit);
assert.deepEqual(sort(revisions), sorted);
});
test('getParentIndex', () => {
assert.equal(Gerrit.PatchSetBehavior.getParentIndex('-13'), 13);
assert.equal(Gerrit.PatchSetBehavior.getParentIndex(-4), 4);
});
<script type="module">
import '../../test/test-pre-setup.js';
import '../../test/common-test-setup.js';
import './gr-patch-set-behavior.js';
suite('gr-patch-set-behavior tests', () => {
test('getRevisionByPatchNum', () => {
const get = Gerrit.PatchSetBehavior.getRevisionByPatchNum;
const revisions = [
{_number: 0},
{_number: 1},
{_number: 2},
];
assert.deepEqual(get(revisions, '1'), revisions[1]);
assert.deepEqual(get(revisions, 2), revisions[2]);
assert.equal(get(revisions, '3'), undefined);
});
test('fetchChangeUpdates on latest', done => {
const knownChange = {
revisions: {
sha1: {description: 'patch 1', _number: 1},
sha2: {description: 'patch 2', _number: 2},
},
status: 'NEW',
messages: [],
};
const mockRestApi = {
getChangeDetail() {
return Promise.resolve(knownChange);
},
};
Gerrit.PatchSetBehavior.fetchChangeUpdates(knownChange, mockRestApi)
.then(result => {
assert.isTrue(result.isLatest);
assert.isNotOk(result.newStatus);
assert.isFalse(result.newMessages);
done();
});
});
test('fetchChangeUpdates not on latest', done => {
const knownChange = {
revisions: {
sha1: {description: 'patch 1', _number: 1},
sha2: {description: 'patch 2', _number: 2},
},
status: 'NEW',
messages: [],
};
const actualChange = {
revisions: {
sha1: {description: 'patch 1', _number: 1},
sha2: {description: 'patch 2', _number: 2},
sha3: {description: 'patch 3', _number: 3},
},
status: 'NEW',
messages: [],
};
const mockRestApi = {
getChangeDetail() {
return Promise.resolve(actualChange);
},
};
Gerrit.PatchSetBehavior.fetchChangeUpdates(knownChange, mockRestApi)
.then(result => {
assert.isFalse(result.isLatest);
assert.isNotOk(result.newStatus);
assert.isFalse(result.newMessages);
done();
});
});
test('fetchChangeUpdates new status', done => {
const knownChange = {
revisions: {
sha1: {description: 'patch 1', _number: 1},
sha2: {description: 'patch 2', _number: 2},
},
status: 'NEW',
messages: [],
};
const actualChange = {
revisions: {
sha1: {description: 'patch 1', _number: 1},
sha2: {description: 'patch 2', _number: 2},
},
status: 'MERGED',
messages: [],
};
const mockRestApi = {
getChangeDetail() {
return Promise.resolve(actualChange);
},
};
Gerrit.PatchSetBehavior.fetchChangeUpdates(knownChange, mockRestApi)
.then(result => {
assert.isTrue(result.isLatest);
assert.equal(result.newStatus, 'MERGED');
assert.isFalse(result.newMessages);
done();
});
});
test('fetchChangeUpdates new messages', done => {
const knownChange = {
revisions: {
sha1: {description: 'patch 1', _number: 1},
sha2: {description: 'patch 2', _number: 2},
},
status: 'NEW',
messages: [],
};
const actualChange = {
revisions: {
sha1: {description: 'patch 1', _number: 1},
sha2: {description: 'patch 2', _number: 2},
},
status: 'NEW',
messages: [{message: 'blah blah'}],
};
const mockRestApi = {
getChangeDetail() {
return Promise.resolve(actualChange);
},
};
Gerrit.PatchSetBehavior.fetchChangeUpdates(knownChange, mockRestApi)
.then(result => {
assert.isTrue(result.isLatest);
assert.isNotOk(result.newStatus);
assert.isTrue(result.newMessages);
done();
});
});
test('_computeWipForPatchSets', () => {
// Compute patch sets for a given timeline on a change. The initial WIP
// property of the change can be true or false. The map of tags by
// revision is keyed by patch set number. Each value is a list of change
// message tags in the order that they occurred in the timeline. These
// indicate actions that modify the WIP property of the change and/or
// create new patch sets.
//
// Returns the actual results with an assertWip method that can be used
// to compare against an expected value for a particular patch set.
const compute = (initialWip, tagsByRevision) => {
const change = {
messages: [],
work_in_progress: initialWip,
};
const revs = Object.keys(tagsByRevision).sort((a, b) => a - b);
for (const rev of revs) {
for (const tag of tagsByRevision[rev]) {
change.messages.push({
tag,
_revision_number: rev,
});
}
}
let patchNums = revs.map(rev => { return {num: rev}; });
patchNums = Gerrit.PatchSetBehavior._computeWipForPatchSets(
change, patchNums);
const actualWipsByRevision = {};
for (const patchNum of patchNums) {
actualWipsByRevision[patchNum.num] = patchNum.wip;
}
const verifier = {
assertWip(revision, expectedWip) {
const patchNum = patchNums.find(patchNum => patchNum.num == revision);
if (!patchNum) {
assert.fail('revision ' + revision + ' not found');
}
assert.equal(patchNum.wip, expectedWip,
'wip state for ' + revision + ' is ' +
patchNum.wip + '; expected ' + expectedWip);
return verifier;
},
};
return verifier;
};
compute(false, {1: ['upload']}).assertWip(1, false);
compute(true, {1: ['upload']}).assertWip(1, true);
const setWip = 'autogenerated:gerrit:setWorkInProgress';
const uploadInWip = 'autogenerated:gerrit:newWipPatchSet';
const clearWip = 'autogenerated:gerrit:setReadyForReview';
compute(false, {
1: ['upload', setWip],
2: ['upload'],
3: ['upload', clearWip],
4: ['upload', setWip],
}).assertWip(1, false) // Change was created with PS1 ready for review
.assertWip(2, true) // PS2 was uploaded during WIP
.assertWip(3, false) // PS3 was marked ready for review after upload
.assertWip(4, false); // PS4 was uploaded ready for review
compute(false, {
1: [uploadInWip, null, 'addReviewer'],
2: ['upload'],
3: ['upload', clearWip, setWip],
4: ['upload'],
5: ['upload', clearWip],
6: [uploadInWip],
}).assertWip(1, true) // Change was created in WIP
.assertWip(2, true) // PS2 was uploaded during WIP
.assertWip(3, false) // PS3 was marked ready for review
.assertWip(4, true) // PS4 was uploaded during WIP
.assertWip(5, false) // PS5 was marked ready for review
.assertWip(6, true); // PS6 was uploaded with WIP option
});
test('patchNumEquals', () => {
const equals = Gerrit.PatchSetBehavior.patchNumEquals;
assert.isFalse(equals('edit', 'PARENT'));
assert.isFalse(equals('edit', NaN));
assert.isFalse(equals(1, '2'));
assert.isTrue(equals(1, '1'));
assert.isTrue(equals(1, 1));
assert.isTrue(equals('edit', 'edit'));
assert.isTrue(equals('PARENT', 'PARENT'));
});
test('isMergeParent', () => {
const isParent = Gerrit.PatchSetBehavior.isMergeParent;
assert.isFalse(isParent(1));
assert.isFalse(isParent(4321));
assert.isFalse(isParent('52'));
assert.isFalse(isParent('edit'));
assert.isFalse(isParent('PARENT'));
assert.isFalse(isParent(0));
assert.isTrue(isParent(-23));
assert.isTrue(isParent(-1));
assert.isTrue(isParent('-42'));
});
test('findEditParentRevision', () => {
const findParent = Gerrit.PatchSetBehavior.findEditParentRevision;
let revisions = [
{_number: 0},
{_number: 1},
{_number: 2},
];
assert.strictEqual(findParent(revisions), null);
revisions = [...revisions, {_number: 'edit', basePatchNum: 3}];
assert.strictEqual(findParent(revisions), null);
revisions = [...revisions, {_number: 3}];
assert.deepEqual(findParent(revisions), {_number: 3});
});
test('findEditParentPatchNum', () => {
const findNum = Gerrit.PatchSetBehavior.findEditParentPatchNum;
let revisions = [
{_number: 0},
{_number: 1},
{_number: 2},
];
assert.equal(findNum(revisions), -1);
revisions =
[...revisions, {_number: 'edit', basePatchNum: 3}, {_number: 3}];
assert.deepEqual(findNum(revisions), 3);
});
test('sortRevisions', () => {
const sort = Gerrit.PatchSetBehavior.sortRevisions;
const revisions = [
{_number: 0},
{_number: 2},
{_number: 1},
];
const sorted = [
{_number: 2},
{_number: 1},
{_number: 0},
];
assert.deepEqual(sort(revisions), sorted);
// Edit patchset should follow directly after its basePatchNum.
revisions.push({_number: 'edit', basePatchNum: 2});
sorted.unshift({_number: 'edit', basePatchNum: 2});
assert.deepEqual(sort(revisions), sorted);
revisions[0].basePatchNum = 0;
const edit = sorted.shift();
edit.basePatchNum = 0;
// Edit patchset should be at index 2.
sorted.splice(2, 0, edit);
assert.deepEqual(sort(revisions), sorted);
});
test('getParentIndex', () => {
assert.equal(Gerrit.PatchSetBehavior.getParentIndex('-13'), 13);
assert.equal(Gerrit.PatchSetBehavior.getParentIndex(-4), 4);
});
});
</script>

View File

@ -1,21 +1,21 @@
<!--
@license
Copyright (C) 2016 The Android Open Source Project
/**
* @license
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import '../../scripts/util.js';
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<script src="../../scripts/util.js"></script>
<script>
(function(window) {
'use strict';
@ -137,4 +137,3 @@ limitations under the License.
};
}
})(window);
</script>

View File

@ -15,89 +15,91 @@ See the License for the specific language governing permissions and
limitations under the License.
-->
<!-- Polymer included for the html import polyfill. -->
<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/bower_components/web-component-tester/browser.js"></script>
<script src="../../test/test-pre-setup.js"></script>
<link rel="import" href="../../test/common-test-setup.html"/>
<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/components/wct-browser-legacy/browser.js"></script>
<script type="module" src="../../test/test-pre-setup.js"></script>
<script type="module" src="../../test/common-test-setup.js"></script>
<title>gr-path-list-behavior</title>
<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<link rel="import" href="gr-path-list-behavior.html">
<script type="module" src="./gr-path-list-behavior.js"></script>
<script>
suite('gr-path-list-behavior tests', async () => {
await readyToTest();
test('special sort', () => {
const sort = Gerrit.PathListBehavior.specialFilePathCompare;
const testFiles = [
'/a.h',
'/MERGE_LIST',
'/a.cpp',
'/COMMIT_MSG',
'/asdasd',
'/mrPeanutbutter.py',
];
assert.deepEqual(
testFiles.sort(sort),
[
'/COMMIT_MSG',
'/MERGE_LIST',
'/a.h',
'/a.cpp',
'/asdasd',
'/mrPeanutbutter.py',
]);
});
test('file display name', () => {
const name = Gerrit.PathListBehavior.computeDisplayPath;
assert.equal(name('/foo/bar/baz'), '/foo/bar/baz');
assert.equal(name('/foobarbaz'), '/foobarbaz');
assert.equal(name('/COMMIT_MSG'), 'Commit message');
assert.equal(name('/MERGE_LIST'), 'Merge list');
});
test('isMagicPath', () => {
const isMagic = Gerrit.PathListBehavior.isMagicPath;
assert.isFalse(isMagic(undefined));
assert.isFalse(isMagic('/foo.cc'));
assert.isTrue(isMagic('/COMMIT_MSG'));
assert.isTrue(isMagic('/MERGE_LIST'));
});
test('truncatePath with long path should add ellipsis', () => {
const truncatePath = Gerrit.PathListBehavior.truncatePath;
let path = 'level1/level2/level3/level4/file.js';
let shortenedPath = truncatePath(path);
// The expected path is truncated with an ellipsis.
const expectedPath = '\u2026/file.js';
assert.equal(shortenedPath, expectedPath);
path = 'level2/file.js';
shortenedPath = truncatePath(path);
assert.equal(shortenedPath, expectedPath);
});
test('truncatePath with opt_threshold', () => {
const truncatePath = Gerrit.PathListBehavior.truncatePath;
let path = 'level1/level2/level3/level4/file.js';
let shortenedPath = truncatePath(path, 2);
// The expected path is truncated with an ellipsis.
const expectedPath = '\u2026/level4/file.js';
assert.equal(shortenedPath, expectedPath);
path = 'level2/file.js';
shortenedPath = truncatePath(path, 2);
assert.equal(shortenedPath, path);
});
test('truncatePath with short path should not add ellipsis', () => {
const truncatePath = Gerrit.PathListBehavior.truncatePath;
const path = 'file.js';
const expectedPath = 'file.js';
const shortenedPath = truncatePath(path);
assert.equal(shortenedPath, expectedPath);
});
<script type="module">
import '../../test/test-pre-setup.js';
import '../../test/common-test-setup.js';
import './gr-path-list-behavior.js';
suite('gr-path-list-behavior tests', () => {
test('special sort', () => {
const sort = Gerrit.PathListBehavior.specialFilePathCompare;
const testFiles = [
'/a.h',
'/MERGE_LIST',
'/a.cpp',
'/COMMIT_MSG',
'/asdasd',
'/mrPeanutbutter.py',
];
assert.deepEqual(
testFiles.sort(sort),
[
'/COMMIT_MSG',
'/MERGE_LIST',
'/a.h',
'/a.cpp',
'/asdasd',
'/mrPeanutbutter.py',
]);
});
test('file display name', () => {
const name = Gerrit.PathListBehavior.computeDisplayPath;
assert.equal(name('/foo/bar/baz'), '/foo/bar/baz');
assert.equal(name('/foobarbaz'), '/foobarbaz');
assert.equal(name('/COMMIT_MSG'), 'Commit message');
assert.equal(name('/MERGE_LIST'), 'Merge list');
});
test('isMagicPath', () => {
const isMagic = Gerrit.PathListBehavior.isMagicPath;
assert.isFalse(isMagic(undefined));
assert.isFalse(isMagic('/foo.cc'));
assert.isTrue(isMagic('/COMMIT_MSG'));
assert.isTrue(isMagic('/MERGE_LIST'));
});
test('truncatePath with long path should add ellipsis', () => {
const truncatePath = Gerrit.PathListBehavior.truncatePath;
let path = 'level1/level2/level3/level4/file.js';
let shortenedPath = truncatePath(path);
// The expected path is truncated with an ellipsis.
const expectedPath = '\u2026/file.js';
assert.equal(shortenedPath, expectedPath);
path = 'level2/file.js';
shortenedPath = truncatePath(path);
assert.equal(shortenedPath, expectedPath);
});
test('truncatePath with opt_threshold', () => {
const truncatePath = Gerrit.PathListBehavior.truncatePath;
let path = 'level1/level2/level3/level4/file.js';
let shortenedPath = truncatePath(path, 2);
// The expected path is truncated with an ellipsis.
const expectedPath = '\u2026/level4/file.js';
assert.equal(shortenedPath, expectedPath);
path = 'level2/file.js';
shortenedPath = truncatePath(path, 2);
assert.equal(shortenedPath, path);
});
test('truncatePath with short path should not add ellipsis', () => {
const truncatePath = Gerrit.PathListBehavior.truncatePath;
const path = 'file.js';
const expectedPath = 'file.js';
const shortenedPath = truncatePath(path);
assert.equal(shortenedPath, expectedPath);
});
});
</script>

View File

@ -1,20 +1,19 @@
<!--
@license
Copyright (C) 2019 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<script>
/**
* @license
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function(window) {
'use strict';
@ -52,4 +51,3 @@ limitations under the License.
};
}
})(window);
</script>

View File

@ -1,21 +0,0 @@
<!--
@license
Copyright (C) 2016 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<link rel="import" href="/bower_components/polymer/polymer.html">
<link rel="import" href="../../elements/shared/gr-tooltip/gr-tooltip.html">
<script src="../../scripts/rootElement.js"></script>
<script src="gr-tooltip-behavior.js"></script>

View File

@ -14,6 +14,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import '../../scripts/bundled-polymer.js';
import '../../elements/shared/gr-tooltip/gr-tooltip.js';
import '../../scripts/rootElement.js';
import {flush} from '@polymer/polymer/lib/legacy/polymer.dom.js';
(function(window) {
'use strict';
@ -135,7 +141,7 @@
_positionTooltip(tooltip) {
// This flush is needed for tooltips to be positioned correctly in Firefox
// and Safari.
Polymer.dom.flush();
flush();
const rect = this.getBoundingClientRect();
const boxRect = tooltip.getBoundingClientRect();
const parentRect = tooltip.parentElement.getBoundingClientRect();

View File

@ -18,15 +18,20 @@ limitations under the License.
<title>tooltip-behavior</title>
<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/bower_components/web-component-tester/browser.js"></script>
<script src="../../test/test-pre-setup.js"></script>
<link rel="import" href="../../test/common-test-setup.html"/>
<link rel="import" href="gr-tooltip-behavior.html">
<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/components/wct-browser-legacy/browser.js"></script>
<script type="module" src="../../test/test-pre-setup.js"></script>
<script type="module" src="../../test/common-test-setup.js"></script>
<script type="module" src="./gr-tooltip-behavior.js"></script>
<script>void(0);</script>
<script type="module">
import '../../test/test-pre-setup.js';
import '../../test/common-test-setup.js';
import './gr-tooltip-behavior.js';
void(0);
</script>
<test-fixture id="basic">
<template>
@ -34,124 +39,127 @@ limitations under the License.
</template>
</test-fixture>
<script>
suite('gr-tooltip-behavior tests', async () => {
await readyToTest();
let element;
let sandbox;
<script type="module">
import '../../test/test-pre-setup.js';
import '../../test/common-test-setup.js';
import './gr-tooltip-behavior.js';
import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
suite('gr-tooltip-behavior tests', () => {
let element;
let sandbox;
function makeTooltip(tooltipRect, parentRect) {
return {
getBoundingClientRect() { return tooltipRect; },
updateStyles: sinon.stub(),
style: {left: 0, top: 0},
parentElement: {
getBoundingClientRect() { return parentRect; },
},
};
}
function makeTooltip(tooltipRect, parentRect) {
return {
getBoundingClientRect() { return tooltipRect; },
updateStyles: sinon.stub(),
style: {left: 0, top: 0},
parentElement: {
getBoundingClientRect() { return parentRect; },
},
};
}
suiteSetup(() => {
// Define a Polymer element that uses this behavior.
Polymer({
is: 'tooltip-behavior-element',
behaviors: [Gerrit.TooltipBehavior],
});
});
setup(() => {
sandbox = sinon.sandbox.create();
element = fixture('basic');
});
teardown(() => {
sandbox.restore();
});
test('normal position', () => {
sandbox.stub(element, 'getBoundingClientRect', () => {
return {top: 100, left: 100, width: 200};
});
const tooltip = makeTooltip(
{height: 30, width: 50},
{top: 0, left: 0, width: 1000});
element._positionTooltip(tooltip);
assert.isFalse(tooltip.updateStyles.called);
assert.equal(tooltip.style.left, '175px');
assert.equal(tooltip.style.top, '100px');
});
test('left side position', () => {
sandbox.stub(element, 'getBoundingClientRect', () => {
return {top: 100, left: 10, width: 50};
});
const tooltip = makeTooltip(
{height: 30, width: 120},
{top: 0, left: 0, width: 1000});
element._positionTooltip(tooltip);
assert.isTrue(tooltip.updateStyles.called);
const offset = tooltip.updateStyles
.lastCall.args[0]['--gr-tooltip-arrow-center-offset'];
assert.isBelow(parseFloat(offset.replace(/px$/, '')), 0);
assert.equal(tooltip.style.left, '0px');
assert.equal(tooltip.style.top, '100px');
});
test('right side position', () => {
sandbox.stub(element, 'getBoundingClientRect', () => {
return {top: 100, left: 950, width: 50};
});
const tooltip = makeTooltip(
{height: 30, width: 120},
{top: 0, left: 0, width: 1000});
element._positionTooltip(tooltip);
assert.isTrue(tooltip.updateStyles.called);
const offset = tooltip.updateStyles
.lastCall.args[0]['--gr-tooltip-arrow-center-offset'];
assert.isAbove(parseFloat(offset.replace(/px$/, '')), 0);
assert.equal(tooltip.style.left, '915px');
assert.equal(tooltip.style.top, '100px');
});
test('position to bottom', () => {
sandbox.stub(element, 'getBoundingClientRect', () => {
return {top: 100, left: 950, width: 50, height: 50};
});
const tooltip = makeTooltip(
{height: 30, width: 120},
{top: 0, left: 0, width: 1000});
element.positionBelow = true;
element._positionTooltip(tooltip);
assert.isTrue(tooltip.updateStyles.called);
const offset = tooltip.updateStyles
.lastCall.args[0]['--gr-tooltip-arrow-center-offset'];
assert.isAbove(parseFloat(offset.replace(/px$/, '')), 0);
assert.equal(tooltip.style.left, '915px');
assert.equal(tooltip.style.top, '157.2px');
});
test('hides tooltip when detached', () => {
sandbox.stub(element, '_handleHideTooltip');
element.remove();
flushAsynchronousOperations();
assert.isTrue(element._handleHideTooltip.called);
});
test('sets up listeners when has-tooltip is changed', () => {
const addListenerStub = sandbox.stub(element, 'addEventListener');
element.hasTooltip = true;
assert.isTrue(addListenerStub.called);
});
test('clean up listeners when has-tooltip changed to false', () => {
const removeListenerStub = sandbox.stub(element, 'removeEventListener');
element.hasTooltip = true;
element.hasTooltip = false;
assert.isTrue(removeListenerStub.called);
suiteSetup(() => {
// Define a Polymer element that uses this behavior.
Polymer({
is: 'tooltip-behavior-element',
behaviors: [Gerrit.TooltipBehavior],
});
});
setup(() => {
sandbox = sinon.sandbox.create();
element = fixture('basic');
});
teardown(() => {
sandbox.restore();
});
test('normal position', () => {
sandbox.stub(element, 'getBoundingClientRect', () => {
return {top: 100, left: 100, width: 200};
});
const tooltip = makeTooltip(
{height: 30, width: 50},
{top: 0, left: 0, width: 1000});
element._positionTooltip(tooltip);
assert.isFalse(tooltip.updateStyles.called);
assert.equal(tooltip.style.left, '175px');
assert.equal(tooltip.style.top, '100px');
});
test('left side position', () => {
sandbox.stub(element, 'getBoundingClientRect', () => {
return {top: 100, left: 10, width: 50};
});
const tooltip = makeTooltip(
{height: 30, width: 120},
{top: 0, left: 0, width: 1000});
element._positionTooltip(tooltip);
assert.isTrue(tooltip.updateStyles.called);
const offset = tooltip.updateStyles
.lastCall.args[0]['--gr-tooltip-arrow-center-offset'];
assert.isBelow(parseFloat(offset.replace(/px$/, '')), 0);
assert.equal(tooltip.style.left, '0px');
assert.equal(tooltip.style.top, '100px');
});
test('right side position', () => {
sandbox.stub(element, 'getBoundingClientRect', () => {
return {top: 100, left: 950, width: 50};
});
const tooltip = makeTooltip(
{height: 30, width: 120},
{top: 0, left: 0, width: 1000});
element._positionTooltip(tooltip);
assert.isTrue(tooltip.updateStyles.called);
const offset = tooltip.updateStyles
.lastCall.args[0]['--gr-tooltip-arrow-center-offset'];
assert.isAbove(parseFloat(offset.replace(/px$/, '')), 0);
assert.equal(tooltip.style.left, '915px');
assert.equal(tooltip.style.top, '100px');
});
test('position to bottom', () => {
sandbox.stub(element, 'getBoundingClientRect', () => {
return {top: 100, left: 950, width: 50, height: 50};
});
const tooltip = makeTooltip(
{height: 30, width: 120},
{top: 0, left: 0, width: 1000});
element.positionBelow = true;
element._positionTooltip(tooltip);
assert.isTrue(tooltip.updateStyles.called);
const offset = tooltip.updateStyles
.lastCall.args[0]['--gr-tooltip-arrow-center-offset'];
assert.isAbove(parseFloat(offset.replace(/px$/, '')), 0);
assert.equal(tooltip.style.left, '915px');
assert.equal(tooltip.style.top, '157.2px');
});
test('hides tooltip when detached', () => {
sandbox.stub(element, '_handleHideTooltip');
element.remove();
flushAsynchronousOperations();
assert.isTrue(element._handleHideTooltip.called);
});
test('sets up listeners when has-tooltip is changed', () => {
const addListenerStub = sandbox.stub(element, 'addEventListener');
element.hasTooltip = true;
assert.isTrue(addListenerStub.called);
});
test('clean up listeners when has-tooltip changed to false', () => {
const removeListenerStub = sandbox.stub(element, 'removeEventListener');
element.hasTooltip = true;
element.hasTooltip = false;
assert.isTrue(removeListenerStub.called);
});
});
</script>

View File

@ -1,20 +1,19 @@
<!--
@license
Copyright (C) 2016 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<script>
/**
* @license
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function(window) {
'use strict';
@ -73,4 +72,3 @@ limitations under the License.
};
}
})(window);
</script>

View File

@ -18,15 +18,20 @@ limitations under the License.
<title>gr-url-encoding-behavior</title>
<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/bower_components/web-component-tester/browser.js"></script>
<script src="../../test/test-pre-setup.js"></script>
<link rel="import" href="../../test/common-test-setup.html"/>
<link rel="import" href="gr-url-encoding-behavior.html">
<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/components/wct-browser-legacy/browser.js"></script>
<script type="module" src="../../test/test-pre-setup.js"></script>
<script type="module" src="../../test/common-test-setup.js"></script>
<script type="module" src="./gr-url-encoding-behavior.js"></script>
<script>void(0);</script>
<script type="module">
import '../../test/test-pre-setup.js';
import '../../test/common-test-setup.js';
import './gr-url-encoding-behavior.js';
void(0);
</script>
<test-fixture id="basic">
<template>
@ -34,62 +39,65 @@ limitations under the License.
</template>
</test-fixture>
<script>
suite('gr-url-encoding-behavior tests', async () => {
await readyToTest();
let element;
let sandbox;
<script type="module">
import '../../test/test-pre-setup.js';
import '../../test/common-test-setup.js';
import './gr-url-encoding-behavior.js';
import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
suite('gr-url-encoding-behavior tests', () => {
let element;
let sandbox;
suiteSetup(() => {
// Define a Polymer element that uses this behavior.
Polymer({
is: 'test-element',
behaviors: [Gerrit.URLEncodingBehavior],
});
});
setup(() => {
sandbox = sinon.sandbox.create();
element = fixture('basic');
});
teardown(() => {
sandbox.restore();
});
suite('encodeURL', () => {
test('double encodes', () => {
assert.equal(element.encodeURL('abc?123'), 'abc%253F123');
assert.equal(element.encodeURL('def/ghi'), 'def%252Fghi');
assert.equal(element.encodeURL('jkl'), 'jkl');
assert.equal(element.encodeURL(''), '');
});
test('does not convert colons', () => {
assert.equal(element.encodeURL('mno:pqr'), 'mno:pqr');
});
test('converts spaces to +', () => {
assert.equal(element.encodeURL('words with spaces'), 'words+with+spaces');
});
test('does not convert slashes when configured', () => {
assert.equal(element.encodeURL('stu/vwx', true), 'stu/vwx');
});
test('does not convert slashes when configured', () => {
assert.equal(element.encodeURL('stu/vwx', true), 'stu/vwx');
});
});
suite('singleDecodeUrl', () => {
test('single decodes', () => {
assert.equal(element.singleDecodeURL('abc%3Fdef'), 'abc?def');
});
test('converts + to space', () => {
assert.equal(element.singleDecodeURL('ghi+jkl'), 'ghi jkl');
});
suiteSetup(() => {
// Define a Polymer element that uses this behavior.
Polymer({
is: 'test-element',
behaviors: [Gerrit.URLEncodingBehavior],
});
});
setup(() => {
sandbox = sinon.sandbox.create();
element = fixture('basic');
});
teardown(() => {
sandbox.restore();
});
suite('encodeURL', () => {
test('double encodes', () => {
assert.equal(element.encodeURL('abc?123'), 'abc%253F123');
assert.equal(element.encodeURL('def/ghi'), 'def%252Fghi');
assert.equal(element.encodeURL('jkl'), 'jkl');
assert.equal(element.encodeURL(''), '');
});
test('does not convert colons', () => {
assert.equal(element.encodeURL('mno:pqr'), 'mno:pqr');
});
test('converts spaces to +', () => {
assert.equal(element.encodeURL('words with spaces'), 'words+with+spaces');
});
test('does not convert slashes when configured', () => {
assert.equal(element.encodeURL('stu/vwx', true), 'stu/vwx');
});
test('does not convert slashes when configured', () => {
assert.equal(element.encodeURL('stu/vwx', true), 'stu/vwx');
});
});
suite('singleDecodeUrl', () => {
test('single decodes', () => {
assert.equal(element.singleDecodeURL('abc%3Fdef'), 'abc?def');
});
test('converts + to space', () => {
assert.equal(element.singleDecodeURL('ghi+jkl'), 'ghi jkl');
});
});
});
</script>

View File

@ -1,21 +1,20 @@
<!--
@license
Copyright (C) 2016 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<!--
/**
* @license
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
How to Add a Keyboard Shortcut
==============================
@ -95,12 +94,17 @@ by gr-app, and actually implemented by gr-comment-thread.
NOTE: doc-only shortcuts will not be customizable in the same way that other
shortcuts are.
-->
<link rel="import" href="/bower_components/polymer/polymer.html">
<link rel="import" href="/bower_components/iron-a11y-keys-behavior/iron-a11y-keys-behavior.html">
<script src="../../types/polymer-behaviors.js"></script>
*/
/*
FIXME(polymer-modulizer): the above comments were extracted
from HTML and may be out of place here. Review them and
then delete this comment!
*/
import '../../scripts/bundled-polymer.js';
<script>
import {IronA11yKeysBehavior} from '@polymer/iron-a11y-keys-behavior/iron-a11y-keys-behavior.js';
import '../../types/polymer-behaviors.js';
import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
(function(window) {
'use strict';
@ -307,7 +311,7 @@ shortcuts are.
/** @return {!(Event|PolymerDomApi|PolymerEventApi)} */
const getKeyboardEvent = function(e) {
e = Polymer.dom(e.detail ? e.detail.keyboardEvent : e);
e = dom(e.detail ? e.detail.keyboardEvent : e);
// When e is a keyboardEvent, e.event is not null.
if (e.event) { e = e.event; }
return e;
@ -482,7 +486,7 @@ shortcuts are.
/** @polymerBehavior Gerrit.KeyboardShortcutBehavior*/
Gerrit.KeyboardShortcutBehavior = [
Polymer.IronA11yKeysBehavior,
IronA11yKeysBehavior,
{
// Exports for convenience. Note: Closure compiler crashes when
// object-shorthand syntax is used here.
@ -518,7 +522,7 @@ shortcuts are.
shouldSuppressKeyboardShortcut(e) {
e = getKeyboardEvent(e);
const tagName = Polymer.dom(e).rootTarget.tagName;
const tagName = dom(e).rootTarget.tagName;
if (tagName === 'INPUT' || tagName === 'TEXTAREA' ||
(e.keyCode === 13 && tagName === 'A')) {
// Suppress shortcuts if the key is 'enter' and target is an anchor.
@ -542,7 +546,7 @@ shortcuts are.
},
getRootTarget(e) {
return Polymer.dom(getKeyboardEvent(e)).rootTarget;
return dom(getKeyboardEvent(e)).rootTarget;
},
bindShortcut(shortcut, ...bindings) {
@ -671,4 +675,3 @@ shortcuts are.
};
}
})(window);
</script>

View File

@ -19,13 +19,13 @@ limitations under the License.
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>keyboard-shortcut-behavior</title>
<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/bower_components/web-component-tester/browser.js"></script>
<script src="../../test/test-pre-setup.js"></script>
<link rel="import" href="../../test/common-test-setup.html"/>
<link rel="import" href="keyboard-shortcut-behavior.html">
<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/components/wct-browser-legacy/browser.js"></script>
<script type="module" src="../../test/test-pre-setup.js"></script>
<script type="module" src="../../test/common-test-setup.js"></script>
<script type="module" src="./keyboard-shortcut-behavior.js"></script>
<test-fixture id="basic">
<template>
@ -41,403 +41,406 @@ limitations under the License.
</template>
</test-fixture>
<script>
suite('keyboard-shortcut-behavior tests', async () => {
await readyToTest();
const kb = window.Gerrit.KeyboardShortcutBinder;
<script type="module">
import '../../test/test-pre-setup.js';
import '../../test/common-test-setup.js';
import './keyboard-shortcut-behavior.js';
import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
suite('keyboard-shortcut-behavior tests', () => {
const kb = window.Gerrit.KeyboardShortcutBinder;
let element;
let overlay;
let sandbox;
let element;
let overlay;
let sandbox;
suiteSetup(() => {
// Define a Polymer element that uses this behavior.
Polymer({
is: 'test-element',
behaviors: [Gerrit.KeyboardShortcutBehavior],
keyBindings: {
k: '_handleKey',
enter: '_handleKey',
},
_handleKey() {},
});
suiteSetup(() => {
// Define a Polymer element that uses this behavior.
Polymer({
is: 'test-element',
behaviors: [Gerrit.KeyboardShortcutBehavior],
keyBindings: {
k: '_handleKey',
enter: '_handleKey',
},
_handleKey() {},
});
});
setup(() => {
element = fixture('basic');
overlay = fixture('within-overlay');
sandbox = sinon.sandbox.create();
});
teardown(() => {
sandbox.restore();
});
suite('ShortcutManager', () => {
test('bindings management', () => {
const mgr = new kb.ShortcutManager();
const {NEXT_FILE} = kb.Shortcut;
assert.isUndefined(mgr.getBindingsForShortcut(NEXT_FILE));
mgr.bindShortcut(NEXT_FILE, ']', '}', 'right');
assert.deepEqual(
mgr.getBindingsForShortcut(NEXT_FILE),
[']', '}', 'right']);
});
setup(() => {
element = fixture('basic');
overlay = fixture('within-overlay');
sandbox = sinon.sandbox.create();
});
suite('binding descriptions', () => {
function mapToObject(m) {
const o = {};
m.forEach((v, k) => o[k] = v);
return o;
}
teardown(() => {
sandbox.restore();
});
suite('ShortcutManager', () => {
test('bindings management', () => {
test('single combo description', () => {
const mgr = new kb.ShortcutManager();
const {NEXT_FILE} = kb.Shortcut;
assert.isUndefined(mgr.getBindingsForShortcut(NEXT_FILE));
mgr.bindShortcut(NEXT_FILE, ']', '}', 'right');
assert.deepEqual(mgr.describeBinding('a'), ['a']);
assert.deepEqual(mgr.describeBinding('a:keyup'), ['a']);
assert.deepEqual(mgr.describeBinding('ctrl+a'), ['Ctrl', 'a']);
assert.deepEqual(
mgr.getBindingsForShortcut(NEXT_FILE),
[']', '}', 'right']);
mgr.describeBinding('ctrl+shift+up:keyup'),
['Ctrl', 'Shift', '↑']);
});
suite('binding descriptions', () => {
function mapToObject(m) {
const o = {};
m.forEach((v, k) => o[k] = v);
return o;
}
test('combo set description', () => {
const {GO_KEY, DOC_ONLY, ShortcutManager} = kb;
const {GO_TO_OPENED_CHANGES, NEXT_FILE, PREV_FILE} = kb.Shortcut;
test('single combo description', () => {
const mgr = new kb.ShortcutManager();
assert.deepEqual(mgr.describeBinding('a'), ['a']);
assert.deepEqual(mgr.describeBinding('a:keyup'), ['a']);
assert.deepEqual(mgr.describeBinding('ctrl+a'), ['Ctrl', 'a']);
assert.deepEqual(
mgr.describeBinding('ctrl+shift+up:keyup'),
['Ctrl', 'Shift', '↑']);
const mgr = new ShortcutManager();
assert.isNull(mgr.describeBindings(NEXT_FILE));
mgr.bindShortcut(GO_TO_OPENED_CHANGES, GO_KEY, 'o');
assert.deepEqual(
mgr.describeBindings(GO_TO_OPENED_CHANGES),
[['g', 'o']]);
mgr.bindShortcut(NEXT_FILE, DOC_ONLY, ']', 'ctrl+shift+right:keyup');
assert.deepEqual(
mgr.describeBindings(NEXT_FILE),
[[']'], ['Ctrl', 'Shift', '→']]);
mgr.bindShortcut(PREV_FILE, '[');
assert.deepEqual(mgr.describeBindings(PREV_FILE), [['[']]);
});
test('combo set description width', () => {
const mgr = new kb.ShortcutManager();
assert.strictEqual(mgr.comboSetDisplayWidth([['u']]), 1);
assert.strictEqual(mgr.comboSetDisplayWidth([['g', 'o']]), 2);
assert.strictEqual(mgr.comboSetDisplayWidth([['Shift', 'r']]), 6);
assert.strictEqual(mgr.comboSetDisplayWidth([['x'], ['y']]), 4);
assert.strictEqual(
mgr.comboSetDisplayWidth([['x'], ['y'], ['Shift', 'z']]),
12);
});
test('distribute shortcut help', () => {
const mgr = new kb.ShortcutManager();
assert.deepEqual(mgr.distributeBindingDesc([['o']]), [[['o']]]);
assert.deepEqual(
mgr.distributeBindingDesc([['g', 'o']]),
[[['g', 'o']]]);
assert.deepEqual(
mgr.distributeBindingDesc([['ctrl', 'shift', 'meta', 'enter']]),
[[['ctrl', 'shift', 'meta', 'enter']]]);
assert.deepEqual(
mgr.distributeBindingDesc([
['ctrl', 'shift', 'meta', 'enter'],
['o'],
]),
[
[['ctrl', 'shift', 'meta', 'enter']],
[['o']],
]);
assert.deepEqual(
mgr.distributeBindingDesc([
['ctrl', 'enter'],
['meta', 'enter'],
['ctrl', 's'],
['meta', 's'],
]),
[
[['ctrl', 'enter'], ['meta', 'enter']],
[['ctrl', 's'], ['meta', 's']],
]);
});
test('active shortcuts by section', () => {
const {NEXT_FILE, NEXT_LINE, GO_TO_OPENED_CHANGES, SEARCH} =
kb.Shortcut;
const {DIFFS, EVERYWHERE, NAVIGATION} = kb.ShortcutSection;
const mgr = new kb.ShortcutManager();
mgr.bindShortcut(NEXT_FILE, ']');
mgr.bindShortcut(NEXT_LINE, 'j');
mgr.bindShortcut(GO_TO_OPENED_CHANGES, 'g+o');
mgr.bindShortcut(SEARCH, '/');
assert.deepEqual(
mapToObject(mgr.activeShortcutsBySection()),
{});
mgr.attachHost({
keyboardShortcuts() {
return {
[NEXT_FILE]: null,
};
},
});
assert.deepEqual(
mapToObject(mgr.activeShortcutsBySection()),
{
[NAVIGATION]: [
{shortcut: NEXT_FILE, text: 'Go to next file'},
],
});
test('combo set description', () => {
const {GO_KEY, DOC_ONLY, ShortcutManager} = kb;
const {GO_TO_OPENED_CHANGES, NEXT_FILE, PREV_FILE} = kb.Shortcut;
const mgr = new ShortcutManager();
assert.isNull(mgr.describeBindings(NEXT_FILE));
mgr.bindShortcut(GO_TO_OPENED_CHANGES, GO_KEY, 'o');
assert.deepEqual(
mgr.describeBindings(GO_TO_OPENED_CHANGES),
[['g', 'o']]);
mgr.bindShortcut(NEXT_FILE, DOC_ONLY, ']', 'ctrl+shift+right:keyup');
assert.deepEqual(
mgr.describeBindings(NEXT_FILE),
[[']'], ['Ctrl', 'Shift', '→']]);
mgr.bindShortcut(PREV_FILE, '[');
assert.deepEqual(mgr.describeBindings(PREV_FILE), [['[']]);
mgr.attachHost({
keyboardShortcuts() {
return {
[NEXT_LINE]: null,
};
},
});
assert.deepEqual(
mapToObject(mgr.activeShortcutsBySection()),
{
[DIFFS]: [
{shortcut: NEXT_LINE, text: 'Go to next line'},
],
[NAVIGATION]: [
{shortcut: NEXT_FILE, text: 'Go to next file'},
],
});
test('combo set description width', () => {
const mgr = new kb.ShortcutManager();
assert.strictEqual(mgr.comboSetDisplayWidth([['u']]), 1);
assert.strictEqual(mgr.comboSetDisplayWidth([['g', 'o']]), 2);
assert.strictEqual(mgr.comboSetDisplayWidth([['Shift', 'r']]), 6);
assert.strictEqual(mgr.comboSetDisplayWidth([['x'], ['y']]), 4);
assert.strictEqual(
mgr.comboSetDisplayWidth([['x'], ['y'], ['Shift', 'z']]),
12);
mgr.attachHost({
keyboardShortcuts() {
return {
[SEARCH]: null,
[GO_TO_OPENED_CHANGES]: null,
};
},
});
assert.deepEqual(
mapToObject(mgr.activeShortcutsBySection()),
{
[DIFFS]: [
{shortcut: NEXT_LINE, text: 'Go to next line'},
],
[EVERYWHERE]: [
{shortcut: SEARCH, text: 'Search'},
{
shortcut: GO_TO_OPENED_CHANGES,
text: 'Go to Opened Changes',
},
],
[NAVIGATION]: [
{shortcut: NEXT_FILE, text: 'Go to next file'},
],
});
});
test('distribute shortcut help', () => {
const mgr = new kb.ShortcutManager();
assert.deepEqual(mgr.distributeBindingDesc([['o']]), [[['o']]]);
assert.deepEqual(
mgr.distributeBindingDesc([['g', 'o']]),
[[['g', 'o']]]);
assert.deepEqual(
mgr.distributeBindingDesc([['ctrl', 'shift', 'meta', 'enter']]),
[[['ctrl', 'shift', 'meta', 'enter']]]);
assert.deepEqual(
mgr.distributeBindingDesc([
['ctrl', 'shift', 'meta', 'enter'],
['o'],
]),
[
[['ctrl', 'shift', 'meta', 'enter']],
[['o']],
]);
assert.deepEqual(
mgr.distributeBindingDesc([
['ctrl', 'enter'],
['meta', 'enter'],
['ctrl', 's'],
['meta', 's'],
]),
[
[['ctrl', 'enter'], ['meta', 'enter']],
[['ctrl', 's'], ['meta', 's']],
]);
test('directory view', () => {
const {
NEXT_FILE, NEXT_LINE, GO_TO_OPENED_CHANGES, SEARCH,
SAVE_COMMENT,
} = kb.Shortcut;
const {DIFFS, EVERYWHERE, NAVIGATION} = kb.ShortcutSection;
const {GO_KEY, ShortcutManager} = kb;
const mgr = new ShortcutManager();
mgr.bindShortcut(NEXT_FILE, ']');
mgr.bindShortcut(NEXT_LINE, 'j');
mgr.bindShortcut(GO_TO_OPENED_CHANGES, GO_KEY, 'o');
mgr.bindShortcut(SEARCH, '/');
mgr.bindShortcut(
SAVE_COMMENT, 'ctrl+enter', 'meta+enter', 'ctrl+s', 'meta+s');
assert.deepEqual(mapToObject(mgr.directoryView()), {});
mgr.attachHost({
keyboardShortcuts() {
return {
[GO_TO_OPENED_CHANGES]: null,
[NEXT_FILE]: null,
[NEXT_LINE]: null,
[SAVE_COMMENT]: null,
[SEARCH]: null,
};
},
});
test('active shortcuts by section', () => {
const {NEXT_FILE, NEXT_LINE, GO_TO_OPENED_CHANGES, SEARCH} =
kb.Shortcut;
const {DIFFS, EVERYWHERE, NAVIGATION} = kb.ShortcutSection;
const mgr = new kb.ShortcutManager();
mgr.bindShortcut(NEXT_FILE, ']');
mgr.bindShortcut(NEXT_LINE, 'j');
mgr.bindShortcut(GO_TO_OPENED_CHANGES, 'g+o');
mgr.bindShortcut(SEARCH, '/');
assert.deepEqual(
mapToObject(mgr.activeShortcutsBySection()),
{});
mgr.attachHost({
keyboardShortcuts() {
return {
[NEXT_FILE]: null,
};
},
});
assert.deepEqual(
mapToObject(mgr.activeShortcutsBySection()),
{
[NAVIGATION]: [
{shortcut: NEXT_FILE, text: 'Go to next file'},
],
});
mgr.attachHost({
keyboardShortcuts() {
return {
[NEXT_LINE]: null,
};
},
});
assert.deepEqual(
mapToObject(mgr.activeShortcutsBySection()),
{
[DIFFS]: [
{shortcut: NEXT_LINE, text: 'Go to next line'},
],
[NAVIGATION]: [
{shortcut: NEXT_FILE, text: 'Go to next file'},
],
});
mgr.attachHost({
keyboardShortcuts() {
return {
[SEARCH]: null,
[GO_TO_OPENED_CHANGES]: null,
};
},
});
assert.deepEqual(
mapToObject(mgr.activeShortcutsBySection()),
{
[DIFFS]: [
{shortcut: NEXT_LINE, text: 'Go to next line'},
],
[EVERYWHERE]: [
{shortcut: SEARCH, text: 'Search'},
{
shortcut: GO_TO_OPENED_CHANGES,
text: 'Go to Opened Changes',
},
],
[NAVIGATION]: [
{shortcut: NEXT_FILE, text: 'Go to next file'},
],
});
});
test('directory view', () => {
const {
NEXT_FILE, NEXT_LINE, GO_TO_OPENED_CHANGES, SEARCH,
SAVE_COMMENT,
} = kb.Shortcut;
const {DIFFS, EVERYWHERE, NAVIGATION} = kb.ShortcutSection;
const {GO_KEY, ShortcutManager} = kb;
const mgr = new ShortcutManager();
mgr.bindShortcut(NEXT_FILE, ']');
mgr.bindShortcut(NEXT_LINE, 'j');
mgr.bindShortcut(GO_TO_OPENED_CHANGES, GO_KEY, 'o');
mgr.bindShortcut(SEARCH, '/');
mgr.bindShortcut(
SAVE_COMMENT, 'ctrl+enter', 'meta+enter', 'ctrl+s', 'meta+s');
assert.deepEqual(mapToObject(mgr.directoryView()), {});
mgr.attachHost({
keyboardShortcuts() {
return {
[GO_TO_OPENED_CHANGES]: null,
[NEXT_FILE]: null,
[NEXT_LINE]: null,
[SAVE_COMMENT]: null,
[SEARCH]: null,
};
},
});
assert.deepEqual(
mapToObject(mgr.directoryView()),
{
[DIFFS]: [
{binding: [['j']], text: 'Go to next line'},
{
binding: [['Ctrl', 'Enter'], ['Meta', 'Enter']],
text: 'Save comment',
},
{
binding: [['Ctrl', 's'], ['Meta', 's']],
text: 'Save comment',
},
],
[EVERYWHERE]: [
{binding: [['/']], text: 'Search'},
{binding: [['g', 'o']], text: 'Go to Opened Changes'},
],
[NAVIGATION]: [
{binding: [[']']], text: 'Go to next file'},
],
});
});
});
});
test('doesnt block kb shortcuts for non-whitelisted els', done => {
const divEl = document.createElement('div');
element.appendChild(divEl);
element._handleKey = e => {
assert.isFalse(element.shouldSuppressKeyboardShortcut(e));
done();
};
MockInteractions.keyDownOn(divEl, 75, null, 'k');
});
test('blocks kb shortcuts for input els', done => {
const inputEl = document.createElement('input');
element.appendChild(inputEl);
element._handleKey = e => {
assert.isTrue(element.shouldSuppressKeyboardShortcut(e));
done();
};
MockInteractions.keyDownOn(inputEl, 75, null, 'k');
});
test('blocks kb shortcuts for textarea els', done => {
const textareaEl = document.createElement('textarea');
element.appendChild(textareaEl);
element._handleKey = e => {
assert.isTrue(element.shouldSuppressKeyboardShortcut(e));
done();
};
MockInteractions.keyDownOn(textareaEl, 75, null, 'k');
});
test('blocks kb shortcuts for anything in a gr-overlay', done => {
const divEl = document.createElement('div');
const element = overlay.querySelector('test-element');
element.appendChild(divEl);
element._handleKey = e => {
assert.isTrue(element.shouldSuppressKeyboardShortcut(e));
done();
};
MockInteractions.keyDownOn(divEl, 75, null, 'k');
});
test('blocks enter shortcut on an anchor', done => {
const anchorEl = document.createElement('a');
const element = overlay.querySelector('test-element');
element.appendChild(anchorEl);
element._handleKey = e => {
assert.isTrue(element.shouldSuppressKeyboardShortcut(e));
done();
};
MockInteractions.keyDownOn(anchorEl, 13, null, 'enter');
});
test('modifierPressed returns accurate values', () => {
const spy = sandbox.spy(element, 'modifierPressed');
element._handleKey = e => {
element.modifierPressed(e);
};
MockInteractions.keyDownOn(element, 75, 'shift', 'k');
assert.isTrue(spy.lastCall.returnValue);
MockInteractions.keyDownOn(element, 75, null, 'k');
assert.isFalse(spy.lastCall.returnValue);
MockInteractions.keyDownOn(element, 75, 'ctrl', 'k');
assert.isTrue(spy.lastCall.returnValue);
MockInteractions.keyDownOn(element, 75, null, 'k');
assert.isFalse(spy.lastCall.returnValue);
MockInteractions.keyDownOn(element, 75, 'meta', 'k');
assert.isTrue(spy.lastCall.returnValue);
MockInteractions.keyDownOn(element, 75, null, 'k');
assert.isFalse(spy.lastCall.returnValue);
MockInteractions.keyDownOn(element, 75, 'alt', 'k');
assert.isTrue(spy.lastCall.returnValue);
});
test('isModifierPressed returns accurate value', () => {
const spy = sandbox.spy(element, 'isModifierPressed');
element._handleKey = e => {
element.isModifierPressed(e, 'shiftKey');
};
MockInteractions.keyDownOn(element, 75, 'shift', 'k');
assert.isTrue(spy.lastCall.returnValue);
MockInteractions.keyDownOn(element, 75, null, 'k');
assert.isFalse(spy.lastCall.returnValue);
MockInteractions.keyDownOn(element, 75, 'ctrl', 'k');
assert.isFalse(spy.lastCall.returnValue);
MockInteractions.keyDownOn(element, 75, null, 'k');
assert.isFalse(spy.lastCall.returnValue);
MockInteractions.keyDownOn(element, 75, 'meta', 'k');
assert.isFalse(spy.lastCall.returnValue);
MockInteractions.keyDownOn(element, 75, null, 'k');
assert.isFalse(spy.lastCall.returnValue);
MockInteractions.keyDownOn(element, 75, 'alt', 'k');
assert.isFalse(spy.lastCall.returnValue);
});
suite('GO_KEY timing', () => {
let handlerStub;
setup(() => {
element._shortcut_go_table.set('a', '_handleA');
handlerStub = element._handleA = sinon.stub();
sandbox.stub(Date, 'now').returns(10000);
});
test('success', () => {
const e = {detail: {key: 'a'}, preventDefault: () => {}};
sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
element._shortcut_go_key_last_pressed = 9000;
element._handleGoAction(e);
assert.isTrue(handlerStub.calledOnce);
assert.strictEqual(handlerStub.lastCall.args[0], e);
});
test('go key not pressed', () => {
const e = {detail: {key: 'a'}, preventDefault: () => {}};
sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
element._shortcut_go_key_last_pressed = null;
element._handleGoAction(e);
assert.isFalse(handlerStub.called);
});
test('go key pressed too long ago', () => {
const e = {detail: {key: 'a'}, preventDefault: () => {}};
sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
element._shortcut_go_key_last_pressed = 3000;
element._handleGoAction(e);
assert.isFalse(handlerStub.called);
});
test('should suppress', () => {
const e = {detail: {key: 'a'}, preventDefault: () => {}};
sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(true);
element._shortcut_go_key_last_pressed = 9000;
element._handleGoAction(e);
assert.isFalse(handlerStub.called);
});
test('unrecognized key', () => {
const e = {detail: {key: 'f'}, preventDefault: () => {}};
sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
element._shortcut_go_key_last_pressed = 9000;
element._handleGoAction(e);
assert.isFalse(handlerStub.called);
assert.deepEqual(
mapToObject(mgr.directoryView()),
{
[DIFFS]: [
{binding: [['j']], text: 'Go to next line'},
{
binding: [['Ctrl', 'Enter'], ['Meta', 'Enter']],
text: 'Save comment',
},
{
binding: [['Ctrl', 's'], ['Meta', 's']],
text: 'Save comment',
},
],
[EVERYWHERE]: [
{binding: [['/']], text: 'Search'},
{binding: [['g', 'o']], text: 'Go to Opened Changes'},
],
[NAVIGATION]: [
{binding: [[']']], text: 'Go to next file'},
],
});
});
});
});
test('doesnt block kb shortcuts for non-whitelisted els', done => {
const divEl = document.createElement('div');
element.appendChild(divEl);
element._handleKey = e => {
assert.isFalse(element.shouldSuppressKeyboardShortcut(e));
done();
};
MockInteractions.keyDownOn(divEl, 75, null, 'k');
});
test('blocks kb shortcuts for input els', done => {
const inputEl = document.createElement('input');
element.appendChild(inputEl);
element._handleKey = e => {
assert.isTrue(element.shouldSuppressKeyboardShortcut(e));
done();
};
MockInteractions.keyDownOn(inputEl, 75, null, 'k');
});
test('blocks kb shortcuts for textarea els', done => {
const textareaEl = document.createElement('textarea');
element.appendChild(textareaEl);
element._handleKey = e => {
assert.isTrue(element.shouldSuppressKeyboardShortcut(e));
done();
};
MockInteractions.keyDownOn(textareaEl, 75, null, 'k');
});
test('blocks kb shortcuts for anything in a gr-overlay', done => {
const divEl = document.createElement('div');
const element = overlay.querySelector('test-element');
element.appendChild(divEl);
element._handleKey = e => {
assert.isTrue(element.shouldSuppressKeyboardShortcut(e));
done();
};
MockInteractions.keyDownOn(divEl, 75, null, 'k');
});
test('blocks enter shortcut on an anchor', done => {
const anchorEl = document.createElement('a');
const element = overlay.querySelector('test-element');
element.appendChild(anchorEl);
element._handleKey = e => {
assert.isTrue(element.shouldSuppressKeyboardShortcut(e));
done();
};
MockInteractions.keyDownOn(anchorEl, 13, null, 'enter');
});
test('modifierPressed returns accurate values', () => {
const spy = sandbox.spy(element, 'modifierPressed');
element._handleKey = e => {
element.modifierPressed(e);
};
MockInteractions.keyDownOn(element, 75, 'shift', 'k');
assert.isTrue(spy.lastCall.returnValue);
MockInteractions.keyDownOn(element, 75, null, 'k');
assert.isFalse(spy.lastCall.returnValue);
MockInteractions.keyDownOn(element, 75, 'ctrl', 'k');
assert.isTrue(spy.lastCall.returnValue);
MockInteractions.keyDownOn(element, 75, null, 'k');
assert.isFalse(spy.lastCall.returnValue);
MockInteractions.keyDownOn(element, 75, 'meta', 'k');
assert.isTrue(spy.lastCall.returnValue);
MockInteractions.keyDownOn(element, 75, null, 'k');
assert.isFalse(spy.lastCall.returnValue);
MockInteractions.keyDownOn(element, 75, 'alt', 'k');
assert.isTrue(spy.lastCall.returnValue);
});
test('isModifierPressed returns accurate value', () => {
const spy = sandbox.spy(element, 'isModifierPressed');
element._handleKey = e => {
element.isModifierPressed(e, 'shiftKey');
};
MockInteractions.keyDownOn(element, 75, 'shift', 'k');
assert.isTrue(spy.lastCall.returnValue);
MockInteractions.keyDownOn(element, 75, null, 'k');
assert.isFalse(spy.lastCall.returnValue);
MockInteractions.keyDownOn(element, 75, 'ctrl', 'k');
assert.isFalse(spy.lastCall.returnValue);
MockInteractions.keyDownOn(element, 75, null, 'k');
assert.isFalse(spy.lastCall.returnValue);
MockInteractions.keyDownOn(element, 75, 'meta', 'k');
assert.isFalse(spy.lastCall.returnValue);
MockInteractions.keyDownOn(element, 75, null, 'k');
assert.isFalse(spy.lastCall.returnValue);
MockInteractions.keyDownOn(element, 75, 'alt', 'k');
assert.isFalse(spy.lastCall.returnValue);
});
suite('GO_KEY timing', () => {
let handlerStub;
setup(() => {
element._shortcut_go_table.set('a', '_handleA');
handlerStub = element._handleA = sinon.stub();
sandbox.stub(Date, 'now').returns(10000);
});
test('success', () => {
const e = {detail: {key: 'a'}, preventDefault: () => {}};
sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
element._shortcut_go_key_last_pressed = 9000;
element._handleGoAction(e);
assert.isTrue(handlerStub.calledOnce);
assert.strictEqual(handlerStub.lastCall.args[0], e);
});
test('go key not pressed', () => {
const e = {detail: {key: 'a'}, preventDefault: () => {}};
sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
element._shortcut_go_key_last_pressed = null;
element._handleGoAction(e);
assert.isFalse(handlerStub.called);
});
test('go key pressed too long ago', () => {
const e = {detail: {key: 'a'}, preventDefault: () => {}};
sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
element._shortcut_go_key_last_pressed = 3000;
element._handleGoAction(e);
assert.isFalse(handlerStub.called);
});
test('should suppress', () => {
const e = {detail: {key: 'a'}, preventDefault: () => {}};
sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(true);
element._shortcut_go_key_last_pressed = 9000;
element._handleGoAction(e);
assert.isFalse(handlerStub.called);
});
test('unrecognized key', () => {
const e = {detail: {key: 'f'}, preventDefault: () => {}};
sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
element._shortcut_go_key_last_pressed = 9000;
element._handleGoAction(e);
assert.isFalse(handlerStub.called);
});
});
});
</script>

View File

@ -1,22 +1,22 @@
<!--
@license
Copyright (C) 2016 The Android Open Source Project
/**
* @license
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import '../../scripts/bundled-polymer.js';
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<link rel="import" href="/bower_components/polymer/polymer.html">
<link rel="import" href="../base-url-behavior/base-url-behavior.html">
<script>
import '../base-url-behavior/base-url-behavior.js';
(function(window) {
'use strict';
@ -198,4 +198,3 @@ limitations under the License.
};
}
})(window);
</script>

View File

@ -19,19 +19,23 @@ limitations under the License.
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>keyboard-shortcut-behavior</title>
<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/bower_components/web-component-tester/browser.js"></script>
<script src="../../test/test-pre-setup.js"></script>
<link rel="import" href="../../test/common-test-setup.html"/>
<script>
/** @type {string} */
window.CANONICAL_PATH = '/r';
<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/components/wct-browser-legacy/browser.js"></script>
<script type="module" src="../../test/test-pre-setup.js"></script>
<script type="module" src="../../test/common-test-setup.js"></script>
<script type="module">
import '../../test/test-pre-setup.js';
import '../../test/common-test-setup.js';
import '../base-url-behavior/base-url-behavior.js';
import './rest-client-behavior.js';
/** @type {string} */
window.CANONICAL_PATH = '/r';
</script>
<link rel="import" href="../base-url-behavior/base-url-behavior.html">
<link rel="import" href="rest-client-behavior.html">
<script type="module" src="../base-url-behavior/base-url-behavior.js"></script>
<script type="module" src="./rest-client-behavior.js"></script>
<test-fixture id="basic">
<template>
@ -47,191 +51,195 @@ limitations under the License.
</template>
</test-fixture>
<script>
suite('rest-client-behavior tests', async () => {
await readyToTest();
let element;
// eslint-disable-next-line no-unused-vars
let overlay;
<script type="module">
import '../../test/test-pre-setup.js';
import '../../test/common-test-setup.js';
import '../base-url-behavior/base-url-behavior.js';
import './rest-client-behavior.js';
import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
suite('rest-client-behavior tests', () => {
let element;
// eslint-disable-next-line no-unused-vars
let overlay;
suiteSetup(() => {
// Define a Polymer element that uses this behavior.
Polymer({
is: 'test-element',
behaviors: [
Gerrit.BaseUrlBehavior,
Gerrit.RESTClientBehavior,
],
});
});
setup(() => {
element = fixture('basic');
overlay = fixture('within-overlay');
});
test('changeBaseURL', () => {
assert.deepEqual(
element.changeBaseURL('test/project', '1', '2'),
'/r/changes/test%2Fproject~1/revisions/2'
);
});
test('changePath', () => {
assert.deepEqual(element.changePath('1'), '/r/c/1');
});
test('Open status', () => {
const change = {
change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
revisions: {
rev1: {_number: 1},
},
current_revision: 'rev1',
status: 'NEW',
labels: {},
mergeable: true,
};
let statuses = element.changeStatuses(change);
const statusString = element.changeStatusString(change);
assert.deepEqual(statuses, []);
assert.equal(statusString, '');
change.submittable = false;
statuses = element.changeStatuses(change,
{includeDerived: true});
assert.deepEqual(statuses, ['Active']);
// With no missing labels but no submitEnabled option.
change.submittable = true;
statuses = element.changeStatuses(change,
{includeDerived: true});
assert.deepEqual(statuses, ['Active']);
// Without missing labels and enabled submit
statuses = element.changeStatuses(change,
{includeDerived: true, submitEnabled: true});
assert.deepEqual(statuses, ['Ready to submit']);
change.mergeable = false;
change.submittable = true;
statuses = element.changeStatuses(change,
{includeDerived: true});
assert.deepEqual(statuses, ['Merge Conflict']);
delete change.mergeable;
change.submittable = true;
statuses = element.changeStatuses(change,
{includeDerived: true, mergeable: true, submitEnabled: true});
assert.deepEqual(statuses, ['Ready to submit']);
change.submittable = true;
statuses = element.changeStatuses(change,
{includeDerived: true, mergeable: false});
assert.deepEqual(statuses, ['Merge Conflict']);
});
test('Merge conflict', () => {
const change = {
change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
revisions: {
rev1: {_number: 1},
},
current_revision: 'rev1',
status: 'NEW',
labels: {},
mergeable: false,
};
const statuses = element.changeStatuses(change);
const statusString = element.changeStatusString(change);
assert.deepEqual(statuses, ['Merge Conflict']);
assert.equal(statusString, 'Merge Conflict');
});
test('mergeable prop undefined', () => {
const change = {
change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
revisions: {
rev1: {_number: 1},
},
current_revision: 'rev1',
status: 'NEW',
labels: {},
};
const statuses = element.changeStatuses(change);
const statusString = element.changeStatusString(change);
assert.deepEqual(statuses, []);
assert.equal(statusString, '');
});
test('Merged status', () => {
const change = {
change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
revisions: {
rev1: {_number: 1},
},
current_revision: 'rev1',
status: 'MERGED',
labels: {},
};
const statuses = element.changeStatuses(change);
const statusString = element.changeStatusString(change);
assert.deepEqual(statuses, ['Merged']);
assert.equal(statusString, 'Merged');
});
test('Abandoned status', () => {
const change = {
change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
revisions: {
rev1: {_number: 1},
},
current_revision: 'rev1',
status: 'ABANDONED',
labels: {},
};
const statuses = element.changeStatuses(change);
const statusString = element.changeStatusString(change);
assert.deepEqual(statuses, ['Abandoned']);
assert.equal(statusString, 'Abandoned');
});
test('Open status with private and wip', () => {
const change = {
change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
revisions: {
rev1: {_number: 1},
},
current_revision: 'rev1',
status: 'NEW',
is_private: true,
work_in_progress: true,
labels: {},
mergeable: true,
};
const statuses = element.changeStatuses(change);
const statusString = element.changeStatusString(change);
assert.deepEqual(statuses, ['WIP', 'Private']);
assert.equal(statusString, 'WIP, Private');
});
test('Merge conflict with private and wip', () => {
const change = {
change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
revisions: {
rev1: {_number: 1},
},
current_revision: 'rev1',
status: 'NEW',
is_private: true,
work_in_progress: true,
labels: {},
mergeable: false,
};
const statuses = element.changeStatuses(change);
const statusString = element.changeStatusString(change);
assert.deepEqual(statuses, ['Merge Conflict', 'WIP', 'Private']);
assert.equal(statusString, 'Merge Conflict, WIP, Private');
suiteSetup(() => {
// Define a Polymer element that uses this behavior.
Polymer({
is: 'test-element',
behaviors: [
Gerrit.BaseUrlBehavior,
Gerrit.RESTClientBehavior,
],
});
});
setup(() => {
element = fixture('basic');
overlay = fixture('within-overlay');
});
test('changeBaseURL', () => {
assert.deepEqual(
element.changeBaseURL('test/project', '1', '2'),
'/r/changes/test%2Fproject~1/revisions/2'
);
});
test('changePath', () => {
assert.deepEqual(element.changePath('1'), '/r/c/1');
});
test('Open status', () => {
const change = {
change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
revisions: {
rev1: {_number: 1},
},
current_revision: 'rev1',
status: 'NEW',
labels: {},
mergeable: true,
};
let statuses = element.changeStatuses(change);
const statusString = element.changeStatusString(change);
assert.deepEqual(statuses, []);
assert.equal(statusString, '');
change.submittable = false;
statuses = element.changeStatuses(change,
{includeDerived: true});
assert.deepEqual(statuses, ['Active']);
// With no missing labels but no submitEnabled option.
change.submittable = true;
statuses = element.changeStatuses(change,
{includeDerived: true});
assert.deepEqual(statuses, ['Active']);
// Without missing labels and enabled submit
statuses = element.changeStatuses(change,
{includeDerived: true, submitEnabled: true});
assert.deepEqual(statuses, ['Ready to submit']);
change.mergeable = false;
change.submittable = true;
statuses = element.changeStatuses(change,
{includeDerived: true});
assert.deepEqual(statuses, ['Merge Conflict']);
delete change.mergeable;
change.submittable = true;
statuses = element.changeStatuses(change,
{includeDerived: true, mergeable: true, submitEnabled: true});
assert.deepEqual(statuses, ['Ready to submit']);
change.submittable = true;
statuses = element.changeStatuses(change,
{includeDerived: true, mergeable: false});
assert.deepEqual(statuses, ['Merge Conflict']);
});
test('Merge conflict', () => {
const change = {
change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
revisions: {
rev1: {_number: 1},
},
current_revision: 'rev1',
status: 'NEW',
labels: {},
mergeable: false,
};
const statuses = element.changeStatuses(change);
const statusString = element.changeStatusString(change);
assert.deepEqual(statuses, ['Merge Conflict']);
assert.equal(statusString, 'Merge Conflict');
});
test('mergeable prop undefined', () => {
const change = {
change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
revisions: {
rev1: {_number: 1},
},
current_revision: 'rev1',
status: 'NEW',
labels: {},
};
const statuses = element.changeStatuses(change);
const statusString = element.changeStatusString(change);
assert.deepEqual(statuses, []);
assert.equal(statusString, '');
});
test('Merged status', () => {
const change = {
change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
revisions: {
rev1: {_number: 1},
},
current_revision: 'rev1',
status: 'MERGED',
labels: {},
};
const statuses = element.changeStatuses(change);
const statusString = element.changeStatusString(change);
assert.deepEqual(statuses, ['Merged']);
assert.equal(statusString, 'Merged');
});
test('Abandoned status', () => {
const change = {
change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
revisions: {
rev1: {_number: 1},
},
current_revision: 'rev1',
status: 'ABANDONED',
labels: {},
};
const statuses = element.changeStatuses(change);
const statusString = element.changeStatusString(change);
assert.deepEqual(statuses, ['Abandoned']);
assert.equal(statusString, 'Abandoned');
});
test('Open status with private and wip', () => {
const change = {
change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
revisions: {
rev1: {_number: 1},
},
current_revision: 'rev1',
status: 'NEW',
is_private: true,
work_in_progress: true,
labels: {},
mergeable: true,
};
const statuses = element.changeStatuses(change);
const statusString = element.changeStatusString(change);
assert.deepEqual(statuses, ['WIP', 'Private']);
assert.equal(statusString, 'WIP, Private');
});
test('Merge conflict with private and wip', () => {
const change = {
change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
revisions: {
rev1: {_number: 1},
},
current_revision: 'rev1',
status: 'NEW',
is_private: true,
work_in_progress: true,
labels: {},
mergeable: false,
};
const statuses = element.changeStatuses(change);
const statusString = element.changeStatusString(change);
assert.deepEqual(statuses, ['Merge Conflict', 'WIP', 'Private']);
assert.equal(statusString, 'Merge Conflict, WIP, Private');
});
});
</script>

View File

@ -1,20 +1,19 @@
<!--
@license
Copyright (C) 2018 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<script>
/**
* @license
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function(window) {
'use strict';
@ -74,4 +73,3 @@ limitations under the License.
throw new Error(`Refused to bind value as ${type}: ${value}`);
};
})(window);
</script>

View File

@ -18,15 +18,20 @@ limitations under the License.
<title>safe-types-behavior</title>
<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/bower_components/web-component-tester/browser.js"></script>
<script src="../../test/test-pre-setup.js"></script>
<link rel="import" href="../../test/common-test-setup.html"/>
<link rel="import" href="safe-types-behavior.html">
<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/components/wct-browser-legacy/browser.js"></script>
<script type="module" src="../../test/test-pre-setup.js"></script>
<script type="module" src="../../test/common-test-setup.js"></script>
<script type="module" src="./safe-types-behavior.js"></script>
<script>void(0);</script>
<script type="module">
import '../../test/test-pre-setup.js';
import '../../test/common-test-setup.js';
import './safe-types-behavior.js';
void(0);
</script>
<test-fixture id="basic">
<template>
@ -34,92 +39,95 @@ limitations under the License.
</template>
</test-fixture>
<script>
suite('gr-tooltip-behavior tests', async () => {
await readyToTest();
let element;
let sandbox;
<script type="module">
import '../../test/test-pre-setup.js';
import '../../test/common-test-setup.js';
import './safe-types-behavior.js';
import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
suite('gr-tooltip-behavior tests', () => {
let element;
let sandbox;
suiteSetup(() => {
Polymer({
is: 'safe-types-element',
behaviors: [Gerrit.SafeTypes],
});
});
setup(() => {
sandbox = sinon.sandbox.create();
element = fixture('basic');
});
teardown(() => {
sandbox.restore();
});
test('SafeUrl accepts valid urls', () => {
function accepts(url) {
const safeUrl = new element.SafeUrl(url);
assert.isOk(safeUrl);
assert.equal(url, safeUrl.asString());
}
accepts('http://www.google.com/');
accepts('https://www.google.com/');
accepts('HtTpS://www.google.com/');
accepts('//www.google.com/');
accepts('/c/1234/file/path.html@45');
accepts('#hash-url');
accepts('mailto:name@example.com');
});
test('SafeUrl rejects invalid urls', () => {
function rejects(url) {
assert.throws(() => { new element.SafeUrl(url); });
}
rejects('javascript://alert("evil");');
rejects('ftp:example.com');
rejects('data:text/html,scary business');
});
suite('safeTypesBridge', () => {
function acceptsString(value, type) {
assert.equal(Gerrit.SafeTypes.safeTypesBridge(value, type),
value);
}
function rejects(value, type) {
assert.throws(() => { Gerrit.SafeTypes.safeTypesBridge(value, type); });
}
test('accepts valid URL strings', () => {
acceptsString('/foo/bar', 'URL');
acceptsString('#baz', 'URL');
});
test('rejects invalid URL strings', () => {
rejects('javascript://void();', 'URL');
});
test('accepts SafeUrl values', () => {
const url = '/abc/123';
const safeUrl = new element.SafeUrl(url);
assert.equal(Gerrit.SafeTypes.safeTypesBridge(safeUrl, 'URL'), url);
});
test('rejects non-string or non-SafeUrl types', () => {
rejects(3.1415926, 'URL');
});
test('accepts any binding to STRING or CONSTANT', () => {
acceptsString('foo/bar/baz', 'STRING');
acceptsString('lorem ipsum dolor', 'CONSTANT');
});
test('rejects all other types', () => {
rejects('foo', 'JAVASCRIPT');
rejects('foo', 'HTML');
rejects('foo', 'RESOURCE_URL');
rejects('foo', 'STYLE');
});
suiteSetup(() => {
Polymer({
is: 'safe-types-element',
behaviors: [Gerrit.SafeTypes],
});
});
setup(() => {
sandbox = sinon.sandbox.create();
element = fixture('basic');
});
teardown(() => {
sandbox.restore();
});
test('SafeUrl accepts valid urls', () => {
function accepts(url) {
const safeUrl = new element.SafeUrl(url);
assert.isOk(safeUrl);
assert.equal(url, safeUrl.asString());
}
accepts('http://www.google.com/');
accepts('https://www.google.com/');
accepts('HtTpS://www.google.com/');
accepts('//www.google.com/');
accepts('/c/1234/file/path.html@45');
accepts('#hash-url');
accepts('mailto:name@example.com');
});
test('SafeUrl rejects invalid urls', () => {
function rejects(url) {
assert.throws(() => { new element.SafeUrl(url); });
}
rejects('javascript://alert("evil");');
rejects('ftp:example.com');
rejects('data:text/html,scary business');
});
suite('safeTypesBridge', () => {
function acceptsString(value, type) {
assert.equal(Gerrit.SafeTypes.safeTypesBridge(value, type),
value);
}
function rejects(value, type) {
assert.throws(() => { Gerrit.SafeTypes.safeTypesBridge(value, type); });
}
test('accepts valid URL strings', () => {
acceptsString('/foo/bar', 'URL');
acceptsString('#baz', 'URL');
});
test('rejects invalid URL strings', () => {
rejects('javascript://void();', 'URL');
});
test('accepts SafeUrl values', () => {
const url = '/abc/123';
const safeUrl = new element.SafeUrl(url);
assert.equal(Gerrit.SafeTypes.safeTypesBridge(safeUrl, 'URL'), url);
});
test('rejects non-string or non-SafeUrl types', () => {
rejects(3.1415926, 'URL');
});
test('accepts any binding to STRING or CONSTANT', () => {
acceptsString('foo/bar/baz', 'STRING');
acceptsString('lorem ipsum dolor', 'CONSTANT');
});
test('rejects all other types', () => {
rejects('foo', 'JAVASCRIPT');
rejects('foo', 'HTML');
rejects('foo', 'RESOURCE_URL');
rejects('foo', 'STYLE');
});
});
});
</script>

View File

@ -14,291 +14,308 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function() {
'use strict';
import '../../../scripts/bundled-polymer.js';
import '../../../behaviors/fire-behavior/fire-behavior.js';
import '../../../behaviors/gr-access-behavior/gr-access-behavior.js';
import '@polymer/iron-input/iron-input.js';
import '../../../styles/gr-form-styles.js';
import '../../../styles/shared-styles.js';
import '../../shared/gr-button/gr-button.js';
import '../../shared/gr-icons/gr-icons.js';
import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
import '../gr-permission/gr-permission.js';
import '../../../scripts/util.js';
import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
import {PolymerElement} from '@polymer/polymer/polymer-element.js';
import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
import {htmlTemplate} from './gr-access-section_html.js';
/**
* Fired when the section has been modified or removed.
*
* @event access-modified
*/
/**
* Fired when a section that was previously added was removed.
*
* @event added-section-removed
*/
const GLOBAL_NAME = 'GLOBAL_CAPABILITIES';
// The name that gets automatically input when a new reference is added.
const NEW_NAME = 'refs/heads/*';
const REFS_NAME = 'refs/';
const ON_BEHALF_OF = '(On Behalf Of)';
const LABEL = 'Label';
/**
* @appliesMixin Gerrit.AccessMixin
* @appliesMixin Gerrit.FireMixin
* @extends Polymer.Element
*/
class GrAccessSection extends mixinBehaviors( [
Gerrit.AccessBehavior,
/**
* Fired when the section has been modified or removed.
*
* @event access-modified
* Unused in this element, but called by other elements in tests
* e.g gr-repo-access_test.
*/
Gerrit.FireBehavior,
], GestureEventListeners(
LegacyElementMixin(
PolymerElement))) {
static get template() { return htmlTemplate; }
/**
* Fired when a section that was previously added was removed.
*
* @event added-section-removed
*/
static get is() { return 'gr-access-section'; }
const GLOBAL_NAME = 'GLOBAL_CAPABILITIES';
static get properties() {
return {
capabilities: Object,
/** @type {?} */
section: {
type: Object,
notify: true,
observer: '_updateSection',
},
groups: Object,
labels: Object,
editing: {
type: Boolean,
value: false,
observer: '_handleEditingChanged',
},
canUpload: Boolean,
ownerOf: Array,
_originalId: String,
_editingRef: {
type: Boolean,
value: false,
},
_deleted: {
type: Boolean,
value: false,
},
_permissions: Array,
};
}
// The name that gets automatically input when a new reference is added.
const NEW_NAME = 'refs/heads/*';
const REFS_NAME = 'refs/';
const ON_BEHALF_OF = '(On Behalf Of)';
const LABEL = 'Label';
/** @override */
created() {
super.created();
this.addEventListener('access-saved',
() => this._handleAccessSaved());
}
/**
* @appliesMixin Gerrit.AccessMixin
* @appliesMixin Gerrit.FireMixin
* @extends Polymer.Element
*/
class GrAccessSection extends Polymer.mixinBehaviors( [
Gerrit.AccessBehavior,
/**
* Unused in this element, but called by other elements in tests
* e.g gr-repo-access_test.
*/
Gerrit.FireBehavior,
], Polymer.GestureEventListeners(
Polymer.LegacyElementMixin(
Polymer.Element))) {
static get is() { return 'gr-access-section'; }
_updateSection(section) {
this._permissions = this.toSortedArray(section.value.permissions);
this._originalId = section.id;
}
static get properties() {
return {
capabilities: Object,
/** @type {?} */
section: {
type: Object,
notify: true,
observer: '_updateSection',
},
groups: Object,
labels: Object,
editing: {
type: Boolean,
value: false,
observer: '_handleEditingChanged',
},
canUpload: Boolean,
ownerOf: Array,
_originalId: String,
_editingRef: {
type: Boolean,
value: false,
},
_deleted: {
type: Boolean,
value: false,
},
_permissions: Array,
};
_handleAccessSaved() {
// Set a new 'original' value to keep track of after the value has been
// saved.
this._updateSection(this.section);
}
_handleValueChange() {
if (!this.section.value.added) {
this.section.value.modified = this.section.id !== this._originalId;
// Allows overall access page to know a change has been made.
// For a new section, this is not fired because new permissions and
// rules have to be added in order to save, modifying the ref is not
// enough.
this.dispatchEvent(new CustomEvent(
'access-modified', {bubbles: true, composed: true}));
}
this.section.value.updatedId = this.section.id;
}
/** @override */
created() {
super.created();
this.addEventListener('access-saved',
() => this._handleAccessSaved());
}
_updateSection(section) {
this._permissions = this.toSortedArray(section.value.permissions);
this._originalId = section.id;
}
_handleAccessSaved() {
// Set a new 'original' value to keep track of after the value has been
// saved.
this._updateSection(this.section);
}
_handleValueChange() {
if (!this.section.value.added) {
this.section.value.modified = this.section.id !== this._originalId;
// Allows overall access page to know a change has been made.
// For a new section, this is not fired because new permissions and
// rules have to be added in order to save, modifying the ref is not
// enough.
this.dispatchEvent(new CustomEvent(
'access-modified', {bubbles: true, composed: true}));
}
this.section.value.updatedId = this.section.id;
}
_handleEditingChanged(editing, editingOld) {
// Ignore when editing gets set initially.
if (!editingOld) { return; }
// Restore original values if no longer editing.
if (!editing) {
this._editingRef = false;
this._deleted = false;
delete this.section.value.deleted;
// Restore section ref.
this.set(['section', 'id'], this._originalId);
// Remove any unsaved but added permissions.
this._permissions = this._permissions.filter(p => !p.value.added);
for (const key of Object.keys(this.section.value.permissions)) {
if (this.section.value.permissions[key].added) {
delete this.section.value.permissions[key];
}
}
}
}
_computePermissions(name, capabilities, labels) {
let allPermissions;
if (!this.section || !this.section.value) {
return [];
}
if (name === GLOBAL_NAME) {
allPermissions = this.toSortedArray(capabilities);
} else {
const labelOptions = this._computeLabelOptions(labels);
allPermissions = labelOptions.concat(
this.toSortedArray(this.permissionValues));
}
return allPermissions
.filter(permission => !this.section.value.permissions[permission.id]);
}
_computeHideEditClass(section) {
return section.id === 'GLOBAL_CAPABILITIES' ? 'hide' : '';
}
_handleAddedPermissionRemoved(e) {
const index = e.model.index;
this._permissions = this._permissions.slice(0, index).concat(
this._permissions.slice(index + 1, this._permissions.length));
}
_computeLabelOptions(labels) {
const labelOptions = [];
if (!labels) { return []; }
for (const labelName of Object.keys(labels)) {
labelOptions.push({
id: 'label-' + labelName,
value: {
name: `${LABEL} ${labelName}`,
id: 'label-' + labelName,
},
});
labelOptions.push({
id: 'labelAs-' + labelName,
value: {
name: `${LABEL} ${labelName} ${ON_BEHALF_OF}`,
id: 'labelAs-' + labelName,
},
});
}
return labelOptions;
}
_computePermissionName(name, permission, permissionValues, capabilities) {
if (name === GLOBAL_NAME) {
return capabilities[permission.id].name;
} else if (permissionValues[permission.id]) {
return permissionValues[permission.id].name;
} else if (permission.value.label) {
let behalfOf = '';
if (permission.id.startsWith('labelAs-')) {
behalfOf = ON_BEHALF_OF;
}
return `${LABEL} ${permission.value.label}${behalfOf}`;
}
}
_computeSectionName(name) {
// When a new section is created, it doesn't yet have a ref. Set into
// edit mode so that the user can input one.
if (!name) {
this._editingRef = true;
// Needed for the title value. This is the same default as GWT.
name = NEW_NAME;
// Needed for the input field value.
this.set('section.id', name);
}
if (name === GLOBAL_NAME) {
return 'Global Capabilities';
} else if (name.startsWith(REFS_NAME)) {
return `Reference: ${name}`;
}
return name;
}
_handleRemoveReference() {
if (this.section.value.added) {
this.dispatchEvent(new CustomEvent(
'added-section-removed', {bubbles: true, composed: true}));
}
this._deleted = true;
this.section.value.deleted = true;
this.dispatchEvent(
new CustomEvent('access-modified', {bubbles: true, composed: true}));
}
_handleUndoRemove() {
_handleEditingChanged(editing, editingOld) {
// Ignore when editing gets set initially.
if (!editingOld) { return; }
// Restore original values if no longer editing.
if (!editing) {
this._editingRef = false;
this._deleted = false;
delete this.section.value.deleted;
}
editRefInput() {
return Polymer.dom(this.root).querySelector(Polymer.Element ?
'iron-input.editRefInput' :
'input[is=iron-input].editRefInput');
}
editReference() {
this._editingRef = true;
this.editRefInput().focus();
}
_isEditEnabled(canUpload, ownerOf, sectionId) {
return canUpload || (ownerOf && ownerOf.indexOf(sectionId) >= 0);
}
_computeSectionClass(editing, canUpload, ownerOf, editingRef, deleted) {
const classList = [];
if (editing
&& this._isEditEnabled(canUpload, ownerOf, this.section.id)) {
classList.push('editing');
// Restore section ref.
this.set(['section', 'id'], this._originalId);
// Remove any unsaved but added permissions.
this._permissions = this._permissions.filter(p => !p.value.added);
for (const key of Object.keys(this.section.value.permissions)) {
if (this.section.value.permissions[key].added) {
delete this.section.value.permissions[key];
}
}
if (editingRef) {
classList.push('editingRef');
}
if (deleted) {
classList.push('deleted');
}
return classList.join(' ');
}
_computeEditBtnClass(name) {
return name === GLOBAL_NAME ? 'global' : '';
}
_handleAddPermission() {
const value = this.$.permissionSelect.value;
const permission = {
id: value,
value: {rules: {}, added: true},
};
// This is needed to update the 'label' property of the
// 'label-<label-name>' permission.
//
// The value from the add permission dropdown will either be
// label-<label-name> or labelAs-<labelName>.
// But, the format of the API response is as such:
// "permissions": {
// "label-Code-Review": {
// "label": "Code-Review",
// "rules": {...}
// }
// }
// }
// When we add a new item, we have to push the new permission in the same
// format as the ones that have been returned by the API.
if (value.startsWith('label')) {
permission.value.label =
value.replace('label-', '').replace('labelAs-', '');
}
// Add to the end of the array (used in dom-repeat) and also to the
// section object that is two way bound with its parent element.
this.push('_permissions', permission);
this.set(['section.value.permissions', permission.id],
permission.value);
}
}
customElements.define(GrAccessSection.is, GrAccessSection);
})();
_computePermissions(name, capabilities, labels) {
let allPermissions;
if (!this.section || !this.section.value) {
return [];
}
if (name === GLOBAL_NAME) {
allPermissions = this.toSortedArray(capabilities);
} else {
const labelOptions = this._computeLabelOptions(labels);
allPermissions = labelOptions.concat(
this.toSortedArray(this.permissionValues));
}
return allPermissions
.filter(permission => !this.section.value.permissions[permission.id]);
}
_computeHideEditClass(section) {
return section.id === 'GLOBAL_CAPABILITIES' ? 'hide' : '';
}
_handleAddedPermissionRemoved(e) {
const index = e.model.index;
this._permissions = this._permissions.slice(0, index).concat(
this._permissions.slice(index + 1, this._permissions.length));
}
_computeLabelOptions(labels) {
const labelOptions = [];
if (!labels) { return []; }
for (const labelName of Object.keys(labels)) {
labelOptions.push({
id: 'label-' + labelName,
value: {
name: `${LABEL} ${labelName}`,
id: 'label-' + labelName,
},
});
labelOptions.push({
id: 'labelAs-' + labelName,
value: {
name: `${LABEL} ${labelName} ${ON_BEHALF_OF}`,
id: 'labelAs-' + labelName,
},
});
}
return labelOptions;
}
_computePermissionName(name, permission, permissionValues, capabilities) {
if (name === GLOBAL_NAME) {
return capabilities[permission.id].name;
} else if (permissionValues[permission.id]) {
return permissionValues[permission.id].name;
} else if (permission.value.label) {
let behalfOf = '';
if (permission.id.startsWith('labelAs-')) {
behalfOf = ON_BEHALF_OF;
}
return `${LABEL} ${permission.value.label}${behalfOf}`;
}
}
_computeSectionName(name) {
// When a new section is created, it doesn't yet have a ref. Set into
// edit mode so that the user can input one.
if (!name) {
this._editingRef = true;
// Needed for the title value. This is the same default as GWT.
name = NEW_NAME;
// Needed for the input field value.
this.set('section.id', name);
}
if (name === GLOBAL_NAME) {
return 'Global Capabilities';
} else if (name.startsWith(REFS_NAME)) {
return `Reference: ${name}`;
}
return name;
}
_handleRemoveReference() {
if (this.section.value.added) {
this.dispatchEvent(new CustomEvent(
'added-section-removed', {bubbles: true, composed: true}));
}
this._deleted = true;
this.section.value.deleted = true;
this.dispatchEvent(
new CustomEvent('access-modified', {bubbles: true, composed: true}));
}
_handleUndoRemove() {
this._deleted = false;
delete this.section.value.deleted;
}
editRefInput() {
return dom(this.root).querySelector(PolymerElement ?
'iron-input.editRefInput' :
'input[is=iron-input].editRefInput');
}
editReference() {
this._editingRef = true;
this.editRefInput().focus();
}
_isEditEnabled(canUpload, ownerOf, sectionId) {
return canUpload || (ownerOf && ownerOf.indexOf(sectionId) >= 0);
}
_computeSectionClass(editing, canUpload, ownerOf, editingRef, deleted) {
const classList = [];
if (editing
&& this._isEditEnabled(canUpload, ownerOf, this.section.id)) {
classList.push('editing');
}
if (editingRef) {
classList.push('editingRef');
}
if (deleted) {
classList.push('deleted');
}
return classList.join(' ');
}
_computeEditBtnClass(name) {
return name === GLOBAL_NAME ? 'global' : '';
}
_handleAddPermission() {
const value = this.$.permissionSelect.value;
const permission = {
id: value,
value: {rules: {}, added: true},
};
// This is needed to update the 'label' property of the
// 'label-<label-name>' permission.
//
// The value from the add permission dropdown will either be
// label-<label-name> or labelAs-<labelName>.
// But, the format of the API response is as such:
// "permissions": {
// "label-Code-Review": {
// "label": "Code-Review",
// "rules": {...}
// }
// }
// }
// When we add a new item, we have to push the new permission in the same
// format as the ones that have been returned by the API.
if (value.startsWith('label')) {
permission.value.label =
value.replace('label-', '').replace('labelAs-', '');
}
// Add to the end of the array (used in dom-repeat) and also to the
// section object that is two way bound with its parent element.
this.push('_permissions', permission);
this.set(['section.value.permissions', permission.id],
permission.value);
}
}
customElements.define(GrAccessSection.is, GrAccessSection);

View File

@ -1,36 +1,22 @@
<!--
@license
Copyright (C) 2017 The Android Open Source Project
/**
* @license
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {html} from '@polymer/polymer/lib/utils/html-tag.js';
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<link rel="import" href="/bower_components/polymer/polymer.html">
<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
<link rel="import" href="../../../behaviors/gr-access-behavior/gr-access-behavior.html">
<link rel="import" href="/bower_components/iron-input/iron-input.html">
<link rel="import" href="../../../styles/gr-form-styles.html">
<link rel="import" href="../../../styles/shared-styles.html">
<link rel="import" href="../../shared/gr-button/gr-button.html">
<link rel="import" href="../../shared/gr-icons/gr-icons.html">
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
<link rel="import" href="../gr-permission/gr-permission.html">
<script src="../../../scripts/util.js"></script>
<dom-module id="gr-access-section">
<template>
export const htmlTemplate = html`
<style include="shared-styles">
:host {
display: block;
@ -89,50 +75,23 @@ limitations under the License.
<style include="gr-form-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
<fieldset id="section"
class$="gr-form-styles [[_computeSectionClass(editing, canUpload, ownerOf, _editingRef, _deleted)]]">
<fieldset id="section" class\$="gr-form-styles [[_computeSectionClass(editing, canUpload, ownerOf, _editingRef, _deleted)]]">
<div id="mainContainer">
<div class="header">
<div class="name">
<h3>[[_computeSectionName(section.id)]]</h3>
<gr-button
id="editBtn"
link
class$="[[_computeEditBtnClass(section.id)]]"
on-click="editReference">
<gr-button id="editBtn" link="" class\$="[[_computeEditBtnClass(section.id)]]" on-click="editReference">
<iron-icon id="icon" icon="gr-icons:create"></iron-icon>
</gr-button>
</div>
<iron-input
class="editRefInput"
bind-value="{{section.id}}"
type="text"
on-input="_handleValueChange">
<input
class="editRefInput"
bind-value="{{section.id}}"
is="iron-input"
type="text"
on-input="_handleValueChange">
<iron-input class="editRefInput" bind-value="{{section.id}}" type="text" on-input="_handleValueChange">
<input class="editRefInput" bind-value="{{section.id}}" is="iron-input" type="text" on-input="_handleValueChange">
</iron-input>
<gr-button
link
id="deleteBtn"
on-click="_handleRemoveReference">Remove</gr-button>
<gr-button link="" id="deleteBtn" on-click="_handleRemoveReference">Remove</gr-button>
</div><!-- end header -->
<div class="sectionContent">
<template
is="dom-repeat"
items="{{_permissions}}"
as="permission">
<gr-permission
name="[[_computePermissionName(section.id, permission, permissionValues, capabilities)]]"
permission="{{permission}}"
labels="[[labels]]"
section="[[section.id]]"
editing="[[editing]]"
groups="[[groups]]"
on-added-permission-removed="_handleAddedPermissionRemoved">
<template is="dom-repeat" items="{{_permissions}}" as="permission">
<gr-permission name="[[_computePermissionName(section.id, permission, permissionValues, capabilities)]]" permission="{{permission}}" labels="[[labels]]" section="[[section.id]]" editing="[[editing]]" groups="[[groups]]" on-added-permission-removed="_handleAddedPermissionRemoved">
</gr-permission>
</template>
<div id="addPermission">
@ -140,29 +99,19 @@ limitations under the License.
<select id="permissionSelect">
<!-- called with a third parameter so that permissions update
after a new section is added. -->
<template
is="dom-repeat"
items="[[_computePermissions(section.id, capabilities, labels, section.value.permissions.*)]]">
<template is="dom-repeat" items="[[_computePermissions(section.id, capabilities, labels, section.value.permissions.*)]]">
<option value="[[item.value.id]]">[[item.value.name]]</option>
</template>
</select>
<gr-button
link
id="addBtn"
on-click="_handleAddPermission">Add</gr-button>
<gr-button link="" id="addBtn" on-click="_handleAddPermission">Add</gr-button>
</div>
<!-- end addPermission -->
</div><!-- end sectionContent -->
</div><!-- end mainContainer -->
<div id="deletedContainer">
<span>[[_computeSectionName(section.id)]] was deleted</span>
<gr-button
link
id="undoRemoveBtn"
on-click="_handleUndoRemove">Undo</gr-button>
<gr-button link="" id="undoRemoveBtn" on-click="_handleUndoRemove">Undo</gr-button>
</div><!-- end deletedContainer -->
</fieldset>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
</template>
<script src="gr-access-section.js"></script>
</dom-module>
`;

View File

@ -19,16 +19,21 @@ limitations under the License.
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-access-section</title>
<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/bower_components/page/page.js"></script>
<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/bower_components/web-component-tester/browser.js"></script>
<script src="../../../test/test-pre-setup.js"></script>
<link rel="import" href="../../../test/common-test-setup.html"/>
<link rel="import" href="gr-access-section.html">
<script src="/node_modules/page/page.js"></script>
<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/components/wct-browser-legacy/browser.js"></script>
<script type="module" src="../../../test/test-pre-setup.js"></script>
<script type="module" src="../../../test/common-test-setup.js"></script>
<script type="module" src="./gr-access-section.js"></script>
<script>void(0);</script>
<script type="module">
import '../../../test/test-pre-setup.js';
import '../../../test/common-test-setup.js';
import './gr-access-section.js';
void(0);
</script>
<test-fixture id="basic">
<template>
@ -36,28 +41,310 @@ limitations under the License.
</template>
</test-fixture>
<script>
suite('gr-access-section tests', async () => {
await readyToTest();
let element;
let sandbox;
<script type="module">
import '../../../test/test-pre-setup.js';
import '../../../test/common-test-setup.js';
import './gr-access-section.js';
suite('gr-access-section tests', () => {
let element;
let sandbox;
setup(() => {
sandbox = sinon.sandbox.create();
element = fixture('basic');
});
teardown(() => {
sandbox.restore();
});
suite('unit tests', () => {
setup(() => {
sandbox = sinon.sandbox.create();
element = fixture('basic');
element.section = {
id: 'refs/*',
value: {
permissions: {
read: {
rules: {},
},
},
},
};
element.capabilities = {
accessDatabase: {
id: 'accessDatabase',
name: 'Access Database',
},
administrateServer: {
id: 'administrateServer',
name: 'Administrate Server',
},
batchChangesLimit: {
id: 'batchChangesLimit',
name: 'Batch Changes Limit',
},
createAccount: {
id: 'createAccount',
name: 'Create Account',
},
};
element.labels = {
'Code-Review': {
values: {
' 0': 'No score',
'-1': 'I would prefer this is not merged as is',
'-2': 'This shall not be merged',
'+1': 'Looks good to me, but someone else must approve',
'+2': 'Looks good to me, approved',
},
default_value: 0,
},
};
element._updateSection(element.section);
flushAsynchronousOperations();
});
teardown(() => {
sandbox.restore();
test('_updateSection', () => {
// _updateSection was called in setup, so just make assertions.
const expectedPermissions = [
{
id: 'read',
value: {
rules: {},
},
},
];
assert.deepEqual(element._permissions, expectedPermissions);
assert.equal(element._originalId, element.section.id);
});
suite('unit tests', () => {
test('_computeLabelOptions', () => {
const expectedLabelOptions = [
{
id: 'label-Code-Review',
value: {
name: 'Label Code-Review',
id: 'label-Code-Review',
},
},
{
id: 'labelAs-Code-Review',
value: {
name: 'Label Code-Review (On Behalf Of)',
id: 'labelAs-Code-Review',
},
},
];
assert.deepEqual(element._computeLabelOptions(element.labels),
expectedLabelOptions);
});
test('_handleAccessSaved', () => {
assert.equal(element._originalId, 'refs/*');
element.section.id = 'refs/for/bar';
element._handleAccessSaved();
assert.equal(element._originalId, 'refs/for/bar');
});
test('_computePermissions', () => {
sandbox.stub(element, 'toSortedArray').returns(
[{
id: 'push',
value: {
rules: {},
},
},
{
id: 'read',
value: {
rules: {},
},
},
]);
const expectedPermissions = [{
id: 'push',
value: {
rules: {},
},
},
];
const labelOptions = [
{
id: 'label-Code-Review',
value: {
name: 'Label Code-Review',
id: 'label-Code-Review',
},
},
{
id: 'labelAs-Code-Review',
value: {
name: 'Label Code-Review (On Behalf Of)',
id: 'labelAs-Code-Review',
},
},
];
// For global capabilities, just return the sorted array filtered by
// existing permissions.
let name = 'GLOBAL_CAPABILITIES';
assert.deepEqual(element._computePermissions(name, element.capabilities,
element.labels), expectedPermissions);
// Uses the capabilities array to come up with possible values.
assert.isTrue(element.toSortedArray.lastCall.
calledWithExactly(element.capabilities));
// For everything else, include possible label values before filtering.
name = 'refs/for/*';
assert.deepEqual(element._computePermissions(name, element.capabilities,
element.labels), labelOptions.concat(expectedPermissions));
// Uses permissionValues (defined in gr-access-behavior) to come up with
// possible values.
assert.isTrue(element.toSortedArray.lastCall.
calledWithExactly(element.permissionValues));
});
test('_computePermissionName', () => {
let name = 'GLOBAL_CAPABILITIES';
let permission = {
id: 'administrateServer',
value: {},
};
assert.equal(element._computePermissionName(name, permission,
element.permissionValues, element.capabilities),
element.capabilities[permission.id].name);
name = 'refs/for/*';
permission = {
id: 'abandon',
value: {},
};
assert.equal(element._computePermissionName(
name, permission, element.permissionValues, element.capabilities),
element.permissionValues[permission.id].name);
name = 'refs/for/*';
permission = {
id: 'label-Code-Review',
value: {
label: 'Code-Review',
},
};
assert.equal(element._computePermissionName(name, permission,
element.permissionValues, element.capabilities),
'Label Code-Review');
permission = {
id: 'labelAs-Code-Review',
value: {
label: 'Code-Review',
},
};
assert.equal(element._computePermissionName(name, permission,
element.permissionValues, element.capabilities),
'Label Code-Review(On Behalf Of)');
});
test('_computeSectionName', () => {
let name;
// When computing the section name for an undefined name, it means a
// new section is being added. In this case, it should defualt to
// 'refs/heads/*'.
element._editingRef = false;
assert.equal(element._computeSectionName(name),
'Reference: refs/heads/*');
assert.isTrue(element._editingRef);
assert.equal(element.section.id, 'refs/heads/*');
// Reset editing to false.
element._editingRef = false;
name = 'GLOBAL_CAPABILITIES';
assert.equal(element._computeSectionName(name), 'Global Capabilities');
assert.isFalse(element._editingRef);
name = 'refs/for/*';
assert.equal(element._computeSectionName(name),
'Reference: refs/for/*');
assert.isFalse(element._editingRef);
});
test('editReference', () => {
element.editReference();
assert.isTrue(element._editingRef);
});
test('_computeSectionClass', () => {
let editingRef = false;
let canUpload = false;
let ownerOf = [];
let editing = false;
let deleted = false;
assert.equal(element._computeSectionClass(editing, canUpload, ownerOf,
editingRef, deleted), '');
editing = true;
assert.equal(element._computeSectionClass(editing, canUpload, ownerOf,
editingRef, deleted), '');
ownerOf = ['refs/*'];
assert.equal(element._computeSectionClass(editing, canUpload, ownerOf,
editingRef, deleted), 'editing');
ownerOf = [];
canUpload = true;
assert.equal(element._computeSectionClass(editing, canUpload, ownerOf,
editingRef, deleted), 'editing');
editingRef = true;
assert.equal(element._computeSectionClass(editing, canUpload, ownerOf,
editingRef, deleted), 'editing editingRef');
deleted = true;
assert.equal(element._computeSectionClass(editing, canUpload, ownerOf,
editingRef, deleted), 'editing editingRef deleted');
editingRef = false;
assert.equal(element._computeSectionClass(editing, canUpload, ownerOf,
editingRef, deleted), 'editing deleted');
});
test('_computeEditBtnClass', () => {
let name = 'GLOBAL_CAPABILITIES';
assert.equal(element._computeEditBtnClass(name), 'global');
name = 'refs/for/*';
assert.equal(element._computeEditBtnClass(name), '');
});
});
suite('interactive tests', () => {
setup(() => {
element.labels = {
'Code-Review': {
values: {
' 0': 'No score',
'-1': 'I would prefer this is not merged as is',
'-2': 'This shall not be merged',
'+1': 'Looks good to me, but someone else must approve',
'+2': 'Looks good to me, approved',
},
default_value: 0,
},
};
});
suite('Global section', () => {
setup(() => {
element.section = {
id: 'refs/*',
id: 'GLOBAL_CAPABILITIES',
value: {
permissions: {
read: {
accessDatabase: {
rules: {},
},
},
@ -81,476 +368,196 @@ limitations under the License.
name: 'Create Account',
},
};
element.labels = {
'Code-Review': {
values: {
' 0': 'No score',
'-1': 'I would prefer this is not merged as is',
'-2': 'This shall not be merged',
'+1': 'Looks good to me, but someone else must approve',
'+2': 'Looks good to me, approved',
},
default_value: 0,
},
};
element._updateSection(element.section);
flushAsynchronousOperations();
});
test('_updateSection', () => {
// _updateSection was called in setup, so just make assertions.
const expectedPermissions = [
{
id: 'read',
value: {
rules: {},
},
},
];
assert.deepEqual(element._permissions, expectedPermissions);
assert.equal(element._originalId, element.section.id);
});
test('_computeLabelOptions', () => {
const expectedLabelOptions = [
{
id: 'label-Code-Review',
value: {
name: 'Label Code-Review',
id: 'label-Code-Review',
},
},
{
id: 'labelAs-Code-Review',
value: {
name: 'Label Code-Review (On Behalf Of)',
id: 'labelAs-Code-Review',
},
},
];
assert.deepEqual(element._computeLabelOptions(element.labels),
expectedLabelOptions);
});
test('_handleAccessSaved', () => {
assert.equal(element._originalId, 'refs/*');
element.section.id = 'refs/for/bar';
element._handleAccessSaved();
assert.equal(element._originalId, 'refs/for/bar');
});
test('_computePermissions', () => {
sandbox.stub(element, 'toSortedArray').returns(
[{
id: 'push',
value: {
rules: {},
},
},
{
id: 'read',
value: {
rules: {},
},
},
]);
const expectedPermissions = [{
id: 'push',
value: {
rules: {},
},
},
];
const labelOptions = [
{
id: 'label-Code-Review',
value: {
name: 'Label Code-Review',
id: 'label-Code-Review',
},
},
{
id: 'labelAs-Code-Review',
value: {
name: 'Label Code-Review (On Behalf Of)',
id: 'labelAs-Code-Review',
},
},
];
// For global capabilities, just return the sorted array filtered by
// existing permissions.
let name = 'GLOBAL_CAPABILITIES';
assert.deepEqual(element._computePermissions(name, element.capabilities,
element.labels), expectedPermissions);
// Uses the capabilities array to come up with possible values.
assert.isTrue(element.toSortedArray.lastCall.
calledWithExactly(element.capabilities));
// For everything else, include possible label values before filtering.
name = 'refs/for/*';
assert.deepEqual(element._computePermissions(name, element.capabilities,
element.labels), labelOptions.concat(expectedPermissions));
// Uses permissionValues (defined in gr-access-behavior) to come up with
// possible values.
assert.isTrue(element.toSortedArray.lastCall.
calledWithExactly(element.permissionValues));
});
test('_computePermissionName', () => {
let name = 'GLOBAL_CAPABILITIES';
let permission = {
id: 'administrateServer',
value: {},
};
assert.equal(element._computePermissionName(name, permission,
element.permissionValues, element.capabilities),
element.capabilities[permission.id].name);
name = 'refs/for/*';
permission = {
id: 'abandon',
value: {},
};
assert.equal(element._computePermissionName(
name, permission, element.permissionValues, element.capabilities),
element.permissionValues[permission.id].name);
name = 'refs/for/*';
permission = {
id: 'label-Code-Review',
value: {
label: 'Code-Review',
},
};
assert.equal(element._computePermissionName(name, permission,
element.permissionValues, element.capabilities),
'Label Code-Review');
permission = {
id: 'labelAs-Code-Review',
value: {
label: 'Code-Review',
},
};
assert.equal(element._computePermissionName(name, permission,
element.permissionValues, element.capabilities),
'Label Code-Review(On Behalf Of)');
});
test('_computeSectionName', () => {
let name;
// When computing the section name for an undefined name, it means a
// new section is being added. In this case, it should defualt to
// 'refs/heads/*'.
element._editingRef = false;
assert.equal(element._computeSectionName(name),
'Reference: refs/heads/*');
assert.isTrue(element._editingRef);
assert.equal(element.section.id, 'refs/heads/*');
// Reset editing to false.
element._editingRef = false;
name = 'GLOBAL_CAPABILITIES';
assert.equal(element._computeSectionName(name), 'Global Capabilities');
assert.isFalse(element._editingRef);
name = 'refs/for/*';
assert.equal(element._computeSectionName(name),
'Reference: refs/for/*');
assert.isFalse(element._editingRef);
});
test('editReference', () => {
element.editReference();
assert.isTrue(element._editingRef);
});
test('_computeSectionClass', () => {
let editingRef = false;
let canUpload = false;
let ownerOf = [];
let editing = false;
let deleted = false;
assert.equal(element._computeSectionClass(editing, canUpload, ownerOf,
editingRef, deleted), '');
editing = true;
assert.equal(element._computeSectionClass(editing, canUpload, ownerOf,
editingRef, deleted), '');
ownerOf = ['refs/*'];
assert.equal(element._computeSectionClass(editing, canUpload, ownerOf,
editingRef, deleted), 'editing');
ownerOf = [];
canUpload = true;
assert.equal(element._computeSectionClass(editing, canUpload, ownerOf,
editingRef, deleted), 'editing');
editingRef = true;
assert.equal(element._computeSectionClass(editing, canUpload, ownerOf,
editingRef, deleted), 'editing editingRef');
deleted = true;
assert.equal(element._computeSectionClass(editing, canUpload, ownerOf,
editingRef, deleted), 'editing editingRef deleted');
editingRef = false;
assert.equal(element._computeSectionClass(editing, canUpload, ownerOf,
editingRef, deleted), 'editing deleted');
});
test('_computeEditBtnClass', () => {
let name = 'GLOBAL_CAPABILITIES';
assert.equal(element._computeEditBtnClass(name), 'global');
name = 'refs/for/*';
assert.equal(element._computeEditBtnClass(name), '');
test('classes are assigned correctly', () => {
assert.isFalse(element.$.section.classList.contains('editing'));
assert.isFalse(element.$.section.classList.contains('deleted'));
assert.isTrue(element.$.editBtn.classList.contains('global'));
element.editing = true;
element.canUpload = true;
element.ownerOf = [];
assert.equal(getComputedStyle(element.$.editBtn).display, 'none');
});
});
suite('interactive tests', () => {
suite('Non-global section', () => {
setup(() => {
element.labels = {
'Code-Review': {
values: {
' 0': 'No score',
'-1': 'I would prefer this is not merged as is',
'-2': 'This shall not be merged',
'+1': 'Looks good to me, but someone else must approve',
'+2': 'Looks good to me, approved',
element.section = {
id: 'refs/*',
value: {
permissions: {
read: {
rules: {},
},
},
default_value: 0,
},
};
});
suite('Global section', () => {
setup(() => {
element.section = {
id: 'GLOBAL_CAPABILITIES',
value: {
permissions: {
accessDatabase: {
rules: {},
},
},
},
};
element.capabilities = {
accessDatabase: {
id: 'accessDatabase',
name: 'Access Database',
},
administrateServer: {
id: 'administrateServer',
name: 'Administrate Server',
},
batchChangesLimit: {
id: 'batchChangesLimit',
name: 'Batch Changes Limit',
},
createAccount: {
id: 'createAccount',
name: 'Create Account',
},
};
element._updateSection(element.section);
flushAsynchronousOperations();
});
test('classes are assigned correctly', () => {
assert.isFalse(element.$.section.classList.contains('editing'));
assert.isFalse(element.$.section.classList.contains('deleted'));
assert.isTrue(element.$.editBtn.classList.contains('global'));
element.editing = true;
element.canUpload = true;
element.ownerOf = [];
assert.equal(getComputedStyle(element.$.editBtn).display, 'none');
});
element.capabilities = {};
element._updateSection(element.section);
flushAsynchronousOperations();
});
suite('Non-global section', () => {
setup(() => {
element.section = {
id: 'refs/*',
value: {
permissions: {
read: {
rules: {},
},
},
},
};
element.capabilities = {};
element._updateSection(element.section);
flushAsynchronousOperations();
});
test('classes are assigned correctly', () => {
assert.isFalse(element.$.section.classList.contains('editing'));
assert.isFalse(element.$.section.classList.contains('deleted'));
assert.isFalse(element.$.editBtn.classList.contains('global'));
element.editing = true;
element.canUpload = true;
element.ownerOf = [];
flushAsynchronousOperations();
assert.notEqual(getComputedStyle(element.$.editBtn).display, 'none');
});
test('classes are assigned correctly', () => {
assert.isFalse(element.$.section.classList.contains('editing'));
assert.isFalse(element.$.section.classList.contains('deleted'));
assert.isFalse(element.$.editBtn.classList.contains('global'));
element.editing = true;
element.canUpload = true;
element.ownerOf = [];
flushAsynchronousOperations();
assert.notEqual(getComputedStyle(element.$.editBtn).display, 'none');
});
test('add permission', () => {
element.editing = true;
element.$.permissionSelect.value = 'label-Code-Review';
assert.equal(element._permissions.length, 1);
assert.equal(Object.keys(element.section.value.permissions).length,
1);
MockInteractions.tap(element.$.addBtn);
flushAsynchronousOperations();
test('add permission', () => {
element.editing = true;
element.$.permissionSelect.value = 'label-Code-Review';
assert.equal(element._permissions.length, 1);
assert.equal(Object.keys(element.section.value.permissions).length,
1);
MockInteractions.tap(element.$.addBtn);
flushAsynchronousOperations();
// The permission is added to both the permissions array and also
// the section's permission object.
assert.equal(element._permissions.length, 2);
let permission = {
id: 'label-Code-Review',
value: {
added: true,
label: 'Code-Review',
rules: {},
},
};
assert.equal(element._permissions.length, 2);
assert.deepEqual(element._permissions[1], permission);
assert.equal(Object.keys(element.section.value.permissions).length,
2);
assert.deepEqual(
element.section.value.permissions['label-Code-Review'],
permission.value);
// The permission is added to both the permissions array and also
// the section's permission object.
assert.equal(element._permissions.length, 2);
let permission = {
id: 'label-Code-Review',
value: {
added: true,
label: 'Code-Review',
rules: {},
},
};
assert.equal(element._permissions.length, 2);
assert.deepEqual(element._permissions[1], permission);
assert.equal(Object.keys(element.section.value.permissions).length,
2);
assert.deepEqual(
element.section.value.permissions['label-Code-Review'],
permission.value);
element.$.permissionSelect.value = 'abandon';
MockInteractions.tap(element.$.addBtn);
flushAsynchronousOperations();
element.$.permissionSelect.value = 'abandon';
MockInteractions.tap(element.$.addBtn);
flushAsynchronousOperations();
permission = {
id: 'abandon',
value: {
added: true,
rules: {},
},
};
permission = {
id: 'abandon',
value: {
added: true,
rules: {},
},
};
assert.equal(element._permissions.length, 3);
assert.deepEqual(element._permissions[2], permission);
assert.equal(Object.keys(element.section.value.permissions).length,
3);
assert.deepEqual(element.section.value.permissions['abandon'],
permission.value);
assert.equal(element._permissions.length, 3);
assert.deepEqual(element._permissions[2], permission);
assert.equal(Object.keys(element.section.value.permissions).length,
3);
assert.deepEqual(element.section.value.permissions['abandon'],
permission.value);
// Unsaved changes are discarded when editing is cancelled.
element.editing = false;
assert.equal(element._permissions.length, 1);
assert.equal(Object.keys(element.section.value.permissions).length,
1);
});
// Unsaved changes are discarded when editing is cancelled.
test('edit section reference', done => {
element.canUpload = true;
element.ownerOf = [];
element.section = {id: 'refs/for/bar', value: {permissions: {}}};
assert.isFalse(element.$.section.classList.contains('editing'));
element.editing = true;
assert.isTrue(element.$.section.classList.contains('editing'));
assert.isFalse(element._editingRef);
MockInteractions.tap(element.$.editBtn);
element.editRefInput().bindValue='new/ref';
setTimeout(() => {
assert.equal(element.section.id, 'new/ref');
assert.isTrue(element._editingRef);
assert.isTrue(element.$.section.classList.contains('editingRef'));
element.editing = false;
assert.equal(element._permissions.length, 1);
assert.equal(Object.keys(element.section.value.permissions).length,
1);
});
test('edit section reference', done => {
element.canUpload = true;
element.ownerOf = [];
element.section = {id: 'refs/for/bar', value: {permissions: {}}};
assert.isFalse(element.$.section.classList.contains('editing'));
element.editing = true;
assert.isTrue(element.$.section.classList.contains('editing'));
assert.isFalse(element._editingRef);
MockInteractions.tap(element.$.editBtn);
element.editRefInput().bindValue='new/ref';
setTimeout(() => {
assert.equal(element.section.id, 'new/ref');
assert.isTrue(element._editingRef);
assert.isTrue(element.$.section.classList.contains('editingRef'));
element.editing = false;
assert.isFalse(element._editingRef);
assert.equal(element.section.id, 'refs/for/bar');
done();
});
assert.equal(element.section.id, 'refs/for/bar');
done();
});
});
test('_handleValueChange', () => {
// For an exising section.
const modifiedHandler = sandbox.stub();
element.section = {id: 'refs/for/bar', value: {permissions: {}}};
assert.notOk(element.section.value.updatedId);
element.section.id = 'refs/for/baz';
element.addEventListener('access-modified', modifiedHandler);
assert.isNotOk(element.section.value.modified);
element._handleValueChange();
assert.equal(element.section.value.updatedId, 'refs/for/baz');
assert.isTrue(element.section.value.modified);
assert.equal(modifiedHandler.callCount, 1);
element.section.id = 'refs/for/bar';
element._handleValueChange();
assert.isFalse(element.section.value.modified);
assert.equal(modifiedHandler.callCount, 2);
test('_handleValueChange', () => {
// For an exising section.
const modifiedHandler = sandbox.stub();
element.section = {id: 'refs/for/bar', value: {permissions: {}}};
assert.notOk(element.section.value.updatedId);
element.section.id = 'refs/for/baz';
element.addEventListener('access-modified', modifiedHandler);
assert.isNotOk(element.section.value.modified);
element._handleValueChange();
assert.equal(element.section.value.updatedId, 'refs/for/baz');
assert.isTrue(element.section.value.modified);
assert.equal(modifiedHandler.callCount, 1);
element.section.id = 'refs/for/bar';
element._handleValueChange();
assert.isFalse(element.section.value.modified);
assert.equal(modifiedHandler.callCount, 2);
// For a new section.
element.section.value.added = true;
element._handleValueChange();
assert.isFalse(element.section.value.modified);
assert.equal(modifiedHandler.callCount, 2);
element.section.id = 'refs/for/bar';
element._handleValueChange();
assert.isFalse(element.section.value.modified);
assert.equal(modifiedHandler.callCount, 2);
});
// For a new section.
element.section.value.added = true;
element._handleValueChange();
assert.isFalse(element.section.value.modified);
assert.equal(modifiedHandler.callCount, 2);
element.section.id = 'refs/for/bar';
element._handleValueChange();
assert.isFalse(element.section.value.modified);
assert.equal(modifiedHandler.callCount, 2);
});
test('remove section', () => {
element.editing = true;
element.canUpload = true;
element.ownerOf = [];
assert.isFalse(element._deleted);
assert.isNotOk(element.section.value.deleted);
MockInteractions.tap(element.$.deleteBtn);
flushAsynchronousOperations();
assert.isTrue(element._deleted);
assert.isTrue(element.section.value.deleted);
assert.isTrue(element.$.section.classList.contains('deleted'));
assert.isTrue(element.section.value.deleted);
test('remove section', () => {
element.editing = true;
element.canUpload = true;
element.ownerOf = [];
assert.isFalse(element._deleted);
assert.isNotOk(element.section.value.deleted);
MockInteractions.tap(element.$.deleteBtn);
flushAsynchronousOperations();
assert.isTrue(element._deleted);
assert.isTrue(element.section.value.deleted);
assert.isTrue(element.$.section.classList.contains('deleted'));
assert.isTrue(element.section.value.deleted);
MockInteractions.tap(element.$.undoRemoveBtn);
flushAsynchronousOperations();
assert.isFalse(element._deleted);
assert.isNotOk(element.section.value.deleted);
MockInteractions.tap(element.$.undoRemoveBtn);
flushAsynchronousOperations();
assert.isFalse(element._deleted);
assert.isNotOk(element.section.value.deleted);
MockInteractions.tap(element.$.deleteBtn);
assert.isTrue(element._deleted);
assert.isTrue(element.section.value.deleted);
element.editing = false;
assert.isFalse(element._deleted);
assert.isNotOk(element.section.value.deleted);
});
MockInteractions.tap(element.$.deleteBtn);
assert.isTrue(element._deleted);
assert.isTrue(element.section.value.deleted);
element.editing = false;
assert.isFalse(element._deleted);
assert.isNotOk(element.section.value.deleted);
});
test('removing an added permission', () => {
element.editing = true;
assert.equal(element._permissions.length, 1);
element.shadowRoot
.querySelector('gr-permission').fire('added-permission-removed');
flushAsynchronousOperations();
assert.equal(element._permissions.length, 0);
});
test('removing an added permission', () => {
element.editing = true;
assert.equal(element._permissions.length, 1);
element.shadowRoot
.querySelector('gr-permission').fire('added-permission-removed');
flushAsynchronousOperations();
assert.equal(element._permissions.length, 0);
});
test('remove an added section', () => {
const removeStub = sandbox.stub();
element.addEventListener('added-section-removed', removeStub);
element.editing = true;
element.section.value.added = true;
MockInteractions.tap(element.$.deleteBtn);
assert.isTrue(removeStub.called);
});
test('remove an added section', () => {
const removeStub = sandbox.stub();
element.addEventListener('added-section-removed', removeStub);
element.editing = true;
element.section.value.added = true;
MockInteractions.tap(element.$.deleteBtn);
assert.isTrue(removeStub.called);
});
});
});
});
</script>

View File

@ -14,155 +14,171 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function() {
'use strict';
import '../../../scripts/bundled-polymer.js';
import '../../../behaviors/fire-behavior/fire-behavior.js';
import '../../../behaviors/gr-list-view-behavior/gr-list-view-behavior.js';
import '../../../styles/gr-table-styles.js';
import '../../../styles/shared-styles.js';
import '../../core/gr-navigation/gr-navigation.js';
import '../../shared/gr-dialog/gr-dialog.js';
import '../../shared/gr-list-view/gr-list-view.js';
import '../../shared/gr-overlay/gr-overlay.js';
import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
import '../gr-create-group-dialog/gr-create-group-dialog.js';
import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
import {PolymerElement} from '@polymer/polymer/polymer-element.js';
import {htmlTemplate} from './gr-admin-group-list_html.js';
/**
* @appliesMixin Gerrit.FireMixin
* @appliesMixin Gerrit.ListViewMixin
* @extends Polymer.Element
*/
class GrAdminGroupList extends mixinBehaviors( [
Gerrit.FireBehavior,
Gerrit.ListViewBehavior,
], GestureEventListeners(
LegacyElementMixin(
PolymerElement))) {
static get template() { return htmlTemplate; }
static get is() { return 'gr-admin-group-list'; }
static get properties() {
return {
/**
* URL params passed from the router.
*/
params: {
type: Object,
observer: '_paramsChanged',
},
/**
* Offset of currently visible query results.
*/
_offset: Number,
_path: {
type: String,
readOnly: true,
value: '/admin/groups',
},
_hasNewGroupName: Boolean,
_createNewCapability: {
type: Boolean,
value: false,
},
_groups: Array,
/**
* Because we request one more than the groupsPerPage, _shownGroups
* may be one less than _groups.
* */
_shownGroups: {
type: Array,
computed: 'computeShownItems(_groups)',
},
_groupsPerPage: {
type: Number,
value: 25,
},
_loading: {
type: Boolean,
value: true,
},
_filter: String,
};
}
/** @override */
attached() {
super.attached();
this._getCreateGroupCapability();
this.fire('title-change', {title: 'Groups'});
this._maybeOpenCreateOverlay(this.params);
}
_paramsChanged(params) {
this._loading = true;
this._filter = this.getFilterValue(params);
this._offset = this.getOffsetValue(params);
return this._getGroups(this._filter, this._groupsPerPage,
this._offset);
}
/**
* @appliesMixin Gerrit.FireMixin
* @appliesMixin Gerrit.ListViewMixin
* @extends Polymer.Element
* Opens the create overlay if the route has a hash 'create'
*
* @param {!Object} params
*/
class GrAdminGroupList extends Polymer.mixinBehaviors( [
Gerrit.FireBehavior,
Gerrit.ListViewBehavior,
], Polymer.GestureEventListeners(
Polymer.LegacyElementMixin(
Polymer.Element))) {
static get is() { return 'gr-admin-group-list'; }
static get properties() {
return {
/**
* URL params passed from the router.
*/
params: {
type: Object,
observer: '_paramsChanged',
},
/**
* Offset of currently visible query results.
*/
_offset: Number,
_path: {
type: String,
readOnly: true,
value: '/admin/groups',
},
_hasNewGroupName: Boolean,
_createNewCapability: {
type: Boolean,
value: false,
},
_groups: Array,
/**
* Because we request one more than the groupsPerPage, _shownGroups
* may be one less than _groups.
* */
_shownGroups: {
type: Array,
computed: 'computeShownItems(_groups)',
},
_groupsPerPage: {
type: Number,
value: 25,
},
_loading: {
type: Boolean,
value: true,
},
_filter: String,
};
}
/** @override */
attached() {
super.attached();
this._getCreateGroupCapability();
this.fire('title-change', {title: 'Groups'});
this._maybeOpenCreateOverlay(this.params);
}
_paramsChanged(params) {
this._loading = true;
this._filter = this.getFilterValue(params);
this._offset = this.getOffsetValue(params);
return this._getGroups(this._filter, this._groupsPerPage,
this._offset);
}
/**
* Opens the create overlay if the route has a hash 'create'
*
* @param {!Object} params
*/
_maybeOpenCreateOverlay(params) {
if (params && params.openCreateModal) {
this.$.createOverlay.open();
}
}
_computeGroupUrl(id) {
return Gerrit.Nav.getUrlForGroup(id);
}
_getCreateGroupCapability() {
return this.$.restAPI.getAccount().then(account => {
if (!account) { return; }
return this.$.restAPI.getAccountCapabilities(['createGroup'])
.then(capabilities => {
if (capabilities.createGroup) {
this._createNewCapability = true;
}
});
});
}
_getGroups(filter, groupsPerPage, offset) {
this._groups = [];
return this.$.restAPI.getGroups(filter, groupsPerPage, offset)
.then(groups => {
if (!groups) {
return;
}
this._groups = Object.keys(groups)
.map(key => {
const group = groups[key];
group.name = key;
return group;
});
this._loading = false;
});
}
_refreshGroupsList() {
this.$.restAPI.invalidateGroupsCache();
return this._getGroups(this._filter, this._groupsPerPage,
this._offset);
}
_handleCreateGroup() {
this.$.createNewModal.handleCreateGroup().then(() => {
this._refreshGroupsList();
});
}
_handleCloseCreate() {
this.$.createOverlay.close();
}
_handleCreateClicked() {
_maybeOpenCreateOverlay(params) {
if (params && params.openCreateModal) {
this.$.createOverlay.open();
}
_visibleToAll(item) {
return item.options.visible_to_all === true ? 'Y' : 'N';
}
}
customElements.define(GrAdminGroupList.is, GrAdminGroupList);
})();
_computeGroupUrl(id) {
return Gerrit.Nav.getUrlForGroup(id);
}
_getCreateGroupCapability() {
return this.$.restAPI.getAccount().then(account => {
if (!account) { return; }
return this.$.restAPI.getAccountCapabilities(['createGroup'])
.then(capabilities => {
if (capabilities.createGroup) {
this._createNewCapability = true;
}
});
});
}
_getGroups(filter, groupsPerPage, offset) {
this._groups = [];
return this.$.restAPI.getGroups(filter, groupsPerPage, offset)
.then(groups => {
if (!groups) {
return;
}
this._groups = Object.keys(groups)
.map(key => {
const group = groups[key];
group.name = key;
return group;
});
this._loading = false;
});
}
_refreshGroupsList() {
this.$.restAPI.invalidateGroupsCache();
return this._getGroups(this._filter, this._groupsPerPage,
this._offset);
}
_handleCreateGroup() {
this.$.createNewModal.handleCreateGroup().then(() => {
this._refreshGroupsList();
});
}
_handleCloseCreate() {
this.$.createOverlay.close();
}
_handleCreateClicked() {
this.$.createOverlay.open();
}
_visibleToAll(item) {
return item.options.visible_to_all === true ? 'Y' : 'N';
}
}
customElements.define(GrAdminGroupList.is, GrAdminGroupList);

View File

@ -1,64 +1,43 @@
<!--
@license
Copyright (C) 2017 The Android Open Source Project
/**
* @license
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {html} from '@polymer/polymer/lib/utils/html-tag.js';
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<link rel="import" href="/bower_components/polymer/polymer.html">
<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
<link rel="import" href="../../../behaviors/gr-list-view-behavior/gr-list-view-behavior.html">
<link rel="import" href="../../../styles/gr-table-styles.html">
<link rel="import" href="../../../styles/shared-styles.html">
<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
<link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
<link rel="import" href="../../shared/gr-list-view/gr-list-view.html">
<link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
<link rel="import" href="../gr-create-group-dialog/gr-create-group-dialog.html">
<dom-module id="gr-admin-group-list">
<template>
export const htmlTemplate = html`
<style include="shared-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
<style include="gr-table-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
<gr-list-view
create-new="[[_createNewCapability]]"
filter="[[_filter]]"
items="[[_groups]]"
items-per-page="[[_groupsPerPage]]"
loading="[[_loading]]"
offset="[[_offset]]"
on-create-clicked="_handleCreateClicked"
path="[[_path]]">
<gr-list-view create-new="[[_createNewCapability]]" filter="[[_filter]]" items="[[_groups]]" items-per-page="[[_groupsPerPage]]" loading="[[_loading]]" offset="[[_offset]]" on-create-clicked="_handleCreateClicked" path="[[_path]]">
<table id="list" class="genericList">
<tr class="headerRow">
<tbody><tr class="headerRow">
<th class="name topHeader">Group Name</th>
<th class="description topHeader">Group Description</th>
<th class="visibleToAll topHeader">Visible To All</th>
</tr>
<tr id="loading" class$="loadingMsg [[computeLoadingClass(_loading)]]">
<tr id="loading" class\$="loadingMsg [[computeLoadingClass(_loading)]]">
<td>Loading...</td>
</tr>
<tbody class$="[[computeLoadingClass(_loading)]]">
</tbody><tbody class\$="[[computeLoadingClass(_loading)]]">
<template is="dom-repeat" items="[[_shownGroups]]">
<tr class="table">
<td class="name">
<a href$="[[_computeGroupUrl(item.group_id)]]">[[item.name]]</a>
<a href\$="[[_computeGroupUrl(item.group_id)]]">[[item.name]]</a>
</td>
<td class="description">[[item.description]]</td>
<td class="visibleToAll">[[_visibleToAll(item)]]</td>
@ -67,27 +46,15 @@ limitations under the License.
</tbody>
</table>
</gr-list-view>
<gr-overlay id="createOverlay" with-backdrop>
<gr-dialog
id="createDialog"
class="confirmDialog"
disabled="[[!_hasNewGroupName]]"
confirm-label="Create"
confirm-on-enter
on-confirm="_handleCreateGroup"
on-cancel="_handleCloseCreate">
<gr-overlay id="createOverlay" with-backdrop="">
<gr-dialog id="createDialog" class="confirmDialog" disabled="[[!_hasNewGroupName]]" confirm-label="Create" confirm-on-enter="" on-confirm="_handleCreateGroup" on-cancel="_handleCloseCreate">
<div class="header" slot="header">
Create Group
</div>
<div class="main" slot="main">
<gr-create-group-dialog
has-new-group-name="{{_hasNewGroupName}}"
params="[[params]]"
id="createNewModal"></gr-create-group-dialog>
<gr-create-group-dialog has-new-group-name="{{_hasNewGroupName}}" params="[[params]]" id="createNewModal"></gr-create-group-dialog>
</div>
</gr-dialog>
</gr-overlay>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
</template>
<script src="gr-admin-group-list.js"></script>
</dom-module>
`;

View File

@ -19,18 +19,23 @@ limitations under the License.
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-admin-group-list</title>
<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/bower_components/page/page.js"></script>
<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/bower_components/web-component-tester/browser.js"></script>
<script src="/node_modules/page/page.js"></script>
<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/components/wct-browser-legacy/browser.js"></script>
<script src="../../../test/test-pre-setup.js"></script>
<link rel="import" href="../../../test/common-test-setup.html"/>
<script type="module" src="../../../test/test-pre-setup.js"></script>
<script type="module" src="../../../test/common-test-setup.js"></script>
<link rel="import" href="gr-admin-group-list.html">
<script type="module" src="./gr-admin-group-list.js"></script>
<script>void(0);</script>
<script type="module">
import '../../../test/test-pre-setup.js';
import '../../../test/common-test-setup.js';
import './gr-admin-group-list.js';
void(0);
</script>
<test-fixture id="basic">
<template>
@ -38,153 +43,155 @@ limitations under the License.
</template>
</test-fixture>
<script>
let counter = 0;
const groupGenerator = () => {
return {
name: `test${++counter}`,
id: '59b92f35489e62c80d1ab1bf0c2d17843038df8b',
url: '#/admin/groups/uuid-59b92f35489e62c80d1ab1bf0c2d17843038df8b',
options: {
visible_to_all: false,
},
description: 'Gerrit Site Administrators',
group_id: 1,
owner: 'Administrators',
owner_id: '7ca042f4d5847936fcb90ca91057673157fd06fc',
};
<script type="module">
import '../../../test/test-pre-setup.js';
import '../../../test/common-test-setup.js';
import './gr-admin-group-list.js';
let counter = 0;
const groupGenerator = () => {
return {
name: `test${++counter}`,
id: '59b92f35489e62c80d1ab1bf0c2d17843038df8b',
url: '#/admin/groups/uuid-59b92f35489e62c80d1ab1bf0c2d17843038df8b',
options: {
visible_to_all: false,
},
description: 'Gerrit Site Administrators',
group_id: 1,
owner: 'Administrators',
owner_id: '7ca042f4d5847936fcb90ca91057673157fd06fc',
};
};
suite('gr-admin-group-list tests', async () => {
await readyToTest();
let element;
let groups;
let sandbox;
let value;
suite('gr-admin-group-list tests', () => {
let element;
let groups;
let sandbox;
let value;
setup(() => {
sandbox = sinon.sandbox.create();
element = fixture('basic');
setup(() => {
sandbox = sinon.sandbox.create();
element = fixture('basic');
});
teardown(() => {
sandbox.restore();
});
suite('list with groups', () => {
setup(done => {
groups = _.times(26, groupGenerator);
stub('gr-rest-api-interface', {
getGroups(num, offset) {
return Promise.resolve(groups);
},
});
element._paramsChanged(value).then(() => { flush(done); });
});
teardown(() => {
sandbox.restore();
});
suite('list with groups', () => {
setup(done => {
groups = _.times(26, groupGenerator);
stub('gr-rest-api-interface', {
getGroups(num, offset) {
return Promise.resolve(groups);
},
});
element._paramsChanged(value).then(() => { flush(done); });
});
test('test for test group in the list', done => {
flush(() => {
assert.equal(element._groups[1].name, '1');
assert.equal(element._groups[1].options.visible_to_all, false);
done();
});
});
test('_shownGroups', () => {
assert.equal(element._shownGroups.length, 25);
});
test('_maybeOpenCreateOverlay', () => {
const overlayOpen = sandbox.stub(element.$.createOverlay, 'open');
element._maybeOpenCreateOverlay();
assert.isFalse(overlayOpen.called);
const params = {};
element._maybeOpenCreateOverlay(params);
assert.isFalse(overlayOpen.called);
params.openCreateModal = true;
element._maybeOpenCreateOverlay(params);
assert.isTrue(overlayOpen.called);
test('test for test group in the list', done => {
flush(() => {
assert.equal(element._groups[1].name, '1');
assert.equal(element._groups[1].options.visible_to_all, false);
done();
});
});
suite('test with less then 25 groups', () => {
setup(done => {
groups = _.times(25, groupGenerator);
stub('gr-rest-api-interface', {
getGroups(num, offset) {
return Promise.resolve(groups);
},
});
element._paramsChanged(value).then(() => { flush(done); });
});
test('_shownGroups', () => {
assert.equal(element._shownGroups.length, 25);
});
test('_shownGroups', () => {
assert.equal(element._shownGroups.length, 25);
});
suite('filter', () => {
test('_paramsChanged', done => {
sandbox.stub(
element.$.restAPI,
'getGroups',
() => Promise.resolve(groups));
const value = {
filter: 'test',
offset: 25,
};
element._paramsChanged(value).then(() => {
assert.isTrue(element.$.restAPI.getGroups.lastCall
.calledWithExactly('test', 25, 25));
done();
});
test('_maybeOpenCreateOverlay', () => {
const overlayOpen = sandbox.stub(element.$.createOverlay, 'open');
element._maybeOpenCreateOverlay();
assert.isFalse(overlayOpen.called);
const params = {};
element._maybeOpenCreateOverlay(params);
assert.isFalse(overlayOpen.called);
params.openCreateModal = true;
element._maybeOpenCreateOverlay(params);
assert.isTrue(overlayOpen.called);
});
});
suite('test with less then 25 groups', () => {
setup(done => {
groups = _.times(25, groupGenerator);
stub('gr-rest-api-interface', {
getGroups(num, offset) {
return Promise.resolve(groups);
},
});
element._paramsChanged(value).then(() => { flush(done); });
});
suite('loading', () => {
test('correct contents are displayed', () => {
assert.isTrue(element._loading);
assert.equal(element.computeLoadingClass(element._loading), 'loading');
assert.equal(getComputedStyle(element.$.loading).display, 'block');
element._loading = false;
element._groups = _.times(25, groupGenerator);
flushAsynchronousOperations();
assert.equal(element.computeLoadingClass(element._loading), '');
assert.equal(getComputedStyle(element.$.loading).display, 'none');
});
test('_shownGroups', () => {
assert.equal(element._shownGroups.length, 25);
});
});
suite('create new', () => {
test('_handleCreateClicked called when create-click fired', () => {
sandbox.stub(element, '_handleCreateClicked');
element.shadowRoot
.querySelector('gr-list-view').fire('create-clicked');
assert.isTrue(element._handleCreateClicked.called);
});
test('_handleCreateClicked opens modal', () => {
const openStub = sandbox.stub(element.$.createOverlay, 'open');
element._handleCreateClicked();
assert.isTrue(openStub.called);
});
test('_handleCreateGroup called when confirm fired', () => {
sandbox.stub(element, '_handleCreateGroup');
element.$.createDialog.fire('confirm');
assert.isTrue(element._handleCreateGroup.called);
});
test('_handleCloseCreate called when cancel fired', () => {
sandbox.stub(element, '_handleCloseCreate');
element.$.createDialog.fire('cancel');
assert.isTrue(element._handleCloseCreate.called);
suite('filter', () => {
test('_paramsChanged', done => {
sandbox.stub(
element.$.restAPI,
'getGroups',
() => Promise.resolve(groups));
const value = {
filter: 'test',
offset: 25,
};
element._paramsChanged(value).then(() => {
assert.isTrue(element.$.restAPI.getGroups.lastCall
.calledWithExactly('test', 25, 25));
done();
});
});
});
suite('loading', () => {
test('correct contents are displayed', () => {
assert.isTrue(element._loading);
assert.equal(element.computeLoadingClass(element._loading), 'loading');
assert.equal(getComputedStyle(element.$.loading).display, 'block');
element._loading = false;
element._groups = _.times(25, groupGenerator);
flushAsynchronousOperations();
assert.equal(element.computeLoadingClass(element._loading), '');
assert.equal(getComputedStyle(element.$.loading).display, 'none');
});
});
suite('create new', () => {
test('_handleCreateClicked called when create-click fired', () => {
sandbox.stub(element, '_handleCreateClicked');
element.shadowRoot
.querySelector('gr-list-view').fire('create-clicked');
assert.isTrue(element._handleCreateClicked.called);
});
test('_handleCreateClicked opens modal', () => {
const openStub = sandbox.stub(element.$.createOverlay, 'open');
element._handleCreateClicked();
assert.isTrue(openStub.called);
});
test('_handleCreateGroup called when confirm fired', () => {
sandbox.stub(element, '_handleCreateGroup');
element.$.createDialog.fire('confirm');
assert.isTrue(element._handleCreateGroup.called);
});
test('_handleCloseCreate called when cancel fired', () => {
sandbox.stub(element, '_handleCloseCreate');
element.$.createDialog.fire('cancel');
assert.isTrue(element._handleCloseCreate.called);
});
});
});
</script>

View File

@ -14,281 +14,310 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function() {
'use strict';
import '../../../scripts/bundled-polymer.js';
const INTERNAL_GROUP_REGEX = /^[\da-f]{40}$/;
import '../../../behaviors/base-url-behavior/base-url-behavior.js';
import '../../../behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior.js';
import '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
import '../../../styles/gr-menu-page-styles.js';
import '../../../styles/gr-page-nav-styles.js';
import '../../../styles/shared-styles.js';
import '../../core/gr-navigation/gr-navigation.js';
import '../../shared/gr-dropdown-list/gr-dropdown-list.js';
import '../../shared/gr-icons/gr-icons.js';
import '../../shared/gr-js-api-interface/gr-js-api-interface.js';
import '../../shared/gr-page-nav/gr-page-nav.js';
import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
import '../gr-admin-group-list/gr-admin-group-list.js';
import '../gr-group/gr-group.js';
import '../gr-group-audit-log/gr-group-audit-log.js';
import '../gr-group-members/gr-group-members.js';
import '../gr-plugin-list/gr-plugin-list.js';
import '../gr-repo/gr-repo.js';
import '../gr-repo-access/gr-repo-access.js';
import '../gr-repo-commands/gr-repo-commands.js';
import '../gr-repo-dashboards/gr-repo-dashboards.js';
import '../gr-repo-detail-list/gr-repo-detail-list.js';
import '../gr-repo-list/gr-repo-list.js';
import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
import {PolymerElement} from '@polymer/polymer/polymer-element.js';
import {htmlTemplate} from './gr-admin-view_html.js';
/**
* @appliesMixin Gerrit.AdminNavMixin
* @appliesMixin Gerrit.BaseUrlMixin
* @appliesMixin Gerrit.URLEncodingMixin
* @extends Polymer.Element
*/
class GrAdminView extends Polymer.mixinBehaviors( [
Gerrit.AdminNavBehavior,
Gerrit.BaseUrlBehavior,
Gerrit.URLEncodingBehavior,
], Polymer.GestureEventListeners(
Polymer.LegacyElementMixin(
Polymer.Element))) {
static get is() { return 'gr-admin-view'; }
const INTERNAL_GROUP_REGEX = /^[\da-f]{40}$/;
static get properties() {
return {
/** @type {?} */
params: Object,
path: String,
adminView: String,
/**
* @appliesMixin Gerrit.AdminNavMixin
* @appliesMixin Gerrit.BaseUrlMixin
* @appliesMixin Gerrit.URLEncodingMixin
* @extends Polymer.Element
*/
class GrAdminView extends mixinBehaviors( [
Gerrit.AdminNavBehavior,
Gerrit.BaseUrlBehavior,
Gerrit.URLEncodingBehavior,
], GestureEventListeners(
LegacyElementMixin(
PolymerElement))) {
static get template() { return htmlTemplate; }
_breadcrumbParentName: String,
_repoName: String,
_groupId: {
type: Number,
observer: '_computeGroupName',
},
_groupIsInternal: Boolean,
_groupName: String,
_groupOwner: {
type: Boolean,
value: false,
},
_subsectionLinks: Array,
_filteredLinks: Array,
_showDownload: {
type: Boolean,
value: false,
},
_isAdmin: {
type: Boolean,
value: false,
},
_showGroup: Boolean,
_showGroupAuditLog: Boolean,
_showGroupList: Boolean,
_showGroupMembers: Boolean,
_showRepoAccess: Boolean,
_showRepoCommands: Boolean,
_showRepoDashboards: Boolean,
_showRepoDetailList: Boolean,
_showRepoMain: Boolean,
_showRepoList: Boolean,
_showPluginList: Boolean,
};
}
static get is() { return 'gr-admin-view'; }
static get observers() {
return [
'_paramsChanged(params)',
];
}
static get properties() {
return {
/** @type {?} */
params: Object,
path: String,
adminView: String,
/** @override */
attached() {
super.attached();
this.reload();
}
reload() {
const promises = [
this.$.restAPI.getAccount(),
Gerrit.awaitPluginsLoaded(),
];
return Promise.all(promises).then(result => {
this._account = result[0];
let options;
if (this._repoName) {
options = {repoName: this._repoName};
} else if (this._groupId) {
options = {
groupId: this._groupId,
groupName: this._groupName,
groupIsInternal: this._groupIsInternal,
isAdmin: this._isAdmin,
groupOwner: this._groupOwner,
};
}
return this.getAdminLinks(this._account,
this.$.restAPI.getAccountCapabilities.bind(this.$.restAPI),
this.$.jsAPI.getAdminMenuLinks.bind(this.$.jsAPI),
options)
.then(res => {
this._filteredLinks = res.links;
this._breadcrumbParentName = res.expandedSection ?
res.expandedSection.name : '';
if (!res.expandedSection) {
this._subsectionLinks = [];
return;
}
this._subsectionLinks = [res.expandedSection]
.concat(res.expandedSection.children).map(section => {
return {
text: !section.detailType ? 'Home' : section.name,
value: section.view + (section.detailType || ''),
view: section.view,
url: section.url,
detailType: section.detailType,
parent: this._groupId || this._repoName || '',
};
});
});
});
}
_computeSelectValue(params) {
if (!params || !params.view) { return; }
return params.view + (params.detail || '');
}
_selectedIsCurrentPage(selected) {
return (selected.parent === (this._repoName || this._groupId) &&
selected.view === this.params.view &&
selected.detailType === this.params.detail);
}
_handleSubsectionChange(e) {
const selected = this._subsectionLinks
.find(section => section.value === e.detail.value);
// This is when it gets set initially.
if (this._selectedIsCurrentPage(selected)) {
return;
}
Gerrit.Nav.navigateToRelativeUrl(selected.url);
}
_paramsChanged(params) {
const isGroupView = params.view === Gerrit.Nav.View.GROUP;
const isRepoView = params.view === Gerrit.Nav.View.REPO;
const isAdminView = params.view === Gerrit.Nav.View.ADMIN;
this.set('_showGroup', isGroupView && !params.detail);
this.set('_showGroupAuditLog', isGroupView &&
params.detail === Gerrit.Nav.GroupDetailView.LOG);
this.set('_showGroupMembers', isGroupView &&
params.detail === Gerrit.Nav.GroupDetailView.MEMBERS);
this.set('_showGroupList', isAdminView &&
params.adminView === 'gr-admin-group-list');
this.set('_showRepoAccess', isRepoView &&
params.detail === Gerrit.Nav.RepoDetailView.ACCESS);
this.set('_showRepoCommands', isRepoView &&
params.detail === Gerrit.Nav.RepoDetailView.COMMANDS);
this.set('_showRepoDetailList', isRepoView &&
(params.detail === Gerrit.Nav.RepoDetailView.BRANCHES ||
params.detail === Gerrit.Nav.RepoDetailView.TAGS));
this.set('_showRepoDashboards', isRepoView &&
params.detail === Gerrit.Nav.RepoDetailView.DASHBOARDS);
this.set('_showRepoMain', isRepoView && !params.detail);
this.set('_showRepoList', isAdminView &&
params.adminView === 'gr-repo-list');
this.set('_showPluginList', isAdminView &&
params.adminView === 'gr-plugin-list');
let needsReload = false;
if (params.repo !== this._repoName) {
this._repoName = params.repo || '';
// Reloads the admin menu.
needsReload = true;
}
if (params.groupId !== this._groupId) {
this._groupId = params.groupId || '';
// Reloads the admin menu.
needsReload = true;
}
if (this._breadcrumbParentName && !params.groupId && !params.repo) {
needsReload = true;
}
if (!needsReload) { return; }
this.reload();
}
// TODO (beckysiegel): Update these functions after router abstraction is
// updated. They are currently copied from gr-dropdown (and should be
// updated there as well once complete).
_computeURLHelper(host, path) {
return '//' + host + this.getBaseUrl() + path;
}
_computeRelativeURL(path) {
const host = window.location.host;
return this._computeURLHelper(host, path);
}
_computeLinkURL(link) {
if (!link || typeof link.url === 'undefined') { return ''; }
if (link.target || !link.noBaseUrl) {
return link.url;
}
return this._computeRelativeURL(link.url);
}
/**
* @param {string} itemView
* @param {Object} params
* @param {string=} opt_detailType
*/
_computeSelectedClass(itemView, params, opt_detailType) {
if (!params) return '';
// Group params are structured differently from admin params. Compute
// selected differently for groups.
// TODO(wyatta): Simplify this when all routes work like group params.
if (params.view === Gerrit.Nav.View.GROUP &&
itemView === Gerrit.Nav.View.GROUP) {
if (!params.detail && !opt_detailType) { return 'selected'; }
if (params.detail === opt_detailType) { return 'selected'; }
return '';
}
if (params.view === Gerrit.Nav.View.REPO &&
itemView === Gerrit.Nav.View.REPO) {
if (!params.detail && !opt_detailType) { return 'selected'; }
if (params.detail === opt_detailType) { return 'selected'; }
return '';
}
if (params.detailType && params.detailType !== opt_detailType) {
return '';
}
return itemView === params.adminView ? 'selected' : '';
}
_computeGroupName(groupId) {
if (!groupId) { return ''; }
const promises = [];
this.$.restAPI.getGroupConfig(groupId).then(group => {
if (!group || !group.name) { return; }
this._groupName = group.name;
this._groupIsInternal = !!group.id.match(INTERNAL_GROUP_REGEX);
this.reload();
promises.push(this.$.restAPI.getIsAdmin().then(isAdmin => {
this._isAdmin = isAdmin;
}));
promises.push(this.$.restAPI.getIsGroupOwner(group.name).then(
isOwner => {
this._groupOwner = isOwner;
}));
return Promise.all(promises).then(() => {
this.reload();
});
});
}
_updateGroupName(e) {
this._groupName = e.detail.name;
this.reload();
}
_breadcrumbParentName: String,
_repoName: String,
_groupId: {
type: Number,
observer: '_computeGroupName',
},
_groupIsInternal: Boolean,
_groupName: String,
_groupOwner: {
type: Boolean,
value: false,
},
_subsectionLinks: Array,
_filteredLinks: Array,
_showDownload: {
type: Boolean,
value: false,
},
_isAdmin: {
type: Boolean,
value: false,
},
_showGroup: Boolean,
_showGroupAuditLog: Boolean,
_showGroupList: Boolean,
_showGroupMembers: Boolean,
_showRepoAccess: Boolean,
_showRepoCommands: Boolean,
_showRepoDashboards: Boolean,
_showRepoDetailList: Boolean,
_showRepoMain: Boolean,
_showRepoList: Boolean,
_showPluginList: Boolean,
};
}
customElements.define(GrAdminView.is, GrAdminView);
})();
static get observers() {
return [
'_paramsChanged(params)',
];
}
/** @override */
attached() {
super.attached();
this.reload();
}
reload() {
const promises = [
this.$.restAPI.getAccount(),
Gerrit.awaitPluginsLoaded(),
];
return Promise.all(promises).then(result => {
this._account = result[0];
let options;
if (this._repoName) {
options = {repoName: this._repoName};
} else if (this._groupId) {
options = {
groupId: this._groupId,
groupName: this._groupName,
groupIsInternal: this._groupIsInternal,
isAdmin: this._isAdmin,
groupOwner: this._groupOwner,
};
}
return this.getAdminLinks(this._account,
this.$.restAPI.getAccountCapabilities.bind(this.$.restAPI),
this.$.jsAPI.getAdminMenuLinks.bind(this.$.jsAPI),
options)
.then(res => {
this._filteredLinks = res.links;
this._breadcrumbParentName = res.expandedSection ?
res.expandedSection.name : '';
if (!res.expandedSection) {
this._subsectionLinks = [];
return;
}
this._subsectionLinks = [res.expandedSection]
.concat(res.expandedSection.children).map(section => {
return {
text: !section.detailType ? 'Home' : section.name,
value: section.view + (section.detailType || ''),
view: section.view,
url: section.url,
detailType: section.detailType,
parent: this._groupId || this._repoName || '',
};
});
});
});
}
_computeSelectValue(params) {
if (!params || !params.view) { return; }
return params.view + (params.detail || '');
}
_selectedIsCurrentPage(selected) {
return (selected.parent === (this._repoName || this._groupId) &&
selected.view === this.params.view &&
selected.detailType === this.params.detail);
}
_handleSubsectionChange(e) {
const selected = this._subsectionLinks
.find(section => section.value === e.detail.value);
// This is when it gets set initially.
if (this._selectedIsCurrentPage(selected)) {
return;
}
Gerrit.Nav.navigateToRelativeUrl(selected.url);
}
_paramsChanged(params) {
const isGroupView = params.view === Gerrit.Nav.View.GROUP;
const isRepoView = params.view === Gerrit.Nav.View.REPO;
const isAdminView = params.view === Gerrit.Nav.View.ADMIN;
this.set('_showGroup', isGroupView && !params.detail);
this.set('_showGroupAuditLog', isGroupView &&
params.detail === Gerrit.Nav.GroupDetailView.LOG);
this.set('_showGroupMembers', isGroupView &&
params.detail === Gerrit.Nav.GroupDetailView.MEMBERS);
this.set('_showGroupList', isAdminView &&
params.adminView === 'gr-admin-group-list');
this.set('_showRepoAccess', isRepoView &&
params.detail === Gerrit.Nav.RepoDetailView.ACCESS);
this.set('_showRepoCommands', isRepoView &&
params.detail === Gerrit.Nav.RepoDetailView.COMMANDS);
this.set('_showRepoDetailList', isRepoView &&
(params.detail === Gerrit.Nav.RepoDetailView.BRANCHES ||
params.detail === Gerrit.Nav.RepoDetailView.TAGS));
this.set('_showRepoDashboards', isRepoView &&
params.detail === Gerrit.Nav.RepoDetailView.DASHBOARDS);
this.set('_showRepoMain', isRepoView && !params.detail);
this.set('_showRepoList', isAdminView &&
params.adminView === 'gr-repo-list');
this.set('_showPluginList', isAdminView &&
params.adminView === 'gr-plugin-list');
let needsReload = false;
if (params.repo !== this._repoName) {
this._repoName = params.repo || '';
// Reloads the admin menu.
needsReload = true;
}
if (params.groupId !== this._groupId) {
this._groupId = params.groupId || '';
// Reloads the admin menu.
needsReload = true;
}
if (this._breadcrumbParentName && !params.groupId && !params.repo) {
needsReload = true;
}
if (!needsReload) { return; }
this.reload();
}
// TODO (beckysiegel): Update these functions after router abstraction is
// updated. They are currently copied from gr-dropdown (and should be
// updated there as well once complete).
_computeURLHelper(host, path) {
return '//' + host + this.getBaseUrl() + path;
}
_computeRelativeURL(path) {
const host = window.location.host;
return this._computeURLHelper(host, path);
}
_computeLinkURL(link) {
if (!link || typeof link.url === 'undefined') { return ''; }
if (link.target || !link.noBaseUrl) {
return link.url;
}
return this._computeRelativeURL(link.url);
}
/**
* @param {string} itemView
* @param {Object} params
* @param {string=} opt_detailType
*/
_computeSelectedClass(itemView, params, opt_detailType) {
if (!params) return '';
// Group params are structured differently from admin params. Compute
// selected differently for groups.
// TODO(wyatta): Simplify this when all routes work like group params.
if (params.view === Gerrit.Nav.View.GROUP &&
itemView === Gerrit.Nav.View.GROUP) {
if (!params.detail && !opt_detailType) { return 'selected'; }
if (params.detail === opt_detailType) { return 'selected'; }
return '';
}
if (params.view === Gerrit.Nav.View.REPO &&
itemView === Gerrit.Nav.View.REPO) {
if (!params.detail && !opt_detailType) { return 'selected'; }
if (params.detail === opt_detailType) { return 'selected'; }
return '';
}
if (params.detailType && params.detailType !== opt_detailType) {
return '';
}
return itemView === params.adminView ? 'selected' : '';
}
_computeGroupName(groupId) {
if (!groupId) { return ''; }
const promises = [];
this.$.restAPI.getGroupConfig(groupId).then(group => {
if (!group || !group.name) { return; }
this._groupName = group.name;
this._groupIsInternal = !!group.id.match(INTERNAL_GROUP_REGEX);
this.reload();
promises.push(this.$.restAPI.getIsAdmin().then(isAdmin => {
this._isAdmin = isAdmin;
}));
promises.push(this.$.restAPI.getIsGroupOwner(group.name).then(
isOwner => {
this._groupOwner = isOwner;
}));
return Promise.all(promises).then(() => {
this.reload();
});
});
}
_updateGroupName(e) {
this._groupName = e.detail.name;
this.reload();
}
}
customElements.define(GrAdminView.is, GrAdminView);

View File

@ -1,48 +1,22 @@
<!--
@license
Copyright (C) 2017 The Android Open Source Project
/**
* @license
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {html} from '@polymer/polymer/lib/utils/html-tag.js';
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<link rel="import" href="/bower_components/polymer/polymer.html">
<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
<link rel="import" href="../../../behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior.html">
<link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html">
<link rel="import" href="../../../styles/gr-menu-page-styles.html">
<link rel="import" href="../../../styles/gr-page-nav-styles.html">
<link rel="import" href="../../../styles/shared-styles.html">
<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
<link rel="import" href="../../shared/gr-dropdown-list/gr-dropdown-list.html">
<link rel="import" href="../../shared/gr-icons/gr-icons.html">
<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
<link rel="import" href="../../shared/gr-page-nav/gr-page-nav.html">
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
<link rel="import" href="../gr-admin-group-list/gr-admin-group-list.html">
<link rel="import" href="../gr-group/gr-group.html">
<link rel="import" href="../gr-group-audit-log/gr-group-audit-log.html">
<link rel="import" href="../gr-group-members/gr-group-members.html">
<link rel="import" href="../gr-plugin-list/gr-plugin-list.html">
<link rel="import" href="../gr-repo/gr-repo.html">
<link rel="import" href="../gr-repo-access/gr-repo-access.html">
<link rel="import" href="../gr-repo-commands/gr-repo-commands.html">
<link rel="import" href="../gr-repo-dashboards/gr-repo-dashboards.html">
<link rel="import" href="../gr-repo-detail-list/gr-repo-detail-list.html">
<link rel="import" href="../gr-repo-list/gr-repo-list.html">
<dom-module id="gr-admin-view">
<template>
export const htmlTemplate = html`
<style include="shared-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
@ -84,28 +58,24 @@ limitations under the License.
<gr-page-nav class="navStyles">
<ul class="sectionContent">
<template id="adminNav" is="dom-repeat" items="[[_filteredLinks]]">
<li class$="sectionTitle [[_computeSelectedClass(item.view, params)]]">
<a class="title" href="[[_computeLinkURL(item)]]"
rel="noopener">[[item.name]]</a>
<li class\$="sectionTitle [[_computeSelectedClass(item.view, params)]]">
<a class="title" href="[[_computeLinkURL(item)]]" rel="noopener">[[item.name]]</a>
</li>
<template is="dom-repeat" items="[[item.children]]" as="child">
<li class$="[[_computeSelectedClass(child.view, params)]]">
<a href$="[[_computeLinkURL(child)]]"
rel="noopener">[[child.name]]</a>
<li class\$="[[_computeSelectedClass(child.view, params)]]">
<a href\$="[[_computeLinkURL(child)]]" rel="noopener">[[child.name]]</a>
</li>
</template>
<template is="dom-if" if="[[item.subsection]]">
<!--If a section has a subsection, render that.-->
<li class$="[[_computeSelectedClass(item.subsection.view, params)]]">
<a class="title" href$="[[_computeLinkURL(item.subsection)]]"
rel="noopener">
<li class\$="[[_computeSelectedClass(item.subsection.view, params)]]">
<a class="title" href\$="[[_computeLinkURL(item.subsection)]]" rel="noopener">
[[item.subsection.name]]</a>
</li>
<!--Loop through the links in the sub-section.-->
<template is="dom-repeat"
items="[[item.subsection.children]]" as="child">
<li class$="subsectionItem [[_computeSelectedClass(child.view, params, child.detailType)]]">
<a href$="[[_computeLinkURL(child)]]">[[child.name]]</a>
<template is="dom-repeat" items="[[item.subsection.children]]" as="child">
<li class\$="subsectionItem [[_computeSelectedClass(child.view, params, child.detailType)]]">
<a href\$="[[_computeLinkURL(child)]]">[[child.name]]</a>
</li>
</template>
</template>
@ -118,12 +88,7 @@ limitations under the License.
<span class="breadcrumbText">[[_breadcrumbParentName]]</span>
<iron-icon icon="gr-icons:chevron-right"></iron-icon>
</span>
<gr-dropdown-list
lowercase
id="pageSelect"
value="[[_computeSelectValue(params)]]"
items="[[_subsectionLinks]]"
on-value-change="_handleSubsectionChange">
<gr-dropdown-list lowercase="" id="pageSelect" value="[[_computeSelectValue(params)]]" items="[[_subsectionLinks]]" on-value-change="_handleSubsectionChange">
</gr-dropdown-list>
</section>
</template>
@ -150,42 +115,32 @@ limitations under the License.
</template>
<template is="dom-if" if="[[_showGroup]]" restamp="true">
<main class="breadcrumbs">
<gr-group
group-id="[[params.groupId]]"
on-name-changed="_updateGroupName"></gr-group>
<gr-group group-id="[[params.groupId]]" on-name-changed="_updateGroupName"></gr-group>
</main>
</template>
<template is="dom-if" if="[[_showGroupMembers]]" restamp="true">
<main class="breadcrumbs">
<gr-group-members
group-id="[[params.groupId]]"></gr-group-members>
<gr-group-members group-id="[[params.groupId]]"></gr-group-members>
</main>
</template>
<template is="dom-if" if="[[_showRepoDetailList]]" restamp="true">
<main class="table breadcrumbs">
<gr-repo-detail-list
params="[[params]]"
class="table"></gr-repo-detail-list>
<gr-repo-detail-list params="[[params]]" class="table"></gr-repo-detail-list>
</main>
</template>
<template is="dom-if" if="[[_showGroupAuditLog]]" restamp="true">
<main class="table breadcrumbs">
<gr-group-audit-log
group-id="[[params.groupId]]"
class="table"></gr-group-audit-log>
<gr-group-audit-log group-id="[[params.groupId]]" class="table"></gr-group-audit-log>
</main>
</template>
<template is="dom-if" if="[[_showRepoCommands]]" restamp="true">
<main class="breadcrumbs">
<gr-repo-commands
repo="[[params.repo]]"></gr-repo-commands>
<gr-repo-commands repo="[[params.repo]]"></gr-repo-commands>
</main>
</template>
<template is="dom-if" if="[[_showRepoAccess]]" restamp="true">
<main class="breadcrumbs">
<gr-repo-access
path="[[path]]"
repo="[[params.repo]]"></gr-repo-access>
<gr-repo-access path="[[path]]" repo="[[params.repo]]"></gr-repo-access>
</main>
</template>
<template is="dom-if" if="[[_showRepoDashboards]]" restamp="true">
@ -195,6 +150,4 @@ limitations under the License.
</template>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
<gr-js-api-interface id="jsAPI"></gr-js-api-interface>
</template>
<script src="gr-admin-view.js"></script>
</dom-module>
`;

View File

@ -14,67 +14,76 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function() {
'use strict';
import '../../../scripts/bundled-polymer.js';
const DETAIL_TYPES = {
BRANCHES: 'branches',
ID: 'id',
TAGS: 'tags',
};
import '../../../behaviors/fire-behavior/fire-behavior.js';
import '../../shared/gr-dialog/gr-dialog.js';
import '../../../styles/shared-styles.js';
import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
import {PolymerElement} from '@polymer/polymer/polymer-element.js';
import {htmlTemplate} from './gr-confirm-delete-item-dialog_html.js';
const DETAIL_TYPES = {
BRANCHES: 'branches',
ID: 'id',
TAGS: 'tags',
};
/**
* @appliesMixin Gerrit.FireMixin
* @extends Polymer.Element
*/
class GrConfirmDeleteItemDialog extends mixinBehaviors( [
Gerrit.FireBehavior,
], GestureEventListeners(
LegacyElementMixin(
PolymerElement))) {
static get template() { return htmlTemplate; }
static get is() { return 'gr-confirm-delete-item-dialog'; }
/**
* Fired when the confirm button is pressed.
*
* @event confirm
*/
/**
* @appliesMixin Gerrit.FireMixin
* @extends Polymer.Element
* Fired when the cancel button is pressed.
*
* @event cancel
*/
class GrConfirmDeleteItemDialog extends Polymer.mixinBehaviors( [
Gerrit.FireBehavior,
], Polymer.GestureEventListeners(
Polymer.LegacyElementMixin(
Polymer.Element))) {
static get is() { return 'gr-confirm-delete-item-dialog'; }
/**
* Fired when the confirm button is pressed.
*
* @event confirm
*/
/**
* Fired when the cancel button is pressed.
*
* @event cancel
*/
static get properties() {
return {
item: String,
itemType: String,
};
}
_handleConfirmTap(e) {
e.preventDefault();
e.stopPropagation();
this.fire('confirm', null, {bubbles: false});
}
_handleCancelTap(e) {
e.preventDefault();
e.stopPropagation();
this.fire('cancel', null, {bubbles: false});
}
_computeItemName(detailType) {
if (detailType === DETAIL_TYPES.BRANCHES) {
return 'Branch';
} else if (detailType === DETAIL_TYPES.TAGS) {
return 'Tag';
} else if (detailType === DETAIL_TYPES.ID) {
return 'ID';
}
}
static get properties() {
return {
item: String,
itemType: String,
};
}
customElements.define(GrConfirmDeleteItemDialog.is,
GrConfirmDeleteItemDialog);
})();
_handleConfirmTap(e) {
e.preventDefault();
e.stopPropagation();
this.fire('confirm', null, {bubbles: false});
}
_handleCancelTap(e) {
e.preventDefault();
e.stopPropagation();
this.fire('cancel', null, {bubbles: false});
}
_computeItemName(detailType) {
if (detailType === DETAIL_TYPES.BRANCHES) {
return 'Branch';
} else if (detailType === DETAIL_TYPES.TAGS) {
return 'Tag';
} else if (detailType === DETAIL_TYPES.ID) {
return 'ID';
}
}
}
customElements.define(GrConfirmDeleteItemDialog.is,
GrConfirmDeleteItemDialog);

View File

@ -1,38 +1,29 @@
<!--
@license
Copyright (C) 2017 The Android Open Source Project
/**
* @license
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {html} from '@polymer/polymer/lib/utils/html-tag.js';
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<link rel="import" href="/bower_components/polymer/polymer.html">
<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
<link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
<link rel="import" href="../../../styles/shared-styles.html">
<dom-module id="gr-confirm-delete-item-dialog">
<template>
export const htmlTemplate = html`
<style include="shared-styles">
:host {
display: block;
width: 30em;
}
</style>
<gr-dialog
confirm-label="Delete [[_computeItemName(itemType)]]"
confirm-on-enter
on-confirm="_handleConfirmTap"
on-cancel="_handleCancelTap">
<gr-dialog confirm-label="Delete [[_computeItemName(itemType)]]" confirm-on-enter="" on-confirm="_handleConfirmTap" on-cancel="_handleCancelTap">
<div class="header" slot="header">[[_computeItemName(itemType)]] Deletion</div>
<div class="main" slot="main">
<label for="branchInput">
@ -43,6 +34,4 @@ limitations under the License.
</div>
</div>
</gr-dialog>
</template>
<script src="gr-confirm-delete-item-dialog.js"></script>
</dom-module>
`;

View File

@ -19,15 +19,20 @@ limitations under the License.
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-confirm-delete-item-dialog</title>
<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/bower_components/web-component-tester/browser.js"></script>
<script src="../../../test/test-pre-setup.js"></script>
<link rel="import" href="../../../test/common-test-setup.html"/>
<link rel="import" href="gr-confirm-delete-item-dialog.html">
<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/components/wct-browser-legacy/browser.js"></script>
<script type="module" src="../../../test/test-pre-setup.js"></script>
<script type="module" src="../../../test/common-test-setup.js"></script>
<script type="module" src="./gr-confirm-delete-item-dialog.js"></script>
<script>void(0);</script>
<script type="module">
import '../../../test/test-pre-setup.js';
import '../../../test/common-test-setup.js';
import './gr-confirm-delete-item-dialog.js';
void(0);
</script>
<test-fixture id="basic">
<template>
@ -35,53 +40,55 @@ limitations under the License.
</template>
</test-fixture>
<script>
suite('gr-confirm-delete-item-dialog tests', async () => {
await readyToTest();
let element;
let sandbox;
<script type="module">
import '../../../test/test-pre-setup.js';
import '../../../test/common-test-setup.js';
import './gr-confirm-delete-item-dialog.js';
suite('gr-confirm-delete-item-dialog tests', () => {
let element;
let sandbox;
setup(() => {
sandbox = sinon.sandbox.create();
element = fixture('basic');
});
teardown(() => {
sandbox.restore();
});
test('_handleConfirmTap', () => {
const confirmHandler = sandbox.stub();
element.addEventListener('confirm', confirmHandler);
sandbox.spy(element, '_handleConfirmTap');
element.shadowRoot
.querySelector('gr-dialog').fire('confirm');
assert.isTrue(confirmHandler.called);
assert.isTrue(confirmHandler.calledOnce);
assert.isTrue(element._handleConfirmTap.called);
assert.isTrue(element._handleConfirmTap.calledOnce);
});
test('_handleCancelTap', () => {
const cancelHandler = sandbox.stub();
element.addEventListener('cancel', cancelHandler);
sandbox.spy(element, '_handleCancelTap');
element.shadowRoot
.querySelector('gr-dialog').fire('cancel');
assert.isTrue(cancelHandler.called);
assert.isTrue(cancelHandler.calledOnce);
assert.isTrue(element._handleCancelTap.called);
assert.isTrue(element._handleCancelTap.calledOnce);
});
test('_computeItemName function for branches', () => {
assert.deepEqual(element._computeItemName('branches'), 'Branch');
assert.notEqual(element._computeItemName('branches'), 'Tag');
});
test('_computeItemName function for tags', () => {
assert.deepEqual(element._computeItemName('tags'), 'Tag');
assert.notEqual(element._computeItemName('tags'), 'Branch');
});
setup(() => {
sandbox = sinon.sandbox.create();
element = fixture('basic');
});
teardown(() => {
sandbox.restore();
});
test('_handleConfirmTap', () => {
const confirmHandler = sandbox.stub();
element.addEventListener('confirm', confirmHandler);
sandbox.spy(element, '_handleConfirmTap');
element.shadowRoot
.querySelector('gr-dialog').fire('confirm');
assert.isTrue(confirmHandler.called);
assert.isTrue(confirmHandler.calledOnce);
assert.isTrue(element._handleConfirmTap.called);
assert.isTrue(element._handleConfirmTap.calledOnce);
});
test('_handleCancelTap', () => {
const cancelHandler = sandbox.stub();
element.addEventListener('cancel', cancelHandler);
sandbox.spy(element, '_handleCancelTap');
element.shadowRoot
.querySelector('gr-dialog').fire('cancel');
assert.isTrue(cancelHandler.called);
assert.isTrue(cancelHandler.calledOnce);
assert.isTrue(element._handleCancelTap.called);
assert.isTrue(element._handleCancelTap.calledOnce);
});
test('_computeItemName function for branches', () => {
assert.deepEqual(element._computeItemName('branches'), 'Branch');
assert.notEqual(element._computeItemName('branches'), 'Tag');
});
test('_computeItemName function for tags', () => {
assert.deepEqual(element._computeItemName('tags'), 'Tag');
assert.notEqual(element._computeItemName('tags'), 'Branch');
});
});
</script>

View File

@ -14,148 +14,166 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function() {
'use strict';
import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
const SUGGESTIONS_LIMIT = 15;
const REF_PREFIX = 'refs/heads/';
import '@polymer/iron-input/iron-input.js';
import '../../../scripts/bundled-polymer.js';
import '../../../behaviors/base-url-behavior/base-url-behavior.js';
import '../../../behaviors/fire-behavior/fire-behavior.js';
import '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
import '../../../styles/gr-form-styles.js';
import '../../../styles/shared-styles.js';
import '../../core/gr-navigation/gr-navigation.js';
import '../../shared/gr-autocomplete/gr-autocomplete.js';
import '../../shared/gr-button/gr-button.js';
import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
import '../../shared/gr-select/gr-select.js';
import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
import {PolymerElement} from '@polymer/polymer/polymer-element.js';
import {htmlTemplate} from './gr-create-change-dialog_html.js';
const SUGGESTIONS_LIMIT = 15;
const REF_PREFIX = 'refs/heads/';
/**
* @appliesMixin Gerrit.BaseUrlMixin
* @appliesMixin Gerrit.FireMixin
* @appliesMixin Gerrit.URLEncodingMixin
* @extends Polymer.Element
*/
class GrCreateChangeDialog extends mixinBehaviors( [
Gerrit.BaseUrlBehavior,
/**
* @appliesMixin Gerrit.BaseUrlMixin
* @appliesMixin Gerrit.FireMixin
* @appliesMixin Gerrit.URLEncodingMixin
* @extends Polymer.Element
* Unused in this element, but called by other elements in tests
* e.g gr-repo-commands_test.
*/
class GrCreateChangeDialog extends Polymer.mixinBehaviors( [
Gerrit.BaseUrlBehavior,
/**
* Unused in this element, but called by other elements in tests
* e.g gr-repo-commands_test.
*/
Gerrit.FireBehavior,
Gerrit.URLEncodingBehavior,
], Polymer.GestureEventListeners(
Polymer.LegacyElementMixin(
Polymer.Element))) {
static get is() { return 'gr-create-change-dialog'; }
Gerrit.FireBehavior,
Gerrit.URLEncodingBehavior,
], GestureEventListeners(
LegacyElementMixin(
PolymerElement))) {
static get template() { return htmlTemplate; }
static get properties() {
return {
repoName: String,
branch: String,
/** @type {?} */
_repoConfig: Object,
subject: String,
topic: String,
_query: {
type: Function,
value() {
return this._getRepoBranchesSuggestions.bind(this);
},
static get is() { return 'gr-create-change-dialog'; }
static get properties() {
return {
repoName: String,
branch: String,
/** @type {?} */
_repoConfig: Object,
subject: String,
topic: String,
_query: {
type: Function,
value() {
return this._getRepoBranchesSuggestions.bind(this);
},
baseChange: String,
baseCommit: String,
privateByDefault: String,
canCreate: {
type: Boolean,
notify: true,
value: false,
},
_privateChangesEnabled: Boolean,
};
},
baseChange: String,
baseCommit: String,
privateByDefault: String,
canCreate: {
type: Boolean,
notify: true,
value: false,
},
_privateChangesEnabled: Boolean,
};
}
/** @override */
attached() {
super.attached();
if (!this.repoName) { return Promise.resolve(); }
const promises = [];
promises.push(this.$.restAPI.getProjectConfig(this.repoName)
.then(config => {
this.privateByDefault = config.private_by_default;
}));
promises.push(this.$.restAPI.getConfig().then(config => {
if (!config) { return; }
this._privateConfig = config && config.change &&
config.change.disable_private_changes;
}));
return Promise.all(promises);
}
static get observers() {
return [
'_allowCreate(branch, subject)',
];
}
_computeBranchClass(baseChange) {
return baseChange ? 'hide' : '';
}
_allowCreate(branch, subject) {
this.canCreate = !!branch && !!subject;
}
handleCreateChange() {
const isPrivate = this.$.privateChangeCheckBox.checked;
const isWip = true;
return this.$.restAPI.createChange(this.repoName, this.branch,
this.subject, this.topic, isPrivate, isWip, this.baseChange,
this.baseCommit || null)
.then(changeCreated => {
if (!changeCreated) { return; }
Gerrit.Nav.navigateToChange(changeCreated);
});
}
_getRepoBranchesSuggestions(input) {
if (input.startsWith(REF_PREFIX)) {
input = input.substring(REF_PREFIX.length);
}
/** @override */
attached() {
super.attached();
if (!this.repoName) { return Promise.resolve(); }
const promises = [];
promises.push(this.$.restAPI.getProjectConfig(this.repoName)
.then(config => {
this.privateByDefault = config.private_by_default;
}));
promises.push(this.$.restAPI.getConfig().then(config => {
if (!config) { return; }
this._privateConfig = config && config.change &&
config.change.disable_private_changes;
}));
return Promise.all(promises);
}
static get observers() {
return [
'_allowCreate(branch, subject)',
];
}
_computeBranchClass(baseChange) {
return baseChange ? 'hide' : '';
}
_allowCreate(branch, subject) {
this.canCreate = !!branch && !!subject;
}
handleCreateChange() {
const isPrivate = this.$.privateChangeCheckBox.checked;
const isWip = true;
return this.$.restAPI.createChange(this.repoName, this.branch,
this.subject, this.topic, isPrivate, isWip, this.baseChange,
this.baseCommit || null)
.then(changeCreated => {
if (!changeCreated) { return; }
Gerrit.Nav.navigateToChange(changeCreated);
});
}
_getRepoBranchesSuggestions(input) {
if (input.startsWith(REF_PREFIX)) {
input = input.substring(REF_PREFIX.length);
}
return this.$.restAPI.getRepoBranches(
input, this.repoName, SUGGESTIONS_LIMIT).then(response => {
const branches = [];
let branch;
for (const key in response) {
if (!response.hasOwnProperty(key)) { continue; }
if (response[key].ref.startsWith('refs/heads/')) {
branch = response[key].ref.substring('refs/heads/'.length);
} else {
branch = response[key].ref;
}
branches.push({
name: branch,
});
}
return branches;
});
}
_formatBooleanString(config) {
if (config && config.configured_value === 'TRUE') {
return true;
} else if (config && config.configured_value === 'FALSE') {
return false;
} else if (config && config.configured_value === 'INHERIT') {
if (config && config.inherited_value) {
return true;
return this.$.restAPI.getRepoBranches(
input, this.repoName, SUGGESTIONS_LIMIT).then(response => {
const branches = [];
let branch;
for (const key in response) {
if (!response.hasOwnProperty(key)) { continue; }
if (response[key].ref.startsWith('refs/heads/')) {
branch = response[key].ref.substring('refs/heads/'.length);
} else {
return false;
branch = response[key].ref;
}
branches.push({
name: branch,
});
}
return branches;
});
}
_formatBooleanString(config) {
if (config && config.configured_value === 'TRUE') {
return true;
} else if (config && config.configured_value === 'FALSE') {
return false;
} else if (config && config.configured_value === 'INHERIT') {
if (config && config.inherited_value) {
return true;
} else {
return false;
}
}
_computePrivateSectionClass(config) {
return config ? 'hide' : '';
} else {
return false;
}
}
customElements.define(GrCreateChangeDialog.is, GrCreateChangeDialog);
})();
_computePrivateSectionClass(config) {
return config ? 'hide' : '';
}
}
customElements.define(GrCreateChangeDialog.is, GrCreateChangeDialog);

View File

@ -1,36 +1,22 @@
<!--
@license
Copyright (C) 2017 The Android Open Source Project
/**
* @license
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {html} from '@polymer/polymer/lib/utils/html-tag.js';
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
<link rel="import" href="/bower_components/iron-input/iron-input.html">
<link rel="import" href="/bower_components/polymer/polymer.html">
<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
<link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html">
<link rel="import" href="../../../styles/gr-form-styles.html">
<link rel="import" href="../../../styles/shared-styles.html">
<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
<link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html">
<link rel="import" href="../../shared/gr-button/gr-button.html">
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
<link rel="import" href="../../shared/gr-select/gr-select.html">
<dom-module id="gr-create-change-dialog">
<template>
export const htmlTemplate = html`
<style include="shared-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
@ -53,76 +39,42 @@ limitations under the License.
}
</style>
<div class="gr-form-styles">
<section class$="[[_computeBranchClass(baseChange)]]">
<section class\$="[[_computeBranchClass(baseChange)]]">
<span class="title">Select branch for new change</span>
<span class="value">
<gr-autocomplete
id="branchInput"
text="{{branch}}"
query="[[_query]]"
placeholder="Destination branch">
<gr-autocomplete id="branchInput" text="{{branch}}" query="[[_query]]" placeholder="Destination branch">
</gr-autocomplete>
</span>
</section>
<section class$="[[_computeBranchClass(baseChange)]]">
<section class\$="[[_computeBranchClass(baseChange)]]">
<span class="title">Provide base commit sha1 for change</span>
<span class="value">
<iron-input
maxlength="40"
placeholder="(optional)"
bind-value="{{baseCommit}}">
<input
is="iron-input"
id="baseCommitInput"
maxlength="40"
placeholder="(optional)"
bind-value="{{baseCommit}}">
<iron-input maxlength="40" placeholder="(optional)" bind-value="{{baseCommit}}">
<input is="iron-input" id="baseCommitInput" maxlength="40" placeholder="(optional)" bind-value="{{baseCommit}}">
</iron-input>
</span>
</section>
<section>
<span class="title">Enter topic for new change</span>
<span class="value">
<iron-input
maxlength="1024"
placeholder="(optional)"
bind-value="{{topic}}">
<input
is="iron-input"
id="tagNameInput"
maxlength="1024"
placeholder="(optional)"
bind-value="{{topic}}">
<iron-input maxlength="1024" placeholder="(optional)" bind-value="{{topic}}">
<input is="iron-input" id="tagNameInput" maxlength="1024" placeholder="(optional)" bind-value="{{topic}}">
</iron-input>
</span>
</section>
<section id="description">
<span class="title">Description</span>
<span class="value">
<iron-autogrow-textarea
id="messageInput"
class="message"
autocomplete="on"
rows="4"
max-rows="15"
bind-value="{{subject}}"
placeholder="Insert the description of the change.">
<iron-autogrow-textarea id="messageInput" class="message" autocomplete="on" rows="4" max-rows="15" bind-value="{{subject}}" placeholder="Insert the description of the change.">
</iron-autogrow-textarea>
</span>
</section>
<section class$="[[_computePrivateSectionClass(_privateChangesEnabled)]]">
<label
class="title"
for="privateChangeCheckBox">Private change</label>
<section class\$="[[_computePrivateSectionClass(_privateChangesEnabled)]]">
<label class="title" for="privateChangeCheckBox">Private change</label>
<span class="value">
<input
type="checkbox"
id="privateChangeCheckBox"
checked$="[[_formatBooleanString(privateByDefault)]]">
<input type="checkbox" id="privateChangeCheckBox" checked\$="[[_formatBooleanString(privateByDefault)]]">
</span>
</section>
</div>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
</template>
<script src="gr-create-change-dialog.js"></script>
</dom-module>
`;

View File

@ -19,15 +19,20 @@ limitations under the License.
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-create-change-dialog</title>
<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/bower_components/web-component-tester/browser.js"></script>
<script src="../../../test/test-pre-setup.js"></script>
<link rel="import" href="../../../test/common-test-setup.html"/>
<link rel="import" href="gr-create-change-dialog.html">
<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/components/wct-browser-legacy/browser.js"></script>
<script type="module" src="../../../test/test-pre-setup.js"></script>
<script type="module" src="../../../test/common-test-setup.js"></script>
<script type="module" src="./gr-create-change-dialog.js"></script>
<script>void(0);</script>
<script type="module">
import '../../../test/test-pre-setup.js';
import '../../../test/common-test-setup.js';
import './gr-create-change-dialog.js';
void(0);
</script>
<test-fixture id="basic">
<template>
@ -35,136 +40,138 @@ limitations under the License.
</template>
</test-fixture>
<script>
suite('gr-create-change-dialog tests', async () => {
await readyToTest();
let element;
let sandbox;
<script type="module">
import '../../../test/test-pre-setup.js';
import '../../../test/common-test-setup.js';
import './gr-create-change-dialog.js';
suite('gr-create-change-dialog tests', () => {
let element;
let sandbox;
setup(() => {
sandbox = sinon.sandbox.create();
stub('gr-rest-api-interface', {
getLoggedIn() { return Promise.resolve(true); },
getRepoBranches(input) {
if (input.startsWith('test')) {
return Promise.resolve([
{
ref: 'refs/heads/test-branch',
revision: '67ebf73496383c6777035e374d2d664009e2aa5c',
can_delete: true,
},
]);
} else {
return Promise.resolve({});
}
},
});
element = fixture('basic');
element.repoName = 'test-repo',
element._repoConfig = {
private_by_default: {
configured_value: 'FALSE',
inherited_value: false,
},
};
setup(() => {
sandbox = sinon.sandbox.create();
stub('gr-rest-api-interface', {
getLoggedIn() { return Promise.resolve(true); },
getRepoBranches(input) {
if (input.startsWith('test')) {
return Promise.resolve([
{
ref: 'refs/heads/test-branch',
revision: '67ebf73496383c6777035e374d2d664009e2aa5c',
can_delete: true,
},
]);
} else {
return Promise.resolve({});
}
},
});
teardown(() => {
sandbox.restore();
});
test('new change created with default', done => {
const configInputObj = {
branch: 'test-branch',
subject: 'first change created with polygerrit ui',
topic: 'test-topic',
is_private: false,
work_in_progress: true,
};
const saveStub = sandbox.stub(element.$.restAPI,
'createChange', () => Promise.resolve({}));
element.branch = 'test-branch';
element.topic = 'test-topic';
element.subject = 'first change created with polygerrit ui';
assert.isFalse(element.$.privateChangeCheckBox.checked);
element.$.branchInput.bindValue = configInputObj.branch;
element.$.tagNameInput.bindValue = configInputObj.topic;
element.$.messageInput.bindValue = configInputObj.subject;
element.handleCreateChange().then(() => {
// Private change
assert.isFalse(saveStub.lastCall.args[4]);
// WIP Change
assert.isTrue(saveStub.lastCall.args[5]);
assert.isTrue(saveStub.called);
done();
});
});
test('new change created with private', done => {
element.privateByDefault = {
configured_value: 'TRUE',
element = fixture('basic');
element.repoName = 'test-repo',
element._repoConfig = {
private_by_default: {
configured_value: 'FALSE',
inherited_value: false,
};
sandbox.stub(element, '_formatBooleanString', () => Promise.resolve(true));
flushAsynchronousOperations();
},
};
});
const configInputObj = {
branch: 'test-branch',
subject: 'first change created with polygerrit ui',
topic: 'test-topic',
is_private: true,
work_in_progress: true,
};
teardown(() => {
sandbox.restore();
});
const saveStub = sandbox.stub(element.$.restAPI,
'createChange', () => Promise.resolve({}));
test('new change created with default', done => {
const configInputObj = {
branch: 'test-branch',
subject: 'first change created with polygerrit ui',
topic: 'test-topic',
is_private: false,
work_in_progress: true,
};
element.branch = 'test-branch';
element.topic = 'test-topic';
element.subject = 'first change created with polygerrit ui';
assert.isTrue(element.$.privateChangeCheckBox.checked);
const saveStub = sandbox.stub(element.$.restAPI,
'createChange', () => Promise.resolve({}));
element.$.branchInput.bindValue = configInputObj.branch;
element.$.tagNameInput.bindValue = configInputObj.topic;
element.$.messageInput.bindValue = configInputObj.subject;
element.branch = 'test-branch';
element.topic = 'test-topic';
element.subject = 'first change created with polygerrit ui';
assert.isFalse(element.$.privateChangeCheckBox.checked);
element.handleCreateChange().then(() => {
// Private change
assert.isTrue(saveStub.lastCall.args[4]);
// WIP Change
assert.isTrue(saveStub.lastCall.args[5]);
assert.isTrue(saveStub.called);
done();
});
});
element.$.branchInput.bindValue = configInputObj.branch;
element.$.tagNameInput.bindValue = configInputObj.topic;
element.$.messageInput.bindValue = configInputObj.subject;
test('_getRepoBranchesSuggestions empty', done => {
element._getRepoBranchesSuggestions('nonexistent').then(branches => {
assert.equal(branches.length, 0);
done();
});
});
test('_getRepoBranchesSuggestions non-empty', done => {
element._getRepoBranchesSuggestions('test-branch').then(branches => {
assert.equal(branches.length, 1);
assert.equal(branches[0].name, 'test-branch');
done();
});
});
test('_computeBranchClass', () => {
assert.equal(element._computeBranchClass(true), 'hide');
assert.equal(element._computeBranchClass(false), '');
});
test('_computePrivateSectionClass', () => {
assert.equal(element._computePrivateSectionClass(true), 'hide');
assert.equal(element._computePrivateSectionClass(false), '');
element.handleCreateChange().then(() => {
// Private change
assert.isFalse(saveStub.lastCall.args[4]);
// WIP Change
assert.isTrue(saveStub.lastCall.args[5]);
assert.isTrue(saveStub.called);
done();
});
});
test('new change created with private', done => {
element.privateByDefault = {
configured_value: 'TRUE',
inherited_value: false,
};
sandbox.stub(element, '_formatBooleanString', () => Promise.resolve(true));
flushAsynchronousOperations();
const configInputObj = {
branch: 'test-branch',
subject: 'first change created with polygerrit ui',
topic: 'test-topic',
is_private: true,
work_in_progress: true,
};
const saveStub = sandbox.stub(element.$.restAPI,
'createChange', () => Promise.resolve({}));
element.branch = 'test-branch';
element.topic = 'test-topic';
element.subject = 'first change created with polygerrit ui';
assert.isTrue(element.$.privateChangeCheckBox.checked);
element.$.branchInput.bindValue = configInputObj.branch;
element.$.tagNameInput.bindValue = configInputObj.topic;
element.$.messageInput.bindValue = configInputObj.subject;
element.handleCreateChange().then(() => {
// Private change
assert.isTrue(saveStub.lastCall.args[4]);
// WIP Change
assert.isTrue(saveStub.lastCall.args[5]);
assert.isTrue(saveStub.called);
done();
});
});
test('_getRepoBranchesSuggestions empty', done => {
element._getRepoBranchesSuggestions('nonexistent').then(branches => {
assert.equal(branches.length, 0);
done();
});
});
test('_getRepoBranchesSuggestions non-empty', done => {
element._getRepoBranchesSuggestions('test-branch').then(branches => {
assert.equal(branches.length, 1);
assert.equal(branches[0].name, 'test-branch');
done();
});
});
test('_computeBranchClass', () => {
assert.equal(element._computeBranchClass(true), 'hide');
assert.equal(element._computeBranchClass(false), '');
});
test('_computePrivateSectionClass', () => {
assert.equal(element._computePrivateSectionClass(true), 'hide');
assert.equal(element._computePrivateSectionClass(false), '');
});
});
</script>

View File

@ -14,65 +14,77 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function() {
'use strict';
import '../../../scripts/bundled-polymer.js';
/**
* @appliesMixin Gerrit.BaseUrlMixin
* @appliesMixin Gerrit.URLEncodingMixin
* @extends Polymer.Element
*/
class GrCreateGroupDialog extends Polymer.mixinBehaviors( [
Gerrit.BaseUrlBehavior,
Gerrit.URLEncodingBehavior,
], Polymer.GestureEventListeners(
Polymer.LegacyElementMixin(
Polymer.Element))) {
static get is() { return 'gr-create-group-dialog'; }
import '../../../behaviors/base-url-behavior/base-url-behavior.js';
import '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
import '@polymer/iron-input/iron-input.js';
import '../../../styles/gr-form-styles.js';
import '../../../styles/shared-styles.js';
import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
import {PolymerElement} from '@polymer/polymer/polymer-element.js';
import {htmlTemplate} from './gr-create-group-dialog_html.js';
static get properties() {
return {
params: Object,
hasNewGroupName: {
type: Boolean,
notify: true,
value: false,
},
_name: Object,
_groupCreated: {
type: Boolean,
value: false,
},
};
}
/**
* @appliesMixin Gerrit.BaseUrlMixin
* @appliesMixin Gerrit.URLEncodingMixin
* @extends Polymer.Element
*/
class GrCreateGroupDialog extends mixinBehaviors( [
Gerrit.BaseUrlBehavior,
Gerrit.URLEncodingBehavior,
], GestureEventListeners(
LegacyElementMixin(
PolymerElement))) {
static get template() { return htmlTemplate; }
static get observers() {
return [
'_updateGroupName(_name)',
];
}
static get is() { return 'gr-create-group-dialog'; }
_computeGroupUrl(groupId) {
return this.getBaseUrl() + '/admin/groups/' +
this.encodeURL(groupId, true);
}
_updateGroupName(name) {
this.hasNewGroupName = !!name;
}
handleCreateGroup() {
return this.$.restAPI.createGroup({name: this._name})
.then(groupRegistered => {
if (groupRegistered.status !== 201) { return; }
this._groupCreated = true;
return this.$.restAPI.getGroupConfig(this._name)
.then(group => {
page.show(this._computeGroupUrl(group.group_id));
});
});
}
static get properties() {
return {
params: Object,
hasNewGroupName: {
type: Boolean,
notify: true,
value: false,
},
_name: Object,
_groupCreated: {
type: Boolean,
value: false,
},
};
}
customElements.define(GrCreateGroupDialog.is, GrCreateGroupDialog);
})();
static get observers() {
return [
'_updateGroupName(_name)',
];
}
_computeGroupUrl(groupId) {
return this.getBaseUrl() + '/admin/groups/' +
this.encodeURL(groupId, true);
}
_updateGroupName(name) {
this.hasNewGroupName = !!name;
}
handleCreateGroup() {
return this.$.restAPI.createGroup({name: this._name})
.then(groupRegistered => {
if (groupRegistered.status !== 201) { return; }
this._groupCreated = true;
return this.$.restAPI.getGroupConfig(this._name)
.then(group => {
page.show(this._computeGroupUrl(group.group_id));
});
});
}
}
customElements.define(GrCreateGroupDialog.is, GrCreateGroupDialog);

View File

@ -1,31 +1,22 @@
<!--
@license
Copyright (C) 2017 The Android Open Source Project
/**
* @license
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {html} from '@polymer/polymer/lib/utils/html-tag.js';
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<link rel="import" href="/bower_components/polymer/polymer.html">
<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
<link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html">
<link rel="import" href="/bower_components/iron-input/iron-input.html">
<link rel="import" href="../../../styles/gr-form-styles.html">
<link rel="import" href="../../../styles/shared-styles.html">
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
<dom-module id="gr-create-group-dialog">
<template>
export const htmlTemplate = html`
<style include="shared-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
@ -41,16 +32,11 @@ limitations under the License.
<div id="form">
<section>
<span class="title">Group name</span>
<iron-input
bind-value="{{_name}}">
<input
is="iron-input"
bind-value="{{_name}}">
<iron-input bind-value="{{_name}}">
<input is="iron-input" bind-value="{{_name}}">
</iron-input>
</section>
</div>
</div>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
</template>
<script src="gr-create-group-dialog.js"></script>
</dom-module>
`;

View File

@ -19,16 +19,21 @@ limitations under the License.
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-create-group-dialog</title>
<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/bower_components/page/page.js"></script>
<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/bower_components/web-component-tester/browser.js"></script>
<script src="../../../test/test-pre-setup.js"></script>
<link rel="import" href="../../../test/common-test-setup.html"/>
<link rel="import" href="gr-create-group-dialog.html">
<script src="/node_modules/page/page.js"></script>
<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/components/wct-browser-legacy/browser.js"></script>
<script type="module" src="../../../test/test-pre-setup.js"></script>
<script type="module" src="../../../test/common-test-setup.js"></script>
<script type="module" src="./gr-create-group-dialog.js"></script>
<script>void(0);</script>
<script type="module">
import '../../../test/test-pre-setup.js';
import '../../../test/common-test-setup.js';
import './gr-create-group-dialog.js';
void(0);
</script>
<test-fixture id="basic">
<template>
@ -36,66 +41,68 @@ limitations under the License.
</template>
</test-fixture>
<script>
suite('gr-create-group-dialog tests', async () => {
await readyToTest();
let element;
let sandbox;
const GROUP_NAME = 'test-group';
<script type="module">
import '../../../test/test-pre-setup.js';
import '../../../test/common-test-setup.js';
import './gr-create-group-dialog.js';
suite('gr-create-group-dialog tests', () => {
let element;
let sandbox;
const GROUP_NAME = 'test-group';
setup(() => {
sandbox = sinon.sandbox.create();
stub('gr-rest-api-interface', {
getLoggedIn() { return Promise.resolve(true); },
});
element = fixture('basic');
setup(() => {
sandbox = sinon.sandbox.create();
stub('gr-rest-api-interface', {
getLoggedIn() { return Promise.resolve(true); },
});
element = fixture('basic');
});
teardown(() => {
sandbox.restore();
});
teardown(() => {
sandbox.restore();
});
test('name is updated correctly', done => {
assert.isFalse(element.hasNewGroupName);
test('name is updated correctly', done => {
assert.isFalse(element.hasNewGroupName);
const inputEl = element.root.querySelector('iron-input');
inputEl.bindValue = GROUP_NAME;
const inputEl = element.root.querySelector('iron-input');
inputEl.bindValue = GROUP_NAME;
setTimeout(() => {
assert.isTrue(element.hasNewGroupName);
assert.deepEqual(element._name, GROUP_NAME);
done();
});
});
test('test for redirecting to group on successful creation', done => {
sandbox.stub(element.$.restAPI, 'createGroup')
.returns(Promise.resolve({status: 201}));
sandbox.stub(element.$.restAPI, 'getGroupConfig')
.returns(Promise.resolve({group_id: 551}));
const showStub = sandbox.stub(page, 'show');
element.handleCreateGroup()
.then(() => {
assert.isTrue(showStub.calledWith('/admin/groups/551'));
done();
});
});
test('test for unsuccessful group creation', done => {
sandbox.stub(element.$.restAPI, 'createGroup')
.returns(Promise.resolve({status: 409}));
sandbox.stub(element.$.restAPI, 'getGroupConfig')
.returns(Promise.resolve({group_id: 551}));
const showStub = sandbox.stub(page, 'show');
element.handleCreateGroup()
.then(() => {
assert.isFalse(showStub.called);
done();
});
setTimeout(() => {
assert.isTrue(element.hasNewGroupName);
assert.deepEqual(element._name, GROUP_NAME);
done();
});
});
test('test for redirecting to group on successful creation', done => {
sandbox.stub(element.$.restAPI, 'createGroup')
.returns(Promise.resolve({status: 201}));
sandbox.stub(element.$.restAPI, 'getGroupConfig')
.returns(Promise.resolve({group_id: 551}));
const showStub = sandbox.stub(page, 'show');
element.handleCreateGroup()
.then(() => {
assert.isTrue(showStub.calledWith('/admin/groups/551'));
done();
});
});
test('test for unsuccessful group creation', done => {
sandbox.stub(element.$.restAPI, 'createGroup')
.returns(Promise.resolve({status: 409}));
sandbox.stub(element.$.restAPI, 'getGroupConfig')
.returns(Promise.resolve({group_id: 551}));
const showStub = sandbox.stub(page, 'show');
element.handleCreateGroup()
.then(() => {
assert.isFalse(showStub.called);
done();
});
});
});
</script>

View File

@ -14,89 +14,103 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function() {
'use strict';
import '../../../scripts/bundled-polymer.js';
const DETAIL_TYPES = {
branches: 'branches',
tags: 'tags',
};
import '../../../behaviors/base-url-behavior/base-url-behavior.js';
import '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
import '@polymer/iron-input/iron-input.js';
import '../../../styles/gr-form-styles.js';
import '../../../styles/shared-styles.js';
import '../../shared/gr-button/gr-button.js';
import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
import '../../shared/gr-select/gr-select.js';
import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
import {PolymerElement} from '@polymer/polymer/polymer-element.js';
import {htmlTemplate} from './gr-create-pointer-dialog_html.js';
/**
* @appliesMixin Gerrit.BaseUrlMixin
* @appliesMixin Gerrit.URLEncodingMixin
* @extends Polymer.Element
*/
class GrCreatePointerDialog extends Polymer.mixinBehaviors( [
Gerrit.BaseUrlBehavior,
Gerrit.URLEncodingBehavior,
], Polymer.GestureEventListeners(
Polymer.LegacyElementMixin(
Polymer.Element))) {
static get is() { return 'gr-create-pointer-dialog'; }
const DETAIL_TYPES = {
branches: 'branches',
tags: 'tags',
};
static get properties() {
return {
detailType: String,
repoName: String,
hasNewItemName: {
type: Boolean,
notify: true,
value: false,
},
itemDetail: String,
_itemName: String,
_itemRevision: String,
_itemAnnotation: String,
};
}
/**
* @appliesMixin Gerrit.BaseUrlMixin
* @appliesMixin Gerrit.URLEncodingMixin
* @extends Polymer.Element
*/
class GrCreatePointerDialog extends mixinBehaviors( [
Gerrit.BaseUrlBehavior,
Gerrit.URLEncodingBehavior,
], GestureEventListeners(
LegacyElementMixin(
PolymerElement))) {
static get template() { return htmlTemplate; }
static get observers() {
return [
'_updateItemName(_itemName)',
];
}
static get is() { return 'gr-create-pointer-dialog'; }
_updateItemName(name) {
this.hasNewItemName = !!name;
}
static get properties() {
return {
detailType: String,
repoName: String,
hasNewItemName: {
type: Boolean,
notify: true,
value: false,
},
itemDetail: String,
_itemName: String,
_itemRevision: String,
_itemAnnotation: String,
};
}
_computeItemUrl(project) {
if (this.itemDetail === DETAIL_TYPES.branches) {
return this.getBaseUrl() + '/admin/repos/' +
this.encodeURL(this.repoName, true) + ',branches';
} else if (this.itemDetail === DETAIL_TYPES.tags) {
return this.getBaseUrl() + '/admin/repos/' +
this.encodeURL(this.repoName, true) + ',tags';
}
}
static get observers() {
return [
'_updateItemName(_itemName)',
];
}
handleCreateItem() {
const USE_HEAD = this._itemRevision ? this._itemRevision : 'HEAD';
if (this.itemDetail === DETAIL_TYPES.branches) {
return this.$.restAPI.createRepoBranch(this.repoName,
this._itemName, {revision: USE_HEAD})
.then(itemRegistered => {
if (itemRegistered.status === 201) {
page.show(this._computeItemUrl(this.itemDetail));
}
});
} else if (this.itemDetail === DETAIL_TYPES.tags) {
return this.$.restAPI.createRepoTag(this.repoName,
this._itemName,
{revision: USE_HEAD, message: this._itemAnnotation || null})
.then(itemRegistered => {
if (itemRegistered.status === 201) {
page.show(this._computeItemUrl(this.itemDetail));
}
});
}
}
_updateItemName(name) {
this.hasNewItemName = !!name;
}
_computeHideItemClass(type) {
return type === DETAIL_TYPES.branches ? 'hideItem' : '';
_computeItemUrl(project) {
if (this.itemDetail === DETAIL_TYPES.branches) {
return this.getBaseUrl() + '/admin/repos/' +
this.encodeURL(this.repoName, true) + ',branches';
} else if (this.itemDetail === DETAIL_TYPES.tags) {
return this.getBaseUrl() + '/admin/repos/' +
this.encodeURL(this.repoName, true) + ',tags';
}
}
customElements.define(GrCreatePointerDialog.is, GrCreatePointerDialog);
})();
handleCreateItem() {
const USE_HEAD = this._itemRevision ? this._itemRevision : 'HEAD';
if (this.itemDetail === DETAIL_TYPES.branches) {
return this.$.restAPI.createRepoBranch(this.repoName,
this._itemName, {revision: USE_HEAD})
.then(itemRegistered => {
if (itemRegistered.status === 201) {
page.show(this._computeItemUrl(this.itemDetail));
}
});
} else if (this.itemDetail === DETAIL_TYPES.tags) {
return this.$.restAPI.createRepoTag(this.repoName,
this._itemName,
{revision: USE_HEAD, message: this._itemAnnotation || null})
.then(itemRegistered => {
if (itemRegistered.status === 201) {
page.show(this._computeItemUrl(this.itemDetail));
}
});
}
}
_computeHideItemClass(type) {
return type === DETAIL_TYPES.branches ? 'hideItem' : '';
}
}
customElements.define(GrCreatePointerDialog.is, GrCreatePointerDialog);

View File

@ -1,33 +1,22 @@
<!--
@license
Copyright (C) 2017 The Android Open Source Project
/**
* @license
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {html} from '@polymer/polymer/lib/utils/html-tag.js';
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<link rel="import" href="/bower_components/polymer/polymer.html">
<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
<link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html">
<link rel="import" href="/bower_components/iron-input/iron-input.html">
<link rel="import" href="../../../styles/gr-form-styles.html">
<link rel="import" href="../../../styles/shared-styles.html">
<link rel="import" href="../../shared/gr-button/gr-button.html">
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
<link rel="import" href="../../shared/gr-select/gr-select.html">
<dom-module id="gr-create-pointer-dialog">
<template>
export const htmlTemplate = html`
<style include="shared-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
@ -49,41 +38,23 @@ limitations under the License.
<div id="form">
<section id="itemNameSection">
<span class="title">[[detailType]] name</span>
<iron-input
placeholder="[[detailType]] Name"
bind-value="{{_itemName}}">
<input
is="iron-input"
placeholder="[[detailType]] Name"
bind-value="{{_itemName}}">
<iron-input placeholder="[[detailType]] Name" bind-value="{{_itemName}}">
<input is="iron-input" placeholder="[[detailType]] Name" bind-value="{{_itemName}}">
</iron-input>
</section>
<section id="itemRevisionSection">
<span class="title">Initial Revision</span>
<iron-input
placeholder="Revision (Branch or SHA-1)"
bind-value="{{_itemRevision}}">
<input
is="iron-input"
placeholder="Revision (Branch or SHA-1)"
bind-value="{{_itemRevision}}">
<iron-input placeholder="Revision (Branch or SHA-1)" bind-value="{{_itemRevision}}">
<input is="iron-input" placeholder="Revision (Branch or SHA-1)" bind-value="{{_itemRevision}}">
</iron-input>
</section>
<section id="itemAnnotationSection"
class$="[[_computeHideItemClass(itemDetail)]]">
<section id="itemAnnotationSection" class\$="[[_computeHideItemClass(itemDetail)]]">
<span class="title">Annotation</span>
<iron-input
placeholder="Annotation (Optional)"
bind-value="{{_itemAnnotation}}">
<input
is="iron-input"
placeholder="Annotation (Optional)"
bind-value="{{_itemAnnotation}}">
<iron-input placeholder="Annotation (Optional)" bind-value="{{_itemAnnotation}}">
<input is="iron-input" placeholder="Annotation (Optional)" bind-value="{{_itemAnnotation}}">
</iron-input>
</section>
</div>
</div>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
</template>
<script src="gr-create-pointer-dialog.js"></script>
</dom-module>
`;

View File

@ -19,15 +19,20 @@ limitations under the License.
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-create-pointer-dialog</title>
<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/bower_components/web-component-tester/browser.js"></script>
<script src="../../../test/test-pre-setup.js"></script>
<link rel="import" href="../../../test/common-test-setup.html"/>
<link rel="import" href="gr-create-pointer-dialog.html">
<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/components/wct-browser-legacy/browser.js"></script>
<script type="module" src="../../../test/test-pre-setup.js"></script>
<script type="module" src="../../../test/common-test-setup.js"></script>
<script type="module" src="./gr-create-pointer-dialog.js"></script>
<script>void(0);</script>
<script type="module">
import '../../../test/test-pre-setup.js';
import '../../../test/common-test-setup.js';
import './gr-create-pointer-dialog.js';
void(0);
</script>
<test-fixture id="basic">
<template>
@ -35,103 +40,106 @@ limitations under the License.
</template>
</test-fixture>
<script>
suite('gr-create-pointer-dialog tests', async () => {
await readyToTest();
let element;
let sandbox;
<script type="module">
import '../../../test/test-pre-setup.js';
import '../../../test/common-test-setup.js';
import './gr-create-pointer-dialog.js';
import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
suite('gr-create-pointer-dialog tests', () => {
let element;
let sandbox;
const ironInput = function(element) {
return Polymer.dom(element).querySelector('iron-input');
};
const ironInput = function(element) {
return dom(element).querySelector('iron-input');
};
setup(() => {
sandbox = sinon.sandbox.create();
stub('gr-rest-api-interface', {
getLoggedIn() { return Promise.resolve(true); },
});
element = fixture('basic');
setup(() => {
sandbox = sinon.sandbox.create();
stub('gr-rest-api-interface', {
getLoggedIn() { return Promise.resolve(true); },
});
element = fixture('basic');
});
teardown(() => {
sandbox.restore();
});
teardown(() => {
sandbox.restore();
});
test('branch created', done => {
sandbox.stub(
element.$.restAPI,
'createRepoBranch',
() => Promise.resolve({}));
test('branch created', done => {
sandbox.stub(
element.$.restAPI,
'createRepoBranch',
() => Promise.resolve({}));
assert.isFalse(element.hasNewItemName);
assert.isFalse(element.hasNewItemName);
element._itemName = 'test-branch';
element.itemDetail = 'branches';
element._itemName = 'test-branch';
element.itemDetail = 'branches';
ironInput(element.$.itemNameSection).bindValue = 'test-branch2';
ironInput(element.$.itemRevisionSection).bindValue = 'HEAD';
ironInput(element.$.itemNameSection).bindValue = 'test-branch2';
ironInput(element.$.itemRevisionSection).bindValue = 'HEAD';
setTimeout(() => {
assert.isTrue(element.hasNewItemName);
assert.equal(element._itemName, 'test-branch2');
assert.equal(element._itemRevision, 'HEAD');
done();
});
});
test('tag created', done => {
sandbox.stub(
element.$.restAPI,
'createRepoTag',
() => Promise.resolve({}));
assert.isFalse(element.hasNewItemName);
element._itemName = 'test-tag';
element.itemDetail = 'tags';
ironInput(element.$.itemNameSection).bindValue = 'test-tag2';
ironInput(element.$.itemRevisionSection).bindValue = 'HEAD';
setTimeout(() => {
assert.isTrue(element.hasNewItemName);
assert.equal(element._itemName, 'test-tag2');
assert.equal(element._itemRevision, 'HEAD');
done();
});
});
test('tag created with annotations', done => {
sandbox.stub(
element.$.restAPI,
'createRepoTag',
() => Promise.resolve({}));
assert.isFalse(element.hasNewItemName);
element._itemName = 'test-tag';
element._itemAnnotation = 'test-message';
element.itemDetail = 'tags';
ironInput(element.$.itemNameSection).bindValue = 'test-tag2';
ironInput(element.$.itemAnnotationSection).bindValue = 'test-message2';
ironInput(element.$.itemRevisionSection).bindValue = 'HEAD';
setTimeout(() => {
assert.isTrue(element.hasNewItemName);
assert.equal(element._itemName, 'test-tag2');
assert.equal(element._itemAnnotation, 'test-message2');
assert.equal(element._itemRevision, 'HEAD');
done();
});
});
test('_computeHideItemClass returns hideItem if type is branches', () => {
assert.equal(element._computeHideItemClass('branches'), 'hideItem');
});
test('_computeHideItemClass returns strings if not branches', () => {
assert.equal(element._computeHideItemClass('tags'), '');
setTimeout(() => {
assert.isTrue(element.hasNewItemName);
assert.equal(element._itemName, 'test-branch2');
assert.equal(element._itemRevision, 'HEAD');
done();
});
});
test('tag created', done => {
sandbox.stub(
element.$.restAPI,
'createRepoTag',
() => Promise.resolve({}));
assert.isFalse(element.hasNewItemName);
element._itemName = 'test-tag';
element.itemDetail = 'tags';
ironInput(element.$.itemNameSection).bindValue = 'test-tag2';
ironInput(element.$.itemRevisionSection).bindValue = 'HEAD';
setTimeout(() => {
assert.isTrue(element.hasNewItemName);
assert.equal(element._itemName, 'test-tag2');
assert.equal(element._itemRevision, 'HEAD');
done();
});
});
test('tag created with annotations', done => {
sandbox.stub(
element.$.restAPI,
'createRepoTag',
() => Promise.resolve({}));
assert.isFalse(element.hasNewItemName);
element._itemName = 'test-tag';
element._itemAnnotation = 'test-message';
element.itemDetail = 'tags';
ironInput(element.$.itemNameSection).bindValue = 'test-tag2';
ironInput(element.$.itemAnnotationSection).bindValue = 'test-message2';
ironInput(element.$.itemRevisionSection).bindValue = 'HEAD';
setTimeout(() => {
assert.isTrue(element.hasNewItemName);
assert.equal(element._itemName, 'test-tag2');
assert.equal(element._itemAnnotation, 'test-message2');
assert.equal(element._itemRevision, 'HEAD');
done();
});
});
test('_computeHideItemClass returns hideItem if type is branches', () => {
assert.equal(element._computeHideItemClass('branches'), 'hideItem');
});
test('_computeHideItemClass returns strings if not branches', () => {
assert.equal(element._computeHideItemClass('tags'), '');
});
});
</script>

View File

@ -14,130 +14,145 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function() {
'use strict';
import '../../../scripts/bundled-polymer.js';
/**
* @appliesMixin Gerrit.BaseUrlMixin
* @appliesMixin Gerrit.URLEncodingMixin
* @extends Polymer.Element
*/
class GrCreateRepoDialog extends Polymer.mixinBehaviors( [
Gerrit.BaseUrlBehavior,
Gerrit.URLEncodingBehavior,
], Polymer.GestureEventListeners(
Polymer.LegacyElementMixin(
Polymer.Element))) {
static get is() { return 'gr-create-repo-dialog'; }
import '../../../behaviors/base-url-behavior/base-url-behavior.js';
import '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
import '@polymer/iron-input/iron-input.js';
import '../../../styles/gr-form-styles.js';
import '../../../styles/shared-styles.js';
import '../../shared/gr-autocomplete/gr-autocomplete.js';
import '../../shared/gr-button/gr-button.js';
import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
import '../../shared/gr-select/gr-select.js';
import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
import {PolymerElement} from '@polymer/polymer/polymer-element.js';
import {htmlTemplate} from './gr-create-repo-dialog_html.js';
static get properties() {
return {
params: Object,
hasNewRepoName: {
type: Boolean,
notify: true,
value: false,
/**
* @appliesMixin Gerrit.BaseUrlMixin
* @appliesMixin Gerrit.URLEncodingMixin
* @extends Polymer.Element
*/
class GrCreateRepoDialog extends mixinBehaviors( [
Gerrit.BaseUrlBehavior,
Gerrit.URLEncodingBehavior,
], GestureEventListeners(
LegacyElementMixin(
PolymerElement))) {
static get template() { return htmlTemplate; }
static get is() { return 'gr-create-repo-dialog'; }
static get properties() {
return {
params: Object,
hasNewRepoName: {
type: Boolean,
notify: true,
value: false,
},
/** @type {?} */
_repoConfig: {
type: Object,
value: () => {
// Set default values for dropdowns.
return {
create_empty_commit: true,
permissions_only: false,
};
},
},
_repoCreated: {
type: Boolean,
value: false,
},
_repoOwner: String,
_repoOwnerId: {
type: String,
observer: '_repoOwnerIdUpdate',
},
/** @type {?} */
_repoConfig: {
type: Object,
value: () => {
// Set default values for dropdowns.
return {
create_empty_commit: true,
permissions_only: false,
};
},
_query: {
type: Function,
value() {
return this._getRepoSuggestions.bind(this);
},
_repoCreated: {
type: Boolean,
value: false,
},
_repoOwner: String,
_repoOwnerId: {
type: String,
observer: '_repoOwnerIdUpdate',
},
_queryGroups: {
type: Function,
value() {
return this._getGroupSuggestions.bind(this);
},
},
};
}
_query: {
type: Function,
value() {
return this._getRepoSuggestions.bind(this);
},
},
_queryGroups: {
type: Function,
value() {
return this._getGroupSuggestions.bind(this);
},
},
};
}
static get observers() {
return [
'_updateRepoName(_repoConfig.name)',
];
}
static get observers() {
return [
'_updateRepoName(_repoConfig.name)',
];
}
_computeRepoUrl(repoName) {
return this.getBaseUrl() + '/admin/repos/' +
this.encodeURL(repoName, true);
}
_computeRepoUrl(repoName) {
return this.getBaseUrl() + '/admin/repos/' +
this.encodeURL(repoName, true);
}
_updateRepoName(name) {
this.hasNewRepoName = !!name;
}
_updateRepoName(name) {
this.hasNewRepoName = !!name;
}
_repoOwnerIdUpdate(id) {
if (id) {
this.set('_repoConfig.owners', [id]);
} else {
this.set('_repoConfig.owners', undefined);
}
}
handleCreateRepo() {
return this.$.restAPI.createRepo(this._repoConfig)
.then(repoRegistered => {
if (repoRegistered.status === 201) {
this._repoCreated = true;
page.show(this._computeRepoUrl(this._repoConfig.name));
}
});
}
_getRepoSuggestions(input) {
return this.$.restAPI.getSuggestedProjects(input)
.then(response => {
const repos = [];
for (const key in response) {
if (!response.hasOwnProperty(key)) { continue; }
repos.push({
name: key,
value: response[key],
});
}
return repos;
});
}
_getGroupSuggestions(input) {
return this.$.restAPI.getSuggestedGroups(input)
.then(response => {
const groups = [];
for (const key in response) {
if (!response.hasOwnProperty(key)) { continue; }
groups.push({
name: key,
value: decodeURIComponent(response[key].id),
});
}
return groups;
});
_repoOwnerIdUpdate(id) {
if (id) {
this.set('_repoConfig.owners', [id]);
} else {
this.set('_repoConfig.owners', undefined);
}
}
customElements.define(GrCreateRepoDialog.is, GrCreateRepoDialog);
})();
handleCreateRepo() {
return this.$.restAPI.createRepo(this._repoConfig)
.then(repoRegistered => {
if (repoRegistered.status === 201) {
this._repoCreated = true;
page.show(this._computeRepoUrl(this._repoConfig.name));
}
});
}
_getRepoSuggestions(input) {
return this.$.restAPI.getSuggestedProjects(input)
.then(response => {
const repos = [];
for (const key in response) {
if (!response.hasOwnProperty(key)) { continue; }
repos.push({
name: key,
value: response[key],
});
}
return repos;
});
}
_getGroupSuggestions(input) {
return this.$.restAPI.getSuggestedGroups(input)
.then(response => {
const groups = [];
for (const key in response) {
if (!response.hasOwnProperty(key)) { continue; }
groups.push({
name: key,
value: decodeURIComponent(response[key].id),
});
}
return groups;
});
}
}
customElements.define(GrCreateRepoDialog.is, GrCreateRepoDialog);

View File

@ -1,34 +1,22 @@
<!--
@license
Copyright (C) 2017 The Android Open Source Project
/**
* @license
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {html} from '@polymer/polymer/lib/utils/html-tag.js';
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<link rel="import" href="/bower_components/polymer/polymer.html">
<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
<link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html">
<link rel="import" href="/bower_components/iron-input/iron-input.html">
<link rel="import" href="../../../styles/gr-form-styles.html">
<link rel="import" href="../../../styles/shared-styles.html">
<link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html">
<link rel="import" href="../../shared/gr-button/gr-button.html">
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
<link rel="import" href="../../shared/gr-select/gr-select.html">
<dom-module id="gr-create-repo-dialog">
<template>
export const htmlTemplate = html`
<style include="shared-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
@ -48,42 +36,28 @@ limitations under the License.
<div id="form">
<section>
<span class="title">Repository name</span>
<iron-input autocomplete="on"
bind-value="{{_repoConfig.name}}">
<input is="iron-input"
id="repoNameInput"
autocomplete="on"
bind-value="{{_repoConfig.name}}">
<iron-input autocomplete="on" bind-value="{{_repoConfig.name}}">
<input is="iron-input" id="repoNameInput" autocomplete="on" bind-value="{{_repoConfig.name}}">
</iron-input>
</section>
<section>
<span class="title">Rights inherit from</span>
<span class="value">
<gr-autocomplete
id="rightsInheritFromInput"
text="{{_repoConfig.parent}}"
query="[[_query]]"
placeholder="Optional, defaults to 'All-Projects'">
<gr-autocomplete id="rightsInheritFromInput" text="{{_repoConfig.parent}}" query="[[_query]]" placeholder="Optional, defaults to 'All-Projects'">
</gr-autocomplete>
</span>
</section>
<section>
<span class="title">Owner</span>
<span class="value">
<gr-autocomplete
id="ownerInput"
text="{{_repoOwner}}"
value="{{_repoOwnerId}}"
query="[[_queryGroups]]">
<gr-autocomplete id="ownerInput" text="{{_repoOwner}}" value="{{_repoOwnerId}}" query="[[_queryGroups]]">
</gr-autocomplete>
</span>
</section>
<section>
<span class="title">Create initial empty commit</span>
<span class="value">
<gr-select
id="initialCommit"
bind-value="{{_repoConfig.create_empty_commit}}">
<gr-select id="initialCommit" bind-value="{{_repoConfig.create_empty_commit}}">
<select>
<option value="false">False</option>
<option value="true">True</option>
@ -94,9 +68,7 @@ limitations under the License.
<section>
<span class="title">Only serve as parent for other repositories</span>
<span class="value">
<gr-select
id="parentRepo"
bind-value="{{_repoConfig.permissions_only}}">
<gr-select id="parentRepo" bind-value="{{_repoConfig.permissions_only}}">
<select>
<option value="false">False</option>
<option value="true">True</option>
@ -107,6 +79,4 @@ limitations under the License.
</div>
</div>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
</template>
<script src="gr-create-repo-dialog.js"></script>
</dom-module>
`;

View File

@ -19,15 +19,20 @@ limitations under the License.
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-create-repo-dialog</title>
<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/bower_components/web-component-tester/browser.js"></script>
<script src="../../../test/test-pre-setup.js"></script>
<link rel="import" href="../../../test/common-test-setup.html"/>
<link rel="import" href="gr-create-repo-dialog.html">
<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/components/wct-browser-legacy/browser.js"></script>
<script type="module" src="../../../test/test-pre-setup.js"></script>
<script type="module" src="../../../test/common-test-setup.js"></script>
<script type="module" src="./gr-create-repo-dialog.js"></script>
<script>void(0);</script>
<script type="module">
import '../../../test/test-pre-setup.js';
import '../../../test/common-test-setup.js';
import './gr-create-repo-dialog.js';
void(0);
</script>
<test-fixture id="basic">
<template>
@ -35,74 +40,76 @@ limitations under the License.
</template>
</test-fixture>
<script>
suite('gr-create-repo-dialog tests', async () => {
await readyToTest();
let element;
let sandbox;
<script type="module">
import '../../../test/test-pre-setup.js';
import '../../../test/common-test-setup.js';
import './gr-create-repo-dialog.js';
suite('gr-create-repo-dialog tests', () => {
let element;
let sandbox;
setup(() => {
sandbox = sinon.sandbox.create();
stub('gr-rest-api-interface', {
getLoggedIn() { return Promise.resolve(true); },
});
element = fixture('basic');
setup(() => {
sandbox = sinon.sandbox.create();
stub('gr-rest-api-interface', {
getLoggedIn() { return Promise.resolve(true); },
});
element = fixture('basic');
});
teardown(() => {
sandbox.restore();
});
teardown(() => {
sandbox.restore();
});
test('default values are populated', () => {
assert.isTrue(element.$.initialCommit.bindValue);
assert.isFalse(element.$.parentRepo.bindValue);
});
test('default values are populated', () => {
assert.isTrue(element.$.initialCommit.bindValue);
assert.isFalse(element.$.parentRepo.bindValue);
});
test('repo created', done => {
const configInputObj = {
name: 'test-repo',
create_empty_commit: true,
parent: 'All-Project',
permissions_only: false,
owners: ['testId'],
};
test('repo created', done => {
const configInputObj = {
name: 'test-repo',
create_empty_commit: true,
parent: 'All-Project',
permissions_only: false,
owners: ['testId'],
};
const saveStub = sandbox.stub(element.$.restAPI,
'createRepo', () => Promise.resolve({}));
const saveStub = sandbox.stub(element.$.restAPI,
'createRepo', () => Promise.resolve({}));
assert.isFalse(element.hasNewRepoName);
assert.isFalse(element.hasNewRepoName);
element._repoConfig = {
name: 'test-repo',
create_empty_commit: true,
parent: 'All-Project',
permissions_only: false,
};
element._repoConfig = {
name: 'test-repo',
create_empty_commit: true,
parent: 'All-Project',
permissions_only: false,
};
element._repoOwner = 'test';
element._repoOwnerId = 'testId';
element._repoOwner = 'test';
element._repoOwnerId = 'testId';
element.$.repoNameInput.bindValue = configInputObj.name;
element.$.rightsInheritFromInput.bindValue = configInputObj.parent;
element.$.ownerInput.text = configInputObj.owners[0];
element.$.initialCommit.bindValue =
configInputObj.create_empty_commit;
element.$.parentRepo.bindValue =
configInputObj.permissions_only;
element.$.repoNameInput.bindValue = configInputObj.name;
element.$.rightsInheritFromInput.bindValue = configInputObj.parent;
element.$.ownerInput.text = configInputObj.owners[0];
element.$.initialCommit.bindValue =
configInputObj.create_empty_commit;
element.$.parentRepo.bindValue =
configInputObj.permissions_only;
assert.isTrue(element.hasNewRepoName);
assert.isTrue(element.hasNewRepoName);
assert.deepEqual(element._repoConfig, configInputObj);
assert.deepEqual(element._repoConfig, configInputObj);
element.handleCreateRepo().then(() => {
assert.isTrue(saveStub.lastCall.calledWithExactly(configInputObj));
done();
});
});
test('testing observer of _repoOwner', () => {
element._repoOwnerId = 'test-5';
assert.deepEqual(element._repoConfig.owners, ['test-5']);
element.handleCreateRepo().then(() => {
assert.isTrue(saveStub.lastCall.calledWithExactly(configInputObj));
done();
});
});
test('testing observer of _repoOwner', () => {
element._repoOwnerId = 'test-5';
assert.deepEqual(element._repoConfig.owners, ['test-5']);
});
});
</script>

View File

@ -14,113 +14,127 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function() {
'use strict';
import '../../../behaviors/gr-list-view-behavior/gr-list-view-behavior.js';
const GROUP_EVENTS = ['ADD_GROUP', 'REMOVE_GROUP'];
import '../../../scripts/bundled-polymer.js';
import '../../../behaviors/fire-behavior/fire-behavior.js';
import '../../../styles/gr-table-styles.js';
import '../../../styles/shared-styles.js';
import '../../core/gr-navigation/gr-navigation.js';
import '../../shared/gr-date-formatter/gr-date-formatter.js';
import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
import '../../shared/gr-account-link/gr-account-link.js';
import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
import {PolymerElement} from '@polymer/polymer/polymer-element.js';
import {htmlTemplate} from './gr-group-audit-log_html.js';
/**
* @appliesMixin Gerrit.FireMixin
* @appliesMixin Gerrit.ListViewMixin
* @extends Polymer.Element
*/
class GrGroupAuditLog extends Polymer.mixinBehaviors( [
Gerrit.FireBehavior,
Gerrit.ListViewBehavior,
], Polymer.GestureEventListeners(
Polymer.LegacyElementMixin(
Polymer.Element))) {
static get is() { return 'gr-group-audit-log'; }
const GROUP_EVENTS = ['ADD_GROUP', 'REMOVE_GROUP'];
static get properties() {
return {
groupId: String,
_auditLog: Array,
_loading: {
type: Boolean,
value: true,
},
};
}
/**
* @appliesMixin Gerrit.FireMixin
* @appliesMixin Gerrit.ListViewMixin
* @extends Polymer.Element
*/
class GrGroupAuditLog extends mixinBehaviors( [
Gerrit.FireBehavior,
Gerrit.ListViewBehavior,
], GestureEventListeners(
LegacyElementMixin(
PolymerElement))) {
static get template() { return htmlTemplate; }
/** @override */
attached() {
super.attached();
this.fire('title-change', {title: 'Audit Log'});
}
static get is() { return 'gr-group-audit-log'; }
/** @override */
ready() {
super.ready();
this._getAuditLogs();
}
_getAuditLogs() {
if (!this.groupId) { return ''; }
const errFn = response => {
this.fire('page-error', {response});
};
return this.$.restAPI.getGroupAuditLog(this.groupId, errFn)
.then(auditLog => {
if (!auditLog) {
this._auditLog = [];
return;
}
this._auditLog = auditLog;
this._loading = false;
});
}
_status(item) {
return item.disabled ? 'Disabled' : 'Enabled';
}
itemType(type) {
let item;
switch (type) {
case 'ADD_GROUP':
case 'ADD_USER':
item = 'Added';
break;
case 'REMOVE_GROUP':
case 'REMOVE_USER':
item = 'Removed';
break;
default:
item = '';
}
return item;
}
_isGroupEvent(type) {
return GROUP_EVENTS.indexOf(type) !== -1;
}
_computeGroupUrl(group) {
if (group && group.url && group.id) {
return Gerrit.Nav.getUrlForGroup(group.id);
}
return '';
}
_getIdForUser(account) {
return account._account_id ? ' (' + account._account_id + ')' : '';
}
_getNameForGroup(group) {
if (group && group.name) {
return group.name;
} else if (group && group.id) {
// The URL encoded id of the member
return decodeURIComponent(group.id);
}
return '';
}
static get properties() {
return {
groupId: String,
_auditLog: Array,
_loading: {
type: Boolean,
value: true,
},
};
}
customElements.define(GrGroupAuditLog.is, GrGroupAuditLog);
})();
/** @override */
attached() {
super.attached();
this.fire('title-change', {title: 'Audit Log'});
}
/** @override */
ready() {
super.ready();
this._getAuditLogs();
}
_getAuditLogs() {
if (!this.groupId) { return ''; }
const errFn = response => {
this.fire('page-error', {response});
};
return this.$.restAPI.getGroupAuditLog(this.groupId, errFn)
.then(auditLog => {
if (!auditLog) {
this._auditLog = [];
return;
}
this._auditLog = auditLog;
this._loading = false;
});
}
_status(item) {
return item.disabled ? 'Disabled' : 'Enabled';
}
itemType(type) {
let item;
switch (type) {
case 'ADD_GROUP':
case 'ADD_USER':
item = 'Added';
break;
case 'REMOVE_GROUP':
case 'REMOVE_USER':
item = 'Removed';
break;
default:
item = '';
}
return item;
}
_isGroupEvent(type) {
return GROUP_EVENTS.indexOf(type) !== -1;
}
_computeGroupUrl(group) {
if (group && group.url && group.id) {
return Gerrit.Nav.getUrlForGroup(group.id);
}
return '';
}
_getIdForUser(account) {
return account._account_id ? ' (' + account._account_id + ')' : '';
}
_getNameForGroup(group) {
if (group && group.name) {
return group.name;
} else if (group && group.id) {
// The URL encoded id of the member
return decodeURIComponent(group.id);
}
return '';
}
}
customElements.define(GrGroupAuditLog.is, GrGroupAuditLog);

View File

@ -1,32 +1,22 @@
<!--
@license
Copyright (C) 2017 The Android Open Source Project
/**
* @license
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {html} from '@polymer/polymer/lib/utils/html-tag.js';
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<link rel="import" href="../../../behaviors/gr-list-view-behavior/gr-list-view-behavior.html">
<link rel="import" href="/bower_components/polymer/polymer.html">
<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
<link rel="import" href="../../../styles/gr-table-styles.html">
<link rel="import" href="../../../styles/shared-styles.html">
<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
<link rel="import" href="../../shared/gr-account-link/gr-account-link.html">
<dom-module id="gr-group-audit-log">
<template>
export const htmlTemplate = html`
<style include="shared-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
@ -38,28 +28,26 @@ limitations under the License.
}
</style>
<table id="list" class="genericList">
<tr class="headerRow">
<tbody><tr class="headerRow">
<th class="date topHeader">Date</th>
<th class="type topHeader">Type</th>
<th class="member topHeader">Member</th>
<th class="by-user topHeader">By User</th>
</tr>
<tr id="loading" class$="loadingMsg [[computeLoadingClass(_loading)]]">
<tr id="loading" class\$="loadingMsg [[computeLoadingClass(_loading)]]">
<td>Loading...</td>
</tr>
<tbody class$="[[computeLoadingClass(_loading)]]">
</tbody><tbody class\$="[[computeLoadingClass(_loading)]]">
<template is="dom-repeat" items="[[_auditLog]]">
<tr class="table">
<td class="date">
<gr-date-formatter
has-tooltip
date-str="[[item.date]]">
<gr-date-formatter has-tooltip="" date-str="[[item.date]]">
</gr-date-formatter>
</td>
<td class="type">[[itemType(item.type)]]</td>
<td class="member">
<template is="dom-if" if="[[_isGroupEvent(item.type)]]">
<a href$="[[_computeGroupUrl(item.member)]]">
<a href\$="[[_computeGroupUrl(item.member)]]">
[[_getNameForGroup(item.member)]]
</a>
</template>
@ -77,6 +65,4 @@ limitations under the License.
</tbody>
</table>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
</template>
<script src="gr-group-audit-log.js"></script>
</dom-module>
`;

View File

@ -19,15 +19,20 @@ limitations under the License.
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-group-audit-log</title>
<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/bower_components/web-component-tester/browser.js"></script>
<script src="../../../test/test-pre-setup.js"></script>
<link rel="import" href="../../../test/common-test-setup.html"/>
<link rel="import" href="gr-group-audit-log.html">
<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/components/wct-browser-legacy/browser.js"></script>
<script type="module" src="../../../test/test-pre-setup.js"></script>
<script type="module" src="../../../test/common-test-setup.js"></script>
<script type="module" src="./gr-group-audit-log.js"></script>
<script>void(0);</script>
<script type="module">
import '../../../test/test-pre-setup.js';
import '../../../test/common-test-setup.js';
import './gr-group-audit-log.js';
void(0);
</script>
<test-fixture id="basic">
<template>
@ -35,85 +40,87 @@ limitations under the License.
</template>
</test-fixture>
<script>
suite('gr-group-audit-log tests', async () => {
await readyToTest();
let element;
let sandbox;
<script type="module">
import '../../../test/test-pre-setup.js';
import '../../../test/common-test-setup.js';
import './gr-group-audit-log.js';
suite('gr-group-audit-log tests', () => {
let element;
let sandbox;
setup(() => {
sandbox = sinon.sandbox.create();
element = fixture('basic');
setup(() => {
sandbox = sinon.sandbox.create();
element = fixture('basic');
});
teardown(() => {
sandbox.restore();
});
suite('members', () => {
test('test _getNameForGroup', () => {
let group = {
member: {
name: 'test-name',
},
};
assert.equal(element._getNameForGroup(group.member), 'test-name');
group = {
member: {
id: 'test-id',
},
};
assert.equal(element._getNameForGroup(group.member), 'test-id');
});
teardown(() => {
sandbox.restore();
});
test('test _isGroupEvent', () => {
assert.isTrue(element._isGroupEvent('ADD_GROUP'));
assert.isTrue(element._isGroupEvent('REMOVE_GROUP'));
suite('members', () => {
test('test _getNameForGroup', () => {
let group = {
member: {
name: 'test-name',
},
};
assert.equal(element._getNameForGroup(group.member), 'test-name');
group = {
member: {
id: 'test-id',
},
};
assert.equal(element._getNameForGroup(group.member), 'test-id');
});
test('test _isGroupEvent', () => {
assert.isTrue(element._isGroupEvent('ADD_GROUP'));
assert.isTrue(element._isGroupEvent('REMOVE_GROUP'));
assert.isFalse(element._isGroupEvent('ADD_USER'));
assert.isFalse(element._isGroupEvent('REMOVE_USER'));
});
});
suite('users', () => {
test('test _getIdForUser', () => {
const account = {
user: {
username: 'test-user',
_account_id: 12,
},
};
assert.equal(element._getIdForUser(account.user), ' (12)');
});
test('test _account_id not present', () => {
const account = {
user: {
username: 'test-user',
},
};
assert.equal(element._getIdForUser(account.user), '');
});
});
suite('404', () => {
test('fires page-error', done => {
element.groupId = 1;
const response = {status: 404};
sandbox.stub(
element.$.restAPI, 'getGroupAuditLog', (group, errFn) => {
errFn(response);
});
element.addEventListener('page-error', e => {
assert.deepEqual(e.detail.response, response);
done();
});
element._getAuditLogs();
});
assert.isFalse(element._isGroupEvent('ADD_USER'));
assert.isFalse(element._isGroupEvent('REMOVE_USER'));
});
});
suite('users', () => {
test('test _getIdForUser', () => {
const account = {
user: {
username: 'test-user',
_account_id: 12,
},
};
assert.equal(element._getIdForUser(account.user), ' (12)');
});
test('test _account_id not present', () => {
const account = {
user: {
username: 'test-user',
},
};
assert.equal(element._getIdForUser(account.user), '');
});
});
suite('404', () => {
test('fires page-error', done => {
element.groupId = 1;
const response = {status: 404};
sandbox.stub(
element.$.restAPI, 'getGroupAuditLog', (group, errFn) => {
errFn(response);
});
element.addEventListener('page-error', e => {
assert.deepEqual(e.detail.response, response);
done();
});
element._getAuditLogs();
});
});
});
</script>

View File

@ -14,280 +14,300 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function() {
'use strict';
import '../../../behaviors/base-url-behavior/base-url-behavior.js';
const SUGGESTIONS_LIMIT = 15;
const SAVING_ERROR_TEXT = 'Group may not exist, or you may not have '+
'permission to add it';
import '../../../behaviors/fire-behavior/fire-behavior.js';
import '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
import '../../../scripts/bundled-polymer.js';
import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
import '../../../styles/gr-form-styles.js';
import '../../../styles/gr-subpage-styles.js';
import '../../../styles/gr-table-styles.js';
import '../../../styles/shared-styles.js';
import '../../shared/gr-account-link/gr-account-link.js';
import '../../shared/gr-autocomplete/gr-autocomplete.js';
import '../../shared/gr-button/gr-button.js';
import '../../shared/gr-overlay/gr-overlay.js';
import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
import '../gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.js';
import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
import {PolymerElement} from '@polymer/polymer/polymer-element.js';
import {htmlTemplate} from './gr-group-members_html.js';
const URL_REGEX = '^(?:[a-z]+:)?//';
const SUGGESTIONS_LIMIT = 15;
const SAVING_ERROR_TEXT = 'Group may not exist, or you may not have '+
'permission to add it';
/**
* @appliesMixin Gerrit.BaseUrlMixin
* @appliesMixin Gerrit.FireMixin
* @appliesMixin Gerrit.URLEncodingMixin
* @extends Polymer.Element
*/
class GrGroupMembers extends Polymer.mixinBehaviors( [
Gerrit.BaseUrlBehavior,
Gerrit.FireBehavior,
Gerrit.URLEncodingBehavior,
], Polymer.GestureEventListeners(
Polymer.LegacyElementMixin(
Polymer.Element))) {
static get is() { return 'gr-group-members'; }
const URL_REGEX = '^(?:[a-z]+:)?//';
static get properties() {
return {
groupId: Number,
_groupMemberSearchId: String,
_groupMemberSearchName: String,
_includedGroupSearchId: String,
_includedGroupSearchName: String,
_loading: {
type: Boolean,
value: true,
/**
* @appliesMixin Gerrit.BaseUrlMixin
* @appliesMixin Gerrit.FireMixin
* @appliesMixin Gerrit.URLEncodingMixin
* @extends Polymer.Element
*/
class GrGroupMembers extends mixinBehaviors( [
Gerrit.BaseUrlBehavior,
Gerrit.FireBehavior,
Gerrit.URLEncodingBehavior,
], GestureEventListeners(
LegacyElementMixin(
PolymerElement))) {
static get template() { return htmlTemplate; }
static get is() { return 'gr-group-members'; }
static get properties() {
return {
groupId: Number,
_groupMemberSearchId: String,
_groupMemberSearchName: String,
_includedGroupSearchId: String,
_includedGroupSearchName: String,
_loading: {
type: Boolean,
value: true,
},
_groupName: String,
_groupMembers: Object,
_includedGroups: Object,
_itemName: String,
_itemType: String,
_queryMembers: {
type: Function,
value() {
return this._getAccountSuggestions.bind(this);
},
_groupName: String,
_groupMembers: Object,
_includedGroups: Object,
_itemName: String,
_itemType: String,
_queryMembers: {
type: Function,
value() {
return this._getAccountSuggestions.bind(this);
},
},
_queryIncludedGroup: {
type: Function,
value() {
return this._getGroupSuggestions.bind(this);
},
_queryIncludedGroup: {
type: Function,
value() {
return this._getGroupSuggestions.bind(this);
},
},
_groupOwner: {
type: Boolean,
value: false,
},
_isAdmin: {
type: Boolean,
value: false,
},
};
}
},
_groupOwner: {
type: Boolean,
value: false,
},
_isAdmin: {
type: Boolean,
value: false,
},
};
}
/** @override */
attached() {
super.attached();
this._loadGroupDetails();
/** @override */
attached() {
super.attached();
this._loadGroupDetails();
this.fire('title-change', {title: 'Members'});
}
this.fire('title-change', {title: 'Members'});
}
_loadGroupDetails() {
if (!this.groupId) { return; }
_loadGroupDetails() {
if (!this.groupId) { return; }
const promises = [];
const promises = [];
const errFn = response => {
this.fire('page-error', {response});
};
const errFn = response => {
this.fire('page-error', {response});
};
return this.$.restAPI.getGroupConfig(this.groupId, errFn)
.then(config => {
if (!config || !config.name) { return Promise.resolve(); }
return this.$.restAPI.getGroupConfig(this.groupId, errFn)
.then(config => {
if (!config || !config.name) { return Promise.resolve(); }
this._groupName = config.name;
this._groupName = config.name;
promises.push(this.$.restAPI.getIsAdmin().then(isAdmin => {
this._isAdmin = isAdmin ? true : false;
}));
promises.push(this.$.restAPI.getIsAdmin().then(isAdmin => {
this._isAdmin = isAdmin ? true : false;
}));
promises.push(this.$.restAPI.getIsGroupOwner(config.name)
.then(isOwner => {
this._groupOwner = isOwner ? true : false;
}));
promises.push(this.$.restAPI.getGroupMembers(config.name).then(
members => {
this._groupMembers = members;
}));
promises.push(this.$.restAPI.getIncludedGroup(config.name)
.then(includedGroup => {
this._includedGroups = includedGroup;
}));
return Promise.all(promises).then(() => {
this._loading = false;
});
});
}
_computeLoadingClass(loading) {
return loading ? 'loading' : '';
}
_isLoading() {
return this._loading || this._loading === undefined;
}
_computeGroupUrl(url) {
if (!url) { return; }
const r = new RegExp(URL_REGEX, 'i');
if (r.test(url)) {
return url;
}
// For GWT compatibility
if (url.startsWith('#')) {
return this.getBaseUrl() + url.slice(1);
}
return this.getBaseUrl() + url;
}
_handleSavingGroupMember() {
return this.$.restAPI.saveGroupMembers(this._groupName,
this._groupMemberSearchId).then(config => {
if (!config) {
return;
}
this.$.restAPI.getGroupMembers(this._groupName).then(members => {
this._groupMembers = members;
});
this._groupMemberSearchName = '';
this._groupMemberSearchId = '';
});
}
_handleDeleteConfirm() {
this.$.overlay.close();
if (this._itemType === 'member') {
return this.$.restAPI.deleteGroupMembers(this._groupName,
this._itemId)
.then(itemDeleted => {
if (itemDeleted.status === 204) {
this.$.restAPI.getGroupMembers(this._groupName)
.then(members => {
this._groupMembers = members;
});
}
});
} else if (this._itemType === 'includedGroup') {
return this.$.restAPI.deleteIncludedGroup(this._groupName,
this._itemId)
.then(itemDeleted => {
if (itemDeleted.status === 204 || itemDeleted.status === 205) {
this.$.restAPI.getIncludedGroup(this._groupName)
.then(includedGroup => {
this._includedGroups = includedGroup;
});
}
});
}
}
_handleConfirmDialogCancel() {
this.$.overlay.close();
}
_handleDeleteMember(e) {
const id = e.model.get('item._account_id');
const name = e.model.get('item.name');
const username = e.model.get('item.username');
const email = e.model.get('item.email');
const item = username || name || email || id;
if (!item) {
return '';
}
this._itemName = item;
this._itemId = id;
this._itemType = 'member';
this.$.overlay.open();
}
_handleSavingIncludedGroups() {
return this.$.restAPI.saveIncludedGroup(this._groupName,
this._includedGroupSearchId.replace(/\+/g, ' '), err => {
if (err.status === 404) {
this.dispatchEvent(new CustomEvent('show-alert', {
detail: {message: SAVING_ERROR_TEXT},
bubbles: true,
composed: true,
promises.push(this.$.restAPI.getIsGroupOwner(config.name)
.then(isOwner => {
this._groupOwner = isOwner ? true : false;
}));
return err;
}
throw Error(err.statusText);
})
.then(config => {
if (!config) {
return;
}
this.$.restAPI.getIncludedGroup(this._groupName)
.then(includedGroup => {
this._includedGroups = includedGroup;
});
this._includedGroupSearchName = '';
this._includedGroupSearchId = '';
promises.push(this.$.restAPI.getGroupMembers(config.name).then(
members => {
this._groupMembers = members;
}));
promises.push(this.$.restAPI.getIncludedGroup(config.name)
.then(includedGroup => {
this._includedGroups = includedGroup;
}));
return Promise.all(promises).then(() => {
this._loading = false;
});
});
}
_computeLoadingClass(loading) {
return loading ? 'loading' : '';
}
_isLoading() {
return this._loading || this._loading === undefined;
}
_computeGroupUrl(url) {
if (!url) { return; }
const r = new RegExp(URL_REGEX, 'i');
if (r.test(url)) {
return url;
}
_handleDeleteIncludedGroup(e) {
const id = decodeURIComponent(e.model.get('item.id')).replace(/\+/g, ' ');
const name = e.model.get('item.name');
const item = name || id;
if (!item) { return ''; }
this._itemName = item;
this._itemId = id;
this._itemType = 'includedGroup';
this.$.overlay.open();
// For GWT compatibility
if (url.startsWith('#')) {
return this.getBaseUrl() + url.slice(1);
}
return this.getBaseUrl() + url;
}
_getAccountSuggestions(input) {
if (input.length === 0) { return Promise.resolve([]); }
return this.$.restAPI.getSuggestedAccounts(
input, SUGGESTIONS_LIMIT).then(accounts => {
const accountSuggestions = [];
let nameAndEmail;
if (!accounts) { return []; }
for (const key in accounts) {
if (!accounts.hasOwnProperty(key)) { continue; }
if (accounts[key].email !== undefined) {
nameAndEmail = accounts[key].name +
' <' + accounts[key].email + '>';
} else {
nameAndEmail = accounts[key].name;
}
accountSuggestions.push({
name: nameAndEmail,
value: accounts[key]._account_id,
});
}
return accountSuggestions;
_handleSavingGroupMember() {
return this.$.restAPI.saveGroupMembers(this._groupName,
this._groupMemberSearchId).then(config => {
if (!config) {
return;
}
this.$.restAPI.getGroupMembers(this._groupName).then(members => {
this._groupMembers = members;
});
}
this._groupMemberSearchName = '';
this._groupMemberSearchId = '';
});
}
_getGroupSuggestions(input) {
return this.$.restAPI.getSuggestedGroups(input)
.then(response => {
const groups = [];
for (const key in response) {
if (!response.hasOwnProperty(key)) { continue; }
groups.push({
name: key,
value: decodeURIComponent(response[key].id),
});
_handleDeleteConfirm() {
this.$.overlay.close();
if (this._itemType === 'member') {
return this.$.restAPI.deleteGroupMembers(this._groupName,
this._itemId)
.then(itemDeleted => {
if (itemDeleted.status === 204) {
this.$.restAPI.getGroupMembers(this._groupName)
.then(members => {
this._groupMembers = members;
});
}
});
} else if (this._itemType === 'includedGroup') {
return this.$.restAPI.deleteIncludedGroup(this._groupName,
this._itemId)
.then(itemDeleted => {
if (itemDeleted.status === 204 || itemDeleted.status === 205) {
this.$.restAPI.getIncludedGroup(this._groupName)
.then(includedGroup => {
this._includedGroups = includedGroup;
});
}
return groups;
});
}
_computeHideItemClass(owner, admin) {
return admin || owner ? '' : 'canModify';
}
}
customElements.define(GrGroupMembers.is, GrGroupMembers);
})();
_handleConfirmDialogCancel() {
this.$.overlay.close();
}
_handleDeleteMember(e) {
const id = e.model.get('item._account_id');
const name = e.model.get('item.name');
const username = e.model.get('item.username');
const email = e.model.get('item.email');
const item = username || name || email || id;
if (!item) {
return '';
}
this._itemName = item;
this._itemId = id;
this._itemType = 'member';
this.$.overlay.open();
}
_handleSavingIncludedGroups() {
return this.$.restAPI.saveIncludedGroup(this._groupName,
this._includedGroupSearchId.replace(/\+/g, ' '), err => {
if (err.status === 404) {
this.dispatchEvent(new CustomEvent('show-alert', {
detail: {message: SAVING_ERROR_TEXT},
bubbles: true,
composed: true,
}));
return err;
}
throw Error(err.statusText);
})
.then(config => {
if (!config) {
return;
}
this.$.restAPI.getIncludedGroup(this._groupName)
.then(includedGroup => {
this._includedGroups = includedGroup;
});
this._includedGroupSearchName = '';
this._includedGroupSearchId = '';
});
}
_handleDeleteIncludedGroup(e) {
const id = decodeURIComponent(e.model.get('item.id')).replace(/\+/g, ' ');
const name = e.model.get('item.name');
const item = name || id;
if (!item) { return ''; }
this._itemName = item;
this._itemId = id;
this._itemType = 'includedGroup';
this.$.overlay.open();
}
_getAccountSuggestions(input) {
if (input.length === 0) { return Promise.resolve([]); }
return this.$.restAPI.getSuggestedAccounts(
input, SUGGESTIONS_LIMIT).then(accounts => {
const accountSuggestions = [];
let nameAndEmail;
if (!accounts) { return []; }
for (const key in accounts) {
if (!accounts.hasOwnProperty(key)) { continue; }
if (accounts[key].email !== undefined) {
nameAndEmail = accounts[key].name +
' <' + accounts[key].email + '>';
} else {
nameAndEmail = accounts[key].name;
}
accountSuggestions.push({
name: nameAndEmail,
value: accounts[key]._account_id,
});
}
return accountSuggestions;
});
}
_getGroupSuggestions(input) {
return this.$.restAPI.getSuggestedGroups(input)
.then(response => {
const groups = [];
for (const key in response) {
if (!response.hasOwnProperty(key)) { continue; }
groups.push({
name: key,
value: decodeURIComponent(response[key].id),
});
}
return groups;
});
}
_computeHideItemClass(owner, admin) {
return admin || owner ? '' : 'canModify';
}
}
customElements.define(GrGroupMembers.is, GrGroupMembers);

View File

@ -1,38 +1,22 @@
<!--
@license
Copyright (C) 2017 The Android Open Source Project
/**
* @license
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {html} from '@polymer/polymer/lib/utils/html-tag.js';
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
<link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html">
<link rel="import" href="/bower_components/polymer/polymer.html">
<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
<link rel="import" href="../../../styles/gr-form-styles.html">
<link rel="import" href="../../../styles/gr-subpage-styles.html">
<link rel="import" href="../../../styles/gr-table-styles.html">
<link rel="import" href="../../../styles/shared-styles.html">
<link rel="import" href="../../shared/gr-account-link/gr-account-link.html">
<link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html">
<link rel="import" href="../../shared/gr-button/gr-button.html">
<link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
<link rel="import" href="../gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.html">
<dom-module id="gr-group-members">
<template>
export const htmlTemplate = html`
<style include="gr-form-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
@ -72,37 +56,29 @@ limitations under the License.
display: none;
}
</style>
<main class$="gr-form-styles [[_computeHideItemClass(_groupOwner, _isAdmin)]]">
<div id="loading" class$="[[_computeLoadingClass(_loading)]]">
<main class\$="gr-form-styles [[_computeHideItemClass(_groupOwner, _isAdmin)]]">
<div id="loading" class\$="[[_computeLoadingClass(_loading)]]">
Loading...
</div>
<div id="loadedContent" class$="[[_computeLoadingClass(_loading)]]">
<div id="loadedContent" class\$="[[_computeLoadingClass(_loading)]]">
<h1 id="Title">[[_groupName]]</h1>
<div id="form">
<h3 id="members">Members</h3>
<fieldset>
<span class="value">
<gr-autocomplete
id="groupMemberSearchInput"
text="{{_groupMemberSearchName}}"
value="{{_groupMemberSearchId}}"
query="[[_queryMembers]]"
placeholder="Name Or Email">
<gr-autocomplete id="groupMemberSearchInput" text="{{_groupMemberSearchName}}" value="{{_groupMemberSearchId}}" query="[[_queryMembers]]" placeholder="Name Or Email">
</gr-autocomplete>
</span>
<gr-button
id="saveGroupMember"
on-click="_handleSavingGroupMember"
disabled="[[!_groupMemberSearchId]]">
<gr-button id="saveGroupMember" on-click="_handleSavingGroupMember" disabled="[[!_groupMemberSearchId]]">
Add
</gr-button>
<table id="groupMembers">
<tr class="headerRow">
<tbody><tr class="headerRow">
<th class="nameHeader">Name</th>
<th class="emailAddressHeader">Email Address</th>
<th class="deleteHeader">Delete Member</th>
</tr>
<tbody>
</tbody><tbody>
<template is="dom-repeat" items="[[_groupMembers]]">
<tr>
<td class="nameColumn">
@ -110,9 +86,7 @@ limitations under the License.
</td>
<td>[[item.email]]</td>
<td class="deleteColumn">
<gr-button
class="deleteMembersButton"
on-click="_handleDeleteMember">
<gr-button class="deleteMembersButton" on-click="_handleDeleteMember">
Delete
</gr-button>
</td>
@ -124,35 +98,26 @@ limitations under the License.
<h3 id="includedGroups">Included Groups</h3>
<fieldset>
<span class="value">
<gr-autocomplete
id="includedGroupSearchInput"
text="{{_includedGroupSearchName}}"
value="{{_includedGroupSearchId}}"
query="[[_queryIncludedGroup]]"
placeholder="Group Name">
<gr-autocomplete id="includedGroupSearchInput" text="{{_includedGroupSearchName}}" value="{{_includedGroupSearchId}}" query="[[_queryIncludedGroup]]" placeholder="Group Name">
</gr-autocomplete>
</span>
<gr-button
id="saveIncludedGroups"
on-click="_handleSavingIncludedGroups"
disabled="[[!_includedGroupSearchId]]">
<gr-button id="saveIncludedGroups" on-click="_handleSavingIncludedGroups" disabled="[[!_includedGroupSearchId]]">
Add
</gr-button>
<table id="includedGroups">
<tr class="headerRow">
<tbody><tr class="headerRow">
<th class="groupNameHeader">Group Name</th>
<th class="descriptionHeader">Description</th>
<th class="deleteIncludedHeader">
Delete Group
</th>
</tr>
<tbody>
</tbody><tbody>
<template is="dom-repeat" items="[[_includedGroups]]">
<tr>
<td class="nameColumn">
<template is="dom-if" if="[[item.url]]">
<a href$="[[_computeGroupUrl(item.url)]]"
rel="noopener">
<a href\$="[[_computeGroupUrl(item.url)]]" rel="noopener">
[[item.name]]
</a>
</template>
@ -162,9 +127,7 @@ limitations under the License.
</td>
<td>[[item.description]]</td>
<td class="deleteColumn">
<gr-button
class="deleteIncludedGroupButton"
on-click="_handleDeleteIncludedGroup">
<gr-button class="deleteIncludedGroupButton" on-click="_handleDeleteIncludedGroup">
Delete
</gr-button>
</td>
@ -176,15 +139,8 @@ limitations under the License.
</div>
</div>
</main>
<gr-overlay id="overlay" with-backdrop>
<gr-confirm-delete-item-dialog
class="confirmDialog"
on-confirm="_handleDeleteConfirm"
on-cancel="_handleConfirmDialogCancel"
item="[[_itemName]]"
item-type="[[_itemType]]"></gr-confirm-delete-item-dialog>
<gr-overlay id="overlay" with-backdrop="">
<gr-confirm-delete-item-dialog class="confirmDialog" on-confirm="_handleDeleteConfirm" on-cancel="_handleConfirmDialogCancel" item="[[_itemName]]" item-type="[[_itemType]]"></gr-confirm-delete-item-dialog>
</gr-overlay>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
</template>
<script src="gr-group-members.js"></script>
</dom-module>
`;

View File

@ -19,15 +19,20 @@ limitations under the License.
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-group-members</title>
<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/bower_components/web-component-tester/browser.js"></script>
<script src="../../../test/test-pre-setup.js"></script>
<link rel="import" href="../../../test/common-test-setup.html"/>
<link rel="import" href="gr-group-members.html">
<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/components/wct-browser-legacy/browser.js"></script>
<script type="module" src="../../../test/test-pre-setup.js"></script>
<script type="module" src="../../../test/common-test-setup.js"></script>
<script type="module" src="./gr-group-members.js"></script>
<script>void(0);</script>
<script type="module">
import '../../../test/test-pre-setup.js';
import '../../../test/common-test-setup.js';
import './gr-group-members.js';
void(0);
</script>
<test-fixture id="basic">
<template>
@ -35,342 +40,345 @@ limitations under the License.
</template>
</test-fixture>
<script>
suite('gr-group-members tests', async () => {
await readyToTest();
let element;
let sandbox;
let groups;
let groupMembers;
let includedGroups;
let groupStub;
<script type="module">
import '../../../test/test-pre-setup.js';
import '../../../test/common-test-setup.js';
import './gr-group-members.js';
import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
suite('gr-group-members tests', () => {
let element;
let sandbox;
let groups;
let groupMembers;
let includedGroups;
let groupStub;
setup(() => {
sandbox = sinon.sandbox.create();
setup(() => {
sandbox = sinon.sandbox.create();
groups = {
name: 'Administrators',
owner: 'Administrators',
group_id: 1,
};
groups = {
name: 'Administrators',
owner: 'Administrators',
group_id: 1,
};
groupMembers = [
{
_account_id: 1000097,
name: 'Jane Roe',
email: 'jane.roe@example.com',
username: 'jane',
},
{
_account_id: 1000096,
name: 'Test User',
email: 'john.doe@example.com',
},
{
_account_id: 1000095,
name: 'Gerrit',
},
{
_account_id: 1000098,
},
];
includedGroups = [{
url: 'https://group/url',
options: {},
id: 'testId',
name: 'testName',
groupMembers = [
{
_account_id: 1000097,
name: 'Jane Roe',
email: 'jane.roe@example.com',
username: 'jane',
},
{
url: '/group/url',
options: {},
id: 'testId2',
name: 'testName2',
_account_id: 1000096,
name: 'Test User',
email: 'john.doe@example.com',
},
{
url: '#/group/url',
options: {},
id: 'testId3',
name: 'testName3',
_account_id: 1000095,
name: 'Gerrit',
},
];
{
_account_id: 1000098,
},
];
stub('gr-rest-api-interface', {
getSuggestedAccounts(input) {
if (input.startsWith('test')) {
return Promise.resolve([
{
_account_id: 1000096,
name: 'test-account',
email: 'test.account@example.com',
username: 'test123',
},
{
_account_id: 1001439,
name: 'test-admin',
email: 'test.admin@example.com',
username: 'test_admin',
},
{
_account_id: 1001439,
name: 'test-git',
username: 'test_git',
},
]);
} else {
return Promise.resolve({});
}
},
getSuggestedGroups(input) {
if (input.startsWith('test')) {
return Promise.resolve({
'test-admin': {
id: '1ce023d3fb4e4260776fb92cd08b52bbd21ce70a',
},
'test/Administrator (admin)': {
id: 'test%3Aadmin',
},
});
} else {
return Promise.resolve({});
}
},
getLoggedIn() { return Promise.resolve(true); },
getConfig() {
return Promise.resolve();
},
getGroupMembers() {
return Promise.resolve(groupMembers);
},
getIsGroupOwner() {
return Promise.resolve(true);
},
getIncludedGroup() {
return Promise.resolve(includedGroups);
},
getAccountCapabilities() {
return Promise.resolve();
},
});
element = fixture('basic');
sandbox.stub(element, 'getBaseUrl').returns('https://test/site');
element.groupId = 1;
groupStub = sandbox.stub(
element.$.restAPI,
'getGroupConfig',
() => Promise.resolve(groups));
return element._loadGroupDetails();
includedGroups = [{
url: 'https://group/url',
options: {},
id: 'testId',
name: 'testName',
},
{
url: '/group/url',
options: {},
id: 'testId2',
name: 'testName2',
},
{
url: '#/group/url',
options: {},
id: 'testId3',
name: 'testName3',
},
];
stub('gr-rest-api-interface', {
getSuggestedAccounts(input) {
if (input.startsWith('test')) {
return Promise.resolve([
{
_account_id: 1000096,
name: 'test-account',
email: 'test.account@example.com',
username: 'test123',
},
{
_account_id: 1001439,
name: 'test-admin',
email: 'test.admin@example.com',
username: 'test_admin',
},
{
_account_id: 1001439,
name: 'test-git',
username: 'test_git',
},
]);
} else {
return Promise.resolve({});
}
},
getSuggestedGroups(input) {
if (input.startsWith('test')) {
return Promise.resolve({
'test-admin': {
id: '1ce023d3fb4e4260776fb92cd08b52bbd21ce70a',
},
'test/Administrator (admin)': {
id: 'test%3Aadmin',
},
});
} else {
return Promise.resolve({});
}
},
getLoggedIn() { return Promise.resolve(true); },
getConfig() {
return Promise.resolve();
},
getGroupMembers() {
return Promise.resolve(groupMembers);
},
getIsGroupOwner() {
return Promise.resolve(true);
},
getIncludedGroup() {
return Promise.resolve(includedGroups);
},
getAccountCapabilities() {
return Promise.resolve();
},
});
element = fixture('basic');
sandbox.stub(element, 'getBaseUrl').returns('https://test/site');
element.groupId = 1;
groupStub = sandbox.stub(
element.$.restAPI,
'getGroupConfig',
() => Promise.resolve(groups));
return element._loadGroupDetails();
});
teardown(() => {
sandbox.restore();
});
teardown(() => {
sandbox.restore();
});
test('_includedGroups', () => {
assert.equal(element._includedGroups.length, 3);
assert.equal(Polymer.dom(element.root)
.querySelectorAll('.nameColumn a')[0].href, includedGroups[0].url);
assert.equal(Polymer.dom(element.root)
.querySelectorAll('.nameColumn a')[1].href,
'https://test/site/group/url');
assert.equal(Polymer.dom(element.root)
.querySelectorAll('.nameColumn a')[2].href,
'https://test/site/group/url');
});
test('_includedGroups', () => {
assert.equal(element._includedGroups.length, 3);
assert.equal(dom(element.root)
.querySelectorAll('.nameColumn a')[0].href, includedGroups[0].url);
assert.equal(dom(element.root)
.querySelectorAll('.nameColumn a')[1].href,
'https://test/site/group/url');
assert.equal(dom(element.root)
.querySelectorAll('.nameColumn a')[2].href,
'https://test/site/group/url');
});
test('save members correctly', () => {
element._groupOwner = true;
test('save members correctly', () => {
element._groupOwner = true;
const memberName = 'test-admin';
const memberName = 'test-admin';
const saveStub = sandbox.stub(element.$.restAPI, 'saveGroupMembers',
() => Promise.resolve({}));
const saveStub = sandbox.stub(element.$.restAPI, 'saveGroupMembers',
() => Promise.resolve({}));
const button = element.$.saveGroupMember;
const button = element.$.saveGroupMember;
assert.isTrue(button.hasAttribute('disabled'));
element.$.groupMemberSearchInput.text = memberName;
element.$.groupMemberSearchInput.value = 1234;
assert.isFalse(button.hasAttribute('disabled'));
return element._handleSavingGroupMember().then(() => {
assert.isTrue(button.hasAttribute('disabled'));
element.$.groupMemberSearchInput.text = memberName;
element.$.groupMemberSearchInput.value = 1234;
assert.isFalse(button.hasAttribute('disabled'));
return element._handleSavingGroupMember().then(() => {
assert.isTrue(button.hasAttribute('disabled'));
assert.isFalse(element.$.Title.classList.contains('edited'));
assert.isTrue(saveStub.lastCall.calledWithExactly('Administrators',
1234));
});
});
test('save included groups correctly', () => {
element._groupOwner = true;
const includedGroupName = 'testName';
const saveIncludedGroupStub = sandbox.stub(
element.$.restAPI, 'saveIncludedGroup', () => Promise.resolve({}));
const button = element.$.saveIncludedGroups;
assert.isTrue(button.hasAttribute('disabled'));
element.$.includedGroupSearchInput.text = includedGroupName;
element.$.includedGroupSearchInput.value = 'testId';
assert.isFalse(button.hasAttribute('disabled'));
return element._handleSavingIncludedGroups().then(() => {
assert.isTrue(button.hasAttribute('disabled'));
assert.isFalse(element.$.Title.classList.contains('edited'));
assert.equal(saveIncludedGroupStub.lastCall.args[0], 'Administrators');
assert.equal(saveIncludedGroupStub.lastCall.args[1], 'testId');
});
});
test('add included group 404 shows helpful error text', () => {
element._groupOwner = true;
const memberName = 'bad-name';
const alertStub = sandbox.stub();
element.addEventListener('show-alert', alertStub);
const error = new Error('error');
error.status = 404;
sandbox.stub(element.$.restAPI, 'saveGroupMembers',
() => Promise.reject(error));
element.$.groupMemberSearchInput.text = memberName;
element.$.groupMemberSearchInput.value = 1234;
return element._handleSavingIncludedGroups().then(() => {
assert.isTrue(alertStub.called);
});
});
test('_getAccountSuggestions empty', done => {
element
._getAccountSuggestions('nonexistent').then(accounts => {
assert.equal(accounts.length, 0);
done();
});
});
test('_getAccountSuggestions non-empty', done => {
element
._getAccountSuggestions('test-').then(accounts => {
assert.equal(accounts.length, 3);
assert.equal(accounts[0].name,
'test-account <test.account@example.com>');
assert.equal(accounts[1].name, 'test-admin <test.admin@example.com>');
assert.equal(accounts[2].name, 'test-git');
done();
});
});
test('_getGroupSuggestions empty', done => {
element
._getGroupSuggestions('nonexistent').then(groups => {
assert.equal(groups.length, 0);
done();
});
});
test('_getGroupSuggestions non-empty', done => {
element
._getGroupSuggestions('test').then(groups => {
assert.equal(groups.length, 2);
assert.equal(groups[0].name, 'test-admin');
assert.equal(groups[1].name, 'test/Administrator (admin)');
done();
});
});
test('_computeHideItemClass returns string for admin', () => {
const admin = true;
const owner = false;
assert.equal(element._computeHideItemClass(owner, admin), '');
});
test('_computeHideItemClass returns hideItem for admin and owner', () => {
const admin = false;
const owner = false;
assert.equal(element._computeHideItemClass(owner, admin), 'canModify');
});
test('_computeHideItemClass returns string for owner', () => {
const admin = false;
const owner = true;
assert.equal(element._computeHideItemClass(owner, admin), '');
});
test('delete member', () => {
const deletelBtns = Polymer.dom(element.root)
.querySelectorAll('.deleteMembersButton');
MockInteractions.tap(deletelBtns[0]);
assert.equal(element._itemId, '1000097');
assert.equal(element._itemName, 'jane');
MockInteractions.tap(deletelBtns[1]);
assert.equal(element._itemId, '1000096');
assert.equal(element._itemName, 'Test User');
MockInteractions.tap(deletelBtns[2]);
assert.equal(element._itemId, '1000095');
assert.equal(element._itemName, 'Gerrit');
MockInteractions.tap(deletelBtns[3]);
assert.equal(element._itemId, '1000098');
assert.equal(element._itemName, '1000098');
});
test('delete included groups', () => {
const deletelBtns = Polymer.dom(element.root)
.querySelectorAll('.deleteIncludedGroupButton');
MockInteractions.tap(deletelBtns[0]);
assert.equal(element._itemId, 'testId');
assert.equal(element._itemName, 'testName');
MockInteractions.tap(deletelBtns[1]);
assert.equal(element._itemId, 'testId2');
assert.equal(element._itemName, 'testName2');
MockInteractions.tap(deletelBtns[2]);
assert.equal(element._itemId, 'testId3');
assert.equal(element._itemName, 'testName3');
});
test('_computeLoadingClass', () => {
assert.equal(element._computeLoadingClass(true), 'loading');
assert.equal(element._computeLoadingClass(false), '');
});
test('_computeGroupUrl', () => {
assert.isUndefined(element._computeGroupUrl(undefined));
assert.isUndefined(element._computeGroupUrl(false));
let url = '#/admin/groups/uuid-529b3c2605bb1029c8146f9de4a91c776fe64498';
assert.equal(element._computeGroupUrl(url),
'https://test/site/admin/groups/' +
'uuid-529b3c2605bb1029c8146f9de4a91c776fe64498');
url = 'https://gerrit.local/admin/groups/' +
'uuid-529b3c2605bb1029c8146f9de4a91c776fe64498';
assert.equal(element._computeGroupUrl(url), url);
});
test('fires page-error', done => {
groupStub.restore();
element.groupId = 1;
const response = {status: 404};
sandbox.stub(
element.$.restAPI, 'getGroupConfig', (group, errFn) => {
errFn(response);
});
element.addEventListener('page-error', e => {
assert.deepEqual(e.detail.response, response);
done();
});
element._loadGroupDetails();
assert.isFalse(element.$.Title.classList.contains('edited'));
assert.isTrue(saveStub.lastCall.calledWithExactly('Administrators',
1234));
});
});
test('save included groups correctly', () => {
element._groupOwner = true;
const includedGroupName = 'testName';
const saveIncludedGroupStub = sandbox.stub(
element.$.restAPI, 'saveIncludedGroup', () => Promise.resolve({}));
const button = element.$.saveIncludedGroups;
assert.isTrue(button.hasAttribute('disabled'));
element.$.includedGroupSearchInput.text = includedGroupName;
element.$.includedGroupSearchInput.value = 'testId';
assert.isFalse(button.hasAttribute('disabled'));
return element._handleSavingIncludedGroups().then(() => {
assert.isTrue(button.hasAttribute('disabled'));
assert.isFalse(element.$.Title.classList.contains('edited'));
assert.equal(saveIncludedGroupStub.lastCall.args[0], 'Administrators');
assert.equal(saveIncludedGroupStub.lastCall.args[1], 'testId');
});
});
test('add included group 404 shows helpful error text', () => {
element._groupOwner = true;
const memberName = 'bad-name';
const alertStub = sandbox.stub();
element.addEventListener('show-alert', alertStub);
const error = new Error('error');
error.status = 404;
sandbox.stub(element.$.restAPI, 'saveGroupMembers',
() => Promise.reject(error));
element.$.groupMemberSearchInput.text = memberName;
element.$.groupMemberSearchInput.value = 1234;
return element._handleSavingIncludedGroups().then(() => {
assert.isTrue(alertStub.called);
});
});
test('_getAccountSuggestions empty', done => {
element
._getAccountSuggestions('nonexistent').then(accounts => {
assert.equal(accounts.length, 0);
done();
});
});
test('_getAccountSuggestions non-empty', done => {
element
._getAccountSuggestions('test-').then(accounts => {
assert.equal(accounts.length, 3);
assert.equal(accounts[0].name,
'test-account <test.account@example.com>');
assert.equal(accounts[1].name, 'test-admin <test.admin@example.com>');
assert.equal(accounts[2].name, 'test-git');
done();
});
});
test('_getGroupSuggestions empty', done => {
element
._getGroupSuggestions('nonexistent').then(groups => {
assert.equal(groups.length, 0);
done();
});
});
test('_getGroupSuggestions non-empty', done => {
element
._getGroupSuggestions('test').then(groups => {
assert.equal(groups.length, 2);
assert.equal(groups[0].name, 'test-admin');
assert.equal(groups[1].name, 'test/Administrator (admin)');
done();
});
});
test('_computeHideItemClass returns string for admin', () => {
const admin = true;
const owner = false;
assert.equal(element._computeHideItemClass(owner, admin), '');
});
test('_computeHideItemClass returns hideItem for admin and owner', () => {
const admin = false;
const owner = false;
assert.equal(element._computeHideItemClass(owner, admin), 'canModify');
});
test('_computeHideItemClass returns string for owner', () => {
const admin = false;
const owner = true;
assert.equal(element._computeHideItemClass(owner, admin), '');
});
test('delete member', () => {
const deletelBtns = dom(element.root)
.querySelectorAll('.deleteMembersButton');
MockInteractions.tap(deletelBtns[0]);
assert.equal(element._itemId, '1000097');
assert.equal(element._itemName, 'jane');
MockInteractions.tap(deletelBtns[1]);
assert.equal(element._itemId, '1000096');
assert.equal(element._itemName, 'Test User');
MockInteractions.tap(deletelBtns[2]);
assert.equal(element._itemId, '1000095');
assert.equal(element._itemName, 'Gerrit');
MockInteractions.tap(deletelBtns[3]);
assert.equal(element._itemId, '1000098');
assert.equal(element._itemName, '1000098');
});
test('delete included groups', () => {
const deletelBtns = dom(element.root)
.querySelectorAll('.deleteIncludedGroupButton');
MockInteractions.tap(deletelBtns[0]);
assert.equal(element._itemId, 'testId');
assert.equal(element._itemName, 'testName');
MockInteractions.tap(deletelBtns[1]);
assert.equal(element._itemId, 'testId2');
assert.equal(element._itemName, 'testName2');
MockInteractions.tap(deletelBtns[2]);
assert.equal(element._itemId, 'testId3');
assert.equal(element._itemName, 'testName3');
});
test('_computeLoadingClass', () => {
assert.equal(element._computeLoadingClass(true), 'loading');
assert.equal(element._computeLoadingClass(false), '');
});
test('_computeGroupUrl', () => {
assert.isUndefined(element._computeGroupUrl(undefined));
assert.isUndefined(element._computeGroupUrl(false));
let url = '#/admin/groups/uuid-529b3c2605bb1029c8146f9de4a91c776fe64498';
assert.equal(element._computeGroupUrl(url),
'https://test/site/admin/groups/' +
'uuid-529b3c2605bb1029c8146f9de4a91c776fe64498');
url = 'https://gerrit.local/admin/groups/' +
'uuid-529b3c2605bb1029c8146f9de4a91c776fe64498';
assert.equal(element._computeGroupUrl(url), url);
});
test('fires page-error', done => {
groupStub.restore();
element.groupId = 1;
const response = {status: 404};
sandbox.stub(
element.$.restAPI, 'getGroupConfig', (group, errFn) => {
errFn(response);
});
element.addEventListener('page-error', e => {
assert.deepEqual(e.detail.response, response);
done();
});
element._loadGroupDetails();
});
});
</script>

View File

@ -14,238 +14,253 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function() {
'use strict';
import '../../../behaviors/fire-behavior/fire-behavior.js';
const INTERNAL_GROUP_REGEX = /^[\da-f]{40}$/;
import '../../../scripts/bundled-polymer.js';
import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
import '../../../styles/gr-form-styles.js';
import '../../../styles/gr-subpage-styles.js';
import '../../../styles/shared-styles.js';
import '../../shared/gr-autocomplete/gr-autocomplete.js';
import '../../shared/gr-copy-clipboard/gr-copy-clipboard.js';
import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
import '../../shared/gr-select/gr-select.js';
import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
import {PolymerElement} from '@polymer/polymer/polymer-element.js';
import {htmlTemplate} from './gr-group_html.js';
const OPTIONS = {
submitFalse: {
value: false,
label: 'False',
},
submitTrue: {
value: true,
label: 'True',
},
};
const INTERNAL_GROUP_REGEX = /^[\da-f]{40}$/;
const OPTIONS = {
submitFalse: {
value: false,
label: 'False',
},
submitTrue: {
value: true,
label: 'True',
},
};
/**
* @appliesMixin Gerrit.FireMixin
* @extends Polymer.Element
*/
class GrGroup extends mixinBehaviors( [
Gerrit.FireBehavior,
], GestureEventListeners(
LegacyElementMixin(
PolymerElement))) {
static get template() { return htmlTemplate; }
static get is() { return 'gr-group'; }
/**
* @appliesMixin Gerrit.FireMixin
* @extends Polymer.Element
* Fired when the group name changes.
*
* @event name-changed
*/
class GrGroup extends Polymer.mixinBehaviors( [
Gerrit.FireBehavior,
], Polymer.GestureEventListeners(
Polymer.LegacyElementMixin(
Polymer.Element))) {
static get is() { return 'gr-group'; }
/**
* Fired when the group name changes.
*
* @event name-changed
*/
static get properties() {
return {
groupId: Number,
_rename: {
type: Boolean,
value: false,
static get properties() {
return {
groupId: Number,
_rename: {
type: Boolean,
value: false,
},
_groupIsInternal: Boolean,
_description: {
type: Boolean,
value: false,
},
_owner: {
type: Boolean,
value: false,
},
_options: {
type: Boolean,
value: false,
},
_loading: {
type: Boolean,
value: true,
},
/** @type {?} */
_groupConfig: Object,
_groupConfigOwner: String,
_groupName: Object,
_groupOwner: {
type: Boolean,
value: false,
},
_submitTypes: {
type: Array,
value() {
return Object.values(OPTIONS);
},
_groupIsInternal: Boolean,
_description: {
type: Boolean,
value: false,
},
_query: {
type: Function,
value() {
return this._getGroupSuggestions.bind(this);
},
_owner: {
type: Boolean,
value: false,
},
_options: {
type: Boolean,
value: false,
},
_loading: {
type: Boolean,
value: true,
},
/** @type {?} */
_groupConfig: Object,
_groupConfigOwner: String,
_groupName: Object,
_groupOwner: {
type: Boolean,
value: false,
},
_submitTypes: {
type: Array,
value() {
return Object.values(OPTIONS);
},
},
_query: {
type: Function,
value() {
return this._getGroupSuggestions.bind(this);
},
},
_isAdmin: {
type: Boolean,
value: false,
},
};
}
static get observers() {
return [
'_handleConfigName(_groupConfig.name)',
'_handleConfigOwner(_groupConfig.owner, _groupConfigOwner)',
'_handleConfigDescription(_groupConfig.description)',
'_handleConfigOptions(_groupConfig.options.visible_to_all)',
];
}
/** @override */
attached() {
super.attached();
this._loadGroup();
}
_loadGroup() {
if (!this.groupId) { return; }
const promises = [];
const errFn = response => {
this.fire('page-error', {response});
};
return this.$.restAPI.getGroupConfig(this.groupId, errFn)
.then(config => {
if (!config || !config.name) { return Promise.resolve(); }
this._groupName = config.name;
this._groupIsInternal = !!config.id.match(INTERNAL_GROUP_REGEX);
promises.push(this.$.restAPI.getIsAdmin().then(isAdmin => {
this._isAdmin = isAdmin ? true : false;
}));
promises.push(this.$.restAPI.getIsGroupOwner(config.name)
.then(isOwner => {
this._groupOwner = isOwner ? true : false;
}));
// If visible to all is undefined, set to false. If it is defined
// as false, setting to false is fine. If any optional values
// are added with a default of true, then this would need to be an
// undefined check and not a truthy/falsy check.
if (!config.options.visible_to_all) {
config.options.visible_to_all = false;
}
this._groupConfig = config;
this.fire('title-change', {title: config.name});
return Promise.all(promises).then(() => {
this._loading = false;
});
});
}
_computeLoadingClass(loading) {
return loading ? 'loading' : '';
}
_isLoading() {
return this._loading || this._loading === undefined;
}
_handleSaveName() {
return this.$.restAPI.saveGroupName(this.groupId, this._groupConfig.name)
.then(config => {
if (config.status === 200) {
this._groupName = this._groupConfig.name;
this.fire('name-changed', {name: this._groupConfig.name,
external: this._groupIsExtenral});
this._rename = false;
}
});
}
_handleSaveOwner() {
let owner = this._groupConfig.owner;
if (this._groupConfigOwner) {
owner = decodeURIComponent(this._groupConfigOwner);
}
return this.$.restAPI.saveGroupOwner(this.groupId,
owner).then(config => {
this._owner = false;
});
}
_handleSaveDescription() {
return this.$.restAPI.saveGroupDescription(this.groupId,
this._groupConfig.description).then(config => {
this._description = false;
});
}
_handleSaveOptions() {
const visible = this._groupConfig.options.visible_to_all;
const options = {visible_to_all: visible};
return this.$.restAPI.saveGroupOptions(this.groupId,
options).then(config => {
this._options = false;
});
}
_handleConfigName() {
if (this._isLoading()) { return; }
this._rename = true;
}
_handleConfigOwner() {
if (this._isLoading()) { return; }
this._owner = true;
}
_handleConfigDescription() {
if (this._isLoading()) { return; }
this._description = true;
}
_handleConfigOptions() {
if (this._isLoading()) { return; }
this._options = true;
}
_computeHeaderClass(configChanged) {
return configChanged ? 'edited' : '';
}
_getGroupSuggestions(input) {
return this.$.restAPI.getSuggestedGroups(input)
.then(response => {
const groups = [];
for (const key in response) {
if (!response.hasOwnProperty(key)) { continue; }
groups.push({
name: key,
value: decodeURIComponent(response[key].id),
});
}
return groups;
});
}
_computeGroupDisabled(owner, admin, groupIsInternal) {
return groupIsInternal && (admin || owner) ? false : true;
}
},
_isAdmin: {
type: Boolean,
value: false,
},
};
}
customElements.define(GrGroup.is, GrGroup);
})();
static get observers() {
return [
'_handleConfigName(_groupConfig.name)',
'_handleConfigOwner(_groupConfig.owner, _groupConfigOwner)',
'_handleConfigDescription(_groupConfig.description)',
'_handleConfigOptions(_groupConfig.options.visible_to_all)',
];
}
/** @override */
attached() {
super.attached();
this._loadGroup();
}
_loadGroup() {
if (!this.groupId) { return; }
const promises = [];
const errFn = response => {
this.fire('page-error', {response});
};
return this.$.restAPI.getGroupConfig(this.groupId, errFn)
.then(config => {
if (!config || !config.name) { return Promise.resolve(); }
this._groupName = config.name;
this._groupIsInternal = !!config.id.match(INTERNAL_GROUP_REGEX);
promises.push(this.$.restAPI.getIsAdmin().then(isAdmin => {
this._isAdmin = isAdmin ? true : false;
}));
promises.push(this.$.restAPI.getIsGroupOwner(config.name)
.then(isOwner => {
this._groupOwner = isOwner ? true : false;
}));
// If visible to all is undefined, set to false. If it is defined
// as false, setting to false is fine. If any optional values
// are added with a default of true, then this would need to be an
// undefined check and not a truthy/falsy check.
if (!config.options.visible_to_all) {
config.options.visible_to_all = false;
}
this._groupConfig = config;
this.fire('title-change', {title: config.name});
return Promise.all(promises).then(() => {
this._loading = false;
});
});
}
_computeLoadingClass(loading) {
return loading ? 'loading' : '';
}
_isLoading() {
return this._loading || this._loading === undefined;
}
_handleSaveName() {
return this.$.restAPI.saveGroupName(this.groupId, this._groupConfig.name)
.then(config => {
if (config.status === 200) {
this._groupName = this._groupConfig.name;
this.fire('name-changed', {name: this._groupConfig.name,
external: this._groupIsExtenral});
this._rename = false;
}
});
}
_handleSaveOwner() {
let owner = this._groupConfig.owner;
if (this._groupConfigOwner) {
owner = decodeURIComponent(this._groupConfigOwner);
}
return this.$.restAPI.saveGroupOwner(this.groupId,
owner).then(config => {
this._owner = false;
});
}
_handleSaveDescription() {
return this.$.restAPI.saveGroupDescription(this.groupId,
this._groupConfig.description).then(config => {
this._description = false;
});
}
_handleSaveOptions() {
const visible = this._groupConfig.options.visible_to_all;
const options = {visible_to_all: visible};
return this.$.restAPI.saveGroupOptions(this.groupId,
options).then(config => {
this._options = false;
});
}
_handleConfigName() {
if (this._isLoading()) { return; }
this._rename = true;
}
_handleConfigOwner() {
if (this._isLoading()) { return; }
this._owner = true;
}
_handleConfigDescription() {
if (this._isLoading()) { return; }
this._description = true;
}
_handleConfigOptions() {
if (this._isLoading()) { return; }
this._options = true;
}
_computeHeaderClass(configChanged) {
return configChanged ? 'edited' : '';
}
_getGroupSuggestions(input) {
return this.$.restAPI.getSuggestedGroups(input)
.then(response => {
const groups = [];
for (const key in response) {
if (!response.hasOwnProperty(key)) { continue; }
groups.push({
name: key,
value: decodeURIComponent(response[key].id),
});
}
return groups;
});
}
_computeGroupDisabled(owner, admin, groupIsInternal) {
return groupIsInternal && (admin || owner) ? false : true;
}
}
customElements.define(GrGroup.is, GrGroup);

View File

@ -1,33 +1,22 @@
<!--
@license
Copyright (C) 2017 The Android Open Source Project
/**
* @license
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {html} from '@polymer/polymer/lib/utils/html-tag.js';
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
<link rel="import" href="/bower_components/polymer/polymer.html">
<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
<link rel="import" href="../../../styles/gr-form-styles.html">
<link rel="import" href="../../../styles/gr-subpage-styles.html">
<link rel="import" href="../../../styles/shared-styles.html">
<link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html">
<link rel="import" href="../../shared/gr-copy-clipboard/gr-copy-clipboard.html">
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
<link rel="import" href="../../shared/gr-select/gr-select.html">
<dom-module id="gr-group">
<template>
export const htmlTemplate = html`
<style include="shared-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
@ -44,77 +33,57 @@ limitations under the License.
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
<main class="gr-form-styles read-only">
<div id="loading" class$="[[_computeLoadingClass(_loading)]]">
<div id="loading" class\$="[[_computeLoadingClass(_loading)]]">
Loading...
</div>
<div id="loadedContent" class$="[[_computeLoadingClass(_loading)]]">
<div id="loadedContent" class\$="[[_computeLoadingClass(_loading)]]">
<h1 id="Title">[[_groupName]]</h1>
<h2 id="configurations">General</h2>
<div id="form">
<fieldset>
<h3 id="groupUUID">Group UUID</h3>
<fieldset>
<gr-copy-clipboard
text="[[groupId]]"></gr-copy-clipboard>
<gr-copy-clipboard text="[[groupId]]"></gr-copy-clipboard>
</fieldset>
<h3 id="groupName" class$="[[_computeHeaderClass(_rename)]]">
<h3 id="groupName" class\$="[[_computeHeaderClass(_rename)]]">
Group Name
</h3>
<fieldset>
<span class="value">
<gr-autocomplete
id="groupNameInput"
text="{{_groupConfig.name}}"
disabled="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]"></gr-autocomplete>
<gr-autocomplete id="groupNameInput" text="{{_groupConfig.name}}" disabled="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]"></gr-autocomplete>
</span>
<span class="value" disabled$="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]">
<gr-button
id="inputUpdateNameBtn"
on-click="_handleSaveName"
disabled="[[!_rename]]">
<span class="value" disabled\$="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]">
<gr-button id="inputUpdateNameBtn" on-click="_handleSaveName" disabled="[[!_rename]]">
Rename Group</gr-button>
</span>
</fieldset>
<h3 class$="[[_computeHeaderClass(_owner)]]">
<h3 class\$="[[_computeHeaderClass(_owner)]]">
Owners
</h3>
<fieldset>
<span class="value">
<gr-autocomplete
id="groupOwnerInput"
text="{{_groupConfig.owner}}"
value="{{_groupConfigOwner}}"
query="[[_query]]"
disabled="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]">
<gr-autocomplete id="groupOwnerInput" text="{{_groupConfig.owner}}" value="{{_groupConfigOwner}}" query="[[_query]]" disabled="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]">
</gr-autocomplete>
</span>
<span class="value" disabled$="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]">
<gr-button
on-click="_handleSaveOwner"
disabled="[[!_owner]]">
<span class="value" disabled\$="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]">
<gr-button on-click="_handleSaveOwner" disabled="[[!_owner]]">
Change Owners</gr-button>
</span>
</fieldset>
<h3 class$="[[_computeHeaderClass(_description)]]">
<h3 class\$="[[_computeHeaderClass(_description)]]">
Description
</h3>
<fieldset>
<div>
<iron-autogrow-textarea
class="description"
autocomplete="on"
bind-value="{{_groupConfig.description}}"
disabled="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]"></iron-autogrow-textarea>
<iron-autogrow-textarea class="description" autocomplete="on" bind-value="{{_groupConfig.description}}" disabled="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]"></iron-autogrow-textarea>
</div>
<span class="value" disabled$="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]">
<gr-button
on-click="_handleSaveDescription"
disabled="[[!_description]]">
<span class="value" disabled\$="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]">
<gr-button on-click="_handleSaveDescription" disabled="[[!_description]]">
Save Description
</gr-button>
</span>
</fieldset>
<h3 id="options" class$="[[_computeHeaderClass(_options)]]">
<h3 id="options" class\$="[[_computeHeaderClass(_options)]]">
Group Options
</h3>
<fieldset id="visableToAll">
@ -123,10 +92,8 @@ limitations under the License.
Make group visible to all registered users
</span>
<span class="value">
<gr-select
id="visibleToAll"
bind-value="{{_groupConfig.options.visible_to_all}}">
<select disabled$="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]">
<gr-select id="visibleToAll" bind-value="{{_groupConfig.options.visible_to_all}}">
<select disabled\$="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]">
<template is="dom-repeat" items="[[_submitTypes]]">
<option value="[[item.value]]">[[item.label]]</option>
</template>
@ -134,10 +101,8 @@ limitations under the License.
</gr-select>
</span>
</section>
<span class="value" disabled$="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]">
<gr-button
on-click="_handleSaveOptions"
disabled="[[!_options]]">
<span class="value" disabled\$="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]">
<gr-button on-click="_handleSaveOptions" disabled="[[!_options]]">
Save Group Options
</gr-button>
</span>
@ -147,6 +112,4 @@ limitations under the License.
</div>
</main>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
</template>
<script src="gr-group.js"></script>
</dom-module>
`;

View File

@ -19,15 +19,20 @@ limitations under the License.
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-group</title>
<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/bower_components/web-component-tester/browser.js"></script>
<script src="../../../test/test-pre-setup.js"></script>
<link rel="import" href="../../../test/common-test-setup.html"/>
<link rel="import" href="gr-group.html">
<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/components/wct-browser-legacy/browser.js"></script>
<script type="module" src="../../../test/test-pre-setup.js"></script>
<script type="module" src="../../../test/common-test-setup.js"></script>
<script type="module" src="./gr-group.js"></script>
<script>void(0);</script>
<script type="module">
import '../../../test/test-pre-setup.js';
import '../../../test/common-test-setup.js';
import './gr-group.js';
void(0);
</script>
<test-fixture id="basic">
<template>
@ -35,222 +40,224 @@ limitations under the License.
</template>
</test-fixture>
<script>
suite('gr-group tests', async () => {
await readyToTest();
let element;
let sandbox;
let groupStub;
const group = {
id: '6a1e70e1a88782771a91808c8af9bbb7a9871389',
url: '#/admin/groups/uuid-6a1e70e1a88782771a91808c8af9bbb7a9871389',
options: {},
description: 'Gerrit Site Administrators',
group_id: 1,
owner: 'Administrators',
owner_id: '6a1e70e1a88782771a91808c8af9bbb7a9871389',
name: 'Administrators',
};
<script type="module">
import '../../../test/test-pre-setup.js';
import '../../../test/common-test-setup.js';
import './gr-group.js';
suite('gr-group tests', () => {
let element;
let sandbox;
let groupStub;
const group = {
id: '6a1e70e1a88782771a91808c8af9bbb7a9871389',
url: '#/admin/groups/uuid-6a1e70e1a88782771a91808c8af9bbb7a9871389',
options: {},
description: 'Gerrit Site Administrators',
group_id: 1,
owner: 'Administrators',
owner_id: '6a1e70e1a88782771a91808c8af9bbb7a9871389',
name: 'Administrators',
};
setup(() => {
sandbox = sinon.sandbox.create();
stub('gr-rest-api-interface', {
getLoggedIn() { return Promise.resolve(true); },
});
element = fixture('basic');
groupStub = sandbox.stub(
element.$.restAPI,
'getGroupConfig',
() => Promise.resolve(group)
);
setup(() => {
sandbox = sinon.sandbox.create();
stub('gr-rest-api-interface', {
getLoggedIn() { return Promise.resolve(true); },
});
element = fixture('basic');
groupStub = sandbox.stub(
element.$.restAPI,
'getGroupConfig',
() => Promise.resolve(group)
);
});
teardown(() => {
sandbox.restore();
});
teardown(() => {
sandbox.restore();
});
test('loading displays before group config is loaded', () => {
assert.isTrue(element.$.loading.classList.contains('loading'));
assert.isFalse(getComputedStyle(element.$.loading).display === 'none');
assert.isTrue(element.$.loadedContent.classList.contains('loading'));
assert.isTrue(getComputedStyle(element.$.loadedContent)
.display === 'none');
});
test('loading displays before group config is loaded', () => {
assert.isTrue(element.$.loading.classList.contains('loading'));
assert.isFalse(getComputedStyle(element.$.loading).display === 'none');
assert.isTrue(element.$.loadedContent.classList.contains('loading'));
assert.isTrue(getComputedStyle(element.$.loadedContent)
.display === 'none');
});
test('default values are populated with internal group', done => {
sandbox.stub(
element.$.restAPI,
'getIsGroupOwner',
() => Promise.resolve(true));
element.groupId = 1;
element._loadGroup().then(() => {
assert.isTrue(element._groupIsInternal);
assert.isFalse(element.$.visibleToAll.bindValue);
done();
});
});
test('default values with external group', done => {
const groupExternal = Object.assign({}, group);
groupExternal.id = 'external-group-id';
groupStub.restore();
groupStub = sandbox.stub(
element.$.restAPI,
'getGroupConfig',
() => Promise.resolve(groupExternal));
sandbox.stub(
element.$.restAPI,
'getIsGroupOwner',
() => Promise.resolve(true));
element.groupId = 1;
element._loadGroup().then(() => {
assert.isFalse(element._groupIsInternal);
assert.isFalse(element.$.visibleToAll.bindValue);
done();
});
});
test('rename group', done => {
const groupName = 'test-group';
const groupName2 = 'test-group2';
element.groupId = 1;
element._groupConfig = {
name: groupName,
};
element._groupConfigOwner = 'testId';
element._groupName = groupName;
element._groupOwner = true;
sandbox.stub(
element.$.restAPI,
'getIsGroupOwner',
() => Promise.resolve(true));
sandbox.stub(
element.$.restAPI,
'saveGroupName',
() => Promise.resolve({status: 200}));
const button = element.$.inputUpdateNameBtn;
element._loadGroup().then(() => {
assert.isTrue(button.hasAttribute('disabled'));
assert.isFalse(element.$.Title.classList.contains('edited'));
element.$.groupNameInput.text = groupName2;
element.$.groupOwnerInput.text = 'testId2';
assert.isFalse(button.hasAttribute('disabled'));
assert.isTrue(element.$.groupName.classList.contains('edited'));
element._handleSaveName().then(() => {
assert.isTrue(button.hasAttribute('disabled'));
assert.isFalse(element.$.Title.classList.contains('edited'));
assert.equal(element._groupName, groupName2);
done();
});
element._handleSaveOwner().then(() => {
assert.isTrue(button.hasAttribute('disabled'));
assert.isFalse(element.$.Title.classList.contains('edited'));
assert.equal(element._groupConfigOwner, 'testId2');
done();
});
});
});
test('test for undefined group name', done => {
groupStub.restore();
sandbox.stub(
element.$.restAPI,
'getGroupConfig',
() => Promise.resolve({}));
assert.isUndefined(element.groupId);
element.groupId = 1;
assert.isDefined(element.groupId);
// Test that loading shows instead of filling
// in group details
element._loadGroup().then(() => {
assert.isTrue(element.$.loading.classList.contains('loading'));
assert.isTrue(element._loading);
done();
});
});
test('test fire event', done => {
element._groupConfig = {
name: 'test-group',
};
sandbox.stub(element.$.restAPI, 'saveGroupName')
.returns(Promise.resolve({status: 200}));
const showStub = sandbox.stub(element, 'fire');
element._handleSaveName()
.then(() => {
assert.isTrue(showStub.called);
done();
});
});
test('_computeGroupDisabled', () => {
let admin = true;
let owner = false;
let groupIsInternal = true;
assert.equal(element._computeGroupDisabled(owner, admin,
groupIsInternal), false);
admin = false;
assert.equal(element._computeGroupDisabled(owner, admin,
groupIsInternal), true);
owner = true;
assert.equal(element._computeGroupDisabled(owner, admin,
groupIsInternal), false);
owner = false;
assert.equal(element._computeGroupDisabled(owner, admin,
groupIsInternal), true);
groupIsInternal = false;
assert.equal(element._computeGroupDisabled(owner, admin,
groupIsInternal), true);
admin = true;
assert.equal(element._computeGroupDisabled(owner, admin,
groupIsInternal), true);
});
test('_computeLoadingClass', () => {
assert.equal(element._computeLoadingClass(true), 'loading');
assert.equal(element._computeLoadingClass(false), '');
});
test('fires page-error', done => {
groupStub.restore();
element.groupId = 1;
const response = {status: 404};
sandbox.stub(
element.$.restAPI, 'getGroupConfig', (group, errFn) => {
errFn(response);
});
element.addEventListener('page-error', e => {
assert.deepEqual(e.detail.response, response);
done();
});
element._loadGroup();
test('default values are populated with internal group', done => {
sandbox.stub(
element.$.restAPI,
'getIsGroupOwner',
() => Promise.resolve(true));
element.groupId = 1;
element._loadGroup().then(() => {
assert.isTrue(element._groupIsInternal);
assert.isFalse(element.$.visibleToAll.bindValue);
done();
});
});
test('default values with external group', done => {
const groupExternal = Object.assign({}, group);
groupExternal.id = 'external-group-id';
groupStub.restore();
groupStub = sandbox.stub(
element.$.restAPI,
'getGroupConfig',
() => Promise.resolve(groupExternal));
sandbox.stub(
element.$.restAPI,
'getIsGroupOwner',
() => Promise.resolve(true));
element.groupId = 1;
element._loadGroup().then(() => {
assert.isFalse(element._groupIsInternal);
assert.isFalse(element.$.visibleToAll.bindValue);
done();
});
});
test('rename group', done => {
const groupName = 'test-group';
const groupName2 = 'test-group2';
element.groupId = 1;
element._groupConfig = {
name: groupName,
};
element._groupConfigOwner = 'testId';
element._groupName = groupName;
element._groupOwner = true;
sandbox.stub(
element.$.restAPI,
'getIsGroupOwner',
() => Promise.resolve(true));
sandbox.stub(
element.$.restAPI,
'saveGroupName',
() => Promise.resolve({status: 200}));
const button = element.$.inputUpdateNameBtn;
element._loadGroup().then(() => {
assert.isTrue(button.hasAttribute('disabled'));
assert.isFalse(element.$.Title.classList.contains('edited'));
element.$.groupNameInput.text = groupName2;
element.$.groupOwnerInput.text = 'testId2';
assert.isFalse(button.hasAttribute('disabled'));
assert.isTrue(element.$.groupName.classList.contains('edited'));
element._handleSaveName().then(() => {
assert.isTrue(button.hasAttribute('disabled'));
assert.isFalse(element.$.Title.classList.contains('edited'));
assert.equal(element._groupName, groupName2);
done();
});
element._handleSaveOwner().then(() => {
assert.isTrue(button.hasAttribute('disabled'));
assert.isFalse(element.$.Title.classList.contains('edited'));
assert.equal(element._groupConfigOwner, 'testId2');
done();
});
});
});
test('test for undefined group name', done => {
groupStub.restore();
sandbox.stub(
element.$.restAPI,
'getGroupConfig',
() => Promise.resolve({}));
assert.isUndefined(element.groupId);
element.groupId = 1;
assert.isDefined(element.groupId);
// Test that loading shows instead of filling
// in group details
element._loadGroup().then(() => {
assert.isTrue(element.$.loading.classList.contains('loading'));
assert.isTrue(element._loading);
done();
});
});
test('test fire event', done => {
element._groupConfig = {
name: 'test-group',
};
sandbox.stub(element.$.restAPI, 'saveGroupName')
.returns(Promise.resolve({status: 200}));
const showStub = sandbox.stub(element, 'fire');
element._handleSaveName()
.then(() => {
assert.isTrue(showStub.called);
done();
});
});
test('_computeGroupDisabled', () => {
let admin = true;
let owner = false;
let groupIsInternal = true;
assert.equal(element._computeGroupDisabled(owner, admin,
groupIsInternal), false);
admin = false;
assert.equal(element._computeGroupDisabled(owner, admin,
groupIsInternal), true);
owner = true;
assert.equal(element._computeGroupDisabled(owner, admin,
groupIsInternal), false);
owner = false;
assert.equal(element._computeGroupDisabled(owner, admin,
groupIsInternal), true);
groupIsInternal = false;
assert.equal(element._computeGroupDisabled(owner, admin,
groupIsInternal), true);
admin = true;
assert.equal(element._computeGroupDisabled(owner, admin,
groupIsInternal), true);
});
test('_computeLoadingClass', () => {
assert.equal(element._computeLoadingClass(true), 'loading');
assert.equal(element._computeLoadingClass(false), '');
});
test('fires page-error', done => {
groupStub.restore();
element.groupId = 1;
const response = {status: 404};
sandbox.stub(
element.$.restAPI, 'getGroupConfig', (group, errFn) => {
errFn(response);
});
element.addEventListener('page-error', e => {
assert.deepEqual(e.detail.response, response);
done();
});
element._loadGroup();
});
});
</script>

View File

@ -14,301 +14,318 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function() {
'use strict';
import '../../../scripts/bundled-polymer.js';
const MAX_AUTOCOMPLETE_RESULTS = 20;
import '../../../behaviors/fire-behavior/fire-behavior.js';
import '../../../behaviors/gr-access-behavior/gr-access-behavior.js';
import '@polymer/paper-toggle-button/paper-toggle-button.js';
import '../../../styles/gr-form-styles.js';
import '../../../styles/gr-menu-page-styles.js';
import '../../../styles/shared-styles.js';
import '../../shared/gr-autocomplete/gr-autocomplete.js';
import '../../shared/gr-button/gr-button.js';
import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
import '../gr-rule-editor/gr-rule-editor.js';
import {flush} from '@polymer/polymer/lib/legacy/polymer.dom.js';
import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
import {PolymerElement} from '@polymer/polymer/polymer-element.js';
import {htmlTemplate} from './gr-permission_html.js';
const RANGE_NAMES = [
'QUERY LIMIT',
'BATCH CHANGES LIMIT',
];
const MAX_AUTOCOMPLETE_RESULTS = 20;
const RANGE_NAMES = [
'QUERY LIMIT',
'BATCH CHANGES LIMIT',
];
/**
* @appliesMixin Gerrit.AccessMixin
* @appliesMixin Gerrit.FireMixin
*/
/**
* Fired when the permission has been modified or removed.
*
* @event access-modified
*/
/**
* Fired when a permission that was previously added was removed.
*
* @event added-permission-removed
* @extends Polymer.Element
*/
class GrPermission extends mixinBehaviors( [
Gerrit.AccessBehavior,
/**
* @appliesMixin Gerrit.AccessMixin
* @appliesMixin Gerrit.FireMixin
* Unused in this element, but called by other elements in tests
* e.g gr-access-section_test.
*/
/**
* Fired when the permission has been modified or removed.
*
* @event access-modified
*/
/**
* Fired when a permission that was previously added was removed.
*
* @event added-permission-removed
* @extends Polymer.Element
*/
class GrPermission extends Polymer.mixinBehaviors( [
Gerrit.AccessBehavior,
/**
* Unused in this element, but called by other elements in tests
* e.g gr-access-section_test.
*/
Gerrit.FireBehavior,
], Polymer.GestureEventListeners(
Polymer.LegacyElementMixin(
Polymer.Element))) {
static get is() { return 'gr-permission'; }
Gerrit.FireBehavior,
], GestureEventListeners(
LegacyElementMixin(
PolymerElement))) {
static get template() { return htmlTemplate; }
static get properties() {
return {
labels: Object,
name: String,
/** @type {?} */
permission: {
type: Object,
observer: '_sortPermission',
notify: true,
static get is() { return 'gr-permission'; }
static get properties() {
return {
labels: Object,
name: String,
/** @type {?} */
permission: {
type: Object,
observer: '_sortPermission',
notify: true,
},
groups: Object,
section: String,
editing: {
type: Boolean,
value: false,
observer: '_handleEditingChanged',
},
_label: {
type: Object,
computed: '_computeLabel(permission, labels)',
},
_groupFilter: String,
_query: {
type: Function,
value() {
return this._getGroupSuggestions.bind(this);
},
groups: Object,
section: String,
editing: {
type: Boolean,
value: false,
observer: '_handleEditingChanged',
},
_label: {
type: Object,
computed: '_computeLabel(permission, labels)',
},
_groupFilter: String,
_query: {
type: Function,
value() {
return this._getGroupSuggestions.bind(this);
},
},
_rules: Array,
_groupsWithRules: Object,
_deleted: {
type: Boolean,
value: false,
},
_originalExclusiveValue: Boolean,
};
}
},
_rules: Array,
_groupsWithRules: Object,
_deleted: {
type: Boolean,
value: false,
},
_originalExclusiveValue: Boolean,
};
}
static get observers() {
return [
'_handleRulesChanged(_rules.splices)',
];
}
static get observers() {
return [
'_handleRulesChanged(_rules.splices)',
];
}
/** @override */
created() {
super.created();
this.addEventListener('access-saved',
() => this._handleAccessSaved());
}
/** @override */
created() {
super.created();
this.addEventListener('access-saved',
() => this._handleAccessSaved());
}
/** @override */
ready() {
super.ready();
this._setupValues();
}
/** @override */
ready() {
super.ready();
this._setupValues();
}
_setupValues() {
if (!this.permission) { return; }
this._originalExclusiveValue = !!this.permission.value.exclusive;
Polymer.dom.flush();
}
_setupValues() {
if (!this.permission) { return; }
this._originalExclusiveValue = !!this.permission.value.exclusive;
flush();
}
_handleAccessSaved() {
// Set a new 'original' value to keep track of after the value has been
// saved.
this._setupValues();
}
_handleAccessSaved() {
// Set a new 'original' value to keep track of after the value has been
// saved.
this._setupValues();
}
_permissionIsOwnerOrGlobal(permissionId, section) {
return permissionId === 'owner' || section === 'GLOBAL_CAPABILITIES';
}
_permissionIsOwnerOrGlobal(permissionId, section) {
return permissionId === 'owner' || section === 'GLOBAL_CAPABILITIES';
}
_handleEditingChanged(editing, editingOld) {
// Ignore when editing gets set initially.
if (!editingOld) { return; }
// Restore original values if no longer editing.
if (!editing) {
this._deleted = false;
delete this.permission.value.deleted;
this._groupFilter = '';
this._rules = this._rules.filter(rule => !rule.value.added);
for (const key of Object.keys(this.permission.value.rules)) {
if (this.permission.value.rules[key].added) {
delete this.permission.value.rules[key];
}
}
// Restore exclusive bit to original.
this.set(['permission', 'value', 'exclusive'],
this._originalExclusiveValue);
}
}
_handleAddedRuleRemoved(e) {
const index = e.model.index;
this._rules = this._rules.slice(0, index)
.concat(this._rules.slice(index + 1, this._rules.length));
}
_handleValueChange() {
this.permission.value.modified = true;
// Allows overall access page to know a change has been made.
this.dispatchEvent(
new CustomEvent('access-modified', {bubbles: true, composed: true}));
}
_handleRemovePermission() {
if (this.permission.value.added) {
this.dispatchEvent(new CustomEvent(
'added-permission-removed', {bubbles: true, composed: true}));
}
this._deleted = true;
this.permission.value.deleted = true;
this.dispatchEvent(
new CustomEvent('access-modified', {bubbles: true, composed: true}));
}
_handleRulesChanged(changeRecord) {
// Update the groups to exclude in the autocomplete.
this._groupsWithRules = this._computeGroupsWithRules(this._rules);
}
_sortPermission(permission) {
this._rules = this.toSortedArray(permission.value.rules);
}
_computeSectionClass(editing, deleted) {
const classList = [];
if (editing) {
classList.push('editing');
}
if (deleted) {
classList.push('deleted');
}
return classList.join(' ');
}
_handleUndoRemove() {
_handleEditingChanged(editing, editingOld) {
// Ignore when editing gets set initially.
if (!editingOld) { return; }
// Restore original values if no longer editing.
if (!editing) {
this._deleted = false;
delete this.permission.value.deleted;
}
_computeLabel(permission, labels) {
if (!labels || !permission ||
!permission.value || !permission.value.label) { return; }
const labelName = permission.value.label;
// It is possible to have a label name that is not included in the
// 'labels' object. In this case, treat it like anything else.
if (!labels[labelName]) { return; }
const label = {
name: labelName,
values: this._computeLabelValues(labels[labelName].values),
};
return label;
}
_computeLabelValues(values) {
const valuesArr = [];
const keys = Object.keys(values)
.sort((a, b) => parseInt(a, 10) - parseInt(b, 10));
for (const key of keys) {
let text = values[key];
if (!text) { text = ''; }
// The value from the server being used to choose which item is
// selected is in integer form, so this must be converted.
valuesArr.push({value: parseInt(key, 10), text});
}
return valuesArr;
}
/**
* @param {!Array} rules
* @return {!Object} Object with groups with rues as keys, and true as
* value.
*/
_computeGroupsWithRules(rules) {
const groups = {};
for (const rule of rules) {
groups[rule.id] = true;
}
return groups;
}
_computeGroupName(groups, groupId) {
return groups && groups[groupId] && groups[groupId].name ?
groups[groupId].name : groupId;
}
_getGroupSuggestions() {
return this.$.restAPI.getSuggestedGroups(
this._groupFilter,
MAX_AUTOCOMPLETE_RESULTS)
.then(response => {
const groups = [];
for (const key in response) {
if (!response.hasOwnProperty(key)) { continue; }
groups.push({
name: key,
value: response[key],
});
}
// Does not return groups in which we already have rules for.
return groups
.filter(group => !this._groupsWithRules[group.value.id]);
});
}
/**
* Handles adding a skeleton item to the dom-repeat.
* gr-rule-editor handles setting the default values.
*/
_handleAddRuleItem(e) {
// The group id is encoded, but have to decode in order for the access
// API to work as expected.
const groupId = decodeURIComponent(e.detail.value.id)
.replace(/\+/g, ' ');
// We cannot use "this.set(...)" here, because groupId may contain dots,
// and dots in property path names are totally unsupported by Polymer.
// Apparently Polymer picks up this change anyway, otherwise we should
// have looked at using MutableData:
// https://polymer-library.polymer-project.org/2.0/docs/devguide/data-system#mutable-data
this.permission.value.rules[groupId] = {};
// Purposely don't recompute sorted array so that the newly added rule
// is the last item of the array.
this.push('_rules', {
id: groupId,
});
// Add the new group name to the groups object so the name renders
// correctly.
if (this.groups && !this.groups[groupId]) {
this.groups[groupId] = {name: this.$.groupAutocomplete.text};
this._groupFilter = '';
this._rules = this._rules.filter(rule => !rule.value.added);
for (const key of Object.keys(this.permission.value.rules)) {
if (this.permission.value.rules[key].added) {
delete this.permission.value.rules[key];
}
}
// Wait for new rule to get value populated via gr-rule-editor, and then
// add to permission values as well, so that the change gets propogated
// back to the section. Since the rule is inside a dom-repeat, a flush
// is needed.
Polymer.dom.flush();
const value = this._rules[this._rules.length - 1].value;
value.added = true;
// See comment above for why we cannot use "this.set(...)" here.
this.permission.value.rules[groupId] = value;
this.dispatchEvent(
new CustomEvent('access-modified', {bubbles: true, composed: true}));
}
_computeHasRange(name) {
if (!name) { return false; }
return RANGE_NAMES.includes(name.toUpperCase());
// Restore exclusive bit to original.
this.set(['permission', 'value', 'exclusive'],
this._originalExclusiveValue);
}
}
customElements.define(GrPermission.is, GrPermission);
})();
_handleAddedRuleRemoved(e) {
const index = e.model.index;
this._rules = this._rules.slice(0, index)
.concat(this._rules.slice(index + 1, this._rules.length));
}
_handleValueChange() {
this.permission.value.modified = true;
// Allows overall access page to know a change has been made.
this.dispatchEvent(
new CustomEvent('access-modified', {bubbles: true, composed: true}));
}
_handleRemovePermission() {
if (this.permission.value.added) {
this.dispatchEvent(new CustomEvent(
'added-permission-removed', {bubbles: true, composed: true}));
}
this._deleted = true;
this.permission.value.deleted = true;
this.dispatchEvent(
new CustomEvent('access-modified', {bubbles: true, composed: true}));
}
_handleRulesChanged(changeRecord) {
// Update the groups to exclude in the autocomplete.
this._groupsWithRules = this._computeGroupsWithRules(this._rules);
}
_sortPermission(permission) {
this._rules = this.toSortedArray(permission.value.rules);
}
_computeSectionClass(editing, deleted) {
const classList = [];
if (editing) {
classList.push('editing');
}
if (deleted) {
classList.push('deleted');
}
return classList.join(' ');
}
_handleUndoRemove() {
this._deleted = false;
delete this.permission.value.deleted;
}
_computeLabel(permission, labels) {
if (!labels || !permission ||
!permission.value || !permission.value.label) { return; }
const labelName = permission.value.label;
// It is possible to have a label name that is not included in the
// 'labels' object. In this case, treat it like anything else.
if (!labels[labelName]) { return; }
const label = {
name: labelName,
values: this._computeLabelValues(labels[labelName].values),
};
return label;
}
_computeLabelValues(values) {
const valuesArr = [];
const keys = Object.keys(values)
.sort((a, b) => parseInt(a, 10) - parseInt(b, 10));
for (const key of keys) {
let text = values[key];
if (!text) { text = ''; }
// The value from the server being used to choose which item is
// selected is in integer form, so this must be converted.
valuesArr.push({value: parseInt(key, 10), text});
}
return valuesArr;
}
/**
* @param {!Array} rules
* @return {!Object} Object with groups with rues as keys, and true as
* value.
*/
_computeGroupsWithRules(rules) {
const groups = {};
for (const rule of rules) {
groups[rule.id] = true;
}
return groups;
}
_computeGroupName(groups, groupId) {
return groups && groups[groupId] && groups[groupId].name ?
groups[groupId].name : groupId;
}
_getGroupSuggestions() {
return this.$.restAPI.getSuggestedGroups(
this._groupFilter,
MAX_AUTOCOMPLETE_RESULTS)
.then(response => {
const groups = [];
for (const key in response) {
if (!response.hasOwnProperty(key)) { continue; }
groups.push({
name: key,
value: response[key],
});
}
// Does not return groups in which we already have rules for.
return groups
.filter(group => !this._groupsWithRules[group.value.id]);
});
}
/**
* Handles adding a skeleton item to the dom-repeat.
* gr-rule-editor handles setting the default values.
*/
_handleAddRuleItem(e) {
// The group id is encoded, but have to decode in order for the access
// API to work as expected.
const groupId = decodeURIComponent(e.detail.value.id)
.replace(/\+/g, ' ');
// We cannot use "this.set(...)" here, because groupId may contain dots,
// and dots in property path names are totally unsupported by Polymer.
// Apparently Polymer picks up this change anyway, otherwise we should
// have looked at using MutableData:
// https://polymer-library.polymer-project.org/2.0/docs/devguide/data-system#mutable-data
this.permission.value.rules[groupId] = {};
// Purposely don't recompute sorted array so that the newly added rule
// is the last item of the array.
this.push('_rules', {
id: groupId,
});
// Add the new group name to the groups object so the name renders
// correctly.
if (this.groups && !this.groups[groupId]) {
this.groups[groupId] = {name: this.$.groupAutocomplete.text};
}
// Wait for new rule to get value populated via gr-rule-editor, and then
// add to permission values as well, so that the change gets propogated
// back to the section. Since the rule is inside a dom-repeat, a flush
// is needed.
flush();
const value = this._rules[this._rules.length - 1].value;
value.added = true;
// See comment above for why we cannot use "this.set(...)" here.
this.permission.value.rules[groupId] = value;
this.dispatchEvent(
new CustomEvent('access-modified', {bubbles: true, composed: true}));
}
_computeHasRange(name) {
if (!name) { return false; }
return RANGE_NAMES.includes(name.toUpperCase());
}
}
customElements.define(GrPermission.is, GrPermission);

View File

@ -1,34 +1,22 @@
<!--
@license
Copyright (C) 2017 The Android Open Source Project
/**
* @license
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {html} from '@polymer/polymer/lib/utils/html-tag.js';
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<link rel="import" href="/bower_components/polymer/polymer.html">
<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
<link rel="import" href="../../../behaviors/gr-access-behavior/gr-access-behavior.html">
<link rel="import" href="/bower_components/paper-toggle-button/paper-toggle-button.html">
<link rel="import" href="../../../styles/gr-form-styles.html">
<link rel="import" href="../../../styles/gr-menu-page-styles.html">
<link rel="import" href="../../../styles/shared-styles.html">
<link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html">
<link rel="import" href="../../shared/gr-button/gr-button.html">
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
<link rel="import" href="../gr-rule-editor/gr-rule-editor.html">
<dom-module id="gr-permission">
<template>
export const htmlTemplate = html`
<style include="shared-styles">
:host {
display: block;
@ -88,49 +76,23 @@ limitations under the License.
<style include="gr-menu-page-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
<section
id="permission"
class$="gr-form-styles [[_computeSectionClass(editing, _deleted)]]">
<section id="permission" class\$="gr-form-styles [[_computeSectionClass(editing, _deleted)]]">
<div id="mainContainer">
<div class="header">
<span class="title">[[name]]</span>
<div class="right">
<template is=dom-if if="[[!_permissionIsOwnerOrGlobal(permission.id, section)]]">
<paper-toggle-button
id="exclusiveToggle"
checked="{{permission.value.exclusive}}"
on-change="_handleValueChange"
disabled$="[[!editing]]"></paper-toggle-button>Exclusive
<template is="dom-if" if="[[!_permissionIsOwnerOrGlobal(permission.id, section)]]">
<paper-toggle-button id="exclusiveToggle" checked="{{permission.value.exclusive}}" on-change="_handleValueChange" disabled\$="[[!editing]]"></paper-toggle-button>Exclusive
</template>
<gr-button
link
id="removeBtn"
on-click="_handleRemovePermission">Remove</gr-button>
<gr-button link="" id="removeBtn" on-click="_handleRemovePermission">Remove</gr-button>
</div>
</div><!-- end header -->
<div class="rules">
<template
is="dom-repeat"
items="{{_rules}}"
as="rule">
<gr-rule-editor
has-range="[[_computeHasRange(name)]]"
label="[[_label]]"
editing="[[editing]]"
group-id="[[rule.id]]"
group-name="[[_computeGroupName(groups, rule.id)]]"
permission="[[permission.id]]"
rule="{{rule}}"
section="[[section]]"
on-added-rule-removed="_handleAddedRuleRemoved"></gr-rule-editor>
<template is="dom-repeat" items="{{_rules}}" as="rule">
<gr-rule-editor has-range="[[_computeHasRange(name)]]" label="[[_label]]" editing="[[editing]]" group-id="[[rule.id]]" group-name="[[_computeGroupName(groups, rule.id)]]" permission="[[permission.id]]" rule="{{rule}}" section="[[section]]" on-added-rule-removed="_handleAddedRuleRemoved"></gr-rule-editor>
</template>
<div id="addRule">
<gr-autocomplete
id="groupAutocomplete"
text="{{_groupFilter}}"
query="[[_query]]"
placeholder="Add group"
on-commit="_handleAddRuleItem">
<gr-autocomplete id="groupAutocomplete" text="{{_groupFilter}}" query="[[_query]]" placeholder="Add group" on-commit="_handleAddRuleItem">
</gr-autocomplete>
</div>
<!-- end addRule -->
@ -138,13 +100,8 @@ limitations under the License.
</div><!-- end mainContainer -->
<div id="deletedContainer">
<span>[[name]] was deleted</span>
<gr-button
link
id="undoRemoveBtn"
on-click="_handleUndoRemove">Undo</gr-button>
<gr-button link="" id="undoRemoveBtn" on-click="_handleUndoRemove">Undo</gr-button>
</div><!-- end deletedContainer -->
</section>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
</template>
<script src="gr-permission.js"></script>
</dom-module>
`;

View File

@ -19,16 +19,21 @@ limitations under the License.
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-permission</title>
<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/bower_components/page/page.js"></script>
<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/bower_components/web-component-tester/browser.js"></script>
<script src="../../../test/test-pre-setup.js"></script>
<link rel="import" href="../../../test/common-test-setup.html"/>
<link rel="import" href="gr-permission.html">
<script src="/node_modules/page/page.js"></script>
<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/components/wct-browser-legacy/browser.js"></script>
<script type="module" src="../../../test/test-pre-setup.js"></script>
<script type="module" src="../../../test/common-test-setup.js"></script>
<script type="module" src="./gr-permission.js"></script>
<script>void(0);</script>
<script type="module">
import '../../../test/test-pre-setup.js';
import '../../../test/common-test-setup.js';
import './gr-permission.js';
void(0);
</script>
<test-fixture id="basic">
<template>
@ -36,399 +41,401 @@ limitations under the License.
</template>
</test-fixture>
<script>
suite('gr-permission tests', async () => {
await readyToTest();
let element;
let sandbox;
<script type="module">
import '../../../test/test-pre-setup.js';
import '../../../test/common-test-setup.js';
import './gr-permission.js';
suite('gr-permission tests', () => {
let element;
let sandbox;
setup(() => {
sandbox = sinon.sandbox.create();
element = fixture('basic');
sandbox.stub(element.$.restAPI, 'getSuggestedGroups').returns(
Promise.resolve({
'Administrators': {
id: '4c97682e6ce61b7247f3381b6f1789356666de7f',
setup(() => {
sandbox = sinon.sandbox.create();
element = fixture('basic');
sandbox.stub(element.$.restAPI, 'getSuggestedGroups').returns(
Promise.resolve({
'Administrators': {
id: '4c97682e6ce61b7247f3381b6f1789356666de7f',
},
'Anonymous Users': {
id: 'global%3AAnonymous-Users',
},
}));
});
teardown(() => {
sandbox.restore();
});
suite('unit tests', () => {
test('_sortPermission', () => {
const permission = {
id: 'submit',
value: {
rules: {
'global:Project-Owners': {
action: 'ALLOW',
force: false,
},
'Anonymous Users': {
id: 'global%3AAnonymous-Users',
'4c97682e6ce6b7247f3381b6f1789356666de7f': {
action: 'ALLOW',
force: false,
},
}));
},
},
};
const expectedRules = [
{
id: '4c97682e6ce6b7247f3381b6f1789356666de7f',
value: {action: 'ALLOW', force: false},
},
{
id: 'global:Project-Owners',
value: {action: 'ALLOW', force: false},
},
];
element._sortPermission(permission);
assert.deepEqual(element._rules, expectedRules);
});
teardown(() => {
sandbox.restore();
test('_computeLabel and _computeLabelValues', () => {
const labels = {
'Code-Review': {
default_value: 0,
values: {
' 0': 'No score',
'-1': 'I would prefer this is not merged as is',
'-2': 'This shall not be merged',
'+1': 'Looks good to me, but someone else must approve',
'+2': 'Looks good to me, approved',
},
},
};
let permission = {
id: 'label-Code-Review',
value: {
label: 'Code-Review',
rules: {
'global:Project-Owners': {
action: 'ALLOW',
force: false,
min: -2,
max: 2,
},
'4c97682e6ce6b7247f3381b6f1789356666de7f': {
action: 'ALLOW',
force: false,
min: -2,
max: 2,
},
},
},
};
const expectedLabelValues = [
{value: -2, text: 'This shall not be merged'},
{value: -1, text: 'I would prefer this is not merged as is'},
{value: 0, text: 'No score'},
{value: 1, text: 'Looks good to me, but someone else must approve'},
{value: 2, text: 'Looks good to me, approved'},
];
const expectedLabel = {
name: 'Code-Review',
values: expectedLabelValues,
};
assert.deepEqual(element._computeLabelValues(
labels['Code-Review'].values), expectedLabelValues);
assert.deepEqual(element._computeLabel(permission, labels),
expectedLabel);
permission = {
id: 'label-reviewDB',
value: {
label: 'reviewDB',
rules: {
'global:Project-Owners': {
action: 'ALLOW',
force: false,
},
'4c97682e6ce6b7247f3381b6f1789356666de7f': {
action: 'ALLOW',
force: false,
},
},
},
};
assert.isNotOk(element._computeLabel(permission, labels));
});
suite('unit tests', () => {
test('_sortPermission', () => {
const permission = {
id: 'submit',
value: {
rules: {
'global:Project-Owners': {
action: 'ALLOW',
force: false,
},
'4c97682e6ce6b7247f3381b6f1789356666de7f': {
action: 'ALLOW',
force: false,
},
},
},
};
test('_computeSectionClass', () => {
let deleted = true;
let editing = false;
assert.equal(element._computeSectionClass(editing, deleted), 'deleted');
const expectedRules = [
deleted = false;
assert.equal(element._computeSectionClass(editing, deleted), '');
editing = true;
assert.equal(element._computeSectionClass(editing, deleted), 'editing');
deleted = true;
assert.equal(element._computeSectionClass(editing, deleted),
'editing deleted');
});
test('_computeGroupName', () => {
const groups = {
abc123: {name: 'test group'},
bcd234: {},
};
assert.equal(element._computeGroupName(groups, 'abc123'), 'test group');
assert.equal(element._computeGroupName(groups, 'bcd234'), 'bcd234');
});
test('_computeGroupsWithRules', () => {
const rules = [
{
id: '4c97682e6ce6b7247f3381b6f1789356666de7f',
value: {action: 'ALLOW', force: false},
},
{
id: 'global:Project-Owners',
value: {action: 'ALLOW', force: false},
},
];
const groupsWithRules = {
'4c97682e6ce6b7247f3381b6f1789356666de7f': true,
'global:Project-Owners': true,
};
assert.deepEqual(element._computeGroupsWithRules(rules),
groupsWithRules);
});
test('_getGroupSuggestions without existing rules', done => {
element._groupsWithRules = {};
element._getGroupSuggestions().then(groups => {
assert.deepEqual(groups, [
{
id: '4c97682e6ce6b7247f3381b6f1789356666de7f',
value: {action: 'ALLOW', force: false},
},
{
id: 'global:Project-Owners',
value: {action: 'ALLOW', force: false},
},
];
element._sortPermission(permission);
assert.deepEqual(element._rules, expectedRules);
});
test('_computeLabel and _computeLabelValues', () => {
const labels = {
'Code-Review': {
default_value: 0,
values: {
' 0': 'No score',
'-1': 'I would prefer this is not merged as is',
'-2': 'This shall not be merged',
'+1': 'Looks good to me, but someone else must approve',
'+2': 'Looks good to me, approved',
},
},
};
let permission = {
id: 'label-Code-Review',
value: {
label: 'Code-Review',
rules: {
'global:Project-Owners': {
action: 'ALLOW',
force: false,
min: -2,
max: 2,
},
'4c97682e6ce6b7247f3381b6f1789356666de7f': {
action: 'ALLOW',
force: false,
min: -2,
max: 2,
},
},
},
};
const expectedLabelValues = [
{value: -2, text: 'This shall not be merged'},
{value: -1, text: 'I would prefer this is not merged as is'},
{value: 0, text: 'No score'},
{value: 1, text: 'Looks good to me, but someone else must approve'},
{value: 2, text: 'Looks good to me, approved'},
];
const expectedLabel = {
name: 'Code-Review',
values: expectedLabelValues,
};
assert.deepEqual(element._computeLabelValues(
labels['Code-Review'].values), expectedLabelValues);
assert.deepEqual(element._computeLabel(permission, labels),
expectedLabel);
permission = {
id: 'label-reviewDB',
value: {
label: 'reviewDB',
rules: {
'global:Project-Owners': {
action: 'ALLOW',
force: false,
},
'4c97682e6ce6b7247f3381b6f1789356666de7f': {
action: 'ALLOW',
force: false,
},
},
},
};
assert.isNotOk(element._computeLabel(permission, labels));
});
test('_computeSectionClass', () => {
let deleted = true;
let editing = false;
assert.equal(element._computeSectionClass(editing, deleted), 'deleted');
deleted = false;
assert.equal(element._computeSectionClass(editing, deleted), '');
editing = true;
assert.equal(element._computeSectionClass(editing, deleted), 'editing');
deleted = true;
assert.equal(element._computeSectionClass(editing, deleted),
'editing deleted');
});
test('_computeGroupName', () => {
const groups = {
abc123: {name: 'test group'},
bcd234: {},
};
assert.equal(element._computeGroupName(groups, 'abc123'), 'test group');
assert.equal(element._computeGroupName(groups, 'bcd234'), 'bcd234');
});
test('_computeGroupsWithRules', () => {
const rules = [
{
id: '4c97682e6ce6b7247f3381b6f1789356666de7f',
value: {action: 'ALLOW', force: false},
},
{
id: 'global:Project-Owners',
value: {action: 'ALLOW', force: false},
},
];
const groupsWithRules = {
'4c97682e6ce6b7247f3381b6f1789356666de7f': true,
'global:Project-Owners': true,
};
assert.deepEqual(element._computeGroupsWithRules(rules),
groupsWithRules);
});
test('_getGroupSuggestions without existing rules', done => {
element._groupsWithRules = {};
element._getGroupSuggestions().then(groups => {
assert.deepEqual(groups, [
{
name: 'Administrators',
value: {id: '4c97682e6ce61b7247f3381b6f1789356666de7f'},
}, {
name: 'Anonymous Users',
value: {id: 'global%3AAnonymous-Users'},
},
]);
done();
});
});
test('_getGroupSuggestions with existing rules filters them', done => {
element._groupsWithRules = {
'4c97682e6ce61b7247f3381b6f1789356666de7f': true,
};
element._getGroupSuggestions().then(groups => {
assert.deepEqual(groups, [{
name: 'Administrators',
value: {id: '4c97682e6ce61b7247f3381b6f1789356666de7f'},
}, {
name: 'Anonymous Users',
value: {id: 'global%3AAnonymous-Users'},
}]);
done();
});
});
test('_handleRemovePermission', () => {
element.editing = true;
element.permission = {value: {rules: {}}};
element._handleRemovePermission();
assert.isTrue(element._deleted);
assert.isTrue(element.permission.value.deleted);
element.editing = false;
assert.isFalse(element._deleted);
assert.isNotOk(element.permission.value.deleted);
});
test('_handleUndoRemove', () => {
element.permission = {value: {deleted: true, rules: {}}};
element._handleUndoRemove();
assert.isFalse(element._deleted);
assert.isNotOk(element.permission.value.deleted);
});
test('_computeHasRange', () => {
assert.isTrue(element._computeHasRange('Query Limit'));
assert.isTrue(element._computeHasRange('Batch Changes Limit'));
assert.isFalse(element._computeHasRange('test'));
},
]);
done();
});
});
suite('interactions', () => {
setup(() => {
sandbox.spy(element, '_computeLabel');
element.name = 'Priority';
element.section = 'refs/*';
element.labels = {
'Code-Review': {
values: {
' 0': 'No score',
'-1': 'I would prefer this is not merged as is',
'-2': 'This shall not be merged',
'+1': 'Looks good to me, but someone else must approve',
'+2': 'Looks good to me, approved',
},
default_value: 0,
},
};
element.permission = {
id: 'label-Code-Review',
value: {
label: 'Code-Review',
rules: {
'global:Project-Owners': {
action: 'ALLOW',
force: false,
min: -2,
max: 2,
},
'4c97682e6ce6b7247f3381b6f1789356666de7f': {
action: 'ALLOW',
force: false,
min: -2,
max: 2,
},
},
},
};
element._setupValues();
flushAsynchronousOperations();
test('_getGroupSuggestions with existing rules filters them', done => {
element._groupsWithRules = {
'4c97682e6ce61b7247f3381b6f1789356666de7f': true,
};
element._getGroupSuggestions().then(groups => {
assert.deepEqual(groups, [{
name: 'Anonymous Users',
value: {id: 'global%3AAnonymous-Users'},
}]);
done();
});
});
test('adding a rule', () => {
element.name = 'Priority';
element.section = 'refs/*';
element.groups = {};
element.$.groupAutocomplete.text = 'ldap/tests te.st';
const e = {
detail: {
value: {
id: 'ldap:CN=test+te.st',
},
},
};
element.editing = true;
assert.equal(element._rules.length, 2);
assert.equal(Object.keys(element._groupsWithRules).length, 2);
element._handleAddRuleItem(e);
flushAsynchronousOperations();
assert.deepEqual(element.groups, {'ldap:CN=test te.st': {
name: 'ldap/tests te.st'}});
assert.equal(element._rules.length, 3);
assert.equal(Object.keys(element._groupsWithRules).length, 3);
assert.deepEqual(element.permission.value.rules['ldap:CN=test te.st'],
{action: 'ALLOW', min: -2, max: 2, added: true});
// New rule should be removed if cancel from editing.
element.editing = false;
assert.equal(element._rules.length, 2);
assert.equal(Object.keys(element.permission.value.rules).length, 2);
});
test('_handleRemovePermission', () => {
element.editing = true;
element.permission = {value: {rules: {}}};
element._handleRemovePermission();
assert.isTrue(element._deleted);
assert.isTrue(element.permission.value.deleted);
test('removing an added rule', () => {
element.name = 'Priority';
element.section = 'refs/*';
element.groups = {};
element.$.groupAutocomplete.text = 'new group name';
assert.equal(element._rules.length, 2);
element.shadowRoot
.querySelector('gr-rule-editor').fire('added-rule-removed');
flushAsynchronousOperations();
assert.equal(element._rules.length, 1);
});
element.editing = false;
assert.isFalse(element._deleted);
assert.isNotOk(element.permission.value.deleted);
});
test('removing an added permission', () => {
const removeStub = sandbox.stub();
element.addEventListener('added-permission-removed', removeStub);
element.editing = true;
element.name = 'Priority';
element.section = 'refs/*';
element.permission.value.added = true;
MockInteractions.tap(element.$.removeBtn);
assert.isTrue(removeStub.called);
});
test('_handleUndoRemove', () => {
element.permission = {value: {deleted: true, rules: {}}};
element._handleUndoRemove();
assert.isFalse(element._deleted);
assert.isNotOk(element.permission.value.deleted);
});
test('removing the permission', () => {
element.editing = true;
element.name = 'Priority';
element.section = 'refs/*';
test('_computeHasRange', () => {
assert.isTrue(element._computeHasRange('Query Limit'));
const removeStub = sandbox.stub();
element.addEventListener('added-permission-removed', removeStub);
assert.isTrue(element._computeHasRange('Batch Changes Limit'));
assert.isFalse(element.$.permission.classList.contains('deleted'));
assert.isFalse(element._deleted);
MockInteractions.tap(element.$.removeBtn);
assert.isTrue(element.$.permission.classList.contains('deleted'));
assert.isTrue(element._deleted);
MockInteractions.tap(element.$.undoRemoveBtn);
assert.isFalse(element.$.permission.classList.contains('deleted'));
assert.isFalse(element._deleted);
assert.isFalse(removeStub.called);
});
test('modify a permission', () => {
element.editing = true;
element.name = 'Priority';
element.section = 'refs/*';
assert.isFalse(element._originalExclusiveValue);
assert.isNotOk(element.permission.value.modified);
MockInteractions.tap(element.shadowRoot
.querySelector('#exclusiveToggle'));
flushAsynchronousOperations();
assert.isTrue(element.permission.value.exclusive);
assert.isTrue(element.permission.value.modified);
assert.isFalse(element._originalExclusiveValue);
element.editing = false;
assert.isFalse(element.permission.value.exclusive);
});
test('_handleValueChange', () => {
const modifiedHandler = sandbox.stub();
element.permission = {value: {rules: {}}};
element.addEventListener('access-modified', modifiedHandler);
assert.isNotOk(element.permission.value.modified);
element._handleValueChange();
assert.isTrue(element.permission.value.modified);
assert.isTrue(modifiedHandler.called);
});
test('Exclusive hidden for owner permission', () => {
assert.equal(getComputedStyle(element.shadowRoot
.querySelector('#exclusiveToggle')).display,
'flex');
element.set(['permission', 'id'], 'owner');
flushAsynchronousOperations();
assert.equal(getComputedStyle(element.shadowRoot
.querySelector('#exclusiveToggle')).display,
'none');
});
test('Exclusive hidden for any global permissions', () => {
assert.equal(getComputedStyle(element.shadowRoot
.querySelector('#exclusiveToggle')).display,
'flex');
element.section = 'GLOBAL_CAPABILITIES';
flushAsynchronousOperations();
assert.equal(getComputedStyle(element.shadowRoot
.querySelector('#exclusiveToggle')).display,
'none');
});
assert.isFalse(element._computeHasRange('test'));
});
});
suite('interactions', () => {
setup(() => {
sandbox.spy(element, '_computeLabel');
element.name = 'Priority';
element.section = 'refs/*';
element.labels = {
'Code-Review': {
values: {
' 0': 'No score',
'-1': 'I would prefer this is not merged as is',
'-2': 'This shall not be merged',
'+1': 'Looks good to me, but someone else must approve',
'+2': 'Looks good to me, approved',
},
default_value: 0,
},
};
element.permission = {
id: 'label-Code-Review',
value: {
label: 'Code-Review',
rules: {
'global:Project-Owners': {
action: 'ALLOW',
force: false,
min: -2,
max: 2,
},
'4c97682e6ce6b7247f3381b6f1789356666de7f': {
action: 'ALLOW',
force: false,
min: -2,
max: 2,
},
},
},
};
element._setupValues();
flushAsynchronousOperations();
});
test('adding a rule', () => {
element.name = 'Priority';
element.section = 'refs/*';
element.groups = {};
element.$.groupAutocomplete.text = 'ldap/tests te.st';
const e = {
detail: {
value: {
id: 'ldap:CN=test+te.st',
},
},
};
element.editing = true;
assert.equal(element._rules.length, 2);
assert.equal(Object.keys(element._groupsWithRules).length, 2);
element._handleAddRuleItem(e);
flushAsynchronousOperations();
assert.deepEqual(element.groups, {'ldap:CN=test te.st': {
name: 'ldap/tests te.st'}});
assert.equal(element._rules.length, 3);
assert.equal(Object.keys(element._groupsWithRules).length, 3);
assert.deepEqual(element.permission.value.rules['ldap:CN=test te.st'],
{action: 'ALLOW', min: -2, max: 2, added: true});
// New rule should be removed if cancel from editing.
element.editing = false;
assert.equal(element._rules.length, 2);
assert.equal(Object.keys(element.permission.value.rules).length, 2);
});
test('removing an added rule', () => {
element.name = 'Priority';
element.section = 'refs/*';
element.groups = {};
element.$.groupAutocomplete.text = 'new group name';
assert.equal(element._rules.length, 2);
element.shadowRoot
.querySelector('gr-rule-editor').fire('added-rule-removed');
flushAsynchronousOperations();
assert.equal(element._rules.length, 1);
});
test('removing an added permission', () => {
const removeStub = sandbox.stub();
element.addEventListener('added-permission-removed', removeStub);
element.editing = true;
element.name = 'Priority';
element.section = 'refs/*';
element.permission.value.added = true;
MockInteractions.tap(element.$.removeBtn);
assert.isTrue(removeStub.called);
});
test('removing the permission', () => {
element.editing = true;
element.name = 'Priority';
element.section = 'refs/*';
const removeStub = sandbox.stub();
element.addEventListener('added-permission-removed', removeStub);
assert.isFalse(element.$.permission.classList.contains('deleted'));
assert.isFalse(element._deleted);
MockInteractions.tap(element.$.removeBtn);
assert.isTrue(element.$.permission.classList.contains('deleted'));
assert.isTrue(element._deleted);
MockInteractions.tap(element.$.undoRemoveBtn);
assert.isFalse(element.$.permission.classList.contains('deleted'));
assert.isFalse(element._deleted);
assert.isFalse(removeStub.called);
});
test('modify a permission', () => {
element.editing = true;
element.name = 'Priority';
element.section = 'refs/*';
assert.isFalse(element._originalExclusiveValue);
assert.isNotOk(element.permission.value.modified);
MockInteractions.tap(element.shadowRoot
.querySelector('#exclusiveToggle'));
flushAsynchronousOperations();
assert.isTrue(element.permission.value.exclusive);
assert.isTrue(element.permission.value.modified);
assert.isFalse(element._originalExclusiveValue);
element.editing = false;
assert.isFalse(element.permission.value.exclusive);
});
test('_handleValueChange', () => {
const modifiedHandler = sandbox.stub();
element.permission = {value: {rules: {}}};
element.addEventListener('access-modified', modifiedHandler);
assert.isNotOk(element.permission.value.modified);
element._handleValueChange();
assert.isTrue(element.permission.value.modified);
assert.isTrue(modifiedHandler.called);
});
test('Exclusive hidden for owner permission', () => {
assert.equal(getComputedStyle(element.shadowRoot
.querySelector('#exclusiveToggle')).display,
'flex');
element.set(['permission', 'id'], 'owner');
flushAsynchronousOperations();
assert.equal(getComputedStyle(element.shadowRoot
.querySelector('#exclusiveToggle')).display,
'none');
});
test('Exclusive hidden for any global permissions', () => {
assert.equal(getComputedStyle(element.shadowRoot
.querySelector('#exclusiveToggle')).display,
'flex');
element.section = 'GLOBAL_CAPABILITIES';
flushAsynchronousOperations();
assert.equal(getComputedStyle(element.shadowRoot
.querySelector('#exclusiveToggle')).display,
'none');
});
});
});
</script>

View File

@ -14,84 +14,95 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function() {
'use strict';
import '../../../scripts/bundled-polymer.js';
/** @extends Polymer.Element */
class GrPluginConfigArrayEditor extends Polymer.GestureEventListeners(
Polymer.LegacyElementMixin(
Polymer.Element)) {
static get is() { return 'gr-plugin-config-array-editor'; }
/**
* Fired when the plugin config option changes.
*
* @event plugin-config-option-changed
*/
import '@polymer/iron-input/iron-input.js';
import '@polymer/paper-toggle-button/paper-toggle-button.js';
import '../../../styles/gr-form-styles.js';
import '../../../styles/shared-styles.js';
import '../../shared/gr-button/gr-button.js';
import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
import {PolymerElement} from '@polymer/polymer/polymer-element.js';
import {htmlTemplate} from './gr-plugin-config-array-editor_html.js';
static get properties() {
return {
/** @extends Polymer.Element */
class GrPluginConfigArrayEditor extends GestureEventListeners(
LegacyElementMixin(
PolymerElement)) {
static get template() { return htmlTemplate; }
static get is() { return 'gr-plugin-config-array-editor'; }
/**
* Fired when the plugin config option changes.
*
* @event plugin-config-option-changed
*/
static get properties() {
return {
/** @type {?} */
pluginOption: Object,
/** @type {boolean} */
disabled: {
type: Boolean,
computed: '_computeDisabled(pluginOption.*)',
},
/** @type {?} */
pluginOption: Object,
/** @type {boolean} */
disabled: {
type: Boolean,
computed: '_computeDisabled(pluginOption.*)',
},
/** @type {?} */
_newValue: {
type: String,
value: '',
},
};
}
_newValue: {
type: String,
value: '',
},
};
}
_computeDisabled(record) {
return !(record && record.base && record.base.info &&
record.base.info.editable);
}
_computeDisabled(record) {
return !(record && record.base && record.base.info &&
record.base.info.editable);
}
_handleAddTap(e) {
_handleAddTap(e) {
e.preventDefault();
this._handleAdd();
}
_handleInputKeydown(e) {
// Enter.
if (e.keyCode === 13) {
e.preventDefault();
this._handleAdd();
}
_handleInputKeydown(e) {
// Enter.
if (e.keyCode === 13) {
e.preventDefault();
this._handleAdd();
}
}
_handleAdd() {
if (!this._newValue.length) { return; }
this._dispatchChanged(
this.pluginOption.info.values.concat([this._newValue]));
this._newValue = '';
}
_handleDelete(e) {
const value = Polymer.dom(e).localTarget.dataset.item;
this._dispatchChanged(
this.pluginOption.info.values.filter(str => str !== value));
}
_dispatchChanged(values) {
const {_key, info} = this.pluginOption;
const detail = {
_key,
info: Object.assign(info, {values}, {}),
notifyPath: `${_key}.values`,
};
this.dispatchEvent(
new CustomEvent('plugin-config-option-changed', {detail}));
}
_computeShowInputRow(disabled) {
return disabled ? 'hide' : '';
}
}
customElements.define(GrPluginConfigArrayEditor.is,
GrPluginConfigArrayEditor);
})();
_handleAdd() {
if (!this._newValue.length) { return; }
this._dispatchChanged(
this.pluginOption.info.values.concat([this._newValue]));
this._newValue = '';
}
_handleDelete(e) {
const value = dom(e).localTarget.dataset.item;
this._dispatchChanged(
this.pluginOption.info.values.filter(str => str !== value));
}
_dispatchChanged(values) {
const {_key, info} = this.pluginOption;
const detail = {
_key,
info: Object.assign(info, {values}, {}),
notifyPath: `${_key}.values`,
};
this.dispatchEvent(
new CustomEvent('plugin-config-option-changed', {detail}));
}
_computeShowInputRow(disabled) {
return disabled ? 'hide' : '';
}
}
customElements.define(GrPluginConfigArrayEditor.is,
GrPluginConfigArrayEditor);

View File

@ -1,30 +1,22 @@
<!--
@license
Copyright (C) 2018 The Android Open Source Project
/**
* @license
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {html} from '@polymer/polymer/lib/utils/html-tag.js';
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<link rel="import" href="/bower_components/polymer/polymer.html">
<link rel="import" href="/bower_components/iron-input/iron-input.html">
<link rel="import" href="/bower_components/paper-toggle-button/paper-toggle-button.html">
<link rel="import" href="../../../styles/gr-form-styles.html">
<link rel="import" href="../../../styles/shared-styles.html">
<link rel="import" href="../../shared/gr-button/gr-button.html">
<dom-module id="gr-plugin-config-array-editor">
<template>
export const htmlTemplate = html`
<style include="shared-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
@ -72,11 +64,7 @@ limitations under the License.
<template is="dom-repeat" items="[[pluginOption.info.values]]">
<div class="row">
<span>[[item]]</span>
<gr-button
link
disabled$="[[disabled]]"
data-item$="[[item]]"
on-click="_handleDelete">Delete</gr-button>
<gr-button link="" disabled\$="[[disabled]]" data-item\$="[[item]]" on-click="_handleDelete">Delete</gr-button>
</div>
</template>
</div>
@ -84,23 +72,11 @@ limitations under the License.
<template is="dom-if" if="[[!pluginOption.info.values.length]]">
<div class="row placeholder">None configured.</div>
</template>
<div class$="row [[_computeShowInputRow(disabled)]]">
<iron-input
on-keydown="_handleInputKeydown"
bind-value="{{_newValue}}">
<input
is="iron-input"
id="input"
on-keydown="_handleInputKeydown"
bind-value="{{_newValue}}">
<div class\$="row [[_computeShowInputRow(disabled)]]">
<iron-input on-keydown="_handleInputKeydown" bind-value="{{_newValue}}">
<input is="iron-input" id="input" on-keydown="_handleInputKeydown" bind-value="{{_newValue}}">
</iron-input>
<gr-button
id="addButton"
disabled$="[[!_newValue.length]]"
link
on-click="_handleAddTap">Add</gr-button>
<gr-button id="addButton" disabled\$="[[!_newValue.length]]" link="" on-click="_handleAddTap">Add</gr-button>
</div>
</div>
</template>
<script src="gr-plugin-config-array-editor.js"></script>
</dom-module>
`;

View File

@ -19,15 +19,20 @@ limitations under the License.
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-plugin-config-array-editor</title>
<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/bower_components/web-component-tester/browser.js"></script>
<script src="../../../test/test-pre-setup.js"></script>
<link rel="import" href="../../../test/common-test-setup.html"/>
<link rel="import" href="gr-plugin-config-array-editor.html">
<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/components/wct-browser-legacy/browser.js"></script>
<script type="module" src="../../../test/test-pre-setup.js"></script>
<script type="module" src="../../../test/common-test-setup.js"></script>
<script type="module" src="./gr-plugin-config-array-editor.js"></script>
<script>void(0);</script>
<script type="module">
import '../../../test/test-pre-setup.js';
import '../../../test/common-test-setup.js';
import './gr-plugin-config-array-editor.js';
void(0);
</script>
<test-fixture id="basic">
<template>
@ -35,112 +40,115 @@ limitations under the License.
</template>
</test-fixture>
<script>
suite('gr-plugin-config-array-editor tests', async () => {
await readyToTest();
let element;
let sandbox;
let dispatchStub;
<script type="module">
import '../../../test/test-pre-setup.js';
import '../../../test/common-test-setup.js';
import './gr-plugin-config-array-editor.js';
import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
suite('gr-plugin-config-array-editor tests', () => {
let element;
let sandbox;
let dispatchStub;
const getAll = str => Polymer.dom(element.root).querySelectorAll(str);
const getAll = str => dom(element.root).querySelectorAll(str);
setup(() => {
sandbox = sinon.sandbox.create();
element = fixture('basic');
element.pluginOption = {
_key: 'test-key',
info: {
values: [],
},
};
});
teardown(() => sandbox.restore());
test('_computeShowInputRow', () => {
assert.equal(element._computeShowInputRow(true), 'hide');
assert.equal(element._computeShowInputRow(false), '');
});
test('_computeDisabled', () => {
assert.isTrue(element._computeDisabled({}));
assert.isTrue(element._computeDisabled({base: {}}));
assert.isTrue(element._computeDisabled({base: {info: {}}}));
assert.isTrue(
element._computeDisabled({base: {info: {editable: false}}}));
assert.isFalse(
element._computeDisabled({base: {info: {editable: true}}}));
});
suite('adding', () => {
setup(() => {
sandbox = sinon.sandbox.create();
element = fixture('basic');
element.pluginOption = {
_key: 'test-key',
info: {
values: [],
},
};
});
teardown(() => sandbox.restore());
test('_computeShowInputRow', () => {
assert.equal(element._computeShowInputRow(true), 'hide');
assert.equal(element._computeShowInputRow(false), '');
});
test('_computeDisabled', () => {
assert.isTrue(element._computeDisabled({}));
assert.isTrue(element._computeDisabled({base: {}}));
assert.isTrue(element._computeDisabled({base: {info: {}}}));
assert.isTrue(
element._computeDisabled({base: {info: {editable: false}}}));
assert.isFalse(
element._computeDisabled({base: {info: {editable: true}}}));
});
suite('adding', () => {
setup(() => {
dispatchStub = sandbox.stub(element, '_dispatchChanged');
});
test('with enter', () => {
element._newValue = '';
MockInteractions.pressAndReleaseKeyOn(element.$.input, 13); // Enter
flushAsynchronousOperations();
assert.isFalse(dispatchStub.called);
element._newValue = 'test';
MockInteractions.pressAndReleaseKeyOn(element.$.input, 13); // Enter
flushAsynchronousOperations();
assert.isTrue(dispatchStub.called);
assert.equal(dispatchStub.lastCall.args[0], 'test');
assert.equal(element._newValue, '');
});
test('with add btn', () => {
element._newValue = '';
MockInteractions.tap(element.$.addButton);
flushAsynchronousOperations();
assert.isFalse(dispatchStub.called);
element._newValue = 'test';
MockInteractions.tap(element.$.addButton);
flushAsynchronousOperations();
assert.isTrue(dispatchStub.called);
assert.equal(dispatchStub.lastCall.args[0], 'test');
assert.equal(element._newValue, '');
});
});
test('deleting', () => {
dispatchStub = sandbox.stub(element, '_dispatchChanged');
element.pluginOption = {info: {values: ['test', 'test2']}};
flushAsynchronousOperations();
});
const rows = getAll('.existingItems .row');
assert.equal(rows.length, 2);
const button = rows[0].querySelector('gr-button');
MockInteractions.tap(button);
test('with enter', () => {
element._newValue = '';
MockInteractions.pressAndReleaseKeyOn(element.$.input, 13); // Enter
flushAsynchronousOperations();
assert.isFalse(dispatchStub.called);
element.pluginOption.info.editable = true;
element.notifyPath('pluginOption.info.editable');
flushAsynchronousOperations();
MockInteractions.tap(button);
element._newValue = 'test';
MockInteractions.pressAndReleaseKeyOn(element.$.input, 13); // Enter
flushAsynchronousOperations();
assert.isTrue(dispatchStub.called);
assert.deepEqual(dispatchStub.lastCall.args[0], ['test2']);
assert.equal(dispatchStub.lastCall.args[0], 'test');
assert.equal(element._newValue, '');
});
test('_dispatchChanged', () => {
const eventStub = sandbox.stub(element, 'dispatchEvent');
element._dispatchChanged(['new-test-value']);
test('with add btn', () => {
element._newValue = '';
MockInteractions.tap(element.$.addButton);
flushAsynchronousOperations();
assert.isTrue(eventStub.called);
const {detail} = eventStub.lastCall.args[0];
assert.equal(detail._key, 'test-key');
assert.deepEqual(detail.info, {values: ['new-test-value']});
assert.equal(detail.notifyPath, 'test-key.values');
assert.isFalse(dispatchStub.called);
element._newValue = 'test';
MockInteractions.tap(element.$.addButton);
flushAsynchronousOperations();
assert.isTrue(dispatchStub.called);
assert.equal(dispatchStub.lastCall.args[0], 'test');
assert.equal(element._newValue, '');
});
});
test('deleting', () => {
dispatchStub = sandbox.stub(element, '_dispatchChanged');
element.pluginOption = {info: {values: ['test', 'test2']}};
flushAsynchronousOperations();
const rows = getAll('.existingItems .row');
assert.equal(rows.length, 2);
const button = rows[0].querySelector('gr-button');
MockInteractions.tap(button);
flushAsynchronousOperations();
assert.isFalse(dispatchStub.called);
element.pluginOption.info.editable = true;
element.notifyPath('pluginOption.info.editable');
flushAsynchronousOperations();
MockInteractions.tap(button);
flushAsynchronousOperations();
assert.isTrue(dispatchStub.called);
assert.deepEqual(dispatchStub.lastCall.args[0], ['test2']);
});
test('_dispatchChanged', () => {
const eventStub = sandbox.stub(element, 'dispatchEvent');
element._dispatchChanged(['new-test-value']);
assert.isTrue(eventStub.called);
const {detail} = eventStub.lastCall.args[0];
assert.equal(detail._key, 'test-key');
assert.deepEqual(detail.info, {values: ['new-test-value']});
assert.equal(detail.notifyPath, 'test-key.values');
});
});
</script>

View File

@ -14,110 +14,122 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function() {
'use strict';
import '../../../scripts/bundled-polymer.js';
/**
* @appliesMixin Gerrit.FireMixin
* @appliesMixin Gerrit.ListViewMixin
* @extends Polymer.Element
*/
class GrPluginList extends Polymer.mixinBehaviors( [
Gerrit.FireBehavior,
Gerrit.ListViewBehavior,
], Polymer.GestureEventListeners(
Polymer.LegacyElementMixin(
Polymer.Element))) {
static get is() { return 'gr-plugin-list'; }
import '../../../behaviors/fire-behavior/fire-behavior.js';
import '../../../behaviors/gr-list-view-behavior/gr-list-view-behavior.js';
import '../../../styles/gr-table-styles.js';
import '../../../styles/shared-styles.js';
import '../../shared/gr-list-view/gr-list-view.js';
import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
import {PolymerElement} from '@polymer/polymer/polymer-element.js';
import {htmlTemplate} from './gr-plugin-list_html.js';
static get properties() {
return {
/**
* @appliesMixin Gerrit.FireMixin
* @appliesMixin Gerrit.ListViewMixin
* @extends Polymer.Element
*/
class GrPluginList extends mixinBehaviors( [
Gerrit.FireBehavior,
Gerrit.ListViewBehavior,
], GestureEventListeners(
LegacyElementMixin(
PolymerElement))) {
static get template() { return htmlTemplate; }
static get is() { return 'gr-plugin-list'; }
static get properties() {
return {
/**
* URL params passed from the router.
*/
params: {
type: Object,
observer: '_paramsChanged',
},
/**
* URL params passed from the router.
* Offset of currently visible query results.
*/
params: {
type: Object,
observer: '_paramsChanged',
},
/**
* Offset of currently visible query results.
*/
_offset: {
type: Number,
value: 0,
},
_path: {
type: String,
readOnly: true,
value: '/admin/plugins',
},
_plugins: Array,
/**
* Because we request one more than the pluginsPerPage, _shownPlugins
* maybe one less than _plugins.
* */
_shownPlugins: {
type: Array,
computed: 'computeShownItems(_plugins)',
},
_pluginsPerPage: {
type: Number,
value: 25,
},
_loading: {
type: Boolean,
value: true,
},
_filter: {
type: String,
value: '',
},
};
}
/** @override */
attached() {
super.attached();
this.fire('title-change', {title: 'Plugins'});
}
_paramsChanged(params) {
this._loading = true;
this._filter = this.getFilterValue(params);
this._offset = this.getOffsetValue(params);
return this._getPlugins(this._filter, this._pluginsPerPage,
this._offset);
}
_getPlugins(filter, pluginsPerPage, offset) {
const errFn = response => {
this.fire('page-error', {response});
};
return this.$.restAPI.getPlugins(filter, pluginsPerPage, offset, errFn)
.then(plugins => {
if (!plugins) {
this._plugins = [];
return;
}
this._plugins = Object.keys(plugins)
.map(key => {
const plugin = plugins[key];
plugin.name = key;
return plugin;
});
this._loading = false;
});
}
_status(item) {
return item.disabled === true ? 'Disabled' : 'Enabled';
}
_computePluginUrl(id) {
return this.getUrl('/', id);
}
_offset: {
type: Number,
value: 0,
},
_path: {
type: String,
readOnly: true,
value: '/admin/plugins',
},
_plugins: Array,
/**
* Because we request one more than the pluginsPerPage, _shownPlugins
* maybe one less than _plugins.
* */
_shownPlugins: {
type: Array,
computed: 'computeShownItems(_plugins)',
},
_pluginsPerPage: {
type: Number,
value: 25,
},
_loading: {
type: Boolean,
value: true,
},
_filter: {
type: String,
value: '',
},
};
}
customElements.define(GrPluginList.is, GrPluginList);
})();
/** @override */
attached() {
super.attached();
this.fire('title-change', {title: 'Plugins'});
}
_paramsChanged(params) {
this._loading = true;
this._filter = this.getFilterValue(params);
this._offset = this.getOffsetValue(params);
return this._getPlugins(this._filter, this._pluginsPerPage,
this._offset);
}
_getPlugins(filter, pluginsPerPage, offset) {
const errFn = response => {
this.fire('page-error', {response});
};
return this.$.restAPI.getPlugins(filter, pluginsPerPage, offset, errFn)
.then(plugins => {
if (!plugins) {
this._plugins = [];
return;
}
this._plugins = Object.keys(plugins)
.map(key => {
const plugin = plugins[key];
plugin.name = key;
return plugin;
});
this._loading = false;
});
}
_status(item) {
return item.disabled === true ? 'Disabled' : 'Enabled';
}
_computePluginUrl(id) {
return this.getUrl('/', id);
}
}
customElements.define(GrPluginList.is, GrPluginList);

View File

@ -1,58 +1,44 @@
<!--
@license
Copyright (C) 2017 The Android Open Source Project
/**
* @license
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {html} from '@polymer/polymer/lib/utils/html-tag.js';
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<link rel="import" href="/bower_components/polymer/polymer.html">
<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
<link rel="import" href="../../../behaviors/gr-list-view-behavior/gr-list-view-behavior.html">
<link rel="import" href="../../../styles/gr-table-styles.html">
<link rel="import" href="../../../styles/shared-styles.html">
<link rel="import" href="../../shared/gr-list-view/gr-list-view.html">
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
<dom-module id="gr-plugin-list">
<template>
export const htmlTemplate = html`
<style include="shared-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
<style include="gr-table-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
<gr-list-view
filter="[[_filter]]"
items-per-page="[[_pluginsPerPage]]"
items="[[_plugins]]"
loading="[[_loading]]"
offset="[[_offset]]"
path="[[_path]]">
<gr-list-view filter="[[_filter]]" items-per-page="[[_pluginsPerPage]]" items="[[_plugins]]" loading="[[_loading]]" offset="[[_offset]]" path="[[_path]]">
<table id="list" class="genericList">
<tr class="headerRow">
<tbody><tr class="headerRow">
<th class="name topHeader">Plugin Name</th>
<th class="version topHeader">Version</th>
<th class="status topHeader">Status</th>
</tr>
<tr id="loading" class$="loadingMsg [[computeLoadingClass(_loading)]]">
<tr id="loading" class\$="loadingMsg [[computeLoadingClass(_loading)]]">
<td>Loading...</td>
</tr>
<tbody class$="[[computeLoadingClass(_loading)]]">
</tbody><tbody class\$="[[computeLoadingClass(_loading)]]">
<template is="dom-repeat" items="[[_shownPlugins]]">
<tr class="table">
<td class="name">
<template is="dom-if" if="[[item.index_url]]">
<a href$="[[_computePluginUrl(item.index_url)]]">[[item.id]]</a>
<a href\$="[[_computePluginUrl(item.index_url)]]">[[item.id]]</a>
</template>
<template is="dom-if" if="[[!item.index_url]]">
[[item.id]]
@ -66,6 +52,4 @@ limitations under the License.
</table>
</gr-list-view>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
</template>
<script src="gr-plugin-list.js"></script>
</dom-module>
`;

View File

@ -19,16 +19,21 @@ limitations under the License.
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-plugin-list</title>
<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/bower_components/page/page.js"></script>
<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/bower_components/web-component-tester/browser.js"></script>
<script src="../../../test/test-pre-setup.js"></script>
<link rel="import" href="../../../test/common-test-setup.html"/>
<link rel="import" href="gr-plugin-list.html">
<script src="/node_modules/page/page.js"></script>
<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/components/wct-browser-legacy/browser.js"></script>
<script type="module" src="../../../test/test-pre-setup.js"></script>
<script type="module" src="../../../test/common-test-setup.js"></script>
<script type="module" src="./gr-plugin-list.js"></script>
<script>void(0);</script>
<script type="module">
import '../../../test/test-pre-setup.js';
import '../../../test/common-test-setup.js';
import './gr-plugin-list.js';
void(0);
</script>
<test-fixture id="basic">
<template>
@ -36,151 +41,154 @@ limitations under the License.
</template>
</test-fixture>
<script>
let counter;
const pluginGenerator = () => {
const plugin = {
id: `test${++counter}`,
version: '3.0-SNAPSHOT',
disabled: false,
};
if (counter !== 2) {
plugin.index_url = `plugins/test${counter}/`;
}
return plugin;
<script type="module">
import '../../../test/test-pre-setup.js';
import '../../../test/common-test-setup.js';
import './gr-plugin-list.js';
import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
let counter;
const pluginGenerator = () => {
const plugin = {
id: `test${++counter}`,
version: '3.0-SNAPSHOT',
disabled: false,
};
suite('gr-plugin-list tests', async () => {
await readyToTest();
let element;
let plugins;
let sandbox;
let value;
if (counter !== 2) {
plugin.index_url = `plugins/test${counter}/`;
}
return plugin;
};
setup(() => {
sandbox = sinon.sandbox.create();
element = fixture('basic');
counter = 0;
suite('gr-plugin-list tests', () => {
let element;
let plugins;
let sandbox;
let value;
setup(() => {
sandbox = sinon.sandbox.create();
element = fixture('basic');
counter = 0;
});
teardown(() => {
sandbox.restore();
});
suite('list with plugins', () => {
setup(done => {
plugins = _.times(26, pluginGenerator);
stub('gr-rest-api-interface', {
getPlugins(num, offset) {
return Promise.resolve(plugins);
},
});
element._paramsChanged(value).then(() => { flush(done); });
});
teardown(() => {
sandbox.restore();
});
suite('list with plugins', () => {
setup(done => {
plugins = _.times(26, pluginGenerator);
stub('gr-rest-api-interface', {
getPlugins(num, offset) {
return Promise.resolve(plugins);
},
});
element._paramsChanged(value).then(() => { flush(done); });
});
test('plugin in the list is formatted correctly', done => {
flush(() => {
assert.equal(element._plugins[2].id, 'test3');
assert.equal(element._plugins[2].index_url, 'plugins/test3/');
assert.equal(element._plugins[2].version, '3.0-SNAPSHOT');
assert.equal(element._plugins[2].disabled, false);
done();
});
});
test('with and without urls', done => {
flush(() => {
const names = Polymer.dom(element.root).querySelectorAll('.name');
assert.isOk(names[1].querySelector('a'));
assert.equal(names[1].querySelector('a').innerText, 'test1');
assert.isNotOk(names[2].querySelector('a'));
assert.equal(names[2].innerText, 'test2');
done();
});
});
test('_shownPlugins', () => {
assert.equal(element._shownPlugins.length, 25);
test('plugin in the list is formatted correctly', done => {
flush(() => {
assert.equal(element._plugins[2].id, 'test3');
assert.equal(element._plugins[2].index_url, 'plugins/test3/');
assert.equal(element._plugins[2].version, '3.0-SNAPSHOT');
assert.equal(element._plugins[2].disabled, false);
done();
});
});
suite('list with less then 26 plugins', () => {
setup(done => {
plugins = _.times(25, pluginGenerator);
stub('gr-rest-api-interface', {
getPlugins(num, offset) {
return Promise.resolve(plugins);
},
});
element._paramsChanged(value).then(() => { flush(done); });
});
test('_shownPlugins', () => {
assert.equal(element._shownPlugins.length, 25);
test('with and without urls', done => {
flush(() => {
const names = dom(element.root).querySelectorAll('.name');
assert.isOk(names[1].querySelector('a'));
assert.equal(names[1].querySelector('a').innerText, 'test1');
assert.isNotOk(names[2].querySelector('a'));
assert.equal(names[2].innerText, 'test2');
done();
});
});
suite('filter', () => {
test('_paramsChanged', done => {
sandbox.stub(
element.$.restAPI,
'getPlugins',
() => Promise.resolve(plugins));
const value = {
filter: 'test',
offset: 25,
};
element._paramsChanged(value).then(() => {
assert.equal(element.$.restAPI.getPlugins.lastCall.args[0],
'test');
assert.equal(element.$.restAPI.getPlugins.lastCall.args[1],
25);
assert.equal(element.$.restAPI.getPlugins.lastCall.args[2],
25);
done();
});
test('_shownPlugins', () => {
assert.equal(element._shownPlugins.length, 25);
});
});
suite('list with less then 26 plugins', () => {
setup(done => {
plugins = _.times(25, pluginGenerator);
stub('gr-rest-api-interface', {
getPlugins(num, offset) {
return Promise.resolve(plugins);
},
});
element._paramsChanged(value).then(() => { flush(done); });
});
suite('loading', () => {
test('correct contents are displayed', () => {
assert.isTrue(element._loading);
assert.equal(element.computeLoadingClass(element._loading), 'loading');
assert.equal(getComputedStyle(element.$.loading).display, 'block');
element._loading = false;
element._plugins = _.times(25, pluginGenerator);
flushAsynchronousOperations();
assert.equal(element.computeLoadingClass(element._loading), '');
assert.equal(getComputedStyle(element.$.loading).display, 'none');
});
test('_shownPlugins', () => {
assert.equal(element._shownPlugins.length, 25);
});
});
suite('404', () => {
test('fires page-error', done => {
const response = {status: 404};
sandbox.stub(element.$.restAPI, 'getPlugins',
(filter, pluginsPerPage, opt_offset, errFn) => {
errFn(response);
});
element.addEventListener('page-error', e => {
assert.deepEqual(e.detail.response, response);
done();
});
const value = {
filter: 'test',
offset: 25,
};
element._paramsChanged(value);
suite('filter', () => {
test('_paramsChanged', done => {
sandbox.stub(
element.$.restAPI,
'getPlugins',
() => Promise.resolve(plugins));
const value = {
filter: 'test',
offset: 25,
};
element._paramsChanged(value).then(() => {
assert.equal(element.$.restAPI.getPlugins.lastCall.args[0],
'test');
assert.equal(element.$.restAPI.getPlugins.lastCall.args[1],
25);
assert.equal(element.$.restAPI.getPlugins.lastCall.args[2],
25);
done();
});
});
});
suite('loading', () => {
test('correct contents are displayed', () => {
assert.isTrue(element._loading);
assert.equal(element.computeLoadingClass(element._loading), 'loading');
assert.equal(getComputedStyle(element.$.loading).display, 'block');
element._loading = false;
element._plugins = _.times(25, pluginGenerator);
flushAsynchronousOperations();
assert.equal(element.computeLoadingClass(element._loading), '');
assert.equal(getComputedStyle(element.$.loading).display, 'none');
});
});
suite('404', () => {
test('fires page-error', done => {
const response = {status: 404};
sandbox.stub(element.$.restAPI, 'getPlugins',
(filter, pluginsPerPage, opt_offset, errFn) => {
errFn(response);
});
element.addEventListener('page-error', e => {
assert.deepEqual(e.detail.response, response);
done();
});
const value = {
filter: 'test',
offset: 25,
};
element._paramsChanged(value);
});
});
});
</script>

View File

@ -14,497 +14,515 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function() {
'use strict';
import '../../../scripts/bundled-polymer.js';
const Defs = {};
import '../../../behaviors/base-url-behavior/base-url-behavior.js';
import '../../../behaviors/fire-behavior/fire-behavior.js';
import '../../../behaviors/gr-access-behavior/gr-access-behavior.js';
import '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
import '../../../styles/gr-menu-page-styles.js';
import '../../../styles/gr-subpage-styles.js';
import '../../../styles/shared-styles.js';
import '../../core/gr-navigation/gr-navigation.js';
import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
import '../gr-access-section/gr-access-section.js';
import '../../../scripts/util.js';
import {flush, dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
import {PolymerElement} from '@polymer/polymer/polymer-element.js';
import {htmlTemplate} from './gr-repo-access_html.js';
const NOTHING_TO_SAVE = 'No changes to save.';
const Defs = {};
const MAX_AUTOCOMPLETE_RESULTS = 50;
const NOTHING_TO_SAVE = 'No changes to save.';
/**
* Fired when save is a no-op
*
* @event show-alert
*/
const MAX_AUTOCOMPLETE_RESULTS = 50;
/**
* @typedef {{
* value: !Object,
* }}
*/
Defs.rule;
/**
* Fired when save is a no-op
*
* @event show-alert
*/
/**
* @typedef {{
* rules: !Object<string, Defs.rule>
* }}
*/
Defs.permission;
/**
* @typedef {{
* value: !Object,
* }}
*/
Defs.rule;
/**
* Can be an empty object or consist of permissions.
*
* @typedef {{
* permissions: !Object<string, Defs.permission>
* }}
*/
Defs.permissions;
/**
* @typedef {{
* rules: !Object<string, Defs.rule>
* }}
*/
Defs.permission;
/**
* Can be an empty object or consist of permissions.
*
* @typedef {!Object<string, Defs.permissions>}
*/
Defs.sections;
/**
* Can be an empty object or consist of permissions.
*
* @typedef {{
* permissions: !Object<string, Defs.permission>
* }}
*/
Defs.permissions;
/**
* @typedef {{
* remove: !Defs.sections,
* add: !Defs.sections,
* }}
*/
Defs.projectAccessInput;
/**
* Can be an empty object or consist of permissions.
*
* @typedef {!Object<string, Defs.permissions>}
*/
Defs.sections;
/**
* @appliesMixin Gerrit.AccessMixin
* @appliesMixin Gerrit.BaseUrlMixin
* @appliesMixin Gerrit.FireMixin
* @appliesMixin Gerrit.URLEncodingMixin
* @extends Polymer.Element
*/
class GrRepoAccess extends Polymer.mixinBehaviors( [
Gerrit.AccessBehavior,
Gerrit.BaseUrlBehavior,
Gerrit.FireBehavior,
Gerrit.URLEncodingBehavior,
], Polymer.GestureEventListeners(
Polymer.LegacyElementMixin(
Polymer.Element))) {
static get is() { return 'gr-repo-access'; }
/**
* @typedef {{
* remove: !Defs.sections,
* add: !Defs.sections,
* }}
*/
Defs.projectAccessInput;
static get properties() {
return {
repo: {
type: String,
observer: '_repoChanged',
/**
* @appliesMixin Gerrit.AccessMixin
* @appliesMixin Gerrit.BaseUrlMixin
* @appliesMixin Gerrit.FireMixin
* @appliesMixin Gerrit.URLEncodingMixin
* @extends Polymer.Element
*/
class GrRepoAccess extends mixinBehaviors( [
Gerrit.AccessBehavior,
Gerrit.BaseUrlBehavior,
Gerrit.FireBehavior,
Gerrit.URLEncodingBehavior,
], GestureEventListeners(
LegacyElementMixin(
PolymerElement))) {
static get template() { return htmlTemplate; }
static get is() { return 'gr-repo-access'; }
static get properties() {
return {
repo: {
type: String,
observer: '_repoChanged',
},
// The current path
path: String,
_canUpload: {
type: Boolean,
value: false,
},
_inheritFromFilter: String,
_query: {
type: Function,
value() {
return this._getInheritFromSuggestions.bind(this);
},
// The current path
path: String,
},
_ownerOf: Array,
_capabilities: Object,
_groups: Object,
/** @type {?} */
_inheritsFrom: Object,
_labels: Object,
_local: Object,
_editing: {
type: Boolean,
value: false,
observer: '_handleEditingChanged',
},
_modified: {
type: Boolean,
value: false,
},
_sections: Array,
_weblinks: Array,
_loading: {
type: Boolean,
value: true,
},
};
}
_canUpload: {
type: Boolean,
value: false,
},
_inheritFromFilter: String,
_query: {
type: Function,
value() {
return this._getInheritFromSuggestions.bind(this);
},
},
_ownerOf: Array,
_capabilities: Object,
_groups: Object,
/** @type {?} */
_inheritsFrom: Object,
_labels: Object,
_local: Object,
_editing: {
type: Boolean,
value: false,
observer: '_handleEditingChanged',
},
_modified: {
type: Boolean,
value: false,
},
_sections: Array,
_weblinks: Array,
_loading: {
type: Boolean,
value: true,
},
};
}
/** @override */
created() {
super.created();
this.addEventListener('access-modified',
() =>
this._handleAccessModified());
}
/** @override */
created() {
super.created();
this.addEventListener('access-modified',
() =>
this._handleAccessModified());
}
_handleAccessModified() {
this._modified = true;
}
_handleAccessModified() {
this._modified = true;
}
/**
* @param {string} repo
* @return {!Promise}
*/
_repoChanged(repo) {
this._loading = true;
/**
* @param {string} repo
* @return {!Promise}
*/
_repoChanged(repo) {
this._loading = true;
if (!repo) { return Promise.resolve(); }
if (!repo) { return Promise.resolve(); }
return this._reload(repo);
}
return this._reload(repo);
}
_reload(repo) {
const promises = [];
_reload(repo) {
const promises = [];
const errFn = response => {
this.fire('page-error', {response});
};
const errFn = response => {
this.fire('page-error', {response});
};
this._editing = false;
this._editing = false;
// Always reset sections when a project changes.
this._sections = [];
promises.push(this.$.restAPI.getRepoAccessRights(repo, errFn)
.then(res => {
if (!res) { return Promise.resolve(); }
// Always reset sections when a project changes.
this._sections = [];
promises.push(this.$.restAPI.getRepoAccessRights(repo, errFn)
.then(res => {
if (!res) { return Promise.resolve(); }
// Keep a copy of the original inherit from values separate from
// the ones data bound to gr-autocomplete, so the original value
// can be restored if the user cancels.
this._inheritsFrom = res.inherits_from ? Object.assign({},
res.inherits_from) : null;
this._originalInheritsFrom = res.inherits_from ? Object.assign({},
res.inherits_from) : null;
// Initialize the filter value so when the user clicks edit, the
// current value appears. If there is no parent repo, it is
// initialized as an empty string.
this._inheritFromFilter = res.inherits_from ?
this._inheritsFrom.name : '';
this._local = res.local;
this._groups = res.groups;
this._weblinks = res.config_web_links || [];
this._canUpload = res.can_upload;
this._ownerOf = res.owner_of || [];
return this.toSortedArray(this._local);
}));
promises.push(this.$.restAPI.getCapabilities(errFn)
.then(res => {
if (!res) { return Promise.resolve(); }
return res;
}));
promises.push(this.$.restAPI.getRepo(repo, errFn)
.then(res => {
if (!res) { return Promise.resolve(); }
return res.labels;
}));
return Promise.all(promises).then(([sections, capabilities, labels]) => {
this._capabilities = capabilities;
this._labels = labels;
this._sections = sections;
this._loading = false;
});
}
_handleUpdateInheritFrom(e) {
if (!this._inheritsFrom) {
this._inheritsFrom = {};
}
this._inheritsFrom.id = e.detail.value;
this._inheritsFrom.name = this._inheritFromFilter;
this._handleAccessModified();
}
_getInheritFromSuggestions() {
return this.$.restAPI.getRepos(
this._inheritFromFilter,
MAX_AUTOCOMPLETE_RESULTS)
.then(response => {
const projects = [];
for (const key in response) {
if (!response.hasOwnProperty(key)) { continue; }
projects.push({
name: response[key].name,
value: response[key].id,
});
}
return projects;
});
}
_computeLoadingClass(loading) {
return loading ? 'loading' : '';
}
_handleEdit() {
this._editing = !this._editing;
}
_editOrCancel(editing) {
return editing ? 'Cancel' : 'Edit';
}
_computeWebLinkClass(weblinks) {
return weblinks && weblinks.length ? 'show' : '';
}
_computeShowInherit(inheritsFrom) {
return inheritsFrom ? 'show' : '';
}
_handleAddedSectionRemoved(e) {
const index = e.model.index;
this._sections = this._sections.slice(0, index)
.concat(this._sections.slice(index + 1, this._sections.length));
}
_handleEditingChanged(editing, editingOld) {
// Ignore when editing gets set initially.
if (!editingOld || editing) { return; }
// Remove any unsaved but added refs.
if (this._sections) {
this._sections = this._sections.filter(p => !p.value.added);
}
// Restore inheritFrom.
if (this._inheritsFrom) {
this._inheritsFrom = Object.assign({}, this._originalInheritsFrom);
this._inheritFromFilter = this._inheritsFrom.name;
}
for (const key of Object.keys(this._local)) {
if (this._local[key].added) {
delete this._local[key];
}
}
}
/**
* @param {!Defs.projectAccessInput} addRemoveObj
* @param {!Array} path
* @param {string} type add or remove
* @param {!Object=} opt_value value to add if the type is 'add'
* @return {!Defs.projectAccessInput}
*/
_updateAddRemoveObj(addRemoveObj, path, type, opt_value) {
let curPos = addRemoveObj[type];
for (const item of path) {
if (!curPos[item]) {
if (item === path[path.length - 1] && type === 'remove') {
if (path[path.length - 2] === 'permissions') {
curPos[item] = {rules: {}};
} else if (path.length === 1) {
curPos[item] = {permissions: {}};
} else {
curPos[item] = {};
}
} else if (item === path[path.length - 1] && type === 'add') {
curPos[item] = opt_value;
} else {
curPos[item] = {};
}
}
curPos = curPos[item];
}
return addRemoveObj;
}
/**
* Used to recursively remove any objects with a 'deleted' bit.
*/
_recursivelyRemoveDeleted(obj) {
for (const k in obj) {
if (!obj.hasOwnProperty(k)) { continue; }
if (typeof obj[k] == 'object') {
if (obj[k].deleted) {
delete obj[k];
return;
}
this._recursivelyRemoveDeleted(obj[k]);
}
}
}
_recursivelyUpdateAddRemoveObj(obj, addRemoveObj, path = []) {
for (const k in obj) {
if (!obj.hasOwnProperty(k)) { continue; }
if (typeof obj[k] == 'object') {
const updatedId = obj[k].updatedId;
const ref = updatedId ? updatedId : k;
if (obj[k].deleted) {
this._updateAddRemoveObj(addRemoveObj,
path.concat(k), 'remove');
continue;
} else if (obj[k].modified) {
this._updateAddRemoveObj(addRemoveObj,
path.concat(k), 'remove');
this._updateAddRemoveObj(addRemoveObj, path.concat(ref), 'add',
obj[k]);
/* Special case for ref changes because they need to be added and
removed in a different way. The new ref needs to include all
changes but also the initial state. To do this, instead of
continuing with the same recursion, just remove anything that is
deleted in the current state. */
if (updatedId && updatedId !== k) {
this._recursivelyRemoveDeleted(addRemoveObj.add[updatedId]);
}
continue;
} else if (obj[k].added) {
this._updateAddRemoveObj(addRemoveObj,
path.concat(ref), 'add', obj[k]);
/**
* As add / delete both can happen in the new section,
* so here to make sure it will remove the deleted ones.
*
* @see Issue 11339
*/
this._recursivelyRemoveDeleted(addRemoveObj.add[k]);
continue;
}
this._recursivelyUpdateAddRemoveObj(obj[k], addRemoveObj,
path.concat(k));
}
}
}
/**
* Returns an object formatted for saving or submitting access changes for
* review
*
* @return {!Defs.projectAccessInput}
*/
_computeAddAndRemove() {
const addRemoveObj = {
add: {},
remove: {},
};
const originalInheritsFromId = this._originalInheritsFrom ?
this.singleDecodeURL(this._originalInheritsFrom.id) :
null;
const inheritsFromId = this._inheritsFrom ?
this.singleDecodeURL(this._inheritsFrom.id) :
null;
const inheritFromChanged =
// Inherit from changed
(originalInheritsFromId &&
originalInheritsFromId !== inheritsFromId) ||
// Inherit from added (did not have one initially);
(!originalInheritsFromId && inheritsFromId);
this._recursivelyUpdateAddRemoveObj(this._local, addRemoveObj);
if (inheritFromChanged) {
addRemoveObj.parent = inheritsFromId;
}
return addRemoveObj;
}
_handleCreateSection() {
let newRef = 'refs/for/*';
// Avoid using an already used key for the placeholder, since it
// immediately gets added to an object.
while (this._local[newRef]) {
newRef = `${newRef}*`;
}
const section = {permissions: {}, added: true};
this.push('_sections', {id: newRef, value: section});
this.set(['_local', newRef], section);
Polymer.dom.flush();
Polymer.dom(this.root).querySelector('gr-access-section:last-of-type')
.editReference();
}
_getObjforSave() {
const addRemoveObj = this._computeAddAndRemove();
// If there are no changes, don't actually save.
if (!Object.keys(addRemoveObj.add).length &&
!Object.keys(addRemoveObj.remove).length &&
!addRemoveObj.parent) {
this.dispatchEvent(new CustomEvent('show-alert', {
detail: {message: NOTHING_TO_SAVE},
bubbles: true,
composed: true,
// Keep a copy of the original inherit from values separate from
// the ones data bound to gr-autocomplete, so the original value
// can be restored if the user cancels.
this._inheritsFrom = res.inherits_from ? Object.assign({},
res.inherits_from) : null;
this._originalInheritsFrom = res.inherits_from ? Object.assign({},
res.inherits_from) : null;
// Initialize the filter value so when the user clicks edit, the
// current value appears. If there is no parent repo, it is
// initialized as an empty string.
this._inheritFromFilter = res.inherits_from ?
this._inheritsFrom.name : '';
this._local = res.local;
this._groups = res.groups;
this._weblinks = res.config_web_links || [];
this._canUpload = res.can_upload;
this._ownerOf = res.owner_of || [];
return this.toSortedArray(this._local);
}));
return;
}
const obj = {
add: addRemoveObj.add,
remove: addRemoveObj.remove,
};
if (addRemoveObj.parent) {
obj.parent = addRemoveObj.parent;
}
return obj;
}
_handleSave(e) {
const obj = this._getObjforSave();
if (!obj) { return; }
const button = e && e.target;
if (button) {
button.loading = true;
promises.push(this.$.restAPI.getCapabilities(errFn)
.then(res => {
if (!res) { return Promise.resolve(); }
return res;
}));
promises.push(this.$.restAPI.getRepo(repo, errFn)
.then(res => {
if (!res) { return Promise.resolve(); }
return res.labels;
}));
return Promise.all(promises).then(([sections, capabilities, labels]) => {
this._capabilities = capabilities;
this._labels = labels;
this._sections = sections;
this._loading = false;
});
}
_handleUpdateInheritFrom(e) {
if (!this._inheritsFrom) {
this._inheritsFrom = {};
}
this._inheritsFrom.id = e.detail.value;
this._inheritsFrom.name = this._inheritFromFilter;
this._handleAccessModified();
}
_getInheritFromSuggestions() {
return this.$.restAPI.getRepos(
this._inheritFromFilter,
MAX_AUTOCOMPLETE_RESULTS)
.then(response => {
const projects = [];
for (const key in response) {
if (!response.hasOwnProperty(key)) { continue; }
projects.push({
name: response[key].name,
value: response[key].id,
});
}
return projects;
});
}
_computeLoadingClass(loading) {
return loading ? 'loading' : '';
}
_handleEdit() {
this._editing = !this._editing;
}
_editOrCancel(editing) {
return editing ? 'Cancel' : 'Edit';
}
_computeWebLinkClass(weblinks) {
return weblinks && weblinks.length ? 'show' : '';
}
_computeShowInherit(inheritsFrom) {
return inheritsFrom ? 'show' : '';
}
_handleAddedSectionRemoved(e) {
const index = e.model.index;
this._sections = this._sections.slice(0, index)
.concat(this._sections.slice(index + 1, this._sections.length));
}
_handleEditingChanged(editing, editingOld) {
// Ignore when editing gets set initially.
if (!editingOld || editing) { return; }
// Remove any unsaved but added refs.
if (this._sections) {
this._sections = this._sections.filter(p => !p.value.added);
}
// Restore inheritFrom.
if (this._inheritsFrom) {
this._inheritsFrom = Object.assign({}, this._originalInheritsFrom);
this._inheritFromFilter = this._inheritsFrom.name;
}
for (const key of Object.keys(this._local)) {
if (this._local[key].added) {
delete this._local[key];
}
return this.$.restAPI.setRepoAccessRights(this.repo, obj)
.then(() => {
this._reload(this.repo);
})
.finally(() => {
this._modified = false;
if (button) {
button.loading = false;
}
});
}
_handleSaveForReview(e) {
const obj = this._getObjforSave();
if (!obj) { return; }
const button = e && e.target;
if (button) {
button.loading = true;
}
return this.$.restAPI
.setRepoAccessRightsForReview(this.repo, obj)
.then(change => {
Gerrit.Nav.navigateToChange(change);
})
.finally(() => {
this._modified = false;
if (button) {
button.loading = false;
}
});
}
_computeSaveReviewBtnClass(canUpload) {
return !canUpload ? 'invisible' : '';
}
_computeSaveBtnClass(ownerOf) {
return ownerOf && ownerOf.length === 0 ? 'invisible' : '';
}
_computeMainClass(ownerOf, canUpload, editing) {
const classList = [];
if (ownerOf && ownerOf.length > 0 || canUpload) {
classList.push('admin');
}
if (editing) {
classList.push('editing');
}
return classList.join(' ');
}
_computeParentHref(repoName) {
return this.getBaseUrl() +
`/admin/repos/${this.encodeURL(repoName, true)},access`;
}
}
customElements.define(GrRepoAccess.is, GrRepoAccess);
})();
/**
* @param {!Defs.projectAccessInput} addRemoveObj
* @param {!Array} path
* @param {string} type add or remove
* @param {!Object=} opt_value value to add if the type is 'add'
* @return {!Defs.projectAccessInput}
*/
_updateAddRemoveObj(addRemoveObj, path, type, opt_value) {
let curPos = addRemoveObj[type];
for (const item of path) {
if (!curPos[item]) {
if (item === path[path.length - 1] && type === 'remove') {
if (path[path.length - 2] === 'permissions') {
curPos[item] = {rules: {}};
} else if (path.length === 1) {
curPos[item] = {permissions: {}};
} else {
curPos[item] = {};
}
} else if (item === path[path.length - 1] && type === 'add') {
curPos[item] = opt_value;
} else {
curPos[item] = {};
}
}
curPos = curPos[item];
}
return addRemoveObj;
}
/**
* Used to recursively remove any objects with a 'deleted' bit.
*/
_recursivelyRemoveDeleted(obj) {
for (const k in obj) {
if (!obj.hasOwnProperty(k)) { continue; }
if (typeof obj[k] == 'object') {
if (obj[k].deleted) {
delete obj[k];
return;
}
this._recursivelyRemoveDeleted(obj[k]);
}
}
}
_recursivelyUpdateAddRemoveObj(obj, addRemoveObj, path = []) {
for (const k in obj) {
if (!obj.hasOwnProperty(k)) { continue; }
if (typeof obj[k] == 'object') {
const updatedId = obj[k].updatedId;
const ref = updatedId ? updatedId : k;
if (obj[k].deleted) {
this._updateAddRemoveObj(addRemoveObj,
path.concat(k), 'remove');
continue;
} else if (obj[k].modified) {
this._updateAddRemoveObj(addRemoveObj,
path.concat(k), 'remove');
this._updateAddRemoveObj(addRemoveObj, path.concat(ref), 'add',
obj[k]);
/* Special case for ref changes because they need to be added and
removed in a different way. The new ref needs to include all
changes but also the initial state. To do this, instead of
continuing with the same recursion, just remove anything that is
deleted in the current state. */
if (updatedId && updatedId !== k) {
this._recursivelyRemoveDeleted(addRemoveObj.add[updatedId]);
}
continue;
} else if (obj[k].added) {
this._updateAddRemoveObj(addRemoveObj,
path.concat(ref), 'add', obj[k]);
/**
* As add / delete both can happen in the new section,
* so here to make sure it will remove the deleted ones.
*
* @see Issue 11339
*/
this._recursivelyRemoveDeleted(addRemoveObj.add[k]);
continue;
}
this._recursivelyUpdateAddRemoveObj(obj[k], addRemoveObj,
path.concat(k));
}
}
}
/**
* Returns an object formatted for saving or submitting access changes for
* review
*
* @return {!Defs.projectAccessInput}
*/
_computeAddAndRemove() {
const addRemoveObj = {
add: {},
remove: {},
};
const originalInheritsFromId = this._originalInheritsFrom ?
this.singleDecodeURL(this._originalInheritsFrom.id) :
null;
const inheritsFromId = this._inheritsFrom ?
this.singleDecodeURL(this._inheritsFrom.id) :
null;
const inheritFromChanged =
// Inherit from changed
(originalInheritsFromId &&
originalInheritsFromId !== inheritsFromId) ||
// Inherit from added (did not have one initially);
(!originalInheritsFromId && inheritsFromId);
this._recursivelyUpdateAddRemoveObj(this._local, addRemoveObj);
if (inheritFromChanged) {
addRemoveObj.parent = inheritsFromId;
}
return addRemoveObj;
}
_handleCreateSection() {
let newRef = 'refs/for/*';
// Avoid using an already used key for the placeholder, since it
// immediately gets added to an object.
while (this._local[newRef]) {
newRef = `${newRef}*`;
}
const section = {permissions: {}, added: true};
this.push('_sections', {id: newRef, value: section});
this.set(['_local', newRef], section);
flush();
dom(this.root).querySelector('gr-access-section:last-of-type')
.editReference();
}
_getObjforSave() {
const addRemoveObj = this._computeAddAndRemove();
// If there are no changes, don't actually save.
if (!Object.keys(addRemoveObj.add).length &&
!Object.keys(addRemoveObj.remove).length &&
!addRemoveObj.parent) {
this.dispatchEvent(new CustomEvent('show-alert', {
detail: {message: NOTHING_TO_SAVE},
bubbles: true,
composed: true,
}));
return;
}
const obj = {
add: addRemoveObj.add,
remove: addRemoveObj.remove,
};
if (addRemoveObj.parent) {
obj.parent = addRemoveObj.parent;
}
return obj;
}
_handleSave(e) {
const obj = this._getObjforSave();
if (!obj) { return; }
const button = e && e.target;
if (button) {
button.loading = true;
}
return this.$.restAPI.setRepoAccessRights(this.repo, obj)
.then(() => {
this._reload(this.repo);
})
.finally(() => {
this._modified = false;
if (button) {
button.loading = false;
}
});
}
_handleSaveForReview(e) {
const obj = this._getObjforSave();
if (!obj) { return; }
const button = e && e.target;
if (button) {
button.loading = true;
}
return this.$.restAPI
.setRepoAccessRightsForReview(this.repo, obj)
.then(change => {
Gerrit.Nav.navigateToChange(change);
})
.finally(() => {
this._modified = false;
if (button) {
button.loading = false;
}
});
}
_computeSaveReviewBtnClass(canUpload) {
return !canUpload ? 'invisible' : '';
}
_computeSaveBtnClass(ownerOf) {
return ownerOf && ownerOf.length === 0 ? 'invisible' : '';
}
_computeMainClass(ownerOf, canUpload, editing) {
const classList = [];
if (ownerOf && ownerOf.length > 0 || canUpload) {
classList.push('admin');
}
if (editing) {
classList.push('editing');
}
return classList.join(' ');
}
_computeParentHref(repoName) {
return this.getBaseUrl() +
`/admin/repos/${this.encodeURL(repoName, true)},access`;
}
}
customElements.define(GrRepoAccess.is, GrRepoAccess);

View File

@ -1,38 +1,22 @@
<!--
@license
Copyright (C) 2017 The Android Open Source Project
/**
* @license
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {html} from '@polymer/polymer/lib/utils/html-tag.js';
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<link rel="import" href="/bower_components/polymer/polymer.html">
<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
<link rel="import" href="../../../behaviors/gr-access-behavior/gr-access-behavior.html">
<link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html">
<link rel="import" href="../../../styles/gr-menu-page-styles.html">
<link rel="import" href="../../../styles/gr-subpage-styles.html">
<link rel="import" href="../../../styles/shared-styles.html">
<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
<link rel="import" href="../gr-access-section/gr-access-section.html">
<script src="../../../scripts/util.js"></script>
<dom-module id="gr-repo-access">
<template>
export const htmlTemplate = html`
<style include="shared-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
@ -73,25 +57,18 @@ limitations under the License.
<style include="gr-menu-page-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
<main class$="[[_computeMainClass(_ownerOf, _canUpload, _editing)]]">
<div id="loading" class$="[[_computeLoadingClass(_loading)]]">
<main class\$="[[_computeMainClass(_ownerOf, _canUpload, _editing)]]">
<div id="loading" class\$="[[_computeLoadingClass(_loading)]]">
Loading...
</div>
<div id="loadedContent" class$="[[_computeLoadingClass(_loading)]]">
<h3 id="inheritsFrom" class$="[[_computeShowInherit(_inheritsFrom)]]">
<div id="loadedContent" class\$="[[_computeLoadingClass(_loading)]]">
<h3 id="inheritsFrom" class\$="[[_computeShowInherit(_inheritsFrom)]]">
<span class="rightsText">Rights Inherit From</span>
<a
href$="[[_computeParentHref(_inheritsFrom.name)]]"
rel="noopener"
id="inheritFromName">
<a href\$="[[_computeParentHref(_inheritsFrom.name)]]" rel="noopener" id="inheritFromName">
[[_inheritsFrom.name]]</a>
<gr-autocomplete
id="editInheritFromInput"
text="{{_inheritFromFilter}}"
query="[[_query]]"
on-commit="_handleUpdateInheritFrom"></gr-autocomplete>
<gr-autocomplete id="editInheritFromInput" text="{{_inheritFromFilter}}" query="[[_query]]" on-commit="_handleUpdateInheritFrom"></gr-autocomplete>
</h3>
<div class$="weblinks [[_computeWebLinkClass(_weblinks)]]">
<div class\$="weblinks [[_computeWebLinkClass(_weblinks)]]">
History:
<template is="dom-repeat" items="[[_weblinks]]" as="link">
<a href="[[link.url]]" class="weblink" rel="noopener" target="[[link.target]]">
@ -99,41 +76,16 @@ limitations under the License.
</a>
</template>
</div>
<gr-button id="editBtn"
on-click="_handleEdit">[[_editOrCancel(_editing)]]</gr-button>
<gr-button id="saveBtn"
primary
class$="[[_computeSaveBtnClass(_ownerOf)]]"
on-click="_handleSave"
disabled="[[!_modified]]">Save</gr-button>
<gr-button id="saveReviewBtn"
primary
class$="[[_computeSaveReviewBtnClass(_canUpload)]]"
on-click="_handleSaveForReview"
disabled="[[!_modified]]">Save for review</gr-button>
<template
is="dom-repeat"
items="{{_sections}}"
initial-count="5"
target-framerate="60"
as="section">
<gr-access-section
capabilities="[[_capabilities]]"
section="{{section}}"
labels="[[_labels]]"
can-upload="[[_canUpload]]"
editing="[[_editing]]"
owner-of="[[_ownerOf]]"
groups="[[_groups]]"
on-added-section-removed="_handleAddedSectionRemoved"></gr-access-section>
<gr-button id="editBtn" on-click="_handleEdit">[[_editOrCancel(_editing)]]</gr-button>
<gr-button id="saveBtn" primary="" class\$="[[_computeSaveBtnClass(_ownerOf)]]" on-click="_handleSave" disabled="[[!_modified]]">Save</gr-button>
<gr-button id="saveReviewBtn" primary="" class\$="[[_computeSaveReviewBtnClass(_canUpload)]]" on-click="_handleSaveForReview" disabled="[[!_modified]]">Save for review</gr-button>
<template is="dom-repeat" items="{{_sections}}" initial-count="5" target-framerate="60" as="section">
<gr-access-section capabilities="[[_capabilities]]" section="{{section}}" labels="[[_labels]]" can-upload="[[_canUpload]]" editing="[[_editing]]" owner-of="[[_ownerOf]]" groups="[[_groups]]" on-added-section-removed="_handleAddedSectionRemoved"></gr-access-section>
</template>
<div class="referenceContainer">
<gr-button id="addReferenceBtn"
on-click="_handleCreateSection">Add Reference</gr-button>
<gr-button id="addReferenceBtn" on-click="_handleCreateSection">Add Reference</gr-button>
</div>
</div>
</main>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
</template>
<script src="gr-repo-access.js"></script>
</dom-module>
`;

View File

@ -14,34 +14,41 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function() {
'use strict';
import '../../../scripts/bundled-polymer.js';
/** @extends Polymer.Element */
class GrRepoCommand extends Polymer.GestureEventListeners(
Polymer.LegacyElementMixin(
Polymer.Element)) {
static get is() { return 'gr-repo-command'; }
import '../../../styles/shared-styles.js';
import '../../shared/gr-button/gr-button.js';
import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
import {PolymerElement} from '@polymer/polymer/polymer-element.js';
import {htmlTemplate} from './gr-repo-command_html.js';
static get properties() {
return {
title: String,
disabled: Boolean,
tooltip: String,
};
}
/** @extends Polymer.Element */
class GrRepoCommand extends GestureEventListeners(
LegacyElementMixin(
PolymerElement)) {
static get template() { return htmlTemplate; }
/**
* Fired when command button is tapped.
*
* @event command-tap
*/
static get is() { return 'gr-repo-command'; }
_onCommandTap() {
this.dispatchEvent(
new CustomEvent('command-tap', {bubbles: true, composed: true}));
}
static get properties() {
return {
title: String,
disabled: Boolean,
tooltip: String,
};
}
customElements.define(GrRepoCommand.is, GrRepoCommand);
})();
/**
* Fired when command button is tapped.
*
* @event command-tap
*/
_onCommandTap() {
this.dispatchEvent(
new CustomEvent('command-tap', {bubbles: true, composed: true}));
}
}
customElements.define(GrRepoCommand.is, GrRepoCommand);

View File

@ -1,25 +1,22 @@
<!--
@license
Copyright (C) 2017 The Android Open Source Project
/**
* @license
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {html} from '@polymer/polymer/lib/utils/html-tag.js';
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<link rel="import" href="/bower_components/polymer/polymer.html">
<link rel="import" href="../../../styles/shared-styles.html">
<link rel="import" href="../../shared/gr-button/gr-button.html">
<dom-module id="gr-repo-command">
<template>
export const htmlTemplate = html`
<style include="shared-styles">
:host {
display: block;
@ -27,13 +24,7 @@ limitations under the License.
}
</style>
<h3>[[title]]</h3>
<gr-button
title$="[[tooltip]]"
disabled$="[[disabled]]"
on-click
="_onCommandTap">
<gr-button title\$="[[tooltip]]" disabled\$="[[disabled]]" on-click="_onCommandTap">
[[title]]
</gr-button>
</template>
<script src="gr-repo-command.js"></script>
</dom-module>
`;

View File

@ -19,15 +19,20 @@ limitations under the License.
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-repo-command</title>
<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/bower_components/web-component-tester/browser.js"></script>
<script src="../../../test/test-pre-setup.js"></script>
<link rel="import" href="../../../test/common-test-setup.html"/>
<link rel="import" href="gr-repo-command.html">
<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/components/wct-browser-legacy/browser.js"></script>
<script type="module" src="../../../test/test-pre-setup.js"></script>
<script type="module" src="../../../test/common-test-setup.js"></script>
<script type="module" src="./gr-repo-command.js"></script>
<script>void(0);</script>
<script type="module">
import '../../../test/test-pre-setup.js';
import '../../../test/common-test-setup.js';
import './gr-repo-command.js';
void(0);
</script>
<test-fixture id="basic">
<template>
@ -35,21 +40,24 @@ limitations under the License.
</template>
</test-fixture>
<script>
suite('gr-repo-command tests', async () => {
await readyToTest();
let element;
<script type="module">
import '../../../test/test-pre-setup.js';
import '../../../test/common-test-setup.js';
import './gr-repo-command.js';
import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
suite('gr-repo-command tests', () => {
let element;
setup(() => {
element = fixture('basic');
});
test('dispatched command-tap on button tap', done => {
element.addEventListener('command-tap', () => {
done();
});
MockInteractions.tap(
Polymer.dom(element.root).querySelector('gr-button'));
});
setup(() => {
element = fixture('basic');
});
test('dispatched command-tap on button tap', done => {
element.addEventListener('command-tap', () => {
done();
});
MockInteractions.tap(
dom(element.root).querySelector('gr-button'));
});
});
</script>

View File

@ -14,113 +14,131 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function() {
'use strict';
import '../../../scripts/bundled-polymer.js';
const GC_MESSAGE = 'Garbage collection completed successfully.';
import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
import '../../../behaviors/fire-behavior/fire-behavior.js';
import '../../../styles/gr-form-styles.js';
import '../../../styles/gr-subpage-styles.js';
import '../../../styles/shared-styles.js';
import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.js';
import '../../plugins/gr-endpoint-param/gr-endpoint-param.js';
import '../../shared/gr-dialog/gr-dialog.js';
import '../../shared/gr-overlay/gr-overlay.js';
import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
import '../gr-create-change-dialog/gr-create-change-dialog.js';
import '../gr-repo-command/gr-repo-command.js';
import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
import {PolymerElement} from '@polymer/polymer/polymer-element.js';
import {htmlTemplate} from './gr-repo-commands_html.js';
const CONFIG_BRANCH = 'refs/meta/config';
const CONFIG_PATH = 'project.config';
const EDIT_CONFIG_SUBJECT = 'Edit Repo Config';
const INITIAL_PATCHSET = 1;
const CREATE_CHANGE_FAILED_MESSAGE = 'Failed to create change.';
const CREATE_CHANGE_SUCCEEDED_MESSAGE = 'Navigating to change';
const GC_MESSAGE = 'Garbage collection completed successfully.';
/**
* @appliesMixin Gerrit.FireMixin
* @extends Polymer.Element
*/
class GrRepoCommands extends Polymer.mixinBehaviors( [
Gerrit.FireBehavior,
], Polymer.GestureEventListeners(
Polymer.LegacyElementMixin(
Polymer.Element))) {
static get is() { return 'gr-repo-commands'; }
const CONFIG_BRANCH = 'refs/meta/config';
const CONFIG_PATH = 'project.config';
const EDIT_CONFIG_SUBJECT = 'Edit Repo Config';
const INITIAL_PATCHSET = 1;
const CREATE_CHANGE_FAILED_MESSAGE = 'Failed to create change.';
const CREATE_CHANGE_SUCCEEDED_MESSAGE = 'Navigating to change';
static get properties() {
return {
params: Object,
repo: String,
_loading: {
type: Boolean,
value: true,
},
/** @type {?} */
_repoConfig: Object,
_canCreate: Boolean,
};
}
/**
* @appliesMixin Gerrit.FireMixin
* @extends Polymer.Element
*/
class GrRepoCommands extends mixinBehaviors( [
Gerrit.FireBehavior,
], GestureEventListeners(
LegacyElementMixin(
PolymerElement))) {
static get template() { return htmlTemplate; }
/** @override */
attached() {
super.attached();
this._loadRepo();
static get is() { return 'gr-repo-commands'; }
this.fire('title-change', {title: 'Repo Commands'});
}
_loadRepo() {
if (!this.repo) { return Promise.resolve(); }
const errFn = response => {
this.fire('page-error', {response});
};
return this.$.restAPI.getProjectConfig(this.repo, errFn)
.then(config => {
if (!config) { return Promise.resolve(); }
this._repoConfig = config;
this._loading = false;
});
}
_computeLoadingClass(loading) {
return loading ? 'loading' : '';
}
_isLoading() {
return this._loading || this._loading === undefined;
}
_handleRunningGC() {
return this.$.restAPI.runRepoGC(this.repo).then(response => {
if (response.status === 200) {
this.dispatchEvent(new CustomEvent(
'show-alert',
{detail: {message: GC_MESSAGE}, bubbles: true, composed: true}));
}
});
}
_createNewChange() {
this.$.createChangeOverlay.open();
}
_handleCreateChange() {
this.$.createNewChangeModal.handleCreateChange();
this._handleCloseCreateChange();
}
_handleCloseCreateChange() {
this.$.createChangeOverlay.close();
}
_handleEditRepoConfig() {
return this.$.restAPI.createChange(this.repo, CONFIG_BRANCH,
EDIT_CONFIG_SUBJECT, undefined, false, true).then(change => {
const message = change ?
CREATE_CHANGE_SUCCEEDED_MESSAGE :
CREATE_CHANGE_FAILED_MESSAGE;
this.dispatchEvent(new CustomEvent('show-alert',
{detail: {message}, bubbles: true, composed: true}));
if (!change) { return; }
Gerrit.Nav.navigateToRelativeUrl(Gerrit.Nav.getEditUrlForDiff(
change, CONFIG_PATH, INITIAL_PATCHSET));
});
}
static get properties() {
return {
params: Object,
repo: String,
_loading: {
type: Boolean,
value: true,
},
/** @type {?} */
_repoConfig: Object,
_canCreate: Boolean,
};
}
customElements.define(GrRepoCommands.is, GrRepoCommands);
})();
/** @override */
attached() {
super.attached();
this._loadRepo();
this.fire('title-change', {title: 'Repo Commands'});
}
_loadRepo() {
if (!this.repo) { return Promise.resolve(); }
const errFn = response => {
this.fire('page-error', {response});
};
return this.$.restAPI.getProjectConfig(this.repo, errFn)
.then(config => {
if (!config) { return Promise.resolve(); }
this._repoConfig = config;
this._loading = false;
});
}
_computeLoadingClass(loading) {
return loading ? 'loading' : '';
}
_isLoading() {
return this._loading || this._loading === undefined;
}
_handleRunningGC() {
return this.$.restAPI.runRepoGC(this.repo).then(response => {
if (response.status === 200) {
this.dispatchEvent(new CustomEvent(
'show-alert',
{detail: {message: GC_MESSAGE}, bubbles: true, composed: true}));
}
});
}
_createNewChange() {
this.$.createChangeOverlay.open();
}
_handleCreateChange() {
this.$.createNewChangeModal.handleCreateChange();
this._handleCloseCreateChange();
}
_handleCloseCreateChange() {
this.$.createChangeOverlay.close();
}
_handleEditRepoConfig() {
return this.$.restAPI.createChange(this.repo, CONFIG_BRANCH,
EDIT_CONFIG_SUBJECT, undefined, false, true).then(change => {
const message = change ?
CREATE_CHANGE_SUCCEEDED_MESSAGE :
CREATE_CHANGE_FAILED_MESSAGE;
this.dispatchEvent(new CustomEvent('show-alert',
{detail: {message}, bubbles: true, composed: true}));
if (!change) { return; }
Gerrit.Nav.navigateToRelativeUrl(Gerrit.Nav.getEditUrlForDiff(
change, CONFIG_PATH, INITIAL_PATCHSET));
});
}
}
customElements.define(GrRepoCommands.is, GrRepoCommands);

View File

@ -1,36 +1,22 @@
<!--
@license
Copyright (C) 2017 The Android Open Source Project
/**
* @license
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {html} from '@polymer/polymer/lib/utils/html-tag.js';
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<link rel="import" href="/bower_components/polymer/polymer.html">
<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
<link rel="import" href="../../../styles/gr-form-styles.html">
<link rel="import" href="../../../styles/gr-subpage-styles.html">
<link rel="import" href="../../../styles/shared-styles.html">
<link rel="import" href="../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.html">
<link rel="import" href="../../plugins/gr-endpoint-param/gr-endpoint-param.html">
<link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
<link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
<link rel="import" href="../gr-create-change-dialog/gr-create-change-dialog.html">
<link rel="import" href="../gr-repo-command/gr-repo-command.html">
<dom-module id="gr-repo-commands">
<template>
export const htmlTemplate = html`
<style include="shared-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
@ -42,24 +28,15 @@ limitations under the License.
</style>
<main class="gr-form-styles read-only">
<h1 id="Title">Repository Commands</h1>
<div id="loading" class$="[[_computeLoadingClass(_loading)]]">Loading...</div>
<div id="loadedContent" class$="[[_computeLoadingClass(_loading)]]">
<div id="loading" class\$="[[_computeLoadingClass(_loading)]]">Loading...</div>
<div id="loadedContent" class\$="[[_computeLoadingClass(_loading)]]">
<h2 id="options">Command</h2>
<div id="form">
<gr-repo-command
title="Create change"
on-command-tap="_createNewChange">
<gr-repo-command title="Create change" on-command-tap="_createNewChange">
</gr-repo-command>
<gr-repo-command
id="editRepoConfig"
title="Edit repo config"
on-command-tap="_handleEditRepoConfig">
<gr-repo-command id="editRepoConfig" title="Edit repo config" on-command-tap="_handleEditRepoConfig">
</gr-repo-command>
<gr-repo-command
title="[[_repoConfig.actions.gc.label]]"
tooltip="[[_repoConfig.actions.gc.title]]"
hidden$="[[!_repoConfig.actions.gc.enabled]]"
on-command-tap="_handleRunningGC">
<gr-repo-command title="[[_repoConfig.actions.gc.label]]" tooltip="[[_repoConfig.actions.gc.title]]" hidden\$="[[!_repoConfig.actions.gc.enabled]]" on-command-tap="_handleRunningGC">
</gr-repo-command>
<gr-endpoint-decorator name="repo-command">
<gr-endpoint-param name="config" value="[[_repoConfig]]">
@ -70,25 +47,15 @@ limitations under the License.
</div>
</div>
</main>
<gr-overlay id="createChangeOverlay" with-backdrop>
<gr-dialog
id="createChangeDialog"
confirm-label="Create"
disabled="[[!_canCreate]]"
on-confirm="_handleCreateChange"
on-cancel="_handleCloseCreateChange">
<gr-overlay id="createChangeOverlay" with-backdrop="">
<gr-dialog id="createChangeDialog" confirm-label="Create" disabled="[[!_canCreate]]" on-confirm="_handleCreateChange" on-cancel="_handleCloseCreateChange">
<div class="header" slot="header">
Create Change
</div>
<div class="main" slot="main">
<gr-create-change-dialog
id="createNewChangeModal"
can-create="{{_canCreate}}"
repo-name="[[repo]]"></gr-create-change-dialog>
<gr-create-change-dialog id="createNewChangeModal" can-create="{{_canCreate}}" repo-name="[[repo]]"></gr-create-change-dialog>
</div>
</gr-dialog>
</gr-overlay>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
</template>
<script src="gr-repo-commands.js"></script>
</dom-module>
`;

View File

@ -19,16 +19,21 @@ limitations under the License.
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-repo-commands</title>
<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/bower_components/page/page.js"></script>
<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/bower_components/web-component-tester/browser.js"></script>
<script src="../../../test/test-pre-setup.js"></script>
<link rel="import" href="../../../test/common-test-setup.html"/>
<link rel="import" href="gr-repo-commands.html">
<script src="/node_modules/page/page.js"></script>
<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/components/wct-browser-legacy/browser.js"></script>
<script type="module" src="../../../test/test-pre-setup.js"></script>
<script type="module" src="../../../test/common-test-setup.js"></script>
<script type="module" src="./gr-repo-commands.js"></script>
<script>void(0);</script>
<script type="module">
import '../../../test/test-pre-setup.js';
import '../../../test/common-test-setup.js';
import './gr-repo-commands.js';
void(0);
</script>
<test-fixture id="basic">
<template>
@ -36,111 +41,113 @@ limitations under the License.
</template>
</test-fixture>
<script>
suite('gr-repo-commands tests', async () => {
await readyToTest();
let element;
let sandbox;
let repoStub;
<script type="module">
import '../../../test/test-pre-setup.js';
import '../../../test/common-test-setup.js';
import './gr-repo-commands.js';
suite('gr-repo-commands tests', () => {
let element;
let sandbox;
let repoStub;
setup(() => {
sandbox = sinon.sandbox.create();
element = fixture('basic');
repoStub = sandbox.stub(
element.$.restAPI,
'getProjectConfig',
() => Promise.resolve({}));
});
teardown(() => {
sandbox.restore();
});
suite('create new change dialog', () => {
test('_createNewChange opens modal', () => {
const openStub = sandbox.stub(element.$.createChangeOverlay, 'open');
element._createNewChange();
assert.isTrue(openStub.called);
});
test('_handleCreateChange called when confirm fired', () => {
sandbox.stub(element, '_handleCreateChange');
element.$.createChangeDialog.fire('confirm');
assert.isTrue(element._handleCreateChange.called);
});
test('_handleCloseCreateChange called when cancel fired', () => {
sandbox.stub(element, '_handleCloseCreateChange');
element.$.createChangeDialog.fire('cancel');
assert.isTrue(element._handleCloseCreateChange.called);
});
});
suite('edit repo config', () => {
let createChangeStub;
let urlStub;
let handleSpy;
let alertStub;
setup(() => {
sandbox = sinon.sandbox.create();
element = fixture('basic');
repoStub = sandbox.stub(
element.$.restAPI,
'getProjectConfig',
() => Promise.resolve({}));
createChangeStub = sandbox.stub(element.$.restAPI, 'createChange');
urlStub = sandbox.stub(Gerrit.Nav, 'getEditUrlForDiff');
sandbox.stub(Gerrit.Nav, 'navigateToRelativeUrl');
handleSpy = sandbox.spy(element, '_handleEditRepoConfig');
alertStub = sandbox.stub();
element.addEventListener('show-alert', alertStub);
});
teardown(() => {
sandbox.restore();
});
test('successful creation of change', () => {
const change = {_number: '1'};
createChangeStub.returns(Promise.resolve(change));
MockInteractions.tap(element.$.editRepoConfig.shadowRoot
.querySelector('gr-button'));
return handleSpy.lastCall.returnValue.then(() => {
flushAsynchronousOperations();
suite('create new change dialog', () => {
test('_createNewChange opens modal', () => {
const openStub = sandbox.stub(element.$.createChangeOverlay, 'open');
element._createNewChange();
assert.isTrue(openStub.called);
});
test('_handleCreateChange called when confirm fired', () => {
sandbox.stub(element, '_handleCreateChange');
element.$.createChangeDialog.fire('confirm');
assert.isTrue(element._handleCreateChange.called);
});
test('_handleCloseCreateChange called when cancel fired', () => {
sandbox.stub(element, '_handleCloseCreateChange');
element.$.createChangeDialog.fire('cancel');
assert.isTrue(element._handleCloseCreateChange.called);
assert.isTrue(alertStub.called);
assert.equal(alertStub.lastCall.args[0].detail.message,
'Navigating to change');
assert.isTrue(urlStub.called);
assert.deepEqual(urlStub.lastCall.args,
[change, 'project.config', 1]);
});
});
suite('edit repo config', () => {
let createChangeStub;
let urlStub;
let handleSpy;
let alertStub;
test('unsuccessful creation of change', () => {
createChangeStub.returns(Promise.resolve(null));
MockInteractions.tap(element.$.editRepoConfig.shadowRoot
.querySelector('gr-button'));
return handleSpy.lastCall.returnValue.then(() => {
flushAsynchronousOperations();
setup(() => {
createChangeStub = sandbox.stub(element.$.restAPI, 'createChange');
urlStub = sandbox.stub(Gerrit.Nav, 'getEditUrlForDiff');
sandbox.stub(Gerrit.Nav, 'navigateToRelativeUrl');
handleSpy = sandbox.spy(element, '_handleEditRepoConfig');
alertStub = sandbox.stub();
element.addEventListener('show-alert', alertStub);
});
test('successful creation of change', () => {
const change = {_number: '1'};
createChangeStub.returns(Promise.resolve(change));
MockInteractions.tap(element.$.editRepoConfig.shadowRoot
.querySelector('gr-button'));
return handleSpy.lastCall.returnValue.then(() => {
flushAsynchronousOperations();
assert.isTrue(alertStub.called);
assert.equal(alertStub.lastCall.args[0].detail.message,
'Navigating to change');
assert.isTrue(urlStub.called);
assert.deepEqual(urlStub.lastCall.args,
[change, 'project.config', 1]);
});
});
test('unsuccessful creation of change', () => {
createChangeStub.returns(Promise.resolve(null));
MockInteractions.tap(element.$.editRepoConfig.shadowRoot
.querySelector('gr-button'));
return handleSpy.lastCall.returnValue.then(() => {
flushAsynchronousOperations();
assert.isTrue(alertStub.called);
assert.equal(alertStub.lastCall.args[0].detail.message,
'Failed to create change.');
assert.isFalse(urlStub.called);
});
});
});
suite('404', () => {
test('fires page-error', done => {
repoStub.restore();
element.repo = 'test';
const response = {status: 404};
sandbox.stub(
element.$.restAPI, 'getProjectConfig', (repo, errFn) => {
errFn(response);
});
element.addEventListener('page-error', e => {
assert.deepEqual(e.detail.response, response);
done();
});
element._loadRepo();
assert.isTrue(alertStub.called);
assert.equal(alertStub.lastCall.args[0].detail.message,
'Failed to create change.');
assert.isFalse(urlStub.called);
});
});
});
suite('404', () => {
test('fires page-error', done => {
repoStub.restore();
element.repo = 'test';
const response = {status: 404};
sandbox.stub(
element.$.restAPI, 'getProjectConfig', (repo, errFn) => {
errFn(response);
});
element.addEventListener('page-error', e => {
assert.deepEqual(e.detail.response, response);
done();
});
element._loadRepo();
});
});
});
</script>

View File

@ -1,6 +1,6 @@
/**
* @license
* Copyright (C) 2017 The Android Open Source Project
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -14,89 +14,100 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function() {
'use strict';
import '../../../scripts/bundled-polymer.js';
/**
* @appliesMixin Gerrit.FireMixin
* @extends Polymer.Element
*/
class GrRepoDashboards extends Polymer.mixinBehaviors( [
Gerrit.FireBehavior,
], Polymer.GestureEventListeners(
Polymer.LegacyElementMixin(
Polymer.Element))) {
static get is() { return 'gr-repo-dashboards'; }
import '../../../behaviors/fire-behavior/fire-behavior.js';
import '../../../styles/shared-styles.js';
import '../../core/gr-navigation/gr-navigation.js';
import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
import {flush} from '@polymer/polymer/lib/legacy/polymer.dom.js';
import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
import {PolymerElement} from '@polymer/polymer/polymer-element.js';
import {htmlTemplate} from './gr-repo-dashboards_html.js';
static get properties() {
return {
repo: {
type: String,
observer: '_repoChanged',
},
_loading: {
type: Boolean,
value: true,
},
_dashboards: Array,
};
}
/**
* @appliesMixin Gerrit.FireMixin
* @extends Polymer.Element
*/
class GrRepoDashboards extends mixinBehaviors( [
Gerrit.FireBehavior,
], GestureEventListeners(
LegacyElementMixin(
PolymerElement))) {
static get template() { return htmlTemplate; }
_repoChanged(repo) {
this._loading = true;
if (!repo) { return Promise.resolve(); }
static get is() { return 'gr-repo-dashboards'; }
const errFn = response => {
this.fire('page-error', {response});
};
this.$.restAPI.getRepoDashboards(this.repo, errFn).then(res => {
if (!res) { return Promise.resolve(); }
// Group by ref and sort by id.
const dashboards = res.concat.apply([], res).sort((a, b) =>
(a.id < b.id ? -1 : 1));
const dashboardsByRef = {};
dashboards.forEach(d => {
if (!dashboardsByRef[d.ref]) {
dashboardsByRef[d.ref] = [];
}
dashboardsByRef[d.ref].push(d);
});
const dashboardBuilder = [];
Object.keys(dashboardsByRef).sort()
.forEach(ref => {
dashboardBuilder.push({
section: ref,
dashboards: dashboardsByRef[ref],
});
});
this._dashboards = dashboardBuilder;
this._loading = false;
Polymer.dom.flush();
});
}
_getUrl(project, id) {
if (!project || !id) { return ''; }
return Gerrit.Nav.getUrlForRepoDashboard(project, id);
}
_computeLoadingClass(loading) {
return loading ? 'loading' : '';
}
_computeInheritedFrom(project, definingProject) {
return project === definingProject ? '' : definingProject;
}
_computeIsDefault(isDefault) {
return isDefault ? '✓' : '';
}
static get properties() {
return {
repo: {
type: String,
observer: '_repoChanged',
},
_loading: {
type: Boolean,
value: true,
},
_dashboards: Array,
};
}
customElements.define(GrRepoDashboards.is, GrRepoDashboards);
})();
_repoChanged(repo) {
this._loading = true;
if (!repo) { return Promise.resolve(); }
const errFn = response => {
this.fire('page-error', {response});
};
this.$.restAPI.getRepoDashboards(this.repo, errFn).then(res => {
if (!res) { return Promise.resolve(); }
// Group by ref and sort by id.
const dashboards = res.concat.apply([], res).sort((a, b) =>
(a.id < b.id ? -1 : 1));
const dashboardsByRef = {};
dashboards.forEach(d => {
if (!dashboardsByRef[d.ref]) {
dashboardsByRef[d.ref] = [];
}
dashboardsByRef[d.ref].push(d);
});
const dashboardBuilder = [];
Object.keys(dashboardsByRef).sort()
.forEach(ref => {
dashboardBuilder.push({
section: ref,
dashboards: dashboardsByRef[ref],
});
});
this._dashboards = dashboardBuilder;
this._loading = false;
flush();
});
}
_getUrl(project, id) {
if (!project || !id) { return ''; }
return Gerrit.Nav.getUrlForRepoDashboard(project, id);
}
_computeLoadingClass(loading) {
return loading ? 'loading' : '';
}
_computeInheritedFrom(project, definingProject) {
return project === definingProject ? '' : definingProject;
}
_computeIsDefault(isDefault) {
return isDefault ? '✓' : '';
}
}
customElements.define(GrRepoDashboards.is, GrRepoDashboards);

View File

@ -1,27 +1,22 @@
<!--
@license
Copyright (C) 2018 The Android Open Source Project
/**
* @license
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {html} from '@polymer/polymer/lib/utils/html-tag.js';
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<link rel="import" href="/bower_components/polymer/polymer.html">
<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
<link rel="import" href="../../../styles/shared-styles.html">
<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
<dom-module id="gr-repo-dashboards">
<template>
export const htmlTemplate = html`
<style include="shared-styles">
:host {
display: block;
@ -38,8 +33,8 @@ limitations under the License.
<style include="gr-table-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
<table id="list" class$="genericList [[_computeLoadingClass(_loading)]]">
<tr class="headerRow">
<table id="list" class\$="genericList [[_computeLoadingClass(_loading)]]">
<tbody><tr class="headerRow">
<th class="topHeader">Dashboard name</th>
<th class="topHeader">Dashboard title</th>
<th class="topHeader">Dashboard description</th>
@ -49,14 +44,14 @@ limitations under the License.
<tr id="loadingContainer">
<td>Loading...</td>
</tr>
<tbody id="dashboards">
</tbody><tbody id="dashboards">
<template is="dom-repeat" items="[[_dashboards]]">
<tr class="groupHeader">
<td colspan="5">[[item.section]]</td>
</tr>
<template is="dom-repeat" items="[[item.dashboards]]">
<tr class="table">
<td class="name"><a href$="[[_getUrl(item.project, item.id)]]">[[item.path]]</a></td>
<td class="name"><a href\$="[[_getUrl(item.project, item.id)]]">[[item.path]]</a></td>
<td class="title">[[item.title]]</td>
<td class="desc">[[item.description]]</td>
<td class="inherited">[[_computeInheritedFrom(item.project, item.defining_project)]]</td>
@ -67,6 +62,4 @@ limitations under the License.
</tbody>
</table>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
</template>
<script src="gr-repo-dashboards.js"></script>
</dom-module>
`;

View File

@ -19,15 +19,20 @@ limitations under the License.
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-repo-dashboards</title>
<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/bower_components/web-component-tester/browser.js"></script>
<script src="../../../test/test-pre-setup.js"></script>
<link rel="import" href="../../../test/common-test-setup.html"/>
<link rel="import" href="gr-repo-dashboards.html">
<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/components/wct-browser-legacy/browser.js"></script>
<script type="module" src="../../../test/test-pre-setup.js"></script>
<script type="module" src="../../../test/common-test-setup.js"></script>
<script type="module" src="./gr-repo-dashboards.js"></script>
<script>void(0);</script>
<script type="module">
import '../../../test/test-pre-setup.js';
import '../../../test/common-test-setup.js';
import './gr-repo-dashboards.js';
void(0);
</script>
<test-fixture id="basic">
<template>
@ -35,128 +40,130 @@ limitations under the License.
</template>
</test-fixture>
<script>
suite('gr-repo-dashboards tests', async () => {
await readyToTest();
let element;
let sandbox;
<script type="module">
import '../../../test/test-pre-setup.js';
import '../../../test/common-test-setup.js';
import './gr-repo-dashboards.js';
suite('gr-repo-dashboards tests', () => {
let element;
let sandbox;
setup(() => {
sandbox = sinon.sandbox.create();
element = fixture('basic');
});
teardown(() => {
sandbox.restore();
});
suite('dashboard table', () => {
setup(() => {
sandbox = sinon.sandbox.create();
element = fixture('basic');
sandbox.stub(element.$.restAPI, 'getRepoDashboards').returns(
Promise.resolve([
{
id: 'default:contributor',
project: 'gerrit',
defining_project: 'gerrit',
ref: 'default',
path: 'contributor',
description: 'Own contributions.',
foreach: 'owner:self',
url: '/dashboard/?params',
title: 'Contributor Dashboard',
sections: [
{
name: 'Mine To Rebase',
query: 'is:open -is:mergeable',
},
{
name: 'My Recently Merged',
query: 'is:merged limit:10',
},
],
},
{
id: 'custom:custom2',
project: 'gerrit',
defining_project: 'Public-Projects',
ref: 'custom',
path: 'open',
description: 'Recent open changes.',
url: '/dashboard/?params',
title: 'Open Changes',
sections: [
{
name: 'Open Changes',
query: 'status:open project:${project} -age:7w',
},
],
},
{
id: 'default:abc',
project: 'gerrit',
ref: 'default',
},
{
id: 'custom:custom1',
project: 'gerrit',
ref: 'custom',
},
]));
});
teardown(() => {
sandbox.restore();
});
suite('dashboard table', () => {
setup(() => {
sandbox.stub(element.$.restAPI, 'getRepoDashboards').returns(
Promise.resolve([
{
id: 'default:contributor',
project: 'gerrit',
defining_project: 'gerrit',
ref: 'default',
path: 'contributor',
description: 'Own contributions.',
foreach: 'owner:self',
url: '/dashboard/?params',
title: 'Contributor Dashboard',
sections: [
{
name: 'Mine To Rebase',
query: 'is:open -is:mergeable',
},
{
name: 'My Recently Merged',
query: 'is:merged limit:10',
},
],
},
{
id: 'custom:custom2',
project: 'gerrit',
defining_project: 'Public-Projects',
ref: 'custom',
path: 'open',
description: 'Recent open changes.',
url: '/dashboard/?params',
title: 'Open Changes',
sections: [
{
name: 'Open Changes',
query: 'status:open project:${project} -age:7w',
},
],
},
{
id: 'default:abc',
project: 'gerrit',
ref: 'default',
},
{
id: 'custom:custom1',
project: 'gerrit',
ref: 'custom',
},
]));
});
test('loading, sections, and ordering', done => {
assert.isTrue(element._loading);
assert.notEqual(getComputedStyle(element.$.loadingContainer).display,
test('loading, sections, and ordering', done => {
assert.isTrue(element._loading);
assert.notEqual(getComputedStyle(element.$.loadingContainer).display,
'none');
assert.equal(getComputedStyle(element.$.dashboards).display,
'none');
element.repo = 'test';
flush(() => {
assert.equal(getComputedStyle(element.$.loadingContainer).display,
'none');
assert.equal(getComputedStyle(element.$.dashboards).display,
assert.notEqual(getComputedStyle(element.$.dashboards).display,
'none');
element.repo = 'test';
flush(() => {
assert.equal(getComputedStyle(element.$.loadingContainer).display,
'none');
assert.notEqual(getComputedStyle(element.$.dashboards).display,
'none');
assert.equal(element._dashboards.length, 2);
assert.equal(element._dashboards[0].section, 'custom');
assert.equal(element._dashboards[1].section, 'default');
assert.equal(element._dashboards.length, 2);
assert.equal(element._dashboards[0].section, 'custom');
assert.equal(element._dashboards[1].section, 'default');
const dashboards = element._dashboards[0].dashboards;
assert.equal(dashboards.length, 2);
assert.equal(dashboards[0].id, 'custom:custom1');
assert.equal(dashboards[1].id, 'custom:custom2');
const dashboards = element._dashboards[0].dashboards;
assert.equal(dashboards.length, 2);
assert.equal(dashboards[0].id, 'custom:custom1');
assert.equal(dashboards[1].id, 'custom:custom2');
done();
});
});
});
suite('test url', () => {
test('_getUrl', () => {
sandbox.stub(Gerrit.Nav, 'getUrlForRepoDashboard',
() => '/r/dashboard/test');
assert.equal(element._getUrl('/dashboard/test', {}), '/r/dashboard/test');
assert.equal(element._getUrl(undefined, undefined), '');
});
});
suite('404', () => {
test('fires page-error', done => {
const response = {status: 404};
sandbox.stub(
element.$.restAPI, 'getRepoDashboards', (repo, errFn) => {
errFn(response);
});
element.addEventListener('page-error', e => {
assert.deepEqual(e.detail.response, response);
done();
});
element.repo = 'test';
done();
});
});
});
suite('test url', () => {
test('_getUrl', () => {
sandbox.stub(Gerrit.Nav, 'getUrlForRepoDashboard',
() => '/r/dashboard/test');
assert.equal(element._getUrl('/dashboard/test', {}), '/r/dashboard/test');
assert.equal(element._getUrl(undefined, undefined), '');
});
});
suite('404', () => {
test('fires page-error', done => {
const response = {status: 404};
sandbox.stub(
element.$.restAPI, 'getRepoDashboards', (repo, errFn) => {
errFn(response);
});
element.addEventListener('page-error', e => {
assert.deepEqual(e.detail.response, response);
done();
});
element.repo = 'test';
});
});
});
</script>

View File

@ -14,279 +14,302 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function() {
'use strict';
import '../../../behaviors/gr-list-view-behavior/gr-list-view-behavior.js';
const DETAIL_TYPES = {
BRANCHES: 'branches',
TAGS: 'tags',
};
import '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
import '@polymer/iron-input/iron-input.js';
import '../../../scripts/bundled-polymer.js';
import '../../../behaviors/fire-behavior/fire-behavior.js';
import '../../../styles/gr-form-styles.js';
import '../../../styles/gr-table-styles.js';
import '../../../styles/shared-styles.js';
import '../../shared/gr-account-link/gr-account-link.js';
import '../../shared/gr-button/gr-button.js';
import '../../shared/gr-date-formatter/gr-date-formatter.js';
import '../../shared/gr-dialog/gr-dialog.js';
import '../../shared/gr-list-view/gr-list-view.js';
import '../../shared/gr-overlay/gr-overlay.js';
import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
import '../gr-create-pointer-dialog/gr-create-pointer-dialog.js';
import '../gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.js';
import {flush} from '@polymer/polymer/lib/legacy/polymer.dom.js';
import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
import {PolymerElement} from '@polymer/polymer/polymer-element.js';
import {htmlTemplate} from './gr-repo-detail-list_html.js';
const PGP_START = '-----BEGIN PGP SIGNATURE-----';
const DETAIL_TYPES = {
BRANCHES: 'branches',
TAGS: 'tags',
};
/**
* @appliesMixin Gerrit.ListViewMixin
* @appliesMixin Gerrit.FireMixin
* @appliesMixin Gerrit.URLEncodingMixin
* @extends Polymer.Element
*/
class GrRepoDetailList extends Polymer.mixinBehaviors( [
Gerrit.ListViewBehavior,
Gerrit.FireBehavior,
Gerrit.URLEncodingBehavior,
], Polymer.GestureEventListeners(
Polymer.LegacyElementMixin(
Polymer.Element))) {
static get is() { return 'gr-repo-detail-list'; }
const PGP_START = '-----BEGIN PGP SIGNATURE-----';
static get properties() {
return {
/**
* @appliesMixin Gerrit.ListViewMixin
* @appliesMixin Gerrit.FireMixin
* @appliesMixin Gerrit.URLEncodingMixin
* @extends Polymer.Element
*/
class GrRepoDetailList extends mixinBehaviors( [
Gerrit.ListViewBehavior,
Gerrit.FireBehavior,
Gerrit.URLEncodingBehavior,
], GestureEventListeners(
LegacyElementMixin(
PolymerElement))) {
static get template() { return htmlTemplate; }
static get is() { return 'gr-repo-detail-list'; }
static get properties() {
return {
/**
* URL params passed from the router.
*/
params: {
type: Object,
observer: '_paramsChanged',
},
/**
* URL params passed from the router.
* The kind of detail we are displaying, possibilities are determined by
* the const DETAIL_TYPES.
*/
params: {
type: Object,
observer: '_paramsChanged',
},
/**
* The kind of detail we are displaying, possibilities are determined by
* the const DETAIL_TYPES.
*/
detailType: String,
detailType: String,
_editing: {
type: Boolean,
value: false,
},
_isOwner: {
type: Boolean,
value: false,
},
_loggedIn: {
type: Boolean,
value: false,
},
/**
* Offset of currently visible query results.
*/
_offset: Number,
_repo: Object,
_items: Array,
/**
* Because we request one more than the projectsPerPage, _shownProjects
* maybe one less than _projects.
*/
_shownItems: {
type: Array,
computed: 'computeShownItems(_items)',
},
_itemsPerPage: {
type: Number,
value: 25,
},
_loading: {
type: Boolean,
value: true,
},
_filter: String,
_refName: String,
_hasNewItemName: Boolean,
_isEditing: Boolean,
_revisedRef: String,
};
}
_editing: {
type: Boolean,
value: false,
},
_isOwner: {
type: Boolean,
value: false,
},
_loggedIn: {
type: Boolean,
value: false,
},
/**
* Offset of currently visible query results.
*/
_offset: Number,
_repo: Object,
_items: Array,
/**
* Because we request one more than the projectsPerPage, _shownProjects
* maybe one less than _projects.
*/
_shownItems: {
type: Array,
computed: 'computeShownItems(_items)',
},
_itemsPerPage: {
type: Number,
value: 25,
},
_loading: {
type: Boolean,
value: true,
},
_filter: String,
_refName: String,
_hasNewItemName: Boolean,
_isEditing: Boolean,
_revisedRef: String,
};
}
_determineIfOwner(repo) {
return this.$.restAPI.getRepoAccess(repo)
.then(access =>
this._isOwner = access && !!access[repo].is_owner);
}
_determineIfOwner(repo) {
return this.$.restAPI.getRepoAccess(repo)
.then(access =>
this._isOwner = access && !!access[repo].is_owner);
}
_paramsChanged(params) {
if (!params || !params.repo) { return; }
_paramsChanged(params) {
if (!params || !params.repo) { return; }
this._repo = params.repo;
this._repo = params.repo;
this._getLoggedIn().then(loggedIn => {
this._loggedIn = loggedIn;
if (loggedIn) {
this._determineIfOwner(this._repo);
}
this._getLoggedIn().then(loggedIn => {
this._loggedIn = loggedIn;
if (loggedIn) {
this._determineIfOwner(this._repo);
}
});
this.detailType = params.detail;
this._filter = this.getFilterValue(params);
this._offset = this.getOffsetValue(params);
return this._getItems(this._filter, this._repo,
this._itemsPerPage, this._offset, this.detailType);
}
_getItems(filter, repo, itemsPerPage, offset, detailType) {
this._loading = true;
this._items = [];
flush();
const errFn = response => {
this.fire('page-error', {response});
};
if (detailType === DETAIL_TYPES.BRANCHES) {
return this.$.restAPI.getRepoBranches(
filter, repo, itemsPerPage, offset, errFn).then(items => {
if (!items) { return; }
this._items = items;
this._loading = false;
});
this.detailType = params.detail;
this._filter = this.getFilterValue(params);
this._offset = this.getOffsetValue(params);
return this._getItems(this._filter, this._repo,
this._itemsPerPage, this._offset, this.detailType);
}
_getItems(filter, repo, itemsPerPage, offset, detailType) {
this._loading = true;
this._items = [];
Polymer.dom.flush();
const errFn = response => {
this.fire('page-error', {response});
};
if (detailType === DETAIL_TYPES.BRANCHES) {
return this.$.restAPI.getRepoBranches(
filter, repo, itemsPerPage, offset, errFn).then(items => {
if (!items) { return; }
this._items = items;
this._loading = false;
});
} else if (detailType === DETAIL_TYPES.TAGS) {
return this.$.restAPI.getRepoTags(
filter, repo, itemsPerPage, offset, errFn).then(items => {
if (!items) { return; }
this._items = items;
this._loading = false;
});
}
}
_getPath(repo) {
return `/admin/repos/${this.encodeURL(repo, false)},` +
`${this.detailType}`;
}
_computeWeblink(repo) {
if (!repo.web_links) { return ''; }
const webLinks = repo.web_links;
return webLinks.length ? webLinks : null;
}
_computeMessage(message) {
if (!message) { return; }
// Strip PGP info.
return message.split(PGP_START)[0];
}
_stripRefs(item, detailType) {
if (detailType === DETAIL_TYPES.BRANCHES) {
return item.replace('refs/heads/', '');
} else if (detailType === DETAIL_TYPES.TAGS) {
return item.replace('refs/tags/', '');
}
}
_getLoggedIn() {
return this.$.restAPI.getLoggedIn();
}
_computeEditingClass(isEditing) {
return isEditing ? 'editing' : '';
}
_computeCanEditClass(ref, detailType, isOwner) {
return isOwner && this._stripRefs(ref, detailType) === 'HEAD' ?
'canEdit' : '';
}
_handleEditRevision(e) {
this._revisedRef = e.model.get('item.revision');
this._isEditing = true;
}
_handleCancelRevision() {
this._isEditing = false;
}
_handleSaveRevision(e) {
this._setRepoHead(this._repo, this._revisedRef, e);
}
_setRepoHead(repo, ref, e) {
return this.$.restAPI.setRepoHead(repo, ref).then(res => {
if (res.status < 400) {
this._isEditing = false;
e.model.set('item.revision', ref);
// This is needed to refresh _items property with fresh data,
// specifically can_delete from the json response.
this._getItems(
this._filter, this._repo, this._itemsPerPage,
this._offset, this.detailType);
}
} else if (detailType === DETAIL_TYPES.TAGS) {
return this.$.restAPI.getRepoTags(
filter, repo, itemsPerPage, offset, errFn).then(items => {
if (!items) { return; }
this._items = items;
this._loading = false;
});
}
_computeItemName(detailType) {
if (detailType === DETAIL_TYPES.BRANCHES) {
return 'Branch';
} else if (detailType === DETAIL_TYPES.TAGS) {
return 'Tag';
}
}
_handleDeleteItemConfirm() {
this.$.overlay.close();
if (this.detailType === DETAIL_TYPES.BRANCHES) {
return this.$.restAPI.deleteRepoBranches(this._repo, this._refName)
.then(itemDeleted => {
if (itemDeleted.status === 204) {
this._getItems(
this._filter, this._repo, this._itemsPerPage,
this._offset, this.detailType);
}
});
} else if (this.detailType === DETAIL_TYPES.TAGS) {
return this.$.restAPI.deleteRepoTags(this._repo, this._refName)
.then(itemDeleted => {
if (itemDeleted.status === 204) {
this._getItems(
this._filter, this._repo, this._itemsPerPage,
this._offset, this.detailType);
}
});
}
}
_handleConfirmDialogCancel() {
this.$.overlay.close();
}
_handleDeleteItem(e) {
const name = this._stripRefs(e.model.get('item.ref'), this.detailType);
if (!name) { return; }
this._refName = name;
this.$.overlay.open();
}
_computeHideDeleteClass(owner, canDelete) {
if (canDelete || owner) {
return 'show';
}
return '';
}
_handleCreateItem() {
this.$.createNewModal.handleCreateItem();
this._handleCloseCreate();
}
_handleCloseCreate() {
this.$.createOverlay.close();
}
_handleCreateClicked() {
this.$.createOverlay.open();
}
_hideIfBranch(type) {
if (type === DETAIL_TYPES.BRANCHES) {
return 'hideItem';
}
return '';
}
_computeHideTagger(tagger) {
return tagger ? '' : 'hide';
}
}
customElements.define(GrRepoDetailList.is, GrRepoDetailList);
})();
_getPath(repo) {
return `/admin/repos/${this.encodeURL(repo, false)},` +
`${this.detailType}`;
}
_computeWeblink(repo) {
if (!repo.web_links) { return ''; }
const webLinks = repo.web_links;
return webLinks.length ? webLinks : null;
}
_computeMessage(message) {
if (!message) { return; }
// Strip PGP info.
return message.split(PGP_START)[0];
}
_stripRefs(item, detailType) {
if (detailType === DETAIL_TYPES.BRANCHES) {
return item.replace('refs/heads/', '');
} else if (detailType === DETAIL_TYPES.TAGS) {
return item.replace('refs/tags/', '');
}
}
_getLoggedIn() {
return this.$.restAPI.getLoggedIn();
}
_computeEditingClass(isEditing) {
return isEditing ? 'editing' : '';
}
_computeCanEditClass(ref, detailType, isOwner) {
return isOwner && this._stripRefs(ref, detailType) === 'HEAD' ?
'canEdit' : '';
}
_handleEditRevision(e) {
this._revisedRef = e.model.get('item.revision');
this._isEditing = true;
}
_handleCancelRevision() {
this._isEditing = false;
}
_handleSaveRevision(e) {
this._setRepoHead(this._repo, this._revisedRef, e);
}
_setRepoHead(repo, ref, e) {
return this.$.restAPI.setRepoHead(repo, ref).then(res => {
if (res.status < 400) {
this._isEditing = false;
e.model.set('item.revision', ref);
// This is needed to refresh _items property with fresh data,
// specifically can_delete from the json response.
this._getItems(
this._filter, this._repo, this._itemsPerPage,
this._offset, this.detailType);
}
});
}
_computeItemName(detailType) {
if (detailType === DETAIL_TYPES.BRANCHES) {
return 'Branch';
} else if (detailType === DETAIL_TYPES.TAGS) {
return 'Tag';
}
}
_handleDeleteItemConfirm() {
this.$.overlay.close();
if (this.detailType === DETAIL_TYPES.BRANCHES) {
return this.$.restAPI.deleteRepoBranches(this._repo, this._refName)
.then(itemDeleted => {
if (itemDeleted.status === 204) {
this._getItems(
this._filter, this._repo, this._itemsPerPage,
this._offset, this.detailType);
}
});
} else if (this.detailType === DETAIL_TYPES.TAGS) {
return this.$.restAPI.deleteRepoTags(this._repo, this._refName)
.then(itemDeleted => {
if (itemDeleted.status === 204) {
this._getItems(
this._filter, this._repo, this._itemsPerPage,
this._offset, this.detailType);
}
});
}
}
_handleConfirmDialogCancel() {
this.$.overlay.close();
}
_handleDeleteItem(e) {
const name = this._stripRefs(e.model.get('item.ref'), this.detailType);
if (!name) { return; }
this._refName = name;
this.$.overlay.open();
}
_computeHideDeleteClass(owner, canDelete) {
if (canDelete || owner) {
return 'show';
}
return '';
}
_handleCreateItem() {
this.$.createNewModal.handleCreateItem();
this._handleCloseCreate();
}
_handleCloseCreate() {
this.$.createOverlay.close();
}
_handleCreateClicked() {
this.$.createOverlay.open();
}
_hideIfBranch(type) {
if (type === DETAIL_TYPES.BRANCHES) {
return 'hideItem';
}
return '';
}
_computeHideTagger(tagger) {
return tagger ? '' : 'hide';
}
}
customElements.define(GrRepoDetailList.is, GrRepoDetailList);

View File

@ -1,40 +1,22 @@
<!--
@license
Copyright (C) 2017 The Android Open Source Project
/**
* @license
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {html} from '@polymer/polymer/lib/utils/html-tag.js';
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<link rel="import" href="../../../behaviors/gr-list-view-behavior/gr-list-view-behavior.html">
<link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html">
<link rel="import" href="/bower_components/iron-input/iron-input.html">
<link rel="import" href="/bower_components/polymer/polymer.html">
<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
<link rel="import" href="../../../styles/gr-form-styles.html">
<link rel="import" href="../../../styles/gr-table-styles.html">
<link rel="import" href="../../../styles/shared-styles.html">
<link rel="import" href="../../shared/gr-account-link/gr-account-link.html">
<link rel="import" href="../../shared/gr-button/gr-button.html">
<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
<link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
<link rel="import" href="../../shared/gr-list-view/gr-list-view.html">
<link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
<link rel="import" href="../gr-create-pointer-dialog/gr-create-pointer-dialog.html">
<link rel="import" href="../gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.html">
<dom-module id="gr-repo-detail-list">
<template>
export const htmlTemplate = html`
<style include="gr-form-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
@ -88,100 +70,68 @@ limitations under the License.
<style include="gr-table-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
<gr-list-view
create-new="[[_loggedIn]]"
filter="[[_filter]]"
items-per-page="[[_itemsPerPage]]"
items="[[_items]]"
loading="[[_loading]]"
offset="[[_offset]]"
on-create-clicked="_handleCreateClicked"
path="[[_getPath(_repo, detailType)]]">
<gr-list-view create-new="[[_loggedIn]]" filter="[[_filter]]" items-per-page="[[_itemsPerPage]]" items="[[_items]]" loading="[[_loading]]" offset="[[_offset]]" on-create-clicked="_handleCreateClicked" path="[[_getPath(_repo, detailType)]]">
<table id="list" class="genericList gr-form-styles">
<tr class="headerRow">
<tbody><tr class="headerRow">
<th class="name topHeader">Name</th>
<th class="revision topHeader">Revision</th>
<th class$="message topHeader [[_hideIfBranch(detailType)]]">
<th class\$="message topHeader [[_hideIfBranch(detailType)]]">
Message</th>
<th class$="tagger topHeader [[_hideIfBranch(detailType)]]">
<th class\$="tagger topHeader [[_hideIfBranch(detailType)]]">
Tagger</th>
<th class="repositoryBrowser topHeader">
Repository Browser</th>
<th class="delete topHeader"></th>
</tr>
<tr id="loading" class$="loadingMsg [[computeLoadingClass(_loading)]]">
<tr id="loading" class\$="loadingMsg [[computeLoadingClass(_loading)]]">
<td>Loading...</td>
</tr>
<tbody class$="[[computeLoadingClass(_loading)]]">
</tbody><tbody class\$="[[computeLoadingClass(_loading)]]">
<template is="dom-repeat" items="[[_shownItems]]">
<tr class="table">
<td class$="[[detailType]] name">[[_stripRefs(item.ref, detailType)]]</td>
<td class$="[[detailType]] revision [[_computeCanEditClass(item.ref, detailType, _isOwner)]]">
<td class\$="[[detailType]] name">[[_stripRefs(item.ref, detailType)]]</td>
<td class\$="[[detailType]] revision [[_computeCanEditClass(item.ref, detailType, _isOwner)]]">
<span class="revisionNoEditing">
[[item.revision]]
</span>
<span class$="revisionEdit [[_computeEditingClass(_isEditing)]]">
<span class\$="revisionEdit [[_computeEditingClass(_isEditing)]]">
<span class="revisionWithEditing">
[[item.revision]]
</span>
<gr-button
link
on-click="_handleEditRevision"
class="editBtn">
<gr-button link="" on-click="_handleEditRevision" class="editBtn">
edit
</gr-button>
<iron-input
bind-value="{{_revisedRef}}"
class="editItem">
<input
is="iron-input"
bind-value="{{_revisedRef}}">
<iron-input bind-value="{{_revisedRef}}" class="editItem">
<input is="iron-input" bind-value="{{_revisedRef}}">
</iron-input>
<gr-button
link
on-click="_handleCancelRevision"
class="cancelBtn editItem">
<gr-button link="" on-click="_handleCancelRevision" class="cancelBtn editItem">
Cancel
</gr-button>
<gr-button
link
on-click="_handleSaveRevision"
class="saveBtn editItem"
disabled="[[!_revisedRef]]">
<gr-button link="" on-click="_handleSaveRevision" class="saveBtn editItem" disabled="[[!_revisedRef]]">
Save
</gr-button>
</span>
</td>
<td class$="message [[_hideIfBranch(detailType)]]">
<td class\$="message [[_hideIfBranch(detailType)]]">
[[_computeMessage(item.message)]]
</td>
<td class$="tagger [[_hideIfBranch(detailType)]]">
<div class$="tagger [[_computeHideTagger(item.tagger)]]">
<gr-account-link
account="[[item.tagger]]">
<td class\$="tagger [[_hideIfBranch(detailType)]]">
<div class\$="tagger [[_computeHideTagger(item.tagger)]]">
<gr-account-link account="[[item.tagger]]">
</gr-account-link>
(<gr-date-formatter
has-tooltip
date-str="[[item.tagger.date]]">
(<gr-date-formatter has-tooltip="" date-str="[[item.tagger.date]]">
</gr-date-formatter>)
</div>
</td>
<td class="repositoryBrowser">
<template is="dom-repeat"
items="[[_computeWeblink(item)]]" as="link">
<a href$="[[link.url]]"
class="webLink"
rel="noopener"
target="_blank">
<template is="dom-repeat" items="[[_computeWeblink(item)]]" as="link">
<a href\$="[[link.url]]" class="webLink" rel="noopener" target="_blank">
([[link.name]])
</a>
</template>
</td>
<td class="delete">
<gr-button
link
class$="deleteButton [[_computeHideDeleteClass(_isOwner, item.can_delete)]]"
on-click="_handleDeleteItem">
<gr-button link="" class\$="deleteButton [[_computeHideDeleteClass(_isOwner, item.can_delete)]]" on-click="_handleDeleteItem">
Delete
</gr-button>
</td>
@ -189,36 +139,19 @@ limitations under the License.
</template>
</tbody>
</table>
<gr-overlay id="overlay" with-backdrop>
<gr-confirm-delete-item-dialog
class="confirmDialog"
on-confirm="_handleDeleteItemConfirm"
on-cancel="_handleConfirmDialogCancel"
item="[[_refName]]"
item-type="[[detailType]]"></gr-confirm-delete-item-dialog>
<gr-overlay id="overlay" with-backdrop="">
<gr-confirm-delete-item-dialog class="confirmDialog" on-confirm="_handleDeleteItemConfirm" on-cancel="_handleConfirmDialogCancel" item="[[_refName]]" item-type="[[detailType]]"></gr-confirm-delete-item-dialog>
</gr-overlay>
</gr-list-view>
<gr-overlay id="createOverlay" with-backdrop>
<gr-dialog
id="createDialog"
disabled="[[!_hasNewItemName]]"
confirm-label="Create"
on-confirm="_handleCreateItem"
on-cancel="_handleCloseCreate">
<gr-overlay id="createOverlay" with-backdrop="">
<gr-dialog id="createDialog" disabled="[[!_hasNewItemName]]" confirm-label="Create" on-confirm="_handleCreateItem" on-cancel="_handleCloseCreate">
<div class="header" slot="header">
Create [[_computeItemName(detailType)]]
</div>
<div class="main" slot="main">
<gr-create-pointer-dialog
id="createNewModal"
detail-type="[[_computeItemName(detailType)]]"
has-new-item-name="{{_hasNewItemName}}"
item-detail="[[detailType]]"
repo-name="[[_repo]]"></gr-create-pointer-dialog>
<gr-create-pointer-dialog id="createNewModal" detail-type="[[_computeItemName(detailType)]]" has-new-item-name="{{_hasNewItemName}}" item-detail="[[detailType]]" repo-name="[[_repo]]"></gr-create-pointer-dialog>
</div>
</gr-dialog>
</gr-overlay>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
</template>
<script src="gr-repo-detail-list.js"></script>
</dom-module>
`;

View File

@ -14,160 +14,174 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function() {
'use strict';
import '../../../scripts/bundled-polymer.js';
import '../../../behaviors/gr-list-view-behavior/gr-list-view-behavior.js';
import '../../../styles/gr-table-styles.js';
import '../../../styles/shared-styles.js';
import '../../shared/gr-dialog/gr-dialog.js';
import '../../shared/gr-list-view/gr-list-view.js';
import '../../shared/gr-overlay/gr-overlay.js';
import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
import '../gr-create-repo-dialog/gr-create-repo-dialog.js';
import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
import {PolymerElement} from '@polymer/polymer/polymer-element.js';
import {htmlTemplate} from './gr-repo-list_html.js';
/**
* @appliesMixin Gerrit.ListViewMixin
* @extends Polymer.Element
*/
class GrRepoList extends mixinBehaviors( [
Gerrit.ListViewBehavior,
], GestureEventListeners(
LegacyElementMixin(
PolymerElement))) {
static get template() { return htmlTemplate; }
static get is() { return 'gr-repo-list'; }
static get properties() {
return {
/**
* URL params passed from the router.
*/
params: {
type: Object,
observer: '_paramsChanged',
},
/**
* Offset of currently visible query results.
*/
_offset: Number,
_path: {
type: String,
readOnly: true,
value: '/admin/repos',
},
_hasNewRepoName: Boolean,
_createNewCapability: {
type: Boolean,
value: false,
},
_repos: Array,
/**
* Because we request one more than the projectsPerPage, _shownProjects
* maybe one less than _projects.
* */
_shownRepos: {
type: Array,
computed: 'computeShownItems(_repos)',
},
_reposPerPage: {
type: Number,
value: 25,
},
_loading: {
type: Boolean,
value: true,
},
_filter: {
type: String,
value: '',
},
};
}
/** @override */
attached() {
super.attached();
this._getCreateRepoCapability();
this.fire('title-change', {title: 'Repos'});
this._maybeOpenCreateOverlay(this.params);
}
_paramsChanged(params) {
this._loading = true;
this._filter = this.getFilterValue(params);
this._offset = this.getOffsetValue(params);
return this._getRepos(this._filter, this._reposPerPage,
this._offset);
}
/**
* @appliesMixin Gerrit.ListViewMixin
* @extends Polymer.Element
* Opens the create overlay if the route has a hash 'create'
*
* @param {!Object} params
*/
class GrRepoList extends Polymer.mixinBehaviors( [
Gerrit.ListViewBehavior,
], Polymer.GestureEventListeners(
Polymer.LegacyElementMixin(
Polymer.Element))) {
static get is() { return 'gr-repo-list'; }
static get properties() {
return {
/**
* URL params passed from the router.
*/
params: {
type: Object,
observer: '_paramsChanged',
},
/**
* Offset of currently visible query results.
*/
_offset: Number,
_path: {
type: String,
readOnly: true,
value: '/admin/repos',
},
_hasNewRepoName: Boolean,
_createNewCapability: {
type: Boolean,
value: false,
},
_repos: Array,
/**
* Because we request one more than the projectsPerPage, _shownProjects
* maybe one less than _projects.
* */
_shownRepos: {
type: Array,
computed: 'computeShownItems(_repos)',
},
_reposPerPage: {
type: Number,
value: 25,
},
_loading: {
type: Boolean,
value: true,
},
_filter: {
type: String,
value: '',
},
};
}
/** @override */
attached() {
super.attached();
this._getCreateRepoCapability();
this.fire('title-change', {title: 'Repos'});
this._maybeOpenCreateOverlay(this.params);
}
_paramsChanged(params) {
this._loading = true;
this._filter = this.getFilterValue(params);
this._offset = this.getOffsetValue(params);
return this._getRepos(this._filter, this._reposPerPage,
this._offset);
}
/**
* Opens the create overlay if the route has a hash 'create'
*
* @param {!Object} params
*/
_maybeOpenCreateOverlay(params) {
if (params && params.openCreateModal) {
this.$.createOverlay.open();
}
}
_computeRepoUrl(name) {
return this.getUrl(this._path + '/', name);
}
_computeChangesLink(name) {
return Gerrit.Nav.getUrlForProjectChanges(name);
}
_getCreateRepoCapability() {
return this.$.restAPI.getAccount().then(account => {
if (!account) { return; }
return this.$.restAPI.getAccountCapabilities(['createProject'])
.then(capabilities => {
if (capabilities.createProject) {
this._createNewCapability = true;
}
});
});
}
_getRepos(filter, reposPerPage, offset) {
this._repos = [];
return this.$.restAPI.getRepos(filter, reposPerPage, offset)
.then(repos => {
// Late response.
if (filter !== this._filter || !repos) { return; }
this._repos = repos;
this._loading = false;
});
}
_refreshReposList() {
this.$.restAPI.invalidateReposCache();
return this._getRepos(this._filter, this._reposPerPage,
this._offset);
}
_handleCreateRepo() {
this.$.createNewModal.handleCreateRepo().then(() => {
this._refreshReposList();
});
}
_handleCloseCreate() {
this.$.createOverlay.close();
}
_handleCreateClicked() {
_maybeOpenCreateOverlay(params) {
if (params && params.openCreateModal) {
this.$.createOverlay.open();
}
_readOnly(item) {
return item.state === 'READ_ONLY' ? 'Y' : '';
}
_computeWeblink(repo) {
if (!repo.web_links) { return ''; }
const webLinks = repo.web_links;
return webLinks.length ? webLinks : null;
}
}
customElements.define(GrRepoList.is, GrRepoList);
})();
_computeRepoUrl(name) {
return this.getUrl(this._path + '/', name);
}
_computeChangesLink(name) {
return Gerrit.Nav.getUrlForProjectChanges(name);
}
_getCreateRepoCapability() {
return this.$.restAPI.getAccount().then(account => {
if (!account) { return; }
return this.$.restAPI.getAccountCapabilities(['createProject'])
.then(capabilities => {
if (capabilities.createProject) {
this._createNewCapability = true;
}
});
});
}
_getRepos(filter, reposPerPage, offset) {
this._repos = [];
return this.$.restAPI.getRepos(filter, reposPerPage, offset)
.then(repos => {
// Late response.
if (filter !== this._filter || !repos) { return; }
this._repos = repos;
this._loading = false;
});
}
_refreshReposList() {
this.$.restAPI.invalidateReposCache();
return this._getRepos(this._filter, this._reposPerPage,
this._offset);
}
_handleCreateRepo() {
this.$.createNewModal.handleCreateRepo().then(() => {
this._refreshReposList();
});
}
_handleCloseCreate() {
this.$.createOverlay.close();
}
_handleCreateClicked() {
this.$.createOverlay.open();
}
_readOnly(item) {
return item.state === 'READ_ONLY' ? 'Y' : '';
}
_computeWeblink(repo) {
if (!repo.web_links) { return ''; }
const webLinks = repo.web_links;
return webLinks.length ? webLinks : null;
}
}
customElements.define(GrRepoList.is, GrRepoList);

View File

@ -1,32 +1,22 @@
<!--
@license
Copyright (C) 2017 The Android Open Source Project
/**
* @license
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {html} from '@polymer/polymer/lib/utils/html-tag.js';
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<link rel="import" href="/bower_components/polymer/polymer.html">
<link rel="import" href="../../../behaviors/gr-list-view-behavior/gr-list-view-behavior.html">
<link rel="import" href="../../../styles/gr-table-styles.html">
<link rel="import" href="../../../styles/shared-styles.html">
<link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
<link rel="import" href="../../shared/gr-list-view/gr-list-view.html">
<link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
<link rel="import" href="../gr-create-repo-dialog/gr-create-repo-dialog.html">
<dom-module id="gr-repo-list">
<template>
export const htmlTemplate = html`
<style include="shared-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
@ -47,44 +37,32 @@ limitations under the License.
white-space:nowrap;
}
</style>
<gr-list-view
create-new=[[_createNewCapability]]
filter="[[_filter]]"
items-per-page="[[_reposPerPage]]"
items="[[_repos]]"
loading="[[_loading]]"
offset="[[_offset]]"
on-create-clicked="_handleCreateClicked"
path="[[_path]]">
<gr-list-view create-new="[[_createNewCapability]]" filter="[[_filter]]" items-per-page="[[_reposPerPage]]" items="[[_repos]]" loading="[[_loading]]" offset="[[_offset]]" on-create-clicked="_handleCreateClicked" path="[[_path]]">
<table id="list" class="genericList">
<tr class="headerRow">
<tbody><tr class="headerRow">
<th class="name topHeader">Repository Name</th>
<th class="repositoryBrowser topHeader">Repository Browser</th>
<th class="changesLink topHeader">Changes</th>
<th class="topHeader readOnly">Read only</th>
<th class="description topHeader">Repository Description</th>
</tr>
<tr id="loading" class$="loadingMsg [[computeLoadingClass(_loading)]]">
<tr id="loading" class\$="loadingMsg [[computeLoadingClass(_loading)]]">
<td>Loading...</td>
</tr>
<tbody class$="[[computeLoadingClass(_loading)]]">
</tbody><tbody class\$="[[computeLoadingClass(_loading)]]">
<template is="dom-repeat" items="[[_shownRepos]]">
<tr class="table">
<td class="name">
<a href$="[[_computeRepoUrl(item.name)]]">[[item.name]]</a>
<a href\$="[[_computeRepoUrl(item.name)]]">[[item.name]]</a>
</td>
<td class="repositoryBrowser">
<template is="dom-repeat"
items="[[_computeWeblink(item)]]" as="link">
<a href$="[[link.url]]"
class="webLink"
rel="noopener"
target="_blank">
<template is="dom-repeat" items="[[_computeWeblink(item)]]" as="link">
<a href\$="[[link.url]]" class="webLink" rel="noopener" target="_blank">
[[link.name]]
</a>
</template>
</td>
<td class="changesLink"><a href$="[[_computeChangesLink(item.name)]]">view all</a></td>
<td class="changesLink"><a href\$="[[_computeChangesLink(item.name)]]">view all</a></td>
<td class="readOnly">[[_readOnly(item)]]</td>
<td class="description">[[item.description]]</td>
</tr>
@ -92,26 +70,15 @@ limitations under the License.
</tbody>
</table>
</gr-list-view>
<gr-overlay id="createOverlay" with-backdrop>
<gr-dialog
id="createDialog"
class="confirmDialog"
disabled="[[!_hasNewRepoName]]"
confirm-label="Create"
on-confirm="_handleCreateRepo"
on-cancel="_handleCloseCreate">
<gr-overlay id="createOverlay" with-backdrop="">
<gr-dialog id="createDialog" class="confirmDialog" disabled="[[!_hasNewRepoName]]" confirm-label="Create" on-confirm="_handleCreateRepo" on-cancel="_handleCloseCreate">
<div class="header" slot="header">
Create Repository
</div>
<div class="main" slot="main">
<gr-create-repo-dialog
has-new-repo-name="{{_hasNewRepoName}}"
params="[[params]]"
id="createNewModal"></gr-create-repo-dialog>
<gr-create-repo-dialog has-new-repo-name="{{_hasNewRepoName}}" params="[[params]]" id="createNewModal"></gr-create-repo-dialog>
</div>
</gr-dialog>
</gr-overlay>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
</template>
<script src="gr-repo-list.js"></script>
</dom-module>
`;

View File

@ -19,16 +19,21 @@ limitations under the License.
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-repo-list</title>
<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/bower_components/page/page.js"></script>
<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/bower_components/web-component-tester/browser.js"></script>
<script src="../../../test/test-pre-setup.js"></script>
<link rel="import" href="../../../test/common-test-setup.html"/>
<link rel="import" href="gr-repo-list.html">
<script src="/node_modules/page/page.js"></script>
<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/components/wct-browser-legacy/browser.js"></script>
<script type="module" src="../../../test/test-pre-setup.js"></script>
<script type="module" src="../../../test/common-test-setup.js"></script>
<script type="module" src="./gr-repo-list.js"></script>
<script>void(0);</script>
<script type="module">
import '../../../test/test-pre-setup.js';
import '../../../test/common-test-setup.js';
import './gr-repo-list.js';
void(0);
</script>
<test-fixture id="basic">
<template>
@ -36,166 +41,168 @@ limitations under the License.
</template>
</test-fixture>
<script>
let counter;
const repoGenerator = () => {
return {
id: `test${++counter}`,
state: 'ACTIVE',
web_links: [
{
name: 'diffusion',
url: `https://phabricator.example.org/r/project/test${counter}`,
},
],
};
<script type="module">
import '../../../test/test-pre-setup.js';
import '../../../test/common-test-setup.js';
import './gr-repo-list.js';
let counter;
const repoGenerator = () => {
return {
id: `test${++counter}`,
state: 'ACTIVE',
web_links: [
{
name: 'diffusion',
url: `https://phabricator.example.org/r/project/test${counter}`,
},
],
};
};
suite('gr-repo-list tests', async () => {
await readyToTest();
let element;
let repos;
let sandbox;
let value;
suite('gr-repo-list tests', () => {
let element;
let repos;
let sandbox;
let value;
setup(() => {
sandbox = sinon.sandbox.create();
sandbox.stub(page, 'show');
element = fixture('basic');
counter = 0;
});
teardown(() => {
sandbox.restore();
});
suite('list with repos', () => {
setup(done => {
repos = _.times(26, repoGenerator);
stub('gr-rest-api-interface', {
getRepos(num, offset) {
return Promise.resolve(repos);
},
});
element._paramsChanged(value).then(() => { flush(done); });
});
test('test for test repo in the list', done => {
flush(() => {
assert.equal(element._repos[1].id, 'test2');
done();
});
});
test('_shownRepos', () => {
assert.equal(element._shownRepos.length, 25);
});
test('_maybeOpenCreateOverlay', () => {
const overlayOpen = sandbox.stub(element.$.createOverlay, 'open');
element._maybeOpenCreateOverlay();
assert.isFalse(overlayOpen.called);
const params = {};
element._maybeOpenCreateOverlay(params);
assert.isFalse(overlayOpen.called);
params.openCreateModal = true;
element._maybeOpenCreateOverlay(params);
assert.isTrue(overlayOpen.called);
});
});
suite('list with less then 25 repos', () => {
setup(done => {
repos = _.times(25, repoGenerator);
stub('gr-rest-api-interface', {
getRepos(num, offset) {
return Promise.resolve(repos);
},
});
element._paramsChanged(value).then(() => { flush(done); });
});
test('_shownRepos', () => {
assert.equal(element._shownRepos.length, 25);
});
});
suite('filter', () => {
let reposFiltered;
setup(() => {
sandbox = sinon.sandbox.create();
sandbox.stub(page, 'show');
element = fixture('basic');
counter = 0;
repos = _.times(25, repoGenerator);
reposFiltered = _.times(1, repoGenerator);
});
teardown(() => {
sandbox.restore();
});
suite('list with repos', () => {
setup(done => {
repos = _.times(26, repoGenerator);
stub('gr-rest-api-interface', {
getRepos(num, offset) {
return Promise.resolve(repos);
},
});
element._paramsChanged(value).then(() => { flush(done); });
});
test('test for test repo in the list', done => {
flush(() => {
assert.equal(element._repos[1].id, 'test2');
done();
});
});
test('_shownRepos', () => {
assert.equal(element._shownRepos.length, 25);
});
test('_maybeOpenCreateOverlay', () => {
const overlayOpen = sandbox.stub(element.$.createOverlay, 'open');
element._maybeOpenCreateOverlay();
assert.isFalse(overlayOpen.called);
const params = {};
element._maybeOpenCreateOverlay(params);
assert.isFalse(overlayOpen.called);
params.openCreateModal = true;
element._maybeOpenCreateOverlay(params);
assert.isTrue(overlayOpen.called);
test('_paramsChanged', done => {
sandbox.stub(element.$.restAPI, 'getRepos', () => Promise.resolve(repos));
const value = {
filter: 'test',
offset: 25,
};
element._paramsChanged(value).then(() => {
assert.isTrue(element.$.restAPI.getRepos.lastCall
.calledWithExactly('test', 25, 25));
done();
});
});
suite('list with less then 25 repos', () => {
setup(done => {
repos = _.times(25, repoGenerator);
test('latest repos requested are always set', done => {
const repoStub = sandbox.stub(element.$.restAPI, 'getRepos');
repoStub.withArgs('test').returns(Promise.resolve(repos));
repoStub.withArgs('filter').returns(Promise.resolve(reposFiltered));
element._filter = 'test';
stub('gr-rest-api-interface', {
getRepos(num, offset) {
return Promise.resolve(repos);
},
});
element._paramsChanged(value).then(() => { flush(done); });
});
test('_shownRepos', () => {
assert.equal(element._shownRepos.length, 25);
});
});
suite('filter', () => {
let reposFiltered;
setup(() => {
repos = _.times(25, repoGenerator);
reposFiltered = _.times(1, repoGenerator);
});
test('_paramsChanged', done => {
sandbox.stub(element.$.restAPI, 'getRepos', () => Promise.resolve(repos));
const value = {
filter: 'test',
offset: 25,
};
element._paramsChanged(value).then(() => {
assert.isTrue(element.$.restAPI.getRepos.lastCall
.calledWithExactly('test', 25, 25));
done();
});
});
test('latest repos requested are always set', done => {
const repoStub = sandbox.stub(element.$.restAPI, 'getRepos');
repoStub.withArgs('test').returns(Promise.resolve(repos));
repoStub.withArgs('filter').returns(Promise.resolve(reposFiltered));
element._filter = 'test';
// Repos are not set because the element._filter differs.
element._getRepos('filter', 25, 0).then(() => {
assert.deepEqual(element._repos, []);
done();
});
});
});
suite('loading', () => {
test('correct contents are displayed', () => {
assert.isTrue(element._loading);
assert.equal(element.computeLoadingClass(element._loading), 'loading');
assert.equal(getComputedStyle(element.$.loading).display, 'block');
element._loading = false;
element._repos = _.times(25, repoGenerator);
flushAsynchronousOperations();
assert.equal(element.computeLoadingClass(element._loading), '');
assert.equal(getComputedStyle(element.$.loading).display, 'none');
});
});
suite('create new', () => {
test('_handleCreateClicked called when create-click fired', () => {
sandbox.stub(element, '_handleCreateClicked');
element.shadowRoot
.querySelector('gr-list-view').fire('create-clicked');
assert.isTrue(element._handleCreateClicked.called);
});
test('_handleCreateClicked opens modal', () => {
const openStub = sandbox.stub(element.$.createOverlay, 'open');
element._handleCreateClicked();
assert.isTrue(openStub.called);
});
test('_handleCreateRepo called when confirm fired', () => {
sandbox.stub(element, '_handleCreateRepo');
element.$.createDialog.fire('confirm');
assert.isTrue(element._handleCreateRepo.called);
});
test('_handleCloseCreate called when cancel fired', () => {
sandbox.stub(element, '_handleCloseCreate');
element.$.createDialog.fire('cancel');
assert.isTrue(element._handleCloseCreate.called);
// Repos are not set because the element._filter differs.
element._getRepos('filter', 25, 0).then(() => {
assert.deepEqual(element._repos, []);
done();
});
});
});
suite('loading', () => {
test('correct contents are displayed', () => {
assert.isTrue(element._loading);
assert.equal(element.computeLoadingClass(element._loading), 'loading');
assert.equal(getComputedStyle(element.$.loading).display, 'block');
element._loading = false;
element._repos = _.times(25, repoGenerator);
flushAsynchronousOperations();
assert.equal(element.computeLoadingClass(element._loading), '');
assert.equal(getComputedStyle(element.$.loading).display, 'none');
});
});
suite('create new', () => {
test('_handleCreateClicked called when create-click fired', () => {
sandbox.stub(element, '_handleCreateClicked');
element.shadowRoot
.querySelector('gr-list-view').fire('create-clicked');
assert.isTrue(element._handleCreateClicked.called);
});
test('_handleCreateClicked opens modal', () => {
const openStub = sandbox.stub(element.$.createOverlay, 'open');
element._handleCreateClicked();
assert.isTrue(openStub.called);
});
test('_handleCreateRepo called when confirm fired', () => {
sandbox.stub(element, '_handleCreateRepo');
element.$.createDialog.fire('confirm');
assert.isTrue(element._handleCreateRepo.called);
});
test('_handleCloseCreate called when cancel fired', () => {
sandbox.stub(element, '_handleCloseCreate');
element.$.createDialog.fire('cancel');
assert.isTrue(element._handleCloseCreate.called);
});
});
});
</script>

View File

@ -14,128 +14,145 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function() {
'use strict';
import '../../../scripts/bundled-polymer.js';
import '@polymer/iron-input/iron-input.js';
import '@polymer/paper-toggle-button/paper-toggle-button.js';
import '../../../behaviors/gr-repo-plugin-config-behavior/gr-repo-plugin-config-behavior.js';
import '../../../styles/gr-form-styles.js';
import '../../../styles/gr-subpage-styles.js';
import '../../../styles/shared-styles.js';
import '../../shared/gr-icons/gr-icons.js';
import '../../shared/gr-select/gr-select.js';
import '../../shared/gr-tooltip-content/gr-tooltip-content.js';
import '../gr-plugin-config-array-editor/gr-plugin-config-array-editor.js';
import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
import {PolymerElement} from '@polymer/polymer/polymer-element.js';
import {htmlTemplate} from './gr-repo-plugin-config_html.js';
/**
* @appliesMixin Gerrit.RepoPluginConfigMixin
* @extends Polymer.Element
*/
class GrRepoPluginConfig extends mixinBehaviors( [
Gerrit.RepoPluginConfig,
], GestureEventListeners(
LegacyElementMixin(
PolymerElement))) {
static get template() { return htmlTemplate; }
static get is() { return 'gr-repo-plugin-config'; }
/**
* @appliesMixin Gerrit.RepoPluginConfigMixin
* @extends Polymer.Element
* Fired when the plugin config changes.
*
* @event plugin-config-changed
*/
class GrRepoPluginConfig extends Polymer.mixinBehaviors( [
Gerrit.RepoPluginConfig,
], Polymer.GestureEventListeners(
Polymer.LegacyElementMixin(
Polymer.Element))) {
static get is() { return 'gr-repo-plugin-config'; }
/**
* Fired when the plugin config changes.
*
* @event plugin-config-changed
*/
static get properties() {
return {
/** @type {?} */
pluginData: Object,
/** @type {Array} */
_pluginConfigOptions: {
type: Array,
computed: '_computePluginConfigOptions(pluginData.*)',
},
};
}
_computePluginConfigOptions(dataRecord) {
if (!dataRecord || !dataRecord.base || !dataRecord.base.config) {
return [];
}
const {config} = dataRecord.base;
return Object.keys(config)
.map(_key => { return {_key, info: config[_key]}; });
}
_isArray(type) {
return type === this.ENTRY_TYPES.ARRAY;
}
_isBoolean(type) {
return type === this.ENTRY_TYPES.BOOLEAN;
}
_isList(type) {
return type === this.ENTRY_TYPES.LIST;
}
_isString(type) {
// Treat numbers like strings for simplicity.
return type === this.ENTRY_TYPES.STRING ||
type === this.ENTRY_TYPES.INT ||
type === this.ENTRY_TYPES.LONG;
}
_computeDisabled(editable) {
return editable === 'false';
}
/**
* @param {string} value - fallback to 'false' if undefined
*/
_computeChecked(value = 'false') {
return JSON.parse(value);
}
_handleStringChange(e) {
const el = Polymer.dom(e).localTarget;
const _key = el.getAttribute('data-option-key');
const configChangeInfo =
this._buildConfigChangeInfo(el.value, _key);
this._handleChange(configChangeInfo);
}
_handleListChange(e) {
const el = Polymer.dom(e).localTarget;
const _key = el.getAttribute('data-option-key');
const configChangeInfo =
this._buildConfigChangeInfo(el.value, _key);
this._handleChange(configChangeInfo);
}
_handleBooleanChange(e) {
const el = Polymer.dom(e).localTarget;
const _key = el.getAttribute('data-option-key');
const configChangeInfo =
this._buildConfigChangeInfo(JSON.stringify(el.checked), _key);
this._handleChange(configChangeInfo);
}
_buildConfigChangeInfo(value, _key) {
const info = this.pluginData.config[_key];
info.value = value;
return {
_key,
info,
notifyPath: `${_key}.value`,
};
}
_handleArrayChange({detail}) {
this._handleChange(detail);
}
_handleChange({_key, info, notifyPath}) {
const {name, config} = this.pluginData;
/** @type {Object} */
const detail = {
name,
config: Object.assign(config, {[_key]: info}, {}),
notifyPath: `${name}.${notifyPath}`,
};
this.dispatchEvent(new CustomEvent(
this.PLUGIN_CONFIG_CHANGED, {detail, bubbles: true, composed: true}));
}
static get properties() {
return {
/** @type {?} */
pluginData: Object,
/** @type {Array} */
_pluginConfigOptions: {
type: Array,
computed: '_computePluginConfigOptions(pluginData.*)',
},
};
}
customElements.define(GrRepoPluginConfig.is, GrRepoPluginConfig);
})();
_computePluginConfigOptions(dataRecord) {
if (!dataRecord || !dataRecord.base || !dataRecord.base.config) {
return [];
}
const {config} = dataRecord.base;
return Object.keys(config)
.map(_key => { return {_key, info: config[_key]}; });
}
_isArray(type) {
return type === this.ENTRY_TYPES.ARRAY;
}
_isBoolean(type) {
return type === this.ENTRY_TYPES.BOOLEAN;
}
_isList(type) {
return type === this.ENTRY_TYPES.LIST;
}
_isString(type) {
// Treat numbers like strings for simplicity.
return type === this.ENTRY_TYPES.STRING ||
type === this.ENTRY_TYPES.INT ||
type === this.ENTRY_TYPES.LONG;
}
_computeDisabled(editable) {
return editable === 'false';
}
/**
* @param {string} value - fallback to 'false' if undefined
*/
_computeChecked(value = 'false') {
return JSON.parse(value);
}
_handleStringChange(e) {
const el = dom(e).localTarget;
const _key = el.getAttribute('data-option-key');
const configChangeInfo =
this._buildConfigChangeInfo(el.value, _key);
this._handleChange(configChangeInfo);
}
_handleListChange(e) {
const el = dom(e).localTarget;
const _key = el.getAttribute('data-option-key');
const configChangeInfo =
this._buildConfigChangeInfo(el.value, _key);
this._handleChange(configChangeInfo);
}
_handleBooleanChange(e) {
const el = dom(e).localTarget;
const _key = el.getAttribute('data-option-key');
const configChangeInfo =
this._buildConfigChangeInfo(JSON.stringify(el.checked), _key);
this._handleChange(configChangeInfo);
}
_buildConfigChangeInfo(value, _key) {
const info = this.pluginData.config[_key];
info.value = value;
return {
_key,
info,
notifyPath: `${_key}.value`,
};
}
_handleArrayChange({detail}) {
this._handleChange(detail);
}
_handleChange({_key, info, notifyPath}) {
const {name, config} = this.pluginData;
/** @type {Object} */
const detail = {
name,
config: Object.assign(config, {[_key]: info}, {}),
notifyPath: `${name}.${notifyPath}`,
};
this.dispatchEvent(new CustomEvent(
this.PLUGIN_CONFIG_CHANGED, {detail, bubbles: true, composed: true}));
}
}
customElements.define(GrRepoPluginConfig.is, GrRepoPluginConfig);

View File

@ -1,35 +1,22 @@
<!--
@license
Copyright (C) 2018 The Android Open Source Project
/**
* @license
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {html} from '@polymer/polymer/lib/utils/html-tag.js';
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<link rel="import" href="/bower_components/polymer/polymer.html">
<link rel="import" href="/bower_components/iron-input/iron-input.html">
<link rel="import" href="/bower_components/paper-toggle-button/paper-toggle-button.html">
<link rel="import" href="../../../behaviors/gr-repo-plugin-config-behavior/gr-repo-plugin-config-behavior.html">
<link rel="import" href="../../../styles/gr-form-styles.html">
<link rel="import" href="../../../styles/gr-subpage-styles.html">
<link rel="import" href="../../../styles/shared-styles.html">
<link rel="import" href="../../shared/gr-icons/gr-icons.html">
<link rel="import" href="../../shared/gr-select/gr-select.html">
<link rel="import" href="../../shared/gr-tooltip-content/gr-tooltip-content.html">
<link rel="import" href="../gr-plugin-config-array-editor/gr-plugin-config-array-editor.html">
<dom-module id="gr-repo-plugin-config">
<template>
export const htmlTemplate = html`
<style include="shared-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
@ -53,55 +40,31 @@ limitations under the License.
<fieldset>
<h4>[[pluginData.name]]</h4>
<template is="dom-repeat" items="[[_pluginConfigOptions]]" as="option">
<section class$="section [[option.info.type]]">
<section class\$="section [[option.info.type]]">
<span class="title">
<gr-tooltip-content
has-tooltip="[[option.info.description]]"
show-icon="[[option.info.description]]"
title="[[option.info.description]]">
<gr-tooltip-content has-tooltip="[[option.info.description]]" show-icon="[[option.info.description]]" title="[[option.info.description]]">
<span>[[option.info.display_name]]</span>
</gr-tooltip-content>
</span>
<span class="value">
<template is="dom-if" if="[[_isArray(option.info.type)]]">
<gr-plugin-config-array-editor
on-plugin-config-option-changed="_handleArrayChange"
plugin-option="[[option]]"></gr-plugin-config-array-editor>
<gr-plugin-config-array-editor on-plugin-config-option-changed="_handleArrayChange" plugin-option="[[option]]"></gr-plugin-config-array-editor>
</template>
<template is="dom-if" if="[[_isBoolean(option.info.type)]]">
<paper-toggle-button
checked="[[_computeChecked(option.info.value)]]"
on-change="_handleBooleanChange"
data-option-key$="[[option._key]]"
disabled$="[[_computeDisabled(option.info.editable)]]"></paper-toggle-button>
<paper-toggle-button checked="[[_computeChecked(option.info.value)]]" on-change="_handleBooleanChange" data-option-key\$="[[option._key]]" disabled\$="[[_computeDisabled(option.info.editable)]]"></paper-toggle-button>
</template>
<template is="dom-if" if="[[_isList(option.info.type)]]">
<gr-select
bind-value$="[[option.info.value]]"
on-change="_handleListChange">
<select
data-option-key$="[[option._key]]"
disabled$="[[_computeDisabled(option.info.editable)]]">
<template is="dom-repeat"
items="[[option.info.permitted_values]]"
as="value">
<option value$="[[value]]">[[value]]</option>
<gr-select bind-value\$="[[option.info.value]]" on-change="_handleListChange">
<select data-option-key\$="[[option._key]]" disabled\$="[[_computeDisabled(option.info.editable)]]">
<template is="dom-repeat" items="[[option.info.permitted_values]]" as="value">
<option value\$="[[value]]">[[value]]</option>
</template>
</select>
</gr-select>
</template>
<template is="dom-if" if="[[_isString(option.info.type)]]">
<iron-input
bind-value="[[option.info.value]]"
on-input="_handleStringChange"
data-option-key$="[[option._key]]"
disabled$="[[_computeDisabled(option.info.editable)]]">
<input
is="iron-input"
value="[[option.info.value]]"
on-input="_handleStringChange"
data-option-key$="[[option._key]]"
disabled$="[[_computeDisabled(option.info.editable)]]">
<iron-input bind-value="[[option.info.value]]" on-input="_handleStringChange" data-option-key\$="[[option._key]]" disabled\$="[[_computeDisabled(option.info.editable)]]">
<input is="iron-input" value="[[option.info.value]]" on-input="_handleStringChange" data-option-key\$="[[option._key]]" disabled\$="[[_computeDisabled(option.info.editable)]]">
</iron-input>
</template>
<template is="dom-if" if="[[option.info.inherited_value]]">
@ -114,6 +77,4 @@ limitations under the License.
</template>
</fieldset>
</div>
</template>
<script src="gr-repo-plugin-config.js"></script>
</dom-module>
`;

View File

@ -19,15 +19,20 @@ limitations under the License.
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-repo-plugin-config</title>
<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/bower_components/web-component-tester/browser.js"></script>
<script src="../../../test/test-pre-setup.js"></script>
<link rel="import" href="../../../test/common-test-setup.html"/>
<link rel="import" href="gr-repo-plugin-config.html">
<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/components/wct-browser-legacy/browser.js"></script>
<script type="module" src="../../../test/test-pre-setup.js"></script>
<script type="module" src="../../../test/common-test-setup.js"></script>
<script type="module" src="./gr-repo-plugin-config.js"></script>
<script>void(0);</script>
<script type="module">
import '../../../test/test-pre-setup.js';
import '../../../test/common-test-setup.js';
import './gr-repo-plugin-config.js';
void(0);
</script>
<test-fixture id="basic">
<template>
@ -35,150 +40,152 @@ limitations under the License.
</template>
</test-fixture>
<script>
suite('gr-repo-plugin-config tests', async () => {
await readyToTest();
let element;
let sandbox;
<script type="module">
import '../../../test/test-pre-setup.js';
import '../../../test/common-test-setup.js';
import './gr-repo-plugin-config.js';
suite('gr-repo-plugin-config tests', () => {
let element;
let sandbox;
setup(() => {
sandbox = sinon.sandbox.create();
element = fixture('basic');
});
teardown(() => sandbox.restore());
test('_computePluginConfigOptions', () => {
assert.deepEqual(element._computePluginConfigOptions(), []);
assert.deepEqual(element._computePluginConfigOptions({}), []);
assert.deepEqual(element._computePluginConfigOptions({base: {}}), []);
assert.deepEqual(element._computePluginConfigOptions(
{base: {config: {}}}), []);
assert.deepEqual(element._computePluginConfigOptions(
{base: {config: {testKey: 'testInfo'}}}),
[{_key: 'testKey', info: 'testInfo'}]);
});
test('_computeDisabled', () => {
assert.isFalse(element._computeDisabled('true'));
assert.isTrue(element._computeDisabled('false'));
});
test('_handleChange', () => {
const eventStub = sandbox.stub(element, 'dispatchEvent');
element.pluginData = {
name: 'testName',
config: {plugin: {value: 'test'}},
};
element._handleChange({
_key: 'plugin',
info: {value: 'newTest'},
notifyPath: 'plugin.value',
});
assert.isTrue(eventStub.called);
const {detail} = eventStub.lastCall.args[0];
assert.equal(detail.name, 'testName');
assert.deepEqual(detail.config, {plugin: {value: 'newTest'}});
assert.equal(detail.notifyPath, 'testName.plugin.value');
});
suite('option types', () => {
let changeStub;
let buildStub;
setup(() => {
sandbox = sinon.sandbox.create();
element = fixture('basic');
changeStub = sandbox.stub(element, '_handleChange');
buildStub = sandbox.stub(element, '_buildConfigChangeInfo');
});
teardown(() => sandbox.restore());
test('_computePluginConfigOptions', () => {
assert.deepEqual(element._computePluginConfigOptions(), []);
assert.deepEqual(element._computePluginConfigOptions({}), []);
assert.deepEqual(element._computePluginConfigOptions({base: {}}), []);
assert.deepEqual(element._computePluginConfigOptions(
{base: {config: {}}}), []);
assert.deepEqual(element._computePluginConfigOptions(
{base: {config: {testKey: 'testInfo'}}}),
[{_key: 'testKey', info: 'testInfo'}]);
});
test('_computeDisabled', () => {
assert.isFalse(element._computeDisabled('true'));
assert.isTrue(element._computeDisabled('false'));
});
test('_handleChange', () => {
const eventStub = sandbox.stub(element, 'dispatchEvent');
test('ARRAY type option', () => {
element.pluginData = {
name: 'testName',
config: {plugin: {value: 'test'}},
config: {plugin: {value: 'test', type: 'ARRAY'}},
};
element._handleChange({
_key: 'plugin',
info: {value: 'newTest'},
notifyPath: 'plugin.value',
});
flushAsynchronousOperations();
assert.isTrue(eventStub.called);
const {detail} = eventStub.lastCall.args[0];
assert.equal(detail.name, 'testName');
assert.deepEqual(detail.config, {plugin: {value: 'newTest'}});
assert.equal(detail.notifyPath, 'testName.plugin.value');
const editor = element.shadowRoot
.querySelector('gr-plugin-config-array-editor');
assert.ok(editor);
element._handleArrayChange({detail: 'test'});
assert.isTrue(changeStub.called);
assert.equal(changeStub.lastCall.args[0], 'test');
});
suite('option types', () => {
let changeStub;
let buildStub;
setup(() => {
changeStub = sandbox.stub(element, '_handleChange');
buildStub = sandbox.stub(element, '_buildConfigChangeInfo');
});
test('ARRAY type option', () => {
element.pluginData = {
name: 'testName',
config: {plugin: {value: 'test', type: 'ARRAY'}},
};
flushAsynchronousOperations();
const editor = element.shadowRoot
.querySelector('gr-plugin-config-array-editor');
assert.ok(editor);
element._handleArrayChange({detail: 'test'});
assert.isTrue(changeStub.called);
assert.equal(changeStub.lastCall.args[0], 'test');
});
test('BOOLEAN type option', () => {
element.pluginData = {
name: 'testName',
config: {plugin: {value: 'true', type: 'BOOLEAN'}},
};
flushAsynchronousOperations();
const toggle = element.shadowRoot
.querySelector('paper-toggle-button');
assert.ok(toggle);
toggle.click();
flushAsynchronousOperations();
assert.isTrue(buildStub.called);
assert.deepEqual(buildStub.lastCall.args, ['false', 'plugin']);
assert.isTrue(changeStub.called);
});
test('INT/LONG/STRING type option', () => {
element.pluginData = {
name: 'testName',
config: {plugin: {value: 'test', type: 'STRING'}},
};
flushAsynchronousOperations();
const input = element.shadowRoot
.querySelector('input');
assert.ok(input);
input.value = 'newTest';
input.dispatchEvent(new Event('input'));
flushAsynchronousOperations();
assert.isTrue(buildStub.called);
assert.deepEqual(buildStub.lastCall.args, ['newTest', 'plugin']);
assert.isTrue(changeStub.called);
});
test('LIST type option', () => {
const permitted_values = ['test', 'newTest'];
element.pluginData = {
name: 'testName',
config: {plugin: {value: 'test', type: 'LIST', permitted_values}},
};
flushAsynchronousOperations();
const select = element.shadowRoot
.querySelector('select');
assert.ok(select);
select.value = 'newTest';
select.dispatchEvent(new Event(
'change', {bubbles: true, composed: true}));
flushAsynchronousOperations();
assert.isTrue(buildStub.called);
assert.deepEqual(buildStub.lastCall.args, ['newTest', 'plugin']);
assert.isTrue(changeStub.called);
});
});
test('_buildConfigChangeInfo', () => {
test('BOOLEAN type option', () => {
element.pluginData = {
name: 'testName',
config: {plugin: {value: 'test'}},
config: {plugin: {value: 'true', type: 'BOOLEAN'}},
};
const detail = element._buildConfigChangeInfo('newTest', 'plugin');
assert.equal(detail._key, 'plugin');
assert.deepEqual(detail.info, {value: 'newTest'});
assert.equal(detail.notifyPath, 'plugin.value');
flushAsynchronousOperations();
const toggle = element.shadowRoot
.querySelector('paper-toggle-button');
assert.ok(toggle);
toggle.click();
flushAsynchronousOperations();
assert.isTrue(buildStub.called);
assert.deepEqual(buildStub.lastCall.args, ['false', 'plugin']);
assert.isTrue(changeStub.called);
});
test('INT/LONG/STRING type option', () => {
element.pluginData = {
name: 'testName',
config: {plugin: {value: 'test', type: 'STRING'}},
};
flushAsynchronousOperations();
const input = element.shadowRoot
.querySelector('input');
assert.ok(input);
input.value = 'newTest';
input.dispatchEvent(new Event('input'));
flushAsynchronousOperations();
assert.isTrue(buildStub.called);
assert.deepEqual(buildStub.lastCall.args, ['newTest', 'plugin']);
assert.isTrue(changeStub.called);
});
test('LIST type option', () => {
const permitted_values = ['test', 'newTest'];
element.pluginData = {
name: 'testName',
config: {plugin: {value: 'test', type: 'LIST', permitted_values}},
};
flushAsynchronousOperations();
const select = element.shadowRoot
.querySelector('select');
assert.ok(select);
select.value = 'newTest';
select.dispatchEvent(new Event(
'change', {bubbles: true, composed: true}));
flushAsynchronousOperations();
assert.isTrue(buildStub.called);
assert.deepEqual(buildStub.lastCall.args, ['newTest', 'plugin']);
assert.isTrue(changeStub.called);
});
});
test('_buildConfigChangeInfo', () => {
element.pluginData = {
name: 'testName',
config: {plugin: {value: 'test'}},
};
const detail = element._buildConfigChangeInfo('newTest', 'plugin');
assert.equal(detail._key, 'plugin');
assert.deepEqual(detail.info, {value: 'newTest'});
assert.equal(detail.notifyPath, 'plugin.value');
});
});
</script>

View File

@ -14,348 +14,366 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function() {
'use strict';
import '../../../scripts/bundled-polymer.js';
const STATES = {
active: {value: 'ACTIVE', label: 'Active'},
readOnly: {value: 'READ_ONLY', label: 'Read Only'},
hidden: {value: 'HIDDEN', label: 'Hidden'},
};
import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
import '@polymer/iron-input/iron-input.js';
import '../../../behaviors/fire-behavior/fire-behavior.js';
import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.js';
import '../../plugins/gr-endpoint-param/gr-endpoint-param.js';
import '../../shared/gr-download-commands/gr-download-commands.js';
import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
import '../../shared/gr-select/gr-select.js';
import '../../../styles/gr-form-styles.js';
import '../../../styles/gr-subpage-styles.js';
import '../../../styles/shared-styles.js';
import '../gr-repo-plugin-config/gr-repo-plugin-config.js';
import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
import {PolymerElement} from '@polymer/polymer/polymer-element.js';
import {htmlTemplate} from './gr-repo_html.js';
const SUBMIT_TYPES = {
// Exclude INHERIT, which is handled specially.
mergeIfNecessary: {
value: 'MERGE_IF_NECESSARY',
label: 'Merge if necessary',
},
fastForwardOnly: {
value: 'FAST_FORWARD_ONLY',
label: 'Fast forward only',
},
rebaseAlways: {
value: 'REBASE_ALWAYS',
label: 'Rebase Always',
},
rebaseIfNecessary: {
value: 'REBASE_IF_NECESSARY',
label: 'Rebase if necessary',
},
mergeAlways: {
value: 'MERGE_ALWAYS',
label: 'Merge always',
},
cherryPick: {
value: 'CHERRY_PICK',
label: 'Cherry pick',
},
};
const STATES = {
active: {value: 'ACTIVE', label: 'Active'},
readOnly: {value: 'READ_ONLY', label: 'Read Only'},
hidden: {value: 'HIDDEN', label: 'Hidden'},
};
/**
* @appliesMixin Gerrit.FireMixin
* @extends Polymer.Element
*/
class GrRepo extends Polymer.mixinBehaviors( [
Gerrit.FireBehavior,
], Polymer.GestureEventListeners(
Polymer.LegacyElementMixin(
Polymer.Element))) {
static get is() { return 'gr-repo'; }
const SUBMIT_TYPES = {
// Exclude INHERIT, which is handled specially.
mergeIfNecessary: {
value: 'MERGE_IF_NECESSARY',
label: 'Merge if necessary',
},
fastForwardOnly: {
value: 'FAST_FORWARD_ONLY',
label: 'Fast forward only',
},
rebaseAlways: {
value: 'REBASE_ALWAYS',
label: 'Rebase Always',
},
rebaseIfNecessary: {
value: 'REBASE_IF_NECESSARY',
label: 'Rebase if necessary',
},
mergeAlways: {
value: 'MERGE_ALWAYS',
label: 'Merge always',
},
cherryPick: {
value: 'CHERRY_PICK',
label: 'Cherry pick',
},
};
static get properties() {
return {
params: Object,
repo: String,
/**
* @appliesMixin Gerrit.FireMixin
* @extends Polymer.Element
*/
class GrRepo extends mixinBehaviors( [
Gerrit.FireBehavior,
], GestureEventListeners(
LegacyElementMixin(
PolymerElement))) {
static get template() { return htmlTemplate; }
_configChanged: {
type: Boolean,
value: false,
static get is() { return 'gr-repo'; }
static get properties() {
return {
params: Object,
repo: String,
_configChanged: {
type: Boolean,
value: false,
},
_loading: {
type: Boolean,
value: true,
},
_loggedIn: {
type: Boolean,
value: false,
observer: '_loggedInChanged',
},
/** @type {?} */
_repoConfig: Object,
/** @type {?} */
_pluginData: {
type: Array,
computed: '_computePluginData(_repoConfig.plugin_config.*)',
},
_readOnly: {
type: Boolean,
value: true,
},
_states: {
type: Array,
value() {
return Object.values(STATES);
},
_loading: {
type: Boolean,
value: true,
},
_submitTypes: {
type: Array,
value() {
return Object.values(SUBMIT_TYPES);
},
_loggedIn: {
type: Boolean,
value: false,
observer: '_loggedInChanged',
},
/** @type {?} */
_repoConfig: Object,
/** @type {?} */
_pluginData: {
type: Array,
computed: '_computePluginData(_repoConfig.plugin_config.*)',
},
_readOnly: {
type: Boolean,
value: true,
},
_states: {
type: Array,
value() {
return Object.values(STATES);
},
},
_submitTypes: {
type: Array,
value() {
return Object.values(SUBMIT_TYPES);
},
},
_schemes: {
type: Array,
value() { return []; },
computed: '_computeSchemes(_schemesObj)',
observer: '_schemesChanged',
},
_selectedCommand: {
type: String,
value: 'Clone',
},
_selectedScheme: String,
_schemesObj: Object,
};
}
},
_schemes: {
type: Array,
value() { return []; },
computed: '_computeSchemes(_schemesObj)',
observer: '_schemesChanged',
},
_selectedCommand: {
type: String,
value: 'Clone',
},
_selectedScheme: String,
_schemesObj: Object,
};
}
static get observers() {
return [
'_handleConfigChanged(_repoConfig.*)',
];
}
static get observers() {
return [
'_handleConfigChanged(_repoConfig.*)',
];
}
/** @override */
attached() {
super.attached();
this._loadRepo();
/** @override */
attached() {
super.attached();
this._loadRepo();
this.fire('title-change', {title: this.repo});
}
this.fire('title-change', {title: this.repo});
}
_computePluginData(configRecord) {
if (!configRecord ||
!configRecord.base) { return []; }
_computePluginData(configRecord) {
if (!configRecord ||
!configRecord.base) { return []; }
const pluginConfig = configRecord.base;
return Object.keys(pluginConfig)
.map(name => { return {name, config: pluginConfig[name]}; });
}
const pluginConfig = configRecord.base;
return Object.keys(pluginConfig)
.map(name => { return {name, config: pluginConfig[name]}; });
}
_loadRepo() {
if (!this.repo) { return Promise.resolve(); }
_loadRepo() {
if (!this.repo) { return Promise.resolve(); }
const promises = [];
const promises = [];
const errFn = response => {
this.fire('page-error', {response});
};
const errFn = response => {
this.fire('page-error', {response});
};
promises.push(this._getLoggedIn().then(loggedIn => {
this._loggedIn = loggedIn;
if (loggedIn) {
this.$.restAPI.getRepoAccess(this.repo).then(access => {
if (!access) { return Promise.resolve(); }
promises.push(this._getLoggedIn().then(loggedIn => {
this._loggedIn = loggedIn;
if (loggedIn) {
this.$.restAPI.getRepoAccess(this.repo).then(access => {
if (!access) { return Promise.resolve(); }
// If the user is not an owner, is_owner is not a property.
this._readOnly = !access[this.repo].is_owner;
});
}
}));
promises.push(this.$.restAPI.getProjectConfig(this.repo, errFn)
.then(config => {
if (!config) { return Promise.resolve(); }
if (config.default_submit_type) {
// The gr-select is bound to submit_type, which needs to be the
// *configured* submit type. When default_submit_type is
// present, the server reports the *effective* submit type in
// submit_type, so we need to overwrite it before storing the
// config in this.
config.submit_type =
config.default_submit_type.configured_value;
}
if (!config.state) {
config.state = STATES.active.value;
}
this._repoConfig = config;
this._loading = false;
}));
promises.push(this.$.restAPI.getConfig().then(config => {
if (!config) { return Promise.resolve(); }
this._schemesObj = config.download.schemes;
}));
return Promise.all(promises);
}
_computeLoadingClass(loading) {
return loading ? 'loading' : '';
}
_computeHideClass(arr) {
return !arr || !arr.length ? 'hide' : '';
}
_loggedInChanged(_loggedIn) {
if (!_loggedIn) { return; }
this.$.restAPI.getPreferences().then(prefs => {
if (prefs.download_scheme) {
// Note (issue 5180): normalize the download scheme with lower-case.
this._selectedScheme = prefs.download_scheme.toLowerCase();
}
});
}
_formatBooleanSelect(item) {
if (!item) { return; }
let inheritLabel = 'Inherit';
if (!(item.inherited_value === undefined)) {
inheritLabel = `Inherit (${item.inherited_value})`;
}
return [
{
label: inheritLabel,
value: 'INHERIT',
},
{
label: 'True',
value: 'TRUE',
}, {
label: 'False',
value: 'FALSE',
},
];
}
_formatSubmitTypeSelect(projectConfig) {
if (!projectConfig) { return; }
const allValues = Object.values(SUBMIT_TYPES);
const type = projectConfig.default_submit_type;
if (!type) {
// Server is too old to report default_submit_type, so assume INHERIT
// is not a valid value.
return allValues;
}
let inheritLabel = 'Inherit';
if (type.inherited_value) {
let inherited = type.inherited_value;
for (const val of allValues) {
if (val.value === type.inherited_value) {
inherited = val.label;
break;
}
}
inheritLabel = `Inherit (${inherited})`;
}
return [
{
label: inheritLabel,
value: 'INHERIT',
},
...allValues,
];
}
_isLoading() {
return this._loading || this._loading === undefined;
}
_getLoggedIn() {
return this.$.restAPI.getLoggedIn();
}
_formatRepoConfigForSave(repoConfig) {
const configInputObj = {};
for (const key in repoConfig) {
if (repoConfig.hasOwnProperty(key)) {
if (key === 'default_submit_type') {
// default_submit_type is not in the input type, and the
// configured value was already copied to submit_type by
// _loadProject. Omit this property when saving.
continue;
}
if (key === 'plugin_config') {
configInputObj.plugin_config_values = repoConfig[key];
} else if (typeof repoConfig[key] === 'object') {
configInputObj[key] = repoConfig[key].configured_value;
} else {
configInputObj[key] = repoConfig[key];
}
}
}
return configInputObj;
}
_handleSaveRepoConfig() {
return this.$.restAPI.saveRepoConfig(this.repo,
this._formatRepoConfigForSave(this._repoConfig)).then(() => {
this._configChanged = false;
});
}
_handleConfigChanged() {
if (this._isLoading()) { return; }
this._configChanged = true;
}
_computeButtonDisabled(readOnly, configChanged) {
return readOnly || !configChanged;
}
_computeHeaderClass(configChanged) {
return configChanged ? 'edited' : '';
}
_computeSchemes(schemesObj) {
return Object.keys(schemesObj);
}
_schemesChanged(schemes) {
if (schemes.length === 0) { return; }
if (!schemes.includes(this._selectedScheme)) {
this._selectedScheme = schemes.sort()[0];
}
}
_computeCommands(repo, schemesObj, _selectedScheme) {
if (!schemesObj || !repo || !_selectedScheme) {
return [];
}
const commands = [];
let commandObj;
if (schemesObj.hasOwnProperty(_selectedScheme)) {
commandObj = schemesObj[_selectedScheme].clone_commands;
}
for (const title in commandObj) {
if (!commandObj.hasOwnProperty(title)) { continue; }
commands.push({
title,
command: commandObj[title]
.replace(/\$\{project\}/gi, encodeURI(repo))
.replace(/\$\{project-base-name\}/gi,
encodeURI(repo.substring(repo.lastIndexOf('/') + 1))),
// If the user is not an owner, is_owner is not a property.
this._readOnly = !access[this.repo].is_owner;
});
}
return commands;
}));
promises.push(this.$.restAPI.getProjectConfig(this.repo, errFn)
.then(config => {
if (!config) { return Promise.resolve(); }
if (config.default_submit_type) {
// The gr-select is bound to submit_type, which needs to be the
// *configured* submit type. When default_submit_type is
// present, the server reports the *effective* submit type in
// submit_type, so we need to overwrite it before storing the
// config in this.
config.submit_type =
config.default_submit_type.configured_value;
}
if (!config.state) {
config.state = STATES.active.value;
}
this._repoConfig = config;
this._loading = false;
}));
promises.push(this.$.restAPI.getConfig().then(config => {
if (!config) { return Promise.resolve(); }
this._schemesObj = config.download.schemes;
}));
return Promise.all(promises);
}
_computeLoadingClass(loading) {
return loading ? 'loading' : '';
}
_computeHideClass(arr) {
return !arr || !arr.length ? 'hide' : '';
}
_loggedInChanged(_loggedIn) {
if (!_loggedIn) { return; }
this.$.restAPI.getPreferences().then(prefs => {
if (prefs.download_scheme) {
// Note (issue 5180): normalize the download scheme with lower-case.
this._selectedScheme = prefs.download_scheme.toLowerCase();
}
});
}
_formatBooleanSelect(item) {
if (!item) { return; }
let inheritLabel = 'Inherit';
if (!(item.inherited_value === undefined)) {
inheritLabel = `Inherit (${item.inherited_value})`;
}
return [
{
label: inheritLabel,
value: 'INHERIT',
},
{
label: 'True',
value: 'TRUE',
}, {
label: 'False',
value: 'FALSE',
},
];
}
_formatSubmitTypeSelect(projectConfig) {
if (!projectConfig) { return; }
const allValues = Object.values(SUBMIT_TYPES);
const type = projectConfig.default_submit_type;
if (!type) {
// Server is too old to report default_submit_type, so assume INHERIT
// is not a valid value.
return allValues;
}
_computeRepositoriesClass(config) {
return config ? 'showConfig': '';
let inheritLabel = 'Inherit';
if (type.inherited_value) {
let inherited = type.inherited_value;
for (const val of allValues) {
if (val.value === type.inherited_value) {
inherited = val.label;
break;
}
}
inheritLabel = `Inherit (${inherited})`;
}
return [
{
label: inheritLabel,
value: 'INHERIT',
},
...allValues,
];
}
_computeChangesUrl(name) {
return Gerrit.Nav.getUrlForProjectChanges(name);
_isLoading() {
return this._loading || this._loading === undefined;
}
_getLoggedIn() {
return this.$.restAPI.getLoggedIn();
}
_formatRepoConfigForSave(repoConfig) {
const configInputObj = {};
for (const key in repoConfig) {
if (repoConfig.hasOwnProperty(key)) {
if (key === 'default_submit_type') {
// default_submit_type is not in the input type, and the
// configured value was already copied to submit_type by
// _loadProject. Omit this property when saving.
continue;
}
if (key === 'plugin_config') {
configInputObj.plugin_config_values = repoConfig[key];
} else if (typeof repoConfig[key] === 'object') {
configInputObj[key] = repoConfig[key].configured_value;
} else {
configInputObj[key] = repoConfig[key];
}
}
}
return configInputObj;
}
_handlePluginConfigChanged({detail: {name, config, notifyPath}}) {
this._repoConfig.plugin_config[name] = config;
this.notifyPath('_repoConfig.plugin_config.' + notifyPath);
_handleSaveRepoConfig() {
return this.$.restAPI.saveRepoConfig(this.repo,
this._formatRepoConfigForSave(this._repoConfig)).then(() => {
this._configChanged = false;
});
}
_handleConfigChanged() {
if (this._isLoading()) { return; }
this._configChanged = true;
}
_computeButtonDisabled(readOnly, configChanged) {
return readOnly || !configChanged;
}
_computeHeaderClass(configChanged) {
return configChanged ? 'edited' : '';
}
_computeSchemes(schemesObj) {
return Object.keys(schemesObj);
}
_schemesChanged(schemes) {
if (schemes.length === 0) { return; }
if (!schemes.includes(this._selectedScheme)) {
this._selectedScheme = schemes.sort()[0];
}
}
customElements.define(GrRepo.is, GrRepo);
})();
_computeCommands(repo, schemesObj, _selectedScheme) {
if (!schemesObj || !repo || !_selectedScheme) {
return [];
}
const commands = [];
let commandObj;
if (schemesObj.hasOwnProperty(_selectedScheme)) {
commandObj = schemesObj[_selectedScheme].clone_commands;
}
for (const title in commandObj) {
if (!commandObj.hasOwnProperty(title)) { continue; }
commands.push({
title,
command: commandObj[title]
.replace(/\$\{project\}/gi, encodeURI(repo))
.replace(/\$\{project-base-name\}/gi,
encodeURI(repo.substring(repo.lastIndexOf('/') + 1))),
});
}
return commands;
}
_computeRepositoriesClass(config) {
return config ? 'showConfig': '';
}
_computeChangesUrl(name) {
return Gerrit.Nav.getUrlForProjectChanges(name);
}
_handlePluginConfigChanged({detail: {name, config, notifyPath}}) {
this._repoConfig.plugin_config[name] = config;
this.notifyPath('_repoConfig.plugin_config.' + notifyPath);
}
}
customElements.define(GrRepo.is, GrRepo);

View File

@ -1,37 +1,22 @@
<!--
@license
Copyright (C) 2017 The Android Open Source Project
/**
* @license
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {html} from '@polymer/polymer/lib/utils/html-tag.js';
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<link rel="import" href="/bower_components/polymer/polymer.html">
<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
<link rel="import" href="/bower_components/iron-input/iron-input.html">
<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
<link rel="import" href="../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.html">
<link rel="import" href="../../plugins/gr-endpoint-param/gr-endpoint-param.html">
<link rel="import" href="../../shared/gr-download-commands/gr-download-commands.html">
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
<link rel="import" href="../../shared/gr-select/gr-select.html">
<link rel="import" href="../../../styles/gr-form-styles.html">
<link rel="import" href="../../../styles/gr-subpage-styles.html">
<link rel="import" href="../../../styles/shared-styles.html">
<link rel="import" href="../gr-repo-plugin-config/gr-repo-plugin-config.html">
<dom-module id="gr-repo">
<template>
export const htmlTemplate = html`
<style include="shared-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
@ -65,50 +50,37 @@ limitations under the License.
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
<div class="info">
<h1 id="Title" class$="name">
<h1 id="Title" class\$="name">
[[repo]]
<hr/>
<hr>
</h1>
<div>
<a href$="[[_computeChangesUrl(repo)]]">(view changes)</a>
<a href\$="[[_computeChangesUrl(repo)]]">(view changes)</a>
</div>
</div>
<div id="loading" class$="[[_computeLoadingClass(_loading)]]">Loading...</div>
<div id="loadedContent" class$="[[_computeLoadingClass(_loading)]]">
<div id="downloadContent" class$="[[_computeHideClass(_schemes)]]">
<div id="loading" class\$="[[_computeLoadingClass(_loading)]]">Loading...</div>
<div id="loadedContent" class\$="[[_computeLoadingClass(_loading)]]">
<div id="downloadContent" class\$="[[_computeHideClass(_schemes)]]">
<h2 id="download">Download</h2>
<fieldset>
<gr-download-commands
id="downloadCommands"
commands="[[_computeCommands(repo, _schemesObj, _selectedScheme)]]"
schemes="[[_schemes]]"
selected-scheme="{{_selectedScheme}}"></gr-download-commands>
<gr-download-commands id="downloadCommands" commands="[[_computeCommands(repo, _schemesObj, _selectedScheme)]]" schemes="[[_schemes]]" selected-scheme="{{_selectedScheme}}"></gr-download-commands>
</fieldset>
</div>
<h2 id="configurations"
class$="[[_computeHeaderClass(_configChanged)]]">Configurations</h2>
<h2 id="configurations" class\$="[[_computeHeaderClass(_configChanged)]]">Configurations</h2>
<div id="form">
<fieldset>
<h3 id="Description">Description</h3>
<fieldset>
<iron-autogrow-textarea
id="descriptionInput"
class="description"
autocomplete="on"
placeholder="<Insert repo description here>"
bind-value="{{_repoConfig.description}}"
disabled$="[[_readOnly]]"></iron-autogrow-textarea>
<iron-autogrow-textarea id="descriptionInput" class="description" autocomplete="on" placeholder="<Insert repo description here>" bind-value="{{_repoConfig.description}}" disabled\$="[[_readOnly]]"></iron-autogrow-textarea>
</fieldset>
<h3 id="Options">Repository Options</h3>
<fieldset id="options">
<section>
<span class="title">State</span>
<span class="value">
<gr-select
id="stateSelect"
bind-value="{{_repoConfig.state}}">
<select disabled$="[[_readOnly]]">
<template is="dom-repeat" items=[[_states]]>
<gr-select id="stateSelect" bind-value="{{_repoConfig.state}}">
<select disabled\$="[[_readOnly]]">
<template is="dom-repeat" items="[[_states]]">
<option value="[[item.value]]">[[item.label]]</option>
</template>
</select>
@ -118,12 +90,9 @@ limitations under the License.
<section>
<span class="title">Submit type</span>
<span class="value">
<gr-select
id="submitTypeSelect"
bind-value="{{_repoConfig.submit_type}}">
<select disabled$="[[_readOnly]]">
<template is="dom-repeat"
items="[[_formatSubmitTypeSelect(_repoConfig)]]">
<gr-select id="submitTypeSelect" bind-value="{{_repoConfig.submit_type}}">
<select disabled\$="[[_readOnly]]">
<template is="dom-repeat" items="[[_formatSubmitTypeSelect(_repoConfig)]]">
<option value="[[item.value]]">[[item.label]]</option>
</template>
</select>
@ -133,12 +102,9 @@ limitations under the License.
<section>
<span class="title">Allow content merges</span>
<span class="value">
<gr-select
id="contentMergeSelect"
bind-value="{{_repoConfig.use_content_merge.configured_value}}">
<select disabled$="[[_readOnly]]">
<template is="dom-repeat"
items="[[_formatBooleanSelect(_repoConfig.use_content_merge)]]">
<gr-select id="contentMergeSelect" bind-value="{{_repoConfig.use_content_merge.configured_value}}">
<select disabled\$="[[_readOnly]]">
<template is="dom-repeat" items="[[_formatBooleanSelect(_repoConfig.use_content_merge)]]">
<option value="[[item.value]]">[[item.label]]</option>
</template>
</select>
@ -150,12 +116,9 @@ limitations under the License.
Create a new change for every commit not in the target branch
</span>
<span class="value">
<gr-select
id="newChangeSelect"
bind-value="{{_repoConfig.create_new_change_for_all_not_in_target.configured_value}}">
<select disabled$="[[_readOnly]]">
<template is="dom-repeat"
items="[[_formatBooleanSelect(_repoConfig.create_new_change_for_all_not_in_target)]]">
<gr-select id="newChangeSelect" bind-value="{{_repoConfig.create_new_change_for_all_not_in_target.configured_value}}">
<select disabled\$="[[_readOnly]]">
<template is="dom-repeat" items="[[_formatBooleanSelect(_repoConfig.create_new_change_for_all_not_in_target)]]">
<option value="[[item.value]]">[[item.label]]</option>
</template>
</select>
@ -165,46 +128,33 @@ limitations under the License.
<section>
<span class="title">Require Change-Id in commit message</span>
<span class="value">
<gr-select
id="requireChangeIdSelect"
bind-value="{{_repoConfig.require_change_id.configured_value}}">
<select disabled$="[[_readOnly]]">
<template is="dom-repeat"
items="[[_formatBooleanSelect(_repoConfig.require_change_id)]]">
<gr-select id="requireChangeIdSelect" bind-value="{{_repoConfig.require_change_id.configured_value}}">
<select disabled\$="[[_readOnly]]">
<template is="dom-repeat" items="[[_formatBooleanSelect(_repoConfig.require_change_id)]]">
<option value="[[item.value]]">[[item.label]]</option>
</template>
</select>
</gr-select>
</span>
</section>
<section
id="enableSignedPushSettings"
class$="repositorySettings [[_computeRepositoriesClass(_repoConfig.enable_signed_push)]]">
<section id="enableSignedPushSettings" class\$="repositorySettings [[_computeRepositoriesClass(_repoConfig.enable_signed_push)]]">
<span class="title">Enable signed push</span>
<span class="value">
<gr-select
id="enableSignedPush"
bind-value="{{_repoConfig.enable_signed_push.configured_value}}">
<select disabled$="[[_readOnly]]">
<template is="dom-repeat"
items="[[_formatBooleanSelect(_repoConfig.enable_signed_push)]]">
<gr-select id="enableSignedPush" bind-value="{{_repoConfig.enable_signed_push.configured_value}}">
<select disabled\$="[[_readOnly]]">
<template is="dom-repeat" items="[[_formatBooleanSelect(_repoConfig.enable_signed_push)]]">
<option value="[[item.value]]">[[item.label]]</option>
</template>
</select>
</gr-select>
</span>
</section>
<section
id="requireSignedPushSettings"
class$="repositorySettings [[_computeRepositoriesClass(_repoConfig.require_signed_push)]]">
<section id="requireSignedPushSettings" class\$="repositorySettings [[_computeRepositoriesClass(_repoConfig.require_signed_push)]]">
<span class="title">Require signed push</span>
<span class="value">
<gr-select
id="requireSignedPush"
bind-value="{{_repoConfig.require_signed_push.configured_value}}">
<select disabled$="[[_readOnly]]">
<template is="dom-repeat"
items="[[_formatBooleanSelect(_repoConfig.require_signed_push)]]">
<gr-select id="requireSignedPush" bind-value="{{_repoConfig.require_signed_push.configured_value}}">
<select disabled\$="[[_readOnly]]">
<template is="dom-repeat" items="[[_formatBooleanSelect(_repoConfig.require_signed_push)]]">
<option value="[[item.value]]">[[item.label]]</option>
</template>
</select>
@ -215,12 +165,9 @@ limitations under the License.
<span class="title">
Reject implicit merges when changes are pushed for review</span>
<span class="value">
<gr-select
id="rejectImplicitMergesSelect"
bind-value="{{_repoConfig.reject_implicit_merges.configured_value}}">
<select disabled$="[[_readOnly]]">
<template is="dom-repeat"
items="[[_formatBooleanSelect(_repoConfig.reject_implicit_merges)]]">
<gr-select id="rejectImplicitMergesSelect" bind-value="{{_repoConfig.reject_implicit_merges.configured_value}}">
<select disabled\$="[[_readOnly]]">
<template is="dom-repeat" items="[[_formatBooleanSelect(_repoConfig.reject_implicit_merges)]]">
<option value="[[item.value]]">[[item.label]]</option>
</template>
</select>
@ -231,12 +178,9 @@ limitations under the License.
<span class="title">
Enable adding unregistered users as reviewers and CCs on changes</span>
<span class="value">
<gr-select
id="unRegisteredCcSelect"
bind-value="{{_repoConfig.enable_reviewer_by_email.configured_value}}">
<select disabled$="[[_readOnly]]">
<template is="dom-repeat"
items="[[_formatBooleanSelect(_repoConfig.enable_reviewer_by_email)]]">
<gr-select id="unRegisteredCcSelect" bind-value="{{_repoConfig.enable_reviewer_by_email.configured_value}}">
<select disabled\$="[[_readOnly]]">
<template is="dom-repeat" items="[[_formatBooleanSelect(_repoConfig.enable_reviewer_by_email)]]">
<option value="[[item.value]]">[[item.label]]</option>
</template>
</select>
@ -247,12 +191,9 @@ limitations under the License.
<span class="title">
Set all new changes private by default</span>
<span class="value">
<gr-select
id="setAllnewChangesPrivateByDefaultSelect"
bind-value="{{_repoConfig.private_by_default.configured_value}}">
<select disabled$="[[_readOnly]]">
<template is="dom-repeat"
items="[[_formatBooleanSelect(_repoConfig.private_by_default)]]">
<gr-select id="setAllnewChangesPrivateByDefaultSelect" bind-value="{{_repoConfig.private_by_default.configured_value}}">
<select disabled\$="[[_readOnly]]">
<template is="dom-repeat" items="[[_formatBooleanSelect(_repoConfig.private_by_default)]]">
<option value="[[item.value]]">[[item.label]]</option>
</template>
</select>
@ -263,12 +204,9 @@ limitations under the License.
<span class="title">
Set new changes to "work in progress" by default</span>
<span class="value">
<gr-select
id="setAllNewChangesWorkInProgressByDefaultSelect"
bind-value="{{_repoConfig.work_in_progress_by_default.configured_value}}">
<select disabled$="[[_readOnly]]">
<template is="dom-repeat"
items="[[_formatBooleanSelect(_repoConfig.work_in_progress_by_default)]]">
<gr-select id="setAllNewChangesWorkInProgressByDefaultSelect" bind-value="{{_repoConfig.work_in_progress_by_default.configured_value}}">
<select disabled\$="[[_readOnly]]">
<template is="dom-repeat" items="[[_formatBooleanSelect(_repoConfig.work_in_progress_by_default)]]">
<option value="[[item.value]]">[[item.label]]</option>
</template>
</select>
@ -278,17 +216,8 @@ limitations under the License.
<section>
<span class="title">Maximum Git object size limit</span>
<span class="value">
<iron-input
id="maxGitObjSizeIronInput"
bind-value="{{_repoConfig.max_object_size_limit.configured_value}}"
type="text"
disabled$="[[_readOnly]]">
<input
id="maxGitObjSizeInput"
bind-value="{{_repoConfig.max_object_size_limit.configured_value}}"
is="iron-input"
type="text"
disabled$="[[_readOnly]]">
<iron-input id="maxGitObjSizeIronInput" bind-value="{{_repoConfig.max_object_size_limit.configured_value}}" type="text" disabled\$="[[_readOnly]]">
<input id="maxGitObjSizeInput" bind-value="{{_repoConfig.max_object_size_limit.configured_value}}" is="iron-input" type="text" disabled\$="[[_readOnly]]">
</iron-input>
<template is="dom-if" if="[[_repoConfig.max_object_size_limit.value]]">
effective: [[_repoConfig.max_object_size_limit.value]] bytes
@ -298,12 +227,9 @@ limitations under the License.
<section>
<span class="title">Match authored date with committer date upon submit</span>
<span class="value">
<gr-select
id="matchAuthoredDateWithCommitterDateSelect"
bind-value="{{_repoConfig.match_author_to_committer_date.configured_value}}">
<select disabled$="[[_readOnly]]">
<template is="dom-repeat"
items="[[_formatBooleanSelect(_repoConfig.match_author_to_committer_date)]]">
<gr-select id="matchAuthoredDateWithCommitterDateSelect" bind-value="{{_repoConfig.match_author_to_committer_date.configured_value}}">
<select disabled\$="[[_readOnly]]">
<template is="dom-repeat" items="[[_formatBooleanSelect(_repoConfig.match_author_to_committer_date)]]">
<option value="[[item.value]]">[[item.label]]</option>
</template>
</select>
@ -313,12 +239,9 @@ limitations under the License.
<section>
<span class="title">Reject empty commit upon submit</span>
<span class="value">
<gr-select
id="rejectEmptyCommitSelect"
bind-value="{{_repoConfig.reject_empty_commit.configured_value}}">
<select disabled$="[[_readOnly]]">
<template is="dom-repeat"
items="[[_formatBooleanSelect(_repoConfig.reject_empty_commit)]]">
<gr-select id="rejectEmptyCommitSelect" bind-value="{{_repoConfig.reject_empty_commit.configured_value}}">
<select disabled\$="[[_readOnly]]">
<template is="dom-repeat" items="[[_formatBooleanSelect(_repoConfig.reject_empty_commit)]]">
<option value="[[item.value]]">[[item.label]]</option>
</template>
</select>
@ -332,12 +255,9 @@ limitations under the License.
<span class="title">
Require a valid contributor agreement to upload</span>
<span class="value">
<gr-select
id="contributorAgreementSelect"
bind-value="{{_repoConfig.use_contributor_agreements.configured_value}}">
<select disabled$="[[_readOnly]]">
<template is="dom-repeat"
items="[[_formatBooleanSelect(_repoConfig.use_contributor_agreements)]]">
<gr-select id="contributorAgreementSelect" bind-value="{{_repoConfig.use_contributor_agreements.configured_value}}">
<select disabled\$="[[_readOnly]]">
<template is="dom-repeat" items="[[_formatBooleanSelect(_repoConfig.use_contributor_agreements)]]">
<option value="[[item.value]]">[[item.label]]</option>
</template>
</select>
@ -347,12 +267,9 @@ limitations under the License.
<section>
<span class="title">Require Signed-off-by in commit message</span>
<span class="value">
<gr-select
id="useSignedOffBySelect"
bind-value="{{_repoConfig.use_signed_off_by.configured_value}}">
<select disabled$="[[_readOnly]]">
<template is="dom-repeat"
items="[[_formatBooleanSelect(_repoConfig.use_signed_off_by)]]">
<gr-select id="useSignedOffBySelect" bind-value="{{_repoConfig.use_signed_off_by.configured_value}}">
<select disabled\$="[[_readOnly]]">
<template is="dom-repeat" items="[[_formatBooleanSelect(_repoConfig.use_signed_off_by)]]">
<option value="[[item.value]]">[[item.label]]</option>
</template>
</select>
@ -360,18 +277,13 @@ limitations under the License.
</span>
</section>
</fieldset>
<div
class$="pluginConfig [[_computeHideClass(_pluginData)]]"
on-plugin-config-changed="_handlePluginConfigChanged">
<div class\$="pluginConfig [[_computeHideClass(_pluginData)]]" on-plugin-config-changed="_handlePluginConfigChanged">
<h3>Plugins</h3>
<template is="dom-repeat" items="[[_pluginData]]" as="data">
<gr-repo-plugin-config
plugin-data="[[data]]"></gr-repo-plugin-config>
<gr-repo-plugin-config plugin-data="[[data]]"></gr-repo-plugin-config>
</template>
</div>
<gr-button
on-click="_handleSaveRepoConfig"
disabled$="[[_computeButtonDisabled(_readOnly, _configChanged)]]">Save changes</gr-button>
<gr-button on-click="_handleSaveRepoConfig" disabled\$="[[_computeButtonDisabled(_readOnly, _configChanged)]]">Save changes</gr-button>
</fieldset>
<gr-endpoint-decorator name="repo-config">
<gr-endpoint-param name="repoName" value="[[repo]]"></gr-endpoint-param>
@ -381,6 +293,4 @@ limitations under the License.
</div>
</main>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
</template>
<script src="gr-repo.js"></script>
</dom-module>
`;

Some files were not shown because too many files have changed in this diff Show More