Compare commits
221
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f2918b178 | ||
|
|
1eb863bfc0 | ||
|
|
d545e05262 | ||
|
|
b04b7f953a | ||
|
|
4d9b2d15e8 | ||
|
|
5c7db7c887 | ||
|
|
de032de7ee | ||
|
|
5293fc81b4 | ||
|
|
f530034288 | ||
|
|
8cbd79aa4d | ||
|
|
0c839475c3 | ||
|
|
336b490520 | ||
|
|
fce5ce90e5 | ||
|
|
e5862b9924 | ||
|
|
d9da68bd3d | ||
|
|
2ddad5d9c7 | ||
|
|
8b455a8016 | ||
|
|
7391b7b48b | ||
|
|
1a7b1ab077 | ||
|
|
698074e377 | ||
|
|
9cd9e505f3 | ||
|
|
28bd3707d4 | ||
|
|
37978120d0 | ||
|
|
f44f47be1e | ||
|
|
e2bb09e992 | ||
|
|
fdb1aa461e | ||
|
|
45297fd057 | ||
|
|
38e5d85287 | ||
|
|
f58bdd7245 | ||
|
|
547f623ee2 | ||
|
|
022f1f70d8 | ||
|
|
cb50b65496 | ||
|
|
e3228fa467 | ||
|
|
fe96cc6d27 | ||
|
|
51b6effbd2 | ||
|
|
4d8046bc08 | ||
|
|
be1cae4a25 | ||
|
|
b9b8062d39 | ||
|
|
1420ebabbc | ||
|
|
bc93710e83 | ||
|
|
7b2a5ffe76 | ||
|
|
03971aa38a | ||
|
|
c0bcbc1e01 | ||
|
|
2c81170bf3 | ||
|
|
e6d97717d4 | ||
|
|
01bb4214ad | ||
|
|
584f13f0d4 | ||
|
|
7889fc65e0 | ||
|
|
aefe6f67fc | ||
|
|
5b16cde53f | ||
|
|
fcb5d7ee20 | ||
|
|
90de7f818b | ||
|
|
58bc4c4184 | ||
|
|
ea8c91002d | ||
|
|
7bca98fadb | ||
|
|
e5acbcf555 | ||
|
|
b61377c29a | ||
|
|
655ec251af | ||
|
|
4f55adb73c | ||
|
|
7c92aeff20 | ||
|
|
1738fa08f9 | ||
|
|
ab392e7b13 | ||
|
|
67707da339 | ||
|
|
4d123759f1 | ||
|
|
a3e242bd8b | ||
|
|
fa9dba834f | ||
|
|
d24264a302 | ||
|
|
b8a2600ba5 | ||
|
|
d2bf4a7e9a | ||
|
|
eaec7dedcc | ||
|
|
b7798efb4c | ||
|
|
6b27902df2 | ||
|
|
4b0bd71e02 | ||
|
|
2efe056977 | ||
|
|
d21e15556b | ||
|
|
5fc9c725b6 | ||
|
|
f06ce683d4 | ||
|
|
2f1d6f8b7e | ||
|
|
2ae6c52e4b | ||
|
|
919766cced | ||
|
|
b048b7a19d | ||
|
|
a95f15274e | ||
|
|
22469701ae | ||
|
|
326f32e62f | ||
|
|
09bf955538 | ||
|
|
a113468440 | ||
|
|
5b55f915d5 | ||
|
|
26e67db0aa | ||
|
|
af1fcee234 | ||
|
|
f9df7b87f8 | ||
|
|
6f78f9a796 | ||
|
|
8f7b189717 | ||
|
|
e2da06cf3c | ||
|
|
92ba245e87 | ||
|
|
5caa8df0cf | ||
|
|
d8292062cc | ||
|
|
439d81c8d7 | ||
|
|
fae016b6f9 | ||
|
|
8159a0cede | ||
|
|
2b465f5b52 | ||
|
|
19472a9bb5 | ||
|
|
84c804394d | ||
|
|
a3bfa3bba1 | ||
|
|
aaed4215b9 | ||
|
|
44602fc2cc | ||
|
|
55d55a62c5 | ||
|
|
4faee658f0 | ||
|
|
160f7ca574 | ||
|
|
ab4262844b | ||
|
|
195bdac620 | ||
|
|
ef57d5c96e | ||
|
|
f1f0f743ab | ||
|
|
f16b724419 | ||
|
|
cb21655afd | ||
|
|
c12875eddd | ||
|
|
eddf0e11a5 | ||
|
|
dce2e0a0cc | ||
|
|
ea52343c5d | ||
|
|
0956c72da1 | ||
|
|
9af54c53a4 | ||
|
|
409e789c14 | ||
|
|
c9c7fc176e | ||
|
|
fe19af45da | ||
|
|
f82bcb4c16 | ||
|
|
b5d91817cc | ||
|
|
7f9128a720 | ||
|
|
9456ac38dd | ||
|
|
5241c5a297 | ||
|
|
ed42de3508 | ||
|
|
6ec8b57e39 | ||
|
|
fa18caa41d | ||
|
|
76ceaa34c7 | ||
|
|
c76d7ba96d | ||
|
|
521d28db63 | ||
|
|
02e1710319 | ||
|
|
e0b1f8b301 | ||
|
|
5c12b0fe0c | ||
|
|
430f16eea0 | ||
|
|
7268332aec | ||
|
|
485e880dab | ||
|
|
1a047fec3d | ||
|
|
4c4d47b5e6 | ||
|
|
6dd068a8d0 | ||
|
|
8858696e0d | ||
|
|
abf32ad5ee | ||
|
|
61c1e850a9 | ||
|
|
1bf80a3ca4 | ||
|
|
dd0b8de3cc | ||
|
|
1882fdf871 | ||
|
|
09bf32df7b | ||
|
|
a97eafc067 | ||
|
|
923bb1e75d | ||
|
|
8a2b6f8976 | ||
|
|
04c29958d5 | ||
|
|
8912fb6588 | ||
|
|
8672bbc13a | ||
|
|
04e8a67f9a | ||
|
|
27dd5f2ba5 | ||
|
|
a7a55322e1 | ||
|
|
9f44d0a56f | ||
|
|
b7f9112287 | ||
|
|
5462dc5ba7 | ||
|
|
73c529ecc6 | ||
|
|
3d16040a39 | ||
|
|
02dd826738 | ||
|
|
ff1f0aafe3 | ||
|
|
0f9882bcd9 | ||
|
|
20dbfcfc31 | ||
|
|
df86efed33 | ||
|
|
439ac3ffc6 | ||
|
|
e9cd87c284 | ||
|
|
d04df4b52a | ||
|
|
5b6798f9fc | ||
|
|
9c6a117d8a | ||
|
|
1061f81285 | ||
|
|
45af0ca1a0 | ||
|
|
5153c73a6e | ||
|
|
71e4db6bf6 | ||
|
|
8237a41b20 | ||
|
|
ee57a19866 | ||
|
|
d4d06a835a | ||
|
|
6b2bf17bec | ||
|
|
8f079b2768 | ||
|
|
479dabc16f | ||
|
|
3682e544b1 | ||
|
|
ea920aa7e4 | ||
|
|
e1925c5451 | ||
|
|
aeb62f46a4 | ||
|
|
ebef2204b5 | ||
|
|
b9e5dd3ab9 | ||
|
|
b26b28f7e0 | ||
|
|
fc592d3e7d | ||
|
|
41aa60da2e | ||
|
|
5110c27c13 | ||
|
|
2cf2ea6ff8 | ||
|
|
c516f95374 | ||
|
|
b32d04bc79 | ||
|
|
70a338ba91 | ||
|
|
e2d4375bc7 | ||
|
|
ff92bde46b | ||
|
|
c89eee4fdc | ||
|
|
f3f8a0cd02 | ||
|
|
9aaefbb765 | ||
|
|
df50bf41e6 | ||
|
|
38659223c7 | ||
|
|
4611e83719 | ||
|
|
a65322dce0 | ||
|
|
b38039f687 | ||
|
|
9f9c766013 | ||
|
|
9e487f0a44 | ||
|
|
4224522d98 | ||
|
|
2d049774d9 | ||
|
|
e80574b837 | ||
|
|
3672bb7700 | ||
|
|
c8a7ce4ff6 | ||
|
|
2affeaff7e | ||
|
|
986a36a8fd | ||
|
|
ef617aac54 | ||
|
|
2a5c2cf62d | ||
|
|
8cbbc1b6bf | ||
|
|
ad10e0a076 |
@@ -62,9 +62,9 @@ const jestConfig = {
|
||||
extensionsToTreatAsEsm: ['.ts', '.tsx'],
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
statements: 50,
|
||||
lines: 48.9,
|
||||
functions: 40.9,
|
||||
statements: 49.5,
|
||||
lines: 48,
|
||||
functions: 40,
|
||||
},
|
||||
},
|
||||
collectCoverageFrom: ['<rootDir>/src/**/*.ts'],
|
||||
|
||||
@@ -714,6 +714,13 @@ export enum CommandMenuItemAvailabilityType {
|
||||
SINGLE_RECORD = 'SINGLE_RECORD'
|
||||
}
|
||||
|
||||
export type ComputeStepOutputSchemaInput = {
|
||||
/** Step JSON format */
|
||||
step: Scalars['JSON'];
|
||||
/** Workflow version ID */
|
||||
workflowVersionId?: InputMaybe<Scalars['UUID']>;
|
||||
};
|
||||
|
||||
export enum ConfigSource {
|
||||
DATABASE = 'DATABASE',
|
||||
DEFAULT = 'DEFAULT',
|
||||
@@ -960,6 +967,13 @@ export type CreateDefaultLogicFunctionInput = {
|
||||
toolInputSchema?: InputMaybe<Scalars['JSON']>;
|
||||
};
|
||||
|
||||
export type CreateDraftFromWorkflowVersionInput = {
|
||||
/** Workflow ID */
|
||||
workflowId: Scalars['UUID'];
|
||||
/** Workflow version ID */
|
||||
workflowVersionIdToCopy: Scalars['UUID'];
|
||||
};
|
||||
|
||||
export type CreateFieldInput = {
|
||||
defaultValue?: InputMaybe<Scalars['JSON']>;
|
||||
description?: InputMaybe<Scalars['String']>;
|
||||
@@ -994,6 +1008,8 @@ export type CreateFrontComponentInput = {
|
||||
|
||||
export type CreateNavigationMenuItemInput = {
|
||||
folderId?: InputMaybe<Scalars['UUID']>;
|
||||
icon?: InputMaybe<Scalars['String']>;
|
||||
link?: InputMaybe<Scalars['String']>;
|
||||
name?: InputMaybe<Scalars['String']>;
|
||||
position?: InputMaybe<Scalars['Int']>;
|
||||
targetObjectMetadataId?: InputMaybe<Scalars['UUID']>;
|
||||
@@ -1151,6 +1167,36 @@ export type CreateWebhookInput = {
|
||||
targetUrl: Scalars['String'];
|
||||
};
|
||||
|
||||
export type CreateWorkflowVersionEdgeInput = {
|
||||
/** Workflow version source step ID */
|
||||
source: Scalars['String'];
|
||||
/** Workflow version source step connection options */
|
||||
sourceConnectionOptions?: InputMaybe<Scalars['JSON']>;
|
||||
/** Workflow version target step ID */
|
||||
target: Scalars['String'];
|
||||
/** Workflow version ID */
|
||||
workflowVersionId: Scalars['String'];
|
||||
};
|
||||
|
||||
export type CreateWorkflowVersionStepInput = {
|
||||
/** Default settings for the step */
|
||||
defaultSettings?: InputMaybe<Scalars['JSON']>;
|
||||
/** Step ID */
|
||||
id?: InputMaybe<Scalars['String']>;
|
||||
/** Next step ID */
|
||||
nextStepId?: InputMaybe<Scalars['UUID']>;
|
||||
/** Parent step connection options */
|
||||
parentStepConnectionOptions?: InputMaybe<Scalars['JSON']>;
|
||||
/** Parent step ID */
|
||||
parentStepId?: InputMaybe<Scalars['String']>;
|
||||
/** Step position */
|
||||
position?: InputMaybe<WorkflowStepPositionInput>;
|
||||
/** New step type */
|
||||
stepType: Scalars['String'];
|
||||
/** Workflow version ID */
|
||||
workflowVersionId: Scalars['UUID'];
|
||||
};
|
||||
|
||||
export type CursorPaging = {
|
||||
/** Paginate after opaque cursor */
|
||||
after?: InputMaybe<Scalars['ConnectionCursor']>;
|
||||
@@ -1172,6 +1218,17 @@ export enum DatabaseEventAction {
|
||||
UPSERTED = 'UPSERTED'
|
||||
}
|
||||
|
||||
export type DateTimeFilter = {
|
||||
eq?: InputMaybe<Scalars['DateTime']>;
|
||||
gt?: InputMaybe<Scalars['DateTime']>;
|
||||
gte?: InputMaybe<Scalars['DateTime']>;
|
||||
in?: InputMaybe<Array<Scalars['DateTime']>>;
|
||||
is?: InputMaybe<FilterIs>;
|
||||
lt?: InputMaybe<Scalars['DateTime']>;
|
||||
lte?: InputMaybe<Scalars['DateTime']>;
|
||||
neq?: InputMaybe<Scalars['DateTime']>;
|
||||
};
|
||||
|
||||
export type DeleteApprovedAccessDomainInput = {
|
||||
id: Scalars['UUID'];
|
||||
};
|
||||
@@ -1222,6 +1279,13 @@ export type DeleteViewGroupInput = {
|
||||
id: Scalars['UUID'];
|
||||
};
|
||||
|
||||
export type DeleteWorkflowVersionStepInput = {
|
||||
/** Step to delete ID */
|
||||
stepId: Scalars['String'];
|
||||
/** Workflow version ID */
|
||||
workflowVersionId: Scalars['UUID'];
|
||||
};
|
||||
|
||||
export type DeletedWorkspaceMember = {
|
||||
__typename?: 'DeletedWorkspaceMember';
|
||||
avatarUrl?: Maybe<Scalars['String']>;
|
||||
@@ -1262,6 +1326,18 @@ export type DomainValidRecords = {
|
||||
records: Array<DomainRecord>;
|
||||
};
|
||||
|
||||
export type DuplicateWorkflowInput = {
|
||||
/** Workflow ID to duplicate */
|
||||
workflowIdToDuplicate: Scalars['UUID'];
|
||||
/** Workflow version ID to copy */
|
||||
workflowVersionIdToCopy: Scalars['UUID'];
|
||||
};
|
||||
|
||||
export type DuplicateWorkflowVersionStepInput = {
|
||||
stepId: Scalars['String'];
|
||||
workflowVersionId: Scalars['String'];
|
||||
};
|
||||
|
||||
export type DuplicatedDashboard = {
|
||||
__typename?: 'DuplicatedDashboard';
|
||||
createdAt: Scalars['String'];
|
||||
@@ -1422,6 +1498,7 @@ export enum FeatureFlagKey {
|
||||
IS_JSON_FILTER_ENABLED = 'IS_JSON_FILTER_ENABLED',
|
||||
IS_JUNCTION_RELATIONS_ENABLED = 'IS_JUNCTION_RELATIONS_ENABLED',
|
||||
IS_MARKETPLACE_ENABLED = 'IS_MARKETPLACE_ENABLED',
|
||||
IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED = 'IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED',
|
||||
IS_NAVIGATION_MENU_ITEM_ENABLED = 'IS_NAVIGATION_MENU_ITEM_ENABLED',
|
||||
IS_NOTE_TARGET_MIGRATED = 'IS_NOTE_TARGET_MIGRATED',
|
||||
IS_PUBLIC_DOMAIN_ENABLED = 'IS_PUBLIC_DOMAIN_ENABLED',
|
||||
@@ -1576,6 +1653,11 @@ export type FilesConfiguration = {
|
||||
configurationType: WidgetConfigurationType;
|
||||
};
|
||||
|
||||
export enum FilterIs {
|
||||
NotNull = 'NotNull',
|
||||
Null = 'Null'
|
||||
}
|
||||
|
||||
export type FindAvailableSsoidpOutput = {
|
||||
__typename?: 'FindAvailableSSOIDPOutput';
|
||||
id: Scalars['UUID'];
|
||||
@@ -2010,6 +2092,7 @@ export enum ModelProvider {
|
||||
export type Mutation = {
|
||||
__typename?: 'Mutation';
|
||||
activateSkill: Skill;
|
||||
activateWorkflowVersion: Scalars['Boolean'];
|
||||
activateWorkspace: Workspace;
|
||||
addQueryToEventStream: Scalars['Boolean'];
|
||||
assignRoleToAgent: Scalars['Boolean'];
|
||||
@@ -2021,6 +2104,7 @@ export type Mutation = {
|
||||
checkCustomDomainValidRecords?: Maybe<DomainValidRecords>;
|
||||
checkPublicDomainValidRecords?: Maybe<DomainValidRecords>;
|
||||
checkoutSession: BillingSessionOutput;
|
||||
computeStepOutputSchema: Scalars['JSON'];
|
||||
createApiKey: ApiKey;
|
||||
createApprovedAccessDomain: ApprovedAccessDomain;
|
||||
createChatThread: AgentChatThread;
|
||||
@@ -2033,6 +2117,7 @@ export type Mutation = {
|
||||
createCoreViewSort: CoreViewSort;
|
||||
createDatabaseConfigVariable: Scalars['Boolean'];
|
||||
createDefaultLogicFunction: LogicFunction;
|
||||
createDraftFromWorkflowVersion: WorkflowVersionDto;
|
||||
createEmailingDomain: EmailingDomain;
|
||||
createFile: File;
|
||||
createFrontComponent: FrontComponent;
|
||||
@@ -2054,7 +2139,10 @@ export type Mutation = {
|
||||
createSAMLIdentityProvider: SetupSsoOutput;
|
||||
createSkill: Skill;
|
||||
createWebhook: Webhook;
|
||||
createWorkflowVersionEdge: WorkflowVersionStepChanges;
|
||||
createWorkflowVersionStep: WorkflowVersionStepChanges;
|
||||
deactivateSkill: Skill;
|
||||
deactivateWorkflowVersion: Scalars['Boolean'];
|
||||
deleteApprovedAccessDomain: Scalars['Boolean'];
|
||||
deleteCommandMenuItem: CommandMenuItem;
|
||||
deleteCoreView: Scalars['Boolean'];
|
||||
@@ -2082,6 +2170,8 @@ export type Mutation = {
|
||||
deleteUser: User;
|
||||
deleteUserFromWorkspace: UserWorkspace;
|
||||
deleteWebhook: Webhook;
|
||||
deleteWorkflowVersionEdge: WorkflowVersionStepChanges;
|
||||
deleteWorkflowVersionStep: WorkflowVersionStepChanges;
|
||||
deleteWorkspaceInvitation: Scalars['String'];
|
||||
destroyCoreView: Scalars['Boolean'];
|
||||
destroyCoreViewField: CoreViewField;
|
||||
@@ -2093,7 +2183,10 @@ export type Mutation = {
|
||||
destroyPageLayoutTab: Scalars['Boolean'];
|
||||
destroyPageLayoutWidget: Scalars['Boolean'];
|
||||
disablePostgresProxy: PostgresCredentials;
|
||||
dismissReconnectAccountBanner: Scalars['Boolean'];
|
||||
duplicateDashboard: DuplicatedDashboard;
|
||||
duplicateWorkflow: WorkflowVersionDto;
|
||||
duplicateWorkflowVersionStep: WorkflowVersionStepChanges;
|
||||
editSSOIdentityProvider: EditSsoOutput;
|
||||
emailPasswordResetLink: EmailPasswordResetLinkOutput;
|
||||
enablePostgresProxy: PostgresCredentials;
|
||||
@@ -2119,6 +2212,7 @@ export type Mutation = {
|
||||
retryJobs: RetryJobsResponse;
|
||||
revokeApiKey?: Maybe<ApiKey>;
|
||||
runEvaluationInput: AgentTurn;
|
||||
runWorkflowVersion: RunWorkflowVersionOutput;
|
||||
saveImapSmtpCaldavAccount: ImapSmtpCaldavConnectionSuccess;
|
||||
sendInvitations: SendInvitationsOutput;
|
||||
setMeteredSubscriptionPrice: BillingUpdateOutput;
|
||||
@@ -2129,9 +2223,12 @@ export type Mutation = {
|
||||
skipBookOnboardingStep: OnboardingStepSuccess;
|
||||
skipSyncEmailOnboardingStep: OnboardingStepSuccess;
|
||||
startChannelSync: ChannelSyncSuccess;
|
||||
stopWorkflowRun: WorkflowRun;
|
||||
submitFormStep: Scalars['Boolean'];
|
||||
switchBillingPlan: BillingUpdateOutput;
|
||||
switchSubscriptionInterval: BillingUpdateOutput;
|
||||
syncApplication: Scalars['Boolean'];
|
||||
testHttpRequest: TestHttpRequestOutput;
|
||||
trackAnalytics: Analytics;
|
||||
uninstallApplication: Scalars['Boolean'];
|
||||
updateApiKey?: Maybe<ApiKey>;
|
||||
@@ -2160,6 +2257,9 @@ export type Mutation = {
|
||||
updateSkill: Skill;
|
||||
updateUserEmail: Scalars['Boolean'];
|
||||
updateWebhook: Webhook;
|
||||
updateWorkflowRunStep: WorkflowAction;
|
||||
updateWorkflowVersionPositions: Scalars['Boolean'];
|
||||
updateWorkflowVersionStep: WorkflowAction;
|
||||
updateWorkspace: Workspace;
|
||||
updateWorkspaceFeatureFlag: Scalars['Boolean'];
|
||||
updateWorkspaceMemberRole: WorkspaceMember;
|
||||
@@ -2188,6 +2288,11 @@ export type MutationActivateSkillArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationActivateWorkflowVersionArgs = {
|
||||
workflowVersionId: Scalars['UUID'];
|
||||
};
|
||||
|
||||
|
||||
export type MutationActivateWorkspaceArgs = {
|
||||
data: ActivateWorkspaceInput;
|
||||
};
|
||||
@@ -2230,6 +2335,11 @@ export type MutationCheckoutSessionArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationComputeStepOutputSchemaArgs = {
|
||||
input: ComputeStepOutputSchemaInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationCreateApiKeyArgs = {
|
||||
input: CreateApiKeyInput;
|
||||
};
|
||||
@@ -2286,6 +2396,11 @@ export type MutationCreateDefaultLogicFunctionArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationCreateDraftFromWorkflowVersionArgs = {
|
||||
input: CreateDraftFromWorkflowVersionInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationCreateEmailingDomainArgs = {
|
||||
domain: Scalars['String'];
|
||||
driver: EmailingDomainDriver;
|
||||
@@ -2395,11 +2510,26 @@ export type MutationCreateWebhookArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationCreateWorkflowVersionEdgeArgs = {
|
||||
input: CreateWorkflowVersionEdgeInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationCreateWorkflowVersionStepArgs = {
|
||||
input: CreateWorkflowVersionStepInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationDeactivateSkillArgs = {
|
||||
id: Scalars['UUID'];
|
||||
};
|
||||
|
||||
|
||||
export type MutationDeactivateWorkflowVersionArgs = {
|
||||
workflowVersionId: Scalars['UUID'];
|
||||
};
|
||||
|
||||
|
||||
export type MutationDeleteApprovedAccessDomainArgs = {
|
||||
input: DeleteApprovedAccessDomainInput;
|
||||
};
|
||||
@@ -2526,6 +2656,16 @@ export type MutationDeleteWebhookArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationDeleteWorkflowVersionEdgeArgs = {
|
||||
input: CreateWorkflowVersionEdgeInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationDeleteWorkflowVersionStepArgs = {
|
||||
input: DeleteWorkflowVersionStepInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationDeleteWorkspaceInvitationArgs = {
|
||||
appTokenId: Scalars['String'];
|
||||
};
|
||||
@@ -2576,11 +2716,26 @@ export type MutationDestroyPageLayoutWidgetArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationDismissReconnectAccountBannerArgs = {
|
||||
connectedAccountId: Scalars['UUID'];
|
||||
};
|
||||
|
||||
|
||||
export type MutationDuplicateDashboardArgs = {
|
||||
id: Scalars['UUID'];
|
||||
};
|
||||
|
||||
|
||||
export type MutationDuplicateWorkflowArgs = {
|
||||
input: DuplicateWorkflowInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationDuplicateWorkflowVersionStepArgs = {
|
||||
input: DuplicateWorkflowVersionStepInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationEditSsoIdentityProviderArgs = {
|
||||
input: EditSsoInput;
|
||||
};
|
||||
@@ -2697,6 +2852,11 @@ export type MutationRunEvaluationInputArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationRunWorkflowVersionArgs = {
|
||||
input: RunWorkflowVersionInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationSaveImapSmtpCaldavAccountArgs = {
|
||||
accountOwnerId: Scalars['UUID'];
|
||||
connectionParameters: EmailAccountConnectionParameters;
|
||||
@@ -2750,11 +2910,26 @@ export type MutationStartChannelSyncArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationStopWorkflowRunArgs = {
|
||||
workflowRunId: Scalars['UUID'];
|
||||
};
|
||||
|
||||
|
||||
export type MutationSubmitFormStepArgs = {
|
||||
input: SubmitFormStepInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationSyncApplicationArgs = {
|
||||
manifest: Scalars['JSON'];
|
||||
};
|
||||
|
||||
|
||||
export type MutationTestHttpRequestArgs = {
|
||||
input: TestHttpRequestInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationTrackAnalyticsArgs = {
|
||||
event?: InputMaybe<Scalars['String']>;
|
||||
name?: InputMaybe<Scalars['String']>;
|
||||
@@ -2910,6 +3085,21 @@ export type MutationUpdateWebhookArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationUpdateWorkflowRunStepArgs = {
|
||||
input: UpdateWorkflowRunStepInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationUpdateWorkflowVersionPositionsArgs = {
|
||||
input: UpdateWorkflowVersionPositionsInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationUpdateWorkflowVersionStepArgs = {
|
||||
input: UpdateWorkflowVersionStepInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationUpdateWorkspaceArgs = {
|
||||
data: UpdateWorkspaceInput;
|
||||
};
|
||||
@@ -3029,7 +3219,9 @@ export type NavigationMenuItem = {
|
||||
applicationId?: Maybe<Scalars['UUID']>;
|
||||
createdAt: Scalars['DateTime'];
|
||||
folderId?: Maybe<Scalars['UUID']>;
|
||||
icon?: Maybe<Scalars['String']>;
|
||||
id: Scalars['UUID'];
|
||||
link?: Maybe<Scalars['String']>;
|
||||
name?: Maybe<Scalars['String']>;
|
||||
position: Scalars['Float'];
|
||||
targetObjectMetadataId?: Maybe<Scalars['UUID']>;
|
||||
@@ -3169,6 +3361,16 @@ export type ObjectRecordEventProperties = {
|
||||
updatedFields?: Maybe<Array<Scalars['String']>>;
|
||||
};
|
||||
|
||||
export type ObjectRecordFilterInput = {
|
||||
and?: InputMaybe<Array<ObjectRecordFilterInput>>;
|
||||
createdAt?: InputMaybe<DateTimeFilter>;
|
||||
deletedAt?: InputMaybe<DateTimeFilter>;
|
||||
id?: InputMaybe<UuidFilter>;
|
||||
not?: InputMaybe<ObjectRecordFilterInput>;
|
||||
or?: InputMaybe<Array<ObjectRecordFilterInput>>;
|
||||
updatedAt?: InputMaybe<DateTimeFilter>;
|
||||
};
|
||||
|
||||
/** Date granularity options (e.g. DAY, MONTH, QUARTER, YEAR, WEEK, DAY_OF_THE_WEEK, MONTH_OF_THE_YEAR, QUARTER_OF_THE_YEAR) */
|
||||
export enum ObjectRecordGroupByDateGranularity {
|
||||
DAY = 'DAY',
|
||||
@@ -3506,6 +3708,12 @@ export type Query = {
|
||||
getRoles: Array<Role>;
|
||||
getSSOIdentityProviders: Array<FindAvailableSsoidpOutput>;
|
||||
getSystemHealthStatus: SystemHealth;
|
||||
getTimelineCalendarEventsFromCompanyId: TimelineCalendarEventsWithTotal;
|
||||
getTimelineCalendarEventsFromOpportunityId: TimelineCalendarEventsWithTotal;
|
||||
getTimelineCalendarEventsFromPersonId: TimelineCalendarEventsWithTotal;
|
||||
getTimelineThreadsFromCompanyId: TimelineThreadsWithTotal;
|
||||
getTimelineThreadsFromOpportunityId: TimelineThreadsWithTotal;
|
||||
getTimelineThreadsFromPersonId: TimelineThreadsWithTotal;
|
||||
getToolIndex: Array<ToolIndexEntry>;
|
||||
index: Index;
|
||||
indexMetadatas: IndexConnection;
|
||||
@@ -3516,6 +3724,7 @@ export type Query = {
|
||||
object: Object;
|
||||
objects: ObjectConnection;
|
||||
pieChartData: PieChartDataOutput;
|
||||
search: SearchResultConnection;
|
||||
skill?: Maybe<Skill>;
|
||||
skills: Array<Skill>;
|
||||
validatePasswordResetToken: ValidatePasswordResetTokenOutput;
|
||||
@@ -3768,6 +3977,48 @@ export type QueryGetQueueMetricsArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type QueryGetTimelineCalendarEventsFromCompanyIdArgs = {
|
||||
companyId: Scalars['UUID'];
|
||||
page: Scalars['Int'];
|
||||
pageSize: Scalars['Int'];
|
||||
};
|
||||
|
||||
|
||||
export type QueryGetTimelineCalendarEventsFromOpportunityIdArgs = {
|
||||
opportunityId: Scalars['UUID'];
|
||||
page: Scalars['Int'];
|
||||
pageSize: Scalars['Int'];
|
||||
};
|
||||
|
||||
|
||||
export type QueryGetTimelineCalendarEventsFromPersonIdArgs = {
|
||||
page: Scalars['Int'];
|
||||
pageSize: Scalars['Int'];
|
||||
personId: Scalars['UUID'];
|
||||
};
|
||||
|
||||
|
||||
export type QueryGetTimelineThreadsFromCompanyIdArgs = {
|
||||
companyId: Scalars['UUID'];
|
||||
page: Scalars['Int'];
|
||||
pageSize: Scalars['Int'];
|
||||
};
|
||||
|
||||
|
||||
export type QueryGetTimelineThreadsFromOpportunityIdArgs = {
|
||||
opportunityId: Scalars['UUID'];
|
||||
page: Scalars['Int'];
|
||||
pageSize: Scalars['Int'];
|
||||
};
|
||||
|
||||
|
||||
export type QueryGetTimelineThreadsFromPersonIdArgs = {
|
||||
page: Scalars['Int'];
|
||||
pageSize: Scalars['Int'];
|
||||
personId: Scalars['UUID'];
|
||||
};
|
||||
|
||||
|
||||
export type QueryIndexArgs = {
|
||||
id: Scalars['UUID'];
|
||||
};
|
||||
@@ -3805,6 +4056,16 @@ export type QueryPieChartDataArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type QuerySearchArgs = {
|
||||
after?: InputMaybe<Scalars['String']>;
|
||||
excludedObjectNameSingulars?: InputMaybe<Array<Scalars['String']>>;
|
||||
filter?: InputMaybe<ObjectRecordFilterInput>;
|
||||
includedObjectNameSingulars?: InputMaybe<Array<Scalars['String']>>;
|
||||
limit: Scalars['Int'];
|
||||
searchInput: Scalars['String'];
|
||||
};
|
||||
|
||||
|
||||
export type QuerySkillArgs = {
|
||||
id: Scalars['UUID'];
|
||||
};
|
||||
@@ -4031,6 +4292,20 @@ export enum RowLevelPermissionPredicateOperand {
|
||||
VECTOR_SEARCH = 'VECTOR_SEARCH'
|
||||
}
|
||||
|
||||
export type RunWorkflowVersionInput = {
|
||||
/** Execution result in JSON format */
|
||||
payload?: InputMaybe<Scalars['JSON']>;
|
||||
/** Workflow run ID */
|
||||
workflowRunId?: InputMaybe<Scalars['UUID']>;
|
||||
/** Workflow version ID */
|
||||
workflowVersionId: Scalars['UUID'];
|
||||
};
|
||||
|
||||
export type RunWorkflowVersionOutput = {
|
||||
__typename?: 'RunWorkflowVersionOutput';
|
||||
workflowRunId: Scalars['UUID'];
|
||||
};
|
||||
|
||||
export type SsoConnection = {
|
||||
__typename?: 'SSOConnection';
|
||||
id: Scalars['UUID'];
|
||||
@@ -4066,6 +4341,12 @@ export type SearchRecord = {
|
||||
tsRankCD: Scalars['Float'];
|
||||
};
|
||||
|
||||
export type SearchResultConnection = {
|
||||
__typename?: 'SearchResultConnection';
|
||||
edges: Array<SearchResultEdge>;
|
||||
pageInfo: SearchResultPageInfo;
|
||||
};
|
||||
|
||||
export type SearchResultEdge = {
|
||||
__typename?: 'SearchResultEdge';
|
||||
cursor: Scalars['String'];
|
||||
@@ -4159,6 +4440,15 @@ export type StandardOverrides = {
|
||||
translations?: Maybe<Scalars['JSON']>;
|
||||
};
|
||||
|
||||
export type SubmitFormStepInput = {
|
||||
/** Form response in JSON format */
|
||||
response: Scalars['JSON'];
|
||||
/** Workflow step ID */
|
||||
stepId: Scalars['UUID'];
|
||||
/** Workflow run ID */
|
||||
workflowRunId: Scalars['UUID'];
|
||||
};
|
||||
|
||||
export type Subscription = {
|
||||
__typename?: 'Subscription';
|
||||
logicFunctionLogs: LogicFunctionLogs;
|
||||
@@ -4225,6 +4515,35 @@ export type TasksConfiguration = {
|
||||
configurationType: WidgetConfigurationType;
|
||||
};
|
||||
|
||||
export type TestHttpRequestInput = {
|
||||
/** Request body */
|
||||
body?: InputMaybe<Scalars['JSON']>;
|
||||
/** HTTP headers */
|
||||
headers?: InputMaybe<Scalars['JSON']>;
|
||||
/** HTTP method */
|
||||
method: Scalars['String'];
|
||||
/** URL to make the request to */
|
||||
url: Scalars['String'];
|
||||
};
|
||||
|
||||
export type TestHttpRequestOutput = {
|
||||
__typename?: 'TestHttpRequestOutput';
|
||||
/** Error information */
|
||||
error?: Maybe<Scalars['JSON']>;
|
||||
/** Response headers */
|
||||
headers?: Maybe<Scalars['JSON']>;
|
||||
/** Message describing the result */
|
||||
message: Scalars['String'];
|
||||
/** Response data */
|
||||
result?: Maybe<Scalars['JSON']>;
|
||||
/** HTTP status code */
|
||||
status?: Maybe<Scalars['Float']>;
|
||||
/** HTTP status text */
|
||||
statusText?: Maybe<Scalars['String']>;
|
||||
/** Whether the request was successful */
|
||||
success: Scalars['Boolean'];
|
||||
};
|
||||
|
||||
export type TimelineCalendarEvent = {
|
||||
__typename?: 'TimelineCalendarEvent';
|
||||
conferenceLink: LinksMetadata;
|
||||
@@ -4252,6 +4571,12 @@ export type TimelineCalendarEventParticipant = {
|
||||
workspaceMemberId?: Maybe<Scalars['UUID']>;
|
||||
};
|
||||
|
||||
export type TimelineCalendarEventsWithTotal = {
|
||||
__typename?: 'TimelineCalendarEventsWithTotal';
|
||||
timelineCalendarEvents: Array<TimelineCalendarEvent>;
|
||||
totalNumberOfCalendarEvents: Scalars['Int'];
|
||||
};
|
||||
|
||||
export type TimelineConfiguration = {
|
||||
__typename?: 'TimelineConfiguration';
|
||||
configurationType: WidgetConfigurationType;
|
||||
@@ -4282,6 +4607,12 @@ export type TimelineThreadParticipant = {
|
||||
workspaceMemberId?: Maybe<Scalars['UUID']>;
|
||||
};
|
||||
|
||||
export type TimelineThreadsWithTotal = {
|
||||
__typename?: 'TimelineThreadsWithTotal';
|
||||
timelineThreads: Array<TimelineThread>;
|
||||
totalNumberOfThreads: Scalars['Int'];
|
||||
};
|
||||
|
||||
export type ToolIndexEntry = {
|
||||
__typename?: 'ToolIndexEntry';
|
||||
category: Scalars['String'];
|
||||
@@ -4303,6 +4634,17 @@ export type TwoFactorAuthenticationMethodDto = {
|
||||
twoFactorAuthenticationMethodId: Scalars['UUID'];
|
||||
};
|
||||
|
||||
export type UuidFilter = {
|
||||
eq?: InputMaybe<Scalars['UUID']>;
|
||||
gt?: InputMaybe<Scalars['UUID']>;
|
||||
gte?: InputMaybe<Scalars['UUID']>;
|
||||
in?: InputMaybe<Array<Scalars['UUID']>>;
|
||||
is?: InputMaybe<FilterIs>;
|
||||
lt?: InputMaybe<Scalars['UUID']>;
|
||||
lte?: InputMaybe<Scalars['UUID']>;
|
||||
neq?: InputMaybe<Scalars['UUID']>;
|
||||
};
|
||||
|
||||
export type UuidFilterComparison = {
|
||||
eq?: InputMaybe<Scalars['UUID']>;
|
||||
gt?: InputMaybe<Scalars['UUID']>;
|
||||
@@ -4393,6 +4735,8 @@ export type UpdateLogicFunctionSourceInput = {
|
||||
|
||||
export type UpdateNavigationMenuItemInput = {
|
||||
folderId?: InputMaybe<Scalars['UUID']>;
|
||||
icon?: InputMaybe<Scalars['String']>;
|
||||
link?: InputMaybe<Scalars['String']>;
|
||||
name?: InputMaybe<Scalars['String']>;
|
||||
position?: InputMaybe<Scalars['Int']>;
|
||||
};
|
||||
@@ -4598,6 +4942,27 @@ export type UpdateWebhookInputUpdates = {
|
||||
targetUrl?: InputMaybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type UpdateWorkflowRunStepInput = {
|
||||
/** Step to update in JSON format */
|
||||
step: Scalars['JSON'];
|
||||
/** Workflow run ID */
|
||||
workflowRunId: Scalars['UUID'];
|
||||
};
|
||||
|
||||
export type UpdateWorkflowVersionPositionsInput = {
|
||||
/** Workflow version updated positions */
|
||||
positions: Array<WorkflowStepPositionUpdateInput>;
|
||||
/** Workflow version ID */
|
||||
workflowVersionId: Scalars['UUID'];
|
||||
};
|
||||
|
||||
export type UpdateWorkflowVersionStepInput = {
|
||||
/** Step to update in JSON format */
|
||||
step: Scalars['JSON'];
|
||||
/** Workflow version ID */
|
||||
workflowVersionId: Scalars['UUID'];
|
||||
};
|
||||
|
||||
export type UpdateWorkspaceInput = {
|
||||
aiAdditionalInstructions?: InputMaybe<Scalars['String']>;
|
||||
allowImpersonation?: InputMaybe<Scalars['Boolean']>;
|
||||
@@ -4886,27 +5251,104 @@ export type WorkerQueueMetrics = {
|
||||
waiting: Scalars['Float'];
|
||||
};
|
||||
|
||||
export type WorkflowAction = {
|
||||
__typename?: 'WorkflowAction';
|
||||
id: Scalars['UUID'];
|
||||
name: Scalars['String'];
|
||||
nextStepIds?: Maybe<Array<Scalars['UUID']>>;
|
||||
position?: Maybe<WorkflowStepPosition>;
|
||||
settings: Scalars['JSON'];
|
||||
type: WorkflowActionType;
|
||||
valid: Scalars['Boolean'];
|
||||
};
|
||||
|
||||
export enum WorkflowActionType {
|
||||
AI_AGENT = 'AI_AGENT',
|
||||
CODE = 'CODE',
|
||||
CREATE_RECORD = 'CREATE_RECORD',
|
||||
DELAY = 'DELAY',
|
||||
DELETE_RECORD = 'DELETE_RECORD',
|
||||
EMPTY = 'EMPTY',
|
||||
FILTER = 'FILTER',
|
||||
FIND_RECORDS = 'FIND_RECORDS',
|
||||
FORM = 'FORM',
|
||||
HTTP_REQUEST = 'HTTP_REQUEST',
|
||||
IF_ELSE = 'IF_ELSE',
|
||||
ITERATOR = 'ITERATOR',
|
||||
LOGIC_FUNCTION = 'LOGIC_FUNCTION',
|
||||
SEND_EMAIL = 'SEND_EMAIL',
|
||||
UPDATE_RECORD = 'UPDATE_RECORD',
|
||||
UPSERT_RECORD = 'UPSERT_RECORD'
|
||||
}
|
||||
|
||||
export type WorkflowConfiguration = {
|
||||
__typename?: 'WorkflowConfiguration';
|
||||
configurationType: WidgetConfigurationType;
|
||||
};
|
||||
|
||||
export type WorkflowRun = {
|
||||
__typename?: 'WorkflowRun';
|
||||
id: Scalars['UUID'];
|
||||
status: WorkflowRunStatusEnum;
|
||||
};
|
||||
|
||||
export type WorkflowRunConfiguration = {
|
||||
__typename?: 'WorkflowRunConfiguration';
|
||||
configurationType: WidgetConfigurationType;
|
||||
};
|
||||
|
||||
/** Status of the workflow run */
|
||||
export enum WorkflowRunStatusEnum {
|
||||
COMPLETED = 'COMPLETED',
|
||||
ENQUEUED = 'ENQUEUED',
|
||||
FAILED = 'FAILED',
|
||||
NOT_STARTED = 'NOT_STARTED',
|
||||
RUNNING = 'RUNNING',
|
||||
STOPPED = 'STOPPED',
|
||||
STOPPING = 'STOPPING'
|
||||
}
|
||||
|
||||
export type WorkflowStepPosition = {
|
||||
__typename?: 'WorkflowStepPosition';
|
||||
x: Scalars['Float'];
|
||||
y: Scalars['Float'];
|
||||
};
|
||||
|
||||
export type WorkflowStepPositionInput = {
|
||||
x: Scalars['Float'];
|
||||
y: Scalars['Float'];
|
||||
};
|
||||
|
||||
export type WorkflowStepPositionUpdateInput = {
|
||||
/** Step or trigger ID */
|
||||
id: Scalars['String'];
|
||||
/** Position of the step or trigger */
|
||||
position: WorkflowStepPositionInput;
|
||||
};
|
||||
|
||||
export type WorkflowVersionConfiguration = {
|
||||
__typename?: 'WorkflowVersionConfiguration';
|
||||
configurationType: WidgetConfigurationType;
|
||||
};
|
||||
|
||||
export type WorkflowVersionDto = {
|
||||
__typename?: 'WorkflowVersionDTO';
|
||||
createdAt: Scalars['String'];
|
||||
id: Scalars['UUID'];
|
||||
name: Scalars['String'];
|
||||
status: Scalars['String'];
|
||||
steps?: Maybe<Scalars['JSON']>;
|
||||
trigger?: Maybe<Scalars['JSON']>;
|
||||
updatedAt: Scalars['String'];
|
||||
workflowId: Scalars['UUID'];
|
||||
};
|
||||
|
||||
export type WorkflowVersionStepChanges = {
|
||||
__typename?: 'WorkflowVersionStepChanges';
|
||||
stepsDiff?: Maybe<Scalars['JSON']>;
|
||||
triggerDiff?: Maybe<Scalars['JSON']>;
|
||||
};
|
||||
|
||||
export type Workspace = {
|
||||
__typename?: 'Workspace';
|
||||
activationStatus: WorkspaceActivationStatus;
|
||||
@@ -5616,42 +6058,42 @@ export type FindManyMarketplaceAppsQueryVariables = Exact<{ [key: string]: never
|
||||
|
||||
export type FindManyMarketplaceAppsQuery = { __typename?: 'Query', findManyMarketplaceApps: Array<{ __typename?: 'MarketplaceApp', id: string, name: string, description: string, icon: string, version: string, author: string, category: string, logo?: string | null, screenshots: Array<string>, aboutDescription: string, providers: Array<string>, websiteUrl?: string | null, termsUrl?: string | null }> };
|
||||
|
||||
export type NavigationMenuItemFieldsFragment = { __typename?: 'NavigationMenuItem', id: string, userWorkspaceId?: string | null, targetRecordId?: string | null, targetObjectMetadataId?: string | null, viewId?: string | null, folderId?: string | null, name?: string | null, position: number, applicationId?: string | null, createdAt: string, updatedAt: string };
|
||||
export type NavigationMenuItemFieldsFragment = { __typename?: 'NavigationMenuItem', id: string, userWorkspaceId?: string | null, targetRecordId?: string | null, targetObjectMetadataId?: string | null, viewId?: string | null, folderId?: string | null, name?: string | null, link?: string | null, icon?: string | null, position: number, applicationId?: string | null, createdAt: string, updatedAt: string };
|
||||
|
||||
export type NavigationMenuItemQueryFieldsFragment = { __typename?: 'NavigationMenuItem', id: string, userWorkspaceId?: string | null, targetRecordId?: string | null, targetObjectMetadataId?: string | null, viewId?: string | null, folderId?: string | null, name?: string | null, position: number, applicationId?: string | null, createdAt: string, updatedAt: string, targetRecordIdentifier?: { __typename?: 'RecordIdentifier', id: string, labelIdentifier: string, imageIdentifier?: string | null } | null };
|
||||
export type NavigationMenuItemQueryFieldsFragment = { __typename?: 'NavigationMenuItem', id: string, userWorkspaceId?: string | null, targetRecordId?: string | null, targetObjectMetadataId?: string | null, viewId?: string | null, folderId?: string | null, name?: string | null, link?: string | null, icon?: string | null, position: number, applicationId?: string | null, createdAt: string, updatedAt: string, targetRecordIdentifier?: { __typename?: 'RecordIdentifier', id: string, labelIdentifier: string, imageIdentifier?: string | null } | null };
|
||||
|
||||
export type CreateNavigationMenuItemMutationVariables = Exact<{
|
||||
input: CreateNavigationMenuItemInput;
|
||||
}>;
|
||||
|
||||
|
||||
export type CreateNavigationMenuItemMutation = { __typename?: 'Mutation', createNavigationMenuItem: { __typename?: 'NavigationMenuItem', id: string, userWorkspaceId?: string | null, targetRecordId?: string | null, targetObjectMetadataId?: string | null, viewId?: string | null, folderId?: string | null, name?: string | null, position: number, applicationId?: string | null, createdAt: string, updatedAt: string } };
|
||||
export type CreateNavigationMenuItemMutation = { __typename?: 'Mutation', createNavigationMenuItem: { __typename?: 'NavigationMenuItem', id: string, userWorkspaceId?: string | null, targetRecordId?: string | null, targetObjectMetadataId?: string | null, viewId?: string | null, folderId?: string | null, name?: string | null, link?: string | null, icon?: string | null, position: number, applicationId?: string | null, createdAt: string, updatedAt: string } };
|
||||
|
||||
export type DeleteNavigationMenuItemMutationVariables = Exact<{
|
||||
id: Scalars['UUID'];
|
||||
}>;
|
||||
|
||||
|
||||
export type DeleteNavigationMenuItemMutation = { __typename?: 'Mutation', deleteNavigationMenuItem: { __typename?: 'NavigationMenuItem', id: string, userWorkspaceId?: string | null, targetRecordId?: string | null, targetObjectMetadataId?: string | null, viewId?: string | null, folderId?: string | null, name?: string | null, position: number, applicationId?: string | null, createdAt: string, updatedAt: string } };
|
||||
export type DeleteNavigationMenuItemMutation = { __typename?: 'Mutation', deleteNavigationMenuItem: { __typename?: 'NavigationMenuItem', id: string, userWorkspaceId?: string | null, targetRecordId?: string | null, targetObjectMetadataId?: string | null, viewId?: string | null, folderId?: string | null, name?: string | null, link?: string | null, icon?: string | null, position: number, applicationId?: string | null, createdAt: string, updatedAt: string } };
|
||||
|
||||
export type UpdateNavigationMenuItemMutationVariables = Exact<{
|
||||
input: UpdateOneNavigationMenuItemInput;
|
||||
}>;
|
||||
|
||||
|
||||
export type UpdateNavigationMenuItemMutation = { __typename?: 'Mutation', updateNavigationMenuItem: { __typename?: 'NavigationMenuItem', id: string, userWorkspaceId?: string | null, targetRecordId?: string | null, targetObjectMetadataId?: string | null, viewId?: string | null, folderId?: string | null, name?: string | null, position: number, applicationId?: string | null, createdAt: string, updatedAt: string } };
|
||||
export type UpdateNavigationMenuItemMutation = { __typename?: 'Mutation', updateNavigationMenuItem: { __typename?: 'NavigationMenuItem', id: string, userWorkspaceId?: string | null, targetRecordId?: string | null, targetObjectMetadataId?: string | null, viewId?: string | null, folderId?: string | null, name?: string | null, link?: string | null, icon?: string | null, position: number, applicationId?: string | null, createdAt: string, updatedAt: string } };
|
||||
|
||||
export type FindManyNavigationMenuItemsQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type FindManyNavigationMenuItemsQuery = { __typename?: 'Query', navigationMenuItems: Array<{ __typename?: 'NavigationMenuItem', id: string, userWorkspaceId?: string | null, targetRecordId?: string | null, targetObjectMetadataId?: string | null, viewId?: string | null, folderId?: string | null, name?: string | null, position: number, applicationId?: string | null, createdAt: string, updatedAt: string, targetRecordIdentifier?: { __typename?: 'RecordIdentifier', id: string, labelIdentifier: string, imageIdentifier?: string | null } | null }> };
|
||||
export type FindManyNavigationMenuItemsQuery = { __typename?: 'Query', navigationMenuItems: Array<{ __typename?: 'NavigationMenuItem', id: string, userWorkspaceId?: string | null, targetRecordId?: string | null, targetObjectMetadataId?: string | null, viewId?: string | null, folderId?: string | null, name?: string | null, link?: string | null, icon?: string | null, position: number, applicationId?: string | null, createdAt: string, updatedAt: string, targetRecordIdentifier?: { __typename?: 'RecordIdentifier', id: string, labelIdentifier: string, imageIdentifier?: string | null } | null }> };
|
||||
|
||||
export type FindOneNavigationMenuItemQueryVariables = Exact<{
|
||||
id: Scalars['UUID'];
|
||||
}>;
|
||||
|
||||
|
||||
export type FindOneNavigationMenuItemQuery = { __typename?: 'Query', navigationMenuItem?: { __typename?: 'NavigationMenuItem', id: string, userWorkspaceId?: string | null, targetRecordId?: string | null, targetObjectMetadataId?: string | null, viewId?: string | null, folderId?: string | null, name?: string | null, position: number, applicationId?: string | null, createdAt: string, updatedAt: string, targetRecordIdentifier?: { __typename?: 'RecordIdentifier', id: string, labelIdentifier: string, imageIdentifier?: string | null } | null } | null };
|
||||
export type FindOneNavigationMenuItemQuery = { __typename?: 'Query', navigationMenuItem?: { __typename?: 'NavigationMenuItem', id: string, userWorkspaceId?: string | null, targetRecordId?: string | null, targetObjectMetadataId?: string | null, viewId?: string | null, folderId?: string | null, name?: string | null, link?: string | null, icon?: string | null, position: number, applicationId?: string | null, createdAt: string, updatedAt: string, targetRecordIdentifier?: { __typename?: 'RecordIdentifier', id: string, labelIdentifier: string, imageIdentifier?: string | null } | null } | null };
|
||||
|
||||
export type ObjectMetadataFieldsFragment = { __typename?: 'Object', id: string, nameSingular: string, namePlural: string, labelSingular: string, labelPlural: string, description?: string | null, icon?: string | null, isCustom: boolean, isRemote: boolean, isActive: boolean, isSystem: boolean, isUIReadOnly: boolean, createdAt: string, updatedAt: string, labelIdentifierFieldMetadataId?: string | null, imageIdentifierFieldMetadataId?: string | null, applicationId: string, shortcut?: string | null, isLabelSyncedWithName: boolean, isSearchable: boolean, duplicateCriteria?: Array<Array<string>> | null, indexMetadataList: Array<{ __typename?: 'Index', id: string, createdAt: string, updatedAt: string, name: string, indexWhereClause?: string | null, indexType: IndexType, isUnique: boolean, isCustom?: boolean | null, indexFieldMetadataList: Array<{ __typename?: 'IndexField', id: string, fieldMetadataId: string, createdAt: string, updatedAt: string, order: number }> }>, fieldsList: Array<{ __typename?: 'Field', id: string, type: FieldMetadataType, name: string, label: string, description?: string | null, icon?: string | null, isCustom?: boolean | null, isActive?: boolean | null, isSystem?: boolean | null, isUIReadOnly?: boolean | null, isNullable?: boolean | null, isUnique?: boolean | null, createdAt: string, updatedAt: string, defaultValue?: any | null, options?: any | null, settings?: any | null, isLabelSyncedWithName?: boolean | null, morphId?: string | null, applicationId: string, relation?: { __typename?: 'Relation', type: RelationType, sourceObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, targetObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, sourceFieldMetadata: { __typename?: 'Field', id: string, name: string }, targetFieldMetadata: { __typename?: 'Field', id: string, name: string } } | null, morphRelations?: Array<{ __typename?: 'Relation', type: RelationType, sourceObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, targetObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, sourceFieldMetadata: { __typename?: 'Field', id: string, name: string }, targetFieldMetadata: { __typename?: 'Field', id: string, name: string } }> | null }> };
|
||||
|
||||
@@ -7081,6 +7523,8 @@ export const NavigationMenuItemFieldsFragmentDoc = gql`
|
||||
viewId
|
||||
folderId
|
||||
name
|
||||
link
|
||||
icon
|
||||
position
|
||||
applicationId
|
||||
createdAt
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+28
@@ -0,0 +1,28 @@
|
||||
import { Draggable } from '@hello-pangea/dnd';
|
||||
import { type ReactNode } from 'react';
|
||||
|
||||
type CommandMenuAddToNavDraggablePlaceholderProps = {
|
||||
index: number;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export const CommandMenuAddToNavDraggablePlaceholder = ({
|
||||
index,
|
||||
children,
|
||||
}: CommandMenuAddToNavDraggablePlaceholderProps) => (
|
||||
<Draggable
|
||||
draggableId={`add-to-nav-placeholder-${index}`}
|
||||
index={index}
|
||||
isDragDisabled={true}
|
||||
>
|
||||
{(provided) => (
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...provided.draggableProps}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
);
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import { Droppable, type DroppableProvided } from '@hello-pangea/dnd';
|
||||
import { type ReactNode, useContext } from 'react';
|
||||
|
||||
import { ADD_TO_NAV_SOURCE_DROPPABLE_ID } from '@/navigation-menu-item/constants/AddToNavSourceDroppableId';
|
||||
import { NavigationDragSourceContext } from '@/navigation-menu-item/contexts/NavigationDragSourceContext';
|
||||
|
||||
type CommandMenuAddToNavDroppableProps = {
|
||||
children: (provided: DroppableProvided) => ReactNode;
|
||||
};
|
||||
|
||||
export const CommandMenuAddToNavDroppable = ({
|
||||
children,
|
||||
}: CommandMenuAddToNavDroppableProps) => {
|
||||
const { sourceDroppableId } = useContext(NavigationDragSourceContext);
|
||||
const isDropDisabled = sourceDroppableId === ADD_TO_NAV_SOURCE_DROPPABLE_ID;
|
||||
|
||||
return (
|
||||
<Droppable
|
||||
droppableId={ADD_TO_NAV_SOURCE_DROPPABLE_ID}
|
||||
isDropDisabled={isDropDisabled}
|
||||
>
|
||||
{(provided) => children(provided)}
|
||||
</Droppable>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,115 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
|
||||
import { CommandMenuPageInfoLayout } from '@/command-menu/components/CommandMenuPageInfoLayout';
|
||||
import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState';
|
||||
import { commandMenuShouldFocusTitleInputComponentState } from '@/command-menu/states/commandMenuShouldFocusTitleInputComponentState';
|
||||
import { StyledNavigationMenuItemIconContainer } from '@/navigation-menu-item/components/NavigationMenuItemIconContainer';
|
||||
import { FOLDER_ICON_DEFAULT } from '@/navigation-menu-item/constants/folderIconDefault';
|
||||
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
|
||||
import { useUpdateFolderInDraft } from '@/navigation-menu-item/hooks/useUpdateFolderInDraft';
|
||||
import { useWorkspaceSectionItems } from '@/navigation-menu-item/hooks/useWorkspaceSectionItems';
|
||||
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/states/selectedNavigationMenuItemInEditModeState';
|
||||
import { getNavigationMenuItemIconColors } from '@/navigation-menu-item/utils/getNavigationMenuItemIconColors';
|
||||
import { IconPicker } from '@/ui/input/components/IconPicker';
|
||||
import { TitleInput } from '@/ui/input/components/TitleInput';
|
||||
import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState';
|
||||
|
||||
const StyledClickableIconWrapper = styled.div`
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
export const CommandMenuFolderInfo = () => {
|
||||
const theme = useTheme();
|
||||
const { t } = useLingui();
|
||||
const { getIcon } = useIcons();
|
||||
const commandMenuPageInfo = useRecoilValue(commandMenuPageInfoState);
|
||||
const [shouldFocusTitleInput, setShouldFocusTitleInput] =
|
||||
useRecoilComponentState(
|
||||
commandMenuShouldFocusTitleInputComponentState,
|
||||
commandMenuPageInfo.instanceId,
|
||||
);
|
||||
const selectedNavigationMenuItemInEditMode = useRecoilValue(
|
||||
selectedNavigationMenuItemInEditModeState,
|
||||
);
|
||||
const items = useWorkspaceSectionItems();
|
||||
const { updateFolderInDraft } = useUpdateFolderInDraft();
|
||||
|
||||
const defaultLabel = t`New folder`;
|
||||
const placeholder = t`Folder name`;
|
||||
|
||||
const selectedItem = selectedNavigationMenuItemInEditMode
|
||||
? items.find(
|
||||
(item) =>
|
||||
item.itemType === NavigationMenuItemType.FOLDER &&
|
||||
item.id === selectedNavigationMenuItemInEditMode,
|
||||
)
|
||||
: undefined;
|
||||
|
||||
if (!selectedItem) return null;
|
||||
|
||||
const itemId = selectedItem.id;
|
||||
const itemName = selectedItem.name ?? defaultLabel;
|
||||
|
||||
const handleChange = (text: string) => {
|
||||
updateFolderInDraft(itemId, { name: text });
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
const trimmed = itemName.trim();
|
||||
const finalName = trimmed.length > 0 ? trimmed : defaultLabel;
|
||||
|
||||
if (finalName !== itemName) {
|
||||
updateFolderInDraft(itemId, { name: finalName });
|
||||
}
|
||||
};
|
||||
|
||||
const selectedIconKey = selectedItem.icon ?? FOLDER_ICON_DEFAULT;
|
||||
const FolderIconComponent = getIcon(selectedIconKey);
|
||||
|
||||
return (
|
||||
<CommandMenuPageInfoLayout
|
||||
icon={
|
||||
<IconPicker
|
||||
dropdownId="command-menu-folder-icon-picker"
|
||||
selectedIconKey={selectedIconKey}
|
||||
onChange={({ iconKey }) =>
|
||||
updateFolderInDraft(itemId, { icon: iconKey })
|
||||
}
|
||||
clickableComponent={
|
||||
<StyledClickableIconWrapper>
|
||||
<StyledNavigationMenuItemIconContainer
|
||||
$backgroundColor={getNavigationMenuItemIconColors(theme).folder}
|
||||
>
|
||||
<FolderIconComponent
|
||||
size={theme.spacing(3.5)}
|
||||
color={theme.grayScale.gray1}
|
||||
stroke={theme.icon.stroke.md}
|
||||
/>
|
||||
</StyledNavigationMenuItemIconContainer>
|
||||
</StyledClickableIconWrapper>
|
||||
}
|
||||
/>
|
||||
}
|
||||
title={
|
||||
<TitleInput
|
||||
instanceId={`folder-name-${itemId}`}
|
||||
sizeVariant="sm"
|
||||
value={itemName}
|
||||
onChange={handleChange}
|
||||
placeholder={placeholder}
|
||||
onEnter={handleSave}
|
||||
onEscape={handleSave}
|
||||
onClickOutside={handleSave}
|
||||
onTab={handleSave}
|
||||
onShiftTab={handleSave}
|
||||
shouldFocus={shouldFocusTitleInput}
|
||||
onFocus={() => setShouldFocusTitleInput(false)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,11 +1,11 @@
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { type ReactNode } from 'react';
|
||||
import { IconArrowUpRight, type IconComponent } from 'twenty-ui/display';
|
||||
import { MenuItem } from 'twenty-ui/navigation';
|
||||
|
||||
import { useCommandMenuOnItemClick } from '@/command-menu/hooks/useCommandMenuOnItemClick';
|
||||
import { isSelectedItemIdComponentFamilySelector } from '@/ui/layout/selectable-list/states/selectors/isSelectedItemIdComponentFamilySelector';
|
||||
import { useRecoilComponentFamilyValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValue';
|
||||
import { type ReactNode } from 'react';
|
||||
import { IconArrowUpRight, type IconComponent } from 'twenty-ui/display';
|
||||
import { MenuItem } from 'twenty-ui/navigation';
|
||||
|
||||
export type CommandMenuItemProps = {
|
||||
label: string;
|
||||
@@ -15,6 +15,7 @@ export type CommandMenuItemProps = {
|
||||
onClick?: () => void;
|
||||
Icon?: IconComponent;
|
||||
hotKeys?: string[];
|
||||
LeftComponent?: ReactNode;
|
||||
RightComponent?: ReactNode;
|
||||
contextualTextPosition?: 'left' | 'right';
|
||||
hasSubMenu?: boolean;
|
||||
@@ -31,6 +32,7 @@ export const CommandMenuItem = ({
|
||||
onClick,
|
||||
Icon,
|
||||
hotKeys,
|
||||
LeftComponent,
|
||||
RightComponent,
|
||||
hasSubMenu = false,
|
||||
isSubMenuOpened = false,
|
||||
@@ -49,8 +51,9 @@ export const CommandMenuItem = ({
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
withIconContainer={true}
|
||||
LeftIcon={Icon}
|
||||
withIconContainer={!LeftComponent}
|
||||
LeftIcon={LeftComponent ? undefined : Icon}
|
||||
LeftComponent={LeftComponent}
|
||||
text={label}
|
||||
contextualText={description}
|
||||
contextualTextPosition={contextualTextPosition}
|
||||
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { Draggable } from '@hello-pangea/dnd';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { type ReactNode, useState } from 'react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { type IconComponent } from 'twenty-ui/display';
|
||||
|
||||
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
|
||||
import { AddToNavigationDragHandle } from '@/navigation-menu-item/components/AddToNavigationDragHandle';
|
||||
import { addToNavPayloadRegistryState } from '@/navigation-menu-item/states/addToNavPayloadRegistryState';
|
||||
import type { AddToNavigationDragPayload } from '@/navigation-menu-item/types/add-to-navigation-drag-payload';
|
||||
|
||||
type CommandMenuItemWithAddToNavigationDragProps = {
|
||||
icon?: IconComponent;
|
||||
customIconContent?: ReactNode;
|
||||
label: string;
|
||||
description?: string;
|
||||
id: string;
|
||||
onClick: () => void;
|
||||
payload: AddToNavigationDragPayload;
|
||||
dragIndex?: number;
|
||||
};
|
||||
|
||||
const StyledDraggableMenuItem = styled.div`
|
||||
cursor: grab;
|
||||
width: 100%;
|
||||
|
||||
&:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
`;
|
||||
|
||||
export const CommandMenuItemWithAddToNavigationDrag = ({
|
||||
icon,
|
||||
customIconContent,
|
||||
label,
|
||||
description,
|
||||
id,
|
||||
onClick,
|
||||
payload,
|
||||
dragIndex,
|
||||
}: CommandMenuItemWithAddToNavigationDragProps) => {
|
||||
const { t } = useLingui();
|
||||
const setRegistry = useSetRecoilState(addToNavPayloadRegistryState);
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
const contextualDescription = isHovered
|
||||
? t`Drag to add to navbar`
|
||||
: description;
|
||||
|
||||
const DragHandleIcon = () => (
|
||||
<AddToNavigationDragHandle
|
||||
icon={icon}
|
||||
customIconContent={customIconContent}
|
||||
payload={payload}
|
||||
isHovered={isHovered}
|
||||
/>
|
||||
);
|
||||
|
||||
const registerPayload = () => {
|
||||
if (dragIndex !== undefined) {
|
||||
setRegistry((prev) => new Map(prev).set(id, payload));
|
||||
}
|
||||
};
|
||||
|
||||
const menuItemContent = (
|
||||
<StyledDraggableMenuItem
|
||||
onMouseEnter={() => {
|
||||
setIsHovered(true);
|
||||
registerPayload();
|
||||
}}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
onMouseDown={registerPayload}
|
||||
>
|
||||
<CommandMenuItem
|
||||
Icon={DragHandleIcon}
|
||||
label={label}
|
||||
description={contextualDescription}
|
||||
id={id}
|
||||
onClick={onClick}
|
||||
/>
|
||||
</StyledDraggableMenuItem>
|
||||
);
|
||||
|
||||
if (dragIndex !== undefined) {
|
||||
return (
|
||||
<Draggable draggableId={id} index={dragIndex} isDragDisabled={false}>
|
||||
{(provided) => (
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...provided.draggableProps}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...provided.dragHandleProps}
|
||||
>
|
||||
{menuItemContent}
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
);
|
||||
}
|
||||
|
||||
return menuItemContent;
|
||||
};
|
||||
@@ -0,0 +1,94 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { IconLink } from 'twenty-ui/display';
|
||||
|
||||
import { CommandMenuPageInfoLayout } from '@/command-menu/components/CommandMenuPageInfoLayout';
|
||||
import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState';
|
||||
import { commandMenuShouldFocusTitleInputComponentState } from '@/command-menu/states/commandMenuShouldFocusTitleInputComponentState';
|
||||
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
|
||||
import { StyledNavigationMenuItemIconContainer } from '@/navigation-menu-item/components/NavigationMenuItemIconContainer';
|
||||
import { useUpdateLinkInDraft } from '@/navigation-menu-item/hooks/useUpdateLinkInDraft';
|
||||
import { useWorkspaceSectionItems } from '@/navigation-menu-item/hooks/useWorkspaceSectionItems';
|
||||
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/states/selectedNavigationMenuItemInEditModeState';
|
||||
import { getNavigationMenuItemIconColors } from '@/navigation-menu-item/utils/getNavigationMenuItemIconColors';
|
||||
import { TitleInput } from '@/ui/input/components/TitleInput';
|
||||
import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState';
|
||||
|
||||
export const CommandMenuLinkInfo = () => {
|
||||
const theme = useTheme();
|
||||
const { t } = useLingui();
|
||||
const commandMenuPageInfo = useRecoilValue(commandMenuPageInfoState);
|
||||
const [shouldFocusTitleInput, setShouldFocusTitleInput] =
|
||||
useRecoilComponentState(
|
||||
commandMenuShouldFocusTitleInputComponentState,
|
||||
commandMenuPageInfo.instanceId,
|
||||
);
|
||||
const selectedNavigationMenuItemInEditMode = useRecoilValue(
|
||||
selectedNavigationMenuItemInEditModeState,
|
||||
);
|
||||
const items = useWorkspaceSectionItems();
|
||||
const { updateLinkInDraft } = useUpdateLinkInDraft();
|
||||
|
||||
const defaultLabel = t`Link label`;
|
||||
const placeholder = t`Link label`;
|
||||
|
||||
const selectedItem = selectedNavigationMenuItemInEditMode
|
||||
? items.find(
|
||||
(item) =>
|
||||
item.itemType === NavigationMenuItemType.LINK &&
|
||||
item.id === selectedNavigationMenuItemInEditMode,
|
||||
)
|
||||
: undefined;
|
||||
|
||||
if (!selectedItem) return null;
|
||||
|
||||
const itemId = selectedItem.id;
|
||||
const itemName = selectedItem.name ?? defaultLabel;
|
||||
|
||||
const handleChange = (text: string) => {
|
||||
updateLinkInDraft(itemId, { name: text });
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
const trimmed = itemName.trim();
|
||||
const finalName = trimmed.length > 0 ? trimmed : defaultLabel;
|
||||
|
||||
if (finalName !== itemName) {
|
||||
updateLinkInDraft(itemId, { name: finalName });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<CommandMenuPageInfoLayout
|
||||
icon={
|
||||
<StyledNavigationMenuItemIconContainer
|
||||
$backgroundColor={getNavigationMenuItemIconColors(theme).link}
|
||||
>
|
||||
<IconLink
|
||||
size={theme.spacing(3.5)}
|
||||
color={theme.grayScale.gray1}
|
||||
stroke={theme.icon.stroke.md}
|
||||
/>
|
||||
</StyledNavigationMenuItemIconContainer>
|
||||
}
|
||||
title={
|
||||
<TitleInput
|
||||
instanceId={`link-label-${itemId}`}
|
||||
sizeVariant="sm"
|
||||
value={itemName}
|
||||
onChange={handleChange}
|
||||
placeholder={placeholder}
|
||||
onEnter={handleSave}
|
||||
onEscape={handleSave}
|
||||
onClickOutside={handleSave}
|
||||
onTab={handleSave}
|
||||
onShiftTab={handleSave}
|
||||
shouldFocus={shouldFocusTitleInput}
|
||||
onFocus={() => setShouldFocusTitleInput(false)}
|
||||
/>
|
||||
}
|
||||
label={t`link`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -20,6 +20,7 @@ export type CommandMenuListProps = {
|
||||
children?: React.ReactNode;
|
||||
loading?: boolean;
|
||||
noResults?: boolean;
|
||||
noResultsText?: string;
|
||||
};
|
||||
|
||||
const StyledInnerList = styled.div`
|
||||
@@ -60,6 +61,7 @@ export const CommandMenuList = ({
|
||||
children,
|
||||
loading = false,
|
||||
noResults = false,
|
||||
noResultsText,
|
||||
}: CommandMenuListProps) => {
|
||||
const setHasUserSelectedCommand = useSetRecoilState(
|
||||
hasUserSelectedCommandState,
|
||||
@@ -91,7 +93,7 @@ export const CommandMenuList = ({
|
||||
) : null,
|
||||
)}
|
||||
{noResults && !loading && (
|
||||
<StyledEmpty>{t`No results found`}</StyledEmpty>
|
||||
<StyledEmpty>{noResultsText ?? t`No results found`}</StyledEmpty>
|
||||
)}
|
||||
</SelectableList>
|
||||
</StyledInnerList>
|
||||
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { OverflowingTextWithTooltip } from 'twenty-ui/display';
|
||||
|
||||
import { CommandMenuPageInfoLayout } from '@/command-menu/components/CommandMenuPageInfoLayout';
|
||||
import { NavigationMenuItemIcon } from '@/navigation-menu-item/components/NavigationMenuItemIcon';
|
||||
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
|
||||
import { useSelectedNavigationMenuItemEditItem } from '@/navigation-menu-item/hooks/useSelectedNavigationMenuItemEditItem';
|
||||
import { useSelectedNavigationMenuItemEditItemLabel } from '@/navigation-menu-item/hooks/useSelectedNavigationMenuItemEditItemLabel';
|
||||
|
||||
export const CommandMenuObjectViewRecordInfo = () => {
|
||||
const { t } = useLingui();
|
||||
const { selectedItem } = useSelectedNavigationMenuItemEditItem();
|
||||
const { selectedItemLabel } = useSelectedNavigationMenuItemEditItemLabel();
|
||||
|
||||
const processedItem =
|
||||
selectedItem && selectedItem.itemType !== NavigationMenuItemType.FOLDER
|
||||
? selectedItem
|
||||
: undefined;
|
||||
|
||||
if (!processedItem || !selectedItemLabel) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isViewOrRecord = [
|
||||
NavigationMenuItemType.VIEW,
|
||||
NavigationMenuItemType.RECORD,
|
||||
].includes(processedItem.itemType);
|
||||
|
||||
if (!isViewOrRecord) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const label = isViewOrRecord
|
||||
? processedItem.itemType === NavigationMenuItemType.RECORD
|
||||
? t`record`
|
||||
: t`view`
|
||||
: t`object`;
|
||||
|
||||
return (
|
||||
<CommandMenuPageInfoLayout
|
||||
icon={<NavigationMenuItemIcon navigationMenuItem={processedItem} />}
|
||||
title={<OverflowingTextWithTooltip text={selectedItemLabel} />}
|
||||
label={label}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,11 +1,20 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { OverflowingTextWithTooltip } from 'twenty-ui/display';
|
||||
|
||||
import { CommandMenuFolderInfo } from '@/command-menu/components/CommandMenuFolderInfo';
|
||||
import { CommandMenuLinkInfo } from '@/command-menu/components/CommandMenuLinkInfo';
|
||||
import { CommandMenuMultipleRecordsInfo } from '@/command-menu/components/CommandMenuMultipleRecordsInfo';
|
||||
import { CommandMenuObjectViewRecordInfo } from '@/command-menu/components/CommandMenuObjectViewRecordInfo';
|
||||
import { CommandMenuPageLayoutInfo } from '@/command-menu/components/CommandMenuPageLayoutInfo';
|
||||
import { CommandMenuRecordInfo } from '@/command-menu/components/CommandMenuRecordInfo';
|
||||
import { CommandMenuWorkflowStepInfo } from '@/command-menu/components/CommandMenuWorkflowStepInfo';
|
||||
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
|
||||
import styled from '@emotion/styled';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { OverflowingTextWithTooltip } from 'twenty-ui/display';
|
||||
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
|
||||
import { useWorkspaceSectionItems } from '@/navigation-menu-item/hooks/useWorkspaceSectionItems';
|
||||
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/states/selectedNavigationMenuItemInEditModeState';
|
||||
|
||||
import { type CommandMenuContextChipProps } from './CommandMenuContextChip';
|
||||
|
||||
const StyledPageTitle = styled.div`
|
||||
@@ -19,10 +28,40 @@ type CommandMenuPageInfoProps = {
|
||||
};
|
||||
|
||||
export const CommandMenuPageInfo = ({ pageChip }: CommandMenuPageInfoProps) => {
|
||||
const selectedNavigationMenuItemInEditMode = useRecoilValue(
|
||||
selectedNavigationMenuItemInEditModeState,
|
||||
);
|
||||
const items = useWorkspaceSectionItems();
|
||||
|
||||
if (!isDefined(pageChip)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isNavigationMenuItemEditPage =
|
||||
pageChip.page?.page === CommandMenuPages.NavigationMenuItemEdit;
|
||||
const selectedNavItem = isNavigationMenuItemEditPage
|
||||
? items.find((item) => item.id === selectedNavigationMenuItemInEditMode)
|
||||
: undefined;
|
||||
|
||||
if (isNavigationMenuItemEditPage && isDefined(selectedNavItem)) {
|
||||
const itemType = selectedNavItem.itemType;
|
||||
|
||||
if (itemType === NavigationMenuItemType.FOLDER) {
|
||||
return <CommandMenuFolderInfo />;
|
||||
}
|
||||
|
||||
if (itemType === NavigationMenuItemType.LINK) {
|
||||
return <CommandMenuLinkInfo />;
|
||||
}
|
||||
|
||||
if (
|
||||
itemType === NavigationMenuItemType.VIEW ||
|
||||
itemType === NavigationMenuItemType.RECORD
|
||||
) {
|
||||
return <CommandMenuObjectViewRecordInfo />;
|
||||
}
|
||||
}
|
||||
|
||||
const isRecordPage = pageChip.page?.page === CommandMenuPages.ViewRecord;
|
||||
|
||||
if (isRecordPage && isDefined(pageChip.page?.pageId)) {
|
||||
|
||||
+1
@@ -134,6 +134,7 @@ export const CommandMenuSidePanelForDesktop = () => {
|
||||
isOpen={isCommandMenuOpened}
|
||||
isResizing={isResizing}
|
||||
onTransitionEnd={handleTransitionEnd}
|
||||
data-command-menu-panel=""
|
||||
>
|
||||
<StyledSidePanel>
|
||||
<StyledModalContainer ref={handleModalContainerRef} />
|
||||
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { type ReactNode } from 'react';
|
||||
|
||||
import { SidePanelSubPageNavigationHeader } from '@/command-menu/pages/common/components/SidePanelSubPageNavigationHeader';
|
||||
|
||||
const StyledSubViewContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const StyledSearchContainer = styled.div`
|
||||
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
|
||||
min-width: 0;
|
||||
padding: ${({ theme }) => theme.spacing(2, 3)};
|
||||
`;
|
||||
|
||||
const StyledSearchInput = styled.input`
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
box-sizing: border-box;
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
font-size: ${({ theme }) => theme.font.size.md};
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
outline: none;
|
||||
|
||||
&::placeholder {
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledScrollableListWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
|
||||
& > * {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
type CommandMenuSubViewWithSearchProps = {
|
||||
backBarTitle: string;
|
||||
onBack: () => void;
|
||||
searchPlaceholder: string;
|
||||
searchValue: string;
|
||||
onSearchChange: (value: string) => void;
|
||||
searchInputProps?: React.InputHTMLAttributes<HTMLInputElement>;
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
export const CommandMenuSubViewWithSearch = ({
|
||||
backBarTitle,
|
||||
onBack,
|
||||
searchPlaceholder,
|
||||
searchValue,
|
||||
onSearchChange,
|
||||
searchInputProps,
|
||||
children,
|
||||
}: CommandMenuSubViewWithSearchProps) => (
|
||||
<StyledSubViewContainer>
|
||||
<SidePanelSubPageNavigationHeader
|
||||
title={backBarTitle}
|
||||
onBackClick={onBack}
|
||||
/>
|
||||
<StyledSearchContainer>
|
||||
<StyledSearchInput
|
||||
placeholder={searchPlaceholder}
|
||||
value={searchValue}
|
||||
onChange={(event) => onSearchChange(event.target.value)}
|
||||
autoFocus
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...searchInputProps}
|
||||
/>
|
||||
</StyledSearchContainer>
|
||||
{children != null && (
|
||||
<StyledScrollableListWrapper>{children}</StyledScrollableListWrapper>
|
||||
)}
|
||||
</StyledSubViewContainer>
|
||||
);
|
||||
@@ -4,6 +4,8 @@ import { CommandMenuAskAIPage } from '@/command-menu/pages/ask-ai/components/Com
|
||||
import { CommandMenuCalendarEventPage } from '@/command-menu/pages/calendar-event/components/CommandMenuCalendarEventPage';
|
||||
import { CommandMenuFrontComponentPage } from '@/command-menu/pages/front-component/components/CommandMenuFrontComponentPage';
|
||||
import { CommandMenuMessageThreadPage } from '@/command-menu/pages/message-thread/components/CommandMenuMessageThreadPage';
|
||||
import { CommandMenuNavigationMenuItemEditPage } from '@/command-menu/pages/navigation-menu-item/components/CommandMenuNavigationMenuItemEditPage';
|
||||
import { CommandMenuNewSidebarItemPage } from '@/command-menu/pages/navigation-menu-item/components/CommandMenuNewSidebarItemPage';
|
||||
import { CommandMenuPageLayoutChartSettings } from '@/command-menu/pages/page-layout/components/CommandMenuPageLayoutChartSettings';
|
||||
import { CommandMenuPageLayoutFieldsLayout } from '@/command-menu/pages/page-layout/components/CommandMenuPageLayoutFieldsLayout';
|
||||
import { CommandMenuPageLayoutFieldsSettings } from '@/command-menu/pages/page-layout/components/CommandMenuPageLayoutFieldsSettings';
|
||||
@@ -76,4 +78,9 @@ export const COMMAND_MENU_PAGES_CONFIG = new Map<
|
||||
<CommandMenuPageLayoutFieldsLayout />,
|
||||
],
|
||||
[CommandMenuPages.ViewFrontComponent, <CommandMenuFrontComponentPage />],
|
||||
[
|
||||
CommandMenuPages.NavigationMenuItemEdit,
|
||||
<CommandMenuNavigationMenuItemEditPage />,
|
||||
],
|
||||
[CommandMenuPages.NavigationMenuAddItem, <CommandMenuNewSidebarItemPage />],
|
||||
]);
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
|
||||
|
||||
import { SIDE_PANEL_FOCUS_ID } from '@/command-menu/constants/SidePanelFocusId';
|
||||
import { useNavigateCommandMenu } from '@/command-menu/hooks/useNavigateCommandMenu';
|
||||
import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
|
||||
import { isCommandMenuClosingState } from '@/command-menu/states/isCommandMenuClosingState';
|
||||
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
|
||||
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
|
||||
import { addToNavPayloadRegistryState } from '@/navigation-menu-item/states/addToNavPayloadRegistryState';
|
||||
import { useCloseAnyOpenDropdown } from '@/ui/layout/dropdown/hooks/useCloseAnyOpenDropdown';
|
||||
import { emitSidePanelOpenEvent } from '@/ui/layout/right-drawer/utils/emitSidePanelOpenEvent';
|
||||
import { useRemoveFocusItemFromFocusStackById } from '@/ui/utilities/focus/hooks/useRemoveFocusItemFromFocusStackById';
|
||||
@@ -29,6 +29,7 @@ export const useCommandMenu = () => {
|
||||
.getValue();
|
||||
|
||||
if (isCommandMenuOpened) {
|
||||
set(addToNavPayloadRegistryState, new Map());
|
||||
set(isCommandMenuOpenedState, false);
|
||||
set(isCommandMenuClosingState, true);
|
||||
closeAnyOpenDropdown();
|
||||
|
||||
@@ -86,6 +86,9 @@ export const useCommandMenuHotKeys = () => {
|
||||
},
|
||||
focusId: SIDE_PANEL_FOCUS_ID,
|
||||
dependencies: [goBackFromCommandMenu],
|
||||
options: {
|
||||
enableOnFormTags: false,
|
||||
},
|
||||
});
|
||||
|
||||
useHotkeysOnFocusedElement({
|
||||
@@ -119,6 +122,7 @@ export const useCommandMenuHotKeys = () => {
|
||||
],
|
||||
options: {
|
||||
preventDefault: false,
|
||||
enableOnFormTags: false,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { filterBySearchQuery } from '~/utils/filterBySearchQuery';
|
||||
|
||||
type UseFilteredPickerItemsParams<T> = {
|
||||
items: T[];
|
||||
searchQuery: string;
|
||||
getSearchableValues: (item: T) => string[];
|
||||
appendSelectableIds?: string[];
|
||||
};
|
||||
|
||||
type UseFilteredPickerItemsResult<T> = {
|
||||
filteredItems: T[];
|
||||
selectableItemIds: string[];
|
||||
isEmpty: boolean;
|
||||
hasSearchQuery: boolean;
|
||||
};
|
||||
|
||||
export const useFilteredPickerItems = <T extends { id: string }>({
|
||||
items,
|
||||
searchQuery,
|
||||
getSearchableValues,
|
||||
appendSelectableIds = [],
|
||||
}: UseFilteredPickerItemsParams<T>): UseFilteredPickerItemsResult<T> => {
|
||||
const filteredItems = filterBySearchQuery({
|
||||
items,
|
||||
searchQuery,
|
||||
getSearchableValues,
|
||||
});
|
||||
|
||||
const selectableItemIds =
|
||||
filteredItems.length > 0
|
||||
? [...filteredItems.map((item) => item.id), ...appendSelectableIds]
|
||||
: appendSelectableIds;
|
||||
|
||||
const isEmpty = filteredItems.length === 0;
|
||||
const hasSearchQuery = searchQuery.trim().length > 0;
|
||||
|
||||
return {
|
||||
filteredItems,
|
||||
selectableItemIds,
|
||||
isEmpty,
|
||||
hasSearchQuery,
|
||||
};
|
||||
};
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
export enum CommandMenuNavigationItemActions {
|
||||
MOVE_UP = 'move-up',
|
||||
MOVE_DOWN = 'move-down',
|
||||
MOVE_TO_FOLDER = 'move-to-folder',
|
||||
ADD_BEFORE = 'add-before',
|
||||
ADD_AFTER = 'add-after',
|
||||
REMOVE = 'remove',
|
||||
}
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
import { useState } from 'react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { IconFolderPlus } from 'twenty-ui/display';
|
||||
|
||||
import { CommandGroup } from '@/command-menu/components/CommandGroup';
|
||||
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
|
||||
import { CommandMenuList } from '@/command-menu/components/CommandMenuList';
|
||||
import { CommandMenuSubViewWithSearch } from '@/command-menu/components/CommandMenuSubViewWithSearch';
|
||||
import { useFolderPickerSelectionData } from '@/command-menu/pages/navigation-menu-item/hooks/useFolderPickerSelectionData';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
import { filterBySearchQuery } from '~/utils/filterBySearchQuery';
|
||||
|
||||
type CommandMenuEditFolderPickerSubViewProps = {
|
||||
onBack: () => void;
|
||||
};
|
||||
|
||||
export const CommandMenuEditFolderPickerSubView = ({
|
||||
onBack,
|
||||
}: CommandMenuEditFolderPickerSubViewProps) => {
|
||||
const { t } = useLingui();
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
const { foldersToShow, includeNoFolderOption, handleSelectFolder } =
|
||||
useFolderPickerSelectionData({ onCloseSubView: onBack });
|
||||
|
||||
const filteredFolders = filterBySearchQuery({
|
||||
items: foldersToShow,
|
||||
searchQuery: searchValue,
|
||||
getSearchableValues: (folder) => [folder.name],
|
||||
});
|
||||
const isEmpty = filteredFolders.length === 0 && !includeNoFolderOption;
|
||||
const selectableItemIds = [
|
||||
...(includeNoFolderOption ? ['no-folder'] : []),
|
||||
...(filteredFolders.length > 0 ? filteredFolders.map((f) => f.id) : []),
|
||||
];
|
||||
const noResultsText =
|
||||
searchValue.trim().length > 0
|
||||
? t`No results found`
|
||||
: t`No folders available`;
|
||||
|
||||
return (
|
||||
<CommandMenuSubViewWithSearch
|
||||
backBarTitle={t`Move to a folder`}
|
||||
onBack={onBack}
|
||||
searchPlaceholder={t`Search a folder...`}
|
||||
searchValue={searchValue}
|
||||
onSearchChange={setSearchValue}
|
||||
>
|
||||
<CommandMenuList
|
||||
commandGroups={[]}
|
||||
selectableItemIds={selectableItemIds}
|
||||
noResults={isEmpty}
|
||||
noResultsText={noResultsText}
|
||||
>
|
||||
<CommandGroup heading={t`Folders`}>
|
||||
{includeNoFolderOption && (
|
||||
<SelectableListItem
|
||||
itemId="no-folder"
|
||||
onEnter={() => handleSelectFolder(null)}
|
||||
>
|
||||
<CommandMenuItem
|
||||
label={t`No folder`}
|
||||
id="no-folder"
|
||||
onClick={() => handleSelectFolder(null)}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
)}
|
||||
{filteredFolders.map((folder) => (
|
||||
<SelectableListItem
|
||||
key={folder.id}
|
||||
itemId={folder.id}
|
||||
onEnter={() => handleSelectFolder(folder.id)}
|
||||
>
|
||||
<CommandMenuItem
|
||||
Icon={IconFolderPlus}
|
||||
label={folder.name}
|
||||
id={folder.id}
|
||||
onClick={() => handleSelectFolder(folder.id)}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandMenuList>
|
||||
</CommandMenuSubViewWithSearch>
|
||||
);
|
||||
};
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { useState } from 'react';
|
||||
import { getAbsoluteUrl } from 'twenty-shared/utils';
|
||||
|
||||
import { CommandGroup } from '@/command-menu/components/CommandGroup';
|
||||
import { CommandMenuList } from '@/command-menu/components/CommandMenuList';
|
||||
import {
|
||||
type OrganizeActionsProps,
|
||||
CommandMenuEditOrganizeActions,
|
||||
} from '@/command-menu/pages/navigation-menu-item/components/CommandMenuEditOrganizeActions';
|
||||
import { getOrganizeActionsSelectableItemIds } from '@/command-menu/pages/navigation-menu-item/utils/getOrganizeActionsSelectableItemIds';
|
||||
import { CommandMenuEditOwnerSection } from '@/command-menu/pages/navigation-menu-item/components/CommandMenuEditOwnerSection';
|
||||
import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/types/processed-navigation-menu-item';
|
||||
import { TextInput } from '@/ui/input/components/TextInput';
|
||||
|
||||
type CommandMenuEditLinkItemViewProps = OrganizeActionsProps & {
|
||||
selectedItem: ProcessedNavigationMenuItem;
|
||||
onUpdateLink: (linkId: string, link: string) => void;
|
||||
onOpenFolderPicker: () => void;
|
||||
};
|
||||
|
||||
export const CommandMenuEditLinkItemView = ({
|
||||
selectedItem,
|
||||
onUpdateLink,
|
||||
canMoveUp,
|
||||
canMoveDown,
|
||||
onOpenFolderPicker,
|
||||
onMoveUp,
|
||||
onMoveDown,
|
||||
onRemove,
|
||||
onAddBefore,
|
||||
onAddAfter,
|
||||
}: CommandMenuEditLinkItemViewProps) => {
|
||||
const { t } = useLingui();
|
||||
const [urlEditInput, setUrlEditInput] = useState('');
|
||||
|
||||
const selectableItemIds = getOrganizeActionsSelectableItemIds(true);
|
||||
|
||||
return (
|
||||
<CommandMenuList commandGroups={[]} selectableItemIds={selectableItemIds}>
|
||||
<CommandGroup heading={t`Customize`}>
|
||||
<TextInput
|
||||
fullWidth
|
||||
placeholder="www.google.com"
|
||||
value={urlEditInput || selectedItem.link}
|
||||
onChange={(value) => setUrlEditInput(value)}
|
||||
onBlur={(event) => {
|
||||
const value = event.target.value.trim();
|
||||
if (isNonEmptyString(value)) {
|
||||
onUpdateLink(selectedItem.id, getAbsoluteUrl(value));
|
||||
setUrlEditInput('');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</CommandGroup>
|
||||
<CommandMenuEditOrganizeActions
|
||||
canMoveUp={canMoveUp}
|
||||
canMoveDown={canMoveDown}
|
||||
onMoveUp={onMoveUp}
|
||||
onMoveDown={onMoveDown}
|
||||
onRemove={onRemove}
|
||||
onAddBefore={onAddBefore}
|
||||
onAddAfter={onAddAfter}
|
||||
showMoveToFolder
|
||||
onMoveToFolder={onOpenFolderPicker}
|
||||
moveToFolderHasSubMenu
|
||||
/>
|
||||
<CommandMenuEditOwnerSection applicationId={selectedItem.applicationId} />
|
||||
</CommandMenuList>
|
||||
);
|
||||
};
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
import {
|
||||
type OrganizeActionsProps,
|
||||
CommandMenuEditOrganizeActions,
|
||||
} from '@/command-menu/pages/navigation-menu-item/components/CommandMenuEditOrganizeActions';
|
||||
import { getOrganizeActionsSelectableItemIds } from '@/command-menu/pages/navigation-menu-item/utils/getOrganizeActionsSelectableItemIds';
|
||||
import { CommandMenuList } from '@/command-menu/components/CommandMenuList';
|
||||
|
||||
type CommandMenuEditObjectViewBaseProps = OrganizeActionsProps & {
|
||||
onOpenFolderPicker: () => void;
|
||||
};
|
||||
|
||||
export const CommandMenuEditObjectViewBase = ({
|
||||
onOpenFolderPicker,
|
||||
canMoveUp,
|
||||
canMoveDown,
|
||||
onMoveUp,
|
||||
onMoveDown,
|
||||
onRemove,
|
||||
onAddBefore,
|
||||
onAddAfter,
|
||||
}: CommandMenuEditObjectViewBaseProps) => {
|
||||
const selectableItemIds = getOrganizeActionsSelectableItemIds(true);
|
||||
|
||||
return (
|
||||
<CommandMenuList commandGroups={[]} selectableItemIds={selectableItemIds}>
|
||||
<CommandMenuEditOrganizeActions
|
||||
canMoveUp={canMoveUp}
|
||||
canMoveDown={canMoveDown}
|
||||
onMoveUp={onMoveUp}
|
||||
onMoveDown={onMoveDown}
|
||||
onRemove={onRemove}
|
||||
onAddBefore={onAddBefore}
|
||||
onAddAfter={onAddAfter}
|
||||
showMoveToFolder
|
||||
onMoveToFolder={onOpenFolderPicker}
|
||||
/>
|
||||
</CommandMenuList>
|
||||
);
|
||||
};
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import {
|
||||
IconChevronDown,
|
||||
IconChevronUp,
|
||||
IconFolderSymlink,
|
||||
IconRowInsertBottom,
|
||||
IconRowInsertTop,
|
||||
IconTrash,
|
||||
} from 'twenty-ui/display';
|
||||
|
||||
import { CommandGroup } from '@/command-menu/components/CommandGroup';
|
||||
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
|
||||
import { CommandMenuNavigationItemActions } from '@/command-menu/pages/navigation-menu-item/command-menu-navigation-item-actions';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
|
||||
export type OrganizeActionsProps = {
|
||||
canMoveUp: boolean;
|
||||
canMoveDown: boolean;
|
||||
onMoveUp: () => void;
|
||||
onMoveDown: () => void;
|
||||
onRemove: () => void;
|
||||
onAddBefore?: () => void;
|
||||
onAddAfter?: () => void;
|
||||
};
|
||||
|
||||
type CommandMenuEditOrganizeActionsProps = OrganizeActionsProps & {
|
||||
showMoveToFolder?: boolean;
|
||||
onMoveToFolder?: () => void;
|
||||
moveToFolderHasSubMenu?: boolean;
|
||||
};
|
||||
|
||||
export const CommandMenuEditOrganizeActions = ({
|
||||
canMoveUp,
|
||||
canMoveDown,
|
||||
onMoveUp,
|
||||
onMoveDown,
|
||||
onRemove,
|
||||
onAddBefore,
|
||||
onAddAfter,
|
||||
showMoveToFolder = false,
|
||||
onMoveToFolder,
|
||||
moveToFolderHasSubMenu = false,
|
||||
}: CommandMenuEditOrganizeActionsProps) => {
|
||||
const { t } = useLingui();
|
||||
|
||||
return (
|
||||
<CommandGroup heading={t`Organize`}>
|
||||
<SelectableListItem
|
||||
itemId={CommandMenuNavigationItemActions.MOVE_UP}
|
||||
onEnter={canMoveUp ? onMoveUp : undefined}
|
||||
>
|
||||
<CommandMenuItem
|
||||
Icon={IconChevronUp}
|
||||
label={t`Move up`}
|
||||
id={CommandMenuNavigationItemActions.MOVE_UP}
|
||||
onClick={onMoveUp}
|
||||
disabled={!canMoveUp}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
<SelectableListItem
|
||||
itemId={CommandMenuNavigationItemActions.MOVE_DOWN}
|
||||
onEnter={canMoveDown ? onMoveDown : undefined}
|
||||
>
|
||||
<CommandMenuItem
|
||||
Icon={IconChevronDown}
|
||||
label={t`Move down`}
|
||||
id={CommandMenuNavigationItemActions.MOVE_DOWN}
|
||||
onClick={onMoveDown}
|
||||
disabled={!canMoveDown}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
{showMoveToFolder && onMoveToFolder && (
|
||||
<SelectableListItem
|
||||
itemId={CommandMenuNavigationItemActions.MOVE_TO_FOLDER}
|
||||
onEnter={onMoveToFolder}
|
||||
>
|
||||
<CommandMenuItem
|
||||
Icon={IconFolderSymlink}
|
||||
label={t`Move to folder`}
|
||||
id={CommandMenuNavigationItemActions.MOVE_TO_FOLDER}
|
||||
hasSubMenu={moveToFolderHasSubMenu}
|
||||
onClick={onMoveToFolder}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
)}
|
||||
{onAddBefore && (
|
||||
<SelectableListItem
|
||||
itemId={CommandMenuNavigationItemActions.ADD_BEFORE}
|
||||
onEnter={onAddBefore}
|
||||
>
|
||||
<CommandMenuItem
|
||||
Icon={IconRowInsertTop}
|
||||
label={t`Add menu item before`}
|
||||
id={CommandMenuNavigationItemActions.ADD_BEFORE}
|
||||
onClick={onAddBefore}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
)}
|
||||
{onAddAfter && (
|
||||
<SelectableListItem
|
||||
itemId={CommandMenuNavigationItemActions.ADD_AFTER}
|
||||
onEnter={onAddAfter}
|
||||
>
|
||||
<CommandMenuItem
|
||||
Icon={IconRowInsertBottom}
|
||||
label={t`Add menu item after`}
|
||||
id={CommandMenuNavigationItemActions.ADD_AFTER}
|
||||
onClick={onAddAfter}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
)}
|
||||
<SelectableListItem
|
||||
itemId={CommandMenuNavigationItemActions.REMOVE}
|
||||
onEnter={onRemove}
|
||||
>
|
||||
<CommandMenuItem
|
||||
Icon={IconTrash}
|
||||
label={t`Remove from sidebar`}
|
||||
id={CommandMenuNavigationItemActions.REMOVE}
|
||||
onClick={onRemove}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
</CommandGroup>
|
||||
);
|
||||
};
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IconApps } from 'twenty-ui/display';
|
||||
|
||||
import { CommandGroup } from '@/command-menu/components/CommandGroup';
|
||||
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
|
||||
import { useDraftNavigationMenuItems } from '@/navigation-menu-item/hooks/useDraftNavigationMenuItems';
|
||||
import { useSelectedNavigationMenuItemEditItem } from '@/navigation-menu-item/hooks/useSelectedNavigationMenuItemEditItem';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
import { useFindOneApplicationQuery } from '~/generated-metadata/graphql';
|
||||
|
||||
type CommandMenuEditOwnerSectionProps = {
|
||||
applicationId?: string | null;
|
||||
};
|
||||
|
||||
export const CommandMenuEditOwnerSection = ({
|
||||
applicationId: applicationIdProp,
|
||||
}: CommandMenuEditOwnerSectionProps) => {
|
||||
const { t } = useLingui();
|
||||
|
||||
const { selectedItem } = useSelectedNavigationMenuItemEditItem();
|
||||
const { currentDraft } = useDraftNavigationMenuItems();
|
||||
|
||||
const applicationIdFromDraft =
|
||||
selectedItem && currentDraft
|
||||
? currentDraft.find((item) => item.id === selectedItem.id)?.applicationId
|
||||
: undefined;
|
||||
|
||||
const applicationId = applicationIdProp ?? applicationIdFromDraft;
|
||||
|
||||
const { data } = useFindOneApplicationQuery({
|
||||
variables: { id: applicationId ?? '' },
|
||||
skip: !isDefined(applicationId),
|
||||
});
|
||||
|
||||
const applicationName = data?.findOneApplication?.name;
|
||||
|
||||
if (!isDefined(applicationName)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<CommandGroup heading={t`Owner`}>
|
||||
<SelectableListItem itemId="owner-app" onEnter={() => {}}>
|
||||
<CommandMenuItem
|
||||
Icon={IconApps}
|
||||
label={applicationName}
|
||||
id="owner-app"
|
||||
disabled
|
||||
/>
|
||||
</SelectableListItem>
|
||||
</CommandGroup>
|
||||
);
|
||||
};
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
import { CommandMenuList } from '@/command-menu/components/CommandMenuList';
|
||||
import { CommandMenuEditFolderPickerSubView } from '@/command-menu/pages/navigation-menu-item/components/CommandMenuEditFolderPickerSubView';
|
||||
import { CommandMenuEditLinkItemView } from '@/command-menu/pages/navigation-menu-item/components/CommandMenuEditLinkItemView';
|
||||
import { CommandMenuEditObjectViewBase } from '@/command-menu/pages/navigation-menu-item/components/CommandMenuEditObjectViewBase';
|
||||
import { CommandMenuEditOrganizeActions } from '@/command-menu/pages/navigation-menu-item/components/CommandMenuEditOrganizeActions';
|
||||
import { CommandMenuEditOwnerSection } from '@/command-menu/pages/navigation-menu-item/components/CommandMenuEditOwnerSection';
|
||||
import { useNavigationMenuItemEditOrganizeActions } from '@/command-menu/pages/navigation-menu-item/hooks/useNavigationMenuItemEditOrganizeActions';
|
||||
import { getOrganizeActionsSelectableItemIds } from '@/command-menu/pages/navigation-menu-item/utils/getOrganizeActionsSelectableItemIds';
|
||||
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
|
||||
import { useSelectedNavigationMenuItemEditItem } from '@/navigation-menu-item/hooks/useSelectedNavigationMenuItemEditItem';
|
||||
import { useSelectedNavigationMenuItemEditItemLabel } from '@/navigation-menu-item/hooks/useSelectedNavigationMenuItemEditItemLabel';
|
||||
import { useSelectedNavigationMenuItemEditItemObjectMetadata } from '@/navigation-menu-item/hooks/useSelectedNavigationMenuItemEditItemObjectMetadata';
|
||||
import { useUpdateLinkInDraft } from '@/navigation-menu-item/hooks/useUpdateLinkInDraft';
|
||||
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/states/selectedNavigationMenuItemInEditModeState';
|
||||
import styled from '@emotion/styled';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useState } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
const StyledCommandMenuPlaceholder = styled.p`
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
font-size: ${({ theme }) => theme.font.size.sm};
|
||||
`;
|
||||
|
||||
const StyledCommandMenuPageContainer = styled.div`
|
||||
padding: ${({ theme }) => theme.spacing(3)};
|
||||
`;
|
||||
|
||||
export const CommandMenuNavigationMenuItemEditPage = () => {
|
||||
const { t } = useLingui();
|
||||
|
||||
const selectedNavigationMenuItemInEditMode = useRecoilValue(
|
||||
selectedNavigationMenuItemInEditModeState,
|
||||
);
|
||||
const { selectedItemLabel } = useSelectedNavigationMenuItemEditItemLabel();
|
||||
const { selectedItem } = useSelectedNavigationMenuItemEditItem();
|
||||
const { selectedItemObjectMetadata } =
|
||||
useSelectedNavigationMenuItemEditItemObjectMetadata();
|
||||
const selectedItemType = selectedItem?.itemType ?? null;
|
||||
|
||||
const [isFolderPickerOpen, setIsFolderPickerOpen] = useState(false);
|
||||
const setFolderPicker = () => setIsFolderPickerOpen(true);
|
||||
|
||||
const {
|
||||
canMoveUp,
|
||||
canMoveDown,
|
||||
onMoveUp,
|
||||
onMoveDown,
|
||||
onRemove,
|
||||
onAddBefore,
|
||||
onAddAfter,
|
||||
} = useNavigationMenuItemEditOrganizeActions();
|
||||
|
||||
const { updateLinkInDraft } = useUpdateLinkInDraft();
|
||||
|
||||
if (!selectedNavigationMenuItemInEditMode || !selectedItemLabel) {
|
||||
return (
|
||||
<StyledCommandMenuPageContainer>
|
||||
<StyledCommandMenuPlaceholder>
|
||||
{t`Select a navigation item to edit`}
|
||||
</StyledCommandMenuPlaceholder>
|
||||
</StyledCommandMenuPageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
if (isFolderPickerOpen) {
|
||||
return (
|
||||
<CommandMenuEditFolderPickerSubView
|
||||
onBack={() => setIsFolderPickerOpen(false)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
switch (selectedItemType) {
|
||||
case NavigationMenuItemType.VIEW:
|
||||
if (!selectedItemObjectMetadata) return null;
|
||||
return (
|
||||
<CommandMenuEditObjectViewBase
|
||||
onOpenFolderPicker={setFolderPicker}
|
||||
canMoveUp={canMoveUp}
|
||||
canMoveDown={canMoveDown}
|
||||
onMoveUp={onMoveUp}
|
||||
onMoveDown={onMoveDown}
|
||||
onRemove={onRemove}
|
||||
onAddBefore={onAddBefore}
|
||||
onAddAfter={onAddAfter}
|
||||
/>
|
||||
);
|
||||
case NavigationMenuItemType.LINK:
|
||||
if (
|
||||
isDefined(selectedItem) &&
|
||||
selectedItem.itemType === NavigationMenuItemType.LINK
|
||||
) {
|
||||
return (
|
||||
<CommandMenuEditLinkItemView
|
||||
key={selectedItem.id}
|
||||
selectedItem={selectedItem}
|
||||
onUpdateLink={(linkId, link) => updateLinkInDraft(linkId, { link })}
|
||||
onOpenFolderPicker={setFolderPicker}
|
||||
canMoveUp={canMoveUp}
|
||||
canMoveDown={canMoveDown}
|
||||
onMoveUp={onMoveUp}
|
||||
onMoveDown={onMoveDown}
|
||||
onRemove={onRemove}
|
||||
onAddBefore={onAddBefore}
|
||||
onAddAfter={onAddAfter}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
case NavigationMenuItemType.FOLDER:
|
||||
return (
|
||||
<CommandMenuList
|
||||
commandGroups={[]}
|
||||
selectableItemIds={getOrganizeActionsSelectableItemIds(false)}
|
||||
>
|
||||
<CommandMenuEditOrganizeActions
|
||||
canMoveUp={canMoveUp}
|
||||
canMoveDown={canMoveDown}
|
||||
onMoveUp={onMoveUp}
|
||||
onMoveDown={onMoveDown}
|
||||
onRemove={onRemove}
|
||||
onAddBefore={onAddBefore}
|
||||
onAddAfter={onAddAfter}
|
||||
/>
|
||||
<CommandMenuEditOwnerSection />
|
||||
</CommandMenuList>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<CommandMenuList
|
||||
commandGroups={[]}
|
||||
selectableItemIds={getOrganizeActionsSelectableItemIds(true)}
|
||||
>
|
||||
<CommandMenuEditOrganizeActions
|
||||
canMoveUp={canMoveUp}
|
||||
canMoveDown={canMoveDown}
|
||||
onMoveUp={onMoveUp}
|
||||
onMoveDown={onMoveDown}
|
||||
onRemove={onRemove}
|
||||
onAddBefore={onAddBefore}
|
||||
onAddAfter={onAddAfter}
|
||||
showMoveToFolder
|
||||
onMoveToFolder={setFolderPicker}
|
||||
/>
|
||||
</CommandMenuList>
|
||||
);
|
||||
}
|
||||
};
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import {
|
||||
IconAddressBook,
|
||||
IconCube,
|
||||
IconFolder,
|
||||
IconLink,
|
||||
IconList,
|
||||
} from 'twenty-ui/display';
|
||||
|
||||
import { CommandGroup } from '@/command-menu/components/CommandGroup';
|
||||
import { CommandMenuAddToNavDraggablePlaceholder } from '@/command-menu/components/CommandMenuAddToNavDraggablePlaceholder';
|
||||
import { CommandMenuAddToNavDroppable } from '@/command-menu/components/CommandMenuAddToNavDroppable';
|
||||
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
|
||||
import { CommandMenuItemWithAddToNavigationDrag } from '@/command-menu/components/CommandMenuItemWithAddToNavigationDrag';
|
||||
import { CommandMenuList } from '@/command-menu/components/CommandMenuList';
|
||||
import { useAddFolderAndLinkToNavigationMenu } from '@/command-menu/pages/navigation-menu-item/hooks/useAddFolderAndLinkToNavigationMenu';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
|
||||
|
||||
type CommandMenuNewSidebarItemMainMenuProps = {
|
||||
onSelectObject: () => void;
|
||||
onSelectView: () => void;
|
||||
onSelectRecord: () => void;
|
||||
};
|
||||
|
||||
export const CommandMenuNewSidebarItemMainMenu = ({
|
||||
onSelectObject,
|
||||
onSelectView,
|
||||
onSelectRecord,
|
||||
}: CommandMenuNewSidebarItemMainMenuProps) => {
|
||||
const { t } = useLingui();
|
||||
const { handleAddFolder, handleAddLink } =
|
||||
useAddFolderAndLinkToNavigationMenu();
|
||||
|
||||
return (
|
||||
<CommandMenuAddToNavDroppable>
|
||||
{({ innerRef, droppableProps, placeholder }) => (
|
||||
<CommandMenuList
|
||||
commandGroups={[]}
|
||||
selectableItemIds={['object', 'view', 'record', 'folder', 'link']}
|
||||
>
|
||||
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
|
||||
<div ref={innerRef} {...droppableProps}>
|
||||
<CommandGroup heading={t`Data`}>
|
||||
<CommandMenuAddToNavDraggablePlaceholder index={0}>
|
||||
<SelectableListItem itemId="object" onEnter={onSelectObject}>
|
||||
<CommandMenuItem
|
||||
Icon={IconCube}
|
||||
label={t`Object`}
|
||||
id="object"
|
||||
hasSubMenu={true}
|
||||
onClick={onSelectObject}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
</CommandMenuAddToNavDraggablePlaceholder>
|
||||
<CommandMenuAddToNavDraggablePlaceholder index={1}>
|
||||
<SelectableListItem itemId="view" onEnter={onSelectView}>
|
||||
<CommandMenuItem
|
||||
Icon={IconList}
|
||||
label={t`View`}
|
||||
id="view"
|
||||
hasSubMenu={true}
|
||||
onClick={onSelectView}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
</CommandMenuAddToNavDraggablePlaceholder>
|
||||
<CommandMenuAddToNavDraggablePlaceholder index={2}>
|
||||
<SelectableListItem itemId="record" onEnter={onSelectRecord}>
|
||||
<CommandMenuItem
|
||||
Icon={IconAddressBook}
|
||||
label={t`Record`}
|
||||
id="record"
|
||||
hasSubMenu={true}
|
||||
onClick={onSelectRecord}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
</CommandMenuAddToNavDraggablePlaceholder>
|
||||
</CommandGroup>
|
||||
<CommandGroup heading={t`Other`}>
|
||||
<SelectableListItem itemId="folder" onEnter={handleAddFolder}>
|
||||
<CommandMenuItemWithAddToNavigationDrag
|
||||
icon={IconFolder}
|
||||
label={t`Folder`}
|
||||
id="folder"
|
||||
onClick={handleAddFolder}
|
||||
dragIndex={3}
|
||||
payload={{
|
||||
type: NavigationMenuItemType.FOLDER,
|
||||
folderId: 'new',
|
||||
name: t`New folder`,
|
||||
}}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
<SelectableListItem itemId="link" onEnter={handleAddLink}>
|
||||
<CommandMenuItemWithAddToNavigationDrag
|
||||
icon={IconLink}
|
||||
label={t`Link`}
|
||||
id="link"
|
||||
onClick={handleAddLink}
|
||||
dragIndex={4}
|
||||
payload={{
|
||||
type: NavigationMenuItemType.LINK,
|
||||
linkId: 'new',
|
||||
name: t`Link label`,
|
||||
link: 'https://www.example.com',
|
||||
}}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
</CommandGroup>
|
||||
{placeholder}
|
||||
</div>
|
||||
</CommandMenuList>
|
||||
)}
|
||||
</CommandMenuAddToNavDroppable>
|
||||
);
|
||||
};
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useState } from 'react';
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
|
||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||
import { CommandMenuObjectPickerSubView } from '@/command-menu/pages/navigation-menu-item/components/CommandMenuObjectPickerSubView';
|
||||
import { CommandMenuSystemObjectPickerSubView } from '@/command-menu/pages/navigation-menu-item/components/CommandMenuSystemObjectPickerSubView';
|
||||
import { getAvailableObjectMetadataForNewSidebarItem } from '@/command-menu/pages/navigation-menu-item/utils/getAvailableObjectMetadataForNewSidebarItem';
|
||||
import { useAddObjectToNavigationMenuDraft } from '@/navigation-menu-item/hooks/useAddObjectToNavigationMenuDraft';
|
||||
import { useDraftNavigationMenuItems } from '@/navigation-menu-item/hooks/useDraftNavigationMenuItems';
|
||||
import { useNavigationMenuObjectMetadataFromDraft } from '@/navigation-menu-item/hooks/useNavigationMenuObjectMetadataFromDraft';
|
||||
import { addMenuItemInsertionContextState } from '@/navigation-menu-item/states/addMenuItemInsertionContextState';
|
||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { ViewKey } from '@/views/types/ViewKey';
|
||||
|
||||
type CommandMenuNewSidebarItemObjectFlowProps = {
|
||||
onBack: () => void;
|
||||
};
|
||||
|
||||
export const CommandMenuNewSidebarItemObjectFlow = ({
|
||||
onBack,
|
||||
}: CommandMenuNewSidebarItemObjectFlowProps) => {
|
||||
const { t } = useLingui();
|
||||
const { closeCommandMenu } = useCommandMenu();
|
||||
const [objectSearchInput, setObjectSearchInput] = useState('');
|
||||
const [systemObjectSearchInput, setSystemObjectSearchInput] = useState('');
|
||||
const [isInSystemPicker, setIsInSystemPicker] = useState(false);
|
||||
|
||||
const { currentDraft } = useDraftNavigationMenuItems();
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
const { addObjectToDraft } = useAddObjectToNavigationMenuDraft();
|
||||
const { activeNonSystemObjectMetadataItems } =
|
||||
useFilteredObjectMetadataItems();
|
||||
const addMenuItemInsertionContext = useRecoilValue(
|
||||
addMenuItemInsertionContextState,
|
||||
);
|
||||
const setAddMenuItemInsertionContext = useSetRecoilState(
|
||||
addMenuItemInsertionContextState,
|
||||
);
|
||||
const { views, objectMetadataIdsWithIndexView } =
|
||||
useNavigationMenuObjectMetadataFromDraft(currentDraft);
|
||||
|
||||
const objectMetadataIdsWithDisplayableViews = new Set(
|
||||
views
|
||||
.filter((view) => view.key !== ViewKey.Index)
|
||||
.map((view) => view.objectMetadataId),
|
||||
);
|
||||
|
||||
const { availableObjectMetadataItems, availableSystemObjectMetadataItems } =
|
||||
getAvailableObjectMetadataForNewSidebarItem({
|
||||
objectMetadataItems,
|
||||
activeNonSystemObjectMetadataItems,
|
||||
objectMetadataIdsWithIndexView,
|
||||
objectMetadataIdsWithDisplayableViews,
|
||||
});
|
||||
|
||||
const handleSelectObject = (
|
||||
objectMetadataItem: ObjectMetadataItem,
|
||||
defaultViewId: string,
|
||||
) => {
|
||||
addObjectToDraft(
|
||||
objectMetadataItem.id,
|
||||
defaultViewId,
|
||||
currentDraft,
|
||||
addMenuItemInsertionContext?.targetFolderId,
|
||||
addMenuItemInsertionContext?.targetIndex,
|
||||
);
|
||||
setAddMenuItemInsertionContext(null);
|
||||
closeCommandMenu();
|
||||
};
|
||||
|
||||
const handleBackToObjectList = () => {
|
||||
setIsInSystemPicker(false);
|
||||
setSystemObjectSearchInput('');
|
||||
};
|
||||
|
||||
if (isInSystemPicker) {
|
||||
return (
|
||||
<CommandMenuSystemObjectPickerSubView
|
||||
systemObjects={availableSystemObjectMetadataItems}
|
||||
searchValue={systemObjectSearchInput}
|
||||
onSearchChange={setSystemObjectSearchInput}
|
||||
onBack={handleBackToObjectList}
|
||||
isViewItem={false}
|
||||
onChangeObject={handleSelectObject}
|
||||
objectMenuItemVariant="add"
|
||||
emptyNoResultsText={t`All system objects are already in the sidebar`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<CommandMenuObjectPickerSubView
|
||||
objects={availableObjectMetadataItems}
|
||||
searchValue={objectSearchInput}
|
||||
onSearchChange={setObjectSearchInput}
|
||||
onBack={onBack}
|
||||
onOpenSystemPicker={() => setIsInSystemPicker(true)}
|
||||
isViewItem={false}
|
||||
onChangeObject={handleSelectObject}
|
||||
objectMenuItemVariant="add"
|
||||
/>
|
||||
);
|
||||
};
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { CommandMenuNewSidebarItemMainMenu } from '@/command-menu/pages/navigation-menu-item/components/CommandMenuNewSidebarItemMainMenu';
|
||||
import { CommandMenuNewSidebarItemObjectFlow } from '@/command-menu/pages/navigation-menu-item/components/CommandMenuNewSidebarItemObjectFlow';
|
||||
import { CommandMenuNewSidebarItemRecordSubView } from '@/command-menu/pages/navigation-menu-item/components/CommandMenuNewSidebarItemRecordSubView';
|
||||
import { CommandMenuNewSidebarItemViewFlow } from '@/command-menu/pages/navigation-menu-item/components/CommandMenuNewSidebarItemViewFlow';
|
||||
|
||||
type SelectedOption = 'object' | 'record' | 'view' | null;
|
||||
|
||||
export const CommandMenuNewSidebarItemPage = () => {
|
||||
const [selectedOption, setSelectedOption] = useState<SelectedOption>(null);
|
||||
|
||||
const handleBackToMain = () => {
|
||||
setSelectedOption(null);
|
||||
};
|
||||
|
||||
switch (selectedOption) {
|
||||
case 'view':
|
||||
return <CommandMenuNewSidebarItemViewFlow onBack={handleBackToMain} />;
|
||||
case 'object':
|
||||
return <CommandMenuNewSidebarItemObjectFlow onBack={handleBackToMain} />;
|
||||
case 'record':
|
||||
return (
|
||||
<CommandMenuNewSidebarItemRecordSubView onBack={handleBackToMain} />
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<CommandMenuNewSidebarItemMainMenu
|
||||
onSelectRecord={() => setSelectedOption('record')}
|
||||
onSelectObject={() => setSelectedOption('object')}
|
||||
onSelectView={() => setSelectedOption('view')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import { Avatar } from 'twenty-ui/display';
|
||||
|
||||
import { CommandMenuItemWithAddToNavigationDrag } from '@/command-menu/components/CommandMenuItemWithAddToNavigationDrag';
|
||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
|
||||
import { useDraftNavigationMenuItems } from '@/navigation-menu-item/hooks/useDraftNavigationMenuItems';
|
||||
import { useAddRecordToNavigationMenuDraft } from '@/navigation-menu-item/hooks/useAddRecordToNavigationMenuDraft';
|
||||
import { addMenuItemInsertionContextState } from '@/navigation-menu-item/states/addMenuItemInsertionContextState';
|
||||
import type { AddToNavigationDragPayload } from '@/navigation-menu-item/types/add-to-navigation-drag-payload';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
|
||||
type SearchRecord = {
|
||||
recordId: string;
|
||||
objectNameSingular: string;
|
||||
label: string;
|
||||
imageUrl?: string | null;
|
||||
};
|
||||
|
||||
type CommandMenuNewSidebarItemRecordItemProps = {
|
||||
record: SearchRecord;
|
||||
dragIndex?: number;
|
||||
};
|
||||
|
||||
export const CommandMenuNewSidebarItemRecordItem = ({
|
||||
record,
|
||||
dragIndex,
|
||||
}: CommandMenuNewSidebarItemRecordItemProps) => {
|
||||
const { closeCommandMenu } = useCommandMenu();
|
||||
const { addRecordToDraft } = useAddRecordToNavigationMenuDraft();
|
||||
const { currentDraft } = useDraftNavigationMenuItems();
|
||||
const addMenuItemInsertionContext = useRecoilValue(
|
||||
addMenuItemInsertionContextState,
|
||||
);
|
||||
const setAddMenuItemInsertionContext = useSetRecoilState(
|
||||
addMenuItemInsertionContextState,
|
||||
);
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
const objectMetadataItem = objectMetadataItems.find(
|
||||
(item) => item.nameSingular === record.objectNameSingular,
|
||||
);
|
||||
const recordPayload: AddToNavigationDragPayload = {
|
||||
type: NavigationMenuItemType.RECORD,
|
||||
recordId: record.recordId,
|
||||
objectMetadataId: objectMetadataItem?.id ?? '',
|
||||
objectNameSingular: record.objectNameSingular,
|
||||
label: record.label,
|
||||
imageUrl: record.imageUrl,
|
||||
};
|
||||
|
||||
const handleSelectRecord = () => {
|
||||
addRecordToDraft(
|
||||
{
|
||||
recordId: record.recordId,
|
||||
objectNameSingular: record.objectNameSingular,
|
||||
label: record.label,
|
||||
imageUrl: record.imageUrl,
|
||||
},
|
||||
currentDraft,
|
||||
addMenuItemInsertionContext?.targetFolderId ?? null,
|
||||
addMenuItemInsertionContext?.targetIndex,
|
||||
);
|
||||
setAddMenuItemInsertionContext(null);
|
||||
closeCommandMenu();
|
||||
};
|
||||
|
||||
return (
|
||||
<SelectableListItem itemId={record.recordId} onEnter={handleSelectRecord}>
|
||||
<CommandMenuItemWithAddToNavigationDrag
|
||||
customIconContent={
|
||||
<Avatar
|
||||
type={
|
||||
record.objectNameSingular === CoreObjectNameSingular.Company
|
||||
? 'squared'
|
||||
: 'rounded'
|
||||
}
|
||||
avatarUrl={record.imageUrl}
|
||||
placeholderColorSeed={record.recordId}
|
||||
placeholder={record.label}
|
||||
/>
|
||||
}
|
||||
label={record.label}
|
||||
description={
|
||||
objectMetadataItem?.labelSingular ?? record.objectNameSingular
|
||||
}
|
||||
id={record.recordId}
|
||||
onClick={handleSelectRecord}
|
||||
dragIndex={dragIndex}
|
||||
payload={recordPayload}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
);
|
||||
};
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useState } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useDebounce } from 'use-debounce';
|
||||
|
||||
import { CommandGroup } from '@/command-menu/components/CommandGroup';
|
||||
import { CommandMenuAddToNavDroppable } from '@/command-menu/components/CommandMenuAddToNavDroppable';
|
||||
import { CommandMenuList } from '@/command-menu/components/CommandMenuList';
|
||||
import { CommandMenuSubViewWithSearch } from '@/command-menu/components/CommandMenuSubViewWithSearch';
|
||||
import { MAX_SEARCH_RESULTS } from '@/command-menu/constants/MaxSearchResults';
|
||||
import { CommandMenuNewSidebarItemRecordItem } from '@/command-menu/pages/navigation-menu-item/components/CommandMenuNewSidebarItemRecordItem';
|
||||
import { useDraftNavigationMenuItems } from '@/navigation-menu-item/hooks/useDraftNavigationMenuItems';
|
||||
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
|
||||
import { getObjectPermissionsFromMapByObjectMetadataId } from '@/settings/roles/role-permissions/objects-permissions/utils/getObjectPermissionsFromMapByObjectMetadataId';
|
||||
import { useSearchQuery } from '~/generated/graphql';
|
||||
|
||||
type SearchRecordBase = {
|
||||
recordId: string;
|
||||
objectNameSingular: string;
|
||||
label: string;
|
||||
imageUrl?: string | null;
|
||||
};
|
||||
|
||||
type CommandMenuNewSidebarItemRecordSubViewProps = {
|
||||
onBack: () => void;
|
||||
};
|
||||
|
||||
export const CommandMenuNewSidebarItemRecordSubView = ({
|
||||
onBack,
|
||||
}: CommandMenuNewSidebarItemRecordSubViewProps) => {
|
||||
const { t } = useLingui();
|
||||
const { currentDraft } = useDraftNavigationMenuItems();
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
const [recordSearchInput, setRecordSearchInput] = useState('');
|
||||
const [deferredRecordSearchInput] = useDebounce(recordSearchInput, 300);
|
||||
const coreClient = useApolloCoreClient();
|
||||
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
|
||||
|
||||
const nonReadableObjectMetadataItemsNameSingular = objectMetadataItems
|
||||
.filter(
|
||||
(objectMetadataItem) =>
|
||||
!getObjectPermissionsFromMapByObjectMetadataId({
|
||||
objectPermissionsByObjectMetadataId,
|
||||
objectMetadataId: objectMetadataItem.id,
|
||||
})?.canReadObjectRecords,
|
||||
)
|
||||
.map((objectMetadataItem) => objectMetadataItem.nameSingular);
|
||||
|
||||
const { data: searchData, loading: recordSearchLoading } = useSearchQuery({
|
||||
client: coreClient,
|
||||
variables: {
|
||||
searchInput: deferredRecordSearchInput ?? '',
|
||||
limit: MAX_SEARCH_RESULTS,
|
||||
excludedObjectNameSingulars: [
|
||||
'workspaceMember',
|
||||
...nonReadableObjectMetadataItemsNameSingular,
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const workspaceRecordIds = new Set(
|
||||
currentDraft.flatMap((item) =>
|
||||
isDefined(item.targetRecordId) ? [item.targetRecordId] : [],
|
||||
),
|
||||
);
|
||||
|
||||
const searchRecords =
|
||||
searchData?.search?.edges?.map((edge) => edge.node) ?? [];
|
||||
const availableSearchRecords = searchRecords.filter(
|
||||
(record) => !workspaceRecordIds.has(record.recordId),
|
||||
) as SearchRecordBase[];
|
||||
|
||||
const isEmpty = availableSearchRecords.length === 0 && !recordSearchLoading;
|
||||
const selectableItemIds = isEmpty
|
||||
? []
|
||||
: availableSearchRecords.map((record) => record.recordId);
|
||||
const noResultsText =
|
||||
deferredRecordSearchInput.length > 0
|
||||
? t`No results found`
|
||||
: t`Type to search records`;
|
||||
|
||||
return (
|
||||
<CommandMenuSubViewWithSearch
|
||||
backBarTitle={t`Add a record`}
|
||||
onBack={onBack}
|
||||
searchPlaceholder={t`Search records...`}
|
||||
searchValue={recordSearchInput}
|
||||
onSearchChange={setRecordSearchInput}
|
||||
>
|
||||
<CommandMenuAddToNavDroppable>
|
||||
{({ innerRef, droppableProps, placeholder }) => (
|
||||
<CommandMenuList
|
||||
commandGroups={[]}
|
||||
selectableItemIds={selectableItemIds}
|
||||
loading={recordSearchLoading}
|
||||
noResults={isEmpty}
|
||||
noResultsText={noResultsText}
|
||||
>
|
||||
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
|
||||
<div ref={innerRef} {...droppableProps}>
|
||||
<CommandGroup heading={t`Results`}>
|
||||
{availableSearchRecords.map((record, index) => (
|
||||
<CommandMenuNewSidebarItemRecordItem
|
||||
key={record.recordId}
|
||||
record={record}
|
||||
dragIndex={index}
|
||||
/>
|
||||
))}
|
||||
</CommandGroup>
|
||||
{placeholder}
|
||||
</div>
|
||||
</CommandMenuList>
|
||||
)}
|
||||
</CommandMenuAddToNavDroppable>
|
||||
</CommandMenuSubViewWithSearch>
|
||||
);
|
||||
};
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
import { useState } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { CommandMenuNewSidebarItemViewObjectPickerSubView } from '@/command-menu/pages/navigation-menu-item/components/CommandMenuNewSidebarItemViewObjectPickerSubView';
|
||||
import { CommandMenuNewSidebarItemViewPickerSubView } from '@/command-menu/pages/navigation-menu-item/components/CommandMenuNewSidebarItemViewPickerSubView';
|
||||
import { CommandMenuNewSidebarItemViewSystemSubView } from '@/command-menu/pages/navigation-menu-item/components/CommandMenuNewSidebarItemViewSystemSubView';
|
||||
import { getAvailableObjectMetadataForNewSidebarItem } from '@/command-menu/pages/navigation-menu-item/utils/getAvailableObjectMetadataForNewSidebarItem';
|
||||
import { useDraftNavigationMenuItems } from '@/navigation-menu-item/hooks/useDraftNavigationMenuItems';
|
||||
import { useNavigationMenuObjectMetadataFromDraft } from '@/navigation-menu-item/hooks/useNavigationMenuObjectMetadataFromDraft';
|
||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { ViewKey } from '@/views/types/ViewKey';
|
||||
|
||||
type CommandMenuNewSidebarItemViewFlowProps = {
|
||||
onBack: () => void;
|
||||
};
|
||||
|
||||
export const CommandMenuNewSidebarItemViewFlow = ({
|
||||
onBack,
|
||||
}: CommandMenuNewSidebarItemViewFlowProps) => {
|
||||
const [selectedObjectMetadataIdForView, setSelectedObjectMetadataIdForView] =
|
||||
useState<string | null>(null);
|
||||
const [objectSearchInput, setObjectSearchInput] = useState('');
|
||||
const [systemObjectSearchInput, setSystemObjectSearchInput] = useState('');
|
||||
const [isInSystemPicker, setIsInSystemPicker] = useState(false);
|
||||
|
||||
const { currentDraft } = useDraftNavigationMenuItems();
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
const { activeNonSystemObjectMetadataItems } =
|
||||
useFilteredObjectMetadataItems();
|
||||
const { views, objectMetadataIdsWithIndexView } =
|
||||
useNavigationMenuObjectMetadataFromDraft(currentDraft);
|
||||
|
||||
const objectMetadataIdsWithDisplayableViews = new Set(
|
||||
views
|
||||
.filter((view) => view.key !== ViewKey.Index)
|
||||
.map((view) => view.objectMetadataId),
|
||||
);
|
||||
|
||||
const {
|
||||
objectMetadataItemsWithViews,
|
||||
availableSystemObjectMetadataItemsForView,
|
||||
} = getAvailableObjectMetadataForNewSidebarItem({
|
||||
objectMetadataItems,
|
||||
activeNonSystemObjectMetadataItems,
|
||||
objectMetadataIdsWithIndexView,
|
||||
objectMetadataIdsWithDisplayableViews,
|
||||
});
|
||||
|
||||
const handleBackFromViewPicker = () => {
|
||||
const selectedObjectMetadataItem = isDefined(
|
||||
selectedObjectMetadataIdForView,
|
||||
)
|
||||
? objectMetadataItems.find(
|
||||
(item) => item.id === selectedObjectMetadataIdForView,
|
||||
)
|
||||
: undefined;
|
||||
const cameFromSystemObjects = selectedObjectMetadataItem?.isSystem ?? false;
|
||||
|
||||
setSelectedObjectMetadataIdForView(null);
|
||||
setIsInSystemPicker(cameFromSystemObjects);
|
||||
};
|
||||
|
||||
const handleBackFromSystemPicker = () => {
|
||||
setIsInSystemPicker(false);
|
||||
};
|
||||
|
||||
const handleSelectObject = (objectId: string) => {
|
||||
setSelectedObjectMetadataIdForView(objectId);
|
||||
};
|
||||
|
||||
const handleSelectObjectFromSystem = (objectId: string) => {
|
||||
setSelectedObjectMetadataIdForView(objectId);
|
||||
setIsInSystemPicker(false);
|
||||
};
|
||||
|
||||
if (isDefined(selectedObjectMetadataIdForView) && !isInSystemPicker) {
|
||||
return (
|
||||
<CommandMenuNewSidebarItemViewPickerSubView
|
||||
selectedObjectMetadataIdForView={selectedObjectMetadataIdForView}
|
||||
onBack={handleBackFromViewPicker}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isInSystemPicker) {
|
||||
return (
|
||||
<CommandMenuNewSidebarItemViewSystemSubView
|
||||
systemObjects={availableSystemObjectMetadataItemsForView}
|
||||
searchValue={systemObjectSearchInput}
|
||||
onSearchChange={setSystemObjectSearchInput}
|
||||
onBack={handleBackFromSystemPicker}
|
||||
onSelectObject={(item) => handleSelectObjectFromSystem(item.id)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<CommandMenuNewSidebarItemViewObjectPickerSubView
|
||||
objects={objectMetadataItemsWithViews}
|
||||
searchValue={objectSearchInput}
|
||||
onSearchChange={setObjectSearchInput}
|
||||
onBack={onBack}
|
||||
onOpenSystemPicker={() => setIsInSystemPicker(true)}
|
||||
onSelectObject={(item) => handleSelectObject(item.id)}
|
||||
showSystemObjectsOption={
|
||||
availableSystemObjectMetadataItemsForView.length > 0
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { IconSettings, useIcons } from 'twenty-ui/display';
|
||||
|
||||
import { CommandGroup } from '@/command-menu/components/CommandGroup';
|
||||
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
|
||||
import { CommandMenuList } from '@/command-menu/components/CommandMenuList';
|
||||
import { CommandMenuSubViewWithSearch } from '@/command-menu/components/CommandMenuSubViewWithSearch';
|
||||
import { useFilteredPickerItems } from '@/command-menu/hooks/useFilteredPickerItems';
|
||||
import { IconWithBackground } from '@/navigation-menu-item/components/IconWithBackground';
|
||||
import { getNavigationMenuItemIconColors } from '@/navigation-menu-item/utils/getNavigationMenuItemIconColors';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
|
||||
type CommandMenuNewSidebarItemViewObjectPickerSubViewProps = {
|
||||
objects: ObjectMetadataItem[];
|
||||
searchValue: string;
|
||||
onSearchChange: (value: string) => void;
|
||||
onBack: () => void;
|
||||
onOpenSystemPicker: () => void;
|
||||
onSelectObject: (objectMetadataItem: ObjectMetadataItem) => void;
|
||||
showSystemObjectsOption?: boolean;
|
||||
};
|
||||
|
||||
export const CommandMenuNewSidebarItemViewObjectPickerSubView = ({
|
||||
objects,
|
||||
searchValue,
|
||||
onSearchChange,
|
||||
onBack,
|
||||
onOpenSystemPicker,
|
||||
onSelectObject,
|
||||
showSystemObjectsOption = true,
|
||||
}: CommandMenuNewSidebarItemViewObjectPickerSubViewProps) => {
|
||||
const { t } = useLingui();
|
||||
const theme = useTheme();
|
||||
const { getIcon } = useIcons();
|
||||
const iconColors = getNavigationMenuItemIconColors(theme);
|
||||
const { filteredItems, selectableItemIds, isEmpty, hasSearchQuery } =
|
||||
useFilteredPickerItems({
|
||||
items: objects,
|
||||
searchQuery: searchValue,
|
||||
getSearchableValues: (item) => [item.labelPlural],
|
||||
appendSelectableIds: showSystemObjectsOption ? ['system'] : [],
|
||||
});
|
||||
const noResultsText = hasSearchQuery
|
||||
? t`No results found`
|
||||
: t`No objects with views found`;
|
||||
|
||||
return (
|
||||
<CommandMenuSubViewWithSearch
|
||||
backBarTitle={t`Pick an object`}
|
||||
onBack={onBack}
|
||||
searchPlaceholder={t`Search an object...`}
|
||||
searchValue={searchValue}
|
||||
onSearchChange={onSearchChange}
|
||||
>
|
||||
<CommandMenuList
|
||||
commandGroups={[]}
|
||||
selectableItemIds={selectableItemIds}
|
||||
noResults={isEmpty}
|
||||
noResultsText={noResultsText}
|
||||
>
|
||||
<CommandGroup heading={t`Objects`}>
|
||||
{filteredItems.map((objectMetadataItem) => (
|
||||
<SelectableListItem
|
||||
key={objectMetadataItem.id}
|
||||
itemId={objectMetadataItem.id}
|
||||
onEnter={() => onSelectObject(objectMetadataItem)}
|
||||
>
|
||||
<CommandMenuItem
|
||||
Icon={({ size, stroke }) => (
|
||||
<IconWithBackground
|
||||
Icon={getIcon(objectMetadataItem.icon)}
|
||||
backgroundColor={iconColors.object}
|
||||
size={size}
|
||||
stroke={stroke}
|
||||
/>
|
||||
)}
|
||||
label={objectMetadataItem.labelPlural}
|
||||
id={objectMetadataItem.id}
|
||||
hasSubMenu
|
||||
onClick={() => onSelectObject(objectMetadataItem)}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
))}
|
||||
{showSystemObjectsOption && (
|
||||
<SelectableListItem itemId="system" onEnter={onOpenSystemPicker}>
|
||||
<CommandMenuItem
|
||||
Icon={IconSettings}
|
||||
label={t`System objects`}
|
||||
id="system"
|
||||
hasSubMenu={true}
|
||||
onClick={onOpenSystemPicker}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
)}
|
||||
</CommandGroup>
|
||||
</CommandMenuList>
|
||||
</CommandMenuSubViewWithSearch>
|
||||
);
|
||||
};
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useState } from 'react';
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
|
||||
import { CommandGroup } from '@/command-menu/components/CommandGroup';
|
||||
import { CommandMenuAddToNavDroppable } from '@/command-menu/components/CommandMenuAddToNavDroppable';
|
||||
import { CommandMenuItemWithAddToNavigationDrag } from '@/command-menu/components/CommandMenuItemWithAddToNavigationDrag';
|
||||
import { CommandMenuList } from '@/command-menu/components/CommandMenuList';
|
||||
import { CommandMenuSubViewWithSearch } from '@/command-menu/components/CommandMenuSubViewWithSearch';
|
||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||
import { useFilteredPickerItems } from '@/command-menu/hooks/useFilteredPickerItems';
|
||||
import { useDraftNavigationMenuItems } from '@/navigation-menu-item/hooks/useDraftNavigationMenuItems';
|
||||
import { ObjectIconWithViewOverlay } from '@/navigation-menu-item/components/ObjectIconWithViewOverlay';
|
||||
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
|
||||
import { useAddViewToNavigationMenuDraft } from '@/navigation-menu-item/hooks/useAddViewToNavigationMenuDraft';
|
||||
import { useNavigationMenuObjectMetadataFromDraft } from '@/navigation-menu-item/hooks/useNavigationMenuObjectMetadataFromDraft';
|
||||
import { addMenuItemInsertionContextState } from '@/navigation-menu-item/states/addMenuItemInsertionContextState';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
import { type View } from '@/views/types/View';
|
||||
import { ViewKey } from '@/views/types/ViewKey';
|
||||
|
||||
type CommandMenuNewSidebarItemViewPickerSubViewProps = {
|
||||
selectedObjectMetadataIdForView: string;
|
||||
onBack: () => void;
|
||||
};
|
||||
|
||||
export const CommandMenuNewSidebarItemViewPickerSubView = ({
|
||||
selectedObjectMetadataIdForView,
|
||||
onBack,
|
||||
}: CommandMenuNewSidebarItemViewPickerSubViewProps) => {
|
||||
const { t } = useLingui();
|
||||
const { getIcon } = useIcons();
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
const { closeCommandMenu } = useCommandMenu();
|
||||
const { addViewToDraft } = useAddViewToNavigationMenuDraft();
|
||||
const { currentDraft } = useDraftNavigationMenuItems();
|
||||
const addMenuItemInsertionContext = useRecoilValue(
|
||||
addMenuItemInsertionContextState,
|
||||
);
|
||||
const setAddMenuItemInsertionContext = useSetRecoilState(
|
||||
addMenuItemInsertionContextState,
|
||||
);
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
const { views } = useNavigationMenuObjectMetadataFromDraft(currentDraft);
|
||||
|
||||
const viewsForSelectedObject = views
|
||||
.filter(
|
||||
(view) =>
|
||||
view.objectMetadataId === selectedObjectMetadataIdForView &&
|
||||
view.key !== ViewKey.Index,
|
||||
)
|
||||
.sort((a, b) => a.position - b.position);
|
||||
|
||||
const selectedObjectMetadataItem = objectMetadataItems.find(
|
||||
(item) => item.id === selectedObjectMetadataIdForView,
|
||||
);
|
||||
const backBarTitle =
|
||||
selectedObjectMetadataItem?.labelPlural ?? t`Pick a view`;
|
||||
|
||||
const {
|
||||
filteredItems: filteredViews,
|
||||
selectableItemIds,
|
||||
isEmpty,
|
||||
hasSearchQuery,
|
||||
} = useFilteredPickerItems({
|
||||
items: viewsForSelectedObject,
|
||||
searchQuery: searchValue,
|
||||
getSearchableValues: (view) => [view.name],
|
||||
});
|
||||
const noResultsText = hasSearchQuery
|
||||
? t`No results found`
|
||||
: t`No custom views available`;
|
||||
|
||||
const handleSelectView = (view: View) => {
|
||||
addViewToDraft(
|
||||
view.id,
|
||||
currentDraft,
|
||||
addMenuItemInsertionContext?.targetFolderId ?? null,
|
||||
addMenuItemInsertionContext?.targetIndex,
|
||||
);
|
||||
setAddMenuItemInsertionContext(null);
|
||||
closeCommandMenu();
|
||||
};
|
||||
|
||||
return (
|
||||
<CommandMenuSubViewWithSearch
|
||||
backBarTitle={backBarTitle}
|
||||
onBack={onBack}
|
||||
searchPlaceholder={t`Search a view...`}
|
||||
searchValue={searchValue}
|
||||
onSearchChange={setSearchValue}
|
||||
>
|
||||
<CommandMenuAddToNavDroppable>
|
||||
{({ innerRef, droppableProps, placeholder }) => (
|
||||
<CommandMenuList
|
||||
commandGroups={[]}
|
||||
selectableItemIds={selectableItemIds}
|
||||
noResults={isEmpty}
|
||||
noResultsText={noResultsText}
|
||||
>
|
||||
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
|
||||
<div ref={innerRef} {...droppableProps}>
|
||||
<CommandGroup heading={t`Views`}>
|
||||
{filteredViews.map((view, index) => (
|
||||
<SelectableListItem
|
||||
key={view.id}
|
||||
itemId={view.id}
|
||||
onEnter={() => handleSelectView(view)}
|
||||
>
|
||||
<CommandMenuItemWithAddToNavigationDrag
|
||||
customIconContent={
|
||||
selectedObjectMetadataItem ? (
|
||||
<ObjectIconWithViewOverlay
|
||||
ObjectIcon={getIcon(
|
||||
selectedObjectMetadataItem.icon,
|
||||
)}
|
||||
ViewIcon={getIcon(view.icon)}
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
icon={
|
||||
selectedObjectMetadataItem
|
||||
? undefined
|
||||
: getIcon(view.icon)
|
||||
}
|
||||
label={view.name}
|
||||
id={view.id}
|
||||
onClick={() => handleSelectView(view)}
|
||||
dragIndex={index}
|
||||
payload={{
|
||||
type: NavigationMenuItemType.VIEW,
|
||||
viewId: view.id,
|
||||
label: view.name,
|
||||
}}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
{placeholder}
|
||||
</div>
|
||||
</CommandMenuList>
|
||||
)}
|
||||
</CommandMenuAddToNavDroppable>
|
||||
</CommandMenuSubViewWithSearch>
|
||||
);
|
||||
};
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
|
||||
import { CommandGroup } from '@/command-menu/components/CommandGroup';
|
||||
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
|
||||
import { CommandMenuList } from '@/command-menu/components/CommandMenuList';
|
||||
import { CommandMenuSubViewWithSearch } from '@/command-menu/components/CommandMenuSubViewWithSearch';
|
||||
import { IconWithBackground } from '@/navigation-menu-item/components/IconWithBackground';
|
||||
import { getNavigationMenuItemIconColors } from '@/navigation-menu-item/utils/getNavigationMenuItemIconColors';
|
||||
import { useFilteredPickerItems } from '@/command-menu/hooks/useFilteredPickerItems';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
|
||||
type CommandMenuNewSidebarItemViewSystemSubViewProps = {
|
||||
systemObjects: ObjectMetadataItem[];
|
||||
searchValue: string;
|
||||
onSearchChange: (value: string) => void;
|
||||
onBack: () => void;
|
||||
onSelectObject: (objectMetadataItem: ObjectMetadataItem) => void;
|
||||
};
|
||||
|
||||
export const CommandMenuNewSidebarItemViewSystemSubView = ({
|
||||
systemObjects,
|
||||
searchValue,
|
||||
onSearchChange,
|
||||
onBack,
|
||||
onSelectObject,
|
||||
}: CommandMenuNewSidebarItemViewSystemSubViewProps) => {
|
||||
const { t } = useLingui();
|
||||
const theme = useTheme();
|
||||
const { getIcon } = useIcons();
|
||||
const iconColors = getNavigationMenuItemIconColors(theme);
|
||||
const {
|
||||
filteredItems: filteredSystemObjectMetadataItems,
|
||||
selectableItemIds,
|
||||
isEmpty,
|
||||
hasSearchQuery,
|
||||
} = useFilteredPickerItems({
|
||||
items: systemObjects,
|
||||
searchQuery: searchValue,
|
||||
getSearchableValues: (item) => [item.labelPlural],
|
||||
});
|
||||
const noResultsText = hasSearchQuery
|
||||
? t`No results found`
|
||||
: t`No system objects with views found`;
|
||||
|
||||
return (
|
||||
<CommandMenuSubViewWithSearch
|
||||
backBarTitle={t`System objects`}
|
||||
onBack={onBack}
|
||||
searchPlaceholder={t`Search a system object...`}
|
||||
searchValue={searchValue}
|
||||
onSearchChange={onSearchChange}
|
||||
>
|
||||
<CommandMenuList
|
||||
commandGroups={[]}
|
||||
selectableItemIds={selectableItemIds}
|
||||
noResults={isEmpty}
|
||||
noResultsText={noResultsText}
|
||||
>
|
||||
<CommandGroup heading={t`System objects`}>
|
||||
{filteredSystemObjectMetadataItems.map((objectMetadataItem) => (
|
||||
<SelectableListItem
|
||||
key={objectMetadataItem.id}
|
||||
itemId={objectMetadataItem.id}
|
||||
onEnter={() => onSelectObject(objectMetadataItem)}
|
||||
>
|
||||
<CommandMenuItem
|
||||
Icon={({ size, stroke }) => (
|
||||
<IconWithBackground
|
||||
Icon={getIcon(objectMetadataItem.icon)}
|
||||
backgroundColor={iconColors.object}
|
||||
size={size}
|
||||
stroke={stroke}
|
||||
/>
|
||||
)}
|
||||
label={objectMetadataItem.labelPlural}
|
||||
id={objectMetadataItem.id}
|
||||
onClick={() => onSelectObject(objectMetadataItem)}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandMenuList>
|
||||
</CommandMenuSubViewWithSearch>
|
||||
);
|
||||
};
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
|
||||
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
|
||||
import { CommandMenuItemWithAddToNavigationDrag } from '@/command-menu/components/CommandMenuItemWithAddToNavigationDrag';
|
||||
import { IconWithBackground } from '@/navigation-menu-item/components/IconWithBackground';
|
||||
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
|
||||
import { getNavigationMenuItemIconColors } from '@/navigation-menu-item/utils/getNavigationMenuItemIconColors';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
import { coreIndexViewIdFromObjectMetadataItemFamilySelector } from '@/views/states/selectors/coreIndexViewIdFromObjectMetadataItemFamilySelector';
|
||||
|
||||
type CommandMenuObjectMenuItemProps = {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
onSelect: (
|
||||
objectMetadataItem: ObjectMetadataItem,
|
||||
defaultViewId: string,
|
||||
) => void;
|
||||
variant: 'add' | 'edit';
|
||||
dragIndex?: number;
|
||||
};
|
||||
|
||||
export const CommandMenuObjectMenuItem = ({
|
||||
objectMetadataItem,
|
||||
onSelect,
|
||||
variant,
|
||||
dragIndex,
|
||||
}: CommandMenuObjectMenuItemProps) => {
|
||||
const theme = useTheme();
|
||||
const { getIcon } = useIcons();
|
||||
const iconColors = getNavigationMenuItemIconColors(theme);
|
||||
const defaultViewId = useRecoilValue(
|
||||
coreIndexViewIdFromObjectMetadataItemFamilySelector({
|
||||
objectMetadataItemId: objectMetadataItem.id,
|
||||
}),
|
||||
);
|
||||
const Icon = getIcon(objectMetadataItem.icon);
|
||||
const isDisabled = !isDefined(defaultViewId);
|
||||
|
||||
const handleClick = () => {
|
||||
if (!defaultViewId) {
|
||||
return;
|
||||
}
|
||||
onSelect(objectMetadataItem, defaultViewId);
|
||||
};
|
||||
|
||||
return (
|
||||
<SelectableListItem itemId={objectMetadataItem.id} onEnter={handleClick}>
|
||||
{variant === 'add' && !isDisabled ? (
|
||||
<CommandMenuItemWithAddToNavigationDrag
|
||||
icon={Icon}
|
||||
label={objectMetadataItem.labelPlural}
|
||||
id={objectMetadataItem.id}
|
||||
onClick={handleClick}
|
||||
dragIndex={dragIndex}
|
||||
payload={{
|
||||
type: NavigationMenuItemType.OBJECT,
|
||||
objectMetadataId: objectMetadataItem.id,
|
||||
defaultViewId: defaultViewId ?? '',
|
||||
label: objectMetadataItem.labelPlural,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<CommandMenuItem
|
||||
Icon={({ size, stroke }) => (
|
||||
<IconWithBackground
|
||||
Icon={Icon}
|
||||
backgroundColor={iconColors.object}
|
||||
size={size}
|
||||
stroke={stroke}
|
||||
/>
|
||||
)}
|
||||
label={objectMetadataItem.labelPlural}
|
||||
id={objectMetadataItem.id}
|
||||
onClick={handleClick}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
)}
|
||||
</SelectableListItem>
|
||||
);
|
||||
};
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
|
||||
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
|
||||
import { CommandMenuObjectMenuItem } from '@/command-menu/pages/navigation-menu-item/components/CommandMenuObjectMenuItem';
|
||||
import { IconWithBackground } from '@/navigation-menu-item/components/IconWithBackground';
|
||||
import { getNavigationMenuItemIconColors } from '@/navigation-menu-item/utils/getNavigationMenuItemIconColors';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
|
||||
type CommandMenuObjectPickerItemProps = {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
isViewItem: boolean;
|
||||
onSelectObjectForViewEdit?: (objectMetadataItem: ObjectMetadataItem) => void;
|
||||
onChangeObject: (
|
||||
objectMetadataItem: ObjectMetadataItem,
|
||||
defaultViewId: string,
|
||||
) => void;
|
||||
objectMenuItemVariant?: 'add' | 'edit';
|
||||
dragIndex?: number;
|
||||
};
|
||||
|
||||
export const CommandMenuObjectPickerItem = ({
|
||||
objectMetadataItem,
|
||||
isViewItem,
|
||||
onSelectObjectForViewEdit,
|
||||
onChangeObject,
|
||||
objectMenuItemVariant = 'edit',
|
||||
dragIndex,
|
||||
}: CommandMenuObjectPickerItemProps) => {
|
||||
const theme = useTheme();
|
||||
const { getIcon } = useIcons();
|
||||
const iconColors = getNavigationMenuItemIconColors(theme);
|
||||
|
||||
if (isViewItem && isDefined(onSelectObjectForViewEdit)) {
|
||||
return (
|
||||
<SelectableListItem
|
||||
itemId={objectMetadataItem.id}
|
||||
onEnter={() => onSelectObjectForViewEdit(objectMetadataItem)}
|
||||
>
|
||||
<CommandMenuItem
|
||||
Icon={({ size, stroke }) => (
|
||||
<IconWithBackground
|
||||
Icon={getIcon(objectMetadataItem.icon)}
|
||||
backgroundColor={iconColors.object}
|
||||
size={size}
|
||||
stroke={stroke}
|
||||
/>
|
||||
)}
|
||||
label={objectMetadataItem.labelPlural}
|
||||
id={objectMetadataItem.id}
|
||||
onClick={() => onSelectObjectForViewEdit(objectMetadataItem)}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<CommandMenuObjectMenuItem
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
onSelect={onChangeObject}
|
||||
variant={objectMenuItemVariant}
|
||||
dragIndex={dragIndex}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { IconSettings } from 'twenty-ui/display';
|
||||
|
||||
import { CommandGroup } from '@/command-menu/components/CommandGroup';
|
||||
import { CommandMenuAddToNavDraggablePlaceholder } from '@/command-menu/components/CommandMenuAddToNavDraggablePlaceholder';
|
||||
import { CommandMenuAddToNavDroppable } from '@/command-menu/components/CommandMenuAddToNavDroppable';
|
||||
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
|
||||
import { CommandMenuList } from '@/command-menu/components/CommandMenuList';
|
||||
import { CommandMenuSubViewWithSearch } from '@/command-menu/components/CommandMenuSubViewWithSearch';
|
||||
import { useFilteredPickerItems } from '@/command-menu/hooks/useFilteredPickerItems';
|
||||
import { CommandMenuObjectPickerItem } from '@/command-menu/pages/navigation-menu-item/components/CommandMenuObjectPickerItem';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
|
||||
type CommandMenuObjectPickerSubViewProps = {
|
||||
objects: ObjectMetadataItem[];
|
||||
searchValue: string;
|
||||
onSearchChange: (value: string) => void;
|
||||
onBack: () => void;
|
||||
onOpenSystemPicker: () => void;
|
||||
isViewItem: boolean;
|
||||
onSelectObjectForViewEdit?: (objectMetadataItem: ObjectMetadataItem) => void;
|
||||
onChangeObject: (
|
||||
objectMetadataItem: ObjectMetadataItem,
|
||||
defaultViewId: string,
|
||||
) => void;
|
||||
objectMenuItemVariant?: 'add' | 'edit';
|
||||
emptyNoResultsText?: string;
|
||||
};
|
||||
|
||||
export const CommandMenuObjectPickerSubView = ({
|
||||
objects,
|
||||
searchValue,
|
||||
onSearchChange,
|
||||
onBack,
|
||||
onOpenSystemPicker,
|
||||
isViewItem,
|
||||
onSelectObjectForViewEdit,
|
||||
onChangeObject,
|
||||
objectMenuItemVariant = 'edit',
|
||||
emptyNoResultsText,
|
||||
}: CommandMenuObjectPickerSubViewProps) => {
|
||||
const { t } = useLingui();
|
||||
const { filteredItems, selectableItemIds, isEmpty, hasSearchQuery } =
|
||||
useFilteredPickerItems({
|
||||
items: objects,
|
||||
searchQuery: searchValue,
|
||||
getSearchableValues: (item) => [item.labelPlural],
|
||||
appendSelectableIds: ['system'],
|
||||
});
|
||||
|
||||
const noResultsText = hasSearchQuery
|
||||
? t`No results found`
|
||||
: (emptyNoResultsText ?? t`All objects are already in the sidebar`);
|
||||
|
||||
const isAddVariant = objectMenuItemVariant === 'add';
|
||||
|
||||
const listContent = (
|
||||
<CommandGroup heading={t`Objects`}>
|
||||
{filteredItems.map((objectMetadataItem, index) => (
|
||||
<CommandMenuObjectPickerItem
|
||||
key={objectMetadataItem.id}
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
isViewItem={isViewItem}
|
||||
onSelectObjectForViewEdit={onSelectObjectForViewEdit}
|
||||
onChangeObject={onChangeObject}
|
||||
objectMenuItemVariant={objectMenuItemVariant}
|
||||
dragIndex={isAddVariant ? index : undefined}
|
||||
/>
|
||||
))}
|
||||
{isAddVariant ? (
|
||||
<CommandMenuAddToNavDraggablePlaceholder index={filteredItems.length}>
|
||||
<SelectableListItem itemId="system" onEnter={onOpenSystemPicker}>
|
||||
<CommandMenuItem
|
||||
Icon={IconSettings}
|
||||
label={t`System objects`}
|
||||
id="system"
|
||||
hasSubMenu={true}
|
||||
onClick={onOpenSystemPicker}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
</CommandMenuAddToNavDraggablePlaceholder>
|
||||
) : (
|
||||
<SelectableListItem itemId="system" onEnter={onOpenSystemPicker}>
|
||||
<CommandMenuItem
|
||||
Icon={IconSettings}
|
||||
label={t`System objects`}
|
||||
id="system"
|
||||
hasSubMenu={true}
|
||||
onClick={onOpenSystemPicker}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
)}
|
||||
</CommandGroup>
|
||||
);
|
||||
|
||||
return (
|
||||
<CommandMenuSubViewWithSearch
|
||||
backBarTitle={t`Pick an object`}
|
||||
onBack={onBack}
|
||||
searchPlaceholder={t`Search an object...`}
|
||||
searchValue={searchValue}
|
||||
onSearchChange={onSearchChange}
|
||||
>
|
||||
{isAddVariant ? (
|
||||
<CommandMenuAddToNavDroppable>
|
||||
{({ innerRef, droppableProps, placeholder }) => (
|
||||
<CommandMenuList
|
||||
commandGroups={[]}
|
||||
selectableItemIds={selectableItemIds}
|
||||
noResults={isEmpty}
|
||||
noResultsText={noResultsText}
|
||||
>
|
||||
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
|
||||
<div ref={innerRef} {...droppableProps}>
|
||||
{listContent}
|
||||
{placeholder}
|
||||
</div>
|
||||
</CommandMenuList>
|
||||
)}
|
||||
</CommandMenuAddToNavDroppable>
|
||||
) : (
|
||||
<CommandMenuList
|
||||
commandGroups={[]}
|
||||
selectableItemIds={selectableItemIds}
|
||||
noResults={isEmpty}
|
||||
noResultsText={noResultsText}
|
||||
>
|
||||
{listContent}
|
||||
</CommandMenuList>
|
||||
)}
|
||||
</CommandMenuSubViewWithSearch>
|
||||
);
|
||||
};
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
|
||||
import { CommandGroup } from '@/command-menu/components/CommandGroup';
|
||||
import { CommandMenuList } from '@/command-menu/components/CommandMenuList';
|
||||
import { CommandMenuObjectPickerItem } from '@/command-menu/pages/navigation-menu-item/components/CommandMenuObjectPickerItem';
|
||||
import { CommandMenuSubViewWithSearch } from '@/command-menu/components/CommandMenuSubViewWithSearch';
|
||||
import { useFilteredPickerItems } from '@/command-menu/hooks/useFilteredPickerItems';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
|
||||
type CommandMenuSystemObjectPickerSubViewProps = {
|
||||
systemObjects: ObjectMetadataItem[];
|
||||
searchValue: string;
|
||||
onSearchChange: (value: string) => void;
|
||||
onBack: () => void;
|
||||
isViewItem: boolean;
|
||||
onSelectObjectForViewEdit?: (objectMetadataItem: ObjectMetadataItem) => void;
|
||||
onChangeObject: (
|
||||
objectMetadataItem: ObjectMetadataItem,
|
||||
defaultViewId: string,
|
||||
) => void;
|
||||
objectMenuItemVariant?: 'add' | 'edit';
|
||||
emptyNoResultsText?: string;
|
||||
};
|
||||
|
||||
export const CommandMenuSystemObjectPickerSubView = ({
|
||||
systemObjects,
|
||||
searchValue,
|
||||
onSearchChange,
|
||||
onBack,
|
||||
isViewItem,
|
||||
onSelectObjectForViewEdit,
|
||||
onChangeObject,
|
||||
objectMenuItemVariant = 'edit',
|
||||
emptyNoResultsText,
|
||||
}: CommandMenuSystemObjectPickerSubViewProps) => {
|
||||
const { t } = useLingui();
|
||||
const { filteredItems, selectableItemIds, isEmpty, hasSearchQuery } =
|
||||
useFilteredPickerItems({
|
||||
items: systemObjects,
|
||||
searchQuery: searchValue,
|
||||
getSearchableValues: (item) => [item.labelPlural],
|
||||
});
|
||||
|
||||
const noResultsText = hasSearchQuery
|
||||
? t`No results found`
|
||||
: (emptyNoResultsText ?? t`No system objects available`);
|
||||
|
||||
return (
|
||||
<CommandMenuSubViewWithSearch
|
||||
backBarTitle={t`System objects`}
|
||||
onBack={onBack}
|
||||
searchPlaceholder={t`Search a system object...`}
|
||||
searchValue={searchValue}
|
||||
onSearchChange={onSearchChange}
|
||||
>
|
||||
<CommandMenuList
|
||||
commandGroups={[]}
|
||||
selectableItemIds={selectableItemIds}
|
||||
noResults={isEmpty}
|
||||
noResultsText={noResultsText}
|
||||
>
|
||||
<CommandGroup heading={t`System objects`}>
|
||||
{filteredItems.map((objectMetadataItem) => (
|
||||
<CommandMenuObjectPickerItem
|
||||
key={objectMetadataItem.id}
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
isViewItem={isViewItem}
|
||||
onSelectObjectForViewEdit={onSelectObjectForViewEdit}
|
||||
onChangeObject={onChangeObject}
|
||||
objectMenuItemVariant={objectMenuItemVariant}
|
||||
/>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandMenuList>
|
||||
</CommandMenuSubViewWithSearch>
|
||||
);
|
||||
};
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IconFolder, IconLink } from 'twenty-ui/display';
|
||||
|
||||
import { useAddFolderToNavigationMenuDraft } from '@/navigation-menu-item/hooks/useAddFolderToNavigationMenuDraft';
|
||||
import { useAddLinkToNavigationMenuDraft } from '@/navigation-menu-item/hooks/useAddLinkToNavigationMenuDraft';
|
||||
import { useNavigationMenuItemsDraftState } from '@/navigation-menu-item/hooks/useNavigationMenuItemsDraftState';
|
||||
import { useOpenNavigationMenuItemInCommandMenu } from '@/navigation-menu-item/hooks/useOpenNavigationMenuItemInCommandMenu';
|
||||
import { addMenuItemInsertionContextState } from '@/navigation-menu-item/states/addMenuItemInsertionContextState';
|
||||
import { navigationMenuItemsDraftState } from '@/navigation-menu-item/states/navigationMenuItemsDraftState';
|
||||
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/states/selectedNavigationMenuItemInEditModeState';
|
||||
|
||||
export const useAddFolderAndLinkToNavigationMenu = () => {
|
||||
const { t } = useLingui();
|
||||
const { addFolderToDraft } = useAddFolderToNavigationMenuDraft();
|
||||
const { addLinkToDraft } = useAddLinkToNavigationMenuDraft();
|
||||
const { workspaceNavigationMenuItems } = useNavigationMenuItemsDraftState();
|
||||
const navigationMenuItemsDraft = useRecoilValue(
|
||||
navigationMenuItemsDraftState,
|
||||
);
|
||||
const setSelectedNavigationMenuItemInEditMode = useSetRecoilState(
|
||||
selectedNavigationMenuItemInEditModeState,
|
||||
);
|
||||
const { openNavigationMenuItemInCommandMenu } =
|
||||
useOpenNavigationMenuItemInCommandMenu();
|
||||
const addMenuItemInsertionContext = useRecoilValue(
|
||||
addMenuItemInsertionContextState,
|
||||
);
|
||||
const setAddMenuItemInsertionContext = useSetRecoilState(
|
||||
addMenuItemInsertionContextState,
|
||||
);
|
||||
|
||||
const currentDraft = isDefined(navigationMenuItemsDraft)
|
||||
? navigationMenuItemsDraft
|
||||
: workspaceNavigationMenuItems;
|
||||
|
||||
const addAndOpenEdit = (type: 'folder' | 'link') => {
|
||||
const targetFolderId = addMenuItemInsertionContext?.targetFolderId ?? null;
|
||||
const targetIndex = addMenuItemInsertionContext?.targetIndex;
|
||||
|
||||
const itemId =
|
||||
type === 'folder'
|
||||
? addFolderToDraft(
|
||||
t`New folder`,
|
||||
currentDraft,
|
||||
targetFolderId,
|
||||
targetIndex,
|
||||
)
|
||||
: addLinkToDraft(
|
||||
t`Link label`,
|
||||
'www.example.com',
|
||||
currentDraft,
|
||||
targetFolderId,
|
||||
targetIndex,
|
||||
);
|
||||
|
||||
setAddMenuItemInsertionContext(null);
|
||||
setSelectedNavigationMenuItemInEditMode(itemId);
|
||||
openNavigationMenuItemInCommandMenu({
|
||||
pageTitle: type === 'folder' ? t`Edit folder` : t`Edit link`,
|
||||
pageIcon: type === 'folder' ? IconFolder : IconLink,
|
||||
focusTitleInput: true,
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
handleAddFolder: () => addAndOpenEdit('folder'),
|
||||
handleAddLink: () => addAndOpenEdit('link'),
|
||||
};
|
||||
};
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||
import { useDraftNavigationMenuItemsAllFolders } from '@/navigation-menu-item/hooks/useDraftNavigationMenuItemsAllFolders';
|
||||
import { useDraftNavigationMenuItemsWorkspaceFolders } from '@/navigation-menu-item/hooks/useDraftNavigationMenuItemsWorkspaceFolders';
|
||||
import { useSelectedNavigationMenuItemEditItem } from '@/navigation-menu-item/hooks/useSelectedNavigationMenuItemEditItem';
|
||||
import { useNavigationMenuItemMoveRemove } from '@/navigation-menu-item/hooks/useNavigationMenuItemMoveRemove';
|
||||
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/states/selectedNavigationMenuItemInEditModeState';
|
||||
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
|
||||
|
||||
type FolderOption = {
|
||||
id: string;
|
||||
name: string;
|
||||
folderId?: string;
|
||||
};
|
||||
|
||||
const getDescendantFolderIds = (
|
||||
folderId: string,
|
||||
allFolders: FolderOption[],
|
||||
): Set<string> => {
|
||||
const result = new Set<string>();
|
||||
for (const folder of allFolders) {
|
||||
if (folder.folderId !== folderId) continue;
|
||||
result.add(folder.id);
|
||||
getDescendantFolderIds(folder.id, allFolders).forEach((id) =>
|
||||
result.add(id),
|
||||
);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const excludeCurrentFolder = <T extends { id: string }>(
|
||||
folders: T[],
|
||||
currentFolderId: string | null,
|
||||
): T[] =>
|
||||
!isDefined(currentFolderId)
|
||||
? folders
|
||||
: folders.filter((folder) => folder.id !== currentFolderId);
|
||||
|
||||
type UseFolderPickerSelectionDataParams = {
|
||||
onCloseSubView: () => void;
|
||||
};
|
||||
|
||||
export const useFolderPickerSelectionData = ({
|
||||
onCloseSubView,
|
||||
}: UseFolderPickerSelectionDataParams) => {
|
||||
const { closeCommandMenu } = useCommandMenu();
|
||||
const { moveToFolder } = useNavigationMenuItemMoveRemove();
|
||||
const selectedNavigationMenuItemInEditMode = useRecoilValue(
|
||||
selectedNavigationMenuItemInEditModeState,
|
||||
);
|
||||
const { selectedItem } = useSelectedNavigationMenuItemEditItem();
|
||||
const selectedItemType = selectedItem?.itemType ?? null;
|
||||
const { allFolders } = useDraftNavigationMenuItemsAllFolders();
|
||||
const { workspaceFolders } = useDraftNavigationMenuItemsWorkspaceFolders();
|
||||
|
||||
const selectedFolderId =
|
||||
selectedItemType === NavigationMenuItemType.FOLDER
|
||||
? selectedNavigationMenuItemInEditMode
|
||||
: null;
|
||||
const currentFolderId =
|
||||
selectedItemType === NavigationMenuItemType.FOLDER
|
||||
? (selectedItem?.id ?? null)
|
||||
: (selectedItem?.folderId ?? null);
|
||||
|
||||
const descendantFolderIds =
|
||||
selectedItemType === NavigationMenuItemType.FOLDER &&
|
||||
isDefined(selectedFolderId)
|
||||
? getDescendantFolderIds(selectedFolderId, allFolders)
|
||||
: new Set<string>();
|
||||
|
||||
const includeNoFolderOption =
|
||||
(selectedItemType === NavigationMenuItemType.FOLDER &&
|
||||
isDefined(selectedFolderId)) ||
|
||||
(selectedItemType === NavigationMenuItemType.LINK &&
|
||||
isDefined(currentFolderId));
|
||||
|
||||
const folders =
|
||||
includeNoFolderOption &&
|
||||
selectedItemType === NavigationMenuItemType.FOLDER &&
|
||||
isDefined(selectedFolderId)
|
||||
? allFolders.filter(
|
||||
(folder) =>
|
||||
folder.id !== selectedFolderId &&
|
||||
!descendantFolderIds.has(folder.id),
|
||||
)
|
||||
: excludeCurrentFolder(allFolders, currentFolderId);
|
||||
|
||||
const foldersToShow = includeNoFolderOption
|
||||
? folders
|
||||
: excludeCurrentFolder(workspaceFolders, currentFolderId);
|
||||
|
||||
const handleSelectFolder = (folderId: string | null) => {
|
||||
if (isDefined(selectedNavigationMenuItemInEditMode)) {
|
||||
moveToFolder(selectedNavigationMenuItemInEditMode, folderId);
|
||||
onCloseSubView();
|
||||
closeCommandMenu();
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
foldersToShow,
|
||||
includeNoFolderOption,
|
||||
handleSelectFolder,
|
||||
};
|
||||
};
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IconPlus } from 'twenty-ui/display';
|
||||
|
||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||
import { useNavigateCommandMenu } from '@/command-menu/hooks/useNavigateCommandMenu';
|
||||
import { type OrganizeActionsProps } from '@/command-menu/pages/navigation-menu-item/components/CommandMenuEditOrganizeActions';
|
||||
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
|
||||
import { useNavigationMenuItemMoveRemove } from '@/navigation-menu-item/hooks/useNavigationMenuItemMoveRemove';
|
||||
import { useNavigationMenuItemsDraftState } from '@/navigation-menu-item/hooks/useNavigationMenuItemsDraftState';
|
||||
import { useWorkspaceSectionItems } from '@/navigation-menu-item/hooks/useWorkspaceSectionItems';
|
||||
import { addMenuItemInsertionContextState } from '@/navigation-menu-item/states/addMenuItemInsertionContextState';
|
||||
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/states/selectedNavigationMenuItemInEditModeState';
|
||||
import { type AddMenuItemInsertionContext } from '@/navigation-menu-item/types/AddMenuItemInsertionContext';
|
||||
|
||||
const getAddMenuItemInsertionContext = (
|
||||
selectedItem: { id: string; folderId?: string | null },
|
||||
workspaceNavigationMenuItems: Array<{
|
||||
id: string;
|
||||
folderId?: string | null;
|
||||
userWorkspaceId?: string | null;
|
||||
}>,
|
||||
offset: 0 | 1,
|
||||
): AddMenuItemInsertionContext | null => {
|
||||
const targetFolderId = selectedItem.folderId ?? null;
|
||||
const itemsInFolder = workspaceNavigationMenuItems.filter(
|
||||
(item) =>
|
||||
(item.folderId ?? null) === targetFolderId &&
|
||||
!isDefined(item.userWorkspaceId),
|
||||
);
|
||||
const selectedIndexInFolder = itemsInFolder.findIndex(
|
||||
(item) => item.id === selectedItem.id,
|
||||
);
|
||||
|
||||
if (selectedIndexInFolder < 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
targetFolderId,
|
||||
targetIndex: selectedIndexInFolder + offset,
|
||||
};
|
||||
};
|
||||
|
||||
export const useNavigationMenuItemEditOrganizeActions =
|
||||
(): OrganizeActionsProps => {
|
||||
const { t } = useLingui();
|
||||
const { closeCommandMenu } = useCommandMenu();
|
||||
const { navigateCommandMenu } = useNavigateCommandMenu();
|
||||
const selectedNavigationMenuItemInEditMode = useRecoilValue(
|
||||
selectedNavigationMenuItemInEditModeState,
|
||||
);
|
||||
const setSelectedNavigationMenuItemInEditMode = useSetRecoilState(
|
||||
selectedNavigationMenuItemInEditModeState,
|
||||
);
|
||||
const setAddMenuItemInsertionContext = useSetRecoilState(
|
||||
addMenuItemInsertionContextState,
|
||||
);
|
||||
const { workspaceNavigationMenuItems } = useNavigationMenuItemsDraftState();
|
||||
const items = useWorkspaceSectionItems();
|
||||
const { moveUp, moveDown, remove } = useNavigationMenuItemMoveRemove();
|
||||
|
||||
const selectedItem = selectedNavigationMenuItemInEditMode
|
||||
? items.find((item) => item.id === selectedNavigationMenuItemInEditMode)
|
||||
: undefined;
|
||||
|
||||
const folderId = selectedItem?.folderId ?? null;
|
||||
const siblings = items
|
||||
.filter(
|
||||
(item) =>
|
||||
('folderId' in item ? (item.folderId ?? null) : null) === folderId,
|
||||
)
|
||||
.sort((a, b) => a.position - b.position);
|
||||
const selectedIndexInSiblings = selectedItem
|
||||
? siblings.findIndex((item) => item.id === selectedItem.id)
|
||||
: -1;
|
||||
|
||||
const canMoveUp =
|
||||
selectedIndexInSiblings > 0 &&
|
||||
selectedItem != null &&
|
||||
isDefined(selectedNavigationMenuItemInEditMode);
|
||||
const canMoveDown =
|
||||
selectedIndexInSiblings >= 0 &&
|
||||
selectedIndexInSiblings < siblings.length - 1 &&
|
||||
selectedItem != null &&
|
||||
isDefined(selectedNavigationMenuItemInEditMode);
|
||||
|
||||
const handleMoveUp = () => {
|
||||
if (canMoveUp && isDefined(selectedNavigationMenuItemInEditMode)) {
|
||||
moveUp(selectedNavigationMenuItemInEditMode);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMoveDown = () => {
|
||||
if (canMoveDown && isDefined(selectedNavigationMenuItemInEditMode)) {
|
||||
moveDown(selectedNavigationMenuItemInEditMode);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemove = () => {
|
||||
if (isDefined(selectedNavigationMenuItemInEditMode)) {
|
||||
remove(selectedNavigationMenuItemInEditMode);
|
||||
setSelectedNavigationMenuItemInEditMode(null);
|
||||
closeCommandMenu();
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddAtOffset = (offset: 0 | 1) => {
|
||||
if (!isDefined(selectedItem)) return;
|
||||
const context = getAddMenuItemInsertionContext(
|
||||
selectedItem,
|
||||
workspaceNavigationMenuItems,
|
||||
offset,
|
||||
);
|
||||
if (!context) return;
|
||||
setAddMenuItemInsertionContext(context);
|
||||
navigateCommandMenu({
|
||||
page: CommandMenuPages.NavigationMenuAddItem,
|
||||
pageTitle: t`New sidebar item`,
|
||||
pageIcon: IconPlus,
|
||||
resetNavigationStack: true,
|
||||
});
|
||||
};
|
||||
|
||||
const handleAddBefore = () => handleAddAtOffset(0);
|
||||
const handleAddAfter = () => handleAddAtOffset(1);
|
||||
|
||||
return {
|
||||
canMoveUp,
|
||||
canMoveDown,
|
||||
onMoveUp: handleMoveUp,
|
||||
onMoveDown: handleMoveDown,
|
||||
onRemove: handleRemove,
|
||||
onAddBefore: handleAddBefore,
|
||||
onAddAfter: handleAddAfter,
|
||||
};
|
||||
};
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
|
||||
type GetAvailableObjectMetadataForNewSidebarItemProps = {
|
||||
objectMetadataItems: ObjectMetadataItem[];
|
||||
activeNonSystemObjectMetadataItems: ObjectMetadataItem[];
|
||||
objectMetadataIdsWithIndexView: Set<string>;
|
||||
objectMetadataIdsWithDisplayableViews: Set<string>;
|
||||
};
|
||||
|
||||
type GetAvailableObjectMetadataForNewSidebarItemResult = {
|
||||
availableObjectMetadataItems: ObjectMetadataItem[];
|
||||
availableSystemObjectMetadataItems: ObjectMetadataItem[];
|
||||
objectMetadataItemsWithViews: ObjectMetadataItem[];
|
||||
availableSystemObjectMetadataItemsForView: ObjectMetadataItem[];
|
||||
};
|
||||
|
||||
export const getAvailableObjectMetadataForNewSidebarItem = ({
|
||||
objectMetadataItems,
|
||||
activeNonSystemObjectMetadataItems,
|
||||
objectMetadataIdsWithIndexView,
|
||||
objectMetadataIdsWithDisplayableViews,
|
||||
}: GetAvailableObjectMetadataForNewSidebarItemProps): GetAvailableObjectMetadataForNewSidebarItemResult => {
|
||||
const availableObjectMetadataItems = [
|
||||
...activeNonSystemObjectMetadataItems,
|
||||
].sort((a, b) => a.labelPlural.localeCompare(b.labelPlural));
|
||||
|
||||
const activeSystemObjectMetadataItems = objectMetadataItems
|
||||
.filter((item) => item.isActive && item.isSystem)
|
||||
.sort((a, b) => a.labelPlural.localeCompare(b.labelPlural));
|
||||
|
||||
const availableSystemObjectMetadataItems =
|
||||
activeSystemObjectMetadataItems.filter((item) =>
|
||||
objectMetadataIdsWithIndexView.has(item.id),
|
||||
);
|
||||
|
||||
const objectMetadataItemsWithViews = objectMetadataItems
|
||||
.filter(
|
||||
(item) =>
|
||||
item.isActive && objectMetadataIdsWithDisplayableViews.has(item.id),
|
||||
)
|
||||
.filter((item) => !item.isSystem)
|
||||
.sort((a, b) => a.labelPlural.localeCompare(b.labelPlural));
|
||||
|
||||
const availableSystemObjectMetadataItemsForView =
|
||||
activeSystemObjectMetadataItems.filter((item) =>
|
||||
objectMetadataIdsWithDisplayableViews.has(item.id),
|
||||
);
|
||||
|
||||
return {
|
||||
availableObjectMetadataItems,
|
||||
availableSystemObjectMetadataItems,
|
||||
objectMetadataItemsWithViews,
|
||||
availableSystemObjectMetadataItemsForView,
|
||||
};
|
||||
};
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
import { CommandMenuNavigationItemActions } from '@/command-menu/pages/navigation-menu-item/command-menu-navigation-item-actions';
|
||||
|
||||
const ORGANIZE_ACTIONS_WITHOUT_MOVE_TO_FOLDER: CommandMenuNavigationItemActions[] =
|
||||
[
|
||||
CommandMenuNavigationItemActions.MOVE_UP,
|
||||
CommandMenuNavigationItemActions.MOVE_DOWN,
|
||||
CommandMenuNavigationItemActions.ADD_BEFORE,
|
||||
CommandMenuNavigationItemActions.ADD_AFTER,
|
||||
CommandMenuNavigationItemActions.REMOVE,
|
||||
];
|
||||
|
||||
const ORGANIZE_ACTIONS_WITH_MOVE_TO_FOLDER: CommandMenuNavigationItemActions[] =
|
||||
[
|
||||
CommandMenuNavigationItemActions.MOVE_UP,
|
||||
CommandMenuNavigationItemActions.MOVE_DOWN,
|
||||
CommandMenuNavigationItemActions.MOVE_TO_FOLDER,
|
||||
CommandMenuNavigationItemActions.ADD_BEFORE,
|
||||
CommandMenuNavigationItemActions.ADD_AFTER,
|
||||
CommandMenuNavigationItemActions.REMOVE,
|
||||
];
|
||||
|
||||
export const getOrganizeActionsSelectableItemIds = (
|
||||
includeMoveToFolder: boolean,
|
||||
): CommandMenuNavigationItemActions[] =>
|
||||
includeMoveToFolder
|
||||
? ORGANIZE_ACTIONS_WITH_MOVE_TO_FOLDER
|
||||
: ORGANIZE_ACTIONS_WITHOUT_MOVE_TO_FOLDER;
|
||||
@@ -24,4 +24,6 @@ export enum CommandMenuPages {
|
||||
PageLayoutFieldsSettings = 'page-layout-fields-settings',
|
||||
PageLayoutFieldsLayout = 'page-layout-fields-layout',
|
||||
ViewFrontComponent = 'view-front-component',
|
||||
NavigationMenuItemEdit = 'navigation-menu-item-edit',
|
||||
NavigationMenuAddItem = 'navigation-menu-add-item',
|
||||
}
|
||||
|
||||
+2
-3
@@ -1,6 +1,5 @@
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { CurrentWorkspaceMemberOrphanFavorites } from '@/favorites/components/CurrentWorkspaceMemberOrphanFavorites';
|
||||
import { FavoritesDragProvider } from '@/favorites/components/FavoritesDragProvider';
|
||||
import { FavoriteFolders } from '@/favorites/components/FavoritesFolders';
|
||||
import { FavoritesSkeletonLoader } from '@/favorites/components/FavoritesSkeletonLoader';
|
||||
import { useFavorites } from '@/favorites/hooks/useFavorites';
|
||||
@@ -69,10 +68,10 @@ export const CurrentWorkspaceMemberFavoritesFolders = () => {
|
||||
/>
|
||||
</NavigationDrawerAnimatedCollapseWrapper>
|
||||
{isNavigationSectionOpen && (
|
||||
<FavoritesDragProvider>
|
||||
<>
|
||||
<FavoriteFolders isNavigationSectionOpen={isNavigationSectionOpen} />
|
||||
<CurrentWorkspaceMemberOrphanFavorites />
|
||||
</FavoritesDragProvider>
|
||||
</>
|
||||
)}
|
||||
</NavigationDrawerSection>
|
||||
);
|
||||
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
import { css, useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import type { ReactNode } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IconGripVertical, type IconComponent } from 'twenty-ui/display';
|
||||
|
||||
import { StyledNavigationMenuItemIconContainer } from '@/navigation-menu-item/components/NavigationMenuItemIconContainer';
|
||||
import type { AddToNavigationDragPayload } from '@/navigation-menu-item/types/add-to-navigation-drag-payload';
|
||||
import { getIconBackgroundColorForPayload } from '@/navigation-menu-item/utils/getIconBackgroundColorForPayload';
|
||||
|
||||
const StyledIconSlot = styled.div<{ $hasFixedSize: boolean }>`
|
||||
align-items: center;
|
||||
cursor: grab;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
justify-content: center;
|
||||
|
||||
${({ theme, $hasFixedSize }) =>
|
||||
$hasFixedSize &&
|
||||
css`
|
||||
height: ${theme.spacing(4.5)};
|
||||
width: ${theme.spacing(4.5)};
|
||||
`}
|
||||
|
||||
&:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
`;
|
||||
|
||||
type AddToNavigationDragHandleIconProps = {
|
||||
icon?: IconComponent;
|
||||
customIconContent?: ReactNode;
|
||||
};
|
||||
|
||||
const AddToNavigationDragHandleIcon = ({
|
||||
icon,
|
||||
customIconContent,
|
||||
}: AddToNavigationDragHandleIconProps) => {
|
||||
const theme = useTheme();
|
||||
const iconSize = theme.icon.size.md;
|
||||
const iconStroke = theme.icon.stroke.sm;
|
||||
|
||||
if (isDefined(customIconContent)) {
|
||||
return <>{customIconContent}</>;
|
||||
}
|
||||
|
||||
if (isDefined(icon)) {
|
||||
const Icon = icon;
|
||||
return (
|
||||
<Icon size={iconSize} stroke={iconStroke} color={theme.grayScale.gray1} />
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
type AddToNavigationDragHandleProps = {
|
||||
icon?: IconComponent;
|
||||
customIconContent?: ReactNode;
|
||||
payload: AddToNavigationDragPayload;
|
||||
isHovered: boolean;
|
||||
};
|
||||
|
||||
export const AddToNavigationDragHandle = ({
|
||||
icon,
|
||||
customIconContent,
|
||||
payload,
|
||||
isHovered,
|
||||
}: AddToNavigationDragHandleProps) => {
|
||||
const theme = useTheme();
|
||||
const iconBackgroundColor = getIconBackgroundColorForPayload(payload, theme);
|
||||
const hasBackgroundColor = !!iconBackgroundColor && !isHovered;
|
||||
const payloadHasBackgroundColor = !!iconBackgroundColor;
|
||||
const iconSize = theme.icon.size.md;
|
||||
const iconStroke = theme.icon.stroke.sm;
|
||||
|
||||
const showCustomContentWithoutWrapper = isDefined(customIconContent);
|
||||
|
||||
return (
|
||||
<StyledIconSlot
|
||||
$hasFixedSize={
|
||||
payloadHasBackgroundColor || showCustomContentWithoutWrapper
|
||||
}
|
||||
>
|
||||
{isHovered ? (
|
||||
<IconGripVertical
|
||||
size={iconSize}
|
||||
stroke={iconStroke}
|
||||
color={theme.font.color.tertiary}
|
||||
/>
|
||||
) : showCustomContentWithoutWrapper ? (
|
||||
customIconContent
|
||||
) : hasBackgroundColor ? (
|
||||
<StyledNavigationMenuItemIconContainer
|
||||
$backgroundColor={iconBackgroundColor}
|
||||
>
|
||||
<AddToNavigationDragHandleIcon
|
||||
icon={icon}
|
||||
customIconContent={customIconContent}
|
||||
/>
|
||||
</StyledNavigationMenuItemIconContainer>
|
||||
) : (
|
||||
<AddToNavigationDragHandleIcon
|
||||
icon={icon}
|
||||
customIconContent={customIconContent}
|
||||
/>
|
||||
)}
|
||||
</StyledIconSlot>
|
||||
);
|
||||
};
|
||||
+5
-5
@@ -6,7 +6,6 @@ import { LightIconButton } from 'twenty-ui/input';
|
||||
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { CurrentWorkspaceMemberOrphanNavigationMenuItems } from '@/navigation-menu-item/components/CurrentWorkspaceMemberOrphanNavigationMenuItems';
|
||||
import { NavigationMenuItemDragProvider } from '@/navigation-menu-item/components/NavigationMenuItemDragProvider';
|
||||
import { NavigationMenuItemFolders } from '@/navigation-menu-item/components/NavigationMenuItemFolders';
|
||||
import { NavigationMenuItemSkeletonLoader } from '@/navigation-menu-item/components/NavigationMenuItemSkeletonLoader';
|
||||
import { useNavigationMenuItemsByFolder } from '@/navigation-menu-item/hooks/useNavigationMenuItemsByFolder';
|
||||
@@ -21,7 +20,7 @@ import { useNavigationSection } from '@/ui/navigation/navigation-drawer/hooks/us
|
||||
export const CurrentWorkspaceMemberNavigationMenuItemFolders = () => {
|
||||
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
|
||||
const { navigationMenuItemsSorted } = useSortedNavigationMenuItems();
|
||||
const { navigationMenuItemsByFolder } = useNavigationMenuItemsByFolder();
|
||||
const { userNavigationMenuItemsByFolder } = useNavigationMenuItemsByFolder();
|
||||
|
||||
const [
|
||||
isNavigationMenuItemFolderCreating,
|
||||
@@ -51,7 +50,8 @@ export const CurrentWorkspaceMemberNavigationMenuItemFolders = () => {
|
||||
if (
|
||||
(!navigationMenuItemsSorted || navigationMenuItemsSorted.length === 0) &&
|
||||
!isNavigationMenuItemFolderCreating &&
|
||||
(!navigationMenuItemsByFolder || navigationMenuItemsByFolder.length === 0)
|
||||
(!userNavigationMenuItemsByFolder ||
|
||||
userNavigationMenuItemsByFolder.length === 0)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
@@ -72,12 +72,12 @@ export const CurrentWorkspaceMemberNavigationMenuItemFolders = () => {
|
||||
/>
|
||||
</NavigationDrawerAnimatedCollapseWrapper>
|
||||
{isNavigationSectionOpen && (
|
||||
<NavigationMenuItemDragProvider>
|
||||
<>
|
||||
<NavigationMenuItemFolders
|
||||
isNavigationSectionOpen={isNavigationSectionOpen}
|
||||
/>
|
||||
<CurrentWorkspaceMemberOrphanNavigationMenuItems />
|
||||
</NavigationMenuItemDragProvider>
|
||||
</>
|
||||
)}
|
||||
</NavigationDrawerSection>
|
||||
);
|
||||
|
||||
+82
-38
@@ -1,18 +1,26 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { Droppable } from '@hello-pangea/dnd';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useContext, useState } from 'react';
|
||||
|
||||
import { useIsDropDisabledForSection } from '@/navigation-menu-item/hooks/useIsDropDisabledForSection';
|
||||
import { NavigationSections } from '@/navigation-menu-item/constants/NavigationSections.constants';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import { IconFolder, IconFolderOpen, IconHeartOff } from 'twenty-ui/display';
|
||||
|
||||
import { getNavigationMenuItemIconColors } from '@/navigation-menu-item/utils/getNavigationMenuItemIconColors';
|
||||
import { LightIconButton } from 'twenty-ui/input';
|
||||
import { AnimatedExpandableContainer } from 'twenty-ui/layout';
|
||||
import { useIsMobile } from 'twenty-ui/utilities';
|
||||
|
||||
import { NavigationItemDropTarget } from '@/navigation-menu-item/components/NavigationItemDropTarget';
|
||||
import { NavigationMenuItemDroppable } from '@/navigation-menu-item/components/NavigationMenuItemDroppable';
|
||||
import { NavigationMenuItemFolderNavigationDrawerItemDropdown } from '@/navigation-menu-item/components/NavigationMenuItemFolderNavigationDrawerItemDropdown';
|
||||
import { NavigationMenuItemIcon } from '@/navigation-menu-item/components/NavigationMenuItemIcon';
|
||||
import { NAVIGATION_MENU_ITEM_FOLDER_DELETE_MODAL_ID } from '@/navigation-menu-item/constants/NavigationMenuItemFolderDeleteModalId';
|
||||
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
|
||||
import { NavigationMenuItemDragContext } from '@/navigation-menu-item/contexts/NavigationMenuItemDragContext';
|
||||
import { useDeleteNavigationMenuItem } from '@/navigation-menu-item/hooks/useDeleteNavigationMenuItem';
|
||||
import { useDeleteNavigationMenuItemFolder } from '@/navigation-menu-item/hooks/useDeleteNavigationMenuItemFolder';
|
||||
@@ -35,24 +43,32 @@ import { NavigationDrawerSubItem } from '@/ui/navigation/navigation-drawer/compo
|
||||
import { currentNavigationMenuItemFolderIdState } from '@/ui/navigation/navigation-drawer/states/currentNavigationMenuItemFolderIdState';
|
||||
import { getNavigationSubItemLeftAdornment } from '@/ui/navigation/navigation-drawer/utils/getNavigationSubItemLeftAdornment';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type CurrentWorkspaceMemberNavigationMenuItemsProps = {
|
||||
folder: {
|
||||
folderId: string;
|
||||
id: string;
|
||||
folderName: string;
|
||||
navigationMenuItems: ProcessedNavigationMenuItem[];
|
||||
};
|
||||
isGroup: boolean;
|
||||
isWorkspaceFolder?: boolean;
|
||||
};
|
||||
|
||||
export const CurrentWorkspaceMemberNavigationMenuItems = ({
|
||||
folder,
|
||||
isGroup,
|
||||
isWorkspaceFolder = false,
|
||||
}: CurrentWorkspaceMemberNavigationMenuItemsProps) => {
|
||||
const { t } = useLingui();
|
||||
const theme = useTheme();
|
||||
const iconColors = getNavigationMenuItemIconColors(theme);
|
||||
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
|
||||
const currentPath = useLocation().pathname;
|
||||
const currentViewPath = useLocation().pathname + useLocation().search;
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const currentPath = location.pathname;
|
||||
const currentViewPath = location.pathname + location.search;
|
||||
const { isDragging } = useContext(NavigationMenuItemDragContext);
|
||||
const [
|
||||
isNavigationMenuItemFolderRenaming,
|
||||
@@ -71,22 +87,31 @@ export const CurrentWorkspaceMemberNavigationMenuItems = ({
|
||||
currentNavigationMenuItemFolderIdState,
|
||||
);
|
||||
|
||||
const isOpen = openNavigationMenuItemFolderIds.includes(folder.folderId);
|
||||
const isOpen = openNavigationMenuItemFolderIds.includes(folder.id);
|
||||
|
||||
const handleToggle = () => {
|
||||
if (isMobile) {
|
||||
setCurrentFolderId((prev) =>
|
||||
prev === folder.folderId ? null : folder.folderId,
|
||||
);
|
||||
setCurrentFolderId((prev) => (prev === folder.id ? null : folder.id));
|
||||
} else {
|
||||
setOpenNavigationMenuItemFolderIds((currentOpenFolders) => {
|
||||
if (isOpen) {
|
||||
return currentOpenFolders.filter((id) => id !== folder.folderId);
|
||||
return currentOpenFolders.filter((id) => id !== folder.id);
|
||||
} else {
|
||||
return [...currentOpenFolders, folder.folderId];
|
||||
return [...currentOpenFolders, folder.id];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!isOpen) {
|
||||
const firstNonLinkItem = folder.navigationMenuItems.find(
|
||||
(item) =>
|
||||
item.itemType !== NavigationMenuItemType.LINK &&
|
||||
isNonEmptyString(item.link),
|
||||
);
|
||||
if (isDefined(firstNonLinkItem?.link)) {
|
||||
navigate(firstNonLinkItem.link);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const { renameNavigationMenuItemFolder } =
|
||||
@@ -94,7 +119,7 @@ export const CurrentWorkspaceMemberNavigationMenuItems = ({
|
||||
const { deleteNavigationMenuItemFolder } =
|
||||
useDeleteNavigationMenuItemFolder();
|
||||
|
||||
const dropdownId = `navigation-menu-item-folder-edit-${folder.folderId}`;
|
||||
const dropdownId = `navigation-menu-item-folder-edit-${folder.id}`;
|
||||
|
||||
const isDropdownOpenComponent = useRecoilComponentValue(
|
||||
isDropdownOpenComponentState,
|
||||
@@ -109,13 +134,15 @@ export const CurrentWorkspaceMemberNavigationMenuItems = ({
|
||||
);
|
||||
|
||||
const { deleteNavigationMenuItem } = useDeleteNavigationMenuItem();
|
||||
const folderContentDropDisabled =
|
||||
useIsDropDisabledForSection(isWorkspaceFolder);
|
||||
|
||||
const navigationMenuItemFolderContentLength =
|
||||
folder.navigationMenuItems.length;
|
||||
|
||||
const handleSubmitRename = async (value: string) => {
|
||||
if (value === '') return;
|
||||
await renameNavigationMenuItemFolder(folder.folderId, value);
|
||||
await renameNavigationMenuItemFolder(folder.id, value);
|
||||
setIsNavigationMenuItemFolderRenaming(false);
|
||||
return true;
|
||||
};
|
||||
@@ -134,29 +161,29 @@ export const CurrentWorkspaceMemberNavigationMenuItems = ({
|
||||
return;
|
||||
}
|
||||
|
||||
await renameNavigationMenuItemFolder(folder.folderId, value);
|
||||
await renameNavigationMenuItemFolder(folder.id, value);
|
||||
setIsNavigationMenuItemFolderRenaming(false);
|
||||
};
|
||||
|
||||
const modalId = `${NAVIGATION_MENU_ITEM_FOLDER_DELETE_MODAL_ID}-${folder.folderId}`;
|
||||
const modalId = `${NAVIGATION_MENU_ITEM_FOLDER_DELETE_MODAL_ID}-${folder.id}`;
|
||||
|
||||
const handleNavigationMenuItemFolderDelete = async () => {
|
||||
if (folder.navigationMenuItems.length > 0) {
|
||||
openModal(modalId);
|
||||
closeDropdown(dropdownId);
|
||||
} else {
|
||||
await deleteNavigationMenuItemFolder(folder.folderId);
|
||||
await deleteNavigationMenuItemFolder(folder.id);
|
||||
closeDropdown(dropdownId);
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmDelete = async () => {
|
||||
await deleteNavigationMenuItemFolder(folder.folderId);
|
||||
await deleteNavigationMenuItemFolder(folder.id);
|
||||
};
|
||||
|
||||
const rightOptions = (
|
||||
const rightOptions = isWorkspaceFolder ? undefined : (
|
||||
<NavigationMenuItemFolderNavigationDrawerItemDropdown
|
||||
folderId={folder.folderId}
|
||||
folderId={folder.id}
|
||||
onRename={() => setIsNavigationMenuItemFolderRenaming(true)}
|
||||
onDelete={handleNavigationMenuItemFolderDelete}
|
||||
closeDropdown={() => {
|
||||
@@ -175,7 +202,7 @@ export const CurrentWorkspaceMemberNavigationMenuItems = ({
|
||||
return (
|
||||
<>
|
||||
<NavigationDrawerItemsCollapsableContainer
|
||||
key={folder.folderId}
|
||||
key={folder.id}
|
||||
isGroup={isGroup}
|
||||
>
|
||||
{isNavigationMenuItemFolderRenaming ? (
|
||||
@@ -189,18 +216,30 @@ export const CurrentWorkspaceMemberNavigationMenuItems = ({
|
||||
/>
|
||||
) : (
|
||||
<NavigationMenuItemDroppable
|
||||
droppableId={`folder-header-${folder.folderId}`}
|
||||
droppableId={`folder-header-${folder.id}`}
|
||||
isWorkspaceSection={isWorkspaceFolder}
|
||||
>
|
||||
<NavigationDrawerItem
|
||||
label={folder.folderName}
|
||||
Icon={isOpen ? IconFolderOpen : IconFolder}
|
||||
onClick={handleToggle}
|
||||
rightOptions={rightOptions}
|
||||
className="navigation-drawer-item"
|
||||
isRightOptionsDropdownOpen={isDropdownOpenComponent}
|
||||
triggerEvent="CLICK"
|
||||
preventCollapseOnMobile={isMobile}
|
||||
/>
|
||||
<NavigationItemDropTarget
|
||||
folderId={folder.id}
|
||||
index={0}
|
||||
sectionId={
|
||||
isWorkspaceFolder
|
||||
? NavigationSections.WORKSPACE
|
||||
: NavigationSections.FAVORITES
|
||||
}
|
||||
>
|
||||
<NavigationDrawerItem
|
||||
label={folder.folderName}
|
||||
Icon={isOpen ? IconFolderOpen : IconFolder}
|
||||
iconBackgroundColor={iconColors.folder}
|
||||
onClick={handleToggle}
|
||||
rightOptions={rightOptions}
|
||||
className="navigation-drawer-item"
|
||||
isRightOptionsDropdownOpen={isDropdownOpenComponent}
|
||||
triggerEvent="CLICK"
|
||||
preventCollapseOnMobile={isMobile}
|
||||
/>
|
||||
</NavigationItemDropTarget>
|
||||
</NavigationMenuItemDroppable>
|
||||
)}
|
||||
|
||||
@@ -210,7 +249,10 @@ export const CurrentWorkspaceMemberNavigationMenuItems = ({
|
||||
mode="fit-content"
|
||||
containAnimation
|
||||
>
|
||||
<Droppable droppableId={`folder-${folder.folderId}`}>
|
||||
<Droppable
|
||||
droppableId={`folder-${folder.id}`}
|
||||
isDropDisabled={folderContentDropDisabled}
|
||||
>
|
||||
{(provided) => (
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
@@ -244,13 +286,15 @@ export const CurrentWorkspaceMemberNavigationMenuItems = ({
|
||||
selectedIndex: selectedNavigationMenuItemIndex,
|
||||
})}
|
||||
rightOptions={
|
||||
<LightIconButton
|
||||
Icon={IconHeartOff}
|
||||
onClick={() =>
|
||||
deleteNavigationMenuItem(navigationMenuItem.id)
|
||||
}
|
||||
accent="tertiary"
|
||||
/>
|
||||
isWorkspaceFolder ? undefined : (
|
||||
<LightIconButton
|
||||
Icon={IconHeartOff}
|
||||
onClick={() =>
|
||||
deleteNavigationMenuItem(navigationMenuItem.id)
|
||||
}
|
||||
accent="tertiary"
|
||||
/>
|
||||
)
|
||||
}
|
||||
isDragging={isDragging}
|
||||
triggerEvent="CLICK"
|
||||
|
||||
+59
-41
@@ -5,9 +5,11 @@ import { useRecoilValue } from 'recoil';
|
||||
import { IconHeartOff } from 'twenty-ui/display';
|
||||
import { LightIconButton } from 'twenty-ui/input';
|
||||
|
||||
import { NavigationItemDropTarget } from '@/navigation-menu-item/components/NavigationItemDropTarget';
|
||||
import { NavigationSections } from '@/navigation-menu-item/constants/NavigationSections.constants';
|
||||
import { NavigationMenuItemDroppable } from '@/navigation-menu-item/components/NavigationMenuItemDroppable';
|
||||
import { NavigationMenuItemIcon } from '@/navigation-menu-item/components/NavigationMenuItemIcon';
|
||||
import { ORPHAN_NAVIGATION_MENU_ITEMS_DROPPABLE_ID } from '@/navigation-menu-item/constants/NavigationMenuItemDroppableIds';
|
||||
import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/constants/NavigationMenuItemDroppableIds';
|
||||
import { NavigationMenuItemDragContext } from '@/navigation-menu-item/contexts/NavigationMenuItemDragContext';
|
||||
import { useDeleteNavigationMenuItem } from '@/navigation-menu-item/hooks/useDeleteNavigationMenuItem';
|
||||
import { useSortedNavigationMenuItems } from '@/navigation-menu-item/hooks/useSortedNavigationMenuItems';
|
||||
@@ -39,51 +41,67 @@ export const CurrentWorkspaceMemberOrphanNavigationMenuItems = () => {
|
||||
|
||||
return (
|
||||
<NavigationMenuItemDroppable
|
||||
droppableId={ORPHAN_NAVIGATION_MENU_ITEMS_DROPPABLE_ID}
|
||||
droppableId={
|
||||
NavigationMenuItemDroppableIds.ORPHAN_NAVIGATION_MENU_ITEMS
|
||||
}
|
||||
isWorkspaceSection={false}
|
||||
>
|
||||
{orphanNavigationMenuItems.length > 0 ? (
|
||||
orphanNavigationMenuItems.map((navigationMenuItem, index) => (
|
||||
<DraggableItem
|
||||
key={navigationMenuItem.id}
|
||||
draggableId={navigationMenuItem.id}
|
||||
index={index}
|
||||
isInsideScrollableContainer={true}
|
||||
itemComponent={
|
||||
<StyledOrphanNavigationMenuItemsContainer>
|
||||
<NavigationDrawerItem
|
||||
secondaryLabel={getNavigationMenuItemSecondaryLabel({
|
||||
objectMetadataItems,
|
||||
navigationMenuItemObjectNameSingular:
|
||||
navigationMenuItem.objectNameSingular,
|
||||
})}
|
||||
label={navigationMenuItem.labelIdentifier}
|
||||
Icon={() => (
|
||||
<NavigationMenuItemIcon
|
||||
navigationMenuItem={navigationMenuItem}
|
||||
/>
|
||||
)}
|
||||
active={isLocationMatchingNavigationMenuItem(
|
||||
currentPath,
|
||||
currentViewPath,
|
||||
navigationMenuItem,
|
||||
)}
|
||||
to={isDragging ? undefined : navigationMenuItem.link}
|
||||
rightOptions={
|
||||
<LightIconButton
|
||||
Icon={IconHeartOff}
|
||||
onClick={() =>
|
||||
deleteNavigationMenuItem(navigationMenuItem.id)
|
||||
<>
|
||||
{orphanNavigationMenuItems.map((navigationMenuItem, index) => (
|
||||
<NavigationItemDropTarget
|
||||
key={navigationMenuItem.id}
|
||||
folderId={null}
|
||||
index={index}
|
||||
sectionId={NavigationSections.FAVORITES}
|
||||
>
|
||||
<DraggableItem
|
||||
draggableId={navigationMenuItem.id}
|
||||
index={index}
|
||||
isInsideScrollableContainer={true}
|
||||
itemComponent={
|
||||
<StyledOrphanNavigationMenuItemsContainer>
|
||||
<NavigationDrawerItem
|
||||
secondaryLabel={getNavigationMenuItemSecondaryLabel({
|
||||
objectMetadataItems,
|
||||
navigationMenuItemObjectNameSingular:
|
||||
navigationMenuItem.objectNameSingular,
|
||||
})}
|
||||
label={navigationMenuItem.labelIdentifier}
|
||||
Icon={() => (
|
||||
<NavigationMenuItemIcon
|
||||
navigationMenuItem={navigationMenuItem}
|
||||
/>
|
||||
)}
|
||||
active={isLocationMatchingNavigationMenuItem(
|
||||
currentPath,
|
||||
currentViewPath,
|
||||
navigationMenuItem,
|
||||
)}
|
||||
to={isDragging ? undefined : navigationMenuItem.link}
|
||||
rightOptions={
|
||||
<LightIconButton
|
||||
Icon={IconHeartOff}
|
||||
onClick={() =>
|
||||
deleteNavigationMenuItem(navigationMenuItem.id)
|
||||
}
|
||||
accent="tertiary"
|
||||
/>
|
||||
}
|
||||
accent="tertiary"
|
||||
isDragging={isDragging}
|
||||
triggerEvent="CLICK"
|
||||
/>
|
||||
}
|
||||
isDragging={isDragging}
|
||||
triggerEvent="CLICK"
|
||||
/>
|
||||
</StyledOrphanNavigationMenuItemsContainer>
|
||||
}
|
||||
</StyledOrphanNavigationMenuItemsContainer>
|
||||
}
|
||||
/>
|
||||
</NavigationItemDropTarget>
|
||||
))}
|
||||
<NavigationItemDropTarget
|
||||
folderId={null}
|
||||
index={orphanNavigationMenuItems.length}
|
||||
sectionId={NavigationSections.FAVORITES}
|
||||
/>
|
||||
))
|
||||
</>
|
||||
) : (
|
||||
<StyledEmptyContainer style={{ height: isDragging ? '24px' : '1px' }} />
|
||||
)}
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import type { IconComponent } from 'twenty-ui/display';
|
||||
|
||||
import { StyledNavigationMenuItemIconContainer } from '@/navigation-menu-item/components/NavigationMenuItemIconContainer';
|
||||
|
||||
type IconWithBackgroundProps = {
|
||||
Icon: IconComponent;
|
||||
backgroundColor: string;
|
||||
size?: number | string;
|
||||
stroke?: number | string;
|
||||
};
|
||||
|
||||
export const IconWithBackground = ({
|
||||
Icon,
|
||||
backgroundColor,
|
||||
size,
|
||||
stroke,
|
||||
}: IconWithBackgroundProps) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<StyledNavigationMenuItemIconContainer $backgroundColor={backgroundColor}>
|
||||
<Icon
|
||||
size={size ?? theme.icon.size.md}
|
||||
stroke={stroke ?? theme.icon.stroke.md}
|
||||
color={theme.grayScale.gray1}
|
||||
/>
|
||||
</StyledNavigationMenuItemIconContainer>
|
||||
);
|
||||
};
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { type ReactNode, useContext } from 'react';
|
||||
|
||||
import { type NavigationSections } from '@/navigation-menu-item/constants/NavigationSections.constants';
|
||||
import { NavigationDropTargetContext } from '@/navigation-menu-item/contexts/NavigationDropTargetContext';
|
||||
|
||||
const StyledDropTarget = styled.div<{
|
||||
$isDragOver: boolean;
|
||||
$isDropForbidden: boolean;
|
||||
$compact?: boolean;
|
||||
}>`
|
||||
min-height: ${({ theme, $compact }) => ($compact ? 0 : theme.spacing(2))};
|
||||
position: relative;
|
||||
transition: all 150ms ease-in-out;
|
||||
|
||||
${({ $isDragOver, theme }) =>
|
||||
$isDragOver &&
|
||||
`
|
||||
background-color: ${theme.background.transparent.blue};
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background-color: ${theme.color.blue};
|
||||
border-radius: ${theme.border.radius.sm} ${theme.border.radius.sm} 0 0;
|
||||
}
|
||||
`}
|
||||
|
||||
${({ $isDropForbidden, theme }) =>
|
||||
$isDropForbidden &&
|
||||
`
|
||||
background-color: ${theme.background.transparent.danger};
|
||||
cursor: not-allowed;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background-color: ${theme.color.red};
|
||||
border-radius: ${theme.border.radius.sm} ${theme.border.radius.sm} 0 0;
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
type NavigationItemDropTargetProps = {
|
||||
folderId: string | null;
|
||||
index: number;
|
||||
sectionId: NavigationSections;
|
||||
children?: ReactNode;
|
||||
compact?: boolean;
|
||||
};
|
||||
|
||||
export const NavigationItemDropTarget = ({
|
||||
folderId,
|
||||
index,
|
||||
sectionId,
|
||||
children,
|
||||
compact = false,
|
||||
}: NavigationItemDropTargetProps) => {
|
||||
const { activeDropTargetId, forbiddenDropTargetId } = useContext(
|
||||
NavigationDropTargetContext,
|
||||
);
|
||||
const dropTargetId = `${sectionId}-${folderId ?? 'orphan'}-${index}`;
|
||||
const isDragOver = activeDropTargetId === dropTargetId;
|
||||
const isDropForbidden = forbiddenDropTargetId === dropTargetId;
|
||||
|
||||
return (
|
||||
<StyledDropTarget
|
||||
$isDragOver={isDragOver}
|
||||
$isDropForbidden={isDropForbidden}
|
||||
$compact={compact}
|
||||
>
|
||||
{children}
|
||||
</StyledDropTarget>
|
||||
);
|
||||
};
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useState } from 'react';
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import { IconCheck, useIcons } from 'twenty-ui/display';
|
||||
|
||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||
import { useNavigationMenuItemsDraftState } from '@/navigation-menu-item/hooks/useNavigationMenuItemsDraftState';
|
||||
import { useSaveNavigationMenuItemsDraft } from '@/navigation-menu-item/hooks/useSaveNavigationMenuItemsDraft';
|
||||
import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/states/isNavigationMenuInEditModeState';
|
||||
import { navigationMenuItemsDraftState } from '@/navigation-menu-item/states/navigationMenuItemsDraftState';
|
||||
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/states/selectedNavigationMenuItemInEditModeState';
|
||||
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { FeatureFlagKey } from '~/generated/graphql';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
align-items: center;
|
||||
background: ${({ theme }) => theme.color.blue};
|
||||
box-sizing: border-box;
|
||||
color: ${({ theme }) => theme.font.color.inverted};
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: ${({ theme }) => theme.spacing(2, 3)};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledTitle = styled.span`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
export const NavigationMenuEditModeBar = () => {
|
||||
const theme = useTheme();
|
||||
const { t } = useLingui();
|
||||
const { getIcon } = useIcons();
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const { closeCommandMenu } = useCommandMenu();
|
||||
const { enqueueErrorSnackBar } = useSnackBar();
|
||||
const setNavigationMenuItemsDraft = useSetRecoilState(
|
||||
navigationMenuItemsDraftState,
|
||||
);
|
||||
const setSelectedNavigationMenuItemInEditMode = useSetRecoilState(
|
||||
selectedNavigationMenuItemInEditModeState,
|
||||
);
|
||||
const setIsNavigationMenuInEditMode = useSetRecoilState(
|
||||
isNavigationMenuInEditModeState,
|
||||
);
|
||||
const { saveDraft } = useSaveNavigationMenuItemsDraft();
|
||||
const { isDirty } = useNavigationMenuItemsDraftState();
|
||||
|
||||
const cancelEditMode = () => {
|
||||
setNavigationMenuItemsDraft(null);
|
||||
setSelectedNavigationMenuItemInEditMode(null);
|
||||
setIsNavigationMenuInEditMode(false);
|
||||
};
|
||||
|
||||
const isNavigationMenuInEditMode = useRecoilValue(
|
||||
isNavigationMenuInEditModeState,
|
||||
);
|
||||
const isNavigationMenuItemEditingEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED,
|
||||
);
|
||||
|
||||
const showNavigationMenuEditModeBar =
|
||||
isNavigationMenuItemEditingEnabled && isNavigationMenuInEditMode;
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!isDirty) return;
|
||||
|
||||
setIsSaving(true);
|
||||
try {
|
||||
await saveDraft();
|
||||
cancelEditMode();
|
||||
closeCommandMenu();
|
||||
} catch {
|
||||
enqueueErrorSnackBar({
|
||||
message: t`Failed to save navigation layout`,
|
||||
});
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const IconPaint = getIcon('IconPaint');
|
||||
|
||||
if (!showNavigationMenuEditModeBar) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<StyledTitle>
|
||||
<IconPaint size={theme.icon.size.md} />
|
||||
{t`Layout customization`}
|
||||
</StyledTitle>
|
||||
<SaveAndCancelButtons
|
||||
onSave={handleSave}
|
||||
onCancel={cancelEditMode}
|
||||
isSaveDisabled={!isDirty || isSaving}
|
||||
isLoading={isSaving}
|
||||
inverted
|
||||
saveIcon={IconCheck}
|
||||
/>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
+7
-1
@@ -1,11 +1,14 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { Droppable } from '@hello-pangea/dnd';
|
||||
|
||||
import { useIsDropDisabledForSection } from '@/navigation-menu-item/hooks/useIsDropDisabledForSection';
|
||||
|
||||
type NavigationMenuItemDroppableProps = {
|
||||
droppableId: string;
|
||||
children: React.ReactNode;
|
||||
isDragIndicatorVisible?: boolean;
|
||||
showDropLine?: boolean;
|
||||
isWorkspaceSection?: boolean;
|
||||
};
|
||||
|
||||
const StyledDroppableWrapper = styled.div<{
|
||||
@@ -46,9 +49,12 @@ export const NavigationMenuItemDroppable = ({
|
||||
children,
|
||||
isDragIndicatorVisible = true,
|
||||
showDropLine = true,
|
||||
isWorkspaceSection = false,
|
||||
}: NavigationMenuItemDroppableProps) => {
|
||||
const isDropDisabled = useIsDropDisabledForSection(isWorkspaceSection);
|
||||
|
||||
return (
|
||||
<Droppable droppableId={droppableId}>
|
||||
<Droppable droppableId={droppableId} isDropDisabled={isDropDisabled}>
|
||||
{(provided, snapshot) => (
|
||||
<StyledDroppableWrapper
|
||||
isDraggingOver={snapshot.isDraggingOver}
|
||||
|
||||
+45
-48
@@ -4,7 +4,6 @@ import { IconHeartOff } from 'twenty-ui/display';
|
||||
import { LightIconButton } from 'twenty-ui/input';
|
||||
|
||||
import { NavigationMenuItemBackButton } from '@/navigation-menu-item/components/NavigationMenuItemBackButton';
|
||||
import { NavigationMenuItemDragProvider } from '@/navigation-menu-item/components/NavigationMenuItemDragProvider';
|
||||
import { NavigationMenuItemIcon } from '@/navigation-menu-item/components/NavigationMenuItemIcon';
|
||||
import { useDeleteNavigationMenuItem } from '@/navigation-menu-item/hooks/useDeleteNavigationMenuItem';
|
||||
import { getNavigationMenuItemSecondaryLabel } from '@/navigation-menu-item/utils/getNavigationMenuItemSecondaryLabel';
|
||||
@@ -30,53 +29,51 @@ export const NavigationMenuItemFolderContent = ({
|
||||
return (
|
||||
<>
|
||||
<NavigationMenuItemBackButton folderName={folderName} />
|
||||
<NavigationMenuItemDragProvider>
|
||||
<Droppable droppableId={`folder-${folderId}`}>
|
||||
{(provided) => (
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...provided.droppableProps}
|
||||
>
|
||||
{navigationMenuItems.map((navigationMenuItem, index) => (
|
||||
<DraggableItem
|
||||
key={navigationMenuItem.id}
|
||||
draggableId={navigationMenuItem.id}
|
||||
index={index}
|
||||
isInsideScrollableContainer
|
||||
itemComponent={
|
||||
<NavigationDrawerItem
|
||||
secondaryLabel={getNavigationMenuItemSecondaryLabel({
|
||||
objectMetadataItems,
|
||||
navigationMenuItemObjectNameSingular:
|
||||
navigationMenuItem.objectNameSingular,
|
||||
})}
|
||||
label={navigationMenuItem.labelIdentifier}
|
||||
Icon={() => (
|
||||
<NavigationMenuItemIcon
|
||||
navigationMenuItem={navigationMenuItem}
|
||||
/>
|
||||
)}
|
||||
rightOptions={
|
||||
<LightIconButton
|
||||
Icon={IconHeartOff}
|
||||
onClick={() =>
|
||||
deleteNavigationMenuItem(navigationMenuItem.id)
|
||||
}
|
||||
accent="tertiary"
|
||||
/>
|
||||
}
|
||||
triggerEvent="CLICK"
|
||||
to={navigationMenuItem.link}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
</NavigationMenuItemDragProvider>
|
||||
<Droppable droppableId={`folder-${folderId}`}>
|
||||
{(provided) => (
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...provided.droppableProps}
|
||||
>
|
||||
{navigationMenuItems.map((navigationMenuItem, index) => (
|
||||
<DraggableItem
|
||||
key={navigationMenuItem.id}
|
||||
draggableId={navigationMenuItem.id}
|
||||
index={index}
|
||||
isInsideScrollableContainer
|
||||
itemComponent={
|
||||
<NavigationDrawerItem
|
||||
secondaryLabel={getNavigationMenuItemSecondaryLabel({
|
||||
objectMetadataItems,
|
||||
navigationMenuItemObjectNameSingular:
|
||||
navigationMenuItem.objectNameSingular,
|
||||
})}
|
||||
label={navigationMenuItem.labelIdentifier}
|
||||
Icon={() => (
|
||||
<NavigationMenuItemIcon
|
||||
navigationMenuItem={navigationMenuItem}
|
||||
/>
|
||||
)}
|
||||
rightOptions={
|
||||
<LightIconButton
|
||||
Icon={IconHeartOff}
|
||||
onClick={() =>
|
||||
deleteNavigationMenuItem(navigationMenuItem.id)
|
||||
}
|
||||
accent="tertiary"
|
||||
/>
|
||||
}
|
||||
triggerEvent="CLICK"
|
||||
to={navigationMenuItem.link}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+4
-4
@@ -18,7 +18,7 @@ export const NavigationMenuItemFolders = ({
|
||||
}: NavigationMenuItemFoldersProps) => {
|
||||
const [newFolderName, setNewFolderName] = useState('');
|
||||
|
||||
const { navigationMenuItemsByFolder } = useNavigationMenuItemsByFolder();
|
||||
const { userNavigationMenuItemsByFolder } = useNavigationMenuItemsByFolder();
|
||||
const { createNewNavigationMenuItemFolder } =
|
||||
useCreateNavigationMenuItemFolder();
|
||||
|
||||
@@ -79,11 +79,11 @@ export const NavigationMenuItemFolders = ({
|
||||
/>
|
||||
</NavigationDrawerAnimatedCollapseWrapper>
|
||||
)}
|
||||
{navigationMenuItemsByFolder.map((folder) => (
|
||||
{userNavigationMenuItemsByFolder.map((folder) => (
|
||||
<CurrentWorkspaceMemberNavigationMenuItems
|
||||
key={folder.folderId}
|
||||
key={folder.id}
|
||||
folder={folder}
|
||||
isGroup={navigationMenuItemsByFolder.length > 1}
|
||||
isGroup={userNavigationMenuItemsByFolder.length > 1}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
|
||||
+70
-3
@@ -1,8 +1,16 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Avatar, useIcons } from 'twenty-ui/display';
|
||||
|
||||
import { StyledNavigationMenuItemIconContainer } from '@/navigation-menu-item/components/NavigationMenuItemIconContainer';
|
||||
import { ObjectIconWithViewOverlay } from '@/navigation-menu-item/components/ObjectIconWithViewOverlay';
|
||||
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
|
||||
import { getNavigationMenuItemIconColors } from '@/navigation-menu-item/utils/getNavigationMenuItemIconColors';
|
||||
import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
|
||||
import { useGetStandardObjectIcon } from '@/object-metadata/hooks/useGetStandardObjectIcon';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { ViewKey } from '@/views/types/ViewKey';
|
||||
|
||||
export const NavigationMenuItemIcon = ({
|
||||
navigationMenuItem,
|
||||
@@ -11,19 +19,66 @@ export const NavigationMenuItemIcon = ({
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const { getIcon } = useIcons();
|
||||
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
|
||||
const { Icon: StandardIcon, IconColor } = useGetStandardObjectIcon(
|
||||
navigationMenuItem.objectNameSingular || '',
|
||||
);
|
||||
|
||||
const isRecord =
|
||||
navigationMenuItem.itemType === NavigationMenuItemType.RECORD;
|
||||
const isLink = navigationMenuItem.itemType === NavigationMenuItemType.LINK;
|
||||
const isObjectIndexView =
|
||||
navigationMenuItem.itemType === NavigationMenuItemType.VIEW &&
|
||||
navigationMenuItem.viewKey === ViewKey.Index;
|
||||
const isViewWithOverlay =
|
||||
navigationMenuItem.itemType === NavigationMenuItemType.VIEW &&
|
||||
!isObjectIndexView;
|
||||
|
||||
const objectMetadataItem = objectMetadataItems.find(
|
||||
(item) => item.nameSingular === navigationMenuItem.objectNameSingular,
|
||||
);
|
||||
const ObjectIconForView =
|
||||
objectMetadataItem?.icon != null
|
||||
? getIcon(objectMetadataItem.icon)
|
||||
: StandardIcon;
|
||||
|
||||
if (
|
||||
isViewWithOverlay &&
|
||||
isDefined(ObjectIconForView) &&
|
||||
isDefined(navigationMenuItem.Icon)
|
||||
) {
|
||||
const ViewIcon = getIcon(navigationMenuItem.Icon);
|
||||
return (
|
||||
<ObjectIconWithViewOverlay
|
||||
ObjectIcon={ObjectIconForView}
|
||||
ViewIcon={ViewIcon}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const IconToUse =
|
||||
StandardIcon ||
|
||||
(navigationMenuItem.Icon ? getIcon(navigationMenuItem.Icon) : undefined);
|
||||
const iconColorToUse = StandardIcon ? IconColor : theme.font.color.secondary;
|
||||
|
||||
const placeholderColorSeed = navigationMenuItem.targetRecordId ?? undefined;
|
||||
const iconColors = getNavigationMenuItemIconColors(theme);
|
||||
const iconBackgroundColor = isRecord
|
||||
? undefined
|
||||
: isLink
|
||||
? iconColors.link
|
||||
: isViewWithOverlay
|
||||
? iconColors.view
|
||||
: iconColors.object;
|
||||
|
||||
return (
|
||||
const iconColorToUse = iconBackgroundColor
|
||||
? theme.grayScale.gray1
|
||||
: StandardIcon
|
||||
? IconColor
|
||||
: theme.font.color.secondary;
|
||||
|
||||
const avatar = (
|
||||
<Avatar
|
||||
size="md"
|
||||
size={iconBackgroundColor ? 'sm' : 'md'}
|
||||
type={navigationMenuItem.avatarType}
|
||||
Icon={IconToUse}
|
||||
iconColor={iconColorToUse}
|
||||
@@ -32,4 +87,16 @@ export const NavigationMenuItemIcon = ({
|
||||
placeholderColorSeed={placeholderColorSeed}
|
||||
/>
|
||||
);
|
||||
|
||||
if (!iconBackgroundColor) {
|
||||
return avatar;
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledNavigationMenuItemIconContainer
|
||||
$backgroundColor={iconBackgroundColor}
|
||||
>
|
||||
{avatar}
|
||||
</StyledNavigationMenuItemIconContainer>
|
||||
);
|
||||
};
|
||||
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
export const StyledNavigationMenuItemIconContainer = styled.div<{
|
||||
$backgroundColor?: string;
|
||||
}>`
|
||||
align-items: center;
|
||||
border-radius: ${({ theme }) => theme.border.radius.xs};
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
height: ${({ theme }) => theme.spacing(4.5)};
|
||||
justify-content: center;
|
||||
width: ${({ theme }) => theme.spacing(4.5)};
|
||||
|
||||
${({ $backgroundColor }) =>
|
||||
$backgroundColor ? `background-color: ${$backgroundColor};` : ''}
|
||||
`;
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import type { IconComponent } from 'twenty-ui/display';
|
||||
|
||||
import { getNavigationMenuItemIconColors } from '@/navigation-menu-item/utils/getNavigationMenuItemIconColors';
|
||||
|
||||
const StyledCompositeContainer = styled.div`
|
||||
align-items: center;
|
||||
border-radius: ${({ theme }) => theme.border.radius.xs};
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
height: ${({ theme }) => theme.spacing(4.5)};
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
width: ${({ theme }) => theme.spacing(4.5)};
|
||||
`;
|
||||
|
||||
const StyledObjectIconWrapper = styled.div<{ $backgroundColor: string }>`
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: ${({ theme }) => theme.border.radius.xs};
|
||||
background-color: ${({ $backgroundColor }) => $backgroundColor};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const StyledViewOverlay = styled.div<{ $backgroundColor: string }>`
|
||||
align-items: center;
|
||||
background-color: ${({ $backgroundColor }) => $backgroundColor};
|
||||
border: 1px solid ${({ theme }) => theme.background.primary};
|
||||
border-radius: ${({ theme }) => theme.border.radius.xs};
|
||||
bottom: -7px;
|
||||
display: flex;
|
||||
height: ${({ theme }) => theme.spacing(3.5)};
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
right: -7px;
|
||||
width: ${({ theme }) => theme.spacing(3.5)};
|
||||
`;
|
||||
|
||||
export type ObjectIconWithViewOverlayProps = {
|
||||
ObjectIcon: IconComponent;
|
||||
ViewIcon: IconComponent;
|
||||
};
|
||||
|
||||
export const ObjectIconWithViewOverlay = ({
|
||||
ObjectIcon,
|
||||
ViewIcon,
|
||||
}: ObjectIconWithViewOverlayProps) => {
|
||||
const theme = useTheme();
|
||||
const iconColors = getNavigationMenuItemIconColors(theme);
|
||||
|
||||
return (
|
||||
<StyledCompositeContainer>
|
||||
<StyledObjectIconWrapper $backgroundColor={iconColors.object}>
|
||||
<ObjectIcon
|
||||
size={theme.spacing(3.5)}
|
||||
stroke={theme.icon.stroke.md}
|
||||
color={theme.grayScale.gray1}
|
||||
/>
|
||||
</StyledObjectIconWrapper>
|
||||
<StyledViewOverlay $backgroundColor={iconColors.view}>
|
||||
<ViewIcon
|
||||
size={theme.spacing(2)}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
color={theme.grayScale.gray1}
|
||||
/>
|
||||
</StyledViewOverlay>
|
||||
</StyledCompositeContainer>
|
||||
);
|
||||
};
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import {
|
||||
type DraggableProvided,
|
||||
type DraggableRubric,
|
||||
type DraggableStateSnapshot,
|
||||
} from '@hello-pangea/dnd';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { NavigationMenuItemIcon } from '@/navigation-menu-item/components/NavigationMenuItemIcon';
|
||||
import { getNavigationMenuItemSecondaryLabel } from '@/navigation-menu-item/utils/getNavigationMenuItemSecondaryLabel';
|
||||
import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { NavigationDrawerSubItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSubItem';
|
||||
import { getNavigationSubItemLeftAdornment } from '@/ui/navigation/navigation-drawer/utils/getNavigationSubItemLeftAdornment';
|
||||
import { ViewKey } from '@/views/types/ViewKey';
|
||||
|
||||
type WorkspaceNavigationMenuItemFolderDragCloneProps = {
|
||||
draggableProvided: DraggableProvided;
|
||||
draggableSnapshot: DraggableStateSnapshot;
|
||||
rubric: DraggableRubric;
|
||||
navigationMenuItems: ProcessedNavigationMenuItem[];
|
||||
navigationMenuItemFolderContentLength: number;
|
||||
selectedNavigationMenuItemIndex: number;
|
||||
};
|
||||
|
||||
export const WorkspaceNavigationMenuItemFolderDragClone = ({
|
||||
draggableProvided,
|
||||
draggableSnapshot,
|
||||
rubric,
|
||||
navigationMenuItems,
|
||||
navigationMenuItemFolderContentLength,
|
||||
selectedNavigationMenuItemIndex,
|
||||
}: WorkspaceNavigationMenuItemFolderDragCloneProps) => {
|
||||
const theme = useTheme();
|
||||
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
|
||||
const navigationMenuItem = navigationMenuItems[rubric.source.index];
|
||||
|
||||
if (!isDefined(navigationMenuItem)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={draggableProvided.innerRef}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...draggableProvided.draggableProps}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...draggableProvided.dragHandleProps}
|
||||
style={{
|
||||
...draggableProvided.draggableProps.style,
|
||||
background: draggableSnapshot.isDragging
|
||||
? theme.background.transparent.light
|
||||
: undefined,
|
||||
}}
|
||||
>
|
||||
<NavigationDrawerSubItem
|
||||
secondaryLabel={
|
||||
navigationMenuItem.viewKey === ViewKey.Index
|
||||
? undefined
|
||||
: getNavigationMenuItemSecondaryLabel({
|
||||
objectMetadataItems,
|
||||
navigationMenuItemObjectNameSingular:
|
||||
navigationMenuItem.objectNameSingular,
|
||||
})
|
||||
}
|
||||
label={navigationMenuItem.labelIdentifier}
|
||||
Icon={() => (
|
||||
<NavigationMenuItemIcon navigationMenuItem={navigationMenuItem} />
|
||||
)}
|
||||
to={undefined}
|
||||
active={false}
|
||||
isDragging={true}
|
||||
subItemState={getNavigationSubItemLeftAdornment({
|
||||
index: rubric.source.index,
|
||||
arrayLength: navigationMenuItemFolderContentLength,
|
||||
selectedIndex: selectedNavigationMenuItemIndex,
|
||||
})}
|
||||
triggerEvent="CLICK"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+164
-7
@@ -1,26 +1,183 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import {
|
||||
useRecoilCallback,
|
||||
useRecoilState,
|
||||
useRecoilValue,
|
||||
useSetRecoilState,
|
||||
} from 'recoil';
|
||||
import { IconLink, IconPlus, IconTool, useIcons } from 'twenty-ui/display';
|
||||
import { LightIconButton } from 'twenty-ui/input';
|
||||
import { FeatureFlagKey } from '~/generated-metadata/graphql';
|
||||
|
||||
import { useWorkspaceNavigationMenuItems } from '@/navigation-menu-item/hooks/useWorkspaceNavigationMenuItems';
|
||||
import { NavigationDrawerSectionForObjectMetadataItems } from '@/object-metadata/components/NavigationDrawerSectionForObjectMetadataItems';
|
||||
import { useNavigateCommandMenu } from '@/command-menu/hooks/useNavigateCommandMenu';
|
||||
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
|
||||
import { useOpenNavigationMenuItemInCommandMenu } from '@/navigation-menu-item/hooks/useOpenNavigationMenuItemInCommandMenu';
|
||||
import {
|
||||
type NavigationMenuItemClickParams,
|
||||
useWorkspaceSectionItems,
|
||||
} from '@/navigation-menu-item/hooks/useWorkspaceSectionItems';
|
||||
import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/states/isNavigationMenuInEditModeState';
|
||||
import { FOLDER_ICON_DEFAULT } from '@/navigation-menu-item/constants/folderIconDefault';
|
||||
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
|
||||
import { navigationMenuItemsDraftState } from '@/navigation-menu-item/states/navigationMenuItemsDraftState';
|
||||
import { filterWorkspaceNavigationMenuItems } from '@/navigation-menu-item/utils/filterWorkspaceNavigationMenuItems';
|
||||
import { openNavigationMenuItemFolderIdsState } from '@/navigation-menu-item/states/openNavigationMenuItemFolderIdsState';
|
||||
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/states/selectedNavigationMenuItemInEditModeState';
|
||||
import { NavigationDrawerSectionForObjectMetadataItemsSkeletonLoader } from '@/object-metadata/components/NavigationDrawerSectionForObjectMetadataItemsSkeletonLoader';
|
||||
import { NavigationDrawerSectionForWorkspaceItems } from '@/object-metadata/components/NavigationDrawerSectionForWorkspaceItems';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { useIsPrefetchLoading } from '@/prefetch/hooks/useIsPrefetchLoading';
|
||||
import { prefetchNavigationMenuItemsState } from '@/prefetch/states/prefetchNavigationMenuItemsState';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
const StyledRightIconsContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
export const WorkspaceNavigationMenuItems = () => {
|
||||
const { workspaceNavigationMenuItemsObjectMetadataItems } =
|
||||
useWorkspaceNavigationMenuItems();
|
||||
const items = useWorkspaceSectionItems();
|
||||
const enterEditMode = useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
() => {
|
||||
const prefetchNavigationMenuItems = snapshot
|
||||
.getLoadable(prefetchNavigationMenuItemsState)
|
||||
.getValue();
|
||||
const workspaceNavigationMenuItems = filterWorkspaceNavigationMenuItems(
|
||||
prefetchNavigationMenuItems,
|
||||
);
|
||||
set(navigationMenuItemsDraftState, workspaceNavigationMenuItems);
|
||||
set(isNavigationMenuInEditModeState, true);
|
||||
},
|
||||
[],
|
||||
);
|
||||
const isNavigationMenuItemEditingEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED,
|
||||
);
|
||||
const isNavigationMenuInEditMode = useRecoilValue(
|
||||
isNavigationMenuInEditModeState,
|
||||
);
|
||||
const [
|
||||
selectedNavigationMenuItemInEditMode,
|
||||
setSelectedNavigationMenuItemInEditMode,
|
||||
] = useRecoilState(selectedNavigationMenuItemInEditModeState);
|
||||
const setOpenNavigationMenuItemFolderIds = useSetRecoilState(
|
||||
openNavigationMenuItemFolderIdsState,
|
||||
);
|
||||
const { navigateCommandMenu } = useNavigateCommandMenu();
|
||||
const { openNavigationMenuItemInCommandMenu } =
|
||||
useOpenNavigationMenuItemInCommandMenu();
|
||||
const { getIcon } = useIcons();
|
||||
|
||||
const loading = useIsPrefetchLoading();
|
||||
const { t } = useLingui();
|
||||
|
||||
const handleEditClick = (event: React.MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
enterEditMode();
|
||||
};
|
||||
|
||||
const handleNavigationMenuItemClick = (
|
||||
params: NavigationMenuItemClickParams,
|
||||
) => {
|
||||
const { item, objectMetadataItem } = params;
|
||||
const id = item.id;
|
||||
setSelectedNavigationMenuItemInEditMode(id);
|
||||
if (item.itemType === NavigationMenuItemType.FOLDER) {
|
||||
setOpenNavigationMenuItemFolderIds((currentOpenFolders) =>
|
||||
currentOpenFolders.includes(id)
|
||||
? currentOpenFolders
|
||||
: [...currentOpenFolders, id],
|
||||
);
|
||||
openNavigationMenuItemInCommandMenu({
|
||||
pageTitle: t`Edit folder`,
|
||||
pageIcon: getIcon(item.icon ?? item.Icon ?? FOLDER_ICON_DEFAULT),
|
||||
});
|
||||
} else if (item.itemType === NavigationMenuItemType.LINK) {
|
||||
openNavigationMenuItemInCommandMenu({
|
||||
pageTitle: t`Edit link`,
|
||||
pageIcon: IconLink,
|
||||
});
|
||||
} else if (isDefined(objectMetadataItem)) {
|
||||
openNavigationMenuItemInCommandMenu({
|
||||
pageTitle: objectMetadataItem.labelPlural,
|
||||
pageIcon: getIcon(objectMetadataItem.icon),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleActiveObjectMetadataItemClick = (
|
||||
objectMetadataItem: ObjectMetadataItem,
|
||||
navigationMenuItemId: string,
|
||||
) => {
|
||||
enterEditMode();
|
||||
setSelectedNavigationMenuItemInEditMode(navigationMenuItemId);
|
||||
openNavigationMenuItemInCommandMenu({
|
||||
pageTitle: objectMetadataItem.labelPlural,
|
||||
pageIcon: getIcon(objectMetadataItem.icon),
|
||||
});
|
||||
};
|
||||
|
||||
const handleAddMenuItem = (event?: React.MouseEvent) => {
|
||||
event?.stopPropagation();
|
||||
navigateCommandMenu({
|
||||
page: CommandMenuPages.NavigationMenuAddItem,
|
||||
pageTitle: t`New sidebar item`,
|
||||
pageIcon: IconPlus,
|
||||
resetNavigationStack: true,
|
||||
});
|
||||
};
|
||||
|
||||
const isEditMode =
|
||||
isNavigationMenuItemEditingEnabled && isNavigationMenuInEditMode;
|
||||
|
||||
if (loading) {
|
||||
return <NavigationDrawerSectionForObjectMetadataItemsSkeletonLoader />;
|
||||
}
|
||||
|
||||
return (
|
||||
<NavigationDrawerSectionForObjectMetadataItems
|
||||
<NavigationDrawerSectionForWorkspaceItems
|
||||
sectionTitle={t`Workspace`}
|
||||
objectMetadataItems={workspaceNavigationMenuItemsObjectMetadataItems}
|
||||
isRemote={false}
|
||||
items={items}
|
||||
rightIcon={
|
||||
isNavigationMenuItemEditingEnabled ? (
|
||||
<StyledRightIconsContainer>
|
||||
{isEditMode ? (
|
||||
<LightIconButton
|
||||
Icon={IconPlus}
|
||||
accent="tertiary"
|
||||
size="small"
|
||||
onClick={handleAddMenuItem}
|
||||
/>
|
||||
) : (
|
||||
<LightIconButton
|
||||
Icon={IconTool}
|
||||
accent="tertiary"
|
||||
size="small"
|
||||
onClick={handleEditClick}
|
||||
/>
|
||||
)}
|
||||
</StyledRightIconsContainer>
|
||||
) : undefined
|
||||
}
|
||||
onAddMenuItem={
|
||||
isNavigationMenuItemEditingEnabled && isEditMode
|
||||
? handleAddMenuItem
|
||||
: undefined
|
||||
}
|
||||
isEditMode={isEditMode}
|
||||
selectedNavigationMenuItemId={selectedNavigationMenuItemInEditMode}
|
||||
onNavigationMenuItemClick={
|
||||
isEditMode ? handleNavigationMenuItemClick : undefined
|
||||
}
|
||||
onActiveObjectMetadataItemClick={
|
||||
isNavigationMenuItemEditingEnabled
|
||||
? handleActiveObjectMetadataItemClick
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
+299
@@ -0,0 +1,299 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { Droppable } from '@hello-pangea/dnd';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { useContext } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { useIsDropDisabledForSection } from '@/navigation-menu-item/hooks/useIsDropDisabledForSection';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import { IconChevronDown, IconChevronRight, useIcons } from 'twenty-ui/display';
|
||||
import { AnimatedExpandableContainer } from 'twenty-ui/layout';
|
||||
import { useIsMobile } from 'twenty-ui/utilities';
|
||||
|
||||
import { NavigationMenuItemDroppable } from '@/navigation-menu-item/components/NavigationMenuItemDroppable';
|
||||
import { NavigationMenuItemIcon } from '@/navigation-menu-item/components/NavigationMenuItemIcon';
|
||||
import { WorkspaceNavigationMenuItemFolderDragClone } from '@/navigation-menu-item/components/WorkspaceNavigationMenuItemFolderDragClone';
|
||||
import { FOLDER_ICON_DEFAULT } from '@/navigation-menu-item/constants/folderIconDefault';
|
||||
import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/constants/NavigationMenuItemDroppableIds';
|
||||
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
|
||||
import { NavigationMenuItemDragContext } from '@/navigation-menu-item/contexts/NavigationMenuItemDragContext';
|
||||
import { type NavigationMenuItemClickParams } from '@/navigation-menu-item/hooks/useWorkspaceSectionItems';
|
||||
import { openNavigationMenuItemFolderIdsState } from '@/navigation-menu-item/states/openNavigationMenuItemFolderIdsState';
|
||||
import { getNavigationMenuItemIconColors } from '@/navigation-menu-item/utils/getNavigationMenuItemIconColors';
|
||||
import { getNavigationMenuItemSecondaryLabel } from '@/navigation-menu-item/utils/getNavigationMenuItemSecondaryLabel';
|
||||
import { getObjectMetadataForNavigationMenuItem } from '@/navigation-menu-item/utils/getObjectMetadataForNavigationMenuItem';
|
||||
import { isLocationMatchingNavigationMenuItem } from '@/navigation-menu-item/utils/isLocationMatchingNavigationMenuItem';
|
||||
import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem';
|
||||
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
|
||||
import { NavigationDrawerItemsCollapsableContainer } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItemsCollapsableContainer';
|
||||
import { NavigationDrawerSubItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSubItem';
|
||||
import { currentNavigationMenuItemFolderIdState } from '@/ui/navigation/navigation-drawer/states/currentNavigationMenuItemFolderIdState';
|
||||
import { getNavigationSubItemLeftAdornment } from '@/ui/navigation/navigation-drawer/utils/getNavigationSubItemLeftAdornment';
|
||||
import { coreViewsState } from '@/views/states/coreViewState';
|
||||
import { ViewKey } from '@/views/types/ViewKey';
|
||||
import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView';
|
||||
|
||||
const StyledFolderContainer = styled.div<{ $isSelectedInEditMode: boolean }>`
|
||||
border: ${({ theme, $isSelectedInEditMode }) =>
|
||||
$isSelectedInEditMode
|
||||
? `1px solid ${theme.color.blue}`
|
||||
: '1px solid transparent'};
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
`;
|
||||
|
||||
const StyledFolderDroppableContent = styled.div<{
|
||||
$compact: boolean;
|
||||
}>`
|
||||
padding-bottom: ${({ theme, $compact }) => ($compact ? 0 : theme.spacing(2))};
|
||||
`;
|
||||
|
||||
const StyledFolderExpandableWrapper = styled.div`
|
||||
& > div {
|
||||
overflow: visible !important;
|
||||
}
|
||||
`;
|
||||
|
||||
type WorkspaceNavigationMenuItemsFolderProps = {
|
||||
folderId: string;
|
||||
folderName: string;
|
||||
folderIconKey?: string | null;
|
||||
navigationMenuItems: ProcessedNavigationMenuItem[];
|
||||
isGroup: boolean;
|
||||
isEditMode?: boolean;
|
||||
isSelectedInEditMode?: boolean;
|
||||
onEditModeClick?: () => void;
|
||||
onNavigationMenuItemClick?: (params: NavigationMenuItemClickParams) => void;
|
||||
selectedNavigationMenuItemId?: string | null;
|
||||
isDragging?: boolean;
|
||||
};
|
||||
|
||||
export const WorkspaceNavigationMenuItemsFolder = ({
|
||||
folderId,
|
||||
folderName,
|
||||
folderIconKey,
|
||||
navigationMenuItems,
|
||||
isGroup,
|
||||
isEditMode = false,
|
||||
isSelectedInEditMode = false,
|
||||
onEditModeClick,
|
||||
onNavigationMenuItemClick,
|
||||
selectedNavigationMenuItemId = null,
|
||||
isDragging = false,
|
||||
}: WorkspaceNavigationMenuItemsFolderProps) => {
|
||||
const theme = useTheme();
|
||||
const { getIcon } = useIcons();
|
||||
const iconColors = getNavigationMenuItemIconColors(theme);
|
||||
const FolderIcon = getIcon(folderIconKey ?? FOLDER_ICON_DEFAULT);
|
||||
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
|
||||
const coreViews = useRecoilValue(coreViewsState);
|
||||
const views = coreViews.map(convertCoreViewToView);
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const currentPath = location.pathname;
|
||||
const currentViewPath = location.pathname + location.search;
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const [openNavigationMenuItemFolderIds, setOpenNavigationMenuItemFolderIds] =
|
||||
useRecoilState(openNavigationMenuItemFolderIdsState);
|
||||
|
||||
const setCurrentFolderId = useSetRecoilState(
|
||||
currentNavigationMenuItemFolderIdState,
|
||||
);
|
||||
|
||||
const isOpen = openNavigationMenuItemFolderIds.includes(folderId);
|
||||
|
||||
const handleToggle = () => {
|
||||
if (isMobile) {
|
||||
setCurrentFolderId((prev) => (prev === folderId ? null : folderId));
|
||||
} else {
|
||||
setOpenNavigationMenuItemFolderIds((current) =>
|
||||
isOpen
|
||||
? current.filter((id) => id !== folderId)
|
||||
: [...current, folderId],
|
||||
);
|
||||
}
|
||||
|
||||
if (!isOpen) {
|
||||
const firstNonLinkItem = navigationMenuItems.find(
|
||||
(item) =>
|
||||
item.itemType !== NavigationMenuItemType.LINK &&
|
||||
isNonEmptyString(item.link),
|
||||
);
|
||||
if (isDefined(firstNonLinkItem?.link)) {
|
||||
navigate(firstNonLinkItem.link);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const shouldUseEditModeClick = isEditMode && isDefined(onEditModeClick);
|
||||
const handleClick = shouldUseEditModeClick ? onEditModeClick : handleToggle;
|
||||
|
||||
const selectedNavigationMenuItemIndex = navigationMenuItems.findIndex(
|
||||
(item) =>
|
||||
isLocationMatchingNavigationMenuItem(currentPath, currentViewPath, item),
|
||||
);
|
||||
|
||||
const navigationMenuItemFolderContentLength = navigationMenuItems.length;
|
||||
const { isDragging: isContextDragging } = useContext(
|
||||
NavigationMenuItemDragContext,
|
||||
);
|
||||
const folderContentDropDisabled = useIsDropDisabledForSection(true);
|
||||
|
||||
return (
|
||||
<StyledFolderContainer $isSelectedInEditMode={isSelectedInEditMode}>
|
||||
<NavigationDrawerItemsCollapsableContainer isGroup={isGroup}>
|
||||
<NavigationMenuItemDroppable
|
||||
droppableId={`${NavigationMenuItemDroppableIds.WORKSPACE_FOLDER_HEADER_PREFIX}${folderId}`}
|
||||
isWorkspaceSection={true}
|
||||
>
|
||||
<NavigationDrawerItem
|
||||
label={folderName}
|
||||
Icon={FolderIcon}
|
||||
iconBackgroundColor={iconColors.folder}
|
||||
onClick={handleClick}
|
||||
className="navigation-drawer-item"
|
||||
triggerEvent="CLICK"
|
||||
preventCollapseOnMobile={isMobile}
|
||||
isDragging={isDragging}
|
||||
rightOptions={
|
||||
isOpen ? (
|
||||
<IconChevronDown
|
||||
size={theme.icon.size.sm}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
color={theme.font.color.tertiary}
|
||||
/>
|
||||
) : (
|
||||
<IconChevronRight
|
||||
size={theme.icon.size.sm}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
color={theme.font.color.tertiary}
|
||||
/>
|
||||
)
|
||||
}
|
||||
alwaysShowRightOptions
|
||||
/>
|
||||
</NavigationMenuItemDroppable>
|
||||
|
||||
<StyledFolderExpandableWrapper>
|
||||
<AnimatedExpandableContainer
|
||||
isExpanded={isOpen}
|
||||
dimension="height"
|
||||
mode="fit-content"
|
||||
containAnimation
|
||||
>
|
||||
<Droppable
|
||||
droppableId={`${NavigationMenuItemDroppableIds.WORKSPACE_FOLDER_PREFIX}${folderId}`}
|
||||
isDropDisabled={folderContentDropDisabled}
|
||||
ignoreContainerClipping
|
||||
renderClone={(provided, snapshot, rubric) => (
|
||||
<WorkspaceNavigationMenuItemFolderDragClone
|
||||
draggableProvided={provided}
|
||||
draggableSnapshot={snapshot}
|
||||
rubric={rubric}
|
||||
navigationMenuItems={navigationMenuItems}
|
||||
navigationMenuItemFolderContentLength={
|
||||
navigationMenuItemFolderContentLength
|
||||
}
|
||||
selectedNavigationMenuItemIndex={
|
||||
selectedNavigationMenuItemIndex
|
||||
}
|
||||
/>
|
||||
)}
|
||||
getContainerForClone={() => document.body}
|
||||
>
|
||||
{(provided) => (
|
||||
<StyledFolderDroppableContent
|
||||
ref={provided.innerRef}
|
||||
$compact={isEditMode || navigationMenuItems.length === 0}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...provided.droppableProps}
|
||||
>
|
||||
{navigationMenuItems.map((navigationMenuItem, index) => {
|
||||
const objectMetadataItem =
|
||||
navigationMenuItem.itemType ===
|
||||
NavigationMenuItemType.VIEW ||
|
||||
navigationMenuItem.itemType ===
|
||||
NavigationMenuItemType.RECORD
|
||||
? getObjectMetadataForNavigationMenuItem(
|
||||
navigationMenuItem,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
)
|
||||
: null;
|
||||
const handleEditModeClick =
|
||||
isEditMode &&
|
||||
isDefined(onNavigationMenuItemClick) &&
|
||||
(navigationMenuItem.itemType ===
|
||||
NavigationMenuItemType.LINK ||
|
||||
isDefined(objectMetadataItem))
|
||||
? () =>
|
||||
onNavigationMenuItemClick({
|
||||
item: navigationMenuItem,
|
||||
objectMetadataItem:
|
||||
objectMetadataItem ?? undefined,
|
||||
})
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<DraggableItem
|
||||
key={navigationMenuItem.id}
|
||||
draggableId={navigationMenuItem.id}
|
||||
index={index}
|
||||
isInsideScrollableContainer
|
||||
isDragDisabled={!isEditMode}
|
||||
disableInteractiveElementBlocking={isEditMode}
|
||||
itemComponent={
|
||||
<NavigationDrawerSubItem
|
||||
secondaryLabel={
|
||||
navigationMenuItem.viewKey === ViewKey.Index
|
||||
? undefined
|
||||
: getNavigationMenuItemSecondaryLabel({
|
||||
objectMetadataItems,
|
||||
navigationMenuItemObjectNameSingular:
|
||||
navigationMenuItem.objectNameSingular,
|
||||
})
|
||||
}
|
||||
label={navigationMenuItem.labelIdentifier}
|
||||
Icon={() => (
|
||||
<NavigationMenuItemIcon
|
||||
navigationMenuItem={navigationMenuItem}
|
||||
/>
|
||||
)}
|
||||
to={
|
||||
isContextDragging || handleEditModeClick
|
||||
? undefined
|
||||
: navigationMenuItem.link
|
||||
}
|
||||
onClick={handleEditModeClick}
|
||||
active={index === selectedNavigationMenuItemIndex}
|
||||
isSelectedInEditMode={
|
||||
selectedNavigationMenuItemId ===
|
||||
navigationMenuItem.id
|
||||
}
|
||||
subItemState={getNavigationSubItemLeftAdornment({
|
||||
index,
|
||||
arrayLength:
|
||||
navigationMenuItemFolderContentLength,
|
||||
selectedIndex: selectedNavigationMenuItemIndex,
|
||||
})}
|
||||
isDragging={isContextDragging}
|
||||
triggerEvent="CLICK"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{provided.placeholder}
|
||||
</StyledFolderDroppableContent>
|
||||
)}
|
||||
</Droppable>
|
||||
</AnimatedExpandableContainer>
|
||||
</StyledFolderExpandableWrapper>
|
||||
</NavigationDrawerItemsCollapsableContainer>
|
||||
</StyledFolderContainer>
|
||||
);
|
||||
};
|
||||
+1
@@ -0,0 +1 @@
|
||||
export const ADD_TO_NAV_SOURCE_DROPPABLE_ID = 'add-to-nav-source';
|
||||
+6
-2
@@ -1,2 +1,6 @@
|
||||
export const ORPHAN_NAVIGATION_MENU_ITEMS_DROPPABLE_ID =
|
||||
'orphan-navigation-menu-items';
|
||||
export enum NavigationMenuItemDroppableIds {
|
||||
ORPHAN_NAVIGATION_MENU_ITEMS = 'orphan-navigation-menu-items',
|
||||
WORKSPACE_ORPHAN_NAVIGATION_MENU_ITEMS = 'workspace-orphan-navigation-menu-items',
|
||||
WORKSPACE_FOLDER_PREFIX = 'workspace-folder-',
|
||||
WORKSPACE_FOLDER_HEADER_PREFIX = 'workspace-folder-header-',
|
||||
}
|
||||
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
export enum NavigationMenuItemType {
|
||||
FOLDER = 'folder',
|
||||
LINK = 'link',
|
||||
OBJECT = 'object',
|
||||
RECORD = 'record',
|
||||
VIEW = 'view',
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
export enum NavigationSections {
|
||||
WORKSPACE = 'workspace',
|
||||
FAVORITES = 'favorites',
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export const FOLDER_ICON_DEFAULT = 'IconFolder';
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
type NavigationDragSourceContextType = {
|
||||
sourceDroppableId: string | null;
|
||||
};
|
||||
|
||||
export const NavigationDragSourceContext =
|
||||
createContext<NavigationDragSourceContextType>({
|
||||
sourceDroppableId: null,
|
||||
});
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
type NavigationDropTargetContextType = {
|
||||
activeDropTargetId: string | null;
|
||||
setActiveDropTargetId: (id: string | null) => void;
|
||||
forbiddenDropTargetId: string | null;
|
||||
setForbiddenDropTargetId: (id: string | null) => void;
|
||||
};
|
||||
|
||||
export const NavigationDropTargetContext =
|
||||
createContext<NavigationDropTargetContextType>({
|
||||
activeDropTargetId: null,
|
||||
setActiveDropTargetId: () => {},
|
||||
forbiddenDropTargetId: null,
|
||||
setForbiddenDropTargetId: () => {},
|
||||
});
|
||||
+2
@@ -9,6 +9,8 @@ export const NAVIGATION_MENU_ITEM_FRAGMENT = gql`
|
||||
viewId
|
||||
folderId
|
||||
name
|
||||
link
|
||||
icon
|
||||
position
|
||||
applicationId
|
||||
createdAt
|
||||
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { v4 } from 'uuid';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import type { NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
import { navigationMenuItemsDraftState } from '@/navigation-menu-item/states/navigationMenuItemsDraftState';
|
||||
import { computeInsertIndexAndPosition } from '@/navigation-menu-item/utils/computeInsertIndexAndPosition';
|
||||
|
||||
export const useAddFolderToNavigationMenuDraft = () => {
|
||||
const setNavigationMenuItemsDraft = useSetRecoilState(
|
||||
navigationMenuItemsDraftState,
|
||||
);
|
||||
|
||||
const addFolderToDraft = (
|
||||
name: string,
|
||||
currentDraft: NavigationMenuItem[],
|
||||
targetFolderId?: string | null,
|
||||
targetIndex?: number,
|
||||
): string => {
|
||||
const folderId = targetFolderId ?? null;
|
||||
|
||||
const itemsInFolder = currentDraft.filter(
|
||||
(item) =>
|
||||
(item.folderId ?? null) === folderId &&
|
||||
!isDefined(item.userWorkspaceId),
|
||||
);
|
||||
const index = targetIndex ?? itemsInFolder.length;
|
||||
|
||||
const { flatIndex, position } = computeInsertIndexAndPosition(
|
||||
currentDraft,
|
||||
folderId,
|
||||
index,
|
||||
);
|
||||
|
||||
const newItemId = v4();
|
||||
const newItem: NavigationMenuItem = {
|
||||
__typename: 'NavigationMenuItem',
|
||||
id: newItemId,
|
||||
viewId: undefined,
|
||||
targetObjectMetadataId: undefined,
|
||||
targetRecordId: undefined,
|
||||
folderId: folderId ?? undefined,
|
||||
position,
|
||||
userWorkspaceId: undefined,
|
||||
name: name.trim(),
|
||||
applicationId: undefined,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const newDraft = [
|
||||
...currentDraft.slice(0, flatIndex),
|
||||
newItem,
|
||||
...currentDraft.slice(flatIndex),
|
||||
];
|
||||
setNavigationMenuItemsDraft(newDraft);
|
||||
return newItemId;
|
||||
};
|
||||
|
||||
return { addFolderToDraft };
|
||||
};
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { v4 } from 'uuid';
|
||||
import type { NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
import { navigationMenuItemsDraftState } from '@/navigation-menu-item/states/navigationMenuItemsDraftState';
|
||||
import { computeInsertIndexAndPosition } from '@/navigation-menu-item/utils/computeInsertIndexAndPosition';
|
||||
import { normalizeUrl } from '@/navigation-menu-item/utils/normalizeUrl';
|
||||
|
||||
export const useAddLinkToNavigationMenuDraft = () => {
|
||||
const setNavigationMenuItemsDraft = useSetRecoilState(
|
||||
navigationMenuItemsDraftState,
|
||||
);
|
||||
|
||||
const addLinkToDraft = (
|
||||
label: string,
|
||||
url: string,
|
||||
currentDraft: NavigationMenuItem[],
|
||||
targetFolderId?: string | null,
|
||||
targetIndex?: number,
|
||||
): string => {
|
||||
const normalizedUrl = normalizeUrl(url);
|
||||
const folderId = targetFolderId ?? null;
|
||||
|
||||
const itemsInFolder = currentDraft.filter(
|
||||
(item) =>
|
||||
(item.folderId ?? null) === folderId &&
|
||||
!isDefined(item.userWorkspaceId),
|
||||
);
|
||||
const index = targetIndex ?? itemsInFolder.length;
|
||||
|
||||
const { flatIndex, position } = computeInsertIndexAndPosition(
|
||||
currentDraft,
|
||||
folderId,
|
||||
index,
|
||||
);
|
||||
|
||||
const newItemId = v4();
|
||||
const newItem: NavigationMenuItem = {
|
||||
__typename: 'NavigationMenuItem',
|
||||
id: newItemId,
|
||||
viewId: undefined,
|
||||
targetObjectMetadataId: undefined,
|
||||
targetRecordId: undefined,
|
||||
folderId: folderId ?? undefined,
|
||||
position,
|
||||
userWorkspaceId: undefined,
|
||||
name: label.trim() || 'Link',
|
||||
link: normalizedUrl,
|
||||
applicationId: undefined,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const newDraft = [
|
||||
...currentDraft.slice(0, flatIndex),
|
||||
newItem,
|
||||
...currentDraft.slice(flatIndex),
|
||||
];
|
||||
setNavigationMenuItemsDraft(newDraft);
|
||||
return newItemId;
|
||||
};
|
||||
|
||||
return { addLinkToDraft };
|
||||
};
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { v4 } from 'uuid';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import type { NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
import { navigationMenuItemsDraftState } from '@/navigation-menu-item/states/navigationMenuItemsDraftState';
|
||||
import { computeInsertIndexAndPosition } from '@/navigation-menu-item/utils/computeInsertIndexAndPosition';
|
||||
|
||||
export const useAddObjectToNavigationMenuDraft = () => {
|
||||
const setNavigationMenuItemsDraft = useSetRecoilState(
|
||||
navigationMenuItemsDraftState,
|
||||
);
|
||||
|
||||
const addObjectToDraft = (
|
||||
objectMetadataId: string,
|
||||
defaultViewId: string,
|
||||
currentDraft: NavigationMenuItem[],
|
||||
targetFolderId?: string | null,
|
||||
targetIndex?: number,
|
||||
): string => {
|
||||
const folderId = targetFolderId ?? null;
|
||||
|
||||
const itemsInFolder = currentDraft.filter(
|
||||
(item) =>
|
||||
(item.folderId ?? null) === folderId &&
|
||||
!isDefined(item.userWorkspaceId),
|
||||
);
|
||||
const index = targetIndex ?? itemsInFolder.length;
|
||||
|
||||
const { flatIndex, position } = computeInsertIndexAndPosition(
|
||||
currentDraft,
|
||||
folderId,
|
||||
index,
|
||||
);
|
||||
|
||||
const newItemId = v4();
|
||||
const newItem: NavigationMenuItem = {
|
||||
__typename: 'NavigationMenuItem',
|
||||
id: newItemId,
|
||||
viewId: defaultViewId,
|
||||
targetObjectMetadataId: objectMetadataId,
|
||||
position,
|
||||
userWorkspaceId: undefined,
|
||||
targetRecordId: undefined,
|
||||
folderId: folderId ?? undefined,
|
||||
name: undefined,
|
||||
applicationId: undefined,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const newDraft = [
|
||||
...currentDraft.slice(0, flatIndex),
|
||||
newItem,
|
||||
...currentDraft.slice(flatIndex),
|
||||
];
|
||||
setNavigationMenuItemsDraft(newDraft);
|
||||
return newItemId;
|
||||
};
|
||||
|
||||
return { addObjectToDraft };
|
||||
};
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import { v4 } from 'uuid';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import type { NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
import { navigationMenuItemsDraftState } from '@/navigation-menu-item/states/navigationMenuItemsDraftState';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { computeInsertIndexAndPosition } from '@/navigation-menu-item/utils/computeInsertIndexAndPosition';
|
||||
|
||||
type SearchRecord = {
|
||||
recordId: string;
|
||||
objectNameSingular: string;
|
||||
label: string;
|
||||
imageUrl?: string | null;
|
||||
};
|
||||
|
||||
type SearchRecordWithOptionalMetadataId = SearchRecord & {
|
||||
objectMetadataId?: string;
|
||||
};
|
||||
|
||||
export const useAddRecordToNavigationMenuDraft = () => {
|
||||
const setNavigationMenuItemsDraft = useSetRecoilState(
|
||||
navigationMenuItemsDraftState,
|
||||
);
|
||||
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
|
||||
|
||||
const addRecordToDraft = (
|
||||
searchRecord: SearchRecordWithOptionalMetadataId,
|
||||
currentDraft: NavigationMenuItem[],
|
||||
targetFolderId?: string | null,
|
||||
targetIndex?: number,
|
||||
): string | undefined => {
|
||||
const objectMetadataId =
|
||||
searchRecord.objectMetadataId ??
|
||||
objectMetadataItems.find(
|
||||
(item) => item.nameSingular === searchRecord.objectNameSingular,
|
||||
)?.id;
|
||||
|
||||
if (!isDefined(objectMetadataId)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const folderId = targetFolderId ?? null;
|
||||
|
||||
const itemsInFolder = currentDraft.filter(
|
||||
(item) =>
|
||||
(item.folderId ?? null) === folderId &&
|
||||
!isDefined(item.userWorkspaceId),
|
||||
);
|
||||
const index = targetIndex ?? itemsInFolder.length;
|
||||
|
||||
const { flatIndex, position } = computeInsertIndexAndPosition(
|
||||
currentDraft,
|
||||
folderId,
|
||||
index,
|
||||
);
|
||||
|
||||
const newItemId = v4();
|
||||
const newItem: NavigationMenuItem = {
|
||||
__typename: 'NavigationMenuItem',
|
||||
id: newItemId,
|
||||
viewId: undefined,
|
||||
targetObjectMetadataId: objectMetadataId,
|
||||
targetRecordId: searchRecord.recordId,
|
||||
targetRecordIdentifier: {
|
||||
id: searchRecord.recordId,
|
||||
labelIdentifier: searchRecord.label,
|
||||
imageIdentifier: searchRecord.imageUrl ?? null,
|
||||
},
|
||||
position,
|
||||
userWorkspaceId: undefined,
|
||||
folderId: folderId ?? undefined,
|
||||
name: undefined,
|
||||
applicationId: undefined,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const newDraft = [
|
||||
...currentDraft.slice(0, flatIndex),
|
||||
newItem,
|
||||
...currentDraft.slice(flatIndex),
|
||||
];
|
||||
setNavigationMenuItemsDraft(newDraft);
|
||||
return newItemId;
|
||||
};
|
||||
|
||||
return { addRecordToDraft };
|
||||
};
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { v4 } from 'uuid';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import type { NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
import { navigationMenuItemsDraftState } from '@/navigation-menu-item/states/navigationMenuItemsDraftState';
|
||||
import { computeInsertIndexAndPosition } from '@/navigation-menu-item/utils/computeInsertIndexAndPosition';
|
||||
|
||||
export const useAddViewToNavigationMenuDraft = () => {
|
||||
const setNavigationMenuItemsDraft = useSetRecoilState(
|
||||
navigationMenuItemsDraftState,
|
||||
);
|
||||
|
||||
const addViewToDraft = (
|
||||
viewId: string,
|
||||
currentDraft: NavigationMenuItem[],
|
||||
targetFolderId?: string | null,
|
||||
targetIndex?: number,
|
||||
): string => {
|
||||
const folderId = targetFolderId ?? null;
|
||||
|
||||
const itemsInFolder = currentDraft.filter(
|
||||
(item) =>
|
||||
(item.folderId ?? null) === folderId &&
|
||||
!isDefined(item.userWorkspaceId),
|
||||
);
|
||||
const index = targetIndex ?? itemsInFolder.length;
|
||||
|
||||
const { flatIndex, position } = computeInsertIndexAndPosition(
|
||||
currentDraft,
|
||||
folderId,
|
||||
index,
|
||||
);
|
||||
|
||||
const newItemId = v4();
|
||||
const newItem: NavigationMenuItem = {
|
||||
__typename: 'NavigationMenuItem',
|
||||
id: newItemId,
|
||||
viewId,
|
||||
targetObjectMetadataId: undefined,
|
||||
position,
|
||||
userWorkspaceId: undefined,
|
||||
targetRecordId: undefined,
|
||||
folderId: folderId ?? undefined,
|
||||
name: undefined,
|
||||
applicationId: undefined,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const newDraft = [
|
||||
...currentDraft.slice(0, flatIndex),
|
||||
newItem,
|
||||
...currentDraft.slice(flatIndex),
|
||||
];
|
||||
setNavigationMenuItemsDraft(newDraft);
|
||||
return newItemId;
|
||||
};
|
||||
|
||||
return { addViewToDraft };
|
||||
};
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { useNavigationMenuItemsDraftState } from '@/navigation-menu-item/hooks/useNavigationMenuItemsDraftState';
|
||||
import { navigationMenuItemsDraftState } from '@/navigation-menu-item/states/navigationMenuItemsDraftState';
|
||||
|
||||
export const useDraftNavigationMenuItems = () => {
|
||||
const { workspaceNavigationMenuItems } = useNavigationMenuItemsDraftState();
|
||||
const navigationMenuItemsDraft = useRecoilValue(
|
||||
navigationMenuItemsDraftState,
|
||||
);
|
||||
|
||||
const currentDraft = navigationMenuItemsDraft ?? workspaceNavigationMenuItems;
|
||||
|
||||
return { currentDraft };
|
||||
};
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import { useDraftNavigationMenuItems } from '@/navigation-menu-item/hooks/useDraftNavigationMenuItems';
|
||||
import { isNavigationMenuItemFolder } from '@/navigation-menu-item/utils/isNavigationMenuItemFolder';
|
||||
|
||||
export const useDraftNavigationMenuItemsAllFolders = () => {
|
||||
const { currentDraft } = useDraftNavigationMenuItems();
|
||||
|
||||
const allFolders =
|
||||
currentDraft?.filter(isNavigationMenuItemFolder).map((item) => ({
|
||||
id: item.id,
|
||||
name: item.name ?? 'Folder',
|
||||
folderId: item.folderId ?? undefined,
|
||||
})) ?? [];
|
||||
|
||||
return { allFolders };
|
||||
};
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import { useNavigationMenuItemsByFolder } from '@/navigation-menu-item/hooks/useNavigationMenuItemsByFolder';
|
||||
|
||||
export const useDraftNavigationMenuItemsWorkspaceFolders = () => {
|
||||
const { workspaceNavigationMenuItemsByFolder } =
|
||||
useNavigationMenuItemsByFolder();
|
||||
|
||||
const workspaceFolders = workspaceNavigationMenuItemsByFolder.map(
|
||||
(folder) => ({
|
||||
id: folder.id,
|
||||
name: folder.folderName,
|
||||
}),
|
||||
);
|
||||
|
||||
return { workspaceFolders };
|
||||
};
|
||||
+213
@@ -0,0 +1,213 @@
|
||||
import type { DropResult, ResponderProvided } from '@hello-pangea/dnd';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useRecoilCallback, useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IconFolder, IconLink, useIcons } from 'twenty-ui/display';
|
||||
|
||||
import { ADD_TO_NAV_SOURCE_DROPPABLE_ID } from '@/navigation-menu-item/constants/AddToNavSourceDroppableId';
|
||||
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
|
||||
import { useAddFolderToNavigationMenuDraft } from '@/navigation-menu-item/hooks/useAddFolderToNavigationMenuDraft';
|
||||
import { useAddLinkToNavigationMenuDraft } from '@/navigation-menu-item/hooks/useAddLinkToNavigationMenuDraft';
|
||||
import { useAddObjectToNavigationMenuDraft } from '@/navigation-menu-item/hooks/useAddObjectToNavigationMenuDraft';
|
||||
import { useAddRecordToNavigationMenuDraft } from '@/navigation-menu-item/hooks/useAddRecordToNavigationMenuDraft';
|
||||
import { useAddViewToNavigationMenuDraft } from '@/navigation-menu-item/hooks/useAddViewToNavigationMenuDraft';
|
||||
import { useNavigationMenuItemsDraftState } from '@/navigation-menu-item/hooks/useNavigationMenuItemsDraftState';
|
||||
import { useOpenNavigationMenuItemInCommandMenu } from '@/navigation-menu-item/hooks/useOpenNavigationMenuItemInCommandMenu';
|
||||
import { addToNavPayloadRegistryState } from '@/navigation-menu-item/states/addToNavPayloadRegistryState';
|
||||
import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/states/isNavigationMenuInEditModeState';
|
||||
import { navigationMenuItemsDraftState } from '@/navigation-menu-item/states/navigationMenuItemsDraftState';
|
||||
import { openNavigationMenuItemFolderIdsState } from '@/navigation-menu-item/states/openNavigationMenuItemFolderIdsState';
|
||||
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/states/selectedNavigationMenuItemInEditModeState';
|
||||
import { isWorkspaceDroppableId } from '@/navigation-menu-item/utils/isWorkspaceDroppableId';
|
||||
import { validateAndExtractWorkspaceFolderId } from '@/navigation-menu-item/utils/validateAndExtractWorkspaceFolderId';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
|
||||
import { coreViewsState } from '@/views/states/coreViewState';
|
||||
import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView';
|
||||
|
||||
export const useHandleAddToNavigationDrop = () => {
|
||||
const { addObjectToDraft } = useAddObjectToNavigationMenuDraft();
|
||||
const { addViewToDraft } = useAddViewToNavigationMenuDraft();
|
||||
const { addRecordToDraft } = useAddRecordToNavigationMenuDraft();
|
||||
const { addFolderToDraft } = useAddFolderToNavigationMenuDraft();
|
||||
const { addLinkToDraft } = useAddLinkToNavigationMenuDraft();
|
||||
const { workspaceNavigationMenuItems } = useNavigationMenuItemsDraftState();
|
||||
const navigationMenuItemsDraft = useRecoilValue(
|
||||
navigationMenuItemsDraftState,
|
||||
);
|
||||
const { openNavigationMenuItemInCommandMenu } =
|
||||
useOpenNavigationMenuItemInCommandMenu();
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
const coreViews = useRecoilValue(coreViewsState);
|
||||
const { getIcon } = useIcons();
|
||||
const setSelectedNavigationMenuItemInEditMode = useSetRecoilState(
|
||||
selectedNavigationMenuItemInEditModeState,
|
||||
);
|
||||
const setIsNavigationMenuInEditMode = useSetRecoilState(
|
||||
isNavigationMenuInEditModeState,
|
||||
);
|
||||
const setOpenNavigationMenuItemFolderIds = useSetRecoilState(
|
||||
openNavigationMenuItemFolderIdsState,
|
||||
);
|
||||
|
||||
const handleAddToNavigationDrop = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
(result: DropResult, _provided: ResponderProvided) => {
|
||||
const { source, destination, draggableId } = result;
|
||||
if (
|
||||
source.droppableId !== ADD_TO_NAV_SOURCE_DROPPABLE_ID ||
|
||||
!destination ||
|
||||
!isWorkspaceDroppableId(destination.droppableId)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const payload =
|
||||
getSnapshotValue(snapshot, addToNavPayloadRegistryState).get(
|
||||
draggableId,
|
||||
) ?? null;
|
||||
if (!payload) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentDraft = isDefined(navigationMenuItemsDraft)
|
||||
? navigationMenuItemsDraft
|
||||
: workspaceNavigationMenuItems;
|
||||
const folderId = validateAndExtractWorkspaceFolderId(
|
||||
destination.droppableId,
|
||||
);
|
||||
const index = destination.index;
|
||||
|
||||
if (
|
||||
payload.type === NavigationMenuItemType.FOLDER &&
|
||||
folderId !== null
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDefined(folderId)) {
|
||||
setOpenNavigationMenuItemFolderIds((current) =>
|
||||
current.includes(folderId) ? current : [...current, folderId],
|
||||
);
|
||||
}
|
||||
|
||||
const openEditForNewNavItem = (
|
||||
newItemId: string,
|
||||
options: Parameters<typeof openNavigationMenuItemInCommandMenu>[0],
|
||||
) => {
|
||||
setIsNavigationMenuInEditMode(true);
|
||||
setSelectedNavigationMenuItemInEditMode(newItemId);
|
||||
openNavigationMenuItemInCommandMenu(options);
|
||||
};
|
||||
|
||||
switch (payload.type) {
|
||||
case NavigationMenuItemType.FOLDER: {
|
||||
const newFolderId = addFolderToDraft(
|
||||
payload.name,
|
||||
currentDraft,
|
||||
null,
|
||||
index,
|
||||
);
|
||||
openEditForNewNavItem(newFolderId, {
|
||||
pageTitle: t`Edit folder`,
|
||||
pageIcon: IconFolder,
|
||||
focusTitleInput: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
case NavigationMenuItemType.LINK: {
|
||||
const newLinkId = addLinkToDraft(
|
||||
payload.name || t`Link label`,
|
||||
payload.link,
|
||||
currentDraft,
|
||||
folderId,
|
||||
index,
|
||||
);
|
||||
openEditForNewNavItem(newLinkId, {
|
||||
pageTitle: t`Edit link`,
|
||||
pageIcon: IconLink,
|
||||
focusTitleInput: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
case NavigationMenuItemType.OBJECT: {
|
||||
const newItemId = addObjectToDraft(
|
||||
payload.objectMetadataId,
|
||||
payload.defaultViewId,
|
||||
currentDraft,
|
||||
folderId,
|
||||
index,
|
||||
);
|
||||
const objectMetadataItem = objectMetadataItems.find(
|
||||
(item) => item.id === payload.objectMetadataId,
|
||||
);
|
||||
openEditForNewNavItem(newItemId, {
|
||||
pageTitle: objectMetadataItem?.labelPlural ?? payload.label,
|
||||
pageIcon: objectMetadataItem
|
||||
? getIcon(objectMetadataItem.icon)
|
||||
: IconFolder,
|
||||
});
|
||||
return;
|
||||
}
|
||||
case NavigationMenuItemType.VIEW: {
|
||||
const newItemId = addViewToDraft(
|
||||
payload.viewId,
|
||||
currentDraft,
|
||||
folderId,
|
||||
index,
|
||||
);
|
||||
const views = coreViews.map(convertCoreViewToView);
|
||||
const view = views.find((v) => v.id === payload.viewId);
|
||||
openEditForNewNavItem(newItemId, {
|
||||
pageTitle: view?.name ?? payload.label,
|
||||
pageIcon: view ? getIcon(view.icon) : IconFolder,
|
||||
});
|
||||
return;
|
||||
}
|
||||
case NavigationMenuItemType.RECORD: {
|
||||
const newItemId = addRecordToDraft(
|
||||
{
|
||||
recordId: payload.recordId,
|
||||
objectMetadataId: payload.objectMetadataId,
|
||||
objectNameSingular: payload.objectNameSingular,
|
||||
label: payload.label,
|
||||
imageUrl: payload.imageUrl,
|
||||
},
|
||||
currentDraft,
|
||||
folderId,
|
||||
index,
|
||||
);
|
||||
if (!isDefined(newItemId)) return;
|
||||
const objectMetadataItem = objectMetadataItems.find(
|
||||
(item) => item.id === payload.objectMetadataId,
|
||||
);
|
||||
openEditForNewNavItem(newItemId, {
|
||||
pageTitle: payload.label,
|
||||
pageIcon: objectMetadataItem
|
||||
? getIcon(objectMetadataItem.icon)
|
||||
: IconFolder,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
addFolderToDraft,
|
||||
addLinkToDraft,
|
||||
addObjectToDraft,
|
||||
addRecordToDraft,
|
||||
addViewToDraft,
|
||||
coreViews,
|
||||
getIcon,
|
||||
navigationMenuItemsDraft,
|
||||
objectMetadataItems,
|
||||
openNavigationMenuItemInCommandMenu,
|
||||
setOpenNavigationMenuItemFolderIds,
|
||||
setIsNavigationMenuInEditMode,
|
||||
setSelectedNavigationMenuItemInEditMode,
|
||||
workspaceNavigationMenuItems,
|
||||
],
|
||||
);
|
||||
|
||||
return { handleAddToNavigationDrop };
|
||||
};
|
||||
+10
-3
@@ -1,7 +1,8 @@
|
||||
import { type OnDragEndResponder } from '@hello-pangea/dnd';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
|
||||
import { ORPHAN_NAVIGATION_MENU_ITEMS_DROPPABLE_ID } from '@/navigation-menu-item/constants/NavigationMenuItemDroppableIds';
|
||||
import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/constants/NavigationMenuItemDroppableIds';
|
||||
import { isWorkspaceDroppableId } from '@/navigation-menu-item/utils/isWorkspaceDroppableId';
|
||||
import { useSortedNavigationMenuItems } from '@/navigation-menu-item/hooks/useSortedNavigationMenuItems';
|
||||
import { useUpdateNavigationMenuItem } from '@/navigation-menu-item/hooks/useUpdateNavigationMenuItem';
|
||||
import { openNavigationMenuItemFolderIdsState } from '@/navigation-menu-item/states/openNavigationMenuItemFolderIdsState';
|
||||
@@ -48,6 +49,10 @@ export const useHandleNavigationMenuItemDragAndDrop = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isWorkspaceDroppableId(destination.droppableId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const draggedNavigationMenuItem = navigationMenuItems.find(
|
||||
(item) => item.id === draggableId,
|
||||
);
|
||||
@@ -57,11 +62,13 @@ export const useHandleNavigationMenuItemDragAndDrop = () => {
|
||||
|
||||
const destinationFolderId = validateAndExtractFolderId({
|
||||
droppableId: destination.droppableId,
|
||||
orphanDroppableId: ORPHAN_NAVIGATION_MENU_ITEMS_DROPPABLE_ID,
|
||||
orphanDroppableId:
|
||||
NavigationMenuItemDroppableIds.ORPHAN_NAVIGATION_MENU_ITEMS,
|
||||
});
|
||||
const sourceFolderId = validateAndExtractFolderId({
|
||||
droppableId: source.droppableId,
|
||||
orphanDroppableId: ORPHAN_NAVIGATION_MENU_ITEMS_DROPPABLE_ID,
|
||||
orphanDroppableId:
|
||||
NavigationMenuItemDroppableIds.ORPHAN_NAVIGATION_MENU_ITEMS,
|
||||
});
|
||||
|
||||
if (
|
||||
|
||||
+177
@@ -0,0 +1,177 @@
|
||||
import { type OnDragEndResponder } from '@hello-pangea/dnd';
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/constants/NavigationMenuItemDroppableIds';
|
||||
import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/states/isNavigationMenuInEditModeState';
|
||||
import { navigationMenuItemsDraftState } from '@/navigation-menu-item/states/navigationMenuItemsDraftState';
|
||||
import { openNavigationMenuItemFolderIdsState } from '@/navigation-menu-item/states/openNavigationMenuItemFolderIdsState';
|
||||
import {
|
||||
matchesWorkspaceFolderId,
|
||||
validateAndExtractWorkspaceFolderId,
|
||||
} from '@/navigation-menu-item/utils/validateAndExtractWorkspaceFolderId';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { usePrefetchedNavigationMenuItemsData } from './usePrefetchedNavigationMenuItemsData';
|
||||
|
||||
export const useHandleWorkspaceNavigationMenuItemDragAndDrop = () => {
|
||||
const { workspaceNavigationMenuItems } =
|
||||
usePrefetchedNavigationMenuItemsData();
|
||||
const isNavigationMenuInEditMode = useRecoilValue(
|
||||
isNavigationMenuInEditModeState,
|
||||
);
|
||||
const navigationMenuItemsDraft = useRecoilValue(
|
||||
navigationMenuItemsDraftState,
|
||||
);
|
||||
const setNavigationMenuItemsDraft = useSetRecoilState(
|
||||
navigationMenuItemsDraftState,
|
||||
);
|
||||
const setOpenNavigationMenuItemFolderIds = useSetRecoilState(
|
||||
openNavigationMenuItemFolderIdsState,
|
||||
);
|
||||
|
||||
const openDestinationFolder = (folderId: string | null) => {
|
||||
if (!folderId) {
|
||||
return;
|
||||
}
|
||||
|
||||
setOpenNavigationMenuItemFolderIds((current) => {
|
||||
if (!current.includes(folderId)) {
|
||||
return [...current, folderId];
|
||||
}
|
||||
return current;
|
||||
});
|
||||
};
|
||||
|
||||
const handleWorkspaceNavigationMenuItemDragAndDrop: OnDragEndResponder = (
|
||||
result,
|
||||
) => {
|
||||
const { destination, source, draggableId } = result;
|
||||
|
||||
if (!destination) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
destination.droppableId === source.droppableId &&
|
||||
destination.index === source.index
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isWorkspaceDrop =
|
||||
source.droppableId.startsWith('workspace-') &&
|
||||
destination.droppableId.startsWith('workspace-');
|
||||
|
||||
if (!isWorkspaceDrop) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isNavigationMenuInEditMode || !navigationMenuItemsDraft) {
|
||||
return;
|
||||
}
|
||||
|
||||
const draggedItem = workspaceNavigationMenuItems.find(
|
||||
(item) => item.id === draggableId,
|
||||
);
|
||||
|
||||
if (!draggedItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
const destinationFolderId = validateAndExtractWorkspaceFolderId(
|
||||
destination.droppableId,
|
||||
);
|
||||
const sourceFolderId = validateAndExtractWorkspaceFolderId(
|
||||
source.droppableId,
|
||||
);
|
||||
|
||||
const isDropOnFolderHeader = destination.droppableId.startsWith(
|
||||
NavigationMenuItemDroppableIds.WORKSPACE_FOLDER_HEADER_PREFIX,
|
||||
);
|
||||
|
||||
if (isDropOnFolderHeader && isDefined(destinationFolderId)) {
|
||||
openDestinationFolder(destinationFolderId);
|
||||
}
|
||||
|
||||
const sourceList = (navigationMenuItemsDraft ?? [])
|
||||
.filter((item) => matchesWorkspaceFolderId(item, sourceFolderId))
|
||||
.sort((a, b) => a.position - b.position);
|
||||
|
||||
const destinationList = (navigationMenuItemsDraft ?? [])
|
||||
.filter((item) => matchesWorkspaceFolderId(item, destinationFolderId))
|
||||
.sort((a, b) => a.position - b.position);
|
||||
|
||||
if (!sourceList.some((item) => item.id === draggableId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isSameList = sourceFolderId === destinationFolderId;
|
||||
let reorderedDestinationList: NavigationMenuItem[];
|
||||
|
||||
if (isSameList) {
|
||||
const listWithoutDragged = sourceList.filter(
|
||||
(item) => item.id !== draggableId,
|
||||
);
|
||||
reorderedDestinationList = [
|
||||
...listWithoutDragged.slice(0, destination.index),
|
||||
draggedItem,
|
||||
...listWithoutDragged.slice(destination.index),
|
||||
];
|
||||
} else {
|
||||
const destinationListWithInsertedItem = [
|
||||
...destinationList.slice(0, destination.index),
|
||||
{ ...draggedItem, folderId: destinationFolderId },
|
||||
...destinationList.slice(destination.index),
|
||||
];
|
||||
reorderedDestinationList = destinationListWithInsertedItem;
|
||||
}
|
||||
|
||||
const destinationWithNormalizedPositions = reorderedDestinationList.map(
|
||||
(item, index) => ({
|
||||
...item,
|
||||
position: index,
|
||||
folderId: isSameList ? item.folderId : destinationFolderId,
|
||||
}),
|
||||
);
|
||||
|
||||
const positionUpdates = new Map<
|
||||
string,
|
||||
{ position: number; folderId: string | null }
|
||||
>();
|
||||
destinationWithNormalizedPositions.forEach((item) => {
|
||||
positionUpdates.set(item.id, {
|
||||
position: item.position,
|
||||
folderId: item.folderId ?? null,
|
||||
});
|
||||
});
|
||||
|
||||
if (!isSameList) {
|
||||
const sourceListWithoutDragged = sourceList.filter(
|
||||
(item) => item.id !== draggableId,
|
||||
);
|
||||
sourceListWithoutDragged.forEach((item, index) => {
|
||||
positionUpdates.set(item.id, {
|
||||
position: index,
|
||||
folderId: sourceFolderId,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const updatedDraft = navigationMenuItemsDraft.map(
|
||||
(item): NavigationMenuItem => {
|
||||
const update = positionUpdates.get(item.id);
|
||||
if (!update) return item;
|
||||
return {
|
||||
...item,
|
||||
position: update.position,
|
||||
folderId: update.folderId,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
setNavigationMenuItemsDraft(updatedDraft);
|
||||
};
|
||||
|
||||
return { handleWorkspaceNavigationMenuItemDragAndDrop };
|
||||
};
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
import { ADD_TO_NAV_SOURCE_DROPPABLE_ID } from '@/navigation-menu-item/constants/AddToNavSourceDroppableId';
|
||||
import { NavigationDragSourceContext } from '@/navigation-menu-item/contexts/NavigationDragSourceContext';
|
||||
import { isWorkspaceDroppableId } from '@/navigation-menu-item/utils/isWorkspaceDroppableId';
|
||||
import { useContext } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useIsDropDisabledForSection = (isWorkspaceSection: boolean) => {
|
||||
const { sourceDroppableId } = useContext(NavigationDragSourceContext);
|
||||
if (!isDefined(sourceDroppableId)) {
|
||||
return false;
|
||||
}
|
||||
if (sourceDroppableId === ADD_TO_NAV_SOURCE_DROPPABLE_ID) {
|
||||
return !isWorkspaceSection;
|
||||
}
|
||||
return isWorkspaceDroppableId(sourceDroppableId) !== isWorkspaceSection;
|
||||
};
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import type { NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
import { navigationMenuItemsDraftState } from '@/navigation-menu-item/states/navigationMenuItemsDraftState';
|
||||
import { isNavigationMenuItemFolder } from '@/navigation-menu-item/utils/isNavigationMenuItemFolder';
|
||||
|
||||
const swapPositionsInDraft = (
|
||||
draft: NavigationMenuItem[],
|
||||
itemA: NavigationMenuItem,
|
||||
itemB: NavigationMenuItem,
|
||||
): NavigationMenuItem[] =>
|
||||
draft.map((item) => {
|
||||
if (item.id === itemA.id) {
|
||||
return { ...item, position: itemB.position };
|
||||
}
|
||||
if (item.id === itemB.id) {
|
||||
return { ...item, position: itemA.position };
|
||||
}
|
||||
return item;
|
||||
});
|
||||
|
||||
export const useNavigationMenuItemMoveRemove = () => {
|
||||
const setNavigationMenuItemsDraft = useSetRecoilState(
|
||||
navigationMenuItemsDraftState,
|
||||
);
|
||||
|
||||
const moveUp = (navigationMenuItemId: string) => {
|
||||
setNavigationMenuItemsDraft((draft) => {
|
||||
if (!draft) return draft;
|
||||
|
||||
const currentItem = draft.find(
|
||||
(item) => item.id === navigationMenuItemId,
|
||||
);
|
||||
if (!currentItem) return draft;
|
||||
|
||||
const folderId = currentItem.folderId ?? null;
|
||||
const siblings = draft
|
||||
.filter((item) => (item.folderId ?? null) === folderId)
|
||||
.sort((a, b) => a.position - b.position);
|
||||
|
||||
const currentIndex = siblings.findIndex(
|
||||
(item) => item.id === navigationMenuItemId,
|
||||
);
|
||||
if (currentIndex <= 0) return draft;
|
||||
|
||||
const itemAbove = siblings[currentIndex - 1];
|
||||
return swapPositionsInDraft(draft, currentItem, itemAbove);
|
||||
});
|
||||
};
|
||||
|
||||
const moveDown = (navigationMenuItemId: string) => {
|
||||
setNavigationMenuItemsDraft((draft) => {
|
||||
if (!draft) return draft;
|
||||
|
||||
const currentItem = draft.find(
|
||||
(item) => item.id === navigationMenuItemId,
|
||||
);
|
||||
if (!currentItem) return draft;
|
||||
|
||||
const folderId = currentItem.folderId ?? null;
|
||||
const siblings = draft
|
||||
.filter((item) => (item.folderId ?? null) === folderId)
|
||||
.sort((a, b) => a.position - b.position);
|
||||
|
||||
const currentIndex = siblings.findIndex(
|
||||
(item) => item.id === navigationMenuItemId,
|
||||
);
|
||||
if (currentIndex < 0 || currentIndex >= siblings.length - 1) {
|
||||
return draft;
|
||||
}
|
||||
|
||||
const itemBelow = siblings[currentIndex + 1];
|
||||
return swapPositionsInDraft(draft, currentItem, itemBelow);
|
||||
});
|
||||
};
|
||||
|
||||
const remove = (navigationMenuItemId: string) => {
|
||||
setNavigationMenuItemsDraft((draft) => {
|
||||
if (!draft) return draft;
|
||||
|
||||
const itemToRemove = draft.find(
|
||||
(item) => item.id === navigationMenuItemId,
|
||||
);
|
||||
if (!itemToRemove) return draft;
|
||||
|
||||
const isFolder = isNavigationMenuItemFolder(itemToRemove);
|
||||
|
||||
if (isFolder) {
|
||||
return draft.filter(
|
||||
(item) =>
|
||||
item.id !== navigationMenuItemId &&
|
||||
item.folderId !== navigationMenuItemId,
|
||||
);
|
||||
}
|
||||
|
||||
return draft.filter((item) => item.id !== navigationMenuItemId);
|
||||
});
|
||||
};
|
||||
|
||||
const moveToFolder = (
|
||||
navigationMenuItemId: string,
|
||||
targetFolderId: string | null,
|
||||
) => {
|
||||
setNavigationMenuItemsDraft((draft) => {
|
||||
if (!draft) return draft;
|
||||
|
||||
const itemToMove = draft.find((item) => item.id === navigationMenuItemId);
|
||||
if (!itemToMove) return draft;
|
||||
|
||||
const isFolder = isNavigationMenuItemFolder(itemToMove);
|
||||
if (isFolder && targetFolderId === navigationMenuItemId) {
|
||||
return draft;
|
||||
}
|
||||
|
||||
if (isFolder && isDefined(targetFolderId)) {
|
||||
const descendantFolderIds = new Set<string>();
|
||||
const collectDescendants = (folderId: string) => {
|
||||
draft
|
||||
?.filter(
|
||||
(item) =>
|
||||
isNavigationMenuItemFolder(item) && item.folderId === folderId,
|
||||
)
|
||||
.forEach((item) => {
|
||||
descendantFolderIds.add(item.id);
|
||||
collectDescendants(item.id);
|
||||
});
|
||||
};
|
||||
collectDescendants(navigationMenuItemId);
|
||||
if (descendantFolderIds.has(targetFolderId)) {
|
||||
return draft;
|
||||
}
|
||||
}
|
||||
|
||||
const itemsInTargetFolder = draft.filter((item) =>
|
||||
targetFolderId === null
|
||||
? !isDefined(item.folderId)
|
||||
: item.folderId === targetFolderId,
|
||||
);
|
||||
const maxPositionInTarget =
|
||||
itemsInTargetFolder.length > 0
|
||||
? Math.max(...itemsInTargetFolder.map((item) => item.position))
|
||||
: -1;
|
||||
const newPosition = maxPositionInTarget + 1;
|
||||
|
||||
return draft.map((item) =>
|
||||
item.id === navigationMenuItemId
|
||||
? {
|
||||
...item,
|
||||
folderId: targetFolderId ?? undefined,
|
||||
position: newPosition,
|
||||
}
|
||||
: item,
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
return { moveUp, moveDown, remove, moveToFolder };
|
||||
};
|
||||
+102
-78
@@ -2,6 +2,7 @@ import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
import { isNavigationMenuItemFolder } from '@/navigation-menu-item/utils/isNavigationMenuItemFolder';
|
||||
import { recordIdentifierToObjectRecordIdentifier } from '@/navigation-menu-item/utils/recordIdentifierToObjectRecordIdentifier';
|
||||
import { sortNavigationMenuItems } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
@@ -12,7 +13,7 @@ import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView';
|
||||
import { usePrefetchedNavigationMenuItemsData } from './usePrefetchedNavigationMenuItemsData';
|
||||
|
||||
type NavigationMenuItemFolder = {
|
||||
folderId: string;
|
||||
id: string;
|
||||
folderName: string;
|
||||
navigationMenuItems: ReturnType<typeof sortNavigationMenuItems>[number][];
|
||||
};
|
||||
@@ -27,91 +28,114 @@ export const useNavigationMenuItemsByFolder = () => {
|
||||
const views = coreViews.map(convertCoreViewToView);
|
||||
|
||||
const allNavigationMenuItems = [
|
||||
...navigationMenuItems,
|
||||
...workspaceNavigationMenuItems,
|
||||
...navigationMenuItems,
|
||||
];
|
||||
|
||||
const { folders, itemsByFolderId } = allNavigationMenuItems.reduce<{
|
||||
folders: Array<{ id: string; name: string }>;
|
||||
itemsByFolderId: Map<string, NavigationMenuItem[]>;
|
||||
}>(
|
||||
(acc, item) => {
|
||||
const isFolder =
|
||||
isDefined(item.name) &&
|
||||
!isDefined(item.folderId) &&
|
||||
!isDefined(item.targetRecordId) &&
|
||||
!isDefined(item.targetObjectMetadataId) &&
|
||||
!isDefined(item.viewId);
|
||||
|
||||
if (isFolder) {
|
||||
acc.folders.push({ id: item.id, name: item.name || 'Folder' });
|
||||
} else if (isDefined(item.folderId)) {
|
||||
const existingItems = acc.itemsByFolderId.get(item.folderId);
|
||||
if (isDefined(existingItems)) {
|
||||
existingItems.push(item);
|
||||
} else {
|
||||
acc.itemsByFolderId.set(item.folderId, [item]);
|
||||
const { workspaceFolders, userFolders, itemsByFolderId } =
|
||||
allNavigationMenuItems.reduce<{
|
||||
workspaceFolders: Array<{ id: string; name: string }>;
|
||||
userFolders: Array<{ id: string; name: string }>;
|
||||
itemsByFolderId: Map<string, NavigationMenuItem[]>;
|
||||
}>(
|
||||
(acc, item) => {
|
||||
if (isNavigationMenuItemFolder(item)) {
|
||||
const folderEntry = { id: item.id, name: item.name || 'Folder' };
|
||||
if (isDefined(item.userWorkspaceId)) {
|
||||
acc.userFolders.push(folderEntry);
|
||||
} else {
|
||||
acc.workspaceFolders.push(folderEntry);
|
||||
}
|
||||
} else if (isDefined(item.folderId)) {
|
||||
const existingItems = acc.itemsByFolderId.get(item.folderId);
|
||||
if (isDefined(existingItems)) {
|
||||
existingItems.push(item);
|
||||
} else {
|
||||
acc.itemsByFolderId.set(item.folderId, [item]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
{ folders: [], itemsByFolderId: new Map() },
|
||||
);
|
||||
|
||||
const navigationMenuItemsByFolder = folders.reduce<
|
||||
NavigationMenuItemFolder[]
|
||||
>((acc, folder) => {
|
||||
const itemsInFolder = itemsByFolderId.get(folder.id) || [];
|
||||
|
||||
const targetRecordIdentifiersMap = itemsInFolder.reduce<
|
||||
Map<string, ObjectRecordIdentifier>
|
||||
>((map, item) => {
|
||||
const itemTargetRecordId = item.targetRecordId;
|
||||
if (!isDefined(itemTargetRecordId) || isDefined(item.viewId)) {
|
||||
return map;
|
||||
}
|
||||
|
||||
const targetRecordIdentifier = item.targetRecordIdentifier;
|
||||
|
||||
if (!isDefined(targetRecordIdentifier)) {
|
||||
return map;
|
||||
}
|
||||
|
||||
const itemObjectMetadata = objectMetadataItems.find(
|
||||
(meta) => meta.id === item.targetObjectMetadataId,
|
||||
);
|
||||
|
||||
if (isDefined(itemObjectMetadata)) {
|
||||
const objectRecordIdentifier = recordIdentifierToObjectRecordIdentifier(
|
||||
{
|
||||
recordIdentifier: targetRecordIdentifier,
|
||||
objectMetadataItem: itemObjectMetadata,
|
||||
},
|
||||
);
|
||||
|
||||
map.set(itemTargetRecordId, objectRecordIdentifier);
|
||||
}
|
||||
|
||||
return map;
|
||||
}, new Map());
|
||||
|
||||
const sortedItems = sortNavigationMenuItems(
|
||||
itemsInFolder,
|
||||
true,
|
||||
views,
|
||||
objectMetadataItems,
|
||||
targetRecordIdentifiersMap,
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
workspaceFolders: [],
|
||||
userFolders: [],
|
||||
itemsByFolderId: new Map(),
|
||||
},
|
||||
);
|
||||
|
||||
acc.push({
|
||||
folderId: folder.id,
|
||||
folderName: folder.name,
|
||||
navigationMenuItems: sortedItems,
|
||||
const buildFoldersList = (folders: Array<{ id: string; name: string }>) => {
|
||||
const sortedFolders = [...folders].sort((a, b) => {
|
||||
const folderA = allNavigationMenuItems.find((item) => item.id === a.id);
|
||||
const folderB = allNavigationMenuItems.find((item) => item.id === b.id);
|
||||
const positionA = folderA?.position ?? 0;
|
||||
const positionB = folderB?.position ?? 0;
|
||||
return positionA - positionB;
|
||||
});
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
return sortedFolders.reduce<NavigationMenuItemFolder[]>((acc, folder) => {
|
||||
const itemsInFolder = itemsByFolderId.get(folder.id) || [];
|
||||
|
||||
return { navigationMenuItemsByFolder };
|
||||
const targetRecordIdentifiersMap = itemsInFolder.reduce<
|
||||
Map<string, ObjectRecordIdentifier>
|
||||
>((map, item) => {
|
||||
const itemTargetRecordId = item.targetRecordId;
|
||||
if (!isDefined(itemTargetRecordId) || isDefined(item.viewId)) {
|
||||
return map;
|
||||
}
|
||||
|
||||
const targetRecordIdentifier = item.targetRecordIdentifier;
|
||||
|
||||
if (!isDefined(targetRecordIdentifier)) {
|
||||
return map;
|
||||
}
|
||||
|
||||
const itemObjectMetadata = objectMetadataItems.find(
|
||||
(meta) => meta.id === item.targetObjectMetadataId,
|
||||
);
|
||||
|
||||
if (isDefined(itemObjectMetadata)) {
|
||||
const objectRecordIdentifier =
|
||||
recordIdentifierToObjectRecordIdentifier({
|
||||
recordIdentifier: targetRecordIdentifier,
|
||||
objectMetadataItem: itemObjectMetadata,
|
||||
});
|
||||
|
||||
map.set(itemTargetRecordId, objectRecordIdentifier);
|
||||
}
|
||||
|
||||
return map;
|
||||
}, new Map());
|
||||
|
||||
const sortedItems = sortNavigationMenuItems(
|
||||
itemsInFolder,
|
||||
true,
|
||||
views,
|
||||
objectMetadataItems,
|
||||
targetRecordIdentifiersMap,
|
||||
);
|
||||
|
||||
acc.push({
|
||||
id: folder.id,
|
||||
folderName: folder.name,
|
||||
navigationMenuItems: sortedItems,
|
||||
});
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
};
|
||||
|
||||
const workspaceNavigationMenuItemsByFolder =
|
||||
buildFoldersList(workspaceFolders);
|
||||
const userNavigationMenuItemsByFolder = buildFoldersList(userFolders);
|
||||
const navigationMenuItemsByFolder = [
|
||||
...workspaceNavigationMenuItemsByFolder,
|
||||
...userNavigationMenuItemsByFolder,
|
||||
];
|
||||
|
||||
return {
|
||||
navigationMenuItemsByFolder,
|
||||
workspaceNavigationMenuItemsByFolder,
|
||||
userNavigationMenuItemsByFolder,
|
||||
};
|
||||
};
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
|
||||
import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/states/isNavigationMenuInEditModeState';
|
||||
import { filterWorkspaceNavigationMenuItems } from '@/navigation-menu-item/utils/filterWorkspaceNavigationMenuItems';
|
||||
import { navigationMenuItemsDraftState } from '@/navigation-menu-item/states/navigationMenuItemsDraftState';
|
||||
import { prefetchNavigationMenuItemsState } from '@/prefetch/states/prefetchNavigationMenuItemsState';
|
||||
|
||||
export const useNavigationMenuItemsDraftState = () => {
|
||||
const isNavigationMenuInEditMode = useRecoilValue(
|
||||
isNavigationMenuInEditModeState,
|
||||
);
|
||||
const prefetchNavigationMenuItems = useRecoilValue(
|
||||
prefetchNavigationMenuItemsState,
|
||||
);
|
||||
const navigationMenuItemsDraft = useRecoilValue(
|
||||
navigationMenuItemsDraftState,
|
||||
);
|
||||
|
||||
const workspaceNavigationMenuItemsFromPrefetch =
|
||||
filterWorkspaceNavigationMenuItems(prefetchNavigationMenuItems);
|
||||
|
||||
const workspaceNavigationMenuItems =
|
||||
isNavigationMenuInEditMode && isDefined(navigationMenuItemsDraft)
|
||||
? navigationMenuItemsDraft
|
||||
: workspaceNavigationMenuItemsFromPrefetch;
|
||||
|
||||
const isDirty =
|
||||
isNavigationMenuInEditMode &&
|
||||
isDefined(navigationMenuItemsDraft) &&
|
||||
!isDeeplyEqual(
|
||||
navigationMenuItemsDraft,
|
||||
workspaceNavigationMenuItemsFromPrefetch,
|
||||
);
|
||||
|
||||
return {
|
||||
workspaceNavigationMenuItems,
|
||||
isDirty,
|
||||
};
|
||||
};
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { coreViewsState } from '@/views/states/coreViewState';
|
||||
import { ViewKey } from '@/views/types/ViewKey';
|
||||
import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView';
|
||||
|
||||
type NavigationMenuItemDraft = {
|
||||
id?: string;
|
||||
viewId?: string | null;
|
||||
targetObjectMetadataId?: string | null;
|
||||
};
|
||||
|
||||
export const useNavigationMenuObjectMetadataFromDraft = (
|
||||
currentDraft: NavigationMenuItemDraft[],
|
||||
) => {
|
||||
const coreViews = useRecoilValue(coreViewsState);
|
||||
const views = coreViews.map(convertCoreViewToView);
|
||||
|
||||
const objectMetadataIdsInWorkspace = currentDraft.reduce<Set<string>>(
|
||||
(ids, item) => {
|
||||
const view = isDefined(item.viewId)
|
||||
? views.find((view) => view.id === item.viewId)
|
||||
: undefined;
|
||||
if (isDefined(view)) {
|
||||
ids.add(view.objectMetadataId);
|
||||
}
|
||||
if (isDefined(item.targetObjectMetadataId)) {
|
||||
ids.add(item.targetObjectMetadataId);
|
||||
}
|
||||
return ids;
|
||||
},
|
||||
new Set<string>(),
|
||||
);
|
||||
|
||||
const objectMetadataIdsWithIndexView = new Set(
|
||||
views
|
||||
.filter((view) => view.key === ViewKey.Index)
|
||||
.map((view) => view.objectMetadataId),
|
||||
);
|
||||
|
||||
const objectMetadataIdsWithAnyView = new Set(
|
||||
views.map((view) => view.objectMetadataId),
|
||||
);
|
||||
|
||||
const viewIdsInWorkspace = new Set(
|
||||
currentDraft.flatMap((item) =>
|
||||
isDefined(item.viewId) ? [item.viewId] : [],
|
||||
),
|
||||
);
|
||||
|
||||
return {
|
||||
views,
|
||||
objectMetadataIdsInWorkspace,
|
||||
objectMetadataIdsWithIndexView,
|
||||
objectMetadataIdsWithAnyView,
|
||||
viewIdsInWorkspace,
|
||||
};
|
||||
};
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import { useNavigateCommandMenu } from '@/command-menu/hooks/useNavigateCommandMenu';
|
||||
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
|
||||
import type { IconComponent } from 'twenty-ui/display';
|
||||
|
||||
export const useOpenNavigationMenuItemInCommandMenu = () => {
|
||||
const { navigateCommandMenu } = useNavigateCommandMenu();
|
||||
|
||||
const openNavigationMenuItemInCommandMenu = ({
|
||||
pageTitle,
|
||||
pageIcon,
|
||||
focusTitleInput = false,
|
||||
}: {
|
||||
pageTitle: string;
|
||||
pageIcon: IconComponent;
|
||||
focusTitleInput?: boolean;
|
||||
}) => {
|
||||
navigateCommandMenu({
|
||||
page: CommandMenuPages.NavigationMenuItemEdit,
|
||||
pageTitle,
|
||||
pageIcon,
|
||||
resetNavigationStack: true,
|
||||
focusTitleInput,
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
openNavigationMenuItemInCommandMenu,
|
||||
};
|
||||
};
|
||||
+16
-3
@@ -1,4 +1,7 @@
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/states/isNavigationMenuInEditModeState';
|
||||
import { navigationMenuItemsDraftState } from '@/navigation-menu-item/states/navigationMenuItemsDraftState';
|
||||
import { filterWorkspaceNavigationMenuItems } from '@/navigation-menu-item/utils/filterWorkspaceNavigationMenuItems';
|
||||
import { prefetchNavigationMenuItemsState } from '@/prefetch/states/prefetchNavigationMenuItemsState';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
@@ -17,14 +20,24 @@ export const usePrefetchedNavigationMenuItemsData =
|
||||
const prefetchNavigationMenuItems = useRecoilValue(
|
||||
prefetchNavigationMenuItemsState,
|
||||
);
|
||||
const isNavigationMenuInEditMode = useRecoilValue(
|
||||
isNavigationMenuInEditModeState,
|
||||
);
|
||||
const navigationMenuItemsDraft = useRecoilValue(
|
||||
navigationMenuItemsDraftState,
|
||||
);
|
||||
|
||||
const navigationMenuItems = prefetchNavigationMenuItems.filter((item) =>
|
||||
isDefined(item.userWorkspaceId),
|
||||
);
|
||||
|
||||
const workspaceNavigationMenuItems = prefetchNavigationMenuItems.filter(
|
||||
(item) => !isDefined(item.userWorkspaceId),
|
||||
);
|
||||
const workspaceNavigationMenuItemsFromPrefetch =
|
||||
filterWorkspaceNavigationMenuItems(prefetchNavigationMenuItems);
|
||||
|
||||
const workspaceNavigationMenuItems =
|
||||
isNavigationMenuInEditMode && isDefined(navigationMenuItemsDraft)
|
||||
? navigationMenuItemsDraft
|
||||
: workspaceNavigationMenuItemsFromPrefetch;
|
||||
|
||||
return {
|
||||
navigationMenuItems,
|
||||
|
||||
+204
@@ -0,0 +1,204 @@
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { useCreateNavigationMenuItemMutation } from '~/generated-metadata/graphql';
|
||||
|
||||
import { useDeleteNavigationMenuItem } from '@/navigation-menu-item/hooks/useDeleteNavigationMenuItem';
|
||||
import { useUpdateNavigationMenuItem } from '@/navigation-menu-item/hooks/useUpdateNavigationMenuItem';
|
||||
import { navigationMenuItemsDraftState } from '@/navigation-menu-item/states/navigationMenuItemsDraftState';
|
||||
import { filterWorkspaceNavigationMenuItems } from '@/navigation-menu-item/utils/filterWorkspaceNavigationMenuItems';
|
||||
import { isNavigationMenuItemFolder } from '@/navigation-menu-item/utils/isNavigationMenuItemFolder';
|
||||
import { isNavigationMenuItemLink } from '@/navigation-menu-item/utils/isNavigationMenuItemLink';
|
||||
import { prefetchNavigationMenuItemsState } from '@/prefetch/states/prefetchNavigationMenuItemsState';
|
||||
|
||||
export const useSaveNavigationMenuItemsDraft = () => {
|
||||
const { updateNavigationMenuItem } = useUpdateNavigationMenuItem();
|
||||
const { deleteNavigationMenuItem } = useDeleteNavigationMenuItem();
|
||||
const [createNavigationMenuItemMutation] =
|
||||
useCreateNavigationMenuItemMutation({
|
||||
refetchQueries: ['FindManyNavigationMenuItems'],
|
||||
});
|
||||
|
||||
const saveDraft = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
async () => {
|
||||
const draft = snapshot
|
||||
.getLoadable(navigationMenuItemsDraftState)
|
||||
.getValue();
|
||||
const prefetch = snapshot
|
||||
.getLoadable(prefetchNavigationMenuItemsState)
|
||||
.getValue();
|
||||
|
||||
if (!draft) return;
|
||||
|
||||
const workspacePrefetch = filterWorkspaceNavigationMenuItems(prefetch);
|
||||
const topLevelWorkspace = workspacePrefetch.filter(
|
||||
(item) => !isDefined(item.folderId),
|
||||
);
|
||||
const draftIds = new Set(draft.map((i) => i.id));
|
||||
|
||||
const topLevelToDelete = topLevelWorkspace.filter(
|
||||
(item) => !draftIds.has(item.id),
|
||||
);
|
||||
const folderIdsToDelete = new Set(
|
||||
topLevelToDelete
|
||||
.filter(isNavigationMenuItemFolder)
|
||||
.map((item) => item.id),
|
||||
);
|
||||
const folderChildrenToDelete = prefetch.filter(
|
||||
(item) =>
|
||||
isDefined(item.folderId) && folderIdsToDelete.has(item.folderId),
|
||||
);
|
||||
|
||||
for (const item of folderChildrenToDelete) {
|
||||
await deleteNavigationMenuItem(item.id);
|
||||
}
|
||||
for (const item of topLevelToDelete) {
|
||||
await deleteNavigationMenuItem(item.id);
|
||||
}
|
||||
|
||||
const prefetchIds = new Set(workspacePrefetch.map((i) => i.id));
|
||||
const workspacePrefetchById = new Map(
|
||||
workspacePrefetch.map((i) => [i.id, i]),
|
||||
);
|
||||
const idsToCreate = draft.filter((item) => !prefetchIds.has(item.id));
|
||||
const idsToRecreate = draft.filter((item) => {
|
||||
const original = workspacePrefetchById.get(item.id);
|
||||
if (!original) return false;
|
||||
return (
|
||||
original.viewId !== item.viewId ||
|
||||
original.targetObjectMetadataId !== item.targetObjectMetadataId ||
|
||||
original.targetRecordId !== item.targetRecordId
|
||||
);
|
||||
});
|
||||
|
||||
for (const draftItem of idsToRecreate) {
|
||||
await deleteNavigationMenuItem(draftItem.id);
|
||||
}
|
||||
|
||||
const idsToCreateIncludingRecreated = [
|
||||
...idsToCreate,
|
||||
...idsToRecreate,
|
||||
];
|
||||
|
||||
for (const draftItem of idsToCreateIncludingRecreated) {
|
||||
const input: {
|
||||
position: number;
|
||||
folderId?: string | null;
|
||||
name?: string;
|
||||
link?: string;
|
||||
icon?: string | null;
|
||||
viewId?: string;
|
||||
targetObjectMetadataId?: string;
|
||||
targetRecordId?: string;
|
||||
} = {
|
||||
position: Math.max(0, Math.round(draftItem.position)),
|
||||
};
|
||||
|
||||
if (isNavigationMenuItemFolder(draftItem)) {
|
||||
input.name = draftItem.name ?? undefined;
|
||||
input.icon = draftItem.icon ?? null;
|
||||
} else if (isNavigationMenuItemLink(draftItem)) {
|
||||
input.name = draftItem.name ?? 'Link';
|
||||
const linkUrl = (draftItem.link ?? '').trim();
|
||||
input.link =
|
||||
linkUrl.startsWith('http://') || linkUrl.startsWith('https://')
|
||||
? linkUrl
|
||||
: linkUrl
|
||||
? `https://${linkUrl}`
|
||||
: undefined;
|
||||
} else if (isDefined(draftItem.viewId)) {
|
||||
input.viewId = draftItem.viewId;
|
||||
} else if (isDefined(draftItem.targetRecordId)) {
|
||||
input.targetRecordId = draftItem.targetRecordId;
|
||||
input.targetObjectMetadataId =
|
||||
draftItem.targetObjectMetadataId ?? undefined;
|
||||
}
|
||||
|
||||
if (isDefined(draftItem.folderId)) {
|
||||
input.folderId = draftItem.folderId;
|
||||
}
|
||||
|
||||
await createNavigationMenuItemMutation({
|
||||
variables: { input },
|
||||
});
|
||||
}
|
||||
|
||||
const idsToRecreateSet = new Set(idsToRecreate.map((i) => i.id));
|
||||
for (const draftItem of draft) {
|
||||
if (idsToRecreateSet.has(draftItem.id)) continue;
|
||||
|
||||
const original = workspacePrefetchById.get(draftItem.id);
|
||||
if (!original) continue;
|
||||
|
||||
const positionChanged = original.position !== draftItem.position;
|
||||
const folderIdChanged =
|
||||
(original.folderId ?? null) !== (draftItem.folderId ?? null);
|
||||
const nameChanged =
|
||||
(isNavigationMenuItemFolder(draftItem) ||
|
||||
isNavigationMenuItemLink(draftItem)) &&
|
||||
(original.name ?? null) !== (draftItem.name ?? null);
|
||||
const linkChanged =
|
||||
isNavigationMenuItemLink(draftItem) &&
|
||||
(original.link ?? null) !== (draftItem.link ?? null);
|
||||
const iconChanged =
|
||||
isNavigationMenuItemFolder(draftItem) &&
|
||||
(original.icon ?? null) !== (draftItem.icon ?? null);
|
||||
|
||||
if (
|
||||
positionChanged ||
|
||||
folderIdChanged ||
|
||||
nameChanged ||
|
||||
linkChanged ||
|
||||
iconChanged
|
||||
) {
|
||||
const updateInput: {
|
||||
id: string;
|
||||
position?: number;
|
||||
folderId?: string | null;
|
||||
name?: string;
|
||||
link?: string | null;
|
||||
icon?: string | null;
|
||||
} = { id: draftItem.id };
|
||||
|
||||
if (positionChanged) {
|
||||
updateInput.position = Math.max(
|
||||
0,
|
||||
Math.round(draftItem.position),
|
||||
);
|
||||
}
|
||||
if (folderIdChanged) {
|
||||
updateInput.folderId = draftItem.folderId ?? null;
|
||||
}
|
||||
if (nameChanged && isNavigationMenuItemFolder(draftItem)) {
|
||||
updateInput.name = draftItem.name ?? undefined;
|
||||
}
|
||||
if (nameChanged && isNavigationMenuItemLink(draftItem)) {
|
||||
updateInput.name = draftItem.name ?? undefined;
|
||||
}
|
||||
if (linkChanged && isNavigationMenuItemLink(draftItem)) {
|
||||
const linkUrl = (draftItem.link ?? '').trim();
|
||||
updateInput.link = linkUrl
|
||||
? linkUrl.startsWith('http://') ||
|
||||
linkUrl.startsWith('https://')
|
||||
? linkUrl
|
||||
: `https://${linkUrl}`
|
||||
: null;
|
||||
}
|
||||
if (iconChanged && isNavigationMenuItemFolder(draftItem)) {
|
||||
updateInput.icon = draftItem.icon ?? null;
|
||||
}
|
||||
|
||||
await updateNavigationMenuItem(updateInput);
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
updateNavigationMenuItem,
|
||||
deleteNavigationMenuItem,
|
||||
createNavigationMenuItemMutation,
|
||||
],
|
||||
);
|
||||
|
||||
return { saveDraft };
|
||||
};
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { useWorkspaceSectionItems } from '@/navigation-menu-item/hooks/useWorkspaceSectionItems';
|
||||
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/states/selectedNavigationMenuItemInEditModeState';
|
||||
|
||||
export const useSelectedNavigationMenuItemEditItem = () => {
|
||||
const selectedNavigationMenuItemInEditMode = useRecoilValue(
|
||||
selectedNavigationMenuItemInEditModeState,
|
||||
);
|
||||
const items = useWorkspaceSectionItems();
|
||||
|
||||
const selectedItem = selectedNavigationMenuItemInEditMode
|
||||
? items.find((item) => item.id === selectedNavigationMenuItemInEditMode)
|
||||
: undefined;
|
||||
|
||||
return { selectedItem };
|
||||
};
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import { useSelectedNavigationMenuItemEditItem } from '@/navigation-menu-item/hooks/useSelectedNavigationMenuItemEditItem';
|
||||
import { useSelectedNavigationMenuItemEditItemObjectMetadata } from '@/navigation-menu-item/hooks/useSelectedNavigationMenuItemEditItemObjectMetadata';
|
||||
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
|
||||
|
||||
export const useSelectedNavigationMenuItemEditItemLabel = () => {
|
||||
const { selectedItem } = useSelectedNavigationMenuItemEditItem();
|
||||
const { selectedItemObjectMetadata } =
|
||||
useSelectedNavigationMenuItemEditItemObjectMetadata();
|
||||
|
||||
const selectedItemLabel = selectedItem
|
||||
? selectedItem.itemType === NavigationMenuItemType.FOLDER
|
||||
? (selectedItem.name ?? 'Folder')
|
||||
: selectedItem.itemType === NavigationMenuItemType.LINK
|
||||
? (selectedItem.name ?? 'Link')
|
||||
: (selectedItemObjectMetadata?.labelPlural ?? '')
|
||||
: null;
|
||||
|
||||
return { selectedItemLabel };
|
||||
};
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { useSelectedNavigationMenuItemEditItem } from '@/navigation-menu-item/hooks/useSelectedNavigationMenuItemEditItem';
|
||||
import { getObjectMetadataForNavigationMenuItem } from '@/navigation-menu-item/utils/getObjectMetadataForNavigationMenuItem';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { coreViewsState } from '@/views/states/coreViewState';
|
||||
import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView';
|
||||
|
||||
export const useSelectedNavigationMenuItemEditItemObjectMetadata = () => {
|
||||
const { selectedItem } = useSelectedNavigationMenuItemEditItem();
|
||||
const coreViews = useRecoilValue(coreViewsState);
|
||||
const views = coreViews.map(convertCoreViewToView);
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
|
||||
const selectedItemObjectMetadata = selectedItem
|
||||
? getObjectMetadataForNavigationMenuItem(
|
||||
selectedItem,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
)
|
||||
: null;
|
||||
|
||||
return { selectedItemObjectMetadata };
|
||||
};
|
||||
+9
-1
@@ -2,6 +2,8 @@ import { useMemo } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { isNavigationMenuItemFolder } from '@/navigation-menu-item/utils/isNavigationMenuItemFolder';
|
||||
import { isNavigationMenuItemLink } from '@/navigation-menu-item/utils/isNavigationMenuItemLink';
|
||||
import { recordIdentifierToObjectRecordIdentifier } from '@/navigation-menu-item/utils/recordIdentifierToObjectRecordIdentifier';
|
||||
import { sortNavigationMenuItems } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
@@ -76,6 +78,12 @@ export const useSortedNavigationMenuItems = () => {
|
||||
|
||||
const workspaceNavigationMenuItemsSorted = useMemo(() => {
|
||||
const filtered = workspaceNavigationMenuItems.filter((item) => {
|
||||
if (isNavigationMenuItemFolder(item)) {
|
||||
return true;
|
||||
}
|
||||
if (isNavigationMenuItemLink(item)) {
|
||||
return true;
|
||||
}
|
||||
if (isDefined(item.viewId)) {
|
||||
return coreViews.some((view) => view.id === item.viewId);
|
||||
}
|
||||
@@ -90,7 +98,7 @@ export const useSortedNavigationMenuItems = () => {
|
||||
});
|
||||
return sortNavigationMenuItems(
|
||||
filtered,
|
||||
false,
|
||||
true,
|
||||
coreViews,
|
||||
objectMetadataItems,
|
||||
targetRecordIdentifiers,
|
||||
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
|
||||
import { navigationMenuItemsDraftState } from '@/navigation-menu-item/states/navigationMenuItemsDraftState';
|
||||
import { isNavigationMenuItemFolder } from '@/navigation-menu-item/utils/isNavigationMenuItemFolder';
|
||||
|
||||
export type UpdateFolderInDraftUpdates = {
|
||||
name?: string;
|
||||
icon?: string;
|
||||
};
|
||||
|
||||
export const useUpdateFolderInDraft = () => {
|
||||
const setNavigationMenuItemsDraft = useSetRecoilState(
|
||||
navigationMenuItemsDraftState,
|
||||
);
|
||||
|
||||
const updateFolderInDraft = (
|
||||
folderId: string,
|
||||
updates: UpdateFolderInDraftUpdates,
|
||||
) => {
|
||||
setNavigationMenuItemsDraft((draft) => {
|
||||
if (!draft) return draft;
|
||||
|
||||
return draft.map((item) => {
|
||||
if (!isNavigationMenuItemFolder(item) || item.id !== folderId) {
|
||||
return item;
|
||||
}
|
||||
return { ...item, ...updates };
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return { updateFolderInDraft };
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
|
||||
import { navigationMenuItemsDraftState } from '@/navigation-menu-item/states/navigationMenuItemsDraftState';
|
||||
import { isNavigationMenuItemLink } from '@/navigation-menu-item/utils/isNavigationMenuItemLink';
|
||||
|
||||
export const useUpdateLinkInDraft = () => {
|
||||
const setNavigationMenuItemsDraft = useSetRecoilState(
|
||||
navigationMenuItemsDraftState,
|
||||
);
|
||||
|
||||
const updateLinkInDraft = (
|
||||
linkId: string,
|
||||
updates: { name?: string; link?: string },
|
||||
) => {
|
||||
setNavigationMenuItemsDraft((draft) => {
|
||||
if (!draft) return draft;
|
||||
|
||||
return draft.map((item) => {
|
||||
if (item.id !== linkId || !isNavigationMenuItemLink(item)) return item;
|
||||
|
||||
const updated = { ...item };
|
||||
if (updates.name !== undefined) {
|
||||
updated.name = updates.name.trim() || 'Link';
|
||||
}
|
||||
if (updates.link !== undefined && updates.link.trim() !== '') {
|
||||
updated.link = updates.link.trim();
|
||||
}
|
||||
return updated;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return { updateLinkInDraft };
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
|
||||
import { navigationMenuItemsDraftState } from '@/navigation-menu-item/states/navigationMenuItemsDraftState';
|
||||
import { type View } from '@/views/types/View';
|
||||
|
||||
export const useUpdateViewInDraft = () => {
|
||||
const setNavigationMenuItemsDraft = useSetRecoilState(
|
||||
navigationMenuItemsDraftState,
|
||||
);
|
||||
|
||||
const updateViewInDraft = (navigationMenuItemId: string, view: View) => {
|
||||
setNavigationMenuItemsDraft((draft) => {
|
||||
if (!draft) return draft;
|
||||
|
||||
return draft.map((item) =>
|
||||
item.id === navigationMenuItemId
|
||||
? {
|
||||
...item,
|
||||
viewId: view.id,
|
||||
targetObjectMetadataId: undefined,
|
||||
targetRecordId: undefined,
|
||||
}
|
||||
: item,
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
return { updateViewInDraft };
|
||||
};
|
||||
+24
-4
@@ -1,5 +1,7 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { isNavigationMenuItemFolder } from '@/navigation-menu-item/utils/isNavigationMenuItemFolder';
|
||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { coreViewsState } from '@/views/states/coreViewState';
|
||||
@@ -7,20 +9,38 @@ import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { usePrefetchedNavigationMenuItemsData } from './usePrefetchedNavigationMenuItemsData';
|
||||
import { useSortedNavigationMenuItems } from './useSortedNavigationMenuItems';
|
||||
|
||||
export const useWorkspaceNavigationMenuItems = (): {
|
||||
workspaceNavigationMenuItemsObjectMetadataItems: ObjectMetadataItem[];
|
||||
} => {
|
||||
const { workspaceNavigationMenuItemsSorted } = useSortedNavigationMenuItems();
|
||||
const { workspaceNavigationMenuItems: rawWorkspaceNavigationMenuItems } =
|
||||
usePrefetchedNavigationMenuItemsData();
|
||||
const coreViews = useRecoilValue(coreViewsState);
|
||||
|
||||
const views = coreViews.map(convertCoreViewToView);
|
||||
|
||||
const workspaceFolderIds = useMemo(
|
||||
() =>
|
||||
new Set(
|
||||
rawWorkspaceNavigationMenuItems
|
||||
.filter(isNavigationMenuItemFolder)
|
||||
.map((item) => item.id),
|
||||
),
|
||||
[rawWorkspaceNavigationMenuItems],
|
||||
);
|
||||
|
||||
const workspaceNavigationMenuItemsIncludingFolderItems = useMemo(
|
||||
() =>
|
||||
rawWorkspaceNavigationMenuItems.filter(
|
||||
(item) =>
|
||||
!isDefined(item.folderId) ||
|
||||
(isDefined(item.folderId) && workspaceFolderIds.has(item.folderId)),
|
||||
),
|
||||
[rawWorkspaceNavigationMenuItems, workspaceFolderIds],
|
||||
);
|
||||
|
||||
const workspaceNavigationMenuItemViewIds = new Set(
|
||||
workspaceNavigationMenuItemsSorted
|
||||
workspaceNavigationMenuItemsIncludingFolderItems
|
||||
.map((item) => item.viewId)
|
||||
.filter((viewId) => isDefined(viewId)),
|
||||
);
|
||||
@@ -35,7 +55,7 @@ export const useWorkspaceNavigationMenuItems = (): {
|
||||
);
|
||||
|
||||
const navigationMenuItemRecordObjectMetadataIds = new Set(
|
||||
rawWorkspaceNavigationMenuItems
|
||||
workspaceNavigationMenuItemsIncludingFolderItems
|
||||
.map((item) => item.targetObjectMetadataId)
|
||||
.filter((objectMetadataId) => isDefined(objectMetadataId)),
|
||||
);
|
||||
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
import { getObjectMetadataForNavigationMenuItem } from '@/navigation-menu-item/utils/getObjectMetadataForNavigationMenuItem';
|
||||
import { FOLDER_ICON_DEFAULT } from '@/navigation-menu-item/constants/folderIconDefault';
|
||||
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
|
||||
import { isNavigationMenuItemFolder } from '@/navigation-menu-item/utils/isNavigationMenuItemFolder';
|
||||
import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/types/processed-navigation-menu-item';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { coreViewsState } from '@/views/states/coreViewState';
|
||||
import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { useNavigationMenuItemsByFolder } from './useNavigationMenuItemsByFolder';
|
||||
import { usePrefetchedNavigationMenuItemsData } from './usePrefetchedNavigationMenuItemsData';
|
||||
import { useSortedNavigationMenuItems } from './useSortedNavigationMenuItems';
|
||||
|
||||
export type FlatWorkspaceItem =
|
||||
| ProcessedNavigationMenuItem
|
||||
| (NavigationMenuItem & {
|
||||
itemType: NavigationMenuItemType.FOLDER;
|
||||
Icon: string;
|
||||
});
|
||||
|
||||
export type NavigationMenuItemClickParams = {
|
||||
item: FlatWorkspaceItem;
|
||||
objectMetadataItem?: ObjectMetadataItem | null;
|
||||
};
|
||||
|
||||
export const useWorkspaceSectionItems = (): FlatWorkspaceItem[] => {
|
||||
const { workspaceNavigationMenuItems } =
|
||||
usePrefetchedNavigationMenuItemsData();
|
||||
const { workspaceNavigationMenuItemsSorted } = useSortedNavigationMenuItems();
|
||||
const { workspaceNavigationMenuItemsByFolder } =
|
||||
useNavigationMenuItemsByFolder();
|
||||
const coreViews = useRecoilValue(coreViewsState);
|
||||
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
|
||||
|
||||
const views = coreViews.map(convertCoreViewToView);
|
||||
|
||||
const flatWorkspaceItems = workspaceNavigationMenuItems
|
||||
.filter((item) => !isDefined(item.folderId))
|
||||
.sort((a, b) => a.position - b.position);
|
||||
|
||||
const processedObjectViewsById = new Map(
|
||||
workspaceNavigationMenuItemsSorted.map((item) => [item.id, item]),
|
||||
);
|
||||
|
||||
const folderChildrenById = new Map(
|
||||
workspaceNavigationMenuItemsByFolder.map((folder) => [
|
||||
folder.id,
|
||||
folder.navigationMenuItems,
|
||||
]),
|
||||
);
|
||||
|
||||
const flatItems: FlatWorkspaceItem[] = flatWorkspaceItems.reduce<
|
||||
FlatWorkspaceItem[]
|
||||
>((acc, item) => {
|
||||
if (isNavigationMenuItemFolder(item)) {
|
||||
acc.push({
|
||||
...item,
|
||||
itemType: NavigationMenuItemType.FOLDER,
|
||||
Icon: item.icon ?? FOLDER_ICON_DEFAULT,
|
||||
});
|
||||
} else {
|
||||
const processedItem = processedObjectViewsById.get(item.id);
|
||||
if (!isDefined(processedItem)) {
|
||||
return acc;
|
||||
}
|
||||
if (processedItem.itemType === NavigationMenuItemType.LINK) {
|
||||
acc.push(processedItem);
|
||||
} else {
|
||||
const objectMetadataItem = getObjectMetadataForNavigationMenuItem(
|
||||
processedItem,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
);
|
||||
if (isDefined(objectMetadataItem)) {
|
||||
acc.push(processedItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
return flatItems.flatMap((item) =>
|
||||
item.itemType === NavigationMenuItemType.FOLDER
|
||||
? [item, ...(folderChildrenById.get(item.id) ?? [])]
|
||||
: [item],
|
||||
);
|
||||
};
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
import type { AddMenuItemInsertionContext } from '@/navigation-menu-item/types/AddMenuItemInsertionContext';
|
||||
|
||||
export const addMenuItemInsertionContextState =
|
||||
atom<AddMenuItemInsertionContext | null>({
|
||||
key: 'addMenuItemInsertionContextState',
|
||||
default: null,
|
||||
});
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
import type { AddToNavigationDragPayload } from '@/navigation-menu-item/types/add-to-navigation-drag-payload';
|
||||
|
||||
export const addToNavPayloadRegistryState = atom<
|
||||
Map<string, AddToNavigationDragPayload>
|
||||
>({
|
||||
key: 'navigation-menu-item/addToNavPayloadRegistryState',
|
||||
default: new Map(),
|
||||
});
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
export const isNavigationMenuInEditModeState = atom<boolean>({
|
||||
key: 'isNavigationMenuInEditModeState',
|
||||
default: false,
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user