Merge branch 'main' into c--fix-block-editor-changed-content

This commit is contained in:
Weiko 2024-12-18 17:51:21 +01:00
commit 0a0be421d7
184 changed files with 3412 additions and 1307 deletions

59
.github/workflows/ci-demo-check.yml vendored Normal file
View File

@ -0,0 +1,59 @@
name: CI demo check
on:
schedule:
- cron: '30 7,19 * * *'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
timeout-minutes: 15
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: lts/*
- name: Install dependencies
uses: ./.github/workflows/actions/yarn-install
- name: Install Playwright Browsers
run: yarn playwright install --with-deps
- name: Run Playwright tests
id: test
run: yarn playwright test --grep @demo-only
- name: Upload report after tests
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: playwright-report/
retention-days: 90
- name: Send Discord notification
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@0.3.2
with:
args: 'Demo check ${{ steps.test.outcome }} - check ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
- name: Send email if demo is not working
if: steps.test.outcome == 'failure'
uses: dawidd6/action-send-mail@v3.12.0
with:
connection_url: ${{ secrets.MAIL_CONNECTION }}
server_address: smtp.gmail.com
server_port: 465
secure: true
username: ${{ secrets.MAIL_USERNAME }}
subject: 'Demo is not working'
from: 'Github CI Demo check'
to: ${{ secrets.RECIPIENTS }}
body: '<a href="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}">Link</a>'
priority: high

View File

@ -11,6 +11,7 @@ COPY ./packages/twenty-emails/package.json /app/packages/twenty-emails/
COPY ./packages/twenty-server/package.json /app/packages/twenty-server/ COPY ./packages/twenty-server/package.json /app/packages/twenty-server/
COPY ./packages/twenty-server/patches /app/packages/twenty-server/patches COPY ./packages/twenty-server/patches /app/packages/twenty-server/patches
COPY ./packages/twenty-ui/package.json /app/packages/twenty-ui/ COPY ./packages/twenty-ui/package.json /app/packages/twenty-ui/
COPY ./packages/twenty-shared/package.json /app/packages/twenty-shared/
COPY ./packages/twenty-front/package.json /app/packages/twenty-front/ COPY ./packages/twenty-front/package.json /app/packages/twenty-front/
# Install all dependencies # Install all dependencies
@ -22,6 +23,7 @@ FROM common-deps as twenty-server-build
# Copy sourcecode after installing dependences to accelerate subsequents builds # Copy sourcecode after installing dependences to accelerate subsequents builds
COPY ./packages/twenty-emails /app/packages/twenty-emails COPY ./packages/twenty-emails /app/packages/twenty-emails
COPY ./packages/twenty-shared /app/packages/twenty-shared
COPY ./packages/twenty-server /app/packages/twenty-server COPY ./packages/twenty-server /app/packages/twenty-server
RUN npx nx run twenty-server:build RUN npx nx run twenty-server:build
@ -31,7 +33,7 @@ RUN mv /app/packages/twenty-server/dist/package.json /app/packages/twenty-server
RUN rm -rf /app/packages/twenty-server/dist RUN rm -rf /app/packages/twenty-server/dist
RUN mv /app/packages/twenty-server/build /app/packages/twenty-server/dist RUN mv /app/packages/twenty-server/build /app/packages/twenty-server/dist
RUN yarn workspaces focus --production twenty-emails twenty-server RUN yarn workspaces focus --production twenty-emails twenty-shared twenty-server
# Build the front # Build the front
@ -41,6 +43,7 @@ ARG REACT_APP_SERVER_BASE_URL
COPY ./packages/twenty-front /app/packages/twenty-front COPY ./packages/twenty-front /app/packages/twenty-front
COPY ./packages/twenty-ui /app/packages/twenty-ui COPY ./packages/twenty-ui /app/packages/twenty-ui
COPY ./packages/twenty-shared /app/packages/twenty-shared
RUN npx nx build twenty-front RUN npx nx build twenty-front

View File

@ -34,12 +34,19 @@ export default defineConfig({
expect: { expect: {
timeout: 5000, timeout: 5000,
}, },
reporter: [['html', { open: 'never' }]], reporter: process.env.CI ? 'github' : 'list',
projects: [ projects: [
{ {
name: 'Login setup', name: 'Login setup',
testMatch: /login\.setup\.ts/, // finds all tests matching this regex, in this case only 1 test should be found testMatch: /login\.setup\.ts/, // finds all tests matching this regex, in this case only 1 test should be found
}, },
{
name: 'Demo check',
use: {
...devices['Desktop Chrome'],
},
testMatch: /demo\/demo_basic\.spec\.ts/,
},
{ {
name: 'chromium', name: 'chromium',
use: { use: {
@ -47,6 +54,7 @@ export default defineConfig({
storageState: path.resolve(__dirname, '.auth', 'user.json'), // takes saved cookies from directory storageState: path.resolve(__dirname, '.auth', 'user.json'), // takes saved cookies from directory
}, },
dependencies: ['Login setup'], // forces to run login setup before running tests from this project - CASE SENSITIVE dependencies: ['Login setup'], // forces to run login setup before running tests from this project - CASE SENSITIVE
testMatch: /all\/.+\.spec\.ts/,
}, },
{ {
name: 'firefox', name: 'firefox',
@ -55,6 +63,7 @@ export default defineConfig({
storageState: path.resolve(__dirname, '.auth', 'user.json'), storageState: path.resolve(__dirname, '.auth', 'user.json'),
}, },
dependencies: ['Login setup'], dependencies: ['Login setup'],
testMatch: /all\/.+\.spec\.ts/,
}, },
{ {
name: 'Authentication', name: 'Authentication',

View File

@ -1,4 +1,4 @@
import { test, expect } from '../lib/fixtures/screenshot'; import { test, expect } from '../../lib/fixtures/screenshot';
test.describe('Basic check', () => { test.describe('Basic check', () => {
test('Checking if table in Companies is visible', async ({ page }) => { test('Checking if table in Companies is visible', async ({ page }) => {

View File

@ -1,5 +1,5 @@
import { test, expect } from '@playwright/test'; import { test, expect } from '@playwright/test';
import { sh } from '../drivers/shell_driver'; import { sh } from '../../drivers/shell_driver';
test.describe('', () => { test.describe('', () => {
test('Testing logging', async ({ page }) => { test('Testing logging', async ({ page }) => {

View File

@ -0,0 +1,15 @@
import { test, expect } from '@playwright/test';
test('Check if demo account is working properly @demo-only', async ({
page,
}) => {
await page.goto('https://demo.twenty.com/');
await page.getByRole('button', { name: 'Continue With Email' }).click();
await page.getByRole('button', { name: 'Continue', exact: true }).click();
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page.getByText('Welcome to Twenty')).not.toBeVisible();
await page.waitForTimeout(5000);
await expect(page.getByText('Servers on a coffee break')).not.toBeVisible({
timeout: 5000,
});
});

View File

@ -8,7 +8,15 @@
"outputs": ["{options.outputPath}"], "outputs": ["{options.outputPath}"],
"options": { "options": {
"outputPath": "{projectRoot}/build" "outputPath": "{projectRoot}/build"
} },
"dependsOn": ["^build"]
},
"build:sourcemaps": {
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "{projectRoot}/build"
},
"dependsOn": ["^build"]
}, },
"serve": { "serve": {
"executor": "nx:run-commands", "executor": "nx:run-commands",

View File

@ -1333,6 +1333,7 @@ export type UserEdge = {
export type UserExists = { export type UserExists = {
__typename?: 'UserExists'; __typename?: 'UserExists';
availableWorkspaces: Array<AvailableWorkspaceOutput>; availableWorkspaces: Array<AvailableWorkspaceOutput>;
defaultWorkspaceId: Scalars['String'];
exists: Scalars['Boolean']; exists: Scalars['Boolean'];
}; };
@ -1927,7 +1928,7 @@ export type CheckUserExistsQueryVariables = Exact<{
}>; }>;
export type CheckUserExistsQuery = { __typename?: 'Query', checkUserExists: { __typename: 'UserExists', exists: boolean, availableWorkspaces: Array<{ __typename?: 'AvailableWorkspaceOutput', id: string, displayName?: string | null, subdomain: string, logo?: string | null, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }> } | { __typename: 'UserNotExists', exists: boolean } }; export type CheckUserExistsQuery = { __typename?: 'Query', checkUserExists: { __typename: 'UserExists', exists: boolean, defaultWorkspaceId: string, availableWorkspaces: Array<{ __typename?: 'AvailableWorkspaceOutput', id: string, displayName?: string | null, subdomain: string, logo?: string | null, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }> } | { __typename: 'UserNotExists', exists: boolean } };
export type GetPublicWorkspaceDataBySubdomainQueryVariables = Exact<{ [key: string]: never; }>; export type GetPublicWorkspaceDataBySubdomainQueryVariables = Exact<{ [key: string]: never; }>;
@ -3069,6 +3070,7 @@ export const CheckUserExistsDocument = gql`
__typename __typename
... on UserExists { ... on UserExists {
exists exists
defaultWorkspaceId
availableWorkspaces { availableWorkspaces {
id id
displayName displayName

View File

@ -1,9 +1,12 @@
import { MultipleRecordsActionMenuEntrySetterEffect } from '@/action-menu/actions/record-actions/multiple-records/components/MultipleRecordsActionMenuEntrySetterEffect'; import { MultipleRecordsActionMenuEntrySetterEffect } from '@/action-menu/actions/record-actions/multiple-records/components/MultipleRecordsActionMenuEntrySetterEffect';
import { NoSelectionActionMenuEntrySetterEffect } from '@/action-menu/actions/record-actions/no-selection/components/NoSelectionActionMenuEntrySetterEffect'; import { NoSelectionActionMenuEntrySetterEffect } from '@/action-menu/actions/record-actions/no-selection/components/NoSelectionActionMenuEntrySetterEffect';
import { ShowPageSingleRecordActionMenuEntrySetterEffect } from '@/action-menu/actions/record-actions/single-record/components/ShowPageSingleRecordActionMenuEntrySetterEffect';
import { SingleRecordActionMenuEntrySetterEffect } from '@/action-menu/actions/record-actions/single-record/components/SingleRecordActionMenuEntrySetterEffect'; import { SingleRecordActionMenuEntrySetterEffect } from '@/action-menu/actions/record-actions/single-record/components/SingleRecordActionMenuEntrySetterEffect';
import { WorkflowRunRecordActionMenuEntrySetterEffect } from '@/action-menu/actions/record-actions/workflow-run-record-actions/components/WorkflowRunRecordActionMenuEntrySetter'; import { WorkflowRunRecordActionMenuEntrySetterEffect } from '@/action-menu/actions/record-actions/workflow-run-record-actions/components/WorkflowRunRecordActionMenuEntrySetter';
import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState'; import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType';
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById'; import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
@ -46,6 +49,10 @@ const ActionEffects = ({
contextStoreTargetedRecordsRuleComponentState, contextStoreTargetedRecordsRuleComponentState,
); );
const contextStoreCurrentViewType = useRecoilComponentValueV2(
contextStoreCurrentViewTypeComponentState,
);
const isWorkflowEnabled = useIsFeatureEnabled('IS_WORKFLOW_ENABLED'); const isWorkflowEnabled = useIsFeatureEnabled('IS_WORKFLOW_ENABLED');
return ( return (
@ -59,9 +66,17 @@ const ActionEffects = ({
{contextStoreTargetedRecordsRule.mode === 'selection' && {contextStoreTargetedRecordsRule.mode === 'selection' &&
contextStoreTargetedRecordsRule.selectedRecordIds.length === 1 && ( contextStoreTargetedRecordsRule.selectedRecordIds.length === 1 && (
<> <>
{contextStoreCurrentViewType === ContextStoreViewType.ShowPage && (
<ShowPageSingleRecordActionMenuEntrySetterEffect
objectMetadataItem={objectMetadataItem}
/>
)}
{(contextStoreCurrentViewType === ContextStoreViewType.Table ||
contextStoreCurrentViewType === ContextStoreViewType.Kanban) && (
<SingleRecordActionMenuEntrySetterEffect <SingleRecordActionMenuEntrySetterEffect
objectMetadataItem={objectMetadataItem} objectMetadataItem={objectMetadataItem}
/> />
)}
{isWorkflowEnabled && ( {isWorkflowEnabled && (
<WorkflowRunRecordActionMenuEntrySetterEffect <WorkflowRunRecordActionMenuEntrySetterEffect
objectMetadataItem={objectMetadataItem} objectMetadataItem={objectMetadataItem}

View File

@ -0,0 +1,76 @@
import { getActionConfig } from '@/action-menu/actions/record-actions/single-record/utils/getActionConfig';
import { ActionAvailableOn } from '@/action-menu/actions/types/actionAvailableOn';
import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useEffect } from 'react';
import { isDefined } from 'twenty-ui';
export const ShowPageSingleRecordActionMenuEntrySetterEffect = ({
objectMetadataItem,
}: {
objectMetadataItem: ObjectMetadataItem;
}) => {
const isPageHeaderV2Enabled = useIsFeatureEnabled(
'IS_PAGE_HEADER_V2_ENABLED',
);
const actionConfig = getActionConfig(
objectMetadataItem,
isPageHeaderV2Enabled,
);
const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries();
const contextStoreTargetedRecordsRule = useRecoilComponentValueV2(
contextStoreTargetedRecordsRuleComponentState,
);
const selectedRecordId =
contextStoreTargetedRecordsRule.mode === 'selection'
? contextStoreTargetedRecordsRule.selectedRecordIds[0]
: undefined;
if (!isDefined(selectedRecordId)) {
throw new Error('Selected record ID is required');
}
const actionMenuEntries = Object.values(actionConfig ?? {})
.filter((action) =>
action.availableOn?.includes(ActionAvailableOn.SHOW_PAGE),
)
.map((action) => {
const { shouldBeRegistered, onClick, ConfirmationModal } =
action.actionHook({
recordId: selectedRecordId,
objectMetadataItem,
});
if (shouldBeRegistered) {
return {
...action,
onClick,
ConfirmationModal,
};
}
return undefined;
})
.filter(isDefined);
useEffect(() => {
for (const action of actionMenuEntries) {
addActionMenuEntry(action);
}
return () => {
for (const action of actionMenuEntries) {
removeActionMenuEntry(action.key);
}
};
}, [actionMenuEntries, addActionMenuEntry, removeActionMenuEntry]);
return null;
};

View File

@ -1,4 +1,5 @@
import { getActionConfig } from '@/action-menu/actions/record-actions/single-record/utils/getActionConfig'; import { getActionConfig } from '@/action-menu/actions/record-actions/single-record/utils/getActionConfig';
import { ActionAvailableOn } from '@/action-menu/actions/types/actionAvailableOn';
import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries'; import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
@ -37,6 +38,11 @@ export const SingleRecordActionMenuEntrySetterEffect = ({
} }
const actionMenuEntries = Object.values(actionConfig ?? {}) const actionMenuEntries = Object.values(actionConfig ?? {})
.filter((action) =>
action.availableOn?.includes(
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
),
)
.map((action) => { .map((action) => {
const { shouldBeRegistered, onClick, ConfirmationModal } = const { shouldBeRegistered, onClick, ConfirmationModal } =
action.actionHook({ action.actionHook({

View File

@ -1,6 +1,7 @@
import { useAddToFavoritesSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useAddToFavoritesSingleRecordAction'; import { useAddToFavoritesSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useAddToFavoritesSingleRecordAction';
import { useDeleteSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useDeleteSingleRecordAction'; import { useDeleteSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useDeleteSingleRecordAction';
import { useRemoveFromFavoritesSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useRemoveFromFavoritesSingleRecordAction'; import { useRemoveFromFavoritesSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useRemoveFromFavoritesSingleRecordAction';
import { ActionAvailableOn } from '@/action-menu/actions/types/actionAvailableOn';
import { SingleRecordActionHook } from '@/action-menu/actions/types/singleRecordActionHook'; import { SingleRecordActionHook } from '@/action-menu/actions/types/singleRecordActionHook';
import { import {
ActionMenuEntry, ActionMenuEntry,
@ -22,6 +23,10 @@ export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V1: Record<
label: 'Add to favorites', label: 'Add to favorites',
position: 0, position: 0,
Icon: IconHeart, Icon: IconHeart,
availableOn: [
ActionAvailableOn.SHOW_PAGE,
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
],
actionHook: useAddToFavoritesSingleRecordAction, actionHook: useAddToFavoritesSingleRecordAction,
}, },
removeFromFavoritesSingleRecord: { removeFromFavoritesSingleRecord: {
@ -31,6 +36,10 @@ export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V1: Record<
label: 'Remove from favorites', label: 'Remove from favorites',
position: 1, position: 1,
Icon: IconHeartOff, Icon: IconHeartOff,
availableOn: [
ActionAvailableOn.SHOW_PAGE,
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
],
actionHook: useRemoveFromFavoritesSingleRecordAction, actionHook: useRemoveFromFavoritesSingleRecordAction,
}, },
deleteSingleRecord: { deleteSingleRecord: {
@ -42,6 +51,10 @@ export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V1: Record<
Icon: IconTrash, Icon: IconTrash,
accent: 'danger', accent: 'danger',
isPinned: true, isPinned: true,
availableOn: [
ActionAvailableOn.SHOW_PAGE,
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
],
actionHook: useDeleteSingleRecordAction, actionHook: useDeleteSingleRecordAction,
}, },
}; };

View File

@ -1,13 +1,22 @@
import { useAddToFavoritesSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useAddToFavoritesSingleRecordAction'; import { useAddToFavoritesSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useAddToFavoritesSingleRecordAction';
import { useDeleteSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useDeleteSingleRecordAction'; import { useDeleteSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useDeleteSingleRecordAction';
import { useNavigateToNextRecordSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useNavigateToNextRecordSingleRecordAction';
import { useNavigateToPreviousRecordSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useNavigateToPreviousRecordSingleRecordAction';
import { useRemoveFromFavoritesSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useRemoveFromFavoritesSingleRecordAction'; import { useRemoveFromFavoritesSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useRemoveFromFavoritesSingleRecordAction';
import { ActionAvailableOn } from '@/action-menu/actions/types/actionAvailableOn';
import { SingleRecordActionHook } from '@/action-menu/actions/types/singleRecordActionHook'; import { SingleRecordActionHook } from '@/action-menu/actions/types/singleRecordActionHook';
import { import {
ActionMenuEntry, ActionMenuEntry,
ActionMenuEntryScope, ActionMenuEntryScope,
ActionMenuEntryType, ActionMenuEntryType,
} from '@/action-menu/types/ActionMenuEntry'; } from '@/action-menu/types/ActionMenuEntry';
import { IconHeart, IconHeartOff, IconTrash } from 'twenty-ui'; import {
IconChevronDown,
IconChevronUp,
IconHeart,
IconHeartOff,
IconTrash,
} from 'twenty-ui';
export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V2: Record< export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V2: Record<
string, string,
@ -20,9 +29,14 @@ export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V2: Record<
scope: ActionMenuEntryScope.RecordSelection, scope: ActionMenuEntryScope.RecordSelection,
key: 'add-to-favorites-single-record', key: 'add-to-favorites-single-record',
label: 'Add to favorites', label: 'Add to favorites',
shortLabel: 'Add to favorites',
position: 0, position: 0,
isPinned: true, isPinned: true,
Icon: IconHeart, Icon: IconHeart,
availableOn: [
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
ActionAvailableOn.SHOW_PAGE,
],
actionHook: useAddToFavoritesSingleRecordAction, actionHook: useAddToFavoritesSingleRecordAction,
}, },
removeFromFavoritesSingleRecord: { removeFromFavoritesSingleRecord: {
@ -30,20 +44,54 @@ export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V2: Record<
scope: ActionMenuEntryScope.RecordSelection, scope: ActionMenuEntryScope.RecordSelection,
key: 'remove-from-favorites-single-record', key: 'remove-from-favorites-single-record',
label: 'Remove from favorites', label: 'Remove from favorites',
shortLabel: 'Remove from favorites',
isPinned: true, isPinned: true,
position: 1, position: 1,
Icon: IconHeartOff, Icon: IconHeartOff,
availableOn: [
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
ActionAvailableOn.SHOW_PAGE,
],
actionHook: useRemoveFromFavoritesSingleRecordAction, actionHook: useRemoveFromFavoritesSingleRecordAction,
}, },
deleteSingleRecord: { deleteSingleRecord: {
type: ActionMenuEntryType.Standard, type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection, scope: ActionMenuEntryScope.RecordSelection,
key: 'delete-single-record', key: 'delete-single-record',
label: 'Delete', label: 'Delete record',
shortLabel: 'Delete',
position: 2, position: 2,
Icon: IconTrash, Icon: IconTrash,
accent: 'danger', accent: 'danger',
isPinned: true, isPinned: true,
availableOn: [
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
ActionAvailableOn.SHOW_PAGE,
],
actionHook: useDeleteSingleRecordAction, actionHook: useDeleteSingleRecordAction,
}, },
navigateToPreviousRecord: {
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
key: 'navigate-to-previous-record',
label: 'Navigate to previous record',
shortLabel: '',
position: 3,
isPinned: true,
Icon: IconChevronUp,
availableOn: [ActionAvailableOn.SHOW_PAGE],
actionHook: useNavigateToPreviousRecordSingleRecordAction,
},
navigateToNextRecord: {
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
key: 'navigate-to-next-record',
label: 'Navigate to next record',
shortLabel: '',
position: 4,
isPinned: true,
Icon: IconChevronDown,
availableOn: [ActionAvailableOn.SHOW_PAGE],
actionHook: useNavigateToNextRecordSingleRecordAction,
},
}; };

View File

@ -0,0 +1,15 @@
import { SingleRecordActionHookWithObjectMetadataItem } from '@/action-menu/actions/types/singleRecordActionHook';
import { useRecordShowPagePagination } from '@/object-record/record-show/hooks/useRecordShowPagePagination';
export const useNavigateToNextRecordSingleRecordAction: SingleRecordActionHookWithObjectMetadataItem =
({ recordId, objectMetadataItem }) => {
const { navigateToNextRecord } = useRecordShowPagePagination(
objectMetadataItem.nameSingular,
recordId,
);
return {
shouldBeRegistered: true,
onClick: navigateToNextRecord,
};
};

View File

@ -0,0 +1,15 @@
import { SingleRecordActionHookWithObjectMetadataItem } from '@/action-menu/actions/types/singleRecordActionHook';
import { useRecordShowPagePagination } from '@/object-record/record-show/hooks/useRecordShowPagePagination';
export const useNavigateToPreviousRecordSingleRecordAction: SingleRecordActionHookWithObjectMetadataItem =
({ recordId, objectMetadataItem }) => {
const { navigateToPreviousRecord } = useRecordShowPagePagination(
objectMetadataItem.nameSingular,
recordId,
);
return {
shouldBeRegistered: true,
onClick: navigateToPreviousRecord,
};
};

View File

@ -1,3 +1,5 @@
import { useNavigateToNextRecordSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useNavigateToNextRecordSingleRecordAction';
import { useNavigateToPreviousRecordSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useNavigateToPreviousRecordSingleRecordAction';
import { useActivateDraftWorkflowSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useActivateDraftWorkflowSingleRecordAction'; import { useActivateDraftWorkflowSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useActivateDraftWorkflowSingleRecordAction';
import { useActivateLastPublishedVersionWorkflowSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useActivateLastPublishedVersionWorkflowSingleRecordAction'; import { useActivateLastPublishedVersionWorkflowSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useActivateLastPublishedVersionWorkflowSingleRecordAction';
import { useDeactivateWorkflowSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useDeactivateWorkflowSingleRecordAction'; import { useDeactivateWorkflowSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useDeactivateWorkflowSingleRecordAction';
@ -6,6 +8,7 @@ import { useSeeActiveVersionWorkflowSingleRecordAction } from '@/action-menu/act
import { useSeeRunsWorkflowSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useSeeRunsWorkflowSingleRecordAction'; import { useSeeRunsWorkflowSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useSeeRunsWorkflowSingleRecordAction';
import { useSeeVersionsWorkflowSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useSeeVersionsWorkflowSingleRecordAction'; import { useSeeVersionsWorkflowSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useSeeVersionsWorkflowSingleRecordAction';
import { useTestWorkflowSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useTestWorkflowSingleRecordAction'; import { useTestWorkflowSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useTestWorkflowSingleRecordAction';
import { ActionAvailableOn } from '@/action-menu/actions/types/actionAvailableOn';
import { SingleRecordActionHook } from '@/action-menu/actions/types/singleRecordActionHook'; import { SingleRecordActionHook } from '@/action-menu/actions/types/singleRecordActionHook';
import { import {
ActionMenuEntry, ActionMenuEntry,
@ -13,6 +16,8 @@ import {
ActionMenuEntryType, ActionMenuEntryType,
} from '@/action-menu/types/ActionMenuEntry'; } from '@/action-menu/types/ActionMenuEntry';
import { import {
IconChevronDown,
IconChevronUp,
IconHistory, IconHistory,
IconHistoryToggle, IconHistoryToggle,
IconPlayerPause, IconPlayerPause,
@ -30,81 +35,143 @@ export const WORKFLOW_SINGLE_RECORD_ACTIONS_CONFIG: Record<
activateWorkflowDraftSingleRecord: { activateWorkflowDraftSingleRecord: {
key: 'activate-workflow-draft-single-record', key: 'activate-workflow-draft-single-record',
label: 'Activate Draft', label: 'Activate Draft',
shortLabel: 'Activate Draft',
isPinned: true, isPinned: true,
position: 1, position: 1,
Icon: IconPower, Icon: IconPower,
type: ActionMenuEntryType.Standard, type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection, scope: ActionMenuEntryScope.RecordSelection,
availableOn: [
ActionAvailableOn.SHOW_PAGE,
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
],
actionHook: useActivateDraftWorkflowSingleRecordAction, actionHook: useActivateDraftWorkflowSingleRecordAction,
}, },
activateWorkflowLastPublishedVersionSingleRecord: { activateWorkflowLastPublishedVersionSingleRecord: {
key: 'activate-workflow-last-published-version-single-record', key: 'activate-workflow-last-published-version-single-record',
label: 'Activate last published version', label: 'Activate last published version',
shortLabel: 'Activate last version',
isPinned: true, isPinned: true,
position: 2, position: 2,
Icon: IconPower, Icon: IconPower,
type: ActionMenuEntryType.Standard, type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection, scope: ActionMenuEntryScope.RecordSelection,
availableOn: [
ActionAvailableOn.SHOW_PAGE,
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
],
actionHook: useActivateLastPublishedVersionWorkflowSingleRecordAction, actionHook: useActivateLastPublishedVersionWorkflowSingleRecordAction,
}, },
deactivateWorkflowSingleRecord: { deactivateWorkflowSingleRecord: {
key: 'deactivate-workflow-single-record', key: 'deactivate-workflow-single-record',
label: 'Deactivate Workflow', label: 'Deactivate Workflow',
shortLabel: 'Deactivate',
isPinned: true, isPinned: true,
position: 3, position: 3,
Icon: IconPlayerPause, Icon: IconPlayerPause,
type: ActionMenuEntryType.Standard, type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection, scope: ActionMenuEntryScope.RecordSelection,
availableOn: [
ActionAvailableOn.SHOW_PAGE,
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
],
actionHook: useDeactivateWorkflowSingleRecordAction, actionHook: useDeactivateWorkflowSingleRecordAction,
}, },
discardWorkflowDraftSingleRecord: { discardWorkflowDraftSingleRecord: {
key: 'discard-workflow-draft-single-record', key: 'discard-workflow-draft-single-record',
label: 'Discard Draft', label: 'Discard Draft',
shortLabel: 'Discard Draft',
isPinned: true, isPinned: true,
position: 4, position: 4,
Icon: IconTrash, Icon: IconTrash,
type: ActionMenuEntryType.Standard, type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection, scope: ActionMenuEntryScope.RecordSelection,
availableOn: [
ActionAvailableOn.SHOW_PAGE,
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
],
actionHook: useDiscardDraftWorkflowSingleRecordAction, actionHook: useDiscardDraftWorkflowSingleRecordAction,
}, },
seeWorkflowActiveVersionSingleRecord: { seeWorkflowActiveVersionSingleRecord: {
key: 'see-workflow-active-version-single-record', key: 'see-workflow-active-version-single-record',
label: 'See active version', label: 'See active version',
shortLabel: 'See active version',
isPinned: false, isPinned: false,
position: 5, position: 5,
Icon: IconHistory, Icon: IconHistory,
type: ActionMenuEntryType.Standard, type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection, scope: ActionMenuEntryScope.RecordSelection,
availableOn: [
ActionAvailableOn.SHOW_PAGE,
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
],
actionHook: useSeeActiveVersionWorkflowSingleRecordAction, actionHook: useSeeActiveVersionWorkflowSingleRecordAction,
}, },
seeWorkflowRunsSingleRecord: { seeWorkflowRunsSingleRecord: {
key: 'see-workflow-runs-single-record', key: 'see-workflow-runs-single-record',
label: 'See runs', label: 'See runs',
shortLabel: 'See runs',
isPinned: false, isPinned: false,
position: 6, position: 6,
Icon: IconHistoryToggle, Icon: IconHistoryToggle,
type: ActionMenuEntryType.Standard, type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection, scope: ActionMenuEntryScope.RecordSelection,
availableOn: [
ActionAvailableOn.SHOW_PAGE,
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
],
actionHook: useSeeRunsWorkflowSingleRecordAction, actionHook: useSeeRunsWorkflowSingleRecordAction,
}, },
seeWorkflowVersionsHistorySingleRecord: { seeWorkflowVersionsHistorySingleRecord: {
key: 'see-workflow-versions-history-single-record', key: 'see-workflow-versions-history-single-record',
label: 'See versions history', label: 'See versions history',
shortLabel: 'See versions',
isPinned: false, isPinned: false,
position: 7, position: 7,
Icon: IconHistory, Icon: IconHistory,
type: ActionMenuEntryType.Standard, type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection, scope: ActionMenuEntryScope.RecordSelection,
availableOn: [
ActionAvailableOn.SHOW_PAGE,
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
],
actionHook: useSeeVersionsWorkflowSingleRecordAction, actionHook: useSeeVersionsWorkflowSingleRecordAction,
}, },
testWorkflowSingleRecord: { testWorkflowSingleRecord: {
key: 'test-workflow-single-record', key: 'test-workflow-single-record',
label: 'Test Workflow', label: 'Test Workflow',
shortLabel: 'Test',
isPinned: true, isPinned: true,
position: 8, position: 8,
Icon: IconPlayerPlay, Icon: IconPlayerPlay,
type: ActionMenuEntryType.Standard, type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection, scope: ActionMenuEntryScope.RecordSelection,
availableOn: [
ActionAvailableOn.SHOW_PAGE,
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
],
actionHook: useTestWorkflowSingleRecordAction, actionHook: useTestWorkflowSingleRecordAction,
}, },
navigateToPreviousRecord: {
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
key: 'navigate-to-previous-record',
label: 'Navigate to previous workflow',
shortLabel: '',
position: 9,
Icon: IconChevronUp,
availableOn: [ActionAvailableOn.SHOW_PAGE],
actionHook: useNavigateToPreviousRecordSingleRecordAction,
},
navigateToNextRecord: {
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
key: 'navigate-to-next-record',
label: 'Navigate to next workflow',
shortLabel: '',
position: 10,
Icon: IconChevronDown,
availableOn: [ActionAvailableOn.SHOW_PAGE],
actionHook: useNavigateToNextRecordSingleRecordAction,
},
}; };

View File

@ -1,13 +1,22 @@
import { useNavigateToNextRecordSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useNavigateToNextRecordSingleRecordAction';
import { useNavigateToPreviousRecordSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useNavigateToPreviousRecordSingleRecordAction';
import { useSeeExecutionsWorkflowVersionSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-version-actions/hooks/useSeeExecutionsWorkflowVersionSingleRecordAction'; import { useSeeExecutionsWorkflowVersionSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-version-actions/hooks/useSeeExecutionsWorkflowVersionSingleRecordAction';
import { useSeeVersionsWorkflowVersionSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-version-actions/hooks/useSeeVersionsWorkflowVersionSingleRecordAction'; import { useSeeVersionsWorkflowVersionSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-version-actions/hooks/useSeeVersionsWorkflowVersionSingleRecordAction';
import { useUseAsDraftWorkflowVersionSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-version-actions/hooks/useUseAsDraftWorkflowVersionSingleRecordAction'; import { useUseAsDraftWorkflowVersionSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-version-actions/hooks/useUseAsDraftWorkflowVersionSingleRecordAction';
import { ActionAvailableOn } from '@/action-menu/actions/types/actionAvailableOn';
import { SingleRecordActionHook } from '@/action-menu/actions/types/singleRecordActionHook'; import { SingleRecordActionHook } from '@/action-menu/actions/types/singleRecordActionHook';
import { import {
ActionMenuEntry, ActionMenuEntry,
ActionMenuEntryScope, ActionMenuEntryScope,
ActionMenuEntryType, ActionMenuEntryType,
} from '@/action-menu/types/ActionMenuEntry'; } from '@/action-menu/types/ActionMenuEntry';
import { IconHistory, IconHistoryToggle, IconPencil } from 'twenty-ui'; import {
IconChevronDown,
IconChevronUp,
IconHistory,
IconHistoryToggle,
IconPencil,
} from 'twenty-ui';
export const WORKFLOW_VERSIONS_SINGLE_RECORD_ACTIONS_CONFIG: Record< export const WORKFLOW_VERSIONS_SINGLE_RECORD_ACTIONS_CONFIG: Record<
string, string,
@ -23,6 +32,10 @@ export const WORKFLOW_VERSIONS_SINGLE_RECORD_ACTIONS_CONFIG: Record<
type: ActionMenuEntryType.Standard, type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection, scope: ActionMenuEntryScope.RecordSelection,
Icon: IconPencil, Icon: IconPencil,
availableOn: [
ActionAvailableOn.SHOW_PAGE,
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
],
actionHook: useUseAsDraftWorkflowVersionSingleRecordAction, actionHook: useUseAsDraftWorkflowVersionSingleRecordAction,
}, },
seeWorkflowExecutionsSingleRecord: { seeWorkflowExecutionsSingleRecord: {
@ -32,6 +45,10 @@ export const WORKFLOW_VERSIONS_SINGLE_RECORD_ACTIONS_CONFIG: Record<
type: ActionMenuEntryType.Standard, type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection, scope: ActionMenuEntryScope.RecordSelection,
Icon: IconHistoryToggle, Icon: IconHistoryToggle,
availableOn: [
ActionAvailableOn.SHOW_PAGE,
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
],
actionHook: useSeeExecutionsWorkflowVersionSingleRecordAction, actionHook: useSeeExecutionsWorkflowVersionSingleRecordAction,
}, },
seeWorkflowVersionsHistorySingleRecord: { seeWorkflowVersionsHistorySingleRecord: {
@ -41,6 +58,32 @@ export const WORKFLOW_VERSIONS_SINGLE_RECORD_ACTIONS_CONFIG: Record<
type: ActionMenuEntryType.Standard, type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection, scope: ActionMenuEntryScope.RecordSelection,
Icon: IconHistory, Icon: IconHistory,
availableOn: [
ActionAvailableOn.SHOW_PAGE,
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
],
actionHook: useSeeVersionsWorkflowVersionSingleRecordAction, actionHook: useSeeVersionsWorkflowVersionSingleRecordAction,
}, },
navigateToPreviousRecord: {
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
key: 'navigate-to-previous-record',
label: 'Navigate to previous version',
shortLabel: '',
position: 9,
Icon: IconChevronUp,
availableOn: [ActionAvailableOn.SHOW_PAGE],
actionHook: useNavigateToPreviousRecordSingleRecordAction,
},
navigateToNextRecord: {
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
key: 'navigate-to-next-record',
label: 'Navigate to next version',
shortLabel: '',
position: 10,
Icon: IconChevronDown,
availableOn: [ActionAvailableOn.SHOW_PAGE],
actionHook: useNavigateToNextRecordSingleRecordAction,
},
}; };

View File

@ -0,0 +1,6 @@
export enum ActionAvailableOn {
INDEX_PAGE_BULK_SELECTION = 'INDEX_PAGE_BULK_SELECTION',
INDEX_PAGE_SINGLE_RECORD_SELECTION = 'INDEX_PAGE_SINGLE_RECORD_SELECTION',
INDEX_PAGE_NO_SELECTION = 'INDEX_PAGE_NO_SELECTION',
SHOW_PAGE = 'SHOW_PAGE',
}

View File

@ -1,7 +1,7 @@
import { actionMenuEntriesComponentSelector } from '@/action-menu/states/actionMenuEntriesComponentSelector'; import { actionMenuEntriesComponentSelector } from '@/action-menu/states/actionMenuEntriesComponentSelector';
import { PageHeaderOpenCommandMenuButton } from '@/ui/layout/page-header/components/PageHeaderOpenCommandMenuButton'; import { PageHeaderOpenCommandMenuButton } from '@/ui/layout/page-header/components/PageHeaderOpenCommandMenuButton';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { Button, useIsMobile } from 'twenty-ui'; import { Button, IconButton, useIsMobile } from 'twenty-ui';
export const RecordShowActionMenuButtons = () => { export const RecordShowActionMenuButtons = () => {
const actionMenuEntries = useRecoilComponentValueV2( const actionMenuEntries = useRecoilComponentValueV2(
@ -15,18 +15,29 @@ export const RecordShowActionMenuButtons = () => {
return ( return (
<> <>
{!isMobile && {!isMobile &&
pinnedEntries.map((entry, index) => ( pinnedEntries.map((entry, index) =>
entry.shortLabel ? (
<Button <Button
key={index} key={index}
Icon={entry.Icon} Icon={entry.Icon}
size="small" size="small"
variant="secondary" variant="secondary"
accent="default" accent="default"
title={entry.label} title={entry.shortLabel}
onClick={() => entry.onClick?.()} onClick={() => entry.onClick?.()}
ariaLabel={entry.label} ariaLabel={entry.label}
/> />
))} ) : (
<IconButton
Icon={entry.Icon}
size="small"
variant="secondary"
accent="default"
onClick={() => entry.onClick?.()}
ariaLabel={entry.label}
/>
),
)}
<PageHeaderOpenCommandMenuButton key="more" /> <PageHeaderOpenCommandMenuButton key="more" />
</> </>
); );

View File

@ -1,3 +1,4 @@
import { ActionAvailableOn } from '@/action-menu/actions/types/actionAvailableOn';
import { MouseEvent, ReactElement } from 'react'; import { MouseEvent, ReactElement } from 'react';
import { IconComponent, MenuItemAccent } from 'twenty-ui'; import { IconComponent, MenuItemAccent } from 'twenty-ui';
@ -16,10 +17,12 @@ export type ActionMenuEntry = {
scope: ActionMenuEntryScope; scope: ActionMenuEntryScope;
key: string; key: string;
label: string; label: string;
shortLabel?: string;
position: number; position: number;
Icon: IconComponent; Icon: IconComponent;
isPinned?: boolean; isPinned?: boolean;
accent?: MenuItemAccent; accent?: MenuItemAccent;
availableOn?: ActionAvailableOn[];
onClick?: (event?: MouseEvent<HTMLElement>) => void; onClick?: (event?: MouseEvent<HTMLElement>) => void;
ConfirmationModal?: ReactElement; ConfirmationModal?: ReactElement;
}; };

View File

@ -1,4 +1,3 @@
import styled from '@emotion/styled';
import { isNull } from '@sniptt/guards'; import { isNull } from '@sniptt/guards';
import { useRecoilCallback, useRecoilState, useSetRecoilState } from 'recoil'; import { useRecoilCallback, useRecoilState, useSetRecoilState } from 'recoil';
import { v4 } from 'uuid'; import { v4 } from 'uuid';
@ -32,12 +31,6 @@ import { MultiRecordSelect } from '@/object-record/relation-picker/components/Mu
import { RecordPickerComponentInstanceContext } from '@/object-record/relation-picker/states/contexts/RecordPickerComponentInstanceContext'; import { RecordPickerComponentInstanceContext } from '@/object-record/relation-picker/states/contexts/RecordPickerComponentInstanceContext';
import { prefillRecord } from '@/object-record/utils/prefillRecord'; import { prefillRecord } from '@/object-record/utils/prefillRecord';
const StyledSelectContainer = styled.div`
position: absolute;
left: 0;
top: 0;
`;
type ActivityTargetInlineCellEditModeProps = { type ActivityTargetInlineCellEditModeProps = {
activity: Task | Note; activity: Task | Note;
activityTargetWithTargetRecords: ActivityTargetWithTargetRecord[]; activityTargetWithTargetRecords: ActivityTargetWithTargetRecord[];
@ -282,7 +275,7 @@ export const ActivityTargetInlineCellEditMode = ({
); );
return ( return (
<StyledSelectContainer> <>
<RecordPickerComponentInstanceContext.Provider <RecordPickerComponentInstanceContext.Provider
value={{ instanceId: recordPickerInstanceId }} value={{ instanceId: recordPickerInstanceId }}
> >
@ -295,6 +288,6 @@ export const ActivityTargetInlineCellEditMode = ({
<ActivityTargetInlineCellEditModeMultiRecordsSearchFilterEffect /> <ActivityTargetInlineCellEditModeMultiRecordsSearchFilterEffect />
<MultiRecordSelect onSubmit={handleSubmit} onChange={handleChange} /> <MultiRecordSelect onSubmit={handleSubmit} onChange={handleChange} />
</RecordPickerComponentInstanceContext.Provider> </RecordPickerComponentInstanceContext.Provider>
</StyledSelectContainer> </>
); );
}; };

View File

@ -1,4 +1,5 @@
import { Modal } from '@/ui/layout/modal/components/Modal'; import { Modal } from '@/ui/layout/modal/components/Modal';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import React from 'react'; import React from 'react';
@ -11,6 +12,11 @@ type AuthModalProps = { children: React.ReactNode };
export const AuthModal = ({ children }: AuthModalProps) => ( export const AuthModal = ({ children }: AuthModalProps) => (
<Modal padding={'none'} modalVariant="primary"> <Modal padding={'none'} modalVariant="primary">
<ScrollWrapper
contextProviderName="modalContent"
componentInstanceId="scroll-wrapper-modal-content"
>
<StyledContent>{children}</StyledContent> <StyledContent>{children}</StyledContent>
</ScrollWrapper>
</Modal> </Modal>
); );

View File

@ -6,10 +6,16 @@ import { useIsLogged } from '@/auth/hooks/useIsLogged';
import { isAppWaitingForFreshObjectMetadataState } from '@/object-metadata/states/isAppWaitingForFreshObjectMetadataState'; import { isAppWaitingForFreshObjectMetadataState } from '@/object-metadata/states/isAppWaitingForFreshObjectMetadataState';
import { AppPath } from '@/types/AppPath'; import { AppPath } from '@/types/AppPath';
import { useSetRecoilState } from 'recoil'; import { useSetRecoilState } from 'recoil';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { isDefined } from 'twenty-ui';
export const VerifyEffect = () => { export const VerifyEffect = () => {
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const loginToken = searchParams.get('loginToken'); const loginToken = searchParams.get('loginToken');
const errorMessage = searchParams.get('errorMessage');
const { enqueueSnackBar } = useSnackBar();
const isLogged = useIsLogged(); const isLogged = useIsLogged();
const navigate = useNavigate(); const navigate = useNavigate();
@ -22,6 +28,11 @@ export const VerifyEffect = () => {
useEffect(() => { useEffect(() => {
const getTokens = async () => { const getTokens = async () => {
if (isDefined(errorMessage)) {
enqueueSnackBar(errorMessage, {
variant: SnackBarVariant.Error,
});
}
if (!loginToken) { if (!loginToken) {
navigate(AppPath.SignInUp); navigate(AppPath.SignInUp);
} else { } else {

View File

@ -6,6 +6,7 @@ export const CHECK_USER_EXISTS = gql`
__typename __typename
... on UserExists { ... on UserExists {
exists exists
defaultWorkspaceId
availableWorkspaces { availableWorkspaces {
id id
displayName displayName

View File

@ -136,8 +136,10 @@ export const useAuth = () => {
await client.clearStore(); await client.clearStore();
sessionStorage.clear(); sessionStorage.clear();
localStorage.clear(); localStorage.clear();
// We need to explicitly clear the state to trigger the cookie deletion which include the parent domain
setLastAuthenticateWorkspaceDomain(null);
}, },
[client, goToRecoilSnapshot], [client, goToRecoilSnapshot, setLastAuthenticateWorkspaceDomain],
); );
const handleChallenge = useCallback( const handleChallenge = useCallback(

View File

@ -83,21 +83,19 @@ export const SignInUpGlobalScopeForm = () => {
}, },
onCompleted: (data) => { onCompleted: (data) => {
requestFreshCaptchaToken(); requestFreshCaptchaToken();
if (data.checkUserExists.__typename === 'UserExists') { const response = data.checkUserExists;
if ( if (response.__typename === 'UserExists') {
isDefined(data?.checkUserExists.availableWorkspaces) && if (response.availableWorkspaces.length >= 1) {
data.checkUserExists.availableWorkspaces.length >= 1 const workspace =
) { response.availableWorkspaces.find(
return redirectToWorkspaceDomain( (workspace) => workspace.id === response.defaultWorkspaceId,
data?.checkUserExists.availableWorkspaces[0].subdomain, ) ?? response.availableWorkspaces[0];
pathname, return redirectToWorkspaceDomain(workspace.subdomain, pathname, {
{
email: form.getValues('email'), email: form.getValues('email'),
}, });
);
} }
} }
if (data.checkUserExists.__typename === 'UserNotExists') { if (response.__typename === 'UserNotExists') {
setSignInUpMode(SignInUpMode.SignUp); setSignInUpMode(SignInUpMode.SignUp);
setSignInUpStep(SignInUpStep.Password); setSignInUpStep(SignInUpStep.Password);
} }

View File

@ -11,6 +11,7 @@ import { isDefined } from '~/utils/isDefined';
import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState'; import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState'; import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState';
import { contextStoreFiltersComponentState } from '@/context-store/states/contextStoreFiltersComponentState'; import { contextStoreFiltersComponentState } from '@/context-store/states/contextStoreFiltersComponentState';
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState'; import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
@ -108,6 +109,21 @@ export const useCommandMenu = () => {
}), }),
contextStoreCurrentViewId, contextStoreCurrentViewId,
); );
const contextStoreCurrentViewType = snapshot
.getLoadable(
contextStoreCurrentViewTypeComponentState.atomFamily({
instanceId: mainContextStoreComponentInstanceId,
}),
)
.getValue();
set(
contextStoreCurrentViewTypeComponentState.atomFamily({
instanceId: 'command-menu',
}),
contextStoreCurrentViewType,
);
} }
setIsCommandMenuOpened(true); setIsCommandMenuOpened(true);
@ -165,6 +181,13 @@ export const useCommandMenu = () => {
null, null,
); );
set(
contextStoreCurrentViewTypeComponentState.atomFamily({
instanceId: 'command-menu',
}),
null,
);
if (isCommandMenuOpened) { if (isCommandMenuOpened) {
setIsCommandMenuOpened(false); setIsCommandMenuOpened(false);
resetSelectedItem(); resetSelectedItem();

View File

@ -0,0 +1,24 @@
import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState';
import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { useEffect } from 'react';
export const ContextStoreCurrentViewTypeEffect = ({
viewType,
}: {
viewType: ContextStoreViewType | null;
}) => {
const setContextStoreCurrentViewType = useSetRecoilComponentStateV2(
contextStoreCurrentViewTypeComponentState,
);
useEffect(() => {
setContextStoreCurrentViewType(viewType);
return () => {
setContextStoreCurrentViewType(null);
};
}, [setContextStoreCurrentViewType, viewType]);
return null;
};

View File

@ -0,0 +1,10 @@
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType';
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
export const contextStoreCurrentViewTypeComponentState =
createComponentStateV2<ContextStoreViewType | null>({
key: 'contextStoreCurrentViewTypeComponentState',
defaultValue: null,
componentInstanceContext: ContextStoreComponentInstanceContext,
});

View File

@ -0,0 +1,5 @@
export enum ContextStoreViewType {
Table = 'table',
Kanban = 'kanban',
ShowPage = 'show-page',
}

View File

@ -17,6 +17,10 @@ const StyledEmptyContainer = styled.div`
width: 100%; width: 100%;
`; `;
const StyledOrphanFavoritesContainer = styled.div`
margin-bottom: ${({ theme }) => theme.betweenSiblingsGap};
`;
export const CurrentWorkspaceMemberOrphanFavorites = () => { export const CurrentWorkspaceMemberOrphanFavorites = () => {
const { sortedFavorites: favorites } = useFavorites(); const { sortedFavorites: favorites } = useFavorites();
const { deleteFavorite } = useDeleteFavorite(); const { deleteFavorite } = useDeleteFavorite();
@ -38,6 +42,7 @@ export const CurrentWorkspaceMemberOrphanFavorites = () => {
index={index} index={index}
isInsideScrollableContainer={true} isInsideScrollableContainer={true}
itemComponent={ itemComponent={
<StyledOrphanFavoritesContainer>
<NavigationDrawerItem <NavigationDrawerItem
label={favorite.labelIdentifier} label={favorite.labelIdentifier}
Icon={() => <FavoriteIcon favorite={favorite} />} Icon={() => <FavoriteIcon favorite={favorite} />}
@ -56,6 +61,7 @@ export const CurrentWorkspaceMemberOrphanFavorites = () => {
} }
isDragging={isDragging} isDragging={isDragging}
/> />
</StyledOrphanFavoritesContainer>
} }
/> />
)) ))

View File

@ -137,7 +137,6 @@ export const AdvancedFilterAddFilterRuleSelect = ({
return ( return (
<Dropdown <Dropdown
disableBlur
dropdownId={dropdownId} dropdownId={dropdownId}
clickableComponent={ clickableComponent={
<LightButton Icon={IconPlus} title="Add filter rule" /> <LightButton Icon={IconPlus} title="Add filter rule" />

View File

@ -22,7 +22,6 @@ export const AdvancedFilterLogicalOperatorDropdown = ({
return ( return (
<Select <Select
disableBlur
fullWidth fullWidth
dropdownId={`advanced-filter-logical-operator-${viewFilterGroup.id}`} dropdownId={`advanced-filter-logical-operator-${viewFilterGroup.id}`}
value={viewFilterGroup.logicalOperator} value={viewFilterGroup.logicalOperator}

View File

@ -68,7 +68,6 @@ export const AdvancedFilterRuleOptionsDropdown = ({
return ( return (
<Dropdown <Dropdown
disableBlur
dropdownId={dropdownId} dropdownId={dropdownId}
clickableComponent={ clickableComponent={
<AdvancedFilterRuleOptionsDropdownButton dropdownId={dropdownId} /> <AdvancedFilterRuleOptionsDropdownButton dropdownId={dropdownId} />

View File

@ -41,7 +41,6 @@ export const AdvancedFilterViewFilterFieldSelect = ({
return ( return (
<StyledContainer> <StyledContainer>
<Dropdown <Dropdown
disableBlur
dropdownId={advancedFilterDropdownId} dropdownId={advancedFilterDropdownId}
clickableComponent={ clickableComponent={
<SelectControl <SelectControl

View File

@ -76,7 +76,6 @@ export const AdvancedFilterViewFilterOperandSelect = ({
return ( return (
<StyledContainer> <StyledContainer>
<Dropdown <Dropdown
disableBlur
dropdownId={dropdownId} dropdownId={dropdownId}
clickableComponent={ clickableComponent={
<SelectControl <SelectControl

View File

@ -39,7 +39,6 @@ export const AdvancedFilterViewFilterValueInput = ({
return ( return (
<Dropdown <Dropdown
disableBlur
dropdownId={dropdownId} dropdownId={dropdownId}
clickableComponent={ clickableComponent={
<SelectControl <SelectControl

View File

@ -98,7 +98,7 @@ export const ObjectOptionsDropdownMenuContent = () => {
{/** TODO: Should be removed when view settings contains more options */} {/** TODO: Should be removed when view settings contains more options */}
{viewType === ViewType.Kanban && ( {viewType === ViewType.Kanban && (
<> <>
<DropdownMenuItemsContainer> <DropdownMenuItemsContainer withoutScrollWrapper>
<MenuItem <MenuItem
onClick={() => onContentChange('viewSettings')} onClick={() => onContentChange('viewSettings')}
LeftIcon={IconLayout} LeftIcon={IconLayout}
@ -109,7 +109,7 @@ export const ObjectOptionsDropdownMenuContent = () => {
<DropdownMenuSeparator /> <DropdownMenuSeparator />
</> </>
)} )}
<DropdownMenuItemsContainer> <DropdownMenuItemsContainer withoutScrollWrapper>
<MenuItem <MenuItem
onClick={() => onContentChange('fields')} onClick={() => onContentChange('fields')}
LeftIcon={IconTag} LeftIcon={IconTag}

View File

@ -46,7 +46,6 @@ const StyledColumnContainer = styled.div`
const StyledContainerContainer = styled.div` const StyledContainerContainer = styled.div`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%;
`; `;
const StyledBoardContentContainer = styled.div` const StyledBoardContentContainer = styled.div`

View File

@ -81,11 +81,8 @@ const StyledBoardCard = styled.div<{ selected: boolean }>`
`; `;
const StyledTextInput = styled(TextInput)` const StyledTextInput = styled(TextInput)`
backdrop-filter: blur(12px) saturate(200%) contrast(50%) brightness(130%);
background: ${({ theme }) => theme.background.primary};
box-shadow: ${({ theme }) => theme.boxShadow.strong};
width: ${({ theme }) => theme.spacing(53)};
border-radius: ${({ theme }) => theme.border.radius.sm}; border-radius: ${({ theme }) => theme.border.radius.sm};
width: ${({ theme }) => theme.spacing(53)};
`; `;
const StyledBoardCardWrapper = styled.div` const StyledBoardCardWrapper = styled.div`

View File

@ -6,6 +6,7 @@ import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { MenuItem } from 'twenty-ui'; import { MenuItem } from 'twenty-ui';
import { OverlayContainer } from '@/ui/layout/overlay/components/OverlayContainer';
const StyledMenuContainer = styled.div` const StyledMenuContainer = styled.div`
position: absolute; position: absolute;
@ -20,6 +21,7 @@ type RecordBoardColumnDropdownMenuProps = {
stageId: string; stageId: string;
}; };
// TODO: unify and use Dropdown component
export const RecordBoardColumnDropdownMenu = ({ export const RecordBoardColumnDropdownMenu = ({
onClose, onClose,
}: RecordBoardColumnDropdownMenuProps) => { }: RecordBoardColumnDropdownMenuProps) => {
@ -39,6 +41,7 @@ export const RecordBoardColumnDropdownMenu = ({
return ( return (
<StyledMenuContainer ref={boardColumnMenuRef}> <StyledMenuContainer ref={boardColumnMenuRef}>
<OverlayContainer>
<DropdownMenu data-select-disable> <DropdownMenu data-select-disable>
<DropdownMenuItemsContainer> <DropdownMenuItemsContainer>
{recordGroupActions.map((action) => ( {recordGroupActions.map((action) => (
@ -54,6 +57,7 @@ export const RecordBoardColumnDropdownMenu = ({
))} ))}
</DropdownMenuItemsContainer> </DropdownMenuItemsContainer>
</DropdownMenu> </DropdownMenu>
</OverlayContainer>
</StyledMenuContainer> </StyledMenuContainer>
); );
}; };

View File

@ -1,23 +1,17 @@
import styled from '@emotion/styled';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
import { useAddNewCard } from '@/object-record/record-board/record-board-column/hooks/useAddNewCard'; import { useAddNewCard } from '@/object-record/record-board/record-board-column/hooks/useAddNewCard';
import { recordBoardNewRecordByColumnIdSelector } from '@/object-record/record-board/states/selectors/recordBoardNewRecordByColumnIdSelector'; import { recordBoardNewRecordByColumnIdSelector } from '@/object-record/record-board/states/selectors/recordBoardNewRecordByColumnIdSelector';
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
import { viewableRecordNameSingularState } from '@/object-record/record-right-drawer/states/viewableRecordNameSingularState';
import { SingleRecordSelect } from '@/object-record/relation-picker/components/SingleRecordSelect'; import { SingleRecordSelect } from '@/object-record/relation-picker/components/SingleRecordSelect';
import { useRecoilValue } from 'recoil'; import { RecordPickerComponentInstanceContext } from '@/object-record/relation-picker/states/contexts/RecordPickerComponentInstanceContext';
import { OverlayContainer } from '@/ui/layout/overlay/components/OverlayContainer';
const StyledCompanyPickerContainer = styled.div` import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
align-items: center; import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
align-self: baseline; import { useRecoilValue, useSetRecoilState } from 'recoil';
background-color: ${({ theme }) => theme.background.primary}; import { v4 } from 'uuid';
border: none; import { isDefined } from '~/utils/isDefined';
border-radius: ${({ theme }) => theme.border.radius.sm};
color: ${({ theme }) => theme.font.color.tertiary};
cursor: pointer;
display: flex;
gap: ${({ theme }) => theme.spacing(1)};
`;
export const RecordBoardColumnNewOpportunity = ({ export const RecordBoardColumnNewOpportunity = ({
columnId, columnId,
position, position,
@ -31,23 +25,56 @@ export const RecordBoardColumnNewOpportunity = ({
scopeId: columnId, scopeId: columnId,
}), }),
); );
const { handleCreateSuccess, handleEntitySelect } = useAddNewCard(); const { handleCreateSuccess, handleEntitySelect } = useAddNewCard();
const { createOneRecord: createCompany } = useCreateOneRecord({
objectNameSingular: CoreObjectNameSingular.Company,
});
const { openRightDrawer } = useRightDrawer();
const setViewableRecordId = useSetRecoilState(viewableRecordIdState);
const setViewableRecordNameSingular = useSetRecoilState(
viewableRecordNameSingularState,
);
const createCompanyOpportunityAndOpenRightDrawer = async (
searchInput?: string,
) => {
const newRecordId = v4();
const createdCompany = await createCompany({
id: newRecordId,
name: searchInput,
});
setViewableRecordId(newRecordId);
setViewableRecordNameSingular(CoreObjectNameSingular.Company);
openRightDrawer(RightDrawerPages.ViewRecord);
if (isDefined(createdCompany)) {
handleEntitySelect(position, createdCompany);
}
};
return ( return (
<> <>
{newRecord.isCreating && newRecord.position === position && ( {newRecord.isCreating && newRecord.position === position && (
<StyledCompanyPickerContainer> <OverlayContainer>
<RecordPickerComponentInstanceContext.Provider
value={{ instanceId: 'relation-picker' }}
>
<SingleRecordSelect <SingleRecordSelect
disableBackgroundBlur
onCancel={() => handleCreateSuccess(position, columnId, false)} onCancel={() => handleCreateSuccess(position, columnId, false)}
onRecordSelected={(company) => onRecordSelected={(company) =>
company ? handleEntitySelect(position, company) : null company ? handleEntitySelect(position, company) : null
} }
objectNameSingular={CoreObjectNameSingular.Company} objectNameSingular={CoreObjectNameSingular.Company}
recordPickerInstanceId="relation-picker"
selectedRecordIds={[]} selectedRecordIds={[]}
onCreate={createCompanyOpportunityAndOpenRightDrawer}
/> />
</StyledCompanyPickerContainer> </RecordPickerComponentInstanceContext.Provider>
</OverlayContainer>
)} )}
</> </>
); );

View File

@ -1,12 +1,15 @@
import { FormAddressFieldInput } from '@/object-record/record-field/form-types/components/FormAddressFieldInput'; import { FormAddressFieldInput } from '@/object-record/record-field/form-types/components/FormAddressFieldInput';
import { FormBooleanFieldInput } from '@/object-record/record-field/form-types/components/FormBooleanFieldInput'; import { FormBooleanFieldInput } from '@/object-record/record-field/form-types/components/FormBooleanFieldInput';
import { FormDateFieldInput } from '@/object-record/record-field/form-types/components/FormDateFieldInput';
import { FormEmailsFieldInput } from '@/object-record/record-field/form-types/components/FormEmailsFieldInput'; import { FormEmailsFieldInput } from '@/object-record/record-field/form-types/components/FormEmailsFieldInput';
import { FormFullNameFieldInput } from '@/object-record/record-field/form-types/components/FormFullNameFieldInput'; import { FormFullNameFieldInput } from '@/object-record/record-field/form-types/components/FormFullNameFieldInput';
import { FormLinksFieldInput } from '@/object-record/record-field/form-types/components/FormLinksFieldInput'; import { FormLinksFieldInput } from '@/object-record/record-field/form-types/components/FormLinksFieldInput';
import { FormDateFieldInput } from '@/object-record/record-field/form-types/components/FormDateFieldInput'; import { FormMultiSelectFieldInput } from '@/object-record/record-field/form-types/components/FormMultiSelectFieldInput';
import { FormNumberFieldInput } from '@/object-record/record-field/form-types/components/FormNumberFieldInput'; import { FormNumberFieldInput } from '@/object-record/record-field/form-types/components/FormNumberFieldInput';
import { FormRawJsonFieldInput } from '@/object-record/record-field/form-types/components/FormRawJsonFieldInput';
import { FormSelectFieldInput } from '@/object-record/record-field/form-types/components/FormSelectFieldInput'; import { FormSelectFieldInput } from '@/object-record/record-field/form-types/components/FormSelectFieldInput';
import { FormTextFieldInput } from '@/object-record/record-field/form-types/components/FormTextFieldInput'; import { FormTextFieldInput } from '@/object-record/record-field/form-types/components/FormTextFieldInput';
import { FormUuidFieldInput } from '@/object-record/record-field/form-types/components/FormUuidFieldInput';
import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent'; import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent';
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition'; import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
import { import {
@ -15,16 +18,20 @@ import {
FieldFullNameValue, FieldFullNameValue,
FieldLinksValue, FieldLinksValue,
FieldMetadata, FieldMetadata,
FieldMultiSelectValue,
} from '@/object-record/record-field/types/FieldMetadata'; } from '@/object-record/record-field/types/FieldMetadata';
import { isFieldAddress } from '@/object-record/record-field/types/guards/isFieldAddress'; import { isFieldAddress } from '@/object-record/record-field/types/guards/isFieldAddress';
import { isFieldBoolean } from '@/object-record/record-field/types/guards/isFieldBoolean'; import { isFieldBoolean } from '@/object-record/record-field/types/guards/isFieldBoolean';
import { isFieldDate } from '@/object-record/record-field/types/guards/isFieldDate';
import { isFieldEmails } from '@/object-record/record-field/types/guards/isFieldEmails'; import { isFieldEmails } from '@/object-record/record-field/types/guards/isFieldEmails';
import { isFieldFullName } from '@/object-record/record-field/types/guards/isFieldFullName'; import { isFieldFullName } from '@/object-record/record-field/types/guards/isFieldFullName';
import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldLinks'; import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldLinks';
import { isFieldDate } from '@/object-record/record-field/types/guards/isFieldDate'; import { isFieldMultiSelect } from '@/object-record/record-field/types/guards/isFieldMultiSelect';
import { isFieldNumber } from '@/object-record/record-field/types/guards/isFieldNumber'; import { isFieldNumber } from '@/object-record/record-field/types/guards/isFieldNumber';
import { isFieldRawJson } from '@/object-record/record-field/types/guards/isFieldRawJson';
import { isFieldSelect } from '@/object-record/record-field/types/guards/isFieldSelect'; import { isFieldSelect } from '@/object-record/record-field/types/guards/isFieldSelect';
import { isFieldText } from '@/object-record/record-field/types/guards/isFieldText'; import { isFieldText } from '@/object-record/record-field/types/guards/isFieldText';
import { isFieldUuid } from '@/object-record/record-field/types/guards/isFieldUuid';
import { JsonValue } from 'type-fest'; import { JsonValue } from 'type-fest';
type FormFieldInputProps = { type FormFieldInputProps = {
@ -107,5 +114,29 @@ export const FormFieldInput = ({
onPersist={onPersist} onPersist={onPersist}
VariablePicker={VariablePicker} VariablePicker={VariablePicker}
/> />
) : isFieldMultiSelect(field) ? (
<FormMultiSelectFieldInput
label={field.label}
defaultValue={defaultValue as FieldMultiSelectValue | string | undefined}
onPersist={onPersist}
VariablePicker={VariablePicker}
options={field.metadata.options}
/>
) : isFieldRawJson(field) ? (
<FormRawJsonFieldInput
label={field.label}
defaultValue={defaultValue as string | undefined}
onPersist={onPersist}
placeholder={field.label}
VariablePicker={VariablePicker}
/>
) : isFieldUuid(field) ? (
<FormUuidFieldInput
label={field.label}
defaultValue={defaultValue as string | null | undefined}
onPersist={onPersist}
placeholder={field.label}
VariablePicker={VariablePicker}
/>
) : null; ) : null;
}; };

View File

@ -3,7 +3,6 @@ import { FormFieldInputInputContainer } from '@/object-record/record-field/form-
import { FormFieldInputRowContainer } from '@/object-record/record-field/form-types/components/FormFieldInputRowContainer'; import { FormFieldInputRowContainer } from '@/object-record/record-field/form-types/components/FormFieldInputRowContainer';
import { VariableChip } from '@/object-record/record-field/form-types/components/VariableChip'; import { VariableChip } from '@/object-record/record-field/form-types/components/VariableChip';
import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent'; import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent';
import { StyledCalendarContainer } from '@/ui/field/input/components/DateInput';
import { InputLabel } from '@/ui/input/components/InputLabel'; import { InputLabel } from '@/ui/input/components/InputLabel';
import { import {
InternalDatePicker, InternalDatePicker,
@ -16,6 +15,7 @@ import { MIN_DATE } from '@/ui/input/components/internal/date/constants/MinDate'
import { parseDateToString } from '@/ui/input/components/internal/date/utils/parseDateToString'; import { parseDateToString } from '@/ui/input/components/internal/date/utils/parseDateToString';
import { parseStringToDate } from '@/ui/input/components/internal/date/utils/parseStringToDate'; import { parseStringToDate } from '@/ui/input/components/internal/date/utils/parseStringToDate';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { OverlayContainer } from '@/ui/layout/overlay/components/OverlayContainer';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { UserContext } from '@/users/contexts/UserContext'; import { UserContext } from '@/users/contexts/UserContext';
import { isStandaloneVariableString } from '@/workflow/utils/isStandaloneVariableString'; import { isStandaloneVariableString } from '@/workflow/utils/isStandaloneVariableString';
@ -338,7 +338,7 @@ export const FormDateFieldInput = ({
{draftValue.mode === 'edit' ? ( {draftValue.mode === 'edit' ? (
<StyledDateInputContainer> <StyledDateInputContainer>
<StyledDateInputAbsoluteContainer> <StyledDateInputAbsoluteContainer>
<StyledCalendarContainer> <OverlayContainer>
<InternalDatePicker <InternalDatePicker
date={pickerDate ?? new Date()} date={pickerDate ?? new Date()}
isDateTimeInput={false} isDateTimeInput={false}
@ -349,7 +349,7 @@ export const FormDateFieldInput = ({
onClear={handlePickerClear} onClear={handlePickerClear}
hideHeaderInput hideHeaderInput
/> />
</StyledCalendarContainer> </OverlayContainer>
</StyledDateInputAbsoluteContainer> </StyledDateInputAbsoluteContainer>
</StyledDateInputContainer> </StyledDateInputContainer>
) : null} ) : null}

View File

@ -0,0 +1,214 @@
import styled from '@emotion/styled';
import { FormFieldInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputContainer';
import { FormFieldInputInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputInputContainer';
import { FormFieldInputRowContainer } from '@/object-record/record-field/form-types/components/FormFieldInputRowContainer';
import { VariableChip } from '@/object-record/record-field/form-types/components/VariableChip';
import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent';
import { FieldMultiSelectValue } from '@/object-record/record-field/types/FieldMetadata';
import { MULTI_OBJECT_RECORD_SELECT_SELECTABLE_LIST_ID } from '@/object-record/relation-picker/constants/MultiObjectRecordSelectSelectableListId';
import { SelectOption } from '@/spreadsheet-import/types';
import { MultiSelectDisplay } from '@/ui/field/display/components/MultiSelectDisplay';
import { MultiSelectInput } from '@/ui/field/input/components/MultiSelectInput';
import { InputLabel } from '@/ui/input/components/InputLabel';
import { OverlayContainer } from '@/ui/layout/overlay/components/OverlayContainer';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { isStandaloneVariableString } from '@/workflow/utils/isStandaloneVariableString';
import { useId, useState } from 'react';
import { VisibilityHidden } from 'twenty-ui';
import { isDefined } from '~/utils/isDefined';
type FormMultiSelectFieldInputProps = {
label?: string;
defaultValue: FieldMultiSelectValue | string | undefined;
options: SelectOption[];
onPersist: (value: FieldMultiSelectValue | string) => void;
VariablePicker?: VariablePickerComponent;
};
const StyledDisplayModeContainer = styled.button`
width: 100%;
align-items: center;
display: flex;
cursor: pointer;
border: none;
background: transparent;
font-family: inherit;
padding-inline: ${({ theme }) => theme.spacing(2)};
&:hover,
&[data-open='true'] {
background-color: ${({ theme }) => theme.background.transparent.lighter};
}
`;
const StyledSelectInputContainer = styled.div`
position: absolute;
z-index: 1;
top: ${({ theme }) => theme.spacing(8)};
`;
export const FormMultiSelectFieldInput = ({
label,
defaultValue,
options,
onPersist,
VariablePicker,
}: FormMultiSelectFieldInputProps) => {
const inputId = useId();
const hotkeyScope = MULTI_OBJECT_RECORD_SELECT_SELECTABLE_LIST_ID;
const {
setHotkeyScopeAndMemorizePreviousScope,
goBackToPreviousHotkeyScope,
} = usePreviousHotkeyScope();
const [draftValue, setDraftValue] = useState<
| {
type: 'static';
value: FieldMultiSelectValue;
editingMode: 'view' | 'edit';
}
| {
type: 'variable';
value: string;
}
>(
isStandaloneVariableString(defaultValue)
? {
type: 'variable',
value: defaultValue,
}
: {
type: 'static',
value: isDefined(defaultValue) ? defaultValue : [],
editingMode: 'view',
},
);
const handleDisplayModeClick = () => {
if (draftValue.type !== 'static') {
throw new Error(
'This function can only be called when editing a static value.',
);
}
setDraftValue({
...draftValue,
editingMode: 'edit',
});
setHotkeyScopeAndMemorizePreviousScope(hotkeyScope);
};
const onOptionSelected = (value: FieldMultiSelectValue) => {
if (draftValue.type !== 'static') {
throw new Error('Can only be called when editing a static value');
}
setDraftValue({
type: 'static',
value,
editingMode: 'edit',
});
onPersist(value);
};
const onCancel = () => {
if (draftValue.type !== 'static') {
throw new Error('Can only be called when editing a static value');
}
setDraftValue({
...draftValue,
editingMode: 'view',
});
goBackToPreviousHotkeyScope();
};
const handleVariableTagInsert = (variableName: string) => {
setDraftValue({
type: 'variable',
value: variableName,
});
onPersist(variableName);
};
const handleUnlinkVariable = () => {
setDraftValue({
type: 'static',
value: [],
editingMode: 'view',
});
onPersist([]);
};
const selectedNames =
draftValue.type === 'static' ? draftValue.value : undefined;
const selectedOptions =
isDefined(selectedNames) && isDefined(options)
? options.filter((option) =>
selectedNames.some((name) => option.value === name),
)
: undefined;
return (
<FormFieldInputContainer>
{label ? <InputLabel>{label}</InputLabel> : null}
<FormFieldInputRowContainer>
<FormFieldInputInputContainer
hasRightElement={isDefined(VariablePicker)}
>
{draftValue.type === 'static' ? (
<StyledDisplayModeContainer
data-open={draftValue.editingMode === 'edit'}
onClick={handleDisplayModeClick}
>
<VisibilityHidden>Edit</VisibilityHidden>
{isDefined(selectedOptions) ? (
<MultiSelectDisplay
values={selectedNames}
options={selectedOptions}
/>
) : null}
</StyledDisplayModeContainer>
) : (
<VariableChip
rawVariableName={draftValue.value}
onRemove={handleUnlinkVariable}
/>
)}
</FormFieldInputInputContainer>
<StyledSelectInputContainer>
{draftValue.type === 'static' &&
draftValue.editingMode === 'edit' && (
<OverlayContainer>
<MultiSelectInput
hotkeyScope={hotkeyScope}
options={options}
onCancel={onCancel}
onOptionSelected={onOptionSelected}
values={draftValue.value}
/>
</OverlayContainer>
)}
</StyledSelectInputContainer>
{VariablePicker && (
<VariablePicker
inputId={inputId}
onVariableSelect={handleVariableTagInsert}
/>
)}
</FormFieldInputRowContainer>
</FormFieldInputContainer>
);
};

View File

@ -0,0 +1,85 @@
import { FormFieldInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputContainer';
import { FormFieldInputInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputInputContainer';
import { FormFieldInputRowContainer } from '@/object-record/record-field/form-types/components/FormFieldInputRowContainer';
import { TextVariableEditor } from '@/object-record/record-field/form-types/components/TextVariableEditor';
import { useTextVariableEditor } from '@/object-record/record-field/form-types/hooks/useTextVariableEditor';
import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent';
import { InputLabel } from '@/ui/input/components/InputLabel';
import { useId } from 'react';
import { isDefined } from 'twenty-ui';
import { turnIntoEmptyStringIfWhitespacesOnly } from '~/utils/string/turnIntoEmptyStringIfWhitespacesOnly';
type FormRawJsonFieldInputProps = {
label?: string;
defaultValue: string | null | undefined;
placeholder: string;
onPersist: (value: string | null) => void;
readonly?: boolean;
VariablePicker?: VariablePickerComponent;
};
export const FormRawJsonFieldInput = ({
label,
defaultValue,
placeholder,
onPersist,
readonly,
VariablePicker,
}: FormRawJsonFieldInputProps) => {
const inputId = useId();
const editor = useTextVariableEditor({
placeholder,
multiline: true,
readonly,
defaultValue: defaultValue ?? undefined,
onUpdate: (editor) => {
const text = turnIntoEmptyStringIfWhitespacesOnly(editor.getText());
if (text === '') {
onPersist(null);
return;
}
onPersist(text);
},
});
const handleVariableTagInsert = (variableName: string) => {
if (!isDefined(editor)) {
throw new Error(
'Expected the editor to be defined when a variable is selected',
);
}
editor.commands.insertVariableTag(variableName);
};
if (!isDefined(editor)) {
return null;
}
return (
<FormFieldInputContainer>
{label ? <InputLabel>{label}</InputLabel> : null}
<FormFieldInputRowContainer multiline>
<FormFieldInputInputContainer
hasRightElement={isDefined(VariablePicker)}
multiline
>
<TextVariableEditor editor={editor} multiline readonly={readonly} />
</FormFieldInputInputContainer>
{VariablePicker ? (
<VariablePicker
inputId={inputId}
multiline
onVariableSelect={handleVariableTagInsert}
/>
) : null}
</FormFieldInputRowContainer>
</FormFieldInputContainer>
);
};

View File

@ -9,6 +9,7 @@ import { SelectOption } from '@/spreadsheet-import/types';
import { SelectDisplay } from '@/ui/field/display/components/SelectDisplay'; import { SelectDisplay } from '@/ui/field/display/components/SelectDisplay';
import { SelectInput } from '@/ui/field/input/components/SelectInput'; import { SelectInput } from '@/ui/field/input/components/SelectInput';
import { InputLabel } from '@/ui/input/components/InputLabel'; import { InputLabel } from '@/ui/input/components/InputLabel';
import { OverlayContainer } from '@/ui/layout/overlay/components/OverlayContainer';
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList'; import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
@ -238,6 +239,7 @@ export const FormSelectFieldInput = ({
<StyledSelectInputContainer> <StyledSelectInputContainer>
{draftValue.type === 'static' && {draftValue.type === 'static' &&
draftValue.editingMode === 'edit' && ( draftValue.editingMode === 'edit' && (
<OverlayContainer>
<SelectInput <SelectInput
selectableListId={SINGLE_RECORD_SELECT_BASE_LIST} selectableListId={SINGLE_RECORD_SELECT_BASE_LIST}
selectableItemIdArray={optionIds} selectableItemIdArray={optionIds}
@ -251,6 +253,7 @@ export const FormSelectFieldInput = ({
onClear={handleClearField} onClear={handleClearField}
clearLabel={clearLabel} clearLabel={clearLabel}
/> />
</OverlayContainer>
)} )}
</StyledSelectInputContainer> </StyledSelectInputContainer>

View File

@ -0,0 +1,125 @@
import { FormFieldInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputContainer';
import { FormFieldInputInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputInputContainer';
import { FormFieldInputRowContainer } from '@/object-record/record-field/form-types/components/FormFieldInputRowContainer';
import { VariableChip } from '@/object-record/record-field/form-types/components/VariableChip';
import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent';
import { TextInput } from '@/ui/field/input/components/TextInput';
import { InputLabel } from '@/ui/input/components/InputLabel';
import { isStandaloneVariableString } from '@/workflow/utils/isStandaloneVariableString';
import styled from '@emotion/styled';
import { useId, useState } from 'react';
import { isDefined } from 'twenty-ui';
const StyledInput = styled(TextInput)`
padding: ${({ theme }) => `${theme.spacing(1)} ${theme.spacing(2)}`};
`;
type FormUuidFieldInputProps = {
label?: string;
defaultValue: string | null | undefined;
placeholder: string;
onPersist: (value: string | null) => void;
readonly?: boolean;
VariablePicker?: VariablePickerComponent;
};
export const FormUuidFieldInput = ({
label,
defaultValue,
placeholder,
onPersist,
VariablePicker,
}: FormUuidFieldInputProps) => {
const inputId = useId();
const [draftValue, setDraftValue] = useState<
| {
type: 'static';
value: string;
}
| {
type: 'variable';
value: string;
}
>(
isStandaloneVariableString(defaultValue)
? {
type: 'variable',
value: defaultValue,
}
: {
type: 'static',
value: isDefined(defaultValue) ? String(defaultValue) : '',
},
);
const handleChange = (newText: string) => {
setDraftValue({
type: 'static',
value: newText,
});
const trimmedNewText = newText.trim();
if (trimmedNewText === '') {
onPersist(null);
return;
}
onPersist(trimmedNewText);
};
const handleUnlinkVariable = () => {
setDraftValue({
type: 'static',
value: '',
});
onPersist(null);
};
const handleVariableTagInsert = (variableName: string) => {
setDraftValue({
type: 'variable',
value: variableName,
});
onPersist(variableName);
};
return (
<FormFieldInputContainer>
{label ? <InputLabel htmlFor={inputId}>{label}</InputLabel> : null}
<FormFieldInputRowContainer>
<FormFieldInputInputContainer
hasRightElement={isDefined(VariablePicker)}
>
{draftValue.type === 'static' ? (
<StyledInput
inputId={inputId}
placeholder={placeholder}
value={draftValue.value}
copyButton={false}
hotkeyScope="record-create"
onChange={handleChange}
/>
) : (
<VariableChip
rawVariableName={draftValue.value}
onRemove={handleUnlinkVariable}
/>
)}
</FormFieldInputInputContainer>
{VariablePicker ? (
<VariablePicker
inputId={inputId}
onVariableSelect={handleVariableTagInsert}
/>
) : null}
</FormFieldInputRowContainer>
</FormFieldInputContainer>
);
};

View File

@ -0,0 +1,50 @@
import { Meta, StoryObj } from '@storybook/react';
import { within } from '@storybook/test';
import { FormMultiSelectFieldInput } from '../FormMultiSelectFieldInput';
const meta: Meta<typeof FormMultiSelectFieldInput> = {
title: 'UI/Data/Field/Form/Input/FormMultiSelectFieldInput',
component: FormMultiSelectFieldInput,
args: {},
argTypes: {},
};
export default meta;
type Story = StoryObj<typeof FormMultiSelectFieldInput>;
export const Default: Story = {
args: {
label: 'Work Policy',
defaultValue: ['WORK_POLICY_1', 'WORK_POLICY_2'],
options: [
{
label: 'Work Policy 1',
value: 'WORK_POLICY_1',
color: 'blue',
},
{
label: 'Work Policy 2',
value: 'WORK_POLICY_2',
color: 'green',
},
{
label: 'Work Policy 3',
value: 'WORK_POLICY_3',
color: 'red',
},
{
label: 'Work Policy 4',
value: 'WORK_POLICY_4',
color: 'yellow',
},
],
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await canvas.findByText('Work Policy');
await canvas.findByText('Work Policy 1');
await canvas.findByText('Work Policy 2');
},
};

View File

@ -0,0 +1,244 @@
import { expect } from '@storybook/jest';
import { Meta, StoryObj } from '@storybook/react';
import { fn, userEvent, waitFor, within } from '@storybook/test';
import { FormRawJsonFieldInput } from '../FormRawJsonFieldInput';
const meta: Meta<typeof FormRawJsonFieldInput> = {
title: 'UI/Data/Field/Form/Input/FormRawJsonFieldInput',
component: FormRawJsonFieldInput,
args: {},
argTypes: {},
};
export default meta;
type Story = StoryObj<typeof FormRawJsonFieldInput>;
export const Default: Story = {
args: {
label: 'JSON field',
placeholder: 'Enter valid json',
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await canvas.findByText('JSON field');
},
};
export const Readonly: Story = {
args: {
label: 'JSON field',
placeholder: 'Enter valid json',
readonly: true,
onPersist: fn(),
},
play: async ({ canvasElement, args }) => {
const editor = canvasElement.querySelector('.ProseMirror > p');
expect(editor).toBeVisible();
await userEvent.type(editor, '{{ "a": {{ "b" : "d" } }');
await waitFor(() => {
const allParagraphs = canvasElement.querySelectorAll('.ProseMirror > p');
expect(allParagraphs).toHaveLength(1);
expect(allParagraphs[0]).toHaveTextContent('');
});
expect(args.onPersist).not.toHaveBeenCalled();
},
};
export const SaveValidJson: Story = {
args: {
placeholder: 'Enter valid json',
onPersist: fn(),
},
play: async ({ canvasElement, args }) => {
const editor = canvasElement.querySelector('.ProseMirror > p');
expect(editor).toBeVisible();
await userEvent.type(editor, '{{ "a": {{ "b" : "d" } }');
await waitFor(() => {
expect(args.onPersist).toHaveBeenCalledWith('{ "a": { "b" : "d" } }');
});
},
};
export const DoesNotIgnoreInvalidJson: Story = {
args: {
placeholder: 'Enter valid json',
onPersist: fn(),
},
play: async ({ canvasElement, args }) => {
const editor = canvasElement.querySelector('.ProseMirror > p');
expect(editor).toBeVisible();
await userEvent.type(editor, 'lol');
await userEvent.click(canvasElement);
expect(args.onPersist).toHaveBeenCalledWith('lol');
},
};
export const DisplayDefaultValueWithVariablesProperly: Story = {
args: {
placeholder: 'Enter valid json',
defaultValue: '{ "a": { "b" : {{var.test}} } }',
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await canvas.findByText(/{ "a": { "b" : /);
await waitFor(() => {
const variableTag = canvasElement.querySelector(
'[data-type="variableTag"]',
);
expect(variableTag).toBeVisible();
expect(variableTag).toHaveTextContent('test');
});
await canvas.findByText(/ } }/);
},
};
export const InsertVariableInTheMiddleOnTextInput: Story = {
args: {
placeholder: 'Enter valid json',
VariablePicker: ({ onVariableSelect }) => {
return (
<button
onClick={() => {
onVariableSelect('{{test}}');
}}
>
Add variable
</button>
);
},
},
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
const editor = canvasElement.querySelector('.ProseMirror > p');
expect(editor).toBeVisible();
const addVariableButton = await canvas.findByRole('button', {
name: 'Add variable',
});
await userEvent.type(editor, '{{ "a": {{ "b" : ');
await userEvent.click(addVariableButton);
await userEvent.type(editor, ' } }');
await Promise.all([
waitFor(() => {
expect(args.onPersist).toHaveBeenCalledWith(
'{ "a": { "b" : {{test}} } }',
);
}),
]);
},
};
export const CanUseVariableAsObjectProperty: Story = {
args: {
placeholder: 'Enter valid json',
VariablePicker: ({ onVariableSelect }) => {
return (
<button
onClick={() => {
onVariableSelect('{{test}}');
}}
>
Add variable
</button>
);
},
},
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
const editor = canvasElement.querySelector('.ProseMirror > p');
expect(editor).toBeVisible();
const addVariableButton = await canvas.findByRole('button', {
name: 'Add variable',
});
await userEvent.type(editor, '{{ "');
await userEvent.click(addVariableButton);
await userEvent.type(editor, '": 2 }');
await waitFor(() => {
expect(args.onPersist).toHaveBeenCalledWith('{ "{{test}}": 2 }');
});
},
};
export const ClearField: Story = {
args: {
placeholder: 'Enter valid json',
defaultValue: '{ "a": 2 }',
},
play: async ({ canvasElement, args }) => {
const defaultValueStringLength = args.defaultValue!.length;
const editor = canvasElement.querySelector('.ProseMirror > p');
expect(editor).toBeVisible();
await Promise.all([
userEvent.type(editor, `{Backspace>${defaultValueStringLength}}`),
waitFor(() => {
expect(args.onPersist).toHaveBeenCalledWith(null);
}),
]);
},
};
/**
* Line breaks are not authorized in JSON strings. Users should instead put newlines characters themselves.
* See https://stackoverflow.com/a/42073.
*/
export const DoesNotBreakWhenUserInsertsNewlineInJsonString: Story = {
args: {
placeholder: 'Enter valid json',
onPersist: fn(),
},
play: async ({ canvasElement, args }) => {
const editor = canvasElement.querySelector('.ProseMirror > p');
expect(editor).toBeVisible();
await userEvent.type(editor, '"a{Enter}b"');
await userEvent.click(canvasElement);
expect(args.onPersist).toHaveBeenCalled();
},
};
export const AcceptsJsonEncodedNewline: Story = {
args: {
placeholder: 'Enter valid json',
onPersist: fn(),
},
play: async ({ canvasElement, args }) => {
const editor = canvasElement.querySelector('.ProseMirror > p');
expect(editor).toBeVisible();
await userEvent.type(editor, '"a\\nb"');
await userEvent.click(canvasElement);
expect(args.onPersist).toHaveBeenCalledWith('"a\\nb"');
},
};

View File

@ -0,0 +1,212 @@
import { expect } from '@storybook/jest';
import { Meta, StoryObj } from '@storybook/react';
import {
fn,
userEvent,
waitFor,
waitForElementToBeRemoved,
within,
} from '@storybook/test';
import { FormUuidFieldInput } from '../FormUuidFieldInput';
const meta: Meta<typeof FormUuidFieldInput> = {
title: 'UI/Data/Field/Form/Input/FormUuidFieldInput',
component: FormUuidFieldInput,
args: {},
argTypes: {},
};
export default meta;
type Story = StoryObj<typeof FormUuidFieldInput>;
export const Default: Story = {
args: {
label: 'UUID field',
placeholder: 'Enter UUID',
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await canvas.findByText('UUID field');
},
};
export const SetUuidWithDashes: Story = {
args: {
label: 'UUID field',
placeholder: 'Enter UUID',
onPersist: fn(),
},
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
const uuid = 'fc50139a-9047-467e-a313-700fd75700ac';
const input = await canvas.findByPlaceholderText('Enter UUID');
expect(input).toBeVisible();
await userEvent.type(input, uuid);
await waitFor(() => {
expect(args.onPersist).toHaveBeenCalledWith(uuid);
});
},
};
export const SetUuidWithoutDashes: Story = {
args: {
label: 'UUID field',
placeholder: 'Enter UUID',
onPersist: fn(),
},
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
const uuid = 'fc50139a9047467ea313700fd75700ac';
const input = await canvas.findByPlaceholderText('Enter UUID');
expect(input).toBeVisible();
await userEvent.type(input, uuid);
await waitFor(() => {
expect(args.onPersist).toHaveBeenCalledWith(uuid);
});
},
};
export const SetInvalidUuidWithNoValidation: Story = {
args: {
label: 'UUID field',
placeholder: 'Enter UUID',
onPersist: fn(),
},
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
const uuid = 'invalid';
const input = await canvas.findByPlaceholderText('Enter UUID');
expect(input).toBeVisible();
await userEvent.type(input, uuid);
await waitFor(() => {
expect(args.onPersist).toHaveBeenCalledWith(uuid);
});
},
};
export const TrimInputBeforePersisting: Story = {
args: {
label: 'UUID field',
placeholder: 'Enter UUID',
onPersist: fn(),
},
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
const uuid = 'fc50139a9047467ea313700fd75700ac';
const input = await canvas.findByPlaceholderText('Enter UUID');
expect(input).toBeVisible();
await userEvent.type(input, `{Space>2}${uuid}{Space>3}`);
await waitFor(() => {
expect(args.onPersist).toHaveBeenCalledWith(uuid);
});
},
};
export const ClearField: Story = {
args: {
label: 'UUID field',
placeholder: 'Enter UUID',
onPersist: fn(),
},
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
const input = await canvas.findByPlaceholderText('Enter UUID');
expect(input).toBeVisible();
const uuid = 'test';
await userEvent.type(input, uuid);
await waitFor(() => {
expect(args.onPersist).toHaveBeenCalledWith(uuid);
});
await Promise.all([
userEvent.clear(input),
waitFor(() => {
expect(args.onPersist).toHaveBeenCalledWith(null);
}),
]);
},
};
export const ReplaceStaticValueWithVariable: Story = {
args: {
label: 'UUID field',
placeholder: 'Enter UUID',
onPersist: fn(),
VariablePicker: ({ onVariableSelect }) => {
return (
<button
onClick={() => {
onVariableSelect('{{test}}');
}}
>
Add variable
</button>
);
},
},
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
const input = await canvas.findByPlaceholderText('Enter UUID');
expect(input).toBeVisible();
expect(input).toHaveDisplayValue('');
const addVariableButton = await canvas.findByRole('button', {
name: 'Add variable',
});
const [, , variableTag] = await Promise.all([
userEvent.click(addVariableButton),
waitForElementToBeRemoved(input),
waitFor(() => {
const variableTag = canvas.getByText('test');
expect(variableTag).toBeVisible();
return variableTag;
}),
waitFor(() => {
expect(args.onPersist).toHaveBeenCalledWith('{{test}}');
}),
]);
const removeVariableButton = await canvas.findByTestId(/^remove-icon/);
await Promise.all([
userEvent.click(removeVariableButton),
waitForElementToBeRemoved(variableTag),
waitFor(() => {
const input = canvas.getByPlaceholderText('Enter UUID');
expect(input).toBeVisible();
}),
waitFor(() => {
expect(args.onPersist).toHaveBeenCalledWith(null);
}),
]);
},
};

View File

@ -1,25 +1,10 @@
import { styled } from '@linaria/react'; import { Tag } from 'twenty-ui';
import { Tag, THEME_COMMON } from 'twenty-ui';
import { useFieldFocus } from '@/object-record/record-field/hooks/useFieldFocus'; import { useFieldFocus } from '@/object-record/record-field/hooks/useFieldFocus';
import { useMultiSelectFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useMultiSelectFieldDisplay'; import { useMultiSelectFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useMultiSelectFieldDisplay';
import { MultiSelectDisplay } from '@/ui/field/display/components/MultiSelectDisplay';
import { ExpandableList } from '@/ui/layout/expandable-list/components/ExpandableList'; import { ExpandableList } from '@/ui/layout/expandable-list/components/ExpandableList';
const spacing1 = THEME_COMMON.spacing(1);
const StyledContainer = styled.div`
align-items: center;
display: flex;
gap: ${spacing1};
justify-content: flex-start;
max-width: 100%;
overflow: hidden;
width: 100%;
`;
export const MultiSelectFieldDisplay = () => { export const MultiSelectFieldDisplay = () => {
const { fieldValue, fieldDefinition } = useMultiSelectFieldDisplay(); const { fieldValue, fieldDefinition } = useMultiSelectFieldDisplay();
@ -44,15 +29,9 @@ export const MultiSelectFieldDisplay = () => {
))} ))}
</ExpandableList> </ExpandableList>
) : ( ) : (
<StyledContainer> <MultiSelectDisplay
{selectedOptions.map((selectedOption, index) => ( values={fieldValue}
<Tag options={fieldDefinition.metadata.options}
preventShrink
key={index}
color={selectedOption.color}
text={selectedOption.label}
/> />
))}
</StyledContainer>
); );
}; };

View File

@ -4,7 +4,6 @@ import { CurrencyCode } from '@/object-record/record-field/types/CurrencyCode';
import { FieldCurrencyValue } from '@/object-record/record-field/types/FieldMetadata'; import { FieldCurrencyValue } from '@/object-record/record-field/types/FieldMetadata';
import { CurrencyInput } from '@/ui/field/input/components/CurrencyInput'; import { CurrencyInput } from '@/ui/field/input/components/CurrencyInput';
import { FieldInputOverlay } from '../../../../../ui/field/input/components/FieldInputOverlay';
import { useCurrencyField } from '../../hooks/useCurrencyField'; import { useCurrencyField } from '../../hooks/useCurrencyField';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
@ -118,7 +117,6 @@ export const CurrencyFieldInput = ({
}; };
return ( return (
<FieldInputOverlay>
<CurrencyInput <CurrencyInput
value={draftValue?.amount?.toString() ?? ''} value={draftValue?.amount?.toString() ?? ''}
currencyCode={currencyCode} currencyCode={currencyCode}
@ -133,6 +131,5 @@ export const CurrencyFieldInput = ({
onSelect={handleSelect} onSelect={handleSelect}
hotkeyScope={hotkeyScope} hotkeyScope={hotkeyScope}
/> />
</FieldInputOverlay>
); );
}; };

View File

@ -1,7 +1,6 @@
import { useFullNameField } from '@/object-record/record-field/meta-types/hooks/useFullNameField'; import { useFullNameField } from '@/object-record/record-field/meta-types/hooks/useFullNameField';
import { FieldDoubleText } from '@/object-record/record-field/types/FieldDoubleText'; import { FieldDoubleText } from '@/object-record/record-field/types/FieldDoubleText';
import { DoubleTextInput } from '@/ui/field/input/components/DoubleTextInput'; import { DoubleTextInput } from '@/ui/field/input/components/DoubleTextInput';
import { FieldInputOverlay } from '@/ui/field/input/components/FieldInputOverlay';
import { FIRST_NAME_PLACEHOLDER_WITH_SPECIAL_CHARACTER_TO_AVOID_PASSWORD_MANAGERS } from '@/object-record/record-field/meta-types/input/constants/FirstNamePlaceholder'; import { FIRST_NAME_PLACEHOLDER_WITH_SPECIAL_CHARACTER_TO_AVOID_PASSWORD_MANAGERS } from '@/object-record/record-field/meta-types/input/constants/FirstNamePlaceholder';
import { LAST_NAME_PLACEHOLDER_WITH_SPECIAL_CHARACTER_TO_AVOID_PASSWORD_MANAGERS } from '@/object-record/record-field/meta-types/input/constants/LastNamePlaceholder'; import { LAST_NAME_PLACEHOLDER_WITH_SPECIAL_CHARACTER_TO_AVOID_PASSWORD_MANAGERS } from '@/object-record/record-field/meta-types/input/constants/LastNamePlaceholder';
@ -79,7 +78,6 @@ export const FullNameFieldInput = ({
}; };
return ( return (
<FieldInputOverlay>
<DoubleTextInput <DoubleTextInput
firstValue={draftValue?.firstName ?? ''} firstValue={draftValue?.firstName ?? ''}
secondValue={draftValue?.lastName ?? ''} secondValue={draftValue?.lastName ?? ''}
@ -98,6 +96,5 @@ export const FullNameFieldInput = ({
hotkeyScope={hotkeyScope} hotkeyScope={hotkeyScope}
onChange={handleChange} onChange={handleChange}
/> />
</FieldInputOverlay>
); );
}; };

View File

@ -1,4 +1,3 @@
import styled from '@emotion/styled';
import React, { useRef, useState } from 'react'; import React, { useRef, useState } from 'react';
import { Key } from 'ts-key-enum'; import { Key } from 'ts-key-enum';
import { IconCheck, IconPlus, LightIconButton, MenuItem } from 'twenty-ui'; import { IconCheck, IconPlus, LightIconButton, MenuItem } from 'twenty-ui';
@ -18,11 +17,6 @@ import { moveArrayItem } from '~/utils/array/moveArrayItem';
import { toSpliced } from '~/utils/array/toSpliced'; import { toSpliced } from '~/utils/array/toSpliced';
import { turnIntoEmptyStringIfWhitespacesOnly } from '~/utils/string/turnIntoEmptyStringIfWhitespacesOnly'; import { turnIntoEmptyStringIfWhitespacesOnly } from '~/utils/string/turnIntoEmptyStringIfWhitespacesOnly';
const StyledDropdownMenu = styled(DropdownMenu)`
margin: -1px;
position: relative;
`;
type MultiItemFieldInputProps<T> = { type MultiItemFieldInputProps<T> = {
items: T[]; items: T[];
onPersist: (updatedItems: T[]) => void; onPersist: (updatedItems: T[]) => void;
@ -164,7 +158,7 @@ export const MultiItemFieldInput = <T,>({
}; };
return ( return (
<StyledDropdownMenu ref={containerRef} width={200}> <DropdownMenu ref={containerRef} width={200}>
{!!items.length && ( {!!items.length && (
<> <>
<DropdownMenuItemsContainer> <DropdownMenuItemsContainer>
@ -222,6 +216,6 @@ export const MultiItemFieldInput = <T,>({
/> />
</DropdownMenuItemsContainer> </DropdownMenuItemsContainer>
)} )}
</StyledDropdownMenu> </DropdownMenu>
); );
}; };

View File

@ -1,28 +1,5 @@
import styled from '@emotion/styled';
import { useRef, useState } from 'react';
import { useRecoilValue } from 'recoil';
import { Key } from 'ts-key-enum';
import { useMultiSelectField } from '@/object-record/record-field/meta-types/hooks/useMultiSelectField'; import { useMultiSelectField } from '@/object-record/record-field/meta-types/hooks/useMultiSelectField';
import { MULTI_OBJECT_RECORD_SELECT_SELECTABLE_LIST_ID } from '@/object-record/relation-picker/constants/MultiObjectRecordSelectSelectableListId'; import { MultiSelectInput } from '@/ui/field/input/components/MultiSelectInput';
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
import { useSelectableListStates } from '@/ui/layout/selectable-list/hooks/internal/useSelectableListStates';
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { MenuItemMultiSelectTag } from 'twenty-ui';
import { isDefined } from '~/utils/isDefined';
import { turnIntoEmptyStringIfWhitespacesOnly } from '~/utils/string/turnIntoEmptyStringIfWhitespacesOnly';
const StyledRelationPickerContainer = styled.div`
left: -1px;
position: absolute;
top: -1px;
`;
type MultiSelectFieldInputProps = { type MultiSelectFieldInputProps = {
onCancel?: () => void; onCancel?: () => void;
@ -31,112 +8,16 @@ type MultiSelectFieldInputProps = {
export const MultiSelectFieldInput = ({ export const MultiSelectFieldInput = ({
onCancel, onCancel,
}: MultiSelectFieldInputProps) => { }: MultiSelectFieldInputProps) => {
const { selectedItemIdState } = useSelectableListStates({
selectableListScopeId: MULTI_OBJECT_RECORD_SELECT_SELECTABLE_LIST_ID,
});
const { resetSelectedItem } = useSelectableList(
MULTI_OBJECT_RECORD_SELECT_SELECTABLE_LIST_ID,
);
const { persistField, fieldDefinition, fieldValues, hotkeyScope } = const { persistField, fieldDefinition, fieldValues, hotkeyScope } =
useMultiSelectField(); useMultiSelectField();
const selectedItemId = useRecoilValue(selectedItemIdState);
const [searchFilter, setSearchFilter] = useState('');
const containerRef = useRef<HTMLDivElement>(null);
const selectedOptions = fieldDefinition.metadata.options.filter((option) =>
fieldValues?.includes(option.value),
);
const filteredOptionsInDropDown = fieldDefinition.metadata.options.filter(
(option) => option.label.toLowerCase().includes(searchFilter.toLowerCase()),
);
const formatNewSelectedOptions = (value: string) => {
const selectedOptionsValues = selectedOptions.map(
(selectedOption) => selectedOption.value,
);
if (!selectedOptionsValues.includes(value)) {
return [value, ...selectedOptionsValues];
} else {
return selectedOptionsValues.filter(
(selectedOptionsValue) => selectedOptionsValue !== value,
);
}
};
useScopedHotkeys(
Key.Escape,
() => {
onCancel?.();
resetSelectedItem();
},
hotkeyScope,
[onCancel, resetSelectedItem],
);
useListenClickOutside({
refs: [containerRef],
callback: (event) => {
event.stopImmediatePropagation();
const weAreNotInAnHTMLInput = !(
event.target instanceof HTMLInputElement &&
event.target.tagName === 'INPUT'
);
if (weAreNotInAnHTMLInput && isDefined(onCancel)) {
onCancel();
}
resetSelectedItem();
},
listenerId: 'MultiSelectFieldInput',
});
const optionIds = filteredOptionsInDropDown.map((option) => option.value);
return ( return (
<SelectableList <MultiSelectInput
selectableListId={MULTI_OBJECT_RECORD_SELECT_SELECTABLE_LIST_ID}
selectableItemIdArray={optionIds}
hotkeyScope={hotkeyScope} hotkeyScope={hotkeyScope}
onEnter={(itemId) => { options={fieldDefinition.metadata.options}
const option = filteredOptionsInDropDown.find( onCancel={onCancel}
(option) => option.value === itemId, onOptionSelected={persistField}
); values={fieldValues}
if (isDefined(option)) {
persistField(formatNewSelectedOptions(option.value));
}
}}
>
<StyledRelationPickerContainer ref={containerRef}>
<DropdownMenu data-select-disable>
<DropdownMenuSearchInput
value={searchFilter}
onChange={(event) =>
setSearchFilter(
turnIntoEmptyStringIfWhitespacesOnly(event.currentTarget.value),
)
}
autoFocus
/> />
<DropdownMenuSeparator />
<DropdownMenuItemsContainer hasMaxHeight>
{filteredOptionsInDropDown.map((option) => {
return (
<MenuItemMultiSelectTag
key={option.value}
selected={fieldValues?.includes(option.value) || false}
text={option.label}
color={option.color}
onClick={() =>
persistField(formatNewSelectedOptions(option.value))
}
isKeySelected={selectedItemId === option.value}
/>
);
})}
</DropdownMenuItemsContainer>
</DropdownMenu>
</StyledRelationPickerContainer>
</SelectableList>
); );
}; };

View File

@ -1,8 +1,8 @@
import { TextInput } from '@/ui/field/input/components/TextInput'; import { TextInput } from '@/ui/field/input/components/TextInput';
import { FieldInputOverlay } from '../../../../../ui/field/input/components/FieldInputOverlay';
import { useNumberField } from '../../hooks/useNumberField';
import { FieldInputClickOutsideEvent } from '@/object-record/record-field/meta-types/input/components/DateTimeFieldInput'; import { FieldInputClickOutsideEvent } from '@/object-record/record-field/meta-types/input/components/DateTimeFieldInput';
import { FieldInputContainer } from '@/ui/field/input/components/FieldInputContainer';
import { useNumberField } from '../../hooks/useNumberField';
export type FieldInputEvent = (persist: () => void) => void; export type FieldInputEvent = (persist: () => void) => void;
@ -57,7 +57,7 @@ export const NumberFieldInput = ({
}; };
return ( return (
<FieldInputOverlay> <FieldInputContainer>
<TextInput <TextInput
placeholder={fieldDefinition.metadata.placeHolder} placeholder={fieldDefinition.metadata.placeHolder}
autoFocus autoFocus
@ -70,6 +70,6 @@ export const NumberFieldInput = ({
hotkeyScope={hotkeyScope} hotkeyScope={hotkeyScope}
onChange={handleChange} onChange={handleChange}
/> />
</FieldInputOverlay> </FieldInputContainer>
); );
}; };

View File

@ -18,7 +18,6 @@ export const DEFAULT_PHONE_COUNTRY_CODE = '1';
const StyledCustomPhoneInput = styled(ReactPhoneNumberInput)` const StyledCustomPhoneInput = styled(ReactPhoneNumberInput)`
font-family: ${({ theme }) => theme.font.family}; font-family: ${({ theme }) => theme.font.family};
height: 32px;
${TEXT_INPUT_STYLE} ${TEXT_INPUT_STYLE}
padding: 0; padding: 0;

View File

@ -1,4 +1,3 @@
import { FieldTextAreaOverlay } from '@/ui/field/input/components/FieldTextAreaOverlay';
import { TextAreaInput } from '@/ui/field/input/components/TextAreaInput'; import { TextAreaInput } from '@/ui/field/input/components/TextAreaInput';
import { useJsonField } from '../../hooks/useJsonField'; import { useJsonField } from '../../hooks/useJsonField';
@ -59,7 +58,6 @@ export const RawJsonFieldInput = ({
}; };
return ( return (
<FieldTextAreaOverlay>
<TextAreaInput <TextAreaInput
placeholder={fieldDefinition.metadata.placeHolder} placeholder={fieldDefinition.metadata.placeHolder}
autoFocus autoFocus
@ -73,6 +71,5 @@ export const RawJsonFieldInput = ({
onChange={handleChange} onChange={handleChange}
maxRows={25} maxRows={25}
/> />
</FieldTextAreaOverlay>
); );
}; };

View File

@ -1,5 +1,3 @@
import styled from '@emotion/styled';
import { RelationPicker } from '@/object-record/relation-picker/components/RelationPicker'; import { RelationPicker } from '@/object-record/relation-picker/components/RelationPicker';
import { RecordForSelect } from '@/object-record/relation-picker/types/RecordForSelect'; import { RecordForSelect } from '@/object-record/relation-picker/types/RecordForSelect';
@ -8,12 +6,6 @@ import { useRelationField } from '../../hooks/useRelationField';
import { FieldInputEvent } from './DateTimeFieldInput'; import { FieldInputEvent } from './DateTimeFieldInput';
const StyledRelationPickerContainer = styled.div`
left: -1px;
position: absolute;
top: -1px;
`;
export type RelationToOneFieldInputProps = { export type RelationToOneFieldInputProps = {
onSubmit?: FieldInputEvent; onSubmit?: FieldInputEvent;
onCancel?: () => void; onCancel?: () => void;
@ -33,7 +25,6 @@ export const RelationToOneFieldInput = ({
}; };
return ( return (
<StyledRelationPickerContainer>
<RelationPicker <RelationPicker
fieldDefinition={fieldDefinition} fieldDefinition={fieldDefinition}
selectedRecordId={fieldValue?.id} selectedRecordId={fieldValue?.id}
@ -41,6 +32,5 @@ export const RelationToOneFieldInput = ({
onCancel={onCancel} onCancel={onCancel}
initialSearchFilter={initialSearchValue} initialSearchFilter={initialSearchValue}
/> />
</StyledRelationPickerContainer>
); );
}; };

View File

@ -1,9 +1,9 @@
import { FieldTextAreaOverlay } from '@/ui/field/input/components/FieldTextAreaOverlay';
import { TextAreaInput } from '@/ui/field/input/components/TextAreaInput'; import { TextAreaInput } from '@/ui/field/input/components/TextAreaInput';
import { usePersistField } from '../../../hooks/usePersistField'; import { usePersistField } from '../../../hooks/usePersistField';
import { useTextField } from '../../hooks/useTextField'; import { useTextField } from '../../hooks/useTextField';
import { FieldInputContainer } from '@/ui/field/input/components/FieldInputContainer';
import { turnIntoUndefinedIfWhitespacesOnly } from '~/utils/string/turnIntoUndefinedIfWhitespacesOnly'; import { turnIntoUndefinedIfWhitespacesOnly } from '~/utils/string/turnIntoUndefinedIfWhitespacesOnly';
import { import {
FieldInputClickOutsideEvent, FieldInputClickOutsideEvent,
@ -57,7 +57,7 @@ export const TextFieldInput = ({
}; };
return ( return (
<FieldTextAreaOverlay> <FieldInputContainer>
<TextAreaInput <TextAreaInput
placeholder={fieldDefinition.metadata.placeHolder} placeholder={fieldDefinition.metadata.placeHolder}
autoFocus autoFocus
@ -70,6 +70,6 @@ export const TextFieldInput = ({
hotkeyScope={hotkeyScope} hotkeyScope={hotkeyScope}
onChange={handleChange} onChange={handleChange}
/> />
</FieldTextAreaOverlay> </FieldInputContainer>
); );
}; };

View File

@ -23,7 +23,9 @@ import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTabl
import { SpreadsheetImportProvider } from '@/spreadsheet-import/provider/components/SpreadsheetImportProvider'; import { SpreadsheetImportProvider } from '@/spreadsheet-import/provider/components/SpreadsheetImportProvider';
import { RecordIndexActionMenu } from '@/action-menu/components/RecordIndexActionMenu'; import { RecordIndexActionMenu } from '@/action-menu/components/RecordIndexActionMenu';
import { ContextStoreCurrentViewTypeEffect } from '@/context-store/components/ContextStoreCurrentViewTypeEffect';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType';
import { useSetRecordGroup } from '@/object-record/record-group/hooks/useSetRecordGroup'; import { useSetRecordGroup } from '@/object-record/record-group/hooks/useSetRecordGroup';
import { RecordIndexFiltersToContextStoreEffect } from '@/object-record/record-index/components/RecordIndexFiltersToContextStoreEffect'; import { RecordIndexFiltersToContextStoreEffect } from '@/object-record/record-index/components/RecordIndexFiltersToContextStoreEffect';
import { recordIndexKanbanAggregateOperationState } from '@/object-record/record-index/states/recordIndexKanbanAggregateOperationState'; import { recordIndexKanbanAggregateOperationState } from '@/object-record/record-index/states/recordIndexKanbanAggregateOperationState';
@ -52,9 +54,9 @@ const StyledContainer = styled.div`
`; `;
const StyledContainerWithPadding = styled.div` const StyledContainerWithPadding = styled.div`
height: calc(100% - 40px); box-sizing: border-box;
height: calc(100% - ${({ theme }) => theme.spacing(10)});
margin-left: ${({ theme }) => theme.spacing(2)}; margin-left: ${({ theme }) => theme.spacing(2)};
width: 100%;
`; `;
export const RecordIndexContainer = () => { export const RecordIndexContainer = () => {
@ -164,6 +166,14 @@ export const RecordIndexContainer = () => {
); );
return ( return (
<>
<ContextStoreCurrentViewTypeEffect
viewType={
recordIndexViewType === ViewType.Table
? ContextStoreViewType.Table
: ContextStoreViewType.Kanban
}
/>
<StyledContainer> <StyledContainer>
<InformationBannerWrapper /> <InformationBannerWrapper />
<RecordFieldValueSelectorContextProvider> <RecordFieldValueSelectorContextProvider>
@ -248,5 +258,6 @@ export const RecordIndexContainer = () => {
{!isPageHeaderV2Enabled && <RecordIndexActionMenu />} {!isPageHeaderV2Enabled && <RecordIndexActionMenu />}
</RecordFieldValueSelectorContextProvider> </RecordFieldValueSelectorContextProvider>
</StyledContainer> </StyledContainer>
</>
); );
}; };

View File

@ -73,6 +73,20 @@ export const useHandleRecordGroupField = ({
}) satisfies ViewGroup, }) satisfies ViewGroup,
); );
if (
!existingGroupKeys.has(`${fieldMetadataItem.id}:`) &&
fieldMetadataItem.isNullable === true
) {
viewGroupsToCreate.push({
__typename: 'ViewGroup',
id: v4(),
fieldValue: '',
isVisible: true,
position: fieldMetadataItem.options.length,
fieldMetadataId: fieldMetadataItem.id,
} satisfies ViewGroup);
}
const viewGroupsToDelete = view.viewGroups.filter( const viewGroupsToDelete = view.viewGroups.filter(
(group) => group.fieldMetadataId !== fieldMetadataItem.id, (group) => group.fieldMetadataId !== fieldMetadataItem.id,
); );

View File

@ -9,6 +9,7 @@ const StyledInlineCellButtonContainer = styled.div`
align-items: center; align-items: center;
display: flex; display: flex;
`; `;
export const RecordInlineCellButton = ({ Icon }: { Icon: IconComponent }) => { export const RecordInlineCellButton = ({ Icon }: { Icon: IconComponent }) => {
return ( return (
<AnimatedContainer> <AnimatedContainer>

View File

@ -1,4 +1,5 @@
import { RecordInlineCellContext } from '@/object-record/record-inline-cell/components/RecordInlineCellContext'; import { RecordInlineCellContext } from '@/object-record/record-inline-cell/components/RecordInlineCellContext';
import { OverlayContainer } from '@/ui/layout/overlay/components/OverlayContainer';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { autoUpdate, flip, offset, useFloating } from '@floating-ui/react'; import { autoUpdate, flip, offset, useFloating } from '@floating-ui/react';
import { useContext } from 'react'; import { useContext } from 'react';
@ -11,24 +12,14 @@ const StyledInlineCellEditModeContainer = styled.div`
width: 100%; width: 100%;
position: absolute; position: absolute;
height: 24px; height: 24px;
`;
const StyledInlineCellInput = styled.div` background: transparent;
align-items: center;
display: flex;
min-height: 32px;
width: 240px;
z-index: 30;
`; `;
type RecordInlineCellEditModeProps = { type RecordInlineCellEditModeProps = {
children: React.ReactNode; children: React.ReactNode;
}; };
// TODO: Refactor this to avoid setting absolute px values.
export const RecordInlineCellEditMode = ({ export const RecordInlineCellEditMode = ({
children, children,
}: RecordInlineCellEditModeProps) => { }: RecordInlineCellEditModeProps) => {
@ -46,7 +37,7 @@ export const RecordInlineCellEditMode = ({
} }
: { : {
mainAxis: -29, mainAxis: -29,
crossAxis: -4, crossAxis: -5,
}, },
), ),
], ],
@ -59,9 +50,9 @@ export const RecordInlineCellEditMode = ({
data-testid="inline-cell-edit-mode-container" data-testid="inline-cell-edit-mode-container"
> >
{createPortal( {createPortal(
<StyledInlineCellInput ref={refs.setFloating} style={floatingStyles}> <OverlayContainer ref={refs.setFloating} style={floatingStyles}>
{children} {children}
</StyledInlineCellInput>, </OverlayContainer>,
document.body, document.body,
)} )}
</StyledInlineCellEditModeContainer> </StyledInlineCellEditModeContainer>

View File

@ -11,6 +11,7 @@ const StyledClickableContainer = styled.div<{
readonly?: boolean; readonly?: boolean;
isCentered?: boolean; isCentered?: boolean;
}>` }>`
align-items: center;
display: flex; display: flex;
gap: ${({ theme }) => theme.spacing(1)}; gap: ${({ theme }) => theme.spacing(1)};
width: 100%; width: 100%;

View File

@ -1,4 +1,3 @@
/* eslint-disable @nx/workspace-no-navigate-prefer-link */
import { isNonEmptyString } from '@sniptt/guards'; import { isNonEmptyString } from '@sniptt/guards';
import { useState } from 'react'; import { useState } from 'react';
import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
@ -11,6 +10,7 @@ import { useRecordIdsFromFindManyCacheRootQuery } from '@/object-record/record-s
import { buildShowPageURL } from '@/object-record/record-show/utils/buildShowPageURL'; import { buildShowPageURL } from '@/object-record/record-show/utils/buildShowPageURL';
import { buildIndexTablePageURL } from '@/object-record/record-table/utils/buildIndexTableURL'; import { buildIndexTablePageURL } from '@/object-record/record-table/utils/buildIndexTableURL';
import { useQueryVariablesFromActiveFieldsOfViewOrDefaultView } from '@/views/hooks/useQueryVariablesFromActiveFieldsOfViewOrDefaultView'; import { useQueryVariablesFromActiveFieldsOfViewOrDefaultView } from '@/views/hooks/useQueryVariablesFromActiveFieldsOfViewOrDefaultView';
import { isDefined } from 'twenty-ui';
import { capitalize } from '~/utils/string/capitalize'; import { capitalize } from '~/utils/string/capitalize';
export const useRecordShowPagePagination = ( export const useRecordShowPagePagination = (
@ -100,22 +100,43 @@ export const useRecordShowPagePagination = (
const loading = loadingRecordAfter || loadingRecordBefore || loadingCursor; const loading = loadingRecordAfter || loadingRecordBefore || loadingCursor;
const isThereARecordBefore = recordsBefore.length > 0;
const isThereARecordAfter = recordsAfter.length > 0;
const recordBefore = recordsBefore[0]; const recordBefore = recordsBefore[0];
const recordAfter = recordsAfter[0]; const recordAfter = recordsAfter[0];
const { recordIdsInCache } = useRecordIdsFromFindManyCacheRootQuery({
objectNamePlural: objectMetadataItem.namePlural,
fieldVariables: {
filter,
orderBy,
},
});
const navigateToPreviousRecord = () => { const navigateToPreviousRecord = () => {
if (isDefined(recordBefore)) {
navigate( navigate(
buildShowPageURL(objectNameSingular, recordBefore.id, viewIdQueryParam), buildShowPageURL(objectNameSingular, recordBefore.id, viewIdQueryParam),
); );
}
if (!loadingRecordBefore && !isDefined(recordBefore)) {
const firstRecordId = recordIdsInCache[recordIdsInCache.length - 1];
navigate(
buildShowPageURL(objectNameSingular, firstRecordId, viewIdQueryParam),
);
}
}; };
const navigateToNextRecord = () => { const navigateToNextRecord = () => {
if (isDefined(recordAfter)) {
navigate( navigate(
buildShowPageURL(objectNameSingular, recordAfter.id, viewIdQueryParam), buildShowPageURL(objectNameSingular, recordAfter.id, viewIdQueryParam),
); );
}
if (!loadingRecordAfter && !isDefined(recordAfter)) {
const lastRecordId = recordIdsInCache[0];
navigate(
buildShowPageURL(objectNameSingular, lastRecordId, viewIdQueryParam),
);
}
}; };
const navigateToIndexView = () => { const navigateToIndexView = () => {
@ -129,31 +150,21 @@ export const useRecordShowPagePagination = (
navigate(indexTableURL); navigate(indexTableURL);
}; };
const { recordIdsInCache } = useRecordIdsFromFindManyCacheRootQuery({
objectNamePlural: objectMetadataItem.namePlural,
fieldVariables: {
filter,
orderBy,
},
});
const rankInView = recordIdsInCache.findIndex((id) => id === objectRecordId); const rankInView = recordIdsInCache.findIndex((id) => id === objectRecordId);
const rankFoundInFiew = rankInView > -1; const rankFoundInView = rankInView > -1;
const objectLabel = capitalize(objectMetadataItem.labelPlural); const objectLabel = capitalize(objectMetadataItem.labelPlural);
const totalCount = Math.max(1, totalCountBefore, totalCountAfter); const totalCount = Math.max(1, totalCountBefore, totalCountAfter);
const viewNameWithCount = rankFoundInFiew const viewNameWithCount = rankFoundInView
? `${rankInView + 1} of ${totalCount} in ${objectLabel}` ? `${rankInView + 1} of ${totalCount} in ${objectLabel}`
: `${objectLabel} (${totalCount})`; : `${objectLabel} (${totalCount})`;
return { return {
viewName: viewNameWithCount, viewName: viewNameWithCount,
hasPreviousRecord: isThereARecordBefore,
isLoadingPagination: loading, isLoadingPagination: loading,
hasNextRecord: isThereARecordAfter,
navigateToPreviousRecord, navigateToPreviousRecord,
navigateToNextRecord, navigateToNextRecord,
navigateToIndexView, navigateToIndexView,

View File

@ -23,7 +23,7 @@ export const RecordTableBodyDroppable = ({
> >
{(provided) => ( {(provided) => (
<RecordTableBody <RecordTableBody
id={`record-table-body${recordGroupId ? `-${recordGroupId}` : ''}`} id="record-table-body"
ref={provided.innerRef} ref={provided.innerRef}
// eslint-disable-next-line react/jsx-props-no-spreading // eslint-disable-next-line react/jsx-props-no-spreading
{...provided.droppableProps} {...provided.droppableProps}

View File

@ -1,3 +1,4 @@
import { OverlayContainer } from '@/ui/layout/overlay/components/OverlayContainer';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { autoUpdate, flip, offset, useFloating } from '@floating-ui/react'; import { autoUpdate, flip, offset, useFloating } from '@floating-ui/react';
import { ReactElement } from 'react'; import { ReactElement } from 'react';
@ -12,16 +13,6 @@ const StyledEditableCellEditModeContainer = styled.div<RecordTableCellEditModePr
height: 100%; height: 100%;
`; `;
const StyledTableCellInput = styled.div`
align-items: center;
display: flex;
min-height: 32px;
min-width: 200px;
z-index: 10;
`;
export type RecordTableCellEditModeProps = { export type RecordTableCellEditModeProps = {
children: ReactElement; children: ReactElement;
transparent?: boolean; transparent?: boolean;
@ -37,8 +28,8 @@ export const RecordTableCellEditMode = ({
middleware: [ middleware: [
flip(), flip(),
offset({ offset({
mainAxis: -31, mainAxis: -33,
crossAxis: -2, crossAxis: -3,
}), }),
], ],
whileElementsMounted: autoUpdate, whileElementsMounted: autoUpdate,
@ -49,9 +40,9 @@ export const RecordTableCellEditMode = ({
ref={refs.setReference} ref={refs.setReference}
data-testid="editable-cell-edit-mode-container" data-testid="editable-cell-edit-mode-container"
> >
<StyledTableCellInput ref={refs.setFloating} style={floatingStyles}> <OverlayContainer ref={refs.setFloating} style={floatingStyles}>
{children} {children}
</StyledTableCellInput> </OverlayContainer>
</StyledEditableCellEditModeContainer> </StyledEditableCellEditModeContainer>
); );
}; };

View File

@ -65,9 +65,10 @@ export const RecordTableActionRow = ({
<StyledIconContainer> <StyledIconContainer>
<LeftIcon size={theme.icon.size.sm} color={theme.font.color.tertiary} /> <LeftIcon size={theme.icon.size.sm} color={theme.font.color.tertiary} />
</StyledIconContainer> </StyledIconContainer>
<StyledRecordTableTdTextContainer colSpan={visibleTableColumns.length}> <StyledRecordTableTdTextContainer>
<StyledText>{text}</StyledText> <StyledText>{text}</StyledText>
</StyledRecordTableTdTextContainer> </StyledRecordTableTdTextContainer>
<StyledEmptyTd colSpan={visibleTableColumns.length - 1} />
<StyledEmptyTd /> <StyledEmptyTd />
<StyledEmptyTd /> <StyledEmptyTd />
</StyledRecordTableDraggableTr> </StyledRecordTableDraggableTr>

View File

@ -7,6 +7,7 @@ import { RecordTableTr } from '@/object-record/record-table/record-table-row/com
import { combineRefs } from '~/utils/combineRefs'; import { combineRefs } from '~/utils/combineRefs';
type RecordTableDraggableTrProps = { type RecordTableDraggableTrProps = {
className?: string;
draggableId: DraggableId; draggableId: DraggableId;
draggableIndex: number; draggableIndex: number;
isDragDisabled?: boolean; isDragDisabled?: boolean;
@ -17,7 +18,18 @@ type RecordTableDraggableTrProps = {
export const RecordTableDraggableTr = forwardRef< export const RecordTableDraggableTr = forwardRef<
HTMLTableRowElement, HTMLTableRowElement,
RecordTableDraggableTrProps RecordTableDraggableTrProps
>(({ draggableId, draggableIndex, isDragDisabled, onClick, children }, ref) => { >(
(
{
className,
draggableId,
draggableIndex,
isDragDisabled,
onClick,
children,
},
ref,
) => {
const theme = useTheme(); const theme = useTheme();
return ( return (
@ -32,6 +44,7 @@ export const RecordTableDraggableTr = forwardRef<
ref, ref,
draggableProvided.innerRef, draggableProvided.innerRef,
)} )}
className={className}
// eslint-disable-next-line react/jsx-props-no-spreading // eslint-disable-next-line react/jsx-props-no-spreading
{...draggableProvided.draggableProps} {...draggableProvided.draggableProps}
style={{ style={{
@ -60,4 +73,5 @@ export const RecordTableDraggableTr = forwardRef<
)} )}
</Draggable> </Draggable>
); );
}); },
);

View File

@ -91,7 +91,7 @@ export const RecordTableRecordGroupSection = () => {
<IconChevronUp size={theme.icon.size.md} /> <IconChevronUp size={theme.icon.size.md} />
</motion.span> </motion.span>
</StyledChevronContainer> </StyledChevronContainer>
<StyledRecordGroupSection colSpan={visibleColumns.length}> <StyledRecordGroupSection>
<Tag <Tag
variant={ variant={
recordGroup.type !== RecordGroupDefinitionType.NoValue recordGroup.type !== RecordGroupDefinitionType.NoValue
@ -108,6 +108,7 @@ export const RecordTableRecordGroupSection = () => {
/> />
<StyledTotalRow>{recordIdsByGroup.length}</StyledTotalRow> <StyledTotalRow>{recordIdsByGroup.length}</StyledTotalRow>
</StyledRecordGroupSection> </StyledRecordGroupSection>
<StyledEmptyTd colSpan={visibleColumns.length - 1} />
<StyledEmptyTd /> <StyledEmptyTd />
<StyledEmptyTd /> <StyledEmptyTd />
</StyledTrContainer> </StyledTrContainer>

View File

@ -96,6 +96,7 @@ export const MultiRecordSelect = ({
[setSearchFilter], [setSearchFilter],
); );
// TODO: refactor this in a separate component
const results = ( const results = (
<DropdownMenuItemsContainer hasMaxHeight> <DropdownMenuItemsContainer hasMaxHeight>
<SelectableList <SelectableList
@ -139,7 +140,7 @@ export const MultiRecordSelect = ({
onSubmit?.(); onSubmit?.();
}} }}
/> />
<DropdownMenu ref={containerRef} data-select-disable> <DropdownMenu ref={containerRef} data-select-disable width={200}>
{dropdownPlacement?.includes('end') && ( {dropdownPlacement?.includes('end') && (
<> <>
{isDefined(onCreate) && ( {isDefined(onCreate) && (

View File

@ -9,12 +9,10 @@ import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useLis
import { isDefined } from '~/utils/isDefined'; import { isDefined } from '~/utils/isDefined';
export type SingleRecordSelectProps = { export type SingleRecordSelectProps = {
disableBackgroundBlur?: boolean;
width?: number; width?: number;
} & SingleRecordSelectMenuItemsWithSearchProps; } & SingleRecordSelectMenuItemsWithSearchProps;
export const SingleRecordSelect = ({ export const SingleRecordSelect = ({
disableBackgroundBlur = false,
EmptyIcon, EmptyIcon,
emptyLabel, emptyLabel,
excludedRecordIds, excludedRecordIds,
@ -22,7 +20,6 @@ export const SingleRecordSelect = ({
onCreate, onCreate,
onRecordSelected, onRecordSelected,
objectNameSingular, objectNameSingular,
recordPickerInstanceId,
selectedRecordIds, selectedRecordIds,
width = 200, width = 200,
}: SingleRecordSelectProps) => { }: SingleRecordSelectProps) => {
@ -45,12 +42,7 @@ export const SingleRecordSelect = ({
}); });
return ( return (
<DropdownMenu <DropdownMenu ref={containerRef} width={width} data-select-disable>
disableBlur={disableBackgroundBlur}
ref={containerRef}
width={width}
data-select-disable
>
<SingleRecordSelectMenuItemsWithSearch <SingleRecordSelectMenuItemsWithSearch
{...{ {...{
EmptyIcon, EmptyIcon,
@ -60,7 +52,6 @@ export const SingleRecordSelect = ({
onCreate, onCreate,
onRecordSelected, onRecordSelected,
objectNameSingular, objectNameSingular,
recordPickerInstanceId,
selectedRecordIds, selectedRecordIds,
}} }}
/> />

View File

@ -4,10 +4,12 @@ import {
} from '@/object-record/relation-picker/components/SingleRecordSelectMenuItems'; } from '@/object-record/relation-picker/components/SingleRecordSelectMenuItems';
import { useRecordPickerRecordsOptions } from '@/object-record/relation-picker/hooks/useRecordPickerRecordsOptions'; import { useRecordPickerRecordsOptions } from '@/object-record/relation-picker/hooks/useRecordPickerRecordsOptions';
import { useRecordSelectSearch } from '@/object-record/relation-picker/hooks/useRecordSelectSearch'; import { useRecordSelectSearch } from '@/object-record/relation-picker/hooks/useRecordSelectSearch';
import { RecordPickerComponentInstanceContext } from '@/object-record/relation-picker/states/contexts/RecordPickerComponentInstanceContext';
import { CreateNewButton } from '@/ui/input/relation-picker/components/CreateNewButton'; import { CreateNewButton } from '@/ui/input/relation-picker/components/CreateNewButton';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput'; import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { Placement } from '@floating-ui/react'; import { Placement } from '@floating-ui/react';
import { IconPlus } from 'twenty-ui'; import { IconPlus } from 'twenty-ui';
import { isDefined } from '~/utils/isDefined'; import { isDefined } from '~/utils/isDefined';
@ -37,13 +39,14 @@ export const SingleRecordSelectMenuItemsWithSearch = ({
onCreate, onCreate,
onRecordSelected, onRecordSelected,
objectNameSingular, objectNameSingular,
recordPickerInstanceId = 'record-picker',
selectedRecordIds, selectedRecordIds,
dropdownPlacement, dropdownPlacement,
}: SingleRecordSelectMenuItemsWithSearchProps) => { }: SingleRecordSelectMenuItemsWithSearchProps) => {
const { handleSearchFilterChange } = useRecordSelectSearch({ const { handleSearchFilterChange } = useRecordSelectSearch();
recordPickerInstanceId,
}); const recordPickerInstanceId = useAvailableComponentInstanceIdOrThrow(
RecordPickerComponentInstanceContext,
);
const { records, recordPickerSearchFilter } = useRecordPickerRecordsOptions({ const { records, recordPickerSearchFilter } = useRecordPickerRecordsOptions({
objectNameSingular, objectNameSingular,

View File

@ -60,6 +60,7 @@ export const useFilteredSearchRecordQuery = ({
filter: notFilter, filter: notFilter,
limit: limit ?? DEFAULT_SEARCH_REQUEST_LIMIT, limit: limit ?? DEFAULT_SEARCH_REQUEST_LIMIT,
searchInput: searchFilter, searchInput: searchFilter,
fetchPolicy: 'cache-and-network',
}); });
return { return {

View File

@ -1,7 +1,6 @@
import { EnvironmentVariable } from '@/settings/serverless-functions/components/tabs/SettingsServerlessFunctionTabEnvironmentVariablesSection'; import { EnvironmentVariable } from '@/settings/serverless-functions/components/tabs/SettingsServerlessFunctionTabEnvironmentVariablesSection';
import { TextInputV2 } from '@/ui/input/components/TextInputV2'; import { TextInputV2 } from '@/ui/input/components/TextInputV2';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { TableCell } from '@/ui/layout/table/components/TableCell'; import { TableCell } from '@/ui/layout/table/components/TableCell';
@ -101,7 +100,6 @@ export const SettingsServerlessFunctionTabEnvironmentVariableTableRow = ({
</TableCell> </TableCell>
<TableCell> <TableCell>
<Dropdown <Dropdown
dropdownMenuWidth="100px"
dropdownId={dropDownId} dropdownId={dropDownId}
clickableComponent={ clickableComponent={
<LightIconButton <LightIconButton
@ -111,7 +109,6 @@ export const SettingsServerlessFunctionTabEnvironmentVariableTableRow = ({
/> />
} }
dropdownComponents={ dropdownComponents={
<DropdownMenu disableBlur disableBorder width="auto">
<DropdownMenuItemsContainer> <DropdownMenuItemsContainer>
<MenuItem <MenuItem
text={'Edit'} text={'Edit'}
@ -130,7 +127,6 @@ export const SettingsServerlessFunctionTabEnvironmentVariableTableRow = ({
}} }}
/> />
</DropdownMenuItemsContainer> </DropdownMenuItemsContainer>
</DropdownMenu>
} }
dropdownHotkeyScope={{ dropdownHotkeyScope={{
scope: dropDownId, scope: dropDownId,

View File

@ -0,0 +1,48 @@
import { Tag, THEME_COMMON } from 'twenty-ui';
import { FieldMultiSelectValue } from '@/object-record/record-field/types/FieldMetadata';
import { SelectOption } from '@/spreadsheet-import/types';
import styled from '@emotion/styled';
const spacing1 = THEME_COMMON.spacing(1);
const StyledContainer = styled.div`
align-items: center;
display: flex;
gap: ${spacing1};
justify-content: flex-start;
max-width: 100%;
overflow: hidden;
width: 100%;
`;
export const MultiSelectDisplay = ({
values,
options,
}: {
values: FieldMultiSelectValue | undefined;
options: SelectOption[];
}) => {
const selectedOptions = values
? options?.filter((option) => values.includes(option.value))
: [];
if (!selectedOptions) return null;
return (
<StyledContainer>
{selectedOptions.map((selectedOption, index) => (
<Tag
preventShrink
key={index}
color={selectedOption.color ?? 'transparent'}
text={selectedOption.label}
Icon={selectedOption.icon ?? undefined}
/>
))}
</StyledContainer>
);
};

View File

@ -15,11 +15,6 @@ import { useRecoilValue } from 'recoil';
import { isDefined, MOBILE_VIEWPORT } from 'twenty-ui'; import { isDefined, MOBILE_VIEWPORT } from 'twenty-ui';
const StyledAddressContainer = styled.div` const StyledAddressContainer = styled.div`
background: ${({ theme }) => theme.background.secondary};
border: 1px solid ${({ theme }) => theme.border.color.light};
border-radius: ${({ theme }) => theme.border.radius.md};
box-shadow: ${({ theme }) => theme.boxShadow.strong};
padding: 4px 8px; padding: 4px 8px;
width: 344px; width: 344px;
@ -27,11 +22,6 @@ const StyledAddressContainer = styled.div`
margin-bottom: 6px; margin-bottom: 6px;
} }
input {
background-color: ${({ theme }) => theme.background.transparent.secondary};
backdrop-filter: ${({ theme }) => theme.blur.medium};
}
@media (max-width: ${MOBILE_VIEWPORT}px) { @media (max-width: ${MOBILE_VIEWPORT}px) {
width: auto; width: auto;
min-width: 100px; min-width: 100px;

View File

@ -21,10 +21,6 @@ export const StyledIMaskInput = styled(IMaskInput)<StyledInputProps>`
const StyledContainer = styled.div` const StyledContainer = styled.div`
align-items: center; align-items: center;
border: none;
border-radius: ${({ theme }) => theme.border.radius.sm};
box-shadow: ${({ theme }) => theme.boxShadow.strong};
display: flex; display: flex;
justify-content: center; justify-content: center;
`; `;

View File

@ -1,4 +1,4 @@
import styled from '@emotion/styled'; import { useRef, useState } from 'react';
import { Nullable } from 'twenty-ui'; import { Nullable } from 'twenty-ui';
import { import {
@ -9,15 +9,6 @@ import {
} from '@/ui/input/components/internal/date/components/InternalDatePicker'; } from '@/ui/input/components/internal/date/components/InternalDatePicker';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { useRef, useState } from 'react';
export const StyledCalendarContainer = styled.div`
background: ${({ theme }) => theme.background.transparent.secondary};
backdrop-filter: ${({ theme }) => theme.blur.medium};
border: 1px solid ${({ theme }) => theme.border.color.light};
border-radius: ${({ theme }) => theme.border.radius.md};
box-shadow: ${({ theme }) => theme.boxShadow.strong};
`;
export type DateInputProps = { export type DateInputProps = {
value: Nullable<Date>; value: Nullable<Date>;
@ -89,7 +80,6 @@ export const DateInput = ({
return ( return (
<div ref={wrapperRef}> <div ref={wrapperRef}>
<StyledCalendarContainer>
<InternalDatePicker <InternalDatePicker
date={internalValue ?? new Date()} date={internalValue ?? new Date()}
onChange={handleChange} onChange={handleChange}
@ -101,7 +91,6 @@ export const DateInput = ({
onClear={handleClear} onClear={handleClear}
hideHeaderInput={hideHeaderInput} hideHeaderInput={hideHeaderInput}
/> />
</StyledCalendarContainer>
</div> </div>
); );
}; };

View File

@ -12,22 +12,18 @@ import { FieldDoubleText } from '@/object-record/record-field/types/FieldDoubleT
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { isDefined } from '~/utils/isDefined'; import { isDefined } from '~/utils/isDefined';
import { FieldInputContainer } from '@/ui/field/input/components/FieldInputContainer';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { splitFullName } from '~/utils/format/spiltFullName'; import { splitFullName } from '~/utils/format/spiltFullName';
import { turnIntoEmptyStringIfWhitespacesOnly } from '~/utils/string/turnIntoEmptyStringIfWhitespacesOnly'; import { turnIntoEmptyStringIfWhitespacesOnly } from '~/utils/string/turnIntoEmptyStringIfWhitespacesOnly';
import { StyledTextInput } from './TextInput'; import { StyledTextInput } from './TextInput';
const StyledContainer = styled.div` const StyledContainer = styled.div`
align-items: center;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
input {
width: 100%;
}
& > input:last-child { & > input:last-child {
border-left: 1px solid ${({ theme }) => theme.border.color.medium}; border-left: 1px solid ${({ theme }) => theme.border.color.strong};
padding-left: ${({ theme }) => theme.spacing(2)}; padding-left: ${({ theme }) => theme.spacing(2)};
} }
`; `;
@ -186,6 +182,7 @@ export const DoubleTextInput = ({
}; };
return ( return (
<FieldInputContainer>
<StyledContainer ref={containerRef}> <StyledContainer ref={containerRef}>
<StyledTextInput <StyledTextInput
autoComplete="off" autoComplete="off"
@ -220,5 +217,6 @@ export const DoubleTextInput = ({
onClick={handleClickToPreventParentClickEvents} onClick={handleClickToPreventParentClickEvents}
/> />
</StyledContainer> </StyledContainer>
</FieldInputContainer>
); );
}; };

View File

@ -0,0 +1,10 @@
import styled from '@emotion/styled';
// eslint-disable-next-line @nx/workspace-styled-components-prefixed-with-styled
export const FieldInputContainer = styled.div`
align-items: center;
display: flex;
min-height: 32px;
min-width: 200px;
width: 100%;
`;

View File

@ -1,16 +0,0 @@
import styled from '@emotion/styled';
import { OVERLAY_BACKGROUND } from 'twenty-ui';
const StyledFieldInputOverlay = styled.div`
align-items: center;
border: ${({ theme }) => `1px solid ${theme.border.color.light}`};
border-radius: ${({ theme }) => theme.border.radius.sm};
display: flex;
height: 32px;
justify-content: space-between;
margin: -1px;
width: 100%;
${OVERLAY_BACKGROUND}
`;
export const FieldInputOverlay = StyledFieldInputOverlay;

View File

@ -1,16 +0,0 @@
import styled from '@emotion/styled';
import { OVERLAY_BACKGROUND } from 'twenty-ui';
const StyledFieldTextAreaOverlay = styled.div`
align-items: center;
border-radius: ${({ theme }) => theme.border.radius.sm};
display: flex;
margin: -1px;
max-height: 420px;
position: absolute;
top: 0;
width: 100%;
${OVERLAY_BACKGROUND}
`;
export const FieldTextAreaOverlay = StyledFieldTextAreaOverlay;

View File

@ -0,0 +1,142 @@
import { useRef, useState } from 'react';
import { useRecoilValue } from 'recoil';
import { Key } from 'ts-key-enum';
import { FieldMultiSelectValue } from '@/object-record/record-field/types/FieldMetadata';
import { MULTI_OBJECT_RECORD_SELECT_SELECTABLE_LIST_ID } from '@/object-record/relation-picker/constants/MultiObjectRecordSelectSelectableListId';
import { SelectOption } from '@/spreadsheet-import/types';
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
import { useSelectableListStates } from '@/ui/layout/selectable-list/hooks/internal/useSelectableListStates';
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { MenuItemMultiSelectTag } from 'twenty-ui';
import { isDefined } from '~/utils/isDefined';
import { turnIntoEmptyStringIfWhitespacesOnly } from '~/utils/string/turnIntoEmptyStringIfWhitespacesOnly';
type MultiSelectInputProps = {
values: FieldMultiSelectValue;
hotkeyScope: string;
onCancel?: () => void;
options: SelectOption[];
onOptionSelected: (value: FieldMultiSelectValue) => void;
};
export const MultiSelectInput = ({
values,
options,
hotkeyScope,
onCancel,
onOptionSelected,
}: MultiSelectInputProps) => {
const { selectedItemIdState } = useSelectableListStates({
selectableListScopeId: MULTI_OBJECT_RECORD_SELECT_SELECTABLE_LIST_ID,
});
const { resetSelectedItem } = useSelectableList(
MULTI_OBJECT_RECORD_SELECT_SELECTABLE_LIST_ID,
);
const selectedItemId = useRecoilValue(selectedItemIdState);
const [searchFilter, setSearchFilter] = useState('');
const containerRef = useRef<HTMLDivElement>(null);
const selectedOptions = options.filter((option) =>
values?.includes(option.value),
);
const filteredOptionsInDropDown = options.filter((option) =>
option.label.toLowerCase().includes(searchFilter.toLowerCase()),
);
const formatNewSelectedOptions = (value: string) => {
const selectedOptionsValues = selectedOptions.map(
(selectedOption) => selectedOption.value,
);
if (!selectedOptionsValues.includes(value)) {
return [value, ...selectedOptionsValues];
} else {
return selectedOptionsValues.filter(
(selectedOptionsValue) => selectedOptionsValue !== value,
);
}
};
useScopedHotkeys(
Key.Escape,
() => {
onCancel?.();
resetSelectedItem();
},
hotkeyScope,
[onCancel, resetSelectedItem],
);
useListenClickOutside({
refs: [containerRef],
callback: (event) => {
event.stopImmediatePropagation();
const weAreNotInAnHTMLInput = !(
event.target instanceof HTMLInputElement &&
event.target.tagName === 'INPUT'
);
if (weAreNotInAnHTMLInput && isDefined(onCancel)) {
onCancel();
}
resetSelectedItem();
},
listenerId: 'MultiSelectFieldInput',
});
const optionIds = filteredOptionsInDropDown.map((option) => option.value);
return (
<SelectableList
selectableListId={MULTI_OBJECT_RECORD_SELECT_SELECTABLE_LIST_ID}
selectableItemIdArray={optionIds}
hotkeyScope={hotkeyScope}
onEnter={(itemId) => {
const option = filteredOptionsInDropDown.find(
(option) => option.value === itemId,
);
if (isDefined(option)) {
onOptionSelected(formatNewSelectedOptions(option.value));
}
}}
>
<DropdownMenu data-select-disable ref={containerRef}>
<DropdownMenuSearchInput
value={searchFilter}
onChange={(event) =>
setSearchFilter(
turnIntoEmptyStringIfWhitespacesOnly(event.currentTarget.value),
)
}
autoFocus
/>
<DropdownMenuSeparator />
<DropdownMenuItemsContainer hasMaxHeight>
{filteredOptionsInDropDown.map((option) => {
return (
<MenuItemMultiSelectTag
key={option.value}
selected={values?.includes(option.value) || false}
text={option.label}
color={option.color ?? 'transparent'}
Icon={option.icon ?? undefined}
onClick={() =>
onOptionSelected(formatNewSelectedOptions(option.value))
}
isKeySelected={selectedItemId === option.value}
/>
);
})}
</DropdownMenuItemsContainer>
</DropdownMenu>
</SelectableList>
);
};

View File

@ -33,33 +33,14 @@ const StyledTextArea = styled(TextareaAutosize)`
resize: none; resize: none;
max-height: 400px; max-height: 400px;
width: calc(100% - ${({ theme }) => theme.spacing(7)}); width: calc(100% - ${({ theme }) => theme.spacing(7)});
background: transparent;
line-height: 18px; line-height: 18px;
`; `;
const StyledTextAreaContainer = styled.div`
background: ${({ theme }) => theme.background.primary};
border: ${({ theme }) => `1px solid ${theme.border.color.medium}`};
position: relative;
width: 100%;
padding-top: ${({ theme }) => theme.spacing(2)};
padding-bottom: ${({ theme }) => theme.spacing(2)};
border-radius: ${({ theme }) => theme.border.radius.sm};
@supports (
(backdrop-filter: blur(20px)) or (-webkit-backdrop-filter: blur(20px))
) {
background: ${({ theme }) => theme.background.transparent.secondary};
backdrop-filter: ${({ theme }) => theme.blur.medium};
-webkit-backdrop-filter: ${({ theme }) => theme.blur.medium};
}
`;
const StyledLightIconButtonContainer = styled.div` const StyledLightIconButtonContainer = styled.div`
background: transparent; background: transparent;
position: absolute; position: absolute;
top: 18px; top: 16px;
transform: translateY(-50%); transform: translateY(-50%);
right: 0; right: 0;
`; `;
@ -114,7 +95,7 @@ export const TextAreaInput = ({
}); });
return ( return (
<StyledTextAreaContainer> <>
<StyledTextArea <StyledTextArea
placeholder={placeholder} placeholder={placeholder}
disabled={disabled} disabled={disabled}
@ -130,6 +111,6 @@ export const TextAreaInput = ({
<LightCopyIconButton copyText={internalText} /> <LightCopyIconButton copyText={internalText} />
</StyledLightIconButtonContainer> </StyledLightIconButtonContainer>
)} )}
</StyledTextAreaContainer> </>
); );
}; };

View File

@ -3,11 +3,11 @@ import { useMemo, useState } from 'react';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { import {
IconApps, IconApps,
IconComponent,
useIcons,
IconButton, IconButton,
IconButtonVariant, IconButtonVariant,
IconComponent,
LightIconButton, LightIconButton,
useIcons,
} from 'twenty-ui'; } from 'twenty-ui';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
@ -33,7 +33,6 @@ export type IconPickerProps = {
onOpen?: () => void; onOpen?: () => void;
variant?: IconButtonVariant; variant?: IconButtonVariant;
className?: string; className?: string;
disableBlur?: boolean;
}; };
const StyledMenuIconItemsContainer = styled.div` const StyledMenuIconItemsContainer = styled.div`
@ -90,7 +89,6 @@ export const IconPicker = ({
onClose, onClose,
onOpen, onOpen,
variant = 'secondary', variant = 'secondary',
disableBlur = false,
className, className,
}: IconPickerProps) => { }: IconPickerProps) => {
const [searchString, setSearchString] = useState(''); const [searchString, setSearchString] = useState('');
@ -172,7 +170,6 @@ export const IconPicker = ({
/> />
} }
dropdownMenuWidth={176} dropdownMenuWidth={176}
disableBlur={disableBlur}
dropdownComponents={ dropdownComponents={
<SelectableList <SelectableList
selectableListId="icon-list" selectableListId="icon-list"

View File

@ -32,7 +32,6 @@ export type SelectProps<Value extends SelectValue> = {
className?: string; className?: string;
disabled?: boolean; disabled?: boolean;
selectSizeVariant?: SelectSizeVariant; selectSizeVariant?: SelectSizeVariant;
disableBlur?: boolean;
dropdownId: string; dropdownId: string;
dropdownWidth?: `${string}px` | 'auto' | number; dropdownWidth?: `${string}px` | 'auto' | number;
dropdownWidthAuto?: boolean; dropdownWidthAuto?: boolean;
@ -63,7 +62,6 @@ export const Select = <Value extends SelectValue>({
className, className,
disabled: disabledFromProps, disabled: disabledFromProps,
selectSizeVariant, selectSizeVariant,
disableBlur = false,
dropdownId, dropdownId,
dropdownWidth = 176, dropdownWidth = 176,
dropdownWidthAuto = false, dropdownWidthAuto = false,
@ -135,7 +133,6 @@ export const Select = <Value extends SelectValue>({
selectSizeVariant={selectSizeVariant} selectSizeVariant={selectSizeVariant}
/> />
} }
disableBlur={disableBlur}
dropdownComponents={ dropdownComponents={
<> <>
{!!withSearchInput && ( {!!withSearchInput && (

View File

@ -32,7 +32,7 @@ export const CurrencyPickerDropdownSelect = ({
); );
return ( return (
<DropdownMenu disableBlur> <DropdownMenu>
<DropdownMenuSearchInput <DropdownMenuSearchInput
value={searchFilter} value={searchFilter}
onChange={(event) => setSearchFilter(event.target.value)} onChange={(event) => setSearchFilter(event.target.value)}

View File

@ -81,7 +81,6 @@ export const AbsoluteDatePickerHeader = ({
<Select <Select
dropdownId={MONTH_AND_YEAR_DROPDOWN_MONTH_SELECT_ID} dropdownId={MONTH_AND_YEAR_DROPDOWN_MONTH_SELECT_ID}
options={getMonthSelectOptions()} options={getMonthSelectOptions()}
disableBlur
onChange={onChangeMonth} onChange={onChangeMonth}
value={endOfDayInLocalTimezone.getMonth()} value={endOfDayInLocalTimezone.getMonth()}
fullWidth fullWidth
@ -91,7 +90,6 @@ export const AbsoluteDatePickerHeader = ({
onChange={onChangeYear} onChange={onChangeYear}
value={endOfDayInLocalTimezone.getFullYear()} value={endOfDayInLocalTimezone.getFullYear()}
options={years} options={years}
disableBlur
fullWidth fullWidth
/> />
<LightIconButton <LightIconButton

View File

@ -16,8 +16,6 @@ import { isDefined } from 'twenty-ui';
const StyledInputContainer = styled.div` const StyledInputContainer = styled.div`
align-items: center; align-items: center;
background-color: ${({ theme }) => theme.background.transparent.secondary};
backdrop-filter: ${({ theme }) => theme.blur.medium};
border-bottom: 1px solid ${({ theme }) => theme.border.color.light}; border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
border-top-left-radius: ${({ theme }) => theme.border.radius.md}; border-top-left-radius: ${({ theme }) => theme.border.radius.md};
border-top-right-radius: ${({ theme }) => theme.border.radius.md}; border-top-right-radius: ${({ theme }) => theme.border.radius.md};

View File

@ -5,7 +5,6 @@ import { Key } from 'ts-key-enum';
import { import {
IconCalendarX, IconCalendarX,
MenuItemLeftContent, MenuItemLeftContent,
OVERLAY_BACKGROUND,
StyledHoverableMenuItemBase, StyledHoverableMenuItemBase,
} from 'twenty-ui'; } from 'twenty-ui';
@ -122,8 +121,6 @@ const StyledContainer = styled.div<{ calendarDisabled?: boolean }>`
& .react-datepicker__month-dropdown, & .react-datepicker__month-dropdown,
& .react-datepicker__year-dropdown { & .react-datepicker__year-dropdown {
border: ${({ theme }) => theme.border.color.light};
${OVERLAY_BACKGROUND}
overflow-y: scroll; overflow-y: scroll;
top: ${({ theme }) => theme.spacing(2)}; top: ${({ theme }) => theme.spacing(2)};
} }

View File

@ -57,7 +57,6 @@ export const RelativeDatePickerHeader = (
return ( return (
<StyledContainer> <StyledContainer>
<Select <Select
disableBlur
dropdownId="direction-select" dropdownId="direction-select"
value={direction} value={direction}
onChange={(newDirection) => { onChange={(newDirection) => {
@ -95,7 +94,6 @@ export const RelativeDatePickerHeader = (
disabled={direction === 'THIS'} disabled={direction === 'THIS'}
/> />
<Select <Select
disableBlur
dropdownId="unit-select" dropdownId="unit-select"
value={unit} value={unit}
onChange={(newUnit) => { onChange={(newUnit) => {

View File

@ -2,7 +2,6 @@ import styled from '@emotion/styled';
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { Country } from '@/ui/input/components/internal/types/Country'; import { Country } from '@/ui/input/components/internal/types/Country';
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput'; import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
@ -47,7 +46,7 @@ export const PhoneCountryPickerDropdownSelect = ({
); );
return ( return (
<DropdownMenu width="auto" disableBlur> <>
<DropdownMenuSearchInput <DropdownMenuSearchInput
value={searchFilter} value={searchFilter}
onChange={(event) => setSearchFilter(event.currentTarget.value)} onChange={(event) => setSearchFilter(event.currentTarget.value)}
@ -91,6 +90,6 @@ export const PhoneCountryPickerDropdownSelect = ({
</> </>
)} )}
</DropdownMenuItemsContainer> </DropdownMenuItemsContainer>
</DropdownMenu> </>
); );
}; };

View File

@ -53,7 +53,7 @@ const StyledEditor = styled.div`
} }
& .bn-drag-handle-menu { & .bn-drag-handle-menu {
background: ${({ theme }) => theme.background.transparent.secondary}; background: ${({ theme }) => theme.background.transparent.secondary};
backdrop-filter: blur(12px) saturate(200%) contrast(50%) brightness(130%); backdrop-filter: ${({ theme }) => theme.blur.medium};
box-shadow: box-shadow:
0px 2px 4px rgba(0, 0, 0, 0.04), 0px 2px 4px rgba(0, 0, 0, 0.04),
2px 4px 16px rgba(0, 0, 0, 0.12); 2px 4px 16px rgba(0, 0, 0, 0.12);
@ -64,6 +64,19 @@ const StyledEditor = styled.div`
border: 1px solid ${({ theme }) => theme.border.color.medium}; border: 1px solid ${({ theme }) => theme.border.color.medium};
left: 26px; left: 26px;
} }
& .bn-container .bn-suggestion-menu-item:hover {
background-color: blue;
}
& .bn-suggestion-menu {
padding: 4px;
border-radius: 8px;
border: 1px solid ${({ theme }) => theme.border.color.medium};
background: ${({ theme }) => theme.background.transparent.secondary};
backdrop-filter: ${({ theme }) => theme.blur.medium};
}
& .mantine-Menu-item { & .mantine-Menu-item {
background-color: transparent; background-color: transparent;
min-width: 152px; min-width: 152px;

View File

@ -4,6 +4,9 @@ import { IconComponent, MenuItemSuggestion } from 'twenty-ui';
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu'; import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { OverlayContainer } from '@/ui/layout/overlay/components/OverlayContainer';
import { useFloating } from '@floating-ui/react';
import { createPortal } from 'react-dom';
export type SuggestionItem = { export type SuggestionItem = {
title: string; title: string;
@ -14,15 +17,26 @@ export type SuggestionItem = {
type CustomSlashMenuProps = SuggestionMenuProps<SuggestionItem>; type CustomSlashMenuProps = SuggestionMenuProps<SuggestionItem>;
const StyledSlashMenu = styled.div` const StyledContainer = styled.div`
* { height: 1px;
box-sizing: content-box; width: 1px;
} `;
const StyledInnerContainer = styled.div`
height: 250px;
width: 100%;
`; `;
export const CustomSlashMenu = (props: CustomSlashMenuProps) => { export const CustomSlashMenu = (props: CustomSlashMenuProps) => {
const { refs, floatingStyles } = useFloating({
placement: 'bottom-start',
});
return ( return (
<StyledSlashMenu> <StyledContainer ref={refs.setReference}>
{createPortal(
<OverlayContainer ref={refs.setFloating} style={floatingStyles}>
<StyledInnerContainer>
<DropdownMenu style={{ zIndex: 2001 }}> <DropdownMenu style={{ zIndex: 2001 }}>
<DropdownMenuItemsContainer> <DropdownMenuItemsContainer>
{props.items.map((item, index) => ( {props.items.map((item, index) => (
@ -36,6 +50,10 @@ export const CustomSlashMenu = (props: CustomSlashMenuProps) => {
))} ))}
</DropdownMenuItemsContainer> </DropdownMenuItemsContainer>
</DropdownMenu> </DropdownMenu>
</StyledSlashMenu> </StyledInnerContainer>
</OverlayContainer>,
document.body,
)}
</StyledContainer>
); );
}; };

View File

@ -37,7 +37,6 @@ type DropdownProps = {
dropdownMenuWidth?: `${string}px` | `${number}%` | 'auto' | number; dropdownMenuWidth?: `${string}px` | `${number}%` | 'auto' | number;
dropdownOffset?: { x?: number; y?: number }; dropdownOffset?: { x?: number; y?: number };
dropdownStrategy?: 'fixed' | 'absolute'; dropdownStrategy?: 'fixed' | 'absolute';
disableBlur?: boolean;
onClickOutside?: () => void; onClickOutside?: () => void;
onClose?: () => void; onClose?: () => void;
onOpen?: () => void; onOpen?: () => void;
@ -55,7 +54,6 @@ export const Dropdown = ({
dropdownPlacement = 'bottom-end', dropdownPlacement = 'bottom-end',
dropdownStrategy = 'absolute', dropdownStrategy = 'absolute',
dropdownOffset = { x: 0, y: 0 }, dropdownOffset = { x: 0, y: 0 },
disableBlur = false,
onClickOutside, onClickOutside,
onClose, onClose,
onOpen, onOpen,
@ -123,7 +121,6 @@ export const Dropdown = ({
<DropdownContent <DropdownContent
className={className} className={className}
floatingStyles={floatingStyles} floatingStyles={floatingStyles}
disableBlur={disableBlur}
dropdownMenuWidth={dropdownMenuWidth} dropdownMenuWidth={dropdownMenuWidth}
dropdownComponents={dropdownComponents} dropdownComponents={dropdownComponents}
dropdownId={dropdownId} dropdownId={dropdownId}

View File

@ -3,12 +3,14 @@ import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { useInternalHotkeyScopeManagement } from '@/ui/layout/dropdown/hooks/useInternalHotkeyScopeManagement'; import { useInternalHotkeyScopeManagement } from '@/ui/layout/dropdown/hooks/useInternalHotkeyScopeManagement';
import { activeDropdownFocusIdState } from '@/ui/layout/dropdown/states/activeDropdownFocusIdState'; import { activeDropdownFocusIdState } from '@/ui/layout/dropdown/states/activeDropdownFocusIdState';
import { dropdownMaxHeightComponentStateV2 } from '@/ui/layout/dropdown/states/dropdownMaxHeightComponentStateV2'; import { dropdownMaxHeightComponentStateV2 } from '@/ui/layout/dropdown/states/dropdownMaxHeightComponentStateV2';
import { OverlayContainer } from '@/ui/layout/overlay/components/OverlayContainer';
import { HotkeyEffect } from '@/ui/utilities/hotkey/components/HotkeyEffect'; import { HotkeyEffect } from '@/ui/utilities/hotkey/components/HotkeyEffect';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope'; import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId'; import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId';
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import styled from '@emotion/styled';
import { import {
FloatingPortal, FloatingPortal,
Placement, Placement,
@ -19,6 +21,11 @@ import { Keys } from 'react-hotkeys-hook';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { Key } from 'ts-key-enum'; import { Key } from 'ts-key-enum';
export const StyledDropdownContentContainer = styled.div`
display: flex;
z-index: 30;
`;
export type DropdownContentProps = { export type DropdownContentProps = {
className?: string; className?: string;
dropdownId: string; dropdownId: string;
@ -32,7 +39,6 @@ export type DropdownContentProps = {
scope: string; scope: string;
}; };
onHotkeyTriggered?: () => void; onHotkeyTriggered?: () => void;
disableBlur?: boolean;
dropdownMenuWidth?: `${string}px` | `${number}%` | 'auto' | number; dropdownMenuWidth?: `${string}px` | `${number}%` | 'auto' | number;
dropdownComponents: React.ReactNode; dropdownComponents: React.ReactNode;
parentDropdownId?: string; parentDropdownId?: string;
@ -49,7 +55,6 @@ export const DropdownContent = ({
floatingStyles, floatingStyles,
hotkey, hotkey,
onHotkeyTriggered, onHotkeyTriggered,
disableBlur,
dropdownMenuWidth, dropdownMenuWidth,
dropdownComponents, dropdownComponents,
avoidPortal, avoidPortal,
@ -59,7 +64,7 @@ export const DropdownContent = ({
const activeDropdownFocusId = useRecoilValue(activeDropdownFocusIdState); const activeDropdownFocusId = useRecoilValue(activeDropdownFocusIdState);
const [dropdownMaxHeight] = useRecoilComponentStateV2( const dropdownMaxHeight = useRecoilComponentValueV2(
dropdownMaxHeightComponentStateV2, dropdownMaxHeightComponentStateV2,
dropdownId, dropdownId,
); );
@ -114,28 +119,36 @@ export const DropdownContent = ({
<HotkeyEffect hotkey={hotkey} onHotkeyTriggered={onHotkeyTriggered} /> <HotkeyEffect hotkey={hotkey} onHotkeyTriggered={onHotkeyTriggered} />
)} )}
{avoidPortal ? ( {avoidPortal ? (
<DropdownMenu <StyledDropdownContentContainer
className={className}
disableBlur={disableBlur}
width={dropdownMenuWidth ?? dropdownWidth}
data-select-disable
ref={floatingUiRefs.setFloating} ref={floatingUiRefs.setFloating}
style={dropdownMenuStyles} style={dropdownMenuStyles}
>
<OverlayContainer>
<DropdownMenu
className={className}
width={dropdownMenuWidth ?? dropdownWidth}
data-select-disable
> >
{dropdownComponents} {dropdownComponents}
</DropdownMenu> </DropdownMenu>
</OverlayContainer>
</StyledDropdownContentContainer>
) : ( ) : (
<FloatingPortal> <FloatingPortal>
<DropdownMenu <StyledDropdownContentContainer
className={className}
disableBlur={disableBlur}
width={dropdownMenuWidth ?? dropdownWidth}
data-select-disable
ref={floatingUiRefs.setFloating} ref={floatingUiRefs.setFloating}
style={dropdownMenuStyles} style={dropdownMenuStyles}
>
<OverlayContainer>
<DropdownMenu
className={className}
width={dropdownMenuWidth ?? dropdownWidth}
data-select-disable
> >
{dropdownComponents} {dropdownComponents}
</DropdownMenu> </DropdownMenu>
</OverlayContainer>
</StyledDropdownContentContainer>
</FloatingPortal> </FloatingPortal>
)} )}
</> </>

View File

@ -1,29 +1,12 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
const StyledDropdownMenu = styled.div<{ const StyledDropdownMenu = styled.div<{
disableBlur?: boolean;
disableBorder?: boolean;
width?: `${string}px` | `${number}%` | 'auto' | number; width?: `${string}px` | `${number}%` | 'auto' | number;
}>` }>`
backdrop-filter: ${({ theme, disableBlur }) =>
disableBlur ? 'none' : theme.blur.medium};
color: ${({ theme }) => theme.font.color.secondary};
background: ${({ theme, disableBlur }) =>
disableBlur
? theme.background.primary
: theme.background.transparent.primary};
border: ${({ disableBorder, theme }) =>
disableBorder ? 'none' : `1px solid ${theme.border.color.medium}`};
border-radius: ${({ theme }) => theme.border.radius.md};
box-shadow: ${({ theme }) => theme.boxShadow.strong};
display: flex; display: flex;
flex-direction: column; flex-direction: column;
z-index: 30; height: 100%;
width: ${({ width = 200 }) => width: ${({ width = 200 }) =>
typeof width === 'number' ? `${width}px` : width}; typeof width === 'number' ? `${width}px` : width};
`; `;

Some files were not shown because too many files have changed in this diff Show More