Skip to content

Commit 2c34db0

Browse files
Merge pull request #1771 from exogee-technology/feature/searchable-relationship-filters
Feature / Searchable Relationship Filters - Case Insensitive / Partial Text Filters
2 parents d55dbbd + b061123 commit 2c34db0

File tree

38 files changed

+89
-14
lines changed

38 files changed

+89
-14
lines changed

src/examples/auth-zero/src/frontend/types.generated.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ export type AdminUiFieldMetadata = {
8989

9090
export type AdminUiFilterMetadata = {
9191
__typename?: 'AdminUiFilterMetadata';
92+
options?: Maybe<Scalars['JSON']['output']>;
9293
type: AdminUiFilterType;
9394
};
9495

src/examples/auth-zero/src/types.generated.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ export type AdminUiFieldMetadata = {
8989

9090
export type AdminUiFilterMetadata = {
9191
__typename?: 'AdminUiFilterMetadata';
92+
options?: Maybe<Scalars['JSON']['output']>;
9293
type: AdminUiFilterType;
9394
};
9495

src/examples/aws-cognito/src/frontend/types.generated.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ export type AdminUiFieldMetadata = {
8787

8888
export type AdminUiFilterMetadata = {
8989
__typename?: 'AdminUiFilterMetadata';
90+
options?: Maybe<Scalars['JSON']['output']>;
9091
type: AdminUiFilterType;
9192
};
9293

src/examples/aws-cognito/src/types.generated.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ export type AdminUiFieldMetadata = {
8787

8888
export type AdminUiFilterMetadata = {
8989
__typename?: 'AdminUiFilterMetadata';
90+
options?: Maybe<Scalars['JSON']['output']>;
9091
type: AdminUiFilterType;
9192
};
9293

src/examples/databases/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ CREATE TABLE "user" (
2121
age INTEGER
2222
);
2323
24+
CREATE EXTENSION IF NOT EXISTS pg_trgm;
25+
CREATE INDEX user_username_trigram ON "user" USING gin (username gin_trgm_ops);
26+
2427
-- Seed data for user table
2528
INSERT INTO "user" (username, email)
2629
VALUES

src/examples/databases/src/backend/schema/task.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,17 @@ export class Task {
3131
@Field(() => Boolean)
3232
isCompleted!: boolean;
3333

34+
@RelationshipField<OrmTask>(() => User, {
35+
id: (entity) => entity.userId,
36+
adminUIOptions: {
37+
filterOptions: {
38+
orderBy: { username: 'ASC' },
39+
searchableFields: ['username'],
40+
},
41+
},
42+
})
43+
user!: User;
44+
3445
@Field(() => Date)
3546
createdAt!: Date;
3647

@@ -53,7 +64,4 @@ export class Task {
5364
description: task.description,
5465
};
5566
}
56-
57-
@RelationshipField<OrmTask>(() => User, { id: (entity) => entity.userId })
58-
user!: User;
5967
}

src/examples/databases/src/backend/schema/user.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,14 @@ export class User {
2525
@Field(() => ID)
2626
id!: string;
2727

28-
@Field(() => String)
28+
@Field(() => String, {
29+
adminUIOptions: {
30+
filterOptions: {
31+
caseInsensitive: true,
32+
substringMatch: true,
33+
},
34+
},
35+
})
2936
username!: string;
3037

3138
@Field(() => String)

src/examples/databases/src/frontend/types.generated.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ export type AdminUiFieldMetadata = {
9393

9494
export type AdminUiFilterMetadata = {
9595
__typename?: 'AdminUiFilterMetadata';
96+
options?: Maybe<Scalars['JSON']['output']>;
9697
type: AdminUiFilterType;
9798
};
9899

src/examples/databases/src/types.generated.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ export type AdminUiFieldMetadata = {
9393

9494
export type AdminUiFilterMetadata = {
9595
__typename?: 'AdminUiFilterMetadata';
96+
options?: Maybe<Scalars['JSON']['output']>;
9697
type: AdminUiFilterType;
9798
};
9899

src/examples/federation/src/frontend/types.generated.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ export type AdminUiFieldMetadata = {
9090

9191
export type AdminUiFilterMetadata = {
9292
__typename?: 'AdminUiFilterMetadata';
93+
options?: Maybe<Scalars['JSON']['output']>;
9394
type: AdminUiFilterType;
9495
};
9596

src/examples/federation/src/types.generated.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ export type AdminUiFieldMetadata = {
9090

9191
export type AdminUiFilterMetadata = {
9292
__typename?: 'AdminUiFilterMetadata';
93+
options?: Maybe<Scalars['JSON']['output']>;
9394
type: AdminUiFilterType;
9495
};
9596

src/examples/microsoft-entra/src/frontend/types.generated.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ export type AdminUiFieldMetadata = {
8989

9090
export type AdminUiFilterMetadata = {
9191
__typename?: 'AdminUiFilterMetadata';
92+
options?: Maybe<Scalars['JSON']['output']>;
9293
type: AdminUiFilterType;
9394
};
9495

src/examples/microsoft-entra/src/types.generated.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ export type AdminUiFieldMetadata = {
8989

9090
export type AdminUiFilterMetadata = {
9191
__typename?: 'AdminUiFilterMetadata';
92+
options?: Maybe<Scalars['JSON']['output']>;
9293
type: AdminUiFilterType;
9394
};
9495

src/examples/okta/src/frontend/types.generated.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ export type AdminUiFieldMetadata = {
8989

9090
export type AdminUiFilterMetadata = {
9191
__typename?: 'AdminUiFilterMetadata';
92+
options?: Maybe<Scalars['JSON']['output']>;
9293
type: AdminUiFilterType;
9394
};
9495

src/examples/okta/src/types.generated.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ export type AdminUiFieldMetadata = {
8989

9090
export type AdminUiFilterMetadata = {
9191
__typename?: 'AdminUiFilterMetadata';
92+
options?: Maybe<Scalars['JSON']['output']>;
9293
type: AdminUiFilterType;
9394
};
9495

src/examples/rest-with-auth/src/frontend/types.generated.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ export type AdminUiFieldMetadata = {
9191

9292
export type AdminUiFilterMetadata = {
9393
__typename?: 'AdminUiFilterMetadata';
94+
options?: Maybe<Scalars['JSON']['output']>;
9495
type: AdminUiFilterType;
9596
};
9697

src/examples/rest-with-auth/src/types.generated.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ export type AdminUiFieldMetadata = {
9191

9292
export type AdminUiFilterMetadata = {
9393
__typename?: 'AdminUiFilterMetadata';
94+
options?: Maybe<Scalars['JSON']['output']>;
9495
type: AdminUiFilterType;
9596
};
9697

src/examples/rest-with-auth/types.generated.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ export type AdminUiFieldMetadata = {
9191

9292
export type AdminUiFilterMetadata = {
9393
__typename?: 'AdminUiFilterMetadata';
94+
options?: Maybe<Scalars['JSON']['output']>;
9495
type: AdminUiFilterType;
9596
};
9697

src/examples/rest/src/frontend/types.generated.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ export type AdminUiFieldMetadata = {
8787

8888
export type AdminUiFilterMetadata = {
8989
__typename?: 'AdminUiFilterMetadata';
90+
options?: Maybe<Scalars['JSON']['output']>;
9091
type: AdminUiFilterType;
9192
};
9293

src/examples/rest/src/types.generated.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ export type AdminUiFieldMetadata = {
8787

8888
export type AdminUiFilterMetadata = {
8989
__typename?: 'AdminUiFilterMetadata';
90+
options?: Maybe<Scalars['JSON']['output']>;
9091
type: AdminUiFilterType;
9192
};
9293

src/examples/s3-storage/src/frontend/types.generated.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ export type AdminUiFieldMetadata = {
8787

8888
export type AdminUiFilterMetadata = {
8989
__typename?: 'AdminUiFilterMetadata';
90+
options?: Maybe<Scalars['JSON']['output']>;
9091
type: AdminUiFilterType;
9192
};
9293

src/examples/s3-storage/src/types.generated.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ export type AdminUiFieldMetadata = {
8787

8888
export type AdminUiFilterMetadata = {
8989
__typename?: 'AdminUiFilterMetadata';
90+
options?: Maybe<Scalars['JSON']['output']>;
9091
type: AdminUiFilterType;
9192
};
9293

src/examples/sqlite/src/frontend/types.generated.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ export type AdminUiFieldMetadata = {
9393

9494
export type AdminUiFilterMetadata = {
9595
__typename?: 'AdminUiFilterMetadata';
96+
options?: Maybe<Scalars['JSON']['output']>;
9697
type: AdminUiFilterType;
9798
};
9899

src/examples/sqlite/src/types.generated.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ export type AdminUiFieldMetadata = {
9393

9494
export type AdminUiFilterMetadata = {
9595
__typename?: 'AdminUiFilterMetadata';
96+
options?: Maybe<Scalars['JSON']['output']>;
9697
type: AdminUiFilterType;
9798
};
9899

src/examples/xero/src/frontend/types.generated.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@ export type AdminUiFieldMetadata = {
220220

221221
export type AdminUiFilterMetadata = {
222222
__typename?: 'AdminUiFilterMetadata';
223+
options?: Maybe<Scalars['JSON']['output']>;
223224
type: AdminUiFilterType;
224225
};
225226

src/examples/xero/src/types.generated.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@ export type AdminUiFieldMetadata = {
220220

221221
export type AdminUiFilterMetadata = {
222222
__typename?: 'AdminUiFilterMetadata';
223+
options?: Maybe<Scalars['JSON']['output']>;
223224
type: AdminUiFilterType;
224225
};
225226

src/packages/admin-ui-components/src/combo-box/component.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ export const ComboBox = ({
144144
ref: inputRef,
145145
onBlur: handleBlur,
146146
onFocus: toggleMenu,
147-
placeholder,
147+
placeholder: valueArray.length === 0 ? placeholder : undefined,
148148
})}
149149
/>
150150
</div>

src/packages/admin-ui-components/src/combo-box/styles.module.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
.selectedOptions {
3838
display: flex;
3939
flex-wrap: nowrap;
40-
padding: 4px;
40+
padding: 0 4px;
4141
z-index: 2;
4242
}
4343

src/packages/admin-ui-components/src/filter-bar/component.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ export const FilterBar = ({ iconBefore }: { iconBefore?: ReactNode }) => {
125125
return fields.map((field) => {
126126
if (field.hideInFilterBar || !field.filter?.type) return null;
127127
const options = {
128+
...field.filter.options,
128129
fieldName: field.name,
129130
entity: entityName,
130131
onChange: onFilter,

src/packages/admin-ui-components/src/filters/text-filter.tsx

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,39 @@ export interface TextFilterProps {
44
fieldName: string;
55
entity: string;
66
onChange?: (fieldName: string, newFilter: Filter) => void;
7+
caseInsensitive?: boolean;
8+
substringMatch?: boolean;
79
filter?: Filter;
810
}
911

10-
export const TextFilter = ({ fieldName, onChange, filter }: TextFilterProps) => {
11-
const handleOnChange = (fieldName: string, newValue?: string) => {
12-
onChange?.(fieldName, newValue ? { [fieldName]: newValue } : {});
13-
};
12+
export const TextFilter = ({
13+
fieldName,
14+
onChange,
15+
filter,
16+
caseInsensitive,
17+
substringMatch,
18+
}: TextFilterProps) => {
19+
let filterKey = fieldName;
20+
if (substringMatch) filterKey = `${fieldName}_like`;
21+
if (caseInsensitive) filterKey = `${fieldName}_ilike`;
22+
23+
let value = String(filter?.[filterKey] ?? '');
24+
if (caseInsensitive || substringMatch) value = value.replaceAll('\\%', '%');
25+
if (substringMatch) value = value.slice(1, -1);
1426

1527
return (
1628
<Input
1729
key={fieldName}
1830
inputMode="text"
1931
fieldName={fieldName}
20-
value={String(filter?.[fieldName] ?? '')}
21-
onChange={handleOnChange}
32+
value={value}
33+
onChange={(fieldName: string, newValue?: string) => {
34+
// In either case we'll use ilike so we should escape any literal % characters
35+
if (caseInsensitive || substringMatch) newValue = newValue?.replaceAll('%', '\\%');
36+
if (substringMatch && newValue) newValue = `%${newValue}%`;
37+
38+
onChange?.(fieldName, newValue ? { [filterKey]: newValue } : {});
39+
}}
2240
data-testid={`${fieldName}-filter`}
2341
/>
2442
);

src/packages/admin-ui-components/src/filters/utils.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@ const getValidFilterProperties = (fields: EntityField[]): Set<string> => {
2828
break;
2929
case AdminUIFilterType.TEXT:
3030
supportedKeys.add(field.name);
31+
// Support for case insensitive and substring matching filters
32+
if (field.filter?.options?.caseInsensitive) {
33+
supportedKeys.add(`${field.name}_ilike`);
34+
}
35+
if (field.filter?.options?.substringMatch) {
36+
supportedKeys.add(`${field.name}_like`);
37+
}
3138
break;
3239
}
3340
}

src/packages/admin-ui-components/src/utils/graphql.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export const SCHEMA_QUERY = gql`
2626
relatedEntity
2727
filter {
2828
type
29+
options
2930
}
3031
attributes {
3132
isReadOnly

src/packages/admin-ui-components/src/utils/use-schema.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ export interface EntityField {
9797
relationshipType?: 'MANY_TO_MANY' | 'MANY_TO_ONE' | 'ONE_TO_MANY' | 'ONE_TO_ONE';
9898
filter?: {
9999
type: AdminUIFilterType;
100+
options?: Record<string, unknown>;
100101
};
101102
attributes?: EntityFieldAttributes;
102103
initialValue?: string | number | boolean;

src/packages/core/src/decorators/field.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ export interface FieldOptions {
100100
readonly?: boolean;
101101
adminUIOptions?: {
102102
filterType?: AdminUIFilterType;
103+
filterOptions?: Record<string, unknown>;
103104
hideInTable?: boolean;
104105
hideInFilterBar?: boolean;
105106
hideInDetailForm?: boolean;

src/packages/core/src/decorators/relationship-field.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ type RelationshipFieldOptions<D> = {
1010
hideInFilterBar?: boolean;
1111
hideInDetailForm?: boolean;
1212
readonly?: boolean;
13+
filterOptions?: Record<string, unknown>;
1314
};
1415

1516
// Add custom field directives to this field

src/packages/core/src/metadata-service/filter.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { GraphQLJSON } from '@exogee/graphweaver-scalars';
12
import { Entity, Field } from '../decorators';
23
import { graphweaverMetadata } from '../metadata';
34
import { AdminUIFilterType } from '../types';
@@ -13,4 +14,7 @@ graphweaverMetadata.collectEnumInformation({
1314
export class AdminUiFilterMetadata {
1415
@Field(() => AdminUIFilterType)
1516
type!: AdminUIFilterType;
17+
18+
@Field(() => GraphQLJSON, { nullable: true })
19+
options?: Record<string, unknown>;
1620
}

src/packages/core/src/metadata-service/resolver.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,8 +148,7 @@ export const resolveAdminUiMetadata = (hooks?: Hooks) => {
148148
}
149149

150150
const filterType = field.adminUIOptions?.filterType ?? mapFilterType(fieldObject);
151-
152-
fieldObject.filter = { type: filterType };
151+
fieldObject.filter = { type: filterType, options: field.adminUIOptions?.filterOptions };
153152

154153
return fieldObject;
155154
});

src/packages/core/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,7 @@ export interface FieldMetadata<G = unknown, D = unknown> {
253253
readonly?: boolean;
254254
fieldForDetailPanelNavigationId?: boolean;
255255
filterType?: AdminUIFilterType;
256+
filterOptions?: Record<string, unknown>;
256257
detailPanelInputComponent?: DetailPanelInputComponentOption | DetailPanelInputComponent;
257258
};
258259
apiOptions?: {

0 commit comments

Comments
 (0)