Commit fa159909 authored by peterhegman's avatar peterhegman

Add projects field to personal access token form

Projects selector will be implemented in follow-up MRs
parent f43ee18b
<script>
import { GlFormGroup, GlFormRadio, GlFormText } from '@gitlab/ui';
export default {
name: 'ProjectsField',
ALL_PROJECTS: 'ALL_PROJECTS',
SELECTED_PROJECTS: 'SELECTED_PROJECTS',
components: { GlFormGroup, GlFormRadio, GlFormText },
props: {
inputAttrs: {
type: Object,
required: true,
},
},
data() {
return {
selectedRadio: this.$options.ALL_PROJECTS,
};
},
};
</script>
<template>
<div>
<gl-form-group :label="__('Projects')" label-class="gl-pb-0!">
<gl-form-text class="gl-pb-3">{{
__('Set access permissions for this token.')
}}</gl-form-text>
<gl-form-radio v-model="selectedRadio" :value="$options.ALL_PROJECTS">{{
__('All projects')
}}</gl-form-radio>
<gl-form-radio v-model="selectedRadio" :value="$options.SELECTED_PROJECTS">{{
__('Selected projects')
}}</gl-form-radio>
<input :id="inputAttrs.id" type="hidden" :name="inputAttrs.name" />
</gl-form-group>
</div>
</template>
import Vue from 'vue';
import ExpiresAtField from './components/expires_at_field.vue';
import ProjectsField from './components/projects_field.vue';
const getInputAttrs = (el) => {
const input = el.querySelector('input');
......@@ -11,7 +12,7 @@ const getInputAttrs = (el) => {
};
};
const initExpiresAtField = () => {
export const initExpiresAtField = () => {
const el = document.querySelector('.js-access-tokens-expires-at');
if (!el) {
......@@ -32,4 +33,23 @@ const initExpiresAtField = () => {
});
};
export default initExpiresAtField;
export const initProjectsField = () => {
const el = document.querySelector('.js-access-tokens-projects');
if (!el) {
return null;
}
const inputAttrs = getInputAttrs(el);
return new Vue({
el,
render(h) {
return h(ProjectsField, {
props: {
inputAttrs,
},
});
},
});
};
import initExpiresAtField from '~/access_tokens';
import { initExpiresAtField } from '~/access_tokens';
document.addEventListener('DOMContentLoaded', initExpiresAtField);
initExpiresAtField();
import initExpiresAtField from '~/access_tokens';
import { initExpiresAtField } from '~/access_tokens';
import createFlash from '~/flash';
import { __ } from '~/locale';
document.addEventListener('DOMContentLoaded', initExpiresAtField);
initExpiresAtField();
if (window.gon.features.personalAccessTokensScopedToProjects) {
import('~/access_tokens')
.then(({ initProjectsField }) => {
initProjectsField();
})
.catch(() => {
createFlash(__('An error occurred while loading the access tokens form, please try again.'));
});
}
import initExpiresAtField from '~/access_tokens';
import { initExpiresAtField } from '~/access_tokens';
initExpiresAtField();
......@@ -3,6 +3,10 @@
class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
feature_category :authentication_and_authorization
before_action do
push_frontend_feature_flag(:personal_access_tokens_scoped_to_projects, current_user)
end
def index
set_index_vars
@personal_access_token = finder.build
......
......@@ -29,5 +29,9 @@
= f.label :scopes, _('Scopes'), class: 'label-bold'
= render 'shared/tokens/scopes_form', prefix: prefix, token: token, scopes: scopes
- if prefix == :personal_access_token && Feature.enabled?(:personal_access_tokens_scoped_to_projects, current_user)
.js-access-tokens-projects
%input{ type: 'hidden', name: 'temporary-name', id: 'temporary-id' }
.gl-mt-3
= f.submit _('Create %{type}') % { type: type }, class: 'gl-button btn btn-success', data: { qa_selector: 'create_token_button' }
---
name: personal_access_tokens_scoped_to_projects
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/54617
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/322187
milestone: '13.10'
type: development
group: group::access
default_enabled: false
......@@ -3433,6 +3433,9 @@ msgstr ""
msgid "An error occurred while loading project creation UI"
msgstr ""
msgid "An error occurred while loading the access tokens form, please try again."
msgstr ""
msgid "An error occurred while loading the data. Please try again."
msgstr ""
......@@ -26734,6 +26737,9 @@ msgstr ""
msgid "Selected levels cannot be used by non-admin users for groups, projects or snippets. If the public level is restricted, user profiles are only visible to logged in users."
msgstr ""
msgid "Selected projects"
msgstr ""
msgid "Selecting a GitLab user will add a link to the GitLab user in the descriptions of issues and comments (e.g. \"By %{link_open}@johnsmith%{link_close}\"). It will also associate and/or assign these issues and comments with the selected user."
msgstr ""
......@@ -26953,6 +26959,9 @@ msgstr ""
msgid "Set a template repository for projects in this group"
msgstr ""
msgid "Set access permissions for this token."
msgstr ""
msgid "Set an instance-wide domain that will be available to all clusters when installing Knative."
msgstr ""
......
......@@ -138,4 +138,10 @@ RSpec.describe 'Profile > Personal Access Tokens', :js do
end
end
end
it 'pushes `personal_access_tokens_scoped_to_projects` feature flag to the frontend' do
visit profile_personal_access_tokens_path
expect(page).to have_pushed_frontend_feature_flags(personalAccessTokensScopedToProjects: true)
end
end
import { within } from '@testing-library/dom';
import { mount } from '@vue/test-utils';
import ProjectsField from '~/access_tokens/components/projects_field.vue';
describe('ProjectsField', () => {
let wrapper;
const createComponent = () => {
wrapper = mount(ProjectsField, {
propsData: {
inputAttrs: {
id: 'projects',
name: 'projects',
},
},
});
};
const queryByLabelText = (text) => within(wrapper.element).queryByLabelText(text);
const queryByText = (text) => within(wrapper.element).queryByText(text);
beforeEach(() => {
createComponent();
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('renders label and sub-label', () => {
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 = queryByLabelText('All projects');
expect(allProjectsRadio).not.toBe(null);
expect(allProjectsRadio.checked).toBe(true);
});
it('renders "Selected projects" radio unchecked by default', () => {
const selectedProjectsRadio = queryByLabelText('Selected projects');
expect(selectedProjectsRadio).not.toBe(null);
expect(selectedProjectsRadio.checked).toBe(false);
});
it('renders hidden input with correct `name` and `id` attributes', () => {
expect(wrapper.find('input[type="hidden"]').attributes()).toEqual(
expect.objectContaining({
id: 'projects',
name: 'projects',
}),
);
});
});
import { createWrapper } from '@vue/test-utils';
import { initExpiresAtField, initProjectsField } from '~/access_tokens';
import ExpiresAtField from '~/access_tokens/components/expires_at_field.vue';
import ProjectsField from '~/access_tokens/components/projects_field.vue';
describe('access tokens', () => {
afterEach(() => {
document.body.innerHTML = '';
});
describe.each`
initFunction | mountSelector | expectedComponent
${initExpiresAtField} | ${'js-access-tokens-expires-at'} | ${ExpiresAtField}
${initProjectsField} | ${'js-access-tokens-projects'} | ${ProjectsField}
`('$initFunction', ({ initFunction, mountSelector, expectedComponent }) => {
describe('when mount element exists', () => {
beforeEach(() => {
const mountEl = document.createElement('div');
mountEl.classList.add(mountSelector);
const input = document.createElement('input');
input.setAttribute('name', 'foo-bar');
input.setAttribute('id', 'foo-bar');
input.setAttribute('placeholder', 'Foo bar');
mountEl.appendChild(input);
document.body.appendChild(mountEl);
});
it(`mounts component and sets \`inputAttrs\` prop`, () => {
const wrapper = createWrapper(initFunction());
const component = wrapper.findComponent(expectedComponent);
expect(component.exists()).toBe(true);
expect(component.props('inputAttrs')).toEqual({
name: 'foo-bar',
id: 'foo-bar',
placeholder: 'Foo bar',
});
});
});
describe('when mount element does not exist', () => {
it('returns `null`', () => {
expect(initFunction()).toBe(null);
});
});
});
});
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