Skip to content

Commit 7e11c06

Browse files
committed
Merge branch 'main' into revert-11344-revert-rest-api-get
2 parents 7218abd + 0b729cb commit 7e11c06

File tree

79 files changed

+728
-188
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

79 files changed

+728
-188
lines changed

packages/twenty-front/src/modules/activities/tasks/components/TaskList.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { Task } from '@/activities/types/Task';
66
import { TaskRow } from './TaskRow';
77

88
type TaskListProps = {
9-
title?: string;
9+
title: string;
1010
tasks: Task[];
1111
button?: ReactElement | false;
1212
};

packages/twenty-front/src/modules/activities/tasks/components/__stories__/TaskGroups.stories.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { ObjectFilterDropdownComponentInstanceContext } from '@/object-record/ob
66
import { TabListComponentInstanceContext } from '@/ui/layout/tab/states/contexts/TabListComponentInstanceContext';
77
import { ComponentWithRecoilScopeDecorator } from '~/testing/decorators/ComponentWithRecoilScopeDecorator';
88
import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWithRouterDecorator';
9+
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
910
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
1011
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
1112
import { graphqlMocks } from '~/testing/graphqlMocks';
@@ -30,6 +31,7 @@ const meta: Meta<typeof TaskGroups> = {
3031
ComponentWithRecoilScopeDecorator,
3132
ObjectMetadataItemsDecorator,
3233
SnackBarDecorator,
34+
I18nFrontDecorator,
3335
],
3436
};
3537

packages/twenty-front/src/modules/activities/timeline-activities/rows/message/components/__stories__/EventCardMessage.stories.tsx

+12-1
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import { Meta, StoryObj } from '@storybook/react';
2+
import { within } from '@storybook/testing-library';
23
import { HttpResponse, graphql } from 'msw';
34

45
import { TimelineActivityContext } from '@/activities/timeline-activities/contexts/TimelineActivityContext';
56
import { EventCardMessage } from '@/activities/timeline-activities/rows/message/components/EventCardMessage';
7+
import { ComponentDecorator } from 'twenty-ui/testing';
68
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
79
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
8-
import { ComponentDecorator } from 'twenty-ui/testing';
910

1011
const meta: Meta<typeof EventCardMessage> = {
1112
title: 'Modules/TimelineActivities/Rows/Message/EventCardMessage',
@@ -32,6 +33,11 @@ export const Default: Story = {
3233
messageId: '1',
3334
authorFullName: 'John Doe',
3435
},
36+
play: async ({ canvasElement }) => {
37+
const canvas = within(canvasElement);
38+
39+
await canvas.findByText('Mock title');
40+
},
3541
parameters: {
3642
msw: {
3743
handlers: [
@@ -57,6 +63,11 @@ export const NotShared: Story = {
5763
messageId: '1',
5864
authorFullName: 'John Doe',
5965
},
66+
play: async ({ canvasElement }) => {
67+
const canvas = within(canvasElement);
68+
69+
await canvas.findByText('Subject not shared');
70+
},
6071
parameters: {
6172
msw: {
6273
handlers: [
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
2+
3+
import { isFilterOperandExpectingValue } from '../isFilterOperandExpectingValue';
4+
5+
describe('isFilterOperandExpectingValue', () => {
6+
const testCases = [
7+
{ operand: ViewFilterOperand.Contains, expectedResult: true },
8+
{ operand: ViewFilterOperand.DoesNotContain, expectedResult: true },
9+
{ operand: ViewFilterOperand.GreaterThan, expectedResult: true },
10+
{ operand: ViewFilterOperand.LessThan, expectedResult: true },
11+
{ operand: ViewFilterOperand.Is, expectedResult: true },
12+
{ operand: ViewFilterOperand.IsNot, expectedResult: true },
13+
{ operand: ViewFilterOperand.IsRelative, expectedResult: true },
14+
{ operand: ViewFilterOperand.IsBefore, expectedResult: true },
15+
{ operand: ViewFilterOperand.IsAfter, expectedResult: true },
16+
17+
{ operand: ViewFilterOperand.IsNotNull, expectedResult: false },
18+
{ operand: ViewFilterOperand.IsEmpty, expectedResult: false },
19+
{ operand: ViewFilterOperand.IsNotEmpty, expectedResult: false },
20+
{ operand: ViewFilterOperand.IsInPast, expectedResult: false },
21+
{ operand: ViewFilterOperand.IsInFuture, expectedResult: false },
22+
{ operand: ViewFilterOperand.IsToday, expectedResult: false },
23+
];
24+
25+
testCases.forEach(({ operand, expectedResult }) => {
26+
it(`should return ${expectedResult} for ViewFilterOperand.${operand}`, () => {
27+
expect(isFilterOperandExpectingValue(operand)).toBe(expectedResult);
28+
});
29+
});
30+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
2+
3+
export const isFilterOperandExpectingValue = (operand: ViewFilterOperand) => {
4+
switch (operand) {
5+
case ViewFilterOperand.IsNotNull:
6+
case ViewFilterOperand.IsEmpty:
7+
case ViewFilterOperand.IsNotEmpty:
8+
case ViewFilterOperand.IsInPast:
9+
case ViewFilterOperand.IsInFuture:
10+
case ViewFilterOperand.IsToday:
11+
return false;
12+
case ViewFilterOperand.IsNot:
13+
case ViewFilterOperand.Contains:
14+
case ViewFilterOperand.DoesNotContain:
15+
case ViewFilterOperand.GreaterThan:
16+
case ViewFilterOperand.LessThan:
17+
case ViewFilterOperand.IsBefore:
18+
case ViewFilterOperand.IsAfter:
19+
case ViewFilterOperand.Is:
20+
case ViewFilterOperand.IsRelative:
21+
default:
22+
return true;
23+
}
24+
};

packages/twenty-front/src/modules/settings/accounts/components/__stories__/SettingsAccountsBlocklistTableRow.stories.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { Decorator, Meta, StoryObj } from '@storybook/react';
22
import { expect, fn, userEvent, within } from '@storybook/test';
33

4-
import { mockedBlocklist } from '@/settings/accounts/components/__stories__/mockedBlocklist';
54
import { SettingsAccountsBlocklistTableRow } from '@/settings/accounts/components/SettingsAccountsBlocklistTableRow';
6-
import { formatToHumanReadableDate } from '~/utils/date-utils';
5+
import { mockedBlocklist } from '@/settings/accounts/components/__stories__/mockedBlocklist';
76
import { ComponentDecorator } from 'twenty-ui/testing';
7+
import { formatToHumanReadableDate } from '~/utils/date-utils';
88

99
const onRemoveJestFn = fn();
1010

packages/twenty-front/src/modules/settings/accounts/components/__stories__/mockedBlocklist.ts

+16-4
Original file line numberDiff line numberDiff line change
@@ -7,28 +7,40 @@ export const mockedBlocklist: BlocklistItem[] = [
77
id: '1',
88
handle: 'test1@twenty.com',
99
workspaceMemberId: '1',
10-
createdAt: DateTime.now().minus({ hours: 2 }).toISO() ?? '',
10+
createdAt:
11+
DateTime.fromISO('2023-04-26T10:12:42.33625+00:00')
12+
.minus({ hours: 2 })
13+
.toISO() ?? '',
1114
__typename: 'BlocklistItem',
1215
},
1316
{
1417
id: '2',
1518
handle: 'test2@twenty.com',
1619
workspaceMemberId: '1',
17-
createdAt: DateTime.now().minus({ days: 2 }).toISO() ?? '',
20+
createdAt:
21+
DateTime.fromISO('2023-04-26T10:12:42.33625+00:00')
22+
.minus({ days: 2 })
23+
.toISO() ?? '',
1824
__typename: 'BlocklistItem',
1925
},
2026
{
2127
id: '3',
2228
handle: 'test3@twenty.com',
2329
workspaceMemberId: '1',
24-
createdAt: DateTime.now().minus({ days: 3 }).toISO() ?? '',
30+
createdAt:
31+
DateTime.fromISO('2023-04-26T10:12:42.33625+00:00')
32+
.minus({ days: 3 })
33+
.toISO() ?? '',
2534
__typename: 'BlocklistItem',
2635
},
2736
{
2837
id: '4',
2938
handle: '@twenty.com',
3039
workspaceMemberId: '1',
31-
createdAt: DateTime.now().minus({ days: 4 }).toISO() ?? '',
40+
createdAt:
41+
DateTime.fromISO('2023-04-26T10:12:42.33625+00:00')
42+
.minus({ days: 4 })
43+
.toISO() ?? '',
3244
__typename: 'BlocklistItem',
3345
},
3446
];

packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownContent.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ export const DropdownContent = ({
8282
useListenClickOutside({
8383
refs: [floatingUiRefs.floating, floatingUiRefs.domReference],
8484
listenerId: dropdownId,
85+
excludeClassNames: ['confirmation-modal'],
8586
callback: (event) => {
8687
if (activeDropdownFocusId !== dropdownId) return;
8788

packages/twenty-front/src/modules/views/components/EditableFilterChip.tsx

+6-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { useFieldMetadataItemById } from '@/object-metadata/hooks/useFieldMetada
22
import { getCompositeSubFieldLabel } from '@/object-record/object-filter-dropdown/utils/getCompositeSubFieldLabel';
33
import { getOperandLabelShort } from '@/object-record/object-filter-dropdown/utils/getOperandLabel';
44
import { isCompositeField } from '@/object-record/object-filter-dropdown/utils/isCompositeField';
5+
import { isFilterOperandExpectingValue } from '@/object-record/object-filter-dropdown/utils/isFilterOperandExpectingValue';
56
import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
67
import { isValidSubFieldName } from '@/settings/data-model/utils/isValidSubFieldName';
78
import { SortOrFilterChip } from '@/views/components/SortOrFilterChip';
@@ -43,7 +44,11 @@ export const EditableFilterChip = ({
4344
? `${recordFilter.label} / ${subFieldLabel}`
4445
: recordFilter.label;
4546

46-
const labelKey = `${fieldNameLabel}${isNonEmptyString(recordFilter.value) ? operandLabelShort : ''}`;
47+
const shouldDisplayOperandLabelShort =
48+
isNonEmptyString(recordFilter.value) ||
49+
!isFilterOperandExpectingValue(recordFilter.operand);
50+
51+
const labelKey = `${fieldNameLabel}${shouldDisplayOperandLabelShort ? operandLabelShort : ''}`;
4752

4853
return (
4954
<SortOrFilterChip

packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/__stories__/WorkflowEditActionUpdateRecord.stories.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { WorkflowUpdateRecordAction } from '@/workflow/types/Workflow';
22
import { Meta, StoryObj } from '@storybook/react';
33
import { expect, fn, userEvent, within } from '@storybook/test';
4+
import { ComponentDecorator, RouterDecorator } from 'twenty-ui/testing';
45
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
56
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
67
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
@@ -11,7 +12,6 @@ import { graphqlMocks } from '~/testing/graphqlMocks';
1112
import { allMockPersonRecords } from '~/testing/mock-data/people';
1213
import { getWorkflowNodeIdMock } from '~/testing/mock-data/workflow';
1314
import { WorkflowEditActionUpdateRecord } from '../WorkflowEditActionUpdateRecord';
14-
import { ComponentDecorator, RouterDecorator } from 'twenty-ui/testing';
1515

1616
const DEFAULT_ACTION = {
1717
id: getWorkflowNodeIdMock(),
@@ -176,7 +176,7 @@ export const DisabledWithDefaultStaticValues: Story = {
176176
const selectedRecord = await canvas.findByText(
177177
`${peopleMock.name.firstName} ${peopleMock.name.lastName}`,
178178
undefined,
179-
{ timeout: 3000 },
179+
{ timeout: 5000 },
180180
);
181181
expect(selectedRecord).toBeVisible();
182182

packages/twenty-front/src/testing/mock-data/tasks.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,13 @@ export const mockedTasks: Array<MockedTask> = [
2525
id: '3ecaa1be-aac7-463a-a38e-64078dd451d5',
2626
createdAt: '2023-04-26T10:12:42.33625+00:00',
2727
updatedAt: '2023-04-26T10:23:42.33625+00:00',
28-
title: 'My very first note',
28+
title: 'My very first task',
2929
bodyV2: {
3030
blocknote: null,
3131
markdown: null,
3232
},
3333
dueAt: '2023-04-26T10:12:42.33625+00:00',
34-
status: null,
34+
status: 'TODO',
3535
assignee: workspaceMember,
3636
assigneeId: workspaceMember.id,
3737
taskTargets: [

packages/twenty-server/.env.example

-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ PG_DATABASE_URL=postgres://postgres:postgres@localhost:5432/default
44
REDIS_URL=redis://localhost:6379
55
APP_SECRET=replace_me_with_a_random_string
66
SIGN_IN_PREFILLED=true
7-
SYNC_METADATA_INDEX_ENABLED=false
87

98
FRONTEND_URL=http://localhost:3001
109

packages/twenty-server/@types/jest.d.ts

+2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ declare module '@jest/types' {
99
INVALID_ACCESS_TOKEN: string;
1010
MEMBER_ACCESS_TOKEN: string;
1111
GUEST_ACCESS_TOKEN: string;
12+
API_KEY_ACCESS_TOKEN: string;
1213
}
1314
}
1415
}
@@ -20,6 +21,7 @@ declare global {
2021
const INVALID_ACCESS_TOKEN: string;
2122
const MEMBER_ACCESS_TOKEN: string;
2223
const GUEST_ACCESS_TOKEN: string;
24+
const API_KEY_ACCESS_TOKEN: string;
2325
}
2426

2527
export {};

packages/twenty-server/jest-integration.config.ts

+2
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ const jestConfig: JestConfigWithTsJest = {
7676
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC0zOTU3LTQ5MDgtOWMzNi0yOTI5YTIzZjgzNTciLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNzdkNS00Y2I2LWI2MGEtZjRhODM1YTg1ZDYxIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMzk1Ny00OTA4LTljMzYtMjkyOWEyM2Y4MzUzIiwiaWF0IjoxNzM5NDU5NTcwLCJleHAiOjMzMjk3MDU5NTcwfQ.Er7EEU4IP4YlGN79jCLR_6sUBqBfKx2M3G_qGiDpPRo',
7777
GUEST_ACCESS_TOKEN:
7878
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC03MTY5LTQyY2YtYmM0Ny0xY2ZlZjE1MjY0YjgiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtMTU1My00NWM2LWEwMjgtNWE5MDY0Y2NlMDdmIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtNzE2OS00MmNmLWJjNDctMWNmZWYxNTI2NGIxIiwiaWF0IjoxNzM5ODg4NDcwLCJleHAiOjMzMjk3NDg4NDcwfQ.0NEu-AWGv3l77rs-56Z5Gt0UTU7HDl6qUTHUcMWNrCc',
79+
API_KEY_ACCESS_TOKEN:
80+
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC0xYzI1LTRkMDItYmYyNS02YWVjY2Y3ZWE0MTkiLCJ0eXBlIjoiQVBJX0tFWSIsIndvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWMyNS00ZDAyLWJmMjUtNmFlY2NmN2VhNDE5IiwiaWF0IjoxNzQ0OTgzNzUwLCJleHAiOjQ4OTg1ODM2OTMsImp0aSI6IjIwMjAyMDIwLWY0MDEtNGQ4YS1hNzMxLTY0ZDAwN2MyN2JhZCJ9.4xkkwz_uu2xzs_V8hJSaM15fGziT5zS3vq2lM48OHr0',
7981
},
8082
};
8183

packages/twenty-server/nest-cli.json

+4
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@
2525
{
2626
"include": "**/serverless/drivers/constants/executor/index.mjs",
2727
"outDir": "dist/assets"
28+
},
29+
{
30+
"include": "**/database/clickhouse/migrations/*.sql",
31+
"outDir": "dist/src"
2832
}
2933
],
3034
"watchAssets": true

packages/twenty-server/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"worker:prod": "node dist/src/queue-worker/queue-worker",
1212
"database:init:prod": "npx ts-node ./scripts/setup-db.ts && yarn database:migrate:prod",
1313
"database:migrate:prod": "npx -y typeorm migration:run -d dist/src/database/typeorm/metadata/metadata.datasource && npx -y typeorm migration:run -d dist/src/database/typeorm/core/core.datasource",
14+
"clickhouse:migrate:prod": "node dist/src/database/clickhouse/migrations/run-migrations.js",
1415
"typeorm": "../../node_modules/typeorm/.bin/typeorm"
1516
},
1617
"dependencies": {

packages/twenty-server/src/database/clickhouse/migrations/run-migrations.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import fs from 'fs';
33
import path from 'path';
44

5-
import { createClient, ClickHouseClient } from '@clickhouse/client';
5+
import { ClickHouseClient, createClient } from '@clickhouse/client';
66
import { config } from 'dotenv';
77

88
config({
@@ -29,9 +29,6 @@ async function ensureDatabaseExists() {
2929
await client.command({
3030
query: `CREATE DATABASE IF NOT EXISTS "${database}"`,
3131
});
32-
await client.command({
33-
query: `SET enable_json_type = 1`,
34-
});
3532

3633
await client.close();
3734
}
@@ -78,6 +75,9 @@ async function runMigrations() {
7875

7976
const client = createClient({
8077
url: clickhouseUrl(),
78+
clickhouse_settings: {
79+
allow_experimental_json_type: 1,
80+
},
8181
});
8282

8383
await ensureMigrationTable(client);

packages/twenty-server/src/database/commands/data-seed-dev-workspace.command.ts

+2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
getDevSeedCompanyCustomFields,
1313
getDevSeedPeopleCustomFields,
1414
} from 'src/database/typeorm-seeds/metadata/fieldsMetadata';
15+
import { seedApiKey } from 'src/database/typeorm-seeds/workspace/api-key';
1516
import { seedCalendarChannels } from 'src/database/typeorm-seeds/workspace/calendar-channel';
1617
import { seedCalendarChannelEventAssociations } from 'src/database/typeorm-seeds/workspace/calendar-channel-event-association';
1718
import { seedCalendarEventParticipants } from 'src/database/typeorm-seeds/workspace/calendar-event-participants';
@@ -184,6 +185,7 @@ export class DataSeedWorkspaceCommand extends CommandRunner {
184185
);
185186

186187
if (dataSourceMetadata.workspaceId === SEED_APPLE_WORKSPACE_ID) {
188+
await seedApiKey(entityManager, dataSourceMetadata.schema);
187189
await seedMessageThread(entityManager, dataSourceMetadata.schema);
188190
await seedConnectedAccount(entityManager, dataSourceMetadata.schema);
189191

packages/twenty-server/src/database/commands/upgrade-version-command/0-43/0-43-add-tasks-assigned-to-me-view.command.ts

-4
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,6 @@ export class AddTasksAssignedToMeViewCommand extends ActiveOrSuspendedWorkspaces
8888
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ViewWorkspaceEntity>(
8989
workspaceId,
9090
'view',
91-
false,
9291
);
9392

9493
const existingView = await viewRepository.findOne({
@@ -126,7 +125,6 @@ export class AddTasksAssignedToMeViewCommand extends ActiveOrSuspendedWorkspaces
126125
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ViewFieldWorkspaceEntity>(
127126
workspaceId,
128127
'viewField',
129-
false,
130128
);
131129

132130
const viewFields = viewDefinition.fields.map((field) => ({
@@ -145,7 +143,6 @@ export class AddTasksAssignedToMeViewCommand extends ActiveOrSuspendedWorkspaces
145143
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ViewFilterWorkspaceEntity>(
146144
workspaceId,
147145
'viewFilter',
148-
false,
149146
);
150147

151148
const viewFilters = viewDefinition.filters.map((filter) => ({
@@ -202,7 +199,6 @@ export class AddTasksAssignedToMeViewCommand extends ActiveOrSuspendedWorkspaces
202199
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ViewGroupWorkspaceEntity>(
203200
workspaceId,
204201
'viewGroup',
205-
false,
206202
);
207203

208204
await viewGroupRepository.insert(viewGroups);

packages/twenty-server/src/database/commands/upgrade-version-command/0-43/0-43-update-default-view-record-opening-on-workflow-objects.command.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,9 @@ export class UpdateDefaultViewRecordOpeningOnWorkflowObjectsCommand extends Acti
8181
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
8282
workspaceId,
8383
'view',
84-
failOnMetadataCacheMiss,
84+
{
85+
shouldFailIfMetadataNotFound: failOnMetadataCacheMiss,
86+
},
8587
);
8688

8789
await viewRepository.update(

0 commit comments

Comments
 (0)