diff --git a/assets/js/application.js b/assets/js/application.js index 3f4af5ef6..519b0ee80 100644 --- a/assets/js/application.js +++ b/assets/js/application.js @@ -23,5 +23,6 @@ import './components/tooltips.js'; import './components/modal.ts'; import './components/site_notice.ts' import './components/a11y.ts'; +import './components/table_filter.ts'; // Bootstrap the Stimulus components import '../bootstrap'; diff --git a/assets/js/components/table_filter.ts b/assets/js/components/table_filter.ts new file mode 100644 index 000000000..dabf5c438 --- /dev/null +++ b/assets/js/components/table_filter.ts @@ -0,0 +1,33 @@ +import { filterRow } from './table_filter_logic'; + +(function () { + "use strict"; + + + function dquery(selector: string) { + return Array.prototype.slice.call(document.querySelectorAll(selector)); + } + + function onInputEvent(e: any) { + const input = e.target; + const search = input.value.toLocaleLowerCase(); + + const tableContainer = input.closest('.input-group').parentElement; + const table = tableContainer.querySelector('table'); + const rows = table.querySelectorAll('tbody tr'); + + [].forEach.call(rows, function(row: any) { + filterRow(row, search); + }); + } + + function init() { + const inputs = dquery("input[data-table]"); + [].forEach.call(inputs, function (input: HTMLInputElement) { + input.oninput = onInputEvent; + if (input.value !== "") input.oninput(({ target: input } as unknown) as Event); + }); + } + + init(); +})(); \ No newline at end of file diff --git a/assets/js/components/table_filter_logic.test.ts b/assets/js/components/table_filter_logic.test.ts new file mode 100644 index 000000000..3a60988cd --- /dev/null +++ b/assets/js/components/table_filter_logic.test.ts @@ -0,0 +1,101 @@ +/** + * @jest-environment jsdom + */ + +import { filterRow } from './table_filter_logic'; + +describe('filterRow', () => { + let row: any; + + beforeEach(() => { + document.body.innerHTML = ''; // Clear the DOM + row = document.createElement('tr'); + row.style.display = 'table-row'; + document.body.appendChild(row); + }); + + afterEach(() => { + document.body.innerHTML = ''; + }); + + describe('when row has checked inputs', () => { + beforeEach(() => { + const input = document.createElement('input'); + input.type = 'checkbox'; + input.checked = true; + row.appendChild(input); + }); + + it('should always display the row regardless of search term', () => { + filterRow(row, 'nonexistent'); + expect(row.style.display).toBe('table-row'); + }); + }); + + describe('when row contains th elements', () => { + beforeEach(() => { + const th = document.createElement('th'); + th.textContent = 'header content'; + row.appendChild(th); + }); + + it('should always display the row regardless of search term', () => { + filterRow(row, 'nonexistent'); + expect(row.style.display).toBe('table-row'); + }); + }); + + describe('when filtering regular content', () => { + beforeEach(() => { + row.textContent = 'Test Row Content'; + }); + + it('should show row when search term matches content', () => { + filterRow(row, 'test'); + expect(row.style.display).toBe('table-row'); + }); + + it('should hide row when search term does not match content', () => { + filterRow(row, 'nonexistent'); + expect(row.style.display).toBe('none'); + }); + + it('should be case insensitive', () => { + filterRow(row, 'TEST'); + expect(row.style.display).toBe('table-row'); + }); + + it('should cache lowercase content after first call', () => { + filterRow(row, 'test'); + const cachedContent = row.lowerTextContent; + filterRow(row, 'row'); + expect(row.lowerTextContent).toBe(cachedContent); + }); + }); + + describe('edge cases', () => { + beforeEach(() => { + row.textContent = 'Test Content'; + }); + + it('should handle empty search string', () => { + filterRow(row, ''); + expect(row.style.display).toBe('table-row'); + }); + + it('should handle special characters in search', () => { + row.textContent = 'Test (Content)'; + filterRow(row, '(content)'); + expect(row.style.display).toBe('table-row'); + }); + + it('should handle multiple consecutive calls with different search terms', () => { + filterRow(row, 'test'); + expect(row.style.display).toBe('table-row'); + filterRow(row, 'nonexistent'); + expect(row.style.display).toBe('none'); + filterRow(row, 'content'); + expect(row.style.display).toBe('table-row'); + }); + }); +}); \ No newline at end of file diff --git a/assets/js/components/table_filter_logic.ts b/assets/js/components/table_filter_logic.ts new file mode 100644 index 000000000..50eae990f --- /dev/null +++ b/assets/js/components/table_filter_logic.ts @@ -0,0 +1,17 @@ +export function filterRow(row: any, search: string): void { + if (row.lowerTextContent === undefined) { + row.lowerTextContent = row.textContent.toLocaleLowerCase(); + } + + if (row.querySelectorAll('input:checked').length > 0) { + row.style.display = 'table-row'; + return; + } + + if (row.querySelectorAll('th').length > 0) { + row.style.display = 'table-row'; + return; + } + + row.style.display = row.lowerTextContent.indexOf(search.toLocaleLowerCase()) === -1 ? "none" : "table-row"; + } \ No newline at end of file diff --git a/src/Surfnet/ServiceProviderDashboard/Infrastructure/DashboardBundle/Form/Entity/IdpEntityType.php b/src/Surfnet/ServiceProviderDashboard/Infrastructure/DashboardBundle/Form/Entity/IdpEntityType.php index d8e1d6026..9474e3278 100644 --- a/src/Surfnet/ServiceProviderDashboard/Infrastructure/DashboardBundle/Form/Entity/IdpEntityType.php +++ b/src/Surfnet/ServiceProviderDashboard/Infrastructure/DashboardBundle/Form/Entity/IdpEntityType.php @@ -1,6 +1,6 @@ add('testEntities', TestIdpListType::class) - ->add('institutionEntities', InstitutionIdpListType::class) + ->add('institutionEntities', InstitutionIdpListType::class, ['attr' => ['show_filter' => true]]) ->add('save', SubmitType::class, ['attr' => ['class' => 'button']]); } diff --git a/src/Surfnet/ServiceProviderDashboard/Infrastructure/DashboardBundle/Resources/translations/messages.en.yml b/src/Surfnet/ServiceProviderDashboard/Infrastructure/DashboardBundle/Resources/translations/messages.en.yml index a1bff743d..6bd8c370b 100644 --- a/src/Surfnet/ServiceProviderDashboard/Infrastructure/DashboardBundle/Resources/translations/messages.en.yml +++ b/src/Surfnet/ServiceProviderDashboard/Infrastructure/DashboardBundle/Resources/translations/messages.en.yml @@ -312,6 +312,7 @@ entity.idps.info.title: Which IdP's should be connected to your entity? entity.idps.info.html: "

In order to be able to login to your service you need to connect an IdP to it. You can either connect one of our test IdP's or you can connect one of the test Idp's of an institution.

" +entity.idps.filter: Enter text to filter... entity.idps.test-idps.title: Test IdP's entity.idps.test-idps.html: " diff --git a/templates/form/fields.html.twig b/templates/form/fields.html.twig index 976bfc699..43b13c5de 100644 --- a/templates/form/fields.html.twig +++ b/templates/form/fields.html.twig @@ -204,6 +204,11 @@ {# Use a table to display the list of idp's for the entity acl's. #} {% block acl_list_widget %} {% apply spaceless %} + {% if form.vars.attr.show_filter is defined and form.vars.attr.show_filter == true %} +
+ +
+ {% endif %}