Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce428985aa | ||
|
|
2039986684 | ||
|
|
1ed9de2300 | ||
|
|
ee2810281e | ||
|
|
50bd91262f | ||
|
|
bf92860d19 | ||
|
|
22203bfd3c | ||
|
|
d747366bf3 | ||
|
|
7a3e92fe0b | ||
|
|
ba51c091f0 | ||
|
|
f269f8b905 | ||
|
|
6fb81e757b | ||
|
|
0571eb2cf6 | ||
|
|
5863c45d4b | ||
|
|
29d079babc | ||
|
|
ec9587414b | ||
|
|
311fc402d5 | ||
|
|
4720afe3fb | ||
|
|
9f454c565b | ||
|
|
e301c7856b | ||
|
|
c46f7848b7 |
@@ -1,7 +1,7 @@
|
||||
# Pull down translations from Crowdin every two hours or when triggered manually.
|
||||
# When force_pull input is true, translations will be pulled regardless of compilation status.
|
||||
|
||||
name: 'Pull translations'
|
||||
name: 'Pull translations from Crowdin'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
@@ -53,7 +53,7 @@ jobs:
|
||||
|
||||
# Strict mode fails if there are missing translations.
|
||||
- name: Compile translations
|
||||
id: compile_translations
|
||||
id: compile_translations_strict
|
||||
run: |
|
||||
npx nx run twenty-server:lingui:compile --strict
|
||||
npx nx run twenty-emails:lingui:compile --strict
|
||||
@@ -68,7 +68,7 @@ jobs:
|
||||
git stash
|
||||
|
||||
- name: Pull translations from Crowdin
|
||||
if: inputs.force_pull || steps.compile_translations.outcome == 'failure'
|
||||
if: inputs.force_pull || steps.compile_translations_strict.outcome == 'failure'
|
||||
uses: crowdin/github-action@v2
|
||||
with:
|
||||
upload_sources: false
|
||||
@@ -98,18 +98,13 @@ jobs:
|
||||
run: sudo chown -R runner:docker .
|
||||
|
||||
- name: Compile translations
|
||||
if: inputs.force_pull || steps.compile_translations.outcome == 'failure'
|
||||
id: compile_translations
|
||||
if: inputs.force_pull || steps.compile_translations_strict.outcome == 'failure'
|
||||
run: |
|
||||
npx nx run twenty-server:lingui:compile
|
||||
npx nx run twenty-emails:lingui:compile
|
||||
npx nx run twenty-front:lingui:compile
|
||||
|
||||
- name: Debug git status
|
||||
run: git status
|
||||
|
||||
- name: Check and commit compiled files
|
||||
id: check_changes
|
||||
run: |
|
||||
git status
|
||||
git config --global user.name 'github-actions'
|
||||
git config --global user.email '[email protected]'
|
||||
git add .
|
||||
@@ -121,11 +116,11 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Push changes
|
||||
if: steps.check_changes.outputs.changes_detected == 'true'
|
||||
if: steps.compile_translations.outputs.changes_detected == 'true'
|
||||
run: git push origin HEAD:i18n
|
||||
|
||||
- name: Create pull request
|
||||
if: steps.check_changes.outputs.changes_detected == 'true'
|
||||
if: steps.compile_translations.outputs.changes_detected == 'true'
|
||||
run: |
|
||||
if git diff --name-only origin/main..HEAD | grep -q .; then
|
||||
gh pr create -B main -H i18n --title 'i18n - translations' --body 'Created by Github action' || true
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: 'Extract translations when there is a push to main, and upload them to Crowdin'
|
||||
name: 'Push translations to Crowdin'
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -16,6 +16,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "twenty-e2e-testing",
|
||||
"version": "0.42.11",
|
||||
"version": "0.42.15",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "twenty-emails",
|
||||
"version": "0.42.11",
|
||||
"version": "0.42.15",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { Img } from '@react-email/components';
|
||||
import { emailTheme } from 'src/common-style';
|
||||
|
||||
import { BaseEmail } from 'src/components/BaseEmail';
|
||||
import { CallToAction } from 'src/components/CallToAction';
|
||||
import { HighlightedContainer } from 'src/components/HighlightedContainer';
|
||||
import { HighlightedText } from 'src/components/HighlightedText';
|
||||
import { Link } from 'src/components/Link';
|
||||
import { MainText } from 'src/components/MainText';
|
||||
import { Title } from 'src/components/Title';
|
||||
import { WhatIsTwenty } from 'src/components/WhatIsTwenty';
|
||||
import { capitalize } from 'src/utils/capitalize';
|
||||
import { APP_LOCALES, getImageAbsoluteURI } from 'twenty-shared';
|
||||
|
||||
type SendApprovedAccessDomainValidationProps = {
|
||||
link: string;
|
||||
domain: string;
|
||||
workspace: { name: string | undefined; logo: string | undefined };
|
||||
sender: {
|
||||
email: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
};
|
||||
serverUrl: string;
|
||||
locale: keyof typeof APP_LOCALES;
|
||||
};
|
||||
|
||||
export const SendApprovedAccessDomainValidation = ({
|
||||
link,
|
||||
domain,
|
||||
workspace,
|
||||
sender,
|
||||
serverUrl,
|
||||
locale,
|
||||
}: SendApprovedAccessDomainValidationProps) => {
|
||||
const workspaceLogo = workspace.logo
|
||||
? getImageAbsoluteURI({ imageUrl: workspace.logo, baseUrl: serverUrl })
|
||||
: null;
|
||||
|
||||
return (
|
||||
<BaseEmail width={333} locale={locale}>
|
||||
<Title value={t`Validate domain`} />
|
||||
<MainText>
|
||||
{capitalize(sender.firstName)} (
|
||||
<Link
|
||||
href={`mailto:${sender.email}`}
|
||||
value={sender.email}
|
||||
color={emailTheme.font.colors.blue}
|
||||
/>
|
||||
)
|
||||
<Trans>
|
||||
Please validate this domain to allow users with <b>@{domain}</b> email
|
||||
addresses to join your workspace without requiring an invitation.
|
||||
</Trans>
|
||||
<br />
|
||||
</MainText>
|
||||
<HighlightedContainer>
|
||||
{workspaceLogo && <Img src={workspaceLogo} width={40} height={40} />}
|
||||
{workspace.name && <HighlightedText value={workspace.name} />}
|
||||
<CallToAction href={link} value={t`Validate domain`} />
|
||||
</HighlightedContainer>
|
||||
<WhatIsTwenty />
|
||||
</BaseEmail>
|
||||
);
|
||||
};
|
||||
@@ -4,3 +4,4 @@ export * from './emails/password-update-notify.email';
|
||||
export * from './emails/send-email-verification-link.email';
|
||||
export * from './emails/send-invite-link.email';
|
||||
export * from './emails/warn-suspended-workspace.email';
|
||||
export * from './emails/validate-approved-access-domain.email';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "twenty-front",
|
||||
"version": "0.42.11",
|
||||
"version": "0.42.15",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -102,6 +102,14 @@ export type AppTokenEdge = {
|
||||
node: AppToken;
|
||||
};
|
||||
|
||||
export type ApprovedAccessDomain = {
|
||||
__typename?: 'ApprovedAccessDomain';
|
||||
createdAt: Scalars['DateTime']['output'];
|
||||
domain: Scalars['String']['output'];
|
||||
id: Scalars['UUID']['output'];
|
||||
isValidated: Scalars['Boolean']['output'];
|
||||
};
|
||||
|
||||
export type AuthProviders = {
|
||||
__typename?: 'AuthProviders';
|
||||
google: Scalars['Boolean']['output'];
|
||||
@@ -300,6 +308,11 @@ export type CreateAppTokenInput = {
|
||||
expiresAt: Scalars['DateTime']['input'];
|
||||
};
|
||||
|
||||
export type CreateApprovedAccessDomainInput = {
|
||||
domain: Scalars['String']['input'];
|
||||
email: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
export type CreateDraftFromWorkflowVersionInput = {
|
||||
/** Workflow ID */
|
||||
workflowId: Scalars['String']['input'];
|
||||
@@ -423,6 +436,10 @@ export type CustomDomainValidRecords = {
|
||||
records: Array<CustomDomainRecord>;
|
||||
};
|
||||
|
||||
export type DeleteApprovedAccessDomainInput = {
|
||||
id: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
export type DeleteOneFieldInput = {
|
||||
/** The id of the field to delete. */
|
||||
id: Scalars['UUID']['input'];
|
||||
@@ -545,6 +562,7 @@ export enum FeatureFlagKey {
|
||||
IsAdvancedFiltersEnabled = 'IsAdvancedFiltersEnabled',
|
||||
IsAirtableIntegrationEnabled = 'IsAirtableIntegrationEnabled',
|
||||
IsAnalyticsV2Enabled = 'IsAnalyticsV2Enabled',
|
||||
IsApprovedAccessDomainsEnabled = 'IsApprovedAccessDomainsEnabled',
|
||||
IsBillingPlansEnabled = 'IsBillingPlansEnabled',
|
||||
IsCommandMenuV2Enabled = 'IsCommandMenuV2Enabled',
|
||||
IsCopilotEnabled = 'IsCopilotEnabled',
|
||||
@@ -833,6 +851,7 @@ export type Mutation = {
|
||||
checkCustomDomainValidRecords?: Maybe<CustomDomainValidRecords>;
|
||||
checkoutSession: BillingSessionOutput;
|
||||
computeStepOutputSchema: Scalars['JSON']['output'];
|
||||
createApprovedAccessDomain: ApprovedAccessDomain;
|
||||
createDraftFromWorkflowVersion: WorkflowVersion;
|
||||
createOIDCIdentityProvider: SetupSsoOutput;
|
||||
createOneAppToken: AppToken;
|
||||
@@ -844,6 +863,7 @@ export type Mutation = {
|
||||
createSAMLIdentityProvider: SetupSsoOutput;
|
||||
createWorkflowVersionStep: WorkflowAction;
|
||||
deactivateWorkflowVersion: Scalars['Boolean']['output'];
|
||||
deleteApprovedAccessDomain: Scalars['Boolean']['output'];
|
||||
deleteCurrentWorkspace: Workspace;
|
||||
deleteOneField: Field;
|
||||
deleteOneObject: Object;
|
||||
@@ -894,6 +914,7 @@ export type Mutation = {
|
||||
uploadProfilePicture: Scalars['String']['output'];
|
||||
uploadWorkspaceLogo: Scalars['String']['output'];
|
||||
userLookupAdminPanel: UserLookup;
|
||||
validateApprovedAccessDomain: ApprovedAccessDomain;
|
||||
};
|
||||
|
||||
|
||||
@@ -927,6 +948,11 @@ export type MutationComputeStepOutputSchemaArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationCreateApprovedAccessDomainArgs = {
|
||||
input: CreateApprovedAccessDomainInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationCreateDraftFromWorkflowVersionArgs = {
|
||||
input: CreateDraftFromWorkflowVersionInput;
|
||||
};
|
||||
@@ -982,6 +1008,11 @@ export type MutationDeactivateWorkflowVersionArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationDeleteApprovedAccessDomainArgs = {
|
||||
input: DeleteApprovedAccessDomainInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationDeleteOneFieldArgs = {
|
||||
input: DeleteOneFieldInput;
|
||||
};
|
||||
@@ -1214,6 +1245,11 @@ export type MutationUserLookupAdminPanelArgs = {
|
||||
userIdentifier: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
|
||||
export type MutationValidateApprovedAccessDomainArgs = {
|
||||
input: ValidateApprovedAccessDomainInput;
|
||||
};
|
||||
|
||||
export type Object = {
|
||||
__typename?: 'Object';
|
||||
createdAt: Scalars['DateTime']['output'];
|
||||
@@ -1382,6 +1418,7 @@ export type Query = {
|
||||
findOneServerlessFunction: ServerlessFunction;
|
||||
findWorkspaceFromInviteHash: Workspace;
|
||||
findWorkspaceInvitations: Array<WorkspaceInvitation>;
|
||||
getApprovedAccessDomains: Array<ApprovedAccessDomain>;
|
||||
getAvailablePackages: Scalars['JSON']['output'];
|
||||
getEnvironmentVariablesGrouped: EnvironmentVariablesOutput;
|
||||
getIndicatorHealthStatus: AdminPanelHealthServiceData;
|
||||
@@ -1767,7 +1804,7 @@ export enum ServerlessFunctionSyncStatus {
|
||||
READY = 'READY'
|
||||
}
|
||||
|
||||
export enum SettingsFeatures {
|
||||
export enum SettingsPermissions {
|
||||
ADMIN_PANEL = 'ADMIN_PANEL',
|
||||
API_KEYS_AND_WEBHOOKS = 'API_KEYS_AND_WEBHOOKS',
|
||||
DATA_MODEL = 'DATA_MODEL',
|
||||
@@ -2093,7 +2130,7 @@ export type UserWorkspace = {
|
||||
deletedAt?: Maybe<Scalars['DateTime']['output']>;
|
||||
id: Scalars['UUID']['output'];
|
||||
objectRecordsPermissions?: Maybe<Array<PermissionsOnAllObjectRecords>>;
|
||||
settingsPermissions?: Maybe<Array<SettingsFeatures>>;
|
||||
settingsPermissions?: Maybe<Array<SettingsPermissions>>;
|
||||
updatedAt: Scalars['DateTime']['output'];
|
||||
user: User;
|
||||
userId: Scalars['String']['output'];
|
||||
@@ -2101,6 +2138,11 @@ export type UserWorkspace = {
|
||||
workspaceId: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
export type ValidateApprovedAccessDomainInput = {
|
||||
approvedAccessDomainId: Scalars['String']['input'];
|
||||
validationToken: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
export type ValidatePasswordResetToken = {
|
||||
__typename?: 'ValidatePasswordResetToken';
|
||||
email: Scalars['String']['output'];
|
||||
|
||||
@@ -27,7 +27,10 @@ export type ActivateWorkspaceInput = {
|
||||
|
||||
export type AdminPanelHealthServiceData = {
|
||||
__typename?: 'AdminPanelHealthServiceData';
|
||||
description: Scalars['String'];
|
||||
details?: Maybe<Scalars['String']>;
|
||||
id: Scalars['String'];
|
||||
label: Scalars['String'];
|
||||
queues?: Maybe<Array<AdminPanelWorkerQueueHealth>>;
|
||||
status: AdminPanelHealthServiceStatus;
|
||||
};
|
||||
@@ -37,17 +40,11 @@ export enum AdminPanelHealthServiceStatus {
|
||||
OUTAGE = 'OUTAGE'
|
||||
}
|
||||
|
||||
export enum AdminPanelIndicatorHealthStatusInputEnum {
|
||||
DATABASE = 'DATABASE',
|
||||
MESSAGE_SYNC = 'MESSAGE_SYNC',
|
||||
REDIS = 'REDIS',
|
||||
WORKER = 'WORKER'
|
||||
}
|
||||
|
||||
export type AdminPanelWorkerQueueHealth = {
|
||||
__typename?: 'AdminPanelWorkerQueueHealth';
|
||||
id: Scalars['String'];
|
||||
metrics: WorkerQueueMetrics;
|
||||
name: Scalars['String'];
|
||||
queueName: Scalars['String'];
|
||||
status: AdminPanelHealthServiceStatus;
|
||||
workers: Scalars['Float'];
|
||||
};
|
||||
@@ -95,6 +92,14 @@ export type AppTokenEdge = {
|
||||
node: AppToken;
|
||||
};
|
||||
|
||||
export type ApprovedAccessDomain = {
|
||||
__typename?: 'ApprovedAccessDomain';
|
||||
createdAt: Scalars['DateTime'];
|
||||
domain: Scalars['String'];
|
||||
id: Scalars['UUID'];
|
||||
isValidated: Scalars['Boolean'];
|
||||
};
|
||||
|
||||
export type AuthProviders = {
|
||||
__typename?: 'AuthProviders';
|
||||
google: Scalars['Boolean'];
|
||||
@@ -289,6 +294,11 @@ export type ComputeStepOutputSchemaInput = {
|
||||
step: Scalars['JSON'];
|
||||
};
|
||||
|
||||
export type CreateApprovedAccessDomainInput = {
|
||||
domain: Scalars['String'];
|
||||
email: Scalars['String'];
|
||||
};
|
||||
|
||||
export type CreateDraftFromWorkflowVersionInput = {
|
||||
/** Workflow ID */
|
||||
workflowId: Scalars['String'];
|
||||
@@ -360,6 +370,10 @@ export type CustomDomainValidRecords = {
|
||||
records: Array<CustomDomainRecord>;
|
||||
};
|
||||
|
||||
export type DeleteApprovedAccessDomainInput = {
|
||||
id: Scalars['String'];
|
||||
};
|
||||
|
||||
export type DeleteOneFieldInput = {
|
||||
/** The id of the field to delete. */
|
||||
id: Scalars['UUID'];
|
||||
@@ -477,6 +491,7 @@ export enum FeatureFlagKey {
|
||||
IsAdvancedFiltersEnabled = 'IsAdvancedFiltersEnabled',
|
||||
IsAirtableIntegrationEnabled = 'IsAirtableIntegrationEnabled',
|
||||
IsAnalyticsV2Enabled = 'IsAnalyticsV2Enabled',
|
||||
IsApprovedAccessDomainsEnabled = 'IsApprovedAccessDomainsEnabled',
|
||||
IsBillingPlansEnabled = 'IsBillingPlansEnabled',
|
||||
IsCommandMenuV2Enabled = 'IsCommandMenuV2Enabled',
|
||||
IsCopilotEnabled = 'IsCopilotEnabled',
|
||||
@@ -614,6 +629,13 @@ export type GetServerlessFunctionSourceCodeInput = {
|
||||
version?: Scalars['String'];
|
||||
};
|
||||
|
||||
export enum HealthIndicatorId {
|
||||
connectedAccount = 'connectedAccount',
|
||||
database = 'database',
|
||||
redis = 'redis',
|
||||
worker = 'worker'
|
||||
}
|
||||
|
||||
export enum IdentityProviderType {
|
||||
OIDC = 'OIDC',
|
||||
SAML = 'SAML'
|
||||
@@ -758,6 +780,7 @@ export type Mutation = {
|
||||
checkCustomDomainValidRecords?: Maybe<CustomDomainValidRecords>;
|
||||
checkoutSession: BillingSessionOutput;
|
||||
computeStepOutputSchema: Scalars['JSON'];
|
||||
createApprovedAccessDomain: ApprovedAccessDomain;
|
||||
createDraftFromWorkflowVersion: WorkflowVersion;
|
||||
createOIDCIdentityProvider: SetupSsoOutput;
|
||||
createOneAppToken: AppToken;
|
||||
@@ -767,6 +790,7 @@ export type Mutation = {
|
||||
createSAMLIdentityProvider: SetupSsoOutput;
|
||||
createWorkflowVersionStep: WorkflowAction;
|
||||
deactivateWorkflowVersion: Scalars['Boolean'];
|
||||
deleteApprovedAccessDomain: Scalars['Boolean'];
|
||||
deleteCurrentWorkspace: Workspace;
|
||||
deleteOneField: Field;
|
||||
deleteOneObject: Object;
|
||||
@@ -811,6 +835,7 @@ export type Mutation = {
|
||||
uploadProfilePicture: Scalars['String'];
|
||||
uploadWorkspaceLogo: Scalars['String'];
|
||||
userLookupAdminPanel: UserLookup;
|
||||
validateApprovedAccessDomain: ApprovedAccessDomain;
|
||||
};
|
||||
|
||||
|
||||
@@ -844,6 +869,11 @@ export type MutationComputeStepOutputSchemaArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationCreateApprovedAccessDomainArgs = {
|
||||
input: CreateApprovedAccessDomainInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationCreateDraftFromWorkflowVersionArgs = {
|
||||
input: CreateDraftFromWorkflowVersionInput;
|
||||
};
|
||||
@@ -879,6 +909,11 @@ export type MutationDeactivateWorkflowVersionArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationDeleteApprovedAccessDomainArgs = {
|
||||
input: DeleteApprovedAccessDomainInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationDeleteOneFieldArgs = {
|
||||
input: DeleteOneFieldInput;
|
||||
};
|
||||
@@ -1081,6 +1116,11 @@ export type MutationUserLookupAdminPanelArgs = {
|
||||
userIdentifier: Scalars['String'];
|
||||
};
|
||||
|
||||
|
||||
export type MutationValidateApprovedAccessDomainArgs = {
|
||||
input: ValidateApprovedAccessDomainInput;
|
||||
};
|
||||
|
||||
export type Object = {
|
||||
__typename?: 'Object';
|
||||
createdAt: Scalars['DateTime'];
|
||||
@@ -1246,6 +1286,7 @@ export type Query = {
|
||||
findOneServerlessFunction: ServerlessFunction;
|
||||
findWorkspaceFromInviteHash: Workspace;
|
||||
findWorkspaceInvitations: Array<WorkspaceInvitation>;
|
||||
getApprovedAccessDomains: Array<ApprovedAccessDomain>;
|
||||
getAvailablePackages: Scalars['JSON'];
|
||||
getEnvironmentVariablesGrouped: EnvironmentVariablesOutput;
|
||||
getIndicatorHealthStatus: AdminPanelHealthServiceData;
|
||||
@@ -1306,7 +1347,7 @@ export type QueryGetAvailablePackagesArgs = {
|
||||
|
||||
|
||||
export type QueryGetIndicatorHealthStatusArgs = {
|
||||
indicatorName: AdminPanelIndicatorHealthStatusInputEnum;
|
||||
indicatorId: HealthIndicatorId;
|
||||
};
|
||||
|
||||
|
||||
@@ -1563,7 +1604,7 @@ export enum ServerlessFunctionSyncStatus {
|
||||
READY = 'READY'
|
||||
}
|
||||
|
||||
export enum SettingsFeatures {
|
||||
export enum SettingsPermissions {
|
||||
ADMIN_PANEL = 'ADMIN_PANEL',
|
||||
API_KEYS_AND_WEBHOOKS = 'API_KEYS_AND_WEBHOOKS',
|
||||
DATA_MODEL = 'DATA_MODEL',
|
||||
@@ -1630,10 +1671,14 @@ export type Support = {
|
||||
|
||||
export type SystemHealth = {
|
||||
__typename?: 'SystemHealth';
|
||||
database: AdminPanelHealthServiceData;
|
||||
messageSync: AdminPanelHealthServiceData;
|
||||
redis: AdminPanelHealthServiceData;
|
||||
worker: AdminPanelHealthServiceData;
|
||||
services: Array<SystemHealthService>;
|
||||
};
|
||||
|
||||
export type SystemHealthService = {
|
||||
__typename?: 'SystemHealthService';
|
||||
id: HealthIndicatorId;
|
||||
label: Scalars['String'];
|
||||
status: AdminPanelHealthServiceStatus;
|
||||
};
|
||||
|
||||
export type TimelineCalendarEvent = {
|
||||
@@ -1871,7 +1916,7 @@ export type UserWorkspace = {
|
||||
deletedAt?: Maybe<Scalars['DateTime']>;
|
||||
id: Scalars['UUID'];
|
||||
objectRecordsPermissions?: Maybe<Array<PermissionsOnAllObjectRecords>>;
|
||||
settingsPermissions?: Maybe<Array<SettingsFeatures>>;
|
||||
settingsPermissions?: Maybe<Array<SettingsPermissions>>;
|
||||
updatedAt: Scalars['DateTime'];
|
||||
user: User;
|
||||
userId: Scalars['String'];
|
||||
@@ -1879,6 +1924,11 @@ export type UserWorkspace = {
|
||||
workspaceId: Scalars['String'];
|
||||
};
|
||||
|
||||
export type ValidateApprovedAccessDomainInput = {
|
||||
approvedAccessDomainId: Scalars['String'];
|
||||
validationToken: Scalars['String'];
|
||||
};
|
||||
|
||||
export type ValidatePasswordResetToken = {
|
||||
__typename?: 'ValidatePasswordResetToken';
|
||||
email: Scalars['String'];
|
||||
@@ -2292,16 +2342,16 @@ export type GetEnvironmentVariablesGroupedQueryVariables = Exact<{ [key: string]
|
||||
export type GetEnvironmentVariablesGroupedQuery = { __typename?: 'Query', getEnvironmentVariablesGrouped: { __typename?: 'EnvironmentVariablesOutput', groups: Array<{ __typename?: 'EnvironmentVariablesGroupData', name: EnvironmentVariablesGroup, description: string, isHiddenOnLoad: boolean, variables: Array<{ __typename?: 'EnvironmentVariable', name: string, description: string, value: string, sensitive: boolean }> }> } };
|
||||
|
||||
export type GetIndicatorHealthStatusQueryVariables = Exact<{
|
||||
indicatorName: AdminPanelIndicatorHealthStatusInputEnum;
|
||||
indicatorId: HealthIndicatorId;
|
||||
}>;
|
||||
|
||||
|
||||
export type GetIndicatorHealthStatusQuery = { __typename?: 'Query', getIndicatorHealthStatus: { __typename?: 'AdminPanelHealthServiceData', status: AdminPanelHealthServiceStatus, details?: string | null, queues?: Array<{ __typename?: 'AdminPanelWorkerQueueHealth', name: string, status: AdminPanelHealthServiceStatus, workers: number, metrics: { __typename?: 'WorkerQueueMetrics', failed: number, completed: number, waiting: number, active: number, delayed: number, prioritized: number } }> | null } };
|
||||
export type GetIndicatorHealthStatusQuery = { __typename?: 'Query', getIndicatorHealthStatus: { __typename?: 'AdminPanelHealthServiceData', id: string, label: string, description: string, status: AdminPanelHealthServiceStatus, details?: string | null, queues?: Array<{ __typename?: 'AdminPanelWorkerQueueHealth', id: string, queueName: string, status: AdminPanelHealthServiceStatus, workers: number, metrics: { __typename?: 'WorkerQueueMetrics', failed: number, completed: number, waiting: number, active: number, delayed: number, prioritized: number } }> | null } };
|
||||
|
||||
export type GetSystemHealthStatusQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type GetSystemHealthStatusQuery = { __typename?: 'Query', getSystemHealthStatus: { __typename?: 'SystemHealth', database: { __typename?: 'AdminPanelHealthServiceData', status: AdminPanelHealthServiceStatus, details?: string | null }, redis: { __typename?: 'AdminPanelHealthServiceData', status: AdminPanelHealthServiceStatus, details?: string | null }, worker: { __typename?: 'AdminPanelHealthServiceData', status: AdminPanelHealthServiceStatus, queues?: Array<{ __typename?: 'AdminPanelWorkerQueueHealth', name: string, workers: number, status: AdminPanelHealthServiceStatus, metrics: { __typename?: 'WorkerQueueMetrics', failed: number, completed: number, waiting: number, active: number, delayed: number, prioritized: number } }> | null }, messageSync: { __typename?: 'AdminPanelHealthServiceData', status: AdminPanelHealthServiceStatus, details?: string | null } } };
|
||||
export type GetSystemHealthStatusQuery = { __typename?: 'Query', getSystemHealthStatus: { __typename?: 'SystemHealth', services: Array<{ __typename?: 'SystemHealthService', id: HealthIndicatorId, label: string, status: AdminPanelHealthServiceStatus }> } };
|
||||
|
||||
export type UpdateLabPublicFeatureFlagMutationVariables = Exact<{
|
||||
input: UpdateLabPublicFeatureFlagInput;
|
||||
@@ -2325,6 +2375,13 @@ export type GetRolesQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
export type GetRolesQuery = { __typename?: 'Query', getRoles: Array<{ __typename?: 'Role', id: string, label: string, description?: string | null, canUpdateAllSettings: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean, workspaceMembers: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> }> };
|
||||
|
||||
export type CreateApprovedAccessDomainMutationVariables = Exact<{
|
||||
input: CreateApprovedAccessDomainInput;
|
||||
}>;
|
||||
|
||||
|
||||
export type CreateApprovedAccessDomainMutation = { __typename?: 'Mutation', createApprovedAccessDomain: { __typename?: 'ApprovedAccessDomain', id: any, domain: string, isValidated: boolean, createdAt: string } };
|
||||
|
||||
export type CreateOidcIdentityProviderMutationVariables = Exact<{
|
||||
input: SetupOidcSsoInput;
|
||||
}>;
|
||||
@@ -2339,6 +2396,13 @@ export type CreateSamlIdentityProviderMutationVariables = Exact<{
|
||||
|
||||
export type CreateSamlIdentityProviderMutation = { __typename?: 'Mutation', createSAMLIdentityProvider: { __typename?: 'SetupSsoOutput', id: string, type: IdentityProviderType, issuer: string, name: string, status: SsoIdentityProviderStatus } };
|
||||
|
||||
export type DeleteApprovedAccessDomainMutationVariables = Exact<{
|
||||
input: DeleteApprovedAccessDomainInput;
|
||||
}>;
|
||||
|
||||
|
||||
export type DeleteApprovedAccessDomainMutation = { __typename?: 'Mutation', deleteApprovedAccessDomain: boolean };
|
||||
|
||||
export type DeleteSsoIdentityProviderMutationVariables = Exact<{
|
||||
input: DeleteSsoInput;
|
||||
}>;
|
||||
@@ -2353,12 +2417,24 @@ export type EditSsoIdentityProviderMutationVariables = Exact<{
|
||||
|
||||
export type EditSsoIdentityProviderMutation = { __typename?: 'Mutation', editSSOIdentityProvider: { __typename?: 'EditSsoOutput', id: string, type: IdentityProviderType, issuer: string, name: string, status: SsoIdentityProviderStatus } };
|
||||
|
||||
export type ValidateApprovedAccessDomainMutationVariables = Exact<{
|
||||
input: ValidateApprovedAccessDomainInput;
|
||||
}>;
|
||||
|
||||
|
||||
export type ValidateApprovedAccessDomainMutation = { __typename?: 'Mutation', validateApprovedAccessDomain: { __typename?: 'ApprovedAccessDomain', id: any, isValidated: boolean, domain: string, createdAt: string } };
|
||||
|
||||
export type GetApprovedAccessDomainsQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type GetApprovedAccessDomainsQuery = { __typename?: 'Query', getApprovedAccessDomains: Array<{ __typename?: 'ApprovedAccessDomain', id: any, createdAt: string, domain: string, isValidated: boolean }> };
|
||||
|
||||
export type GetSsoIdentityProvidersQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type GetSsoIdentityProvidersQuery = { __typename?: 'Query', getSSOIdentityProviders: Array<{ __typename?: 'FindAvailableSSOIDPOutput', type: IdentityProviderType, id: string, name: string, issuer: string, status: SsoIdentityProviderStatus }> };
|
||||
|
||||
export type UserQueryFragmentFragment = { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, analyticsTinybirdJwts?: { __typename?: 'AnalyticsTinybirdJwtMap', getWebhookAnalytics: string, getPageviewsAnalytics: string, getUsersAnalytics: string, getServerlessFunctionDuration: string, getServerlessFunctionSuccessRate: string, getServerlessFunctionErrorCount: string } | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentUserWorkspace?: { __typename?: 'UserWorkspace', settingsPermissions?: Array<SettingsFeatures> | null, objectRecordsPermissions?: Array<PermissionsOnAllObjectRecords> | null } | null, currentWorkspace?: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEnterpriseKey: boolean, customDomain?: string | null, metadataVersion: number, workspaceMembersCount?: number | null, workspaceUrls: { __typename?: 'workspaceUrls', subdomainUrl: string, customUrl?: string | null }, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: FeatureFlagKey, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus }> } | null, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, subdomain: string, customDomain?: string | null, workspaceUrls: { __typename?: 'workspaceUrls', subdomainUrl: string, customUrl?: string | null } } | null }> };
|
||||
export type UserQueryFragmentFragment = { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, analyticsTinybirdJwts?: { __typename?: 'AnalyticsTinybirdJwtMap', getWebhookAnalytics: string, getPageviewsAnalytics: string, getUsersAnalytics: string, getServerlessFunctionDuration: string, getServerlessFunctionSuccessRate: string, getServerlessFunctionErrorCount: string } | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentUserWorkspace?: { __typename?: 'UserWorkspace', settingsPermissions?: Array<SettingsPermissions> | null, objectRecordsPermissions?: Array<PermissionsOnAllObjectRecords> | null } | null, currentWorkspace?: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEnterpriseKey: boolean, customDomain?: string | null, metadataVersion: number, workspaceMembersCount?: number | null, workspaceUrls: { __typename?: 'workspaceUrls', subdomainUrl: string, customUrl?: string | null }, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: FeatureFlagKey, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus }> } | null, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, subdomain: string, customDomain?: string | null, workspaceUrls: { __typename?: 'workspaceUrls', subdomainUrl: string, customUrl?: string | null } } | null }> };
|
||||
|
||||
export type DeleteUserAccountMutationVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
@@ -2375,7 +2451,7 @@ export type UploadProfilePictureMutation = { __typename?: 'Mutation', uploadProf
|
||||
export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, analyticsTinybirdJwts?: { __typename?: 'AnalyticsTinybirdJwtMap', getWebhookAnalytics: string, getPageviewsAnalytics: string, getUsersAnalytics: string, getServerlessFunctionDuration: string, getServerlessFunctionSuccessRate: string, getServerlessFunctionErrorCount: string } | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentUserWorkspace?: { __typename?: 'UserWorkspace', settingsPermissions?: Array<SettingsFeatures> | null, objectRecordsPermissions?: Array<PermissionsOnAllObjectRecords> | null } | null, currentWorkspace?: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEnterpriseKey: boolean, customDomain?: string | null, metadataVersion: number, workspaceMembersCount?: number | null, workspaceUrls: { __typename?: 'workspaceUrls', subdomainUrl: string, customUrl?: string | null }, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: FeatureFlagKey, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus }> } | null, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, subdomain: string, customDomain?: string | null, workspaceUrls: { __typename?: 'workspaceUrls', subdomainUrl: string, customUrl?: string | null } } | null }> } };
|
||||
export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, analyticsTinybirdJwts?: { __typename?: 'AnalyticsTinybirdJwtMap', getWebhookAnalytics: string, getPageviewsAnalytics: string, getUsersAnalytics: string, getServerlessFunctionDuration: string, getServerlessFunctionSuccessRate: string, getServerlessFunctionErrorCount: string } | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentUserWorkspace?: { __typename?: 'UserWorkspace', settingsPermissions?: Array<SettingsPermissions> | null, objectRecordsPermissions?: Array<PermissionsOnAllObjectRecords> | null } | null, currentWorkspace?: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEnterpriseKey: boolean, customDomain?: string | null, metadataVersion: number, workspaceMembersCount?: number | null, workspaceUrls: { __typename?: 'workspaceUrls', subdomainUrl: string, customUrl?: string | null }, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: FeatureFlagKey, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus }> } | null, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, subdomain: string, customDomain?: string | null, workspaceUrls: { __typename?: 'workspaceUrls', subdomainUrl: string, customUrl?: string | null } } | null }> } };
|
||||
|
||||
export type ActivateWorkflowVersionMutationVariables = Exact<{
|
||||
workflowVersionId: Scalars['String'];
|
||||
@@ -4018,12 +4094,16 @@ export type GetEnvironmentVariablesGroupedQueryHookResult = ReturnType<typeof us
|
||||
export type GetEnvironmentVariablesGroupedLazyQueryHookResult = ReturnType<typeof useGetEnvironmentVariablesGroupedLazyQuery>;
|
||||
export type GetEnvironmentVariablesGroupedQueryResult = Apollo.QueryResult<GetEnvironmentVariablesGroupedQuery, GetEnvironmentVariablesGroupedQueryVariables>;
|
||||
export const GetIndicatorHealthStatusDocument = gql`
|
||||
query GetIndicatorHealthStatus($indicatorName: AdminPanelIndicatorHealthStatusInputEnum!) {
|
||||
getIndicatorHealthStatus(indicatorName: $indicatorName) {
|
||||
query GetIndicatorHealthStatus($indicatorId: HealthIndicatorId!) {
|
||||
getIndicatorHealthStatus(indicatorId: $indicatorId) {
|
||||
id
|
||||
label
|
||||
description
|
||||
status
|
||||
details
|
||||
queues {
|
||||
name
|
||||
id
|
||||
queueName
|
||||
status
|
||||
workers
|
||||
metrics {
|
||||
@@ -4051,7 +4131,7 @@ export const GetIndicatorHealthStatusDocument = gql`
|
||||
* @example
|
||||
* const { data, loading, error } = useGetIndicatorHealthStatusQuery({
|
||||
* variables: {
|
||||
* indicatorName: // value for 'indicatorName'
|
||||
* indicatorId: // value for 'indicatorId'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
@@ -4069,33 +4149,10 @@ export type GetIndicatorHealthStatusQueryResult = Apollo.QueryResult<GetIndicato
|
||||
export const GetSystemHealthStatusDocument = gql`
|
||||
query GetSystemHealthStatus {
|
||||
getSystemHealthStatus {
|
||||
database {
|
||||
services {
|
||||
id
|
||||
label
|
||||
status
|
||||
details
|
||||
}
|
||||
redis {
|
||||
status
|
||||
details
|
||||
}
|
||||
worker {
|
||||
status
|
||||
queues {
|
||||
name
|
||||
workers
|
||||
status
|
||||
metrics {
|
||||
failed
|
||||
completed
|
||||
waiting
|
||||
active
|
||||
delayed
|
||||
prioritized
|
||||
}
|
||||
}
|
||||
}
|
||||
messageSync {
|
||||
status
|
||||
details
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4241,6 +4298,42 @@ export function useGetRolesLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<G
|
||||
export type GetRolesQueryHookResult = ReturnType<typeof useGetRolesQuery>;
|
||||
export type GetRolesLazyQueryHookResult = ReturnType<typeof useGetRolesLazyQuery>;
|
||||
export type GetRolesQueryResult = Apollo.QueryResult<GetRolesQuery, GetRolesQueryVariables>;
|
||||
export const CreateApprovedAccessDomainDocument = gql`
|
||||
mutation CreateApprovedAccessDomain($input: CreateApprovedAccessDomainInput!) {
|
||||
createApprovedAccessDomain(input: $input) {
|
||||
id
|
||||
domain
|
||||
isValidated
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
`;
|
||||
export type CreateApprovedAccessDomainMutationFn = Apollo.MutationFunction<CreateApprovedAccessDomainMutation, CreateApprovedAccessDomainMutationVariables>;
|
||||
|
||||
/**
|
||||
* __useCreateApprovedAccessDomainMutation__
|
||||
*
|
||||
* To run a mutation, you first call `useCreateApprovedAccessDomainMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `useCreateApprovedAccessDomainMutation` returns a tuple that includes:
|
||||
* - A mutate function that you can call at any time to execute the mutation
|
||||
* - An object with fields that represent the current status of the mutation's execution
|
||||
*
|
||||
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
|
||||
*
|
||||
* @example
|
||||
* const [createApprovedAccessDomainMutation, { data, loading, error }] = useCreateApprovedAccessDomainMutation({
|
||||
* variables: {
|
||||
* input: // value for 'input'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useCreateApprovedAccessDomainMutation(baseOptions?: Apollo.MutationHookOptions<CreateApprovedAccessDomainMutation, CreateApprovedAccessDomainMutationVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useMutation<CreateApprovedAccessDomainMutation, CreateApprovedAccessDomainMutationVariables>(CreateApprovedAccessDomainDocument, options);
|
||||
}
|
||||
export type CreateApprovedAccessDomainMutationHookResult = ReturnType<typeof useCreateApprovedAccessDomainMutation>;
|
||||
export type CreateApprovedAccessDomainMutationResult = Apollo.MutationResult<CreateApprovedAccessDomainMutation>;
|
||||
export type CreateApprovedAccessDomainMutationOptions = Apollo.BaseMutationOptions<CreateApprovedAccessDomainMutation, CreateApprovedAccessDomainMutationVariables>;
|
||||
export const CreateOidcIdentityProviderDocument = gql`
|
||||
mutation CreateOIDCIdentityProvider($input: SetupOIDCSsoInput!) {
|
||||
createOIDCIdentityProvider(input: $input) {
|
||||
@@ -4315,6 +4408,37 @@ export function useCreateSamlIdentityProviderMutation(baseOptions?: Apollo.Mutat
|
||||
export type CreateSamlIdentityProviderMutationHookResult = ReturnType<typeof useCreateSamlIdentityProviderMutation>;
|
||||
export type CreateSamlIdentityProviderMutationResult = Apollo.MutationResult<CreateSamlIdentityProviderMutation>;
|
||||
export type CreateSamlIdentityProviderMutationOptions = Apollo.BaseMutationOptions<CreateSamlIdentityProviderMutation, CreateSamlIdentityProviderMutationVariables>;
|
||||
export const DeleteApprovedAccessDomainDocument = gql`
|
||||
mutation DeleteApprovedAccessDomain($input: DeleteApprovedAccessDomainInput!) {
|
||||
deleteApprovedAccessDomain(input: $input)
|
||||
}
|
||||
`;
|
||||
export type DeleteApprovedAccessDomainMutationFn = Apollo.MutationFunction<DeleteApprovedAccessDomainMutation, DeleteApprovedAccessDomainMutationVariables>;
|
||||
|
||||
/**
|
||||
* __useDeleteApprovedAccessDomainMutation__
|
||||
*
|
||||
* To run a mutation, you first call `useDeleteApprovedAccessDomainMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `useDeleteApprovedAccessDomainMutation` returns a tuple that includes:
|
||||
* - A mutate function that you can call at any time to execute the mutation
|
||||
* - An object with fields that represent the current status of the mutation's execution
|
||||
*
|
||||
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
|
||||
*
|
||||
* @example
|
||||
* const [deleteApprovedAccessDomainMutation, { data, loading, error }] = useDeleteApprovedAccessDomainMutation({
|
||||
* variables: {
|
||||
* input: // value for 'input'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useDeleteApprovedAccessDomainMutation(baseOptions?: Apollo.MutationHookOptions<DeleteApprovedAccessDomainMutation, DeleteApprovedAccessDomainMutationVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useMutation<DeleteApprovedAccessDomainMutation, DeleteApprovedAccessDomainMutationVariables>(DeleteApprovedAccessDomainDocument, options);
|
||||
}
|
||||
export type DeleteApprovedAccessDomainMutationHookResult = ReturnType<typeof useDeleteApprovedAccessDomainMutation>;
|
||||
export type DeleteApprovedAccessDomainMutationResult = Apollo.MutationResult<DeleteApprovedAccessDomainMutation>;
|
||||
export type DeleteApprovedAccessDomainMutationOptions = Apollo.BaseMutationOptions<DeleteApprovedAccessDomainMutation, DeleteApprovedAccessDomainMutationVariables>;
|
||||
export const DeleteSsoIdentityProviderDocument = gql`
|
||||
mutation DeleteSSOIdentityProvider($input: DeleteSsoInput!) {
|
||||
deleteSSOIdentityProvider(input: $input) {
|
||||
@@ -4385,6 +4509,79 @@ export function useEditSsoIdentityProviderMutation(baseOptions?: Apollo.Mutation
|
||||
export type EditSsoIdentityProviderMutationHookResult = ReturnType<typeof useEditSsoIdentityProviderMutation>;
|
||||
export type EditSsoIdentityProviderMutationResult = Apollo.MutationResult<EditSsoIdentityProviderMutation>;
|
||||
export type EditSsoIdentityProviderMutationOptions = Apollo.BaseMutationOptions<EditSsoIdentityProviderMutation, EditSsoIdentityProviderMutationVariables>;
|
||||
export const ValidateApprovedAccessDomainDocument = gql`
|
||||
mutation ValidateApprovedAccessDomain($input: ValidateApprovedAccessDomainInput!) {
|
||||
validateApprovedAccessDomain(input: $input) {
|
||||
id
|
||||
isValidated
|
||||
domain
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
`;
|
||||
export type ValidateApprovedAccessDomainMutationFn = Apollo.MutationFunction<ValidateApprovedAccessDomainMutation, ValidateApprovedAccessDomainMutationVariables>;
|
||||
|
||||
/**
|
||||
* __useValidateApprovedAccessDomainMutation__
|
||||
*
|
||||
* To run a mutation, you first call `useValidateApprovedAccessDomainMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `useValidateApprovedAccessDomainMutation` returns a tuple that includes:
|
||||
* - A mutate function that you can call at any time to execute the mutation
|
||||
* - An object with fields that represent the current status of the mutation's execution
|
||||
*
|
||||
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
|
||||
*
|
||||
* @example
|
||||
* const [validateApprovedAccessDomainMutation, { data, loading, error }] = useValidateApprovedAccessDomainMutation({
|
||||
* variables: {
|
||||
* input: // value for 'input'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useValidateApprovedAccessDomainMutation(baseOptions?: Apollo.MutationHookOptions<ValidateApprovedAccessDomainMutation, ValidateApprovedAccessDomainMutationVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useMutation<ValidateApprovedAccessDomainMutation, ValidateApprovedAccessDomainMutationVariables>(ValidateApprovedAccessDomainDocument, options);
|
||||
}
|
||||
export type ValidateApprovedAccessDomainMutationHookResult = ReturnType<typeof useValidateApprovedAccessDomainMutation>;
|
||||
export type ValidateApprovedAccessDomainMutationResult = Apollo.MutationResult<ValidateApprovedAccessDomainMutation>;
|
||||
export type ValidateApprovedAccessDomainMutationOptions = Apollo.BaseMutationOptions<ValidateApprovedAccessDomainMutation, ValidateApprovedAccessDomainMutationVariables>;
|
||||
export const GetApprovedAccessDomainsDocument = gql`
|
||||
query GetApprovedAccessDomains {
|
||||
getApprovedAccessDomains {
|
||||
id
|
||||
createdAt
|
||||
domain
|
||||
isValidated
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useGetApprovedAccessDomainsQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useGetApprovedAccessDomainsQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useGetApprovedAccessDomainsQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useGetApprovedAccessDomainsQuery({
|
||||
* variables: {
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useGetApprovedAccessDomainsQuery(baseOptions?: Apollo.QueryHookOptions<GetApprovedAccessDomainsQuery, GetApprovedAccessDomainsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<GetApprovedAccessDomainsQuery, GetApprovedAccessDomainsQueryVariables>(GetApprovedAccessDomainsDocument, options);
|
||||
}
|
||||
export function useGetApprovedAccessDomainsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetApprovedAccessDomainsQuery, GetApprovedAccessDomainsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<GetApprovedAccessDomainsQuery, GetApprovedAccessDomainsQueryVariables>(GetApprovedAccessDomainsDocument, options);
|
||||
}
|
||||
export type GetApprovedAccessDomainsQueryHookResult = ReturnType<typeof useGetApprovedAccessDomainsQuery>;
|
||||
export type GetApprovedAccessDomainsLazyQueryHookResult = ReturnType<typeof useGetApprovedAccessDomainsLazyQuery>;
|
||||
export type GetApprovedAccessDomainsQueryResult = Apollo.QueryResult<GetApprovedAccessDomainsQuery, GetApprovedAccessDomainsQueryVariables>;
|
||||
export const GetSsoIdentityProvidersDocument = gql`
|
||||
query GetSSOIdentityProviders {
|
||||
getSSOIdentityProviders {
|
||||
|
||||
+5
-5
@@ -2,7 +2,7 @@ import { useIsLogged } from '@/auth/hooks/useIsLogged';
|
||||
import { useDefaultHomePagePath } from '@/navigation/hooks/useDefaultHomePagePath';
|
||||
import { useOnboardingStatus } from '@/onboarding/hooks/useOnboardingStatus';
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
import { useIsWorkspaceActivationStatusSuspended } from '@/workspace/hooks/useIsWorkspaceActivationStatusSuspended';
|
||||
import { useIsWorkspaceActivationStatusEqualsTo } from '@/workspace/hooks/useIsWorkspaceActivationStatusEqualsTo';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
@@ -19,12 +19,12 @@ const setupMockOnboardingStatus = (
|
||||
jest.mocked(useOnboardingStatus).mockReturnValueOnce(onboardingStatus);
|
||||
};
|
||||
|
||||
jest.mock('@/workspace/hooks/useIsWorkspaceActivationStatusSuspended');
|
||||
const setupMockIsWorkspaceActivationStatusSuspended = (
|
||||
jest.mock('@/workspace/hooks/useIsWorkspaceActivationStatusEqualsTo');
|
||||
const setupMockIsWorkspaceActivationStatusEqualsTo = (
|
||||
isWorkspaceSuspended: boolean,
|
||||
) => {
|
||||
jest
|
||||
.mocked(useIsWorkspaceActivationStatusSuspended)
|
||||
.mocked(useIsWorkspaceActivationStatusEqualsTo)
|
||||
.mockReturnValueOnce(isWorkspaceSuspended);
|
||||
};
|
||||
|
||||
@@ -270,7 +270,7 @@ describe('usePageChangeEffectNavigateLocation', () => {
|
||||
it(`with location ${testCase.loc} and onboardingStatus ${testCase.onboardingStatus} and isWorkspaceSuspended ${testCase.isWorkspaceSuspended} should return ${testCase.res}`, () => {
|
||||
setupMockIsMatchingLocation(testCase.loc);
|
||||
setupMockOnboardingStatus(testCase.onboardingStatus);
|
||||
setupMockIsWorkspaceActivationStatusSuspended(
|
||||
setupMockIsWorkspaceActivationStatusEqualsTo(
|
||||
testCase.isWorkspaceSuspended,
|
||||
);
|
||||
setupMockIsLogged(testCase.isLoggedIn);
|
||||
|
||||
@@ -4,10 +4,10 @@ import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadat
|
||||
import { useOnboardingStatus } from '@/onboarding/hooks/useOnboardingStatus';
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
import { SettingsPath } from '@/types/SettingsPath';
|
||||
import { useIsWorkspaceActivationStatusSuspended } from '@/workspace/hooks/useIsWorkspaceActivationStatusSuspended';
|
||||
import { useIsWorkspaceActivationStatusEqualsTo } from '@/workspace/hooks/useIsWorkspaceActivationStatusEqualsTo';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import { WorkspaceActivationStatus, isDefined } from 'twenty-shared';
|
||||
import { OnboardingStatus } from '~/generated/graphql';
|
||||
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
|
||||
|
||||
@@ -15,7 +15,9 @@ export const usePageChangeEffectNavigateLocation = () => {
|
||||
const { isMatchingLocation } = useIsMatchingLocation();
|
||||
const isLoggedIn = useIsLogged();
|
||||
const onboardingStatus = useOnboardingStatus();
|
||||
const isWorkspaceSuspended = useIsWorkspaceActivationStatusSuspended();
|
||||
const isWorkspaceSuspended = useIsWorkspaceActivationStatusEqualsTo(
|
||||
WorkspaceActivationStatus.SUSPENDED,
|
||||
);
|
||||
const { defaultHomePagePath } = useDefaultHomePagePath();
|
||||
|
||||
const isMatchingOpenRoute =
|
||||
|
||||
+4
@@ -11,6 +11,7 @@ import { useLazyFetchAllRecords } from '@/object-record/hooks/useLazyFetchAllRec
|
||||
import { useCheckIsSoftDeleteFilter } from '@/object-record/record-filter/hooks/useCheckIsSoftDeleteFilter';
|
||||
import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies';
|
||||
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
|
||||
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
|
||||
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { useCallback, useState } from 'react';
|
||||
@@ -21,6 +22,8 @@ export const useDeleteMultipleRecordsAction: ActionHookWithObjectMetadataItem =
|
||||
const [isDeleteRecordsModalOpen, setIsDeleteRecordsModalOpen] =
|
||||
useState(false);
|
||||
|
||||
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
|
||||
|
||||
const { resetTableRowSelection } = useRecordTable({
|
||||
recordTableId: objectMetadataItem.namePlural,
|
||||
});
|
||||
@@ -77,6 +80,7 @@ export const useDeleteMultipleRecordsAction: ActionHookWithObjectMetadataItem =
|
||||
const isRemoteObject = objectMetadataItem.isRemote;
|
||||
|
||||
const shouldBeRegistered =
|
||||
!hasObjectReadOnlyPermission &&
|
||||
!isRemoteObject &&
|
||||
!isDeletedFilterActive &&
|
||||
isDefined(contextStoreNumberOfSelectedRecords) &&
|
||||
|
||||
+4
@@ -12,6 +12,7 @@ import { useLazyFetchAllRecords } from '@/object-record/hooks/useLazyFetchAllRec
|
||||
import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies';
|
||||
import { RecordFilterOperand } from '@/object-record/record-filter/types/RecordFilterOperand';
|
||||
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
|
||||
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
|
||||
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { useCallback, useState } from 'react';
|
||||
@@ -26,6 +27,8 @@ export const useDestroyMultipleRecordsAction: ActionHookWithObjectMetadataItem =
|
||||
recordTableId: objectMetadataItem.namePlural,
|
||||
});
|
||||
|
||||
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
|
||||
|
||||
const { destroyManyRecords } = useDestroyManyRecords({
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
});
|
||||
@@ -86,6 +89,7 @@ export const useDestroyMultipleRecordsAction: ActionHookWithObjectMetadataItem =
|
||||
const isRemoteObject = objectMetadataItem.isRemote;
|
||||
|
||||
const shouldBeRegistered =
|
||||
!hasObjectReadOnlyPermission &&
|
||||
!isRemoteObject &&
|
||||
isDeletedFilterActive &&
|
||||
isDefined(contextStoreNumberOfSelectedRecords) &&
|
||||
|
||||
+4
-1
@@ -1,6 +1,7 @@
|
||||
import { ActionHookWithObjectMetadataItem } from '@/action-menu/actions/types/ActionHook';
|
||||
import { useCreateNewTableRecord } from '@/object-record/record-table/hooks/useCreateNewTableRecords';
|
||||
import { getRecordIndexIdFromObjectNamePlural } from '@/object-record/utils/getRecordIndexIdFromObjectNamePlural';
|
||||
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
|
||||
|
||||
export const useCreateNewTableRecordNoSelectionRecordAction: ActionHookWithObjectMetadataItem =
|
||||
({ objectMetadataItem }) => {
|
||||
@@ -8,6 +9,8 @@ export const useCreateNewTableRecordNoSelectionRecordAction: ActionHookWithObjec
|
||||
objectMetadataItem.namePlural,
|
||||
);
|
||||
|
||||
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
|
||||
|
||||
const { createNewTableRecord } = useCreateNewTableRecord({
|
||||
objectMetadataItem,
|
||||
recordTableId,
|
||||
@@ -18,7 +21,7 @@ export const useCreateNewTableRecordNoSelectionRecordAction: ActionHookWithObjec
|
||||
};
|
||||
|
||||
return {
|
||||
shouldBeRegistered: true,
|
||||
shouldBeRegistered: !hasObjectReadOnlyPermission,
|
||||
onClick,
|
||||
};
|
||||
};
|
||||
|
||||
+6
-1
@@ -6,6 +6,7 @@ import { useFavorites } from '@/favorites/hooks/useFavorites';
|
||||
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
|
||||
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
|
||||
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
|
||||
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
|
||||
import { isNull } from '@sniptt/guards';
|
||||
@@ -21,6 +22,8 @@ export const useDeleteSingleRecordAction: ActionHookWithObjectMetadataItem = ({
|
||||
const [isDeleteRecordsModalOpen, setIsDeleteRecordsModalOpen] =
|
||||
useState(false);
|
||||
|
||||
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
|
||||
|
||||
const { resetTableRowSelection } = useRecordTable({
|
||||
recordTableId: objectMetadataItem.namePlural,
|
||||
});
|
||||
@@ -61,7 +64,9 @@ export const useDeleteSingleRecordAction: ActionHookWithObjectMetadataItem = ({
|
||||
const { isInRightDrawer } = useContext(ActionMenuContext);
|
||||
|
||||
const shouldBeRegistered =
|
||||
!isRemoteObject && isNull(selectedRecord?.deletedAt);
|
||||
!isRemoteObject &&
|
||||
isNull(selectedRecord?.deletedAt) &&
|
||||
!hasObjectReadOnlyPermission;
|
||||
|
||||
const onClick = () => {
|
||||
if (!shouldBeRegistered) {
|
||||
|
||||
+6
-1
@@ -4,6 +4,7 @@ import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
|
||||
import { useDestroyOneRecord } from '@/object-record/hooks/useDestroyOneRecord';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
|
||||
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
|
||||
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
|
||||
@@ -20,6 +21,8 @@ export const useDestroySingleRecordAction: ActionHookWithObjectMetadataItem = ({
|
||||
const [isDestroyRecordsModalOpen, setIsDestroyRecordsModalOpen] =
|
||||
useState(false);
|
||||
|
||||
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
|
||||
|
||||
const navigateApp = useNavigateApp();
|
||||
|
||||
const { resetTableRowSelection } = useRecordTable({
|
||||
@@ -54,7 +57,9 @@ export const useDestroySingleRecordAction: ActionHookWithObjectMetadataItem = ({
|
||||
const { isInRightDrawer } = useContext(ActionMenuContext);
|
||||
|
||||
const shouldBeRegistered =
|
||||
!isRemoteObject && isDefined(selectedRecord?.deletedAt);
|
||||
!hasObjectReadOnlyPermission &&
|
||||
!isRemoteObject &&
|
||||
isDefined(selectedRecord?.deletedAt);
|
||||
|
||||
const onClick = () => {
|
||||
if (!shouldBeRegistered) {
|
||||
|
||||
+12
-2
@@ -2,6 +2,10 @@ import { actionMenuEntriesComponentSelector } from '@/action-menu/states/actionM
|
||||
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
|
||||
import { recordIndexActionMenuDropdownPositionComponentState } from '@/action-menu/states/recordIndexActionMenuDropdownPositionComponentState';
|
||||
import { ActionMenuDropdownHotkeyScope } from '@/action-menu/types/ActionMenuDropdownHotKeyScope';
|
||||
import {
|
||||
ActionMenuEntryScope,
|
||||
ActionMenuEntryType,
|
||||
} from '@/action-menu/types/ActionMenuEntry';
|
||||
import { getActionMenuDropdownIdFromActionMenuId } from '@/action-menu/utils/getActionMenuDropdownIdFromActionMenuId';
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
@@ -29,6 +33,12 @@ export const RecordIndexActionMenuDropdown = () => {
|
||||
actionMenuEntriesComponentSelector,
|
||||
);
|
||||
|
||||
const recordIndexActions = actionMenuEntries.filter(
|
||||
(actionMenuEntry) =>
|
||||
actionMenuEntry.type === ActionMenuEntryType.Standard &&
|
||||
actionMenuEntry.scope === ActionMenuEntryScope.RecordSelection,
|
||||
);
|
||||
|
||||
const actionMenuId = useAvailableComponentInstanceIdOrThrow(
|
||||
ActionMenuComponentInstanceContext,
|
||||
);
|
||||
@@ -44,7 +54,7 @@ export const RecordIndexActionMenuDropdown = () => {
|
||||
);
|
||||
|
||||
//TODO: remove this
|
||||
const width = actionMenuEntries.some(
|
||||
const width = recordIndexActions.some(
|
||||
(actionMenuEntry) =>
|
||||
i18n._(actionMenuEntry.label) === 'Remove from favorites',
|
||||
)
|
||||
@@ -68,7 +78,7 @@ export const RecordIndexActionMenuDropdown = () => {
|
||||
dropdownComponents={
|
||||
<StyledDropdownMenuContainer className="action-menu-dropdown">
|
||||
<DropdownMenuItemsContainer>
|
||||
{actionMenuEntries.map((item) => (
|
||||
{recordIndexActions.map((item) => (
|
||||
<MenuItem
|
||||
key={item.key}
|
||||
LeftIcon={item.Icon}
|
||||
|
||||
@@ -17,6 +17,7 @@ import { DropZone } from '@/activities/files/components/DropZone';
|
||||
import { useAttachments } from '@/activities/files/hooks/useAttachments';
|
||||
import { useUploadAttachmentFile } from '@/activities/files/hooks/useUploadAttachmentFile';
|
||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
|
||||
const StyledAttachmentsContainer = styled.div`
|
||||
@@ -46,6 +47,8 @@ export const Attachments = ({
|
||||
|
||||
const [isDraggingFile, setIsDraggingFile] = useState(false);
|
||||
|
||||
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
|
||||
|
||||
const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
if (isDefined(e.target.files)) onUploadFile?.(e.target.files[0]);
|
||||
};
|
||||
@@ -91,12 +94,14 @@ export const Attachments = ({
|
||||
onChange={handleFileChange}
|
||||
type="file"
|
||||
/>
|
||||
<Button
|
||||
Icon={IconPlus}
|
||||
title="Add file"
|
||||
variant="secondary"
|
||||
onClick={handleUploadFileClick}
|
||||
/>
|
||||
{!hasObjectReadOnlyPermission && (
|
||||
<Button
|
||||
Icon={IconPlus}
|
||||
title="Add file"
|
||||
variant="secondary"
|
||||
onClick={handleUploadFileClick}
|
||||
/>
|
||||
)}
|
||||
</AnimatedPlaceholderEmptyContainer>
|
||||
)}
|
||||
</StyledDropZoneContainer>
|
||||
@@ -115,13 +120,15 @@ export const Attachments = ({
|
||||
title="All"
|
||||
attachments={attachments ?? []}
|
||||
button={
|
||||
<Button
|
||||
Icon={IconPlus}
|
||||
size="small"
|
||||
variant="secondary"
|
||||
title="Add file"
|
||||
onClick={handleUploadFileClick}
|
||||
></Button>
|
||||
!hasObjectReadOnlyPermission && (
|
||||
<Button
|
||||
Icon={IconPlus}
|
||||
size="small"
|
||||
variant="secondary"
|
||||
title="Add file"
|
||||
onClick={handleUploadFileClick}
|
||||
></Button>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</StyledAttachmentsContainer>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { NoteList } from '@/activities/notes/components/NoteList';
|
||||
import { useNotes } from '@/activities/notes/hooks/useNotes';
|
||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
|
||||
import styled from '@emotion/styled';
|
||||
import {
|
||||
AnimatedPlaceholder,
|
||||
@@ -31,6 +32,8 @@ export const Notes = ({
|
||||
}) => {
|
||||
const { notes, loading } = useNotes(targetableObject);
|
||||
|
||||
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
|
||||
|
||||
const openCreateActivity = useOpenCreateActivityDrawer({
|
||||
activityObjectNameSingular: CoreObjectNameSingular.Note,
|
||||
});
|
||||
@@ -56,16 +59,18 @@ export const Notes = ({
|
||||
There are no associated notes with this record.
|
||||
</AnimatedPlaceholderEmptySubTitle>
|
||||
</AnimatedPlaceholderEmptyTextContainer>
|
||||
<Button
|
||||
Icon={IconPlus}
|
||||
title="New note"
|
||||
variant="secondary"
|
||||
onClick={() =>
|
||||
openCreateActivity({
|
||||
targetableObjects: [targetableObject],
|
||||
})
|
||||
}
|
||||
/>
|
||||
{!hasObjectReadOnlyPermission && (
|
||||
<Button
|
||||
Icon={IconPlus}
|
||||
title="New note"
|
||||
variant="secondary"
|
||||
onClick={() =>
|
||||
openCreateActivity({
|
||||
targetableObjects: [targetableObject],
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</AnimatedPlaceholderEmptyContainer>
|
||||
);
|
||||
}
|
||||
@@ -76,17 +81,19 @@ export const Notes = ({
|
||||
title="All"
|
||||
notes={notes}
|
||||
button={
|
||||
<Button
|
||||
Icon={IconPlus}
|
||||
size="small"
|
||||
variant="secondary"
|
||||
title="Add note"
|
||||
onClick={() =>
|
||||
openCreateActivity({
|
||||
targetableObjects: [targetableObject],
|
||||
})
|
||||
}
|
||||
></Button>
|
||||
!hasObjectReadOnlyPermission && (
|
||||
<Button
|
||||
Icon={IconPlus}
|
||||
size="small"
|
||||
variant="secondary"
|
||||
title="Add note"
|
||||
onClick={() =>
|
||||
openCreateActivity({
|
||||
targetableObjects: [targetableObject],
|
||||
})
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</StyledNotesContainer>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Button, IconPlus } from 'twenty-ui';
|
||||
import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer';
|
||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
|
||||
|
||||
export const AddTaskButton = ({
|
||||
activityTargetableObjects,
|
||||
@@ -14,8 +15,13 @@ export const AddTaskButton = ({
|
||||
activityObjectNameSingular: CoreObjectNameSingular.Task,
|
||||
});
|
||||
|
||||
if (!isNonEmptyArray(activityTargetableObjects)) {
|
||||
return <></>;
|
||||
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
|
||||
|
||||
if (
|
||||
!isNonEmptyArray(activityTargetableObjects) ||
|
||||
hasObjectReadOnlyPermission
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -29,6 +35,6 @@ export const AddTaskButton = ({
|
||||
targetableObjects: activityTargetableObjects,
|
||||
})
|
||||
}
|
||||
></Button>
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -17,6 +17,7 @@ import { useTasks } from '@/activities/tasks/hooks/useTasks';
|
||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { Task } from '@/activities/types/Task';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
|
||||
import { useTabList } from '@/ui/layout/tab/hooks/useTabList';
|
||||
import groupBy from 'lodash.groupby';
|
||||
import { AddTaskButton } from './AddTaskButton';
|
||||
@@ -38,6 +39,8 @@ export const TaskGroups = ({ targetableObjects }: TaskGroupsProps) => {
|
||||
targetableObjects: targetableObjects ?? [],
|
||||
});
|
||||
|
||||
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
|
||||
|
||||
const openCreateActivity = useOpenCreateActivityDrawer({
|
||||
activityObjectNameSingular: CoreObjectNameSingular.Task,
|
||||
});
|
||||
@@ -71,16 +74,18 @@ export const TaskGroups = ({ targetableObjects }: TaskGroupsProps) => {
|
||||
All tasks addressed. Maintain the momentum.
|
||||
</AnimatedPlaceholderEmptySubTitle>
|
||||
</AnimatedPlaceholderEmptyTextContainer>
|
||||
<Button
|
||||
Icon={IconPlus}
|
||||
title="New task"
|
||||
variant={'secondary'}
|
||||
onClick={() =>
|
||||
openCreateActivity({
|
||||
targetableObjects: targetableObjects ?? [],
|
||||
})
|
||||
}
|
||||
/>
|
||||
{!hasObjectReadOnlyPermission && (
|
||||
<Button
|
||||
Icon={IconPlus}
|
||||
title="New task"
|
||||
variant={'secondary'}
|
||||
onClick={() =>
|
||||
openCreateActivity({
|
||||
targetableObjects: targetableObjects ?? [],
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</AnimatedPlaceholderEmptyContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Route, Routes } from 'react-router-dom';
|
||||
import { SettingsProtectedRouteWrapper } from '@/settings/components/SettingsProtectedRouteWrapper';
|
||||
import { SettingsSkeletonLoader } from '@/settings/components/SettingsSkeletonLoader';
|
||||
import { SettingsPath } from '@/types/SettingsPath';
|
||||
import { SettingsFeatures } from 'twenty-shared';
|
||||
import { SettingsPermissions } from 'twenty-shared';
|
||||
import { FeatureFlagKey } from '~/generated-metadata/graphql';
|
||||
|
||||
const SettingsAccountsCalendars = lazy(() =>
|
||||
@@ -234,6 +234,14 @@ const SettingsSecuritySSOIdentifyProvider = lazy(() =>
|
||||
),
|
||||
);
|
||||
|
||||
const SettingsSecurityApprovedAccessDomain = lazy(() =>
|
||||
import('~/pages/settings/security/SettingsSecurityApprovedAccessDomain').then(
|
||||
(module) => ({
|
||||
default: module.SettingsSecurityApprovedAccessDomain,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const SettingsAdmin = lazy(() =>
|
||||
import('~/pages/settings/admin-panel/SettingsAdmin').then((module) => ({
|
||||
default: module.SettingsAdmin,
|
||||
@@ -300,7 +308,7 @@ export const SettingsRoutes = ({
|
||||
<Route
|
||||
element={
|
||||
<SettingsProtectedRouteWrapper
|
||||
settingsPermission={SettingsFeatures.WORKSPACE}
|
||||
settingsPermission={SettingsPermissions.WORKSPACE}
|
||||
/>
|
||||
}
|
||||
>
|
||||
@@ -315,7 +323,7 @@ export const SettingsRoutes = ({
|
||||
<Route
|
||||
element={
|
||||
<SettingsProtectedRouteWrapper
|
||||
settingsPermission={SettingsFeatures.DATA_MODEL}
|
||||
settingsPermission={SettingsPermissions.DATA_MODEL}
|
||||
/>
|
||||
}
|
||||
>
|
||||
@@ -333,7 +341,7 @@ export const SettingsRoutes = ({
|
||||
<Route
|
||||
element={
|
||||
<SettingsProtectedRouteWrapper
|
||||
settingsPermission={SettingsFeatures.ROLES}
|
||||
settingsPermission={SettingsPermissions.ROLES}
|
||||
requiredFeatureFlag={FeatureFlagKey.IsPermissionsEnabled}
|
||||
/>
|
||||
}
|
||||
@@ -408,6 +416,11 @@ export const SettingsRoutes = ({
|
||||
path={SettingsPath.NewSSOIdentityProvider}
|
||||
element={<SettingsSecuritySSOIdentifyProvider />}
|
||||
/>
|
||||
<Route
|
||||
path={SettingsPath.NewApprovedAccessDomain}
|
||||
element={<SettingsSecurityApprovedAccessDomain />}
|
||||
/>
|
||||
|
||||
{isAdminPageEnabled && (
|
||||
<>
|
||||
<Route path={SettingsPath.AdminPanel} element={<SettingsAdmin />} />
|
||||
@@ -424,7 +437,7 @@ export const SettingsRoutes = ({
|
||||
<Route
|
||||
element={
|
||||
<SettingsProtectedRouteWrapper
|
||||
settingsPermission={SettingsFeatures.WORKSPACE}
|
||||
settingsPermission={SettingsPermissions.WORKSPACE}
|
||||
/>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { createState } from '@ui/utilities/state/utils/createState';
|
||||
import { UserWorkspace } from '~/generated/graphql';
|
||||
|
||||
export type CurrentUserWorkspace = Pick<UserWorkspace, 'settingsPermissions'>;
|
||||
export type CurrentUserWorkspace = Pick<
|
||||
UserWorkspace,
|
||||
'settingsPermissions' | 'objectRecordsPermissions'
|
||||
>;
|
||||
|
||||
export const currentUserWorkspaceState =
|
||||
createState<CurrentUserWorkspace | null>({
|
||||
|
||||
@@ -36,18 +36,6 @@ export const CommandMenu = () => {
|
||||
commandMenuSearch,
|
||||
});
|
||||
|
||||
const selectableItems: Command[] = copilotCommands
|
||||
.concat(
|
||||
matchingStandardActionRecordSelectionCommands,
|
||||
matchingStandardActionObjectCommands,
|
||||
matchingWorkflowRunRecordSelectionCommands,
|
||||
matchingStandardActionGlobalCommands,
|
||||
matchingWorkflowRunGlobalCommands,
|
||||
matchingNavigateCommands,
|
||||
fallbackCommands,
|
||||
)
|
||||
.filter(isDefined);
|
||||
|
||||
const previousContextStoreCurrentObjectMetadataItem =
|
||||
useRecoilComponentValueV2(
|
||||
contextStoreCurrentObjectMetadataItemComponentState,
|
||||
@@ -58,12 +46,6 @@ export const CommandMenu = () => {
|
||||
contextStoreCurrentObjectMetadataItemComponentState,
|
||||
);
|
||||
|
||||
const selectableItemIds = selectableItems.map((item) => item.id);
|
||||
|
||||
if (isDefined(previousContextStoreCurrentObjectMetadataItem)) {
|
||||
selectableItemIds.unshift(RESET_CONTEXT_TO_SELECTION);
|
||||
}
|
||||
|
||||
const commandGroups: CommandGroupConfig[] = [
|
||||
{
|
||||
heading: t`Copilot`,
|
||||
@@ -91,6 +73,16 @@ export const CommandMenu = () => {
|
||||
},
|
||||
];
|
||||
|
||||
const selectableItems: Command[] = commandGroups.flatMap(
|
||||
(group) => group.items ?? [],
|
||||
);
|
||||
|
||||
const selectableItemIds = selectableItems.map((item) => item.id);
|
||||
|
||||
if (isDefined(previousContextStoreCurrentObjectMetadataItem)) {
|
||||
selectableItemIds.unshift(RESET_CONTEXT_TO_SELECTION);
|
||||
}
|
||||
|
||||
return (
|
||||
<CommandMenuList
|
||||
commandGroups={commandGroups}
|
||||
|
||||
@@ -48,7 +48,11 @@ export const CommandMenuContainer = ({
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
const { toggleCommandMenu, closeCommandMenu } = useCommandMenu();
|
||||
const {
|
||||
toggleCommandMenu,
|
||||
closeCommandMenu,
|
||||
onCommandMenuCloseAnimationComplete,
|
||||
} = useCommandMenu();
|
||||
|
||||
const isCommandMenuOpened = useRecoilValue(isCommandMenuOpenedState);
|
||||
|
||||
@@ -98,7 +102,7 @@ export const CommandMenuContainer = ({
|
||||
>
|
||||
<ActionMenuContext.Provider
|
||||
value={{
|
||||
isInRightDrawer: false,
|
||||
isInRightDrawer: true,
|
||||
onActionExecutedCallback: ({ key }) => {
|
||||
if (
|
||||
key !== RecordAgnosticActionsKey.SEARCH_RECORDS &&
|
||||
@@ -121,7 +125,10 @@ export const CommandMenuContainer = ({
|
||||
<RunWorkflowRecordAgnosticActionMenuEntriesSetter />
|
||||
)}
|
||||
<ActionMenuConfirmationModals />
|
||||
<AnimatePresence mode="wait">
|
||||
<AnimatePresence
|
||||
mode="wait"
|
||||
onExitComplete={onCommandMenuCloseAnimationComplete}
|
||||
>
|
||||
{isCommandMenuOpened && (
|
||||
<StyledCommandMenu
|
||||
data-testid="command-menu"
|
||||
|
||||
@@ -8,6 +8,7 @@ const StyledChip = styled.button<{
|
||||
onClick?: () => void;
|
||||
}>`
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: ${({ theme }) => theme.background.transparent.light};
|
||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
border-radius: ${({ theme }) => theme.border.radius.md};
|
||||
|
||||
+11
-10
@@ -34,16 +34,17 @@ export const CommandMenuContextChipGroupsWithRecordSelection = ({
|
||||
/>
|
||||
));
|
||||
|
||||
const recordSelectionContextChip = totalCount
|
||||
? {
|
||||
text: getSelectedRecordsContextText(
|
||||
objectMetadataItem,
|
||||
records,
|
||||
totalCount,
|
||||
),
|
||||
Icons: Avatars,
|
||||
}
|
||||
: undefined;
|
||||
const recordSelectionContextChip =
|
||||
totalCount && records.length > 0
|
||||
? {
|
||||
text: getSelectedRecordsContextText(
|
||||
objectMetadataItem,
|
||||
records,
|
||||
totalCount,
|
||||
),
|
||||
Icons: Avatars,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const contextChipsWithRecordSelection = [
|
||||
recordSelectionContextChip,
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ export const CommandMenuContextRecordChip = ({
|
||||
instanceId,
|
||||
});
|
||||
|
||||
if (loading || !totalCount) {
|
||||
if (loading || !totalCount || records.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -192,6 +192,7 @@ describe('useCommandMenu', () => {
|
||||
|
||||
act(() => {
|
||||
result.current.commandMenu.goBackFromCommandMenu();
|
||||
result.current.commandMenu.onCommandMenuCloseAnimationComplete();
|
||||
});
|
||||
|
||||
expect(result.current.commandMenuNavigationStack).toEqual([]);
|
||||
|
||||
@@ -74,31 +74,33 @@ export const useCommandMenu = () => {
|
||||
);
|
||||
|
||||
const closeCommandMenu = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
({ set }) =>
|
||||
() => {
|
||||
const isCommandMenuOpened = snapshot
|
||||
.getLoadable(isCommandMenuOpenedState)
|
||||
.getValue();
|
||||
set(isCommandMenuOpenedState, false);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
if (isCommandMenuOpened) {
|
||||
resetContextStoreStates('command-menu');
|
||||
resetContextStoreStates('command-menu-previous');
|
||||
const onCommandMenuCloseAnimationComplete = useRecoilCallback(
|
||||
({ set }) =>
|
||||
() => {
|
||||
resetContextStoreStates('command-menu');
|
||||
resetContextStoreStates('command-menu-previous');
|
||||
|
||||
set(viewableRecordIdState, null);
|
||||
set(commandMenuPageState, CommandMenuPages.Root);
|
||||
set(commandMenuPageInfoState, {
|
||||
title: undefined,
|
||||
Icon: undefined,
|
||||
});
|
||||
set(isCommandMenuOpenedState, false);
|
||||
set(commandMenuSearchState, '');
|
||||
set(commandMenuNavigationStackState, []);
|
||||
resetSelectedItem();
|
||||
set(hasUserSelectedCommandState, false);
|
||||
goBackToPreviousHotkeyScope();
|
||||
set(viewableRecordIdState, null);
|
||||
set(commandMenuPageState, CommandMenuPages.Root);
|
||||
set(commandMenuPageInfoState, {
|
||||
title: undefined,
|
||||
Icon: undefined,
|
||||
});
|
||||
set(isCommandMenuOpenedState, false);
|
||||
set(commandMenuSearchState, '');
|
||||
set(commandMenuNavigationStackState, []);
|
||||
resetSelectedItem();
|
||||
set(hasUserSelectedCommandState, false);
|
||||
goBackToPreviousHotkeyScope();
|
||||
|
||||
emitRightDrawerCloseEvent();
|
||||
}
|
||||
emitRightDrawerCloseEvent();
|
||||
},
|
||||
[goBackToPreviousHotkeyScope, resetContextStoreStates, resetSelectedItem],
|
||||
);
|
||||
@@ -109,7 +111,10 @@ export const useCommandMenu = () => {
|
||||
page,
|
||||
pageTitle,
|
||||
pageIcon,
|
||||
}: CommandMenuNavigationStackItem) => {
|
||||
resetNavigationStack = false,
|
||||
}: CommandMenuNavigationStackItem & {
|
||||
resetNavigationStack?: boolean;
|
||||
}) => {
|
||||
set(commandMenuPageState, page);
|
||||
set(commandMenuPageInfoState, {
|
||||
title: pageTitle,
|
||||
@@ -120,10 +125,14 @@ export const useCommandMenu = () => {
|
||||
.getLoadable(commandMenuNavigationStackState)
|
||||
.getValue();
|
||||
|
||||
set(commandMenuNavigationStackState, [
|
||||
...currentNavigationStack,
|
||||
{ page, pageTitle, pageIcon },
|
||||
]);
|
||||
if (resetNavigationStack) {
|
||||
set(commandMenuNavigationStackState, [{ page, pageTitle, pageIcon }]);
|
||||
} else {
|
||||
set(commandMenuNavigationStackState, [
|
||||
...currentNavigationStack,
|
||||
{ page, pageTitle, pageIcon },
|
||||
]);
|
||||
}
|
||||
openCommandMenu();
|
||||
};
|
||||
},
|
||||
@@ -248,6 +257,7 @@ export const useCommandMenu = () => {
|
||||
? t`New ${capitalizedObjectNameSingular}`
|
||||
: capitalizedObjectNameSingular,
|
||||
pageIcon: Icon,
|
||||
resetNavigationStack: true,
|
||||
});
|
||||
};
|
||||
},
|
||||
@@ -315,6 +325,7 @@ export const useCommandMenu = () => {
|
||||
return {
|
||||
openRootCommandMenu,
|
||||
closeCommandMenu,
|
||||
onCommandMenuCloseAnimationComplete,
|
||||
navigateCommandMenu,
|
||||
navigateCommandMenuHistory,
|
||||
goBackFromCommandMenu,
|
||||
|
||||
+5
-3
@@ -3,10 +3,10 @@ import { InformationBannerFailPaymentInfo } from '@/information-banner/component
|
||||
import { InformationBannerNoBillingSubscription } from '@/information-banner/components/billing/InformationBannerNoBillingSubscription';
|
||||
import { InformationBannerReconnectAccountEmailAliases } from '@/information-banner/components/reconnect-account/InformationBannerReconnectAccountEmailAliases';
|
||||
import { InformationBannerReconnectAccountInsufficientPermissions } from '@/information-banner/components/reconnect-account/InformationBannerReconnectAccountInsufficientPermissions';
|
||||
import { useIsWorkspaceActivationStatusSuspended } from '@/workspace/hooks/useIsWorkspaceActivationStatusSuspended';
|
||||
import { useIsWorkspaceActivationStatusEqualsTo } from '@/workspace/hooks/useIsWorkspaceActivationStatusEqualsTo';
|
||||
import { useSubscriptionStatus } from '@/workspace/hooks/useSubscriptionStatus';
|
||||
import styled from '@emotion/styled';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import { WorkspaceActivationStatus, isDefined } from 'twenty-shared';
|
||||
import { SubscriptionStatus } from '~/generated-metadata/graphql';
|
||||
|
||||
const StyledInformationBannerWrapper = styled.div`
|
||||
@@ -20,7 +20,9 @@ const StyledInformationBannerWrapper = styled.div`
|
||||
|
||||
export const InformationBannerWrapper = () => {
|
||||
const subscriptionStatus = useSubscriptionStatus();
|
||||
const isWorkspaceSuspended = useIsWorkspaceActivationStatusSuspended();
|
||||
const isWorkspaceSuspended = useIsWorkspaceActivationStatusEqualsTo(
|
||||
WorkspaceActivationStatus.SUSPENDED,
|
||||
);
|
||||
|
||||
const displayBillingSubscriptionPausedBanner =
|
||||
isWorkspaceSuspended && subscriptionStatus === SubscriptionStatus.Paused;
|
||||
|
||||
+2
@@ -7,6 +7,7 @@ import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadat
|
||||
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
|
||||
import { prefetchViewsState } from '@/prefetch/states/prefetchViewsState';
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
import { ViewOpenRecordInType } from '@/views/types/ViewOpenRecordInType';
|
||||
import { ViewType } from '@/views/types/ViewType';
|
||||
import { getCompanyObjectMetadataItem } from '~/testing/mock-data/companies';
|
||||
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
|
||||
@@ -38,6 +39,7 @@ const renderHooks = ({
|
||||
type: ViewType.Table,
|
||||
key: null,
|
||||
isCompact: false,
|
||||
openRecordIn: ViewOpenRecordInType.SIDE_PANEL,
|
||||
viewFields: [],
|
||||
viewGroups: [],
|
||||
viewSorts: [],
|
||||
|
||||
+1
-2
@@ -63,9 +63,8 @@ describe('useColumnDefinitionsFromFieldMetadata', () => {
|
||||
},
|
||||
);
|
||||
|
||||
const { columnDefinitions, sortDefinitions } = result.current;
|
||||
const { columnDefinitions } = result.current;
|
||||
|
||||
expect(columnDefinitions.length).toBe(21);
|
||||
expect(sortDefinitions.length).toBe(14);
|
||||
});
|
||||
});
|
||||
|
||||
+10
-7
@@ -4,9 +4,9 @@ import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefin
|
||||
import { filterAvailableTableColumns } from '@/object-record/utils/filterAvailableTableColumns';
|
||||
|
||||
import { availableFieldMetadataItemsForFilterFamilySelector } from '@/object-metadata/states/availableFieldMetadataItemsForFilterFamilySelector';
|
||||
import { availableFieldMetadataItemsForSortFamilySelector } from '@/object-metadata/states/availableFieldMetadataItemsForSortFamilySelector';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { formatFieldMetadataItemAsColumnDefinition } from '../utils/formatFieldMetadataItemAsColumnDefinition';
|
||||
import { formatFieldMetadataItemsAsSortDefinitions } from '../utils/formatFieldMetadataItemsAsSortDefinitions';
|
||||
|
||||
export const useColumnDefinitionsFromFieldMetadata = (
|
||||
objectMetadataItem: ObjectMetadataItem,
|
||||
@@ -21,9 +21,11 @@ export const useColumnDefinitionsFromFieldMetadata = (
|
||||
}),
|
||||
);
|
||||
|
||||
const sortDefinitions = formatFieldMetadataItemsAsSortDefinitions({
|
||||
fields: activeFieldMetadataItems,
|
||||
});
|
||||
const sortableFieldMetadataItems = useRecoilValue(
|
||||
availableFieldMetadataItemsForSortFamilySelector({
|
||||
objectMetadataItemId: objectMetadataItem.id,
|
||||
}),
|
||||
);
|
||||
|
||||
const columnDefinitions: ColumnDefinition<FieldMetadata>[] =
|
||||
activeFieldMetadataItems
|
||||
@@ -40,8 +42,10 @@ export const useColumnDefinitionsFromFieldMetadata = (
|
||||
(fieldMetadataItem) =>
|
||||
fieldMetadataItem.id === column.fieldMetadataId,
|
||||
);
|
||||
const existsInSortDefinitions = sortDefinitions.some(
|
||||
(sort) => sort.fieldMetadataId === column.fieldMetadataId,
|
||||
|
||||
const existsInSortDefinitions = sortableFieldMetadataItems.some(
|
||||
(fieldMetadataItem) =>
|
||||
fieldMetadataItem.id === column.fieldMetadataId,
|
||||
);
|
||||
return {
|
||||
...column,
|
||||
@@ -52,6 +56,5 @@ export const useColumnDefinitionsFromFieldMetadata = (
|
||||
|
||||
return {
|
||||
columnDefinitions,
|
||||
sortDefinitions,
|
||||
};
|
||||
};
|
||||
|
||||
-24
@@ -1,24 +0,0 @@
|
||||
import { SortDefinition } from '@/object-record/object-sort-dropdown/types/SortDefinition';
|
||||
|
||||
import { SORTABLE_FIELD_METADATA_TYPES } from '@/object-metadata/constants/SortableFieldMetadataTypes';
|
||||
import { ObjectMetadataItem } from '../types/ObjectMetadataItem';
|
||||
|
||||
export const formatFieldMetadataItemsAsSortDefinitions = ({
|
||||
fields,
|
||||
}: {
|
||||
fields: Array<ObjectMetadataItem['fields'][0]>;
|
||||
}): SortDefinition[] =>
|
||||
fields.reduce((acc, field) => {
|
||||
if (!SORTABLE_FIELD_METADATA_TYPES.includes(field.type)) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
return [
|
||||
...acc,
|
||||
{
|
||||
fieldMetadataId: field.id,
|
||||
label: field.label,
|
||||
iconName: field.icon ?? 'Icon123',
|
||||
},
|
||||
];
|
||||
}, [] as SortDefinition[]);
|
||||
@@ -1,9 +1,13 @@
|
||||
import { AvatarChip, AvatarChipVariant } from 'twenty-ui';
|
||||
|
||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||
import { getLinkToShowPage } from '@/object-metadata/utils/getLinkToShowPage';
|
||||
import { useRecordChipData } from '@/object-record/hooks/useRecordChipData';
|
||||
import { recordIndexOpenRecordInSelector } from '@/object-record/record-index/states/selectors/recordIndexOpenRecordInSelector';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { ViewOpenRecordInType } from '@/views/types/ViewOpenRecordInType';
|
||||
import { MouseEvent } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
export type RecordChipProps = {
|
||||
objectNameSingular: string;
|
||||
@@ -23,8 +27,20 @@ export const RecordChip = ({
|
||||
record,
|
||||
});
|
||||
|
||||
const { openRecordInCommandMenu } = useCommandMenu();
|
||||
|
||||
const recordIndexOpenRecordIn = useRecoilValue(
|
||||
recordIndexOpenRecordInSelector,
|
||||
);
|
||||
|
||||
const handleClick = (e: MouseEvent<Element>) => {
|
||||
e.stopPropagation();
|
||||
if (recordIndexOpenRecordIn === ViewOpenRecordInType.SIDE_PANEL) {
|
||||
openRecordInCommandMenu({
|
||||
recordId: record.id,
|
||||
objectNameSingular,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -36,7 +52,11 @@ export const RecordChip = ({
|
||||
className={className}
|
||||
variant={variant}
|
||||
onClick={handleClick}
|
||||
to={getLinkToShowPage(objectNameSingular, record)}
|
||||
to={
|
||||
recordIndexOpenRecordIn === ViewOpenRecordInType.RECORD_PAGE
|
||||
? getLinkToShowPage(objectNameSingular, record)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
+3
@@ -6,6 +6,7 @@ import { ObjectOptionsDropdownRecordGroupFieldsContent } from '@/object-record/o
|
||||
import { ObjectOptionsDropdownRecordGroupsContent } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupsContent';
|
||||
import { ObjectOptionsDropdownRecordGroupSortContent } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupSortContent';
|
||||
import { ObjectOptionsDropdownViewSettingsContent } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdownViewSettingsContent';
|
||||
import { ObjectOptionsDropdownViewSettingsOpenInContent } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdownViewSettingsOpenInContent';
|
||||
import { useOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useOptionsDropdown';
|
||||
|
||||
export const ObjectOptionsDropdownContent = () => {
|
||||
@@ -14,6 +15,8 @@ export const ObjectOptionsDropdownContent = () => {
|
||||
switch (currentContentId) {
|
||||
case 'viewSettings':
|
||||
return <ObjectOptionsDropdownViewSettingsContent />;
|
||||
case 'viewSettingsOpenIn':
|
||||
return <ObjectOptionsDropdownViewSettingsOpenInContent />;
|
||||
case 'fields':
|
||||
return <ObjectOptionsDropdownFieldsContent />;
|
||||
case 'hiddenFields':
|
||||
|
||||
+9
-2
@@ -32,7 +32,9 @@ import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
|
||||
import { ViewType } from '@/views/types/ViewType';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import { FeatureFlagKey } from '~/generated-metadata/graphql';
|
||||
|
||||
export const ObjectOptionsDropdownMenuContent = () => {
|
||||
const {
|
||||
@@ -99,13 +101,17 @@ export const ObjectOptionsDropdownMenuContent = () => {
|
||||
objectMetadataItem.nameSingular !== CoreObjectNameSingular.Note &&
|
||||
objectMetadataItem.nameSingular !== CoreObjectNameSingular.Task;
|
||||
|
||||
const isCommandMenuV2Enabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IsCommandMenuV2Enabled,
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenuHeader StartIcon={CurrentViewIcon ?? IconList}>
|
||||
{currentView?.name}
|
||||
</DropdownMenuHeader>
|
||||
{/** TODO: Should be removed when view settings contains more options */}
|
||||
{viewType === ViewType.Kanban && (
|
||||
|
||||
{(isCommandMenuV2Enabled || viewType === ViewType.Kanban) && (
|
||||
<>
|
||||
<DropdownMenuItemsContainer scrollable={false}>
|
||||
<MenuItem
|
||||
@@ -118,6 +124,7 @@ export const ObjectOptionsDropdownMenuContent = () => {
|
||||
<DropdownMenuSeparator />
|
||||
</>
|
||||
)}
|
||||
|
||||
<DropdownMenuItemsContainer scrollable={false}>
|
||||
<MenuItem
|
||||
onClick={() => onContentChange('fields')}
|
||||
|
||||
+38
-2
@@ -1,21 +1,34 @@
|
||||
import {
|
||||
IconBaselineDensitySmall,
|
||||
IconChevronLeft,
|
||||
IconLayoutNavbar,
|
||||
IconLayoutSidebarRight,
|
||||
MenuItem,
|
||||
MenuItemToggle,
|
||||
} from 'twenty-ui';
|
||||
|
||||
import { useObjectOptionsForBoard } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForBoard';
|
||||
import { useOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useOptionsDropdown';
|
||||
import { recordIndexOpenRecordInState } from '@/object-record/record-index/states/recordIndexOpenRecordInState';
|
||||
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
|
||||
import { ViewOpenRecordInType } from '@/views/types/ViewOpenRecordInType';
|
||||
import { ViewType } from '@/views/types/ViewType';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { FeatureFlagKey } from '~/generated-metadata/graphql';
|
||||
|
||||
export const ObjectOptionsDropdownViewSettingsContent = () => {
|
||||
const { currentViewWithCombinedFiltersAndSorts } = useGetCurrentView();
|
||||
|
||||
const { recordIndexId, objectMetadataItem, viewType, resetContent } =
|
||||
useOptionsDropdown();
|
||||
const {
|
||||
recordIndexId,
|
||||
objectMetadataItem,
|
||||
viewType,
|
||||
resetContent,
|
||||
onContentChange,
|
||||
} = useOptionsDropdown();
|
||||
|
||||
const { isCompactModeActive, setAndPersistIsCompactModeActive } =
|
||||
useObjectOptionsForBoard({
|
||||
@@ -24,12 +37,35 @@ export const ObjectOptionsDropdownViewSettingsContent = () => {
|
||||
viewBarId: recordIndexId,
|
||||
});
|
||||
|
||||
const recordIndexOpenRecordIn = useRecoilValue(recordIndexOpenRecordInState);
|
||||
|
||||
const isCommandMenuV2Enabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IsCommandMenuV2Enabled,
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenuHeader StartIcon={IconChevronLeft} onClick={resetContent}>
|
||||
View settings
|
||||
</DropdownMenuHeader>
|
||||
<DropdownMenuItemsContainer>
|
||||
{isCommandMenuV2Enabled && (
|
||||
<MenuItem
|
||||
onClick={() => onContentChange('viewSettingsOpenIn')}
|
||||
LeftIcon={
|
||||
recordIndexOpenRecordIn === ViewOpenRecordInType.SIDE_PANEL
|
||||
? IconLayoutSidebarRight
|
||||
: IconLayoutNavbar
|
||||
}
|
||||
text="Open in"
|
||||
contextualText={
|
||||
recordIndexOpenRecordIn === ViewOpenRecordInType.SIDE_PANEL
|
||||
? 'Side Panel'
|
||||
: 'Record Page'
|
||||
}
|
||||
hasSubMenu
|
||||
/>
|
||||
)}
|
||||
{viewType === ViewType.Kanban && (
|
||||
<MenuItemToggle
|
||||
LeftIcon={IconBaselineDensitySmall}
|
||||
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
import {
|
||||
IconChevronLeft,
|
||||
IconLayoutNavbar,
|
||||
IconLayoutSidebarRight,
|
||||
MenuItemSelect,
|
||||
} from 'twenty-ui';
|
||||
|
||||
import { useObjectOptions } from '@/object-record/object-options-dropdown/hooks/useObjectOptions';
|
||||
import { useOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useOptionsDropdown';
|
||||
import { recordIndexOpenRecordInState } from '@/object-record/record-index/states/recordIndexOpenRecordInState';
|
||||
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
|
||||
import { ViewOpenRecordInType } from '@/views/types/ViewOpenRecordInType';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
export const ObjectOptionsDropdownViewSettingsOpenInContent = () => {
|
||||
const { onContentChange } = useOptionsDropdown();
|
||||
const recordIndexOpenRecordIn = useRecoilValue(recordIndexOpenRecordInState);
|
||||
const { currentViewWithCombinedFiltersAndSorts } = useGetCurrentView();
|
||||
const { setAndPersistOpenRecordIn } = useObjectOptions();
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenuHeader
|
||||
StartIcon={IconChevronLeft}
|
||||
onClick={() => onContentChange('viewSettings')}
|
||||
>
|
||||
{t`Open in`}
|
||||
</DropdownMenuHeader>
|
||||
<DropdownMenuItemsContainer>
|
||||
<MenuItemSelect
|
||||
LeftIcon={IconLayoutSidebarRight}
|
||||
text="Side Panel"
|
||||
selected={recordIndexOpenRecordIn === ViewOpenRecordInType.SIDE_PANEL}
|
||||
onClick={() =>
|
||||
setAndPersistOpenRecordIn(
|
||||
ViewOpenRecordInType.SIDE_PANEL,
|
||||
currentViewWithCombinedFiltersAndSorts,
|
||||
)
|
||||
}
|
||||
/>
|
||||
<MenuItemSelect
|
||||
LeftIcon={IconLayoutNavbar}
|
||||
text="Record Page"
|
||||
selected={
|
||||
recordIndexOpenRecordIn === ViewOpenRecordInType.RECORD_PAGE
|
||||
}
|
||||
onClick={() =>
|
||||
setAndPersistOpenRecordIn(
|
||||
ViewOpenRecordInType.RECORD_PAGE,
|
||||
currentViewWithCombinedFiltersAndSorts,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</DropdownMenuItemsContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import { recordIndexOpenRecordInState } from '@/object-record/record-index/states/recordIndexOpenRecordInState';
|
||||
import { useUpdateCurrentView } from '@/views/hooks/useUpdateCurrentView';
|
||||
import { GraphQLView } from '@/views/types/GraphQLView';
|
||||
import { ViewOpenRecordInType } from '@/views/types/ViewOpenRecordInType';
|
||||
import { useCallback } from 'react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
|
||||
export const useObjectOptions = () => {
|
||||
const setRecordIndexOpenRecordIn = useSetRecoilState(
|
||||
recordIndexOpenRecordInState,
|
||||
);
|
||||
|
||||
const { updateCurrentView } = useUpdateCurrentView();
|
||||
|
||||
const setAndPersistOpenRecordIn = useCallback(
|
||||
(openRecordIn: ViewOpenRecordInType, view: GraphQLView | undefined) => {
|
||||
if (!view) return;
|
||||
setRecordIndexOpenRecordIn(openRecordIn);
|
||||
updateCurrentView({
|
||||
openRecordIn,
|
||||
});
|
||||
},
|
||||
[setRecordIndexOpenRecordIn, updateCurrentView],
|
||||
);
|
||||
|
||||
return {
|
||||
setAndPersistOpenRecordIn,
|
||||
};
|
||||
};
|
||||
+1
@@ -1,5 +1,6 @@
|
||||
export type ObjectOptionsContentId =
|
||||
| 'viewSettings'
|
||||
| 'viewSettingsOpenIn'
|
||||
| 'fields'
|
||||
| 'hiddenFields'
|
||||
| 'recordGroups'
|
||||
|
||||
+41
-34
@@ -1,6 +1,8 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { IconChevronDown, MenuItem, useIcons } from 'twenty-ui';
|
||||
|
||||
import { availableFieldMetadataItemsForSortFamilySelector } from '@/object-metadata/states/availableFieldMetadataItemsForSortFamilySelector';
|
||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { OBJECT_SORT_DROPDOWN_ID } from '@/object-record/object-sort-dropdown/constants/ObjectSortDropdownId';
|
||||
import { useCloseSortDropdown } from '@/object-record/object-sort-dropdown/hooks/useCloseSortDropdown';
|
||||
import { useResetRecordSortDropdownSearchInput } from '@/object-record/object-sort-dropdown/hooks/useResetRecordSortDropdownSearchInput';
|
||||
@@ -10,7 +12,6 @@ import { isRecordSortDirectionDropdownMenuUnfoldedComponentState } from '@/objec
|
||||
import { objectSortDropdownSearchInputComponentState } from '@/object-record/object-sort-dropdown/states/objectSortDropdownSearchInputComponentState';
|
||||
import { onSortSelectComponentState } from '@/object-record/object-sort-dropdown/states/onSortSelectScopedState';
|
||||
import { selectedRecordSortDirectionComponentState } from '@/object-record/object-sort-dropdown/states/selectedRecordSortDirectionComponentState';
|
||||
import { SortDefinition } from '@/object-record/object-sort-dropdown/types/SortDefinition';
|
||||
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
|
||||
import {
|
||||
RECORD_SORT_DIRECTIONS,
|
||||
@@ -28,8 +29,8 @@ import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
||||
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||
import { availableSortDefinitionsComponentState } from '@/views/states/availableSortDefinitionsComponentState';
|
||||
import { Trans, useLingui } from '@lingui/react/macro';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
export const StyledInput = styled.input`
|
||||
@@ -89,14 +90,16 @@ export const ObjectSortDropdownButton = ({
|
||||
|
||||
const { resetSortDropdown } = useResetSortDropdown();
|
||||
|
||||
const { recordIndexId } = useRecordIndexContextOrThrow();
|
||||
const { recordIndexId, objectMetadataItem } = useRecordIndexContextOrThrow();
|
||||
|
||||
const objectSortDropdownSearchInput = useRecoilComponentValueV2(
|
||||
objectSortDropdownSearchInputComponentState,
|
||||
);
|
||||
|
||||
const availableSortDefinitions = useRecoilComponentValueV2(
|
||||
availableSortDefinitionsComponentState,
|
||||
const sortableFieldMetadataItems = useRecoilValue(
|
||||
availableFieldMetadataItemsForSortFamilySelector({
|
||||
objectMetadataItemId: objectMetadataItem.id,
|
||||
}),
|
||||
);
|
||||
|
||||
const { getIcon } = useIcons();
|
||||
@@ -105,40 +108,45 @@ export const ObjectSortDropdownButton = ({
|
||||
visibleTableColumnsComponentSelector,
|
||||
recordIndexId,
|
||||
);
|
||||
const visibleColumnsIds = visibleTableColumns.map(
|
||||
const visibleColumnsFieldMetadataIds = visibleTableColumns.map(
|
||||
(column) => column.fieldMetadataId,
|
||||
);
|
||||
const hiddenTableColumns = useRecoilComponentValueV2(
|
||||
hiddenTableColumnsComponentSelector,
|
||||
recordIndexId,
|
||||
);
|
||||
const hiddenColumnIds = hiddenTableColumns.map(
|
||||
const hiddenColumnFieldMetadataIds = hiddenTableColumns.map(
|
||||
(column) => column.fieldMetadataId,
|
||||
);
|
||||
|
||||
const filteredSearchInputSortDefinitions = availableSortDefinitions.filter(
|
||||
(item) =>
|
||||
const filteredSearchInputFieldMetadataItems =
|
||||
sortableFieldMetadataItems.filter((item) =>
|
||||
item.label
|
||||
.toLocaleLowerCase()
|
||||
.includes(objectSortDropdownSearchInput.toLocaleLowerCase()),
|
||||
);
|
||||
);
|
||||
|
||||
const visibleColumnsSortDefinitions = filteredSearchInputSortDefinitions
|
||||
.sort((a, b) => {
|
||||
const visibleFieldMetadataItems = filteredSearchInputFieldMetadataItems
|
||||
.sort((fieldMetadataItemA, fieldMetadataItemB) => {
|
||||
return (
|
||||
visibleColumnsIds.indexOf(a.fieldMetadataId) -
|
||||
visibleColumnsIds.indexOf(b.fieldMetadataId)
|
||||
visibleColumnsFieldMetadataIds.indexOf(fieldMetadataItemA.id) -
|
||||
visibleColumnsFieldMetadataIds.indexOf(fieldMetadataItemB.id)
|
||||
);
|
||||
})
|
||||
.filter((item) => visibleColumnsIds.includes(item.fieldMetadataId));
|
||||
.filter((fieldMetadataItem) =>
|
||||
visibleColumnsFieldMetadataIds.includes(fieldMetadataItem.id),
|
||||
);
|
||||
|
||||
const hiddenColumnsSortDefinitions = filteredSearchInputSortDefinitions
|
||||
.sort((a, b) => a.label.localeCompare(b.label))
|
||||
.filter((item) => hiddenColumnIds.includes(item.fieldMetadataId));
|
||||
const hiddenFieldMetadataItems = filteredSearchInputFieldMetadataItems
|
||||
.sort((fieldMetadataItemA, fieldMetadataItemB) =>
|
||||
fieldMetadataItemA.label.localeCompare(fieldMetadataItemB.label),
|
||||
)
|
||||
.filter((fieldMetadataItem) =>
|
||||
hiddenColumnFieldMetadataIds.includes(fieldMetadataItem.id),
|
||||
);
|
||||
|
||||
const shoudShowSeparator =
|
||||
visibleColumnsSortDefinitions.length > 0 &&
|
||||
hiddenColumnsSortDefinitions.length > 0;
|
||||
const shouldShowSeparator =
|
||||
visibleFieldMetadataItems.length > 0 && hiddenFieldMetadataItems.length > 0;
|
||||
|
||||
const handleButtonClick = () => {
|
||||
toggleSortDropdown();
|
||||
@@ -153,14 +161,13 @@ export const ObjectSortDropdownButton = ({
|
||||
|
||||
const onSortSelect = useRecoilComponentValueV2(onSortSelectComponentState);
|
||||
|
||||
const handleAddSort = (sortDefinition: SortDefinition) => {
|
||||
const handleAddSort = (fieldMetadataItem: FieldMetadataItem) => {
|
||||
setObjectSortDropdownSearchInput('');
|
||||
closeSortDropdown();
|
||||
onSortSelect?.({
|
||||
id: v4(),
|
||||
fieldMetadataId: sortDefinition.fieldMetadataId,
|
||||
fieldMetadataId: fieldMetadataItem.id,
|
||||
direction: selectedRecordSortDirection,
|
||||
definition: sortDefinition,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -231,25 +238,25 @@ export const ObjectSortDropdownButton = ({
|
||||
}
|
||||
/>
|
||||
<DropdownMenuItemsContainer>
|
||||
{visibleColumnsSortDefinitions.map(
|
||||
(visibleSortDefinition, index) => (
|
||||
{visibleFieldMetadataItems.map(
|
||||
(visibleFieldMetadataItem, index) => (
|
||||
<MenuItem
|
||||
testId={`visible-select-sort-${index}`}
|
||||
key={index}
|
||||
onClick={() => handleAddSort(visibleSortDefinition)}
|
||||
LeftIcon={getIcon(visibleSortDefinition.iconName)}
|
||||
text={visibleSortDefinition.label}
|
||||
onClick={() => handleAddSort(visibleFieldMetadataItem)}
|
||||
LeftIcon={getIcon(visibleFieldMetadataItem.icon)}
|
||||
text={visibleFieldMetadataItem.label}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
{shoudShowSeparator && <DropdownMenuSeparator />}
|
||||
{hiddenColumnsSortDefinitions.map((hiddenSortDefinition, index) => (
|
||||
{shouldShowSeparator && <DropdownMenuSeparator />}
|
||||
{hiddenFieldMetadataItems.map((hiddenFieldMetadataItem, index) => (
|
||||
<MenuItem
|
||||
testId={`hidden-select-sort-${index}`}
|
||||
key={index}
|
||||
onClick={() => handleAddSort(hiddenSortDefinition)}
|
||||
LeftIcon={getIcon(hiddenSortDefinition.iconName)}
|
||||
text={hiddenSortDefinition.label}
|
||||
onClick={() => handleAddSort(hiddenFieldMetadataItem)}
|
||||
LeftIcon={getIcon(hiddenFieldMetadataItem.icon)}
|
||||
text={hiddenFieldMetadataItem.label}
|
||||
/>
|
||||
))}
|
||||
</DropdownMenuItemsContainer>
|
||||
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
import { ObjectSortDropdownComponentInstanceContext } from '@/object-record/object-sort-dropdown/states/context/ObjectSortDropdownComponentInstanceContext';
|
||||
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
|
||||
import { SortDefinition } from '../types/SortDefinition';
|
||||
|
||||
export const availableSortDefinitionsComponentState = createComponentStateV2<
|
||||
SortDefinition[]
|
||||
>({
|
||||
key: 'availableSortDefinitionsComponentState',
|
||||
defaultValue: [],
|
||||
componentInstanceContext: ObjectSortDropdownComponentInstanceContext,
|
||||
});
|
||||
-5
@@ -1,5 +0,0 @@
|
||||
export type SortDefinition = {
|
||||
fieldMetadataId: string;
|
||||
label: string;
|
||||
iconName: string;
|
||||
};
|
||||
-12
@@ -1,15 +1,8 @@
|
||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { SortDefinition } from '@/object-record/object-sort-dropdown/types/SortDefinition';
|
||||
import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy';
|
||||
import { RecordSort } from '@/object-record/record-sort/types/RecordSort';
|
||||
|
||||
const sortDefinition: SortDefinition = {
|
||||
fieldMetadataId: 'id',
|
||||
label: 'definition label',
|
||||
iconName: 'icon',
|
||||
};
|
||||
|
||||
const objectMetadataItem: ObjectMetadataItem = {
|
||||
id: 'object1',
|
||||
fields: [],
|
||||
@@ -47,7 +40,6 @@ describe('turnSortsIntoOrderBy', () => {
|
||||
id: 'id',
|
||||
fieldMetadataId: 'field1',
|
||||
direction: 'asc',
|
||||
definition: sortDefinition,
|
||||
},
|
||||
];
|
||||
const fields = [{ id: 'field1', name: 'field1' }] as FieldMetadataItem[];
|
||||
@@ -62,13 +54,11 @@ describe('turnSortsIntoOrderBy', () => {
|
||||
id: 'id',
|
||||
fieldMetadataId: 'field1',
|
||||
direction: 'asc',
|
||||
definition: sortDefinition,
|
||||
},
|
||||
{
|
||||
id: 'id',
|
||||
fieldMetadataId: 'field2',
|
||||
direction: 'desc',
|
||||
definition: sortDefinition,
|
||||
},
|
||||
];
|
||||
const fields = [
|
||||
@@ -90,7 +80,6 @@ describe('turnSortsIntoOrderBy', () => {
|
||||
id: 'id',
|
||||
fieldMetadataId: 'invalidField',
|
||||
direction: 'asc',
|
||||
definition: sortDefinition,
|
||||
},
|
||||
];
|
||||
expect(turnSortsIntoOrderBy(objectMetadataItem, sorts)).toEqual([
|
||||
@@ -104,7 +93,6 @@ describe('turnSortsIntoOrderBy', () => {
|
||||
id: 'id',
|
||||
fieldMetadataId: 'invalidField',
|
||||
direction: 'asc',
|
||||
definition: sortDefinition,
|
||||
},
|
||||
];
|
||||
expect(
|
||||
|
||||
-2
@@ -4,7 +4,6 @@ import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/s
|
||||
import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState';
|
||||
import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { sortRecordsByPosition } from '@/object-record/utils/sortRecordsByPosition';
|
||||
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
|
||||
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
@@ -49,7 +48,6 @@ export const useSetRecordIdsForColumn = (recordBoardId?: string) => {
|
||||
(record) =>
|
||||
record[recordGroupFieldMetadata.name] === recordGroup?.value,
|
||||
)
|
||||
.sort(sortRecordsByPosition)
|
||||
.map((record) => record.id);
|
||||
|
||||
if (!isDeeplyEqual(existingRecordGroupRowIds, recordGroupRowIds)) {
|
||||
|
||||
+51
-28
@@ -1,42 +1,45 @@
|
||||
import {
|
||||
AvatarChipVariant,
|
||||
Checkbox,
|
||||
CheckboxVariant,
|
||||
LightIconButton,
|
||||
IconEye,
|
||||
IconEyeOff,
|
||||
} from 'twenty-ui';
|
||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
|
||||
import { useRecordBoardSelection } from '@/object-record/record-board/hooks/useRecordBoardSelection';
|
||||
import { RecordBoardCardHeaderContainer } from '@/object-record/record-board/record-board-card/components/RecordBoardCardHeaderContainer';
|
||||
import { RecordInlineCellEditMode } from '@/object-record/record-inline-cell/components/RecordInlineCellEditMode';
|
||||
import styled from '@emotion/styled';
|
||||
import { TextInput } from '@/ui/input/components/TextInput';
|
||||
import { Dispatch, SetStateAction, useContext, useState } from 'react';
|
||||
import { StopPropagationContainer } from '@/object-record/record-board/record-board-card/components/StopPropagationContainer';
|
||||
import { RecordBoardCardContext } from '@/object-record/record-board/record-board-card/contexts/RecordBoardCardContext';
|
||||
import { useAddNewCard } from '@/object-record/record-board/record-board-column/hooks/useAddNewCard';
|
||||
import { RecordBoardScopeInternalContext } from '@/object-record/record-board/scopes/scope-internal-context/RecordBoardScopeInternalContext';
|
||||
import { isRecordBoardCardSelectedComponentFamilyState } from '@/object-record/record-board/states/isRecordBoardCardSelectedComponentFamilyState';
|
||||
import { isRecordBoardCompactModeActiveComponentState } from '@/object-record/record-board/states/isRecordBoardCompactModeActiveComponentState';
|
||||
import { RecordBoardFieldDefinition } from '@/object-record/record-board/types/RecordBoardFieldDefinition';
|
||||
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import {
|
||||
FieldContext,
|
||||
RecordUpdateHook,
|
||||
RecordUpdateHookParams,
|
||||
} from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { getFieldButtonIcon } from '@/object-record/record-field/utils/getFieldButtonIcon';
|
||||
import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope';
|
||||
import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
|
||||
import { RecordBoardCardContext } from '@/object-record/record-board/record-board-card/contexts/RecordBoardCardContext';
|
||||
import { RecordIdentifierChip } from '@/object-record/record-index/components/RecordIndexRecordChip';
|
||||
import { useRecoilComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyStateV2';
|
||||
import { isRecordBoardCardSelectedComponentFamilyState } from '@/object-record/record-board/states/isRecordBoardCardSelectedComponentFamilyState';
|
||||
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
|
||||
import { RecordBoardScopeInternalContext } from '@/object-record/record-board/scopes/scope-internal-context/RecordBoardScopeInternalContext';
|
||||
import { useRecordBoardSelection } from '@/object-record/record-board/hooks/useRecordBoardSelection';
|
||||
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
|
||||
import { recordIndexOpenRecordInSelector } from '@/object-record/record-index/states/selectors/recordIndexOpenRecordInSelector';
|
||||
import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell';
|
||||
import { RecordInlineCellEditMode } from '@/object-record/record-inline-cell/components/RecordInlineCellEditMode';
|
||||
import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { TextInput } from '@/ui/input/components/TextInput';
|
||||
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
|
||||
import { useRecoilComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyStateV2';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { isRecordBoardCompactModeActiveComponentState } from '@/object-record/record-board/states/isRecordBoardCompactModeActiveComponentState';
|
||||
import { StopPropagationContainer } from '@/object-record/record-board/record-board-card/components/StopPropagationContainer';
|
||||
import { ViewOpenRecordInType } from '@/views/types/ViewOpenRecordInType';
|
||||
import styled from '@emotion/styled';
|
||||
import { Dispatch, SetStateAction, useContext, useState } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import {
|
||||
AvatarChipVariant,
|
||||
Checkbox,
|
||||
CheckboxVariant,
|
||||
IconEye,
|
||||
IconEyeOff,
|
||||
LightIconButton,
|
||||
} from 'twenty-ui';
|
||||
|
||||
const StyledTextInput = styled(TextInput)`
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
@@ -116,6 +119,12 @@ export const RecordBoardCardHeader = ({
|
||||
return [updateEntity, { loading: false }];
|
||||
};
|
||||
|
||||
const recordIndexOpenRecordIn = useRecoilValue(
|
||||
recordIndexOpenRecordInSelector,
|
||||
);
|
||||
|
||||
const { openRecordInCommandMenu } = useCommandMenu();
|
||||
|
||||
return (
|
||||
<RecordBoardCardHeaderContainer showCompactView={showCompactView}>
|
||||
<StopPropagationContainer>
|
||||
@@ -178,7 +187,21 @@ export const RecordBoardCardHeader = ({
|
||||
record={record as ObjectRecord}
|
||||
variant={AvatarChipVariant.Transparent}
|
||||
maxWidth={150}
|
||||
to={indexIdentifierUrl(recordId)}
|
||||
onClick={
|
||||
recordIndexOpenRecordIn === ViewOpenRecordInType.SIDE_PANEL
|
||||
? () => {
|
||||
openRecordInCommandMenu({
|
||||
recordId,
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
});
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
to={
|
||||
recordIndexOpenRecordIn === ViewOpenRecordInType.RECORD_PAGE
|
||||
? indexIdentifierUrl(recordId)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</StopPropagationContainer>
|
||||
|
||||
+10
-6
@@ -11,6 +11,7 @@ import { useColumnNewCardActions } from '@/object-record/record-board/record-boa
|
||||
import { useIsOpportunitiesCompanyFieldDisabled } from '@/object-record/record-board/record-board-column/hooks/useIsOpportunitiesCompanyFieldDisabled';
|
||||
import { RecordBoardColumnHotkeyScope } from '@/object-record/record-board/types/BoardColumnHotkeyScope';
|
||||
import { RecordGroupDefinitionType } from '@/object-record/record-group/types/RecordGroupDefinition';
|
||||
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
|
||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||
import { IconDotsVertical, IconPlus, LightIconButton, Tag } from 'twenty-ui';
|
||||
|
||||
@@ -97,6 +98,8 @@ export const RecordBoardColumnHeader = () => {
|
||||
columnDefinition.id ?? '',
|
||||
);
|
||||
|
||||
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
|
||||
|
||||
const { isOpportunitiesCompanyFieldDisabled } =
|
||||
useIsOpportunitiesCompanyFieldDisabled();
|
||||
|
||||
@@ -146,12 +149,13 @@ export const RecordBoardColumnHeader = () => {
|
||||
Icon={IconDotsVertical}
|
||||
onClick={handleBoardColumnMenuOpen}
|
||||
/>
|
||||
|
||||
<LightIconButton
|
||||
accent="tertiary"
|
||||
Icon={IconPlus}
|
||||
onClick={() => handleNewButtonClick('first', isOpportunity)}
|
||||
/>
|
||||
{!hasObjectReadOnlyPermission && (
|
||||
<LightIconButton
|
||||
accent="tertiary"
|
||||
Icon={IconPlus}
|
||||
onClick={() => handleNewButtonClick('first', isOpportunity)}
|
||||
/>
|
||||
)}
|
||||
</StyledHeaderActions>
|
||||
)}
|
||||
</StyledRightContainer>
|
||||
|
||||
+8
@@ -1,6 +1,7 @@
|
||||
import { RecordBoardCard } from '@/object-record/record-board/record-board-card/components/RecordBoardCard';
|
||||
import { useAddNewCard } from '@/object-record/record-board/record-board-column/hooks/useAddNewCard';
|
||||
import { recordBoardNewRecordByColumnIdSelector } from '@/object-record/record-board/states/selectors/recordBoardNewRecordByColumnIdSelector';
|
||||
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
export const RecordBoardColumnNewRecord = ({
|
||||
@@ -16,8 +17,15 @@ export const RecordBoardColumnNewRecord = ({
|
||||
scopeId: columnId,
|
||||
}),
|
||||
);
|
||||
|
||||
const { handleCreateSuccess } = useAddNewCard();
|
||||
|
||||
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
|
||||
|
||||
if (hasObjectReadOnlyPermission) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{newRecord.isCreating && newRecord.position === position && (
|
||||
|
||||
+7
@@ -1,4 +1,5 @@
|
||||
import { useColumnNewCardActions } from '@/object-record/record-board/record-board-column/hooks/useColumnNewCardActions';
|
||||
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { IconPlus } from 'twenty-ui';
|
||||
@@ -29,6 +30,12 @@ export const RecordBoardColumnNewRecordButton = ({
|
||||
|
||||
const { handleNewButtonClick } = useColumnNewCardActions(columnId);
|
||||
|
||||
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
|
||||
|
||||
if (hasObjectReadOnlyPermission) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledNewButton onClick={() => handleNewButtonClick('last', false)}>
|
||||
<IconPlus size={theme.icon.size.md} />
|
||||
|
||||
+4
@@ -3,6 +3,7 @@ import { useContext } from 'react';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { FieldContext } from '../contexts/FieldContext';
|
||||
import { isFieldValueReadOnly } from '../utils/isFieldValueReadOnly';
|
||||
@@ -20,11 +21,14 @@ export const useIsFieldValueReadOnly = () => {
|
||||
objectNameSingular: metadata.objectMetadataNameSingular ?? '',
|
||||
});
|
||||
|
||||
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
|
||||
|
||||
return isFieldValueReadOnly({
|
||||
objectNameSingular: metadata.objectMetadataNameSingular,
|
||||
fieldName: metadata.fieldName,
|
||||
fieldType: type,
|
||||
isObjectRemote: objectMetadataItem.isRemote,
|
||||
isRecordDeleted: recordFromStore?.deletedAt,
|
||||
hasObjectReadOnlyPermission,
|
||||
});
|
||||
};
|
||||
|
||||
+25
-1
@@ -1,6 +1,10 @@
|
||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||
import { RecordChip } from '@/object-record/components/RecordChip';
|
||||
import { useChipFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useChipFieldDisplay';
|
||||
import { RecordIdentifierChip } from '@/object-record/record-index/components/RecordIndexRecordChip';
|
||||
import { recordIndexOpenRecordInSelector } from '@/object-record/record-index/states/selectors/recordIndexOpenRecordInSelector';
|
||||
import { ViewOpenRecordInType } from '@/views/types/ViewOpenRecordInType';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { ChipSize } from 'twenty-ui';
|
||||
|
||||
export const ChipFieldDisplay = () => {
|
||||
@@ -11,6 +15,12 @@ export const ChipFieldDisplay = () => {
|
||||
labelIdentifierLink,
|
||||
} = useChipFieldDisplay();
|
||||
|
||||
const recordIndexOpenRecordIn = useRecoilValue(
|
||||
recordIndexOpenRecordInSelector,
|
||||
);
|
||||
|
||||
const { openRecordInCommandMenu } = useCommandMenu();
|
||||
|
||||
if (!recordValue) {
|
||||
return null;
|
||||
}
|
||||
@@ -20,7 +30,21 @@ export const ChipFieldDisplay = () => {
|
||||
objectNameSingular={objectNameSingular}
|
||||
record={recordValue}
|
||||
size={ChipSize.Small}
|
||||
to={labelIdentifierLink}
|
||||
to={
|
||||
recordIndexOpenRecordIn === ViewOpenRecordInType.RECORD_PAGE
|
||||
? labelIdentifierLink
|
||||
: undefined
|
||||
}
|
||||
onClick={
|
||||
recordIndexOpenRecordIn === ViewOpenRecordInType.SIDE_PANEL
|
||||
? () => {
|
||||
openRecordInCommandMenu({
|
||||
recordId: recordValue.id,
|
||||
objectNameSingular,
|
||||
});
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<RecordChip objectNameSingular={objectNameSingular} record={recordValue} />
|
||||
|
||||
+6
@@ -12,6 +12,7 @@ type isFieldValueReadOnlyParams = {
|
||||
fieldType?: FieldMetadataType;
|
||||
isObjectRemote?: boolean;
|
||||
isRecordDeleted?: boolean;
|
||||
hasObjectReadOnlyPermission?: boolean;
|
||||
};
|
||||
|
||||
export const isFieldValueReadOnly = ({
|
||||
@@ -20,6 +21,7 @@ export const isFieldValueReadOnly = ({
|
||||
fieldType,
|
||||
isObjectRemote = false,
|
||||
isRecordDeleted = false,
|
||||
hasObjectReadOnlyPermission = false,
|
||||
}: isFieldValueReadOnlyParams) => {
|
||||
if (fieldName === 'noteTargets' || fieldName === 'taskTargets') {
|
||||
return true;
|
||||
@@ -33,6 +35,10 @@ export const isFieldValueReadOnly = ({
|
||||
return true;
|
||||
}
|
||||
|
||||
if (hasObjectReadOnlyPermission) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isWorkflowSubObjectMetadata(objectNameSingular)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
+32
-32
@@ -4,14 +4,15 @@ import { useRecordGroupVisibility } from '@/object-record/record-group/hooks/use
|
||||
import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState';
|
||||
import { RecordGroupAction } from '@/object-record/record-group/types/RecordGroupActions';
|
||||
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
|
||||
import { useHasSettingsPermission } from '@/settings/roles/hooks/useHasSettingsPermission';
|
||||
import { SettingsPath } from '@/types/SettingsPath';
|
||||
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { ViewType } from '@/views/types/ViewType';
|
||||
import { useCallback, useContext, useMemo } from 'react';
|
||||
import { useCallback, useContext } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import { isDefined, SettingsPermissions } from 'twenty-shared';
|
||||
import { IconEyeOff, IconSettings } from 'twenty-ui';
|
||||
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
|
||||
|
||||
@@ -69,37 +70,36 @@ export const useRecordGroupActions = ({
|
||||
recordGroupFieldMetadata,
|
||||
]);
|
||||
|
||||
const recordGroupActions: RecordGroupAction[] = useMemo(
|
||||
() =>
|
||||
[
|
||||
{
|
||||
id: 'edit',
|
||||
label: 'Edit',
|
||||
icon: IconSettings,
|
||||
position: 0,
|
||||
callback: () => {
|
||||
navigateToSelectSettings();
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'hide',
|
||||
label: 'Hide',
|
||||
icon: IconEyeOff,
|
||||
position: 1,
|
||||
callback: () => {
|
||||
handleRecordGroupVisibilityChange({
|
||||
...recordGroupDefinition,
|
||||
isVisible: false,
|
||||
});
|
||||
},
|
||||
},
|
||||
].filter(isDefined),
|
||||
[
|
||||
handleRecordGroupVisibilityChange,
|
||||
navigateToSelectSettings,
|
||||
recordGroupDefinition,
|
||||
],
|
||||
const hasAccessToDataModelSettings = useHasSettingsPermission(
|
||||
SettingsPermissions.DATA_MODEL,
|
||||
);
|
||||
|
||||
const recordGroupActions: RecordGroupAction[] = [];
|
||||
|
||||
if (hasAccessToDataModelSettings) {
|
||||
recordGroupActions.push({
|
||||
id: 'edit',
|
||||
label: 'Edit',
|
||||
icon: IconSettings,
|
||||
position: 0,
|
||||
callback: () => {
|
||||
navigateToSelectSettings();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
recordGroupActions.push({
|
||||
id: 'hide',
|
||||
label: 'Hide',
|
||||
icon: IconEyeOff,
|
||||
position: 1,
|
||||
callback: () => {
|
||||
handleRecordGroupVisibilityChange({
|
||||
...recordGroupDefinition,
|
||||
isVisible: false,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return recordGroupActions;
|
||||
};
|
||||
|
||||
+3
@@ -11,6 +11,7 @@ export type RecordIdentifierChipProps = {
|
||||
size?: ChipSize;
|
||||
to?: string;
|
||||
maxWidth?: number;
|
||||
onClick?: () => void;
|
||||
};
|
||||
|
||||
export const RecordIdentifierChip = ({
|
||||
@@ -18,6 +19,7 @@ export const RecordIdentifierChip = ({
|
||||
record,
|
||||
variant,
|
||||
size,
|
||||
onClick,
|
||||
to,
|
||||
maxWidth,
|
||||
}: RecordIdentifierChipProps) => {
|
||||
@@ -40,6 +42,7 @@ export const RecordIdentifierChip = ({
|
||||
avatarType={recordChipData.avatarType}
|
||||
avatarUrl={recordChipData.avatarUrl ?? ''}
|
||||
to={to}
|
||||
onClick={onClick}
|
||||
variant={variant}
|
||||
LeftIcon={LeftIcon}
|
||||
LeftIconColor={LeftIconColor}
|
||||
|
||||
+3
-9
@@ -23,27 +23,21 @@ export const RecordIndexViewBarEffect = ({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const { columnDefinitions, sortDefinitions } =
|
||||
const { columnDefinitions } =
|
||||
useColumnDefinitionsFromFieldMetadata(objectMetadataItem);
|
||||
|
||||
const {
|
||||
setViewObjectMetadataId,
|
||||
setAvailableSortDefinitions,
|
||||
setAvailableFieldDefinitions,
|
||||
} = useInitViewBar(viewBarId);
|
||||
const { setViewObjectMetadataId, setAvailableFieldDefinitions } =
|
||||
useInitViewBar(viewBarId);
|
||||
|
||||
useEffect(() => {
|
||||
if (isUndefinedOrNull(objectMetadataItem)) {
|
||||
return;
|
||||
}
|
||||
setViewObjectMetadataId?.(objectMetadataItem.id);
|
||||
setAvailableSortDefinitions?.(sortDefinitions);
|
||||
setAvailableFieldDefinitions?.(columnDefinitions);
|
||||
}, [
|
||||
setViewObjectMetadataId,
|
||||
objectMetadataItem,
|
||||
setAvailableSortDefinitions,
|
||||
sortDefinitions,
|
||||
setAvailableFieldDefinitions,
|
||||
columnDefinitions,
|
||||
]);
|
||||
|
||||
+1
-7
@@ -8,7 +8,6 @@ import { useRecordGroupFilter } from '@/object-record/record-group/hooks/useReco
|
||||
import { tableViewFilterGroupsComponentState } from '@/object-record/record-table/states/tableViewFilterGroupsComponentState';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
|
||||
import { availableSortDefinitionsComponentState } from '@/views/states/availableSortDefinitionsComponentState';
|
||||
import { mapViewSortsToSorts } from '@/views/utils/mapViewSortsToSorts';
|
||||
|
||||
export const useFindManyRecordIndexTableParams = (
|
||||
@@ -33,14 +32,9 @@ export const useFindManyRecordIndexTableParams = (
|
||||
const { currentViewWithCombinedFiltersAndSorts } =
|
||||
useGetCurrentView(recordTableId);
|
||||
|
||||
const availableSortDefinitions = useRecoilComponentValueV2(
|
||||
availableSortDefinitionsComponentState,
|
||||
recordTableId,
|
||||
);
|
||||
|
||||
const viewSorts = currentViewWithCombinedFiltersAndSorts?.viewSorts ?? [];
|
||||
|
||||
const sorts = mapViewSortsToSorts(viewSorts, availableSortDefinitions);
|
||||
const sorts = mapViewSortsToSorts(viewSorts);
|
||||
|
||||
const currentRecordFilters = useRecoilComponentValueV2(
|
||||
currentRecordFiltersComponentState,
|
||||
|
||||
-5
@@ -40,11 +40,6 @@ export const useHandleToggleColumnSort = ({
|
||||
const newSort: RecordSort = {
|
||||
id: v4(),
|
||||
fieldMetadataId,
|
||||
definition: {
|
||||
fieldMetadataId,
|
||||
label: correspondingColumnDefinition.label,
|
||||
iconName: correspondingColumnDefinition.iconName,
|
||||
},
|
||||
direction: 'asc',
|
||||
};
|
||||
|
||||
|
||||
+1
-6
@@ -14,7 +14,6 @@ import { recordIndexViewFilterGroupsState } from '@/object-record/record-index/s
|
||||
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
|
||||
import { availableSortDefinitionsComponentState } from '@/views/states/availableSortDefinitionsComponentState';
|
||||
import { mapViewSortsToSorts } from '@/views/utils/mapViewSortsToSorts';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
|
||||
@@ -52,11 +51,7 @@ export const useLoadRecordIndexBoardColumn = ({
|
||||
|
||||
const viewsorts = currentViewWithCombinedFiltersAndSorts?.viewSorts ?? [];
|
||||
|
||||
const sortDefinitions = useRecoilComponentValueV2(
|
||||
availableSortDefinitionsComponentState,
|
||||
);
|
||||
|
||||
const sorts = mapViewSortsToSorts(viewsorts, sortDefinitions);
|
||||
const sorts = mapViewSortsToSorts(viewsorts);
|
||||
|
||||
const { filterValueDependencies } = useFilterValueDependencies();
|
||||
|
||||
|
||||
+21
-18
@@ -1,8 +1,8 @@
|
||||
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||
import { availableFieldMetadataItemsForFilterFamilySelector } from '@/object-metadata/states/availableFieldMetadataItemsForFilterFamilySelector';
|
||||
import { availableFieldMetadataItemsForSortFamilySelector } from '@/object-metadata/states/availableFieldMetadataItemsForSortFamilySelector';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition';
|
||||
import { formatFieldMetadataItemsAsSortDefinitions } from '@/object-metadata/utils/formatFieldMetadataItemsAsSortDefinitions';
|
||||
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { useSetRecordGroup } from '@/object-record/record-group/hooks/useSetRecordGroup';
|
||||
import { recordIndexFieldDefinitionsState } from '@/object-record/record-index/states/recordIndexFieldDefinitionsState';
|
||||
@@ -10,6 +10,7 @@ import { recordIndexFiltersState } from '@/object-record/record-index/states/rec
|
||||
import { recordIndexIsCompactModeActiveState } from '@/object-record/record-index/states/recordIndexIsCompactModeActiveState';
|
||||
import { recordIndexKanbanAggregateOperationState } from '@/object-record/record-index/states/recordIndexKanbanAggregateOperationState';
|
||||
import { recordIndexKanbanFieldMetadataIdState } from '@/object-record/record-index/states/recordIndexKanbanFieldMetadataIdState';
|
||||
import { recordIndexOpenRecordInState } from '@/object-record/record-index/states/recordIndexOpenRecordInState';
|
||||
import { recordIndexSortsState } from '@/object-record/record-index/states/recordIndexSortsState';
|
||||
import { recordIndexViewFilterGroupsState } from '@/object-record/record-index/states/recordIndexViewFilterGroupsState';
|
||||
import { recordIndexViewTypeState } from '@/object-record/record-index/states/recordIndexViewTypeState';
|
||||
@@ -48,6 +49,9 @@ export const useLoadRecordIndexStates = () => {
|
||||
recordIndexIsCompactModeActiveState,
|
||||
);
|
||||
const setRecordIndexViewType = useSetRecoilState(recordIndexViewTypeState);
|
||||
const setRecordIndexOpenRecordIn = useSetRecoilState(
|
||||
recordIndexOpenRecordInState,
|
||||
);
|
||||
const setRecordIndexViewKanbanFieldMetadataIdState = useSetRecoilState(
|
||||
recordIndexKanbanFieldMetadataIdState,
|
||||
);
|
||||
@@ -77,9 +81,13 @@ export const useLoadRecordIndexStates = () => {
|
||||
)
|
||||
.getValue();
|
||||
|
||||
const sortDefinitions = formatFieldMetadataItemsAsSortDefinitions({
|
||||
fields: activeFieldMetadataItems,
|
||||
});
|
||||
const sortableFieldMetadataItems = snapshot
|
||||
.getLoadable(
|
||||
availableFieldMetadataItemsForSortFamilySelector({
|
||||
objectMetadataItemId: objectMetadataItem.id,
|
||||
}),
|
||||
)
|
||||
.getValue();
|
||||
|
||||
const columnDefinitions: ColumnDefinition<FieldMetadata>[] =
|
||||
activeFieldMetadataItems
|
||||
@@ -97,9 +105,12 @@ export const useLoadRecordIndexStates = () => {
|
||||
(fieldMetadataItem) =>
|
||||
fieldMetadataItem.id === column.fieldMetadataId,
|
||||
);
|
||||
const existsInSortDefinitions = sortDefinitions.some(
|
||||
(sort) => sort.fieldMetadataId === column.fieldMetadataId,
|
||||
|
||||
const existsInSortDefinitions = sortableFieldMetadataItems.some(
|
||||
(fieldMetadataItem) =>
|
||||
fieldMetadataItem.id === column.fieldMetadataId,
|
||||
);
|
||||
|
||||
return {
|
||||
...column,
|
||||
isFilterable: existsInFilterDefinitions,
|
||||
@@ -224,24 +235,15 @@ export const useLoadRecordIndexStates = () => {
|
||||
),
|
||||
}));
|
||||
|
||||
const activeFieldMetadataItems = objectMetadataItem.fields.filter(
|
||||
({ isActive, isSystem }) => isActive && !isSystem,
|
||||
);
|
||||
|
||||
const sortDefinitions = formatFieldMetadataItemsAsSortDefinitions({
|
||||
fields: activeFieldMetadataItems,
|
||||
});
|
||||
|
||||
set(
|
||||
tableSortsComponentState.atomFamily({
|
||||
instanceId: recordIndexId,
|
||||
}),
|
||||
mapViewSortsToSorts(view.viewSorts, sortDefinitions),
|
||||
);
|
||||
setRecordIndexSorts(
|
||||
mapViewSortsToSorts(view.viewSorts, sortDefinitions),
|
||||
mapViewSortsToSorts(view.viewSorts),
|
||||
);
|
||||
setRecordIndexSorts(mapViewSortsToSorts(view.viewSorts));
|
||||
setRecordIndexViewType(view.type);
|
||||
setRecordIndexOpenRecordIn(view.openRecordIn);
|
||||
setRecordIndexViewKanbanFieldMetadataIdState(
|
||||
view.kanbanFieldMetadataId,
|
||||
);
|
||||
@@ -272,6 +274,7 @@ export const useLoadRecordIndexStates = () => {
|
||||
setRecordIndexViewKanbanAggregateOperationState,
|
||||
setRecordIndexViewKanbanFieldMetadataIdState,
|
||||
setRecordIndexViewType,
|
||||
setRecordIndexOpenRecordIn,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
import { createState } from '@ui/utilities/state/utils/createState';
|
||||
|
||||
import { ViewOpenRecordInType } from '@/views/types/ViewOpenRecordInType';
|
||||
|
||||
export const recordIndexOpenRecordInState = createState<ViewOpenRecordInType>({
|
||||
key: 'recordIndexOpenRecordInState',
|
||||
defaultValue: ViewOpenRecordInType.SIDE_PANEL,
|
||||
});
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { recordIndexOpenRecordInState } from '@/object-record/record-index/states/recordIndexOpenRecordInState';
|
||||
import { ViewOpenRecordInType } from '@/views/types/ViewOpenRecordInType';
|
||||
import { checkIfFeatureFlagIsEnabledOnWorkspace } from '@/workspace/utils/checkIfFeatureFlagIsEnabledOnWorkspace';
|
||||
import { selector } from 'recoil';
|
||||
import { FeatureFlagKey } from '~/generated-metadata/graphql';
|
||||
|
||||
export const recordIndexOpenRecordInSelector = selector<ViewOpenRecordInType>({
|
||||
key: 'recordIndexOpenRecordInSelector',
|
||||
get: ({ get }) => {
|
||||
const currentWorkspace = get(currentWorkspaceState);
|
||||
const isCommandMenuV2Enabled = checkIfFeatureFlagIsEnabledOnWorkspace(
|
||||
FeatureFlagKey.IsCommandMenuV2Enabled,
|
||||
currentWorkspace,
|
||||
);
|
||||
|
||||
return isCommandMenuV2Enabled
|
||||
? get(recordIndexOpenRecordInState)
|
||||
: ViewOpenRecordInType.RECORD_PAGE;
|
||||
},
|
||||
});
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
import { availableFieldMetadataItemsForSortFamilySelector } from '@/object-metadata/states/availableFieldMetadataItemsForSortFamilySelector';
|
||||
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
export const useSortableFieldMetadataItemsInRecordIndexContext = () => {
|
||||
const { objectMetadataItem } = useRecordIndexContextOrThrow();
|
||||
|
||||
const sortableFieldMetadataItems = useRecoilValue(
|
||||
availableFieldMetadataItemsForSortFamilySelector({
|
||||
objectMetadataItemId: objectMetadataItem.id,
|
||||
}),
|
||||
);
|
||||
|
||||
return { sortableFieldMetadataItems };
|
||||
};
|
||||
@@ -1,9 +1,7 @@
|
||||
import { SortDefinition } from '@/object-record/object-sort-dropdown/types/SortDefinition';
|
||||
import { RecordSortDirection } from '@/object-record/record-sort/types/RecordSortDirection';
|
||||
|
||||
export type RecordSort = {
|
||||
id: string;
|
||||
fieldMetadataId: string;
|
||||
direction: RecordSortDirection;
|
||||
definition: SortDefinition;
|
||||
};
|
||||
|
||||
+8
@@ -4,9 +4,11 @@ import { useRecordTableContextOrThrow } from '@/object-record/record-table/conte
|
||||
import { RecordTableEmptyStateByGroupNoRecordAtAll } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateByGroupNoRecordAtAll';
|
||||
import { RecordTableEmptyStateNoGroupNoRecordAtAll } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateNoGroupNoRecordAtAll';
|
||||
import { RecordTableEmptyStateNoRecordFoundForFilter } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateNoRecordFoundForFilter';
|
||||
import { RecordTableEmptyStateReadOnly } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateReadOnly';
|
||||
import { RecordTableEmptyStateRemote } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateRemote';
|
||||
import { RecordTableEmptyStateSoftDelete } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateSoftDelete';
|
||||
import { isSoftDeleteFilterActiveComponentState } from '@/object-record/record-table/states/isSoftDeleteFilterActiveComponentState';
|
||||
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
|
||||
export const RecordTableEmptyState = () => {
|
||||
@@ -17,6 +19,8 @@ export const RecordTableEmptyState = () => {
|
||||
hasRecordGroupsComponentSelector,
|
||||
);
|
||||
|
||||
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
|
||||
|
||||
const { totalCount } = useFindManyRecords({ objectNameSingular, limit: 1 });
|
||||
const noRecordAtAll = totalCount === 0;
|
||||
|
||||
@@ -27,6 +31,10 @@ export const RecordTableEmptyState = () => {
|
||||
recordTableId,
|
||||
);
|
||||
|
||||
if (hasObjectReadOnlyPermission) {
|
||||
return <RecordTableEmptyStateReadOnly />;
|
||||
}
|
||||
|
||||
if (isRemote) {
|
||||
return <RecordTableEmptyStateRemote />;
|
||||
} else if (isSoftDeleteActive === true) {
|
||||
|
||||
+2
@@ -19,6 +19,7 @@ type RecordTableEmptyStateDisplayButtonProps = {
|
||||
ButtonIcon: IconComponent;
|
||||
buttonTitle: string;
|
||||
onClick: () => void;
|
||||
buttonIsDisabled?: boolean;
|
||||
};
|
||||
|
||||
type RecordTableEmptyStateDisplayProps = {
|
||||
@@ -54,6 +55,7 @@ export const RecordTableEmptyStateDisplay = (
|
||||
title={props.buttonTitle}
|
||||
variant={'secondary'}
|
||||
onClick={props.onClick}
|
||||
disabled={props.buttonIsDisabled}
|
||||
/>
|
||||
)}
|
||||
</AnimatedPlaceholderEmptyContainer>
|
||||
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
import { useObjectLabel } from '@/object-metadata/hooks/useObjectLabel';
|
||||
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
|
||||
import { RecordTableEmptyStateDisplay } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateDisplay';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { IconPlus } from 'twenty-ui';
|
||||
|
||||
export const RecordTableEmptyStateReadOnly = () => {
|
||||
const { objectMetadataItem } = useRecordTableContextOrThrow();
|
||||
|
||||
const objectLabel = useObjectLabel(objectMetadataItem);
|
||||
|
||||
const buttonTitle = `Add a ${objectLabel}`;
|
||||
|
||||
return (
|
||||
<RecordTableEmptyStateDisplay
|
||||
title={t`No records found`}
|
||||
subTitle={t`You are not allowed to create records in this object`}
|
||||
animatedPlaceholderType="noRecord"
|
||||
buttonTitle={buttonTitle}
|
||||
ButtonIcon={IconPlus}
|
||||
buttonIsDisabled={true}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+60
-50
@@ -1,18 +1,19 @@
|
||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
|
||||
import { recordIndexOpenRecordInState } from '@/object-record/record-index/states/recordIndexOpenRecordInState';
|
||||
import { DEFAULT_CELL_SCOPE } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2';
|
||||
import { useSelectedTableCellEditMode } from '@/object-record/record-table/record-table-cell/hooks/useSelectedTableCellEditMode';
|
||||
import { recordTablePendingRecordIdByGroupComponentFamilyState } from '@/object-record/record-table/states/recordTablePendingRecordIdByGroupComponentFamilyState';
|
||||
import { recordTablePendingRecordIdComponentState } from '@/object-record/record-table/states/recordTablePendingRecordIdComponentState';
|
||||
import { useRecordTitleCell } from '@/object-record/record-title-cell/hooks/useRecordTitleCell';
|
||||
import { getDropdownFocusIdForRecordField } from '@/object-record/utils/getDropdownFocusIdForRecordField';
|
||||
import { shouldRedirectToShowPageOnCreation } from '@/object-record/utils/shouldRedirectToShowPageOnCreation';
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
import { useSetActiveDropdownFocusIdAndMemorizePrevious } from '@/ui/layout/dropdown/hooks/useSetFocusedDropdownIdAndMemorizePrevious';
|
||||
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
||||
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
|
||||
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||
import { ViewOpenRecordInType } from '@/views/types/ViewOpenRecordInType';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
@@ -62,62 +63,71 @@ export const useCreateNewTableRecord = ({
|
||||
|
||||
const { openRecordTitleCell } = useRecordTitleCell();
|
||||
|
||||
const createNewTableRecord = async () => {
|
||||
const recordId = v4();
|
||||
const createNewTableRecord = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
async () => {
|
||||
const recordId = v4();
|
||||
|
||||
if (isCommandMenuV2Enabled) {
|
||||
// TODO: Generalize this behaviour, there will be a view setting to specify
|
||||
// if the new record should be displayed in the side panel or on the record page
|
||||
if (shouldRedirectToShowPageOnCreation(objectMetadataItem.nameSingular)) {
|
||||
await createOneRecord({
|
||||
id: recordId,
|
||||
name: 'Untitled',
|
||||
});
|
||||
if (isCommandMenuV2Enabled) {
|
||||
const recordIndexOpenRecordIn = snapshot
|
||||
.getLoadable(recordIndexOpenRecordInState)
|
||||
.getValue();
|
||||
|
||||
navigate(AppPath.RecordShowPage, {
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
objectRecordId: recordId,
|
||||
});
|
||||
await createOneRecord({ id: recordId });
|
||||
|
||||
// TODO: we should open the record title cell here but because
|
||||
// we are redirecting to the record show page, the hotkey scope will
|
||||
// be overridden by the hotkey scope on mount. We need to deprecate
|
||||
// the useHotkeyScopeOnMount hook.
|
||||
if (recordIndexOpenRecordIn === ViewOpenRecordInType.SIDE_PANEL) {
|
||||
openRecordInCommandMenu({
|
||||
recordId,
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
isNewRecord: true,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
openRecordTitleCell({
|
||||
recordId,
|
||||
fieldMetadataId:
|
||||
objectMetadataItem.labelIdentifierFieldMetadataId,
|
||||
});
|
||||
} else {
|
||||
navigate(AppPath.RecordShowPage, {
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
objectRecordId: recordId,
|
||||
});
|
||||
}
|
||||
|
||||
await createOneRecord({ id: recordId });
|
||||
return;
|
||||
}
|
||||
|
||||
openRecordInCommandMenu({
|
||||
recordId,
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
isNewRecord: true,
|
||||
});
|
||||
|
||||
openRecordTitleCell({
|
||||
recordId,
|
||||
fieldMetadataId: objectMetadataItem.labelIdentifierFieldMetadataId,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setPendingRecordId(recordId);
|
||||
setSelectedTableCellEditMode(-1, 0);
|
||||
setHotkeyScope(DEFAULT_CELL_SCOPE.scope, DEFAULT_CELL_SCOPE.customScopes);
|
||||
|
||||
if (isDefined(objectMetadataItem.labelIdentifierFieldMetadataId)) {
|
||||
setActiveDropdownFocusIdAndMemorizePrevious(
|
||||
getDropdownFocusIdForRecordField(
|
||||
recordId,
|
||||
objectMetadataItem.labelIdentifierFieldMetadataId,
|
||||
'table-cell',
|
||||
),
|
||||
);
|
||||
}
|
||||
};
|
||||
setPendingRecordId(recordId);
|
||||
setSelectedTableCellEditMode(-1, 0);
|
||||
setHotkeyScope(
|
||||
DEFAULT_CELL_SCOPE.scope,
|
||||
DEFAULT_CELL_SCOPE.customScopes,
|
||||
);
|
||||
|
||||
if (isDefined(objectMetadataItem.labelIdentifierFieldMetadataId)) {
|
||||
setActiveDropdownFocusIdAndMemorizePrevious(
|
||||
getDropdownFocusIdForRecordField(
|
||||
recordId,
|
||||
objectMetadataItem.labelIdentifierFieldMetadataId,
|
||||
'table-cell',
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
[
|
||||
createOneRecord,
|
||||
isCommandMenuV2Enabled,
|
||||
navigate,
|
||||
objectMetadataItem.labelIdentifierFieldMetadataId,
|
||||
objectMetadataItem.nameSingular,
|
||||
openRecordInCommandMenu,
|
||||
openRecordTitleCell,
|
||||
setActiveDropdownFocusIdAndMemorizePrevious,
|
||||
setHotkeyScope,
|
||||
setPendingRecordId,
|
||||
setSelectedTableCellEditMode,
|
||||
],
|
||||
);
|
||||
const createNewTableRecordInGroup = useRecoilCallback(
|
||||
({ set }) =>
|
||||
(recordGroupId: string) => {
|
||||
|
||||
+20
-5
@@ -4,6 +4,7 @@ import { BORDER_COMMON, ThemeContext } from 'twenty-ui';
|
||||
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { useFieldFocus } from '@/object-record/record-field/hooks/useFieldFocus';
|
||||
import { useIsFieldValueReadOnly } from '@/object-record/record-field/hooks/useIsFieldValueReadOnly';
|
||||
import { CellHotkeyScopeContext } from '@/object-record/record-table/contexts/CellHotkeyScopeContext';
|
||||
import { useRecordTableBodyContextOrThrow } from '@/object-record/record-table/contexts/RecordTableBodyContext';
|
||||
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
|
||||
@@ -15,11 +16,13 @@ import {
|
||||
const StyledBaseContainer = styled.div<{
|
||||
hasSoftFocus: boolean;
|
||||
fontColorExtraLight: string;
|
||||
fontColorMedium: string;
|
||||
backgroundColorTransparentSecondary: string;
|
||||
isReadOnly: boolean;
|
||||
}>`
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
cursor: ${({ isReadOnly }) => (isReadOnly ? 'default' : 'pointer')};
|
||||
display: flex;
|
||||
height: 32px;
|
||||
position: relative;
|
||||
@@ -28,11 +31,20 @@ const StyledBaseContainer = styled.div<{
|
||||
background: ${({ hasSoftFocus, backgroundColorTransparentSecondary }) =>
|
||||
hasSoftFocus ? backgroundColorTransparentSecondary : 'none'};
|
||||
|
||||
border-radius: ${({ hasSoftFocus }) =>
|
||||
hasSoftFocus ? BORDER_COMMON.radius.sm : 'none'};
|
||||
border-radius: ${({ hasSoftFocus, isReadOnly }) =>
|
||||
hasSoftFocus && !isReadOnly ? BORDER_COMMON.radius.sm : 'none'};
|
||||
|
||||
outline: ${({ hasSoftFocus, fontColorExtraLight }) =>
|
||||
hasSoftFocus ? `1px solid ${fontColorExtraLight}` : 'none'};
|
||||
outline: ${({
|
||||
hasSoftFocus,
|
||||
fontColorExtraLight,
|
||||
fontColorMedium,
|
||||
isReadOnly,
|
||||
}) =>
|
||||
hasSoftFocus
|
||||
? isReadOnly
|
||||
? `1px solid ${fontColorMedium}`
|
||||
: `1px solid ${fontColorExtraLight}`
|
||||
: 'none'};
|
||||
`;
|
||||
|
||||
export const RecordTableCellBaseContainer = ({
|
||||
@@ -44,6 +56,7 @@ export const RecordTableCellBaseContainer = ({
|
||||
const { openTableCell } = useOpenRecordTableCellFromCell();
|
||||
const { theme } = useContext(ThemeContext);
|
||||
|
||||
const isReadOnly = useIsFieldValueReadOnly();
|
||||
const { hasSoftFocus, cellPosition } = useContext(RecordTableCellContext);
|
||||
|
||||
const { onMoveSoftFocusToCell, onCellMouseEnter } =
|
||||
@@ -83,7 +96,9 @@ export const RecordTableCellBaseContainer = ({
|
||||
theme.background.transparent.secondary
|
||||
}
|
||||
fontColorExtraLight={theme.font.color.extraLight}
|
||||
fontColorMedium={theme.border.color.medium}
|
||||
hasSoftFocus={hasSoftFocus}
|
||||
isReadOnly={isReadOnly}
|
||||
>
|
||||
{children}
|
||||
</StyledBaseContainer>
|
||||
|
||||
+21
-2
@@ -20,11 +20,14 @@ import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useC
|
||||
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
|
||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
|
||||
import { recordIndexOpenRecordInSelector } from '@/object-record/record-index/states/selectors/recordIndexOpenRecordInSelector';
|
||||
import { RECORD_TABLE_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-table/constants/RecordTableClickOutsideListenerId';
|
||||
import { getDropdownFocusIdForRecordField } from '@/object-record/utils/getDropdownFocusIdForRecordField';
|
||||
import { useSetActiveDropdownFocusIdAndMemorizePrevious } from '@/ui/layout/dropdown/hooks/useSetFocusedDropdownIdAndMemorizePrevious';
|
||||
import { useClickOustideListenerStates } from '@/ui/utilities/pointer-event/hooks/useClickOustideListenerStates';
|
||||
import { ViewOpenRecordInType } from '@/views/types/ViewOpenRecordInType';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { IconList } from 'twenty-ui';
|
||||
import { TableHotkeyScope } from '../../types/TableHotkeyScope';
|
||||
@@ -75,6 +78,8 @@ export const useOpenRecordTableCellV2 = (tableScopeId: string) => {
|
||||
const { setActiveDropdownFocusIdAndMemorizePrevious } =
|
||||
useSetActiveDropdownFocusIdAndMemorizePrevious();
|
||||
|
||||
const { openRecordInCommandMenu } = useCommandMenu();
|
||||
|
||||
const openTableCell = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
({
|
||||
@@ -115,7 +120,20 @@ export const useOpenRecordTableCellV2 = (tableScopeId: string) => {
|
||||
) {
|
||||
leaveTableFocus();
|
||||
|
||||
navigate(indexIdentifierUrl(recordId));
|
||||
const openRecordIn = snapshot
|
||||
.getLoadable(recordIndexOpenRecordInSelector)
|
||||
.getValue();
|
||||
|
||||
if (openRecordIn === ViewOpenRecordInType.RECORD_PAGE) {
|
||||
navigate(indexIdentifierUrl(recordId));
|
||||
}
|
||||
|
||||
if (openRecordIn === ViewOpenRecordInType.SIDE_PANEL) {
|
||||
openRecordInCommandMenu({
|
||||
recordId,
|
||||
objectNameSingular,
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -170,14 +188,15 @@ export const useOpenRecordTableCellV2 = (tableScopeId: string) => {
|
||||
moveEditModeToTableCellPosition,
|
||||
initDraftValue,
|
||||
toggleClickOutsideListener,
|
||||
setActiveDropdownFocusIdAndMemorizePrevious,
|
||||
leaveTableFocus,
|
||||
navigate,
|
||||
indexIdentifierUrl,
|
||||
openRecordInCommandMenu,
|
||||
setViewableRecordId,
|
||||
setViewableRecordNameSingular,
|
||||
openRightDrawer,
|
||||
setHotkeyScope,
|
||||
setActiveDropdownFocusIdAndMemorizePrevious,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
+5
-1
@@ -13,6 +13,7 @@ import { isRecordTableScrolledLeftComponentState } from '@/object-record/record-
|
||||
import { resizeFieldOffsetComponentState } from '@/object-record/record-table/states/resizeFieldOffsetComponentState';
|
||||
import { tableColumnsComponentState } from '@/object-record/record-table/states/tableColumnsComponentState';
|
||||
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
|
||||
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
|
||||
import { useTrackPointer } from '@/ui/utilities/pointer-event/hooks/useTrackPointer';
|
||||
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
|
||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||
@@ -212,6 +213,8 @@ export const RecordTableHeaderCell = ({
|
||||
|
||||
const isReadOnly = isObjectMetadataReadOnly(objectMetadataItem);
|
||||
|
||||
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
|
||||
|
||||
return (
|
||||
<StyledColumnHeaderCell
|
||||
key={column.fieldMetadataId}
|
||||
@@ -229,7 +232,8 @@ export const RecordTableHeaderCell = ({
|
||||
<RecordTableColumnHeadWithDropdown column={column} />
|
||||
{(useIsMobile() || iconVisibility) &&
|
||||
!!column.isLabelIdentifier &&
|
||||
!isReadOnly && (
|
||||
!isReadOnly &&
|
||||
!hasObjectReadOnlyPermission && (
|
||||
<StyledHeaderIcon>
|
||||
<LightIconButton
|
||||
Icon={IconPlus}
|
||||
|
||||
+7
@@ -3,6 +3,7 @@ import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record
|
||||
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
|
||||
import { useCreateNewTableRecord } from '@/object-record/record-table/hooks/useCreateNewTableRecords';
|
||||
import { RecordTableActionRow } from '@/object-record/record-table/record-table-row/components/RecordTableActionRow';
|
||||
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { IconPlus } from 'twenty-ui';
|
||||
|
||||
@@ -15,6 +16,8 @@ export const RecordTableRecordGroupSectionAddNew = () => {
|
||||
recordIndexAllRecordIdsComponentSelector,
|
||||
);
|
||||
|
||||
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
|
||||
|
||||
const { createNewTableRecordInGroup } = useCreateNewTableRecord({
|
||||
objectMetadataItem,
|
||||
recordTableId,
|
||||
@@ -24,6 +27,10 @@ export const RecordTableRecordGroupSectionAddNew = () => {
|
||||
createNewTableRecordInGroup(currentRecordGroupId);
|
||||
};
|
||||
|
||||
if (hasObjectReadOnlyPermission) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<RecordTableActionRow
|
||||
draggableId={`add-new-record-${currentRecordGroupId}`}
|
||||
|
||||
+4
-1
@@ -4,6 +4,7 @@ import { MultipleObjectRecordSelectItem } from '@/object-record/relation-picker/
|
||||
import { MULTI_OBJECT_RECORD_SELECT_SELECTABLE_LIST_ID } from '@/object-record/relation-picker/constants/MultiObjectRecordSelectSelectableListId';
|
||||
import { RecordPickerComponentInstanceContext } from '@/object-record/relation-picker/states/contexts/RecordPickerComponentInstanceContext';
|
||||
import { recordPickerSearchFilterComponentState } from '@/object-record/relation-picker/states/recordPickerSearchFilterComponentState';
|
||||
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
|
||||
import { CreateNewButton } from '@/ui/input/relation-picker/components/CreateNewButton';
|
||||
import { DropdownMenuSkeletonItem } from '@/ui/input/relation-picker/components/skeletons/DropdownMenuSkeletonItem';
|
||||
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
|
||||
@@ -75,6 +76,8 @@ export const MultiRecordSelect = ({
|
||||
instanceId,
|
||||
);
|
||||
|
||||
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
|
||||
|
||||
useEffect(() => {
|
||||
setHotkeyScope(instanceId);
|
||||
}, [setHotkeyScope, instanceId]);
|
||||
@@ -144,7 +147,7 @@ export const MultiRecordSelect = ({
|
||||
<DropdownMenu ref={containerRef} data-select-disable width={200}>
|
||||
{dropdownPlacement?.includes('end') && (
|
||||
<>
|
||||
{isDefined(onCreate) && (
|
||||
{isDefined(onCreate) && !hasObjectReadOnlyPermission && (
|
||||
<DropdownMenuItemsContainer scrollable={false}>
|
||||
{createNewButton}
|
||||
</DropdownMenuItemsContainer>
|
||||
|
||||
+9
-4
@@ -5,6 +5,7 @@ import {
|
||||
import { useRecordPickerRecordsOptions } from '@/object-record/relation-picker/hooks/useRecordPickerRecordsOptions';
|
||||
import { useRecordSelectSearch } from '@/object-record/relation-picker/hooks/useRecordSelectSearch';
|
||||
import { RecordPickerComponentInstanceContext } from '@/object-record/relation-picker/states/contexts/RecordPickerComponentInstanceContext';
|
||||
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
|
||||
import { CreateNewButton } from '@/ui/input/relation-picker/components/CreateNewButton';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
|
||||
@@ -48,6 +49,8 @@ export const SingleRecordSelectMenuItemsWithSearch = ({
|
||||
RecordPickerComponentInstanceContext,
|
||||
);
|
||||
|
||||
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
|
||||
|
||||
const { records, recordPickerSearchFilter } = useRecordPickerRecordsOptions({
|
||||
objectNameSingular,
|
||||
selectedRecordIds,
|
||||
@@ -69,9 +72,11 @@ export const SingleRecordSelectMenuItemsWithSearch = ({
|
||||
<>
|
||||
{dropdownPlacement?.includes('end') && (
|
||||
<>
|
||||
<DropdownMenuItemsContainer scrollable={false}>
|
||||
{createNewButton}
|
||||
</DropdownMenuItemsContainer>
|
||||
{isDefined(onCreate) && !hasObjectReadOnlyPermission && (
|
||||
<DropdownMenuItemsContainer scrollable={false}>
|
||||
{createNewButton}
|
||||
</DropdownMenuItemsContainer>
|
||||
)}
|
||||
{records.recordsToSelect.length > 0 && <DropdownMenuSeparator />}
|
||||
{shouldDisplayDropdownMenuItems && (
|
||||
<SingleRecordSelectMenuItems
|
||||
@@ -120,7 +125,7 @@ export const SingleRecordSelectMenuItemsWithSearch = ({
|
||||
{records.recordsToSelect.length > 0 && isDefined(onCreate) && (
|
||||
<DropdownMenuSeparator />
|
||||
)}
|
||||
{isDefined(onCreate) && (
|
||||
{isDefined(onCreate) && !hasObjectReadOnlyPermission && (
|
||||
<DropdownMenuItemsContainer scrollable={false}>
|
||||
{createNewButton}
|
||||
</DropdownMenuItemsContainer>
|
||||
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
|
||||
export const shouldRedirectToShowPageOnCreation = (
|
||||
objectNameSingular: string,
|
||||
) => {
|
||||
if (objectNameSingular === CoreObjectNameSingular.Workflow) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
@@ -1,9 +1,11 @@
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { useIsLogged } from '@/auth/hooks/useIsLogged';
|
||||
import { currentUserState } from '@/auth/states/currentUserState';
|
||||
import { OnboardingStatus } from '~/generated/graphql';
|
||||
|
||||
export const useOnboardingStatus = (): OnboardingStatus | null | undefined => {
|
||||
const currentUser = useRecoilValue(currentUserState);
|
||||
return currentUser?.onboardingStatus;
|
||||
const isLoggedIn = useIsLogged();
|
||||
return isLoggedIn ? currentUser?.onboardingStatus : undefined;
|
||||
};
|
||||
|
||||
+7
-5
@@ -13,14 +13,16 @@ import { prefetchFavoriteFoldersState } from '@/prefetch/states/prefetchFavorite
|
||||
import { prefetchFavoritesState } from '@/prefetch/states/prefetchFavoritesState';
|
||||
import { prefetchIsLoadedFamilyState } from '@/prefetch/states/prefetchIsLoadedFamilyState';
|
||||
import { PrefetchKey } from '@/prefetch/types/PrefetchKey';
|
||||
import { useIsWorkspaceActivationStatusSuspended } from '@/workspace/hooks/useIsWorkspaceActivationStatusSuspended';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import { useIsWorkspaceActivationStatusEqualsTo } from '@/workspace/hooks/useIsWorkspaceActivationStatusEqualsTo';
|
||||
import { WorkspaceActivationStatus, isDefined } from 'twenty-shared';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
|
||||
export const PrefetchRunFavoriteQueriesEffect = () => {
|
||||
const currentUser = useRecoilValue(currentUserState);
|
||||
|
||||
const isWorkspaceSuspended = useIsWorkspaceActivationStatusSuspended();
|
||||
const isWorkspaceActive = useIsWorkspaceActivationStatusEqualsTo(
|
||||
WorkspaceActivationStatus.ACTIVE,
|
||||
);
|
||||
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
|
||||
@@ -50,14 +52,14 @@ export const PrefetchRunFavoriteQueriesEffect = () => {
|
||||
objectNameSingular: CoreObjectNameSingular.Favorite,
|
||||
filter: findAllFavoritesOperationSignature.variables.filter,
|
||||
recordGqlFields: findAllFavoritesOperationSignature.fields,
|
||||
skip: !currentUser || isWorkspaceSuspended,
|
||||
skip: !currentUser || !isWorkspaceActive,
|
||||
});
|
||||
|
||||
const { records: favoriteFolders } = useFindManyRecords({
|
||||
objectNameSingular: CoreObjectNameSingular.FavoriteFolder,
|
||||
filter: findAllFavoriteFoldersOperationSignature.variables.filter,
|
||||
recordGqlFields: findAllFavoriteFoldersOperationSignature.fields,
|
||||
skip: !currentUser || isWorkspaceSuspended,
|
||||
skip: !currentUser || !isWorkspaceActive,
|
||||
});
|
||||
|
||||
const setPrefetchFavoritesState = useRecoilCallback(
|
||||
|
||||
+6
-4
@@ -9,14 +9,16 @@ import { findAllViewsOperationSignatureFactory } from '@/prefetch/graphql/operat
|
||||
import { prefetchViewsState } from '@/prefetch/states/prefetchViewsState';
|
||||
import { isPersistingViewFieldsState } from '@/views/states/isPersistingViewFieldsState';
|
||||
import { View } from '@/views/types/View';
|
||||
import { useIsWorkspaceActivationStatusSuspended } from '@/workspace/hooks/useIsWorkspaceActivationStatusSuspended';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import { useIsWorkspaceActivationStatusEqualsTo } from '@/workspace/hooks/useIsWorkspaceActivationStatusEqualsTo';
|
||||
import { WorkspaceActivationStatus, isDefined } from 'twenty-shared';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
|
||||
export const PrefetchRunViewQueryEffect = () => {
|
||||
const currentUser = useRecoilValue(currentUserState);
|
||||
|
||||
const isWorkspaceSuspended = useIsWorkspaceActivationStatusSuspended();
|
||||
const isWorkspaceActive = useIsWorkspaceActivationStatusEqualsTo(
|
||||
WorkspaceActivationStatus.ACTIVE,
|
||||
);
|
||||
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
|
||||
@@ -30,7 +32,7 @@ export const PrefetchRunViewQueryEffect = () => {
|
||||
objectNameSingular: CoreObjectNameSingular.View,
|
||||
filter: findAllViewsOperationSignature.variables.filter,
|
||||
recordGqlFields: findAllViewsOperationSignature.fields,
|
||||
skip: !currentUser || isWorkspaceSuspended,
|
||||
skip: !currentUser || !isWorkspaceActive,
|
||||
});
|
||||
|
||||
const setPrefetchViewsState = useRecoilCallback(
|
||||
|
||||
+1
@@ -10,6 +10,7 @@ export const findAllViewsOperationSignatureFactory: RecordGqlOperationSignatureF
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
isCompact: true,
|
||||
openRecordIn: true,
|
||||
objectMetadataId: true,
|
||||
position: true,
|
||||
type: true,
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { prefetchIsLoadedFamilyState } from '@/prefetch/states/prefetchIsLoadedFamilyState';
|
||||
import { PrefetchKey } from '@/prefetch/types/PrefetchKey';
|
||||
import { useIsWorkspaceActivationStatusSuspended } from '@/workspace/hooks/useIsWorkspaceActivationStatusSuspended';
|
||||
import { useIsWorkspaceActivationStatusEqualsTo } from '@/workspace/hooks/useIsWorkspaceActivationStatusEqualsTo';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { WorkspaceActivationStatus } from 'twenty-shared';
|
||||
|
||||
export const useIsPrefetchLoading = () => {
|
||||
const isWorkspaceSuspended = useIsWorkspaceActivationStatusSuspended();
|
||||
const isWorkspaceActive = useIsWorkspaceActivationStatusEqualsTo(
|
||||
WorkspaceActivationStatus.ACTIVE,
|
||||
);
|
||||
const isFavoriteFoldersPrefetched = useRecoilValue(
|
||||
prefetchIsLoadedFamilyState(PrefetchKey.AllFavoritesFolders),
|
||||
);
|
||||
@@ -14,7 +17,7 @@ export const useIsPrefetchLoading = () => {
|
||||
);
|
||||
|
||||
return (
|
||||
!isWorkspaceSuspended &&
|
||||
isWorkspaceActive &&
|
||||
(!areFavoritesPrefetched || !isFavoriteFoldersPrefetched)
|
||||
);
|
||||
};
|
||||
|
||||
-44
@@ -1,44 +0,0 @@
|
||||
import { Table } from '@/ui/layout/table/components/Table';
|
||||
import { TableCell } from '@/ui/layout/table/components/TableCell';
|
||||
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
|
||||
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||
|
||||
export const SettingsAdminHealthMessageSyncCountersTable = ({
|
||||
details,
|
||||
}: {
|
||||
details: string | null | undefined;
|
||||
}) => {
|
||||
const parsedDetails = details ? JSON.parse(details) : null;
|
||||
if (!parsedDetails) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Table>
|
||||
<TableRow>
|
||||
<TableHeader>Status</TableHeader>
|
||||
<TableHeader align="right">Count</TableHeader>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>Message Not Synced</TableCell>
|
||||
<TableCell align="right">{parsedDetails.counters.NOT_SYNCED}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>Message Sync Ongoing</TableCell>
|
||||
<TableCell align="right">{parsedDetails.counters.ONGOING}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>Total Jobs</TableCell>
|
||||
<TableCell align="right">{parsedDetails.totalJobs}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>Failed Jobs</TableCell>
|
||||
<TableCell align="right">{parsedDetails.failedJobs}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>Failure Rate</TableCell>
|
||||
<TableCell align="right">{parsedDetails.failureRate}%</TableCell>
|
||||
</TableRow>
|
||||
</Table>
|
||||
);
|
||||
};
|
||||
-66
@@ -1,66 +0,0 @@
|
||||
import { SettingsAdminHealthMessageSyncCountersTable } from '@/settings/admin-panel/components/SettingsAdminHealthMessageSyncCountersTable';
|
||||
import { SettingsHealthStatusListCard } from '@/settings/admin-panel/components/SettingsHealthStatusListCard';
|
||||
import { AdminHealthService } from '@/settings/admin-panel/types/AdminHealthService';
|
||||
import styled from '@emotion/styled';
|
||||
import { H2Title, Section } from 'twenty-ui';
|
||||
import {
|
||||
AdminPanelHealthServiceStatus,
|
||||
useGetSystemHealthStatusQuery,
|
||||
} from '~/generated/graphql';
|
||||
|
||||
const StyledErrorMessage = styled.div`
|
||||
color: ${({ theme }) => theme.color.red};
|
||||
margin-top: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
export const SettingsAdminHealthStatus = () => {
|
||||
const { data, loading } = useGetSystemHealthStatusQuery({
|
||||
fetchPolicy: 'network-only',
|
||||
});
|
||||
|
||||
const services = [
|
||||
{
|
||||
id: 'DATABASE',
|
||||
name: 'Database Status',
|
||||
...data?.getSystemHealthStatus.database,
|
||||
},
|
||||
{ id: 'REDIS', name: 'Redis Status', ...data?.getSystemHealthStatus.redis },
|
||||
{
|
||||
id: 'WORKER',
|
||||
name: 'Worker Status',
|
||||
status: data?.getSystemHealthStatus.worker.status,
|
||||
queues: data?.getSystemHealthStatus.worker.queues,
|
||||
},
|
||||
].filter((service): service is AdminHealthService => !!service.status);
|
||||
|
||||
const isMessageSyncCounterDown =
|
||||
!data?.getSystemHealthStatus.messageSync.status ||
|
||||
data?.getSystemHealthStatus.messageSync.status ===
|
||||
AdminPanelHealthServiceStatus.OUTAGE;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Section>
|
||||
<H2Title title="Health Status" description="How your system is doing" />
|
||||
<SettingsHealthStatusListCard services={services} loading={loading} />
|
||||
</Section>
|
||||
|
||||
<Section>
|
||||
<H2Title
|
||||
title="Message Sync Status"
|
||||
description="How your message sync is doing"
|
||||
/>
|
||||
{isMessageSyncCounterDown ? (
|
||||
<StyledErrorMessage>
|
||||
{data?.getSystemHealthStatus.messageSync.details ||
|
||||
'Message sync status is unavailable'}
|
||||
</StyledErrorMessage>
|
||||
) : (
|
||||
<SettingsAdminHealthMessageSyncCountersTable
|
||||
details={data?.getSystemHealthStatus.messageSync.details}
|
||||
/>
|
||||
)}
|
||||
</Section>
|
||||
</>
|
||||
);
|
||||
};
|
||||
-27
@@ -1,27 +0,0 @@
|
||||
import { AdminHealthService } from '@/settings/admin-panel/types/AdminHealthService';
|
||||
import styled from '@emotion/styled';
|
||||
import { Status } from 'twenty-ui';
|
||||
import { AdminPanelHealthServiceStatus } from '~/generated/graphql';
|
||||
|
||||
const StyledRowRightContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
export const SettingsAdminHealthStatusRightContainer = ({
|
||||
service,
|
||||
}: {
|
||||
service: AdminHealthService;
|
||||
}) => {
|
||||
return (
|
||||
<StyledRowRightContainer>
|
||||
{service.status === AdminPanelHealthServiceStatus.OPERATIONAL && (
|
||||
<Status color="green" text="Operational" weight="medium" />
|
||||
)}
|
||||
{service.status === AdminPanelHealthServiceStatus.OUTAGE && (
|
||||
<Status color="red" text="Outage" weight="medium" />
|
||||
)}
|
||||
</StyledRowRightContainer>
|
||||
);
|
||||
};
|
||||
+1
-1
@@ -1,8 +1,8 @@
|
||||
import { SettingsAdminEnvVariables } from '@/settings/admin-panel/components/SettingsAdminEnvVariables';
|
||||
import { SettingsAdminGeneral } from '@/settings/admin-panel/components/SettingsAdminGeneral';
|
||||
import { SettingsAdminHealthStatus } from '@/settings/admin-panel/components/SettingsAdminHealthStatus';
|
||||
import { SETTINGS_ADMIN_TABS } from '@/settings/admin-panel/constants/SettingsAdminTabs';
|
||||
import { SETTINGS_ADMIN_TABS_ID } from '@/settings/admin-panel/constants/SettingsAdminTabsId';
|
||||
import { SettingsAdminHealthStatus } from '@/settings/admin-panel/health-status/components/SettingsAdminHealthStatus';
|
||||
import { useTabList } from '@/ui/layout/tab/hooks/useTabList';
|
||||
|
||||
export const SettingsAdminTabContent = () => {
|
||||
|
||||
-43
@@ -1,43 +0,0 @@
|
||||
import { AdminHealthService } from '@/settings/admin-panel/types/AdminHealthService';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { SettingsPath } from '@/types/SettingsPath';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
|
||||
import { SettingsListCard } from '../../components/SettingsListCard';
|
||||
import { SettingsAdminHealthStatusRightContainer } from './SettingsAdminHealthStatusRightContainer';
|
||||
|
||||
const StyledLink = styled(Link)`
|
||||
text-decoration: none;
|
||||
`;
|
||||
|
||||
export const SettingsHealthStatusListCard = ({
|
||||
services,
|
||||
loading,
|
||||
}: {
|
||||
services: Array<AdminHealthService>;
|
||||
loading?: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
{services.map((service) => (
|
||||
<>
|
||||
<StyledLink
|
||||
to={getSettingsPath(SettingsPath.AdminPanelIndicatorHealthStatus, {
|
||||
indicatorName: service.id,
|
||||
})}
|
||||
>
|
||||
<SettingsListCard
|
||||
items={[service]}
|
||||
getItemLabel={(service) => service.name}
|
||||
isLoading={loading}
|
||||
RowRightComponent={({ item: service }) => (
|
||||
<SettingsAdminHealthStatusRightContainer service={service} />
|
||||
)}
|
||||
/>
|
||||
</StyledLink>
|
||||
</>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
-36
@@ -1,36 +0,0 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const GET_SYSTEM_HEALTH_STATUS = gql`
|
||||
query GetSystemHealthStatus {
|
||||
getSystemHealthStatus {
|
||||
database {
|
||||
status
|
||||
details
|
||||
}
|
||||
redis {
|
||||
status
|
||||
details
|
||||
}
|
||||
worker {
|
||||
status
|
||||
queues {
|
||||
name
|
||||
workers
|
||||
status
|
||||
metrics {
|
||||
failed
|
||||
completed
|
||||
waiting
|
||||
active
|
||||
delayed
|
||||
prioritized
|
||||
}
|
||||
}
|
||||
}
|
||||
messageSync {
|
||||
status
|
||||
details
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
import { SettingsAdminHealthAccountSyncCountersTable } from '@/settings/admin-panel/health-status/components/SettingsAdminHealthAccountSyncCountersTable';
|
||||
import { SettingsAdminIndicatorHealthContext } from '@/settings/admin-panel/health-status/contexts/SettingsAdminIndicatorHealthContext';
|
||||
import styled from '@emotion/styled';
|
||||
import { useContext } from 'react';
|
||||
import { AdminPanelHealthServiceStatus } from '~/generated/graphql';
|
||||
|
||||
const StyledErrorMessage = styled.div`
|
||||
color: ${({ theme }) => theme.color.red};
|
||||
margin-top: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
export const ConnectedAccountHealthStatus = () => {
|
||||
const { indicatorHealth } = useContext(SettingsAdminIndicatorHealthContext);
|
||||
const details = indicatorHealth.details;
|
||||
if (!details) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parsedDetails = JSON.parse(details);
|
||||
|
||||
const isMessageSyncDown =
|
||||
parsedDetails.messageSync?.status === AdminPanelHealthServiceStatus.OUTAGE;
|
||||
const isCalendarSyncDown =
|
||||
parsedDetails.calendarSync?.status === AdminPanelHealthServiceStatus.OUTAGE;
|
||||
|
||||
const errorMessages = [];
|
||||
if (isMessageSyncDown) {
|
||||
errorMessages.push('Message Sync');
|
||||
}
|
||||
if (isCalendarSyncDown) {
|
||||
errorMessages.push('Calendar Sync');
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{errorMessages.length > 0 && (
|
||||
<StyledErrorMessage>
|
||||
{`${errorMessages.join(' and ')} ${errorMessages.length > 1 ? 'are' : 'is'} not available because the service is down`}
|
||||
</StyledErrorMessage>
|
||||
)}
|
||||
|
||||
{!isMessageSyncDown && parsedDetails.messageSync?.details && (
|
||||
<SettingsAdminHealthAccountSyncCountersTable
|
||||
details={parsedDetails.messageSync.details}
|
||||
title="Message Sync Status"
|
||||
/>
|
||||
)}
|
||||
|
||||
{!isCalendarSyncDown && parsedDetails.calendarSync?.details && (
|
||||
<SettingsAdminHealthAccountSyncCountersTable
|
||||
details={parsedDetails.calendarSync.details}
|
||||
title="Calendar Sync Status"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
import { SettingsAdminIndicatorHealthContext } from '@/settings/admin-panel/health-status/contexts/SettingsAdminIndicatorHealthContext';
|
||||
import styled from '@emotion/styled';
|
||||
import { useContext } from 'react';
|
||||
import { Section } from 'twenty-ui';
|
||||
import { AdminPanelHealthServiceStatus } from '~/generated/graphql';
|
||||
|
||||
const StyledDetailsContainer = styled.pre`
|
||||
background-color: ${({ theme }) => theme.background.quaternary};
|
||||
padding: ${({ theme }) => theme.spacing(6)};
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
white-space: pre-wrap;
|
||||
font-size: ${({ theme }) => theme.font.size.sm};
|
||||
margin: 0;
|
||||
`;
|
||||
|
||||
const StyledErrorMessage = styled.div`
|
||||
color: ${({ theme }) => theme.color.red};
|
||||
margin-top: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
export const DatabaseAndRedisHealthStatus = () => {
|
||||
const { indicatorHealth, loading } = useContext(
|
||||
SettingsAdminIndicatorHealthContext,
|
||||
);
|
||||
|
||||
const formattedDetails = indicatorHealth.details
|
||||
? JSON.stringify(JSON.parse(indicatorHealth.details), null, 2)
|
||||
: null;
|
||||
|
||||
const isDatabaseOrRedisDown =
|
||||
!indicatorHealth.status ||
|
||||
indicatorHealth.status === AdminPanelHealthServiceStatus.OUTAGE;
|
||||
|
||||
return (
|
||||
<Section>
|
||||
{isDatabaseOrRedisDown && !loading ? (
|
||||
<StyledErrorMessage>
|
||||
{`${indicatorHealth.label} information is not available because the service is down`}
|
||||
</StyledErrorMessage>
|
||||
) : (
|
||||
<StyledDetailsContainer>{formattedDetails}</StyledDetailsContainer>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
import { Table } from '@/ui/layout/table/components/Table';
|
||||
import { TableCell } from '@/ui/layout/table/components/TableCell';
|
||||
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
|
||||
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||
import styled from '@emotion/styled';
|
||||
import { H2Title } from 'twenty-ui';
|
||||
|
||||
const StyledContainer = styled.div``;
|
||||
|
||||
export const SettingsAdminHealthAccountSyncCountersTable = ({
|
||||
details,
|
||||
title,
|
||||
}: {
|
||||
details: Record<string, any> | null;
|
||||
title: string;
|
||||
}) => {
|
||||
if (!details) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<H2Title
|
||||
title={title}
|
||||
description={`How your ${title.toLowerCase()} is doing`}
|
||||
/>
|
||||
<Table>
|
||||
<TableRow>
|
||||
<TableHeader>Status</TableHeader>
|
||||
<TableHeader align="right">Count</TableHeader>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>Not Synced</TableCell>
|
||||
<TableCell align="right">{details.counters.NOT_SYNCED}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>Sync Ongoing</TableCell>
|
||||
<TableCell align="right">{details.counters.ONGOING}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>Total Jobs</TableCell>
|
||||
<TableCell align="right">{details.totalJobs}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>Failed Jobs</TableCell>
|
||||
<TableCell align="right">{details.failedJobs}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>Failure Rate</TableCell>
|
||||
<TableCell align="right">{details.failureRate}%</TableCell>
|
||||
</TableRow>
|
||||
</Table>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import { SettingsHealthStatusListCard } from '@/settings/admin-panel/health-status/components/SettingsHealthStatusListCard';
|
||||
import { H2Title, Section } from 'twenty-ui';
|
||||
import { useGetSystemHealthStatusQuery } from '~/generated/graphql';
|
||||
|
||||
export const SettingsAdminHealthStatus = () => {
|
||||
const { data, loading } = useGetSystemHealthStatusQuery({
|
||||
fetchPolicy: 'network-only',
|
||||
});
|
||||
|
||||
const services = data?.getSystemHealthStatus.services ?? [];
|
||||
return (
|
||||
<>
|
||||
<Section>
|
||||
<H2Title title="Health Status" description="How your system is doing" />
|
||||
<SettingsHealthStatusListCard services={services} loading={loading} />
|
||||
</Section>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import { Status } from 'twenty-ui';
|
||||
import { AdminPanelHealthServiceStatus } from '~/generated/graphql';
|
||||
|
||||
export const SettingsAdminHealthStatusRightContainer = ({
|
||||
status,
|
||||
}: {
|
||||
status: AdminPanelHealthServiceStatus;
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
{status === AdminPanelHealthServiceStatus.OPERATIONAL && (
|
||||
<Status color="green" text="Operational" weight="medium" />
|
||||
)}
|
||||
{status === AdminPanelHealthServiceStatus.OUTAGE && (
|
||||
<Status color="red" text="Outage" weight="medium" />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import { ConnectedAccountHealthStatus } from '@/settings/admin-panel/health-status/components/ConnectedAccountHealthStatus';
|
||||
import { DatabaseAndRedisHealthStatus } from '@/settings/admin-panel/health-status/components/DatabaseAndRedisHealthStatus';
|
||||
import { WorkerHealthStatus } from '@/settings/admin-panel/health-status/components/WorkerHealthStatus';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { HealthIndicatorId } from '~/generated/graphql';
|
||||
|
||||
export const SettingsAdminIndicatorHealthStatusContent = () => {
|
||||
const { indicatorId } = useParams();
|
||||
|
||||
switch (indicatorId) {
|
||||
case HealthIndicatorId.database:
|
||||
case HealthIndicatorId.redis:
|
||||
return <DatabaseAndRedisHealthStatus />;
|
||||
case HealthIndicatorId.worker:
|
||||
return <WorkerHealthStatus />;
|
||||
case HealthIndicatorId.connectedAccount:
|
||||
return <ConnectedAccountHealthStatus />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
+5
-3
@@ -43,7 +43,7 @@ export const SettingsAdminQueueExpandableContainer = ({
|
||||
selectedQueue: string | null;
|
||||
}) => {
|
||||
const selectedQueueData = queues.find(
|
||||
(queue) => queue.name === selectedQueue,
|
||||
(queue) => queue.queueName === selectedQueue,
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -55,10 +55,12 @@ export const SettingsAdminQueueExpandableContainer = ({
|
||||
<>
|
||||
<StyledContainer>
|
||||
<SettingsListCard
|
||||
items={[{ ...selectedQueueData, id: selectedQueueData.name }]}
|
||||
items={[
|
||||
{ ...selectedQueueData, id: selectedQueueData.queueName },
|
||||
]}
|
||||
getItemLabel={(
|
||||
item: AdminPanelWorkerQueueHealth & { id: string },
|
||||
) => item.name}
|
||||
) => item.queueName}
|
||||
isLoading={false}
|
||||
RowRightComponent={({
|
||||
item,
|
||||
+4
-4
@@ -39,11 +39,11 @@ export const SettingsAdminQueueHealthButtons = ({
|
||||
<StyledQueueButtonsRow>
|
||||
{queues.map((queue) => (
|
||||
<StyledQueueHealthButton
|
||||
key={queue.name}
|
||||
onClick={() => toggleQueueVisibility(queue.name)}
|
||||
title={queue.name}
|
||||
key={queue.queueName}
|
||||
onClick={() => toggleQueueVisibility(queue.queueName)}
|
||||
title={queue.queueName}
|
||||
variant="secondary"
|
||||
isSelected={selectedQueue === queue.name}
|
||||
isSelected={selectedQueue === queue.queueName}
|
||||
status={queue.status}
|
||||
/>
|
||||
))}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import { SettingsListCard } from '@/settings/components/SettingsListCard';
|
||||
import { SettingsPath } from '@/types/SettingsPath';
|
||||
import { SystemHealthService } from '~/generated/graphql';
|
||||
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
|
||||
import { SettingsAdminHealthStatusRightContainer } from './SettingsAdminHealthStatusRightContainer';
|
||||
|
||||
export const SettingsHealthStatusListCard = ({
|
||||
services,
|
||||
loading,
|
||||
}: {
|
||||
services: Array<SystemHealthService>;
|
||||
loading?: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<SettingsListCard
|
||||
items={services}
|
||||
getItemLabel={(service) => service.label}
|
||||
isLoading={loading}
|
||||
RowRightComponent={({ item: service }) => (
|
||||
<SettingsAdminHealthStatusRightContainer status={service.status} />
|
||||
)}
|
||||
to={(service) =>
|
||||
getSettingsPath(SettingsPath.AdminPanelIndicatorHealthStatus, {
|
||||
indicatorId: service.id,
|
||||
})
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
import { SettingsAdminQueueExpandableContainer } from '@/settings/admin-panel/health-status/components/SettingsAdminQueueExpandableContainer';
|
||||
import { SettingsAdminQueueHealthButtons } from '@/settings/admin-panel/health-status/components/SettingsAdminQueueHealthButtons';
|
||||
import { SettingsAdminIndicatorHealthContext } from '@/settings/admin-panel/health-status/contexts/SettingsAdminIndicatorHealthContext';
|
||||
import styled from '@emotion/styled';
|
||||
import { useContext, useState } from 'react';
|
||||
import { H2Title, Section } from 'twenty-ui';
|
||||
import { AdminPanelHealthServiceStatus } from '~/generated/graphql';
|
||||
|
||||
const StyledTitleContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
`;
|
||||
|
||||
const StyledErrorMessage = styled.div`
|
||||
color: ${({ theme }) => theme.color.red};
|
||||
margin-top: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
export const WorkerHealthStatus = () => {
|
||||
const { indicatorHealth, loading } = useContext(
|
||||
SettingsAdminIndicatorHealthContext,
|
||||
);
|
||||
|
||||
const isWorkerDown =
|
||||
!indicatorHealth.status ||
|
||||
indicatorHealth.status === AdminPanelHealthServiceStatus.OUTAGE;
|
||||
|
||||
const [selectedQueue, setSelectedQueue] = useState<string | null>(null);
|
||||
|
||||
const toggleQueueVisibility = (queueName: string) => {
|
||||
setSelectedQueue(selectedQueue === queueName ? null : queueName);
|
||||
};
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<StyledTitleContainer>
|
||||
<H2Title
|
||||
title="Queue Status"
|
||||
description="Background job processing status and metrics"
|
||||
/>
|
||||
</StyledTitleContainer>
|
||||
{isWorkerDown && !loading ? (
|
||||
<StyledErrorMessage>
|
||||
Queue information is not available because the worker is down
|
||||
</StyledErrorMessage>
|
||||
) : (
|
||||
<>
|
||||
<SettingsAdminQueueHealthButtons
|
||||
queues={indicatorHealth.queues ?? []}
|
||||
selectedQueue={selectedQueue}
|
||||
toggleQueueVisibility={toggleQueueVisibility}
|
||||
/>
|
||||
<SettingsAdminQueueExpandableContainer
|
||||
queues={indicatorHealth.queues ?? []}
|
||||
selectedQueue={selectedQueue}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import { createContext } from 'react';
|
||||
import {
|
||||
AdminPanelHealthServiceData,
|
||||
AdminPanelHealthServiceStatus,
|
||||
} from '~/generated/graphql';
|
||||
|
||||
type SettingsAdminIndicatorHealthContextType = {
|
||||
indicatorHealth: AdminPanelHealthServiceData;
|
||||
loading: boolean;
|
||||
};
|
||||
|
||||
export const SettingsAdminIndicatorHealthContext =
|
||||
createContext<SettingsAdminIndicatorHealthContextType>({
|
||||
indicatorHealth: {
|
||||
id: '',
|
||||
label: '',
|
||||
description: '',
|
||||
status: AdminPanelHealthServiceStatus.OPERATIONAL,
|
||||
details: '',
|
||||
queues: [],
|
||||
},
|
||||
loading: false,
|
||||
});
|
||||
+7
-5
@@ -1,14 +1,16 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const GET_INDICATOR_HEALTH_STATUS = gql`
|
||||
query GetIndicatorHealthStatus(
|
||||
$indicatorName: AdminPanelIndicatorHealthStatusInputEnum!
|
||||
) {
|
||||
getIndicatorHealthStatus(indicatorName: $indicatorName) {
|
||||
query GetIndicatorHealthStatus($indicatorId: HealthIndicatorId!) {
|
||||
getIndicatorHealthStatus(indicatorId: $indicatorId) {
|
||||
id
|
||||
label
|
||||
description
|
||||
status
|
||||
details
|
||||
queues {
|
||||
name
|
||||
id
|
||||
queueName
|
||||
status
|
||||
workers
|
||||
metrics {
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const GET_SYSTEM_HEALTH_STATUS = gql`
|
||||
query GetSystemHealthStatus {
|
||||
getSystemHealthStatus {
|
||||
services {
|
||||
id
|
||||
label
|
||||
status
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user