Skip to content

Commit 1e6fa95

Browse files
authored
perf: add filter item scroll to the bottom (#1988)
* perf: add filter item scroll to the bottom * fix: hidden field text too long chore: delete useless hook * perf: longtext display all the text * fix: mix the view rename and drag * perf: mobile toolbutton display gap * perf: view list display * fix: import date string error
1 parent 1cdf186 commit 1e6fa95

12 files changed

Lines changed: 144 additions & 49 deletions

File tree

‎apps/nestjs-backend/src/features/import/open-api/import-open-api.service.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Injectable, Logger, BadRequestException, ForbiddenException } from '@nestjs/common';
22
import type { IFieldRo } from '@teable/core';
3-
import { FieldType, getRandomString } from '@teable/core';
3+
import { FieldType, getRandomString, TimeFormatting } from '@teable/core';
44
import { PrismaService } from '@teable/db-main-prisma';
55
import type {
66
IAnalyzeRo,
@@ -80,7 +80,7 @@ export class ImportOpenApiService {
8080
formatting: {
8181
timeZone: tz,
8282
date: 'YYYY-MM-DD',
83-
time: 'None',
83+
time: TimeFormatting.None,
8484
},
8585
};
8686
}

‎apps/nextjs-app/src/features/app/blocks/view/list/ExpandViewList.tsx‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,15 @@ export const ExpandViewList = () => {
3333
<ChevronDown className="size-4" />
3434
</Button>
3535
</PopoverTrigger>
36-
<PopoverContent side="bottom" align="start" className="p-1">
36+
<PopoverContent side="bottom" align="start" className="w-auto max-w-[456px] p-1">
3737
<Command>
3838
<CommandInput
3939
className="h-9"
4040
placeholder={t('table:view.searchView')}
4141
onValueChange={(value) => setIsDraggable(!value)}
4242
/>
4343
<CommandEmpty>{t('common:noResult')}</CommandEmpty>
44-
<CommandList className="p-0.5">
44+
<CommandList className="max-h-[70vh] p-0.5">
4545
<DraggableWrapper strategy={verticalListSortingStrategy}>
4646
{({
4747
view: { id, name, type },
@@ -79,7 +79,9 @@ export const ExpandViewList = () => {
7979
}}
8080
>
8181
<Icon className="size-4 shrink-0" />
82-
<span className="ml-2 truncate text-sm">{name}</span>
82+
<span className="ml-2 truncate text-sm" title={name}>
83+
{name}
84+
</span>
8385
<span className="grow" />
8486
{isDraggable && permission['view|update'] && (
8587
<div {...attributes} {...listeners} className="pr-1">

‎apps/nextjs-app/src/features/app/blocks/view/list/ViewList.tsx‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useTablePermission, useViewId, useViews, useIsHydrated } from '@teable/sdk';
22
import { horizontalListSortingStrategy } from '@teable/ui-lib/base/dnd-kit';
33
import { cn } from '@teable/ui-lib/shadcn';
4+
import { useState } from 'react';
45
import { DraggableWrapper } from './DraggableWrapper';
56
import { ViewListItem } from './ViewListItem';
67

@@ -9,6 +10,7 @@ export const ViewList = () => {
910
const activeViewId = useViewId();
1011
const isHydrated = useIsHydrated();
1112
const permission = useTablePermission();
13+
const [editing, setEditing] = useState(false);
1214

1315
return isHydrated ? (
1416
views.length ? (
@@ -17,13 +19,14 @@ export const ViewList = () => {
1719
<div
1820
ref={setNodeRef}
1921
{...attributes}
20-
{...listeners}
22+
{...(editing ? {} : listeners)}
2123
style={style}
2224
className={cn('relative', {
2325
'opacity-50': isDragging,
2426
})}
2527
>
2628
<ViewListItem
29+
onEdit={(value) => setEditing(value)}
2730
view={view}
2831
removable={!!permission['view|delete'] && views.length > 1}
2932
isActive={view.id === activeViewId}
@@ -39,6 +42,7 @@ export const ViewList = () => {
3942
{views.map((view) => (
4043
<ViewListItem
4144
key={view.id}
45+
onEdit={(value) => setEditing(value)}
4246
view={view}
4347
removable={!!permission['view|delete'] && views.length > 1}
4448
isActive={view.id === activeViewId}

‎apps/nextjs-app/src/features/app/blocks/view/list/ViewListItem.tsx‎

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,10 @@ interface IProps {
2929
view: IViewInstance;
3030
removable: boolean;
3131
isActive: boolean;
32+
onEdit: (value: boolean) => void;
3233
}
3334

34-
export const ViewListItem: React.FC<IProps> = ({ view, removable, isActive }) => {
35+
export const ViewListItem: React.FC<IProps> = ({ view, removable, isActive, onEdit }) => {
3536
const [isEditing, setIsEditing] = useState(false);
3637
const [open, setOpen] = useState(false);
3738
const tableId = useTableId();
@@ -50,12 +51,13 @@ export const ViewListItem: React.FC<IProps> = ({ view, removable, isActive }) =>
5051
const { resetSearchHandler } = useGridSearchStore();
5152

5253
useEffect(() => {
53-
isActive &&
54+
if (isActive) {
5455
setTimeout(() => {
5556
viewItemRef.current?.scrollIntoView({
5657
behavior: 'smooth',
5758
});
5859
}, 0);
60+
}
5961
}, [isActive]);
6062

6163
const navigateHandler = () => {
@@ -108,13 +110,15 @@ export const ViewListItem: React.FC<IProps> = ({ view, removable, isActive }) =>
108110
view.updateName(e.target.value);
109111
}
110112
setIsEditing(false);
113+
onEdit(false);
111114
}}
112115
onKeyDown={(e) => {
113116
if (e.key === 'Enter') {
114117
if (e.currentTarget.value && e.currentTarget.value !== view.name) {
115118
view.updateName(e.currentTarget.value);
116119
}
117120
setIsEditing(false);
121+
onEdit(false);
118122
}
119123
e.stopPropagation();
120124
}}
@@ -135,7 +139,10 @@ export const ViewListItem: React.FC<IProps> = ({ view, removable, isActive }) =>
135139
}
136140
)}
137141
onDoubleClick={() => {
138-
permission['view|update'] && setIsEditing(true);
142+
if (permission['view|update']) {
143+
setIsEditing(true);
144+
onEdit(true);
145+
}
139146
}}
140147
onKeyDown={(e) => {
141148
if (isEditing) {
@@ -177,6 +184,7 @@ export const ViewListItem: React.FC<IProps> = ({ view, removable, isActive }) =>
177184
variant="ghost"
178185
onClick={() => {
179186
setIsEditing(true);
187+
onEdit(true);
180188
}}
181189
className="flex justify-start"
182190
>

‎apps/nextjs-app/src/features/app/blocks/view/tool-bar/components/GridViewOperators.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export const GridViewOperators: React.FC<{ disabled?: boolean }> = (props) => {
3636
return <div></div>;
3737
}
3838
return (
39-
<div className="flex @sm/toolbar:gap-1">
39+
<div className="flex gap-1">
4040
<HideFields>
4141
{(text, isActive) => (
4242
<ToolBarButton

‎apps/nextjs-app/src/features/app/hooks/useHiddenFields.ts‎

Lines changed: 0 additions & 14 deletions
This file was deleted.

‎packages/core/src/models/field/derivate/date.field.spec.ts‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,38 @@ describe('DateFieldCore', () => {
9999
expect(usField.convertStringToCellValue('5/1/2024 06:50')).toBe('2024-05-01T06:50:00.000Z');
100100
});
101101

102+
it('should parse single-digit month/day with seconds', () => {
103+
const fieldWithAsian = plainToInstance(DateFieldCore, {
104+
...json,
105+
options: {
106+
formatting: {
107+
date: DateFormattingPreset.Asian,
108+
time: TimeFormatting.Hour24,
109+
timeZone: 'Asia/Shanghai',
110+
},
111+
},
112+
});
113+
expect(fieldWithAsian.convertStringToCellValue('2025/7/31 14:15:32')).toBe(
114+
'2025-07-31T06:15:32.000Z'
115+
);
116+
});
117+
118+
it('should parse single-digit month/day with seconds', () => {
119+
const fieldWithAsian = plainToInstance(DateFieldCore, {
120+
...json,
121+
options: {
122+
formatting: {
123+
date: DateFormattingPreset.Asian,
124+
time: TimeFormatting.None,
125+
timeZone: 'Asia/Shanghai',
126+
},
127+
},
128+
});
129+
expect(fieldWithAsian.convertStringToCellValue('2025/7/31 14:15:32')).toBe(
130+
'2025-07-31T06:15:32.000Z'
131+
);
132+
});
133+
102134
it('should repair invalid value', () => {
103135
expect(field.repair(1687158022191)).toBe('2023-06-19T07:00:22.191Z');
104136
expect(field.repair('xxx')).toBe(null);

‎packages/core/src/models/field/derivate/date.field.ts‎

Lines changed: 47 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,39 @@ export class DateFieldCore extends FieldCore {
5858
}
5959
private defaultTzFormat(value: string) {
6060
try {
61-
const formatValue = dayjs.tz(value, this.options.formatting.timeZone);
61+
const formatValue = dayjs.utc(value);
6262
if (!formatValue.isValid()) return null;
6363
return formatValue.toISOString();
64-
} catch (e) {
64+
} catch {
65+
return null;
66+
}
67+
}
68+
69+
private parseUsingFieldFormatting(value: string): string | null {
70+
const hasTime = /\d{1,2}:\d{2}(?::\d{2})?/.test(value);
71+
const hasSeconds = /\d{1,2}:\d{2}:\d{2}/.test(value);
72+
const hasAmPm = /am|pm/i.test(value);
73+
74+
const dateFormat = this.options.formatting.date.replace('MM', 'M').replace('DD', 'D');
75+
76+
let timeFormat: string | null = null;
77+
if (hasTime) {
78+
if (this.options.formatting.time === TimeFormatting.Hour12 || hasAmPm) {
79+
timeFormat = hasSeconds ? 'hh:mm:ss A' : 'hh:mm A';
80+
} else {
81+
timeFormat = hasSeconds ? 'HH:mm:ss' : 'HH:mm';
82+
}
83+
}
84+
85+
const format = timeFormat ? `${dateFormat} ${timeFormat}` : dateFormat;
86+
87+
try {
88+
const formatValue = dayjs.tz(value, format, this.options.formatting.timeZone);
89+
if (!formatValue.isValid()) return null;
90+
const isoString = formatValue.toISOString();
91+
if (isoString.startsWith('-')) return null;
92+
return isoString;
93+
} catch {
6594
return null;
6695
}
6796
}
@@ -77,22 +106,24 @@ export class DateFieldCore extends FieldCore {
77106
return dayjs().toISOString();
78107
}
79108

80-
const hasTime = /\d{1,2}:\d{2}(?::\d{2})?/.test(value);
81-
82-
const format = `${this.options.formatting.date}${hasTime && this.options.formatting.time !== TimeFormatting.None ? ' ' + this.options.formatting.time : ''}`;
83-
84-
try {
85-
const formatValue = dayjs.tz(value, format, this.options.formatting.timeZone);
86-
if (!formatValue.isValid()) return null;
87-
const formatValueISOString = formatValue.toISOString();
88-
// ISOString start with '-' means the date is invalid
89-
if (formatValueISOString.startsWith('-')) {
90-
return null;
109+
if (/^\d+$/.test(String(value))) {
110+
const num = Number(value);
111+
const ms = String(value).length >= 13 ? num : num * 1000;
112+
try {
113+
const d = dayjs(ms).tz(this.options.formatting.timeZone);
114+
if (d.isValid()) {
115+
const iso = d.toISOString();
116+
if (!iso.startsWith('-')) return iso;
117+
}
118+
} catch {
119+
// ignore
91120
}
92-
return formatValue.toISOString();
93-
} catch (e) {
94-
return this.defaultTzFormat(value);
95121
}
122+
123+
const formatted = this.parseUsingFieldFormatting(value);
124+
if (formatted) return formatted;
125+
126+
return this.defaultTzFormat(value);
96127
}
97128

98129
item2String(item?: unknown) {

‎packages/sdk/src/components/cell-value/CellValue.tsx‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type {
1212
IButtonFieldOptions,
1313
} from '@teable/core';
1414
import { CellValueType, FieldType } from '@teable/core';
15+
import { cn } from '@teable/ui-lib';
1516
import type { IFieldInstance } from '../../model';
1617
import { CellAttachment } from './cell-attachment';
1718
import { CellButton } from './cell-button';
@@ -38,7 +39,13 @@ export const CellValue = (props: ICellValueContainer) => {
3839

3940
switch (type) {
4041
case FieldType.LongText: {
41-
return <CellText value={value as string} className={className} ellipsis={ellipsis} />;
42+
return (
43+
<CellText
44+
value={value as string}
45+
className={cn(className, 'line-clamp-none')}
46+
ellipsis={ellipsis}
47+
/>
48+
);
4249
}
4350
case FieldType.SingleLineText: {
4451
return (

‎packages/sdk/src/components/cell-value/components/OverflowTooltip.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export const OverflowTooltip = (props: IOverflowTooltipProps) => {
5353
const Content = (
5454
<div
5555
ref={contentRef}
56-
className={cn(className, 'overflow-hidden whitespace-pre-wrap break-all line-clamp-6')}
56+
className={cn('overflow-hidden whitespace-pre-wrap break-all line-clamp-6', className)}
5757
onClick={(e) => {
5858
if (onClick) {
5959
e.stopPropagation();

0 commit comments

Comments
 (0)