From 074cd22a67825897e8fcd28ca4fd3a692c794b2f Mon Sep 17 00:00:00 2001 From: Weiko Date: Tue, 24 Jun 2025 14:06:50 +0200 Subject: [PATCH] [Permissions] Force open title input for role label when empty (#12710) - Fix empty title in breadcrumb - Enforce role label input open if empty --- .../components/SettingsRoleSettings.tsx | 2 +- .../roles/role/components/SettingsRole.tsx | 26 +++++++++++--- .../components/SettingsRoleLabelContainer.tsx | 5 ++- .../SettingsRoleLabelContainerEffect.tsx | 34 +++++++++++++++++++ .../ui/input/components/TitleInput.tsx | 15 +++++--- .../__stories__/TitleInput.stories.tsx | 1 + .../titleInputComponentInstanceContext.ts | 4 +++ .../selectors/titleInputComponentSelector.ts | 17 ++++++++++ .../input/states/titleInputComponentState.ts | 8 +++++ .../components/WorkflowStepHeader.tsx | 1 + 10 files changed, 102 insertions(+), 11 deletions(-) create mode 100644 packages/twenty-front/src/modules/settings/roles/role/components/SettingsRoleLabelContainerEffect.tsx create mode 100644 packages/twenty-front/src/modules/ui/input/contexts/titleInputComponentInstanceContext.ts create mode 100644 packages/twenty-front/src/modules/ui/input/states/selectors/titleInputComponentSelector.ts create mode 100644 packages/twenty-front/src/modules/ui/input/states/titleInputComponentState.ts diff --git a/packages/twenty-front/src/modules/settings/roles/role-settings/components/SettingsRoleSettings.tsx b/packages/twenty-front/src/modules/settings/roles/role-settings/components/SettingsRoleSettings.tsx index e003a860d..b8e6f30f2 100644 --- a/packages/twenty-front/src/modules/settings/roles/role-settings/components/SettingsRoleSettings.tsx +++ b/packages/twenty-front/src/modules/settings/roles/role-settings/components/SettingsRoleSettings.tsx @@ -68,7 +68,7 @@ export const SettingsRoleSettings = ({ label: value, }); }} - placeholder={t`Role name`} + placeholder={t`Untitled Role`} disabled={!isEditable} /> diff --git a/packages/twenty-front/src/modules/settings/roles/role/components/SettingsRole.tsx b/packages/twenty-front/src/modules/settings/roles/role/components/SettingsRole.tsx index dcbe5cac9..7eb5b8152 100644 --- a/packages/twenty-front/src/modules/settings/roles/role/components/SettingsRole.tsx +++ b/packages/twenty-front/src/modules/settings/roles/role/components/SettingsRole.tsx @@ -7,6 +7,7 @@ import { SettingsRoleAssignment } from '@/settings/roles/role-assignment/compone import { SettingsRolePermissions } from '@/settings/roles/role-permissions/components/SettingsRolePermissions'; import { SettingsRoleSettings } from '@/settings/roles/role-settings/components/SettingsRoleSettings'; import { SettingsRoleLabelContainer } from '@/settings/roles/role/components/SettingsRoleLabelContainer'; +import { SettingsRoleLabelContainerEffect } from '@/settings/roles/role/components/SettingsRoleLabelContainerEffect'; import { SETTINGS_ROLE_DETAIL_TABS } from '@/settings/roles/role/constants/SettingsRoleDetailTabs'; import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState'; import { settingsPersistedRoleFamilyState } from '@/settings/roles/states/settingsPersistedRoleFamilyState'; @@ -19,6 +20,7 @@ import { TabList } from '@/ui/layout/tab-list/components/TabList'; import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { getOperationName } from '@apollo/client/utilities'; +import styled from '@emotion/styled'; import { t } from '@lingui/core/macro'; import { useRecoilState, useRecoilValue } from 'recoil'; import { isDefined } from 'twenty-shared/utils'; @@ -52,6 +54,10 @@ const ROLE_BASIC_KEYS: Array = [ 'canDestroyAllObjectRecords', ]; +const StyledUntitledRole = styled.span` + color: ${({ theme }) => theme.font.color.tertiary}; +`; + export const SettingsRole = ({ roleId, isCreateMode }: SettingsRoleProps) => { const activeTabId = useRecoilComponentValueV2( activeTabIdComponentState, @@ -275,7 +281,12 @@ export const SettingsRole = ({ roleId, isCreateMode }: SettingsRoleProps) => { return ( } + title={ + <> + + + + } links={[ { children: 'Workspace', @@ -286,14 +297,19 @@ export const SettingsRole = ({ roleId, isCreateMode }: SettingsRoleProps) => { href: getSettingsPath(SettingsPath.Roles), }, { - children: settingsDraftRole.label, + children: + isDefined(settingsDraftRole.label) && + settingsDraftRole.label !== '' ? ( + settingsDraftRole.label + ) : ( + {t`Untitled Role`} + ), }, ]} actionButton={ - isRoleEditable && - isDirty && ( + isRoleEditable && isDirty ? ( - ) + ) : null } > diff --git a/packages/twenty-front/src/modules/settings/roles/role/components/SettingsRoleLabelContainer.tsx b/packages/twenty-front/src/modules/settings/roles/role/components/SettingsRoleLabelContainer.tsx index d161fd633..33ce90f42 100644 --- a/packages/twenty-front/src/modules/settings/roles/role/components/SettingsRoleLabelContainer.tsx +++ b/packages/twenty-front/src/modules/settings/roles/role/components/SettingsRoleLabelContainer.tsx @@ -29,6 +29,8 @@ export const SettingsRoleLabelContainer = ({ settingsDraftRoleFamilyState(roleId), ); + const titleInputInstanceId = `settings-role-label-${roleId}`; + const handleChange = (newValue: string) => { setSettingsDraftRole({ ...settingsDraftRole, @@ -43,8 +45,9 @@ export const SettingsRoleLabelContainer = ({ sizeVariant="md" value={settingsDraftRole.label} onChange={handleChange} - placeholder={t`Role name`} + placeholder={t`Untitled Role`} hotkeyScope={ROLE_LABEL_EDIT_HOTKEY_SCOPE} + instanceId={titleInputInstanceId} /> ); diff --git a/packages/twenty-front/src/modules/settings/roles/role/components/SettingsRoleLabelContainerEffect.tsx b/packages/twenty-front/src/modules/settings/roles/role/components/SettingsRoleLabelContainerEffect.tsx new file mode 100644 index 000000000..0f666a9b9 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/roles/role/components/SettingsRoleLabelContainerEffect.tsx @@ -0,0 +1,34 @@ +import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState'; +import { titleInputComponentState } from '@/ui/input/states/titleInputComponentState'; +import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2'; +import { useEffect } from 'react'; +import { useRecoilValue } from 'recoil'; + +export const SettingsRoleLabelContainerEffect = ({ + roleId, +}: { + roleId: string; +}) => { + const settingsDraftRole = useRecoilValue( + settingsDraftRoleFamilyState(roleId), + ); + const titleInputInstanceId = `settings-role-label-${roleId}`; + + const [isTitleInputOpen, setIsTitleInputOpen] = useRecoilComponentStateV2( + titleInputComponentState, + titleInputInstanceId, + ); + + useEffect(() => { + if (settingsDraftRole.label === '' && !isTitleInputOpen) { + setIsTitleInputOpen(true); + } + }, [ + settingsDraftRole.label, + setIsTitleInputOpen, + titleInputInstanceId, + isTitleInputOpen, + ]); + + return <>; +}; diff --git a/packages/twenty-front/src/modules/ui/input/components/TitleInput.tsx b/packages/twenty-front/src/modules/ui/input/components/TitleInput.tsx index c6c544f6d..b677b34c1 100644 --- a/packages/twenty-front/src/modules/ui/input/components/TitleInput.tsx +++ b/packages/twenty-front/src/modules/ui/input/components/TitleInput.tsx @@ -6,7 +6,9 @@ import { useRef, useState } from 'react'; import { isDefined } from 'twenty-shared/utils'; import { useRegisterInputEvents } from '@/object-record/record-field/meta-types/input/hooks/useRegisterInputEvents'; +import { titleInputComponentState } from '@/ui/input/states/titleInputComponentState'; import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; +import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2'; import styled from '@emotion/styled'; import { OverflowingTextWithTooltip } from 'twenty-ui/display'; @@ -25,6 +27,7 @@ type InputProps = { export type TitleInputProps = { disabled?: boolean; + instanceId: string; } & InputProps; const StyledDiv = styled.div<{ @@ -142,14 +145,18 @@ export const TitleInput = ({ onClickOutside, onTab, onShiftTab, + instanceId, }: TitleInputProps) => { - const [isOpened, setIsOpened] = useState(false); + const [isTitleInputOpen, setIsTitleInputOpen] = useRecoilComponentStateV2( + titleInputComponentState, + instanceId, + ); const { setHotkeyScopeAndMemorizePreviousScope } = usePreviousHotkeyScope(); return ( <> - {isOpened ? ( + {isTitleInputOpen ? ( ) : ( { if (!disabled) { - setIsOpened(true); + setIsTitleInputOpen(true); setHotkeyScopeAndMemorizePreviousScope({ scope: hotkeyScope, }); diff --git a/packages/twenty-front/src/modules/ui/input/components/__stories__/TitleInput.stories.tsx b/packages/twenty-front/src/modules/ui/input/components/__stories__/TitleInput.stories.tsx index 496636841..cf6d87ac3 100644 --- a/packages/twenty-front/src/modules/ui/input/components/__stories__/TitleInput.stories.tsx +++ b/packages/twenty-front/src/modules/ui/input/components/__stories__/TitleInput.stories.tsx @@ -11,6 +11,7 @@ const meta: Meta = { placeholder: 'Enter title', hotkeyScope: 'titleInput', sizeVariant: 'md', + instanceId: 'title-input-story', }, argTypes: { hotkeyScope: { control: false }, diff --git a/packages/twenty-front/src/modules/ui/input/contexts/titleInputComponentInstanceContext.ts b/packages/twenty-front/src/modules/ui/input/contexts/titleInputComponentInstanceContext.ts new file mode 100644 index 000000000..cbf80a81b --- /dev/null +++ b/packages/twenty-front/src/modules/ui/input/contexts/titleInputComponentInstanceContext.ts @@ -0,0 +1,4 @@ +import { createComponentInstanceContext } from '@/ui/utilities/state/component-state/utils/createComponentInstanceContext'; + +export const TitleInputComponentInstanceContext = + createComponentInstanceContext(); diff --git a/packages/twenty-front/src/modules/ui/input/states/selectors/titleInputComponentSelector.ts b/packages/twenty-front/src/modules/ui/input/states/selectors/titleInputComponentSelector.ts new file mode 100644 index 000000000..cbc166bff --- /dev/null +++ b/packages/twenty-front/src/modules/ui/input/states/selectors/titleInputComponentSelector.ts @@ -0,0 +1,17 @@ +import { TitleInputComponentInstanceContext } from '@/ui/input/contexts/titleInputComponentInstanceContext'; +import { titleInputComponentState } from '@/ui/input/states/titleInputComponentState'; +import { createComponentSelectorV2 } from '@/ui/utilities/state/component-state/utils/createComponentSelectorV2'; + +export const titleInputComponentSelector = createComponentSelectorV2({ + key: 'titleInputComponentSelector', + get: + ({ instanceId }) => + ({ get }) => { + const isTitleInputOpen = get( + titleInputComponentState.atomFamily({ instanceId }), + ); + + return isTitleInputOpen; + }, + componentInstanceContext: TitleInputComponentInstanceContext, +}); diff --git a/packages/twenty-front/src/modules/ui/input/states/titleInputComponentState.ts b/packages/twenty-front/src/modules/ui/input/states/titleInputComponentState.ts new file mode 100644 index 000000000..1bd5c0b76 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/input/states/titleInputComponentState.ts @@ -0,0 +1,8 @@ +import { TitleInputComponentInstanceContext } from '@/ui/input/contexts/titleInputComponentInstanceContext'; +import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; + +export const titleInputComponentState = createComponentStateV2({ + key: 'titleInputComponentState', + defaultValue: false, + componentInstanceContext: TitleInputComponentInstanceContext, +}); diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/components/WorkflowStepHeader.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/components/WorkflowStepHeader.tsx index ac363b0fd..7f109ea00 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/components/WorkflowStepHeader.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/components/WorkflowStepHeader.tsx @@ -114,6 +114,7 @@ export const WorkflowStepHeader = ({ onClickOutside={saveTitle} onTab={saveTitle} onShiftTab={saveTitle} + instanceId="workflow-step-title" /> {headerType}