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

teableio / teable / 8538004962

03 Apr 2024 11:36AM UTC coverage: 18.233% (-3.3%) from 21.535%
8538004962

Pull #528

github

web-flow
Merge c1a248a6f into 45ee7ebb3
Pull Request #528: feat: Kanban view

575 of 1136 branches covered (50.62%)

29 of 2908 new or added lines in 83 files covered. (1.0%)

5 existing lines in 5 files now uncovered.

6439 of 35315 relevant lines covered (18.23%)

3.94 hits per line

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

13.53
/apps/nextjs-app/src/features/app/components/field-setting/options/SelectOptions.tsx
1
import type { ISelectFieldChoice, ISelectFieldOptions, Colors } from '@teable/core';
1✔
2
import { COLOR_PALETTE, ColorUtils } from '@teable/core';
1✔
3
import { DraggableHandle, Plus, Trash } from '@teable/icons';
1✔
4
import { DndKitContext, Droppable, Draggable } from '@teable/ui-lib/base/dnd-kit';
1✔
5
import type { DragEndEvent } from '@teable/ui-lib/base/dnd-kit';
1✔
6

1✔
7
import { Input, cn } from '@teable/ui-lib/shadcn';
1✔
8
import { Button } from '@teable/ui-lib/shadcn/ui/button';
1✔
9
import { Popover, PopoverContent, PopoverTrigger } from '@teable/ui-lib/shadcn/ui/popover';
1✔
10
import { useTranslation } from 'next-i18next';
1✔
11
import { useMemo, useRef, useState } from 'react';
1✔
12
import { tableConfig } from '@/features/i18n/table.config';
1✔
13

1✔
14
interface IOptionItemProps {
1✔
15
  choice: ISelectFieldChoice;
1✔
16
  readonly?: boolean;
1✔
17
  onChange: (key: keyof ISelectFieldChoice, value: string) => void;
1✔
18
  onKeyDown?: (e: React.KeyboardEvent<HTMLInputElement>) => void;
1✔
19
  onInputRef?: (el: HTMLInputElement | null) => void;
1✔
20
}
1✔
21

1✔
22
const getChoiceId = (choice: ISelectFieldChoice, index: number) => {
1✔
23
  const { id, color, name } = choice;
×
24
  return id ?? `${color}-${name}-${index}`;
×
25
};
×
26

1✔
27
export const SelectOptions = (props: {
1✔
28
  options: Partial<ISelectFieldOptions> | undefined;
×
29
  isLookup?: boolean;
×
30
  onChange?: (options: Partial<ISelectFieldOptions>) => void;
×
31
}) => {
×
32
  const { options, isLookup, onChange } = props;
×
33
  const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
×
34
  const { t } = useTranslation(tableConfig.i18nNamespaces);
×
35

×
36
  const choices = useMemo(() => options?.choices ?? [], [options?.choices]);
×
37
  const choiceIds = useMemo(
×
38
    () => choices.map((choice, index) => getChoiceId(choice, index)),
×
39
    [choices]
×
40
  );
×
41

×
42
  const updateOptionChange = (index: number, key: keyof ISelectFieldChoice, value: string) => {
×
43
    const newChoice = choices.map((v, i) => {
×
44
      if (i === index) {
×
45
        return {
×
46
          ...v,
×
47
          [key]: value,
×
48
        };
×
49
      }
×
50
      return v;
×
51
    });
×
52
    onChange?.({ choices: newChoice });
×
53
  };
×
54

×
55
  const deleteChoice = (index: number) => {
×
56
    onChange?.({
×
57
      choices: choices.filter((_, i) => i !== index),
×
58
    });
×
59
  };
×
60

×
61
  const addOption = () => {
×
62
    const existColors = choices.map((v) => v.color);
×
63
    const choice = {
×
64
      name: '',
×
65
      color: ColorUtils.randomColor(existColors)[0],
×
66
    } as ISelectFieldChoice;
×
67

×
68
    const newChoices = [...choices, choice];
×
69
    onChange?.({ choices: newChoices });
×
70
    setTimeout(() => {
×
71
      inputRefs.current[choices.length]?.focus();
×
72
    });
×
73
  };
×
74

×
75
  const onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
×
76
    if (e.key === 'Enter' && !isLookup) {
×
77
      addOption();
×
78
    }
×
79
  };
×
80

×
81
  const onDragEnd = async (event: DragEndEvent) => {
×
82
    const { over, active } = event;
×
83

×
84
    if (!over) return;
×
85

×
86
    const from = active?.data?.current?.sortable?.index;
×
87
    const to = over?.data?.current?.sortable?.index;
×
88
    const list = [...choices];
×
89
    const [choice] = list.splice(from, 1);
×
90

×
91
    list.splice(to, 0, choice);
×
92

×
93
    onChange?.({ choices: list });
×
94
  };
×
95

×
96
  return (
×
97
    <ul className="space-y-2">
×
98
      <DndKitContext onDragEnd={onDragEnd}>
×
99
        <Droppable items={choiceIds}>
×
100
          {choices.map((choice, i) => {
×
101
            const { name } = choice;
×
102
            return (
×
103
              <Draggable key={`${name}-${i}`} id={getChoiceId(choice, i)}>
×
104
                {({ setNodeRef, style, attributes, listeners, isDragging }) => (
×
105
                  <div
×
106
                    ref={setNodeRef}
×
107
                    style={style}
×
108
                    {...attributes}
×
109
                    className={cn(isDragging ? 'opacity-60' : null, isLookup && 'cursor-default')}
×
110
                  >
×
111
                    <div className="flex items-center">
×
112
                      {!isLookup && (
×
113
                        <DraggableHandle {...listeners} className="mr-1 size-4 cursor-grabbing" />
×
114
                      )}
×
115
                      <ChoiceItem
×
116
                        choice={choice}
×
117
                        readonly={isLookup}
×
NEW
118
                        onChange={(key, value) => updateOptionChange(i, key, value)}
×
119
                        onKeyDown={onKeyDown}
×
NEW
120
                        onInputRef={(el) => (inputRefs.current[i] = el)}
×
121
                      />
×
NEW
122
                      {!isLookup && (
×
NEW
123
                        <Button
×
NEW
124
                          variant={'ghost'}
×
NEW
125
                          className="size-6 rounded-full p-0 focus-visible:ring-transparent focus-visible:ring-offset-0"
×
NEW
126
                          onClick={() => deleteChoice(i)}
×
NEW
127
                        >
×
NEW
128
                          <Trash className="size-4" />
×
NEW
129
                        </Button>
×
NEW
130
                      )}
×
131
                    </div>
×
132
                  </div>
×
133
                )}
×
134
              </Draggable>
×
135
            );
×
136
          })}
×
137
        </Droppable>
×
138
      </DndKitContext>
×
139
      {!isLookup && (
×
140
        <li className="mt-1">
×
141
          <Button
×
142
            className="w-full gap-2 text-sm font-normal"
×
143
            size={'sm'}
×
144
            variant={'outline'}
×
145
            onClick={addOption}
×
146
          >
×
147
            <Plus className="size-4" />
×
148
            {t('table:field.editor.addOption')}
×
149
          </Button>
×
150
        </li>
×
151
      )}
×
152
    </ul>
×
153
  );
×
154
};
×
155

1✔
156
export const ChoiceItem = (props: IOptionItemProps) => {
1✔
NEW
157
  const { choice, readonly, onChange, onKeyDown, onInputRef } = props;
×
158
  const { color, name } = choice;
×
159
  const bgColor = ColorUtils.getHexForColor(color);
×
160

×
161
  return (
×
162
    <li className="flex grow items-center">
×
163
      {readonly ? (
×
164
        <div className="h-auto rounded-full border-2 p-[2px]" style={{ borderColor: bgColor }}>
×
165
          <div style={{ backgroundColor: bgColor }} className="size-3 rounded-full" />
×
166
        </div>
×
167
      ) : (
×
168
        <Popover>
×
169
          <PopoverTrigger>
×
170
            <Button
×
171
              variant={'ghost'}
×
172
              className="h-auto rounded-full border-2 p-[2px]"
×
173
              style={{ borderColor: bgColor }}
×
174
            >
×
175
              <div style={{ backgroundColor: bgColor }} className="size-3 rounded-full" />
×
176
            </Button>
×
177
          </PopoverTrigger>
×
178
          <PopoverContent className="w-auto p-2">
×
NEW
179
            <ColorPicker color={color} onSelect={(color) => onChange('color', color)} />
×
180
          </PopoverContent>
×
181
        </Popover>
×
182
      )}
×
183
      <div className="flex-1 px-2">
×
184
        <ChoiceInput
×
NEW
185
          reRef={(el) => onInputRef?.(el)}
×
186
          name={name}
×
187
          readOnly={readonly}
×
NEW
188
          onKeyDown={(e) => onKeyDown?.(e)}
×
NEW
189
          onChange={(value) => onChange('name', value)}
×
190
        />
×
191
      </div>
×
192
    </li>
×
193
  );
×
194
};
×
195

1✔
196
export const ChoiceInput: React.FC<{
1✔
197
  reRef: React.Ref<HTMLInputElement>;
1✔
198
  readOnly?: boolean;
1✔
199
  name: string;
1✔
200
  onChange: (name: string) => void;
1✔
201
  onKeyDown?: (e: React.KeyboardEvent<HTMLInputElement>) => void;
1✔
202
}> = ({ name, readOnly, onChange, onKeyDown, reRef }) => {
1✔
203
  const [value, setValue] = useState<string>(name);
×
NEW
204
  const onChangeInner = (e: React.ChangeEvent<HTMLInputElement>) => {
×
NEW
205
    const curValue = e.target.value;
×
NEW
206
    if (curValue === name) return;
×
NEW
207
    setValue(curValue);
×
NEW
208
  };
×
NEW
209

×
210
  return (
×
211
    <Input
×
212
      ref={reRef}
×
213
      className="h-7"
×
214
      type="text"
×
215
      value={value}
×
216
      readOnly={readOnly}
×
NEW
217
      onChange={onChangeInner}
×
218
      onKeyDown={onKeyDown}
×
NEW
219
      onBlur={() => onChange(value)}
×
220
    />
×
221
  );
×
222
};
×
223

1✔
224
export const ColorPicker = ({
1✔
225
  color,
×
226
  onSelect,
×
227
}: {
×
228
  color: Colors;
×
229
  onSelect: (color: Colors) => void;
×
230
}) => {
×
231
  return (
×
232
    <div className="flex w-64 flex-wrap p-2">
×
233
      {COLOR_PALETTE.map((group, index) => {
×
234
        return (
×
235
          <div key={index}>
×
236
            {group.map((c) => {
×
237
              const bg = ColorUtils.getHexForColor(c);
×
238

×
239
              return (
×
240
                <Button
×
241
                  key={c}
×
242
                  variant={'ghost'}
×
243
                  className={cn('p-1 my-1 rounded-full h-auto', {
×
244
                    'border-2 p-[2px]': color === c,
×
245
                  })}
×
246
                  style={{ borderColor: bg }}
×
NEW
247
                  onMouseDown={(e) => {
×
NEW
248
                    e.stopPropagation();
×
NEW
249
                    onSelect(c);
×
NEW
250
                  }}
×
251
                >
×
252
                  <div
×
253
                    style={{
×
254
                      backgroundColor: bg,
×
255
                    }}
×
256
                    className="size-4 rounded-full"
×
257
                  />
×
258
                </Button>
×
259
              );
×
260
            })}
×
261
          </div>
×
262
        );
×
263
      })}
×
264
    </div>
×
265
  );
×
266
};
×
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