Skip to content

Conversation

@erohinaelena
Copy link
Member

No description provided.

@changeset-bot
Copy link

changeset-bot bot commented Sep 3, 2025

⚠️ No Changeset found

Latest commit: 39c398c

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@notion-workspace
Copy link

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a 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 PlAdvancedFilter component, 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 PlDropdown and PlElementList components 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 FilterUi model with new filter types, including patternMatchesRegularExpression, patternFuzzyContainSubsequence, numberEquals, and numberNotEquals, significantly broadening the range of filtering operations available.
  • Integration into UI Examples: Integrated the new PlAdvancedFilter component 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

  1. 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.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a 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',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

There seems to be a copy-paste error. The defaultValue for the type field in the numberNotEquals filter metadata is set to 'numberEquals'. It should be 'numberNotEquals' to match the filter type.

Suggested change
defaultValue: () => 'numberEquals',
defaultValue: () => 'numberNotEquals',

Copy link
Contributor

@AStaroverov AStaroverov Sep 4, 2025

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: [] },
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

There seems to be a copy-paste error. The default filter for NotInSet has its type set to 'InSet'. It should be 'NotInSet'.

Suggested change
NotInSet: { type: 'InSet', sourceId: '', reference: [] },
NotInSet: { type: 'NotInSet', sourceId: '', reference: [] },

function toOuterFilterGroup(m: Group): FilterUi {
const res: FilterUi = {
type: m.operand,
filters: m.filters.map(toOuterFilter).filter((v) => v !== null),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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.

Suggested change
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';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The value for background is a string '#fff', which is not valid CSS. It should be the hex color code directly without quotes.

  background: #fff;

>
<PlCheckbox v-model="item.not">NOT</PlCheckbox>
<FilterComponent
v-for="filterIdx of new Array(item.filters.length).fill(0).map((_v, idx)=> idx)"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This v-for loop is difficult to read and unidiomatic. You can iterate directly over item.filters to make the code more readable and maintainable.

            v-for="(filter, filterIdx) in item.filters"

column: 'someColumn' as SUniversalPColumnId,
},
{
type: 'isNotNA' as const,
Copy link
Contributor

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);
Copy link
Contributor

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">
Copy link
Contributor

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;
Copy link
Contributor

@AStaroverov AStaroverov Sep 4, 2025

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 }">
Copy link
Contributor

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;
};
Copy link
Contributor

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 }]"
Copy link
Contributor

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']);
Copy link
Contributor

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 {
Copy link
Contributor

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;
Copy link
Contributor

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;
Copy link
Contributor

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 {
Copy link
Contributor

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"
Copy link
Contributor

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'
Copy link
Contributor

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;
Copy link
Contributor

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"
Copy link
Contributor

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"
Copy link
Contributor

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>>;
Copy link
Contributor

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]}"
Copy link
Contributor

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<{
Copy link
Contributor

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>;
Copy link
Contributor

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 {
Copy link
Contributor

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) {
Copy link
Contributor

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 = {
Copy link
Contributor

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 }> = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what mean "local_filters"?

Copy link
Member Author

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>[]);
Copy link
Contributor

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>[]> {
Copy link
Contributor

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 });
Copy link
Contributor

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';
Copy link
Contributor

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

Copy link
Member Author

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 }">
Copy link
Contributor

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>
Copy link
Contributor

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>([
Copy link
Contributor

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)
Copy link
Contributor

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`);
Copy link
Contributor

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[];
Copy link
Contributor

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') {
Copy link
Contributor

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

@erohinaelena erohinaelena force-pushed the erohinaelena/MILAB-3726 branch from e9b044b to 39c398c Compare October 27, 2025 15:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants