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
5 changes: 3 additions & 2 deletions Website/components/datamodelview/Attributes.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use client'

import { columnMatchesSearch } from "@/lib/columnSearch"
import { EntityType, AttributeType } from "@/lib/Types"
import { useState, useEffect } from "react"
import { AttributeDetails } from "./entity/AttributeDetails"
Expand Down Expand Up @@ -105,7 +106,7 @@ export const Attributes = ({ entity, search = "", onVisibleCountChange }: IAttri
// Also filter by parent search prop if provided
if (search && search.length >= 3) {
const query = search.toLowerCase()
filteredAttributes = filteredAttributes.filter(attr => attributeMatchesSearch(attr, query))
filteredAttributes = filteredAttributes.filter(attr => columnMatchesSearch(attr, query, searchScope))
}

if (hideStandardFields) filteredAttributes = filteredAttributes.filter(attr => (attr.IsCustomAttribute || attr.IsStandardFieldModified) && !attr.SchemaName.endsWith("Base"));
Expand Down Expand Up @@ -457,4 +458,4 @@ function getAttributeComponent(entity: EntityType, attribute: AttributeType, hig
default:
return null;
}
}
}
2 changes: 1 addition & 1 deletion Website/components/datamodelview/List.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export const List = ({ setCurrentIndex, entityActiveTabs }: IListProps) => {

// Only recalculate items when filtered or search changes
const flatItems = useMemo(() => {
if (filtered && filtered.length > 0) return filtered.filter(item => item.type !== 'attribute' && item.type !== 'relationship');
if (search.length >= 3) return filtered.filter(item => item.type !== 'attribute' && item.type !== 'relationship');

const lowerSearch = search.trim().toLowerCase();
const items: Array<
Expand Down
47 changes: 9 additions & 38 deletions Website/components/datamodelview/searchWorker.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { columnMatchesSearch } from "@/lib/columnSearch";
import { GroupType, EntityType, AttributeType, RelationshipType } from "@/lib/Types";

// Worker message types
Expand Down Expand Up @@ -134,7 +135,7 @@ self.onmessage = async function (e: MessageEvent<WorkerMessage>) {
// Apply hideStandardFields filter
if (entityFilter.hideStandardFields) {
const isStandardFieldHidden = !attr.IsCustomAttribute && !attr.IsStandardFieldModified;
if (isStandardFieldHidden) return false;
if (isStandardFieldHidden || attr.SchemaName.endsWith("Base")) return false;
}

// Apply type filter
Expand All @@ -151,43 +152,7 @@ self.onmessage = async function (e: MessageEvent<WorkerMessage>) {
}
}

// Apply search matching based on scope
let matches = false;

// Column names (SchemaName and DisplayName)
if (searchScope.columnNames) {
if (attr.SchemaName.toLowerCase().includes(search)) matches = true;
if (attr.DisplayName && attr.DisplayName.toLowerCase().includes(search)) matches = true;
}

// Column descriptions
if (searchScope.columnDescriptions) {
if (attr.Description && attr.Description.toLowerCase().includes(search)) matches = true;
}

// Column data types
if (searchScope.columnDataTypes) {
if (attr.AttributeType.toLowerCase().includes(search)) matches = true;

// Also search in specific type properties
if (attr.AttributeType === 'ChoiceAttribute' || attr.AttributeType === 'StatusAttribute') {
if (attr.Options.some(option => option.Name.toLowerCase().includes(search))) matches = true;
} else if (attr.AttributeType === 'DateTimeAttribute') {
if (attr.Format.toLowerCase().includes(search) || attr.Behavior.toLowerCase().includes(search)) matches = true;
} else if (attr.AttributeType === 'IntegerAttribute') {
if (attr.Format.toLowerCase().includes(search)) matches = true;
} else if (attr.AttributeType === 'StringAttribute') {
if (attr.Format.toLowerCase().includes(search)) matches = true;
} else if (attr.AttributeType === 'DecimalAttribute') {
if (attr.Type.toLowerCase().includes(search)) matches = true;
} else if (attr.AttributeType === 'LookupAttribute') {
if (attr.Targets.some(target => target.Name.toLowerCase().includes(search))) matches = true;
} else if (attr.AttributeType === 'BooleanAttribute') {
if (attr.TrueLabel.toLowerCase().includes(search) || attr.FalseLabel.toLowerCase().includes(search)) matches = true;
}
}

return matches;
return columnMatchesSearch(attr, search, searchScope);
});

// Check for table description matches
Expand Down Expand Up @@ -260,6 +225,12 @@ self.onmessage = async function (e: MessageEvent<WorkerMessage>) {
}
}

if (allItems.length === 0) {
const response: WorkerResponse = { type: 'results', data: [], complete: true, requestId };
self.postMessage(response);
return;
}

// Send results in chunks to prevent UI blocking
for (let i = 0; i < allItems.length; i += CHUNK_SIZE) {
const chunk = allItems.slice(i, i + CHUNK_SIZE);
Expand Down
47 changes: 47 additions & 0 deletions Website/lib/columnSearch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import type { AttributeType } from "./Types";

export interface ColumnSearchScope {
columnNames: boolean;
columnDescriptions: boolean;
columnDataTypes: boolean;
}

// Keep worker results and the rendered column rows on the same matching rules.
export function columnMatchesSearch(attribute: AttributeType, query: string, scope: ColumnSearchScope): boolean {
const term = query.trim().toLowerCase();
if (!term) return true;
const contains = (value: string | null | undefined) => !!value?.toLowerCase().includes(term);
if (scope.columnNames && (contains(attribute.SchemaName) || contains(attribute.DisplayName))) return true;
if (scope.columnDescriptions && contains(attribute.Description)) return true;
if (!scope.columnDataTypes) return false;

const values: string[] = [attribute.AttributeType];
switch (attribute.AttributeType) {
case "ChoiceAttribute":
values.push("Choice", `${attribute.Type}-select`, ...attribute.Options.map(option => option.Name));
break;
case "StatusAttribute":
values.push("Choice", "Single-select", ...attribute.Options.map(option => option.Name));
break;
case "DateTimeAttribute":
values.push(attribute.Format, attribute.Behavior, `${attribute.Format} - ${attribute.Behavior}`);
break;
case "StringAttribute":
values.push("Text", attribute.Format);
break;
case "IntegerAttribute":
values.push(attribute.Format);
break;
case "GenericAttribute":
case "DecimalAttribute":
values.push(attribute.Type);
break;
case "LookupAttribute":
values.push(...attribute.Targets.map(target => target.Name));
break;
case "BooleanAttribute":
values.push(attribute.TrueLabel, attribute.FalseLabel);
break;
}
return values.some(contains);
}
44 changes: 44 additions & 0 deletions Website/scripts/test-column-search.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const vm = require('node:vm');
const ts = require('typescript');
const root = path.resolve(__dirname, '..');
const compile = file => ts.transpileModule(fs.readFileSync(path.join(root, file), 'utf8'), {compilerOptions: {module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020}}).outputText;
const matcherExports = {};
vm.runInNewContext(compile('lib/columnSearch.ts'), {exports: matcherExports});
const {columnMatchesSearch} = matcherExports;
const names = {columnNames:true,columnDescriptions:false,columnDataTypes:false};
const descriptions = {...names,columnNames:false,columnDescriptions:true};
const types = {...names,columnNames:false,columnDataTypes:true};
const base = {SchemaName:'dmvp_Forum',DisplayName:'Forum',Description:'Discussion forum',IsCustomAttribute:true,IsStandardFieldModified:false};
const text = {...base,AttributeType:'StringAttribute',Format:'Rich Text',MaxLength:2000};
const date = {...base,SchemaName:'dmvp_Start',DisplayName:'Start',AttributeType:'DateTimeAttribute',Format:'Date',Behavior:'DateOnly'};
const choice = {...base,SchemaName:'dmvp_Category',DisplayName:'Category',Description:null,AttributeType:'ChoiceAttribute',Type:'Multi',Options:[{Name:'Selected',Value:1}]};
assert.equal(columnMatchesSearch(text,'forum',types),false,'Data types must not search names/descriptions');
assert.equal(columnMatchesSearch(text,'discussion',names),false,'Names scope must not search descriptions');
assert.equal(columnMatchesSearch(text,'discussion',descriptions),true);
assert.equal(columnMatchesSearch(text,'rich',types),true);
assert.equal(columnMatchesSearch(text,'text',types),true);
assert.equal(columnMatchesSearch(date,'Date - DateOnly',types),true);
assert.equal(columnMatchesSearch({...date,Format:'Date & time'},'date & time',types),true);
assert.equal(columnMatchesSearch(choice,'choice',types),true);
assert.equal(columnMatchesSearch(choice,'multi-select',types),true);
assert.equal(columnMatchesSearch({...choice,Type:'Single'},'single-select',types),true);
assert.equal(columnMatchesSearch(choice,'selected',names),false);
assert.equal(columnMatchesSearch(choice,'selected',types),true);
const messages=[];
const worker={postMessage:message=>messages.push(message)};
vm.runInNewContext(compile('components/datamodelview/searchWorker.ts'), {exports:{},self:worker,setTimeout,require:name=>{assert.equal(name,'@/lib/columnSearch');return matcherExports;}});
(async()=>{
await worker.onmessage({data:{type:'init',groups:[{Name:'Playground',Entities:[{SchemaName:'dmvp_Project',DisplayName:'Project',Description:null,Attributes:[text,date,choice],Relationships:[],SecurityRoles:[]}]}]}});
for(const [query,scope,expected] of [['forum',types,[]],['discussion',names,[]],['rich',types,['dmvp_Forum']],['Date - DateOnly',types,['dmvp_Start']],['multi-select',types,['dmvp_Category']],['nomatches',types,[]]]) {
messages.length=0;
await worker.onmessage({data:{type:'search',data:query,searchScope:scope,requestId:42}});
assert.equal(messages.at(-1).complete,true,`${query}: completion required even without results`);
assert.equal(messages.at(-1).requestId,42);
const rows=messages.flatMap(message=>message.data||[]).filter(item=>item.type==='attribute').map(item=>item.attribute.SchemaName);
assert.deepEqual(Array.from(rows),expected,query);
}
console.log('PASS: 12 scoped matching assertions and 6 worker result/completion scenarios');
})().catch(error=>{console.error(error);process.exitCode=1;});
Loading