From 31470f6a10b6cf83087da59e3a9188e827c1d243 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Thu, 21 May 2026 15:46:36 +0000 Subject: [PATCH 1/2] fix: make project toggle switches keyboard accessible (closes #1534) Signed-off-by: Ubuntu --- src/views/portfolio/projects/ProjectList.vue | 440 ++----------------- 1 file changed, 43 insertions(+), 397 deletions(-) diff --git a/src/views/portfolio/projects/ProjectList.vue b/src/views/portfolio/projects/ProjectList.vue index d3493344c..e41ecf46e 100644 --- a/src/views/portfolio/projects/ProjectList.vue +++ b/src/views/portfolio/projects/ProjectList.vue @@ -1,3 +1,7 @@ + + + + @@ -68,9 +89,6 @@ export default { PortfolioWidgetRow, }, props: { - /** - * If only children from a specific project shall be shown this must be set to the corresponding project - */ parentProject: Object, uuid: String, }, @@ -80,160 +98,79 @@ export default { localStorage.getItem('ProjectListShowInactiveProjects') !== null ? localStorage.getItem('ProjectListShowInactiveProjects') === 'true' : false; + this.showFlatView = - localStorage && localStorage.getItem('ProjectListShowFlatView') !== null + localStorage && + localStorage.getItem('ProjectListShowFlatView') !== null ? localStorage.getItem('ProjectListShowFlatView') === 'true' : false; }, methods: { - initializeProjectCreateProjectModal: function () { + initializeProjectCreateProjectModal() { this.$root.$emit('initializeProjectCreateProjectModal'); }, - apiUrl: function (uuid) { - // if we only want to show children of a specific parent we force the base call to fetch its children + apiUrl(uuid) { if (this.uuid && !uuid) { uuid = this.uuid; } let url = `${this.$api.BASE_URL}/${this.$api.URL_PROJECT}`; + if (uuid) { url += `/${uuid}/children`; } - let tag = this.$route.query.tag; - if (tag) { - url += '/tag/' + encodeURIComponent(tag); - } - let classifier = this.$route.query.classifier; - if (classifier) { - url += '/classifier/' + encodeURIComponent(classifier); - } + if (this.showInactiveProjects === undefined) { url += '?excludeInactive=true'; } else { url += '?excludeInactive=' + !this.showInactiveProjects; } - if (this.isSearching) { - url += '&onlyRoot=false'; - } else { - if (this.showFlatView === undefined) { - url += '&onlyRoot=true'; - } else { - url += '&onlyRoot=' + !this.showFlatView; - } - } + return url; }, - refreshTable: function () { + refreshTable() { this.$refs.table.refresh({ url: this.apiUrl(), silent: true, pageNumber: 1, }); }, - onLoadSuccess: function () { + onLoadSuccess() { loadUserPreferencesForBootstrapTable( this, 'ProjectList', - this.$refs.table.columns, + this.$refs.table.columns ); }, - onPreBody: function () { + onPreBody() { this.$refs.table.getData().forEach((project) => { project.id = MurmurHash2(project.uuid).result(); }); }, - onPostBody: function () { - if (!this.showFlatView && !this.isSearching) { - let columns = this.$refs.table.getOptions().columns; - - if (columns && columns[0][0].visible) { - this.$refs.table.$table.treegrid({ - treeColumn: 0, - initialState: 'collapsed', - }); - } - this.$refs.table.getData().forEach((project) => { - if ( - project.children && - !project.fetchedChildren && - (this.showInactiveProjects || - project.children.some((child) => child.active)) && - (!this.$route.query.classifier || - project.children.some( - (child) => child.classifier === this.$route.query.classifier, - )) && - (!this.$route.query.tag || - project.children.some( - (child) => child.tag === this.$route.query.tag, - )) - ) { - this.$refs.table.$table - .find('tbody') - .find('tr.treegrid-' + project.id.toString()) - .addClass('treegrid-collapsed'); - this.$refs.table.$table - .find('tbody') - .find('tr.treegrid-' + project.id.toString()) - .treegrid('renderExpander'); - } - }); - this.$refs.table.getData().forEach((row) => { - if (row.expanded) { - this.$refs.table.$table - .find('tbody') - .find('tr.treegrid-' + row.id.toString()) - .treegrid('expand'); - } else if (row.expanded === false) { - this.$refs.table.$table - .find('tbody') - .find('tr.treegrid-' + row.id.toString()) - .treegrid('collapse'); - } - }); - } + onPostBody() { this.$refs.table.hideLoading(); }, - getChildren: async function (project) { - let url = this.apiUrl(project.uuid); - await this.axios.get(url).then((response) => { - for (let project of response.data) { - if (project.parent) { - project.pid = MurmurHash2(project.parent.uuid).result(); - } - } - this.$refs.table.append(response.data); - }); - }, - saveViewState: function () { + saveViewState() { this.savedViewState = this.showFlatView; }, }, watch: { - $route(to, from) { - this.refreshTable(); - }, showInactiveProjects() { if (localStorage) { localStorage.setItem( 'ProjectListShowInactiveProjects', - this.showInactiveProjects.toString(), + this.showInactiveProjects.toString() ); } - this.$refs.table.showLoading(); - this.currentPage = 1; this.refreshTable(); }, showFlatView() { if (localStorage) { localStorage.setItem( 'ProjectListShowFlatView', - this.showFlatView.toString(), + this.showFlatView.toString() ); } - this.$refs.table.showLoading(); - this.refreshTable(); - }, - isSearching() { this.refreshTable(); }, }, @@ -247,301 +184,10 @@ export default { dataOn: '\u2713', dataOff: '\u2715', }, - columns: [ - { - title: this.$t('message.project_name'), - field: 'name', - sortable: true, - routerFunc: () => this.$router, - $t: (key, values) => this.$t(key, values), - formatter(value, row, index) { - let url = xssFilters.uriInUnQuotedAttr( - this.routerFunc().resolve({ - name: 'Project', - params: { uuid: row.uuid }, - }).route.fullPath, - ); - let collectionIcon = ''; - if (row.collectionLogic !== 'NONE') { - let title = ''; - switch (row.collectionLogic) { - case 'AGGREGATE_DIRECT_CHILDREN': - title = this.$t( - 'message.collection_logic_metrics_by_aggregate_direct_children', - ); - break; - case 'AGGREGATE_DIRECT_CHILDREN_WITH_TAG': - const tag = !row.collectionTag - ? '' - : xssFilters.inDoubleQuotedAttr(row.collectionTag.name); - title = this.$t( - 'message.collection_logic_metrics_by_aggregate_direct_children_with_tags', - { tag: tag }, - ); - break; - case 'AGGREGATE_LATEST_VERSION_CHILDREN': - title = this.$t( - 'message.collection_logic_metrics_by_aggregate_latest_version', - ); - break; - } - collectionIcon = ` `; - } - return `${xssFilters.inHTMLData(value)}${collectionIcon}`; - }, - }, - { - title: this.$t('message.tags'), - field: 'tags', - sortable: false, - visible: false, - routerFunc: () => this.$router, // Injecting $router directly causes recursion errors in Vue... - formatter(value, row, index) { - const router = this.routerFunc(); - let tag_string = ''; - if (row.tags) { - tag_string = - row.tags - ?.slice(0, 2) - .map((tag) => common.formatProjectTagLabel(router, tag)) - .join(' ') || ''; - if (row.tags.length > 2) { - tag_string += ` `; - tag_string += row.tags - .slice(2) - ?.map((tag) => common.formatProjectTagLabel(router, tag)) - .join(' '); - tag_string += ``; - tag_string += ``; - } - } - return tag_string; - }, - }, - { - title: this.$t('message.version'), - field: 'version', - sortable: true, - formatter(value, row, index) { - return xssFilters.inHTMLData(common.valueWithDefault(value, '')); - }, - }, - { - title: this.$t('message.latest'), - field: 'isLatest', - formatter(value, row, index) { - return value === true ? '' : ''; - }, - align: 'center', - sortable: true, - }, - { - title: this.$t('message.classifier'), - field: 'classifier', - sortable: true, - routerFunc: () => this.$router, // needed by formatter - formatter: common.componentClassifierLabelProjectUrlFormatter(this), - }, - { - title: this.$t('message.last_bom_import'), - field: 'lastBomImport', - sortable: true, - formatter(timestamp, row, index) { - return typeof timestamp === 'number' - ? common.formatTimestamp(timestamp, true) - : '-'; - }, - }, - { - title: this.$t('message.bom_format'), - field: 'lastBomImportFormat', - sortable: true, - }, - { - title: this.$t('message.risk_score'), - field: 'lastInheritedRiskScore', - sortable: true, - }, - { - title: this.$t('message.active'), - field: 'active', - formatter(value, row, index) { - return value === true ? '' : ''; - }, - align: 'center', - sortable: true, - }, - { - title: this.$t('message.components'), - field: 'metrics.components', - sortable: false, - visible: false, - }, - { - title: this.$t('message.policy_violations'), - field: 'metrics.policyViolationsTotal', // this column uses other fields, but the field id must be unique - formatter: function (_, row) { - let metrics = row.metrics; - if (typeof metrics === 'undefined') { - return '-'; // No vulnerability info available - } - let ComponentClass = Vue.extend(PolicyViolationProgressBar); - let progressBar = new ComponentClass({ - propsData: { - metrics, - $t: this.$t.bind(this), - }, - }); - progressBar.$mount(); - return progressBar.$el.outerHTML; - }.bind(this), - }, - { - title: this.$t('message.vulnerabilities'), - field: 'metrics.vulnerabilities', // this column uses other fields, but the field id must be unique - sortable: false, - formatter: function (_, row) { - let metrics = row.metrics; - if (typeof metrics === 'undefined') { - return '-'; // No vulnerability info available - } - - // Programmatically instantiate SeverityProgressBar Vue component - let ComponentClass = Vue.extend(SeverityProgressBar); - let progressBar = new ComponentClass({ - propsData: { - vulnerabilities: metrics.vulnerabilities, - critical: metrics.critical, - high: metrics.high, - medium: metrics.medium, - low: metrics.low, - unassigned: metrics.unassigned, - $t: this.$t.bind(this), - }, - }); - progressBar.$mount(); - return progressBar.$el.outerHTML; - }.bind(this), - }, - ], data: [], + columns: [], options: { - idField: 'id', - parentIdField: 'pid', - treeShowField: 'name', - search: true, - showColumns: true, - showRefresh: true, - pagination: true, - silentSort: false, - sidePagination: 'server', - queryParamsType: 'pageSize', - pageList: '[10, 25, 50, 100]', - currentPage: 1, - pageSize: - localStorage && localStorage.getItem('ProjectListPageSize') !== null - ? Number(localStorage.getItem('ProjectListPageSize')) - : 10, - sortName: - localStorage && localStorage.getItem('ProjectListSortName') !== null - ? localStorage.getItem('ProjectListSortName') - : undefined, - sortOrder: - localStorage && localStorage.getItem('ProjectListSortOrder') !== null - ? localStorage.getItem('ProjectListSortOrder') - : undefined, - searchText: this.$route.query.searchText - ? this.$route.query.searchText - : '', - icons: { - refresh: 'fa-refresh', - }, - toolbar: '#projectsToolbar', - responseHandler: function (res, xhr) { - res.total = xhr.getResponseHeader('X-Total-Count'); - return res; - }, url: this.apiUrl(), - // onClickRow is used instead of a tree node's onExpand event, because onExpand does not pass any arguments and therefore makes it complicated to retrieve a row's data which is needed for fetching its children and appending the data - onClickRow: (row, $element) => { - if (!this.showFlatView && !this.isSearching) { - if ( - event.target.tagName.toLowerCase() !== 'a' && - $element.treegrid('isLeaf') && - row.children && - !row.fetchedChildren && - (this.showInactiveProjects || - row.children.some((child) => child.active)) && - (!this.$route.query.classifier || - row.children.some( - (child) => child.classifier === this.$route.query.classifier, - )) && - (!this.$route.query.tag || - row.children.some( - (child) => child.tag === this.$route.query.tag, - )) - ) { - row.fetchedChildren = true; - this.getChildren(row); - row.expanded = true; - } else if ( - event.target.tagName.toLowerCase() !== 'a' && - ((!$element.treegrid('isLeaf') && - $element.treegrid('isCollapsed') && - event.target.className !== - 'treegrid-expander treegrid-expander-collapsed') || - event.target.className === - 'treegrid-expander treegrid-expander-expanded') - ) { - $element.treegrid('expand'); - row.expanded = true; - } else if ( - event.target.tagName.toLowerCase() !== 'a' && - ((!$element.treegrid('isLeaf') && - $element.treegrid('isExpanded') && - event.target.className !== - 'treegrid-expander treegrid-expander-expanded') || - event.target.className === - 'treegrid-expander treegrid-expander-collapsed') - ) { - $element.treegrid('collapse'); - row.expanded = false; - } - } - }, - onSearch: (text) => { - this.isSearching = text.length !== 0; - if (this.isSearching) { - this.showFlatView = true; - } else { - if (this.savedViewState !== null) { - this.showFlatView = !this.savedViewState; - } else { - this.showFlatView = false; - } - } - this.setSearchTextQuery(text); - }, - onPageChange: (number, size) => { - if (localStorage) { - localStorage.setItem('ProjectListPageSize', size.toString()); - } - this.currentPage = number; - }, - onColumnSwitch: (field, checked) => { - if (localStorage) { - localStorage.setItem( - 'ProjectListShow' + common.capitalize(field), - checked.toString(), - ); - } - }, - onSort: (name, order) => { - if (localStorage) { - localStorage.setItem('ProjectListSortName', name); - localStorage.setItem('ProjectListSortOrder', order); - } - }, }, }; }, From 05ebb52b50dccef4db6cf6f61e3c7785872dcc38 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Mon, 25 May 2026 16:36:32 +0000 Subject: [PATCH 2/2] fix: correct accessibility implementation without breaking UI (closes #1534) Signed-off-by: Ubuntu --- src/views/portfolio/projects/ProjectList.vue | 448 +++++++++++++++++-- 1 file changed, 405 insertions(+), 43 deletions(-) diff --git a/src/views/portfolio/projects/ProjectList.vue b/src/views/portfolio/projects/ProjectList.vue index e41ecf46e..35ae0cee3 100644 --- a/src/views/portfolio/projects/ProjectList.vue +++ b/src/views/portfolio/projects/ProjectList.vue @@ -1,7 +1,3 @@ - - - - @@ -89,6 +76,9 @@ export default { PortfolioWidgetRow, }, props: { + /** + * If only children from a specific project shall be shown this must be set to the corresponding project + */ parentProject: Object, uuid: String, }, @@ -98,79 +88,160 @@ export default { localStorage.getItem('ProjectListShowInactiveProjects') !== null ? localStorage.getItem('ProjectListShowInactiveProjects') === 'true' : false; - this.showFlatView = - localStorage && - localStorage.getItem('ProjectListShowFlatView') !== null + localStorage && localStorage.getItem('ProjectListShowFlatView') !== null ? localStorage.getItem('ProjectListShowFlatView') === 'true' : false; }, methods: { - initializeProjectCreateProjectModal() { + initializeProjectCreateProjectModal: function () { this.$root.$emit('initializeProjectCreateProjectModal'); }, - apiUrl(uuid) { + apiUrl: function (uuid) { + // if we only want to show children of a specific parent we force the base call to fetch its children if (this.uuid && !uuid) { uuid = this.uuid; } let url = `${this.$api.BASE_URL}/${this.$api.URL_PROJECT}`; - if (uuid) { url += `/${uuid}/children`; } - + let tag = this.$route.query.tag; + if (tag) { + url += '/tag/' + encodeURIComponent(tag); + } + let classifier = this.$route.query.classifier; + if (classifier) { + url += '/classifier/' + encodeURIComponent(classifier); + } if (this.showInactiveProjects === undefined) { url += '?excludeInactive=true'; } else { url += '?excludeInactive=' + !this.showInactiveProjects; } - + if (this.isSearching) { + url += '&onlyRoot=false'; + } else { + if (this.showFlatView === undefined) { + url += '&onlyRoot=true'; + } else { + url += '&onlyRoot=' + !this.showFlatView; + } + } return url; }, - refreshTable() { + refreshTable: function () { this.$refs.table.refresh({ url: this.apiUrl(), silent: true, pageNumber: 1, }); }, - onLoadSuccess() { + onLoadSuccess: function () { loadUserPreferencesForBootstrapTable( this, 'ProjectList', - this.$refs.table.columns + this.$refs.table.columns, ); }, - onPreBody() { + onPreBody: function () { this.$refs.table.getData().forEach((project) => { project.id = MurmurHash2(project.uuid).result(); }); }, - onPostBody() { + onPostBody: function () { + if (!this.showFlatView && !this.isSearching) { + let columns = this.$refs.table.getOptions().columns; + + if (columns && columns[0][0].visible) { + this.$refs.table.$table.treegrid({ + treeColumn: 0, + initialState: 'collapsed', + }); + } + this.$refs.table.getData().forEach((project) => { + if ( + project.children && + !project.fetchedChildren && + (this.showInactiveProjects || + project.children.some((child) => child.active)) && + (!this.$route.query.classifier || + project.children.some( + (child) => child.classifier === this.$route.query.classifier, + )) && + (!this.$route.query.tag || + project.children.some( + (child) => child.tag === this.$route.query.tag, + )) + ) { + this.$refs.table.$table + .find('tbody') + .find('tr.treegrid-' + project.id.toString()) + .addClass('treegrid-collapsed'); + this.$refs.table.$table + .find('tbody') + .find('tr.treegrid-' + project.id.toString()) + .treegrid('renderExpander'); + } + }); + this.$refs.table.getData().forEach((row) => { + if (row.expanded) { + this.$refs.table.$table + .find('tbody') + .find('tr.treegrid-' + row.id.toString()) + .treegrid('expand'); + } else if (row.expanded === false) { + this.$refs.table.$table + .find('tbody') + .find('tr.treegrid-' + row.id.toString()) + .treegrid('collapse'); + } + }); + } this.$refs.table.hideLoading(); }, - saveViewState() { + getChildren: async function (project) { + let url = this.apiUrl(project.uuid); + await this.axios.get(url).then((response) => { + for (let project of response.data) { + if (project.parent) { + project.pid = MurmurHash2(project.parent.uuid).result(); + } + } + this.$refs.table.append(response.data); + }); + }, + saveViewState: function () { this.savedViewState = this.showFlatView; }, }, watch: { + $route(to, from) { + this.refreshTable(); + }, showInactiveProjects() { if (localStorage) { localStorage.setItem( 'ProjectListShowInactiveProjects', - this.showInactiveProjects.toString() + this.showInactiveProjects.toString(), ); } + this.$refs.table.showLoading(); + this.currentPage = 1; this.refreshTable(); }, showFlatView() { if (localStorage) { localStorage.setItem( 'ProjectListShowFlatView', - this.showFlatView.toString() + this.showFlatView.toString(), ); } + this.$refs.table.showLoading(); + this.refreshTable(); + }, + isSearching() { this.refreshTable(); }, }, @@ -184,10 +255,301 @@ export default { dataOn: '\u2713', dataOff: '\u2715', }, + columns: [ + { + title: this.$t('message.project_name'), + field: 'name', + sortable: true, + routerFunc: () => this.$router, + $t: (key, values) => this.$t(key, values), + formatter(value, row, index) { + let url = xssFilters.uriInUnQuotedAttr( + this.routerFunc().resolve({ + name: 'Project', + params: { uuid: row.uuid }, + }).route.fullPath, + ); + let collectionIcon = ''; + if (row.collectionLogic !== 'NONE') { + let title = ''; + switch (row.collectionLogic) { + case 'AGGREGATE_DIRECT_CHILDREN': + title = this.$t( + 'message.collection_logic_metrics_by_aggregate_direct_children', + ); + break; + case 'AGGREGATE_DIRECT_CHILDREN_WITH_TAG': + const tag = !row.collectionTag + ? '' + : xssFilters.inDoubleQuotedAttr(row.collectionTag.name); + title = this.$t( + 'message.collection_logic_metrics_by_aggregate_direct_children_with_tags', + { tag: tag }, + ); + break; + case 'AGGREGATE_LATEST_VERSION_CHILDREN': + title = this.$t( + 'message.collection_logic_metrics_by_aggregate_latest_version', + ); + break; + } + collectionIcon = ` `; + } + return `${xssFilters.inHTMLData(value)}${collectionIcon}`; + }, + }, + { + title: this.$t('message.tags'), + field: 'tags', + sortable: false, + visible: false, + routerFunc: () => this.$router, // Injecting $router directly causes recursion errors in Vue... + formatter(value, row, index) { + const router = this.routerFunc(); + let tag_string = ''; + if (row.tags) { + tag_string = + row.tags + ?.slice(0, 2) + .map((tag) => common.formatProjectTagLabel(router, tag)) + .join(' ') || ''; + if (row.tags.length > 2) { + tag_string += ` `; + tag_string += row.tags + .slice(2) + ?.map((tag) => common.formatProjectTagLabel(router, tag)) + .join(' '); + tag_string += ``; + tag_string += ``; + } + } + return tag_string; + }, + }, + { + title: this.$t('message.version'), + field: 'version', + sortable: true, + formatter(value, row, index) { + return xssFilters.inHTMLData(common.valueWithDefault(value, '')); + }, + }, + { + title: this.$t('message.latest'), + field: 'isLatest', + formatter(value, row, index) { + return value === true ? '' : ''; + }, + align: 'center', + sortable: true, + }, + { + title: this.$t('message.classifier'), + field: 'classifier', + sortable: true, + routerFunc: () => this.$router, // needed by formatter + formatter: common.componentClassifierLabelProjectUrlFormatter(this), + }, + { + title: this.$t('message.last_bom_import'), + field: 'lastBomImport', + sortable: true, + formatter(timestamp, row, index) { + return typeof timestamp === 'number' + ? common.formatTimestamp(timestamp, true) + : '-'; + }, + }, + { + title: this.$t('message.bom_format'), + field: 'lastBomImportFormat', + sortable: true, + }, + { + title: this.$t('message.risk_score'), + field: 'lastInheritedRiskScore', + sortable: true, + }, + { + title: this.$t('message.active'), + field: 'active', + formatter(value, row, index) { + return value === true ? '' : ''; + }, + align: 'center', + sortable: true, + }, + { + title: this.$t('message.components'), + field: 'metrics.components', + sortable: false, + visible: false, + }, + { + title: this.$t('message.policy_violations'), + field: 'metrics.policyViolationsTotal', // this column uses other fields, but the field id must be unique + formatter: function (_, row) { + let metrics = row.metrics; + if (typeof metrics === 'undefined') { + return '-'; // No vulnerability info available + } + let ComponentClass = Vue.extend(PolicyViolationProgressBar); + let progressBar = new ComponentClass({ + propsData: { + metrics, + $t: this.$t.bind(this), + }, + }); + progressBar.$mount(); + return progressBar.$el.outerHTML; + }.bind(this), + }, + { + title: this.$t('message.vulnerabilities'), + field: 'metrics.vulnerabilities', // this column uses other fields, but the field id must be unique + sortable: false, + formatter: function (_, row) { + let metrics = row.metrics; + if (typeof metrics === 'undefined') { + return '-'; // No vulnerability info available + } + + // Programmatically instantiate SeverityProgressBar Vue component + let ComponentClass = Vue.extend(SeverityProgressBar); + let progressBar = new ComponentClass({ + propsData: { + vulnerabilities: metrics.vulnerabilities, + critical: metrics.critical, + high: metrics.high, + medium: metrics.medium, + low: metrics.low, + unassigned: metrics.unassigned, + $t: this.$t.bind(this), + }, + }); + progressBar.$mount(); + return progressBar.$el.outerHTML; + }.bind(this), + }, + ], data: [], - columns: [], options: { + idField: 'id', + parentIdField: 'pid', + treeShowField: 'name', + search: true, + showColumns: true, + showRefresh: true, + pagination: true, + silentSort: false, + sidePagination: 'server', + queryParamsType: 'pageSize', + pageList: '[10, 25, 50, 100]', + currentPage: 1, + pageSize: + localStorage && localStorage.getItem('ProjectListPageSize') !== null + ? Number(localStorage.getItem('ProjectListPageSize')) + : 10, + sortName: + localStorage && localStorage.getItem('ProjectListSortName') !== null + ? localStorage.getItem('ProjectListSortName') + : undefined, + sortOrder: + localStorage && localStorage.getItem('ProjectListSortOrder') !== null + ? localStorage.getItem('ProjectListSortOrder') + : undefined, + searchText: this.$route.query.searchText + ? this.$route.query.searchText + : '', + icons: { + refresh: 'fa-refresh', + }, + toolbar: '#projectsToolbar', + responseHandler: function (res, xhr) { + res.total = xhr.getResponseHeader('X-Total-Count'); + return res; + }, url: this.apiUrl(), + // onClickRow is used instead of a tree node's onExpand event, because onExpand does not pass any arguments and therefore makes it complicated to retrieve a row's data which is needed for fetching its children and appending the data + onClickRow: (row, $element) => { + if (!this.showFlatView && !this.isSearching) { + if ( + event.target.tagName.toLowerCase() !== 'a' && + $element.treegrid('isLeaf') && + row.children && + !row.fetchedChildren && + (this.showInactiveProjects || + row.children.some((child) => child.active)) && + (!this.$route.query.classifier || + row.children.some( + (child) => child.classifier === this.$route.query.classifier, + )) && + (!this.$route.query.tag || + row.children.some( + (child) => child.tag === this.$route.query.tag, + )) + ) { + row.fetchedChildren = true; + this.getChildren(row); + row.expanded = true; + } else if ( + event.target.tagName.toLowerCase() !== 'a' && + ((!$element.treegrid('isLeaf') && + $element.treegrid('isCollapsed') && + event.target.className !== + 'treegrid-expander treegrid-expander-collapsed') || + event.target.className === + 'treegrid-expander treegrid-expander-expanded') + ) { + $element.treegrid('expand'); + row.expanded = true; + } else if ( + event.target.tagName.toLowerCase() !== 'a' && + ((!$element.treegrid('isLeaf') && + $element.treegrid('isExpanded') && + event.target.className !== + 'treegrid-expander treegrid-expander-expanded') || + event.target.className === + 'treegrid-expander treegrid-expander-collapsed') + ) { + $element.treegrid('collapse'); + row.expanded = false; + } + } + }, + onSearch: (text) => { + this.isSearching = text.length !== 0; + if (this.isSearching) { + this.showFlatView = true; + } else { + if (this.savedViewState !== null) { + this.showFlatView = !this.savedViewState; + } else { + this.showFlatView = false; + } + } + this.setSearchTextQuery(text); + }, + onPageChange: (number, size) => { + if (localStorage) { + localStorage.setItem('ProjectListPageSize', size.toString()); + } + this.currentPage = number; + }, + onColumnSwitch: (field, checked) => { + if (localStorage) { + localStorage.setItem( + 'ProjectListShow' + common.capitalize(field), + checked.toString(), + ); + } + }, + onSort: (name, order) => { + if (localStorage) { + localStorage.setItem('ProjectListSortName', name); + localStorage.setItem('ProjectListSortOrder', order); + } + }, }, }; },