material-react-table 0.28.0 → 0.29.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,5 +1,12 @@
1
1
  import { FC } from 'react';
2
2
  import type { MRT_Header, MRT_TableInstance } from '..';
3
+ import { MRT_Localization } from '../localization';
4
+ export declare const internalFilterOptions: (localization: MRT_Localization) => {
5
+ option: string;
6
+ symbol: string;
7
+ label: string;
8
+ divider: boolean;
9
+ }[];
3
10
  interface Props {
4
11
  anchorEl: HTMLElement | null;
5
12
  header?: MRT_Header;
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
- "version": "0.28.0",
2
+ "version": "0.29.0",
3
3
  "license": "MIT",
4
4
  "name": "material-react-table",
5
- "description": "A fully featured Material UI implementation of TanStack React Table, inspired by material-table and the MUI X DataGrid, written from the ground up in TypeScript.",
5
+ "description": "A fully featured Material UI V5 implementation of TanStack React Table V8, written from the ground up in TypeScript.",
6
6
  "author": "Kevin Vandy",
7
7
  "keywords": [
8
8
  "react-table",
@@ -62,7 +62,7 @@
62
62
  "@emotion/styled": "^11.9.3",
63
63
  "@faker-js/faker": "^7.3.0",
64
64
  "@mui/icons-material": "^5.8.4",
65
- "@mui/material": "^5.9.1",
65
+ "@mui/material": "^5.9.2",
66
66
  "@size-limit/preset-small-lib": "^7.0.8",
67
67
  "@storybook/addon-a11y": "^6.5.9",
68
68
  "@storybook/addon-actions": "^6.5.9",
@@ -225,6 +225,7 @@ export type MRT_ColumnDef<TData extends Record<string, any> = {}> = Omit<
225
225
  enableEditing?: boolean;
226
226
  filterFn?: MRT_FilterFn<TData>;
227
227
  filterSelectOptions?: (string | { text: string; value: string })[];
228
+ filterVariant?: 'text' | 'select' | 'multiSelect' | 'range';
228
229
  /**
229
230
  * footer must be a string. If you want custom JSX to render the footer, you can also specify a `Footer` option. (Capital F)
230
231
  */
@@ -302,9 +303,11 @@ export type MRT_ColumnDef<TData extends Record<string, any> = {}> = Omit<
302
303
  | (({
303
304
  table,
304
305
  column,
306
+ rangeFilterIndex,
305
307
  }: {
306
308
  table: MRT_TableInstance<TData>;
307
309
  column: MRT_Column<TData>;
310
+ rangeFilterIndex?: number;
308
311
  }) => TextFieldProps);
309
312
  muiTableHeadCellProps?:
310
313
  | TableCellProps
@@ -341,6 +344,7 @@ export type MRT_DefinedColumnDef<TData extends Record<string, any> = {}> = Omit<
341
344
  'id'
342
345
  > & {
343
346
  id: string;
347
+ _filterFn: MRT_FilterOption;
344
348
  };
345
349
 
346
350
  export type MRT_Column<TData extends Record<string, any> = {}> = Omit<
@@ -607,9 +611,11 @@ export type MaterialReactTableProps<TData extends Record<string, any> = {}> =
607
611
  | (({
608
612
  table,
609
613
  column,
614
+ rangeFilterIndex,
610
615
  }: {
611
616
  table: MRT_TableInstance<TData>;
612
617
  column: MRT_Column<TData>;
618
+ rangeFilterIndex?: number;
613
619
  }) => TextFieldProps);
614
620
  muiTableHeadCellProps?:
615
621
  | TableCellProps
@@ -52,6 +52,8 @@ export const prepareColumns = <TData extends Record<string, any> = {}>(
52
52
  if (Object.keys(MRT_FilterFns).includes(currentFilterFns[columnDef.id])) {
53
53
  columnDef.filterFn =
54
54
  MRT_FilterFns[currentFilterFns[columnDef.id]] ?? MRT_FilterFns.fuzzy;
55
+ //@ts-ignore
56
+ columnDef._filterFn = currentFilterFns[columnDef.id];
55
57
  }
56
58
  if (Object.keys(MRT_SortingFns).includes(columnDef.sortingFn as string)) {
57
59
  // @ts-ignore
@@ -123,3 +125,14 @@ export const getDefaultColumnOrderIds = <
123
125
  ),
124
126
  ...getTrailingDisplayColumnIds(props),
125
127
  ].filter(Boolean) as string[];
128
+
129
+ export const getDefaultColumnFilterFn = <
130
+ TData extends Record<string, any> = {},
131
+ >(
132
+ columnDef: MRT_ColumnDef<TData>,
133
+ ): MRT_FilterOption => {
134
+ if (columnDef.filterVariant === 'multiSelect') return 'arrIncludesSome';
135
+ if (columnDef.filterVariant === 'select') return 'equals';
136
+ if (columnDef.filterVariant === 'range') return 'betweenInclusive';
137
+ return 'fuzzy';
138
+ };
@@ -19,7 +19,9 @@ export const MRT_TableHeadCellFilterContainer: FC<Props> = ({
19
19
 
20
20
  return (
21
21
  <Collapse in={showColumnFilters} mountOnEnter unmountOnExit>
22
- {currentFilterFns[column.id] === 'between' ? (
22
+ {['between', 'betweenInclusive', 'inNumberRange'].includes(
23
+ currentFilterFns[column.id],
24
+ ) ? (
23
25
  <MRT_FilterRangeFields header={header} table={table} />
24
26
  ) : (
25
27
  <MRT_FilterTextField header={header} table={table} />
@@ -19,6 +19,11 @@ export const MRT_TableHeadCellFilterLabel: FC<Props> = ({ header, table }) => {
19
19
  const { column } = header;
20
20
  const { columnDef } = column;
21
21
 
22
+ const isRangeFilter = [
23
+ 'between',
24
+ 'betweenInclusive',
25
+ 'inNumberRange',
26
+ ].includes(columnDef._filterFn);
22
27
  const currentFilterOption = currentFilterFns?.[header.id];
23
28
  const filterTooltip = localization.filteringByColumn
24
29
  .replace('{column}', String(columnDef.header))
@@ -37,7 +42,7 @@ export const MRT_TableHeadCellFilterLabel: FC<Props> = ({ header, table }) => {
37
42
  `"${
38
43
  Array.isArray(column.getFilterValue())
39
44
  ? (column.getFilterValue() as [string, string]).join(
40
- `" ${localization.and} "`,
45
+ `" ${isRangeFilter ? localization.and : localization.or} "`,
41
46
  )
42
47
  : (column.getFilterValue() as string)
43
48
  }"`,
@@ -48,8 +53,8 @@ export const MRT_TableHeadCellFilterLabel: FC<Props> = ({ header, table }) => {
48
53
  <Grow
49
54
  unmountOnExit
50
55
  in={
51
- (!!column.getFilterValue() && currentFilterOption !== 'between') ||
52
- (currentFilterOption === 'between' && // @ts-ignore
56
+ (!!column.getFilterValue() && isRangeFilter) ||
57
+ (!isRangeFilter && // @ts-ignore
53
58
  (!!column.getFilterValue()?.[0] || !!column.getFilterValue()?.[1]))
54
59
  }
55
60
  >
@@ -11,8 +11,8 @@ interface Props {
11
11
  export const MRT_FilterRangeFields: FC<Props> = ({ header, table }) => {
12
12
  return (
13
13
  <Box sx={{ display: 'grid', gridTemplateColumns: '6fr 6fr', gap: '1rem' }}>
14
- <MRT_FilterTextField header={header} inputIndex={0} table={table} />
15
- <MRT_FilterTextField header={header} inputIndex={1} table={table} />
14
+ <MRT_FilterTextField header={header} rangeFilterIndex={0} table={table} />
15
+ <MRT_FilterTextField header={header} rangeFilterIndex={1} table={table} />
16
16
  </Box>
17
17
  );
18
18
  };
@@ -6,10 +6,13 @@ import React, {
6
6
  useState,
7
7
  } from 'react';
8
8
  import {
9
+ Box,
10
+ Checkbox,
9
11
  Chip,
10
12
  debounce,
11
13
  IconButton,
12
14
  InputAdornment,
15
+ ListItemText,
13
16
  MenuItem,
14
17
  TextField,
15
18
  TextFieldProps,
@@ -20,13 +23,13 @@ import type { MRT_Header, MRT_TableInstance } from '..';
20
23
 
21
24
  interface Props {
22
25
  header: MRT_Header;
23
- inputIndex?: number;
26
+ rangeFilterIndex?: number;
24
27
  table: MRT_TableInstance;
25
28
  }
26
29
 
27
30
  export const MRT_FilterTextField: FC<Props> = ({
28
31
  header,
29
- inputIndex,
32
+ rangeFilterIndex,
30
33
  table,
31
34
  }) => {
32
35
  const {
@@ -45,11 +48,13 @@ export const MRT_FilterTextField: FC<Props> = ({
45
48
  const { columnDef } = column;
46
49
  const { currentFilterFns } = getState();
47
50
 
48
- const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
49
-
50
51
  const mTableHeadCellFilterTextFieldProps =
51
52
  muiTableHeadCellFilterTextFieldProps instanceof Function
52
- ? muiTableHeadCellFilterTextFieldProps({ column, table })
53
+ ? muiTableHeadCellFilterTextFieldProps({
54
+ column,
55
+ table,
56
+ rangeFilterIndex,
57
+ })
53
58
  : muiTableHeadCellFilterTextFieldProps;
54
59
 
55
60
  const mcTableHeadCellFilterTextFieldProps =
@@ -57,6 +62,7 @@ export const MRT_FilterTextField: FC<Props> = ({
57
62
  ? columnDef.muiTableHeadCellFilterTextFieldProps({
58
63
  column,
59
64
  table,
65
+ rangeFilterIndex,
60
66
  })
61
67
  : columnDef.muiTableHeadCellFilterTextFieldProps;
62
68
 
@@ -65,28 +71,71 @@ export const MRT_FilterTextField: FC<Props> = ({
65
71
  ...mcTableHeadCellFilterTextFieldProps,
66
72
  } as TextFieldProps;
67
73
 
68
- const [filterValue, setFilterValue] = useState<string>(() =>
69
- inputIndex !== undefined
70
- ? (column.getFilterValue() as [string, string])?.[inputIndex] ?? ''
74
+ const isRangeFilter =
75
+ columnDef.filterVariant === 'range' || rangeFilterIndex !== undefined;
76
+ const isSelectFilter = columnDef.filterVariant === 'select';
77
+ const isMultiSelectFilter = columnDef.filterVariant === 'multiSelect';
78
+ const isTextboxFilter = !isSelectFilter && !isMultiSelectFilter;
79
+
80
+ const currentFilterOption = currentFilterFns?.[header.id];
81
+ const filterId = `mrt-${tableId}-${header.id}-filter-text-field${
82
+ rangeFilterIndex ?? ''
83
+ }`;
84
+ const filterChipLabel = ['empty', 'notEmpty'].includes(currentFilterOption)
85
+ ? //@ts-ignore
86
+ localization[
87
+ `filter${
88
+ currentFilterOption?.charAt?.(0)?.toUpperCase() +
89
+ currentFilterOption?.slice(1)
90
+ }`
91
+ ]
92
+ : '';
93
+ const filterPlaceholder = !isRangeFilter
94
+ ? localization.filterByColumn?.replace('{column}', String(columnDef.header))
95
+ : rangeFilterIndex === 0
96
+ ? localization.min
97
+ : rangeFilterIndex === 1
98
+ ? localization.max
99
+ : '';
100
+ const allowedColumnFilterOptions =
101
+ columnDef?.columnFilterModeOptions ?? columnFilterModeOptions;
102
+ const showChangeModeButton =
103
+ enableColumnFilterChangeMode &&
104
+ columnDef.enableColumnFilterChangeMode !== false &&
105
+ !rangeFilterIndex &&
106
+ (allowedColumnFilterOptions === undefined ||
107
+ !!allowedColumnFilterOptions?.length);
108
+
109
+ const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
110
+ const [filterValue, setFilterValue] = useState<string | string[]>(() =>
111
+ isMultiSelectFilter
112
+ ? (column.getFilterValue() as string[]) || []
113
+ : isRangeFilter
114
+ ? (column.getFilterValue() as [string, string])?.[
115
+ rangeFilterIndex as number
116
+ ] || []
71
117
  : (column.getFilterValue() as string) ?? '',
72
118
  );
73
119
 
74
120
  const handleChangeDebounced = useCallback(
75
- debounce((event: ChangeEvent<HTMLInputElement>) => {
76
- let value =
77
- textFieldProps.type === 'date'
78
- ? new Date(event.target.value)
79
- : event.target.value;
80
- if (inputIndex !== undefined) {
81
- column.setFilterValue((old: [string, string | Date]) => {
82
- const newFilterValues = old ?? ['', ''];
83
- newFilterValues[inputIndex] = value;
84
- return newFilterValues;
85
- });
86
- } else {
87
- column.setFilterValue(value ?? undefined);
88
- }
89
- }, 200),
121
+ debounce(
122
+ (event: ChangeEvent<HTMLInputElement>) => {
123
+ let value =
124
+ textFieldProps.type === 'date'
125
+ ? new Date(event.target.value)
126
+ : event.target.value;
127
+ if (isRangeFilter) {
128
+ column.setFilterValue((old: [string, string | Date]) => {
129
+ const newFilterValues = old ?? ['', ''];
130
+ newFilterValues[rangeFilterIndex as number] = value;
131
+ return newFilterValues;
132
+ });
133
+ } else {
134
+ column.setFilterValue(value ?? undefined);
135
+ }
136
+ },
137
+ isTextboxFilter ? 200 : 1,
138
+ ),
90
139
  [],
91
140
  );
92
141
 
@@ -95,24 +144,24 @@ export const MRT_FilterTextField: FC<Props> = ({
95
144
  handleChangeDebounced(event);
96
145
  };
97
146
 
98
- const handleFilterMenuOpen = (event: MouseEvent<HTMLElement>) => {
99
- setAnchorEl(event.currentTarget);
100
- };
101
-
102
147
  const handleClear = () => {
103
- setFilterValue('');
104
- if (inputIndex !== undefined) {
148
+ if (isMultiSelectFilter) {
149
+ setFilterValue([]);
150
+ column.setFilterValue([]);
151
+ } else if (isRangeFilter) {
152
+ setFilterValue('');
105
153
  column.setFilterValue((old: [string | undefined, string | undefined]) => {
106
154
  const newFilterValues = old ?? ['', ''];
107
- newFilterValues[inputIndex] = undefined;
155
+ newFilterValues[rangeFilterIndex as number] = undefined;
108
156
  return newFilterValues;
109
157
  });
110
158
  } else {
159
+ setFilterValue('');
111
160
  column.setFilterValue(undefined);
112
161
  }
113
162
  };
114
163
 
115
- const handleClearFilterChip = () => {
164
+ const handleClearEmptyFilterChip = () => {
116
165
  setFilterValue('');
117
166
  column.setFilterValue(undefined);
118
167
  setCurrentFilterFns((prev) => ({
@@ -121,47 +170,14 @@ export const MRT_FilterTextField: FC<Props> = ({
121
170
  }));
122
171
  };
123
172
 
173
+ const handleFilterMenuOpen = (event: MouseEvent<HTMLElement>) => {
174
+ setAnchorEl(event.currentTarget);
175
+ };
176
+
124
177
  if (columnDef.Filter) {
125
178
  return <>{columnDef.Filter?.({ header, table })}</>;
126
179
  }
127
180
 
128
- const filterId = `mrt-${tableId}-${header.id}-filter-text-field${
129
- inputIndex ?? ''
130
- }`;
131
- const currentFilterOption = currentFilterFns?.[header.id];
132
- const isSelectFilter = !!columnDef.filterSelectOptions;
133
- const filterChipLabel = ['empty', 'notEmpty'].includes(currentFilterOption)
134
- ? //@ts-ignore
135
- localization[
136
- `filter${
137
- currentFilterOption?.charAt(0)?.toUpperCase() +
138
- currentFilterOption?.slice(1)
139
- }`
140
- ]
141
- : '';
142
- const filterPlaceholder =
143
- inputIndex === undefined
144
- ? localization.filterByColumn?.replace(
145
- '{column}',
146
- String(columnDef.header),
147
- )
148
- : inputIndex === 0
149
- ? localization.min
150
- : inputIndex === 1
151
- ? localization.max
152
- : '';
153
-
154
- const allowedColumnFilterOptions =
155
- columnDef?.columnFilterModeOptions ?? columnFilterModeOptions;
156
-
157
- const showChangeModeButton =
158
- enableColumnFilterChangeMode &&
159
- columnDef.enableColumnFilterChangeMode !== false &&
160
- !isSelectFilter &&
161
- !inputIndex &&
162
- (allowedColumnFilterOptions === undefined ||
163
- !!allowedColumnFilterOptions?.length);
164
-
165
181
  return (
166
182
  <>
167
183
  <TextField
@@ -200,12 +216,14 @@ export const MRT_FilterTextField: FC<Props> = ({
200
216
  }}
201
217
  margin="none"
202
218
  placeholder={
203
- filterChipLabel || isSelectFilter ? undefined : filterPlaceholder
219
+ filterChipLabel || isSelectFilter || isMultiSelectFilter
220
+ ? undefined
221
+ : filterPlaceholder
204
222
  }
205
223
  onChange={handleChange}
206
224
  onClick={(e: MouseEvent<HTMLInputElement>) => e.stopPropagation()}
207
- select={isSelectFilter}
208
- value={filterValue ?? ''}
225
+ select={isSelectFilter || isMultiSelectFilter}
226
+ value={filterValue}
209
227
  variant="standard"
210
228
  InputProps={{
211
229
  startAdornment: showChangeModeButton ? (
@@ -224,7 +242,7 @@ export const MRT_FilterTextField: FC<Props> = ({
224
242
  </Tooltip>
225
243
  {filterChipLabel && (
226
244
  <Chip
227
- onDelete={handleClearFilterChip}
245
+ onDelete={handleClearEmptyFilterChip}
228
246
  label={filterChipLabel}
229
247
  />
230
248
  )}
@@ -236,7 +254,6 @@ export const MRT_FilterTextField: FC<Props> = ({
236
254
  <InputAdornment position="end">
237
255
  <Tooltip
238
256
  arrow
239
- disableHoverListener={isSelectFilter}
240
257
  placement="right"
241
258
  title={localization.clearFilter ?? ''}
242
259
  >
@@ -258,10 +275,26 @@ export const MRT_FilterTextField: FC<Props> = ({
258
275
  </InputAdornment>
259
276
  ),
260
277
  }}
278
+ SelectProps={{
279
+ displayEmpty: true,
280
+ multiple: isMultiSelectFilter,
281
+ renderValue: isMultiSelectFilter
282
+ ? (selected: any) =>
283
+ !selected?.length ? (
284
+ <Box sx={{ opacity: 0.5 }}>{filterPlaceholder}</Box>
285
+ ) : (
286
+ <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: '2px' }}>
287
+ {(selected as string[])?.map((value) => (
288
+ <Chip key={value} label={value} />
289
+ ))}
290
+ </Box>
291
+ )
292
+ : undefined,
293
+ }}
261
294
  {...textFieldProps}
262
295
  sx={(theme) => ({
263
296
  p: 0,
264
- minWidth: !filterChipLabel ? '8rem' : 'auto',
297
+ minWidth: !filterChipLabel ? '6rem' : 'auto',
265
298
  width: 'calc(100% + 0.5rem)',
266
299
  '& .MuiSelect-icon': {
267
300
  mr: '1.5rem',
@@ -271,24 +304,32 @@ export const MRT_FilterTextField: FC<Props> = ({
271
304
  : (textFieldProps?.sx as any)),
272
305
  })}
273
306
  >
274
- {isSelectFilter && (
275
- <MenuItem divider disabled={!filterValue} value="">
276
- {localization.clearFilter}
307
+ {(isSelectFilter || isMultiSelectFilter) && (
308
+ <MenuItem divider disabled hidden value="">
309
+ <Box sx={{ opacity: 0.5 }}>{filterPlaceholder}</Box>
277
310
  </MenuItem>
278
311
  )}
279
312
  {columnDef?.filterSelectOptions?.map((option) => {
280
- let value;
281
- let text;
282
- if (typeof option === 'string') {
313
+ let value: string;
314
+ let text: string;
315
+ if (typeof option !== 'object') {
283
316
  value = option;
284
317
  text = option;
285
- } else if (typeof option === 'object') {
318
+ } else {
286
319
  value = option.value;
287
320
  text = option.text;
288
321
  }
289
322
  return (
290
323
  <MenuItem key={value} value={value}>
291
- {text}
324
+ {isMultiSelectFilter && (
325
+ <Checkbox
326
+ checked={(
327
+ (column.getFilterValue() ?? []) as string[]
328
+ ).includes(value)}
329
+ sx={{ mr: '0.5rem' }}
330
+ />
331
+ )}
332
+ <ListItemText>{text}</ListItemText>
292
333
  </MenuItem>
293
334
  );
294
335
  })}
@@ -13,6 +13,9 @@ export interface MRT_Localization {
13
13
  edit: string;
14
14
  expand: string;
15
15
  expandAll: string;
16
+ filterArrIncludes: string;
17
+ filterArrIncludesAll: string;
18
+ filterArrIncludesSome: string;
16
19
  filterBetween: string;
17
20
  filterBetweenInclusive: string;
18
21
  filterByColumn: string;
@@ -20,15 +23,20 @@ export interface MRT_Localization {
20
23
  filterEmpty: string;
21
24
  filterEndsWith: string;
22
25
  filterEquals: string;
26
+ filterEqualsString: string;
23
27
  filterFuzzy: string;
24
28
  filterGreaterThan: string;
25
29
  filterGreaterThanOrEqualTo: string;
30
+ filterInNumberRange: string;
31
+ filterIncludesString: string;
32
+ filterIncludesStringSensitive: string;
26
33
  filterLessThan: string;
27
34
  filterLessThanOrEqualTo: string;
28
35
  filterMode: string;
29
36
  filterNotEmpty: string;
30
37
  filterNotEquals: string;
31
38
  filterStartsWith: string;
39
+ filterWeakEquals: string;
32
40
  filteringByColumn: string;
33
41
  grab: string;
34
42
  groupByColumn: string;
@@ -38,6 +46,7 @@ export interface MRT_Localization {
38
46
  max: string;
39
47
  min: string;
40
48
  move: string;
49
+ or: string;
41
50
  pinToLeft: string;
42
51
  pinToRight: string;
43
52
  resetColumnSize: string;
@@ -85,6 +94,9 @@ export const MRT_DefaultLocalization_EN: MRT_Localization = {
85
94
  edit: 'Edit',
86
95
  expand: 'Expand',
87
96
  expandAll: 'Expand all',
97
+ filterArrIncludes: 'Includes',
98
+ filterArrIncludesAll: 'Includes all',
99
+ filterArrIncludesSome: 'Includes',
88
100
  filterBetween: 'Between',
89
101
  filterBetweenInclusive: 'Between Inclusive',
90
102
  filterByColumn: 'Filter by {column}',
@@ -92,15 +104,20 @@ export const MRT_DefaultLocalization_EN: MRT_Localization = {
92
104
  filterEmpty: 'Empty',
93
105
  filterEndsWith: 'Ends With',
94
106
  filterEquals: 'Equals',
107
+ filterEqualsString: 'Equals',
95
108
  filterFuzzy: 'Fuzzy',
96
109
  filterGreaterThan: 'Greater Than',
97
110
  filterGreaterThanOrEqualTo: 'Greater Than Or Equal To',
111
+ filterInNumberRange: 'Between',
112
+ filterIncludesString: 'Contains',
113
+ filterIncludesStringSensitive: 'Contains',
98
114
  filterLessThan: 'Less Than',
99
115
  filterLessThanOrEqualTo: 'Less Than Or Equal To',
100
116
  filterMode: 'Filter Mode: {filterType}',
101
117
  filterNotEmpty: 'Not Empty',
102
118
  filterNotEquals: 'Not Equals',
103
119
  filterStartsWith: 'Starts With',
120
+ filterWeakEquals: 'Equals',
104
121
  filteringByColumn: 'Filtering by {column} - {filterType} {filterValue}',
105
122
  grab: 'Grab',
106
123
  groupByColumn: 'Group by {column}',
@@ -110,6 +127,7 @@ export const MRT_DefaultLocalization_EN: MRT_Localization = {
110
127
  max: 'Max',
111
128
  min: 'Min',
112
129
  move: 'Move',
130
+ or: 'or',
113
131
  pinToLeft: 'Pin to left',
114
132
  pinToRight: 'Pin to right',
115
133
  resetColumnSize: 'Reset column size',