Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
78c4f9c73c |
@@ -3193,7 +3193,6 @@ type Mutation {
|
||||
updateView(id: String!, input: UpdateViewInput!): View!
|
||||
deleteView(id: String!): Boolean!
|
||||
destroyView(id: String!): Boolean!
|
||||
upsertViewWidget(input: UpsertViewWidgetInput!): View!
|
||||
createViewSort(input: CreateViewSortInput!): ViewSort!
|
||||
updateViewSort(input: UpdateViewSortInput!): ViewSort!
|
||||
deleteViewSort(input: DeleteViewSortInput!): Boolean!
|
||||
@@ -3511,59 +3510,6 @@ input UpdateViewInput {
|
||||
shouldHideEmptyGroups: Boolean
|
||||
}
|
||||
|
||||
input UpsertViewWidgetInput {
|
||||
"""The id of the view widget (page layout widget)."""
|
||||
widgetId: UUID!
|
||||
|
||||
"""The view fields to upsert."""
|
||||
viewFields: [UpsertViewWidgetViewFieldInput!]
|
||||
|
||||
"""The view filters to upsert."""
|
||||
viewFilters: [UpsertViewWidgetViewFilterInput!]
|
||||
|
||||
"""The view filter groups to upsert."""
|
||||
viewFilterGroups: [UpsertViewWidgetViewFilterGroupInput!]
|
||||
|
||||
"""The view sorts to upsert."""
|
||||
viewSorts: [UpsertViewWidgetViewSortInput!]
|
||||
}
|
||||
|
||||
input UpsertViewWidgetViewFieldInput {
|
||||
"""The id of an existing view field to update."""
|
||||
viewFieldId: UUID
|
||||
|
||||
"""
|
||||
The field metadata id. Used to create a new view field when viewFieldId is not provided.
|
||||
"""
|
||||
fieldMetadataId: UUID
|
||||
isVisible: Boolean!
|
||||
position: Float!
|
||||
size: Float
|
||||
}
|
||||
|
||||
input UpsertViewWidgetViewFilterInput {
|
||||
id: UUID
|
||||
fieldMetadataId: UUID!
|
||||
operand: ViewFilterOperand = CONTAINS
|
||||
value: JSON!
|
||||
viewFilterGroupId: UUID
|
||||
positionInViewFilterGroup: Float
|
||||
subFieldName: String
|
||||
}
|
||||
|
||||
input UpsertViewWidgetViewFilterGroupInput {
|
||||
id: UUID
|
||||
parentViewFilterGroupId: UUID
|
||||
logicalOperator: ViewFilterGroupLogicalOperator = AND
|
||||
positionInViewFilterGroup: Float
|
||||
}
|
||||
|
||||
input UpsertViewWidgetViewSortInput {
|
||||
id: UUID
|
||||
fieldMetadataId: UUID!
|
||||
direction: ViewSortDirection = ASC
|
||||
}
|
||||
|
||||
input CreateViewSortInput {
|
||||
id: UUID
|
||||
fieldMetadataId: UUID!
|
||||
|
||||
@@ -2675,7 +2675,6 @@ export interface Mutation {
|
||||
updateView: View
|
||||
deleteView: Scalars['Boolean']
|
||||
destroyView: Scalars['Boolean']
|
||||
upsertViewWidget: View
|
||||
createViewSort: ViewSort
|
||||
updateViewSort: ViewSort
|
||||
deleteViewSort: Scalars['Boolean']
|
||||
@@ -5726,7 +5725,6 @@ export interface MutationGenqlSelection{
|
||||
updateView?: (ViewGenqlSelection & { __args: {id: Scalars['String'], input: UpdateViewInput} })
|
||||
deleteView?: { __args: {id: Scalars['String']} }
|
||||
destroyView?: { __args: {id: Scalars['String']} }
|
||||
upsertViewWidget?: (ViewGenqlSelection & { __args: {input: UpsertViewWidgetInput} })
|
||||
createViewSort?: (ViewSortGenqlSelection & { __args: {input: CreateViewSortInput} })
|
||||
updateViewSort?: (ViewSortGenqlSelection & { __args: {input: UpdateViewSortInput} })
|
||||
deleteViewSort?: { __args: {input: DeleteViewSortInput} }
|
||||
@@ -5946,30 +5944,6 @@ export interface CreateViewInput {id?: (Scalars['UUID'] | null),name: Scalars['S
|
||||
|
||||
export interface UpdateViewInput {id?: (Scalars['UUID'] | null),name?: (Scalars['String'] | null),type?: (ViewType | null),icon?: (Scalars['String'] | null),position?: (Scalars['Float'] | null),isCompact?: (Scalars['Boolean'] | null),openRecordIn?: (ViewOpenRecordIn | null),kanbanAggregateOperation?: (AggregateOperations | null),kanbanAggregateOperationFieldMetadataId?: (Scalars['UUID'] | null),anyFieldFilterValue?: (Scalars['String'] | null),calendarLayout?: (ViewCalendarLayout | null),calendarFieldMetadataId?: (Scalars['UUID'] | null),visibility?: (ViewVisibility | null),mainGroupByFieldMetadataId?: (Scalars['UUID'] | null),shouldHideEmptyGroups?: (Scalars['Boolean'] | null)}
|
||||
|
||||
export interface UpsertViewWidgetInput {
|
||||
/** The id of the view widget (page layout widget). */
|
||||
widgetId: Scalars['UUID'],
|
||||
/** The view fields to upsert. */
|
||||
viewFields?: (UpsertViewWidgetViewFieldInput[] | null),
|
||||
/** The view filters to upsert. */
|
||||
viewFilters?: (UpsertViewWidgetViewFilterInput[] | null),
|
||||
/** The view filter groups to upsert. */
|
||||
viewFilterGroups?: (UpsertViewWidgetViewFilterGroupInput[] | null),
|
||||
/** The view sorts to upsert. */
|
||||
viewSorts?: (UpsertViewWidgetViewSortInput[] | null)}
|
||||
|
||||
export interface UpsertViewWidgetViewFieldInput {
|
||||
/** The id of an existing view field to update. */
|
||||
viewFieldId?: (Scalars['UUID'] | null),
|
||||
/** The field metadata id. Used to create a new view field when viewFieldId is not provided. */
|
||||
fieldMetadataId?: (Scalars['UUID'] | null),isVisible: Scalars['Boolean'],position: Scalars['Float'],size?: (Scalars['Float'] | null)}
|
||||
|
||||
export interface UpsertViewWidgetViewFilterInput {id?: (Scalars['UUID'] | null),fieldMetadataId: Scalars['UUID'],operand?: (ViewFilterOperand | null),value: Scalars['JSON'],viewFilterGroupId?: (Scalars['UUID'] | null),positionInViewFilterGroup?: (Scalars['Float'] | null),subFieldName?: (Scalars['String'] | null)}
|
||||
|
||||
export interface UpsertViewWidgetViewFilterGroupInput {id?: (Scalars['UUID'] | null),parentViewFilterGroupId?: (Scalars['UUID'] | null),logicalOperator?: (ViewFilterGroupLogicalOperator | null),positionInViewFilterGroup?: (Scalars['Float'] | null)}
|
||||
|
||||
export interface UpsertViewWidgetViewSortInput {id?: (Scalars['UUID'] | null),fieldMetadataId: Scalars['UUID'],direction?: (ViewSortDirection | null)}
|
||||
|
||||
export interface CreateViewSortInput {id?: (Scalars['UUID'] | null),fieldMetadataId: Scalars['UUID'],direction?: (ViewSortDirection | null),viewId: Scalars['UUID']}
|
||||
|
||||
export interface UpdateViewSortInput {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -11,12 +11,10 @@ COPY ./nx.json .
|
||||
COPY ./.yarn/releases /app/.yarn/releases
|
||||
COPY ./.yarn/patches /app/.yarn/patches
|
||||
COPY ./packages/twenty-oxlint-rules /app/packages/twenty-oxlint-rules
|
||||
COPY ./packages/twenty-shared/package.json /app/packages/twenty-shared/package.json
|
||||
COPY ./packages/twenty-website-new/package.json /app/packages/twenty-website-new/package.json
|
||||
|
||||
RUN yarn
|
||||
|
||||
COPY ./packages/twenty-shared /app/packages/twenty-shared
|
||||
COPY ./packages/twenty-website-new /app/packages/twenty-website-new
|
||||
RUN npx nx build twenty-website-new
|
||||
|
||||
|
||||
@@ -61,8 +61,8 @@ const jestConfig = {
|
||||
extensionsToTreatAsEsm: ['.ts', '.tsx'],
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
statements: 47.3,
|
||||
lines: 45.9,
|
||||
statements: 47.9,
|
||||
lines: 46,
|
||||
functions: 39.5,
|
||||
},
|
||||
},
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -180,28 +180,6 @@ const SettingsApplicationDetails = lazy(() =>
|
||||
),
|
||||
);
|
||||
|
||||
const SettingsApplicationFrontComponentDetail = lazy(() =>
|
||||
import(
|
||||
'~/pages/settings/applications/SettingsApplicationFrontComponentDetail'
|
||||
).then((module) => ({
|
||||
default: module.SettingsApplicationFrontComponentDetail,
|
||||
})),
|
||||
);
|
||||
|
||||
const SettingsLayoutViewDetail = lazy(() =>
|
||||
import('~/pages/settings/layout/SettingsLayoutViewDetail').then((module) => ({
|
||||
default: module.SettingsLayoutViewDetail,
|
||||
})),
|
||||
);
|
||||
|
||||
const SettingsLayoutPageLayoutDetail = lazy(() =>
|
||||
import('~/pages/settings/layout/SettingsLayoutPageLayoutDetail').then(
|
||||
(module) => ({
|
||||
default: module.SettingsLayoutPageLayoutDetail,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const SettingsAdminApplicationRegistrationDetail = lazy(() =>
|
||||
import(
|
||||
'~/pages/settings/admin-panel/SettingsAdminApplicationRegistrationDetail'
|
||||
@@ -774,18 +752,6 @@ export const SettingsRoutes = ({ isAdminPageEnabled }: SettingsRoutesProps) => (
|
||||
path={SettingsPath.ApplicationLogicFunctionDetail}
|
||||
element={<SettingsLogicFunctionDetail />}
|
||||
/>
|
||||
<Route
|
||||
path={SettingsPath.ApplicationFrontComponentDetail}
|
||||
element={<SettingsApplicationFrontComponentDetail />}
|
||||
/>
|
||||
<Route
|
||||
path={SettingsPath.ApplicationViewDetail}
|
||||
element={<SettingsLayoutViewDetail />}
|
||||
/>
|
||||
<Route
|
||||
path={SettingsPath.ApplicationPageLayoutDetail}
|
||||
element={<SettingsLayoutPageLayoutDetail />}
|
||||
/>
|
||||
<Route
|
||||
path={SettingsPath.ApplicationRegistrationConfigVariableDetails}
|
||||
element={<SettingsApplicationRegistrationConfigVariableDetail />}
|
||||
|
||||
-7
@@ -39,13 +39,6 @@ export const APPLICATION_FRAGMENT = gql`
|
||||
name
|
||||
description
|
||||
applicationId
|
||||
componentName
|
||||
builtComponentChecksum
|
||||
universalIdentifier
|
||||
isHeadless
|
||||
usesSdkClient
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
objects {
|
||||
...ObjectMetadataFields
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { useAuth } from '@/auth/hooks/useAuth';
|
||||
import { billingState } from '@/client-config/states/billingState';
|
||||
import { isDeveloperDefaultSignInPrefilledState } from '@/client-config/states/isDeveloperDefaultSignInPrefilledState';
|
||||
import { supportChatState } from '@/client-config/states/supportChatState';
|
||||
|
||||
import { workspaceAuthProvidersState } from '@/workspace/states/workspaceAuthProvidersState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useApolloClient } from '@apollo/client/react';
|
||||
import { MockedProvider } from '@apollo/client/testing/react';
|
||||
import { type ReactNode, act } from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
@@ -11,8 +17,10 @@ import {
|
||||
results,
|
||||
token,
|
||||
} from '@/auth/hooks/__mocks__/useAuth';
|
||||
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
|
||||
import { SnackBarComponentInstanceContext } from '@/ui/feedback/snack-bar-manager/contexts/SnackBarComponentInstanceContext';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { SupportDriver } from '~/generated-metadata/graphql';
|
||||
|
||||
const redirectSpy = jest.fn();
|
||||
|
||||
@@ -139,15 +147,55 @@ describe('useAuth', () => {
|
||||
});
|
||||
|
||||
it('should handle sign-out', async () => {
|
||||
sessionStorage.setItem('lingering-key', 'should-be-cleared');
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const client = useApolloClient();
|
||||
const workspaceAuthProviders = useAtomStateValue(
|
||||
workspaceAuthProvidersState,
|
||||
);
|
||||
const billing = useAtomStateValue(billingState);
|
||||
const isDeveloperDefaultSignInPrefilled = useAtomStateValue(
|
||||
isDeveloperDefaultSignInPrefilledState,
|
||||
);
|
||||
const supportChat = useAtomStateValue(supportChatState);
|
||||
const isMultiWorkspaceEnabled = useAtomStateValue(
|
||||
isMultiWorkspaceEnabledState,
|
||||
);
|
||||
return {
|
||||
...useAuth(),
|
||||
client,
|
||||
state: {
|
||||
workspaceAuthProviders,
|
||||
billing,
|
||||
isDeveloperDefaultSignInPrefilled,
|
||||
supportChat,
|
||||
isMultiWorkspaceEnabled,
|
||||
},
|
||||
};
|
||||
},
|
||||
{
|
||||
wrapper: Wrapper,
|
||||
},
|
||||
);
|
||||
|
||||
const { result } = renderHooks();
|
||||
const { signOut, client } = result.current;
|
||||
|
||||
await act(async () => {
|
||||
result.current.signOut();
|
||||
await signOut();
|
||||
});
|
||||
|
||||
expect(sessionStorage.length).toBe(0);
|
||||
expect(client.cache.extract()).toEqual({});
|
||||
|
||||
const { state } = result.current;
|
||||
|
||||
expect(state.workspaceAuthProviders).toEqual(null);
|
||||
expect(state.billing).toBeNull();
|
||||
expect(state.isDeveloperDefaultSignInPrefilled).toBe(false);
|
||||
expect(state.supportChat).toEqual({
|
||||
supportDriver: SupportDriver.NONE,
|
||||
supportFrontChatId: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle credential sign-up', async () => {
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { useLazyQuery, useMutation } from '@apollo/client/react';
|
||||
import {
|
||||
useApolloClient,
|
||||
useLazyQuery,
|
||||
useMutation,
|
||||
} from '@apollo/client/react';
|
||||
import { useCallback } from 'react';
|
||||
import { AppPath } from 'twenty-shared/types';
|
||||
|
||||
@@ -24,7 +28,16 @@ import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomState
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
|
||||
import { isAppEffectRedirectEnabledState } from '@/app/states/isAppEffectRedirectEnabledState';
|
||||
import { availableWorkspacesState } from '@/auth/states/availableWorkspacesState';
|
||||
import { currentUserState } from '@/auth/states/currentUserState';
|
||||
import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { currentWorkspaceMembersState } from '@/auth/states/currentWorkspaceMembersState';
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { useSignUpInNewWorkspace } from '@/auth/sign-in-up/hooks/useSignUpInNewWorkspace';
|
||||
import { useLoadMockedMetadata } from '@/metadata-store/hooks/useLoadMockedMetadata';
|
||||
import { preloadMockedMetadata } from '@/metadata-store/utils/preloadMockedMetadata';
|
||||
import { lastAuthenticatedMethodState } from '@/auth/states/lastAuthenticatedMethodState';
|
||||
import { loginTokenState } from '@/auth/states/loginTokenState';
|
||||
import {
|
||||
SignInUpStep,
|
||||
@@ -36,13 +49,18 @@ import {
|
||||
countAvailableWorkspaces,
|
||||
getFirstAvailableWorkspaces,
|
||||
} from '@/auth/utils/availableWorkspacesUtils';
|
||||
import { useRequestFreshCaptchaToken } from '@/captcha/hooks/useRequestFreshCaptchaToken';
|
||||
import { isCaptchaScriptLoadedState } from '@/captcha/states/isCaptchaScriptLoadedState';
|
||||
import { isEmailVerificationRequiredState } from '@/client-config/states/isEmailVerificationRequiredState';
|
||||
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
|
||||
import { useLastAuthenticatedWorkspaceDomain } from '@/domain-manager/hooks/useLastAuthenticatedWorkspaceDomain';
|
||||
import { useOrigin } from '@/domain-manager/hooks/useOrigin';
|
||||
import { useRedirect } from '@/domain-manager/hooks/useRedirect';
|
||||
import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain';
|
||||
import { domainConfigurationState } from '@/domain-manager/states/domainConfigurationState';
|
||||
import { useClearSseClient } from '@/sse-db-event/hooks/useClearSseClient';
|
||||
import { useLoadCurrentUser } from '@/users/hooks/useLoadCurrentUser';
|
||||
import { workspaceAuthProvidersState } from '@/workspace/states/workspaceAuthProvidersState';
|
||||
import { i18n } from '@lingui/core';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { SOURCE_LOCALE } from 'twenty-shared/translations';
|
||||
@@ -60,6 +78,8 @@ export const useAuth = () => {
|
||||
);
|
||||
|
||||
const { origin } = useOrigin();
|
||||
const { requestFreshCaptchaToken } = useRequestFreshCaptchaToken();
|
||||
const isCaptchaScriptLoaded = useAtomStateValue(isCaptchaScriptLoadedState);
|
||||
const isMultiWorkspaceEnabled = useAtomStateValue(
|
||||
isMultiWorkspaceEnabledState,
|
||||
);
|
||||
@@ -67,7 +87,9 @@ export const useAuth = () => {
|
||||
isEmailVerificationRequiredState,
|
||||
);
|
||||
const { loadCurrentUser } = useLoadCurrentUser();
|
||||
const { clearSseClient } = useClearSseClient();
|
||||
|
||||
const { applyMockedMetadata } = useLoadMockedMetadata();
|
||||
const { createWorkspace } = useSignUpInNewWorkspace();
|
||||
|
||||
const setSignInUpStep = useSetAtomState(signInUpStepState);
|
||||
@@ -99,17 +121,64 @@ export const useAuth = () => {
|
||||
CheckUserExistsDocument,
|
||||
);
|
||||
|
||||
const client = useApolloClient();
|
||||
|
||||
const [, setSearchParams] = useSearchParams();
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const clearSession = useCallback(() => {
|
||||
const clearSession = useCallback(async () => {
|
||||
clearSseClient();
|
||||
store.set(isAppEffectRedirectEnabledState.atom, false);
|
||||
|
||||
const mockedData = await preloadMockedMetadata();
|
||||
|
||||
const authProvidersValue = store.get(workspaceAuthProvidersState.atom);
|
||||
const domainConfigurationValue = store.get(domainConfigurationState.atom);
|
||||
const workspacePublicDataValue = store.get(workspacePublicDataState.atom);
|
||||
const lastAuthenticatedMethod = store.get(
|
||||
lastAuthenticatedMethodState.atom,
|
||||
);
|
||||
const isCaptchaScriptLoadedValue = store.get(
|
||||
isCaptchaScriptLoadedState.atom,
|
||||
);
|
||||
|
||||
sessionStorage.clear();
|
||||
clearSessionLocalStorageKeys();
|
||||
|
||||
store.set(workspaceAuthProvidersState.atom, authProvidersValue);
|
||||
store.set(workspacePublicDataState.atom, workspacePublicDataValue);
|
||||
store.set(domainConfigurationState.atom, domainConfigurationValue);
|
||||
store.set(isCaptchaScriptLoadedState.atom, isCaptchaScriptLoadedValue);
|
||||
store.set(lastAuthenticatedMethodState.atom, lastAuthenticatedMethod);
|
||||
|
||||
store.set(tokenPairState.atom, null);
|
||||
store.set(currentUserState.atom, null);
|
||||
store.set(currentWorkspaceState.atom, null);
|
||||
store.set(currentUserWorkspaceState.atom, null);
|
||||
store.set(currentWorkspaceMemberState.atom, null);
|
||||
store.set(currentWorkspaceMembersState.atom, []);
|
||||
store.set(availableWorkspacesState.atom, {
|
||||
availableWorkspacesForSignIn: [],
|
||||
availableWorkspacesForSignUp: [],
|
||||
});
|
||||
store.set(loginTokenState.atom, null);
|
||||
store.set(signInUpStepState.atom, SignInUpStep.Init);
|
||||
|
||||
applyMockedMetadata(mockedData);
|
||||
|
||||
await client.clearStore();
|
||||
setLastAuthenticateWorkspaceDomain(null);
|
||||
window.location.assign(AppPath.SignInUp);
|
||||
}, [store, setLastAuthenticateWorkspaceDomain]);
|
||||
navigate(AppPath.SignInUp);
|
||||
store.set(isAppEffectRedirectEnabledState.atom, true);
|
||||
}, [
|
||||
clearSseClient,
|
||||
client,
|
||||
setLastAuthenticateWorkspaceDomain,
|
||||
applyMockedMetadata,
|
||||
navigate,
|
||||
store,
|
||||
]);
|
||||
|
||||
const handleSetAuthTokens = useCallback(
|
||||
(tokens: AuthTokenPair) => {
|
||||
@@ -406,10 +475,11 @@ export const useAuth = () => {
|
||||
[handleGetLoginTokenFromCredentials, handleGetAuthTokensFromLoginToken],
|
||||
);
|
||||
|
||||
const handleSignOut = useCallback(() => {
|
||||
const handleSignOut = useCallback(async () => {
|
||||
broadcastSignOutToOtherTabs();
|
||||
clearSession();
|
||||
}, [clearSession]);
|
||||
await clearSession();
|
||||
if (isCaptchaScriptLoaded) await requestFreshCaptchaToken();
|
||||
}, [clearSession, isCaptchaScriptLoaded, requestFreshCaptchaToken]);
|
||||
|
||||
const handleCredentialsSignUpInWorkspace = useCallback(
|
||||
async ({
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import { useApolloClient } from '@apollo/client/react';
|
||||
import { useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useStore } from 'jotai';
|
||||
|
||||
import { isAppEffectRedirectEnabledState } from '@/app/states/isAppEffectRedirectEnabledState';
|
||||
import { useAuth } from '@/auth/hooks/useAuth';
|
||||
import { tokenPairState } from '@/auth/states/tokenPairState';
|
||||
import { useClearSseClient } from '@/sse-db-event/hooks/useClearSseClient';
|
||||
import { useLoadCurrentUser } from '@/users/hooks/useLoadCurrentUser';
|
||||
import { type AuthTokenPair } from '~/generated-metadata/graphql';
|
||||
|
||||
const IMPERSONATION_SESSION_KEY = 'impersonation_original_session';
|
||||
@@ -12,27 +17,22 @@ type StoredImpersonationSession = {
|
||||
returnPath: string;
|
||||
};
|
||||
|
||||
// Token swaps without a full reload would require enumerating every
|
||||
// user-scoped atom, localStorage entry, and Apollo cache key — brittle and
|
||||
// silently broken every time a new piece of user state is added. Instead,
|
||||
// set the cookie-backed token pair and let the browser re-bootstrap the app.
|
||||
const reloadWithSession = (returnPath: string) => {
|
||||
window.location.assign(returnPath);
|
||||
};
|
||||
|
||||
export const useImpersonationSession = () => {
|
||||
const store = useStore();
|
||||
const client = useApolloClient();
|
||||
const navigate = useNavigate();
|
||||
const { getAuthTokensFromLoginToken, signOut } = useAuth();
|
||||
const { clearSseClient } = useClearSseClient();
|
||||
const { loadCurrentUser } = useLoadCurrentUser();
|
||||
|
||||
const startImpersonating = useCallback(
|
||||
async (loginToken: string, returnPath?: string) => {
|
||||
const currentTokenPair = store.get(tokenPairState.atom);
|
||||
const targetPath = returnPath ?? window.location.pathname;
|
||||
|
||||
if (currentTokenPair) {
|
||||
const session: StoredImpersonationSession = {
|
||||
tokenPair: currentTokenPair,
|
||||
returnPath: targetPath,
|
||||
returnPath: returnPath ?? window.location.pathname,
|
||||
};
|
||||
sessionStorage.setItem(
|
||||
IMPERSONATION_SESSION_KEY,
|
||||
@@ -40,25 +40,30 @@ export const useImpersonationSession = () => {
|
||||
);
|
||||
}
|
||||
|
||||
clearSseClient();
|
||||
await client.clearStore();
|
||||
|
||||
store.set(isAppEffectRedirectEnabledState.atom, false);
|
||||
await getAuthTokensFromLoginToken(loginToken);
|
||||
reloadWithSession(targetPath);
|
||||
store.set(isAppEffectRedirectEnabledState.atom, true);
|
||||
},
|
||||
[store, getAuthTokensFromLoginToken],
|
||||
[store, client, clearSseClient, getAuthTokensFromLoginToken],
|
||||
);
|
||||
|
||||
const stopImpersonating = useCallback(async () => {
|
||||
const raw = sessionStorage.getItem(IMPERSONATION_SESSION_KEY);
|
||||
|
||||
if (!raw) {
|
||||
// Cross-workspace tab opened via redirect — no stored admin session
|
||||
// to restore. Close the tab; fall back to sign out if the browser
|
||||
// blocks window.close().
|
||||
// No stored session — likely a cross-workspace tab opened via redirect.
|
||||
// Try closing the tab (works when opened via window.open or target=_blank).
|
||||
window.close();
|
||||
// If window.close() was blocked by the browser, fall back to sign out.
|
||||
await signOut();
|
||||
return;
|
||||
}
|
||||
|
||||
let session: StoredImpersonationSession;
|
||||
|
||||
try {
|
||||
session = JSON.parse(raw);
|
||||
} catch {
|
||||
@@ -68,9 +73,19 @@ export const useImpersonationSession = () => {
|
||||
}
|
||||
|
||||
sessionStorage.removeItem(IMPERSONATION_SESSION_KEY);
|
||||
|
||||
clearSseClient();
|
||||
await client.clearStore();
|
||||
|
||||
store.set(isAppEffectRedirectEnabledState.atom, false);
|
||||
store.set(tokenPairState.atom, session.tokenPair);
|
||||
reloadWithSession(session.returnPath);
|
||||
}, [store, signOut]);
|
||||
|
||||
await loadCurrentUser();
|
||||
|
||||
store.set(isAppEffectRedirectEnabledState.atom, true);
|
||||
|
||||
navigate(session.returnPath);
|
||||
}, [store, client, clearSseClient, loadCurrentUser, signOut, navigate]);
|
||||
|
||||
const hasStoredSession = useCallback(() => {
|
||||
return sessionStorage.getItem(IMPERSONATION_SESSION_KEY) !== null;
|
||||
|
||||
-1
@@ -15,7 +15,6 @@ export const LOGIC_FUNCTION_FRAGMENT = gql`
|
||||
databaseEventTriggerSettings
|
||||
httpRouteTriggerSettings
|
||||
applicationId
|
||||
universalIdentifier
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
|
||||
-3
@@ -46,9 +46,6 @@ describe('useLogicFunctionUpdateFormState', () => {
|
||||
properties: {},
|
||||
type: 'object',
|
||||
},
|
||||
cronTriggerSettings: null,
|
||||
databaseEventTriggerSettings: null,
|
||||
httpRouteTriggerSettings: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
-16
@@ -1,11 +1,6 @@
|
||||
import { useGetOneLogicFunction } from '@/logic-functions/hooks/useGetOneLogicFunction';
|
||||
import { type Dispatch, type SetStateAction, useEffect, useState } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
type CronTriggerSettings,
|
||||
type DatabaseEventTriggerSettings,
|
||||
type HttpRouteTriggerSettings,
|
||||
} from 'twenty-shared/application';
|
||||
import { type LogicFunction } from '~/generated-metadata/graphql';
|
||||
import { useGetLogicFunctionSourceCode } from '@/logic-functions/hooks/useGetLogicFunctionSourceCode';
|
||||
import { DEFAULT_TOOL_INPUT_SCHEMA } from 'twenty-shared/logic-function';
|
||||
@@ -17,9 +12,6 @@ export type LogicFunctionFormValues = {
|
||||
timeoutSeconds: number;
|
||||
sourceHandlerCode: string;
|
||||
toolInputSchema?: object;
|
||||
cronTriggerSettings: CronTriggerSettings | null;
|
||||
databaseEventTriggerSettings: DatabaseEventTriggerSettings | null;
|
||||
httpRouteTriggerSettings: HttpRouteTriggerSettings | null;
|
||||
};
|
||||
|
||||
type SetLogicFunctionFormValues = Dispatch<
|
||||
@@ -43,9 +35,6 @@ export const useLogicFunctionUpdateFormState = ({
|
||||
sourceHandlerCode: '',
|
||||
timeoutSeconds: 300,
|
||||
toolInputSchema: DEFAULT_TOOL_INPUT_SCHEMA,
|
||||
cronTriggerSettings: null,
|
||||
databaseEventTriggerSettings: null,
|
||||
httpRouteTriggerSettings: null,
|
||||
});
|
||||
|
||||
const { sourceHandlerCode, loading: logicFunctionSourceCodeLoading } =
|
||||
@@ -68,11 +57,6 @@ export const useLogicFunctionUpdateFormState = ({
|
||||
timeoutSeconds: logicFunction.timeoutSeconds ?? 300,
|
||||
toolInputSchema:
|
||||
logicFunction.toolInputSchema || DEFAULT_TOOL_INPUT_SCHEMA,
|
||||
cronTriggerSettings: logicFunction.cronTriggerSettings ?? null,
|
||||
databaseEventTriggerSettings:
|
||||
logicFunction.databaseEventTriggerSettings ?? null,
|
||||
httpRouteTriggerSettings:
|
||||
logicFunction.httpRouteTriggerSettings ?? null,
|
||||
}));
|
||||
}
|
||||
}, [logicFunction]);
|
||||
|
||||
-55
@@ -1,55 +0,0 @@
|
||||
import { getLogicFunctionTriggerLabel } from '@/logic-functions/utils/getLogicFunctionTriggerLabel';
|
||||
|
||||
describe('getLogicFunctionTriggerLabel', () => {
|
||||
it('returns Post-install when the function matches the post-install identifier', () => {
|
||||
expect(
|
||||
getLogicFunctionTriggerLabel(
|
||||
{ universalIdentifier: 'uid-post' },
|
||||
{ postInstallUniversalIdentifier: 'uid-post' },
|
||||
),
|
||||
).toBe('Post-install');
|
||||
});
|
||||
|
||||
it('returns Pre-install when the function matches the pre-install identifier', () => {
|
||||
expect(
|
||||
getLogicFunctionTriggerLabel(
|
||||
{ universalIdentifier: 'uid-pre' },
|
||||
{ preInstallUniversalIdentifier: 'uid-pre' },
|
||||
),
|
||||
).toBe('Pre-install');
|
||||
});
|
||||
|
||||
it('does not match when both identifiers are undefined', () => {
|
||||
expect(getLogicFunctionTriggerLabel({}, {})).toBe('');
|
||||
});
|
||||
|
||||
it('returns AI tool when isTool is set', () => {
|
||||
expect(getLogicFunctionTriggerLabel({ isTool: true })).toBe('AI tool');
|
||||
});
|
||||
|
||||
it('returns Cron when cron settings are present', () => {
|
||||
expect(getLogicFunctionTriggerLabel({ cronTriggerSettings: {} })).toBe(
|
||||
'Cron',
|
||||
);
|
||||
});
|
||||
|
||||
it('returns HTTP when http settings are present', () => {
|
||||
expect(getLogicFunctionTriggerLabel({ httpRouteTriggerSettings: {} })).toBe(
|
||||
'HTTP',
|
||||
);
|
||||
});
|
||||
|
||||
it('returns the database event name when it exists', () => {
|
||||
expect(
|
||||
getLogicFunctionTriggerLabel({
|
||||
databaseEventTriggerSettings: { eventName: 'person.created' },
|
||||
}),
|
||||
).toBe('person.created');
|
||||
});
|
||||
|
||||
it('falls back to a generic label when the database event name is missing', () => {
|
||||
expect(
|
||||
getLogicFunctionTriggerLabel({ databaseEventTriggerSettings: {} }),
|
||||
).toBe('Database event');
|
||||
});
|
||||
});
|
||||
-38
@@ -1,38 +0,0 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type LogicFunctionLike = {
|
||||
universalIdentifier?: string | null;
|
||||
isTool?: boolean;
|
||||
cronTriggerSettings?: unknown;
|
||||
httpRouteTriggerSettings?: unknown;
|
||||
databaseEventTriggerSettings?: { eventName?: string } | null;
|
||||
};
|
||||
|
||||
export const getLogicFunctionTriggerLabel = (
|
||||
lf: LogicFunctionLike,
|
||||
options: {
|
||||
postInstallUniversalIdentifier?: string;
|
||||
preInstallUniversalIdentifier?: string;
|
||||
} = {},
|
||||
): string => {
|
||||
if (
|
||||
isDefined(lf.universalIdentifier) &&
|
||||
lf.universalIdentifier === options.postInstallUniversalIdentifier
|
||||
) {
|
||||
return t`Post-install`;
|
||||
}
|
||||
if (
|
||||
isDefined(lf.universalIdentifier) &&
|
||||
lf.universalIdentifier === options.preInstallUniversalIdentifier
|
||||
) {
|
||||
return t`Pre-install`;
|
||||
}
|
||||
if (lf.isTool) return t`AI tool`;
|
||||
if (lf.cronTriggerSettings) return t`Cron`;
|
||||
if (lf.httpRouteTriggerSettings) return t`HTTP`;
|
||||
if (lf.databaseEventTriggerSettings) {
|
||||
return lf.databaseEventTriggerSettings.eventName ?? t`Database event`;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`generateDepthRecordGqlFieldsFromObject should generate depth one record gql fields from object 1`] = `
|
||||
{
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`generateDepthRecordGqlFieldsFromRecord should generate depth one record gql fields from empty record 1`] = `
|
||||
{
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`useDeleteOneRecord A. Starting from empty cache 1. Should successfully delete record and update record cache entry 1`] = `
|
||||
{
|
||||
|
||||
+3
-11
@@ -1,7 +1,6 @@
|
||||
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
|
||||
import { useLoadRecordIndexStates } from '@/object-record/record-index/hooks/useLoadRecordIndexStates';
|
||||
import { lastLoadedRecordTableWidgetViewIdComponentState } from '@/object-record/record-table-widget/states/lastLoadedRecordTableWidgetViewIdComponentState';
|
||||
import { computeRecordTableWidgetViewLoadContentSignature } from '@/object-record/record-table-widget/utils/computeRecordTableWidgetViewLoadContentSignature';
|
||||
import { useIsPageLayoutInEditMode } from '@/page-layout/hooks/useIsPageLayoutInEditMode';
|
||||
import { recordTableWidgetViewDraftByWidgetIdComponentFamilySelector } from '@/page-layout/states/selectors/recordTableWidgetViewDraftByWidgetIdComponentFamilySelector';
|
||||
import { constructViewFromRecordTableWidgetViewSnapshot } from '@/page-layout/widgets/record-table/utils/constructViewFromRecordTableWidgetViewSnapshot';
|
||||
@@ -61,17 +60,11 @@ export const RecordTableWidgetViewLoadEffect = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const contentSignature =
|
||||
computeRecordTableWidgetViewLoadContentSignature(currentView);
|
||||
|
||||
const lastLoadedMatches =
|
||||
if (
|
||||
viewId === lastLoadedRecordTableWidgetViewId?.viewId &&
|
||||
objectMetadataItem.updatedAt ===
|
||||
lastLoadedRecordTableWidgetViewId?.objectMetadataItemUpdatedAt &&
|
||||
contentSignature ===
|
||||
lastLoadedRecordTableWidgetViewId?.loadedViewContentSignature;
|
||||
|
||||
if (lastLoadedMatches) {
|
||||
lastLoadedRecordTableWidgetViewId?.objectMetadataItemUpdatedAt
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -80,7 +73,6 @@ export const RecordTableWidgetViewLoadEffect = ({
|
||||
setLastLoadedRecordTableWidgetViewId({
|
||||
viewId,
|
||||
objectMetadataItemUpdatedAt: objectMetadataItem.updatedAt,
|
||||
loadedViewContentSignature: contentSignature,
|
||||
});
|
||||
}, [
|
||||
viewId,
|
||||
|
||||
-1
@@ -5,7 +5,6 @@ export const lastLoadedRecordTableWidgetViewIdComponentState =
|
||||
createAtomComponentState<{
|
||||
viewId: string;
|
||||
objectMetadataItemUpdatedAt: string;
|
||||
loadedViewContentSignature: string;
|
||||
} | null>({
|
||||
key: 'lastLoadedRecordTableWidgetViewIdComponentState',
|
||||
defaultValue: null,
|
||||
|
||||
-34
@@ -1,34 +0,0 @@
|
||||
import { type View } from '@/views/types/View';
|
||||
|
||||
export const computeRecordTableWidgetViewLoadContentSignature = (
|
||||
view: View,
|
||||
): string =>
|
||||
JSON.stringify({
|
||||
fields: [...view.viewFields]
|
||||
.sort((fieldA, fieldB) => fieldA.position - fieldB.position)
|
||||
.map((field) => ({
|
||||
id: field.id,
|
||||
fieldMetadataId: field.fieldMetadataId,
|
||||
isVisible: field.isVisible,
|
||||
position: field.position,
|
||||
})),
|
||||
filters: [...view.viewFilters]
|
||||
.sort((filterA, filterB) => filterA.id.localeCompare(filterB.id))
|
||||
.map((filter) => ({
|
||||
id: filter.id,
|
||||
fieldMetadataId: filter.fieldMetadataId,
|
||||
operand: filter.operand,
|
||||
value: filter.value,
|
||||
viewFilterGroupId: filter.viewFilterGroupId,
|
||||
})),
|
||||
filterGroups: (view.viewFilterGroups ?? []).map((group) => ({
|
||||
id: group.id,
|
||||
logicalOperator: group.logicalOperator,
|
||||
parentViewFilterGroupId: group.parentViewFilterGroupId,
|
||||
})),
|
||||
sorts: view.viewSorts.map((sort) => ({
|
||||
id: sort.id,
|
||||
fieldMetadataId: sort.fieldMetadataId,
|
||||
direction: sort.direction,
|
||||
})),
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
import { VIEW_FRAGMENT } from '@/views/graphql/fragments/viewFragment';
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const UPSERT_VIEW_WIDGET = gql`
|
||||
${VIEW_FRAGMENT}
|
||||
mutation UpsertViewWidget($input: UpsertViewWidgetInput!) {
|
||||
upsertViewWidget(input: $input) {
|
||||
...ViewFragment
|
||||
}
|
||||
}
|
||||
`;
|
||||
+29
-1
@@ -1,8 +1,10 @@
|
||||
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
|
||||
import { pageLayoutPersistedComponentState } from '@/page-layout/states/pageLayoutPersistedComponentState';
|
||||
import { recordTableWidgetViewDraftComponentState } from '@/page-layout/states/recordTableWidgetViewDraftComponentState';
|
||||
import { recordTableWidgetViewPersistedComponentState } from '@/page-layout/states/recordTableWidgetViewPersistedComponentState';
|
||||
import { getWidgetConfigurationViewId } from '@/page-layout/utils/getWidgetConfigurationViewId';
|
||||
import { usePerformViewAPIPersist } from '@/views/hooks/internal/usePerformViewAPIPersist';
|
||||
import { usePerformViewFieldAPIPersist } from '@/views/hooks/internal/usePerformViewFieldAPIPersist';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
@@ -11,6 +13,7 @@ import { WidgetType } from '~/generated-metadata/graphql';
|
||||
export const useCreatePendingRecordTableWidgetViews = () => {
|
||||
const { performViewAPICreate, performViewAPIDestroy } =
|
||||
usePerformViewAPIPersist();
|
||||
const { performViewFieldAPICreate } = usePerformViewFieldAPIPersist();
|
||||
const store = useStore();
|
||||
|
||||
const createPendingRecordTableWidgetViews = useCallback(
|
||||
@@ -98,6 +101,19 @@ export const useCreatePendingRecordTableWidgetViews = () => {
|
||||
`Failed to create view for RECORD_TABLE widget ${widget.id}`,
|
||||
);
|
||||
}
|
||||
|
||||
const viewFieldInputs = widgetViewDraft.viewFields.map((field) => ({
|
||||
id: field.id,
|
||||
viewId: field.viewId,
|
||||
fieldMetadataId: field.fieldMetadataId,
|
||||
position: field.position,
|
||||
size: field.size,
|
||||
isVisible: field.isVisible,
|
||||
}));
|
||||
|
||||
if (viewFieldInputs.length > 0) {
|
||||
await performViewFieldAPICreate({ inputs: viewFieldInputs });
|
||||
}
|
||||
}
|
||||
|
||||
for (const [widgetId, viewId] of persistedRecordTableWidgets) {
|
||||
@@ -105,8 +121,20 @@ export const useCreatePendingRecordTableWidgetViews = () => {
|
||||
await performViewAPIDestroy({ id: viewId });
|
||||
}
|
||||
}
|
||||
|
||||
store.set(
|
||||
recordTableWidgetViewPersistedComponentState.atomFamily({
|
||||
instanceId: pageLayoutId,
|
||||
}),
|
||||
recordTableWidgetViewDraft,
|
||||
);
|
||||
},
|
||||
[performViewAPICreate, performViewAPIDestroy, store],
|
||||
[
|
||||
performViewAPICreate,
|
||||
performViewAPIDestroy,
|
||||
performViewFieldAPICreate,
|
||||
store,
|
||||
],
|
||||
);
|
||||
|
||||
return { createPendingRecordTableWidgetViews };
|
||||
|
||||
-32
@@ -1,32 +0,0 @@
|
||||
import { recordTableWidgetViewDraftComponentState } from '@/page-layout/states/recordTableWidgetViewDraftComponentState';
|
||||
import { recordTableWidgetViewPersistedComponentState } from '@/page-layout/states/recordTableWidgetViewPersistedComponentState';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
|
||||
export const useHasRecordTableWidgetViewChanges = () => {
|
||||
const store = useStore();
|
||||
|
||||
const hasRecordTableWidgetViewChanges = useCallback(
|
||||
(pageLayoutId: string): boolean => {
|
||||
const recordTableWidgetViewDraft = store.get(
|
||||
recordTableWidgetViewDraftComponentState.atomFamily({
|
||||
instanceId: pageLayoutId,
|
||||
}),
|
||||
);
|
||||
const recordTableWidgetViewPersisted = store.get(
|
||||
recordTableWidgetViewPersistedComponentState.atomFamily({
|
||||
instanceId: pageLayoutId,
|
||||
}),
|
||||
);
|
||||
|
||||
return !isDeeplyEqual(
|
||||
recordTableWidgetViewDraft,
|
||||
recordTableWidgetViewPersisted,
|
||||
);
|
||||
},
|
||||
[store],
|
||||
);
|
||||
|
||||
return { hasRecordTableWidgetViewChanges };
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { WidgetConfigurationType } from '~/generated-metadata/graphql';
|
||||
|
||||
export const useHasRecordTableWidgets = () => {
|
||||
const store = useStore();
|
||||
|
||||
const hasRecordTableWidgets = useCallback(
|
||||
(pageLayoutId: string): boolean => {
|
||||
const pageLayoutDraft = store.get(
|
||||
pageLayoutDraftComponentState.atomFamily({
|
||||
instanceId: pageLayoutId,
|
||||
}),
|
||||
);
|
||||
|
||||
return pageLayoutDraft.tabs.some((tab) =>
|
||||
tab.widgets.some(
|
||||
(widget) =>
|
||||
widget.configuration.configurationType ===
|
||||
WidgetConfigurationType.RECORD_TABLE,
|
||||
),
|
||||
);
|
||||
},
|
||||
[store],
|
||||
);
|
||||
|
||||
return { hasRecordTableWidgets };
|
||||
};
|
||||
-23
@@ -9,8 +9,6 @@ import { fieldsWidgetUngroupedFieldsPersistedComponentState } from '@/page-layou
|
||||
import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
|
||||
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
|
||||
import { pageLayoutPersistedComponentState } from '@/page-layout/states/pageLayoutPersistedComponentState';
|
||||
import { recordTableWidgetViewDraftComponentState } from '@/page-layout/states/recordTableWidgetViewDraftComponentState';
|
||||
import { recordTableWidgetViewPersistedComponentState } from '@/page-layout/states/recordTableWidgetViewPersistedComponentState';
|
||||
import { convertPageLayoutToTabLayouts } from '@/page-layout/utils/convertPageLayoutToTabLayouts';
|
||||
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
@@ -85,17 +83,6 @@ export const useResetDraftPageLayoutToPersistedPageLayout = ({
|
||||
componentInstanceId,
|
||||
);
|
||||
|
||||
const recordTableWidgetViewDraftState = useAtomComponentStateCallbackState(
|
||||
recordTableWidgetViewDraftComponentState,
|
||||
componentInstanceId,
|
||||
);
|
||||
|
||||
const recordTableWidgetViewPersistedState =
|
||||
useAtomComponentStateCallbackState(
|
||||
recordTableWidgetViewPersistedComponentState,
|
||||
componentInstanceId,
|
||||
);
|
||||
|
||||
const resetDraftPageLayoutToPersistedPageLayout = useCallback(() => {
|
||||
const pageLayoutPersisted = store.get(pageLayoutPersistedState);
|
||||
|
||||
@@ -145,14 +132,6 @@ export const useResetDraftPageLayoutToPersistedPageLayout = ({
|
||||
fieldsWidgetEditorModeDraftState,
|
||||
fieldsWidgetEditorModePersisted,
|
||||
);
|
||||
|
||||
const recordTableWidgetViewPersisted = store.get(
|
||||
recordTableWidgetViewPersistedState,
|
||||
);
|
||||
store.set(
|
||||
recordTableWidgetViewDraftState,
|
||||
recordTableWidgetViewPersisted,
|
||||
);
|
||||
}
|
||||
}, [
|
||||
pageLayoutDraftState,
|
||||
@@ -164,8 +143,6 @@ export const useResetDraftPageLayoutToPersistedPageLayout = ({
|
||||
fieldsWidgetUngroupedFieldsPersistedState,
|
||||
fieldsWidgetEditorModeDraftState,
|
||||
fieldsWidgetEditorModePersistedState,
|
||||
recordTableWidgetViewDraftState,
|
||||
recordTableWidgetViewPersistedState,
|
||||
activeTabId,
|
||||
store,
|
||||
]);
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { useHasFieldsWidgetChanges } from '@/page-layout/hooks/useHasFieldsWidgetChanges';
|
||||
import { useHasRecordTableWidgetViewChanges } from '@/page-layout/hooks/useHasRecordTableWidgetViewChanges';
|
||||
import { useHasRecordTableWidgets } from '@/page-layout/hooks/useHasRecordTableWidgets';
|
||||
import { useSaveFieldsWidgetGroups } from '@/page-layout/hooks/useSaveFieldsWidgetGroups';
|
||||
import { useSaveRecordTableWidgetViews } from '@/page-layout/hooks/useSaveRecordTableWidgetViews';
|
||||
import { useSaveRecordTableWidgetsViewDataOnDashboardSave } from '@/page-layout/widgets/record-table/hooks/useSaveRecordTableWidgetsViewDataOnDashboardSave';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
export const useSavePageLayoutWidgetsData = () => {
|
||||
const { hasFieldsWidgetChanges } = useHasFieldsWidgetChanges();
|
||||
|
||||
const { hasRecordTableWidgetViewChanges } =
|
||||
useHasRecordTableWidgetViewChanges();
|
||||
const { hasRecordTableWidgets } = useHasRecordTableWidgets();
|
||||
|
||||
const { saveFieldsWidgetGroups } = useSaveFieldsWidgetGroups();
|
||||
|
||||
const { saveRecordTableWidgetViews } = useSaveRecordTableWidgetViews();
|
||||
const { saveRecordTableWidgetsViewDataOnDashboardSave } =
|
||||
useSaveRecordTableWidgetsViewDataOnDashboardSave();
|
||||
|
||||
const savePageLayoutWidgetsData = useCallback(
|
||||
async (pageLayoutId: string) => {
|
||||
@@ -20,15 +20,15 @@ export const useSavePageLayoutWidgetsData = () => {
|
||||
await saveFieldsWidgetGroups(pageLayoutId);
|
||||
}
|
||||
|
||||
if (hasRecordTableWidgetViewChanges(pageLayoutId)) {
|
||||
await saveRecordTableWidgetViews(pageLayoutId);
|
||||
if (hasRecordTableWidgets(pageLayoutId)) {
|
||||
await saveRecordTableWidgetsViewDataOnDashboardSave(pageLayoutId);
|
||||
}
|
||||
},
|
||||
[
|
||||
hasFieldsWidgetChanges,
|
||||
hasRecordTableWidgetViewChanges,
|
||||
saveFieldsWidgetGroups,
|
||||
saveRecordTableWidgetViews,
|
||||
saveRecordTableWidgetsViewDataOnDashboardSave,
|
||||
hasRecordTableWidgets,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
import { UPSERT_VIEW_WIDGET } from '@/page-layout/graphql/mutations/upsertViewWidget';
|
||||
import { useHasRecordTableWidgetViewChanges } from '@/page-layout/hooks/useHasRecordTableWidgetViewChanges';
|
||||
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
|
||||
import { recordTableWidgetViewDraftComponentState } from '@/page-layout/states/recordTableWidgetViewDraftComponentState';
|
||||
import { recordTableWidgetViewPersistedComponentState } from '@/page-layout/states/recordTableWidgetViewPersistedComponentState';
|
||||
import { getWidgetConfigurationViewId } from '@/page-layout/utils/getWidgetConfigurationViewId';
|
||||
import { useMutation } from '@apollo/client/react';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
type UpsertViewWidgetInput,
|
||||
type ViewFragmentFragment,
|
||||
WidgetType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
export const useSaveRecordTableWidgetViews = () => {
|
||||
const [upsertViewWidgetMutation] = useMutation<
|
||||
{ upsertViewWidget: ViewFragmentFragment },
|
||||
{ input: UpsertViewWidgetInput }
|
||||
>(UPSERT_VIEW_WIDGET);
|
||||
|
||||
const { hasRecordTableWidgetViewChanges } =
|
||||
useHasRecordTableWidgetViewChanges();
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const saveRecordTableWidgetViews = useCallback(
|
||||
async (pageLayoutId: string) => {
|
||||
if (!hasRecordTableWidgetViewChanges(pageLayoutId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const draft = store.get(
|
||||
pageLayoutDraftComponentState.atomFamily({
|
||||
instanceId: pageLayoutId,
|
||||
}),
|
||||
);
|
||||
|
||||
const recordTableWidgetViewDraft = store.get(
|
||||
recordTableWidgetViewDraftComponentState.atomFamily({
|
||||
instanceId: pageLayoutId,
|
||||
}),
|
||||
);
|
||||
|
||||
const draftRecordTableWidgets = draft.tabs
|
||||
.flatMap((tab) => tab.widgets)
|
||||
.filter((widget) => widget.type === WidgetType.RECORD_TABLE);
|
||||
|
||||
for (const widget of draftRecordTableWidgets) {
|
||||
const viewId = getWidgetConfigurationViewId(widget.configuration);
|
||||
|
||||
if (!isDefined(viewId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const widgetViewDraft = recordTableWidgetViewDraft[widget.id];
|
||||
|
||||
if (!isDefined(widgetViewDraft)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await upsertViewWidgetMutation({
|
||||
variables: {
|
||||
input: {
|
||||
widgetId: widget.id,
|
||||
viewFields: widgetViewDraft.viewFields.map((field) => ({
|
||||
...(isDefined(field.id) ? { viewFieldId: field.id } : {}),
|
||||
fieldMetadataId: field.fieldMetadataId,
|
||||
isVisible: field.isVisible,
|
||||
position: field.position,
|
||||
size: field.size,
|
||||
})),
|
||||
viewFilters: widgetViewDraft.viewFilters.map((filter) => ({
|
||||
id: filter.id,
|
||||
fieldMetadataId: filter.fieldMetadataId,
|
||||
operand: filter.operand,
|
||||
value: filter.value,
|
||||
viewFilterGroupId: filter.viewFilterGroupId ?? undefined,
|
||||
positionInViewFilterGroup:
|
||||
filter.positionInViewFilterGroup ?? undefined,
|
||||
subFieldName: filter.subFieldName ?? undefined,
|
||||
})),
|
||||
viewFilterGroups: widgetViewDraft.viewFilterGroups.map(
|
||||
(group) => ({
|
||||
id: group.id,
|
||||
parentViewFilterGroupId:
|
||||
group.parentViewFilterGroupId ?? undefined,
|
||||
logicalOperator: group.logicalOperator,
|
||||
positionInViewFilterGroup:
|
||||
group.positionInViewFilterGroup ?? undefined,
|
||||
}),
|
||||
),
|
||||
viewSorts: widgetViewDraft.viewSorts.map((sort) => ({
|
||||
id: sort.id,
|
||||
fieldMetadataId: sort.fieldMetadataId,
|
||||
direction: sort.direction,
|
||||
})),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
store.set(
|
||||
recordTableWidgetViewPersistedComponentState.atomFamily({
|
||||
instanceId: pageLayoutId,
|
||||
}),
|
||||
recordTableWidgetViewDraft,
|
||||
);
|
||||
},
|
||||
[hasRecordTableWidgetViewChanges, store, upsertViewWidgetMutation],
|
||||
);
|
||||
|
||||
return { saveRecordTableWidgetViews };
|
||||
};
|
||||
+1
-7
@@ -70,13 +70,7 @@ export const useAddDraftViewForRecordTableWidget = (pageLayoutId: string) => {
|
||||
|
||||
store.set(recordTableWidgetViewDraftState, (prev) => ({
|
||||
...prev,
|
||||
[widgetId]: {
|
||||
view: flatView,
|
||||
viewFields: flatViewFields,
|
||||
viewFilters: [],
|
||||
viewFilterGroups: [],
|
||||
viewSorts: [],
|
||||
},
|
||||
[widgetId]: { view: flatView, viewFields: flatViewFields },
|
||||
}));
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
|
||||
+1
-31
@@ -1,7 +1,4 @@
|
||||
import { type FlatViewField } from '@/metadata-store/types/FlatViewField';
|
||||
import { type FlatViewFilter } from '@/metadata-store/types/FlatViewFilter';
|
||||
import { type FlatViewFilterGroup } from '@/metadata-store/types/FlatViewFilterGroup';
|
||||
import { type FlatViewSort } from '@/metadata-store/types/FlatViewSort';
|
||||
import { recordTableWidgetViewDraftComponentState } from '@/page-layout/states/recordTableWidgetViewDraftComponentState';
|
||||
import { recordTableWidgetViewPersistedComponentState } from '@/page-layout/states/recordTableWidgetViewPersistedComponentState';
|
||||
import { type RecordTableWidgetViewSnapshot } from '@/page-layout/widgets/record-table/types/RecordTableWidgetViewSnapshot';
|
||||
@@ -41,43 +38,16 @@ export const useInitializeRecordTableWidgetViewDraft = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
viewFields,
|
||||
viewFilters,
|
||||
viewFilterGroups,
|
||||
viewSorts,
|
||||
viewGroups: _viewGroups,
|
||||
...viewProps
|
||||
} = view;
|
||||
const { viewFields, ...viewProps } = view;
|
||||
|
||||
const flatViewFields: FlatViewField[] = viewFields.map((field) => ({
|
||||
...field,
|
||||
viewId: view.id,
|
||||
}));
|
||||
|
||||
const flatViewFilters: FlatViewFilter[] = viewFilters.map((filter) => ({
|
||||
...filter,
|
||||
viewId: view.id,
|
||||
}));
|
||||
|
||||
const flatViewFilterGroups: FlatViewFilterGroup[] = (
|
||||
viewFilterGroups ?? []
|
||||
).map((group) => ({
|
||||
...group,
|
||||
viewId: view.id,
|
||||
}));
|
||||
|
||||
const flatViewSorts: FlatViewSort[] = viewSorts.map((sort) => ({
|
||||
...sort,
|
||||
viewId: view.id,
|
||||
}));
|
||||
|
||||
const snapshot: RecordTableWidgetViewSnapshot = {
|
||||
view: viewProps,
|
||||
viewFields: flatViewFields,
|
||||
viewFilters: flatViewFilters,
|
||||
viewFilterGroups: flatViewFilterGroups,
|
||||
viewSorts: flatViewSorts,
|
||||
};
|
||||
|
||||
store.set(recordTableWidgetViewDraftState, (prev) => ({
|
||||
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
import { type RecordField } from '@/object-record/record-field/types/RecordField';
|
||||
import { viewFieldAggregateOperationState } from '@/object-record/record-table/record-table-footer/states/viewFieldAggregateOperationState';
|
||||
import { convertExtendedAggregateOperationToAggregateOperation } from '@/object-record/utils/convertExtendedAggregateOperationToAggregateOperation';
|
||||
import { type ViewField } from '@/views/types/ViewField';
|
||||
import { mapRecordFieldToViewField } from '@/views/utils/mapRecordFieldToViewField';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useMapRecordFieldToViewFieldWithCurrentAggregateOperation = () => {
|
||||
const store = useStore();
|
||||
|
||||
const mapRecordFieldToViewFieldWithCurrentAggregateOperation = useCallback(
|
||||
(recordField: RecordField): Omit<ViewField, 'definition'> => {
|
||||
const viewField = mapRecordFieldToViewField(recordField);
|
||||
|
||||
const extendedAggregateOperation = store.get(
|
||||
viewFieldAggregateOperationState.atomFamily({
|
||||
viewFieldId: recordField.id,
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
...viewField,
|
||||
aggregateOperation: isDefined(extendedAggregateOperation)
|
||||
? convertExtendedAggregateOperationToAggregateOperation(
|
||||
extendedAggregateOperation,
|
||||
)
|
||||
: viewField.aggregateOperation,
|
||||
};
|
||||
},
|
||||
[store],
|
||||
);
|
||||
|
||||
return { mapRecordFieldToViewFieldWithCurrentAggregateOperation };
|
||||
};
|
||||
-81
@@ -1,81 +0,0 @@
|
||||
import { type RecordField } from '@/object-record/record-field/types/RecordField';
|
||||
import { recordTableWidgetViewDraftComponentState } from '@/page-layout/states/recordTableWidgetViewDraftComponentState';
|
||||
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
|
||||
import { useStore } from 'jotai';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type UseRecordTableWidgetFieldCallbacksParams = {
|
||||
pageLayoutId: string;
|
||||
widgetId: string;
|
||||
viewId: string;
|
||||
};
|
||||
|
||||
export const useRecordTableWidgetFieldCallbacks = ({
|
||||
pageLayoutId,
|
||||
widgetId,
|
||||
viewId,
|
||||
}: UseRecordTableWidgetFieldCallbacksParams) => {
|
||||
const recordTableWidgetViewDraftState = useAtomComponentStateCallbackState(
|
||||
recordTableWidgetViewDraftComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const handleFieldUpdated = (
|
||||
viewFieldId: string,
|
||||
update: Partial<{ position: number; isVisible: boolean }>,
|
||||
) => {
|
||||
store.set(recordTableWidgetViewDraftState, (prev) => {
|
||||
const widgetViewDraft = prev[widgetId];
|
||||
|
||||
if (!isDefined(widgetViewDraft)) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
return {
|
||||
...prev,
|
||||
[widgetId]: {
|
||||
...widgetViewDraft,
|
||||
viewFields: widgetViewDraft.viewFields.map((field) =>
|
||||
field.id === viewFieldId ||
|
||||
field.clientRecordFieldId === viewFieldId
|
||||
? { ...field, ...update }
|
||||
: field,
|
||||
),
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const handleFieldCreated = (recordField: RecordField) => {
|
||||
store.set(recordTableWidgetViewDraftState, (prev) => {
|
||||
const widgetViewDraft = prev[widgetId];
|
||||
|
||||
if (!isDefined(widgetViewDraft)) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
return {
|
||||
...prev,
|
||||
[widgetId]: {
|
||||
...widgetViewDraft,
|
||||
viewFields: [
|
||||
...widgetViewDraft.viewFields,
|
||||
{
|
||||
fieldMetadataId: recordField.fieldMetadataItemId,
|
||||
position: recordField.position,
|
||||
isVisible: recordField.isVisible,
|
||||
size: recordField.size,
|
||||
isActive: true,
|
||||
viewId,
|
||||
clientRecordFieldId: recordField.id,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
return { handleFieldUpdated, handleFieldCreated };
|
||||
};
|
||||
-77
@@ -1,77 +0,0 @@
|
||||
import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState';
|
||||
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
|
||||
import { recordTableWidgetViewDraftComponentState } from '@/page-layout/states/recordTableWidgetViewDraftComponentState';
|
||||
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
|
||||
import { mapRecordFilterGroupToViewFilterGroup } from '@/views/utils/mapRecordFilterGroupToViewFilterGroup';
|
||||
import { useStore } from 'jotai';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type UseRecordTableWidgetFilterCallbacksParams = {
|
||||
pageLayoutId: string;
|
||||
widgetId: string;
|
||||
viewId: string;
|
||||
recordIndexId: string;
|
||||
};
|
||||
|
||||
export const useRecordTableWidgetFilterCallbacks = ({
|
||||
pageLayoutId,
|
||||
widgetId,
|
||||
viewId,
|
||||
recordIndexId,
|
||||
}: UseRecordTableWidgetFilterCallbacksParams) => {
|
||||
const recordTableWidgetViewDraftState = useAtomComponentStateCallbackState(
|
||||
recordTableWidgetViewDraftComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const currentRecordFiltersState = useAtomComponentStateCallbackState(
|
||||
currentRecordFiltersComponentState,
|
||||
recordIndexId,
|
||||
);
|
||||
|
||||
const currentRecordFilterGroupsState = useAtomComponentStateCallbackState(
|
||||
currentRecordFilterGroupsComponentState,
|
||||
recordIndexId,
|
||||
);
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const handleFilterUpdate = () => {
|
||||
const currentRecordFilters = store.get(currentRecordFiltersState);
|
||||
const currentRecordFilterGroups = store.get(currentRecordFilterGroupsState);
|
||||
|
||||
store.set(recordTableWidgetViewDraftState, (prev) => {
|
||||
const widgetViewDraft = prev[widgetId];
|
||||
|
||||
if (!isDefined(widgetViewDraft)) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
return {
|
||||
...prev,
|
||||
[widgetId]: {
|
||||
...widgetViewDraft,
|
||||
viewFilters: currentRecordFilters.map((recordFilter) => ({
|
||||
id: recordFilter.id,
|
||||
fieldMetadataId: recordFilter.fieldMetadataId,
|
||||
operand: recordFilter.operand,
|
||||
value: recordFilter.value,
|
||||
viewId,
|
||||
viewFilterGroupId: recordFilter.recordFilterGroupId ?? null,
|
||||
positionInViewFilterGroup:
|
||||
recordFilter.positionInRecordFilterGroup ?? null,
|
||||
subFieldName: recordFilter.subFieldName ?? null,
|
||||
})),
|
||||
viewFilterGroups: currentRecordFilterGroups.map((recordFilterGroup) =>
|
||||
mapRecordFilterGroupToViewFilterGroup({
|
||||
recordFilterGroup,
|
||||
view: { id: viewId },
|
||||
}),
|
||||
),
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
return { handleFilterUpdate };
|
||||
};
|
||||
-58
@@ -1,58 +0,0 @@
|
||||
import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState';
|
||||
import { recordTableWidgetViewDraftComponentState } from '@/page-layout/states/recordTableWidgetViewDraftComponentState';
|
||||
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
|
||||
import { useStore } from 'jotai';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type UseRecordTableWidgetSortCallbacksParams = {
|
||||
pageLayoutId: string;
|
||||
widgetId: string;
|
||||
viewId: string;
|
||||
recordIndexId: string;
|
||||
};
|
||||
|
||||
export const useRecordTableWidgetSortCallbacks = ({
|
||||
pageLayoutId,
|
||||
widgetId,
|
||||
viewId,
|
||||
recordIndexId,
|
||||
}: UseRecordTableWidgetSortCallbacksParams) => {
|
||||
const recordTableWidgetViewDraftState = useAtomComponentStateCallbackState(
|
||||
recordTableWidgetViewDraftComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const currentRecordSortsState = useAtomComponentStateCallbackState(
|
||||
currentRecordSortsComponentState,
|
||||
recordIndexId,
|
||||
);
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const handleSortUpdate = () => {
|
||||
const currentRecordSorts = store.get(currentRecordSortsState);
|
||||
|
||||
store.set(recordTableWidgetViewDraftState, (prev) => {
|
||||
const widgetViewDraft = prev[widgetId];
|
||||
|
||||
if (!isDefined(widgetViewDraft)) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
return {
|
||||
...prev,
|
||||
[widgetId]: {
|
||||
...widgetViewDraft,
|
||||
viewSorts: currentRecordSorts.map((recordSort) => ({
|
||||
id: recordSort.id,
|
||||
fieldMetadataId: recordSort.fieldMetadataId,
|
||||
direction: recordSort.direction,
|
||||
viewId,
|
||||
})),
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
return { handleSortUpdate };
|
||||
};
|
||||
+1
-5
@@ -1,6 +1,5 @@
|
||||
import { recordTableWidgetViewDraftComponentState } from '@/page-layout/states/recordTableWidgetViewDraftComponentState';
|
||||
import { type RecordTableWidgetViewFieldItem } from '@/page-layout/widgets/record-table/types/RecordTableWidgetViewFieldItem';
|
||||
import { getRecordTableWidgetDraftViewFieldClientId } from '@/page-layout/widgets/record-table/utils/getRecordTableWidgetDraftViewFieldClientId';
|
||||
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
@@ -55,10 +54,7 @@ export const useReorderRecordTableWidgetFields = ({
|
||||
[widgetId]: {
|
||||
...widgetViewDraft,
|
||||
viewFields: widgetViewDraft.viewFields.map((field) => {
|
||||
const newPosition = updatedPositions.get(
|
||||
getRecordTableWidgetDraftViewFieldClientId(field),
|
||||
);
|
||||
|
||||
const newPosition = updatedPositions.get(field.id);
|
||||
return newPosition !== undefined
|
||||
? { ...field, position: newPosition }
|
||||
: field;
|
||||
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState';
|
||||
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
|
||||
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
|
||||
import { usePerformViewFilterAPIPersist } from '@/views/hooks/internal/usePerformViewFilterAPIPersist';
|
||||
import { usePerformViewFilterGroupAPIPersist } from '@/views/hooks/internal/usePerformViewFilterGroupAPIPersist';
|
||||
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
|
||||
import { getViewFilterGroupsToCreate } from '@/views/utils/getViewFilterGroupsToCreate';
|
||||
import { getViewFilterGroupsToDelete } from '@/views/utils/getViewFilterGroupsToDelete';
|
||||
import { getViewFilterGroupsToUpdate } from '@/views/utils/getViewFilterGroupsToUpdate';
|
||||
import { getViewFiltersToCreate } from '@/views/utils/getViewFiltersToCreate';
|
||||
import { getViewFiltersToDelete } from '@/views/utils/getViewFiltersToDelete';
|
||||
import { getViewFiltersToUpdate } from '@/views/utils/getViewFiltersToUpdate';
|
||||
import { mapRecordFilterGroupToViewFilterGroup } from '@/views/utils/mapRecordFilterGroupToViewFilterGroup';
|
||||
import { mapRecordFilterToViewFilter } from '@/views/utils/mapRecordFilterToViewFilter';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useSaveRecordTableWidgetFiltersToView = (
|
||||
viewId: string,
|
||||
instanceId: string,
|
||||
) => {
|
||||
const {
|
||||
performViewFilterAPICreate,
|
||||
performViewFilterAPIUpdate,
|
||||
performViewFilterAPIDelete,
|
||||
} = usePerformViewFilterAPIPersist();
|
||||
|
||||
const {
|
||||
performViewFilterGroupAPICreate,
|
||||
performViewFilterGroupAPIUpdate,
|
||||
performViewFilterGroupAPIDestroy,
|
||||
} = usePerformViewFilterGroupAPIPersist();
|
||||
|
||||
const currentRecordFiltersCallbackState = useAtomComponentStateCallbackState(
|
||||
currentRecordFiltersComponentState,
|
||||
instanceId,
|
||||
);
|
||||
|
||||
const currentRecordFilterGroupsCallbackState =
|
||||
useAtomComponentStateCallbackState(
|
||||
currentRecordFilterGroupsComponentState,
|
||||
instanceId,
|
||||
);
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const saveRecordTableWidgetFiltersToView = useCallback(async () => {
|
||||
const views = store.get(viewsSelector.atom);
|
||||
const currentView = views.find((view) => view.id === viewId);
|
||||
|
||||
if (!isDefined(currentView)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentRecordFilters = store.get(currentRecordFiltersCallbackState);
|
||||
const currentRecordFilterGroups = store.get(
|
||||
currentRecordFilterGroupsCallbackState,
|
||||
);
|
||||
|
||||
const newViewFilterGroups = currentRecordFilterGroups.map(
|
||||
(recordFilterGroup) =>
|
||||
mapRecordFilterGroupToViewFilterGroup({
|
||||
recordFilterGroup,
|
||||
view: currentView,
|
||||
}),
|
||||
);
|
||||
|
||||
const currentViewFilterGroups = currentView.viewFilterGroups ?? [];
|
||||
|
||||
const viewFilterGroupsToCreate = getViewFilterGroupsToCreate(
|
||||
currentViewFilterGroups,
|
||||
newViewFilterGroups,
|
||||
);
|
||||
const viewFilterGroupsToUpdate = getViewFilterGroupsToUpdate(
|
||||
currentViewFilterGroups,
|
||||
newViewFilterGroups,
|
||||
);
|
||||
const viewFilterGroupsToDelete = getViewFilterGroupsToDelete(
|
||||
currentViewFilterGroups,
|
||||
newViewFilterGroups,
|
||||
);
|
||||
|
||||
await performViewFilterGroupAPICreate(
|
||||
viewFilterGroupsToCreate,
|
||||
currentView,
|
||||
);
|
||||
await performViewFilterGroupAPIUpdate(viewFilterGroupsToUpdate);
|
||||
|
||||
const newViewFilters = currentRecordFilters.map(
|
||||
mapRecordFilterToViewFilter,
|
||||
);
|
||||
|
||||
const currentViewFilters = currentView.viewFilters ?? [];
|
||||
|
||||
const viewFiltersToCreate = getViewFiltersToCreate(
|
||||
currentViewFilters,
|
||||
newViewFilters,
|
||||
);
|
||||
const viewFiltersToUpdate = getViewFiltersToUpdate(
|
||||
currentViewFilters,
|
||||
newViewFilters,
|
||||
);
|
||||
const viewFiltersToDelete = getViewFiltersToDelete(
|
||||
currentViewFilters,
|
||||
newViewFilters,
|
||||
);
|
||||
|
||||
await performViewFilterAPIDelete(
|
||||
viewFiltersToDelete.map((viewFilter) => ({
|
||||
input: { id: viewFilter.id },
|
||||
})),
|
||||
);
|
||||
|
||||
await performViewFilterAPIUpdate(
|
||||
viewFiltersToUpdate.map((viewFilter) => ({
|
||||
input: {
|
||||
id: viewFilter.id,
|
||||
update: {
|
||||
value: viewFilter.value,
|
||||
operand: viewFilter.operand,
|
||||
positionInViewFilterGroup: viewFilter.positionInViewFilterGroup,
|
||||
viewFilterGroupId: viewFilter.viewFilterGroupId,
|
||||
subFieldName: viewFilter.subFieldName ?? null,
|
||||
},
|
||||
},
|
||||
})),
|
||||
);
|
||||
|
||||
await performViewFilterAPICreate(
|
||||
viewFiltersToCreate.map((viewFilter) => ({
|
||||
input: {
|
||||
id: viewFilter.id,
|
||||
fieldMetadataId: viewFilter.fieldMetadataId,
|
||||
viewId: currentView.id,
|
||||
value: viewFilter.value,
|
||||
operand: viewFilter.operand,
|
||||
viewFilterGroupId: viewFilter.viewFilterGroupId,
|
||||
positionInViewFilterGroup: viewFilter.positionInViewFilterGroup,
|
||||
subFieldName: viewFilter.subFieldName ?? null,
|
||||
},
|
||||
})),
|
||||
);
|
||||
|
||||
await performViewFilterGroupAPIDestroy(
|
||||
viewFilterGroupsToDelete.map((viewFilterGroup) => viewFilterGroup.id),
|
||||
);
|
||||
}, [
|
||||
store,
|
||||
viewId,
|
||||
currentRecordFiltersCallbackState,
|
||||
currentRecordFilterGroupsCallbackState,
|
||||
performViewFilterAPICreate,
|
||||
performViewFilterAPIUpdate,
|
||||
performViewFilterAPIDelete,
|
||||
performViewFilterGroupAPICreate,
|
||||
performViewFilterGroupAPIUpdate,
|
||||
performViewFilterGroupAPIDestroy,
|
||||
]);
|
||||
|
||||
return { saveRecordTableWidgetFiltersToView };
|
||||
};
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
import { type RecordSort } from '@/object-record/record-sort/types/RecordSort';
|
||||
import { usePerformViewSortAPIPersist } from '@/views/hooks/internal/usePerformViewSortAPIPersist';
|
||||
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
|
||||
import { getViewSortsToCreate } from '@/views/utils/getViewSortsToCreate';
|
||||
import { getViewSortsToDelete } from '@/views/utils/getViewSortsToDelete';
|
||||
import { getViewSortsToUpdate } from '@/views/utils/getViewSortsToUpdate';
|
||||
import { mapRecordSortToViewSort } from '@/views/utils/mapRecordSortToViewSort';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useSaveRecordTableWidgetSortsToView = (viewId: string) => {
|
||||
const {
|
||||
performViewSortAPICreate,
|
||||
performViewSortAPIUpdate,
|
||||
performViewSortAPIDelete,
|
||||
} = usePerformViewSortAPIPersist();
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const saveRecordTableWidgetSortsToView = useCallback(
|
||||
async (recordSorts: RecordSort[]) => {
|
||||
const views = store.get(viewsSelector.atom);
|
||||
const currentView = views.find((view) => view.id === viewId);
|
||||
|
||||
if (!isDefined(currentView)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentViewSorts = currentView.viewSorts ?? [];
|
||||
const newViewSorts = recordSorts.map(mapRecordSortToViewSort);
|
||||
|
||||
const viewSortsToCreate = getViewSortsToCreate(
|
||||
currentViewSorts,
|
||||
newViewSorts,
|
||||
);
|
||||
const viewSortsToUpdate = getViewSortsToUpdate(
|
||||
currentViewSorts,
|
||||
newViewSorts,
|
||||
);
|
||||
const viewSortsToDelete = getViewSortsToDelete(
|
||||
currentViewSorts,
|
||||
newViewSorts,
|
||||
);
|
||||
|
||||
await performViewSortAPICreate(
|
||||
viewSortsToCreate.map((viewSort) => ({
|
||||
input: {
|
||||
id: viewSort.id,
|
||||
fieldMetadataId: viewSort.fieldMetadataId,
|
||||
viewId: currentView.id,
|
||||
direction: viewSort.direction,
|
||||
},
|
||||
})),
|
||||
);
|
||||
|
||||
await performViewSortAPIUpdate(
|
||||
viewSortsToUpdate.map((viewSort) => ({
|
||||
input: {
|
||||
id: viewSort.id,
|
||||
update: {
|
||||
direction: viewSort.direction,
|
||||
},
|
||||
},
|
||||
})),
|
||||
);
|
||||
|
||||
await performViewSortAPIDelete(
|
||||
viewSortsToDelete.map((viewSort) => ({
|
||||
input: {
|
||||
id: viewSort.id,
|
||||
},
|
||||
})),
|
||||
);
|
||||
},
|
||||
[
|
||||
store,
|
||||
viewId,
|
||||
performViewSortAPICreate,
|
||||
performViewSortAPIUpdate,
|
||||
performViewSortAPIDelete,
|
||||
],
|
||||
);
|
||||
|
||||
return { saveRecordTableWidgetSortsToView };
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user