Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
218cb798e6 | ||
|
|
a364692925 | ||
|
|
1991f0d3c0 | ||
|
|
ce428985aa | ||
|
|
2039986684 | ||
|
|
1ed9de2300 | ||
|
|
ee2810281e | ||
|
|
50bd91262f | ||
|
|
bf92860d19 | ||
|
|
22203bfd3c | ||
|
|
d747366bf3 | ||
|
|
7a3e92fe0b | ||
|
|
ba51c091f0 | ||
|
|
f269f8b905 | ||
|
|
6fb81e757b | ||
|
|
0571eb2cf6 | ||
|
|
5863c45d4b | ||
|
|
29d079babc |
@@ -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 'github-actions@twenty.com'
|
||||
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.0-canary",
|
||||
"version": "0.42.16",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "twenty-emails",
|
||||
"version": "0.42.0-canary",
|
||||
"version": "0.42.16",
|
||||
"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,5 +1,5 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<html lang="en" translate="no">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "twenty-front",
|
||||
"version": "0.42.0-canary",
|
||||
"version": "0.42.16",
|
||||
"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'];
|
||||
|
||||
@@ -92,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'];
|
||||
@@ -286,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'];
|
||||
@@ -357,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'];
|
||||
@@ -474,6 +491,7 @@ export enum FeatureFlagKey {
|
||||
IsAdvancedFiltersEnabled = 'IsAdvancedFiltersEnabled',
|
||||
IsAirtableIntegrationEnabled = 'IsAirtableIntegrationEnabled',
|
||||
IsAnalyticsV2Enabled = 'IsAnalyticsV2Enabled',
|
||||
IsApprovedAccessDomainsEnabled = 'IsApprovedAccessDomainsEnabled',
|
||||
IsBillingPlansEnabled = 'IsBillingPlansEnabled',
|
||||
IsCommandMenuV2Enabled = 'IsCommandMenuV2Enabled',
|
||||
IsCopilotEnabled = 'IsCopilotEnabled',
|
||||
@@ -762,6 +780,7 @@ export type Mutation = {
|
||||
checkCustomDomainValidRecords?: Maybe<CustomDomainValidRecords>;
|
||||
checkoutSession: BillingSessionOutput;
|
||||
computeStepOutputSchema: Scalars['JSON'];
|
||||
createApprovedAccessDomain: ApprovedAccessDomain;
|
||||
createDraftFromWorkflowVersion: WorkflowVersion;
|
||||
createOIDCIdentityProvider: SetupSsoOutput;
|
||||
createOneAppToken: AppToken;
|
||||
@@ -771,6 +790,7 @@ export type Mutation = {
|
||||
createSAMLIdentityProvider: SetupSsoOutput;
|
||||
createWorkflowVersionStep: WorkflowAction;
|
||||
deactivateWorkflowVersion: Scalars['Boolean'];
|
||||
deleteApprovedAccessDomain: Scalars['Boolean'];
|
||||
deleteCurrentWorkspace: Workspace;
|
||||
deleteOneField: Field;
|
||||
deleteOneObject: Object;
|
||||
@@ -815,6 +835,7 @@ export type Mutation = {
|
||||
uploadProfilePicture: Scalars['String'];
|
||||
uploadWorkspaceLogo: Scalars['String'];
|
||||
userLookupAdminPanel: UserLookup;
|
||||
validateApprovedAccessDomain: ApprovedAccessDomain;
|
||||
};
|
||||
|
||||
|
||||
@@ -848,6 +869,11 @@ export type MutationComputeStepOutputSchemaArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationCreateApprovedAccessDomainArgs = {
|
||||
input: CreateApprovedAccessDomainInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationCreateDraftFromWorkflowVersionArgs = {
|
||||
input: CreateDraftFromWorkflowVersionInput;
|
||||
};
|
||||
@@ -883,6 +909,11 @@ export type MutationDeactivateWorkflowVersionArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationDeleteApprovedAccessDomainArgs = {
|
||||
input: DeleteApprovedAccessDomainInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationDeleteOneFieldArgs = {
|
||||
input: DeleteOneFieldInput;
|
||||
};
|
||||
@@ -1085,6 +1116,11 @@ export type MutationUserLookupAdminPanelArgs = {
|
||||
userIdentifier: Scalars['String'];
|
||||
};
|
||||
|
||||
|
||||
export type MutationValidateApprovedAccessDomainArgs = {
|
||||
input: ValidateApprovedAccessDomainInput;
|
||||
};
|
||||
|
||||
export type Object = {
|
||||
__typename?: 'Object';
|
||||
createdAt: Scalars['DateTime'];
|
||||
@@ -1250,6 +1286,7 @@ export type Query = {
|
||||
findOneServerlessFunction: ServerlessFunction;
|
||||
findWorkspaceFromInviteHash: Workspace;
|
||||
findWorkspaceInvitations: Array<WorkspaceInvitation>;
|
||||
getApprovedAccessDomains: Array<ApprovedAccessDomain>;
|
||||
getAvailablePackages: Scalars['JSON'];
|
||||
getEnvironmentVariablesGrouped: EnvironmentVariablesOutput;
|
||||
getIndicatorHealthStatus: AdminPanelHealthServiceData;
|
||||
@@ -1567,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',
|
||||
@@ -1879,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'];
|
||||
@@ -1887,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'];
|
||||
@@ -2333,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;
|
||||
}>;
|
||||
@@ -2347,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;
|
||||
}>;
|
||||
@@ -2361,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; }>;
|
||||
|
||||
@@ -2383,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'];
|
||||
@@ -4230,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) {
|
||||
@@ -4304,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) {
|
||||
@@ -4374,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 =
|
||||
|
||||
+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}
|
||||
|
||||
@@ -81,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>
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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};
|
||||
|
||||
+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;
|
||||
|
||||
+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[]);
|
||||
+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)) {
|
||||
|
||||
+2
-2
@@ -12,7 +12,7 @@ import { ViewType } from '@/views/types/ViewType';
|
||||
import { useCallback, useContext } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { isDefined, SettingsFeatures } from 'twenty-shared';
|
||||
import { isDefined, SettingsPermissions } from 'twenty-shared';
|
||||
import { IconEyeOff, IconSettings } from 'twenty-ui';
|
||||
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
|
||||
|
||||
@@ -71,7 +71,7 @@ export const useRecordGroupActions = ({
|
||||
]);
|
||||
|
||||
const hasAccessToDataModelSettings = useHasSettingsPermission(
|
||||
SettingsFeatures.DATA_MODEL,
|
||||
SettingsPermissions.DATA_MODEL,
|
||||
);
|
||||
|
||||
const recordGroupActions: RecordGroupAction[] = [];
|
||||
|
||||
+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();
|
||||
|
||||
|
||||
+15
-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';
|
||||
@@ -81,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
|
||||
@@ -101,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,
|
||||
@@ -228,23 +235,13 @@ 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(
|
||||
|
||||
-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;
|
||||
};
|
||||
|
||||
+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>
|
||||
|
||||
@@ -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 +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)
|
||||
);
|
||||
};
|
||||
|
||||
+2
-2
@@ -3,12 +3,12 @@ import { SettingsPath } from '@/types/SettingsPath';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { ReactNode } from 'react';
|
||||
import { Navigate, Outlet } from 'react-router-dom';
|
||||
import { FeatureFlagKey, SettingsFeatures } from '~/generated/graphql';
|
||||
import { FeatureFlagKey, SettingsPermissions } from '~/generated/graphql';
|
||||
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
|
||||
|
||||
type SettingsProtectedRouteWrapperProps = {
|
||||
children?: ReactNode;
|
||||
settingsPermission?: SettingsFeatures;
|
||||
settingsPermission?: SettingsPermissions;
|
||||
requiredFeatureFlag?: FeatureFlagKey;
|
||||
};
|
||||
|
||||
|
||||
+13
-13
@@ -4,7 +4,7 @@ import { renderHook } from '@testing-library/react';
|
||||
import { ReactNode } from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { MutableSnapshot, RecoilRoot } from 'recoil';
|
||||
import { SettingsFeatures } from 'twenty-shared';
|
||||
import { SettingsPermissions } from 'twenty-shared';
|
||||
import { Billing, FeatureFlagKey, OnboardingStatus } from '~/generated/graphql';
|
||||
|
||||
import { currentUserState } from '@/auth/states/currentUserState';
|
||||
@@ -56,12 +56,12 @@ jest.mock('@/workspace/hooks/useFeatureFlagsMap', () => ({
|
||||
describe('useSettingsNavigationItems', () => {
|
||||
it('should hide workspace settings when no permissions', () => {
|
||||
(useSettingsPermissionMap as jest.Mock).mockImplementation(() => ({
|
||||
[SettingsFeatures.WORKSPACE]: false,
|
||||
[SettingsFeatures.WORKSPACE_USERS]: false,
|
||||
[SettingsFeatures.DATA_MODEL]: false,
|
||||
[SettingsFeatures.API_KEYS_AND_WEBHOOKS]: false,
|
||||
[SettingsFeatures.ROLES]: false,
|
||||
[SettingsFeatures.SECURITY]: false,
|
||||
[SettingsPermissions.WORKSPACE]: false,
|
||||
[SettingsPermissions.WORKSPACE_USERS]: false,
|
||||
[SettingsPermissions.DATA_MODEL]: false,
|
||||
[SettingsPermissions.API_KEYS_AND_WEBHOOKS]: false,
|
||||
[SettingsPermissions.ROLES]: false,
|
||||
[SettingsPermissions.SECURITY]: false,
|
||||
}));
|
||||
|
||||
const { result } = renderHook(() => useSettingsNavigationItems(), {
|
||||
@@ -77,12 +77,12 @@ describe('useSettingsNavigationItems', () => {
|
||||
|
||||
it('should show workspace settings when has permissions', () => {
|
||||
(useSettingsPermissionMap as jest.Mock).mockImplementation(() => ({
|
||||
[SettingsFeatures.WORKSPACE]: true,
|
||||
[SettingsFeatures.WORKSPACE_USERS]: true,
|
||||
[SettingsFeatures.DATA_MODEL]: true,
|
||||
[SettingsFeatures.API_KEYS_AND_WEBHOOKS]: true,
|
||||
[SettingsFeatures.ROLES]: true,
|
||||
[SettingsFeatures.SECURITY]: true,
|
||||
[SettingsPermissions.WORKSPACE]: true,
|
||||
[SettingsPermissions.WORKSPACE_USERS]: true,
|
||||
[SettingsPermissions.DATA_MODEL]: true,
|
||||
[SettingsPermissions.API_KEYS_AND_WEBHOOKS]: true,
|
||||
[SettingsPermissions.ROLES]: true,
|
||||
[SettingsPermissions.SECURITY]: true,
|
||||
}));
|
||||
|
||||
const { result } = renderHook(() => useSettingsNavigationItems(), {
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
} from 'twenty-ui';
|
||||
|
||||
import { SettingsPath } from '@/types/SettingsPath';
|
||||
import { SettingsFeatures } from 'twenty-shared';
|
||||
import { SettingsPermissions } from 'twenty-shared';
|
||||
import { FeatureFlagKey } from '~/generated-metadata/graphql';
|
||||
|
||||
import { currentUserState } from '@/auth/states/currentUserState';
|
||||
@@ -105,20 +105,20 @@ const useSettingsNavigationItems = (): SettingsNavigationSection[] => {
|
||||
label: t`General`,
|
||||
path: SettingsPath.Workspace,
|
||||
Icon: IconSettings,
|
||||
isHidden: !permissionMap[SettingsFeatures.WORKSPACE],
|
||||
isHidden: !permissionMap[SettingsPermissions.WORKSPACE],
|
||||
},
|
||||
{
|
||||
label: t`Members`,
|
||||
path: SettingsPath.WorkspaceMembersPage,
|
||||
Icon: IconUsers,
|
||||
isHidden: !permissionMap[SettingsFeatures.WORKSPACE_USERS],
|
||||
isHidden: !permissionMap[SettingsPermissions.WORKSPACE_USERS],
|
||||
},
|
||||
{
|
||||
label: t`Billing`,
|
||||
path: SettingsPath.Billing,
|
||||
Icon: IconCurrencyDollar,
|
||||
isHidden:
|
||||
!isBillingEnabled || !permissionMap[SettingsFeatures.WORKSPACE],
|
||||
!isBillingEnabled || !permissionMap[SettingsPermissions.WORKSPACE],
|
||||
},
|
||||
{
|
||||
label: t`Roles`,
|
||||
@@ -126,26 +126,26 @@ const useSettingsNavigationItems = (): SettingsNavigationSection[] => {
|
||||
Icon: IconLock,
|
||||
isHidden:
|
||||
!featureFlags[FeatureFlagKey.IsPermissionsEnabled] ||
|
||||
!permissionMap[SettingsFeatures.ROLES],
|
||||
!permissionMap[SettingsPermissions.ROLES],
|
||||
},
|
||||
{
|
||||
label: t`Data model`,
|
||||
path: SettingsPath.Objects,
|
||||
Icon: IconHierarchy2,
|
||||
isHidden: !permissionMap[SettingsFeatures.DATA_MODEL],
|
||||
isHidden: !permissionMap[SettingsPermissions.DATA_MODEL],
|
||||
},
|
||||
{
|
||||
label: t`Integrations`,
|
||||
path: SettingsPath.Integrations,
|
||||
Icon: IconApps,
|
||||
isHidden: !permissionMap[SettingsFeatures.API_KEYS_AND_WEBHOOKS],
|
||||
isHidden: !permissionMap[SettingsPermissions.API_KEYS_AND_WEBHOOKS],
|
||||
},
|
||||
{
|
||||
label: t`Security`,
|
||||
path: SettingsPath.Security,
|
||||
Icon: IconKey,
|
||||
isAdvanced: true,
|
||||
isHidden: !permissionMap[SettingsFeatures.SECURITY],
|
||||
isHidden: !permissionMap[SettingsPermissions.SECURITY],
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -158,7 +158,7 @@ const useSettingsNavigationItems = (): SettingsNavigationSection[] => {
|
||||
path: SettingsPath.Developers,
|
||||
Icon: IconCode,
|
||||
isAdvanced: true,
|
||||
isHidden: !permissionMap[SettingsFeatures.API_KEYS_AND_WEBHOOKS],
|
||||
isHidden: !permissionMap[SettingsPermissions.API_KEYS_AND_WEBHOOKS],
|
||||
},
|
||||
{
|
||||
label: t`Functions`,
|
||||
@@ -184,7 +184,7 @@ const useSettingsNavigationItems = (): SettingsNavigationSection[] => {
|
||||
Icon: IconFlask,
|
||||
isHidden:
|
||||
!labPublicFeatureFlags.length ||
|
||||
!permissionMap[SettingsFeatures.WORKSPACE],
|
||||
!permissionMap[SettingsPermissions.WORKSPACE],
|
||||
},
|
||||
{
|
||||
label: t`Releases`,
|
||||
|
||||
+4
@@ -15,6 +15,10 @@ export const useHasObjectReadOnlyPermission = () => {
|
||||
}
|
||||
|
||||
if (!isDefined(currentUserWorkspace?.objectRecordsPermissions)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (currentUserWorkspace?.objectRecordsPermissions.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { SettingsFeatures } from 'twenty-shared';
|
||||
import { SettingsPermissions } from 'twenty-shared';
|
||||
|
||||
export const useHasSettingsPermission = (
|
||||
settingsPermission?: SettingsFeatures,
|
||||
settingsPermission?: SettingsPermissions,
|
||||
) => {
|
||||
const currentUserWorkspace = useRecoilValue(currentUserWorkspaceState);
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { SettingsFeatures } from 'twenty-shared';
|
||||
import { SettingsPermissions } from 'twenty-shared';
|
||||
import { FeatureFlagKey } from '~/generated-metadata/graphql';
|
||||
import { buildRecordFromKeysWithSameValue } from '~/utils/array/buildRecordFromKeysWithSameValue';
|
||||
|
||||
export const useSettingsPermissionMap = (): Record<
|
||||
SettingsFeatures,
|
||||
SettingsPermissions,
|
||||
boolean
|
||||
> => {
|
||||
const currentUserWorkspace = useRecoilValue(currentUserWorkspaceState);
|
||||
@@ -19,7 +19,7 @@ export const useSettingsPermissionMap = (): Record<
|
||||
currentUserWorkspace?.settingsPermissions;
|
||||
|
||||
const initialPermissions = buildRecordFromKeysWithSameValue(
|
||||
Object.values(SettingsFeatures),
|
||||
Object.values(SettingsPermissions),
|
||||
!isPermissionEnabled,
|
||||
);
|
||||
|
||||
|
||||
+2
-2
@@ -2,8 +2,8 @@
|
||||
|
||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||
import { SettingsRadioCardContainer } from '@/settings/components/SettingsRadioCardContainer';
|
||||
import { SettingsSSOOIDCForm } from '@/settings/security/components/SettingsSSOOIDCForm';
|
||||
import { SettingsSSOSAMLForm } from '@/settings/security/components/SettingsSSOSAMLForm';
|
||||
import { SettingsSSOOIDCForm } from '@/settings/security/components/SSO/SettingsSSOOIDCForm';
|
||||
import { SettingsSSOSAMLForm } from '@/settings/security/components/SSO/SettingsSSOSAMLForm';
|
||||
import { SettingSecurityNewSSOIdentityFormValues } from '@/settings/security/types/SSOIdentityProvider';
|
||||
import { TextInput } from '@/ui/input/components/TextInput';
|
||||
import styled from '@emotion/styled';
|
||||
+1
-1
@@ -6,7 +6,7 @@ import { SettingsPath } from '@/types/SettingsPath';
|
||||
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { SettingsCard } from '@/settings/components/SettingsCard';
|
||||
import { SettingsSSOIdentitiesProvidersListCardWrapper } from '@/settings/security/components/SettingsSSOIdentitiesProvidersListCardWrapper';
|
||||
import { SettingsSSOIdentitiesProvidersListCardWrapper } from '@/settings/security/components/SSO/SettingsSSOIdentitiesProvidersListCardWrapper';
|
||||
import { SSOIdentitiesProvidersState } from '@/settings/security/states/SSOIdentitiesProvidersState';
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
/* @license Enterprise */
|
||||
|
||||
import { SettingsListCard } from '@/settings/components/SettingsListCard';
|
||||
import { SettingsSSOIdentityProviderRowRightContainer } from '@/settings/security/components/SettingsSSOIdentityProviderRowRightContainer';
|
||||
import { SettingsSSOIdentityProviderRowRightContainer } from '@/settings/security/components/SSO/SettingsSSOIdentityProviderRowRightContainer';
|
||||
import { SSOIdentitiesProvidersState } from '@/settings/security/states/SSOIdentitiesProvidersState';
|
||||
import { guessSSOIdentityProviderIconByUrl } from '@/settings/security/utils/guessSSOIdentityProviderIconByUrl';
|
||||
import { SettingsPath } from '@/types/SettingsPath';
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
/* @license Enterprise */
|
||||
|
||||
import { SettingsSecuritySSORowDropdownMenu } from '@/settings/security/components/SettingsSecuritySSORowDropdownMenu';
|
||||
import { SettingsSecuritySSORowDropdownMenu } from '@/settings/security/components/SSO/SettingsSecuritySSORowDropdownMenu';
|
||||
import { SSOIdentitiesProvidersState } from '@/settings/security/states/SSOIdentitiesProvidersState';
|
||||
import { getColorBySSOIdentityProviderStatus } from '@/settings/security/utils/getColorBySSOIdentityProviderStatus';
|
||||
import { Status } from 'twenty-ui';
|
||||
+1
-1
@@ -24,7 +24,7 @@ const StyledSettingsSecurityOptionsList = styled.div`
|
||||
gap: ${({ theme }) => theme.spacing(4)};
|
||||
`;
|
||||
|
||||
export const SettingsSecurityOptionsList = () => {
|
||||
export const SettingsSecurityAuthProvidersOptionsList = () => {
|
||||
const { t } = useLingui();
|
||||
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
|
||||
import { SettingsPath } from '@/types/SettingsPath';
|
||||
|
||||
import { SettingsCard } from '@/settings/components/SettingsCard';
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import styled from '@emotion/styled';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { IconAt, IconMailCog } from 'twenty-ui';
|
||||
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
|
||||
import { SettingsListCard } from '@/settings/components/SettingsListCard';
|
||||
import { approvedAccessDomainsState } from '@/settings/security/states/ApprovedAccessDomainsState';
|
||||
import { SettingsSecurityApprovedAccessDomainRowDropdownMenu } from '@/settings/security/components/approvedAccessDomains/SettingsSecurityApprovedAccessDomainRowDropdownMenu';
|
||||
import { SettingsSecurityApprovedAccessDomainValidationEffect } from '@/settings/security/components/approvedAccessDomains/SettingsSecurityApprovedAccessDomainValidationEffect';
|
||||
import { useGetApprovedAccessDomainsQuery } from '~/generated/graphql';
|
||||
|
||||
const StyledLink = styled(Link)`
|
||||
text-decoration: none;
|
||||
`;
|
||||
|
||||
export const SettingsApprovedAccessDomainsListCard = () => {
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
const navigate = useNavigate();
|
||||
const { t } = useLingui();
|
||||
|
||||
const [approvedAccessDomains, setApprovedAccessDomains] = useRecoilState(
|
||||
approvedAccessDomainsState,
|
||||
);
|
||||
|
||||
const { loading } = useGetApprovedAccessDomainsQuery({
|
||||
fetchPolicy: 'network-only',
|
||||
onCompleted: (data) => {
|
||||
setApprovedAccessDomains(data?.getApprovedAccessDomains ?? []);
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
enqueueSnackBar(error.message, {
|
||||
variant: SnackBarVariant.Error,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return loading || !approvedAccessDomains.length ? (
|
||||
<StyledLink to={getSettingsPath(SettingsPath.NewApprovedAccessDomain)}>
|
||||
<SettingsCard
|
||||
title={t`Add Approved Access Domain`}
|
||||
Icon={<IconMailCog />}
|
||||
/>
|
||||
</StyledLink>
|
||||
) : (
|
||||
<>
|
||||
<SettingsSecurityApprovedAccessDomainValidationEffect />
|
||||
<SettingsListCard
|
||||
items={approvedAccessDomains}
|
||||
getItemLabel={(approvedAccessDomain) =>
|
||||
`${approvedAccessDomain.domain} - ${approvedAccessDomain.createdAt}`
|
||||
}
|
||||
RowIcon={IconAt}
|
||||
RowRightComponent={({ item: approvedAccessDomain }) => (
|
||||
<SettingsSecurityApprovedAccessDomainRowDropdownMenu
|
||||
approvedAccessDomain={approvedAccessDomain}
|
||||
/>
|
||||
)}
|
||||
hasFooter
|
||||
footerButtonLabel="Add Approved Access Domain"
|
||||
onFooterButtonClick={() =>
|
||||
navigate(getSettingsPath(SettingsPath.NewApprovedAccessDomain))
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
import {
|
||||
IconDotsVertical,
|
||||
IconTrash,
|
||||
LightIconButton,
|
||||
MenuItem,
|
||||
} from 'twenty-ui';
|
||||
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||
import { UnwrapRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import { useDeleteApprovedAccessDomainMutation } from '~/generated/graphql';
|
||||
import { approvedAccessDomainsState } from '@/settings/security/states/ApprovedAccessDomainsState';
|
||||
|
||||
type SettingsSecurityApprovedAccessDomainRowDropdownMenuProps = {
|
||||
approvedAccessDomain: UnwrapRecoilValue<typeof approvedAccessDomainsState>[0];
|
||||
};
|
||||
|
||||
export const SettingsSecurityApprovedAccessDomainRowDropdownMenu = ({
|
||||
approvedAccessDomain,
|
||||
}: SettingsSecurityApprovedAccessDomainRowDropdownMenuProps) => {
|
||||
const dropdownId = `settings-approved-access-domain-row-${approvedAccessDomain.id}`;
|
||||
|
||||
const setApprovedAccessDomains = useSetRecoilState(
|
||||
approvedAccessDomainsState,
|
||||
);
|
||||
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
|
||||
const { closeDropdown } = useDropdown(dropdownId);
|
||||
|
||||
const [deleteApprovedAccessDomain] = useDeleteApprovedAccessDomainMutation();
|
||||
|
||||
const handleDeleteApprovedAccessDomain = async () => {
|
||||
const result = await deleteApprovedAccessDomain({
|
||||
variables: {
|
||||
input: {
|
||||
id: approvedAccessDomain.id,
|
||||
},
|
||||
},
|
||||
onCompleted: () => {
|
||||
setApprovedAccessDomains((approvedAccessDomains) => {
|
||||
return approvedAccessDomains.filter(
|
||||
({ id }) => id !== approvedAccessDomain.id,
|
||||
);
|
||||
});
|
||||
},
|
||||
});
|
||||
if (isDefined(result.errors)) {
|
||||
enqueueSnackBar('Error deleting approved access domain', {
|
||||
variant: SnackBarVariant.Error,
|
||||
duration: 2000,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
dropdownId={dropdownId}
|
||||
dropdownPlacement="right-start"
|
||||
dropdownHotkeyScope={{ scope: dropdownId }}
|
||||
clickableComponent={
|
||||
<LightIconButton Icon={IconDotsVertical} accent="tertiary" />
|
||||
}
|
||||
dropdownMenuWidth={160}
|
||||
dropdownComponents={
|
||||
<DropdownMenuItemsContainer>
|
||||
<MenuItem
|
||||
accent="danger"
|
||||
LeftIcon={IconTrash}
|
||||
text="Delete"
|
||||
onClick={() => {
|
||||
handleDeleteApprovedAccessDomain();
|
||||
closeDropdown();
|
||||
}}
|
||||
/>
|
||||
</DropdownMenuItemsContainer>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
import { useEffect } from 'react';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import { useValidateApprovedAccessDomainMutation } from '~/generated/graphql';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
|
||||
export const SettingsSecurityApprovedAccessDomainValidationEffect = () => {
|
||||
const [validateApprovedAccessDomainMutation] =
|
||||
useValidateApprovedAccessDomainMutation();
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
const [searchParams] = useSearchParams();
|
||||
const approvedAccessDomainId = searchParams.get('wtdId');
|
||||
const validationToken = searchParams.get('validationToken');
|
||||
|
||||
useEffect(() => {
|
||||
if (isDefined(validationToken) && isDefined(approvedAccessDomainId)) {
|
||||
validateApprovedAccessDomainMutation({
|
||||
variables: {
|
||||
input: {
|
||||
validationToken,
|
||||
approvedAccessDomainId,
|
||||
},
|
||||
},
|
||||
onCompleted: () => {
|
||||
enqueueSnackBar('Approved access domain validated', {
|
||||
dedupeKey: 'approved-access-domain-validation-dedupe-key',
|
||||
variant: SnackBarVariant.Success,
|
||||
});
|
||||
},
|
||||
onError: () => {
|
||||
enqueueSnackBar('Error validating approved access domain', {
|
||||
dedupeKey: 'approved-access-domain-validation-error-dedupe-key',
|
||||
variant: SnackBarVariant.Error,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
// Validate approved access domain only needs to run once at mount
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return <></>;
|
||||
};
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const CREATE_APPROVED_ACCESS_DOMAIN = gql`
|
||||
mutation CreateApprovedAccessDomain(
|
||||
$input: CreateApprovedAccessDomainInput!
|
||||
) {
|
||||
createApprovedAccessDomain(input: $input) {
|
||||
id
|
||||
domain
|
||||
isValidated
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
`;
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const DELETE_APPROVED_ACCESS_DOMAIN = gql`
|
||||
mutation DeleteApprovedAccessDomain(
|
||||
$input: DeleteApprovedAccessDomainInput!
|
||||
) {
|
||||
deleteApprovedAccessDomain(input: $input)
|
||||
}
|
||||
`;
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const VALIDATE_APPROVED_ACCESS_DOMAIN = gql`
|
||||
mutation ValidateApprovedAccessDomain(
|
||||
$input: ValidateApprovedAccessDomainInput!
|
||||
) {
|
||||
validateApprovedAccessDomain(input: $input) {
|
||||
id
|
||||
isValidated
|
||||
domain
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
`;
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const GET_ALL_APPROVED_ACCESS_DOMAINS = gql`
|
||||
query GetApprovedAccessDomains {
|
||||
getApprovedAccessDomains {
|
||||
id
|
||||
createdAt
|
||||
domain
|
||||
isValidated
|
||||
}
|
||||
}
|
||||
`;
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
import { createState } from '@ui/utilities/state/utils/createState';
|
||||
import { ApprovedAccessDomain } from '~/generated/graphql';
|
||||
|
||||
export const approvedAccessDomainsState = createState<
|
||||
Omit<ApprovedAccessDomain, '__typename'>[]
|
||||
>({
|
||||
key: 'ApprovedAccessDomainsState',
|
||||
defaultValue: [],
|
||||
});
|
||||
+2
-8
@@ -7,7 +7,6 @@ import { useSetRecordIndexEntityCount } from '@/object-record/record-index/hooks
|
||||
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
|
||||
import { useSetTableColumns } from '@/object-record/record-table/hooks/useSetTableColumns';
|
||||
import { SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS } from '@/sign-in-background-mock/constants/SignInBackgroundMockColumnDefinitions';
|
||||
import { SIGN_IN_BACKGROUND_MOCK_SORT_DEFINITIONS } from '@/sign-in-background-mock/constants/SignInBackgroundMockSortDefinitions';
|
||||
import { SIGN_IN_BACKGROUND_MOCK_VIEW_FIELDS } from '@/sign-in-background-mock/constants/SignInBackgroundMockViewFields';
|
||||
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||
import { useInitViewBar } from '@/views/hooks/useInitViewBar';
|
||||
@@ -43,18 +42,14 @@ export const SignInBackgroundMockContainerEffect = ({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const {
|
||||
setAvailableSortDefinitions,
|
||||
setAvailableFieldDefinitions,
|
||||
setViewObjectMetadataId,
|
||||
} = useInitViewBar(viewId);
|
||||
const { setAvailableFieldDefinitions, setViewObjectMetadataId } =
|
||||
useInitViewBar(viewId);
|
||||
|
||||
const { setRecordIndexEntityCount } = useSetRecordIndexEntityCount(viewId);
|
||||
|
||||
useEffect(() => {
|
||||
setViewObjectMetadataId?.(objectMetadataItem.id);
|
||||
|
||||
setAvailableSortDefinitions?.(SIGN_IN_BACKGROUND_MOCK_SORT_DEFINITIONS);
|
||||
setAvailableFieldDefinitions?.(SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS);
|
||||
|
||||
setAvailableTableColumns(SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS);
|
||||
@@ -70,7 +65,6 @@ export const SignInBackgroundMockContainerEffect = ({
|
||||
setContextStoreCurrentObjectMetadataItem(objectMetadataItem);
|
||||
}, [
|
||||
setViewObjectMetadataId,
|
||||
setAvailableSortDefinitions,
|
||||
setAvailableFieldDefinitions,
|
||||
objectMetadataItem,
|
||||
setAvailableTableColumns,
|
||||
|
||||
-34
@@ -1,34 +0,0 @@
|
||||
import { SortDefinition } from '@/object-record/object-sort-dropdown/types/SortDefinition';
|
||||
|
||||
export const SIGN_IN_BACKGROUND_MOCK_SORT_DEFINITIONS = [
|
||||
{
|
||||
fieldMetadataId: '20202020-5e4e-4007-a630-8a2617914889',
|
||||
label: 'Domain Name',
|
||||
iconName: 'IconLink',
|
||||
},
|
||||
{
|
||||
fieldMetadataId: '20202020-7fbd-41ad-b64d-25a15ff62f04',
|
||||
label: 'Employees',
|
||||
iconName: 'IconUsers',
|
||||
},
|
||||
{
|
||||
fieldMetadataId: 'REPLACE_ME',
|
||||
label: 'Name',
|
||||
iconName: 'IconBuildingSkyscraper',
|
||||
},
|
||||
{
|
||||
fieldMetadataId: '20202020-ad10-4117-a039-3f04b7a5f939',
|
||||
label: 'Address',
|
||||
iconName: 'IconMap',
|
||||
},
|
||||
{
|
||||
fieldMetadataId: '20202020-4dc2-47c9-bb15-6e6f19ba9e46',
|
||||
label: 'Creation date',
|
||||
iconName: 'IconCalendar',
|
||||
},
|
||||
{
|
||||
fieldMetadataId: '20202020-9e9f-4235-98b2-c76f3e2d281e',
|
||||
label: 'ICP',
|
||||
iconName: 'IconTarget',
|
||||
},
|
||||
] as SortDefinition[];
|
||||
@@ -29,7 +29,9 @@ export enum SettingsPath {
|
||||
IntegrationNewDatabaseConnection = 'integrations/:databaseKey/new',
|
||||
Security = 'security',
|
||||
NewSSOIdentityProvider = 'security/sso/new',
|
||||
EditSSOIdentityProvider = 'security/sso/:identityProviderId',
|
||||
NewApprovedAccessDomain = 'security/approved-access-domain/new',
|
||||
Webhooks = 'webhooks',
|
||||
DevelopersNewWebhook = 'developers/webhooks/new',
|
||||
DevelopersNewWebhookDetail = 'developers/webhooks/:webhookId',
|
||||
Releases = 'releases',
|
||||
AdminPanel = 'admin-panel',
|
||||
|
||||
+6
-2
@@ -6,7 +6,8 @@ import { IconX, UndecoratedLink } from 'twenty-ui';
|
||||
import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded';
|
||||
import { navigationDrawerExpandedMemorizedState } from '@/ui/navigation/states/navigationDrawerExpandedMemorizedState';
|
||||
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
|
||||
import { useIsWorkspaceActivationStatusSuspended } from '@/workspace/hooks/useIsWorkspaceActivationStatusSuspended';
|
||||
import { useIsWorkspaceActivationStatusEqualsTo } from '@/workspace/hooks/useIsWorkspaceActivationStatusEqualsTo';
|
||||
import { WorkspaceActivationStatus } from 'twenty-shared';
|
||||
|
||||
type NavigationDrawerBackButtonProps = {
|
||||
title: string;
|
||||
@@ -52,7 +53,10 @@ export const NavigationDrawerBackButton = ({
|
||||
navigationDrawerExpandedMemorizedState,
|
||||
);
|
||||
|
||||
const isWorkspaceSuspended = useIsWorkspaceActivationStatusSuspended();
|
||||
const isWorkspaceSuspended = useIsWorkspaceActivationStatusEqualsTo(
|
||||
WorkspaceActivationStatus.SUSPENDED,
|
||||
);
|
||||
|
||||
if (isWorkspaceSuspended) {
|
||||
return <StyledContainer />;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { IconArrowDown, IconArrowUp } from 'twenty-ui';
|
||||
|
||||
import { useFieldMetadataItemById } from '@/object-metadata/hooks/useFieldMetadataItemById';
|
||||
import { useRemoveRecordSort } from '@/object-record/record-sort/hooks/useRemoveRecordSort';
|
||||
import { useUpsertRecordSort } from '@/object-record/record-sort/hooks/useUpsertRecordSort';
|
||||
import { RecordSort } from '@/object-record/record-sort/types/RecordSort';
|
||||
@@ -25,6 +26,10 @@ export const EditableSortChip = ({ recordSort }: EditableSortChipProps) => {
|
||||
removeRecordSort(recordSort.fieldMetadataId);
|
||||
};
|
||||
|
||||
const { fieldMetadataItem } = useFieldMetadataItemById(
|
||||
recordSort.fieldMetadataId,
|
||||
);
|
||||
|
||||
const handleClick = () => {
|
||||
const newSort: RecordSort = {
|
||||
...recordSort,
|
||||
@@ -39,7 +44,7 @@ export const EditableSortChip = ({ recordSort }: EditableSortChipProps) => {
|
||||
<SortOrFilterChip
|
||||
key={recordSort.fieldMetadataId}
|
||||
testId={recordSort.fieldMetadataId}
|
||||
labelValue={recordSort.definition.label}
|
||||
labelValue={fieldMetadataItem.label}
|
||||
Icon={recordSort.direction === 'desc' ? IconArrowDown : IconArrowUp}
|
||||
onRemove={handleRemoveClick}
|
||||
onClick={handleClick}
|
||||
|
||||
@@ -15,6 +15,7 @@ import { useViewFromQueryParams } from '@/views/hooks/internal/useViewFromQueryP
|
||||
|
||||
import { useCheckIsSoftDeleteFilter } from '@/object-record/record-filter/hooks/useCheckIsSoftDeleteFilter';
|
||||
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
|
||||
import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState';
|
||||
import { SoftDeleteFilterChip } from '@/views/components/SoftDeleteFilterChip';
|
||||
import { useApplyCurrentViewFiltersToCurrentRecordFilters } from '@/views/hooks/useApplyCurrentViewFiltersToCurrentRecordFilters';
|
||||
import { useApplyCurrentViewSortsToCurrentRecordSorts } from '@/views/hooks/useApplyCurrentViewSortsToCurrentRecordSorts';
|
||||
@@ -22,9 +23,8 @@ import { useAreViewFiltersDifferentFromRecordFilters } from '@/views/hooks/useAr
|
||||
import { useAreViewSortsDifferentFromRecordSorts } from '@/views/hooks/useAreViewSortsDifferentFromRecordSorts';
|
||||
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
|
||||
import { useResetUnsavedViewStates } from '@/views/hooks/useResetUnsavedViewStates';
|
||||
import { availableSortDefinitionsComponentState } from '@/views/states/availableSortDefinitionsComponentState';
|
||||
|
||||
import { isViewBarExpandedComponentState } from '@/views/states/isViewBarExpandedComponentState';
|
||||
import { mapViewSortsToSorts } from '@/views/utils/mapViewSortsToSorts';
|
||||
import { isNonEmptyArray } from '@sniptt/guards';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
|
||||
@@ -123,8 +123,8 @@ export const ViewBarDetails = ({
|
||||
currentRecordFiltersComponentState,
|
||||
);
|
||||
|
||||
const availableSortDefinitions = useRecoilComponentValueV2(
|
||||
availableSortDefinitionsComponentState,
|
||||
const currentRecordSorts = useRecoilComponentValueV2(
|
||||
currentRecordSortsComponentState,
|
||||
);
|
||||
|
||||
const { objectNameSingular } = useObjectNameSingularFromPlural({
|
||||
@@ -206,19 +206,14 @@ export const ViewBarDetails = ({
|
||||
<StyledSeperator />
|
||||
</StyledSeperatorContainer>
|
||||
)}
|
||||
{mapViewSortsToSorts(
|
||||
currentViewWithCombinedFiltersAndSorts?.viewSorts ?? [],
|
||||
availableSortDefinitions,
|
||||
).map((recordSort) => (
|
||||
{currentRecordSorts.map((recordSort) => (
|
||||
<EditableSortChip
|
||||
key={recordSort.fieldMetadataId}
|
||||
recordSort={recordSort}
|
||||
/>
|
||||
))}
|
||||
{isNonEmptyArray(recordFilters) &&
|
||||
isNonEmptyArray(
|
||||
currentViewWithCombinedFiltersAndSorts?.viewSorts,
|
||||
) && (
|
||||
isNonEmptyArray(currentRecordSorts) && (
|
||||
<StyledSeperatorContainer>
|
||||
<StyledSeperator />
|
||||
</StyledSeperatorContainer>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { contextStoreCurrentObjectMetadataItemComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemComponentState';
|
||||
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
|
||||
import { availableFieldMetadataItemsForSortFamilySelector } from '@/object-metadata/states/availableFieldMetadataItemsForSortFamilySelector';
|
||||
import { formatFieldMetadataItemsAsSortDefinitions } from '@/object-metadata/utils/formatFieldMetadataItemsAsSortDefinitions';
|
||||
import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState';
|
||||
import { prefetchViewFromViewIdFamilySelector } from '@/prefetch/states/selector/prefetchViewFromViewIdFamilySelector';
|
||||
import { useRecoilComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyStateV2';
|
||||
@@ -59,14 +58,8 @@ export const ViewBarRecordSortEffect = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const sortDefinitions = formatFieldMetadataItemsAsSortDefinitions({
|
||||
fields: sortableFieldMetadataItems,
|
||||
});
|
||||
|
||||
if (isDefined(currentView)) {
|
||||
setCurrentRecordSorts(
|
||||
mapViewSortsToSorts(currentView.viewSorts, sortDefinitions),
|
||||
);
|
||||
setCurrentRecordSorts(mapViewSortsToSorts(currentView.viewSorts));
|
||||
setHasInitializedCurrentRecordSorts(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,47 +3,27 @@ import { useEffect } from 'react';
|
||||
import { onSortSelectComponentState } from '@/object-record/object-sort-dropdown/states/onSortSelectScopedState';
|
||||
import { useUpsertRecordSort } from '@/object-record/record-sort/hooks/useUpsertRecordSort';
|
||||
import { RecordSort } from '@/object-record/record-sort/types/RecordSort';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||
import { useUpsertCombinedViewSorts } from '@/views/hooks/useUpsertCombinedViewSorts';
|
||||
import { availableSortDefinitionsComponentState } from '@/views/states/availableSortDefinitionsComponentState';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
|
||||
export const ViewBarSortEffect = () => {
|
||||
const { upsertCombinedViewSort } = useUpsertCombinedViewSorts();
|
||||
|
||||
// TDOO: verify this instance id works
|
||||
const availableSortDefinitions = useRecoilComponentValueV2(
|
||||
availableSortDefinitionsComponentState,
|
||||
);
|
||||
|
||||
const { upsertRecordSort } = useUpsertRecordSort();
|
||||
|
||||
const setOnSortSelect = useSetRecoilComponentStateV2(
|
||||
onSortSelectComponentState,
|
||||
);
|
||||
|
||||
// TDOO: verify this instance id works
|
||||
const setAvailableSortDefinitionsInSortDropdown =
|
||||
useSetRecoilComponentStateV2(availableSortDefinitionsComponentState);
|
||||
|
||||
useEffect(() => {
|
||||
if (isDefined(availableSortDefinitions)) {
|
||||
setAvailableSortDefinitionsInSortDropdown(availableSortDefinitions);
|
||||
}
|
||||
setOnSortSelect(() => (sort: RecordSort | null) => {
|
||||
if (isDefined(sort)) {
|
||||
upsertCombinedViewSort(sort);
|
||||
upsertRecordSort(sort);
|
||||
}
|
||||
});
|
||||
}, [
|
||||
availableSortDefinitions,
|
||||
setAvailableSortDefinitionsInSortDropdown,
|
||||
setOnSortSelect,
|
||||
upsertCombinedViewSort,
|
||||
upsertRecordSort,
|
||||
]);
|
||||
}, [setOnSortSelect, upsertCombinedViewSort, upsertRecordSort]);
|
||||
|
||||
return <></>;
|
||||
};
|
||||
|
||||
-5
@@ -96,11 +96,6 @@ describe('useApplyCurrentViewSortsToCurrentRecordSorts', () => {
|
||||
id: mockViewSort.id,
|
||||
fieldMetadataId: mockViewSort.fieldMetadataId,
|
||||
direction: mockViewSort.direction,
|
||||
definition: {
|
||||
fieldMetadataId: mockViewSort.fieldMetadataId,
|
||||
iconName: mockFieldMetadataItem.icon ?? '',
|
||||
label: mockFieldMetadataItem.label,
|
||||
},
|
||||
} satisfies RecordSort,
|
||||
]);
|
||||
});
|
||||
|
||||
-5
@@ -70,11 +70,6 @@ describe('useApplyViewSortsToCurrentRecordSorts', () => {
|
||||
id: mockViewSort.id,
|
||||
fieldMetadataId: mockViewSort.fieldMetadataId,
|
||||
direction: mockViewSort.direction,
|
||||
definition: {
|
||||
fieldMetadataId: mockViewSort.fieldMetadataId,
|
||||
label: mockFieldMetadataItem.label,
|
||||
iconName: mockFieldMetadataItem.icon ?? '',
|
||||
},
|
||||
} satisfies RecordSort,
|
||||
]);
|
||||
});
|
||||
|
||||
+1
-12
@@ -1,6 +1,4 @@
|
||||
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
|
||||
import { formatFieldMetadataItemsAsSortDefinitions } from '@/object-metadata/utils/formatFieldMetadataItemsAsSortDefinitions';
|
||||
import { useSortableFieldMetadataItemsInRecordIndexContext } from '@/object-record/record-sort/hooks/useSortableFieldMetadataItemsInRecordIndexContext';
|
||||
import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState';
|
||||
import { prefetchViewFromViewIdFamilySelector } from '@/prefetch/states/selector/prefetchViewFromViewIdFamilySelector';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
@@ -25,18 +23,9 @@ export const useApplyCurrentViewSortsToCurrentRecordSorts = () => {
|
||||
currentRecordSortsComponentState,
|
||||
);
|
||||
|
||||
const { sortableFieldMetadataItems } =
|
||||
useSortableFieldMetadataItemsInRecordIndexContext();
|
||||
|
||||
const applyCurrentViewSortsToCurrentRecordSorts = () => {
|
||||
const sortDefinitions = formatFieldMetadataItemsAsSortDefinitions({
|
||||
fields: sortableFieldMetadataItems,
|
||||
});
|
||||
|
||||
if (isDefined(currentView)) {
|
||||
setCurrentRecordSorts(
|
||||
mapViewSortsToSorts(currentView.viewSorts, sortDefinitions),
|
||||
);
|
||||
setCurrentRecordSorts(mapViewSortsToSorts(currentView.viewSorts));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+1
-10
@@ -1,5 +1,3 @@
|
||||
import { formatFieldMetadataItemsAsSortDefinitions } from '@/object-metadata/utils/formatFieldMetadataItemsAsSortDefinitions';
|
||||
import { useSortableFieldMetadataItemsInRecordIndexContext } from '@/object-record/record-sort/hooks/useSortableFieldMetadataItemsInRecordIndexContext';
|
||||
import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState';
|
||||
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||
import { ViewSort } from '@/views/types/ViewSort';
|
||||
@@ -10,15 +8,8 @@ export const useApplyViewSortsToCurrentRecordSorts = () => {
|
||||
currentRecordSortsComponentState,
|
||||
);
|
||||
|
||||
const { sortableFieldMetadataItems } =
|
||||
useSortableFieldMetadataItemsInRecordIndexContext();
|
||||
|
||||
const applyViewSortsToCurrentRecordSorts = (viewSorts: ViewSort[]) => {
|
||||
const sortDefinitions = formatFieldMetadataItemsAsSortDefinitions({
|
||||
fields: sortableFieldMetadataItems,
|
||||
});
|
||||
|
||||
const recordSortsToApply = mapViewSortsToSorts(viewSorts, sortDefinitions);
|
||||
const recordSortsToApply = mapViewSortsToSorts(viewSorts);
|
||||
|
||||
setCurrentRecordSorts(recordSortsToApply);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||
import { availableFieldDefinitionsComponentState } from '@/views/states/availableFieldDefinitionsComponentState';
|
||||
import { availableSortDefinitionsComponentState } from '@/views/states/availableSortDefinitionsComponentState';
|
||||
import { viewObjectMetadataIdComponentState } from '@/views/states/viewObjectMetadataIdComponentState';
|
||||
|
||||
export const useInitViewBar = (viewBarInstanceId?: string) => {
|
||||
@@ -9,11 +8,6 @@ export const useInitViewBar = (viewBarInstanceId?: string) => {
|
||||
viewBarInstanceId,
|
||||
);
|
||||
|
||||
const setAvailableSortDefinitions = useSetRecoilComponentStateV2(
|
||||
availableSortDefinitionsComponentState,
|
||||
viewBarInstanceId,
|
||||
);
|
||||
|
||||
const setViewObjectMetadataId = useSetRecoilComponentStateV2(
|
||||
viewObjectMetadataIdComponentState,
|
||||
viewBarInstanceId,
|
||||
@@ -21,7 +15,6 @@ export const useInitViewBar = (viewBarInstanceId?: string) => {
|
||||
|
||||
return {
|
||||
setAvailableFieldDefinitions,
|
||||
setAvailableSortDefinitions,
|
||||
setViewObjectMetadataId,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
|
||||
import { RecordSort } from '@/object-record/record-sort/types/RecordSort';
|
||||
@@ -103,7 +102,6 @@ export const useUpsertCombinedViewSorts = (viewBarComponentId?: string) => {
|
||||
...unsavedToUpsertViewSorts,
|
||||
{
|
||||
...upsertedSort,
|
||||
id: v4(),
|
||||
__typename: 'ViewSort',
|
||||
} satisfies ViewSort,
|
||||
]);
|
||||
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
import { SortDefinition } from '@/object-record/object-sort-dropdown/types/SortDefinition';
|
||||
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
|
||||
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
|
||||
|
||||
export const availableSortDefinitionsComponentState = createComponentStateV2<
|
||||
SortDefinition[]
|
||||
>({
|
||||
key: 'availableSortDefinitionsComponentState',
|
||||
defaultValue: [],
|
||||
componentInstanceContext: ViewComponentInstanceContext,
|
||||
});
|
||||
@@ -13,13 +13,6 @@ import { mapViewSortsToSorts } from '@/views/utils/mapViewSortsToSorts';
|
||||
|
||||
import { FieldMetadataType } from '~/generated/graphql';
|
||||
|
||||
const baseDefinition = {
|
||||
fieldMetadataId: '05731f68-6e7a-4903-8374-c0b6a9063482',
|
||||
label: 'label',
|
||||
iconName: 'iconName',
|
||||
fieldName: 'fieldName',
|
||||
};
|
||||
|
||||
const baseFieldMetadataItem = {
|
||||
id: '05731f68-6e7a-4903-8374-c0b6a9063482',
|
||||
createdAt: '2021-01-01',
|
||||
@@ -44,12 +37,9 @@ describe('mapViewSortsToSorts', () => {
|
||||
id: 'id',
|
||||
fieldMetadataId: '05731f68-6e7a-4903-8374-c0b6a9063482',
|
||||
direction: 'asc',
|
||||
definition: baseDefinition,
|
||||
},
|
||||
];
|
||||
expect(mapViewSortsToSorts(viewSorts, [baseDefinition])).toEqual(
|
||||
expectedSorts,
|
||||
);
|
||||
expect(mapViewSortsToSorts(viewSorts)).toEqual(expectedSorts);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
|
||||
import { formatFieldMetadataItemsAsSortDefinitions } from '@/object-metadata/utils/formatFieldMetadataItemsAsSortDefinitions';
|
||||
import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy';
|
||||
import { RecordFilterValueDependencies } from '@/object-record/record-filter/types/RecordFilterValueDependencies';
|
||||
|
||||
@@ -31,10 +30,6 @@ export const getQueryVariablesFromView = ({
|
||||
|
||||
const { viewFilterGroups, viewFilters, viewSorts } = view;
|
||||
|
||||
const sortDefinitions = formatFieldMetadataItemsAsSortDefinitions({
|
||||
fields: fieldMetadataItems,
|
||||
});
|
||||
|
||||
const filter = computeViewRecordGqlOperationFilter(
|
||||
filterValueDependencies,
|
||||
mapViewFiltersToFilters(viewFilters, fieldMetadataItems),
|
||||
@@ -44,7 +39,7 @@ export const getQueryVariablesFromView = ({
|
||||
|
||||
const orderBy = turnSortsIntoOrderBy(
|
||||
objectMetadataItem,
|
||||
mapViewSortsToSorts(viewSorts, sortDefinitions),
|
||||
mapViewSortsToSorts(viewSorts),
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,27 +1,15 @@
|
||||
import { SortDefinition } from '@/object-record/object-sort-dropdown/types/SortDefinition';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
|
||||
import { RecordSort } from '@/object-record/record-sort/types/RecordSort';
|
||||
import { ViewSort } from '../types/ViewSort';
|
||||
|
||||
export const mapViewSortsToSorts = (
|
||||
viewSorts: ViewSort[],
|
||||
availableSortDefinitions: SortDefinition[],
|
||||
): RecordSort[] => {
|
||||
export const mapViewSortsToSorts = (viewSorts: ViewSort[]): RecordSort[] => {
|
||||
return viewSorts
|
||||
.map((viewSort) => {
|
||||
const availableSortDefinition = availableSortDefinitions.find(
|
||||
(sortDefinition) =>
|
||||
sortDefinition.fieldMetadataId === viewSort.fieldMetadataId,
|
||||
);
|
||||
|
||||
if (!availableSortDefinition) return null;
|
||||
|
||||
return {
|
||||
id: viewSort.id,
|
||||
fieldMetadataId: viewSort.fieldMetadataId,
|
||||
direction: viewSort.direction,
|
||||
definition: availableSortDefinition,
|
||||
};
|
||||
})
|
||||
.filter(isDefined);
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { WorkspaceActivationStatus } from 'twenty-shared';
|
||||
|
||||
export const useIsWorkspaceActivationStatusEqualsTo = (
|
||||
activationStatus: WorkspaceActivationStatus,
|
||||
): boolean => {
|
||||
const currentWorkspace = useRecoilValue(currentWorkspaceState);
|
||||
return currentWorkspace?.activationStatus === activationStatus;
|
||||
};
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { WorkspaceActivationStatus } from '~/generated/graphql';
|
||||
|
||||
export const useIsWorkspaceActivationStatusSuspended = (): boolean => {
|
||||
const currentWorkspace = useRecoilValue(currentWorkspaceState);
|
||||
return (
|
||||
currentWorkspace?.activationStatus === WorkspaceActivationStatus.SUSPENDED
|
||||
);
|
||||
};
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
IconTrashX,
|
||||
Section,
|
||||
} from 'twenty-ui';
|
||||
import { Role, SettingsFeatures } from '~/generated-metadata/graphql';
|
||||
import { Role, SettingsPermissions } from '~/generated-metadata/graphql';
|
||||
import { RolePermissionsObjectsTableHeader } from '~/pages/settings/roles/components/RolePermissionsObjectsTableHeader';
|
||||
import { RolePermissionsSettingsTableHeader } from '~/pages/settings/roles/components/RolePermissionsSettingsTableHeader';
|
||||
import { RolePermissionsSettingsTableRow } from '~/pages/settings/roles/components/RolePermissionsSettingsTableRow';
|
||||
@@ -63,43 +63,43 @@ export const RolePermissions = ({ role }: RolePermissionsProps) => {
|
||||
|
||||
const settingsPermissionsConfig = [
|
||||
{
|
||||
key: SettingsFeatures.API_KEYS_AND_WEBHOOKS,
|
||||
key: SettingsPermissions.API_KEYS_AND_WEBHOOKS,
|
||||
label: 'API Keys and Webhooks',
|
||||
type: 'Developer',
|
||||
value: role.canUpdateAllSettings,
|
||||
},
|
||||
{
|
||||
key: SettingsFeatures.ROLES,
|
||||
key: SettingsPermissions.ROLES,
|
||||
label: 'Roles',
|
||||
type: 'Members',
|
||||
value: role.canUpdateAllSettings,
|
||||
},
|
||||
{
|
||||
key: SettingsFeatures.WORKSPACE,
|
||||
key: SettingsPermissions.WORKSPACE,
|
||||
label: 'Workspace Settings',
|
||||
type: 'General',
|
||||
value: role.canUpdateAllSettings,
|
||||
},
|
||||
{
|
||||
key: SettingsFeatures.WORKSPACE_USERS,
|
||||
key: SettingsPermissions.WORKSPACE_USERS,
|
||||
label: 'Workspace Users',
|
||||
type: 'Members',
|
||||
value: role.canUpdateAllSettings,
|
||||
},
|
||||
{
|
||||
key: SettingsFeatures.DATA_MODEL,
|
||||
key: SettingsPermissions.DATA_MODEL,
|
||||
label: 'Data Model',
|
||||
type: 'Data Model',
|
||||
value: role.canUpdateAllSettings,
|
||||
},
|
||||
{
|
||||
key: SettingsFeatures.ADMIN_PANEL,
|
||||
key: SettingsPermissions.ADMIN_PANEL,
|
||||
label: 'Admin Panel',
|
||||
type: 'Admin Panel',
|
||||
value: role.canUpdateAllSettings,
|
||||
},
|
||||
{
|
||||
key: SettingsFeatures.SECURITY,
|
||||
key: SettingsPermissions.SECURITY,
|
||||
label: 'Security Settings',
|
||||
type: 'Security',
|
||||
value: role.canUpdateAllSettings,
|
||||
|
||||
@@ -4,11 +4,14 @@ import { H2Title, IconLock, Section, Tag } from 'twenty-ui';
|
||||
|
||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||
import { SettingsReadDocumentationButton } from '@/settings/developers/components/SettingsReadDocumentationButton';
|
||||
import { SettingsSSOIdentitiesProvidersListCard } from '@/settings/security/components/SettingsSSOIdentitiesProvidersListCard';
|
||||
import { SettingsSecurityOptionsList } from '@/settings/security/components/SettingsSecurityOptionsList';
|
||||
import { SettingsSSOIdentitiesProvidersListCard } from '@/settings/security/components/SSO/SettingsSSOIdentitiesProvidersListCard';
|
||||
import { SettingsSecurityAuthProvidersOptionsList } from '@/settings/security/components/SettingsSecurityAuthProvidersOptionsList';
|
||||
import { SettingsPath } from '@/types/SettingsPath';
|
||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
|
||||
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
|
||||
import { SettingsApprovedAccessDomainsListCard } from '@/settings/security/components/approvedAccessDomains/SettingsApprovedAccessDomainsListCard';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { FeatureFlagKey } from '~/generated/graphql';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
width: 100%;
|
||||
@@ -21,13 +24,17 @@ const StyledMainContent = styled.div`
|
||||
min-height: 200px;
|
||||
`;
|
||||
|
||||
const StyledSSOSection = styled(Section)`
|
||||
const StyledSection = styled(Section)`
|
||||
flex-shrink: 0;
|
||||
`;
|
||||
|
||||
export const SettingsSecurity = () => {
|
||||
const { t } = useLingui();
|
||||
|
||||
const IsApprovedAccessDomainsEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IsApprovedAccessDomainsEnabled,
|
||||
);
|
||||
|
||||
return (
|
||||
<SubMenuTopBarContainer
|
||||
title={t`Security`}
|
||||
@@ -42,7 +49,7 @@ export const SettingsSecurity = () => {
|
||||
>
|
||||
<SettingsPageContainer>
|
||||
<StyledMainContent>
|
||||
<StyledSSOSection>
|
||||
<StyledSection>
|
||||
<H2Title
|
||||
title={t`SSO`}
|
||||
description={t`Configure an SSO connection`}
|
||||
@@ -56,14 +63,23 @@ export const SettingsSecurity = () => {
|
||||
}
|
||||
/>
|
||||
<SettingsSSOIdentitiesProvidersListCard />
|
||||
</StyledSSOSection>
|
||||
</StyledSection>
|
||||
{IsApprovedAccessDomainsEnabled && (
|
||||
<StyledSection>
|
||||
<H2Title
|
||||
title={t`Approved Email Domain`}
|
||||
description={t`Anyone with an email address at these domains is allowed to sign up for this workspace.`}
|
||||
/>
|
||||
<SettingsApprovedAccessDomainsListCard />
|
||||
</StyledSection>
|
||||
)}
|
||||
<Section>
|
||||
<StyledContainer>
|
||||
<H2Title
|
||||
title={t`Authentication`}
|
||||
description={t`Customize your workspace security`}
|
||||
/>
|
||||
<SettingsSecurityOptionsList />
|
||||
<SettingsSecurityAuthProvidersOptionsList />
|
||||
</StyledContainer>
|
||||
</Section>
|
||||
</StyledMainContent>
|
||||
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
|
||||
import { SettingsPath } from '@/types/SettingsPath';
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
|
||||
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
|
||||
import { Trans, useLingui } from '@lingui/react/macro';
|
||||
import { TextInput } from '@/ui/input/components/TextInput';
|
||||
import { z } from 'zod';
|
||||
import { H2Title, Section } from 'twenty-ui';
|
||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||
import { useCreateApprovedAccessDomainMutation } from '~/generated/graphql';
|
||||
|
||||
export const SettingsSecurityApprovedAccessDomain = () => {
|
||||
const navigate = useNavigateSettings();
|
||||
|
||||
const { t } = useLingui();
|
||||
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
|
||||
const [createApprovedAccessDomain] = useCreateApprovedAccessDomainMutation();
|
||||
|
||||
const formConfig = useForm<{ domain: string; email: string }>({
|
||||
mode: 'onSubmit',
|
||||
resolver: zodResolver(
|
||||
z
|
||||
.object({
|
||||
domain: z
|
||||
.string()
|
||||
.regex(
|
||||
/^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$/,
|
||||
{
|
||||
message: t`Invalid domain. Domains have to be smaller than 256 characters in length, cannot be IP addresses, cannot contain spaces, cannot contain any special characters such as _~\`!@#$%^*()=+{}[]|\\;:'",<>/? and cannot begin or end with a '-' character.`,
|
||||
},
|
||||
)
|
||||
.max(256),
|
||||
email: z.string().min(1, {
|
||||
message: t`Email can not be empty`,
|
||||
}),
|
||||
})
|
||||
.strict(),
|
||||
),
|
||||
defaultValues: {
|
||||
email: '',
|
||||
domain: '',
|
||||
},
|
||||
});
|
||||
|
||||
const domain = formConfig.watch('domain');
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
if (!formConfig.formState.isValid) {
|
||||
return;
|
||||
}
|
||||
createApprovedAccessDomain({
|
||||
variables: {
|
||||
input: {
|
||||
domain: formConfig.getValues('domain'),
|
||||
email:
|
||||
formConfig.getValues('email') +
|
||||
'@' +
|
||||
formConfig.getValues('domain'),
|
||||
},
|
||||
},
|
||||
onCompleted: () => {
|
||||
enqueueSnackBar(t`Domain added successfully.`, {
|
||||
variant: SnackBarVariant.Success,
|
||||
});
|
||||
navigate(SettingsPath.Security);
|
||||
},
|
||||
onError: (error) => {
|
||||
enqueueSnackBar((error as Error).message, {
|
||||
variant: SnackBarVariant.Error,
|
||||
});
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
enqueueSnackBar((error as Error).message, {
|
||||
variant: SnackBarVariant.Error,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SubMenuTopBarContainer
|
||||
title="New Approved Access Domain"
|
||||
actionButton={
|
||||
<SaveAndCancelButtons
|
||||
onCancel={() => navigate(SettingsPath.Security)}
|
||||
onSave={formConfig.handleSubmit(handleSave)}
|
||||
/>
|
||||
}
|
||||
links={[
|
||||
{
|
||||
children: <Trans>Workspace</Trans>,
|
||||
href: getSettingsPath(SettingsPath.Workspace),
|
||||
},
|
||||
{
|
||||
children: <Trans>Security</Trans>,
|
||||
href: getSettingsPath(SettingsPath.Security),
|
||||
},
|
||||
{ children: <Trans>New Approved Access Domain</Trans> },
|
||||
]}
|
||||
>
|
||||
<SettingsPageContainer>
|
||||
<Section>
|
||||
<H2Title title="Domain" description="The name of your Domain" />
|
||||
<Controller
|
||||
name="domain"
|
||||
control={formConfig.control}
|
||||
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
||||
<TextInput
|
||||
autoComplete="off"
|
||||
value={value}
|
||||
onChange={(domain: string) => {
|
||||
onChange(domain);
|
||||
}}
|
||||
fullWidth
|
||||
placeholder="yourdomain.com"
|
||||
error={error?.message}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Section>
|
||||
<Section>
|
||||
<H2Title
|
||||
title="Email verification"
|
||||
description="We will send your a link to verify domain ownership"
|
||||
/>
|
||||
<Controller
|
||||
name="email"
|
||||
control={formConfig.control}
|
||||
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
||||
<TextInput
|
||||
autoComplete="off"
|
||||
value={value.split('@')[0]}
|
||||
onChange={onChange}
|
||||
fullWidth
|
||||
error={error?.message}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{domain}
|
||||
</Section>
|
||||
</SettingsPageContainer>
|
||||
</SubMenuTopBarContainer>
|
||||
);
|
||||
};
|
||||
+5
-4
@@ -1,7 +1,7 @@
|
||||
/* @license Enterprise */
|
||||
|
||||
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
|
||||
import SettingsSSOIdentitiesProvidersForm from '@/settings/security/components/SettingsSSOIdentitiesProvidersForm';
|
||||
import SettingsSSOIdentitiesProvidersForm from '@/settings/security/components/SSO/SettingsSSOIdentitiesProvidersForm';
|
||||
import { useCreateSSOIdentityProvider } from '@/settings/security/hooks/useCreateSSOIdentityProvider';
|
||||
import { SettingSecurityNewSSOIdentityFormValues } from '@/settings/security/types/SSOIdentityProvider';
|
||||
import { sSOIdentityProviderDefaultValues } from '@/settings/security/utils/sSOIdentityProviderDefaultValues';
|
||||
@@ -11,6 +11,7 @@ import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/Snac
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import pick from 'lodash.pick';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
|
||||
@@ -64,14 +65,14 @@ export const SettingsSecuritySSOIdentifyProvider = () => {
|
||||
}
|
||||
links={[
|
||||
{
|
||||
children: 'Workspace',
|
||||
children: <Trans>Workspace</Trans>,
|
||||
href: getSettingsPath(SettingsPath.Workspace),
|
||||
},
|
||||
{
|
||||
children: 'Security',
|
||||
children: <Trans>Security</Trans>,
|
||||
href: getSettingsPath(SettingsPath.Security),
|
||||
},
|
||||
{ children: 'New' },
|
||||
{ children: <Trans>New SSO provider</Trans> },
|
||||
]}
|
||||
>
|
||||
<FormProvider
|
||||
|
||||
@@ -45,7 +45,7 @@ export const SettingsDomain = () => {
|
||||
.regex(
|
||||
/^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$/,
|
||||
{
|
||||
message: t`Invalid custom domain. Custom domains have to be smaller than 256 characters in length, cannot be IP addresses, cannot contain spaces, cannot contain any special characters such as _~\`!@#$%^*()=+{}[]|\\;:'",<>/? and cannot begin or end with a '-' character.`,
|
||||
message: t`Invalid domain. Domains have to be smaller than 256 characters in length, cannot be IP addresses, cannot contain spaces, cannot contain any special characters such as _~\`!@#$%^*()=+{}[]|\\;:'",<>/? and cannot begin or end with a '-' character.`,
|
||||
},
|
||||
)
|
||||
.max(256)
|
||||
|
||||
@@ -3,7 +3,7 @@ import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
|
||||
import {
|
||||
FeatureFlagKey,
|
||||
OnboardingStatus,
|
||||
SettingsFeatures,
|
||||
SettingsPermissions,
|
||||
SubscriptionInterval,
|
||||
SubscriptionStatus,
|
||||
User,
|
||||
@@ -129,7 +129,7 @@ export const mockedUserData: MockedUser = {
|
||||
workspaceMember: mockedWorkspaceMemberData,
|
||||
currentWorkspace: mockCurrentWorkspace,
|
||||
currentUserWorkspace: {
|
||||
settingsPermissions: [SettingsFeatures.WORKSPACE_USERS],
|
||||
settingsPermissions: [SettingsPermissions.WORKSPACE_USERS],
|
||||
},
|
||||
locale: 'en',
|
||||
workspaces: [{ workspace: mockCurrentWorkspace }],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "twenty-server",
|
||||
"version": "0.42.0-canary",
|
||||
"version": "0.42.16",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
|
||||
@@ -8,6 +8,8 @@ import {
|
||||
BaseCommandRunner,
|
||||
} from 'src/database/commands/base.command';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
export type ActiveWorkspacesCommandOptions = BaseCommandOptions & {
|
||||
workspaceId?: string;
|
||||
startFromWorkspaceId?: string;
|
||||
@@ -19,7 +21,10 @@ export abstract class ActiveWorkspacesCommandRunner extends BaseCommandRunner {
|
||||
private startFromWorkspaceId: string | undefined;
|
||||
private workspaceCountLimit: number | undefined;
|
||||
|
||||
constructor(protected readonly workspaceRepository: Repository<Workspace>) {
|
||||
constructor(
|
||||
protected readonly workspaceRepository: Repository<Workspace>,
|
||||
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
@@ -122,6 +127,54 @@ export abstract class ActiveWorkspacesCommandRunner extends BaseCommandRunner {
|
||||
);
|
||||
}
|
||||
|
||||
protected async processEachWorkspaceWithWorkspaceDataSource(
|
||||
workspaceIds: string[],
|
||||
callback: ({
|
||||
workspaceId,
|
||||
index,
|
||||
total,
|
||||
dataSource,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
index: number;
|
||||
total: number;
|
||||
dataSource: WorkspaceDataSource;
|
||||
}) => Promise<void>,
|
||||
): Promise<void> {
|
||||
this.logger.log(
|
||||
chalk.green(`Running command on ${workspaceIds.length} workspaces`),
|
||||
);
|
||||
for (const [index, workspaceId] of workspaceIds.entries()) {
|
||||
this.logger.log(
|
||||
chalk.green(
|
||||
`Processing workspace ${workspaceId} (${index + 1}/${
|
||||
workspaceIds.length
|
||||
})`,
|
||||
),
|
||||
);
|
||||
|
||||
const dataSource =
|
||||
await this.twentyORMGlobalManager.getDataSourceForWorkspace(
|
||||
workspaceId,
|
||||
false,
|
||||
);
|
||||
|
||||
try {
|
||||
await callback({
|
||||
workspaceId,
|
||||
index,
|
||||
total: workspaceIds.length,
|
||||
dataSource,
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error(`Error in workspace ${workspaceId}: ${error}`);
|
||||
}
|
||||
await this.twentyORMGlobalManager.destroyDataSourceForWorkspace(
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract executeActiveWorkspacesCommand(
|
||||
passedParams: string[],
|
||||
options: BaseCommandOptions,
|
||||
|
||||
@@ -7,8 +7,6 @@ import { DataSeedDemoWorkspaceCommand } from 'src/database/commands/data-seed-de
|
||||
import { DataSeedDemoWorkspaceModule } from 'src/database/commands/data-seed-demo-workspace/data-seed-demo-workspace.module';
|
||||
import { DataSeedWorkspaceCommand } from 'src/database/commands/data-seed-dev-workspace.command';
|
||||
import { ConfirmationQuestion } from 'src/database/commands/questions/confirmation.question';
|
||||
import { UpgradeTo0_40CommandModule } from 'src/database/commands/upgrade-version/0-40/0-40-upgrade-version.module';
|
||||
import { UpgradeTo0_41CommandModule } from 'src/database/commands/upgrade-version/0-41/0-41-upgrade-version.module';
|
||||
import { UpgradeTo0_42CommandModule } from 'src/database/commands/upgrade-version/0-42/0-42-upgrade-version.module';
|
||||
import { UpgradeTo0_43CommandModule } from 'src/database/commands/upgrade-version/0-43/0-43-upgrade-version.module';
|
||||
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
|
||||
@@ -51,8 +49,6 @@ import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/worksp
|
||||
DataSeedDemoWorkspaceModule,
|
||||
WorkspaceCacheStorageModule,
|
||||
WorkspaceMetadataVersionModule,
|
||||
UpgradeTo0_40CommandModule,
|
||||
UpgradeTo0_41CommandModule,
|
||||
UpgradeTo0_42CommandModule,
|
||||
UpgradeTo0_43CommandModule,
|
||||
FeatureFlagModule,
|
||||
|
||||
-245
@@ -1,245 +0,0 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import chalk from 'chalk';
|
||||
import { Command } from 'nest-commander';
|
||||
import { Repository } from 'typeorm';
|
||||
import { v4 } from 'uuid';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
|
||||
import {
|
||||
ActiveWorkspacesCommandOptions,
|
||||
ActiveWorkspacesCommandRunner,
|
||||
} from 'src/database/commands/active-workspaces.command';
|
||||
import { isCommandLogger } from 'src/database/commands/logger';
|
||||
import { AGGREGATE_OPERATIONS } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service';
|
||||
import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util';
|
||||
import {
|
||||
WorkspaceMigrationColumnActionType,
|
||||
WorkspaceMigrationTableAction,
|
||||
WorkspaceMigrationTableActionType,
|
||||
} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
|
||||
import { WorkspaceMigrationFactory } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.factory';
|
||||
import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.service';
|
||||
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
|
||||
import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service';
|
||||
import {
|
||||
VIEW_FIELD_STANDARD_FIELD_IDS,
|
||||
VIEW_STANDARD_FIELD_IDS,
|
||||
} from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
||||
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade-0.40:migrate-aggregate-operation-options',
|
||||
description: 'Add aggregate operations options to relevant fields',
|
||||
})
|
||||
export class MigrateAggregateOperationOptionsCommand extends ActiveWorkspacesCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(Workspace, 'core')
|
||||
protected readonly workspaceRepository: Repository<Workspace>,
|
||||
@InjectRepository(FieldMetadataEntity, 'metadata')
|
||||
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
|
||||
@InjectRepository(ObjectMetadataEntity, 'metadata')
|
||||
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
|
||||
private readonly workspaceMigrationService: WorkspaceMigrationService,
|
||||
private readonly workspaceMigrationFactory: WorkspaceMigrationFactory,
|
||||
private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService,
|
||||
private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService,
|
||||
) {
|
||||
super(workspaceRepository);
|
||||
}
|
||||
|
||||
ADDITIONAL_AGGREGATE_OPERATIONS = [
|
||||
{
|
||||
value: AGGREGATE_OPERATIONS.countEmpty,
|
||||
label: 'Count empty',
|
||||
position: 5,
|
||||
color: 'red',
|
||||
},
|
||||
{
|
||||
value: AGGREGATE_OPERATIONS.countNotEmpty,
|
||||
label: 'Count not empty',
|
||||
position: 6,
|
||||
color: 'purple',
|
||||
},
|
||||
{
|
||||
value: AGGREGATE_OPERATIONS.countUniqueValues,
|
||||
label: 'Count unique values',
|
||||
position: 7,
|
||||
color: 'sky',
|
||||
},
|
||||
{
|
||||
value: AGGREGATE_OPERATIONS.percentageEmpty,
|
||||
label: 'Percent empty',
|
||||
position: 8,
|
||||
color: 'turquoise',
|
||||
},
|
||||
{
|
||||
value: AGGREGATE_OPERATIONS.percentageNotEmpty,
|
||||
label: 'Percent not empty',
|
||||
position: 9,
|
||||
color: 'yellow',
|
||||
},
|
||||
];
|
||||
|
||||
ADDITIONAL_AGGREGATE_OPERATIONS_VALUES =
|
||||
this.ADDITIONAL_AGGREGATE_OPERATIONS.map((option) => option.value);
|
||||
|
||||
async executeActiveWorkspacesCommand(
|
||||
_passedParam: string[],
|
||||
options: ActiveWorkspacesCommandOptions,
|
||||
workspaceIds: string[],
|
||||
): Promise<void> {
|
||||
this.logger.log(
|
||||
'Running command to migrate aggregate operations options to include count operations',
|
||||
);
|
||||
|
||||
if (isCommandLogger(this.logger)) {
|
||||
this.logger.setVerbose(options.verbose ?? false);
|
||||
}
|
||||
|
||||
let workspaceIterator = 1;
|
||||
|
||||
for (const workspaceId of workspaceIds) {
|
||||
this.logger.log(
|
||||
`Running command for workspace ${workspaceId} ${workspaceIterator}/${workspaceIds.length}`,
|
||||
);
|
||||
|
||||
try {
|
||||
const viewFieldObjectMetadata =
|
||||
await this.objectMetadataRepository.findOne({
|
||||
where: {
|
||||
workspaceId,
|
||||
standardId: STANDARD_OBJECT_IDS.viewField,
|
||||
},
|
||||
});
|
||||
|
||||
if (!isDefined(viewFieldObjectMetadata)) {
|
||||
throw new Error(
|
||||
`View field object metadata not found for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
const viewFieldAggregateOperationFieldMetadata =
|
||||
await this.fieldMetadataRepository.findOne({
|
||||
where: {
|
||||
workspaceId,
|
||||
objectMetadataId: viewFieldObjectMetadata.id,
|
||||
standardId: VIEW_FIELD_STANDARD_FIELD_IDS.aggregateOperation,
|
||||
},
|
||||
});
|
||||
|
||||
if (isDefined(viewFieldAggregateOperationFieldMetadata)) {
|
||||
await this.updateAggregateOperationField(
|
||||
workspaceId,
|
||||
viewFieldAggregateOperationFieldMetadata,
|
||||
viewFieldObjectMetadata,
|
||||
);
|
||||
}
|
||||
|
||||
const viewObjectMetadata = await this.objectMetadataRepository.findOne({
|
||||
where: {
|
||||
workspaceId,
|
||||
standardId: STANDARD_OBJECT_IDS.view,
|
||||
},
|
||||
});
|
||||
|
||||
if (!isDefined(viewObjectMetadata)) {
|
||||
throw new Error(
|
||||
`View object metadata not found for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
const viewAggregateOperationFieldMetadata =
|
||||
await this.fieldMetadataRepository.findOne({
|
||||
where: {
|
||||
workspaceId,
|
||||
objectMetadataId: viewObjectMetadata.id,
|
||||
standardId: VIEW_STANDARD_FIELD_IDS.kanbanAggregateOperation,
|
||||
},
|
||||
});
|
||||
|
||||
if (isDefined(viewAggregateOperationFieldMetadata)) {
|
||||
await this.updateAggregateOperationField(
|
||||
workspaceId,
|
||||
viewAggregateOperationFieldMetadata,
|
||||
viewObjectMetadata,
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
isDefined(viewAggregateOperationFieldMetadata) ||
|
||||
isDefined(viewFieldAggregateOperationFieldMetadata)
|
||||
) {
|
||||
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations(
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
await this.workspaceMetadataVersionService.incrementMetadataVersion(
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
workspaceIterator++;
|
||||
this.logger.log(
|
||||
chalk.green(`Command completed for workspace ${workspaceId}.`),
|
||||
);
|
||||
} catch {
|
||||
this.logger.log(chalk.red(`Error in workspace ${workspaceId}.`));
|
||||
workspaceIterator++;
|
||||
}
|
||||
}
|
||||
this.logger.log(chalk.green(`Command completed!`));
|
||||
}
|
||||
|
||||
private async updateAggregateOperationField(
|
||||
workspaceId: string,
|
||||
fieldMetadata: FieldMetadataEntity,
|
||||
objectMetadata: ObjectMetadataEntity,
|
||||
) {
|
||||
if (
|
||||
fieldMetadata.options.some((option) => {
|
||||
return this.ADDITIONAL_AGGREGATE_OPERATIONS_VALUES.includes(
|
||||
option.value as AGGREGATE_OPERATIONS,
|
||||
);
|
||||
})
|
||||
) {
|
||||
this.logger.log(
|
||||
`Aggregate operation field metadata ${fieldMetadata.name} already has the required options`,
|
||||
);
|
||||
} else {
|
||||
const updatedFieldMetadata = {
|
||||
...fieldMetadata,
|
||||
options: [
|
||||
...fieldMetadata.options,
|
||||
...this.ADDITIONAL_AGGREGATE_OPERATIONS.map((operation) => ({
|
||||
...operation,
|
||||
id: v4(),
|
||||
})),
|
||||
],
|
||||
};
|
||||
|
||||
await this.fieldMetadataRepository.save(updatedFieldMetadata);
|
||||
|
||||
await this.workspaceMigrationService.createCustomMigration(
|
||||
generateMigrationName(
|
||||
`update-${objectMetadata.nameSingular}-aggregate-operation`,
|
||||
),
|
||||
workspaceId,
|
||||
[
|
||||
{
|
||||
name: computeObjectTargetTable(objectMetadata),
|
||||
action: WorkspaceMigrationTableActionType.ALTER,
|
||||
columns: this.workspaceMigrationFactory.createColumnActions(
|
||||
WorkspaceMigrationColumnActionType.ALTER,
|
||||
fieldMetadata,
|
||||
updatedFieldMetadata,
|
||||
),
|
||||
} satisfies WorkspaceMigrationTableAction,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
-248
@@ -1,248 +0,0 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import chalk from 'chalk';
|
||||
import { Command, Option } from 'nest-commander';
|
||||
import { WorkspaceActivationStatus } from 'twenty-shared';
|
||||
import { In, Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
BaseCommandOptions,
|
||||
BaseCommandRunner,
|
||||
} from 'src/database/commands/base.command';
|
||||
import { rawDataSource } from 'src/database/typeorm/raw/raw.datasource';
|
||||
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
|
||||
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
|
||||
import { SubscriptionStatus } from 'src/engine/core-modules/billing/enums/billing-subscription-status.enum';
|
||||
import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||
import { KeyValuePair } from 'src/engine/core-modules/key-value-pair/key-value-pair.entity';
|
||||
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
|
||||
import { WorkspaceMigrationEntity } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
|
||||
|
||||
type UpdateInactiveWorkspaceStatusOptions = BaseCommandOptions & {
|
||||
workspaceIds: string[];
|
||||
};
|
||||
|
||||
@Command({
|
||||
name: 'upgrade-0.40:update-inactive-workspace-status',
|
||||
description:
|
||||
'Update the status of inactive workspaces to SUSPENDED and delete them',
|
||||
})
|
||||
export class UpdateInactiveWorkspaceStatusCommand extends BaseCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(Workspace, 'core')
|
||||
protected readonly workspaceRepository: Repository<Workspace>,
|
||||
@InjectRepository(DataSourceEntity, 'metadata')
|
||||
protected readonly datasourceRepository: Repository<DataSourceEntity>,
|
||||
@InjectRepository(WorkspaceMigrationEntity, 'metadata')
|
||||
protected readonly workspaceMigrationRepository: Repository<WorkspaceMigrationEntity>,
|
||||
@InjectRepository(BillingSubscription, 'core')
|
||||
protected readonly subscriptionRepository: Repository<BillingSubscription>,
|
||||
@InjectRepository(FeatureFlag, 'core')
|
||||
protected readonly featureFlagRepository: Repository<FeatureFlag>,
|
||||
@InjectRepository(KeyValuePair, 'core')
|
||||
protected readonly keyValuePairRepository: Repository<KeyValuePair>,
|
||||
@InjectRepository(UserWorkspace, 'core')
|
||||
protected readonly userWorkspaceRepository: Repository<UserWorkspace>,
|
||||
@InjectRepository(User, 'core')
|
||||
protected readonly userRepository: Repository<User>,
|
||||
private readonly typeORMService: TypeORMService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: '-w, --workspace-ids [workspaceIds]',
|
||||
description: 'Workspace ids to process (comma separated)',
|
||||
})
|
||||
parseWorkspaceIds(val: string): string[] {
|
||||
return val.split(',');
|
||||
}
|
||||
|
||||
override async executeBaseCommand(
|
||||
_passedParams: string[],
|
||||
options: UpdateInactiveWorkspaceStatusOptions,
|
||||
): Promise<void> {
|
||||
const whereCondition: any = {
|
||||
activationStatus: WorkspaceActivationStatus.INACTIVE,
|
||||
};
|
||||
|
||||
if (options.workspaceIds?.length > 0) {
|
||||
whereCondition.id = In(options.workspaceIds);
|
||||
}
|
||||
|
||||
const workspaces = await this.workspaceRepository.find({
|
||||
where: whereCondition,
|
||||
});
|
||||
|
||||
if (options.dryRun) {
|
||||
this.logger.log(chalk.yellow('Dry run mode: No changes will be applied'));
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
chalk.blue(
|
||||
`Found ${workspaces.length} inactive workspace${
|
||||
workspaces.length > 1 ? 's' : ''
|
||||
}`,
|
||||
),
|
||||
);
|
||||
|
||||
await rawDataSource.initialize();
|
||||
|
||||
for (const workspace of workspaces) {
|
||||
this.logger.log(
|
||||
chalk.blue(
|
||||
`Processing workspace ${workspace.id} with name ${workspace.displayName}`,
|
||||
),
|
||||
);
|
||||
// Check if the workspace has a datasource
|
||||
const datasource = await this.datasourceRepository.findOne({
|
||||
where: { workspaceId: workspace.id },
|
||||
});
|
||||
|
||||
const schemaName = datasource?.schema;
|
||||
|
||||
const postgresSchemaExists = await this.typeORMService
|
||||
.getMainDataSource()
|
||||
.query(
|
||||
`SELECT COUNT(*) FROM information_schema.schemata WHERE schema_name = '${schemaName}'`,
|
||||
);
|
||||
|
||||
if (!schemaName || !postgresSchemaExists) {
|
||||
await this.deleteWorkspaceAndMarkAsSuspended(workspace, options);
|
||||
continue;
|
||||
}
|
||||
|
||||
const subscriptions = await this.subscriptionRepository.find({
|
||||
where: { workspaceId: workspace.id },
|
||||
});
|
||||
|
||||
if (subscriptions.length > 1) {
|
||||
this.logger.warn(chalk.red('More than one subscription found'));
|
||||
continue;
|
||||
}
|
||||
|
||||
const subscription = subscriptions[0];
|
||||
|
||||
if (!subscription) {
|
||||
this.logger.log(chalk.red('No subscription found'));
|
||||
await this.deleteWorkspaceAndMarkAsSuspendedAndDeleteAllData(
|
||||
workspace,
|
||||
schemaName,
|
||||
options,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const thirtyDaysAgo = new Date();
|
||||
|
||||
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
||||
|
||||
if (
|
||||
([
|
||||
SubscriptionStatus.Canceled,
|
||||
SubscriptionStatus.Incomplete,
|
||||
SubscriptionStatus.IncompleteExpired,
|
||||
SubscriptionStatus.Unpaid,
|
||||
SubscriptionStatus.Paused,
|
||||
].includes(subscription.status) &&
|
||||
subscription.canceledAt &&
|
||||
subscription.canceledAt < thirtyDaysAgo) ||
|
||||
(subscription.canceledAt === null &&
|
||||
subscription.updatedAt &&
|
||||
subscription.updatedAt < thirtyDaysAgo)
|
||||
) {
|
||||
await this.deleteWorkspaceAndMarkAsSuspendedAndDeleteAllData(
|
||||
workspace,
|
||||
schemaName,
|
||||
options,
|
||||
subscription,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
await this.markAsSuspended(workspace, options);
|
||||
}
|
||||
}
|
||||
|
||||
private async deleteWorkspaceAndMarkAsSuspended(
|
||||
workspace: Workspace,
|
||||
options: UpdateInactiveWorkspaceStatusOptions,
|
||||
) {
|
||||
this.logger.log(
|
||||
chalk.blue('(!!) Deleting workspace and marking as suspended'),
|
||||
);
|
||||
|
||||
if (!options.dryRun) {
|
||||
await this.workspaceRepository.update(workspace.id, {
|
||||
activationStatus: WorkspaceActivationStatus.SUSPENDED,
|
||||
});
|
||||
|
||||
await this.workspaceRepository.softRemove({ id: workspace.id });
|
||||
}
|
||||
}
|
||||
|
||||
private async deleteWorkspaceAndMarkAsSuspendedAndDeleteAllData(
|
||||
workspace: Workspace,
|
||||
schemaName: string,
|
||||
options: UpdateInactiveWorkspaceStatusOptions,
|
||||
billingSubscription?: BillingSubscription,
|
||||
) {
|
||||
this.logger.warn(
|
||||
chalk.blue(
|
||||
`(!!!) Deleting workspace and marking as suspended and deleting all data for workspace updated at ${workspace.updatedAt} with subscription status ${billingSubscription?.status} and subscription updatedAt ${billingSubscription?.updatedAt} and canceledAt ${billingSubscription?.canceledAt}`,
|
||||
),
|
||||
);
|
||||
|
||||
if (!options.dryRun) {
|
||||
await this.workspaceRepository.update(workspace.id, {
|
||||
activationStatus: WorkspaceActivationStatus.SUSPENDED,
|
||||
});
|
||||
|
||||
await this.workspaceRepository.softRemove({ id: workspace.id });
|
||||
|
||||
await this.datasourceRepository.delete({ workspaceId: workspace.id });
|
||||
await this.workspaceMigrationRepository.delete({
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
|
||||
await this.featureFlagRepository.delete({ workspaceId: workspace.id });
|
||||
await this.keyValuePairRepository.delete({ workspaceId: workspace.id });
|
||||
|
||||
const userWorkspaces = await this.userWorkspaceRepository.find({
|
||||
where: { workspaceId: workspace.id },
|
||||
});
|
||||
|
||||
for (const userWorkspace of userWorkspaces) {
|
||||
await this.userWorkspaceRepository.delete({ id: userWorkspace.id });
|
||||
|
||||
const remainingUserWorkspaces =
|
||||
await this.userWorkspaceRepository.count({
|
||||
where: { userId: userWorkspace.userId },
|
||||
});
|
||||
|
||||
if (remainingUserWorkspaces === 0) {
|
||||
await this.userRepository.softRemove({ id: userWorkspace.userId });
|
||||
}
|
||||
}
|
||||
|
||||
await this.typeORMService
|
||||
.getMainDataSource()
|
||||
.query(`DROP SCHEMA IF EXISTS ${schemaName} CASCADE`);
|
||||
}
|
||||
}
|
||||
|
||||
private async markAsSuspended(
|
||||
workspace: Workspace,
|
||||
options: UpdateInactiveWorkspaceStatusOptions,
|
||||
) {
|
||||
this.logger.log(chalk.blue('(!) Marking as suspended'));
|
||||
if (!options.dryRun) {
|
||||
await this.workspaceRepository.update(workspace.id, {
|
||||
activationStatus: WorkspaceActivationStatus.SUSPENDED,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
-37
@@ -1,37 +0,0 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { ActiveWorkspacesCommandRunner } from 'src/database/commands/active-workspaces.command';
|
||||
import { BaseCommandOptions } from 'src/database/commands/base.command';
|
||||
import { MigrateAggregateOperationOptionsCommand } from 'src/database/commands/upgrade-version/0-40/0-40-migrate-aggregate-operations-options.command';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade-0.40',
|
||||
description: 'Upgrade to 0.40',
|
||||
})
|
||||
export class UpgradeTo0_40Command extends ActiveWorkspacesCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(Workspace, 'core')
|
||||
protected readonly workspaceRepository: Repository<Workspace>,
|
||||
private readonly migrateAggregateOperationOptionsCommand: MigrateAggregateOperationOptionsCommand,
|
||||
) {
|
||||
super(workspaceRepository);
|
||||
}
|
||||
|
||||
async executeActiveWorkspacesCommand(
|
||||
passedParam: string[],
|
||||
options: BaseCommandOptions,
|
||||
workspaceIds: string[],
|
||||
): Promise<void> {
|
||||
this.logger.log('Running command to upgrade to 0.40');
|
||||
|
||||
await this.migrateAggregateOperationOptionsCommand.executeActiveWorkspacesCommand(
|
||||
passedParam,
|
||||
options,
|
||||
workspaceIds,
|
||||
);
|
||||
}
|
||||
}
|
||||
-55
@@ -1,55 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { MigrateAggregateOperationOptionsCommand } from 'src/database/commands/upgrade-version/0-40/0-40-migrate-aggregate-operations-options.command';
|
||||
import { UpdateInactiveWorkspaceStatusCommand } from 'src/database/commands/upgrade-version/0-40/0-40-update-inactive-workspace-status.command';
|
||||
import { UpgradeTo0_40Command } from 'src/database/commands/upgrade-version/0-40/0-40-upgrade-version.command';
|
||||
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
|
||||
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
|
||||
import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||
import { KeyValuePair } from 'src/engine/core-modules/key-value-pair/key-value-pair.entity';
|
||||
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module';
|
||||
import { WorkspaceMigrationEntity } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
|
||||
import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.module';
|
||||
import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature(
|
||||
[
|
||||
Workspace,
|
||||
BillingSubscription,
|
||||
FeatureFlag,
|
||||
KeyValuePair,
|
||||
User,
|
||||
UserWorkspace,
|
||||
],
|
||||
'core',
|
||||
),
|
||||
TypeOrmModule.forFeature(
|
||||
[
|
||||
ObjectMetadataEntity,
|
||||
FieldMetadataEntity,
|
||||
DataSourceEntity,
|
||||
WorkspaceMigrationEntity,
|
||||
],
|
||||
'metadata',
|
||||
),
|
||||
WorkspaceMigrationRunnerModule,
|
||||
WorkspaceMigrationModule,
|
||||
WorkspaceMetadataVersionModule,
|
||||
TypeORMModule,
|
||||
],
|
||||
providers: [
|
||||
UpgradeTo0_40Command,
|
||||
MigrateAggregateOperationOptionsCommand,
|
||||
UpdateInactiveWorkspaceStatusCommand,
|
||||
],
|
||||
})
|
||||
export class UpgradeTo0_40CommandModule {}
|
||||
-195
@@ -1,195 +0,0 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import chalk from 'chalk';
|
||||
import { Command } from 'nest-commander';
|
||||
import { FieldMetadataType } from 'twenty-shared';
|
||||
import { In, Repository, TableColumn } from 'typeorm';
|
||||
|
||||
import {
|
||||
ActiveWorkspacesCommandOptions,
|
||||
ActiveWorkspacesCommandRunner,
|
||||
} from 'src/database/commands/active-workspaces.command';
|
||||
import { CommandLogger } from 'src/database/commands/logger';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { FieldActorSource } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { computeTableName } from 'src/engine/utils/compute-table-name.util';
|
||||
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade-0.41:add-context-to-actor-composite-type',
|
||||
description: 'Add context to actor composite type.',
|
||||
})
|
||||
export class AddContextToActorCompositeTypeCommand extends ActiveWorkspacesCommandRunner {
|
||||
protected readonly logger;
|
||||
|
||||
constructor(
|
||||
@InjectRepository(Workspace, 'core')
|
||||
protected readonly workspaceRepository: Repository<Workspace>,
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
@InjectRepository(FieldMetadataEntity, 'metadata')
|
||||
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
|
||||
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
|
||||
private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService,
|
||||
) {
|
||||
super(workspaceRepository);
|
||||
this.logger = new CommandLogger({
|
||||
constructorName: this.constructor.name,
|
||||
verbose: false,
|
||||
});
|
||||
this.logger.setVerbose(false);
|
||||
}
|
||||
|
||||
async executeActiveWorkspacesCommand(
|
||||
_passedParam: string[],
|
||||
options: ActiveWorkspacesCommandOptions,
|
||||
workspaceIds: string[],
|
||||
): Promise<void> {
|
||||
this.logger.log(`Running add-context-to-actor-composite-type command`);
|
||||
|
||||
if (options?.dryRun) {
|
||||
this.logger.log(chalk.yellow('Dry run mode: No changes will be applied'));
|
||||
}
|
||||
|
||||
for (const workspaceId of workspaceIds) {
|
||||
try {
|
||||
await this.execute(workspaceId, options?.dryRun);
|
||||
this.logger.verbose(`Added for workspace: ${workspaceId}`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error for workspace: ${workspaceId}`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async execute(workspaceId: string, dryRun = false): Promise<void> {
|
||||
this.logger.verbose(`Adding for workspace: ${workspaceId}`);
|
||||
const actorFields = await this.fieldMetadataRepository.find({
|
||||
where: {
|
||||
type: FieldMetadataType.ACTOR,
|
||||
workspaceId,
|
||||
},
|
||||
relations: ['object'],
|
||||
});
|
||||
|
||||
// Filter and update fields with EMAIL or CALENDAR source
|
||||
for (const field of actorFields) {
|
||||
if (!field || !field.object) {
|
||||
this.logger.verbose(
|
||||
'field.objectMetadata is null',
|
||||
workspaceId,
|
||||
field.id,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
await this.addContextColumn(
|
||||
field,
|
||||
`${field.name}Context`,
|
||||
workspaceId,
|
||||
dryRun,
|
||||
);
|
||||
|
||||
const fieldRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||
workspaceId,
|
||||
field.object.nameSingular,
|
||||
);
|
||||
|
||||
if (!dryRun) {
|
||||
const rowsToUpdate = await fieldRepository.update(
|
||||
{
|
||||
[field.name + 'Source']: In([
|
||||
FieldActorSource.EMAIL,
|
||||
FieldActorSource.CALENDAR,
|
||||
]),
|
||||
[field.name + 'Context']: {},
|
||||
},
|
||||
{
|
||||
[field.name + 'Context']: {
|
||||
provider: 'google',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
this.logger.verbose(
|
||||
`updated ${rowsToUpdate ? rowsToUpdate.affected : 0} rows`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!dryRun) {
|
||||
await this.workspaceMetadataVersionService.incrementMetadataVersion(
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
await this.twentyORMGlobalManager.destroyDataSourceForWorkspace(
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
private async addContextColumn(
|
||||
field: FieldMetadataEntity,
|
||||
newColumnName: string,
|
||||
workspaceId: string,
|
||||
dryRun = false,
|
||||
): Promise<void> {
|
||||
const workspaceDataSource =
|
||||
await this.workspaceDataSourceService.connectToWorkspaceDataSource(
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
if (!workspaceDataSource) {
|
||||
this.logger.verbose('No workspace data source found');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const queryRunner = workspaceDataSource?.createQueryRunner();
|
||||
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
const schemaName =
|
||||
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
||||
|
||||
try {
|
||||
const hasColumn = await queryRunner.hasColumn(
|
||||
`${schemaName}.${computeTableName(
|
||||
field.object.nameSingular,
|
||||
field?.object?.isCustom,
|
||||
)}`,
|
||||
newColumnName,
|
||||
);
|
||||
|
||||
if (hasColumn) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!dryRun) {
|
||||
await queryRunner.addColumn(
|
||||
`${schemaName}.${computeTableName(
|
||||
field.object.nameSingular,
|
||||
field?.object?.isCustom,
|
||||
)}`,
|
||||
new TableColumn({
|
||||
name: newColumnName,
|
||||
type: 'jsonb',
|
||||
default: `'{}'::"jsonb"`,
|
||||
isNullable: true,
|
||||
}),
|
||||
);
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
}
|
||||
} catch (error) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
|
||||
throw error;
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
-140
@@ -1,140 +0,0 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import chalk from 'chalk';
|
||||
import { Command } from 'nest-commander';
|
||||
import { FieldMetadataType } from 'twenty-shared';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
||||
|
||||
import {
|
||||
ActiveWorkspacesCommandOptions,
|
||||
ActiveWorkspacesCommandRunner,
|
||||
} from 'src/database/commands/active-workspaces.command';
|
||||
import { isCommandLogger } from 'src/database/commands/logger';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import {
|
||||
deduceRelationDirection,
|
||||
RelationDirection,
|
||||
} from 'src/engine/utils/deduce-relation-direction.util';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade-0.41:migrate-relations-to-field-metadata',
|
||||
description: 'Migrate relations to field metadata',
|
||||
})
|
||||
export class MigrateRelationsToFieldMetadataCommand extends ActiveWorkspacesCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(Workspace, 'core')
|
||||
protected readonly workspaceRepository: Repository<Workspace>,
|
||||
@InjectRepository(FieldMetadataEntity, 'metadata')
|
||||
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
|
||||
) {
|
||||
super(workspaceRepository);
|
||||
}
|
||||
|
||||
async executeActiveWorkspacesCommand(
|
||||
_passedParam: string[],
|
||||
options: ActiveWorkspacesCommandOptions,
|
||||
workspaceIds: string[],
|
||||
): Promise<void> {
|
||||
this.logger.log('Running command to create many to one relations');
|
||||
|
||||
if (isCommandLogger(this.logger)) {
|
||||
this.logger.setVerbose(options.verbose ?? false);
|
||||
}
|
||||
|
||||
try {
|
||||
for (const [index, workspaceId] of workspaceIds.entries()) {
|
||||
await this.processWorkspace(workspaceId, index, workspaceIds.length);
|
||||
}
|
||||
|
||||
this.logger.log(chalk.green('Command completed!'));
|
||||
} catch (error) {
|
||||
this.logger.log(chalk.red('Error in workspace'));
|
||||
}
|
||||
}
|
||||
|
||||
private async processWorkspace(
|
||||
workspaceId: string,
|
||||
index: number,
|
||||
total: number,
|
||||
): Promise<void> {
|
||||
try {
|
||||
this.logger.log(
|
||||
`Running command for workspace ${workspaceId} ${index + 1}/${total}`,
|
||||
);
|
||||
|
||||
const fieldMetadataCollection = (await this.fieldMetadataRepository.find({
|
||||
where: { workspaceId, type: FieldMetadataType.RELATION },
|
||||
relations: ['fromRelationMetadata', 'toRelationMetadata'],
|
||||
})) as unknown as FieldMetadataEntity<FieldMetadataType.RELATION>[];
|
||||
|
||||
if (!fieldMetadataCollection.length) {
|
||||
this.logger.log(
|
||||
chalk.yellow(
|
||||
`No relation field metadata found for workspace ${workspaceId}.`,
|
||||
),
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const fieldMetadataToUpdateCollection = fieldMetadataCollection.map(
|
||||
(fieldMetadata) => this.mapFieldMetadata(fieldMetadata),
|
||||
);
|
||||
|
||||
if (fieldMetadataToUpdateCollection.length > 0) {
|
||||
await this.fieldMetadataRepository.save(
|
||||
fieldMetadataToUpdateCollection,
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
chalk.green(`Command completed for workspace ${workspaceId}.`),
|
||||
);
|
||||
} catch {
|
||||
this.logger.log(chalk.red(`Error in workspace ${workspaceId}.`));
|
||||
}
|
||||
}
|
||||
|
||||
private mapFieldMetadata(
|
||||
fieldMetadata: FieldMetadataEntity<FieldMetadataType.RELATION>,
|
||||
): FieldMetadataEntity<FieldMetadataType.RELATION> {
|
||||
const relationMetadata =
|
||||
fieldMetadata.fromRelationMetadata ?? fieldMetadata.toRelationMetadata;
|
||||
|
||||
const relationDirection = deduceRelationDirection(
|
||||
fieldMetadata,
|
||||
relationMetadata,
|
||||
);
|
||||
let relationType = relationMetadata.relationType as unknown as RelationType;
|
||||
|
||||
if (
|
||||
relationDirection === RelationDirection.TO &&
|
||||
relationType === RelationType.ONE_TO_MANY
|
||||
) {
|
||||
relationType = RelationType.MANY_TO_ONE;
|
||||
}
|
||||
|
||||
const relationTargetFieldMetadataId =
|
||||
relationDirection === RelationDirection.FROM
|
||||
? relationMetadata.toFieldMetadataId
|
||||
: relationMetadata.fromFieldMetadataId;
|
||||
|
||||
const relationTargetObjectMetadataId =
|
||||
relationDirection === RelationDirection.FROM
|
||||
? relationMetadata.toObjectMetadataId
|
||||
: relationMetadata.fromObjectMetadataId;
|
||||
|
||||
return {
|
||||
...fieldMetadata,
|
||||
settings: {
|
||||
relationType,
|
||||
onDelete: relationMetadata.onDeleteAction,
|
||||
},
|
||||
relationTargetFieldMetadataId,
|
||||
relationTargetObjectMetadataId,
|
||||
};
|
||||
}
|
||||
}
|
||||
-98
@@ -1,98 +0,0 @@
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
ActiveWorkspacesCommandOptions,
|
||||
ActiveWorkspacesCommandRunner,
|
||||
} from 'src/database/commands/active-workspaces.command';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade-0.41:remove-duplicate-mcmas',
|
||||
description: 'Remove duplicate mcmas.',
|
||||
})
|
||||
export class RemoveDuplicateMcmasCommand extends ActiveWorkspacesCommandRunner {
|
||||
protected readonly logger: Logger;
|
||||
|
||||
constructor(
|
||||
@InjectRepository(Workspace, 'core')
|
||||
protected readonly workspaceRepository: Repository<Workspace>,
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
) {
|
||||
super(workspaceRepository);
|
||||
this.logger = new Logger(this.constructor.name);
|
||||
}
|
||||
|
||||
async executeActiveWorkspacesCommand(
|
||||
_passedParam: string[],
|
||||
_options: ActiveWorkspacesCommandOptions,
|
||||
workspaceIds: string[],
|
||||
): Promise<void> {
|
||||
const { dryRun } = _options;
|
||||
|
||||
for (const workspaceId of workspaceIds) {
|
||||
try {
|
||||
await this.execute(workspaceId, dryRun);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Error removing duplicate mcmas for workspace ${workspaceId}: ${error}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async execute(workspaceId: string, dryRun = false): Promise<void> {
|
||||
this.logger.log(`Removing duplicate mcmas for workspace: ${workspaceId}`);
|
||||
|
||||
const repository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||
workspaceId,
|
||||
'messageChannelMessageAssociation',
|
||||
);
|
||||
|
||||
const queryBuilder = repository.createQueryBuilder(
|
||||
'messageChannelMessageAssociation',
|
||||
);
|
||||
|
||||
const duplicateMcmas = await queryBuilder
|
||||
.select(`"messageChannelId"`)
|
||||
.addSelect(`"messageId"`)
|
||||
.where(`"deletedAt" IS NULL`)
|
||||
.groupBy(`"messageId"`)
|
||||
.addGroupBy(`"messageChannelId"`)
|
||||
.having(`COUNT("messageChannelId") > 1`)
|
||||
.getRawMany();
|
||||
|
||||
this.logger.log(`Found ${duplicateMcmas.length} duplicate mcmas`);
|
||||
|
||||
for (const duplicateMca of duplicateMcmas) {
|
||||
const mcmas = await repository.find({
|
||||
where: {
|
||||
messageId: duplicateMca.messageId,
|
||||
messageChannelId: duplicateMca.messageChannelId,
|
||||
},
|
||||
});
|
||||
|
||||
this.logger.log(
|
||||
`Found ${mcmas.length} mcmas for message ${duplicateMca.messageId} and message channel ${duplicateMca.messageChannelId}`,
|
||||
);
|
||||
|
||||
const mcaIdsToDelete = mcmas.slice(1).map((mca) => mca.id);
|
||||
|
||||
if (mcaIdsToDelete.length > 0) {
|
||||
this.logger.log(`Deleting ${mcaIdsToDelete.length} mcas`);
|
||||
if (!dryRun) {
|
||||
await repository.delete(mcaIdsToDelete);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await this.twentyORMGlobalManager.destroyDataSourceForWorkspace(
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
}
|
||||
-207
@@ -1,207 +0,0 @@
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { EntityManager, IsNull, Not, Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
ActiveWorkspacesCommandOptions,
|
||||
ActiveWorkspacesCommandRunner,
|
||||
} from 'src/database/commands/active-workspaces.command';
|
||||
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { createWorkspaceViews } from 'src/engine/workspace-manager/standard-objects-prefill-data/create-workspace-views';
|
||||
import { workflowRunsAllView } from 'src/engine/workspace-manager/standard-objects-prefill-data/views/workflow-runs-all.view';
|
||||
import { workflowVersionsAllView } from 'src/engine/workspace-manager/standard-objects-prefill-data/views/workflow-versions-all.view';
|
||||
import { workflowsAllView } from 'src/engine/workspace-manager/standard-objects-prefill-data/views/workflows-all.view';
|
||||
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade-0.41:workflow-seed-views',
|
||||
description: 'Seed workflow views for workspace.',
|
||||
})
|
||||
export class SeedWorkflowViewsCommand extends ActiveWorkspacesCommandRunner {
|
||||
protected readonly logger: Logger;
|
||||
|
||||
constructor(
|
||||
@InjectRepository(Workspace, 'core')
|
||||
protected readonly workspaceRepository: Repository<Workspace>,
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
private readonly dataSourceService: DataSourceService,
|
||||
private readonly typeORMService: TypeORMService,
|
||||
private readonly objectMetadataService: ObjectMetadataService,
|
||||
) {
|
||||
super(workspaceRepository);
|
||||
this.logger = new Logger(this.constructor.name);
|
||||
}
|
||||
|
||||
async executeActiveWorkspacesCommand(
|
||||
_passedParam: string[],
|
||||
_options: ActiveWorkspacesCommandOptions,
|
||||
_workspaceIds: string[],
|
||||
): Promise<void> {
|
||||
const { dryRun } = _options;
|
||||
|
||||
for (const workspaceId of _workspaceIds) {
|
||||
await this.execute(workspaceId, dryRun);
|
||||
}
|
||||
}
|
||||
|
||||
private async execute(workspaceId: string, dryRun = false): Promise<void> {
|
||||
this.logger.log(`Seeding workflow views for workspace: ${workspaceId}`);
|
||||
|
||||
const workflowObjectMetadata =
|
||||
await this.objectMetadataService.findOneWithinWorkspace(workspaceId, {
|
||||
where: {
|
||||
standardId: STANDARD_OBJECT_IDS.workflow,
|
||||
},
|
||||
});
|
||||
|
||||
if (!workflowObjectMetadata) {
|
||||
this.logger.error('Workflow object metadata not found');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await this.seedWorkflowViews(
|
||||
workspaceId,
|
||||
workflowObjectMetadata.id,
|
||||
dryRun,
|
||||
);
|
||||
|
||||
await this.seedWorkspaceFavorite(
|
||||
workspaceId,
|
||||
workflowObjectMetadata.id,
|
||||
dryRun,
|
||||
);
|
||||
|
||||
await this.twentyORMGlobalManager.destroyDataSourceForWorkspace(
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
private async seedWorkflowViews(
|
||||
workspaceId: string,
|
||||
workflowObjectMetadataId: string,
|
||||
dryRun = false,
|
||||
) {
|
||||
const viewRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||
workspaceId,
|
||||
'view',
|
||||
);
|
||||
|
||||
const existingWorkflowView = await viewRepository.findOne({
|
||||
where: {
|
||||
objectMetadataId: workflowObjectMetadataId,
|
||||
},
|
||||
});
|
||||
|
||||
if (existingWorkflowView) {
|
||||
this.logger.log(`View already exists: ${existingWorkflowView.id}`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (dryRun) {
|
||||
this.logger.log(`Dry run: not creating view`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const { objectMetadataStandardIdToIdMap } =
|
||||
await this.objectMetadataService.getObjectMetadataStandardIdToIdMap(
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
const dataSourceMetadata =
|
||||
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
const workspaceDataSource =
|
||||
await this.typeORMService.connectToDataSource(dataSourceMetadata);
|
||||
|
||||
if (!workspaceDataSource) {
|
||||
this.logger.error('Could not connect to workspace data source');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const viewDefinitions = [
|
||||
workflowsAllView(objectMetadataStandardIdToIdMap),
|
||||
workflowVersionsAllView(objectMetadataStandardIdToIdMap),
|
||||
workflowRunsAllView(objectMetadataStandardIdToIdMap),
|
||||
];
|
||||
|
||||
await workspaceDataSource.transaction(
|
||||
async (entityManager: EntityManager) => {
|
||||
return createWorkspaceViews(
|
||||
entityManager,
|
||||
dataSourceMetadata.schema,
|
||||
viewDefinitions,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private async seedWorkspaceFavorite(
|
||||
workspaceId: string,
|
||||
workflowObjectMetadataId: string,
|
||||
dryRun = false,
|
||||
) {
|
||||
const viewRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||
workspaceId,
|
||||
'view',
|
||||
);
|
||||
|
||||
const workflowView = await viewRepository.findOne({
|
||||
where: {
|
||||
objectMetadataId: workflowObjectMetadataId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!workflowView) {
|
||||
this.logger.error('Workflow view not found');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (dryRun) {
|
||||
this.logger.log(`Dry run: not creating favorite`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const favoriteRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||
workspaceId,
|
||||
'favorite',
|
||||
);
|
||||
|
||||
const existingFavorites = await favoriteRepository.find({
|
||||
where: {
|
||||
viewId: Not(IsNull()),
|
||||
},
|
||||
});
|
||||
|
||||
const workflowFavorite = existingFavorites.find(
|
||||
(favorite) => favorite.viewId === workflowView.id,
|
||||
);
|
||||
|
||||
if (workflowFavorite) {
|
||||
this.logger.log(`Favorite already exists: ${workflowFavorite.id}`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await favoriteRepository.insert({
|
||||
viewId: workflowView.id,
|
||||
position: existingFavorites.length,
|
||||
});
|
||||
}
|
||||
}
|
||||
-72
@@ -1,72 +0,0 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { ActiveWorkspacesCommandRunner } from 'src/database/commands/active-workspaces.command';
|
||||
import { BaseCommandOptions } from 'src/database/commands/base.command';
|
||||
import { AddContextToActorCompositeTypeCommand } from 'src/database/commands/upgrade-version/0-41/0-41-add-context-to-actor-composite-type';
|
||||
import { MigrateRelationsToFieldMetadataCommand } from 'src/database/commands/upgrade-version/0-41/0-41-migrate-relations-to-field-metadata.command';
|
||||
import { RemoveDuplicateMcmasCommand } from 'src/database/commands/upgrade-version/0-41/0-41-remove-duplicate-mcmas';
|
||||
import { SeedWorkflowViewsCommand } from 'src/database/commands/upgrade-version/0-41/0-41-seed-workflow-views.command';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { SyncWorkspaceMetadataCommand } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/sync-workspace-metadata.command';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade-0.41',
|
||||
description: 'Upgrade to 0.41',
|
||||
})
|
||||
export class UpgradeTo0_41Command extends ActiveWorkspacesCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(Workspace, 'core')
|
||||
protected readonly workspaceRepository: Repository<Workspace>,
|
||||
private readonly seedWorkflowViewsCommand: SeedWorkflowViewsCommand,
|
||||
private readonly syncWorkspaceMetadataCommand: SyncWorkspaceMetadataCommand,
|
||||
private readonly migrateRelationsToFieldMetadata: MigrateRelationsToFieldMetadataCommand,
|
||||
private readonly addContextToActorCompositeType: AddContextToActorCompositeTypeCommand,
|
||||
private readonly removeDuplicateMcmasCommand: RemoveDuplicateMcmasCommand,
|
||||
) {
|
||||
super(workspaceRepository);
|
||||
}
|
||||
|
||||
async executeActiveWorkspacesCommand(
|
||||
passedParam: string[],
|
||||
options: BaseCommandOptions,
|
||||
workspaceIds: string[],
|
||||
): Promise<void> {
|
||||
this.logger.log('Running command to upgrade to 0.41');
|
||||
|
||||
await this.removeDuplicateMcmasCommand.executeActiveWorkspacesCommand(
|
||||
passedParam,
|
||||
options,
|
||||
workspaceIds,
|
||||
);
|
||||
|
||||
await this.addContextToActorCompositeType.executeActiveWorkspacesCommand(
|
||||
passedParam,
|
||||
options,
|
||||
workspaceIds,
|
||||
);
|
||||
|
||||
await this.syncWorkspaceMetadataCommand.executeActiveWorkspacesCommand(
|
||||
passedParam,
|
||||
{
|
||||
...options,
|
||||
force: true,
|
||||
},
|
||||
workspaceIds,
|
||||
);
|
||||
|
||||
await this.seedWorkflowViewsCommand.executeActiveWorkspacesCommand(
|
||||
passedParam,
|
||||
options,
|
||||
workspaceIds,
|
||||
);
|
||||
|
||||
await this.migrateRelationsToFieldMetadata.executeActiveWorkspacesCommand(
|
||||
passedParam,
|
||||
options,
|
||||
workspaceIds,
|
||||
);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user