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