Skip to content

Commit 4d04874

Browse files
Cherry-pick twentyhq#16206:修復看板視圖新建時沒有資料的問題
- 新增 mainGroupByFieldMetadataId 欄位到 View entity - 新增資料庫遷移 1764680275312-addMainGroupByFieldMetadataId - 補齊 usePersistViewGroup.ts 的 createViewGroups、deleteViewGroups、destroyViewGroups 函數 - 啟用 viewFragment.ts 的 mainGroupByFieldMetadataId 查詢 - 新增 isPersistingViewFieldsState 狀態 - 更新 ViewPicker 相關組件支援 mainGroupByFieldMetadataId
1 parent 274ae4a commit 4d04874

File tree

66 files changed

+713
-120
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

66 files changed

+713
-120
lines changed

.cursor/rules/README.mdc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ This directory contains Twenty's development guidelines and best practices in th
1212
### Core Guidelines
1313
- **architecture.mdc** - Project overview, technology stack, and infrastructure setup (Always Applied)
1414
- **nx-rules.mdc** - Nx workspace guidelines and best practices (Auto-attached to Nx files)
15+
- **server-migrations.mdc** - Backend migration and TypeORM guidelines for `twenty-server` (Auto-attached to server entities and migration files)
1516

1617
### Code Quality
1718
- **typescript-guidelines.mdc** - TypeScript best practices and conventions (Auto-attached to .ts/.tsx files)
@@ -40,7 +41,7 @@ You can manually reference any rule using the `@ruleName` syntax:
4041
- `@testing-guidelines` - Get testing recommendations
4142

4243
### Rule Types Used
43-
- **Always Applied** - Loaded in every context (architecture.mdc, README.mdc)
44+
- **Always Applied** - Loaded in every context (architecture.mdc, README.mdc)
4445
- **Auto Attached** - Loaded when matching file patterns are referenced
4546
- **Agent Requested** - Available for AI to include when relevant
4647
- **Manual** - Only included when explicitly mentioned
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
description: Guidelines for generating and managing TypeORM migrations in twenty-server
3+
globs: [
4+
"packages/twenty-server/src/**/*.entity.ts",
5+
"packages/twenty-server/src/database/typeorm/**/*.ts"
6+
]
7+
alwaysApply: false
8+
---
9+
10+
## Server Migrations (twenty-server)
11+
12+
- **When changing an entity, always generate a migration**
13+
- If you modify a `*.entity.ts` file in `packages/twenty-server/src`, you **must** generate a corresponding TypeORM migration instead of manually editing the database schema.
14+
- Use the Nx + TypeORM command from the project root:
15+
16+
```bash
17+
npx nx run twenty-server:typeorm migration:generate src/database/typeorm/core/migrations/common/[name] -d src/database/typeorm/core/core.datasource.ts
18+
```
19+
20+
- Replace `[name]` with a descriptive, kebab-case migration name that reflects the change (for example, `add-agent-turn-evaluation`).
21+
22+
- **Prefer generated migrations over manual edits**
23+
- Let TypeORM infer schema changes from the updated entities; only adjust the generated migration file manually if absolutely necessary (for example, for data backfills or complex constraints).
24+
- Keep schema changes (DDL) in these generated migrations and avoid mixing in heavy data migrations unless there is a strong reason and clear comments.
25+
26+
- **Keep migrations consistent and reversible**
27+
- Ensure the generated migration includes both `up` and `down` logic that correctly applies and reverts the entity change when possible.
28+
- Do not delete or rewrite existing, committed migrations unless you are explicitly working on a pre-release branch where history rewrites are allowed by team conventions.
29+

packages/twenty-front/src/generated-metadata/graphql.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -670,6 +670,7 @@ export type CoreView = {
670670
kanbanAggregateOperation?: Maybe<AggregateOperations>;
671671
kanbanAggregateOperationFieldMetadataId?: Maybe<Scalars['UUID']>;
672672
key?: Maybe<ViewKey>;
673+
mainGroupByFieldMetadataId?: Maybe<Scalars['UUID']>;
673674
name: Scalars['String'];
674675
objectMetadataId: Scalars['UUID'];
675676
openRecordIn: ViewOpenRecordIn;
@@ -966,6 +967,7 @@ export type CreateViewInput = {
966967
kanbanAggregateOperation?: InputMaybe<AggregateOperations>;
967968
kanbanAggregateOperationFieldMetadataId?: InputMaybe<Scalars['UUID']>;
968969
key?: InputMaybe<ViewKey>;
970+
mainGroupByFieldMetadataId?: InputMaybe<Scalars['UUID']>;
969971
name: Scalars['String'];
970972
objectMetadataId: Scalars['UUID'];
971973
openRecordIn?: InputMaybe<ViewOpenRecordIn>;
@@ -4399,6 +4401,7 @@ export type UpdateViewInput = {
43994401
isCompact?: InputMaybe<Scalars['Boolean']>;
44004402
kanbanAggregateOperation?: InputMaybe<AggregateOperations>;
44014403
kanbanAggregateOperationFieldMetadataId?: InputMaybe<Scalars['UUID']>;
4404+
mainGroupByFieldMetadataId?: InputMaybe<Scalars['UUID']>;
44024405
name?: InputMaybe<Scalars['String']>;
44034406
openRecordIn?: InputMaybe<ViewOpenRecordIn>;
44044407
position?: InputMaybe<Scalars['Float']>;

packages/twenty-front/src/generated/graphql.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -670,6 +670,7 @@ export type CoreView = {
670670
kanbanAggregateOperation?: Maybe<AggregateOperations>;
671671
kanbanAggregateOperationFieldMetadataId?: Maybe<Scalars['UUID']>;
672672
key?: Maybe<ViewKey>;
673+
mainGroupByFieldMetadataId?: Maybe<Scalars['UUID']>;
673674
name: Scalars['String'];
674675
objectMetadataId: Scalars['UUID'];
675676
openRecordIn: ViewOpenRecordIn;
@@ -930,6 +931,7 @@ export type CreateViewInput = {
930931
kanbanAggregateOperation?: InputMaybe<AggregateOperations>;
931932
kanbanAggregateOperationFieldMetadataId?: InputMaybe<Scalars['UUID']>;
932933
key?: InputMaybe<ViewKey>;
934+
mainGroupByFieldMetadataId?: InputMaybe<Scalars['UUID']>;
933935
name: Scalars['String'];
934936
objectMetadataId: Scalars['UUID'];
935937
openRecordIn?: InputMaybe<ViewOpenRecordIn>;
@@ -4237,6 +4239,7 @@ export type UpdateViewInput = {
42374239
isCompact?: InputMaybe<Scalars['Boolean']>;
42384240
kanbanAggregateOperation?: InputMaybe<AggregateOperations>;
42394241
kanbanAggregateOperationFieldMetadataId?: InputMaybe<Scalars['UUID']>;
4242+
mainGroupByFieldMetadataId?: InputMaybe<Scalars['UUID']>;
42404243
name?: InputMaybe<Scalars['String']>;
42414244
openRecordIn?: InputMaybe<ViewOpenRecordIn>;
42424245
position?: InputMaybe<Scalars['Float']>;

packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupsContent.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -113,13 +113,10 @@ export const ObjectOptionsDropdownRecordGroupsContent = () => {
113113
>
114114
{currentView?.key !== 'INDEX' && (
115115
<>
116-
<SelectableListItem
117-
itemId="GroupBy"
118-
onEnter={() => onContentChange('recordGroupFields')}
119-
>
116+
<SelectableListItem itemId="GroupBy">
120117
<MenuItem
121118
focused={selectedItemId === 'GroupBy'}
122-
onClick={() => onContentChange('recordGroupFields')}
119+
disabled
123120
LeftIcon={IconLayoutList}
124121
text={t`Group by`}
125122
contextualText={recordGroupFieldMetadata?.label}

packages/twenty-front/src/modules/object-record/record-index/hooks/useHandleRecordGroupField.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { useSetRecordGroups } from '@/object-record/record-group/hooks/useSetRec
55
import { useLoadRecordIndexStates } from '@/object-record/record-index/hooks/useLoadRecordIndexStates';
66
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
77
import { usePersistView } from '@/views/hooks/internal/usePersistView';
8+
import { usePersistViewGroupRecords } from '@/views/hooks/internal/usePersistViewGroup';
89
import { useGetViewFromPrefetchState } from '@/views/hooks/useGetViewFromPrefetchState';
910
import { useRefreshCoreViewsByObjectMetadataId } from '@/views/hooks/useRefreshCoreViewsByObjectMetadataId';
1011
import { type ViewGroup } from '@/views/types/ViewGroup';
@@ -16,6 +17,8 @@ import { type CoreView } from '~/generated/graphql';
1617
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
1718

1819
export const useHandleRecordGroupField = () => {
20+
const { createViewGroups, deleteViewGroups } = usePersistViewGroupRecords();
21+
1922
const currentViewIdCallbackState = useRecoilComponentCallbackState(
2023
contextStoreCurrentViewIdComponentState,
2124
);
@@ -138,6 +141,8 @@ export const useHandleRecordGroupField = () => {
138141
objectMetadataItem,
139142
refreshCoreViewsByObjectMetadataId,
140143
loadRecordIndexStates,
144+
createViewGroups,
145+
deleteViewGroups,
141146
],
142147
);
143148

packages/twenty-front/src/modules/views/graphql/fragments/viewFragment.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export const VIEW_FRAGMENT = gql`
2424
openRecordIn
2525
kanbanAggregateOperation
2626
kanbanAggregateOperationFieldMetadataId
27+
mainGroupByFieldMetadataId
2728
anyFieldFilterValue
2829
calendarFieldMetadataId
2930
calendarLayout

packages/twenty-front/src/modules/views/hooks/internal/usePersistViewGroup.ts

Lines changed: 182 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,89 @@ import { ApolloError } from '@apollo/client';
88
import { t } from '@lingui/core/macro';
99
import { isDefined } from 'twenty-shared/utils';
1010
import {
11-
type UpdateCoreViewGroupMutationVariables,
12-
useUpdateCoreViewGroupMutation,
11+
type CreateManyCoreViewGroupsMutationVariables,
12+
type DeleteCoreViewGroupMutationVariables,
13+
type DestroyCoreViewGroupMutationVariables,
14+
type UpdateCoreViewGroupMutationVariables,
15+
useCreateManyCoreViewGroupsMutation,
16+
useDeleteCoreViewGroupMutation,
17+
useDestroyCoreViewGroupMutation,
18+
useUpdateCoreViewGroupMutation,
1319
} from '~/generated/graphql';
1420

1521
export const usePersistViewGroupRecords = () => {
1622
const { triggerViewGroupOptimisticEffect } =
1723
useTriggerViewGroupOptimisticEffect();
1824

25+
const [createManyCoreViewGroupsMutation] =
26+
useCreateManyCoreViewGroupsMutation();
1927
const [updateCoreViewGroupMutation] = useUpdateCoreViewGroupMutation();
28+
const [deleteCoreViewGroupMutation] = useDeleteCoreViewGroupMutation();
29+
const [destroyCoreViewGroupMutation] = useDestroyCoreViewGroupMutation();
2030

2131
const { handleMetadataError } = useMetadataErrorHandler();
2232
const { enqueueErrorSnackBar } = useSnackBar();
2333

34+
const createViewGroups = useCallback(
35+
async (
36+
createCoreViewGroupInputs: CreateManyCoreViewGroupsMutationVariables,
37+
): Promise<
38+
MetadataRequestResult<Awaited<
39+
ReturnType<typeof createManyCoreViewGroupsMutation>
40+
> | null>
41+
> => {
42+
if (
43+
!Array.isArray(createCoreViewGroupInputs.inputs) ||
44+
createCoreViewGroupInputs.inputs.length === 0
45+
) {
46+
return {
47+
status: 'successful',
48+
response: null,
49+
};
50+
}
51+
52+
try {
53+
const result = await createManyCoreViewGroupsMutation({
54+
variables: createCoreViewGroupInputs,
55+
update: (_cache, { data }) => {
56+
const createdViewGroups = data?.createManyCoreViewGroups;
57+
if (!isDefined(createdViewGroups)) {
58+
return;
59+
}
60+
61+
triggerViewGroupOptimisticEffect({
62+
createdViewGroups,
63+
});
64+
},
65+
});
66+
67+
return {
68+
status: 'successful',
69+
response: result,
70+
};
71+
} catch (error) {
72+
if (error instanceof ApolloError) {
73+
handleMetadataError(error, {
74+
primaryMetadataName: 'viewGroup',
75+
});
76+
} else {
77+
enqueueErrorSnackBar({ message: t`An error occurred.` });
78+
}
79+
80+
return {
81+
status: 'failed',
82+
error,
83+
};
84+
}
85+
},
86+
[
87+
triggerViewGroupOptimisticEffect,
88+
createManyCoreViewGroupsMutation,
89+
handleMetadataError,
90+
enqueueErrorSnackBar,
91+
],
92+
);
93+
2494
const updateViewGroups = useCallback(
2595
async (
2696
updateCoreViewGroupInputs: UpdateCoreViewGroupMutationVariables[],
@@ -82,7 +152,117 @@ export const usePersistViewGroupRecords = () => {
82152
],
83153
);
84154

155+
const deleteViewGroups = useCallback(
156+
async (
157+
deleteCoreViewGroupInputs: DeleteCoreViewGroupMutationVariables[],
158+
): Promise<
159+
MetadataRequestResult<
160+
Awaited<ReturnType<typeof deleteCoreViewGroupMutation>>[]
161+
>
162+
> => {
163+
if (deleteCoreViewGroupInputs.length === 0) {
164+
return {
165+
status: 'successful',
166+
response: [],
167+
};
168+
}
169+
170+
try {
171+
const results = await Promise.all(
172+
deleteCoreViewGroupInputs.map((variables) =>
173+
deleteCoreViewGroupMutation({
174+
variables,
175+
update: (_cache, { data }) => {
176+
const deletedViewGroup = data?.deleteCoreViewGroup;
177+
if (!isDefined(deletedViewGroup)) {
178+
return;
179+
}
180+
181+
triggerViewGroupOptimisticEffect({
182+
deletedViewGroups: [deletedViewGroup],
183+
});
184+
},
185+
}),
186+
),
187+
);
188+
189+
return {
190+
status: 'successful',
191+
response: results,
192+
};
193+
} catch (error) {
194+
if (error instanceof ApolloError) {
195+
handleMetadataError(error, {
196+
primaryMetadataName: 'viewGroup',
197+
});
198+
} else {
199+
enqueueErrorSnackBar({ message: t`An error occurred.` });
200+
}
201+
202+
return {
203+
status: 'failed',
204+
error,
205+
};
206+
}
207+
},
208+
[
209+
triggerViewGroupOptimisticEffect,
210+
deleteCoreViewGroupMutation,
211+
handleMetadataError,
212+
enqueueErrorSnackBar,
213+
],
214+
);
215+
216+
const destroyViewGroups = useCallback(
217+
async (
218+
destroyCoreViewGroupInputs: DestroyCoreViewGroupMutationVariables[],
219+
): Promise<
220+
MetadataRequestResult<
221+
Awaited<ReturnType<typeof destroyCoreViewGroupMutation>>[]
222+
>
223+
> => {
224+
if (destroyCoreViewGroupInputs.length === 0) {
225+
return {
226+
status: 'successful',
227+
response: [],
228+
};
229+
}
230+
231+
try {
232+
const results = await Promise.all(
233+
destroyCoreViewGroupInputs.map((variables) =>
234+
destroyCoreViewGroupMutation({
235+
variables,
236+
}),
237+
),
238+
);
239+
240+
return {
241+
status: 'successful',
242+
response: results,
243+
};
244+
} catch (error) {
245+
if (error instanceof ApolloError) {
246+
handleMetadataError(error, {
247+
primaryMetadataName: 'viewGroup',
248+
});
249+
} else {
250+
enqueueErrorSnackBar({ message: t`An error occurred.` });
251+
}
252+
253+
return {
254+
status: 'failed',
255+
error,
256+
};
257+
}
258+
},
259+
[destroyCoreViewGroupMutation, handleMetadataError, enqueueErrorSnackBar],
260+
);
261+
85262
return {
263+
createViewGroups,
86264
updateViewGroups,
265+
deleteViewGroups,
266+
destroyViewGroups,
87267
};
88268
};

0 commit comments

Comments
 (0)