Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion force-app/main/default/lwc/sfpegListCmp/sfpegListCmp.html
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,14 @@ <h2 class="slds-card__header-title">

<!-- Ready state display -->
<template if:true={isReady}>

<template if:true={multipleFilters}>
<c-sfpeg-multiple-filters
columns={configDetails.display.columns}
records={resultList}
is-debug={isDebugFine}
onfilterchange={handleMultipleFiltersChange}>
</c-sfpeg-multiple-filters>
</template>
<!-- Error Message display -->
<template if:true={hasErrorMsg}>
<c-sfpeg-warning-dsp wrapping-class="slds-var-m-horizontal_medium slds-var-m-bottom_small slds-media slds-media_center"
Expand Down
121 changes: 121 additions & 0 deletions force-app/main/default/lwc/sfpegListCmp/sfpegListCmp.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export default class SfpegListCmp extends LightningElement {
@api showCount = 'right'; // Flag to display the items count.
@api showSearch = false; // Flag to show Filter action in header.
@api showExport = false; // Flag to show Export action in header.
@api multipleFilters = false; // Flag to show multiple column filters component
@api displayHeight = 0; // Max-height of the content Div (0 meaning no limit)
@api maxSize = 100; // Header Action list overflow limit
@api isCollapsible = false; // Flag to set the list details as collapsible
Expand Down Expand Up @@ -1241,6 +1242,126 @@ export default class SfpegListCmp extends LightningElement {

}

// Multiple Filters handling
handleMultipleFiltersChange(event) {
if (this.isDebug) console.log('handleMultipleFiltersChange: START', JSON.stringify(event.detail));

this.isFiltering = true;

// Child component already validates and normalizes filters, but add minimal safety check
const rawFilters = event.detail || {};
this.multipleFilterValues = {};

// Lightweight validation: only include valid keys and non-null values (child does full validation)
for (const key in rawFilters) {
if (key && rawFilters[key] != null) {
this.multipleFilterValues[key] = rawFilters[key];
}
}

const filterKeys = Object.keys(this.multipleFilterValues);
const hasFilters = filterKeys.length > 0;

// Store original list if not already stored
if (!this.resultListOrig) {
if (this.isDebug) console.log('handleMultipleFiltersChange: storing original list');
this.resultListOrig = [...this.resultList];
}

// Early exit: no filters - restore original list
if (!hasFilters) {
if (this.isDebug) console.log('handleMultipleFiltersChange: no filters - restoring original list');
this.isFiltered = false;
setTimeout(() => {
this.resultList = [...this.resultListOrig];
if (this.hideCheckbox) this.selectedRecords = this.resultList;
this.isFiltering = false;
if (this.isDebug) console.log('handleMultipleFiltersChange: END (no filters)');
}, 0);
return;
}

// Extract filter criteria
const generalSearchKeywords = this.multipleFilterValues._generalSearch;
const columnFilterKeys = filterKeys.filter(key => key !== '_generalSearch');
const hasColumnFilters = columnFilterKeys.length > 0;
const hasGeneralSearch = generalSearchKeywords && generalSearchKeywords.length > 0;

if (this.isDebug) console.log('handleMultipleFiltersChange: applying filters', JSON.stringify(this.multipleFilterValues));
this.isFiltered = true;

// Apply filters - all logic inline
const filteredList = this.resultListOrig.filter(record => {
if (!record) return false;

// Check column-specific filters
if (hasColumnFilters) {
for (const fieldName of columnFilterKeys) {
const filterValue = this.multipleFilterValues[fieldName];
if (!filterValue) continue;

const fieldValue = record[fieldName];

// Date range filter
if (typeof filterValue === 'object' && filterValue.mode === 'range') {
let recordDateStr = String(fieldValue || '').trim();
if (recordDateStr.includes('T')) recordDateStr = recordDateStr.split('T')[0];
if (!recordDateStr) return false;
if (filterValue.startDate && recordDateStr < filterValue.startDate.trim()) return false;
if (filterValue.endDate && recordDateStr > filterValue.endDate.trim()) return false;
}
// Date specific filter
else if (typeof filterValue === 'object' && filterValue.mode === 'specific') {
let recordDateStr = String(fieldValue || '').trim();
if (recordDateStr.includes('T')) recordDateStr = recordDateStr.split('T')[0];
let filterDateStr = String(filterValue.value || '').trim();
if (filterDateStr.includes('T')) filterDateStr = filterDateStr.split('T')[0];
if (recordDateStr !== filterDateStr) return false;
}
// Multi-select filter (OR logic) - values already normalized to lowercase by child
else if (Array.isArray(filterValue)) {
const fieldValueStr = String(fieldValue || '').toLowerCase();
const matches = filterValue.some(selectedValue => {
const selectedValueLower = String(selectedValue || '').toLowerCase();
return fieldValueStr === selectedValueLower || fieldValueStr.includes(selectedValueLower);
});
if (!matches) return false;
}
// String filter - value already normalized to lowercase by child
else {
const fieldValueLower = String(fieldValue || '').toLowerCase();
const filterValueLower = String(filterValue || '').toLowerCase();
if (!fieldValueLower.includes(filterValueLower)) return false;
}
}
}

// Check general search if present (OR logic - any keyword match)
if (hasGeneralSearch) {
let searchableText = '';
for (const value of Object.values(record)) {
if (value != null) searchableText += String(value).toLowerCase() + ' ';
}
const hasMatch = generalSearchKeywords.some(keyword => searchableText.includes(keyword));
if (!hasMatch) return false;
}

return true;
});

// Use setTimeout to defer assignment (same pattern as filterRecords)
setTimeout(() => {
this.resultList = filteredList;
if (this.hideCheckbox) this.selectedRecords = this.resultList;
this.isFiltering = false;

if (this.isDebug) {
console.log('handleMultipleFiltersChange: filtered', filteredList.length, 'of', this.resultListOrig.length, 'records');
console.log('handleMultipleFiltersChange: END');
}
}, 0);
}

// Expand / Collapse management
handleExpandCollapse(event) {
if (this.isDebug) console.log('handleExpandCollapse: START with isCollapsed ',this.isCollapsed);
Expand Down
10 changes: 10 additions & 0 deletions force-app/main/default/lwc/sfpegListCmp/sfpegListCmp.js-meta.xml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@
type="Boolean"
default="false"
description="Flag to show Filter action in header."/>
<property name="multipleFilters"
label="Show Multiple Filters?"
type="Boolean"
default="false"
description="Flag to show multiple column filters component."/>
<property name="showExport"
label="Show Export?"
type="Boolean"
Expand Down Expand Up @@ -148,6 +153,11 @@
type="Boolean"
default="false"
description="Flag to show Filter action in header."/>
<property name="multipleFilters"
label="Show Multiple Filters?"
type="Boolean"
default="false"
description="Flag to show multiple column filters component."/>
<property name="showExport"
label="Show Export?"
type="Boolean"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<template>

</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { LightningElement,api } from 'lwc';
import customImageTemplate from './sfpegListCustomImageTemplate';

export default class SfpegListCustomDataTypesProvider extends LightningElement {

//----------------------------------------------------------------
// Return Custom Data Types
//----------------------------------------------------------------
@api
getDataTypes() {
return {
//A custom type 'customImage' to be able to display images in any column of the lightning Datatable
customImage: {
template: customImageTemplate, //imported template
typeAttributes: ["imageUrl"], //attributes needed : imageUrl to be used in template
standardCellLayout: true
},
//other custom types can be added here
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>64.0</apiVersion>
<isExposed>false</isExposed>
</LightningComponentBundle>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<template>
<img
src={typeAttributes.imageUrl}
class="slds-avatar slds-avatar_circle slds-avatar_large"
/>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
.slds-form-element {
width: 100%;
max-width: 100%;
position: relative;
box-sizing: border-box;
min-width: 0;
}

.slds-form-element__control {
width: 100%;
max-width: 100%;
position: relative;
display: block;
box-sizing: border-box;
min-width: 0;
}

/* Container for input and dropdown - ensures proper alignment */
.input-dropdown-container {
position: relative !important;
width: 100% !important;
max-width: 100% !important;
display: block !important;
box-sizing: border-box !important;
min-width: 0 !important;
/* Override any parent flex/grid positioning */
flex: none !important;
align-self: auto !important;
}

/* Ensure the input takes full width - responsive */
.input-dropdown-container lightning-input {
display: block !important;
width: 100% !important;
max-width: 100% !important;
position: relative;
box-sizing: border-box !important;
min-width: 0 !important;
}

/* Force the input field inside to be responsive */
.input-dropdown-container lightning-input ::slotted(input),
.input-dropdown-container lightning-input input {
width: 100% !important;
max-width: 100% !important;
box-sizing: border-box !important;
min-width: 0 !important;
}

/* Responsive font sizes for searchable combobox input */
@media (max-width: 1000px) {
.input-dropdown-container lightning-input {
font-size: 0.875rem; /* Smaller font on mobile */
}
}

@media (min-width: 1001px) and (max-width: 2089px) {
.input-dropdown-container lightning-input {
font-size: 0.8125rem; /* Smaller font on tablet */
}
}

@media (min-width: 2090px) {
.input-dropdown-container lightning-input {
font-size: 0.875rem; /* Standard font on extra large screens */
}
}

.slds-dropdown {
z-index: 10000 !important;
position: absolute !important;
top: 100% !important;
left: 0 !important;
right: auto !important;
width: 100% !important;
min-width: 100% !important;
max-width: 100% !important;
max-height: 300px;
overflow-y: auto;
background-color: var(--lwc-colorBackground, #ffffff);
border: 1px solid var(--lwc-colorBorder, #dddbda);
border-radius: 0.25rem;
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.16);
margin-top: 0.125rem;
margin-left: 0 !important;
margin-right: 0 !important;
/* Ensure dropdown is not affected by parent flex/grid */
transform: translateX(0) !important;
}

.slds-listbox__option {
cursor: pointer;
padding: 0.5rem;
}

.slds-listbox__option:hover {
background-color: var(--lwc-colorBackgroundRowHover, #f3f2f2);
}

.slds-listbox__option[aria-selected="true"] {
background-color: var(--lwc-colorBackgroundRowSelected, #ecebea);
}

.pills-container {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
min-height: 1.5rem;
gap: 0.25rem;
width: 100%;
max-width: 100%;
box-sizing: border-box;
min-width: 0;
}

/* Wrapper to allow pill truncation */
.pill-wrapper {
flex: 0 1 auto;
min-width: 0;
}

/* Constrain individual pill width to prevent taking full line - responsive */
.pill-wrapper lightning-pill {
display: inline-block;
max-width: 300px; /* Default - will be overridden by media queries */
box-sizing: border-box;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

/* Responsive pill max-widths */
@media (max-width: 1000px) {
.pill-wrapper lightning-pill {
max-width: 200px !important; /* Smaller on mobile screens */
}
}

@media (min-width: 1001px) and (max-width: 2089px) {
.pill-wrapper lightning-pill {
max-width: 100px !important; /* Medium size for tablet (3 per row layout) */
}
}

@media (min-width: 2090px) {
.pill-wrapper lightning-pill {
max-width: 300px !important; /* Larger size for extra large screens (6 per row layout) */
}
}
Loading