• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

teableio / teable / 8536869866

03 Apr 2024 10:05AM CUT coverage: 21.234% (-0.3%) from 21.535%
8536869866

Pull #514

github

web-flow
Merge 91a25d710 into 45ee7ebb3
Pull Request #514: refactor: user and link selector

1394 of 2532 branches covered (55.06%)

27 of 1620 new or added lines in 60 files covered. (1.67%)

4 existing lines in 2 files now uncovered.

14588 of 68702 relevant lines covered (21.23%)

2.02 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/packages/sdk/src/components/filter/FilterBase.tsx
1
import type { IFilter, IFilterItem, IConjunction } from '@teable/core';
×
2
import { getValidFilterOperators } from '@teable/core';
×
3

×
4
import { Plus } from '@teable/icons';
×
5

×
6
import { Button, Popover, PopoverContent, PopoverTrigger } from '@teable/ui-lib';
×
7

×
8
import { produce } from 'immer';
×
9
import { cloneDeep, isEqual, set, get } from 'lodash';
×
10
import { useCallback, useEffect, useMemo, useState } from 'react';
×
11
import { useDebounce } from 'react-use';
×
12

×
13
import { useTranslation } from '../../context/app/i18n';
×
14
import { Condition, ConditionGroup } from './condition';
×
15
import { FilterContext } from './context';
×
16
import type { IFilterBaseProps, IFiltersPath } from './types';
×
17
import { isFilterItem, ConditionAddType } from './types';
×
18

×
19
const title = 'In this view, show records';
×
20
const emptyText = 'No filter conditions are applied';
×
21
const defaultFilter: NonNullable<IFilter> = {
×
22
  conjunction: 'and',
×
23
  filterSet: [],
×
24
};
×
25
const defaultGroupFilter: NonNullable<IFilter> = {
×
26
  ...defaultFilter,
×
27
  conjunction: 'or',
×
28
};
×
29

×
30
function FilterBase(props: IFilterBaseProps) {
×
NEW
31
  const {
×
NEW
32
    onChange,
×
NEW
33
    filters: initFilter,
×
NEW
34
    fields,
×
NEW
35
    children,
×
NEW
36
    contentHeader,
×
NEW
37
    components,
×
NEW
38
    context,
×
NEW
39
  } = props;
×
40
  const { t } = useTranslation();
×
41
  const [filters, setFilters] = useState<IFilter | null>(initFilter);
×
42

×
43
  const setFilterHandler = (
×
44
    path: IFiltersPath,
×
45
    value: IFilterItem['value'] | IConjunction | IFilterItem['fieldId'] | IFilterItem['operator']
×
46
  ) => {
×
NEW
47
    setFilters((prev) => {
×
NEW
48
      if (prev) {
×
NEW
49
        return produce(prev, (draft) => {
×
NEW
50
          set(draft, path, value);
×
NEW
51
        });
×
NEW
52
      }
×
NEW
53
      return prev;
×
NEW
54
    });
×
55
  };
×
56

×
57
  useEffect(() => {
×
58
    const newFilter = cloneDeep(initFilter);
×
59
    setFilters(newFilter);
×
60
  }, [initFilter]);
×
61

×
62
  useDebounce(
×
63
    () => {
×
64
      if (!isEqual(filters, initFilter)) {
×
NEW
65
        console.log('filters', JSON.stringify(filters));
×
66
        onChange?.(filters);
×
67
      }
×
68
    },
×
69
    500,
×
70
    [filters]
×
71
  );
×
72

×
73
  // use the primary to be default metadata
×
74
  const defaultIFilterItem = useMemo<IFilterItem>(() => {
×
75
    const defaultField = fields.find((field) => field.isPrimary);
×
76
    const defaultOpertor = defaultField && getValidFilterOperators(defaultField);
×
77
    return {
×
78
      operator: defaultOpertor?.[0],
×
79
      value: null,
×
80
      fieldId: defaultField?.id,
×
81
    } as IFilterItem;
×
82
  }, [fields]);
×
83

×
84
  const addCondition = useCallback(
×
85
    (path: IFiltersPath, type = ConditionAddType.ITEM) => {
×
86
      const conditionItem =
×
87
        type === ConditionAddType.ITEM ? { ...defaultIFilterItem } : { ...defaultGroupFilter };
×
88

×
89
      let newFilters = null;
×
90

×
91
      /**
×
92
       * first add from null, set the default
×
93
       */
×
94
      if (!filters) {
×
95
        newFilters = cloneDeep(defaultFilter);
×
96
        newFilters.filterSet.push(conditionItem);
×
97
        setFilters(newFilters);
×
98
        return;
×
99
      }
×
100

×
101
      newFilters = produce(filters, (draft) => {
×
102
        const target = path.length ? get(draft, path) : draft;
×
103
        target.filterSet.push(conditionItem);
×
104
      });
×
105

×
106
      setFilters(newFilters);
×
107
    },
×
108
    [defaultIFilterItem, filters]
×
109
  );
×
110

×
111
  /**
×
112
   * different from other way to update filters, delete need to back to parent path
×
113
   * because current filter item only can delete from it's parent
×
114
   * @param path Filter Object Path
×
115
   * @param index the index of filterSet which need to delete
×
116
   * @returns void
×
117
   */
×
118
  const deleteCondition = (path: IFiltersPath, index: number) => {
×
119
    let newFilters = null;
×
120
    // get the parent path
×
121
    const parentPath = path.slice(0, -2);
×
122

×
123
    newFilters = produce(filters, (draft) => {
×
124
      const target = parentPath?.length ? get(draft, parentPath) : draft;
×
125
      target.filterSet.splice(index, 1);
×
126
    });
×
127

×
128
    // delete all filter, should return null
×
129
    if (!newFilters?.filterSet.length) {
×
130
      setFilters(null);
×
131
      return;
×
132
    }
×
133

×
134
    setFilters(newFilters);
×
135
  };
×
136

×
137
  const conditionCreator = () => {
×
138
    if (!filters?.filterSet?.length) {
×
139
      return null;
×
140
    }
×
141
    const initLevel = 0;
×
142

×
143
    return (
×
144
      <div className="max-h-96 overflow-auto ">
×
145
        {filters?.filterSet?.map((filterItem, index) =>
×
146
          isFilterItem(filterItem) ? (
×
147
            <Condition
×
148
              key={index}
×
149
              filter={filterItem}
×
150
              index={index}
×
151
              conjunction={filters.conjunction}
×
152
              level={initLevel}
×
153
              path={['filterSet', index]}
×
154
            />
×
155
          ) : (
×
156
            <ConditionGroup
×
157
              key={index}
×
158
              filter={filterItem}
×
159
              index={index}
×
160
              conjunction={filters.conjunction}
×
161
              level={initLevel}
×
162
              path={['filterSet', index]}
×
163
            />
×
164
          )
×
165
        )}
×
166
      </div>
×
167
    );
×
168
  };
×
169

×
170
  return (
×
171
    <FilterContext.Provider
×
172
      value={{
×
NEW
173
        fields,
×
NEW
174
        context,
×
NEW
175
        components,
×
176
        setFilters: setFilterHandler,
×
177
        onChange: onChange,
×
178
        addCondition: addCondition,
×
179
        deleteCondition: deleteCondition,
×
180
      }}
×
181
    >
×
182
      <Popover>
×
183
        <PopoverTrigger asChild>{children}</PopoverTrigger>
×
184
        <PopoverContent
×
185
          side="bottom"
×
186
          align="start"
×
187
          className="w-min min-w-[544px] max-w-screen-md p-0"
×
188
        >
×
189
          {contentHeader}
×
190
          <div className="text-[13px]">
×
191
            {filters?.filterSet?.length ? (
×
192
              <div className="px-4 pt-3">{title}</div>
×
193
            ) : (
×
194
              <div className="px-4 pt-4 text-muted-foreground">{emptyText}</div>
×
195
            )}
×
196
          </div>
×
197
          <div className="px-4 pt-3">{conditionCreator()}</div>
×
198
          <div className="flex w-max p-3">
×
199
            <Button
×
200
              variant="ghost"
×
201
              size="xs"
×
202
              className="text-[13px]"
×
203
              onClick={() => addCondition([], ConditionAddType.ITEM)}
×
204
            >
×
205
              <Plus className="size-4" />
×
206
              {t('filter.addCondition')}
×
207
            </Button>
×
208

×
209
            <Button
×
210
              variant="ghost"
×
211
              size="xs"
×
212
              onClick={() => addCondition([], ConditionAddType.GROUP)}
×
213
              className="text-[13px]"
×
214
            >
×
215
              <Plus className="size-4" />
×
216
              {t('filter.addConditionGroup')}
×
217
            </Button>
×
218
          </div>
×
219
        </PopoverContent>
×
220
      </Popover>
×
221
    </FilterContext.Provider>
×
222
  );
×
223
}
×
224

×
225
export { FilterBase };
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc