mirror of
https://github.com/twentyhq/twenty.git
synced 2024-12-22 11:31:39 +03:00
Merge branch 'main' into c--fix-block-editor-changed-content
This commit is contained in:
commit
0a0be421d7
59
.github/workflows/ci-demo-check.yml
vendored
Normal file
59
.github/workflows/ci-demo-check.yml
vendored
Normal 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
|
@ -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/patches /app/packages/twenty-server/patches
|
||||
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/
|
||||
|
||||
# Install all dependencies
|
||||
@ -22,6 +23,7 @@ FROM common-deps as twenty-server-build
|
||||
|
||||
# Copy sourcecode after installing dependences to accelerate subsequents builds
|
||||
COPY ./packages/twenty-emails /app/packages/twenty-emails
|
||||
COPY ./packages/twenty-shared /app/packages/twenty-shared
|
||||
COPY ./packages/twenty-server /app/packages/twenty-server
|
||||
|
||||
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 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
|
||||
@ -41,6 +43,7 @@ ARG REACT_APP_SERVER_BASE_URL
|
||||
|
||||
COPY ./packages/twenty-front /app/packages/twenty-front
|
||||
COPY ./packages/twenty-ui /app/packages/twenty-ui
|
||||
COPY ./packages/twenty-shared /app/packages/twenty-shared
|
||||
RUN npx nx build twenty-front
|
||||
|
||||
|
||||
|
@ -34,12 +34,19 @@ export default defineConfig({
|
||||
expect: {
|
||||
timeout: 5000,
|
||||
},
|
||||
reporter: [['html', { open: 'never' }]],
|
||||
reporter: process.env.CI ? 'github' : 'list',
|
||||
projects: [
|
||||
{
|
||||
name: 'Login setup',
|
||||
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',
|
||||
use: {
|
||||
@ -47,6 +54,7 @@ export default defineConfig({
|
||||
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
|
||||
testMatch: /all\/.+\.spec\.ts/,
|
||||
},
|
||||
{
|
||||
name: 'firefox',
|
||||
@ -55,6 +63,7 @@ export default defineConfig({
|
||||
storageState: path.resolve(__dirname, '.auth', 'user.json'),
|
||||
},
|
||||
dependencies: ['Login setup'],
|
||||
testMatch: /all\/.+\.spec\.ts/,
|
||||
},
|
||||
{
|
||||
name: 'Authentication',
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { test, expect } from '../lib/fixtures/screenshot';
|
||||
import { test, expect } from '../../lib/fixtures/screenshot';
|
||||
|
||||
test.describe('Basic check', () => {
|
||||
test('Checking if table in Companies is visible', async ({ page }) => {
|
@ -1,5 +1,5 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { sh } from '../drivers/shell_driver';
|
||||
import { sh } from '../../drivers/shell_driver';
|
||||
|
||||
test.describe('', () => {
|
||||
test('Testing logging', async ({ page }) => {
|
15
packages/twenty-e2e-testing/tests/demo/demo_basic.spec.ts
Normal file
15
packages/twenty-e2e-testing/tests/demo/demo_basic.spec.ts
Normal 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('Server’s on a coffee break')).not.toBeVisible({
|
||||
timeout: 5000,
|
||||
});
|
||||
});
|
@ -8,7 +8,15 @@
|
||||
"outputs": ["{options.outputPath}"],
|
||||
"options": {
|
||||
"outputPath": "{projectRoot}/build"
|
||||
}
|
||||
},
|
||||
"dependsOn": ["^build"]
|
||||
},
|
||||
"build:sourcemaps": {
|
||||
"outputs": ["{options.outputPath}"],
|
||||
"options": {
|
||||
"outputPath": "{projectRoot}/build"
|
||||
},
|
||||
"dependsOn": ["^build"]
|
||||
},
|
||||
"serve": {
|
||||
"executor": "nx:run-commands",
|
||||
|
@ -1333,6 +1333,7 @@ export type UserEdge = {
|
||||
export type UserExists = {
|
||||
__typename?: 'UserExists';
|
||||
availableWorkspaces: Array<AvailableWorkspaceOutput>;
|
||||
defaultWorkspaceId: Scalars['String'];
|
||||
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; }>;
|
||||
|
||||
@ -3069,6 +3070,7 @@ export const CheckUserExistsDocument = gql`
|
||||
__typename
|
||||
... on UserExists {
|
||||
exists
|
||||
defaultWorkspaceId
|
||||
availableWorkspaces {
|
||||
id
|
||||
displayName
|
||||
|
@ -1,9 +1,12 @@
|
||||
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 { ShowPageSingleRecordActionMenuEntrySetterEffect } from '@/action-menu/actions/record-actions/single-record/components/ShowPageSingleRecordActionMenuEntrySetterEffect';
|
||||
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 { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
|
||||
import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState';
|
||||
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||
import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType';
|
||||
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
@ -46,6 +49,10 @@ const ActionEffects = ({
|
||||
contextStoreTargetedRecordsRuleComponentState,
|
||||
);
|
||||
|
||||
const contextStoreCurrentViewType = useRecoilComponentValueV2(
|
||||
contextStoreCurrentViewTypeComponentState,
|
||||
);
|
||||
|
||||
const isWorkflowEnabled = useIsFeatureEnabled('IS_WORKFLOW_ENABLED');
|
||||
|
||||
return (
|
||||
@ -59,9 +66,17 @@ const ActionEffects = ({
|
||||
{contextStoreTargetedRecordsRule.mode === 'selection' &&
|
||||
contextStoreTargetedRecordsRule.selectedRecordIds.length === 1 && (
|
||||
<>
|
||||
<SingleRecordActionMenuEntrySetterEffect
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
/>
|
||||
{contextStoreCurrentViewType === ContextStoreViewType.ShowPage && (
|
||||
<ShowPageSingleRecordActionMenuEntrySetterEffect
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
/>
|
||||
)}
|
||||
{(contextStoreCurrentViewType === ContextStoreViewType.Table ||
|
||||
contextStoreCurrentViewType === ContextStoreViewType.Kanban) && (
|
||||
<SingleRecordActionMenuEntrySetterEffect
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
/>
|
||||
)}
|
||||
{isWorkflowEnabled && (
|
||||
<WorkflowRunRecordActionMenuEntrySetterEffect
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
|
@ -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;
|
||||
};
|
@ -1,4 +1,5 @@
|
||||
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';
|
||||
@ -37,6 +38,11 @@ export const SingleRecordActionMenuEntrySetterEffect = ({
|
||||
}
|
||||
|
||||
const actionMenuEntries = Object.values(actionConfig ?? {})
|
||||
.filter((action) =>
|
||||
action.availableOn?.includes(
|
||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
),
|
||||
)
|
||||
.map((action) => {
|
||||
const { shouldBeRegistered, onClick, ConfirmationModal } =
|
||||
action.actionHook({
|
||||
|
@ -1,6 +1,7 @@
|
||||
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 { 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 {
|
||||
ActionMenuEntry,
|
||||
@ -22,6 +23,10 @@ export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V1: Record<
|
||||
label: 'Add to favorites',
|
||||
position: 0,
|
||||
Icon: IconHeart,
|
||||
availableOn: [
|
||||
ActionAvailableOn.SHOW_PAGE,
|
||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
],
|
||||
actionHook: useAddToFavoritesSingleRecordAction,
|
||||
},
|
||||
removeFromFavoritesSingleRecord: {
|
||||
@ -31,6 +36,10 @@ export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V1: Record<
|
||||
label: 'Remove from favorites',
|
||||
position: 1,
|
||||
Icon: IconHeartOff,
|
||||
availableOn: [
|
||||
ActionAvailableOn.SHOW_PAGE,
|
||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
],
|
||||
actionHook: useRemoveFromFavoritesSingleRecordAction,
|
||||
},
|
||||
deleteSingleRecord: {
|
||||
@ -42,6 +51,10 @@ export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V1: Record<
|
||||
Icon: IconTrash,
|
||||
accent: 'danger',
|
||||
isPinned: true,
|
||||
availableOn: [
|
||||
ActionAvailableOn.SHOW_PAGE,
|
||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
],
|
||||
actionHook: useDeleteSingleRecordAction,
|
||||
},
|
||||
};
|
||||
|
@ -1,13 +1,22 @@
|
||||
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 { 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 { ActionAvailableOn } from '@/action-menu/actions/types/actionAvailableOn';
|
||||
import { SingleRecordActionHook } from '@/action-menu/actions/types/singleRecordActionHook';
|
||||
import {
|
||||
ActionMenuEntry,
|
||||
ActionMenuEntryScope,
|
||||
ActionMenuEntryType,
|
||||
} 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<
|
||||
string,
|
||||
@ -20,9 +29,14 @@ export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V2: Record<
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
key: 'add-to-favorites-single-record',
|
||||
label: 'Add to favorites',
|
||||
shortLabel: 'Add to favorites',
|
||||
position: 0,
|
||||
isPinned: true,
|
||||
Icon: IconHeart,
|
||||
availableOn: [
|
||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
ActionAvailableOn.SHOW_PAGE,
|
||||
],
|
||||
actionHook: useAddToFavoritesSingleRecordAction,
|
||||
},
|
||||
removeFromFavoritesSingleRecord: {
|
||||
@ -30,20 +44,54 @@ export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V2: Record<
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
key: 'remove-from-favorites-single-record',
|
||||
label: 'Remove from favorites',
|
||||
shortLabel: 'Remove from favorites',
|
||||
isPinned: true,
|
||||
position: 1,
|
||||
Icon: IconHeartOff,
|
||||
availableOn: [
|
||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
ActionAvailableOn.SHOW_PAGE,
|
||||
],
|
||||
actionHook: useRemoveFromFavoritesSingleRecordAction,
|
||||
},
|
||||
deleteSingleRecord: {
|
||||
type: ActionMenuEntryType.Standard,
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
key: 'delete-single-record',
|
||||
label: 'Delete',
|
||||
label: 'Delete record',
|
||||
shortLabel: 'Delete',
|
||||
position: 2,
|
||||
Icon: IconTrash,
|
||||
accent: 'danger',
|
||||
isPinned: true,
|
||||
availableOn: [
|
||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
ActionAvailableOn.SHOW_PAGE,
|
||||
],
|
||||
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,
|
||||
},
|
||||
};
|
||||
|
@ -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,
|
||||
};
|
||||
};
|
@ -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,
|
||||
};
|
||||
};
|
@ -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 { 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';
|
||||
@ -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 { 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 { ActionAvailableOn } from '@/action-menu/actions/types/actionAvailableOn';
|
||||
import { SingleRecordActionHook } from '@/action-menu/actions/types/singleRecordActionHook';
|
||||
import {
|
||||
ActionMenuEntry,
|
||||
@ -13,6 +16,8 @@ import {
|
||||
ActionMenuEntryType,
|
||||
} from '@/action-menu/types/ActionMenuEntry';
|
||||
import {
|
||||
IconChevronDown,
|
||||
IconChevronUp,
|
||||
IconHistory,
|
||||
IconHistoryToggle,
|
||||
IconPlayerPause,
|
||||
@ -30,81 +35,143 @@ export const WORKFLOW_SINGLE_RECORD_ACTIONS_CONFIG: Record<
|
||||
activateWorkflowDraftSingleRecord: {
|
||||
key: 'activate-workflow-draft-single-record',
|
||||
label: 'Activate Draft',
|
||||
shortLabel: 'Activate Draft',
|
||||
isPinned: true,
|
||||
position: 1,
|
||||
Icon: IconPower,
|
||||
type: ActionMenuEntryType.Standard,
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
availableOn: [
|
||||
ActionAvailableOn.SHOW_PAGE,
|
||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
],
|
||||
actionHook: useActivateDraftWorkflowSingleRecordAction,
|
||||
},
|
||||
activateWorkflowLastPublishedVersionSingleRecord: {
|
||||
key: 'activate-workflow-last-published-version-single-record',
|
||||
label: 'Activate last published version',
|
||||
shortLabel: 'Activate last version',
|
||||
isPinned: true,
|
||||
position: 2,
|
||||
Icon: IconPower,
|
||||
type: ActionMenuEntryType.Standard,
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
availableOn: [
|
||||
ActionAvailableOn.SHOW_PAGE,
|
||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
],
|
||||
actionHook: useActivateLastPublishedVersionWorkflowSingleRecordAction,
|
||||
},
|
||||
deactivateWorkflowSingleRecord: {
|
||||
key: 'deactivate-workflow-single-record',
|
||||
label: 'Deactivate Workflow',
|
||||
shortLabel: 'Deactivate',
|
||||
isPinned: true,
|
||||
position: 3,
|
||||
Icon: IconPlayerPause,
|
||||
type: ActionMenuEntryType.Standard,
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
availableOn: [
|
||||
ActionAvailableOn.SHOW_PAGE,
|
||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
],
|
||||
actionHook: useDeactivateWorkflowSingleRecordAction,
|
||||
},
|
||||
discardWorkflowDraftSingleRecord: {
|
||||
key: 'discard-workflow-draft-single-record',
|
||||
label: 'Discard Draft',
|
||||
shortLabel: 'Discard Draft',
|
||||
isPinned: true,
|
||||
position: 4,
|
||||
Icon: IconTrash,
|
||||
type: ActionMenuEntryType.Standard,
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
availableOn: [
|
||||
ActionAvailableOn.SHOW_PAGE,
|
||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
],
|
||||
actionHook: useDiscardDraftWorkflowSingleRecordAction,
|
||||
},
|
||||
seeWorkflowActiveVersionSingleRecord: {
|
||||
key: 'see-workflow-active-version-single-record',
|
||||
label: 'See active version',
|
||||
shortLabel: 'See active version',
|
||||
isPinned: false,
|
||||
position: 5,
|
||||
Icon: IconHistory,
|
||||
type: ActionMenuEntryType.Standard,
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
availableOn: [
|
||||
ActionAvailableOn.SHOW_PAGE,
|
||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
],
|
||||
actionHook: useSeeActiveVersionWorkflowSingleRecordAction,
|
||||
},
|
||||
seeWorkflowRunsSingleRecord: {
|
||||
key: 'see-workflow-runs-single-record',
|
||||
label: 'See runs',
|
||||
shortLabel: 'See runs',
|
||||
isPinned: false,
|
||||
position: 6,
|
||||
Icon: IconHistoryToggle,
|
||||
type: ActionMenuEntryType.Standard,
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
availableOn: [
|
||||
ActionAvailableOn.SHOW_PAGE,
|
||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
],
|
||||
actionHook: useSeeRunsWorkflowSingleRecordAction,
|
||||
},
|
||||
seeWorkflowVersionsHistorySingleRecord: {
|
||||
key: 'see-workflow-versions-history-single-record',
|
||||
label: 'See versions history',
|
||||
shortLabel: 'See versions',
|
||||
isPinned: false,
|
||||
position: 7,
|
||||
Icon: IconHistory,
|
||||
type: ActionMenuEntryType.Standard,
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
availableOn: [
|
||||
ActionAvailableOn.SHOW_PAGE,
|
||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
],
|
||||
actionHook: useSeeVersionsWorkflowSingleRecordAction,
|
||||
},
|
||||
testWorkflowSingleRecord: {
|
||||
key: 'test-workflow-single-record',
|
||||
label: 'Test Workflow',
|
||||
shortLabel: 'Test',
|
||||
isPinned: true,
|
||||
position: 8,
|
||||
Icon: IconPlayerPlay,
|
||||
type: ActionMenuEntryType.Standard,
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
availableOn: [
|
||||
ActionAvailableOn.SHOW_PAGE,
|
||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
],
|
||||
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,
|
||||
},
|
||||
};
|
||||
|
@ -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 { 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 { ActionAvailableOn } from '@/action-menu/actions/types/actionAvailableOn';
|
||||
import { SingleRecordActionHook } from '@/action-menu/actions/types/singleRecordActionHook';
|
||||
import {
|
||||
ActionMenuEntry,
|
||||
ActionMenuEntryScope,
|
||||
ActionMenuEntryType,
|
||||
} 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<
|
||||
string,
|
||||
@ -23,6 +32,10 @@ export const WORKFLOW_VERSIONS_SINGLE_RECORD_ACTIONS_CONFIG: Record<
|
||||
type: ActionMenuEntryType.Standard,
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
Icon: IconPencil,
|
||||
availableOn: [
|
||||
ActionAvailableOn.SHOW_PAGE,
|
||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
],
|
||||
actionHook: useUseAsDraftWorkflowVersionSingleRecordAction,
|
||||
},
|
||||
seeWorkflowExecutionsSingleRecord: {
|
||||
@ -32,6 +45,10 @@ export const WORKFLOW_VERSIONS_SINGLE_RECORD_ACTIONS_CONFIG: Record<
|
||||
type: ActionMenuEntryType.Standard,
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
Icon: IconHistoryToggle,
|
||||
availableOn: [
|
||||
ActionAvailableOn.SHOW_PAGE,
|
||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
],
|
||||
actionHook: useSeeExecutionsWorkflowVersionSingleRecordAction,
|
||||
},
|
||||
seeWorkflowVersionsHistorySingleRecord: {
|
||||
@ -41,6 +58,32 @@ export const WORKFLOW_VERSIONS_SINGLE_RECORD_ACTIONS_CONFIG: Record<
|
||||
type: ActionMenuEntryType.Standard,
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
Icon: IconHistory,
|
||||
availableOn: [
|
||||
ActionAvailableOn.SHOW_PAGE,
|
||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
],
|
||||
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,
|
||||
},
|
||||
};
|
||||
|
@ -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',
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import { actionMenuEntriesComponentSelector } from '@/action-menu/states/actionMenuEntriesComponentSelector';
|
||||
import { PageHeaderOpenCommandMenuButton } from '@/ui/layout/page-header/components/PageHeaderOpenCommandMenuButton';
|
||||
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 = () => {
|
||||
const actionMenuEntries = useRecoilComponentValueV2(
|
||||
@ -15,18 +15,29 @@ export const RecordShowActionMenuButtons = () => {
|
||||
return (
|
||||
<>
|
||||
{!isMobile &&
|
||||
pinnedEntries.map((entry, index) => (
|
||||
<Button
|
||||
key={index}
|
||||
Icon={entry.Icon}
|
||||
size="small"
|
||||
variant="secondary"
|
||||
accent="default"
|
||||
title={entry.label}
|
||||
onClick={() => entry.onClick?.()}
|
||||
ariaLabel={entry.label}
|
||||
/>
|
||||
))}
|
||||
pinnedEntries.map((entry, index) =>
|
||||
entry.shortLabel ? (
|
||||
<Button
|
||||
key={index}
|
||||
Icon={entry.Icon}
|
||||
size="small"
|
||||
variant="secondary"
|
||||
accent="default"
|
||||
title={entry.shortLabel}
|
||||
onClick={() => entry.onClick?.()}
|
||||
ariaLabel={entry.label}
|
||||
/>
|
||||
) : (
|
||||
<IconButton
|
||||
Icon={entry.Icon}
|
||||
size="small"
|
||||
variant="secondary"
|
||||
accent="default"
|
||||
onClick={() => entry.onClick?.()}
|
||||
ariaLabel={entry.label}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
<PageHeaderOpenCommandMenuButton key="more" />
|
||||
</>
|
||||
);
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { ActionAvailableOn } from '@/action-menu/actions/types/actionAvailableOn';
|
||||
import { MouseEvent, ReactElement } from 'react';
|
||||
import { IconComponent, MenuItemAccent } from 'twenty-ui';
|
||||
|
||||
@ -16,10 +17,12 @@ export type ActionMenuEntry = {
|
||||
scope: ActionMenuEntryScope;
|
||||
key: string;
|
||||
label: string;
|
||||
shortLabel?: string;
|
||||
position: number;
|
||||
Icon: IconComponent;
|
||||
isPinned?: boolean;
|
||||
accent?: MenuItemAccent;
|
||||
availableOn?: ActionAvailableOn[];
|
||||
onClick?: (event?: MouseEvent<HTMLElement>) => void;
|
||||
ConfirmationModal?: ReactElement;
|
||||
};
|
||||
|
@ -1,4 +1,3 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { isNull } from '@sniptt/guards';
|
||||
import { useRecoilCallback, useRecoilState, useSetRecoilState } from 'recoil';
|
||||
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 { prefillRecord } from '@/object-record/utils/prefillRecord';
|
||||
|
||||
const StyledSelectContainer = styled.div`
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
`;
|
||||
|
||||
type ActivityTargetInlineCellEditModeProps = {
|
||||
activity: Task | Note;
|
||||
activityTargetWithTargetRecords: ActivityTargetWithTargetRecord[];
|
||||
@ -282,7 +275,7 @@ export const ActivityTargetInlineCellEditMode = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledSelectContainer>
|
||||
<>
|
||||
<RecordPickerComponentInstanceContext.Provider
|
||||
value={{ instanceId: recordPickerInstanceId }}
|
||||
>
|
||||
@ -295,6 +288,6 @@ export const ActivityTargetInlineCellEditMode = ({
|
||||
<ActivityTargetInlineCellEditModeMultiRecordsSearchFilterEffect />
|
||||
<MultiRecordSelect onSubmit={handleSubmit} onChange={handleChange} />
|
||||
</RecordPickerComponentInstanceContext.Provider>
|
||||
</StyledSelectContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { Modal } from '@/ui/layout/modal/components/Modal';
|
||||
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
|
||||
import styled from '@emotion/styled';
|
||||
import React from 'react';
|
||||
|
||||
@ -11,6 +12,11 @@ type AuthModalProps = { children: React.ReactNode };
|
||||
|
||||
export const AuthModal = ({ children }: AuthModalProps) => (
|
||||
<Modal padding={'none'} modalVariant="primary">
|
||||
<StyledContent>{children}</StyledContent>
|
||||
<ScrollWrapper
|
||||
contextProviderName="modalContent"
|
||||
componentInstanceId="scroll-wrapper-modal-content"
|
||||
>
|
||||
<StyledContent>{children}</StyledContent>
|
||||
</ScrollWrapper>
|
||||
</Modal>
|
||||
);
|
||||
|
@ -6,10 +6,16 @@ import { useIsLogged } from '@/auth/hooks/useIsLogged';
|
||||
import { isAppWaitingForFreshObjectMetadataState } from '@/object-metadata/states/isAppWaitingForFreshObjectMetadataState';
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
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 = () => {
|
||||
const [searchParams] = useSearchParams();
|
||||
const loginToken = searchParams.get('loginToken');
|
||||
const errorMessage = searchParams.get('errorMessage');
|
||||
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
|
||||
const isLogged = useIsLogged();
|
||||
const navigate = useNavigate();
|
||||
@ -22,6 +28,11 @@ export const VerifyEffect = () => {
|
||||
|
||||
useEffect(() => {
|
||||
const getTokens = async () => {
|
||||
if (isDefined(errorMessage)) {
|
||||
enqueueSnackBar(errorMessage, {
|
||||
variant: SnackBarVariant.Error,
|
||||
});
|
||||
}
|
||||
if (!loginToken) {
|
||||
navigate(AppPath.SignInUp);
|
||||
} else {
|
||||
|
@ -6,6 +6,7 @@ export const CHECK_USER_EXISTS = gql`
|
||||
__typename
|
||||
... on UserExists {
|
||||
exists
|
||||
defaultWorkspaceId
|
||||
availableWorkspaces {
|
||||
id
|
||||
displayName
|
||||
|
@ -136,8 +136,10 @@ export const useAuth = () => {
|
||||
await client.clearStore();
|
||||
sessionStorage.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(
|
||||
|
@ -83,21 +83,19 @@ export const SignInUpGlobalScopeForm = () => {
|
||||
},
|
||||
onCompleted: (data) => {
|
||||
requestFreshCaptchaToken();
|
||||
if (data.checkUserExists.__typename === 'UserExists') {
|
||||
if (
|
||||
isDefined(data?.checkUserExists.availableWorkspaces) &&
|
||||
data.checkUserExists.availableWorkspaces.length >= 1
|
||||
) {
|
||||
return redirectToWorkspaceDomain(
|
||||
data?.checkUserExists.availableWorkspaces[0].subdomain,
|
||||
pathname,
|
||||
{
|
||||
email: form.getValues('email'),
|
||||
},
|
||||
);
|
||||
const response = data.checkUserExists;
|
||||
if (response.__typename === 'UserExists') {
|
||||
if (response.availableWorkspaces.length >= 1) {
|
||||
const workspace =
|
||||
response.availableWorkspaces.find(
|
||||
(workspace) => workspace.id === response.defaultWorkspaceId,
|
||||
) ?? response.availableWorkspaces[0];
|
||||
return redirectToWorkspaceDomain(workspace.subdomain, pathname, {
|
||||
email: form.getValues('email'),
|
||||
});
|
||||
}
|
||||
}
|
||||
if (data.checkUserExists.__typename === 'UserNotExists') {
|
||||
if (response.__typename === 'UserNotExists') {
|
||||
setSignInUpMode(SignInUpMode.SignUp);
|
||||
setSignInUpStep(SignInUpStep.Password);
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
|
||||
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
|
||||
import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState';
|
||||
import { contextStoreFiltersComponentState } from '@/context-store/states/contextStoreFiltersComponentState';
|
||||
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
|
||||
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||
@ -108,6 +109,21 @@ export const useCommandMenu = () => {
|
||||
}),
|
||||
contextStoreCurrentViewId,
|
||||
);
|
||||
|
||||
const contextStoreCurrentViewType = snapshot
|
||||
.getLoadable(
|
||||
contextStoreCurrentViewTypeComponentState.atomFamily({
|
||||
instanceId: mainContextStoreComponentInstanceId,
|
||||
}),
|
||||
)
|
||||
.getValue();
|
||||
|
||||
set(
|
||||
contextStoreCurrentViewTypeComponentState.atomFamily({
|
||||
instanceId: 'command-menu',
|
||||
}),
|
||||
contextStoreCurrentViewType,
|
||||
);
|
||||
}
|
||||
|
||||
setIsCommandMenuOpened(true);
|
||||
@ -165,6 +181,13 @@ export const useCommandMenu = () => {
|
||||
null,
|
||||
);
|
||||
|
||||
set(
|
||||
contextStoreCurrentViewTypeComponentState.atomFamily({
|
||||
instanceId: 'command-menu',
|
||||
}),
|
||||
null,
|
||||
);
|
||||
|
||||
if (isCommandMenuOpened) {
|
||||
setIsCommandMenuOpened(false);
|
||||
resetSelectedItem();
|
||||
|
@ -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;
|
||||
};
|
@ -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,
|
||||
});
|
@ -0,0 +1,5 @@
|
||||
export enum ContextStoreViewType {
|
||||
Table = 'table',
|
||||
Kanban = 'kanban',
|
||||
ShowPage = 'show-page',
|
||||
}
|
@ -17,6 +17,10 @@ const StyledEmptyContainer = styled.div`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledOrphanFavoritesContainer = styled.div`
|
||||
margin-bottom: ${({ theme }) => theme.betweenSiblingsGap};
|
||||
`;
|
||||
|
||||
export const CurrentWorkspaceMemberOrphanFavorites = () => {
|
||||
const { sortedFavorites: favorites } = useFavorites();
|
||||
const { deleteFavorite } = useDeleteFavorite();
|
||||
@ -38,24 +42,26 @@ export const CurrentWorkspaceMemberOrphanFavorites = () => {
|
||||
index={index}
|
||||
isInsideScrollableContainer={true}
|
||||
itemComponent={
|
||||
<NavigationDrawerItem
|
||||
label={favorite.labelIdentifier}
|
||||
Icon={() => <FavoriteIcon favorite={favorite} />}
|
||||
active={isLocationMatchingFavorite(
|
||||
currentPath,
|
||||
currentViewPath,
|
||||
favorite,
|
||||
)}
|
||||
to={favorite.link}
|
||||
rightOptions={
|
||||
<LightIconButton
|
||||
Icon={IconHeartOff}
|
||||
onClick={() => deleteFavorite(favorite.id)}
|
||||
accent="tertiary"
|
||||
/>
|
||||
}
|
||||
isDragging={isDragging}
|
||||
/>
|
||||
<StyledOrphanFavoritesContainer>
|
||||
<NavigationDrawerItem
|
||||
label={favorite.labelIdentifier}
|
||||
Icon={() => <FavoriteIcon favorite={favorite} />}
|
||||
active={isLocationMatchingFavorite(
|
||||
currentPath,
|
||||
currentViewPath,
|
||||
favorite,
|
||||
)}
|
||||
to={favorite.link}
|
||||
rightOptions={
|
||||
<LightIconButton
|
||||
Icon={IconHeartOff}
|
||||
onClick={() => deleteFavorite(favorite.id)}
|
||||
accent="tertiary"
|
||||
/>
|
||||
}
|
||||
isDragging={isDragging}
|
||||
/>
|
||||
</StyledOrphanFavoritesContainer>
|
||||
}
|
||||
/>
|
||||
))
|
||||
|
@ -137,7 +137,6 @@ export const AdvancedFilterAddFilterRuleSelect = ({
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
disableBlur
|
||||
dropdownId={dropdownId}
|
||||
clickableComponent={
|
||||
<LightButton Icon={IconPlus} title="Add filter rule" />
|
||||
|
@ -22,7 +22,6 @@ export const AdvancedFilterLogicalOperatorDropdown = ({
|
||||
|
||||
return (
|
||||
<Select
|
||||
disableBlur
|
||||
fullWidth
|
||||
dropdownId={`advanced-filter-logical-operator-${viewFilterGroup.id}`}
|
||||
value={viewFilterGroup.logicalOperator}
|
||||
|
@ -68,7 +68,6 @@ export const AdvancedFilterRuleOptionsDropdown = ({
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
disableBlur
|
||||
dropdownId={dropdownId}
|
||||
clickableComponent={
|
||||
<AdvancedFilterRuleOptionsDropdownButton dropdownId={dropdownId} />
|
||||
|
@ -41,7 +41,6 @@ export const AdvancedFilterViewFilterFieldSelect = ({
|
||||
return (
|
||||
<StyledContainer>
|
||||
<Dropdown
|
||||
disableBlur
|
||||
dropdownId={advancedFilterDropdownId}
|
||||
clickableComponent={
|
||||
<SelectControl
|
||||
|
@ -76,7 +76,6 @@ export const AdvancedFilterViewFilterOperandSelect = ({
|
||||
return (
|
||||
<StyledContainer>
|
||||
<Dropdown
|
||||
disableBlur
|
||||
dropdownId={dropdownId}
|
||||
clickableComponent={
|
||||
<SelectControl
|
||||
|
@ -39,7 +39,6 @@ export const AdvancedFilterViewFilterValueInput = ({
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
disableBlur
|
||||
dropdownId={dropdownId}
|
||||
clickableComponent={
|
||||
<SelectControl
|
||||
|
@ -98,7 +98,7 @@ export const ObjectOptionsDropdownMenuContent = () => {
|
||||
{/** TODO: Should be removed when view settings contains more options */}
|
||||
{viewType === ViewType.Kanban && (
|
||||
<>
|
||||
<DropdownMenuItemsContainer>
|
||||
<DropdownMenuItemsContainer withoutScrollWrapper>
|
||||
<MenuItem
|
||||
onClick={() => onContentChange('viewSettings')}
|
||||
LeftIcon={IconLayout}
|
||||
@ -109,7 +109,7 @@ export const ObjectOptionsDropdownMenuContent = () => {
|
||||
<DropdownMenuSeparator />
|
||||
</>
|
||||
)}
|
||||
<DropdownMenuItemsContainer>
|
||||
<DropdownMenuItemsContainer withoutScrollWrapper>
|
||||
<MenuItem
|
||||
onClick={() => onContentChange('fields')}
|
||||
LeftIcon={IconTag}
|
||||
|
@ -46,7 +46,6 @@ const StyledColumnContainer = styled.div`
|
||||
const StyledContainerContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
const StyledBoardContentContainer = styled.div`
|
||||
|
@ -81,11 +81,8 @@ const StyledBoardCard = styled.div<{ selected: boolean }>`
|
||||
`;
|
||||
|
||||
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};
|
||||
width: ${({ theme }) => theme.spacing(53)};
|
||||
`;
|
||||
|
||||
const StyledBoardCardWrapper = styled.div`
|
||||
|
@ -6,6 +6,7 @@ import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||
import { MenuItem } from 'twenty-ui';
|
||||
import { OverlayContainer } from '@/ui/layout/overlay/components/OverlayContainer';
|
||||
|
||||
const StyledMenuContainer = styled.div`
|
||||
position: absolute;
|
||||
@ -20,6 +21,7 @@ type RecordBoardColumnDropdownMenuProps = {
|
||||
stageId: string;
|
||||
};
|
||||
|
||||
// TODO: unify and use Dropdown component
|
||||
export const RecordBoardColumnDropdownMenu = ({
|
||||
onClose,
|
||||
}: RecordBoardColumnDropdownMenuProps) => {
|
||||
@ -39,21 +41,23 @@ export const RecordBoardColumnDropdownMenu = ({
|
||||
|
||||
return (
|
||||
<StyledMenuContainer ref={boardColumnMenuRef}>
|
||||
<DropdownMenu data-select-disable>
|
||||
<DropdownMenuItemsContainer>
|
||||
{recordGroupActions.map((action) => (
|
||||
<MenuItem
|
||||
key={action.id}
|
||||
onClick={() => {
|
||||
action.callback();
|
||||
closeMenu();
|
||||
}}
|
||||
LeftIcon={action.icon}
|
||||
text={action.label}
|
||||
/>
|
||||
))}
|
||||
</DropdownMenuItemsContainer>
|
||||
</DropdownMenu>
|
||||
<OverlayContainer>
|
||||
<DropdownMenu data-select-disable>
|
||||
<DropdownMenuItemsContainer>
|
||||
{recordGroupActions.map((action) => (
|
||||
<MenuItem
|
||||
key={action.id}
|
||||
onClick={() => {
|
||||
action.callback();
|
||||
closeMenu();
|
||||
}}
|
||||
LeftIcon={action.icon}
|
||||
text={action.label}
|
||||
/>
|
||||
))}
|
||||
</DropdownMenuItemsContainer>
|
||||
</DropdownMenu>
|
||||
</OverlayContainer>
|
||||
</StyledMenuContainer>
|
||||
);
|
||||
};
|
||||
|
@ -1,23 +1,17 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
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 { 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 { useRecoilValue } from 'recoil';
|
||||
|
||||
const StyledCompanyPickerContainer = styled.div`
|
||||
align-items: center;
|
||||
align-self: baseline;
|
||||
background-color: ${({ theme }) => theme.background.primary};
|
||||
border: none;
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
import { RecordPickerComponentInstanceContext } from '@/object-record/relation-picker/states/contexts/RecordPickerComponentInstanceContext';
|
||||
import { OverlayContainer } from '@/ui/layout/overlay/components/OverlayContainer';
|
||||
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
|
||||
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import { v4 } from 'uuid';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
export const RecordBoardColumnNewOpportunity = ({
|
||||
columnId,
|
||||
position,
|
||||
@ -31,23 +25,56 @@ export const RecordBoardColumnNewOpportunity = ({
|
||||
scopeId: columnId,
|
||||
}),
|
||||
);
|
||||
|
||||
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 (
|
||||
<>
|
||||
{newRecord.isCreating && newRecord.position === position && (
|
||||
<StyledCompanyPickerContainer>
|
||||
<SingleRecordSelect
|
||||
disableBackgroundBlur
|
||||
onCancel={() => handleCreateSuccess(position, columnId, false)}
|
||||
onRecordSelected={(company) =>
|
||||
company ? handleEntitySelect(position, company) : null
|
||||
}
|
||||
objectNameSingular={CoreObjectNameSingular.Company}
|
||||
recordPickerInstanceId="relation-picker"
|
||||
selectedRecordIds={[]}
|
||||
/>
|
||||
</StyledCompanyPickerContainer>
|
||||
<OverlayContainer>
|
||||
<RecordPickerComponentInstanceContext.Provider
|
||||
value={{ instanceId: 'relation-picker' }}
|
||||
>
|
||||
<SingleRecordSelect
|
||||
onCancel={() => handleCreateSuccess(position, columnId, false)}
|
||||
onRecordSelected={(company) =>
|
||||
company ? handleEntitySelect(position, company) : null
|
||||
}
|
||||
objectNameSingular={CoreObjectNameSingular.Company}
|
||||
selectedRecordIds={[]}
|
||||
onCreate={createCompanyOpportunityAndOpenRightDrawer}
|
||||
/>
|
||||
</RecordPickerComponentInstanceContext.Provider>
|
||||
</OverlayContainer>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
@ -1,12 +1,15 @@
|
||||
import { FormAddressFieldInput } from '@/object-record/record-field/form-types/components/FormAddressFieldInput';
|
||||
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 { FormFullNameFieldInput } from '@/object-record/record-field/form-types/components/FormFullNameFieldInput';
|
||||
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 { FormRawJsonFieldInput } from '@/object-record/record-field/form-types/components/FormRawJsonFieldInput';
|
||||
import { FormSelectFieldInput } from '@/object-record/record-field/form-types/components/FormSelectFieldInput';
|
||||
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 { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
|
||||
import {
|
||||
@ -15,16 +18,20 @@ import {
|
||||
FieldFullNameValue,
|
||||
FieldLinksValue,
|
||||
FieldMetadata,
|
||||
FieldMultiSelectValue,
|
||||
} from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { isFieldAddress } from '@/object-record/record-field/types/guards/isFieldAddress';
|
||||
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 { isFieldFullName } from '@/object-record/record-field/types/guards/isFieldFullName';
|
||||
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 { isFieldRawJson } from '@/object-record/record-field/types/guards/isFieldRawJson';
|
||||
import { isFieldSelect } from '@/object-record/record-field/types/guards/isFieldSelect';
|
||||
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';
|
||||
|
||||
type FormFieldInputProps = {
|
||||
@ -107,5 +114,29 @@ export const FormFieldInput = ({
|
||||
onPersist={onPersist}
|
||||
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;
|
||||
};
|
||||
|
@ -3,7 +3,6 @@ import { FormFieldInputInputContainer } from '@/object-record/record-field/form-
|
||||
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 { StyledCalendarContainer } from '@/ui/field/input/components/DateInput';
|
||||
import { InputLabel } from '@/ui/input/components/InputLabel';
|
||||
import {
|
||||
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 { parseStringToDate } from '@/ui/input/components/internal/date/utils/parseStringToDate';
|
||||
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 { UserContext } from '@/users/contexts/UserContext';
|
||||
import { isStandaloneVariableString } from '@/workflow/utils/isStandaloneVariableString';
|
||||
@ -338,7 +338,7 @@ export const FormDateFieldInput = ({
|
||||
{draftValue.mode === 'edit' ? (
|
||||
<StyledDateInputContainer>
|
||||
<StyledDateInputAbsoluteContainer>
|
||||
<StyledCalendarContainer>
|
||||
<OverlayContainer>
|
||||
<InternalDatePicker
|
||||
date={pickerDate ?? new Date()}
|
||||
isDateTimeInput={false}
|
||||
@ -349,7 +349,7 @@ export const FormDateFieldInput = ({
|
||||
onClear={handlePickerClear}
|
||||
hideHeaderInput
|
||||
/>
|
||||
</StyledCalendarContainer>
|
||||
</OverlayContainer>
|
||||
</StyledDateInputAbsoluteContainer>
|
||||
</StyledDateInputContainer>
|
||||
) : null}
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -9,6 +9,7 @@ import { SelectOption } from '@/spreadsheet-import/types';
|
||||
import { SelectDisplay } from '@/ui/field/display/components/SelectDisplay';
|
||||
import { SelectInput } from '@/ui/field/input/components/SelectInput';
|
||||
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 { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
@ -238,19 +239,21 @@ export const FormSelectFieldInput = ({
|
||||
<StyledSelectInputContainer>
|
||||
{draftValue.type === 'static' &&
|
||||
draftValue.editingMode === 'edit' && (
|
||||
<SelectInput
|
||||
selectableListId={SINGLE_RECORD_SELECT_BASE_LIST}
|
||||
selectableItemIdArray={optionIds}
|
||||
hotkeyScope={hotkeyScope}
|
||||
onEnter={handleSelectEnter}
|
||||
onOptionSelected={handleSubmit}
|
||||
options={options}
|
||||
onCancel={onCancel}
|
||||
defaultOption={selectedOption}
|
||||
onFilterChange={setFilteredOptions}
|
||||
onClear={handleClearField}
|
||||
clearLabel={clearLabel}
|
||||
/>
|
||||
<OverlayContainer>
|
||||
<SelectInput
|
||||
selectableListId={SINGLE_RECORD_SELECT_BASE_LIST}
|
||||
selectableItemIdArray={optionIds}
|
||||
hotkeyScope={hotkeyScope}
|
||||
onEnter={handleSelectEnter}
|
||||
onOptionSelected={handleSubmit}
|
||||
options={options}
|
||||
onCancel={onCancel}
|
||||
defaultOption={selectedOption}
|
||||
onFilterChange={setFilteredOptions}
|
||||
onClear={handleClearField}
|
||||
clearLabel={clearLabel}
|
||||
/>
|
||||
</OverlayContainer>
|
||||
)}
|
||||
</StyledSelectInputContainer>
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
@ -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');
|
||||
},
|
||||
};
|
@ -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"');
|
||||
},
|
||||
};
|
@ -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);
|
||||
}),
|
||||
]);
|
||||
},
|
||||
};
|
@ -1,25 +1,10 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { Tag, THEME_COMMON } from 'twenty-ui';
|
||||
import { Tag } from 'twenty-ui';
|
||||
|
||||
import { useFieldFocus } from '@/object-record/record-field/hooks/useFieldFocus';
|
||||
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';
|
||||
|
||||
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 = () => {
|
||||
const { fieldValue, fieldDefinition } = useMultiSelectFieldDisplay();
|
||||
|
||||
@ -44,15 +29,9 @@ export const MultiSelectFieldDisplay = () => {
|
||||
))}
|
||||
</ExpandableList>
|
||||
) : (
|
||||
<StyledContainer>
|
||||
{selectedOptions.map((selectedOption, index) => (
|
||||
<Tag
|
||||
preventShrink
|
||||
key={index}
|
||||
color={selectedOption.color}
|
||||
text={selectedOption.label}
|
||||
/>
|
||||
))}
|
||||
</StyledContainer>
|
||||
<MultiSelectDisplay
|
||||
values={fieldValue}
|
||||
options={fieldDefinition.metadata.options}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -4,7 +4,6 @@ import { CurrencyCode } from '@/object-record/record-field/types/CurrencyCode';
|
||||
import { FieldCurrencyValue } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { CurrencyInput } from '@/ui/field/input/components/CurrencyInput';
|
||||
|
||||
import { FieldInputOverlay } from '../../../../../ui/field/input/components/FieldInputOverlay';
|
||||
import { useCurrencyField } from '../../hooks/useCurrencyField';
|
||||
|
||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||
@ -118,21 +117,19 @@ export const CurrencyFieldInput = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<FieldInputOverlay>
|
||||
<CurrencyInput
|
||||
value={draftValue?.amount?.toString() ?? ''}
|
||||
currencyCode={currencyCode}
|
||||
autoFocus
|
||||
placeholder="Currency"
|
||||
onClickOutside={handleClickOutside}
|
||||
onEnter={handleEnter}
|
||||
onEscape={handleEscape}
|
||||
onShiftTab={handleShiftTab}
|
||||
onTab={handleTab}
|
||||
onChange={handleChange}
|
||||
onSelect={handleSelect}
|
||||
hotkeyScope={hotkeyScope}
|
||||
/>
|
||||
</FieldInputOverlay>
|
||||
<CurrencyInput
|
||||
value={draftValue?.amount?.toString() ?? ''}
|
||||
currencyCode={currencyCode}
|
||||
autoFocus
|
||||
placeholder="Currency"
|
||||
onClickOutside={handleClickOutside}
|
||||
onEnter={handleEnter}
|
||||
onEscape={handleEscape}
|
||||
onShiftTab={handleShiftTab}
|
||||
onTab={handleTab}
|
||||
onChange={handleChange}
|
||||
onSelect={handleSelect}
|
||||
hotkeyScope={hotkeyScope}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { useFullNameField } from '@/object-record/record-field/meta-types/hooks/useFullNameField';
|
||||
import { FieldDoubleText } from '@/object-record/record-field/types/FieldDoubleText';
|
||||
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 { LAST_NAME_PLACEHOLDER_WITH_SPECIAL_CHARACTER_TO_AVOID_PASSWORD_MANAGERS } from '@/object-record/record-field/meta-types/input/constants/LastNamePlaceholder';
|
||||
@ -79,25 +78,23 @@ export const FullNameFieldInput = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<FieldInputOverlay>
|
||||
<DoubleTextInput
|
||||
firstValue={draftValue?.firstName ?? ''}
|
||||
secondValue={draftValue?.lastName ?? ''}
|
||||
firstValuePlaceholder={
|
||||
FIRST_NAME_PLACEHOLDER_WITH_SPECIAL_CHARACTER_TO_AVOID_PASSWORD_MANAGERS
|
||||
}
|
||||
secondValuePlaceholder={
|
||||
LAST_NAME_PLACEHOLDER_WITH_SPECIAL_CHARACTER_TO_AVOID_PASSWORD_MANAGERS
|
||||
}
|
||||
onClickOutside={handleClickOutside}
|
||||
onEnter={handleEnter}
|
||||
onEscape={handleEscape}
|
||||
onShiftTab={handleShiftTab}
|
||||
onTab={handleTab}
|
||||
onPaste={handlePaste}
|
||||
hotkeyScope={hotkeyScope}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</FieldInputOverlay>
|
||||
<DoubleTextInput
|
||||
firstValue={draftValue?.firstName ?? ''}
|
||||
secondValue={draftValue?.lastName ?? ''}
|
||||
firstValuePlaceholder={
|
||||
FIRST_NAME_PLACEHOLDER_WITH_SPECIAL_CHARACTER_TO_AVOID_PASSWORD_MANAGERS
|
||||
}
|
||||
secondValuePlaceholder={
|
||||
LAST_NAME_PLACEHOLDER_WITH_SPECIAL_CHARACTER_TO_AVOID_PASSWORD_MANAGERS
|
||||
}
|
||||
onClickOutside={handleClickOutside}
|
||||
onEnter={handleEnter}
|
||||
onEscape={handleEscape}
|
||||
onShiftTab={handleShiftTab}
|
||||
onTab={handleTab}
|
||||
onPaste={handlePaste}
|
||||
hotkeyScope={hotkeyScope}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -1,4 +1,3 @@
|
||||
import styled from '@emotion/styled';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { Key } from 'ts-key-enum';
|
||||
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 { turnIntoEmptyStringIfWhitespacesOnly } from '~/utils/string/turnIntoEmptyStringIfWhitespacesOnly';
|
||||
|
||||
const StyledDropdownMenu = styled(DropdownMenu)`
|
||||
margin: -1px;
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
type MultiItemFieldInputProps<T> = {
|
||||
items: T[];
|
||||
onPersist: (updatedItems: T[]) => void;
|
||||
@ -164,7 +158,7 @@ export const MultiItemFieldInput = <T,>({
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledDropdownMenu ref={containerRef} width={200}>
|
||||
<DropdownMenu ref={containerRef} width={200}>
|
||||
{!!items.length && (
|
||||
<>
|
||||
<DropdownMenuItemsContainer>
|
||||
@ -222,6 +216,6 @@ export const MultiItemFieldInput = <T,>({
|
||||
/>
|
||||
</DropdownMenuItemsContainer>
|
||||
)}
|
||||
</StyledDropdownMenu>
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
|
@ -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 { MULTI_OBJECT_RECORD_SELECT_SELECTABLE_LIST_ID } from '@/object-record/relation-picker/constants/MultiObjectRecordSelectSelectableListId';
|
||||
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;
|
||||
`;
|
||||
import { MultiSelectInput } from '@/ui/field/input/components/MultiSelectInput';
|
||||
|
||||
type MultiSelectFieldInputProps = {
|
||||
onCancel?: () => void;
|
||||
@ -31,112 +8,16 @@ type MultiSelectFieldInputProps = {
|
||||
export const MultiSelectFieldInput = ({
|
||||
onCancel,
|
||||
}: 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 } =
|
||||
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 (
|
||||
<SelectableList
|
||||
selectableListId={MULTI_OBJECT_RECORD_SELECT_SELECTABLE_LIST_ID}
|
||||
selectableItemIdArray={optionIds}
|
||||
<MultiSelectInput
|
||||
hotkeyScope={hotkeyScope}
|
||||
onEnter={(itemId) => {
|
||||
const option = filteredOptionsInDropDown.find(
|
||||
(option) => option.value === itemId,
|
||||
);
|
||||
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>
|
||||
options={fieldDefinition.metadata.options}
|
||||
onCancel={onCancel}
|
||||
onOptionSelected={persistField}
|
||||
values={fieldValues}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -1,8 +1,8 @@
|
||||
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 { FieldInputContainer } from '@/ui/field/input/components/FieldInputContainer';
|
||||
import { useNumberField } from '../../hooks/useNumberField';
|
||||
|
||||
export type FieldInputEvent = (persist: () => void) => void;
|
||||
|
||||
@ -57,7 +57,7 @@ export const NumberFieldInput = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<FieldInputOverlay>
|
||||
<FieldInputContainer>
|
||||
<TextInput
|
||||
placeholder={fieldDefinition.metadata.placeHolder}
|
||||
autoFocus
|
||||
@ -70,6 +70,6 @@ export const NumberFieldInput = ({
|
||||
hotkeyScope={hotkeyScope}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</FieldInputOverlay>
|
||||
</FieldInputContainer>
|
||||
);
|
||||
};
|
||||
|
@ -18,7 +18,6 @@ export const DEFAULT_PHONE_COUNTRY_CODE = '1';
|
||||
|
||||
const StyledCustomPhoneInput = styled(ReactPhoneNumberInput)`
|
||||
font-family: ${({ theme }) => theme.font.family};
|
||||
height: 32px;
|
||||
${TEXT_INPUT_STYLE}
|
||||
padding: 0;
|
||||
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { FieldTextAreaOverlay } from '@/ui/field/input/components/FieldTextAreaOverlay';
|
||||
import { TextAreaInput } from '@/ui/field/input/components/TextAreaInput';
|
||||
|
||||
import { useJsonField } from '../../hooks/useJsonField';
|
||||
@ -59,20 +58,18 @@ export const RawJsonFieldInput = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<FieldTextAreaOverlay>
|
||||
<TextAreaInput
|
||||
placeholder={fieldDefinition.metadata.placeHolder}
|
||||
autoFocus
|
||||
value={draftValue ?? ''}
|
||||
onClickOutside={handleClickOutside}
|
||||
onEnter={handleEnter}
|
||||
onEscape={handleEscape}
|
||||
onShiftTab={handleShiftTab}
|
||||
onTab={handleTab}
|
||||
hotkeyScope={hotkeyScope}
|
||||
onChange={handleChange}
|
||||
maxRows={25}
|
||||
/>
|
||||
</FieldTextAreaOverlay>
|
||||
<TextAreaInput
|
||||
placeholder={fieldDefinition.metadata.placeHolder}
|
||||
autoFocus
|
||||
value={draftValue ?? ''}
|
||||
onClickOutside={handleClickOutside}
|
||||
onEnter={handleEnter}
|
||||
onEscape={handleEscape}
|
||||
onShiftTab={handleShiftTab}
|
||||
onTab={handleTab}
|
||||
hotkeyScope={hotkeyScope}
|
||||
onChange={handleChange}
|
||||
maxRows={25}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -1,5 +1,3 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { RelationPicker } from '@/object-record/relation-picker/components/RelationPicker';
|
||||
import { RecordForSelect } from '@/object-record/relation-picker/types/RecordForSelect';
|
||||
|
||||
@ -8,12 +6,6 @@ import { useRelationField } from '../../hooks/useRelationField';
|
||||
|
||||
import { FieldInputEvent } from './DateTimeFieldInput';
|
||||
|
||||
const StyledRelationPickerContainer = styled.div`
|
||||
left: -1px;
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
`;
|
||||
|
||||
export type RelationToOneFieldInputProps = {
|
||||
onSubmit?: FieldInputEvent;
|
||||
onCancel?: () => void;
|
||||
@ -33,14 +25,12 @@ export const RelationToOneFieldInput = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledRelationPickerContainer>
|
||||
<RelationPicker
|
||||
fieldDefinition={fieldDefinition}
|
||||
selectedRecordId={fieldValue?.id}
|
||||
onSubmit={handleSubmit}
|
||||
onCancel={onCancel}
|
||||
initialSearchFilter={initialSearchValue}
|
||||
/>
|
||||
</StyledRelationPickerContainer>
|
||||
<RelationPicker
|
||||
fieldDefinition={fieldDefinition}
|
||||
selectedRecordId={fieldValue?.id}
|
||||
onSubmit={handleSubmit}
|
||||
onCancel={onCancel}
|
||||
initialSearchFilter={initialSearchValue}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { FieldTextAreaOverlay } from '@/ui/field/input/components/FieldTextAreaOverlay';
|
||||
import { TextAreaInput } from '@/ui/field/input/components/TextAreaInput';
|
||||
|
||||
import { usePersistField } from '../../../hooks/usePersistField';
|
||||
import { useTextField } from '../../hooks/useTextField';
|
||||
|
||||
import { FieldInputContainer } from '@/ui/field/input/components/FieldInputContainer';
|
||||
import { turnIntoUndefinedIfWhitespacesOnly } from '~/utils/string/turnIntoUndefinedIfWhitespacesOnly';
|
||||
import {
|
||||
FieldInputClickOutsideEvent,
|
||||
@ -57,7 +57,7 @@ export const TextFieldInput = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<FieldTextAreaOverlay>
|
||||
<FieldInputContainer>
|
||||
<TextAreaInput
|
||||
placeholder={fieldDefinition.metadata.placeHolder}
|
||||
autoFocus
|
||||
@ -70,6 +70,6 @@ export const TextFieldInput = ({
|
||||
hotkeyScope={hotkeyScope}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</FieldTextAreaOverlay>
|
||||
</FieldInputContainer>
|
||||
);
|
||||
};
|
||||
|
@ -23,7 +23,9 @@ import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTabl
|
||||
import { SpreadsheetImportProvider } from '@/spreadsheet-import/provider/components/SpreadsheetImportProvider';
|
||||
|
||||
import { RecordIndexActionMenu } from '@/action-menu/components/RecordIndexActionMenu';
|
||||
import { ContextStoreCurrentViewTypeEffect } from '@/context-store/components/ContextStoreCurrentViewTypeEffect';
|
||||
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||
import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType';
|
||||
import { useSetRecordGroup } from '@/object-record/record-group/hooks/useSetRecordGroup';
|
||||
import { RecordIndexFiltersToContextStoreEffect } from '@/object-record/record-index/components/RecordIndexFiltersToContextStoreEffect';
|
||||
import { recordIndexKanbanAggregateOperationState } from '@/object-record/record-index/states/recordIndexKanbanAggregateOperationState';
|
||||
@ -52,9 +54,9 @@ const StyledContainer = 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)};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const RecordIndexContainer = () => {
|
||||
@ -164,89 +166,98 @@ export const RecordIndexContainer = () => {
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<InformationBannerWrapper />
|
||||
<RecordFieldValueSelectorContextProvider>
|
||||
<SpreadsheetImportProvider>
|
||||
<ViewBar
|
||||
viewBarId={recordIndexId}
|
||||
optionsDropdownButton={
|
||||
<ObjectOptionsDropdown
|
||||
recordIndexId={recordIndexId}
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
viewType={recordIndexViewType ?? ViewType.Table}
|
||||
/>
|
||||
}
|
||||
onCurrentViewChange={(view) => {
|
||||
if (!view) {
|
||||
return;
|
||||
<>
|
||||
<ContextStoreCurrentViewTypeEffect
|
||||
viewType={
|
||||
recordIndexViewType === ViewType.Table
|
||||
? ContextStoreViewType.Table
|
||||
: ContextStoreViewType.Kanban
|
||||
}
|
||||
/>
|
||||
<StyledContainer>
|
||||
<InformationBannerWrapper />
|
||||
<RecordFieldValueSelectorContextProvider>
|
||||
<SpreadsheetImportProvider>
|
||||
<ViewBar
|
||||
viewBarId={recordIndexId}
|
||||
optionsDropdownButton={
|
||||
<ObjectOptionsDropdown
|
||||
recordIndexId={recordIndexId}
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
viewType={recordIndexViewType ?? ViewType.Table}
|
||||
/>
|
||||
}
|
||||
onCurrentViewChange={(view) => {
|
||||
if (!view) {
|
||||
return;
|
||||
}
|
||||
|
||||
onViewFieldsChange(view.viewFields);
|
||||
onViewGroupsChange(view.viewGroups);
|
||||
setTableViewFilterGroups(view.viewFilterGroups ?? []);
|
||||
setTableFilters(
|
||||
mapViewFiltersToFilters(view.viewFilters, filterDefinitions),
|
||||
);
|
||||
setRecordIndexFilters(
|
||||
mapViewFiltersToFilters(view.viewFilters, filterDefinitions),
|
||||
);
|
||||
setRecordIndexViewFilterGroups(view.viewFilterGroups ?? []);
|
||||
setContextStoreTargetedRecordsRule((prev) => ({
|
||||
...prev,
|
||||
filters: mapViewFiltersToFilters(
|
||||
view.viewFilters,
|
||||
filterDefinitions,
|
||||
),
|
||||
}));
|
||||
setTableSorts(
|
||||
mapViewSortsToSorts(view.viewSorts, sortDefinitions),
|
||||
);
|
||||
setRecordIndexSorts(
|
||||
mapViewSortsToSorts(view.viewSorts, sortDefinitions),
|
||||
);
|
||||
setRecordIndexViewType(view.type);
|
||||
setRecordIndexViewKanbanFieldMetadataIdState(
|
||||
view.kanbanFieldMetadataId,
|
||||
);
|
||||
setRecordIndexViewKanbanAggregateOperationState({
|
||||
operation: view.kanbanAggregateOperation,
|
||||
fieldMetadataId: view.kanbanAggregateOperationFieldMetadataId,
|
||||
});
|
||||
setRecordIndexIsCompactModeActive(view.isCompact);
|
||||
}}
|
||||
/>
|
||||
<RecordIndexViewBarEffect
|
||||
objectNamePlural={objectNamePlural}
|
||||
viewBarId={recordIndexId}
|
||||
/>
|
||||
</SpreadsheetImportProvider>
|
||||
<RecordIndexFiltersToContextStoreEffect />
|
||||
{recordIndexViewType === ViewType.Table && (
|
||||
<>
|
||||
<RecordIndexTableContainer
|
||||
recordTableId={recordIndexId}
|
||||
onViewFieldsChange(view.viewFields);
|
||||
onViewGroupsChange(view.viewGroups);
|
||||
setTableViewFilterGroups(view.viewFilterGroups ?? []);
|
||||
setTableFilters(
|
||||
mapViewFiltersToFilters(view.viewFilters, filterDefinitions),
|
||||
);
|
||||
setRecordIndexFilters(
|
||||
mapViewFiltersToFilters(view.viewFilters, filterDefinitions),
|
||||
);
|
||||
setRecordIndexViewFilterGroups(view.viewFilterGroups ?? []);
|
||||
setContextStoreTargetedRecordsRule((prev) => ({
|
||||
...prev,
|
||||
filters: mapViewFiltersToFilters(
|
||||
view.viewFilters,
|
||||
filterDefinitions,
|
||||
),
|
||||
}));
|
||||
setTableSorts(
|
||||
mapViewSortsToSorts(view.viewSorts, sortDefinitions),
|
||||
);
|
||||
setRecordIndexSorts(
|
||||
mapViewSortsToSorts(view.viewSorts, sortDefinitions),
|
||||
);
|
||||
setRecordIndexViewType(view.type);
|
||||
setRecordIndexViewKanbanFieldMetadataIdState(
|
||||
view.kanbanFieldMetadataId,
|
||||
);
|
||||
setRecordIndexViewKanbanAggregateOperationState({
|
||||
operation: view.kanbanAggregateOperation,
|
||||
fieldMetadataId: view.kanbanAggregateOperationFieldMetadataId,
|
||||
});
|
||||
setRecordIndexIsCompactModeActive(view.isCompact);
|
||||
}}
|
||||
/>
|
||||
<RecordIndexViewBarEffect
|
||||
objectNamePlural={objectNamePlural}
|
||||
viewBarId={recordIndexId}
|
||||
/>
|
||||
<RecordIndexTableContainerEffect />
|
||||
</>
|
||||
)}
|
||||
{recordIndexViewType === ViewType.Kanban && (
|
||||
<StyledContainerWithPadding>
|
||||
<RecordIndexBoardContainer
|
||||
recordBoardId={recordIndexId}
|
||||
viewBarId={recordIndexId}
|
||||
objectNameSingular={objectNameSingular}
|
||||
/>
|
||||
<RecordIndexBoardDataLoader
|
||||
objectNameSingular={objectNameSingular}
|
||||
recordBoardId={recordIndexId}
|
||||
/>
|
||||
<RecordIndexBoardDataLoaderEffect recordBoardId={recordIndexId} />
|
||||
</StyledContainerWithPadding>
|
||||
)}
|
||||
{!isPageHeaderV2Enabled && <RecordIndexActionMenu />}
|
||||
</RecordFieldValueSelectorContextProvider>
|
||||
</StyledContainer>
|
||||
</SpreadsheetImportProvider>
|
||||
<RecordIndexFiltersToContextStoreEffect />
|
||||
{recordIndexViewType === ViewType.Table && (
|
||||
<>
|
||||
<RecordIndexTableContainer
|
||||
recordTableId={recordIndexId}
|
||||
viewBarId={recordIndexId}
|
||||
/>
|
||||
<RecordIndexTableContainerEffect />
|
||||
</>
|
||||
)}
|
||||
{recordIndexViewType === ViewType.Kanban && (
|
||||
<StyledContainerWithPadding>
|
||||
<RecordIndexBoardContainer
|
||||
recordBoardId={recordIndexId}
|
||||
viewBarId={recordIndexId}
|
||||
objectNameSingular={objectNameSingular}
|
||||
/>
|
||||
<RecordIndexBoardDataLoader
|
||||
objectNameSingular={objectNameSingular}
|
||||
recordBoardId={recordIndexId}
|
||||
/>
|
||||
<RecordIndexBoardDataLoaderEffect recordBoardId={recordIndexId} />
|
||||
</StyledContainerWithPadding>
|
||||
)}
|
||||
{!isPageHeaderV2Enabled && <RecordIndexActionMenu />}
|
||||
</RecordFieldValueSelectorContextProvider>
|
||||
</StyledContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -73,6 +73,20 @@ export const useHandleRecordGroupField = ({
|
||||
}) 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(
|
||||
(group) => group.fieldMetadataId !== fieldMetadataItem.id,
|
||||
);
|
||||
|
@ -9,6 +9,7 @@ const StyledInlineCellButtonContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
export const RecordInlineCellButton = ({ Icon }: { Icon: IconComponent }) => {
|
||||
return (
|
||||
<AnimatedContainer>
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { RecordInlineCellContext } from '@/object-record/record-inline-cell/components/RecordInlineCellContext';
|
||||
import { OverlayContainer } from '@/ui/layout/overlay/components/OverlayContainer';
|
||||
import styled from '@emotion/styled';
|
||||
import { autoUpdate, flip, offset, useFloating } from '@floating-ui/react';
|
||||
import { useContext } from 'react';
|
||||
@ -11,24 +12,14 @@ const StyledInlineCellEditModeContainer = styled.div`
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
height: 24px;
|
||||
`;
|
||||
|
||||
const StyledInlineCellInput = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
|
||||
min-height: 32px;
|
||||
|
||||
width: 240px;
|
||||
|
||||
z-index: 30;
|
||||
background: transparent;
|
||||
`;
|
||||
|
||||
type RecordInlineCellEditModeProps = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
// TODO: Refactor this to avoid setting absolute px values.
|
||||
export const RecordInlineCellEditMode = ({
|
||||
children,
|
||||
}: RecordInlineCellEditModeProps) => {
|
||||
@ -46,7 +37,7 @@ export const RecordInlineCellEditMode = ({
|
||||
}
|
||||
: {
|
||||
mainAxis: -29,
|
||||
crossAxis: -4,
|
||||
crossAxis: -5,
|
||||
},
|
||||
),
|
||||
],
|
||||
@ -59,9 +50,9 @@ export const RecordInlineCellEditMode = ({
|
||||
data-testid="inline-cell-edit-mode-container"
|
||||
>
|
||||
{createPortal(
|
||||
<StyledInlineCellInput ref={refs.setFloating} style={floatingStyles}>
|
||||
<OverlayContainer ref={refs.setFloating} style={floatingStyles}>
|
||||
{children}
|
||||
</StyledInlineCellInput>,
|
||||
</OverlayContainer>,
|
||||
document.body,
|
||||
)}
|
||||
</StyledInlineCellEditModeContainer>
|
||||
|
@ -11,6 +11,7 @@ const StyledClickableContainer = styled.div<{
|
||||
readonly?: boolean;
|
||||
isCentered?: boolean;
|
||||
}>`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
width: 100%;
|
||||
|
@ -1,4 +1,3 @@
|
||||
/* eslint-disable @nx/workspace-no-navigate-prefer-link */
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { useState } from 'react';
|
||||
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 { buildIndexTablePageURL } from '@/object-record/record-table/utils/buildIndexTableURL';
|
||||
import { useQueryVariablesFromActiveFieldsOfViewOrDefaultView } from '@/views/hooks/useQueryVariablesFromActiveFieldsOfViewOrDefaultView';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
export const useRecordShowPagePagination = (
|
||||
@ -100,22 +100,43 @@ export const useRecordShowPagePagination = (
|
||||
|
||||
const loading = loadingRecordAfter || loadingRecordBefore || loadingCursor;
|
||||
|
||||
const isThereARecordBefore = recordsBefore.length > 0;
|
||||
const isThereARecordAfter = recordsAfter.length > 0;
|
||||
|
||||
const recordBefore = recordsBefore[0];
|
||||
const recordAfter = recordsAfter[0];
|
||||
|
||||
const { recordIdsInCache } = useRecordIdsFromFindManyCacheRootQuery({
|
||||
objectNamePlural: objectMetadataItem.namePlural,
|
||||
fieldVariables: {
|
||||
filter,
|
||||
orderBy,
|
||||
},
|
||||
});
|
||||
|
||||
const navigateToPreviousRecord = () => {
|
||||
navigate(
|
||||
buildShowPageURL(objectNameSingular, recordBefore.id, viewIdQueryParam),
|
||||
);
|
||||
if (isDefined(recordBefore)) {
|
||||
navigate(
|
||||
buildShowPageURL(objectNameSingular, recordBefore.id, viewIdQueryParam),
|
||||
);
|
||||
}
|
||||
if (!loadingRecordBefore && !isDefined(recordBefore)) {
|
||||
const firstRecordId = recordIdsInCache[recordIdsInCache.length - 1];
|
||||
navigate(
|
||||
buildShowPageURL(objectNameSingular, firstRecordId, viewIdQueryParam),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const navigateToNextRecord = () => {
|
||||
navigate(
|
||||
buildShowPageURL(objectNameSingular, recordAfter.id, viewIdQueryParam),
|
||||
);
|
||||
if (isDefined(recordAfter)) {
|
||||
navigate(
|
||||
buildShowPageURL(objectNameSingular, recordAfter.id, viewIdQueryParam),
|
||||
);
|
||||
}
|
||||
if (!loadingRecordAfter && !isDefined(recordAfter)) {
|
||||
const lastRecordId = recordIdsInCache[0];
|
||||
navigate(
|
||||
buildShowPageURL(objectNameSingular, lastRecordId, viewIdQueryParam),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const navigateToIndexView = () => {
|
||||
@ -129,31 +150,21 @@ export const useRecordShowPagePagination = (
|
||||
navigate(indexTableURL);
|
||||
};
|
||||
|
||||
const { recordIdsInCache } = useRecordIdsFromFindManyCacheRootQuery({
|
||||
objectNamePlural: objectMetadataItem.namePlural,
|
||||
fieldVariables: {
|
||||
filter,
|
||||
orderBy,
|
||||
},
|
||||
});
|
||||
|
||||
const rankInView = recordIdsInCache.findIndex((id) => id === objectRecordId);
|
||||
|
||||
const rankFoundInFiew = rankInView > -1;
|
||||
const rankFoundInView = rankInView > -1;
|
||||
|
||||
const objectLabel = capitalize(objectMetadataItem.labelPlural);
|
||||
|
||||
const totalCount = Math.max(1, totalCountBefore, totalCountAfter);
|
||||
|
||||
const viewNameWithCount = rankFoundInFiew
|
||||
const viewNameWithCount = rankFoundInView
|
||||
? `${rankInView + 1} of ${totalCount} in ${objectLabel}`
|
||||
: `${objectLabel} (${totalCount})`;
|
||||
|
||||
return {
|
||||
viewName: viewNameWithCount,
|
||||
hasPreviousRecord: isThereARecordBefore,
|
||||
isLoadingPagination: loading,
|
||||
hasNextRecord: isThereARecordAfter,
|
||||
navigateToPreviousRecord,
|
||||
navigateToNextRecord,
|
||||
navigateToIndexView,
|
||||
|
@ -23,7 +23,7 @@ export const RecordTableBodyDroppable = ({
|
||||
>
|
||||
{(provided) => (
|
||||
<RecordTableBody
|
||||
id={`record-table-body${recordGroupId ? `-${recordGroupId}` : ''}`}
|
||||
id="record-table-body"
|
||||
ref={provided.innerRef}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...provided.droppableProps}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { OverlayContainer } from '@/ui/layout/overlay/components/OverlayContainer';
|
||||
import styled from '@emotion/styled';
|
||||
import { autoUpdate, flip, offset, useFloating } from '@floating-ui/react';
|
||||
import { ReactElement } from 'react';
|
||||
@ -12,16 +13,6 @@ const StyledEditableCellEditModeContainer = styled.div<RecordTableCellEditModePr
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
const StyledTableCellInput = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
|
||||
min-height: 32px;
|
||||
min-width: 200px;
|
||||
|
||||
z-index: 10;
|
||||
`;
|
||||
|
||||
export type RecordTableCellEditModeProps = {
|
||||
children: ReactElement;
|
||||
transparent?: boolean;
|
||||
@ -37,8 +28,8 @@ export const RecordTableCellEditMode = ({
|
||||
middleware: [
|
||||
flip(),
|
||||
offset({
|
||||
mainAxis: -31,
|
||||
crossAxis: -2,
|
||||
mainAxis: -33,
|
||||
crossAxis: -3,
|
||||
}),
|
||||
],
|
||||
whileElementsMounted: autoUpdate,
|
||||
@ -49,9 +40,9 @@ export const RecordTableCellEditMode = ({
|
||||
ref={refs.setReference}
|
||||
data-testid="editable-cell-edit-mode-container"
|
||||
>
|
||||
<StyledTableCellInput ref={refs.setFloating} style={floatingStyles}>
|
||||
<OverlayContainer ref={refs.setFloating} style={floatingStyles}>
|
||||
{children}
|
||||
</StyledTableCellInput>
|
||||
</OverlayContainer>
|
||||
</StyledEditableCellEditModeContainer>
|
||||
);
|
||||
};
|
||||
|
@ -65,9 +65,10 @@ export const RecordTableActionRow = ({
|
||||
<StyledIconContainer>
|
||||
<LeftIcon size={theme.icon.size.sm} color={theme.font.color.tertiary} />
|
||||
</StyledIconContainer>
|
||||
<StyledRecordTableTdTextContainer colSpan={visibleTableColumns.length}>
|
||||
<StyledRecordTableTdTextContainer>
|
||||
<StyledText>{text}</StyledText>
|
||||
</StyledRecordTableTdTextContainer>
|
||||
<StyledEmptyTd colSpan={visibleTableColumns.length - 1} />
|
||||
<StyledEmptyTd />
|
||||
<StyledEmptyTd />
|
||||
</StyledRecordTableDraggableTr>
|
||||
|
@ -7,6 +7,7 @@ import { RecordTableTr } from '@/object-record/record-table/record-table-row/com
|
||||
import { combineRefs } from '~/utils/combineRefs';
|
||||
|
||||
type RecordTableDraggableTrProps = {
|
||||
className?: string;
|
||||
draggableId: DraggableId;
|
||||
draggableIndex: number;
|
||||
isDragDisabled?: boolean;
|
||||
@ -17,47 +18,60 @@ type RecordTableDraggableTrProps = {
|
||||
export const RecordTableDraggableTr = forwardRef<
|
||||
HTMLTableRowElement,
|
||||
RecordTableDraggableTrProps
|
||||
>(({ draggableId, draggableIndex, isDragDisabled, onClick, children }, ref) => {
|
||||
const theme = useTheme();
|
||||
>(
|
||||
(
|
||||
{
|
||||
className,
|
||||
draggableId,
|
||||
draggableIndex,
|
||||
isDragDisabled,
|
||||
onClick,
|
||||
children,
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Draggable
|
||||
draggableId={draggableId}
|
||||
index={draggableIndex}
|
||||
isDragDisabled={isDragDisabled}
|
||||
>
|
||||
{(draggableProvided, draggableSnapshot) => (
|
||||
<RecordTableTr
|
||||
ref={combineRefs<HTMLTableRowElement>(
|
||||
ref,
|
||||
draggableProvided.innerRef,
|
||||
)}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...draggableProvided.draggableProps}
|
||||
style={{
|
||||
...draggableProvided.draggableProps.style,
|
||||
background: draggableSnapshot.isDragging
|
||||
? theme.background.transparent.light
|
||||
: 'none',
|
||||
borderColor: draggableSnapshot.isDragging
|
||||
? `${theme.border.color.medium}`
|
||||
: 'transparent',
|
||||
}}
|
||||
isDragging={draggableSnapshot.isDragging}
|
||||
data-testid={`row-id-${draggableId}`}
|
||||
data-selectable-id={draggableId}
|
||||
onClick={onClick}
|
||||
>
|
||||
<RecordTableRowDraggableContextProvider
|
||||
value={{
|
||||
isDragging: draggableSnapshot.isDragging,
|
||||
dragHandleProps: draggableProvided.dragHandleProps,
|
||||
return (
|
||||
<Draggable
|
||||
draggableId={draggableId}
|
||||
index={draggableIndex}
|
||||
isDragDisabled={isDragDisabled}
|
||||
>
|
||||
{(draggableProvided, draggableSnapshot) => (
|
||||
<RecordTableTr
|
||||
ref={combineRefs<HTMLTableRowElement>(
|
||||
ref,
|
||||
draggableProvided.innerRef,
|
||||
)}
|
||||
className={className}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...draggableProvided.draggableProps}
|
||||
style={{
|
||||
...draggableProvided.draggableProps.style,
|
||||
background: draggableSnapshot.isDragging
|
||||
? theme.background.transparent.light
|
||||
: 'none',
|
||||
borderColor: draggableSnapshot.isDragging
|
||||
? `${theme.border.color.medium}`
|
||||
: 'transparent',
|
||||
}}
|
||||
isDragging={draggableSnapshot.isDragging}
|
||||
data-testid={`row-id-${draggableId}`}
|
||||
data-selectable-id={draggableId}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
</RecordTableRowDraggableContextProvider>
|
||||
</RecordTableTr>
|
||||
)}
|
||||
</Draggable>
|
||||
);
|
||||
});
|
||||
<RecordTableRowDraggableContextProvider
|
||||
value={{
|
||||
isDragging: draggableSnapshot.isDragging,
|
||||
dragHandleProps: draggableProvided.dragHandleProps,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</RecordTableRowDraggableContextProvider>
|
||||
</RecordTableTr>
|
||||
)}
|
||||
</Draggable>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
@ -91,7 +91,7 @@ export const RecordTableRecordGroupSection = () => {
|
||||
<IconChevronUp size={theme.icon.size.md} />
|
||||
</motion.span>
|
||||
</StyledChevronContainer>
|
||||
<StyledRecordGroupSection colSpan={visibleColumns.length}>
|
||||
<StyledRecordGroupSection>
|
||||
<Tag
|
||||
variant={
|
||||
recordGroup.type !== RecordGroupDefinitionType.NoValue
|
||||
@ -108,6 +108,7 @@ export const RecordTableRecordGroupSection = () => {
|
||||
/>
|
||||
<StyledTotalRow>{recordIdsByGroup.length}</StyledTotalRow>
|
||||
</StyledRecordGroupSection>
|
||||
<StyledEmptyTd colSpan={visibleColumns.length - 1} />
|
||||
<StyledEmptyTd />
|
||||
<StyledEmptyTd />
|
||||
</StyledTrContainer>
|
||||
|
@ -96,6 +96,7 @@ export const MultiRecordSelect = ({
|
||||
[setSearchFilter],
|
||||
);
|
||||
|
||||
// TODO: refactor this in a separate component
|
||||
const results = (
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
<SelectableList
|
||||
@ -139,7 +140,7 @@ export const MultiRecordSelect = ({
|
||||
onSubmit?.();
|
||||
}}
|
||||
/>
|
||||
<DropdownMenu ref={containerRef} data-select-disable>
|
||||
<DropdownMenu ref={containerRef} data-select-disable width={200}>
|
||||
{dropdownPlacement?.includes('end') && (
|
||||
<>
|
||||
{isDefined(onCreate) && (
|
||||
|
@ -9,12 +9,10 @@ import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useLis
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export type SingleRecordSelectProps = {
|
||||
disableBackgroundBlur?: boolean;
|
||||
width?: number;
|
||||
} & SingleRecordSelectMenuItemsWithSearchProps;
|
||||
|
||||
export const SingleRecordSelect = ({
|
||||
disableBackgroundBlur = false,
|
||||
EmptyIcon,
|
||||
emptyLabel,
|
||||
excludedRecordIds,
|
||||
@ -22,7 +20,6 @@ export const SingleRecordSelect = ({
|
||||
onCreate,
|
||||
onRecordSelected,
|
||||
objectNameSingular,
|
||||
recordPickerInstanceId,
|
||||
selectedRecordIds,
|
||||
width = 200,
|
||||
}: SingleRecordSelectProps) => {
|
||||
@ -45,12 +42,7 @@ export const SingleRecordSelect = ({
|
||||
});
|
||||
|
||||
return (
|
||||
<DropdownMenu
|
||||
disableBlur={disableBackgroundBlur}
|
||||
ref={containerRef}
|
||||
width={width}
|
||||
data-select-disable
|
||||
>
|
||||
<DropdownMenu ref={containerRef} width={width} data-select-disable>
|
||||
<SingleRecordSelectMenuItemsWithSearch
|
||||
{...{
|
||||
EmptyIcon,
|
||||
@ -60,7 +52,6 @@ export const SingleRecordSelect = ({
|
||||
onCreate,
|
||||
onRecordSelected,
|
||||
objectNameSingular,
|
||||
recordPickerInstanceId,
|
||||
selectedRecordIds,
|
||||
}}
|
||||
/>
|
||||
|
@ -4,10 +4,12 @@ import {
|
||||
} from '@/object-record/relation-picker/components/SingleRecordSelectMenuItems';
|
||||
import { useRecordPickerRecordsOptions } from '@/object-record/relation-picker/hooks/useRecordPickerRecordsOptions';
|
||||
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 { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
|
||||
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 { IconPlus } from 'twenty-ui';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
@ -37,13 +39,14 @@ export const SingleRecordSelectMenuItemsWithSearch = ({
|
||||
onCreate,
|
||||
onRecordSelected,
|
||||
objectNameSingular,
|
||||
recordPickerInstanceId = 'record-picker',
|
||||
selectedRecordIds,
|
||||
dropdownPlacement,
|
||||
}: SingleRecordSelectMenuItemsWithSearchProps) => {
|
||||
const { handleSearchFilterChange } = useRecordSelectSearch({
|
||||
recordPickerInstanceId,
|
||||
});
|
||||
const { handleSearchFilterChange } = useRecordSelectSearch();
|
||||
|
||||
const recordPickerInstanceId = useAvailableComponentInstanceIdOrThrow(
|
||||
RecordPickerComponentInstanceContext,
|
||||
);
|
||||
|
||||
const { records, recordPickerSearchFilter } = useRecordPickerRecordsOptions({
|
||||
objectNameSingular,
|
||||
|
@ -60,6 +60,7 @@ export const useFilteredSearchRecordQuery = ({
|
||||
filter: notFilter,
|
||||
limit: limit ?? DEFAULT_SEARCH_REQUEST_LIMIT,
|
||||
searchInput: searchFilter,
|
||||
fetchPolicy: 'cache-and-network',
|
||||
});
|
||||
|
||||
return {
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { EnvironmentVariable } from '@/settings/serverless-functions/components/tabs/SettingsServerlessFunctionTabEnvironmentVariablesSection';
|
||||
import { TextInputV2 } from '@/ui/input/components/TextInputV2';
|
||||
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 { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||
import { TableCell } from '@/ui/layout/table/components/TableCell';
|
||||
@ -101,7 +100,6 @@ export const SettingsServerlessFunctionTabEnvironmentVariableTableRow = ({
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Dropdown
|
||||
dropdownMenuWidth="100px"
|
||||
dropdownId={dropDownId}
|
||||
clickableComponent={
|
||||
<LightIconButton
|
||||
@ -111,26 +109,24 @@ export const SettingsServerlessFunctionTabEnvironmentVariableTableRow = ({
|
||||
/>
|
||||
}
|
||||
dropdownComponents={
|
||||
<DropdownMenu disableBlur disableBorder width="auto">
|
||||
<DropdownMenuItemsContainer>
|
||||
<MenuItem
|
||||
text={'Edit'}
|
||||
LeftIcon={IconPencil}
|
||||
onClick={() => {
|
||||
setEditMode(true);
|
||||
closeDropdown();
|
||||
}}
|
||||
/>
|
||||
<MenuItem
|
||||
text={'Delete'}
|
||||
LeftIcon={IconTrash}
|
||||
onClick={() => {
|
||||
onDelete();
|
||||
closeDropdown();
|
||||
}}
|
||||
/>
|
||||
</DropdownMenuItemsContainer>
|
||||
</DropdownMenu>
|
||||
<DropdownMenuItemsContainer>
|
||||
<MenuItem
|
||||
text={'Edit'}
|
||||
LeftIcon={IconPencil}
|
||||
onClick={() => {
|
||||
setEditMode(true);
|
||||
closeDropdown();
|
||||
}}
|
||||
/>
|
||||
<MenuItem
|
||||
text={'Delete'}
|
||||
LeftIcon={IconTrash}
|
||||
onClick={() => {
|
||||
onDelete();
|
||||
closeDropdown();
|
||||
}}
|
||||
/>
|
||||
</DropdownMenuItemsContainer>
|
||||
}
|
||||
dropdownHotkeyScope={{
|
||||
scope: dropDownId,
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
@ -15,11 +15,6 @@ import { useRecoilValue } from 'recoil';
|
||||
import { isDefined, MOBILE_VIEWPORT } from 'twenty-ui';
|
||||
|
||||
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;
|
||||
|
||||
width: 344px;
|
||||
@ -27,11 +22,6 @@ const StyledAddressContainer = styled.div`
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
input {
|
||||
background-color: ${({ theme }) => theme.background.transparent.secondary};
|
||||
backdrop-filter: ${({ theme }) => theme.blur.medium};
|
||||
}
|
||||
|
||||
@media (max-width: ${MOBILE_VIEWPORT}px) {
|
||||
width: auto;
|
||||
min-width: 100px;
|
||||
|
@ -21,10 +21,6 @@ export const StyledIMaskInput = styled(IMaskInput)<StyledInputProps>`
|
||||
const StyledContainer = styled.div`
|
||||
align-items: center;
|
||||
|
||||
border: none;
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
box-shadow: ${({ theme }) => theme.boxShadow.strong};
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { useRef, useState } from 'react';
|
||||
import { Nullable } from 'twenty-ui';
|
||||
|
||||
import {
|
||||
@ -9,15 +9,6 @@ import {
|
||||
} from '@/ui/input/components/internal/date/components/InternalDatePicker';
|
||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||
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 = {
|
||||
value: Nullable<Date>;
|
||||
@ -89,19 +80,17 @@ export const DateInput = ({
|
||||
|
||||
return (
|
||||
<div ref={wrapperRef}>
|
||||
<StyledCalendarContainer>
|
||||
<InternalDatePicker
|
||||
date={internalValue ?? new Date()}
|
||||
onChange={handleChange}
|
||||
onMouseSelect={handleMouseSelect}
|
||||
clearable={clearable ? clearable : false}
|
||||
isDateTimeInput={isDateTimeInput}
|
||||
onEnter={onEnter}
|
||||
onEscape={onEscape}
|
||||
onClear={handleClear}
|
||||
hideHeaderInput={hideHeaderInput}
|
||||
/>
|
||||
</StyledCalendarContainer>
|
||||
<InternalDatePicker
|
||||
date={internalValue ?? new Date()}
|
||||
onChange={handleChange}
|
||||
onMouseSelect={handleMouseSelect}
|
||||
clearable={clearable ? clearable : false}
|
||||
isDateTimeInput={isDateTimeInput}
|
||||
onEnter={onEnter}
|
||||
onEscape={onEscape}
|
||||
onClear={handleClear}
|
||||
hideHeaderInput={hideHeaderInput}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -12,22 +12,18 @@ import { FieldDoubleText } from '@/object-record/record-field/types/FieldDoubleT
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
import { FieldInputContainer } from '@/ui/field/input/components/FieldInputContainer';
|
||||
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||
import { splitFullName } from '~/utils/format/spiltFullName';
|
||||
import { turnIntoEmptyStringIfWhitespacesOnly } from '~/utils/string/turnIntoEmptyStringIfWhitespacesOnly';
|
||||
import { StyledTextInput } from './TextInput';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
& > 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)};
|
||||
}
|
||||
`;
|
||||
@ -186,39 +182,41 @@ export const DoubleTextInput = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledContainer ref={containerRef}>
|
||||
<StyledTextInput
|
||||
autoComplete="off"
|
||||
autoFocus
|
||||
onFocus={() => setFocusPosition('left')}
|
||||
ref={firstValueInputRef}
|
||||
placeholder={firstValuePlaceholder}
|
||||
value={firstInternalValue}
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||
handleChange(
|
||||
turnIntoEmptyStringIfWhitespacesOnly(event.target.value),
|
||||
secondInternalValue,
|
||||
);
|
||||
}}
|
||||
onPaste={(event: ClipboardEvent<HTMLInputElement>) =>
|
||||
handleOnPaste(event)
|
||||
}
|
||||
onClick={handleClickToPreventParentClickEvents}
|
||||
/>
|
||||
<StyledTextInput
|
||||
autoComplete="off"
|
||||
onFocus={() => setFocusPosition('right')}
|
||||
ref={secondValueInputRef}
|
||||
placeholder={secondValuePlaceholder}
|
||||
value={secondInternalValue}
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||
handleChange(
|
||||
firstInternalValue,
|
||||
turnIntoEmptyStringIfWhitespacesOnly(event.target.value),
|
||||
);
|
||||
}}
|
||||
onClick={handleClickToPreventParentClickEvents}
|
||||
/>
|
||||
</StyledContainer>
|
||||
<FieldInputContainer>
|
||||
<StyledContainer ref={containerRef}>
|
||||
<StyledTextInput
|
||||
autoComplete="off"
|
||||
autoFocus
|
||||
onFocus={() => setFocusPosition('left')}
|
||||
ref={firstValueInputRef}
|
||||
placeholder={firstValuePlaceholder}
|
||||
value={firstInternalValue}
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||
handleChange(
|
||||
turnIntoEmptyStringIfWhitespacesOnly(event.target.value),
|
||||
secondInternalValue,
|
||||
);
|
||||
}}
|
||||
onPaste={(event: ClipboardEvent<HTMLInputElement>) =>
|
||||
handleOnPaste(event)
|
||||
}
|
||||
onClick={handleClickToPreventParentClickEvents}
|
||||
/>
|
||||
<StyledTextInput
|
||||
autoComplete="off"
|
||||
onFocus={() => setFocusPosition('right')}
|
||||
ref={secondValueInputRef}
|
||||
placeholder={secondValuePlaceholder}
|
||||
value={secondInternalValue}
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||
handleChange(
|
||||
firstInternalValue,
|
||||
turnIntoEmptyStringIfWhitespacesOnly(event.target.value),
|
||||
);
|
||||
}}
|
||||
onClick={handleClickToPreventParentClickEvents}
|
||||
/>
|
||||
</StyledContainer>
|
||||
</FieldInputContainer>
|
||||
);
|
||||
};
|
||||
|
@ -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%;
|
||||
`;
|
@ -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;
|
@ -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;
|
@ -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>
|
||||
);
|
||||
};
|
@ -33,33 +33,14 @@ const StyledTextArea = styled(TextareaAutosize)`
|
||||
resize: none;
|
||||
max-height: 400px;
|
||||
width: calc(100% - ${({ theme }) => theme.spacing(7)});
|
||||
background: transparent;
|
||||
|
||||
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`
|
||||
background: transparent;
|
||||
position: absolute;
|
||||
top: 18px;
|
||||
top: 16px;
|
||||
transform: translateY(-50%);
|
||||
right: 0;
|
||||
`;
|
||||
@ -114,7 +95,7 @@ export const TextAreaInput = ({
|
||||
});
|
||||
|
||||
return (
|
||||
<StyledTextAreaContainer>
|
||||
<>
|
||||
<StyledTextArea
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
@ -130,6 +111,6 @@ export const TextAreaInput = ({
|
||||
<LightCopyIconButton copyText={internalText} />
|
||||
</StyledLightIconButtonContainer>
|
||||
)}
|
||||
</StyledTextAreaContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -3,11 +3,11 @@ import { useMemo, useState } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import {
|
||||
IconApps,
|
||||
IconComponent,
|
||||
useIcons,
|
||||
IconButton,
|
||||
IconButtonVariant,
|
||||
IconComponent,
|
||||
LightIconButton,
|
||||
useIcons,
|
||||
} from 'twenty-ui';
|
||||
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
@ -33,7 +33,6 @@ export type IconPickerProps = {
|
||||
onOpen?: () => void;
|
||||
variant?: IconButtonVariant;
|
||||
className?: string;
|
||||
disableBlur?: boolean;
|
||||
};
|
||||
|
||||
const StyledMenuIconItemsContainer = styled.div`
|
||||
@ -90,7 +89,6 @@ export const IconPicker = ({
|
||||
onClose,
|
||||
onOpen,
|
||||
variant = 'secondary',
|
||||
disableBlur = false,
|
||||
className,
|
||||
}: IconPickerProps) => {
|
||||
const [searchString, setSearchString] = useState('');
|
||||
@ -172,7 +170,6 @@ export const IconPicker = ({
|
||||
/>
|
||||
}
|
||||
dropdownMenuWidth={176}
|
||||
disableBlur={disableBlur}
|
||||
dropdownComponents={
|
||||
<SelectableList
|
||||
selectableListId="icon-list"
|
||||
|
@ -32,7 +32,6 @@ export type SelectProps<Value extends SelectValue> = {
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
selectSizeVariant?: SelectSizeVariant;
|
||||
disableBlur?: boolean;
|
||||
dropdownId: string;
|
||||
dropdownWidth?: `${string}px` | 'auto' | number;
|
||||
dropdownWidthAuto?: boolean;
|
||||
@ -63,7 +62,6 @@ export const Select = <Value extends SelectValue>({
|
||||
className,
|
||||
disabled: disabledFromProps,
|
||||
selectSizeVariant,
|
||||
disableBlur = false,
|
||||
dropdownId,
|
||||
dropdownWidth = 176,
|
||||
dropdownWidthAuto = false,
|
||||
@ -135,7 +133,6 @@ export const Select = <Value extends SelectValue>({
|
||||
selectSizeVariant={selectSizeVariant}
|
||||
/>
|
||||
}
|
||||
disableBlur={disableBlur}
|
||||
dropdownComponents={
|
||||
<>
|
||||
{!!withSearchInput && (
|
||||
|
@ -32,7 +32,7 @@ export const CurrencyPickerDropdownSelect = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<DropdownMenu disableBlur>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuSearchInput
|
||||
value={searchFilter}
|
||||
onChange={(event) => setSearchFilter(event.target.value)}
|
||||
|
@ -81,7 +81,6 @@ export const AbsoluteDatePickerHeader = ({
|
||||
<Select
|
||||
dropdownId={MONTH_AND_YEAR_DROPDOWN_MONTH_SELECT_ID}
|
||||
options={getMonthSelectOptions()}
|
||||
disableBlur
|
||||
onChange={onChangeMonth}
|
||||
value={endOfDayInLocalTimezone.getMonth()}
|
||||
fullWidth
|
||||
@ -91,7 +90,6 @@ export const AbsoluteDatePickerHeader = ({
|
||||
onChange={onChangeYear}
|
||||
value={endOfDayInLocalTimezone.getFullYear()}
|
||||
options={years}
|
||||
disableBlur
|
||||
fullWidth
|
||||
/>
|
||||
<LightIconButton
|
||||
|
@ -16,8 +16,6 @@ import { isDefined } from 'twenty-ui';
|
||||
|
||||
const StyledInputContainer = styled.div`
|
||||
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-top-left-radius: ${({ theme }) => theme.border.radius.md};
|
||||
border-top-right-radius: ${({ theme }) => theme.border.radius.md};
|
||||
|
@ -5,7 +5,6 @@ import { Key } from 'ts-key-enum';
|
||||
import {
|
||||
IconCalendarX,
|
||||
MenuItemLeftContent,
|
||||
OVERLAY_BACKGROUND,
|
||||
StyledHoverableMenuItemBase,
|
||||
} from 'twenty-ui';
|
||||
|
||||
@ -122,8 +121,6 @@ const StyledContainer = styled.div<{ calendarDisabled?: boolean }>`
|
||||
|
||||
& .react-datepicker__month-dropdown,
|
||||
& .react-datepicker__year-dropdown {
|
||||
border: ${({ theme }) => theme.border.color.light};
|
||||
${OVERLAY_BACKGROUND}
|
||||
overflow-y: scroll;
|
||||
top: ${({ theme }) => theme.spacing(2)};
|
||||
}
|
||||
|
@ -57,7 +57,6 @@ export const RelativeDatePickerHeader = (
|
||||
return (
|
||||
<StyledContainer>
|
||||
<Select
|
||||
disableBlur
|
||||
dropdownId="direction-select"
|
||||
value={direction}
|
||||
onChange={(newDirection) => {
|
||||
@ -95,7 +94,6 @@ export const RelativeDatePickerHeader = (
|
||||
disabled={direction === 'THIS'}
|
||||
/>
|
||||
<Select
|
||||
disableBlur
|
||||
dropdownId="unit-select"
|
||||
value={unit}
|
||||
onChange={(newUnit) => {
|
||||
|
@ -2,7 +2,6 @@ import styled from '@emotion/styled';
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
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 { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
|
||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||
@ -47,7 +46,7 @@ export const PhoneCountryPickerDropdownSelect = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<DropdownMenu width="auto" disableBlur>
|
||||
<>
|
||||
<DropdownMenuSearchInput
|
||||
value={searchFilter}
|
||||
onChange={(event) => setSearchFilter(event.currentTarget.value)}
|
||||
@ -91,6 +90,6 @@ export const PhoneCountryPickerDropdownSelect = ({
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuItemsContainer>
|
||||
</DropdownMenu>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -53,7 +53,7 @@ const StyledEditor = styled.div`
|
||||
}
|
||||
& .bn-drag-handle-menu {
|
||||
background: ${({ theme }) => theme.background.transparent.secondary};
|
||||
backdrop-filter: blur(12px) saturate(200%) contrast(50%) brightness(130%);
|
||||
backdrop-filter: ${({ theme }) => theme.blur.medium};
|
||||
box-shadow:
|
||||
0px 2px 4px rgba(0, 0, 0, 0.04),
|
||||
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};
|
||||
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 {
|
||||
background-color: transparent;
|
||||
min-width: 152px;
|
||||
|
@ -4,6 +4,9 @@ import { IconComponent, MenuItemSuggestion } from 'twenty-ui';
|
||||
|
||||
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
|
||||
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 = {
|
||||
title: string;
|
||||
@ -14,28 +17,43 @@ export type SuggestionItem = {
|
||||
|
||||
type CustomSlashMenuProps = SuggestionMenuProps<SuggestionItem>;
|
||||
|
||||
const StyledSlashMenu = styled.div`
|
||||
* {
|
||||
box-sizing: content-box;
|
||||
}
|
||||
const StyledContainer = styled.div`
|
||||
height: 1px;
|
||||
width: 1px;
|
||||
`;
|
||||
|
||||
const StyledInnerContainer = styled.div`
|
||||
height: 250px;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const CustomSlashMenu = (props: CustomSlashMenuProps) => {
|
||||
const { refs, floatingStyles } = useFloating({
|
||||
placement: 'bottom-start',
|
||||
});
|
||||
|
||||
return (
|
||||
<StyledSlashMenu>
|
||||
<DropdownMenu style={{ zIndex: 2001 }}>
|
||||
<DropdownMenuItemsContainer>
|
||||
{props.items.map((item, index) => (
|
||||
<MenuItemSuggestion
|
||||
key={item.title}
|
||||
onClick={() => item.onItemClick()}
|
||||
text={item.title}
|
||||
LeftIcon={item.Icon}
|
||||
selected={props.selectedIndex === index}
|
||||
/>
|
||||
))}
|
||||
</DropdownMenuItemsContainer>
|
||||
</DropdownMenu>
|
||||
</StyledSlashMenu>
|
||||
<StyledContainer ref={refs.setReference}>
|
||||
{createPortal(
|
||||
<OverlayContainer ref={refs.setFloating} style={floatingStyles}>
|
||||
<StyledInnerContainer>
|
||||
<DropdownMenu style={{ zIndex: 2001 }}>
|
||||
<DropdownMenuItemsContainer>
|
||||
{props.items.map((item, index) => (
|
||||
<MenuItemSuggestion
|
||||
key={item.title}
|
||||
onClick={() => item.onItemClick()}
|
||||
text={item.title}
|
||||
LeftIcon={item.Icon}
|
||||
selected={props.selectedIndex === index}
|
||||
/>
|
||||
))}
|
||||
</DropdownMenuItemsContainer>
|
||||
</DropdownMenu>
|
||||
</StyledInnerContainer>
|
||||
</OverlayContainer>,
|
||||
document.body,
|
||||
)}
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
@ -37,7 +37,6 @@ type DropdownProps = {
|
||||
dropdownMenuWidth?: `${string}px` | `${number}%` | 'auto' | number;
|
||||
dropdownOffset?: { x?: number; y?: number };
|
||||
dropdownStrategy?: 'fixed' | 'absolute';
|
||||
disableBlur?: boolean;
|
||||
onClickOutside?: () => void;
|
||||
onClose?: () => void;
|
||||
onOpen?: () => void;
|
||||
@ -55,7 +54,6 @@ export const Dropdown = ({
|
||||
dropdownPlacement = 'bottom-end',
|
||||
dropdownStrategy = 'absolute',
|
||||
dropdownOffset = { x: 0, y: 0 },
|
||||
disableBlur = false,
|
||||
onClickOutside,
|
||||
onClose,
|
||||
onOpen,
|
||||
@ -123,7 +121,6 @@ export const Dropdown = ({
|
||||
<DropdownContent
|
||||
className={className}
|
||||
floatingStyles={floatingStyles}
|
||||
disableBlur={disableBlur}
|
||||
dropdownMenuWidth={dropdownMenuWidth}
|
||||
dropdownComponents={dropdownComponents}
|
||||
dropdownId={dropdownId}
|
||||
|
@ -3,12 +3,14 @@ import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||
import { useInternalHotkeyScopeManagement } from '@/ui/layout/dropdown/hooks/useInternalHotkeyScopeManagement';
|
||||
import { activeDropdownFocusIdState } from '@/ui/layout/dropdown/states/activeDropdownFocusIdState';
|
||||
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 { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
||||
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||
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 {
|
||||
FloatingPortal,
|
||||
Placement,
|
||||
@ -19,6 +21,11 @@ import { Keys } from 'react-hotkeys-hook';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { Key } from 'ts-key-enum';
|
||||
|
||||
export const StyledDropdownContentContainer = styled.div`
|
||||
display: flex;
|
||||
z-index: 30;
|
||||
`;
|
||||
|
||||
export type DropdownContentProps = {
|
||||
className?: string;
|
||||
dropdownId: string;
|
||||
@ -32,7 +39,6 @@ export type DropdownContentProps = {
|
||||
scope: string;
|
||||
};
|
||||
onHotkeyTriggered?: () => void;
|
||||
disableBlur?: boolean;
|
||||
dropdownMenuWidth?: `${string}px` | `${number}%` | 'auto' | number;
|
||||
dropdownComponents: React.ReactNode;
|
||||
parentDropdownId?: string;
|
||||
@ -49,7 +55,6 @@ export const DropdownContent = ({
|
||||
floatingStyles,
|
||||
hotkey,
|
||||
onHotkeyTriggered,
|
||||
disableBlur,
|
||||
dropdownMenuWidth,
|
||||
dropdownComponents,
|
||||
avoidPortal,
|
||||
@ -59,7 +64,7 @@ export const DropdownContent = ({
|
||||
|
||||
const activeDropdownFocusId = useRecoilValue(activeDropdownFocusIdState);
|
||||
|
||||
const [dropdownMaxHeight] = useRecoilComponentStateV2(
|
||||
const dropdownMaxHeight = useRecoilComponentValueV2(
|
||||
dropdownMaxHeightComponentStateV2,
|
||||
dropdownId,
|
||||
);
|
||||
@ -114,28 +119,36 @@ export const DropdownContent = ({
|
||||
<HotkeyEffect hotkey={hotkey} onHotkeyTriggered={onHotkeyTriggered} />
|
||||
)}
|
||||
{avoidPortal ? (
|
||||
<DropdownMenu
|
||||
className={className}
|
||||
disableBlur={disableBlur}
|
||||
width={dropdownMenuWidth ?? dropdownWidth}
|
||||
data-select-disable
|
||||
<StyledDropdownContentContainer
|
||||
ref={floatingUiRefs.setFloating}
|
||||
style={dropdownMenuStyles}
|
||||
>
|
||||
{dropdownComponents}
|
||||
</DropdownMenu>
|
||||
<OverlayContainer>
|
||||
<DropdownMenu
|
||||
className={className}
|
||||
width={dropdownMenuWidth ?? dropdownWidth}
|
||||
data-select-disable
|
||||
>
|
||||
{dropdownComponents}
|
||||
</DropdownMenu>
|
||||
</OverlayContainer>
|
||||
</StyledDropdownContentContainer>
|
||||
) : (
|
||||
<FloatingPortal>
|
||||
<DropdownMenu
|
||||
className={className}
|
||||
disableBlur={disableBlur}
|
||||
width={dropdownMenuWidth ?? dropdownWidth}
|
||||
data-select-disable
|
||||
<StyledDropdownContentContainer
|
||||
ref={floatingUiRefs.setFloating}
|
||||
style={dropdownMenuStyles}
|
||||
>
|
||||
{dropdownComponents}
|
||||
</DropdownMenu>
|
||||
<OverlayContainer>
|
||||
<DropdownMenu
|
||||
className={className}
|
||||
width={dropdownMenuWidth ?? dropdownWidth}
|
||||
data-select-disable
|
||||
>
|
||||
{dropdownComponents}
|
||||
</DropdownMenu>
|
||||
</OverlayContainer>
|
||||
</StyledDropdownContentContainer>
|
||||
</FloatingPortal>
|
||||
)}
|
||||
</>
|
||||
|
@ -1,29 +1,12 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
const StyledDropdownMenu = styled.div<{
|
||||
disableBlur?: boolean;
|
||||
disableBorder?: boolean;
|
||||
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;
|
||||
|
||||
flex-direction: column;
|
||||
z-index: 30;
|
||||
height: 100%;
|
||||
width: ${({ width = 200 }) =>
|
||||
typeof width === 'number' ? `${width}px` : width};
|
||||
`;
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user