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