Add auto-upgrade-in-apps (#23001)
We need to auto upgrade application, lets add this column in application entity, and add an admin button to autoupgrade all applications to latest app registrration version manually <img width="1131" height="372" alt="image" src="https://github.com/user-attachments/assets/4e755abc-38ad-4895-a2e8-d55ee1948ac2" /> <img width="906" height="533" alt="image" src="https://github.com/user-attachments/assets/ce002057-0341-4581-bf9a-66ac2bd84a9b" />
This commit is contained in:
@@ -61,6 +61,10 @@ import {
|
||||
BACKFILL_APPLICATION_INSTALLATION_JOB_NAME,
|
||||
type BackfillApplicationInstallationJobData,
|
||||
} from 'src/engine/core-modules/application/jobs/backfill-application-installation.job-constants';
|
||||
import {
|
||||
UPGRADE_APPLICATIONS_JOB_NAME,
|
||||
type UpgradeApplicationsJobData,
|
||||
} from 'src/engine/core-modules/application/jobs/upgrade-applications.job-constants';
|
||||
import { AuthGraphqlApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-graphql-api-exception.filter';
|
||||
import { type AuthContextUser } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
import { AdminAiModelsDTO } from 'src/engine/core-modules/client-config/client-config.entity';
|
||||
@@ -545,6 +549,26 @@ export class AdminPanelResolver {
|
||||
return true;
|
||||
}
|
||||
|
||||
@UseGuards(AdminPanelGuard)
|
||||
@Mutation(() => Boolean)
|
||||
async upgradeRegistrationApplications(
|
||||
@Args('applicationRegistrationId') applicationRegistrationId: string,
|
||||
): Promise<boolean> {
|
||||
await this.applicationRegistrationService.findOneByIdGlobal(
|
||||
applicationRegistrationId,
|
||||
);
|
||||
|
||||
await this.workspaceQueueService.add<UpgradeApplicationsJobData>(
|
||||
UPGRADE_APPLICATIONS_JOB_NAME,
|
||||
{ applicationRegistrationId, onlyAutoUpgrade: false },
|
||||
{
|
||||
id: `${UPGRADE_APPLICATIONS_JOB_NAME}-${applicationRegistrationId}`,
|
||||
}, // Avoids triggering multiple pending jobs for the same app
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@UseGuards(AdminPanelGuard)
|
||||
@Query(() => GraphQLJSON)
|
||||
async getAiProviders(): Promise<Record<string, unknown>> {
|
||||
|
||||
+23
@@ -7,6 +7,8 @@ import {
|
||||
import { Args, Mutation, Query } from '@nestjs/graphql';
|
||||
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { ApplicationExceptionFilter } from 'src/engine/core-modules/application/application-exception-filter';
|
||||
@@ -18,6 +20,7 @@ import { ApplicationException } from 'src/engine/core-modules/application/applic
|
||||
import { ApplicationRegistrationExceptionFilter } from 'src/engine/core-modules/application/application-registration/application-registration-exception-filter';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { ApplicationDTO } from 'src/engine/core-modules/application/dtos/application.dto';
|
||||
import { UpdateApplicationInput } from 'src/engine/core-modules/application/dtos/update-application.input';
|
||||
import { AuthGraphqlApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-graphql-api-exception.filter';
|
||||
import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service';
|
||||
import { MetricsKeys } from 'src/engine/core-modules/metrics/types/metrics-keys.type';
|
||||
@@ -128,6 +131,26 @@ export class ApplicationInstallResolver {
|
||||
});
|
||||
}
|
||||
|
||||
@Mutation(() => ApplicationDTO)
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.APPLICATIONS))
|
||||
async updateApplication(
|
||||
@Args('id', { type: () => UUIDScalarType }) id: string,
|
||||
@Args('input') input: UpdateApplicationInput,
|
||||
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
|
||||
) {
|
||||
await this.applicationService.findOneApplicationOrThrow({
|
||||
id,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
return this.applicationService.update(id, {
|
||||
...(isDefined(input.autoUpgrade)
|
||||
? { autoUpgrade: input.autoUpgrade }
|
||||
: {}),
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean)
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.APPLICATIONS))
|
||||
async uninstallApplication(
|
||||
|
||||
+1
@@ -161,6 +161,7 @@ export class ApplicationSyncService {
|
||||
defaultRole: null,
|
||||
settingsCustomTabFrontComponentId: null,
|
||||
canBeUninstalled: true,
|
||||
autoUpgrade: false,
|
||||
isSdkLayerStale: false,
|
||||
applicationRegistrationId: null,
|
||||
primaryPublicDomainId: null,
|
||||
|
||||
+6
@@ -10,6 +10,8 @@ import { ApplicationEntity } from 'src/engine/core-modules/application/applicati
|
||||
import { CacheLockService } from 'src/engine/core-modules/cache-lock/cache-lock.service';
|
||||
import { CoreEntityCacheService } from 'src/engine/core-entity-cache/services/core-entity-cache.service';
|
||||
import { ServerFileStorageService } from 'src/engine/core-modules/file-storage/services/server-file-storage.service';
|
||||
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
||||
import { getQueueToken } from 'src/engine/core-modules/message-queue/utils/get-queue-token.util';
|
||||
import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
|
||||
@@ -95,6 +97,10 @@ describe('ApplicationRegistrationService - upsertFromCatalog', () => {
|
||||
provide: MetricsService,
|
||||
useValue: { incrementCounterBy: jest.fn() },
|
||||
},
|
||||
{
|
||||
provide: getQueueToken(MessageQueue.workspaceQueue),
|
||||
useValue: { add: jest.fn() },
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
|
||||
+31
@@ -35,8 +35,15 @@ import { ApplicationRegistrationSourceType } from 'src/engine/core-modules/appli
|
||||
import { buildRegistrationManifestUpdateFields } from 'src/engine/core-modules/application/application-registration/utils/build-registration-manifest-update-fields.util';
|
||||
import { fromManifestApplicationToDisplayFields } from 'src/engine/core-modules/application/application-registration/utils/from-manifest-application-to-display-fields.util';
|
||||
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
|
||||
import {
|
||||
UPGRADE_APPLICATIONS_JOB_NAME,
|
||||
type UpgradeApplicationsJobData,
|
||||
} from 'src/engine/core-modules/application/jobs/upgrade-applications.job-constants';
|
||||
import { validateRedirectUri } from 'src/engine/core-modules/auth/utils/validate-redirect-uri.util';
|
||||
import { CacheLockService } from 'src/engine/core-modules/cache-lock/cache-lock.service';
|
||||
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
|
||||
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
||||
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
|
||||
import { ServerFileStorageService } from 'src/engine/core-modules/file-storage/services/server-file-storage.service';
|
||||
import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service';
|
||||
import { MetricsKeys } from 'src/engine/core-modules/metrics/types/metrics-keys.type';
|
||||
@@ -115,8 +122,28 @@ export class ApplicationRegistrationService {
|
||||
private readonly cacheLockService: CacheLockService,
|
||||
private readonly coreEntityCacheService: CoreEntityCacheService,
|
||||
private readonly metricsService: MetricsService,
|
||||
@InjectMessageQueue(MessageQueue.workspaceQueue)
|
||||
private readonly workspaceQueueService: MessageQueueService,
|
||||
) {}
|
||||
|
||||
// Best-effort: a queue outage must not fail the publish flow that
|
||||
// triggered the upgrade.
|
||||
async enqueueAutoUpgradeApplications(
|
||||
applicationRegistrationId: string,
|
||||
): Promise<void> {
|
||||
try {
|
||||
await this.workspaceQueueService.add<UpgradeApplicationsJobData>(
|
||||
UPGRADE_APPLICATIONS_JOB_NAME,
|
||||
{ applicationRegistrationId, onlyAutoUpgrade: true },
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to enqueue auto-upgrade for registration ${applicationRegistrationId}`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async invalidateMarketplaceAppsCache(): Promise<void> {
|
||||
try {
|
||||
await this.coreEntityCacheService.invalidate(
|
||||
@@ -618,6 +645,8 @@ export class ApplicationRegistrationService {
|
||||
sourceType: params.sourceType,
|
||||
version: params.latestAvailableVersion,
|
||||
});
|
||||
|
||||
await this.enqueueAutoUpgradeApplications(existing.id);
|
||||
}
|
||||
|
||||
return;
|
||||
@@ -659,6 +688,8 @@ export class ApplicationRegistrationService {
|
||||
sourceType: params.sourceType,
|
||||
version: params.latestAvailableVersion,
|
||||
});
|
||||
|
||||
await this.enqueueAutoUpgradeApplications(existing.id);
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
+6
@@ -135,6 +135,12 @@ export class ApplicationTarballService {
|
||||
sourceType: ApplicationRegistrationSourceType.TARBALL,
|
||||
version: incomingVersion,
|
||||
});
|
||||
|
||||
if (!isNewRegistration) {
|
||||
await this.applicationRegistrationService.enqueueAutoUpgradeApplications(
|
||||
appRegistration.id,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return this.appRegistrationRepository.findOneOrFail({
|
||||
|
||||
+5
-1
@@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { ApplicationInstallModule } from 'src/engine/core-modules/application/application-install/application-install.module';
|
||||
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
|
||||
import { ApplicationRegistrationEntity } from 'src/engine/core-modules/application/application-registration/application-registration.entity';
|
||||
import { ApplicationRegistrationModule } from 'src/engine/core-modules/application/application-registration/application-registration.module';
|
||||
import { ApplicationUpgradeResolver } from 'src/engine/core-modules/application/application-upgrade/application-upgrade.resolver';
|
||||
@@ -14,7 +15,10 @@ import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permi
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([ApplicationRegistrationEntity]),
|
||||
TypeOrmModule.forFeature([
|
||||
ApplicationEntity,
|
||||
ApplicationRegistrationEntity,
|
||||
]),
|
||||
ApplicationInstallModule,
|
||||
ApplicationRegistrationModule,
|
||||
FeatureFlagModule,
|
||||
|
||||
+82
-1
@@ -2,10 +2,12 @@ import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import axios from 'axios';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Repository } from 'typeorm';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ApplicationInstallService } from 'src/engine/core-modules/application/application-install/application-install.service';
|
||||
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
|
||||
import { ApplicationRegistrationEntity } from 'src/engine/core-modules/application/application-registration/application-registration.entity';
|
||||
import { ApplicationRegistrationService } from 'src/engine/core-modules/application/application-registration/application-registration.service';
|
||||
import { ApplicationRegistrationSourceType } from 'src/engine/core-modules/application/application-registration/enums/application-registration-source-type.enum';
|
||||
@@ -19,6 +21,8 @@ const npmPackageMetadataSchema = z.object({
|
||||
version: z.string(),
|
||||
});
|
||||
|
||||
const UPGRADE_APPLICATIONS_BATCH_SIZE = 20;
|
||||
|
||||
@Injectable()
|
||||
export class ApplicationUpgradeService {
|
||||
private readonly logger = new Logger(ApplicationUpgradeService.name);
|
||||
@@ -26,6 +30,8 @@ export class ApplicationUpgradeService {
|
||||
constructor(
|
||||
@InjectRepository(ApplicationRegistrationEntity)
|
||||
private readonly appRegistrationRepository: Repository<ApplicationRegistrationEntity>,
|
||||
@InjectRepository(ApplicationEntity)
|
||||
private readonly applicationRepository: Repository<ApplicationEntity>,
|
||||
private readonly applicationInstallService: ApplicationInstallService,
|
||||
private readonly applicationRegistrationService: ApplicationRegistrationService,
|
||||
private readonly twentyConfigService: TwentyConfigService,
|
||||
@@ -79,6 +85,10 @@ export class ApplicationUpgradeService {
|
||||
sourceType: appRegistration.sourceType,
|
||||
version: parsed.data.version,
|
||||
});
|
||||
|
||||
await this.applicationRegistrationService.enqueueAutoUpgradeApplications(
|
||||
appRegistration.id,
|
||||
);
|
||||
}
|
||||
|
||||
return parsed.data.version;
|
||||
@@ -101,6 +111,63 @@ export class ApplicationUpgradeService {
|
||||
}
|
||||
}
|
||||
|
||||
async upgradeAllApplications({
|
||||
applicationRegistrationId,
|
||||
onlyAutoUpgrade = false,
|
||||
}: {
|
||||
applicationRegistrationId: string;
|
||||
onlyAutoUpgrade?: boolean;
|
||||
}): Promise<void> {
|
||||
const appRegistration = await this.appRegistrationRepository.findOneOrFail({
|
||||
where: { id: applicationRegistrationId },
|
||||
});
|
||||
|
||||
const targetVersion = appRegistration.latestAvailableVersion;
|
||||
|
||||
if (!isDefined(targetVersion)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const applications = await this.applicationRepository.find({
|
||||
where: {
|
||||
applicationRegistrationId,
|
||||
...(onlyAutoUpgrade ? { autoUpgrade: true } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
const applicationsToUpgrade = applications.filter(
|
||||
(application) => application.version !== targetVersion,
|
||||
);
|
||||
|
||||
for (
|
||||
let batchStart = 0;
|
||||
batchStart < applicationsToUpgrade.length;
|
||||
batchStart += UPGRADE_APPLICATIONS_BATCH_SIZE
|
||||
) {
|
||||
const batch = applicationsToUpgrade.slice(
|
||||
batchStart,
|
||||
batchStart + UPGRADE_APPLICATIONS_BATCH_SIZE,
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
batch.map(async (application) => {
|
||||
try {
|
||||
await this.upgradeApplicationToVersion({
|
||||
appRegistration,
|
||||
targetVersion,
|
||||
workspaceId: application.workspaceId,
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to upgrade application ${application.id} to version ${targetVersion} in workspace ${application.workspaceId}`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async upgradeApplication(params: {
|
||||
appRegistrationId: string;
|
||||
targetVersion: string;
|
||||
@@ -110,6 +177,20 @@ export class ApplicationUpgradeService {
|
||||
where: { id: params.appRegistrationId },
|
||||
});
|
||||
|
||||
return this.upgradeApplicationToVersion({
|
||||
appRegistration,
|
||||
targetVersion: params.targetVersion,
|
||||
workspaceId: params.workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
private async upgradeApplicationToVersion(params: {
|
||||
appRegistration: ApplicationRegistrationEntity;
|
||||
targetVersion: string;
|
||||
workspaceId: string;
|
||||
}): Promise<boolean> {
|
||||
const { appRegistration } = params;
|
||||
|
||||
// LOCAL apps are updated by dev sync and OAUTH_ONLY registrations have no
|
||||
// code artifacts.
|
||||
if (
|
||||
@@ -125,7 +206,7 @@ export class ApplicationUpgradeService {
|
||||
|
||||
try {
|
||||
return await this.applicationInstallService.installApplication({
|
||||
appRegistrationId: params.appRegistrationId,
|
||||
appRegistrationId: appRegistration.id,
|
||||
version: params.targetVersion,
|
||||
workspaceId: params.workspaceId,
|
||||
});
|
||||
|
||||
@@ -124,6 +124,13 @@ export class ApplicationEntity extends WorkspaceRelatedEntity {
|
||||
@Column({ nullable: false, type: 'boolean', default: true })
|
||||
canBeUninstalled: boolean;
|
||||
|
||||
@Column({ nullable: false, type: 'boolean', default: false })
|
||||
@WasIntroducedInUpgrade({
|
||||
upgradeCommandName:
|
||||
'2.23.0_AddAutoUpgradeToApplicationFastInstanceCommand_1784297307235',
|
||||
})
|
||||
autoUpgrade: boolean;
|
||||
|
||||
@Column({ nullable: false, type: 'boolean', default: false })
|
||||
isSdkLayerStale: boolean;
|
||||
|
||||
|
||||
@@ -86,6 +86,10 @@ export class ApplicationDTO {
|
||||
@IsBoolean()
|
||||
canBeUninstalled: boolean;
|
||||
|
||||
@Field(() => Boolean)
|
||||
@IsBoolean()
|
||||
autoUpgrade: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Field({ nullable: true })
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
|
||||
import { IsBoolean, IsOptional } from 'class-validator';
|
||||
|
||||
@InputType()
|
||||
export class UpdateApplicationInput {
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@Field(() => Boolean, { nullable: true })
|
||||
autoUpgrade?: boolean;
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
export const UPGRADE_APPLICATIONS_JOB_NAME = 'UpgradeApplicationsJob';
|
||||
|
||||
export type UpgradeApplicationsJobData = {
|
||||
applicationRegistrationId: string;
|
||||
onlyAutoUpgrade: boolean;
|
||||
};
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import { ApplicationUpgradeService } from 'src/engine/core-modules/application/application-upgrade/application-upgrade.service';
|
||||
import {
|
||||
UPGRADE_APPLICATIONS_JOB_NAME,
|
||||
type UpgradeApplicationsJobData,
|
||||
} from 'src/engine/core-modules/application/jobs/upgrade-applications.job-constants';
|
||||
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
|
||||
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
|
||||
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
||||
|
||||
@Processor(MessageQueue.workspaceQueue)
|
||||
export class UpgradeApplicationsJob {
|
||||
constructor(
|
||||
private readonly applicationUpgradeService: ApplicationUpgradeService,
|
||||
) {}
|
||||
|
||||
@Process(UPGRADE_APPLICATIONS_JOB_NAME)
|
||||
async handle(data: UpgradeApplicationsJobData): Promise<void> {
|
||||
await this.applicationUpgradeService.upgradeAllApplications({
|
||||
applicationRegistrationId: data.applicationRegistrationId,
|
||||
onlyAutoUpgrade: data.onlyAutoUpgrade,
|
||||
});
|
||||
}
|
||||
}
|
||||
+2
@@ -3,6 +3,7 @@ import { type FlatApplication } from 'src/engine/core-modules/application/types/
|
||||
|
||||
export const fromFlatApplicationToApplicationDto = ({
|
||||
canBeUninstalled,
|
||||
autoUpgrade,
|
||||
description,
|
||||
id,
|
||||
logo,
|
||||
@@ -18,6 +19,7 @@ export const fromFlatApplicationToApplicationDto = ({
|
||||
}: FlatApplication): ApplicationDTO => {
|
||||
return {
|
||||
canBeUninstalled,
|
||||
autoUpgrade,
|
||||
description: description ?? undefined,
|
||||
id,
|
||||
logo: logo ?? undefined,
|
||||
|
||||
@@ -14,7 +14,9 @@ import { BillingReminderCronJob } from 'src/engine/core-modules/billing/reminder
|
||||
import { StripeModule } from 'src/engine/core-modules/billing/stripe/stripe.module';
|
||||
import { ApplicationInstallModule } from 'src/engine/core-modules/application/application-install/application-install.module';
|
||||
import { ApplicationRegistrationModule } from 'src/engine/core-modules/application/application-registration/application-registration.module';
|
||||
import { ApplicationUpgradeModule } from 'src/engine/core-modules/application/application-upgrade/application-upgrade.module';
|
||||
import { BackfillApplicationInstallationJob } from 'src/engine/core-modules/application/jobs/backfill-application-installation.job';
|
||||
import { UpgradeApplicationsJob } from 'src/engine/core-modules/application/jobs/upgrade-applications.job';
|
||||
import { PreInstalledAppsModule } from 'src/engine/core-modules/application/pre-installed-apps/pre-installed-apps.module';
|
||||
import { InstallOnboardingAppsJob } from 'src/engine/core-modules/onboarding/jobs/install-onboarding-apps.job';
|
||||
import { OnboardingModule } from 'src/engine/core-modules/onboarding/onboarding.module';
|
||||
@@ -95,6 +97,7 @@ import { WorkflowModule } from 'src/modules/workflow/workflow.module';
|
||||
PreInstalledAppsModule,
|
||||
ApplicationInstallModule,
|
||||
ApplicationRegistrationModule,
|
||||
ApplicationUpgradeModule,
|
||||
OnboardingModule,
|
||||
BillingReminderModule,
|
||||
],
|
||||
@@ -112,6 +115,7 @@ import { WorkflowModule } from 'src/modules/workflow/workflow.module';
|
||||
UpdateWorkspaceMemberEmailJob,
|
||||
GenerateSdkClientJob,
|
||||
BackfillApplicationInstallationJob,
|
||||
UpgradeApplicationsJob,
|
||||
InstallOnboardingAppsJob,
|
||||
],
|
||||
})
|
||||
|
||||
+1
@@ -41,6 +41,7 @@ const MOCK_FLAT_APPLICATION: FlatApplication = {
|
||||
defaultRole: null,
|
||||
settingsCustomTabFrontComponentId: null,
|
||||
canBeUninstalled: false,
|
||||
autoUpgrade: false,
|
||||
applicationRegistrationId: null,
|
||||
primaryPublicDomainId: null,
|
||||
createdAt: new Date(),
|
||||
|
||||
+1
@@ -36,6 +36,7 @@ const MOCK_FLAT_APPLICATION: FlatApplication = {
|
||||
defaultRole: null,
|
||||
settingsCustomTabFrontComponentId: null,
|
||||
canBeUninstalled: false,
|
||||
autoUpgrade: false,
|
||||
applicationRegistrationId: null,
|
||||
primaryPublicDomainId: null,
|
||||
createdAt: new Date(),
|
||||
|
||||
Reference in New Issue
Block a user