Commit 8ecda454 authored by Vitaly Slobodin's avatar Vitaly Slobodin

Merge branch...

Merge branch '22754-restrict-personal-access-tokens-to-specific-projects-remeber-projects' into 'master'

Access token form - render selected projects on page load

See merge request gitlab-org/gitlab!55805
parents d6a91a37 3a14b9ac
......@@ -15,7 +15,9 @@ export default {
},
data() {
return {
selectedRadio: this.$options.ALL_PROJECTS,
selectedRadio: !this.inputAttrs.value
? this.$options.ALL_PROJECTS
: this.$options.SELECTED_PROJECTS,
selectedProjects: [],
};
},
......@@ -28,6 +30,13 @@ export default {
? null
: this.selectedProjects.map((project) => project.id).join(',');
},
initialProjectIds() {
if (!this.inputAttrs.value) {
return [];
}
return this.inputAttrs.value.split(',');
},
},
methods: {
handleTokenSelectorFocus() {
......@@ -50,7 +59,11 @@ export default {
__('Selected projects')
}}</gl-form-radio>
<input :id="inputAttrs.id" type="hidden" :name="inputAttrs.name" :value="hiddenInputValue" />
<projects-token-selector v-model="selectedProjects" @focus="handleTokenSelectorFocus" />
<projects-token-selector
v-model="selectedProjects"
:initial-project-ids="initialProjectIds"
@focus="handleTokenSelectorFocus"
/>
</gl-form-group>
</div>
</template>
......@@ -8,12 +8,13 @@ import {
} from '@gitlab/ui';
import produce from 'immer';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { getIdFromGraphQLId, convertToGraphQLId } from '~/graphql_shared/utils';
import getProjectsQuery from '../graphql/queries/get_projects.query.graphql';
const DEBOUNCE_DELAY = 250;
const PROJECTS_PER_PAGE = 20;
const GRAPHQL_ENTITY_TYPE = 'Project';
export default {
name: 'ProjectsTokenSelector',
......@@ -32,6 +33,10 @@ export default {
type: Array,
required: true,
},
initialProjectIds: {
type: Array,
required: true,
},
},
apollo: {
projects: {
......@@ -46,10 +51,7 @@ export default {
},
update({ projects }) {
return {
list: projects.nodes.map((project) => ({
...project,
id: getIdFromGraphQLId(project.id),
})),
list: this.formatProjectNodes(projects),
pageInfo: projects.pageInfo,
};
},
......@@ -58,6 +60,21 @@ export default {
this.isSearching = false;
},
},
initialProjects: {
query: getProjectsQuery,
variables() {
return {
ids: this.initialProjectIds.map((id) => convertToGraphQLId(GRAPHQL_ENTITY_TYPE, id)),
};
},
manual: true,
skip() {
return !this.initialProjectIds.length;
},
result({ data: { projects } }) {
this.$emit('input', this.formatProjectNodes(projects));
},
},
},
data() {
return {
......@@ -71,6 +88,12 @@ export default {
};
},
methods: {
formatProjectNodes(projects) {
return projects.nodes.map((project) => ({
...project,
id: getIdFromGraphQLId(project.id),
}));
},
handleSearch(query) {
this.isSearching = true;
this.searchQuery = query;
......
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
query getProjects($search: String!, $after: String = "", $first: Int!) {
query getProjects(
$search: String = ""
$after: String = ""
$first: Int = null
$ids: [ID!] = null
) {
projects(
search: $search
after: $after
first: $first
ids: $ids
membership: true
searchNamespaces: true
sort: "UPDATED_ASC"
......
......@@ -10,6 +10,7 @@ const getInputAttrs = (el) => {
return {
id: input.id,
name: input.name,
value: input.value,
placeholder: input.placeholder,
};
};
......
......@@ -6,12 +6,13 @@ import ProjectsTokenSelector from '~/access_tokens/components/projects_token_sel
describe('ProjectsField', () => {
let wrapper;
const createComponent = () => {
const createComponent = ({ inputAttrsValue = '' } = {}) => {
wrapper = mount(ProjectsField, {
propsData: {
inputAttrs: {
id: 'projects',
name: 'projects',
value: inputAttrsValue,
},
},
});
......@@ -24,39 +25,63 @@ describe('ProjectsField', () => {
const findProjectsTokenSelector = () => wrapper.findComponent(ProjectsTokenSelector);
const findHiddenInput = () => wrapper.find('input[type="hidden"]');
beforeEach(() => {
createComponent();
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('renders label and sub-label', () => {
createComponent();
expect(queryByText('Projects')).not.toBe(null);
expect(queryByText('Set access permissions for this token.')).not.toBe(null);
});
it('renders "All projects" radio selected by default', () => {
const allProjectsRadio = findAllProjectsRadio();
describe('when `inputAttrs.value` is empty', () => {
beforeEach(() => {
createComponent();
});
it('renders "All projects" radio as checked', () => {
expect(findAllProjectsRadio().checked).toBe(true);
});
it('renders "Selected projects" radio as unchecked', () => {
expect(findSelectedProjectsRadio().checked).toBe(false);
});
it('sets `projects-token-selector` `initialProjectIds` prop to an empty array', () => {
expect(findProjectsTokenSelector().props('initialProjectIds')).toEqual([]);
});
});
expect(allProjectsRadio).not.toBe(null);
expect(allProjectsRadio.checked).toBe(true);
describe('when `inputAttrs.value` is a comma separated list of project IDs', () => {
beforeEach(() => {
createComponent({ inputAttrsValue: '1,2' });
});
it('renders "Selected projects" radio unchecked by default', () => {
const selectedProjectsRadio = findSelectedProjectsRadio();
it('renders "All projects" radio as unchecked', () => {
expect(findAllProjectsRadio().checked).toBe(false);
});
it('renders "Selected projects" radio as checked', () => {
expect(findSelectedProjectsRadio().checked).toBe(true);
});
expect(selectedProjectsRadio).not.toBe(null);
expect(selectedProjectsRadio.checked).toBe(false);
it('sets `projects-token-selector` `initialProjectIds` prop to an array of project IDs', () => {
expect(findProjectsTokenSelector().props('initialProjectIds')).toEqual(['1', '2']);
});
});
it('renders `projects-token-selector` component', () => {
createComponent();
expect(findProjectsTokenSelector().exists()).toBe(true);
});
it('renders hidden input with correct `name` and `id` attributes', () => {
createComponent();
expect(findHiddenInput().attributes()).toEqual(
expect.objectContaining({
id: 'projects',
......@@ -67,6 +92,8 @@ describe('ProjectsField', () => {
describe('when `projects-token-selector` is focused', () => {
beforeEach(() => {
createComponent();
findProjectsTokenSelector().vm.$emit('focus');
});
......
......@@ -44,10 +44,15 @@ describe('ProjectsTokenSelector', () => {
let wrapper;
let resolveGetProjectsQuery;
let resolveGetInitialProjectsQuery;
const getProjectsQueryRequestHandler = jest.fn(
() =>
({ ids }) =>
new Promise((resolve) => {
if (ids) {
resolveGetInitialProjectsQuery = resolve;
} else {
resolveGetProjectsQuery = resolve;
}
}),
);
......@@ -63,6 +68,7 @@ describe('ProjectsTokenSelector', () => {
apolloProvider,
propsData: {
selectedProjects: [],
initialProjectIds: [],
...propsData,
},
stubs: ['gl-intersection-observer'],
......@@ -156,6 +162,7 @@ describe('ProjectsTokenSelector', () => {
search: searchTerm,
after: null,
first: 20,
ids: null,
});
});
......@@ -181,6 +188,7 @@ describe('ProjectsTokenSelector', () => {
after: pageInfo.endCursor,
first: 20,
search: '',
ids: null,
});
});
......@@ -221,4 +229,41 @@ describe('ProjectsTokenSelector', () => {
expect(wrapper.emitted('focus')[0]).toEqual([event]);
});
});
describe('when `initialProjectIds` is an empty array', () => {
it('does not request initial projects', async () => {
await createComponent();
expect(getProjectsQueryRequestHandler).toHaveBeenCalledTimes(1);
expect(getProjectsQueryRequestHandler).toHaveBeenCalledWith(
expect.objectContaining({
ids: null,
}),
);
});
});
describe('when `initialProjectIds` is an array of project IDs', () => {
it('requests those projects and emits `input` event with result', async () => {
await createComponent({
propsData: {
initialProjectIds: [getIdFromGraphQLId(project1.id), getIdFromGraphQLId(project2.id)],
},
});
resolveGetInitialProjectsQuery(getProjectsQueryResponse);
await waitForPromises();
expect(getProjectsQueryRequestHandler).toHaveBeenCalledWith({
after: '',
first: null,
search: '',
ids: [project1.id, project2.id],
});
expect(wrapper.emitted('input')[0][0]).toEqual([
{ ...project1, id: getIdFromGraphQLId(project1.id) },
{ ...project2, id: getIdFromGraphQLId(project2.id) },
]);
});
});
});
......@@ -38,6 +38,7 @@ describe('access tokens', () => {
input.setAttribute('name', 'foo-bar');
input.setAttribute('id', 'foo-bar');
input.setAttribute('placeholder', 'Foo bar');
input.setAttribute('value', '1,2');
mountEl.appendChild(input);
......@@ -58,6 +59,7 @@ describe('access tokens', () => {
expect(component.props('inputAttrs')).toEqual({
name: 'foo-bar',
id: 'foo-bar',
value: '1,2',
placeholder: 'Foo bar',
});
});
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment