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

teableio / teable / 8470050671

28 Mar 2024 03:54PM UTC coverage: 21.785% (-0.05%) from 21.838%
8470050671

Pull #507

github

web-flow
Merge c24fe3a68 into 53432a5aa
Pull Request #507: feat: search api

1396 of 2505 branches covered (55.73%)

23 of 544 new or added lines in 74 files covered. (4.23%)

36 existing lines in 4 files now uncovered.

14551 of 66795 relevant lines covered (21.78%)

2.08 hits per line

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

0.0
/apps/nextjs-app/src/features/app/blocks/view/grid/components/FieldMenu.tsx
1
/* eslint-disable jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */
×
2
import { Trash, Edit, EyeOff, ArrowLeft, ArrowRight, FreezeColumn } from '@teable/icons';
×
3
import type { GridView } from '@teable/sdk';
×
4
import { useFields, useIsTouchDevice, useTablePermission, useView } from '@teable/sdk';
×
5
import { insertSingle } from '@teable/sdk/utils';
×
6
import {
×
NEW
7
  cn,
×
8
  Command,
×
9
  CommandGroup,
×
10
  CommandItem,
×
11
  CommandList,
×
12
  CommandSeparator,
×
13
  Sheet,
×
14
  SheetContent,
×
15
  SheetHeader,
×
16
} from '@teable/ui-lib/shadcn';
×
17
import { useTranslation } from 'next-i18next';
×
18
import { Fragment, useRef } from 'react';
×
19
import { useClickAway } from 'react-use';
×
20
import { FieldOperator } from '@/features/app/components/field-setting/type';
×
21
import { tableConfig } from '@/features/i18n/table.config';
×
22
import { useFieldSettingStore } from '../../field/useFieldSettingStore';
×
23
import { useGridViewStore } from '../store/gridView';
×
24
import type { IMenuItemProps } from './RecordMenu';
×
25

×
26
enum MenuItemType {
×
27
  Edit = 'Edit',
×
28
  Freeze = 'Freeze',
×
29
  Hidden = 'Hidden',
×
30
  Delete = 'Delete',
×
31
  InsertLeft = 'InsertLeft',
×
32
  InsertRight = 'InsertRight',
×
33
}
×
34

×
35
const iconClassName = 'mr-2 h-4 w-4';
×
36

×
37
export const FieldMenu = () => {
×
38
  const isTouchDevice = useIsTouchDevice();
×
39
  const view = useView() as GridView | undefined;
×
40
  const { headerMenu, closeHeaderMenu } = useGridViewStore();
×
41
  const { openSetting } = useFieldSettingStore();
×
42
  const permission = useTablePermission();
×
43
  const { t } = useTranslation(tableConfig.i18nNamespaces);
×
44
  const allFields = useFields({ withHidden: true });
×
45
  const fieldSettingRef = useRef<HTMLDivElement>(null);
×
46
  const fields = headerMenu?.fields;
×
47

×
48
  useClickAway(fieldSettingRef, () => {
×
49
    closeHeaderMenu();
×
50
  });
×
51

×
52
  if (!view || !fields?.length || !allFields.length) return null;
×
53

×
54
  const fieldIds = fields.map((f) => f.id);
×
55

×
56
  const visible = Boolean(headerMenu);
×
57
  const position = headerMenu?.position;
×
58
  const style = position
×
59
    ? {
×
60
        left: position.x,
×
61
        top: position.y,
×
62
      }
×
63
    : {};
×
64

×
65
  const insertField = async (isInsertAfter: boolean = true) => {
×
66
    const fieldId = fieldIds[0];
×
67
    const index = allFields.findIndex((f) => f.id === fieldId);
×
68

×
69
    if (index === -1) return;
×
70

×
71
    const newOrder = insertSingle(
×
72
      index,
×
73
      allFields.length,
×
74
      (index: number) => {
×
75
        return view.columnMeta[allFields[index].id].order;
×
76
      },
×
77
      isInsertAfter
×
78
    );
×
79

×
80
    return openSetting({
×
81
      order: newOrder,
×
82
      operator: FieldOperator.Insert,
×
83
    });
×
84
  };
×
85

×
86
  const freezeField = async () => {
×
87
    const fieldId = fieldIds[0];
×
88
    const index = allFields.findIndex((f) => f.id === fieldId);
×
89

×
90
    if (index === -1) return;
×
91

×
92
    view?.updateFrozenColumnCount(index + 1);
×
93
  };
×
94

×
95
  const menuGroups: IMenuItemProps<MenuItemType>[][] = [
×
96
    [
×
97
      {
×
98
        type: MenuItemType.Edit,
×
99
        name: t('table:menu.editField'),
×
100
        icon: <Edit className={iconClassName} />,
×
101
        hidden: fieldIds.length !== 1 || !permission['field|update'],
×
102
        onClick: async () => {
×
103
          openSetting({
×
104
            fieldId: fieldIds[0],
×
105
            operator: FieldOperator.Edit,
×
106
          });
×
107
        },
×
108
      },
×
109
    ],
×
110
    [
×
111
      {
×
112
        type: MenuItemType.InsertLeft,
×
113
        name: t('table:menu.insertFieldLeft'),
×
114
        icon: <ArrowLeft className={iconClassName} />,
×
115
        hidden: fieldIds.length !== 1 || !permission['field|create'],
×
116
        onClick: async () => await insertField(false),
×
117
      },
×
118
      {
×
119
        type: MenuItemType.InsertRight,
×
120
        name: t('table:menu.insertFieldRight'),
×
121
        icon: <ArrowRight className={iconClassName} />,
×
122
        hidden: fieldIds.length !== 1 || !permission['field|create'],
×
123
        onClick: async () => await insertField(),
×
124
      },
×
125
    ],
×
126
    [
×
127
      {
×
128
        type: MenuItemType.Freeze,
×
129
        name: t('table:menu.freezeUpField'),
×
130
        icon: <FreezeColumn className={iconClassName} />,
×
131
        hidden: fieldIds.length !== 1 || !permission['view|update'],
×
132
        onClick: async () => await freezeField(),
×
133
      },
×
134
    ],
×
135
    [
×
136
      {
×
137
        type: MenuItemType.Hidden,
×
138
        name: t('table:menu.hideField'),
×
139
        icon: <EyeOff className={iconClassName} />,
×
140
        hidden: !permission['view|update'],
×
141
        disabled: fields.some((f) => f.isPrimary),
×
142
        onClick: async () => {
×
143
          const fieldIdsSet = new Set(fieldIds);
×
144
          const filteredFields = allFields.filter((f) => fieldIdsSet.has(f.id)).filter(Boolean);
×
145
          if (filteredFields.length === 0) return;
×
146
          await view.updateColumnMeta(
×
147
            filteredFields.map((field) => ({ fieldId: field.id, columnMeta: { hidden: true } }))
×
148
          );
×
149
        },
×
150
      },
×
151
      {
×
152
        type: MenuItemType.Delete,
×
153
        name:
×
154
          fieldIds.length > 1
×
155
            ? t('table:menu.deleteAllSelectedFields')
×
156
            : t('table:menu.deleteField'),
×
157
        icon: <Trash className={iconClassName} />,
×
158
        hidden: !permission['field|delete'],
×
159
        disabled: fields.some((f) => f.isPrimary),
×
160
        className: 'text-red-500 aria-selected:text-red-500',
×
161
        onClick: async () => {
×
162
          const fieldIdsSet = new Set(fieldIds);
×
163
          const filteredFields = allFields.filter((f) => fieldIdsSet.has(f.id)).filter(Boolean);
×
164
          if (filteredFields.length === 0) return;
×
165
          for (const field of filteredFields) {
×
166
            await field.delete();
×
167
          }
×
168
        },
×
169
      },
×
170
    ],
×
171
  ].map((items) => items.filter(({ hidden }) => !hidden));
×
172

×
173
  return (
×
174
    <>
×
175
      {isTouchDevice ? (
×
176
        <Sheet open={visible} onOpenChange={(open) => !open && closeHeaderMenu()}>
×
177
          <SheetContent className="h-5/6 rounded-t-lg py-0" side="bottom">
×
178
            <SheetHeader className="h-16 justify-center border-b text-2xl">
×
179
              {allFields.find((f) => f.id === fieldIds[0])?.name ?? 'Untitled'}
×
180
            </SheetHeader>
×
181
            {menuGroups.flat().map(({ type, name, icon, disabled, className, onClick }) => {
×
182
              return (
×
183
                <div
×
NEW
184
                  className={cn('flex w-full items-center border-b py-3', className, {
×
185
                    'cursor-not-allowed': disabled,
×
186
                    'opacity-50': disabled,
×
187
                  })}
×
188
                  key={type}
×
189
                  onSelect={async () => {
×
190
                    if (disabled) {
×
191
                      return;
×
192
                    }
×
193
                    await onClick();
×
194
                    closeHeaderMenu();
×
195
                  }}
×
196
                >
×
197
                  {icon}
×
198
                  {name}
×
199
                </div>
×
200
              );
×
201
            })}
×
202
          </SheetContent>
×
203
        </Sheet>
×
204
      ) : (
×
205
        <Command
×
206
          ref={fieldSettingRef}
×
NEW
207
          className={cn('absolute rounded-lg shadow-sm w-60 h-auto border', {
×
208
            hidden: !visible,
×
209
          })}
×
210
          style={style}
×
211
        >
×
212
          <CommandList>
×
213
            {menuGroups.map((items, index) => {
×
214
              const nextItems = menuGroups[index + 1] ?? [];
×
215
              if (!items.length) return null;
×
216

×
217
              return (
×
218
                <Fragment key={index}>
×
219
                  <CommandGroup aria-valuetext="name">
×
220
                    {items.map(({ type, name, icon, disabled, className, onClick }) => (
×
221
                      <CommandItem
×
NEW
222
                        className={cn('px-4 py-2', className, {
×
223
                          'cursor-not-allowed': disabled,
×
224
                          'opacity-50': disabled,
×
225
                        })}
×
226
                        key={type}
×
227
                        value={name}
×
228
                        onSelect={async () => {
×
229
                          if (disabled) {
×
230
                            return;
×
231
                          }
×
232
                          await onClick();
×
233
                          closeHeaderMenu();
×
234
                        }}
×
235
                      >
×
236
                        {icon}
×
237
                        {name}
×
238
                      </CommandItem>
×
239
                    ))}
×
240
                  </CommandGroup>
×
241
                  {nextItems.length > 0 && <CommandSeparator />}
×
242
                </Fragment>
×
243
              );
×
244
            })}
×
245
          </CommandList>
×
246
        </Command>
×
247
      )}
×
248
    </>
×
249
  );
×
250
};
×
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