[Permissions] Force open title input for role label when empty (#12710)

- Fix empty title in breadcrumb 
- Enforce role label input open if empty
This commit is contained in:
Weiko
2025-06-24 14:06:50 +02:00
committed by GitHub
parent 4ac208cf1c
commit 074cd22a67
10 changed files with 102 additions and 11 deletions

View File

@ -68,7 +68,7 @@ export const SettingsRoleSettings = ({
label: value, label: value,
}); });
}} }}
placeholder={t`Role name`} placeholder={t`Untitled Role`}
disabled={!isEditable} disabled={!isEditable}
/> />
</StyledInputsContainer> </StyledInputsContainer>

View File

@ -7,6 +7,7 @@ import { SettingsRoleAssignment } from '@/settings/roles/role-assignment/compone
import { SettingsRolePermissions } from '@/settings/roles/role-permissions/components/SettingsRolePermissions'; import { SettingsRolePermissions } from '@/settings/roles/role-permissions/components/SettingsRolePermissions';
import { SettingsRoleSettings } from '@/settings/roles/role-settings/components/SettingsRoleSettings'; import { SettingsRoleSettings } from '@/settings/roles/role-settings/components/SettingsRoleSettings';
import { SettingsRoleLabelContainer } from '@/settings/roles/role/components/SettingsRoleLabelContainer'; 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 { SETTINGS_ROLE_DETAIL_TABS } from '@/settings/roles/role/constants/SettingsRoleDetailTabs';
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState'; import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
import { settingsPersistedRoleFamilyState } from '@/settings/roles/states/settingsPersistedRoleFamilyState'; 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 { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { getOperationName } from '@apollo/client/utilities'; import { getOperationName } from '@apollo/client/utilities';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro'; import { t } from '@lingui/core/macro';
import { useRecoilState, useRecoilValue } from 'recoil'; import { useRecoilState, useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
@ -52,6 +54,10 @@ const ROLE_BASIC_KEYS: Array<keyof Role> = [
'canDestroyAllObjectRecords', 'canDestroyAllObjectRecords',
]; ];
const StyledUntitledRole = styled.span`
color: ${({ theme }) => theme.font.color.tertiary};
`;
export const SettingsRole = ({ roleId, isCreateMode }: SettingsRoleProps) => { export const SettingsRole = ({ roleId, isCreateMode }: SettingsRoleProps) => {
const activeTabId = useRecoilComponentValueV2( const activeTabId = useRecoilComponentValueV2(
activeTabIdComponentState, activeTabIdComponentState,
@ -275,7 +281,12 @@ export const SettingsRole = ({ roleId, isCreateMode }: SettingsRoleProps) => {
return ( return (
<SubMenuTopBarContainer <SubMenuTopBarContainer
title={<SettingsRoleLabelContainer roleId={roleId} />} title={
<>
<SettingsRoleLabelContainer roleId={roleId} />
<SettingsRoleLabelContainerEffect roleId={roleId} />
</>
}
links={[ links={[
{ {
children: 'Workspace', children: 'Workspace',
@ -286,14 +297,19 @@ export const SettingsRole = ({ roleId, isCreateMode }: SettingsRoleProps) => {
href: getSettingsPath(SettingsPath.Roles), href: getSettingsPath(SettingsPath.Roles),
}, },
{ {
children: settingsDraftRole.label, children:
isDefined(settingsDraftRole.label) &&
settingsDraftRole.label !== '' ? (
settingsDraftRole.label
) : (
<StyledUntitledRole>{t`Untitled Role`}</StyledUntitledRole>
),
}, },
]} ]}
actionButton={ actionButton={
isRoleEditable && isRoleEditable && isDirty ? (
isDirty && (
<SaveAndCancelButtons onSave={handleSave} onCancel={handleCancel} /> <SaveAndCancelButtons onSave={handleSave} onCancel={handleCancel} />
) ) : null
} }
> >
<SettingsPageContainer> <SettingsPageContainer>

View File

@ -29,6 +29,8 @@ export const SettingsRoleLabelContainer = ({
settingsDraftRoleFamilyState(roleId), settingsDraftRoleFamilyState(roleId),
); );
const titleInputInstanceId = `settings-role-label-${roleId}`;
const handleChange = (newValue: string) => { const handleChange = (newValue: string) => {
setSettingsDraftRole({ setSettingsDraftRole({
...settingsDraftRole, ...settingsDraftRole,
@ -43,8 +45,9 @@ export const SettingsRoleLabelContainer = ({
sizeVariant="md" sizeVariant="md"
value={settingsDraftRole.label} value={settingsDraftRole.label}
onChange={handleChange} onChange={handleChange}
placeholder={t`Role name`} placeholder={t`Untitled Role`}
hotkeyScope={ROLE_LABEL_EDIT_HOTKEY_SCOPE} hotkeyScope={ROLE_LABEL_EDIT_HOTKEY_SCOPE}
instanceId={titleInputInstanceId}
/> />
</StyledHeaderTitle> </StyledHeaderTitle>
); );

View File

@ -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 <></>;
};

View File

@ -6,7 +6,9 @@ import { useRef, useState } from 'react';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
import { useRegisterInputEvents } from '@/object-record/record-field/meta-types/input/hooks/useRegisterInputEvents'; 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 { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { OverflowingTextWithTooltip } from 'twenty-ui/display'; import { OverflowingTextWithTooltip } from 'twenty-ui/display';
@ -25,6 +27,7 @@ type InputProps = {
export type TitleInputProps = { export type TitleInputProps = {
disabled?: boolean; disabled?: boolean;
instanceId: string;
} & InputProps; } & InputProps;
const StyledDiv = styled.div<{ const StyledDiv = styled.div<{
@ -142,14 +145,18 @@ export const TitleInput = ({
onClickOutside, onClickOutside,
onTab, onTab,
onShiftTab, onShiftTab,
instanceId,
}: TitleInputProps) => { }: TitleInputProps) => {
const [isOpened, setIsOpened] = useState(false); const [isTitleInputOpen, setIsTitleInputOpen] = useRecoilComponentStateV2(
titleInputComponentState,
instanceId,
);
const { setHotkeyScopeAndMemorizePreviousScope } = usePreviousHotkeyScope(); const { setHotkeyScopeAndMemorizePreviousScope } = usePreviousHotkeyScope();
return ( return (
<> <>
{isOpened ? ( {isTitleInputOpen ? (
<Input <Input
sizeVariant={sizeVariant} sizeVariant={sizeVariant}
value={value} value={value}
@ -161,7 +168,7 @@ export const TitleInput = ({
onClickOutside={onClickOutside} onClickOutside={onClickOutside}
onTab={onTab} onTab={onTab}
onShiftTab={onShiftTab} onShiftTab={onShiftTab}
setIsOpened={setIsOpened} setIsOpened={setIsTitleInputOpen}
/> />
) : ( ) : (
<StyledDiv <StyledDiv
@ -169,7 +176,7 @@ export const TitleInput = ({
disabled={disabled} disabled={disabled}
onClick={() => { onClick={() => {
if (!disabled) { if (!disabled) {
setIsOpened(true); setIsTitleInputOpen(true);
setHotkeyScopeAndMemorizePreviousScope({ setHotkeyScopeAndMemorizePreviousScope({
scope: hotkeyScope, scope: hotkeyScope,
}); });

View File

@ -11,6 +11,7 @@ const meta: Meta<typeof TitleInput> = {
placeholder: 'Enter title', placeholder: 'Enter title',
hotkeyScope: 'titleInput', hotkeyScope: 'titleInput',
sizeVariant: 'md', sizeVariant: 'md',
instanceId: 'title-input-story',
}, },
argTypes: { argTypes: {
hotkeyScope: { control: false }, hotkeyScope: { control: false },

View File

@ -0,0 +1,4 @@
import { createComponentInstanceContext } from '@/ui/utilities/state/component-state/utils/createComponentInstanceContext';
export const TitleInputComponentInstanceContext =
createComponentInstanceContext();

View File

@ -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,
});

View File

@ -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<boolean>({
key: 'titleInputComponentState',
defaultValue: false,
componentInstanceContext: TitleInputComponentInstanceContext,
});

View File

@ -114,6 +114,7 @@ export const WorkflowStepHeader = ({
onClickOutside={saveTitle} onClickOutside={saveTitle}
onTab={saveTitle} onTab={saveTitle}
onShiftTab={saveTitle} onShiftTab={saveTitle}
instanceId="workflow-step-title"
/> />
</StyledHeaderTitle> </StyledHeaderTitle>
<StyledHeaderType>{headerType}</StyledHeaderType> <StyledHeaderType>{headerType}</StyledHeaderType>