diff --git a/awx/ui_next/src/api/Base.js b/awx/ui_next/src/api/Base.js index a5f62d0cd9..ef7c53460f 100644 --- a/awx/ui_next/src/api/Base.js +++ b/awx/ui_next/src/api/Base.js @@ -1,48 +1,46 @@ import axios from 'axios'; -import { - encodeQueryString -} from '@util/qs'; +import { encodeQueryString } from '@util/qs'; const defaultHttp = axios.create({ xsrfCookieName: 'csrftoken', xsrfHeaderName: 'X-CSRFToken', paramsSerializer(params) { return encodeQueryString(params); - } + }, }); class Base { - constructor (http = defaultHttp, baseURL) { + constructor(http = defaultHttp, baseURL) { this.http = http; this.baseUrl = baseURL; } - create (data) { + create(data) { return this.http.post(this.baseUrl, data); } - destroy (id) { + destroy(id) { return this.http.delete(`${this.baseUrl}${id}/`); } - read (params) { + read(params) { return this.http.get(this.baseUrl, { params }); } - readDetail (id) { + readDetail(id) { return this.http.get(`${this.baseUrl}${id}/`); } - readOptions () { + readOptions() { return this.http.options(this.baseUrl); } - replace (id, data) { + replace(id, data) { return this.http.put(`${this.baseUrl}${id}/`, data); } - update (id, data) { + update(id, data) { return this.http.patch(`${this.baseUrl}${id}/`, data); } } diff --git a/awx/ui_next/src/api/Base.test.jsx b/awx/ui_next/src/api/Base.test.jsx index 53246dd80a..40907e79f7 100644 --- a/awx/ui_next/src/api/Base.test.jsx +++ b/awx/ui_next/src/api/Base.test.jsx @@ -3,14 +3,14 @@ import Base from './Base'; describe('Base', () => { const createPromise = () => Promise.resolve(); const mockBaseURL = '/api/v2/organizations/'; - const mockHttp = ({ + const mockHttp = { delete: jest.fn(createPromise), get: jest.fn(createPromise), options: jest.fn(createPromise), patch: jest.fn(createPromise), post: jest.fn(createPromise), - put: jest.fn(createPromise) - }); + put: jest.fn(createPromise), + }; const BaseAPI = new Base(mockHttp, mockBaseURL); @@ -18,7 +18,7 @@ describe('Base', () => { jest.clearAllMocks(); }); - test('create calls http method with expected data', async (done) => { + test('create calls http method with expected data', async done => { const data = { name: 'test ' }; await BaseAPI.create(data); @@ -28,19 +28,21 @@ describe('Base', () => { done(); }); - test('destroy calls http method with expected data', async (done) => { + test('destroy calls http method with expected data', async done => { const resourceId = 1; await BaseAPI.destroy(resourceId); expect(mockHttp.delete).toHaveBeenCalledTimes(1); - expect(mockHttp.delete.mock.calls[0][0]).toEqual(`${mockBaseURL}${resourceId}/`); + expect(mockHttp.delete.mock.calls[0][0]).toEqual( + `${mockBaseURL}${resourceId}/` + ); done(); }); - test('read calls http method with expected data', async (done) => { + test('read calls http method with expected data', async done => { const testParams = { foo: 'bar' }; - const testParamsDuplicates = { foo: ['bar', 'baz']}; + const testParamsDuplicates = { foo: ['bar', 'baz'] }; await BaseAPI.read(testParams); await BaseAPI.read(); @@ -48,25 +50,29 @@ describe('Base', () => { expect(mockHttp.get).toHaveBeenCalledTimes(3); expect(mockHttp.get.mock.calls[0][0]).toEqual(`${mockBaseURL}`); - expect(mockHttp.get.mock.calls[0][1]).toEqual({"params": {"foo": "bar"}}); + expect(mockHttp.get.mock.calls[0][1]).toEqual({ params: { foo: 'bar' } }); expect(mockHttp.get.mock.calls[1][0]).toEqual(`${mockBaseURL}`); - expect(mockHttp.get.mock.calls[1][1]).toEqual({"params": undefined}); + expect(mockHttp.get.mock.calls[1][1]).toEqual({ params: undefined }); expect(mockHttp.get.mock.calls[2][0]).toEqual(`${mockBaseURL}`); - expect(mockHttp.get.mock.calls[2][1]).toEqual({"params": {"foo": ["bar", "baz"]}}); + expect(mockHttp.get.mock.calls[2][1]).toEqual({ + params: { foo: ['bar', 'baz'] }, + }); done(); }); - test('readDetail calls http method with expected data', async (done) => { + test('readDetail calls http method with expected data', async done => { const resourceId = 1; await BaseAPI.readDetail(resourceId); expect(mockHttp.get).toHaveBeenCalledTimes(1); - expect(mockHttp.get.mock.calls[0][0]).toEqual(`${mockBaseURL}${resourceId}/`); + expect(mockHttp.get.mock.calls[0][0]).toEqual( + `${mockBaseURL}${resourceId}/` + ); done(); }); - test('readOptions calls http method with expected data', async (done) => { + test('readOptions calls http method with expected data', async done => { await BaseAPI.readOptions(); expect(mockHttp.options).toHaveBeenCalledTimes(1); @@ -74,27 +80,31 @@ describe('Base', () => { done(); }); - test('replace calls http method with expected data', async (done) => { + test('replace calls http method with expected data', async done => { const resourceId = 1; const data = { name: 'test ' }; await BaseAPI.replace(resourceId, data); expect(mockHttp.put).toHaveBeenCalledTimes(1); - expect(mockHttp.put.mock.calls[0][0]).toEqual(`${mockBaseURL}${resourceId}/`); + expect(mockHttp.put.mock.calls[0][0]).toEqual( + `${mockBaseURL}${resourceId}/` + ); expect(mockHttp.put.mock.calls[0][1]).toEqual(data); done(); }); - test('update calls http method with expected data', async (done) => { + test('update calls http method with expected data', async done => { const resourceId = 1; const data = { name: 'test ' }; await BaseAPI.update(resourceId, data); expect(mockHttp.patch).toHaveBeenCalledTimes(1); - expect(mockHttp.patch.mock.calls[0][0]).toEqual(`${mockBaseURL}${resourceId}/`); + expect(mockHttp.patch.mock.calls[0][0]).toEqual( + `${mockBaseURL}${resourceId}/` + ); expect(mockHttp.patch.mock.calls[0][1]).toEqual(data); done(); diff --git a/awx/ui_next/src/api/mixins/InstanceGroups.mixin.js b/awx/ui_next/src/api/mixins/InstanceGroups.mixin.js index a4fba3cc0e..e3b2be1cb5 100644 --- a/awx/ui_next/src/api/mixins/InstanceGroups.mixin.js +++ b/awx/ui_next/src/api/mixins/InstanceGroups.mixin.js @@ -1,15 +1,24 @@ -const InstanceGroupsMixin = (parent) => class extends parent { - readInstanceGroups (resourceId, params) { - return this.http.get(`${this.baseUrl}${resourceId}/instance_groups/`, params); - } +const InstanceGroupsMixin = parent => + class extends parent { + readInstanceGroups(resourceId, params) { + return this.http.get( + `${this.baseUrl}${resourceId}/instance_groups/`, + params + ); + } - associateInstanceGroup (resourceId, instanceGroupId) { - return this.http.post(`${this.baseUrl}${resourceId}/instance_groups/`, { id: instanceGroupId }); - } + associateInstanceGroup(resourceId, instanceGroupId) { + return this.http.post(`${this.baseUrl}${resourceId}/instance_groups/`, { + id: instanceGroupId, + }); + } - disassociateInstanceGroup (resourceId, instanceGroupId) { - return this.http.post(`${this.baseUrl}${resourceId}/instance_groups/`, { id: instanceGroupId, disassociate: true }); - } -}; + disassociateInstanceGroup(resourceId, instanceGroupId) { + return this.http.post(`${this.baseUrl}${resourceId}/instance_groups/`, { + id: instanceGroupId, + disassociate: true, + }); + } + }; export default InstanceGroupsMixin; diff --git a/awx/ui_next/src/api/mixins/Notifications.mixin.js b/awx/ui_next/src/api/mixins/Notifications.mixin.js index f7c934a0c8..e40b7983a3 100644 --- a/awx/ui_next/src/api/mixins/Notifications.mixin.js +++ b/awx/ui_next/src/api/mixins/Notifications.mixin.js @@ -1,65 +1,106 @@ -const NotificationsMixin = (parent) => class extends parent { - readOptionsNotificationTemplates(id) { - return this.http.options(`${this.baseUrl}${id}/notification_templates/`); - } - - readNotificationTemplates (id, params) { - return this.http.get(`${this.baseUrl}${id}/notification_templates/`, params); - } - - readNotificationTemplatesSuccess (id, params) { - return this.http.get(`${this.baseUrl}${id}/notification_templates_success/`, params); - } - - readNotificationTemplatesError (id, params) { - return this.http.get(`${this.baseUrl}${id}/notification_templates_error/`, params); - } - - associateNotificationTemplatesSuccess (resourceId, notificationId) { - return this.http.post(`${this.baseUrl}${resourceId}/notification_templates_success/`, { id: notificationId }); - } - - disassociateNotificationTemplatesSuccess (resourceId, notificationId) { - return this.http.post(`${this.baseUrl}${resourceId}/notification_templates_success/`, { id: notificationId, disassociate: true }); - } - - associateNotificationTemplatesError (resourceId, notificationId) { - return this.http.post(`${this.baseUrl}${resourceId}/notification_templates_error/`, { id: notificationId }); - } - - disassociateNotificationTemplatesError (resourceId, notificationId) { - return this.http.post(`${this.baseUrl}${resourceId}/notification_templates_error/`, { id: notificationId, disassociate: true }); - } - - /** - * This is a helper method meant to simplify setting the "on" or "off" status of - * a related notification. - * - * @param[resourceId] - id of the base resource - * @param[notificationId] - id of the notification - * @param[notificationType] - the type of notification, options are "success" and "error" - * @param[associationState] - Boolean for associating or disassociating, options are true or false - */ - // eslint-disable-next-line max-len - updateNotificationTemplateAssociation (resourceId, notificationId, notificationType, associationState) { - if (notificationType === 'success' && associationState === true) { - return this.associateNotificationTemplatesSuccess(resourceId, notificationId); +const NotificationsMixin = parent => + class extends parent { + readOptionsNotificationTemplates(id) { + return this.http.options(`${this.baseUrl}${id}/notification_templates/`); } - if (notificationType === 'success' && associationState === false) { - return this.disassociateNotificationTemplatesSuccess(resourceId, notificationId); + readNotificationTemplates(id, params) { + return this.http.get( + `${this.baseUrl}${id}/notification_templates/`, + params + ); } - if (notificationType === 'error' && associationState === true) { - return this.associateNotificationTemplatesError(resourceId, notificationId); + readNotificationTemplatesSuccess(id, params) { + return this.http.get( + `${this.baseUrl}${id}/notification_templates_success/`, + params + ); } - if (notificationType === 'error' && associationState === false) { - return this.disassociateNotificationTemplatesError(resourceId, notificationId); + readNotificationTemplatesError(id, params) { + return this.http.get( + `${this.baseUrl}${id}/notification_templates_error/`, + params + ); } - throw new Error(`Unsupported notificationType, associationState combination: ${notificationType}, ${associationState}`); - } -}; + associateNotificationTemplatesSuccess(resourceId, notificationId) { + return this.http.post( + `${this.baseUrl}${resourceId}/notification_templates_success/`, + { id: notificationId } + ); + } + + disassociateNotificationTemplatesSuccess(resourceId, notificationId) { + return this.http.post( + `${this.baseUrl}${resourceId}/notification_templates_success/`, + { id: notificationId, disassociate: true } + ); + } + + associateNotificationTemplatesError(resourceId, notificationId) { + return this.http.post( + `${this.baseUrl}${resourceId}/notification_templates_error/`, + { id: notificationId } + ); + } + + disassociateNotificationTemplatesError(resourceId, notificationId) { + return this.http.post( + `${this.baseUrl}${resourceId}/notification_templates_error/`, + { id: notificationId, disassociate: true } + ); + } + + /** + * This is a helper method meant to simplify setting the "on" or "off" status of + * a related notification. + * + * @param[resourceId] - id of the base resource + * @param[notificationId] - id of the notification + * @param[notificationType] - the type of notification, options are "success" and "error" + * @param[associationState] - Boolean for associating or disassociating, options are true or false + */ + // eslint-disable-next-line max-len + updateNotificationTemplateAssociation( + resourceId, + notificationId, + notificationType, + associationState + ) { + if (notificationType === 'success' && associationState === true) { + return this.associateNotificationTemplatesSuccess( + resourceId, + notificationId + ); + } + + if (notificationType === 'success' && associationState === false) { + return this.disassociateNotificationTemplatesSuccess( + resourceId, + notificationId + ); + } + + if (notificationType === 'error' && associationState === true) { + return this.associateNotificationTemplatesError( + resourceId, + notificationId + ); + } + + if (notificationType === 'error' && associationState === false) { + return this.disassociateNotificationTemplatesError( + resourceId, + notificationId + ); + } + + throw new Error( + `Unsupported notificationType, associationState combination: ${notificationType}, ${associationState}` + ); + } + }; export default NotificationsMixin; diff --git a/awx/ui_next/src/api/models/Organizations.js b/awx/ui_next/src/api/models/Organizations.js index 343c51f5b2..3cbe64c284 100644 --- a/awx/ui_next/src/api/models/Organizations.js +++ b/awx/ui_next/src/api/models/Organizations.js @@ -3,16 +3,16 @@ import NotificationsMixin from '../mixins/Notifications.mixin'; import InstanceGroupsMixin from '../mixins/InstanceGroups.mixin'; class Organizations extends InstanceGroupsMixin(NotificationsMixin(Base)) { - constructor (http) { + constructor(http) { super(http); this.baseUrl = '/api/v2/organizations/'; } - readAccessList (id, params) { + readAccessList(id, params) { return this.http.get(`${this.baseUrl}${id}/access_list/`, { params }); } - readTeams (id, params) { + readTeams(id, params) { return this.http.get(`${this.baseUrl}${id}/teams/`, { params }); } } diff --git a/awx/ui_next/src/api/models/Organizations.test.jsx b/awx/ui_next/src/api/models/Organizations.test.jsx index cb4dfbde70..cd22a09bb8 100644 --- a/awx/ui_next/src/api/models/Organizations.test.jsx +++ b/awx/ui_next/src/api/models/Organizations.test.jsx @@ -4,7 +4,7 @@ import { describeNotificationMixin } from '../../../testUtils/apiReusable'; describe('OrganizationsAPI', () => { const orgId = 1; const createPromise = () => Promise.resolve(); - const mockHttp = ({ get: jest.fn(createPromise) }); + const mockHttp = { get: jest.fn(createPromise) }; const OrganizationsAPI = new Organizations(mockHttp); @@ -12,9 +12,9 @@ describe('OrganizationsAPI', () => { jest.clearAllMocks(); }); - test('read access list calls get with expected params', async (done) => { + test('read access list calls get with expected params', async done => { const testParams = { foo: 'bar' }; - const testParamsDuplicates = { foo: ['bar', 'baz']}; + const testParamsDuplicates = { foo: ['bar', 'baz'] }; const mockBaseURL = `/api/v2/organizations/${orgId}/access_list/`; @@ -24,17 +24,19 @@ describe('OrganizationsAPI', () => { expect(mockHttp.get).toHaveBeenCalledTimes(3); expect(mockHttp.get.mock.calls[0][0]).toEqual(`${mockBaseURL}`); - expect(mockHttp.get.mock.calls[0][1]).toEqual({"params": undefined}); + expect(mockHttp.get.mock.calls[0][1]).toEqual({ params: undefined }); expect(mockHttp.get.mock.calls[1][0]).toEqual(`${mockBaseURL}`); - expect(mockHttp.get.mock.calls[1][1]).toEqual({"params": {"foo": "bar"}}); + expect(mockHttp.get.mock.calls[1][1]).toEqual({ params: { foo: 'bar' } }); expect(mockHttp.get.mock.calls[2][0]).toEqual(`${mockBaseURL}`); - expect(mockHttp.get.mock.calls[2][1]).toEqual({"params": {"foo": ["bar", "baz"]}}); + expect(mockHttp.get.mock.calls[2][1]).toEqual({ + params: { foo: ['bar', 'baz'] }, + }); done(); }); - test('read teams calls get with expected params', async (done) => { + test('read teams calls get with expected params', async done => { const testParams = { foo: 'bar' }; - const testParamsDuplicates = { foo: ['bar', 'baz']}; + const testParamsDuplicates = { foo: ['bar', 'baz'] }; const mockBaseURL = `/api/v2/organizations/${orgId}/teams/`; @@ -44,11 +46,13 @@ describe('OrganizationsAPI', () => { expect(mockHttp.get).toHaveBeenCalledTimes(3); expect(mockHttp.get.mock.calls[0][0]).toEqual(`${mockBaseURL}`); - expect(mockHttp.get.mock.calls[0][1]).toEqual({"params": undefined}); + expect(mockHttp.get.mock.calls[0][1]).toEqual({ params: undefined }); expect(mockHttp.get.mock.calls[1][0]).toEqual(`${mockBaseURL}`); - expect(mockHttp.get.mock.calls[1][1]).toEqual({"params": {"foo": "bar"}}); + expect(mockHttp.get.mock.calls[1][1]).toEqual({ params: { foo: 'bar' } }); expect(mockHttp.get.mock.calls[2][0]).toEqual(`${mockBaseURL}`); - expect(mockHttp.get.mock.calls[2][1]).toEqual({"params": {"foo": ["bar", "baz"]}}); + expect(mockHttp.get.mock.calls[2][1]).toEqual({ + params: { foo: ['bar', 'baz'] }, + }); done(); }); }); diff --git a/awx/ui_next/src/components/AddRole/AddResourceRole.jsx b/awx/ui_next/src/components/AddRole/AddResourceRole.jsx index 1cbf55a3bd..2770afe53e 100644 --- a/awx/ui_next/src/components/AddRole/AddResourceRole.jsx +++ b/awx/ui_next/src/components/AddRole/AddResourceRole.jsx @@ -8,14 +8,13 @@ import SelectRoleStep from './SelectRoleStep'; import SelectableCard from './SelectableCard'; import { TeamsAPI, UsersAPI } from '../../api'; -const readUsers = async (queryParams) => UsersAPI.read( - Object.assign(queryParams, { is_superuser: false }) -); +const readUsers = async queryParams => + UsersAPI.read(Object.assign(queryParams, { is_superuser: false })); -const readTeams = async (queryParams) => TeamsAPI.read(queryParams); +const readTeams = async queryParams => TeamsAPI.read(queryParams); class AddResourceRole extends React.Component { - constructor (props) { + constructor(props) { super(props); this.state = { @@ -25,67 +24,69 @@ class AddResourceRole extends React.Component { currentStepId: 1, }; - this.handleResourceCheckboxClick = this.handleResourceCheckboxClick.bind(this); + this.handleResourceCheckboxClick = this.handleResourceCheckboxClick.bind( + this + ); this.handleResourceSelect = this.handleResourceSelect.bind(this); this.handleRoleCheckboxClick = this.handleRoleCheckboxClick.bind(this); this.handleWizardNext = this.handleWizardNext.bind(this); this.handleWizardSave = this.handleWizardSave.bind(this); } - handleResourceCheckboxClick (user) { + handleResourceCheckboxClick(user) { const { selectedResourceRows } = this.state; - const selectedIndex = selectedResourceRows - .findIndex(selectedRow => selectedRow.id === user.id); + const selectedIndex = selectedResourceRows.findIndex( + selectedRow => selectedRow.id === user.id + ); if (selectedIndex > -1) { selectedResourceRows.splice(selectedIndex, 1); this.setState({ selectedResourceRows }); } else { this.setState(prevState => ({ - selectedResourceRows: [...prevState.selectedResourceRows, user] + selectedResourceRows: [...prevState.selectedResourceRows, user], })); } } - handleRoleCheckboxClick (role) { + handleRoleCheckboxClick(role) { const { selectedRoleRows } = this.state; - const selectedIndex = selectedRoleRows - .findIndex(selectedRow => selectedRow.id === role.id); + const selectedIndex = selectedRoleRows.findIndex( + selectedRow => selectedRow.id === role.id + ); if (selectedIndex > -1) { selectedRoleRows.splice(selectedIndex, 1); this.setState({ selectedRoleRows }); } else { this.setState(prevState => ({ - selectedRoleRows: [...prevState.selectedRoleRows, role] + selectedRoleRows: [...prevState.selectedRoleRows, role], })); } } - handleResourceSelect (resourceType) { + handleResourceSelect(resourceType) { this.setState({ selectedResource: resourceType, selectedResourceRows: [], - selectedRoleRows: [] + selectedRoleRows: [], }); } - handleWizardNext (step) { + handleWizardNext(step) { this.setState({ currentStepId: step.id, }); } - async handleWizardSave () { - const { - onSave - } = this.props; + async handleWizardSave() { + const { onSave } = this.props; const { selectedResourceRows, selectedRoleRows, - selectedResource + selectedResource, } = this.state; try { @@ -95,11 +96,17 @@ class AddResourceRole extends React.Component { for (let j = 0; j < selectedRoleRows.length; j++) { if (selectedResource === 'users') { roleRequests.push( - UsersAPI.associateRole(selectedResourceRows[i].id, selectedRoleRows[j].id) + UsersAPI.associateRole( + selectedResourceRows[i].id, + selectedRoleRows[j].id + ) ); } else if (selectedResource === 'teams') { roleRequests.push( - TeamsAPI.associateRole(selectedResourceRows[i].id, selectedRoleRows[j].id) + TeamsAPI.associateRole( + selectedResourceRows[i].id, + selectedRoleRows[j].id + ) ); } } @@ -112,25 +119,31 @@ class AddResourceRole extends React.Component { } } - render () { + render() { const { selectedResource, selectedResourceRows, selectedRoleRows, currentStepId, } = this.state; - const { - onClose, - roles, - i18n - } = this.props; + const { onClose, roles, i18n } = this.props; const userColumns = [ - { name: i18n._(t`Username`), key: 'username', isSortable: true, isSearchable: true } + { + name: i18n._(t`Username`), + key: 'username', + isSortable: true, + isSearchable: true, + }, ]; const teamColumns = [ - { name: i18n._(t`Name`), key: 'name', isSortable: true, isSearchable: true } + { + name: i18n._(t`Name`), + key: 'name', + isSortable: true, + isSearchable: true, + }, ]; let wizardTitle = ''; @@ -164,7 +177,7 @@ class AddResourceRole extends React.Component { /> ), - enableNext: selectedResource !== null + enableNext: selectedResource !== null, }, { id: 2, @@ -195,7 +208,7 @@ class AddResourceRole extends React.Component { )} ), - enableNext: selectedResourceRows.length > 0 + enableNext: selectedResourceRows.length > 0, }, { id: 3, @@ -211,8 +224,8 @@ class AddResourceRole extends React.Component { /> ), nextButtonText: i18n._(t`Save`), - enableNext: selectedRoleRows.length > 0 - } + enableNext: selectedRoleRows.length > 0, + }, ]; const currentStep = steps.find(step => step.id === currentStepId); @@ -236,11 +249,11 @@ class AddResourceRole extends React.Component { AddResourceRole.propTypes = { onClose: PropTypes.func.isRequired, onSave: PropTypes.func.isRequired, - roles: PropTypes.shape() + roles: PropTypes.shape(), }; AddResourceRole.defaultProps = { - roles: {} + roles: {}, }; export { AddResourceRole as _AddResourceRole }; diff --git a/awx/ui_next/src/components/AddRole/SelectResourceStep.jsx b/awx/ui_next/src/components/AddRole/SelectResourceStep.jsx index fb7b00e4f1..2599c7fd64 100644 --- a/awx/ui_next/src/components/AddRole/SelectResourceStep.jsx +++ b/awx/ui_next/src/components/AddRole/SelectResourceStep.jsx @@ -40,10 +40,7 @@ class SelectResourceStep extends React.Component { async readResourceList() { const { onSearch, location } = this.props; - const queryParams = parseQueryString( - this.qsConfig, - location.search - ); + const queryParams = parseQueryString(this.qsConfig, location.search); this.setState({ isLoading: true, diff --git a/awx/ui_next/src/components/AddRole/SelectResourceStep.test.jsx b/awx/ui_next/src/components/AddRole/SelectResourceStep.test.jsx index 6dced8c9f9..3d32abef06 100644 --- a/awx/ui_next/src/components/AddRole/SelectResourceStep.test.jsx +++ b/awx/ui_next/src/components/AddRole/SelectResourceStep.test.jsx @@ -7,7 +7,7 @@ import SelectResourceStep from './SelectResourceStep'; describe('', () => { const columns = [ - { name: 'Username', key: 'username', isSortable: true, isSearchable: true } + { name: 'Username', key: 'username', isSortable: true, isSearchable: true }, ]; afterEach(() => { jest.restoreAllMocks(); @@ -30,9 +30,9 @@ describe('', () => { count: 2, results: [ { id: 1, username: 'foo', url: 'item/1' }, - { id: 2, username: 'bar', url: 'item/2' } - ] - } + { id: 2, username: 'bar', url: 'item/2' }, + ], + }, }); mountWithContexts( ', () => { expect(handleSearch).toHaveBeenCalledWith({ order_by: 'username', page: 1, - page_size: 5 + page_size: 5, }); }); test('readResourceList properly adds rows to state', async () => { - const selectedResourceRows = [ - { id: 1, username: 'foo', url: 'item/1' } - ]; + const selectedResourceRows = [{ id: 1, username: 'foo', url: 'item/1' }]; const handleSearch = jest.fn().mockResolvedValue({ data: { count: 2, results: [ { id: 1, username: 'foo', url: 'item/1' }, - { id: 2, username: 'bar', url: 'item/2' } - ] - } + { id: 2, username: 'bar', url: 'item/2' }, + ], + }, }); const history = createMemoryHistory({ - initialEntries: ['/organizations/1/access?resource.page=1&resource.order_by=-username'], + initialEntries: [ + '/organizations/1/access?resource.page=1&resource.order_by=-username', + ], }); const wrapper = await mountWithContexts( ', () => { onSearch={handleSearch} selectedResourceRows={selectedResourceRows} sortedColumnKey="username" - />, { context: { router: { history, route: { location: history.location } } } } + />, + { + context: { router: { history, route: { location: history.location } } }, + } ).find('SelectResourceStep'); await wrapper.instance().readResourceList(); expect(handleSearch).toHaveBeenCalledWith({ @@ -84,7 +87,7 @@ describe('', () => { }); expect(wrapper.state('resources')).toEqual([ { id: 1, username: 'foo', url: 'item/1' }, - { id: 2, username: 'bar', url: 'item/2' } + { id: 2, username: 'bar', url: 'item/2' }, ]); }); @@ -94,8 +97,8 @@ describe('', () => { count: 2, results: [ { id: 1, username: 'foo', url: 'item/1' }, - { id: 2, username: 'bar', url: 'item/2' } - ] + { id: 2, username: 'bar', url: 'item/2' }, + ], }; const wrapper = mountWithContexts( ', () => { wrapper.update(); const checkboxListItemWrapper = wrapper.find('CheckboxListItem'); expect(checkboxListItemWrapper.length).toBe(2); - checkboxListItemWrapper.first().find('input[type="checkbox"]') + checkboxListItemWrapper + .first() + .find('input[type="checkbox"]') .simulate('change', { target: { checked: true } }); expect(handleRowClick).toHaveBeenCalledWith(data.results[0]); }); diff --git a/awx/ui_next/src/components/Chip/ChipGroup.jsx b/awx/ui_next/src/components/Chip/ChipGroup.jsx index 24ffdbc220..8d6d6f0593 100644 --- a/awx/ui_next/src/components/Chip/ChipGroup.jsx +++ b/awx/ui_next/src/components/Chip/ChipGroup.jsx @@ -3,14 +3,21 @@ import { number, bool } from 'prop-types'; import styled from 'styled-components'; import Chip from './Chip'; -const ChipGroup = ({ children, className, showOverflowAfter, displayAll, ...props }) => { +const ChipGroup = ({ + children, + className, + showOverflowAfter, + displayAll, + ...props +}) => { const [isExpanded, setIsExpanded] = useState(!showOverflowAfter); const toggleIsOpen = () => setIsExpanded(!isExpanded); - const mappedChildren = React.Children.map(children, c => ( + const mappedChildren = React.Children.map(children, c => React.cloneElement(c, { component: 'li' }) - )); - const showOverflowToggle = showOverflowAfter && children.length > showOverflowAfter; + ); + const showOverflowToggle = + showOverflowAfter && children.length > showOverflowAfter; const numToShow = isExpanded ? children.length : Math.min(showOverflowAfter, children.length); @@ -30,17 +37,17 @@ const ChipGroup = ({ children, className, showOverflowAfter, displayAll, ...prop }; ChipGroup.propTypes = { showOverflowAfter: number, - displayAll: bool + displayAll: bool, }; ChipGroup.defaultProps = { showOverflowAfter: null, - displayAll: false + displayAll: false, }; export default styled(ChipGroup)` --pf-c-chip-group--c-chip--MarginRight: 10px; --pf-c-chip-group--c-chip--MarginBottom: 10px; - + > .pf-c-chip.pf-m-overflow button { padding: 3px 8px; } diff --git a/awx/ui_next/src/components/DataListToolbar/DataListToolbar.test.jsx b/awx/ui_next/src/components/DataListToolbar/DataListToolbar.test.jsx index e8262b3333..8beb597edb 100644 --- a/awx/ui_next/src/components/DataListToolbar/DataListToolbar.test.jsx +++ b/awx/ui_next/src/components/DataListToolbar/DataListToolbar.test.jsx @@ -13,7 +13,9 @@ describe('', () => { }); test('it triggers the expected callbacks', () => { - const columns = [{ name: 'Name', key: 'name', isSortable: true, isSearchable: true }]; + const columns = [ + { name: 'Name', key: 'name', isSortable: true, isSearchable: true }, + ]; const search = 'button[aria-label="Search submit button"]'; const searchTextInput = 'input[aria-label="Search text input"]'; @@ -59,14 +61,16 @@ describe('', () => { test('dropdown items sortable/searchable columns work', () => { const sortDropdownToggleSelector = 'button[id="awx-sort"]'; const searchDropdownToggleSelector = 'button[id="awx-search"]'; - const sortDropdownMenuItems = 'DropdownMenu > ul[aria-labelledby="awx-sort"]'; - const searchDropdownMenuItems = 'DropdownMenu > ul[aria-labelledby="awx-search"]'; + const sortDropdownMenuItems = + 'DropdownMenu > ul[aria-labelledby="awx-sort"]'; + const searchDropdownMenuItems = + 'DropdownMenu > ul[aria-labelledby="awx-search"]'; const multipleColumns = [ { name: 'Foo', key: 'foo', isSortable: true, isSearchable: true }, { name: 'Bar', key: 'bar', isSortable: true, isSearchable: true }, { name: 'Bakery', key: 'bakery', isSortable: true }, - { name: 'Baz', key: 'baz' } + { name: 'Baz', key: 'baz' }, ]; const onSort = jest.fn(); @@ -103,12 +107,16 @@ describe('', () => { ); toolbar.update(); - const sortDropdownToggleDescending = toolbar.find(sortDropdownToggleSelector); + const sortDropdownToggleDescending = toolbar.find( + sortDropdownToggleSelector + ); expect(sortDropdownToggleDescending.length).toBe(1); sortDropdownToggleDescending.simulate('click'); toolbar.update(); - const sortDropdownItemsDescending = toolbar.find(sortDropdownMenuItems).children(); + const sortDropdownItemsDescending = toolbar + .find(sortDropdownMenuItems) + .children(); expect(sortDropdownItemsDescending.length).toBe(2); sortDropdownToggleDescending.simulate('click'); // toggle close the sort dropdown @@ -134,8 +142,12 @@ describe('', () => { const downAlphaIconSelector = 'SortAlphaDownIcon'; const upAlphaIconSelector = 'SortAlphaUpIcon'; - const numericColumns = [{ name: 'ID', key: 'id', isSortable: true, isNumeric: true }]; - const alphaColumns = [{ name: 'Name', key: 'name', isSortable: true, isNumeric: false }]; + const numericColumns = [ + { name: 'ID', key: 'id', isSortable: true, isNumeric: true }, + ]; + const alphaColumns = [ + { name: 'Name', key: 'name', isSortable: true, isNumeric: false }, + ]; toolbar = mountWithContexts( ', () => { }); test('should render additionalControls', () => { - const columns = [{ name: 'Name', key: 'name', isSortable: true, isSearchable: true }]; + const columns = [ + { name: 'Name', key: 'name', isSortable: true, isSearchable: true }, + ]; const onSearch = jest.fn(); const onSort = jest.fn(); const onSelectAll = jest.fn(); @@ -194,7 +208,11 @@ describe('', () => { onSearch={onSearch} onSort={onSort} onSelectAll={onSelectAll} - additionalControls={[click]} + additionalControls={[ + + click + , + ]} /> ); diff --git a/awx/ui_next/src/components/FilterTags/FilterTags.jsx b/awx/ui_next/src/components/FilterTags/FilterTags.jsx index 15be557527..ac66d97969 100644 --- a/awx/ui_next/src/components/FilterTags/FilterTags.jsx +++ b/awx/ui_next/src/components/FilterTags/FilterTags.jsx @@ -9,19 +9,19 @@ import { ChipGroup, Chip } from '@components/Chip'; import VerticalSeparator from '@components/VerticalSeparator'; const FilterTagsRow = styled.div` - display: flex; - padding: 15px 20px; - border-top: 1px solid #d2d2d2; - font-size: 14px; - align-items: center; + display: flex; + padding: 15px 20px; + border-top: 1px solid #d2d2d2; + font-size: 14px; + align-items: center; `; const ResultCount = styled.span` - font-weight: bold; + font-weight: bold; `; const FilterLabel = styled.span` - padding-right: 20px; + padding-right: 20px; `; // remove non-default query params so they don't show up as filter tags @@ -30,50 +30,59 @@ const filterDefaultParams = (paramsArr, config) => { return paramsArr.filter(key => defaultParamsKeys.indexOf(key) === -1); }; -const FilterTags = ({ i18n, itemCount, qsConfig, location, onRemove, onRemoveAll }) => { +const FilterTags = ({ + i18n, + itemCount, + qsConfig, + location, + onRemove, + onRemoveAll, +}) => { const queryParams = parseQueryString(qsConfig, location.search); const queryParamsArr = []; const displayAll = true; - const nonDefaultParams = filterDefaultParams(Object.keys(queryParams), qsConfig); - nonDefaultParams - .forEach(key => { - if (Array.isArray(queryParams[key])) { - queryParams[key].forEach(val => queryParamsArr.push({ key, value: val })); - } else { - queryParamsArr.push({ key, value: queryParams[key] }); - } - }); + const nonDefaultParams = filterDefaultParams( + Object.keys(queryParams), + qsConfig + ); + nonDefaultParams.forEach(key => { + if (Array.isArray(queryParams[key])) { + queryParams[key].forEach(val => queryParamsArr.push({ key, value: val })); + } else { + queryParamsArr.push({ key, value: queryParams[key] }); + } + }); - return (queryParamsArr.length > 0) && ( - - - {`${itemCount} results`} - - - {i18n._(t`Active Filters:`)} - - {queryParamsArr.map(({ key, value }) => ( - onRemove(key, value)} - > - {value} - - ))} - - - {i18n._(t`Clear all`)} - - - - + return ( + queryParamsArr.length > 0 && ( + + {`${itemCount} results`} + + {i18n._(t`Active Filters:`)} + + {queryParamsArr.map(({ key, value }) => ( + onRemove(key, value)} + > + {value} + + ))} + + + {i18n._(t`Clear all`)} + + + + + ) ); }; diff --git a/awx/ui_next/src/components/FilterTags/FilterTags.test.jsx b/awx/ui_next/src/components/FilterTags/FilterTags.test.jsx index 7ad82361f4..df681d7c68 100644 --- a/awx/ui_next/src/components/FilterTags/FilterTags.test.jsx +++ b/awx/ui_next/src/components/FilterTags/FilterTags.test.jsx @@ -26,14 +26,19 @@ describe('', () => { test('renders non-default param tags based on location history', () => { const history = createMemoryHistory({ - initialEntries: ['/foo?item.page=1&item.page_size=2&item.foo=bar&item.baz=bust'], + initialEntries: [ + '/foo?item.page=1&item.page_size=2&item.foo=bar&item.baz=bust', + ], }); const wrapper = mountWithContexts( , { context: { router: { history, route: { location: history.location } } } } + />, + { + context: { router: { history, route: { location: history.location } } }, + } ); const chips = wrapper.find('.pf-c-chip.searchTagChip'); expect(chips.length).toBe(2); diff --git a/awx/ui_next/src/components/ListHeader/ListHeader.jsx b/awx/ui_next/src/components/ListHeader/ListHeader.jsx index aca82ad7f1..19b2367cf8 100644 --- a/awx/ui_next/src/components/ListHeader/ListHeader.jsx +++ b/awx/ui_next/src/components/ListHeader/ListHeader.jsx @@ -10,7 +10,7 @@ import { encodeNonDefaultQueryString, parseQueryString, addParams, - removeParams + removeParams, } from '@util/qs'; import { QSConfig } from '@types'; @@ -21,12 +21,12 @@ const EmptyStateControlsWrapper = styled.div` margin-bottom: 20px; justify-content: flex-end; - & > :not(:first-child) { + & > :not(:first-child) { margin-left: 20px; } `; class ListHeader extends React.Component { - constructor (props) { + constructor(props) { super(props); this.handleSearch = this.handleSearch.bind(this); @@ -35,7 +35,7 @@ class ListHeader extends React.Component { this.handleRemoveAll = this.handleRemoveAll.bind(this); } - getSortOrder () { + getSortOrder() { const { qsConfig, location } = this.props; const queryParams = parseQueryString(qsConfig, location.search); if (queryParams.order_by && queryParams.order_by.startsWith('-')) { @@ -44,45 +44,47 @@ class ListHeader extends React.Component { return [queryParams.order_by, 'ascending']; } - handleSearch (key, value) { + handleSearch(key, value) { const { history, qsConfig } = this.props; const { search } = history.location; this.pushHistoryState(addParams(qsConfig, search, { [key]: value })); } - handleRemove (key, value) { + handleRemove(key, value) { const { history, qsConfig } = this.props; const { search } = history.location; this.pushHistoryState(removeParams(qsConfig, search, { [key]: value })); } - handleRemoveAll () { + handleRemoveAll() { this.pushHistoryState(null); } - handleSort (key, order) { + handleSort(key, order) { const { history, qsConfig } = this.props; const { search } = history.location; - this.pushHistoryState(addParams(qsConfig, search, { - order_by: order === 'ascending' ? key : `-${key}`, - page: null, - })); + this.pushHistoryState( + addParams(qsConfig, search, { + order_by: order === 'ascending' ? key : `-${key}`, + page: null, + }) + ); } - pushHistoryState (params) { + pushHistoryState(params) { const { history, qsConfig } = this.props; const { pathname } = history.location; const encodedParams = encodeNonDefaultQueryString(qsConfig, params); history.push(encodedParams ? `${pathname}?${encodedParams}` : pathname); } - render () { + render() { const { emptyStateControls, itemCount, columns, renderToolbar, - qsConfig + qsConfig, } = this.props; const [orderBy, sortOrder] = this.getSortOrder(); return ( @@ -124,17 +126,19 @@ class ListHeader extends React.Component { ListHeader.propTypes = { itemCount: PropTypes.number.isRequired, qsConfig: QSConfig.isRequired, - columns: arrayOf(shape({ - name: string.isRequired, - key: string.isRequired, - isSortable: bool, - isSearchable: bool - })).isRequired, + columns: arrayOf( + shape({ + name: string.isRequired, + key: string.isRequired, + isSortable: bool, + isSearchable: bool, + }) + ).isRequired, renderToolbar: PropTypes.func, }; ListHeader.defaultProps = { - renderToolbar: (props) => (), + renderToolbar: props => , }; export default withRouter(ListHeader); diff --git a/awx/ui_next/src/components/ListHeader/ListHeader.test.jsx b/awx/ui_next/src/components/ListHeader/ListHeader.test.jsx index 264c4e66b6..9435f2e8b3 100644 --- a/awx/ui_next/src/components/ListHeader/ListHeader.test.jsx +++ b/awx/ui_next/src/components/ListHeader/ListHeader.test.jsx @@ -17,7 +17,9 @@ describe('ListHeader', () => { ); @@ -33,8 +35,11 @@ describe('ListHeader', () => { , { context: { router: { history } } } + columns={[ + { name: 'name', key: 'name', isSearchable: true, isSortable: true }, + ]} + />, + { context: { router: { history } } } ); const toolbar = wrapper.find('DataListToolbar'); diff --git a/awx/ui_next/src/components/PaginatedDataList/PaginatedDataList.jsx b/awx/ui_next/src/components/PaginatedDataList/PaginatedDataList.jsx index d369598b79..5051e545d8 100644 --- a/awx/ui_next/src/components/PaginatedDataList/PaginatedDataList.jsx +++ b/awx/ui_next/src/components/PaginatedDataList/PaginatedDataList.jsx @@ -15,7 +15,7 @@ import DataListToolbar from '@components/DataListToolbar'; import { encodeNonDefaultQueryString, parseQueryString, - addParams + addParams, } from '@util/qs'; import { pluralize, ucFirst } from '@util/strings'; @@ -24,32 +24,32 @@ import { QSConfig } from '@types'; import PaginatedDataListItem from './PaginatedDataListItem'; class PaginatedDataList extends React.Component { - constructor (props) { + constructor(props) { super(props); this.handleSetPage = this.handleSetPage.bind(this); this.handleSetPageSize = this.handleSetPageSize.bind(this); } - handleSetPage (event, pageNumber) { + handleSetPage(event, pageNumber) { const { history, qsConfig } = this.props; const { search } = history.location; this.pushHistoryState(addParams(qsConfig, search, { page: pageNumber })); } - handleSetPageSize (event, pageSize) { + handleSetPageSize(event, pageSize) { const { history, qsConfig } = this.props; const { search } = history.location; this.pushHistoryState(addParams(qsConfig, search, { page_size: pageSize })); } - pushHistoryState (params) { + pushHistoryState(params) { const { history, qsConfig } = this.props; const { pathname } = history.location; const encodedParams = encodeNonDefaultQueryString(qsConfig, params); history.push(encodedParams ? `${pathname}?${encodedParams}` : pathname); } - render () { + render() { const { contentError, hasContentLoading, @@ -66,25 +66,42 @@ class PaginatedDataList extends React.Component { i18n, renderToolbar, } = this.props; - const columns = toolbarColumns.length ? toolbarColumns : [{ name: i18n._(t`Name`), key: 'name', isSortable: true, isSearchable: true }]; + const columns = toolbarColumns.length + ? toolbarColumns + : [ + { + name: i18n._(t`Name`), + key: 'name', + isSortable: true, + isSearchable: true, + }, + ]; const queryParams = parseQueryString(qsConfig, location.search); const itemDisplayName = ucFirst(pluralize(itemName)); - const itemDisplayNamePlural = ucFirst(itemNamePlural || pluralize(itemName)); + const itemDisplayNamePlural = ucFirst( + itemNamePlural || pluralize(itemName) + ); const dataListLabel = i18n._(t`${itemDisplayName} List`); - const emptyContentMessage = i18n._(t`Please add ${itemDisplayNamePlural} to populate this list `); + const emptyContentMessage = i18n._( + t`Please add ${itemDisplayNamePlural} to populate this list ` + ); const emptyContentTitle = i18n._(t`No ${itemDisplayNamePlural} Found `); let Content; if (hasContentLoading && items.length <= 0) { - Content = (); + Content = ; } else if (contentError) { - Content = (); + Content = ; } else if (items.length <= 0) { - Content = (); + Content = ( + + ); } else { - Content = ({items.map(renderItem)}); + Content = ( + {items.map(renderItem)} + ); } if (items.length <= 0) { @@ -115,12 +132,16 @@ class PaginatedDataList extends React.Component { itemCount={itemCount} page={queryParams.page || 1} perPage={queryParams.page_size} - perPageOptions={showPageSizeOptions ? [ - { title: '5', value: 5 }, - { title: '10', value: 10 }, - { title: '20', value: 20 }, - { title: '50', value: 50 } - ] : []} + perPageOptions={ + showPageSizeOptions + ? [ + { title: '5', value: 5 }, + { title: '10', value: 10 }, + { title: '20', value: 20 }, + { title: '50', value: 50 }, + ] + : [] + } onSetPage={this.handleSetPage} onPerPageSelect={this.handleSetPageSize} /> @@ -142,11 +163,13 @@ PaginatedDataList.propTypes = { itemNamePlural: PropTypes.string, qsConfig: QSConfig.isRequired, renderItem: PropTypes.func, - toolbarColumns: arrayOf(shape({ - name: string.isRequired, - key: string.isRequired, - isSortable: bool, - })), + toolbarColumns: arrayOf( + shape({ + name: string.isRequired, + key: string.isRequired, + isSortable: bool, + }) + ), showPageSizeOptions: PropTypes.bool, renderToolbar: PropTypes.func, hasContentLoading: PropTypes.bool, @@ -160,8 +183,8 @@ PaginatedDataList.defaultProps = { itemName: 'item', itemNamePlural: '', showPageSizeOptions: true, - renderItem: (item) => (), - renderToolbar: (props) => (), + renderItem: item => , + renderToolbar: props => , }; export { PaginatedDataList as _PaginatedDataList }; diff --git a/awx/ui_next/src/components/PaginatedDataList/PaginatedDataList.test.jsx b/awx/ui_next/src/components/PaginatedDataList/PaginatedDataList.test.jsx index c03c26d1ff..8ca40de3df 100644 --- a/awx/ui_next/src/components/PaginatedDataList/PaginatedDataList.test.jsx +++ b/awx/ui_next/src/components/PaginatedDataList/PaginatedDataList.test.jsx @@ -51,7 +51,8 @@ describe('', () => { order_by: 'name', }} qsConfig={qsConfig} - />, { context: { router: { history } } } + />, + { context: { router: { history } } } ); const pagination = wrapper.find('Pagination'); @@ -77,7 +78,8 @@ describe('', () => { order_by: 'name', }} qsConfig={qsConfig} - />, { context: { router: { history } } } + />, + { context: { router: { history } } } ); const pagination = wrapper.find('Pagination'); diff --git a/awx/ui_next/src/components/Search/Search.jsx b/awx/ui_next/src/components/Search/Search.jsx index 007e749c7c..54e434688b 100644 --- a/awx/ui_next/src/components/Search/Search.jsx +++ b/awx/ui_next/src/components/Search/Search.jsx @@ -10,11 +10,9 @@ import { DropdownItem, Form, FormGroup, - TextInput as PFTextInput + TextInput as PFTextInput, } from '@patternfly/react-core'; -import { - SearchIcon -} from '@patternfly/react-icons'; +import { SearchIcon } from '@patternfly/react-icons'; import styled from 'styled-components'; @@ -29,7 +27,8 @@ const Button = styled(PFButton)` `; const Dropdown = styled(PFDropdown)` - &&& { /* Higher specificity required because we are selecting unclassed elements */ + &&& { + /* Higher specificity required because we are selecting unclassed elements */ > button { min-height: 30px; min-width: 70px; @@ -37,17 +36,19 @@ const Dropdown = styled(PFDropdown)` padding: 0 10px; margin: 0px; - > span { /* text element */ + > span { + /* text element */ width: auto; } - > svg { /* caret icon */ + > svg { + /* caret icon */ margin: 0px; padding-top: 3px; padding-left: 3px; } } - } + } `; const NoOptionDropdown = styled.div` @@ -61,7 +62,7 @@ const InputFormGroup = styled(FormGroup)` `; class Search extends React.Component { - constructor (props) { + constructor(props) { super(props); const { sortedColumnKey } = this.props; @@ -77,11 +78,11 @@ class Search extends React.Component { this.handleSearch = this.handleSearch.bind(this); } - handleDropdownToggle (isSearchDropdownOpen) { + handleDropdownToggle(isSearchDropdownOpen) { this.setState({ isSearchDropdownOpen }); } - handleDropdownSelect ({ target }) { + handleDropdownSelect({ target }) { const { columns } = this.props; const { innerText } = target; @@ -89,7 +90,7 @@ class Search extends React.Component { this.setState({ isSearchDropdownOpen: false, searchKey }); } - handleSearch (e) { + handleSearch(e) { // keeps page from fully reloading e.preventDefault(); @@ -102,22 +103,17 @@ class Search extends React.Component { this.setState({ searchValue: '' }); } - handleSearchInputChange (searchValue) { + handleSearchInputChange(searchValue) { this.setState({ searchValue }); } - render () { + render() { const { up } = DropdownPosition; - const { - columns, - i18n - } = this.props; - const { - isSearchDropdownOpen, - searchKey, - searchValue, - } = this.state; - const { name: searchColumnName } = columns.find(({ key }) => key === searchKey); + const { columns, i18n } = this.props; + const { isSearchDropdownOpen, searchKey, searchValue } = this.state; + const { name: searchColumnName } = columns.find( + ({ key }) => key === searchKey + ); const searchDropdownItems = columns .filter(({ key, isSearchable }) => isSearchable && key !== searchKey) @@ -133,32 +129,38 @@ class Search extends React.Component { {searchDropdownItems.length > 0 ? ( {i18n._(t`Search key dropdown`)})} + label={ + + {i18n._(t`Search key dropdown`)} + + } > {searchColumnName} - )} + } dropdownItems={searchDropdownItems} /> ) : ( - - {searchColumnName} - + {searchColumnName} )} {i18n._(t`Search value text input`)})} + label={ + + {i18n._(t`Search value text input`)} + + } > ', () => { }); test('it triggers the expected callbacks', () => { - const columns = [{ name: 'Name', key: 'name', isSortable: true, isSearchable: true }]; + const columns = [ + { name: 'Name', key: 'name', isSortable: true, isSearchable: true }, + ]; const searchBtn = 'button[aria-label="Search submit button"]'; const searchTextInput = 'input[aria-label="Search text input"]'; @@ -20,11 +22,7 @@ describe('', () => { const onSearch = jest.fn(); search = mountWithContexts( - + ); search.find(searchTextInput).instance().value = 'test-321'; @@ -36,14 +34,12 @@ describe('', () => { }); test('handleDropdownToggle properly updates state', async () => { - const columns = [{ name: 'Name', key: 'name', isSortable: true, isSearchable: true }]; + const columns = [ + { name: 'Name', key: 'name', isSortable: true, isSearchable: true }, + ]; const onSearch = jest.fn(); const wrapper = mountWithContexts( - + ).find('Search'); expect(wrapper.state('isSearchDropdownOpen')).toEqual(false); wrapper.instance().handleDropdownToggle(true); @@ -53,18 +49,21 @@ describe('', () => { test('handleDropdownSelect properly updates state', async () => { const columns = [ { name: 'Name', key: 'name', isSortable: true, isSearchable: true }, - { name: 'Description', key: 'description', isSortable: true, isSearchable: true } + { + name: 'Description', + key: 'description', + isSortable: true, + isSearchable: true, + }, ]; const onSearch = jest.fn(); const wrapper = mountWithContexts( - + ).find('Search'); expect(wrapper.state('searchKey')).toEqual('name'); - wrapper.instance().handleDropdownSelect({ target: { innerText: 'Description' } }); + wrapper + .instance() + .handleDropdownSelect({ target: { innerText: 'Description' } }); expect(wrapper.state('searchKey')).toEqual('description'); }); }); diff --git a/awx/ui_next/src/components/Sort/Sort.test.jsx b/awx/ui_next/src/components/Sort/Sort.test.jsx index b979ed4ee0..0e0208cd16 100644 --- a/awx/ui_next/src/components/Sort/Sort.test.jsx +++ b/awx/ui_next/src/components/Sort/Sort.test.jsx @@ -12,7 +12,9 @@ describe('', () => { }); test('it triggers the expected callbacks', () => { - const columns = [{ name: 'Name', key: 'name', isSortable: true, isSearchable: true }]; + const columns = [ + { name: 'Name', key: 'name', isSortable: true, isSearchable: true }, + ]; const sortBtn = 'button[aria-label="Sort"]'; @@ -38,7 +40,7 @@ describe('', () => { { name: 'Foo', key: 'foo', isSortable: true }, { name: 'Bar', key: 'bar', isSortable: true }, { name: 'Bakery', key: 'bakery', isSortable: true }, - { name: 'Baz', key: 'baz' } + { name: 'Baz', key: 'baz' }, ]; const onSort = jest.fn(); @@ -62,7 +64,7 @@ describe('', () => { { name: 'Foo', key: 'foo', isSortable: true }, { name: 'Bar', key: 'bar', isSortable: true }, { name: 'Bakery', key: 'bakery', isSortable: true }, - { name: 'Baz', key: 'baz' } + { name: 'Baz', key: 'baz' }, ]; const onSort = jest.fn(); @@ -86,7 +88,7 @@ describe('', () => { { name: 'Foo', key: 'foo', isSortable: true }, { name: 'Bar', key: 'bar', isSortable: true }, { name: 'Bakery', key: 'bakery', isSortable: true }, - { name: 'Baz', key: 'baz' } + { name: 'Baz', key: 'baz' }, ]; const onSort = jest.fn(); @@ -109,7 +111,7 @@ describe('', () => { { name: 'Foo', key: 'foo', isSortable: true }, { name: 'Bar', key: 'bar', isSortable: true }, { name: 'Bakery', key: 'bakery', isSortable: true }, - { name: 'Baz', key: 'baz' } + { name: 'Baz', key: 'baz' }, ]; const onSort = jest.fn(); @@ -133,8 +135,12 @@ describe('', () => { const downAlphaIconSelector = 'SortAlphaDownIcon'; const upAlphaIconSelector = 'SortAlphaUpIcon'; - const numericColumns = [{ name: 'ID', key: 'id', isSortable: true, isNumeric: true }]; - const alphaColumns = [{ name: 'Name', key: 'name', isSortable: true, isNumeric: false }]; + const numericColumns = [ + { name: 'ID', key: 'id', isSortable: true, isNumeric: true }, + ]; + const alphaColumns = [ + { name: 'Name', key: 'name', isSortable: true, isNumeric: false }, + ]; const onSort = jest.fn(); sort = mountWithContexts( diff --git a/awx/ui_next/src/screens/Job/JobList/JobList.jsx b/awx/ui_next/src/screens/Job/JobList/JobList.jsx index 62ca1eb5b5..6a4c44067d 100644 --- a/awx/ui_next/src/screens/Job/JobList/JobList.jsx +++ b/awx/ui_next/src/screens/Job/JobList/JobList.jsx @@ -2,17 +2,13 @@ import React, { Component } from 'react'; import { withRouter } from 'react-router-dom'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; -import { - Card, - PageSection, - PageSectionVariants, -} from '@patternfly/react-core'; +import { Card, PageSection, PageSectionVariants } from '@patternfly/react-core'; import { UnifiedJobsAPI } from '@api'; import AlertModal from '@components/AlertModal'; import DatalistToolbar from '@components/DataListToolbar'; import PaginatedDataList, { - ToolbarDeleteButton + ToolbarDeleteButton, } from '@components/PaginatedDataList'; import { getQSConfig, parseQueryString } from '@util/qs'; @@ -26,12 +22,12 @@ const QS_CONFIG = getQSConfig('job', { }); class JobList extends Component { - constructor (props) { + constructor(props) { super(props); this.state = { hasContentLoading: true, - hasContentError: false, + contentError: null, deletionError: false, selected: [], jobs: [], @@ -44,28 +40,28 @@ class JobList extends Component { this.handleDeleteErrorClose = this.handleDeleteErrorClose.bind(this); } - componentDidMount () { + componentDidMount() { this.loadJobs(); } - componentDidUpdate (prevProps) { + componentDidUpdate(prevProps) { const { location } = this.props; if (location !== prevProps.location) { this.loadJobs(); } } - handleDeleteErrorClose () { + handleDeleteErrorClose() { this.setState({ deletionError: false }); } - handleSelectAll (isSelected) { + handleSelectAll(isSelected) { const { jobs } = this.state; const selected = isSelected ? [...jobs] : []; this.setState({ selected }); } - handleSelect (item) { + handleSelect(item) { const { selected } = this.state; if (selected.some(s => s.id === item.id)) { this.setState({ selected: selected.filter(s => s.id !== item.id) }); @@ -74,7 +70,7 @@ class JobList extends Component { } } - async handleDelete () { + async handleDelete() { const { selected } = this.state; this.setState({ hasContentLoading: true, deletionError: false }); try { @@ -86,38 +82,37 @@ class JobList extends Component { } } - async loadJobs () { + async loadJobs() { const { location } = this.props; const params = parseQueryString(QS_CONFIG, location.search); - this.setState({ hasContentError: false, hasContentLoading: true }); + this.setState({ contentError: null, hasContentLoading: true }); try { - const { data: { count, results } } = await UnifiedJobsAPI.read(params); + const { + data: { count, results }, + } = await UnifiedJobsAPI.read(params); this.setState({ itemCount: count, jobs: results, selected: [], }); } catch (err) { - this.setState({ hasContentError: true }); + this.setState({ contentError: err }); } finally { this.setState({ hasContentLoading: false }); } } - render () { + render() { const { - hasContentError, + contentError, hasContentLoading, deletionError, jobs, itemCount, selected, } = this.state; - const { - match, - i18n - } = this.props; + const { match, i18n } = this.props; const { medium } = PageSectionVariants; const isAllSelected = selected.length === jobs.length; const itemName = i18n._(t`Job`); @@ -125,17 +120,27 @@ class JobList extends Component { ( + renderToolbar={props => ( + />, ]} /> )} - renderItem={(job) => ( + renderItem={job => ( ( + renderToolbar={props => ( - ] : null} + additionalControls={ + canEdit + ? [ + , + ] + : null + } /> )} renderItem={accessRecord => ( diff --git a/awx/ui_next/src/screens/Organization/OrganizationAccess/OrganizationAccess.test.jsx b/awx/ui_next/src/screens/Organization/OrganizationAccess/OrganizationAccess.test.jsx index 12dd0dbc68..b0d0b1bb33 100644 --- a/awx/ui_next/src/screens/Organization/OrganizationAccess/OrganizationAccess.test.jsx +++ b/awx/ui_next/src/screens/Organization/OrganizationAccess/OrganizationAccess.test.jsx @@ -103,7 +103,7 @@ describe('', () => { expect(wrapper.find('OrganizationAccess').state('hasContentLoading')).toBe( false ); - expect(wrapper.find('OrganizationAccess').state('hasContentError')).toBe(false); + expect(wrapper.find('OrganizationAccess').state('contentError')).toBe(null); done(); }); diff --git a/awx/ui_next/src/screens/Organization/OrganizationAccess/__snapshots__/OrganizationAccess.test.jsx.snap b/awx/ui_next/src/screens/Organization/OrganizationAccess/__snapshots__/OrganizationAccess.test.jsx.snap index 1eb2c6cc4f..0d6d397ed1 100644 --- a/awx/ui_next/src/screens/Organization/OrganizationAccess/__snapshots__/OrganizationAccess.test.jsx.snap +++ b/awx/ui_next/src/screens/Organization/OrganizationAccess/__snapshots__/OrganizationAccess.test.jsx.snap @@ -34,7 +34,7 @@ exports[` initially renders succesfully 1`] = ` } > initially renders succesfully 1`] = ` withHash={true} > initially renders succesfully 1`] = ` > s.id === row.id)) { @@ -74,16 +70,16 @@ class OrganizationsList extends Component { } } - handleDeleteErrorClose () { + handleDeleteErrorClose() { this.setState({ hasDeletionError: false }); } - async handleOrgDelete () { + async handleOrgDelete() { const { selected } = this.state; this.setState({ hasContentLoading: true, hasDeletionError: false }); try { - await Promise.all(selected.map((org) => OrganizationsAPI.destroy(org.id))); + await Promise.all(selected.map(org => OrganizationsAPI.destroy(org.id))); } catch (err) { this.setState({ hasDeletionError: true }); } finally { @@ -91,7 +87,7 @@ class OrganizationsList extends Component { } } - async loadOrganizations () { + async loadOrganizations() { const { location } = this.props; const { actions: cachedActions } = this.state; const params = parseQueryString(QS_CONFIG, location.search); @@ -108,9 +104,16 @@ class OrganizationsList extends Component { optionsPromise, ]); - this.setState({ hasContentError: false, hasContentLoading: true }); + this.setState({ contentError: null, hasContentLoading: true }); try { - const [{ data: { count, results } }, { data: { actions } }] = await promises; + const [ + { + data: { count, results }, + }, + { + data: { actions }, + }, + ] = await promises; this.setState({ actions, itemCount: count, @@ -118,20 +121,18 @@ class OrganizationsList extends Component { selected: [], }); } catch (err) { - this.setState(({ hasContentError: true })); + this.setState({ contentError: err }); } finally { this.setState({ hasContentLoading: false }); } } - render () { - const { - medium, - } = PageSectionVariants; + render() { + const { medium } = PageSectionVariants; const { actions, itemCount, - hasContentError, + contentError, hasContentLoading, hasDeletionError, selected, @@ -139,7 +140,8 @@ class OrganizationsList extends Component { } = this.state; const { match, i18n } = this.props; - const canAdd = actions && Object.prototype.hasOwnProperty.call(actions, 'POST'); + const canAdd = + actions && Object.prototype.hasOwnProperty.call(actions, 'POST'); const isAllSelected = selected.length === organizations.length; return ( @@ -147,18 +149,33 @@ class OrganizationsList extends Component { ( + renderToolbar={props => ( , - canAdd - ? - : null, + canAdd ? ( + + ) : null, ]} /> )} - renderItem={(o) => ( + renderItem={o => ( )} emptyStateControls={ - canAdd ? - : null + canAdd ? ( + + ) : null } /> diff --git a/awx/ui_next/src/screens/Organization/OrganizationNotifications/OrganizationNotifications.jsx b/awx/ui_next/src/screens/Organization/OrganizationNotifications/OrganizationNotifications.jsx index 2abe9941c1..a3861d666f 100644 --- a/awx/ui_next/src/screens/Organization/OrganizationNotifications/OrganizationNotifications.jsx +++ b/awx/ui_next/src/screens/Organization/OrganizationNotifications/OrganizationNotifications.jsx @@ -24,7 +24,7 @@ const COLUMNS = [ ]; class OrganizationNotifications extends Component { - constructor (props) { + constructor(props) { super(props); this.state = { contentError: null, @@ -38,22 +38,24 @@ class OrganizationNotifications extends Component { typeLabels: null, }; this.handleNotificationToggle = this.handleNotificationToggle.bind(this); - this.handleNotificationErrorClose = this.handleNotificationErrorClose.bind(this); + this.handleNotificationErrorClose = this.handleNotificationErrorClose.bind( + this + ); this.loadNotifications = this.loadNotifications.bind(this); } - componentDidMount () { + componentDidMount() { this.loadNotifications(); } - componentDidUpdate (prevProps) { + componentDidUpdate(prevProps) { const { location } = this.props; if (location !== prevProps.location) { this.loadNotifications(); } } - async loadNotifications () { + async loadNotifications() { const { id, location } = this.props; const { typeLabels } = this.state; const params = parseQueryString(QS_CONFIG, location.search); @@ -67,13 +69,13 @@ class OrganizationNotifications extends Component { this.setState({ contentError: null, hasContentLoading: true }); try { const { - data: { - count: itemCount = 0, - results: notifications = [], - } + data: { count: itemCount = 0, results: notifications = [] }, } = await OrganizationsAPI.readNotificationTemplates(id, params); - const optionsResponse = await OrganizationsAPI.readOptionsNotificationTemplates(id, params); + const optionsResponse = await OrganizationsAPI.readOptionsNotificationTemplates( + id, + params + ); let idMatchParams; if (notifications.length > 0) { @@ -122,7 +124,7 @@ class OrganizationNotifications extends Component { } } - async handleNotificationToggle (notificationId, isCurrentlyOn, status) { + async handleNotificationToggle(notificationId, isCurrentlyOn, status) { const { id } = this.props; let stateArrayName; @@ -135,13 +137,15 @@ class OrganizationNotifications extends Component { let stateUpdateFunction; if (isCurrentlyOn) { // when switching off, remove the toggled notification id from the array - stateUpdateFunction = (prevState) => ({ - [stateArrayName]: prevState[stateArrayName].filter(i => i !== notificationId) + stateUpdateFunction = prevState => ({ + [stateArrayName]: prevState[stateArrayName].filter( + i => i !== notificationId + ), }); } else { // when switching on, add the toggled notification id to the array - stateUpdateFunction = (prevState) => ({ - [stateArrayName]: prevState[stateArrayName].concat(notificationId) + stateUpdateFunction = prevState => ({ + [stateArrayName]: prevState[stateArrayName].concat(notificationId), }); } @@ -161,11 +165,11 @@ class OrganizationNotifications extends Component { } } - handleNotificationErrorClose () { + handleNotificationErrorClose() { this.setState({ toggleError: false }); } - render () { + render() { const { canToggleNotifications, i18n } = this.props; const { contentError, @@ -176,7 +180,7 @@ class OrganizationNotifications extends Component { notifications, successTemplateIds, errorTemplateIds, - typeLabels + typeLabels, } = this.state; return ( @@ -189,7 +193,7 @@ class OrganizationNotifications extends Component { itemName="notification" qsConfig={QS_CONFIG} toolbarColumns={COLUMNS} - renderItem={(notification) => ( + renderItem={notification => ( initially renders succesfully 1`] = ` InstanceGroupsAPI.read(params); +const getInstanceGroups = async params => InstanceGroupsAPI.read(params); class InstanceGroupsLookup extends React.Component { - render () { + render() { const { value, tooltip, onChange, i18n } = this.props; return ( - {i18n._(t`Instance Groups`)} - {' '} - { - tooltip && ( - - - - ) - } + {i18n._(t`Instance Groups`)}{' '} + {tooltip && ( + + + + )} - )} + } fieldId="org-instance-groups" > diff --git a/awx/ui_next/src/screens/Template/TemplateList/TemplateList.jsx b/awx/ui_next/src/screens/Template/TemplateList/TemplateList.jsx index ebc9b3b81c..aa278fedf8 100644 --- a/awx/ui_next/src/screens/Template/TemplateList/TemplateList.jsx +++ b/awx/ui_next/src/screens/Template/TemplateList/TemplateList.jsx @@ -2,21 +2,17 @@ import React, { Component } from 'react'; import { withRouter } from 'react-router-dom'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; -import { - Card, - PageSection, - PageSectionVariants, -} from '@patternfly/react-core'; +import { Card, PageSection, PageSectionVariants } from '@patternfly/react-core'; import { JobTemplatesAPI, UnifiedJobTemplatesAPI, - WorkflowJobTemplatesAPI + WorkflowJobTemplatesAPI, } from '@api'; import AlertModal from '@components/AlertModal'; import DatalistToolbar from '@components/DataListToolbar'; import PaginatedDataList, { - ToolbarDeleteButton + ToolbarDeleteButton, } from '@components/PaginatedDataList'; import { getQSConfig, parseQueryString } from '@util/qs'; @@ -28,16 +24,16 @@ const QS_CONFIG = getQSConfig('template', { page: 1, page_size: 5, order_by: 'name', - type: 'job_template,workflow_job_template' + type: 'job_template,workflow_job_template', }); class TemplatesList extends Component { - constructor (props) { + constructor(props) { super(props); this.state = { hasContentLoading: true, - hasContentError: false, + contentError: null, hasDeletionError: false, selected: [], templates: [], @@ -50,28 +46,28 @@ class TemplatesList extends Component { this.handleDeleteErrorClose = this.handleDeleteErrorClose.bind(this); } - componentDidMount () { + componentDidMount() { this.loadTemplates(); } - componentDidUpdate (prevProps) { + componentDidUpdate(prevProps) { const { location } = this.props; if (location !== prevProps.location) { this.loadTemplates(); } } - handleDeleteErrorClose () { + handleDeleteErrorClose() { this.setState({ hasDeletionError: false }); } - handleSelectAll (isSelected) { + handleSelectAll(isSelected) { const { templates } = this.state; const selected = isSelected ? [...templates] : []; this.setState({ selected }); } - handleSelect (template) { + handleSelect(template) { const { selected } = this.state; if (selected.some(s => s.id === template.id)) { this.setState({ selected: selected.filter(s => s.id !== template.id) }); @@ -80,20 +76,22 @@ class TemplatesList extends Component { } } - async handleTemplateDelete () { + async handleTemplateDelete() { const { selected } = this.state; this.setState({ hasContentLoading: true, hasDeletionError: false }); try { - await Promise.all(selected.map(({ type, id }) => { - let deletePromise; - if (type === 'job_template') { - deletePromise = JobTemplatesAPI.destroy(id); - } else if (type === 'workflow_job_template') { - deletePromise = WorkflowJobTemplatesAPI.destroy(id); - } - return deletePromise; - })); + await Promise.all( + selected.map(({ type, id }) => { + let deletePromise; + if (type === 'job_template') { + deletePromise = JobTemplatesAPI.destroy(id); + } else if (type === 'workflow_job_template') { + deletePromise = WorkflowJobTemplatesAPI.destroy(id); + } + return deletePromise; + }) + ); } catch (err) { this.setState({ hasDeletionError: true }); } finally { @@ -101,56 +99,70 @@ class TemplatesList extends Component { } } - async loadTemplates () { + async loadTemplates() { const { location } = this.props; const params = parseQueryString(QS_CONFIG, location.search); - this.setState({ hasContentError: false, hasContentLoading: true }); + this.setState({ contentError: null, hasContentLoading: true }); try { - const { data: { count, results } } = await UnifiedJobTemplatesAPI.read(params); + const { + data: { count, results }, + } = await UnifiedJobTemplatesAPI.read(params); this.setState({ itemCount: count, templates: results, selected: [], }); } catch (err) { - this.setState({ hasContentError: true }); + this.setState({ contentError: err }); } finally { this.setState({ hasContentLoading: false }); } } - render () { + render() { const { - hasContentError, + contentError, hasContentLoading, hasDeletionError, templates, itemCount, selected, } = this.state; - const { - match, - i18n - } = this.props; + const { match, i18n } = this.props; const isAllSelected = selected.length === templates.length; const { medium } = PageSectionVariants; return ( ( + renderToolbar={props => ( + />, ]} /> )} - renderItem={(template) => ( + renderItem={template => ( entriesArr.reduce((acc, [key, value]) => { - if (acc[key] && Array.isArray(acc[key])) { - acc[key].push(value); - } else if (acc[key]) { - acc[key] = [acc[key], value]; - } else { - acc[key] = value; - } - return acc; -}, {}); +const toObject = entriesArr => + entriesArr.reduce((acc, [key, value]) => { + if (acc[key] && Array.isArray(acc[key])) { + acc[key].push(value); + } else if (acc[key]) { + acc[key] = [acc[key], value]; + } else { + acc[key] = value; + } + return acc; + }, {}); /** * helper function to namespace params object @@ -30,7 +31,7 @@ const namespaceParams = (namespace, params = {}) => { }); return namespaced || {}; -} +}; /** * helper function to remove namespace from params object @@ -47,7 +48,7 @@ const denamespaceParams = (namespace, params = {}) => { }); return denamespaced || {}; -} +}; /** * helper function to check the namespace of a param is what you expec @@ -59,7 +60,7 @@ const namespaceMatches = (namespace, fieldname) => { if (!namespace) return !fieldname.includes('.'); return fieldname.startsWith(`${namespace}.`); -} +}; /** * helper function to check the value of a param is equal to another @@ -72,13 +73,15 @@ const paramValueIsEqual = (one, two) => { if (Array.isArray(one) && Array.isArray(two)) { isEqual = one.filter(val => two.indexOf(val) > -1).length === 0; - } else if ((typeof(one) === "string" && typeof(two) === "string") || - (typeof(one) === "number" && typeof(two) === "number")){ + } else if ( + (typeof one === 'string' && typeof two === 'string') || + (typeof one === 'number' && typeof two === 'number') + ) { isEqual = one === two; } return isEqual; -} +}; /** * Convert query param object to url query string @@ -87,17 +90,19 @@ const paramValueIsEqual = (one, two) => { * @param {object} query param object * @return {string} url query string */ -export const encodeQueryString = (params) => { +export const encodeQueryString = params => { if (!params) return ''; return Object.keys(params) .sort() .filter(key => params[key] !== null) - .map(key => ([key, params[key]])) + .map(key => [key, params[key]]) .map(([key, value]) => { // if value is array, should return more than one key value pair if (Array.isArray(value)) { - return value.map(val => `${encodeURIComponent(key)}=${encodeURIComponent(val)}`).join('&'); + return value + .map(val => `${encodeURIComponent(key)}=${encodeURIComponent(val)}`) + .join('&'); } return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`; }) @@ -115,22 +120,31 @@ export const encodeNonDefaultQueryString = (config, params) => { if (!params) return ''; const namespacedParams = namespaceParams(config.namespace, params); - const namespacedDefaults = namespaceParams(config.namespace, config.defaultParams); + const namespacedDefaults = namespaceParams( + config.namespace, + config.defaultParams + ); const namespacedDefaultKeys = Object.keys(namespacedDefaults); - const namespacedParamsWithoutDefaultsKeys = Object.keys(namespacedParams) - .filter(key => namespacedDefaultKeys.indexOf(key) === -1 || - !paramValueIsEqual(namespacedParams[key], namespacedDefaults[key])); + const namespacedParamsWithoutDefaultsKeys = Object.keys( + namespacedParams + ).filter( + key => + namespacedDefaultKeys.indexOf(key) === -1 || + !paramValueIsEqual(namespacedParams[key], namespacedDefaults[key]) + ); return namespacedParamsWithoutDefaultsKeys .sort() .filter(key => namespacedParams[key] !== null) .map(key => { - return ([key, namespacedParams[key]]) + return [key, namespacedParams[key]]; }) .map(([key, value]) => { // if value is array, should return more than one key value pair if (Array.isArray(value)) { - return value.map(val => `${encodeURIComponent(key)}=${encodeURIComponent(val)}`).join('&'); + return value + .map(val => `${encodeURIComponent(key)}=${encodeURIComponent(val)}`) + .join('&'); } return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`; }) @@ -144,7 +158,7 @@ export const encodeNonDefaultQueryString = (config, params) => { * @param {array} params that are number fields * @return {object} query param object */ -export function getQSConfig ( +export function getQSConfig( namespace, defaultParams = { page: 1, page_size: 5, order_by: 'name' }, integerFields = ['page', 'page_size'] @@ -165,12 +179,16 @@ export function getQSConfig ( * @param {string} url query string * @return {object} query param object */ -export function parseQueryString (config, queryString) { +export function parseQueryString(config, queryString) { if (!queryString) return config.defaultParams; - const namespacedIntegerFields = config.integerFields.map(f => config.namespace ? `${config.namespace}.${f}` : f); + const namespacedIntegerFields = config.integerFields.map(f => + config.namespace ? `${config.namespace}.${f}` : f + ); - const keyValuePairs = queryString.replace(/^\?/, '').split('&') + const keyValuePairs = queryString + .replace(/^\?/, '') + .split('&') .map(s => s.split('=')) .map(([key, value]) => { if (namespacedIntegerFields.includes(key)) { @@ -185,37 +203,38 @@ export function parseQueryString (config, queryString) { // needs to return array for duplicate keys // ie [[k1, v1], [k1, v2], [k2, v3]] // -> [[k1, [v1, v2]], [k2, v3]] - const dedupedKeyValuePairs = Object.keys(keyValueObject) - .map(key => { - const values = keyValuePairs - .filter(([k]) => k === key) - .map(([, v]) => v); + const dedupedKeyValuePairs = Object.keys(keyValueObject).map(key => { + const values = keyValuePairs.filter(([k]) => k === key).map(([, v]) => v); - if (values.length === 1) { - return [key, values[0]]; - } + if (values.length === 1) { + return [key, values[0]]; + } - return [key, values]; - }); + return [key, values]; + }); - const parsed = Object.assign(...dedupedKeyValuePairs.map(([k, v]) => ({ - [k]: v - }))); + const parsed = Object.assign( + ...dedupedKeyValuePairs.map(([k, v]) => ({ + [k]: v, + })) + ); const namespacedParams = {}; - Object.keys(parsed) - .forEach(field => { - if (namespaceMatches(config.namespace, field)) { - let fieldname = field; - if (config.namespace) { - fieldname = field.substr(config.namespace.length + 1); - } - namespacedParams[fieldname] = parsed[field]; + Object.keys(parsed).forEach(field => { + if (namespaceMatches(config.namespace, field)) { + let fieldname = field; + if (config.namespace) { + fieldname = field.substr(config.namespace.length + 1); } - }); + namespacedParams[fieldname] = parsed[field]; + } + }); - const namespacedDefaults = namespaceParams(config.namespace, config.defaultParams); + const namespacedDefaults = namespaceParams( + config.namespace, + config.defaultParams + ); Object.keys(namespacedDefaults) .filter(key => Object.keys(parsed).indexOf(key) === -1) @@ -238,11 +257,12 @@ export function parseQueryString (config, queryString) { * @param {object} namespaced params object of default params * @return {object} namespaced params object of only defaults */ -const getDefaultParams = (params, defaults) => toObject( - Object.keys(params) - .filter(key => Object.keys(defaults).indexOf(key) > -1) - .map(key => [key, params[key]]) -); +const getDefaultParams = (params, defaults) => + toObject( + Object.keys(params) + .filter(key => Object.keys(defaults).indexOf(key) > -1) + .map(key => [key, params[key]]) + ); /** * helper function to get params that are not defaults @@ -250,11 +270,12 @@ const getDefaultParams = (params, defaults) => toObject( * @param {object} namespaced params object of default params * @return {object} namespaced params object of non-defaults */ -const getNonDefaultParams = (params, defaults) => toObject( - Object.keys(params) - .filter(key => Object.keys(defaults).indexOf(key) === -1) - .map(key => [key, params[key]]) -); +const getNonDefaultParams = (params, defaults) => + toObject( + Object.keys(params) + .filter(key => Object.keys(defaults).indexOf(key) === -1) + .map(key => [key, params[key]]) + ); /** * helper function to merge old and new params together @@ -262,9 +283,9 @@ const getNonDefaultParams = (params, defaults) => toObject( * @param {object} namespaced params object of new params * @return {object} merged namespaced params object */ -const getMergedParams = (oldParams, newParams) => toObject( - Object.keys(oldParams) - .map(key => { +const getMergedParams = (oldParams, newParams) => + toObject( + Object.keys(oldParams).map(key => { let oldVal = oldParams[key]; const newVal = newParams[key]; if (newVal) { @@ -276,7 +297,7 @@ const getMergedParams = (oldParams, newParams) => toObject( } return [key, oldVal]; }) -); + ); /** * helper function to get new params that are not in merged params @@ -284,11 +305,12 @@ const getMergedParams = (oldParams, newParams) => toObject( * @param {object} namespaced params object of new params * @return {object} remaining new namespaced params object */ -const getRemainingNewParams = (mergedParams, newParams) => toObject( - Object.keys(newParams) - .filter(key => Object.keys(mergedParams).indexOf(key) === -1) - .map(key => [key, newParams[key]]) -); +const getRemainingNewParams = (mergedParams, newParams) => + toObject( + Object.keys(newParams) + .filter(key => Object.keys(mergedParams).indexOf(key) === -1) + .map(key => [key, newParams[key]]) + ); /** * Merges existing params of search string with new ones and returns the updated list of params @@ -297,13 +319,16 @@ const getRemainingNewParams = (mergedParams, newParams) => toObject( * @param {object} object with new params to add * @return {object} query param object */ -export function addParams (config, searchString, newParams) { +export function addParams(config, searchString, newParams) { const namespacedOldParams = namespaceParams( config.namespace, parseQueryString(config, searchString) ); const namespacedNewParams = namespaceParams(config.namespace, newParams); - const namespacedDefaultParams = namespaceParams(config.namespace, config.defaultParams); + const namespacedDefaultParams = namespaceParams( + config.namespace, + config.defaultParams + ); const namespacedOldParamsNotDefaults = getNonDefaultParams( namespacedOldParams, @@ -320,7 +345,7 @@ export function addParams (config, searchString, newParams) { return denamespaceParams(config.namespace, { ...getDefaultParams(namespacedOldParams, namespacedDefaultParams), ...namespacedMergedParams, - ...getRemainingNewParams(namespacedMergedParams, namespacedNewParams) + ...getRemainingNewParams(namespacedMergedParams, namespacedNewParams), }); } @@ -331,26 +356,29 @@ export function addParams (config, searchString, newParams) { * @param {object} object with new params to remove * @return {object} query param object */ -export function removeParams (config, searchString, paramsToRemove) { +export function removeParams(config, searchString, paramsToRemove) { const oldParams = parseQueryString(config, searchString); const paramsEntries = []; - Object.entries(oldParams) - .forEach(([key, value]) => { - if (Array.isArray(value)) { - value.forEach(val => { - paramsEntries.push([key, val]); - }) - } else { - paramsEntries.push([key, value]); - } - }) + Object.entries(oldParams).forEach(([key, value]) => { + if (Array.isArray(value)) { + value.forEach(val => { + paramsEntries.push([key, val]); + }); + } else { + paramsEntries.push([key, value]); + } + }); const paramsToRemoveEntries = Object.entries(paramsToRemove); - const remainingEntries = paramsEntries - .filter(([key, value]) => paramsToRemoveEntries - .filter(([newKey, newValue]) => key === newKey && value === newValue).length === 0); + const remainingEntries = paramsEntries.filter( + ([key, value]) => + paramsToRemoveEntries.filter( + ([newKey, newValue]) => key === newKey && value === newValue + ).length === 0 + ); const remainingObject = toObject(remainingEntries); - const defaultEntriesLeftover = Object.entries(config.defaultParams) - .filter(([key]) => !remainingObject[key]); + const defaultEntriesLeftover = Object.entries(config.defaultParams).filter( + ([key]) => !remainingObject[key] + ); const finalParamsEntries = remainingEntries; defaultEntriesLeftover.forEach(value => { finalParamsEntries.push(value); diff --git a/awx/ui_next/src/util/qs.test.js b/awx/ui_next/src/util/qs.test.js index c6440b24dd..bdea7840a2 100644 --- a/awx/ui_next/src/util/qs.test.js +++ b/awx/ui_next/src/util/qs.test.js @@ -4,7 +4,7 @@ import { parseQueryString, getQSConfig, addParams, - removeParams + removeParams, } from './qs'; describe('qs (qs.js)', () => { @@ -13,14 +13,19 @@ describe('qs (qs.js)', () => { [ [null, ''], [{}, ''], - [{ order_by: 'name', page: 1, page_size: 5 }, 'order_by=name&page=1&page_size=5'], - [{ '-order_by': 'name', page: '1', page_size: 5 }, '-order_by=name&page=1&page_size=5'], - ] - .forEach(([params, expectedQueryString]) => { - const actualQueryString = encodeQueryString(params); + [ + { order_by: 'name', page: 1, page_size: 5 }, + 'order_by=name&page=1&page_size=5', + ], + [ + { '-order_by': 'name', page: '1', page_size: 5 }, + '-order_by=name&page=1&page_size=5', + ], + ].forEach(([params, expectedQueryString]) => { + const actualQueryString = encodeQueryString(params); - expect(actualQueryString).toEqual(expectedQueryString); - }); + expect(actualQueryString).toEqual(expectedQueryString); + }); }); test('encodeQueryString omits null values', () => { @@ -35,7 +40,7 @@ describe('qs (qs.js)', () => { describe('encodeNonDefaultQueryString', () => { const config = { namespace: null, - defaultParams: { page: 1, page_size: 5, order_by: 'name'}, + defaultParams: { page: 1, page_size: 5, order_by: 'name' }, integerFields: ['page'], }; @@ -45,14 +50,19 @@ describe('qs (qs.js)', () => { [{}, ''], [{ order_by: 'name', page: 1, page_size: 5 }, ''], [{ order_by: '-name', page: 1, page_size: 5 }, 'order_by=-name'], - [{ order_by: '-name', page: 3, page_size: 10 }, 'order_by=-name&page=3&page_size=10'], - [{ order_by: '-name', page: 3, page_size: 10, foo: 'bar' }, 'foo=bar&order_by=-name&page=3&page_size=10'], - ] - .forEach(([params, expectedQueryString]) => { - const actualQueryString = encodeNonDefaultQueryString(config, params); + [ + { order_by: '-name', page: 3, page_size: 10 }, + 'order_by=-name&page=3&page_size=10', + ], + [ + { order_by: '-name', page: 3, page_size: 10, foo: 'bar' }, + 'foo=bar&order_by=-name&page=3&page_size=10', + ], + ].forEach(([params, expectedQueryString]) => { + const actualQueryString = encodeNonDefaultQueryString(config, params); - expect(actualQueryString).toEqual(expectedQueryString); - }); + expect(actualQueryString).toEqual(expectedQueryString); + }); }); test('encodeNonDefaultQueryString omits null values', () => { @@ -114,7 +124,7 @@ describe('qs (qs.js)', () => { const query = ''; expect(parseQueryString(config, query)).toEqual({ page: 1, - page_size: 15 + page_size: 15, }); }); @@ -222,7 +232,7 @@ describe('qs (qs.js)', () => { integerFields: ['page', 'page_size'], }; const query = '?baz=bar&page=3'; - const newParams = { bag: 'boom' } + const newParams = { bag: 'boom' }; expect(addParams(config, query, newParams)).toEqual({ baz: 'bar', bag: 'boom', @@ -238,7 +248,7 @@ describe('qs (qs.js)', () => { integerFields: ['page', 'page_size'], }; const query = '?baz=bar&baz=bang&page=3'; - const newParams = { baz: 'boom' } + const newParams = { baz: 'boom' }; expect(addParams(config, query, newParams)).toEqual({ baz: ['bar', 'bang', 'boom'], page: 3, @@ -253,7 +263,7 @@ describe('qs (qs.js)', () => { integerFields: ['page', 'page_size'], }; const query = '?baz=bar&baz=bang&page=3'; - const newParams = { page: 5 } + const newParams = { page: 5 }; expect(addParams(config, query, newParams)).toEqual({ baz: ['bar', 'bang'], page: 5, @@ -268,7 +278,7 @@ describe('qs (qs.js)', () => { integerFields: ['page', 'page_size'], }; const query = '?baz=bar&baz=bang&page=3'; - const newParams = { baz: 'bust', pat: 'pal' } + const newParams = { baz: 'bust', pat: 'pal' }; expect(addParams(config, query, newParams)).toEqual({ baz: ['bar', 'bang', 'bust'], pat: 'pal', @@ -284,7 +294,7 @@ describe('qs (qs.js)', () => { integerFields: ['page', 'page_size'], }; const query = '?item.baz=bar&item.page=3'; - const newParams = { bag: 'boom' } + const newParams = { bag: 'boom' }; expect(addParams(config, query, newParams)).toEqual({ baz: 'bar', bag: 'boom', @@ -300,7 +310,7 @@ describe('qs (qs.js)', () => { integerFields: ['page', 'page_size'], }; const query = '?item.baz=bar&foo.page=3'; - const newParams = { bag: 'boom' } + const newParams = { bag: 'boom' }; expect(addParams(config, query, newParams)).toEqual({ baz: 'bar', bag: 'boom', @@ -316,7 +326,7 @@ describe('qs (qs.js)', () => { integerFields: ['page', 'page_size'], }; const query = '?item.baz=bar&item.baz=bang&item.page=3'; - const newParams = { baz: 'boom' } + const newParams = { baz: 'boom' }; expect(addParams(config, query, newParams)).toEqual({ baz: ['bar', 'bang', 'boom'], page: 3, @@ -331,7 +341,7 @@ describe('qs (qs.js)', () => { integerFields: ['page', 'page_size'], }; const query = '?item.baz=bar&item.baz=bang&item.page=3'; - const newParams = { page: 5 } + const newParams = { page: 5 }; expect(addParams(config, query, newParams)).toEqual({ baz: ['bar', 'bang'], page: 5, @@ -346,7 +356,7 @@ describe('qs (qs.js)', () => { integerFields: ['page', 'page_size'], }; const query = '?item.baz=bar&item.baz=bang&item.page=3'; - const newParams = { baz: 'bust', pat: 'pal' } + const newParams = { baz: 'bust', pat: 'pal' }; expect(addParams(config, query, newParams)).toEqual({ baz: ['bar', 'bang', 'bust'], pat: 'pal', @@ -364,7 +374,7 @@ describe('qs (qs.js)', () => { integerFields: ['page', 'page_size'], }; const query = '?baz=bar&page=3&bag=boom'; - const newParams = { bag: 'boom' } + const newParams = { bag: 'boom' }; expect(removeParams(config, query, newParams)).toEqual({ baz: 'bar', page: 3, @@ -379,7 +389,7 @@ describe('qs (qs.js)', () => { integerFields: ['page', 'page_size'], }; const query = '?baz=bar&baz=bang&page=3'; - const newParams = { baz: 'bar' } + const newParams = { baz: 'bar' }; expect(removeParams(config, query, newParams)).toEqual({ baz: 'bang', page: 3, @@ -394,7 +404,7 @@ describe('qs (qs.js)', () => { integerFields: ['page', 'page_size'], }; const query = '?baz=bar&baz=bang&baz=bust&page=3'; - const newParams = { baz: 'bar' } + const newParams = { baz: 'bar' }; expect(removeParams(config, query, newParams)).toEqual({ baz: ['bang', 'bust'], page: 3, @@ -409,7 +419,7 @@ describe('qs (qs.js)', () => { integerFields: ['page', 'page_size'], }; const query = '?baz=bar&baz=bang&page=3'; - const newParams = { page: 3 } + const newParams = { page: 3 }; expect(removeParams(config, query, newParams)).toEqual({ baz: ['bar', 'bang'], page: 1, @@ -424,7 +434,7 @@ describe('qs (qs.js)', () => { integerFields: ['page', 'page_size'], }; const query = '?baz=bar&baz=bang&baz=bust&pat=pal&page=3'; - const newParams = { baz: 'bust', pat: 'pal' } + const newParams = { baz: 'bust', pat: 'pal' }; expect(removeParams(config, query, newParams)).toEqual({ baz: ['bar', 'bang'], page: 3, @@ -439,7 +449,7 @@ describe('qs (qs.js)', () => { integerFields: ['page', 'page_size'], }; const query = '?item.baz=bar&item.page=3'; - const newParams = { baz: 'bar' } + const newParams = { baz: 'bar' }; expect(removeParams(config, query, newParams)).toEqual({ page: 3, page_size: 15, @@ -453,7 +463,7 @@ describe('qs (qs.js)', () => { integerFields: ['page', 'page_size'], }; const query = '?item.baz=bar&foo.page=3'; - const newParams = { baz: 'bar' } + const newParams = { baz: 'bar' }; expect(removeParams(config, query, newParams)).toEqual({ page: 1, page_size: 15, @@ -467,7 +477,7 @@ describe('qs (qs.js)', () => { integerFields: ['page', 'page_size'], }; const query = '?item.baz=bar&item.baz=bang&item.page=3'; - const newParams = { baz: 'bar' } + const newParams = { baz: 'bar' }; expect(removeParams(config, query, newParams)).toEqual({ baz: 'bang', page: 3, @@ -482,7 +492,7 @@ describe('qs (qs.js)', () => { integerFields: ['page', 'page_size'], }; const query = '?item.baz=bar&item.baz=bang&item.baz=bust&item.page=3'; - const newParams = { baz: 'bar' } + const newParams = { baz: 'bar' }; expect(removeParams(config, query, newParams)).toEqual({ baz: ['bang', 'bust'], page: 3, @@ -497,7 +507,7 @@ describe('qs (qs.js)', () => { integerFields: ['page', 'page_size'], }; const query = '?item.baz=bar&item.baz=bang&item.page=3'; - const newParams = { page: 3 } + const newParams = { page: 3 }; expect(removeParams(config, query, newParams)).toEqual({ baz: ['bar', 'bang'], page: 1, @@ -511,8 +521,9 @@ describe('qs (qs.js)', () => { defaultParams: { page: 1, page_size: 15 }, integerFields: ['page', 'page_size'], }; - const query = '?item.baz=bar&item.baz=bang&item.baz=bust&item.pat=pal&item.page=3'; - const newParams = { baz: 'bust', pat: 'pal' } + const query = + '?item.baz=bar&item.baz=bang&item.baz=bust&item.pat=pal&item.page=3'; + const newParams = { baz: 'bust', pat: 'pal' }; expect(removeParams(config, query, newParams)).toEqual({ baz: ['bar', 'bang'], page: 3,