Compare commits

...

14 Commits

Author SHA1 Message Date
Weiko 95815a417b Bump 0.33.4 2024-11-26 15:45:14 +01:00
Raphaël Bosi bc5c5f3fb9 Gmail error handling fixes (#8732)
Gmail error handling fixes
2024-11-26 15:33:41 +01:00
Weiko 7b7dab66ed Fix labelIdentifierFieldMetadata creation for custom objects (#8729) 2024-11-26 15:33:35 +01:00
Weiko d3ffbaba30 Set missing labelIdentifier to custom objects (#8750)
## Context
Following https://github.com/twentyhq/twenty/pull/8729

This command backfills missing labelIdentifier for custom objects
2024-11-26 15:33:29 +01:00
Marie 59b7f1ba68 Fix custom object renaming (#8746)
Currently when renaming an object, we execute
```
await this.fieldMetadataRepository
                    .findOneByOrFail({
                      name: existingObjectMetadata.nameSingular,
                      label: existingObjectMetadata.labelSingular,
                      objectMetadataId: relatedObject.id,
                      workspaceId: workspaceId,
                    })
```
to find the standard relation fields. 
This would throw an error if the label solely was update beforehand
without updating the name too: in that case we will not have migrated
the label of the standard relation fields (which is maybe a mistake?
@Weiko wdyt?).
Let's remove it.
2024-11-26 15:33:22 +01:00
Marie fd3e80aaa1 Fix update of custom object icon (#8730)
Icon update was not triggering a save due to missing onBlur prop drill
2024-11-26 15:33:14 +01:00
ad-elias 212d3dc59d Fix: open filter from column (#8747)
Column filter button (image below) was broken for all filter types, this
PR fixes it.

<img width="1053" alt="broken-filter-button"
src="https://github.com/user-attachments/assets/febd10a8-f360-4245-ba06-ef847c79fde1">

---------

Co-authored-by: Félix Malfait <felix@twenty.com>
2024-11-26 15:33:07 +01:00
Weiko 18a13d2175 Fix mutations with camelCase table names (#8740)
## Context
Some mutations are not working properly, workspaceMember soft deletion
for example. workspaceMember being a camelCase table name, it's probably
not propagated properly to pgql (which needs double quote for the table
name to keep it as camelCase)

I didn't have time to dig too much but if the `where` is before
`softDelete`, the query is `WHERE workspaceMember.id = $1` while if it's
after, the query becomes `WHERE id = $1`.
Probably due to the fact that once you call delete/softDelete/update,
the standard builder (SelectQueryBuilder) becomes a
DeleteQueryBuilder/etc... and filters are not handled the same way.
2024-11-26 15:33:01 +01:00
Weiko 726bb56bb6 add delete view fields without views command (#8728)
## Context
We recently added a command to ensure uniqueness on the viewId column in
the viewField table. This created some issues for some old workspaces
that had viewFields with an empty viewId.
This command should get rid of those and set the column as non-nullable.
Also updating the onDelete action accordingly and set one missing for
FavoriteFolder
2024-11-26 15:32:55 +01:00
Félix Malfait de46f5440a Fix Error field type rich text (#8739)
fix #8445

It seems linked to commandBar search where we filter tasks/notes by body
with ilike.
2024-11-26 15:32:43 +01:00
nitin c266ce4859 Email invite design improvements (#8681)
closes #7140 

![image](https://github.com/user-attachments/assets/d3a31a49-8b37-4456-98e3-a16fbccb3786)
2024-11-26 15:32:32 +01:00
Weiko 2e8fd6cd99 Bump 0.33.3 2024-11-22 18:32:13 +01:00
Weiko 9f55ce1889 Fix mutations with custom objects (#8688) 2024-11-22 18:18:33 +01:00
Weiko c94b79a8a7 Bump 0.33.2 2024-11-22 15:34:38 +01:00
39 changed files with 474 additions and 119 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "twenty-emails",
"version": "0.34.0-canary",
"version": "0.33.4",
"description": "",
"author": "",
"private": true,
@@ -22,6 +22,10 @@ const grayScale = {
gray0: '#ffffff',
};
const colors = {
blue40: '#5e90f2',
};
export const emailTheme = {
font: {
colors: {
@@ -29,6 +33,7 @@ export const emailTheme = {
primary: grayScale.gray50,
tertiary: grayScale.gray35,
inverted: grayScale.gray0,
blue: colors.blue40,
},
family: 'Trebuchet MS', // Google Inter not working, we need to use a web safe font, see https://templates.mailchimp.com/design/typography/
weight: {
@@ -1,25 +1,15 @@
import { Column, Container, Row } from '@react-email/components';
import React, { PropsWithChildren } from 'react';
import { Container } from '@react-email/components';
import { emailTheme } from 'src/common-style';
type HighlightedContainerProps = PropsWithChildren;
const highlightedContainerStyle = {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: emailTheme.background.colors.highlight,
border: `1px solid ${emailTheme.border.color.highlighted}`,
borderRadius: emailTheme.border.radius.md,
padding: '24px 48px',
gap: '24px',
};
const divStyle = {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
} as React.CSSProperties;
export const HighlightedContainer = ({
@@ -27,7 +17,11 @@ export const HighlightedContainer = ({
}: HighlightedContainerProps) => {
return (
<Container style={highlightedContainerStyle}>
<div style={divStyle}>{children}</div>
{React.Children.map(children, (child) => (
<Row>
<Column align="center">{child}</Column>
</Row>
))}
</Container>
);
};
@@ -1,5 +1,5 @@
import React, { ReactNode } from 'react';
import { Text } from '@react-email/text';
import { ReactNode } from 'react';
import { emailTheme } from 'src/common-style';
@@ -12,19 +12,11 @@ const highlightedStyle = {
color: emailTheme.font.colors.highlighted,
};
const divStyle = {
display: 'flex',
};
type HighlightedTextProps = {
value: ReactNode;
centered?: boolean;
};
export const HighlightedText = ({ value }: HighlightedTextProps) => {
return (
<div style={divStyle}>
<Text style={highlightedStyle}>{value}</Text>
</div>
);
return <Text style={highlightedStyle}>{value}</Text>;
};
+10 -4
View File
@@ -1,21 +1,27 @@
import { ReactNode } from 'react';
import { Link as EmailLink } from '@react-email/components';
import { ReactNode } from 'react';
import { emailTheme } from 'src/common-style';
const linkStyle = {
color: emailTheme.font.colors.tertiary,
textDecoration: 'underline',
};
type LinkProps = {
value: ReactNode;
href: string;
color?: string;
};
export const Link = ({ value, href }: LinkProps) => {
export const Link = ({ value, href, color }: LinkProps) => {
return (
<EmailLink href={href} style={linkStyle}>
<EmailLink
href={href}
style={{
...linkStyle,
color: color ?? emailTheme.font.colors.tertiary,
}}
>
{value}
</EmailLink>
);
@@ -1,16 +1,14 @@
import { Column, Row } from '@react-email/components';
import { Link } from 'src/components/Link';
import { MainText } from 'src/components/MainText';
import { ShadowText } from 'src/components/ShadowText';
import { SubTitle } from 'src/components/SubTitle';
export const WhatIsTwenty = () => {
return (
<>
<SubTitle value="What is Twenty?" />
<MainText>
A software to help businesses manage their customer data and
It's a CRM, a software to help businesses manage their customer data and
relationships efficiently.
</MainText>
<Row>
@@ -1,4 +1,5 @@
import { Img } from '@react-email/components';
import { emailTheme } from 'src/common-style';
import { BaseEmail } from 'src/components/BaseEmail';
import { CallToAction } from 'src/components/CallToAction';
@@ -33,8 +34,12 @@ export const SendInviteLinkEmail = ({
<Title value="Join your team on Twenty" />
<MainText>
{capitalize(sender.firstName)} (
<Link href={sender.email} value={sender.email} />) has invited you to
join a workspace called <b>{workspace.name}</b>
<Link
href={sender.email}
value={sender.email}
color={emailTheme.font.colors.blue}
/>
) has invited you to join a workspace called <b>{workspace.name}</b>
<br />
</MainText>
<HighlightedContainer>
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "twenty-front",
"version": "0.34.0-canary",
"version": "0.33.4",
"private": true,
"type": "module",
"scripts": {
@@ -55,7 +55,11 @@ export const ObjectFilterDropdownRecordSelect = ({
const selectedFilter = useRecoilValue(selectedFilterState);
const objectNameSingular =
filterDefinitionUsedInDropdown?.relationObjectMetadataNameSingular ?? '';
filterDefinitionUsedInDropdown?.relationObjectMetadataNameSingular;
if (!isDefined(objectNameSingular)) {
throw new Error('objectNameSingular is not defined');
}
const { loading, filteredSelectedRecords, recordsToSelect, selectedRecords } =
useRecordsForSelect({
@@ -0,0 +1,22 @@
import { useRecoilCallback } from 'recoil';
import { filterDefinitionUsedInDropdownComponentState } from '../states/filterDefinitionUsedInDropdownComponentState';
import { FilterDefinition } from '../types/FilterDefinition';
import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState';
export const useSetFilterDefinitionUsedInDropdownInScope = () => {
const setFilterDefinitionUsedInDropdownInScope = useRecoilCallback(
({ set }) =>
(scopeId: string, filterDefinition: FilterDefinition | null) => {
const filterDefinitionUsedInDropdownState = extractComponentState(
filterDefinitionUsedInDropdownComponentState,
scopeId,
);
set(filterDefinitionUsedInDropdownState, filterDefinition);
},
[],
);
return {
setFilterDefinitionUsedInDropdownInScope,
};
};
@@ -177,6 +177,15 @@ export const isRecordMatchingFilter = ({
value: record[filterKey],
});
}
case FieldMetadataType.RichText: {
// TODO: Implement a better rich text filter once it becomes a composite field
// See this issue for more context: https://github.com/twentyhq/twenty/issues/7613#issuecomment-2408944585
// This should be tackled in Q4'24
return isMatchingStringFilter({
stringFilter: filterValue as StringFilter,
value: record[filterKey],
});
}
case FieldMetadataType.Select:
return isMatchingSelectFilter({
selectFilter: filterValue as SelectFilter,
@@ -3,12 +3,16 @@ import { v4 } from 'uuid';
import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions';
import { useSetFilterDefinitionUsedInDropdownInScope } from '@/object-record/object-filter-dropdown/hooks/useSetFilterDefinitionUsedInDropdownInScope';
import { Filter } from '@/object-record/object-filter-dropdown/types/Filter';
import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition';
import { getOperandsForFilterDefinition } from '@/object-record/object-filter-dropdown/utils/getOperandsForFilterType';
import { useDropdownV2 } from '@/ui/layout/dropdown/hooks/useDropdownV2';
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState';
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
import { useUpsertCombinedViewFilters } from '@/views/hooks/useUpsertCombinedViewFilters';
import { availableFilterDefinitionsComponentState } from '@/views/states/availableFilterDefinitionsComponentState';
import { useRecoilCallback } from 'recoil';
import { isDefined } from '~/utils/isDefined';
type UseHandleToggleColumnFilterProps = {
@@ -17,8 +21,8 @@ type UseHandleToggleColumnFilterProps = {
};
export const useHandleToggleColumnFilter = ({
viewBarId,
objectNameSingular,
viewBarId,
}: UseHandleToggleColumnFilterProps) => {
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular,
@@ -28,10 +32,29 @@ export const useHandleToggleColumnFilter = ({
useColumnDefinitionsFromFieldMetadata(objectMetadataItem);
const { upsertCombinedViewFilter } = useUpsertCombinedViewFilters(viewBarId);
const { openDropdown } = useDropdownV2();
const openDropdown = useRecoilCallback(({ set }) => {
return (dropdownId: string) => {
const dropdownOpenState = extractComponentState(
isDropdownOpenComponentState,
dropdownId,
);
set(dropdownOpenState, true);
};
}, []);
const availableFilterDefinitions = useRecoilComponentValueV2(
availableFilterDefinitionsComponentState,
);
const { currentViewWithCombinedFiltersAndSorts } = useGetCurrentView();
const { setFilterDefinitionUsedInDropdownInScope } =
useSetFilterDefinitionUsedInDropdownInScope();
const handleToggleColumnFilter = useCallback(
(fieldMetadataId: string) => {
async (fieldMetadataId: string) => {
const correspondingColumnDefinition = columnDefinitions.find(
(columnDefinition) =>
columnDefinition.fieldMetadataId === fieldMetadataId,
@@ -39,38 +62,54 @@ export const useHandleToggleColumnFilter = ({
if (!isDefined(correspondingColumnDefinition)) return;
const filterType = getFilterTypeFromFieldType(
correspondingColumnDefinition?.type,
);
const newFilterId = v4();
const filterDefinition = {
label: correspondingColumnDefinition.label,
iconName: correspondingColumnDefinition.iconName,
fieldMetadataId,
type: filterType,
} satisfies FilterDefinition;
const existingViewFilter =
currentViewWithCombinedFiltersAndSorts?.viewFilters.find(
(viewFilter) => viewFilter.fieldMetadataId === fieldMetadataId,
);
const availableOperandsForFilter =
getOperandsForFilterDefinition(filterDefinition);
if (!existingViewFilter) {
const filterDefinition = availableFilterDefinitions.find(
(fd) => fd.fieldMetadataId === fieldMetadataId,
);
const defaultOperand = availableOperandsForFilter[0];
if (!isDefined(filterDefinition)) {
throw new Error('Filter definition not found');
}
const newFilter: Filter = {
id: v4(),
fieldMetadataId,
operand: defaultOperand,
displayValue: '',
definition: filterDefinition,
value: '',
};
const availableOperandsForFilter =
getOperandsForFilterDefinition(filterDefinition);
upsertCombinedViewFilter(newFilter);
const defaultOperand = availableOperandsForFilter[0];
openDropdown(newFilter.id, {
scope: newFilter.id,
});
const newFilter: Filter = {
id: newFilterId,
fieldMetadataId,
operand: defaultOperand,
displayValue: '',
definition: filterDefinition,
value: '',
};
await upsertCombinedViewFilter(newFilter);
setFilterDefinitionUsedInDropdownInScope(
newFilter.id,
filterDefinition,
);
}
openDropdown(existingViewFilter?.id ?? newFilterId);
},
[columnDefinitions, upsertCombinedViewFilter, openDropdown],
[
openDropdown,
columnDefinitions,
upsertCombinedViewFilter,
setFilterDefinitionUsedInDropdownInScope,
currentViewWithCombinedFiltersAndSorts,
availableFilterDefinitions,
],
);
return handleToggleColumnFilter;
@@ -152,7 +152,10 @@ export const SettingsDataModelObjectAboutForm = ({
<IconPicker
disabled={disableEdition}
selectedIconKey={value}
onChange={({ iconKey }) => onChange(iconKey)}
onChange={({ iconKey }) => {
onChange(iconKey);
onBlur?.();
}}
/>
)}
/>
+2 -2
View File
@@ -2,8 +2,8 @@
import { isNonEmptyString } from '@sniptt/guards';
import react from '@vitejs/plugin-react-swc';
import wyw from '@wyw-in-js/vite';
import path from 'path';
import fs from 'fs';
import path from 'path';
import { defineConfig, loadEnv, searchForWorkspaceRoot } from 'vite';
import checker from 'vite-plugin-checker';
import svgr from 'vite-plugin-svgr';
@@ -133,7 +133,7 @@ export default defineConfig(({ command, mode }) => {
],
optimizeDeps: {
exclude: ['node_modules/.vite', 'node_modules/.cache'],
exclude: ['../../node_modules/.vite', '../../node_modules/.cache'],
},
build: {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "twenty-server",
"version": "0.34.0-canary",
"version": "0.33.4",
"description": "",
"author": "",
"private": true,
@@ -6,6 +6,7 @@ import { CommandRunner, Option } from 'nest-commander';
export type BaseCommandOptions = {
workspaceId?: string;
dryRun?: boolean;
verbose?: boolean;
};
export abstract class BaseCommandRunner extends CommandRunner {
@@ -25,6 +26,14 @@ export abstract class BaseCommandRunner extends CommandRunner {
return true;
}
@Option({
flags: '--verbose',
description: 'Verbose output',
})
parseVerbose() {
return true;
}
override async run(
passedParams: string[],
options: BaseCommandOptions,
@@ -0,0 +1,95 @@
import { InjectRepository } from '@nestjs/typeorm';
import chalk from 'chalk';
import { Command } from 'nest-commander';
import { IsNull, 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';
interface DeleteViewFieldsWithoutViewsCommandOptions
extends ActiveWorkspacesCommandOptions {}
@Command({
name: 'upgrade-0.33:delete-view-fields-without-views',
description: 'Delete ViewFields that do not have a View',
})
export class DeleteViewFieldsWithoutViewsCommand extends ActiveWorkspacesCommandRunner {
constructor(
@InjectRepository(Workspace, 'core')
protected readonly workspaceRepository: Repository<Workspace>,
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
) {
super(workspaceRepository);
}
async executeActiveWorkspacesCommand(
_passedParam: string[],
options: DeleteViewFieldsWithoutViewsCommandOptions,
workspaceIds: string[],
): Promise<void> {
this.logger.log(
'Running command to delete ViewFields that do not have a View',
);
for (const workspaceId of workspaceIds) {
this.logger.log(`Running command for workspace ${workspaceId}`);
try {
await this.deleteViewFieldsWithoutViewsForWorkspace(
workspaceId,
options,
);
} catch (error) {
this.logger.log(
chalk.red(
`Running command on workspace ${workspaceId} failed with error: ${error}, ${error.stack}`,
),
);
continue;
} finally {
this.logger.log(
chalk.green(`Finished running command for workspace ${workspaceId}.`),
);
}
}
this.logger.log(chalk.green(`Command completed!`));
}
private async deleteViewFieldsWithoutViewsForWorkspace(
workspaceId: string,
options: DeleteViewFieldsWithoutViewsCommandOptions,
): Promise<void> {
const viewFieldRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
workspaceId,
'viewField',
false,
);
const viewFieldsWithoutViews = await viewFieldRepository.find({
where: {
viewId: IsNull(),
},
});
const viewFieldIds = viewFieldsWithoutViews.map((vf) => vf.id);
if (!options.dryRun && viewFieldIds.length > 0) {
await viewFieldRepository.delete(viewFieldIds);
}
if (options.verbose) {
this.logger.log(
chalk.yellow(
`Deleted ${viewFieldsWithoutViews.length} ViewFields that do not have a View`,
),
);
}
}
}
@@ -17,7 +17,6 @@ interface EnforceUniqueConstraintsCommandOptions
company?: boolean;
viewField?: boolean;
viewSort?: boolean;
verbose?: boolean;
}
@Command({
@@ -42,14 +41,6 @@ export class EnforceUniqueConstraintsCommand extends ActiveWorkspacesCommandRunn
return true;
}
@Option({
flags: '--verbose',
description: 'Verbose output',
})
parseVerbose() {
return true;
}
@Option({
flags: '--company',
description: 'Enforce unique constraints on company domainName',
@@ -250,9 +241,16 @@ export class EnforceUniqueConstraintsCommand extends ActiveWorkspacesCommandRunn
.getRawMany();
for (const duplicate of duplicates) {
const { fieldMetadataId, viewId } = duplicate;
const {
viewField_fieldMetadataId: fieldMetadataId,
viewField_viewId: viewId,
} = duplicate;
const viewFields = await viewFieldRepository.find({
where: { fieldMetadataId, viewId, deletedAt: IsNull() },
where: {
fieldMetadataId,
viewId,
deletedAt: IsNull(),
},
order: { createdAt: 'DESC' },
});
@@ -292,9 +290,16 @@ export class EnforceUniqueConstraintsCommand extends ActiveWorkspacesCommandRunn
.getRawMany();
for (const duplicate of duplicates) {
const { fieldMetadataId, viewId } = duplicate;
const {
viewSort_fieldMetadataId: fieldMetadataId,
viewSort_viewId: viewId,
} = duplicate;
const viewSorts = await viewSortRepository.find({
where: { fieldMetadataId, viewId, deletedAt: IsNull() },
where: {
fieldMetadataId,
viewId,
deletedAt: IsNull(),
},
order: { createdAt: 'DESC' },
});
@@ -0,0 +1,108 @@
import { InjectRepository } from '@nestjs/typeorm';
import chalk from 'chalk';
import { Command } from 'nest-commander';
import { IsNull, Repository } from 'typeorm';
import {
ActiveWorkspacesCommandOptions,
ActiveWorkspacesCommandRunner,
} from 'src/database/commands/active-workspaces.command';
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 { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { CUSTOM_OBJECT_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
interface SetMissingLabelIdentifierToCustomObjectsCommandOptions
extends ActiveWorkspacesCommandOptions {}
@Command({
name: 'upgrade-0.33:set-missing-label-identifier-to-custom-objects',
description: 'Set missing labelIdentifier to custom objects',
})
export class SetMissingLabelIdentifierToCustomObjectsCommand 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 twentyORMGlobalManager: TwentyORMGlobalManager,
) {
super(workspaceRepository);
}
async executeActiveWorkspacesCommand(
_passedParam: string[],
options: SetMissingLabelIdentifierToCustomObjectsCommandOptions,
workspaceIds: string[],
): Promise<void> {
this.logger.log(
'Running command to set missing labelIdentifier to custom objects',
);
for (const workspaceId of workspaceIds) {
this.logger.log(`Running command for workspace ${workspaceId}`);
try {
await this.setMissingLabelIdentifierToCustomObjectsForWorkspace(
workspaceId,
options,
);
} catch (error) {
this.logger.log(
chalk.red(
`Running command on workspace ${workspaceId} failed with error: ${error}, ${error.stack}`,
),
);
continue;
} finally {
this.logger.log(
chalk.green(`Finished running command for workspace ${workspaceId}.`),
);
}
}
this.logger.log(chalk.green(`Command completed!`));
}
private async setMissingLabelIdentifierToCustomObjectsForWorkspace(
workspaceId: string,
options: SetMissingLabelIdentifierToCustomObjectsCommandOptions,
): Promise<void> {
const customObjects = await this.objectMetadataRepository.find({
where: {
workspaceId,
labelIdentifierFieldMetadataId: IsNull(),
isCustom: true,
},
});
for (const customObject of customObjects) {
const labelIdentifierFieldMetadata =
await this.fieldMetadataRepository.findOne({
where: {
workspaceId,
objectMetadataId: customObject.id,
standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.name,
},
});
if (labelIdentifierFieldMetadata && !options.dryRun) {
await this.objectMetadataRepository.update(customObject.id, {
labelIdentifierFieldMetadataId: labelIdentifierFieldMetadata.id,
});
}
if (options.verbose) {
this.logger.log(
chalk.yellow(
`Set labelIdentifierFieldMetadataId for custom object ${customObject.nameSingular}`,
),
);
}
}
}
}
@@ -4,7 +4,9 @@ import { Command } from 'nest-commander';
import { Repository } from 'typeorm';
import { ActiveWorkspacesCommandRunner } from 'src/database/commands/active-workspaces.command';
import { DeleteViewFieldsWithoutViewsCommand } from 'src/database/commands/upgrade-version/0-33/0-33-delete-view-fields-without-views.command';
import { EnforceUniqueConstraintsCommand } from 'src/database/commands/upgrade-version/0-33/0-33-enforce-unique-constraints.command';
import { SetMissingLabelIdentifierToCustomObjectsCommand } from 'src/database/commands/upgrade-version/0-33/0-33-set-missing-label-identifier-to-custom-objects.command';
import { UpdateRichTextSearchVectorCommand } from 'src/database/commands/upgrade-version/0-33/0-33-update-rich-text-search-vector-expression';
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';
@@ -23,7 +25,9 @@ export class UpgradeTo0_33Command extends ActiveWorkspacesCommandRunner {
protected readonly workspaceRepository: Repository<Workspace>,
private readonly updateRichTextSearchVectorCommand: UpdateRichTextSearchVectorCommand,
private readonly enforceUniqueConstraintsCommand: EnforceUniqueConstraintsCommand,
private readonly deleteViewFieldsWithoutViewsCommand: DeleteViewFieldsWithoutViewsCommand,
private readonly syncWorkspaceMetadataCommand: SyncWorkspaceMetadataCommand,
private readonly setMissingLabelIdentifierToCustomObjectsCommand: SetMissingLabelIdentifierToCustomObjectsCommand,
) {
super(workspaceRepository);
}
@@ -33,6 +37,11 @@ export class UpgradeTo0_33Command extends ActiveWorkspacesCommandRunner {
options: UpdateTo0_33CommandOptions,
workspaceIds: string[],
): Promise<void> {
await this.deleteViewFieldsWithoutViewsCommand.executeActiveWorkspacesCommand(
passedParam,
options,
workspaceIds,
);
await this.enforceUniqueConstraintsCommand.executeActiveWorkspacesCommand(
passedParam,
{
@@ -57,5 +66,10 @@ export class UpgradeTo0_33Command extends ActiveWorkspacesCommandRunner {
options,
workspaceIds,
);
await this.setMissingLabelIdentifierToCustomObjectsCommand.executeActiveWorkspacesCommand(
passedParam,
options,
workspaceIds,
);
}
}
@@ -1,7 +1,9 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { DeleteViewFieldsWithoutViewsCommand } from 'src/database/commands/upgrade-version/0-33/0-33-delete-view-fields-without-views.command';
import { EnforceUniqueConstraintsCommand } from 'src/database/commands/upgrade-version/0-33/0-33-enforce-unique-constraints.command';
import { SetMissingLabelIdentifierToCustomObjectsCommand } from 'src/database/commands/upgrade-version/0-33/0-33-set-missing-label-identifier-to-custom-objects.command';
import { UpdateRichTextSearchVectorCommand } from 'src/database/commands/upgrade-version/0-33/0-33-update-rich-text-search-vector-expression';
import { UpgradeTo0_33Command } from 'src/database/commands/upgrade-version/0-33/0-33-upgrade-version.command';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
@@ -26,6 +28,8 @@ import { WorkspaceSyncMetadataCommandsModule } from 'src/engine/workspace-manage
UpgradeTo0_33Command,
UpdateRichTextSearchVectorCommand,
EnforceUniqueConstraintsCommand,
DeleteViewFieldsWithoutViewsCommand,
SetMissingLabelIdentifierToCustomObjectsCommand,
],
})
export class UpgradeTo0_33CommandModule {}
@@ -14,6 +14,7 @@ import { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-que
import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util';
import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util';
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
import { computeTableName } from 'src/engine/utils/compute-table-name.util';
@Injectable()
export class GraphqlQueryDeleteManyResolverService extends GraphqlQueryBaseResolverService<
@@ -30,9 +31,14 @@ export class GraphqlQueryDeleteManyResolverService extends GraphqlQueryBaseResol
objectMetadataItemWithFieldMaps.nameSingular,
);
const tableName = computeTableName(
objectMetadataItemWithFieldMaps.nameSingular,
objectMetadataItemWithFieldMaps.isCustom,
);
executionArgs.graphqlQueryParser.applyFilterToBuilder(
queryBuilder,
objectMetadataItemWithFieldMaps.nameSingular,
tableName,
executionArgs.args.filter,
);
@@ -35,8 +35,8 @@ export class GraphqlQueryDeleteOneResolverService extends GraphqlQueryBaseResolv
);
const nonFormattedDeletedObjectRecords = await queryBuilder
.where({ id: executionArgs.args.id })
.softDelete()
.where({ id: executionArgs.args.id })
.returning('*')
.execute();
@@ -12,6 +12,7 @@ import { QUERY_MAX_RECORDS } from 'src/engine/api/graphql/graphql-query-runner/c
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
import { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper';
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
import { computeTableName } from 'src/engine/utils/compute-table-name.util';
@Injectable()
export class GraphqlQueryDestroyManyResolverService extends GraphqlQueryBaseResolverService<
@@ -28,9 +29,14 @@ export class GraphqlQueryDestroyManyResolverService extends GraphqlQueryBaseReso
objectMetadataItemWithFieldMaps.nameSingular,
);
const tableName = computeTableName(
objectMetadataItemWithFieldMaps.nameSingular,
objectMetadataItemWithFieldMaps.isCustom,
);
executionArgs.graphqlQueryParser.applyFilterToBuilder(
queryBuilder,
objectMetadataItemWithFieldMaps.nameSingular,
tableName,
executionArgs.args.filter,
);
@@ -16,7 +16,6 @@ import {
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
import { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper';
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
import { computeTableName } from 'src/engine/utils/compute-table-name.util';
@Injectable()
export class GraphqlQueryDestroyOneResolverService extends GraphqlQueryBaseResolverService<
@@ -33,17 +32,9 @@ export class GraphqlQueryDestroyOneResolverService extends GraphqlQueryBaseResol
objectMetadataItemWithFieldMaps.nameSingular,
);
const tableName = computeTableName(
objectMetadataItemWithFieldMaps.nameSingular,
objectMetadataItemWithFieldMaps.isCustom,
);
const nonFormattedDeletedObjectRecords = await queryBuilder
.where(`"${tableName}".id = :id`, {
id: executionArgs.args.id,
})
.take(1)
.delete()
.where({ id: executionArgs.args.id })
.returning('*')
.execute();
@@ -14,6 +14,7 @@ import { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-que
import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util';
import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util';
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
import { computeTableName } from 'src/engine/utils/compute-table-name.util';
@Injectable()
export class GraphqlQueryRestoreManyResolverService extends GraphqlQueryBaseResolverService<
@@ -30,9 +31,14 @@ export class GraphqlQueryRestoreManyResolverService extends GraphqlQueryBaseReso
objectMetadataItemWithFieldMaps.nameSingular,
);
const tableName = computeTableName(
objectMetadataItemWithFieldMaps.nameSingular,
objectMetadataItemWithFieldMaps.isCustom,
);
executionArgs.graphqlQueryParser.applyFilterToBuilder(
queryBuilder,
objectMetadataItemWithFieldMaps.nameSingular,
tableName,
executionArgs.args.filter,
);
@@ -35,8 +35,8 @@ export class GraphqlQueryRestoreOneResolverService extends GraphqlQueryBaseResol
);
const nonFormattedRestoredObjectRecords = await queryBuilder
.where({ id: executionArgs.args.id })
.restore()
.where({ id: executionArgs.args.id })
.returning('*')
.execute();
@@ -15,6 +15,7 @@ import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner
import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util';
import { formatData } from 'src/engine/twenty-orm/utils/format-data.util';
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
import { computeTableName } from 'src/engine/utils/compute-table-name.util';
@Injectable()
export class GraphqlQueryUpdateManyResolverService extends GraphqlQueryBaseResolverService<
@@ -31,14 +32,14 @@ export class GraphqlQueryUpdateManyResolverService extends GraphqlQueryBaseResol
objectMetadataItemWithFieldMaps.nameSingular,
);
const existingRecordsBuilder = queryBuilder.clone();
executionArgs.graphqlQueryParser.applyFilterToBuilder(
queryBuilder,
existingRecordsBuilder,
objectMetadataItemWithFieldMaps.nameSingular,
executionArgs.args.filter,
);
const existingRecordsBuilder = queryBuilder.clone();
const existingRecords = await existingRecordsBuilder.getMany();
const formattedExistingRecords = formatResult<ObjectRecord[]>(
@@ -47,6 +48,17 @@ export class GraphqlQueryUpdateManyResolverService extends GraphqlQueryBaseResol
objectMetadataMaps,
);
const tableName = computeTableName(
objectMetadataItemWithFieldMaps.nameSingular,
objectMetadataItemWithFieldMaps.isCustom,
);
executionArgs.graphqlQueryParser.applyFilterToBuilder(
queryBuilder,
tableName,
executionArgs.args.filter,
);
const data = formatData(
executionArgs.args.data,
objectMetadataItemWithFieldMaps,
@@ -12,4 +12,5 @@ export enum ObjectMetadataExceptionCode {
INVALID_OBJECT_INPUT = 'INVALID_OBJECT_INPUT',
OBJECT_MUTATION_NOT_ALLOWED = 'OBJECT_MUTATION_NOT_ALLOWED',
OBJECT_ALREADY_EXISTS = 'OBJECT_ALREADY_EXISTS',
MISSING_CUSTOM_OBJECT_DEFAULT_LABEL_IDENTIFIER_FIELD = 'MISSING_CUSTOM_OBJECT_DEFAULT_LABEL_IDENTIFIER_FIELD',
}
@@ -31,6 +31,7 @@ import { SearchService } from 'src/engine/metadata-modules/search/search.service
import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.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 { CUSTOM_OBJECT_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
import { isSearchableFieldType } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/is-searchable-field.util';
import { ObjectMetadataEntity } from './object-metadata.entity';
@@ -118,6 +119,21 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
: buildDefaultFieldsForCustomObject(objectMetadataInput.workspaceId),
});
const labelIdentifierFieldMetadata = createdObjectMetadata.fields.find(
(field) => field.standardId === CUSTOM_OBJECT_STANDARD_FIELD_IDS.name,
);
if (!labelIdentifierFieldMetadata) {
throw new ObjectMetadataException(
'Label identifier field metadata not created properly',
ObjectMetadataExceptionCode.MISSING_CUSTOM_OBJECT_DEFAULT_LABEL_IDENTIFIER_FIELD,
);
}
await this.objectMetadataRepository.update(createdObjectMetadata.id, {
labelIdentifierFieldMetadataId: labelIdentifierFieldMetadata.id,
});
if (objectMetadataInput.isRemote) {
await this.remoteTableRelationsService.createForeignKeysMetadataAndMigrations(
objectMetadataInput.workspaceId,
@@ -165,12 +165,11 @@ export class ObjectMetadataMigrationService {
});
if (relatedObject) {
// 1. Update to and from relation fieldMetadata)
// 1. Update to and from relation fieldMetadata
const toFieldRelationFieldMetadataId =
await this.fieldMetadataRepository
.findOneByOrFail({
name: existingObjectMetadata.nameSingular,
label: existingObjectMetadata.labelSingular,
objectMetadataId: relatedObject.id,
workspaceId: workspaceId,
})
@@ -2,7 +2,10 @@ import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/i
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
import {
RelationMetadataType,
RelationOnDeleteAction,
} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
@@ -53,6 +56,7 @@ export class FavoriteFolderWorkspaceEntity extends BaseWorkspaceEntity {
icon: 'IconHeart',
inverseSideFieldKey: 'favoriteFolder',
inverseSideTarget: () => FavoriteWorkspaceEntity,
onDelete: RelationOnDeleteAction.SET_NULL,
})
favorites: Relation<FavoriteWorkspaceEntity[]>;
}
@@ -20,11 +20,11 @@ export class GmailHandleErrorService {
) {
throw parseGaxiosError(error);
}
if (error.response?.status !== 410) {
if (error.code != 410) {
const gmailError = {
code: error.response?.status,
reason: `${error.response?.data?.error?.errors?.[0].reason || error.response?.data?.error || ''}`,
message: `${error.response?.data?.error?.errors?.[0].message || error.response?.data?.error_description || ''}${messageExternalId ? ` for message with externalId: ${messageExternalId}` : ''}`,
code: error.code,
reason: `${error?.errors?.[0].reason || error.response?.data?.error || ''}`,
message: `${error?.errors?.[0].message || error.response?.data?.error_description || ''}${messageExternalId ? ` for message with externalId: ${messageExternalId}` : ''}`,
};
throw parseGmailError(gmailError);
@@ -1,9 +1,9 @@
import { Injectable } from '@nestjs/common';
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
import { CALENDAR_THROTTLE_MAX_ATTEMPTS } from 'src/modules/calendar/calendar-event-import-manager/constants/calendar-throttle-max-attempts';
import { MessageChannelSyncStatusService } from 'src/modules/messaging/common/services/message-channel-sync-status.service';
import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { MESSAGING_THROTTLE_MAX_ATTEMPTS } from 'src/modules/messaging/message-import-manager/constants/messaging-throttle-max-attempts';
import {
MessageImportDriverException,
MessageImportDriverExceptionCode,
@@ -77,7 +77,9 @@ export class MessageImportExceptionHandlerService {
>,
workspaceId: string,
): Promise<void> {
if (messageChannel.throttleFailureCount >= CALENDAR_THROTTLE_MAX_ATTEMPTS) {
if (
messageChannel.throttleFailureCount >= MESSAGING_THROTTLE_MAX_ATTEMPTS
) {
await this.messageChannelSyncStatusService.markAsFailedUnknownAndFlushMessagesToImport(
[messageChannel.id],
workspaceId,
@@ -85,8 +85,8 @@ export class MessagingMessagesImportService {
);
} catch (error) {
switch (error.code) {
case (RefreshAccessTokenExceptionCode.REFRESH_ACCESS_TOKEN_FAILED,
RefreshAccessTokenExceptionCode.REFRESH_TOKEN_NOT_FOUND):
case RefreshAccessTokenExceptionCode.REFRESH_ACCESS_TOKEN_FAILED:
case RefreshAccessTokenExceptionCode.REFRESH_TOKEN_NOT_FOUND:
await this.messagingTelemetryService.track({
eventName: `refresh_token.error.insufficient_permissions`,
workspaceId,
@@ -191,7 +191,7 @@ export class MessagingMessagesImportService {
await this.messageImportErrorHandlerService.handleDriverException(
error,
MessageImportSyncStep.PARTIAL_MESSAGE_LIST_FETCH,
MessageImportSyncStep.MESSAGES_IMPORT,
messageChannel,
workspaceId,
);
@@ -1,3 +1,5 @@
import { Relation } from 'typeorm';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
@@ -5,7 +7,6 @@ import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-enti
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
import { WorkspaceIndex } from 'src/engine/twenty-orm/decorators/workspace-index.decorator';
import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator';
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace-join-column.decorator';
import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator';
@@ -77,9 +78,8 @@ export class ViewFieldWorkspaceEntity extends BaseWorkspaceEntity {
inverseSideTarget: () => ViewWorkspaceEntity,
inverseSideFieldKey: 'viewFields',
})
@WorkspaceIsNullable()
view?: ViewWorkspaceEntity | null;
view: Relation<ViewWorkspaceEntity>;
@WorkspaceJoinColumn('view')
viewId: string | null;
viewId: string;
}
@@ -111,7 +111,7 @@ export class ViewWorkspaceEntity extends BaseWorkspaceEntity {
description: 'View Fields',
icon: 'IconTag',
inverseSideTarget: () => ViewFieldWorkspaceEntity,
onDelete: RelationOnDeleteAction.SET_NULL,
onDelete: RelationOnDeleteAction.CASCADE,
})
@WorkspaceIsNullable()
viewFields: Relation<ViewFieldWorkspaceEntity[]>;
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "twenty-ui",
"version": "0.34.0-canary",
"version": "0.33.4",
"type": "module",
"main": "./src/index.ts",
"exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "twenty-website",
"version": "0.34.0-canary",
"version": "0.33.4",
"private": true,
"scripts": {
"nx": "NX_DEFAULT_PROJECT=twenty-website node ../../node_modules/nx/bin/nx.js",