-
Couldn't load subscription status.
- Fork 1
MILAB-3726: advanced filter component #1127
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Summary of Changes
Hello @erohinaelena, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!
This pull request delivers a new advanced filtering system, allowing users to construct sophisticated data queries through a user-friendly interface. The changes encompass the creation of a dedicated advanced filter component, along with necessary enhancements to foundational UI components to support its dynamic behavior and improved visual presentation. This provides a robust and extensible solution for complex data filtering needs across the application.
Highlights
- New Advanced Filter Component: Introduced a new
PlAdvancedFiltercomponent, providing a flexible and powerful user interface for building complex data filters with support for various conditions and logical operators. - Enhanced UI Components: Updated existing
PlDropdownandPlElementListcomponents to include new props and slots, enabling more granular control over error message display and item styling/content placement, which are crucial for the advanced filter's functionality. - Expanded Filter Capabilities: Extended the
FilterUimodel with new filter types, includingpatternMatchesRegularExpression,patternFuzzyContainSubsequence,numberEquals, andnumberNotEquals, significantly broadening the range of filtering operations available. - Integration into UI Examples: Integrated the new
PlAdvancedFiltercomponent into the UI examples application, adding a dedicated page (PlAdvancedFilterPage.vue) to demonstrate its usage, including drag-and-drop functionality and various filter state examples.
Using Gemini Code Assist
The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.
Invoking Gemini
You can request assistance from Gemini at any point in your pull request via creating an issue comment (i.e. comment on the pull request page) using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands.
| Feature | Command | Description |
|---|---|---|
| Code Review | /gemini review |
Performs a code review for the current pull request in its current state. |
| Pull Request Summary | /gemini summary |
Provides a summary of the current pull request in its current state. |
| Comment | @gemini-code-assist | Responds in comments when explicitly tagged, both in issue comments and review comments. |
| Help | /gemini help |
Displays a list of available commands. |
Customization
To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.
Limitations & Feedback
Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.
You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.
Footnotes
-
Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code Review
This pull request introduces a new advanced filter component, along with an example page to demonstrate its usage. Several existing components like PlDropdown and PlElementList have been extended to support the new filter's requirements. The changes are well-structured, with clear separation of concerns into different files for types, constants, and utility functions. My review focuses on a few minor bugs, mostly copy-paste errors in the new code, and a potential TypeScript type issue. I've also suggested a small readability improvement in one of the Vue templates.
| type: { | ||
| label: 'Predicate', | ||
| fieldType: 'FilterUiType', | ||
| defaultValue: () => 'numberEquals', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
agree
| numberEquals: { type: 'numberEquals', sourceId: '', reference: undefined }, | ||
| numberNotEquals: { type: 'numberNotEquals', sourceId: '', reference: undefined }, | ||
| InSet: { type: 'InSet', sourceId: '', reference: [] }, | ||
| NotInSet: { type: 'InSet', sourceId: '', reference: [] }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| function toOuterFilterGroup(m: Group): FilterUi { | ||
| const res: FilterUi = { | ||
| type: m.operand, | ||
| filters: m.filters.map(toOuterFilter).filter((v) => v !== null), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The .filter((v) => v !== null) call doesn't inform TypeScript that null values are removed from the array. This can lead to a type mismatch, as the resulting array is of type (FilterUi | null)[] while FilterUi[] is expected. Using a type guard (v): v is FilterUi => v !== null will resolve this typing issue.
| filters: m.filters.map(toOuterFilter).filter((v) => v !== null), | |
| filters: m.filters.map(toOuterFilter).filter((v): v is FilterUi => v !== null), |
| overflow: auto; | ||
| } | ||
| .columnChip { | ||
| background: '#fff'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| > | ||
| <PlCheckbox v-model="item.not">NOT</PlCheckbox> | ||
| <FilterComponent | ||
| v-for="filterIdx of new Array(item.filters.length).fill(0).map((_v, idx)=> idx)" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| column: 'someColumn' as SUniversalPColumnId, | ||
| }, | ||
| { | ||
| type: 'isNotNA' as const, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's create objet with types for reusing it
const FilterUiTypes = {
IsNa: 'isNa'
...
} as const
| ]; | ||
| watch(() => filtersModel.value, (m) => { | ||
| console.log('m', m); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
m
| <PlDropdown v-model="selectedSavedFilters" :options="filterStatesOptions" label="Examples" :style="{width: '300px'}" /> | ||
| </div> | ||
| <div :class="$style.block"> | ||
| <div v-if="dndMode" class="d-flex flex-column gap-16" :class="$style.leftColumn"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please don't use global styles
| /** | ||
| * If false there is only red border with non-empty error field, without a message | ||
| */ | ||
| showErrorMessage?: boolean; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think that you should add flag that control another. MB errorStatus: true|status: 'error'|'idle' will be better?
| <template v-if="slots['item-content']" #content="{ item, index }"> | ||
| <slot :index="index" :item="item" name="item-content" /> | ||
| </template> | ||
| <template v-if="slots['item-after']" #after="{ item, index }"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why content part not enough?
| return props.itemClassBody(item, index); | ||
| } | ||
| return props.itemClassBody ?? null; | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
3 equal functions
| <div | ||
| v-if="hasContentSlot && props.isExpanded" | ||
| :class="[$style.body, { [$style.disabled]: props.isToggled }]" | ||
| :class="[$style.body, bodyClass, { [$style.disabled]: props.isToggled }]" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
contentClass it's more consistent with slot['content']
| } satisfies CreateFilterUiMetadataMap<FilterUiType>; | ||
|
|
||
| // exist in PlAdvancedFilter | ||
| const ignoredFilters = new Set<FilterUiType>(['numberEquals', 'numberNotEquals', 'patternFuzzyContainSubsequence', 'patternMatchesRegularExpression']); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's create white list instead black list. If somebody add another filter it don't broke this component
| || ui.type === 'numberNotEquals' | ||
| || ui.type === 'patternFuzzyContainSubsequence' | ||
| || ui.type === 'patternMatchesRegularExpression') { | ||
| return { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
throw
|
|
||
| <style lang="scss" module> | ||
| .filter_wrapper { | ||
| --errorColor: #FF5C5C; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
error-color, let's be consistent with another css vars
| color: var(--txt-01); | ||
| text-overflow: ellipsis; | ||
| white-space: nowrap; | ||
| font-family: Manrope; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should we remove it?
| line-height: 20px; | ||
| } | ||
| .column_chip { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
kek, why snake_case?
| <div v-else :class="$style.top" > | ||
| <PlDropdown | ||
| v-model="model.sourceId" | ||
| :error="currentError ? ' ' : undefined" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why we cast error to space?
| || model.type === 'lessThan' | ||
| || model.type === 'lessThanOrEqual' | ||
| || model.type === 'greaterThan' | ||
| || model.type === 'greaterThanOrEqual' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's incapsulate it predicate
| justify-content: center; | ||
| } | ||
| .operand { | ||
| border-radius: 120px; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's change it to 100%
| :get-item-key="(group) => group.id" | ||
|
|
||
| :item-class="$style.filterGroup" | ||
| :item-class-body="$style.filterGroup__body" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
filterGroupBody
|
|
||
| :is-expanded="(group) => expanded[group.id] ?? false" | ||
| :is-expandable="() => true" | ||
| :is-removable="() => true" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are you sure that you need predicates like this () => true?
| sourceInfoBySourceId: SourceOptionsInfo; | ||
| uniqueValuesBySourceId: UniqueValuesInfo; | ||
| searchOptions: (id: string, str: string) => Promise<ListOptionBase<string | number>[]>; | ||
| searchModel: (id: string, str: string) => Promise<ListOptionBase<string | number>>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's allow this predicate return sync result
| :disableToggling="true" | ||
| :disablePinning="true" | ||
|
|
||
| @expand="(group) => {expanded[group.id] = !expanded[group.id]}" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PlElementList don't have expand event, only props on-expand
| const model = defineModel<Filter>({ required: true }); | ||
| defineEmits<{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we use only props and avoid events?
|
|
||
| // Not supported: topN, bottomN, lessThanColumn, lessThanColumnOrEqual | ||
| // or, and, not - in groups | ||
| export type SupportedFilterTypes = Exclude<FilterUi['type'], 'topN' | 'bottomN' | 'lessThanColumn' | 'lessThanColumnOrEqual' | 'or' | 'and' | 'not' | undefined>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Include
|
|
||
| export type Operand = 'or' | 'and'; | ||
|
|
||
| interface FilterBase { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's rewrite with usage FilterUI
| try { | ||
| new RegExp(model.value.reference); | ||
| return false; | ||
| } catch (_err) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you can avoid _err just use } catch {
| return { value: modelStr, label: `Label ${modelStr}` }; | ||
| } | ||
| const errorState = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you highlight where exactly mistake in this state?
| const LOCAL_FILTER_TYPES: FilterType[] = ['inSet', 'notInSet']; | ||
| export const ALL_FILTER_TYPES = new Set<FilterType>([...SUPPORTED_FILTER_TYPES, ...LOCAL_FILTER_TYPES]); | ||
|
|
||
| export const LOCAL_FILTERS_METADATA: Record<'inSet' | 'notInSet', { label: string; supportedFor: (spec: NormalizedSpecData) => boolean }> = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what mean "local_filters"?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"inSet" and "notInSet" are made from combination of 'Or' and "Equals", but it must look like a regular filter in the interface; so I don't add them to UiFilters, just make "local" ones only for component
| last: boolean; | ||
| sourceInfoBySourceId: SourceOptionsInfo; | ||
| uniqueValuesBySourceId: UniqueValuesInfo; | ||
| searchOptionsFn?: (id: string, str: string) => (Promise<ListOptionBase<string | number>[]>) | ((id: string, str: string) => ListOptionBase<string | number>[]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
getSearchOptions, getSearchModel
| const searchFn = props.searchModelFn; | ||
| return searchFn ? searchFn(id, v) as Promise<ListOptionBase<string>> : Promise.resolve({ label: '', value: '' }); | ||
| } | ||
| async function optionsSearch(id: string, str: string): Promise<ListOptionBase<string>[]> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
every function must start from verb
| onChangeOperand: (op: Operand) => void; | ||
| }>(); | ||
| const model = defineModel<Filter>({ required: true }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's rename to filter , this is a more meaningful name
| /** Additional validity check for input value that must return an error text if failed */ | ||
| validate?: (v: number) => string | undefined; | ||
| /** Makes some of corners not rounded */ | ||
| position?: 'top' | 'bottom' | 'left' | 'right' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'middle'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
mb better to rename it to group-position? when I see position I think about how we should position this element
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
edge-position? position-in-combination? border-shape-by-position?
|
|
||
| <template> | ||
| <div class="double-contour"> | ||
| <div class="double-contour" :class="{ [position]: true }"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can be simplified :class="props.position"
| </div> | ||
| </div> | ||
| </template> | ||
| <style> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lets use <style module>
| } satisfies CreateFilterUiMetadataMap<FilterUiType>; | ||
|
|
||
| // exist in PlAdvancedFilter | ||
| const filtersInPlAnnotation = new Set<FilterUiType>([ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this should be defined inside PlAdvancedFilter
| } | ||
|
|
||
| return Object.entries(filterUiMetadata).filter(([_, metadata]) => metadata.supportedFor(columnSpec)).map(([type, metadata]) => ({ | ||
| return Object.entries(filterUiMetadata).filter(([filterName, metadata]) => !filtersInPlAnnotation.has(filterName as FilterUiType) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this code shouldn't specific for some component
| || ui.type === 'numberNotEquals' | ||
| || ui.type === 'patternFuzzyContainSubsequence' | ||
| || ui.type === 'patternMatchesRegularExpression') { | ||
| throw new Error(`Filter "${ui.type}" is not supported in PlAnnotation`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this code haven't to exist here
| const props = defineProps<{ | ||
| operand: Operand; | ||
| sourceIds: string[]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why you split sourceIds and sourceInfoBySourceId? Usually we use array of items, that will be equal SourceOptionsInfo[]
| const wildcardOptions = computed(() => { | ||
| if (model.value.type === 'patternFuzzyContainSubsequence') { | ||
| const alphabet = currentSpec.value ? readDomain(currentSpec.value, Domain.Alphabet) ?? readAnnotation(currentSpec.value, Annotation.Alphabet) : null; | ||
| if (alphabet === 'nucleotide') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is super specific code, it should be implemented outside this component
e9b044b to
39c398c
Compare
No description provided.