Merge branch 'dmp-refactoring' of https://code-repo.d4science.org/MaDgiK-CITE/argos into dmp-refactoring

This commit is contained in:
Sofia Papacharalampous 2024-04-22 16:55:47 +03:00
commit 42a302f77f
47 changed files with 1539 additions and 833 deletions

1
.gitignore vendored
View File

@ -55,3 +55,4 @@ dmp-backend/web/src/main/resources/certificates/
dmp-backend/target/classes/
dmp-backend/core/target/maven-archiver/
dmp-backend/node_modules/.yarn-integrity
dmp-frontend/.nx/

View File

@ -1,11 +1,6 @@
package eu.eudat.authorization;
public final class Permission {
/////// Should Remove after Refactor
public static String AdminRole = "AdminRole";
public static String AuthenticatedRole = "AuthenticatedRole";
/////
public static String DeferredAffiliation = "DeferredAffiliation";
@ -222,5 +217,6 @@ public final class Permission {
public static String ViewMyDmpPage = "ViewMyDmpPage";
public static String ViewHomePage = "ViewHomePage";
public static String ViewMineInAppNotificationPage = "ViewMineInAppNotificationPage";
public static String ViewTenantConfigurationPage = "ViewTenantConfigurationPage";
}

View File

@ -184,4 +184,14 @@ public class ErrorThesaurusProperties {
public void setTenantConfigurationTypeCanNotChange(ErrorDescription tenantConfigurationTypeCanNotChange) {
this.tenantConfigurationTypeCanNotChange = tenantConfigurationTypeCanNotChange;
}
private ErrorDescription multipleTenantConfigurationTypeNotAllowed;
public ErrorDescription getMultipleTenantConfigurationTypeNotAllowed() {
return multipleTenantConfigurationTypeNotAllowed;
}
public void setMultipleTenantConfigurationTypeNotAllowed(ErrorDescription multipleTenantConfigurationTypeNotAllowed) {
this.multipleTenantConfigurationTypeNotAllowed = multipleTenantConfigurationTypeNotAllowed;
}
}

View File

@ -157,19 +157,19 @@ public class TenantConfigurationPersist {
.must(() -> !this.isNull(item.getCssColors()))
.failOn(TenantConfigurationPersist._cssColors).failWith(messageSource.getMessage("Validation_Required", new Object[]{TenantConfigurationPersist._cssColors}, LocaleContextHolder.getLocale())),
this.spec()
.iff(() -> !this.isNull(item.getType()) && TenantConfigurationType.CssColors.equals(item.getType()))
.iff(() -> !this.isNull(item.getType()) && TenantConfigurationType.DefaultUserLocale.equals(item.getType()))
.must(() -> !this.isNull(item.getDefaultUserLocale()))
.failOn(TenantConfigurationPersist._defaultUserLocale).failWith(messageSource.getMessage("Validation_Required", new Object[]{TenantConfigurationPersist._defaultUserLocale}, LocaleContextHolder.getLocale())),
this.spec()
.iff(() -> !this.isNull(item.getType()) && TenantConfigurationType.CssColors.equals(item.getType()))
.iff(() -> !this.isNull(item.getType()) && TenantConfigurationType.DepositPlugins.equals(item.getType()))
.must(() -> !this.isNull(item.getDepositPlugins()))
.failOn(TenantConfigurationPersist._depositPlugins).failWith(messageSource.getMessage("Validation_Required", new Object[]{TenantConfigurationPersist._depositPlugins}, LocaleContextHolder.getLocale())),
this.spec()
.iff(() -> !this.isNull(item.getType()) && TenantConfigurationType.CssColors.equals(item.getType()))
.iff(() -> !this.isNull(item.getType()) && TenantConfigurationType.FileTransformerPlugins.equals(item.getType()))
.must(() -> !this.isNull(item.getFileTransformerPlugins()))
.failOn(TenantConfigurationPersist._fileTransformerPlugins).failWith(messageSource.getMessage("Validation_Required", new Object[]{TenantConfigurationPersist._fileTransformerPlugins}, LocaleContextHolder.getLocale())),
this.spec()
.iff(() -> !this.isNull(item.getType()) && TenantConfigurationType.CssColors.equals(item.getType()))
.iff(() -> !this.isNull(item.getType()) && TenantConfigurationType.Logo.equals(item.getType()))
.must(() -> !this.isNull(item.getLogo()))
.failOn(TenantConfigurationPersist._logo).failWith(messageSource.getMessage("Validation_Required", new Object[]{TenantConfigurationPersist._logo}, LocaleContextHolder.getLocale())),

View File

@ -21,12 +21,14 @@ import eu.eudat.model.persist.deposit.DepositSourcePersist;
import eu.eudat.model.persist.filetransformer.FileTransformerSourcePersist;
import eu.eudat.model.persist.tenantconfiguration.*;
import eu.eudat.model.tenantconfiguration.TenantConfiguration;
import eu.eudat.query.TenantConfigurationQuery;
import eu.eudat.service.encryption.EncryptionService;
import eu.eudat.service.storage.StorageFileService;
import eu.eudat.service.tenant.TenantProperties;
import gr.cite.commons.web.authz.service.AuthorizationService;
import gr.cite.tools.data.builder.BuilderFactory;
import gr.cite.tools.data.deleter.DeleterFactory;
import gr.cite.tools.data.query.QueryFactory;
import gr.cite.tools.exception.MyApplicationException;
import gr.cite.tools.exception.MyForbiddenException;
import gr.cite.tools.exception.MyNotFoundException;
@ -79,6 +81,7 @@ public class TenantConfigurationServiceImpl implements TenantConfigurationServic
private final TenantProperties tenantProperties;
private final StorageFileService storageFileService;
private final QueryFactory queryFactory;
@Autowired
public TenantConfigurationServiceImpl(
TenantEntityManager entityManager,
@ -87,7 +90,7 @@ public class TenantConfigurationServiceImpl implements TenantConfigurationServic
BuilderFactory builderFactory,
ConventionService conventionService,
ErrorThesaurusProperties errors,
MessageSource messageSource, JsonHandlingService jsonHandlingService, EncryptionService encryptionService, TenantProperties tenantProperties, StorageFileService storageFileService) {
MessageSource messageSource, JsonHandlingService jsonHandlingService, EncryptionService encryptionService, TenantProperties tenantProperties, StorageFileService storageFileService, QueryFactory queryFactory) {
this.entityManager = entityManager;
this.authorizationService = authorizationService;
this.deleterFactory = deleterFactory;
@ -99,6 +102,7 @@ public class TenantConfigurationServiceImpl implements TenantConfigurationServic
this.encryptionService = encryptionService;
this.tenantProperties = tenantProperties;
this.storageFileService = storageFileService;
this.queryFactory = queryFactory;
}
public TenantConfiguration persist(TenantConfigurationPersist model, FieldSet fields) throws MyForbiddenException, MyValidationException, MyApplicationException, MyNotFoundException, InvalidApplicationException, JsonProcessingException, InvalidAlgorithmParameterException, NoSuchPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException, BadPaddingException, InvalidKeyException {
@ -122,7 +126,11 @@ public class TenantConfigurationServiceImpl implements TenantConfigurationServic
data.setType(model.getType());
}
TenantConfigurationQuery tenantConfigurationQuery = this.queryFactory.query(TenantConfigurationQuery.class).excludedIds(data.getId()).isActive(IsActive.Active).types(data.getType());
if (data.getTenantId() == null) tenantConfigurationQuery.tenantIsSet(false);
else tenantConfigurationQuery.tenantIsSet(false).tenantIds(data.getTenantId());
if (tenantConfigurationQuery.count() > 0)throw new MyValidationException(this.errors.getMultipleTenantConfigurationTypeNotAllowed().getCode(), this.errors.getMultipleTenantConfigurationTypeNotAllowed().getMessage());
switch (data.getType()){
case CssColors -> data.setValue(this.jsonHandlingService.toJson(this.buildCssColorsTenantConfigurationEntity(model.getCssColors())));
case DefaultUserLocale -> data.setValue(this.jsonHandlingService.toJson(this.buildDefaultUserLocaleTenantConfigurationEntity(model.getDefaultUserLocale())));

View File

@ -3,6 +3,9 @@ package eu.eudat.controllers;
import com.fasterxml.jackson.core.JsonProcessingException;
import eu.eudat.audit.AuditableAction;
import eu.eudat.authorization.AuthorizationFlags;
import eu.eudat.commons.enums.IsActive;
import eu.eudat.commons.enums.TenantConfigurationType;
import eu.eudat.commons.scope.tenant.TenantScope;
import eu.eudat.data.TenantConfigurationEntity;
import eu.eudat.model.DescriptionTemplateType;
import eu.eudat.model.builder.tenantconfiguration.TenantConfigurationBuilder;
@ -56,23 +59,25 @@ public class TenantConfigurationController {
private final QueryFactory queryFactory;
private final MessageSource messageSource;
private final TenantScope tenantScope;
public TenantConfigurationController(
BuilderFactory builderFactory,
AuditService auditService,
TenantConfigurationService tenantConfigurationService, CensorFactory censorFactory,
QueryFactory queryFactory,
MessageSource messageSource) {
BuilderFactory builderFactory,
AuditService auditService,
TenantConfigurationService tenantConfigurationService, CensorFactory censorFactory,
QueryFactory queryFactory,
MessageSource messageSource, TenantScope tenantScope) {
this.builderFactory = builderFactory;
this.auditService = auditService;
this.tenantConfigurationService = tenantConfigurationService;
this.censorFactory = censorFactory;
this.queryFactory = queryFactory;
this.messageSource = messageSource;
this.tenantScope = tenantScope;
}
@PostMapping("query")
public QueryResult<TenantConfiguration> Query(@RequestBody TenantConfigurationLookup lookup) throws MyApplicationException, MyForbiddenException {
public QueryResult<TenantConfiguration> query(@RequestBody TenantConfigurationLookup lookup) throws MyApplicationException, MyForbiddenException {
logger.debug("querying {}", TenantConfiguration.class.getSimpleName());
this.censorFactory.censor(TenantConfigurationCensor.class).censor(lookup.getProject(), null);
@ -89,7 +94,7 @@ public class TenantConfigurationController {
}
@GetMapping("{id}")
public TenantConfiguration Get(@PathVariable("id") UUID id, FieldSet fieldSet, Locale locale) throws MyApplicationException, MyForbiddenException, MyNotFoundException {
public TenantConfiguration get(@PathVariable("id") UUID id, FieldSet fieldSet) throws MyApplicationException, MyForbiddenException, MyNotFoundException {
logger.debug(new MapLogEntry("retrieving" + TenantConfiguration.class.getSimpleName()).And("id", id).And("fields", fieldSet));
this.censorFactory.censor(TenantConfigurationCensor.class).censor(fieldSet, null);
@ -107,10 +112,30 @@ public class TenantConfigurationController {
return model;
}
@GetMapping("current-tenant/{type}")
public TenantConfiguration getCurrentTenantType(@PathVariable("type") Short type, FieldSet fieldSet) throws MyApplicationException, MyForbiddenException, MyNotFoundException, InvalidApplicationException {
logger.debug(new MapLogEntry("retrieving" + TenantConfiguration.class.getSimpleName()).And("type", type).And("fields", fieldSet));
this.censorFactory.censor(TenantConfigurationCensor.class).censor(fieldSet, null);
TenantConfigurationQuery query = this.queryFactory.query(TenantConfigurationQuery.class).authorize(AuthorizationFlags.OwnerOrDmpAssociatedOrPermission).isActive(IsActive.Active).types(TenantConfigurationType.of(type));
if (this.tenantScope.isDefaultTenant()) query.tenantIsSet(false);
else query.tenantIsSet(false).tenantIds(this.tenantScope.getTenant());
TenantConfiguration model = this.builderFactory.builder(TenantConfigurationBuilder.class).authorize(AuthorizationFlags.OwnerOrDmpAssociatedOrPermission).build(fieldSet, query.firstAs(fieldSet));
this.auditService.track(AuditableAction.TenantConfiguration_Lookup, Map.ofEntries(
new AbstractMap.SimpleEntry<String, Object>("type", type),
new AbstractMap.SimpleEntry<String, Object>("fields", fieldSet)
));
return model;
}
@PostMapping("persist")
@Transactional
@ValidationFilterAnnotation(validator = TenantConfigurationPersist.TenantConfigurationPersistValidator.ValidatorName, argumentName = "model")
public TenantConfiguration Persist(@RequestBody TenantConfigurationPersist model, FieldSet fieldSet) throws MyApplicationException, MyForbiddenException, MyNotFoundException, InvalidApplicationException, JsonProcessingException, InvalidAlgorithmParameterException, NoSuchPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException, BadPaddingException, InvalidKeyException {
public TenantConfiguration persist(@RequestBody TenantConfigurationPersist model, FieldSet fieldSet) throws MyApplicationException, MyForbiddenException, MyNotFoundException, InvalidApplicationException, JsonProcessingException, InvalidAlgorithmParameterException, NoSuchPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException, BadPaddingException, InvalidKeyException {
logger.debug(new MapLogEntry("persisting" + DescriptionTemplateType.class.getSimpleName()).And("model", model).And("fieldSet", fieldSet));
TenantConfiguration persisted = this.tenantConfigurationService.persist(model, fieldSet);
@ -124,7 +149,7 @@ public class TenantConfigurationController {
@DeleteMapping("{id}")
@Transactional
public void Delete(@PathVariable("id") UUID id) throws MyForbiddenException, InvalidApplicationException {
public void delete(@PathVariable("id") UUID id) throws MyForbiddenException, InvalidApplicationException {
logger.debug(new MapLogEntry("retrieving" + TenantConfiguration.class.getSimpleName()).And("id", id));
this.tenantConfigurationService.deleteAndSave(id);

View File

@ -62,4 +62,7 @@ error-thesaurus:
tenant-configuration-type-can-not-change:
code: 124
message: Tenant configuration type can not change
multiple-tenant-configuration-type-not-allowed:
code: 125
message: Multiple Tenant Configuration Type Not Allowed

View File

@ -1,20 +1,6 @@
permissions:
extendedClaims: [ ]
policies:
###### Should Remove after Refactor
AdminRole:
roles:
- Admin
clients: [ ]
allowAnonymous: false
allowAuthenticated: false
AuthenticatedRole:
roles: []
clients: [ ]
allowAnonymous: false
allowAuthenticated: true
######
# Affiliation
DeferredAffiliation:
roles:
@ -1027,4 +1013,11 @@ permissions:
roles: [ ]
clients: [ ]
allowAnonymous: false
allowAuthenticated: true
allowAuthenticated: true
ViewTenantConfigurationPage:
roles:
- TenantAdmin
- Admin
clients: [ ]
allowAnonymous: false
allowAuthenticated: false

View File

@ -274,6 +274,18 @@ const appRoutes: Routes = [
})
},
},
{
path: 'tenant-configuration',
loadChildren: () => import('./ui/admin/tenant-configuration/tenant-configuration.module').then(m => m.TenantConfigurationModule),
data: {
authContext: {
permissions: [AppPermission.ViewTenantConfigurationPage]
},
...BreadcrumbService.generateRouteDataConfiguration({
title: 'BREADCRUMBS.TENANT-CONFIGURATION'
})
},
},
{
path: 'notifications',
loadChildren: () => import('@notification-service/ui/admin/notification/notification.module').then(m => m.NotificationModule),

View File

@ -1,75 +1,199 @@
export enum AppPermission {
//DescriptionTemplateType
BrowseDescriptionTemplateType = "BrowseDescriptionTemplateType",
EditDescriptionTemplateType = "EditDescriptionTemplateType",
DeleteDescriptionTemplateType = "DeleteDescriptionTemplateType",
/////
DeferredAffiliation = "DeferredAffiliation",
//DmpBlueprint
BrowseDmpBlueprint = "BrowseDmpBlueprint",
EditDmpBlueprint = "EditDmpBlueprint",
DeleteDmpBlueprint = "DeleteDmpBlueprint",
//Description
BrowseDescription = "BrowseDescription",
EditDescription = "EditDescription",
FinalizeDescription = "FinalizeDescription",
DeleteDescription= "DeleteDescription",
//Dmp
BrowseDmp = "BrowseDmp",
EditDmp = "EditDmp",
NewDmp = "NewDmp",
DeleteDmp = "DeleteDmp",
DepositDmp = "DepositDmp",
CloneDmp = "CloneDmp",
CreateNewVersionDmp = "CreateNewVersionDmp",
ExportDmp = "ExportDmp",
FinalizeDmp = "FinalizeDmp",
AssignDmpUsers = "AssignDmpUsers",
InviteDmpUsers = "InviteDmpUsers",
//DescriptionTemplateType
BrowseDescriptionTemplate = "BrowseDescriptionTemplate",
EditDescriptionTemplate = "EditDescriptionTemplate",
DeleteDescriptionTemplate = "DeleteDescriptionTemplate",
//Public
PublicBrowseDescription = "PublicBrowseDescription",
PublicBrowseDescriptionTemplate = "PublicBrowseDescriptionTemplate",
PublicBrowseDmp = "PublicBrowseDmp",
PublicBrowseDmpReference = "PublicBrowseDmpReference",
PublicBrowseDmpUser = "PublicBrowseDmpUser",
PublicBrowseReference = "PublicBrowseReference",
PublicBrowseUser = "PublicBrowseUser",
PublicBrowseDashboardStatistics = "PublicBrowseDashboardStatistics",
PublicSendContactSupport = "PublicSendContactSupport",
PublicBrowseReferenceType = "PublicBrowseReferenceType",
//Elastic
ManageElastic = "ManageElastic",
//Queue Events
ManageQueueEvents = "ManageQueueEvents",
//ReferenceType
BrowseReferenceType = "BrowseReferenceType",
EditReferenceType = "EditReferenceType",
DeleteReferenceType = "DeleteReferenceType",
//Deposit
BrowseDeposit = "BrowseDeposit",
EditDeposit = "BrowseDeposit",
//Tenant
BrowseTenant = "BrowseTenant",
EditTenant = "EditTenant",
DeleteTenant = "DeleteTenant",
//User
BrowseUser = "BrowseUser",
EditUser = "EditUser",
DeleteUser = "DeleteUser",
ExportUsers = "ExportUsers",
//Reference
BrowseReference = "BrowseReference",
EditReference = "EditReference",
DeleteReference = "DeleteReference",
//Language
BrowseLanguage = "BrowseLanguage",
EditLanguage = "EditLanguage",
DeleteLanguage = "DeleteLanguage",
//Notification Template
//NotificationTemplate
BrowseNotificationTemplate = "BrowseNotificationTemplate",
EditNotificationTemplate = "EditNotificationTemplate",
DeleteNotificationTemplate = "DeleteNotificationTemplate",
//Prefilling Source
BrowsePrefillingSource= "BrowsePrefillingSource",
EditPrefillingSource = "EditPrefillingSource",
//Language
BrowseStatistics = "BrowseStatistics",
BrowsePublicStatistics = "BrowsePublicStatistics",
//DescriptionTemplate
BrowseDescriptionTemplate = "BrowseDescriptionTemplate",
EditDescriptionTemplate = "EditDescriptionTemplate",
DeleteDescriptionTemplate = "DeleteDescriptionTemplate",
CloneDescriptionTemplate = "CloneDescriptionTemplate",
CreateNewVersionDescriptionTemplate = "CreateNewVersionDescriptionTemplate",
ImportDescriptionTemplate = "ImportDescriptionTemplate",
ExportDescriptionTemplate = "ExportDescriptionTemplate",
//User
BrowseUser = "BrowseUser",
EditUser = "EditUser",
DeleteUser = "DeleteUser",
ExportUsers = "ExportUsers",
BrowseDmpAssociatedUser = "BrowseDmpAssociatedUser",
//StorageFile
BrowseStorageFile = "BrowseStorageFile",
EditStorageFile = "EditStorageFile",
DeleteStorageFile = "DeleteStorageFile",
//DescriptionTemplateType
BrowseDescriptionTemplateType = "BrowseDescriptionTemplateType",
EditDescriptionTemplateType = "EditDescriptionTemplateType",
DeleteDescriptionTemplateType = "DeleteDescriptionTemplateType",
//Dmp
BrowseDmp = "BrowseDmp",
EditDmp = "EditDmp",
NewDmp = "NewDmp",
DepositDmp = "DepositDmp",
DeleteDmp = "DeleteDmp",
CloneDmp = "CloneDmp",
ExportDmp = "ExportDmp",
CreateNewVersionDmp = "CreateNewVersionDmp",
FinalizeDmp = "FinalizeDmp",
UndoFinalizeDmp = "UndoFinalizeDmp",
AssignDmpUsers = "AssignDmpUsers",
InviteDmpUsers = "InviteDmpUsers",
//DmpBlueprint
BrowseDmpBlueprint = "BrowseDmpBlueprint",
EditDmpBlueprint = "EditDmpBlueprint",
DeleteDmpBlueprint = "DeleteDmpBlueprint",
CloneDmpBlueprint = "CloneDmpBlueprint",
CreateNewVersionDmpBlueprint = "CreateNewVersionDmpBlueprint",
ExportDmpBlueprint = "ExportDmpBlueprint",
ImportDmpBlueprint = "ImportDmpBlueprint",
//DmpDescriptionTemplate
BrowseDmpDescriptionTemplate = "BrowseDmpDescriptionTemplate",
EditDmpDescriptionTemplate = "EditDmpDescriptionTemplate",
DeleteDmpDescriptionTemplate = "DeleteDmpDescriptionTemplate",
//DmpUser
BrowseDmpUser = "BrowseDmpUser",
EditDmpUser = "EditDmpUser",
DeleteDmpUser = "DeleteDmpUser",
//Description
BrowseDescription = "BrowseDescription",
EditDescription = "EditDescription",
FinalizeDescription = "FinalizeDescription",
DeleteDescription = "DeleteDescription",
CloneDescription = "CloneDescription",
//DescriptionTag
BrowseDescriptionTag = "BrowseDescriptionTag",
EditDescriptionTag = "EditDescriptionTag",
DeleteDescriptionTag = "DeleteDescriptionTag",
//DescriptionTemplateType
BrowseEntityDoi = "BrowseEntityDoi",
EditEntityDoi = "EditEntityDoi",
DeleteEntityDoi = "DeleteEntityDoi",
//UserSettings
BrowseUserSettings = "BrowseUserSettings",
EditUserSettings = "EditUserSettings",
DeleteUserSettings = "DeleteUserSettings",
//Reference
BrowseReference = "BrowseReference",
EditReference = "EditReference",
DeleteReference = "DeleteReference",
//Tag
BrowseTag = "BrowseTag",
EditTag = "EditTag",
DeleteTag = "DeleteTag",
//DmpReference
BrowseDmpReference = "BrowseDmpReference",
EditDmpReference = "EditDmpReference",
DeleteDmpReference = "DeleteDmpReference",
//DescriptionReference
BrowseDescriptionReference = "BrowseDescriptionReference",
EditDescriptionReference = "EditDescriptionReference",
DeleteDescriptionReference = "DeleteDescriptionReference",
//SupportiveMaterial
BrowseSupportiveMaterial = "BrowseSupportiveMaterial",
EditSupportiveMaterial= "EditSupportiveMaterial",
DeleteSupportiveMaterial = "DeleteSupportiveMaterial",
//ReferenceType
BrowseReferenceType = "BrowseReferenceType",
EditReferenceType= "EditReferenceType",
DeleteReferenceType = "DeleteReferenceType",
//Tenant
BrowseTenant = "BrowseTenant",
EditTenant= "EditTenant",
DeleteTenant = "DeleteTenant",
AllowNoTenant = "AllowNoTenant",
//TenantConfiguration
BrowseTenantConfiguration = "BrowseTenantConfiguration",
EditTenantConfiguration = "EditTenantConfiguration",
DeleteTenantConfiguration = "DeleteTenantConfiguration",
//TenantUser
BrowseTenantUser = "BrowseTenantUser",
EditTenantUser = "EditTenantUser",
DeleteTenantUser = "DeleteTenantUser",
//Prefilling
BrowsePrefilling = "BrowsePrefilling",
//Lock
BrowseLock = "BrowseLock",
EditLock = "EditLock",
DeleteLock = "DeleteLock",
//ContactSupport
SendContactSupport = "SendContactSupport",
//ActionConfirmation
BrowseActionConfirmation = "BrowseActionConfirmation",
EditActionConfirmation = "EditActionConfirmation",
DeleteActionConfirmation = "DeleteActionConfirmation",
//PrefillingSource
BrowsePrefillingSource = "BrowsePrefillingSource",
EditPrefillingSource= "EditPrefillingSource",
DeletePrefillingSource = "DeletePrefillingSource",
// UI Pages
ViewDescriptionTemplateTypePage = "ViewDescriptionTemplateTypePage",
ViewMaintenancePage = "ViewMaintenancePage",
@ -91,5 +215,6 @@ export enum AppPermission {
ViewMyDmpPage = "ViewMyDmpPage",
ViewHomePage = "ViewHomePage",
ViewMineInAppNotificationPage = "ViewMineInAppNotificationPage",
ViewTenantConfigurationPage = "ViewTenantConfigurationPage",
}

View File

@ -13,5 +13,8 @@ export enum ResponseErrorCode {
DmpBlueprintHasNoDescriptionTemplates = 120,
DmpBlueprintNewVersionConflict = 121,
DmpDescriptionTemplateCanNotRemove = 122,
TenantTampering = 123
}
TenantTampering = 123,
TenantConfigurationTypeCanNotChange = 124,
MultipleTenantConfigurationTypeNotAllowed = 125,
}

View File

@ -0,0 +1,7 @@
export enum TenantConfigurationType {
DepositPlugins = 0,
FileTransformerPlugins = 1,
DefaultUserLocale = 2,
Logo = 3,
CssColors = 4,
}

View File

@ -46,6 +46,7 @@ import { SemanticsService } from './services/semantic/semantics.service';
import { PrefillingSourceService } from './services/prefilling-source/prefilling-source.service';
import { VisibilityRulesService } from '@app/ui/description/editor/description-form/visibility-rules/visibility-rules.service';
import { StorageFileService } from './services/storage-file/storage-file.service';
import { TenantConfigurationService } from './services/tenant-configuration/tenant-configuration.service';
//
//
// This is shared module that provides all the services. Its imported only once on the AppModule.
@ -110,6 +111,7 @@ export class CoreServiceModule {
SemanticsService,
PrefillingSourceService,
VisibilityRulesService,
TenantConfigurationService,
StorageFileService
],
};

View File

@ -0,0 +1,111 @@
import { TenantConfigurationType } from "@app/core/common/enum/tenant-configuration-type";
import { BaseEntity, BaseEntityPersist } from "@common/base/base-entity.model";
import { StorageFile } from "../storage-file/storage-file";
import { Guid } from "@common/types/guid";
export interface TenantConfiguration extends BaseEntity{
type?: TenantConfigurationType;
cssColors?: CssColorsTenantConfiguration;
defaultUserLocale?: DefaultUserLocaleTenantConfiguration;
depositPlugins?: DepositTenantConfiguration;
fileTransformerPlugins?: FileTransformerTenantConfiguration;
logo?: LogoTenantConfiguration;
}
export interface CssColorsTenantConfiguration{
primaryColor: string;
primaryColor2: string;
primaryColor3: string;
secondaryColor: string;
}
export interface DefaultUserLocaleTenantConfiguration{
timezone: string;
language: string;
culture: string;
}
export interface DepositTenantConfiguration{
sources: DepositSource[];
}
export interface FileTransformerTenantConfiguration{
sources: FileTransformerSource[];
}
export interface DepositSource{
repositoryId: string;
url: string;
issuerUrl: string;
clientId: string;
clientSecret: string;
scope: string;
pdfTransformerId: string;
rdaTransformerId: string;
}
export interface FileTransformerSource{
transformerId: string;
url: string;
issuerUrl: string;
clientId: string;
clientSecret: string;
scope: string;
}
export interface LogoTenantConfiguration{
storageFile: StorageFile;
}
//persist
export interface TenantConfigurationPersist extends BaseEntityPersist{
type: TenantConfigurationType;
cssColors?: CssColorsTenantConfigurationPersist;
defaultUserLocale?: DefaultUserLocaleTenantConfigurationPersist;
depositPlugins?: DepositTenantConfigurationPersist;
fileTransformerPlugins?: FileTransformerTenantConfigurationPersist;
logo?: LogoTenantConfigurationPersist;
}
export interface CssColorsTenantConfigurationPersist{
primaryColor: string;
primaryColor2: string;
primaryColor3: string;
secondaryColor: string;
}
export interface DefaultUserLocaleTenantConfigurationPersist{
timezone: string;
language: string;
culture: string;
}
export interface DepositTenantConfigurationPersist{
sources: DepositSourcePersist[];
}
export interface FileTransformerTenantConfigurationPersist{
sources: FileTransformerSourcePersist[];
}
export interface DepositSourcePersist{
repositoryId: string;
url: string;
issuerUrl: string;
clientId: string;
clientSecret: string;
scope: string;
pdfTransformerId: string;
rdaTransformerId: string;
}
export interface FileTransformerSourcePersist{
transformerId: string;
url: string;
issuerUrl: string;
clientId: string;
clientSecret: string;
scope: string;
}
export interface LogoTenantConfigurationPersist{
storageFileId: Guid;
}

View File

@ -4,33 +4,6 @@ export interface Tenant extends BaseEntity{
name?: string;
code?: string;
description?: string;
config?: TenantConfig;
}
export interface TenantConfig{
deposit: TenantDepositConfig;
fileTransformers: TenantFileTransformersConfig;
}
export interface TenantDepositConfig{
sources: TenantSource[];
}
export interface TenantFileTransformersConfig{
sources: TenantSource[];
}
export interface TenantSource{
url: string;
codes: string[];
issuerUrl: string;
clientId: string;
clientSecret: string;
scope: string;
}
export interface SourceCode{
code: string;
}
//persist
@ -39,31 +12,5 @@ export interface TenantPersist extends BaseEntityPersist{
name: string;
code: string;
description: string;
config?: TenantConfigPersist;
}
export interface TenantConfigPersist{
deposit: TenantDepositConfigPersist;
fileTransformers: TenantFileTransformersConfigPersist;
}
export interface TenantDepositConfigPersist{
sources: TenantSourcePersist[];
}
export interface TenantFileTransformersConfigPersist{
sources: TenantSourcePersist[];
}
export interface TenantSourcePersist{
url: string;
codes: string[];
issuerUrl: string;
clientId: string;
clientSecret: string;
scope: string;
}
export interface SourceCodePersist{
code: string;
}

View File

@ -0,0 +1,26 @@
import { Lookup } from '@common/model/lookup';
import { Guid } from '@common/types/guid';
import { IsActive } from '../common/enum/is-active.enum';
import { TenantConfigurationType } from '../common/enum/tenant-configuration-type';
export class TenantConfigurationLookup extends Lookup implements TenantConfigurationFilter {
ids: Guid[];
excludedIds: Guid[];
types: TenantConfigurationType[];
isActive: IsActive[];
tenantIds: Guid[];
tenantIsSet: boolean;
constructor() {
super();
}
}
export interface TenantConfigurationFilter {
ids: Guid[];
excludedIds: Guid[];
types: TenantConfigurationType[];
tenantIds: Guid[];
tenantIsSet: boolean;
isActive: IsActive[];
}

View File

@ -0,0 +1,60 @@
import { Injectable } from '@angular/core';
import { QueryResult } from '@common/model/query-result';
import { FilterService } from '@common/modules/text-filter/filter-service';
import { Guid } from '@common/types/guid';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { ConfigurationService } from '../configuration/configuration.service';
import { BaseHttpV2Service } from '../http/base-http-v2.service';
import { TenantConfigurationLookup } from '@app/core/query/tenant-configuration.lookup';
import { TenantConfiguration, TenantConfigurationPersist } from '@app/core/model/tenant-configuaration/tenant-configuration';
import { TenantConfigurationType } from '@app/core/common/enum/tenant-configuration-type';
@Injectable()
export class TenantConfigurationService {
constructor(private http: BaseHttpV2Service, private configurationService: ConfigurationService, private filterService: FilterService) {
}
private get apiBase(): string { return `${this.configurationService.server}tenant-configuration`; }
query(q: TenantConfigurationLookup): Observable<QueryResult<TenantConfiguration>> {
const url = `${this.apiBase}/query`;
return this.http.post<QueryResult<TenantConfiguration>>(url, q).pipe(catchError((error: any) => throwError(error)));
}
getSingle(id: Guid, reqFields: string[] = []): Observable<TenantConfiguration> {
const url = `${this.apiBase}/${id}`;
const options = { params: { f: reqFields } };
return this.http
.get<TenantConfiguration>(url, options).pipe(
catchError((error: any) => throwError(error)));
}
getCurrentTenantType(type: TenantConfigurationType, reqFields: string[] = []): Observable<TenantConfiguration> {
const url = `${this.apiBase}/current-tenant/${type}`;
const options = { params: { f: reqFields } };
return this.http
.get<TenantConfiguration>(url, options).pipe(
catchError((error: any) => throwError(error)));
}
persist(item: TenantConfigurationPersist): Observable<TenantConfiguration> {
const url = `${this.apiBase}/persist`;
return this.http
.post<TenantConfiguration>(url, item).pipe(
catchError((error: any) => throwError(error)));
}
delete(id: Guid): Observable<void> {
const url = `${this.apiBase}/${id}`;
return this.http
.delete<void>(url).pipe(
catchError((error: any) => throwError(error)));
}
}

View File

@ -0,0 +1,43 @@
<div *ngIf="formGroup" class="container-fluid css-colors">
<div class="row">
<div class="col-12">
<mat-form-field class="w-100">
<mat-label>{{'TENANT-CONFIGURATION-EDITOR.FIELDS.PRIMARY-COLOR' | translate}}</mat-label>
<input matInput type="text" name="primaryColor" [formControl]="formGroup.get('cssColors')?.get('primaryColor')" required>
<mat-error *ngIf="formGroup.get('cssColors')?.get('primaryColor')?.hasError('backendError')">{{formGroup.get('cssColors')?.get('primaryColor')?.getError('backendError').message}}</mat-error>
<mat-error *ngIf="formGroup.get('cssColors')?.get('primaryColor')?.hasError('required')">{{'GENERAL.VALIDATION.REQUIRED' | translate}}</mat-error>
</mat-form-field>
<mat-form-field class="w-100">
<mat-label>{{'TENANT-CONFIGURATION-EDITOR.FIELDS.PRIMARY-COLOR-2' | translate}}</mat-label>
<input matInput type="text" name="primaryColor2" [formControl]="formGroup.get('cssColors')?.get('primaryColor2')" required>
<mat-error *ngIf="formGroup.get('cssColors')?.get('primaryColor2')?.hasError('backendError')">{{formGroup.get('cssColors')?.get('primaryColor2')?.getError('backendError').message}}</mat-error>
<mat-error *ngIf="formGroup.get('cssColors')?.get('primaryColor2')?.hasError('required')">{{'GENERAL.VALIDATION.REQUIRED' | translate}}</mat-error>
</mat-form-field>
<mat-form-field class="w-100">
<mat-label>{{'TENANT-CONFIGURATION-EDITOR.FIELDS.PRIMARY-COLOR-3' | translate}}</mat-label>
<input matInput type="text" name="primaryColor3" [formControl]="formGroup.get('cssColors')?.get('primaryColor3')" required>
<mat-error *ngIf="formGroup.get('cssColors')?.get('primaryColor3')?.hasError('backendError')">{{formGroup.get('cssColors')?.get('primaryColor3')?.getError('backendError').message}}</mat-error>
<mat-error *ngIf="formGroup.get('cssColors')?.get('primaryColor3')?.hasError('required')">{{'GENERAL.VALIDATION.REQUIRED' | translate}}</mat-error>
</mat-form-field>
<mat-form-field class="w-100">
<mat-label>{{'TENANT-CONFIGURATION-EDITOR.FIELDS.SECONDARY-COLOR' | translate}}</mat-label>
<input matInput type="text" name="secondaryColor" [formControl]="formGroup.get('cssColors')?.get('secondaryColor')" required>
<mat-error *ngIf="formGroup.get('cssColors')?.get('secondaryColor')?.hasError('backendError')">{{formGroup.get('cssColors')?.get('secondaryColor')?.getError('backendError').message}}</mat-error>
<mat-error *ngIf="formGroup.get('cssColors')?.get('secondaryColor')?.hasError('required')">{{'GENERAL.VALIDATION.REQUIRED' | translate}}</mat-error>
</mat-form-field>
</div>
<div class="col-12">
<div class="row actions-row">
<div class="col"></div>
<div class="col-auto" *ngIf="editorModel.id"><button mat-raised-button color="primary" (click)="delete()">
{{'TENANT-CONFIGURATION-EDITOR.ACTIONS.RESET-TO-DEFAULT' | translate}}
</button>
</div>
<div class="col-auto"><button mat-raised-button color="primary" (click)="formSubmit()">
{{'TENANT-CONFIGURATION-EDITOR.ACTIONS.SAVE' | translate}}
</button>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,207 @@
import { Component, OnInit } from '@angular/core';
import { UntypedFormGroup } from '@angular/forms';
import { MatDialog } from '@angular/material/dialog';
import { SnackBarNotificationLevel, UiNotificationService } from '@app/core/services/notification/ui-notification-service';
import { AppPermission } from '@app/core/common/enum/permission.enum';
import { AuthService } from '@app/core/services/auth/auth.service';
import { MatomoService } from '@app/core/services/matomo/matomo-service';
import { FormService } from '@common/forms/form-service';
import { ConfirmationDialogComponent } from '@common/modules/confirmation-dialog/confirmation-dialog.component';
import { HttpError, HttpErrorHandlingService } from '@common/modules/errors/error-handling/http-error-handling.service';
import { TranslateService } from '@ngx-translate/core';
import { map, takeUntil } from 'rxjs/operators';
import { TenantConfigurationEditorModel } from './css-colors-editor.model';
import { TenantConfiguration, TenantConfigurationPersist } from '@app/core/model/tenant-configuaration/tenant-configuration';
import { TenantConfigurationService } from '@app/core/services/tenant-configuration/tenant-configuration.service';
import { CssColorsEditorService } from './css-colors-editor.service';
import { CssColorsEditorResolver } from './css-colors-editor.resolver';
import { BasePendingChangesComponent } from '@common/base/base-pending-changes.component';
import { Observable } from 'rxjs';
import { TenantConfigurationType } from '@app/core/common/enum/tenant-configuration-type';
import { HttpErrorResponse } from '@angular/common/http';
import { ResponseErrorCode } from '@app/core/common/enum/respone-error-code';
import { LoggingService } from '@app/core/services/logging/logging-service';
@Component({
selector: 'app-tenant-configuration-css-colors-editor',
templateUrl: 'css-colors-editor.component.html',
styleUrls: ['./css-colors-editor.component.scss'],
providers: [CssColorsEditorService]
})
export class CssColorsEditorComponent extends BasePendingChangesComponent implements OnInit {
isNew = true;
formGroup: UntypedFormGroup = null;
get editorModel(): TenantConfigurationEditorModel { return this._editorModel; }
set editorModel(value: TenantConfigurationEditorModel) { this._editorModel = value; }
private _editorModel: TenantConfigurationEditorModel;
protected get canDelete(): boolean {
return !this.isNew && this.hasPermission(this.authService.permissionEnum.DeleteTenantConfiguration);
}
protected get canSave(): boolean {
return this.hasPermission(this.authService.permissionEnum.EditTenantConfiguration);
}
private hasPermission(permission: AppPermission): boolean {
return this.authService.hasPermission(permission);
}
constructor(
protected dialog: MatDialog,
protected language: TranslateService,
protected formService: FormService,
protected uiNotificationService: UiNotificationService,
protected httpErrorHandlingService: HttpErrorHandlingService,
protected authService: AuthService,
private logger: LoggingService,
private tenantConfigurationService: TenantConfigurationService,
private cssColorsEditorService: CssColorsEditorService,
private matomoService: MatomoService
) {
super();
}
canDeactivate(): boolean | Observable<boolean> {
return this.formGroup ? !this.formGroup.dirty : true;
}
ngOnInit(): void {
this.matomoService.trackPageView('Admin: TenantConfigurations');
this.getItem((entity) => {
this.prepareForm(entity);
if (this.formGroup && this.editorModel.belongsToCurrentTenant == false) {
this.formGroup.disable();
}
});
}
getItem(successFunction: (item: TenantConfiguration) => void) {
this.tenantConfigurationService.getCurrentTenantType(TenantConfigurationType.CssColors, CssColorsEditorResolver.lookupFields())
.pipe(map(data => data as TenantConfiguration), takeUntil(this._destroyed))
.subscribe(
data => successFunction(data),
error => this.onCallbackError(error)
);
}
onCallbackError(errorResponse: HttpErrorResponse) {
console.log("Error:", errorResponse);
const error: HttpError = this.httpErrorHandlingService.getError(errorResponse);
if (error.statusCode === 400) {
this.editorModel.validationErrorModel.fromJSONObject(errorResponse.error);
if(errorResponse.error.code === ResponseErrorCode.TenantConfigurationTypeCanNotChange){
this.uiNotificationService.snackBarNotification(errorResponse.error.error, SnackBarNotificationLevel.Error);
}
if(errorResponse.error.code === ResponseErrorCode.MultipleTenantConfigurationTypeNotAllowed){
this.uiNotificationService.snackBarNotification(errorResponse.error.error, SnackBarNotificationLevel.Error);
}
this.formService.validateAllFormFields(this.formGroup);
} else {
this.uiNotificationService.snackBarNotification(error.getMessagesString(), SnackBarNotificationLevel.Warning);
}
}
onCallbackSuccess(data?: any): void {
console.log("Success:", data);
this.uiNotificationService.snackBarNotification(this.isNew ? this.language.instant('GENERAL.SNACK-BAR.SUCCESSFUL-CREATION') : this.language.instant('GENERAL.SNACK-BAR.SUCCESSFUL-UPDATE'), SnackBarNotificationLevel.Success);
this.refreshData();
}
prepareForm(data: TenantConfiguration) {
try {
this.editorModel = data ? new TenantConfigurationEditorModel().fromModel(data) : new TenantConfigurationEditorModel();
this.buildForm();
} catch (error) {
this.logger.error('Could not parse TenantConfiguration item: ' + data + error);
this.uiNotificationService.snackBarNotification(this.language.instant('COMMONS.ERRORS.DEFAULT'), SnackBarNotificationLevel.Error);
}
}
buildForm() {
this.formGroup = this.editorModel.buildForm(null, !this.authService.hasPermission(AppPermission.EditTenantConfiguration));
this.cssColorsEditorService.setValidationErrorModel(this.editorModel.validationErrorModel);
}
refreshData(): void {
this.getItem((entity) => {
this.prepareForm(entity);
if (this.formGroup && this.editorModel.belongsToCurrentTenant == false) {
this.formGroup.disable();
}
});
}
persistEntity(onSuccess?: (response) => void): void {
const formData = this.formService.getValue(this.formGroup.value) as TenantConfigurationPersist;
this.tenantConfigurationService.persist(formData)
.pipe(takeUntil(this._destroyed)).subscribe(
complete => onSuccess ? onSuccess(complete) : this.onCallbackSuccess(complete),
error => this.onCallbackError(error)
);
}
formSubmit(): void {
this.clearErrorModel();
this.formService.removeAllBackEndErrors(this.formGroup);
this.formService.touchAllFormFields(this.formGroup);
if (!this.isFormValid()) {
return;
}
this.persistEntity();
}
public isFormValid() {
return this.formGroup.valid;
}
public delete() {
const value = this.formGroup.value;
if (value.id) {
const dialogRef = this.dialog.open(ConfirmationDialogComponent, {
maxWidth: '300px',
data: {
message: this.language.instant('GENERAL.CONFIRMATION-DIALOG.DELETE-ITEM'),
confirmButton: this.language.instant('GENERAL.CONFIRMATION-DIALOG.ACTIONS.CONFIRM'),
cancelButton: this.language.instant('GENERAL.CONFIRMATION-DIALOG.ACTIONS.CANCEL')
}
});
dialogRef.afterClosed().pipe(takeUntil(this._destroyed)).subscribe(result => {
if (result) {
this.tenantConfigurationService.delete(value.id).pipe(takeUntil(this._destroyed))
.subscribe(
complete => this.onCallbackDeleteSuccessConfig(),
error => this.onCallbackError(error)
);
}
});
}
}
onCallbackDeleteSuccessConfig(data?: any): void {
console.log("Success Delete:", data);
this.uiNotificationService.snackBarNotification(this.language.instant('GENERAL.SNACK-BAR.SUCCESSFUL-DELETE'), SnackBarNotificationLevel.Success);
this.prepareForm(null)
}
clearErrorModel() {
this.editorModel.validationErrorModel.clear();
this.formService.validateAllFormFields(this.formGroup);
}
}

View File

@ -0,0 +1,116 @@
import { UntypedFormBuilder, UntypedFormGroup, Validators } from "@angular/forms";
import { TenantConfigurationType } from "@app/core/common/enum/tenant-configuration-type";
import { CssColorsTenantConfiguration, CssColorsTenantConfigurationPersist, TenantConfiguration, TenantConfigurationPersist } from "@app/core/model/tenant-configuaration/tenant-configuration";
import { BaseEditorModel } from "@common/base/base-form-editor-model";
import { BackendErrorValidator } from "@common/forms/validation/custom-validator";
import { ValidationErrorModel } from "@common/forms/validation/error-model/validation-error-model";
import { Validation, ValidationContext } from "@common/forms/validation/validation-context";
export class TenantConfigurationEditorModel extends BaseEditorModel implements TenantConfigurationPersist {
type: TenantConfigurationType;
cssColors: CssColorsTenantConfigurationEditorModel = new CssColorsTenantConfigurationEditorModel(this.validationErrorModel);
public validationErrorModel: ValidationErrorModel = new ValidationErrorModel();
protected formBuilder: UntypedFormBuilder = new UntypedFormBuilder();
constructor() { super(); this.type = TenantConfigurationType.CssColors; }
public fromModel(item: TenantConfiguration): TenantConfigurationEditorModel {
if (item) {
super.fromModel(item);
this.type = item.type;
if (item.cssColors) this.cssColors = new CssColorsTenantConfigurationEditorModel(this.validationErrorModel).fromModel(item.cssColors);
} else {
this.type = TenantConfigurationType.CssColors;
}
return this;
}
buildForm(context: ValidationContext = null, disabled: boolean = false): UntypedFormGroup {
if (context == null) { context = this.createValidationContext(); }
return this.formBuilder.group({
id: [{ value: this.id, disabled: disabled }, context.getValidation('id').validators],
type: [{ value: this.type, disabled: disabled }, context.getValidation('type').validators],
hash: [{ value: this.hash, disabled: disabled }, context.getValidation('hash').validators],
cssColors: this.cssColors.buildForm({
rootPath: `cssColors.`,
}),
});
}
createValidationContext(): ValidationContext {
const baseContext: ValidationContext = new ValidationContext();
const baseValidationArray: Validation[] = new Array<Validation>();
baseValidationArray.push({ key: 'id', validators: [BackendErrorValidator(this.validationErrorModel, 'id')] });
baseValidationArray.push({ key: 'type', validators: [Validators.required, BackendErrorValidator(this.validationErrorModel, 'type')] });
baseValidationArray.push({ key: 'hash', validators: [] });
baseContext.validation = baseValidationArray;
return baseContext;
}
}
export class CssColorsTenantConfigurationEditorModel implements CssColorsTenantConfigurationPersist {
primaryColor: string;
primaryColor2: string;
primaryColor3: string;
secondaryColor: string;
protected formBuilder: UntypedFormBuilder = new UntypedFormBuilder();
constructor(
public validationErrorModel: ValidationErrorModel = new ValidationErrorModel()
) { }
public fromModel(item: CssColorsTenantConfiguration): CssColorsTenantConfigurationEditorModel {
if (item) {
this.primaryColor = item.primaryColor;
this.primaryColor2 = item.primaryColor2;
this.primaryColor3 = item.primaryColor3;
this.secondaryColor = item.secondaryColor;
}
return this;
}
buildForm(params?: {
context?: ValidationContext,
disabled?: boolean,
rootPath?: string
}): UntypedFormGroup {
let { context = null, disabled = false, rootPath } = params ?? {}
if (context == null) {
context = CssColorsTenantConfigurationEditorModel.createValidationContext({
validationErrorModel: this.validationErrorModel,
rootPath
});
}
const form: UntypedFormGroup = this.formBuilder.group({
primaryColor: [{ value: this.primaryColor, disabled: disabled }, context.getValidation('primaryColor').validators],
primaryColor2: [{ value: this.primaryColor2, disabled: disabled }, context.getValidation('primaryColor2').validators],
primaryColor3: [{ value: this.primaryColor3, disabled: disabled }, context.getValidation('primaryColor3').validators],
secondaryColor: [{ value: this.secondaryColor, disabled: disabled }, context.getValidation('secondaryColor').validators],
});
return form;
}
static createValidationContext(params: {
rootPath?: string,
validationErrorModel: ValidationErrorModel
}): ValidationContext {
const { rootPath = '', validationErrorModel } = params;
const baseContext: ValidationContext = new ValidationContext();
const baseValidationArray: Validation[] = new Array<Validation>();
baseValidationArray.push({ key: 'primaryColor', validators: [Validators.required, BackendErrorValidator(validationErrorModel, `${rootPath}primaryColor`)] });
baseValidationArray.push({ key: 'primaryColor2', validators: [Validators.required, BackendErrorValidator(validationErrorModel, `${rootPath}primaryColor2`)] });
baseValidationArray.push({ key: 'primaryColor3', validators: [Validators.required, BackendErrorValidator(validationErrorModel, `${rootPath}primaryColor3`)] });
baseValidationArray.push({ key: 'secondaryColor', validators: [Validators.required, BackendErrorValidator(validationErrorModel, `${rootPath}secondaryColor`)] });
baseContext.validation = baseValidationArray;
return baseContext;
}
}

View File

@ -0,0 +1,46 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { TenantConfigurationType } from '@app/core/common/enum/tenant-configuration-type';
import { CssColorsTenantConfiguration, TenantConfiguration } from '@app/core/model/tenant-configuaration/tenant-configuration';
import { TenantConfigurationService } from '@app/core/services/tenant-configuration/tenant-configuration.service';
import { BreadcrumbService } from '@app/ui/misc/breadcrumb/breadcrumb.service';
import { BaseEditorResolver } from '@common/base/base-editor.resolver';
import { takeUntil, tap } from 'rxjs/operators';
import { nameof } from 'ts-simple-nameof';
@Injectable()
export class CssColorsEditorResolver extends BaseEditorResolver {
constructor(private tenantConfigurationService: TenantConfigurationService, private breadcrumbService: BreadcrumbService) {
super();
}
public static lookupFields(): string[] {
return [
...BaseEditorResolver.lookupFields(),
nameof<TenantConfiguration>(x => x.id),
nameof<TenantConfiguration>(x => x.type),
nameof<TenantConfiguration>(x => x.cssColors),
[nameof<TenantConfiguration>(x => x.cssColors), nameof<CssColorsTenantConfiguration>(x => x.primaryColor)].join('.'),
[nameof<TenantConfiguration>(x => x.cssColors), nameof<CssColorsTenantConfiguration>(x => x.primaryColor2)].join('.'),
[nameof<TenantConfiguration>(x => x.cssColors), nameof<CssColorsTenantConfiguration>(x => x.primaryColor3)].join('.'),
[nameof<TenantConfiguration>(x => x.cssColors), nameof<CssColorsTenantConfiguration>(x => x.secondaryColor)].join('.'),
nameof<TenantConfiguration>(x => x.createdAt),
nameof<TenantConfiguration>(x => x.updatedAt),
nameof<TenantConfiguration>(x => x.hash),
nameof<TenantConfiguration>(x => x.isActive)
]
}
resolve() {
const fields = [
...CssColorsEditorResolver.lookupFields()
];
return this.tenantConfigurationService.getCurrentTenantType(TenantConfigurationType.CssColors, fields).pipe(takeUntil(this._destroyed));
}
}

View File

@ -0,0 +1,15 @@
import { Injectable } from "@angular/core";
import { ValidationErrorModel } from "@common/forms/validation/error-model/validation-error-model";
@Injectable()
export class CssColorsEditorService {
private validationErrorModel: ValidationErrorModel;
public setValidationErrorModel(validationErrorModel: ValidationErrorModel): void {
this.validationErrorModel = validationErrorModel;
}
public getValidationErrorModel(): ValidationErrorModel {
return this.validationErrorModel;
}
}

View File

@ -0,0 +1,49 @@
<div *ngIf="formGroup" class="container-fluid default-user-locale">
<div class="row">
<div class="col-12">
<mat-form-field class="w-100">
<mat-label>{{'TENANT-CONFIGURATION-EDITOR.FIELDS.TIMEZONE' | translate}}</mat-label>
<mat-select [formControl]="this.formGroup.get('defaultUserLocale')?.get('timezone')" name="culture">
<mat-option *ngFor="let timezone of timezones" [value]="timezone">
{{ timezone | timezoneInfoDisplay }}
</mat-option>
</mat-select>
<mat-error *ngIf="formGroup.get('defaultUserLocale')?.get('timezone').hasError('required')">{{'GENERAL.VALIDATION.REQUIRED' | translate}}</mat-error>
<mat-error *ngIf="formGroup.get('defaultUserLocale')?.get('timezone')?.hasError('backendError')">{{formGroup.get('defaultUserLocale')?.get('timezone')?.getError('backendError').message}}</mat-error>
</mat-form-field>
<mat-form-field class="w-100">
<mat-label>{{'TENANT-CONFIGURATION-EDITOR.FIELDS.CULTURE' | translate}}</mat-label>
<mat-select [formControl]="this.formGroup.get('defaultUserLocale')?.get('culture')" name="culture">
<mat-option *ngFor="let culture of cultures" [value]="culture.name">
{{ culture.displayName }} - {{ culture.nativeName }}
</mat-option>
</mat-select>
<mat-error *ngIf="formGroup.get('defaultUserLocale')?.get('culture').hasError('required')">{{'GENERAL.VALIDATION.REQUIRED' | translate}}</mat-error>
<mat-error *ngIf="formGroup.get('defaultUserLocale')?.get('culture')?.hasError('backendError')">{{formGroup.get('defaultUserLocale')?.get('culture')?.getError('backendError').message}}</mat-error>
</mat-form-field>
<mat-form-field class="w-100">
<mat-label>{{'TENANT-CONFIGURATION-EDITOR.FIELDS.LANGUAGE' | translate}}</mat-label>
<mat-select [formControl]="this.formGroup.get('defaultUserLocale')?.get('language')" name="language">
<mat-option *ngFor="let language of languages" [value]="language">
{{ "GENERAL.LANGUAGES."+ language | translate }}
</mat-option>
</mat-select>
<mat-error *ngIf="this.formGroup.get('defaultUserLocale')?.get('language')?.hasError('required')">{{'GENERAL.VALIDATION.REQUIRED' | translate}}</mat-error>
<mat-error *ngIf="formGroup.get('defaultUserLocale')?.get('language')?.hasError('backendError')">{{formGroup.get('defaultUserLocale')?.get('language')?.getError('backendError').message}}</mat-error>
</mat-form-field>
</div>
<div class="col-12">
<div class="row actions-row">
<div class="col"></div>
<div class="col-auto" *ngIf="editorModel.id"><button mat-raised-button color="primary" (click)="delete()">
{{'TENANT-CONFIGURATION-EDITOR.ACTIONS.RESET-TO-DEFAULT' | translate}}
</button>
</div>
<div class="col-auto"><button mat-raised-button color="primary" (click)="formSubmit()">
{{'TENANT-CONFIGURATION-EDITOR.ACTIONS.SAVE' | translate}}
</button>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,229 @@
import { Component, OnInit } from '@angular/core';
import { UntypedFormGroup } from '@angular/forms';
import { MatDialog } from '@angular/material/dialog';
import { SnackBarNotificationLevel, UiNotificationService } from '@app/core/services/notification/ui-notification-service';
// import { BreadcrumbItem } from '@app/ui/misc/breadcrumb/definition/breadcrumb-item';
import { AppPermission } from '@app/core/common/enum/permission.enum';
import { AuthService } from '@app/core/services/auth/auth.service';
import { MatomoService } from '@app/core/services/matomo/matomo-service';
import { FormService } from '@common/forms/form-service';
import { ConfirmationDialogComponent } from '@common/modules/confirmation-dialog/confirmation-dialog.component';
import { HttpError, HttpErrorHandlingService } from '@common/modules/errors/error-handling/http-error-handling.service';
import { TranslateService } from '@ngx-translate/core';
import { map, takeUntil } from 'rxjs/operators';
import { TenantConfigurationEditorModel } from './default-user-locale-editor.model';
import { TenantConfiguration, TenantConfigurationPersist } from '@app/core/model/tenant-configuaration/tenant-configuration';
import { TenantConfigurationService } from '@app/core/services/tenant-configuration/tenant-configuration.service';
import { DefaultUserLocaleEditorService } from './default-user-locale-editor.service';
import { DefaultUserLocaleEditorResolver } from './default-user-locale-editor.resolver';
import { BasePendingChangesComponent } from '@common/base/base-pending-changes.component';
import { Observable, of } from 'rxjs';
import { TenantConfigurationType } from '@app/core/common/enum/tenant-configuration-type';
import { HttpErrorResponse } from '@angular/common/http';
import { ResponseErrorCode } from '@app/core/common/enum/respone-error-code';
import { LoggingService } from '@app/core/services/logging/logging-service';
import { CultureInfo } from '@app/core/model/culture-info';
import * as moment from 'moment';
import { CultureService } from '@app/core/services/culture/culture-service';
import { LanguageService } from '@app/core/services/language/language.service';
@Component({
selector: 'app-tenant-configuration-default-user-locale-editor',
templateUrl: 'default-user-locale-editor.component.html',
styleUrls: ['./default-user-locale-editor.component.scss'],
providers: [DefaultUserLocaleEditorService]
})
export class DefaultUserLocaleEditorComponent extends BasePendingChangesComponent implements OnInit {
isNew = true;
formGroup: UntypedFormGroup = null;
get editorModel(): TenantConfigurationEditorModel { return this._editorModel; }
set editorModel(value: TenantConfigurationEditorModel) { this._editorModel = value; }
private _editorModel: TenantConfigurationEditorModel;
timezones: any[];
cultures: CultureInfo[];
languages = [];
protected get canDelete(): boolean {
return !this.isNew && this.hasPermission(this.authService.permissionEnum.DeleteTenantConfiguration);
}
protected get canSave(): boolean {
return this.hasPermission(this.authService.permissionEnum.EditTenantConfiguration);
}
private hasPermission(permission: AppPermission): boolean {
return this.authService.hasPermission(permission);
}
constructor(
protected dialog: MatDialog,
protected language: TranslateService,
protected formService: FormService,
protected uiNotificationService: UiNotificationService,
protected httpErrorHandlingService: HttpErrorHandlingService,
protected authService: AuthService,
private logger: LoggingService,
private cultureService: CultureService,
private tenantConfigurationService: TenantConfigurationService,
private languageService: LanguageService,
private defaultUserLocaleEditorService: DefaultUserLocaleEditorService,
private matomoService: MatomoService
) {
super();
this.languages = this.languageService.getAvailableLanguagesCodes().sort((x, y) => x.localeCompare(y));
this.cultures = this.cultureService.getCultureValues().sort((x, y) => x.displayName.localeCompare(y.displayName));
this.timezones = moment.tz.names().sort((x, y) => x.localeCompare(y));
}
canDeactivate(): boolean | Observable<boolean> {
return this.formGroup ? !this.formGroup.dirty : true;
}
ngOnInit(): void {
this.matomoService.trackPageView('Admin: TenantConfigurations');
this.getItem((entity) => {
this.prepareForm(entity);
if (this.formGroup && this.editorModel.belongsToCurrentTenant == false) {
this.formGroup.disable();
}
});
}
getItem(successFunction: (item: TenantConfiguration) => void) {
this.tenantConfigurationService.getCurrentTenantType(TenantConfigurationType.DefaultUserLocale, DefaultUserLocaleEditorResolver.lookupFields())
.pipe(map(data => data as TenantConfiguration), takeUntil(this._destroyed))
.subscribe(
data => successFunction(data),
error => this.onCallbackError(error)
);
}
onCallbackError(errorResponse: HttpErrorResponse) {
console.log("Error:", errorResponse);
const error: HttpError = this.httpErrorHandlingService.getError(errorResponse);
if (error.statusCode === 400) {
this.editorModel.validationErrorModel.fromJSONObject(errorResponse.error);
if(errorResponse.error.code === ResponseErrorCode.TenantConfigurationTypeCanNotChange){
this.uiNotificationService.snackBarNotification(errorResponse.error.error, SnackBarNotificationLevel.Error);
}
if(errorResponse.error.code === ResponseErrorCode.MultipleTenantConfigurationTypeNotAllowed){
this.uiNotificationService.snackBarNotification(errorResponse.error.error, SnackBarNotificationLevel.Error);
}
this.formService.validateAllFormFields(this.formGroup);
} else {
this.uiNotificationService.snackBarNotification(error.getMessagesString(), SnackBarNotificationLevel.Warning);
}
}
onCallbackSuccess(data?: any): void {
console.log("Success:", data);
this.uiNotificationService.snackBarNotification(this.isNew ? this.language.instant('GENERAL.SNACK-BAR.SUCCESSFUL-CREATION') : this.language.instant('GENERAL.SNACK-BAR.SUCCESSFUL-UPDATE'), SnackBarNotificationLevel.Success);
this.refreshData();
}
prepareForm(data: TenantConfiguration) {
try {
this.editorModel = data ? new TenantConfigurationEditorModel().fromModel(data) : new TenantConfigurationEditorModel();
this.buildForm();
} catch (error) {
this.logger.error('Could not parse TenantConfiguration item: ' + data + error);
this.uiNotificationService.snackBarNotification(this.language.instant('COMMONS.ERRORS.DEFAULT'), SnackBarNotificationLevel.Error);
}
}
buildForm() {
this.formGroup = this.editorModel.buildForm(null, !this.authService.hasPermission(AppPermission.EditTenantConfiguration));
this.defaultUserLocaleEditorService.setValidationErrorModel(this.editorModel.validationErrorModel);
}
refreshData(): void {
this.getItem((entity) => {
this.prepareForm(entity);
if (this.formGroup && this.editorModel.belongsToCurrentTenant == false) {
this.formGroup.disable();
}
});
}
persistEntity(onSuccess?: (response) => void): void {
const formData = this.formService.getValue(this.formGroup.value) as TenantConfigurationPersist;
this.tenantConfigurationService.persist(formData)
.pipe(takeUntil(this._destroyed)).subscribe(
complete => onSuccess ? onSuccess(complete) : this.onCallbackSuccess(complete),
error => this.onCallbackError(error)
);
}
formSubmit(): void {
this.clearErrorModel();
this.formService.removeAllBackEndErrors(this.formGroup);
this.formService.touchAllFormFields(this.formGroup);
if (!this.isFormValid()) {
return;
}
this.persistEntity();
}
public isFormValid() {
return this.formGroup.valid;
}
public delete() {
const value = this.formGroup.value;
if (value.id) {
const dialogRef = this.dialog.open(ConfirmationDialogComponent, {
maxWidth: '300px',
data: {
message: this.language.instant('GENERAL.CONFIRMATION-DIALOG.RESET-TO-DEFAULT'),
confirmButton: this.language.instant('GENERAL.CONFIRMATION-DIALOG.ACTIONS.CONFIRM'),
cancelButton: this.language.instant('GENERAL.CONFIRMATION-DIALOG.ACTIONS.CANCEL')
}
});
dialogRef.afterClosed().pipe(takeUntil(this._destroyed)).subscribe(result => {
if (result) {
this.tenantConfigurationService.delete(value.id).pipe(takeUntil(this._destroyed))
.subscribe(
complete => this.onCallbackDeleteSuccessConfig(),
error => this.onCallbackError(error)
);
}
});
}
}
onCallbackDeleteSuccessConfig(data?: any): void {
console.log("Success Delete:", data);
this.uiNotificationService.snackBarNotification(this.language.instant('GENERAL.SNACK-BAR.SUCCESSFUL-RESET'), SnackBarNotificationLevel.Success);
this.prepareForm(null)
}
clearErrorModel() {
this.editorModel.validationErrorModel.clear();
this.formService.validateAllFormFields(this.formGroup);
}
displayCultureFn(culture?: CultureInfo): string | undefined {
if (culture == null
|| culture.displayName == null
|| culture.nativeName == null)
return undefined;
return culture.displayName + '-' + culture.nativeName;
}
}

View File

@ -0,0 +1,112 @@
import { UntypedFormBuilder, UntypedFormGroup, Validators } from "@angular/forms";
import { TenantConfigurationType } from "@app/core/common/enum/tenant-configuration-type";
import { DefaultUserLocaleTenantConfiguration, DefaultUserLocaleTenantConfigurationPersist, TenantConfiguration, TenantConfigurationPersist } from "@app/core/model/tenant-configuaration/tenant-configuration";
import { BaseEditorModel } from "@common/base/base-form-editor-model";
import { BackendErrorValidator } from "@common/forms/validation/custom-validator";
import { ValidationErrorModel } from "@common/forms/validation/error-model/validation-error-model";
import { Validation, ValidationContext } from "@common/forms/validation/validation-context";
export class TenantConfigurationEditorModel extends BaseEditorModel implements TenantConfigurationPersist {
type: TenantConfigurationType;
defaultUserLocale: DefaultUserLocaleTenantConfigurationEditorModel = new DefaultUserLocaleTenantConfigurationEditorModel(this.validationErrorModel);
public validationErrorModel: ValidationErrorModel = new ValidationErrorModel();
protected formBuilder: UntypedFormBuilder = new UntypedFormBuilder();
constructor() { super(); this.type = TenantConfigurationType.DefaultUserLocale; }
public fromModel(item: TenantConfiguration): TenantConfigurationEditorModel {
if (item) {
super.fromModel(item);
this.type = item.type;
if (item.defaultUserLocale) this.defaultUserLocale = new DefaultUserLocaleTenantConfigurationEditorModel(this.validationErrorModel).fromModel(item.defaultUserLocale);
} else {
this.type = TenantConfigurationType.DefaultUserLocale;
}
return this;
}
buildForm(context: ValidationContext = null, disabled: boolean = false): UntypedFormGroup {
if (context == null) { context = this.createValidationContext(); }
return this.formBuilder.group({
id: [{ value: this.id, disabled: disabled }, context.getValidation('id').validators],
type: [{ value: this.type, disabled: disabled }, context.getValidation('type').validators],
hash: [{ value: this.hash, disabled: disabled }, context.getValidation('hash').validators],
defaultUserLocale: this.defaultUserLocale.buildForm({
rootPath: `defaultUserLocale.`,
}),
});
}
createValidationContext(): ValidationContext {
const baseContext: ValidationContext = new ValidationContext();
const baseValidationArray: Validation[] = new Array<Validation>();
baseValidationArray.push({ key: 'id', validators: [BackendErrorValidator(this.validationErrorModel, 'id')] });
baseValidationArray.push({ key: 'type', validators: [Validators.required, BackendErrorValidator(this.validationErrorModel, 'type')] });
baseValidationArray.push({ key: 'hash', validators: [] });
baseContext.validation = baseValidationArray;
return baseContext;
}
}
export class DefaultUserLocaleTenantConfigurationEditorModel implements DefaultUserLocaleTenantConfigurationPersist {
timezone: string;
language: string;
culture: string;
protected formBuilder: UntypedFormBuilder = new UntypedFormBuilder();
constructor(
public validationErrorModel: ValidationErrorModel = new ValidationErrorModel()
) { }
public fromModel(item: DefaultUserLocaleTenantConfiguration): DefaultUserLocaleTenantConfigurationEditorModel {
if (item) {
this.timezone = item.timezone;
this.language = item.language;
this.culture = item.culture;
}
return this;
}
buildForm(params?: {
context?: ValidationContext,
disabled?: boolean,
rootPath?: string
}): UntypedFormGroup {
let { context = null, disabled = false, rootPath } = params ?? {}
if (context == null) {
context = DefaultUserLocaleTenantConfigurationEditorModel.createValidationContext({
validationErrorModel: this.validationErrorModel,
rootPath
});
}
const form: UntypedFormGroup = this.formBuilder.group({
timezone: [{ value: this.timezone, disabled: disabled }, context.getValidation('timezone').validators],
language: [{ value: this.language, disabled: disabled }, context.getValidation('language').validators],
culture: [{ value: this.culture, disabled: disabled }, context.getValidation('culture').validators],
});
return form;
}
static createValidationContext(params: {
rootPath?: string,
validationErrorModel: ValidationErrorModel
}): ValidationContext {
const { rootPath = '', validationErrorModel } = params;
const baseContext: ValidationContext = new ValidationContext();
const baseValidationArray: Validation[] = new Array<Validation>();
baseValidationArray.push({ key: 'timezone', validators: [Validators.required, BackendErrorValidator(validationErrorModel, `${rootPath}timezone`)] });
baseValidationArray.push({ key: 'language', validators: [Validators.required, BackendErrorValidator(validationErrorModel, `${rootPath}language`)] });
baseValidationArray.push({ key: 'culture', validators: [Validators.required, BackendErrorValidator(validationErrorModel, `${rootPath}culture`)] });
baseContext.validation = baseValidationArray;
return baseContext;
}
}

View File

@ -0,0 +1,45 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { TenantConfigurationType } from '@app/core/common/enum/tenant-configuration-type';
import { DefaultUserLocaleTenantConfiguration, TenantConfiguration } from '@app/core/model/tenant-configuaration/tenant-configuration';
import { TenantConfigurationService } from '@app/core/services/tenant-configuration/tenant-configuration.service';
import { BreadcrumbService } from '@app/ui/misc/breadcrumb/breadcrumb.service';
import { BaseEditorResolver } from '@common/base/base-editor.resolver';
import { takeUntil, tap } from 'rxjs/operators';
import { nameof } from 'ts-simple-nameof';
@Injectable()
export class DefaultUserLocaleEditorResolver extends BaseEditorResolver {
constructor(private tenantConfigurationService: TenantConfigurationService, private breadcrumbService: BreadcrumbService) {
super();
}
public static lookupFields(): string[] {
return [
...BaseEditorResolver.lookupFields(),
nameof<TenantConfiguration>(x => x.id),
nameof<TenantConfiguration>(x => x.type),
nameof<TenantConfiguration>(x => x.defaultUserLocale),
[nameof<TenantConfiguration>(x => x.defaultUserLocale), nameof<DefaultUserLocaleTenantConfiguration>(x => x.culture)].join('.'),
[nameof<TenantConfiguration>(x => x.defaultUserLocale), nameof<DefaultUserLocaleTenantConfiguration>(x => x.language)].join('.'),
[nameof<TenantConfiguration>(x => x.defaultUserLocale), nameof<DefaultUserLocaleTenantConfiguration>(x => x.timezone)].join('.'),
nameof<TenantConfiguration>(x => x.createdAt),
nameof<TenantConfiguration>(x => x.updatedAt),
nameof<TenantConfiguration>(x => x.hash),
nameof<TenantConfiguration>(x => x.isActive)
]
}
resolve() {
const fields = [
...DefaultUserLocaleEditorResolver.lookupFields()
];
return this.tenantConfigurationService.getCurrentTenantType(TenantConfigurationType.DefaultUserLocale, fields).pipe(takeUntil(this._destroyed));
}
}

View File

@ -0,0 +1,15 @@
import { Injectable } from "@angular/core";
import { ValidationErrorModel } from "@common/forms/validation/error-model/validation-error-model";
@Injectable()
export class DefaultUserLocaleEditorService {
private validationErrorModel: ValidationErrorModel;
public setValidationErrorModel(validationErrorModel: ValidationErrorModel): void {
this.validationErrorModel = validationErrorModel;
}
public getValidationErrorModel(): ValidationErrorModel {
return this.validationErrorModel;
}
}

View File

@ -0,0 +1,43 @@
<div class="container-fluid">
<div class="row">
<div class="col-md-10 offset-md-1 tenant-configuration-component">
<div class="row align-items-center mb-4 mt-4" >
<div class="col-md col-12">
<h3>{{'TENANT-CONFIGURATION-EDITOR.TITLE' | translate}}</h3>
<app-navigation-breadcrumb />
</div>
</div>
<mat-card appearance="outlined">
<mat-card-header>
<mat-card-title>{{'TENANT-CONFIGURATION-EDITOR.TITLE' | translate}}</mat-card-title>
</mat-card-header>
<mat-card-content>
<div class="row">
<mat-accordion class="col-12 headers-align">
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>{{'TENANT-CONFIGURATION-EDITOR.DEFAULT-USER-LOCALE.TITLE' | translate}}</mat-panel-title>
<mat-panel-description>{{'TENANT-CONFIGURATION-EDITOR.DEFAULT-USER-LOCALE.HINT' | translate}}</mat-panel-description>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<app-tenant-configuration-default-user-locale-editor></app-tenant-configuration-default-user-locale-editor>
</ng-template>
</mat-expansion-panel>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>{{'TENANT-CONFIGURATION-EDITOR.CSS-COLORS.TITLE' | translate}}</mat-panel-title>
<mat-panel-description>{{'TENANT-CONFIGURATION-EDITOR.CSS-COLORS.HINT' | translate}}</mat-panel-description>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<app-tenant-configuration-css-colors-editor></app-tenant-configuration-css-colors-editor>
</ng-template>
</mat-expansion-panel>
</mat-accordion>
</div>
</mat-card-content>
</mat-card>
</div>
</div>
</div>

View File

@ -0,0 +1,16 @@
.tenant-configuration-component {
padding-top: 1em;
.configuration-container {
margin-top: 2em;
}
.nav-list-width {
width: 15em;
}
.headers-align .mat-expansion-panel-header-title,
.headers-align .mat-expansion-panel-header-description {
flex-basis: 0;
}
}

View File

@ -0,0 +1,22 @@
import { Component } from '@angular/core';
// import { BreadcrumbItem } from '@app/ui/misc/breadcrumb/definition/breadcrumb-item';
import { BaseComponent } from '@common/base/base.component';
@Component({
selector: 'app-tenant-configuration-editor-component',
templateUrl: 'tenant-configuration-editor.component.html',
styleUrls: ['./tenant-configuration-editor.component.scss'],
})
export class TenantConfigurationEditorComponent extends BaseComponent {
constructor(
) {
super();
}
ngOnInit() {
}
}

View File

@ -0,0 +1,41 @@
import { DragDropModule } from '@angular/cdk/drag-drop';
import { NgModule } from "@angular/core";
import { AutoCompleteModule } from "@app/library/auto-complete/auto-complete.module";
import { CommonFormattingModule } from '@common/formatting/common-formatting.module';
import { CommonFormsModule } from '@common/forms/common-forms.module';
import { ConfirmationDialogModule } from '@common/modules/confirmation-dialog/confirmation-dialog.module';
import { HybridListingModule } from "@common/modules/hybrid-listing/hybrid-listing.module";
import { TextFilterModule } from "@common/modules/text-filter/text-filter.module";
import { UserSettingsModule } from "@common/modules/user-settings/user-settings.module";
import { CommonUiModule } from '@common/ui/common-ui.module';
import { NgxDropzoneModule } from "ngx-dropzone";
import { RichTextEditorModule } from '@app/library/rich-text-editor/rich-text-editor.module';
import { TenantConfigurationRoutingModule } from './tenant-configuration.routing';
import { TenantConfigurationEditorComponent } from './editor/tenant-configuration-editor.component';
import { CssColorsEditorComponent } from './editor/css-colors/css-colors-editor.component';
import { DefaultUserLocaleEditorComponent } from './editor/default-user-locale/default-user-locale-editor.component';
import { FormattingModule } from '@app/core/formatting.module';
@NgModule({
imports: [
CommonUiModule,
CommonFormsModule,
ConfirmationDialogModule,
TenantConfigurationRoutingModule,
NgxDropzoneModule,
DragDropModule,
FormattingModule,
AutoCompleteModule,
HybridListingModule,
TextFilterModule,
UserSettingsModule,
CommonFormattingModule,
RichTextEditorModule
],
declarations: [
TenantConfigurationEditorComponent,
CssColorsEditorComponent,
DefaultUserLocaleEditorComponent
]
})
export class TenantConfigurationModule { }

View File

@ -0,0 +1,21 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AuthGuard } from '@app/core/auth-guard.service';
import { TenantConfigurationEditorComponent } from './editor/tenant-configuration-editor.component';
const routes: Routes = [
{
path: '',
component: TenantConfigurationEditorComponent,
canActivate: [AuthGuard]
},
{ path: '**', loadChildren: () => import('@common/modules/page-not-found/page-not-found.module').then(m => m.PageNotFoundModule) },
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
providers: []
})
export class TenantConfigurationRoutingModule { }

View File

@ -1,83 +0,0 @@
<h3>
{{label}}
<button mat-button class="action-btn" type="button" (click)="addSource()" [disabled]="form.disabled">{{'TENANT-EDITOR.ACTIONS.ADD-SOURCE' | translate}}</button>
</h3>
<div *ngFor="let source of form.get('sources').controls; let sourceIndex=index;" class="row mb-3">
<div class="col-12">
<div class="row mb-3 d-flex align-items-center">
<div class="col-auto d-flex">
<mat-card-title>{{'TENANT-EDITOR.FIELDS.SOURCE' | translate}} {{sourceIndex + 1}}</mat-card-title>
</div>
<div class="col-auto d-flex">
<button mat-icon-button class="action-list-icon" matTooltip="{{'TENANT-EDITOR.ACTIONS.REMOVE-SOURCE' | translate}}" (click)="removeSource(sourceIndex)" [disabled]="form.disabled">
<mat-icon>delete</mat-icon>
</button>
</div>
</div>
<div class="row" >
<div class="col-6">
<mat-form-field class="w-100">
<mat-label>{{'TENANT-EDITOR.FIELDS.URL' | translate}}</mat-label>
<input matInput type="text" name="url" [formControl]="source.get('url')" required>
<mat-error *ngIf="source.get('url').hasError('backendError')">{{source.get('url').getError('backendError').message}}</mat-error>
<mat-error *ngIf="source.get('url').hasError('required')">{{'GENERAL.VALIDATION.REQUIRED' | translate}}</mat-error>
</mat-form-field>
</div>
<div class="col-6">
<mat-form-field class="w-100">
<mat-label>{{'TENANT-EDITOR.FIELDS.ISSUER-URL' | translate}}</mat-label>
<input matInput type="text" name="issuerUrl" [formControl]="source.get('issuerUrl')" required>
<mat-error *ngIf="source.get('issuerUrl').hasError('backendError')">{{source.get('issuerUrl').getError('backendError').message}}</mat-error>
<mat-error *ngIf="source.get('issuerUrl').hasError('required')">{{'GENERAL.VALIDATION.REQUIRED' | translate}}</mat-error>
</mat-form-field>
</div>
<div class="col-6">
<mat-form-field class="w-100">
<mat-label>{{'TENANT-EDITOR.FIELDS.CLIENT-ID' | translate}}</mat-label>
<input matInput type="text" name="clientId" [formControl]="source.get('clientId')" required>
<mat-error *ngIf="source.get('clientId').hasError('backendError')">{{source.get('clientId').getError('backendError').message}}</mat-error>
<mat-error *ngIf="source.get('clientId').hasError('required')">{{'GENERAL.VALIDATION.REQUIRED' | translate}}</mat-error>
</mat-form-field>
</div>
<div class="col-6">
<mat-form-field class="w-100">
<mat-label>{{'TENANT-EDITOR.FIELDS.CLIENT-SECRET' | translate}}</mat-label>
<input matInput type="text" name="clientSecret" [formControl]="source.get('clientSecret')" required>
<mat-error *ngIf="source.get('clientSecret').hasError('backendError')">{{source.get('clientSecret').getError('backendError').message}}</mat-error>
<mat-error *ngIf="source.get('clientSecret').hasError('required')">{{'GENERAL.VALIDATION.REQUIRED' | translate}}</mat-error>
</mat-form-field>
</div>
<div class="col-6">
<mat-form-field class="w-100">
<mat-label>{{'TENANT-EDITOR.FIELDS.SCOPE' | translate}}</mat-label>
<input matInput type="text" name="scope" [formControl]="source.get('scope')" required>
<mat-error *ngIf="source.get('scope').hasError('backendError')">{{source.get('scope').getError('backendError').message}}</mat-error>
<mat-error *ngIf="source.get('scope').hasError('required')">{{'GENERAL.VALIDATION.REQUIRED' | translate}}</mat-error>
</mat-form-field>
</div>
<div class="col-6">
<mat-form-field class="chip-list">
<mat-label>{{'TENANT-EDITOR.FIELDS.CODES' | translate}}</mat-label>
<mat-chip-grid #chipGrid [formControl]="source.get('codes')">
<mat-chip-row *ngFor="let code of codes.get(sourceIndex)"
(removed)="removeCode(code, sourceIndex)"
[editable]="true"
(edited)="editCode(code, $event, sourceIndex)">
{{code}}
<button matChipRemove>
<mat-icon>cancel</mat-icon>
</button>
</mat-chip-row>
<input placeholder="{{'TENANT-EDITOR.FIELDS.CODES-PLACEHOLDER' | translate}}"
[matChipInputFor]="chipGrid"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
[matChipInputAddOnBlur]="true"
(matChipInputTokenEnd)="addCode($event, sourceIndex)"/>
</mat-chip-grid>
<mat-error *ngIf="source.get('codes').hasError('backendError')">{{source.get('codes').getError('backendError').message}}</mat-error>
<mat-error *ngIf="source.get('codes').hasError('required')">{{'GENERAL.VALIDATION.REQUIRED' | translate}}</mat-error>
</mat-form-field>
</div>
</div>
</div>
</div>

View File

@ -1,18 +0,0 @@
.action-btn {
border-radius: 30px;
background-color: var(--secondary-color);
border: 1px solid transparent;
padding-left: 2em;
padding-right: 2em;
box-shadow: 0px 3px 6px #1E202029;
transition-property: background-color, color;
transition-duration: 200ms;
transition-delay: 50ms;
transition-timing-function: ease-in-out;
&:disabled{
background-color: #CBCBCB;
color: #FFF;
border: 0px;
}
}

View File

@ -1,99 +0,0 @@
import { Component, Input, OnInit } from '@angular/core';
import { UntypedFormArray } from '@angular/forms';
import { MatChipEditedEvent, MatChipInputEvent } from '@angular/material/chips';
import { EnumUtils } from '@app/core/services/utilities/enum-utils.service';
import { BaseComponent } from '@common/base/base.component';
import { ValidationErrorModel } from '@common/forms/validation/error-model/validation-error-model';
import { TenantDepositConfigEditorModel, TenantEditorModel, TenantSourceEditorModel } from '../tenant-editor.model';
@Component({
selector: 'app-tenant-source-component',
templateUrl: 'tenant-source.component.html',
styleUrls: ['./tenant-source.component.scss']
})
export class TenantSourceComponent extends BaseComponent implements OnInit {
@Input() form;
@Input() validationErrorModel: ValidationErrorModel = null;
@Input() validationRootPath: string = null;
@Input() codes: Map<number, string[]>;
@Input() label: string = null;
constructor(
public enumUtils: EnumUtils,
) { super(); }
ngOnInit() {
}
//
// source
//
addSource(): void {
const formArray = this.form.get('sources') as UntypedFormArray;
this.codes.set(formArray.length, []);
const source: TenantSourceEditorModel = new TenantSourceEditorModel(this.validationErrorModel);
formArray.push(source.buildForm({ rootPath: this.validationRootPath + 'sources[' + formArray.length + '].' }));
}
removeSource(sourceIndex: number): void {
this.codes.delete((this.form.get('sources') as UntypedFormArray).length);
(this.form.get('sources') as UntypedFormArray).removeAt(sourceIndex);
// Reapply validators
TenantDepositConfigEditorModel.reapplySourcesFieldsValidators(
{
formArray: this.form.get('sources') as UntypedFormArray,
validationErrorModel: this.validationErrorModel,
rootPath: this.validationRootPath
}
)
this.form.get('sources').markAsDirty();
}
// source codes
addCode(event: MatChipInputEvent, key: number): void {
const value = (event.value || '').trim();
if (value){
const values = this.codes.get(key);
values.push(value);
this.codes.set(key, values);
}
event.chipInput!.clear();
}
removeCode(code: string, key: number): void {
const values = this.codes.get(key);
if (values){
const index = values.indexOf(code);
if (index >= 0) {
values.splice(index, 1);
this.codes.set(key, values);
}
}
}
editCode(code: string, event: MatChipEditedEvent, key: number) {
const values = this.codes.get(key);
if (values){
const value = event.value.trim();
// Remove code if it no longer has a value
if (!value) {
this.removeCode(code, key);
return;
}
const index = values.indexOf(code);
if (index >= 0) {
values[index] = value;
this.codes.set(key, values);
}
}
}
}

View File

@ -57,25 +57,6 @@
</div>
</div>
</div>
<!-- Deposit -->
<div class="col-12">
<app-tenant-source-component
[form]="formGroup.get('config').get('deposit')"
[validationErrorModel]="editorModel.validationErrorModel"
[validationRootPath]="'config.deposit.'"
[codes]="depositCodes"
[label]="'TENANT-EDITOR.FIELDS.DEPOSIT' | translate">
</app-tenant-source-component>
</div>
<div class="col-12">
<app-tenant-source-component
[form]="formGroup.get('config').get('fileTransformers')"
[validationErrorModel]="editorModel.validationErrorModel"
[validationRootPath]="'config.fileTransformers.'"
[codes]="fileTransformersCodes"
[label]="'TENANT-EDITOR.FIELDS.FILE-TRANSFORMERS' | translate">
</app-tenant-source-component>
</div>
</div>
</mat-card-content>
</mat-card>

View File

@ -108,10 +108,6 @@ export class TenantEditorComponent extends BaseEditor<TenantEditorModel, Tenant>
try {
this.editorModel = data ? new TenantEditorModel().fromModel(data) : new TenantEditorModel();
if(data && data.config){
if(data.config.deposit && data.config.deposit.sources) data.config.deposit.sources.forEach((source, index) => this.depositCodes.set(index, source.codes));
if(data.config.fileTransformers && data.config.fileTransformers.sources) data.config.fileTransformers.sources.forEach((source, index) => this.fileTransformersCodes.set(index, source.codes));
}
this.isDeleted = data ? data.isActive === IsActive.Inactive : false;
this.buildForm();
} catch (error) {

View File

@ -1,5 +1,5 @@
import { UntypedFormArray, UntypedFormBuilder, UntypedFormGroup, Validators } from "@angular/forms";
import { Tenant, TenantConfig, TenantConfigPersist, TenantDepositConfig, TenantDepositConfigPersist, TenantFileTransformersConfig, TenantFileTransformersConfigPersist, TenantPersist, TenantSource, TenantSourcePersist } from "@app/core/model/tenant/tenant";
import { UntypedFormBuilder, UntypedFormGroup, Validators } from "@angular/forms";
import { Tenant, TenantPersist } from "@app/core/model/tenant/tenant";
import { BaseEditorModel } from "@common/base/base-form-editor-model";
import { BackendErrorValidator } from "@common/forms/validation/custom-validator";
import { ValidationErrorModel } from "@common/forms/validation/error-model/validation-error-model";
@ -9,7 +9,6 @@ export class TenantEditorModel extends BaseEditorModel implements TenantPersist
name: string;
code: string;
description: string;
config: TenantConfigEditorModel = new TenantConfigEditorModel();
permissions: string[];
public validationErrorModel: ValidationErrorModel = new ValidationErrorModel();
@ -23,7 +22,6 @@ export class TenantEditorModel extends BaseEditorModel implements TenantPersist
this.name = item.name;
this.code = item.code;
this.description = item.description;
if (item.config) this.config = new TenantConfigEditorModel(this.validationErrorModel).fromModel(item.config);
}
return this;
}
@ -36,9 +34,6 @@ export class TenantEditorModel extends BaseEditorModel implements TenantPersist
name: [{ value: this.name, disabled: disabled }, context.getValidation('name').validators],
code: [{ value: this.code, disabled: disabled }, context.getValidation('code').validators],
description: [{ value: this.description, disabled: disabled }, context.getValidation('description').validators],
config: this.config.buildForm({
rootPath: `config.`,
}),
hash: [{ value: this.hash, disabled: disabled }, context.getValidation('hash').validators]
});
}
@ -55,301 +50,4 @@ export class TenantEditorModel extends BaseEditorModel implements TenantPersist
baseContext.validation = baseValidationArray;
return baseContext;
}
static reApplyDepositSourcesValidators(params: {
formGroup: UntypedFormGroup,
validationErrorModel: ValidationErrorModel,
}): void {
const { formGroup, validationErrorModel } = params;
const control = formGroup?.get('config').get('deposit');
TenantDepositConfigEditorModel.reapplySourcesFieldsValidators({
formArray: control.get('sources') as UntypedFormArray,
rootPath: `config.deposit.`,
validationErrorModel: validationErrorModel
});
}
static reApplyFileTransformerSourcesValidators(params: {
formGroup: UntypedFormGroup,
validationErrorModel: ValidationErrorModel,
}): void {
const { formGroup, validationErrorModel } = params;
const control = formGroup?.get('config').get('fileTransformers');
TenantDepositConfigEditorModel.reapplySourcesFieldsValidators({
formArray: control.get('sources') as UntypedFormArray,
rootPath: `config.fileTransformers.`,
validationErrorModel: validationErrorModel
});
}
}
export class TenantConfigEditorModel implements TenantConfigPersist {
deposit: TenantDepositConfigEditorModel = new TenantDepositConfigEditorModel();
fileTransformers: TenantFileTransformersConfigEditorModel = new TenantFileTransformersConfigEditorModel();
protected formBuilder: UntypedFormBuilder = new UntypedFormBuilder();
constructor(
public validationErrorModel: ValidationErrorModel = new ValidationErrorModel()
) { }
public fromModel(item: TenantConfig): TenantConfigEditorModel {
if (item) {
if (item.deposit) this.deposit = new TenantDepositConfigEditorModel(this.validationErrorModel).fromModel(item.deposit);
if (item.fileTransformers) this.fileTransformers = new TenantFileTransformersConfigEditorModel(this.validationErrorModel).fromModel(item.fileTransformers);
}
return this;
}
buildForm(params?: {
context?: ValidationContext,
disabled?: boolean,
rootPath?: string
}): UntypedFormGroup {
let { context = null, disabled = false, rootPath } = params ?? {}
if (context == null) {
context = TenantConfigEditorModel.createValidationContext({
validationErrorModel: this.validationErrorModel,
rootPath
});
}
return this.formBuilder.group({
deposit: this.deposit.buildForm({
rootPath: `${rootPath}deposit.`
}),
fileTransformers: this.fileTransformers.buildForm({
rootPath: `${rootPath}fileTransformers.`
}),
});
}
static createValidationContext(params: {
rootPath?: string,
validationErrorModel: ValidationErrorModel
}): ValidationContext {
const { rootPath = '', validationErrorModel } = params;
const baseContext: ValidationContext = new ValidationContext();
const baseValidationArray: Validation[] = new Array<Validation>();
baseContext.validation = baseValidationArray;
return baseContext;
}
}
export class TenantDepositConfigEditorModel implements TenantDepositConfigPersist {
sources: TenantSourceEditorModel[]= [];
protected formBuilder: UntypedFormBuilder = new UntypedFormBuilder();
constructor(
public validationErrorModel: ValidationErrorModel = new ValidationErrorModel()
) { }
public fromModel(item: TenantDepositConfig): TenantDepositConfigEditorModel {
if (item) {
if (item.sources) { item.sources.map(x => this.sources.push(new TenantSourceEditorModel(this.validationErrorModel).fromModel(x))); }
}
return this;
}
buildForm(params?: {
context?: ValidationContext,
disabled?: boolean,
rootPath?: string
}): UntypedFormGroup {
let { context = null, disabled = false, rootPath } = params ?? {}
if (context == null) {
context = TenantDepositConfigEditorModel.createValidationContext({
validationErrorModel: this.validationErrorModel,
rootPath
});
}
return this.formBuilder.group({
sources: this.formBuilder.array(
(this.sources ?? []).map(
(item, index) => item.buildForm({
rootPath: `${rootPath}sources[${index}].`
})
), context.getValidation('sources')
),
});
}
static createValidationContext(params: {
rootPath?: string,
validationErrorModel: ValidationErrorModel
}): ValidationContext {
const { rootPath = '', validationErrorModel } = params;
const baseContext: ValidationContext = new ValidationContext();
const baseValidationArray: Validation[] = new Array<Validation>();
baseValidationArray.push({ key: 'sources', validators: [BackendErrorValidator(validationErrorModel, `${rootPath}sources`)] });
baseContext.validation = baseValidationArray;
return baseContext;
}
static reapplySourcesFieldsValidators(params: {
formArray: UntypedFormArray,
validationErrorModel: ValidationErrorModel,
rootPath: string
}): void {
const { validationErrorModel, rootPath, formArray } = params;
formArray?.controls?.forEach(
(control, index) => TenantSourceEditorModel.reapplyValidators({
formGroup: control as UntypedFormGroup,
rootPath: `${rootPath}sources[${index}].`,
validationErrorModel: validationErrorModel
})
);
}
}
export class TenantFileTransformersConfigEditorModel implements TenantFileTransformersConfigPersist {
sources: TenantSourceEditorModel[]= [];
protected formBuilder: UntypedFormBuilder = new UntypedFormBuilder();
constructor(
public validationErrorModel: ValidationErrorModel = new ValidationErrorModel()
) { }
public fromModel(item: TenantFileTransformersConfig): TenantFileTransformersConfigEditorModel {
if (item) {
if (item.sources) { item.sources.map(x => this.sources.push(new TenantSourceEditorModel(this.validationErrorModel).fromModel(x))); }
}
return this;
}
buildForm(params?: {
context?: ValidationContext,
disabled?: boolean,
rootPath?: string
}): UntypedFormGroup {
let { context = null, disabled = false, rootPath } = params ?? {}
if (context == null) {
context = TenantFileTransformersConfigEditorModel.createValidationContext({
validationErrorModel: this.validationErrorModel,
rootPath
});
}
return this.formBuilder.group({
sources: this.formBuilder.array(
(this.sources ?? []).map(
(item, index) => item.buildForm({
rootPath: `${rootPath}sources[${index}].`
})
), context.getValidation('sources')
),
});
}
static createValidationContext(params: {
rootPath?: string,
validationErrorModel: ValidationErrorModel
}): ValidationContext {
const { rootPath = '', validationErrorModel } = params;
const baseContext: ValidationContext = new ValidationContext();
const baseValidationArray: Validation[] = new Array<Validation>();
baseValidationArray.push({ key: 'sources', validators: [BackendErrorValidator(validationErrorModel, `${rootPath}sources`)] });
baseContext.validation = baseValidationArray;
return baseContext;
}
}
export class TenantSourceEditorModel implements TenantSourcePersist {
url: string;
codes: string[]= [];
issuerUrl: string;
clientId: string;
clientSecret: string;
scope: string;
protected formBuilder: UntypedFormBuilder = new UntypedFormBuilder();
constructor(
public validationErrorModel: ValidationErrorModel = new ValidationErrorModel()
) { }
public fromModel(item: TenantSource): TenantSourceEditorModel {
if (item) {
this.url = item.url;
//if (item.codes) { item.codes.map(x => this.codes.push(new SourceCodeEditorModel().fromModel(x))); }
this.codes = item.codes;
this.issuerUrl = item.issuerUrl;
this.clientId = item.clientId;
this.clientSecret = item.clientSecret;
this.scope = item.scope;
}
return this;
}
buildForm(params?: {
context?: ValidationContext,
disabled?: boolean,
rootPath?: string
}): UntypedFormGroup {
let { context = null, disabled = false, rootPath } = params ?? {}
if (context == null) {
context = TenantSourceEditorModel.createValidationContext({
validationErrorModel: this.validationErrorModel,
rootPath
});
}
return this.formBuilder.group({
url: [{ value: this.url, disabled: disabled }, context.getValidation('url').validators],
issuerUrl: [{ value: this.issuerUrl, disabled: disabled }, context.getValidation('issuerUrl').validators],
clientId: [{ value: this.clientId, disabled: disabled }, context.getValidation('clientId').validators],
clientSecret: [{ value: this.clientSecret, disabled: disabled }, context.getValidation('clientSecret').validators],
scope: [{ value: this.scope, disabled: disabled }, context.getValidation('scope').validators],
codes: [{ value: this.codes, disabled: disabled }, context.getValidation('codes').validators],
});
}
static createValidationContext(params: {
rootPath?: string,
validationErrorModel: ValidationErrorModel
}): ValidationContext {
const { rootPath = '', validationErrorModel } = params;
const baseContext: ValidationContext = new ValidationContext();
const baseValidationArray: Validation[] = new Array<Validation>();
baseValidationArray.push({ key: 'url', validators: [Validators.required, BackendErrorValidator(validationErrorModel, `${rootPath}url`)] });
baseValidationArray.push({ key: 'codes', validators: [Validators.required, BackendErrorValidator(validationErrorModel, `${rootPath}codes`)] });
baseValidationArray.push({ key: 'issuerUrl', validators: [Validators.required, BackendErrorValidator(validationErrorModel, `${rootPath}issuerUrl`)] });
baseValidationArray.push({ key: 'clientId', validators: [Validators.required, BackendErrorValidator(validationErrorModel, `${rootPath}clientId`)] });
baseValidationArray.push({ key: 'clientSecret', validators: [Validators.required, BackendErrorValidator(validationErrorModel, `${rootPath}clientSecret`)] });
baseValidationArray.push({ key: 'scope', validators: [Validators.required, BackendErrorValidator(validationErrorModel, `${rootPath}scope`)] });
baseContext.validation = baseValidationArray;
return baseContext;
}
static reapplyValidators(params: {
formGroup: UntypedFormGroup,
validationErrorModel: ValidationErrorModel,
rootPath: string
}): void {
const { formGroup, rootPath, validationErrorModel } = params;
const context = TenantSourceEditorModel.createValidationContext({
rootPath,
validationErrorModel
});
['url', 'codes', 'issuerUrl', 'clientId', 'clientSecret', 'scope'].forEach(keyField => {
const control = formGroup?.get(keyField);
control?.clearValidators();
control?.addValidators(context.getValidation(keyField).validators);
})
}
}

View File

@ -1,6 +1,6 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { SourceCode, Tenant, TenantConfig, TenantDepositConfig, TenantSource } from '@app/core/model/tenant/tenant';
import { Tenant } from '@app/core/model/tenant/tenant';
import { TenantService } from '@app/core/services/tenant/tenant.service';
import { BreadcrumbService } from '@app/ui/misc/breadcrumb/breadcrumb.service';
import { BaseEditorResolver } from '@common/base/base-editor.resolver';
@ -23,20 +23,6 @@ export class TenantEditorResolver extends BaseEditorResolver {
nameof<Tenant>(x => x.code),
nameof<Tenant>(x => x.description),
[nameof<Tenant>(x => x.config), nameof<TenantConfig>(x => x.deposit), nameof<TenantDepositConfig>(x => x.sources), nameof<TenantSource>(x => x.url)].join('.'),
[nameof<Tenant>(x => x.config), nameof<TenantConfig>(x => x.deposit), nameof<TenantDepositConfig>(x => x.sources), nameof<TenantSource>(x => x.issuerUrl)].join('.'),
[nameof<Tenant>(x => x.config), nameof<TenantConfig>(x => x.deposit), nameof<TenantDepositConfig>(x => x.sources), nameof<TenantSource>(x => x.clientId)].join('.'),
[nameof<Tenant>(x => x.config), nameof<TenantConfig>(x => x.deposit), nameof<TenantDepositConfig>(x => x.sources), nameof<TenantSource>(x => x.clientSecret)].join('.'),
[nameof<Tenant>(x => x.config), nameof<TenantConfig>(x => x.deposit), nameof<TenantDepositConfig>(x => x.sources), nameof<TenantSource>(x => x.scope)].join('.'),
[nameof<Tenant>(x => x.config), nameof<TenantConfig>(x => x.deposit), nameof<TenantDepositConfig>(x => x.sources), nameof<TenantSource>(x => x.codes)].join('.'),
[nameof<Tenant>(x => x.config), nameof<TenantConfig>(x => x.fileTransformers), nameof<TenantDepositConfig>(x => x.sources), nameof<TenantSource>(x => x.url)].join('.'),
[nameof<Tenant>(x => x.config), nameof<TenantConfig>(x => x.fileTransformers), nameof<TenantDepositConfig>(x => x.sources), nameof<TenantSource>(x => x.issuerUrl)].join('.'),
[nameof<Tenant>(x => x.config), nameof<TenantConfig>(x => x.fileTransformers), nameof<TenantDepositConfig>(x => x.sources), nameof<TenantSource>(x => x.clientId)].join('.'),
[nameof<Tenant>(x => x.config), nameof<TenantConfig>(x => x.fileTransformers), nameof<TenantDepositConfig>(x => x.sources), nameof<TenantSource>(x => x.clientSecret)].join('.'),
[nameof<Tenant>(x => x.config), nameof<TenantConfig>(x => x.fileTransformers), nameof<TenantDepositConfig>(x => x.sources), nameof<TenantSource>(x => x.scope)].join('.'),
[nameof<Tenant>(x => x.config), nameof<TenantConfig>(x => x.fileTransformers), nameof<TenantDepositConfig>(x => x.sources), nameof<TenantSource>(x => x.codes)].join('.'),
nameof<Tenant>(x => x.createdAt),
nameof<Tenant>(x => x.updatedAt),

View File

@ -14,7 +14,6 @@ import { TenantEditorComponent } from './editor/tenant-editor.component';
import { TenantListingComponent } from './listing/tenant-listing.component';
import { TenantListingFiltersComponent } from "./listing/filters/tenant-listing-filters.component";
import { RichTextEditorModule } from '@app/library/rich-text-editor/rich-text-editor.module';
import { TenantSourceComponent } from './editor/source/tenant-source.component';
@NgModule({
imports: [
@ -34,8 +33,7 @@ import { TenantSourceComponent } from './editor/source/tenant-source.component';
declarations: [
TenantEditorComponent,
TenantListingComponent,
TenantListingFiltersComponent,
TenantSourceComponent
TenantListingFiltersComponent
]
})
export class TenantModule { }

View File

@ -111,8 +111,8 @@
<div>{{'DESCRIPTION-EDITOR.TOC.NEXT' | translate}}</div>
<span class="material-icons">chevron_right</span>
</div>
<button [disabled]="saving" (click)="save(saveAnd.addNew)" *ngIf="(step === maxStep) && !isLocked && formGroup.get('descriptionTemplateId').value && !viewOnly" mat-raised-button type="button" class="col-auto stepper-btn add-description-btn ml-auto">
{{ 'DESCRIPTION-EDITOR.ACTIONS.SAVE-AND-ADD-NEW' | translate }}
<button [disabled]="saving" (click)="saveAndClose()" *ngIf="(step === maxStep) && !isLocked && formGroup.get('descriptionTemplateId').value && !viewOnly" mat-raised-button type="button" class="col-auto stepper-btn add-description-btn ml-auto">
{{ 'DESCRIPTION-EDITOR.ACTIONS.SAVE' | translate }}
</button>
</div>
<div class="col-auto pr-0">

View File

@ -806,6 +806,37 @@ export class DescriptionEditorComponent extends BaseEditor<DescriptionEditorMode
}
}
get maxStep() {
return this.visibleFieldSets.length;
}
public nextStep() {
if (this.step < this.maxStep) {//view is changing
this.step = Math.floor(this.step + 1);
let entry = this.visibleFieldSets[this.step - 1];
this.table0fContents.onToCentrySelected(entry, false);
this.scroll(entry);
}
}
public previousStep() {
if (this.step > 0) {
this.step = Math.ceil(this.step - 1);
if (this.step >= 1) {
let entry = this.visibleFieldSets[this.step - 1];
this.table0fContents.onToCentrySelected(entry, false);
this.scroll(entry);
} else {
this.table0fContents.onToCentrySelected(null, false);
this.resetScroll();
}
}
}
private scroll(entry: ToCEntry) {
document.getElementById(entry.id).scrollIntoView();
}
private resetScroll() {
document.getElementById('description-editor-form').scrollTop = 0;
}
@ -1968,56 +1999,6 @@ export class DescriptionEditorComponent extends BaseEditor<DescriptionEditorMode
// }
// hasNotReversableStatus(): boolean {
// if (this.formGroup.get('dmp').value && !this.publicMode) {
// return (this.formGroup.get('dmp').value.status == DmpStatus.Finalized && this.formGroup.get('status').value == DescriptionStatus.Finalized);
// } else {
// return false;
// }
// }
// reverse() {
// this.dialog.open(ConfirmationDialogComponent, {
// data: {
// message: this.language.instant('DATASET-WIZARD.ACTIONS.UNDO-FINALIZATION-QUESTION'),
// confirmButton: this.language.instant('DATASET-WIZARD.ACTIONS.CONFIRM'),
// cancelButton: this.language.instant('DATASET-WIZARD.ACTIONS.REJECT'),
// },
// maxWidth: '30em'
// })
// .afterClosed()
// .pipe(
// filter(x => x),
// takeUntil(this._destroyed)
// ).subscribe(result => {
// if (result) {
// // if (!this.isFormValid()) { return; }
// this.formGroup.get('status').setValue(DescriptionStatus.Draft);
// this.submit(SaveType.finalize, () => {
// this.viewOnly = false;
// this.descriptionModel.status = DescriptionStatus.Draft;
// setTimeout(x => {
// this.formGroup = null;
// });
// setTimeout(x => {
// this.formGroup = this.descriptionModel.buildForm();
// this.registerFormListeners();
// });
// }, () => {
// this.formGroup.get('status').setValue(DescriptionStatus.Finalized);
// this.viewOnly = true;
// });
// } else {
// this.saving = false;
// }
// });
// }
// saveFinalize() {
// // this.formService.touchAllFormFields(this.formGroup);
// this.saving = true;
@ -2125,43 +2106,6 @@ export class DescriptionEditorComponent extends BaseEditor<DescriptionEditorMode
// });
// }
// downloadPDF(id: string): void {
// this.descriptionService.downloadPDF(id)
// .pipe(takeUntil(this._destroyed))
// .subscribe(response => {
// const blob = new Blob([response.body], { type: 'application/pdf' });
// const filename = this.fileUtils.getFilenameFromContentDispositionHeader(response.headers.get('Content-Disposition'));
// FileSaver.saveAs(blob, filename);
// this.matomoService.trackDownload('descriptions', "pdf", id);
// });
// }
// downloadDOCX(id: string): void {
// this.descriptionService.downloadDOCX(id)
// .pipe(takeUntil(this._destroyed))
// .subscribe(response => {
// const blob = new Blob([response.body], { type: 'application/msword' });
// const filename = this.fileUtils.getFilenameFromContentDispositionHeader(response.headers.get('Content-Disposition'));
// FileSaver.saveAs(blob, filename);
// this.matomoService.trackDownload('descriptions', "docx", id);
// });
// }
// downloadXML(id: string): void {
// this.descriptionService.downloadXML(id)
// .pipe(takeUntil(this._destroyed))
// .subscribe(response => {
// const blob = new Blob([response.body], { type: 'application/xml' });
// const filename = this.fileUtils.getFilenameFromContentDispositionHeader(response.headers.get('Content-Disposition'));
// FileSaver.saveAs(blob, filename);
// this.matomoService.trackDownload('descriptions', "xml", id);
// });
// }
// // advancedClicked() {
// // const dialogRef = this.dialog.open(ExportMethodDialogComponent, {
// // maxWidth: '500px',
@ -2326,53 +2270,6 @@ export class DescriptionEditorComponent extends BaseEditor<DescriptionEditorMode
// }
// }
// public changeStep(selected: ToCEntry = null, execute: boolean = true) {
// if (execute) {
// if (selected) {
// let fieldSet = this.getFirstFieldSet(selected);
// let index = this.visibleFieldSets.findIndex(entry => entry.id === fieldSet.id);
// this.step = index + (selected.type === ToCEntryType.FieldSet ? 1 : 0.5);
// } else {
// this.step = 0;
// this.resetScroll();
// }
// }
// }
// get maxStep() {
// return this.visibleFieldSets.length;
// }
// public nextStep() {
// if (this.step < this.maxStep) {//view is changing
// this.step = Math.floor(this.step + 1);
// let entry = this.visibleFieldSets[this.step - 1];
// this.table0fContents.onToCentrySelected(entry, false);
// this.scroll(entry);
// }
// }
// public previousStep() {
// if (this.step > 0) {
// this.step = Math.ceil(this.step - 1);
// if (this.step >= 1) {
// let entry = this.visibleFieldSets[this.step - 1];
// this.table0fContents.onToCentrySelected(entry, false);
// this.scroll(entry);
// } else {
// this.table0fContents.onToCentrySelected(null, false);
// this.resetScroll();
// }
// }
// }
// private resetScroll() {
// document.getElementById('description-editor-form').scrollTop = 0;
// }
// private scroll(entry: ToCEntry) {
// document.getElementById(entry.id).scrollIntoView();
// }
// isDirty() {
// return this.formGroup.dirty && this.hasChanges; // do we need this.formGroup.dirty
@ -2411,11 +2308,6 @@ export class DescriptionEditorComponent extends BaseEditor<DescriptionEditorMode
// this.location.go(url);
// }
// backToDmp(id: string) {
// this.router.navigate(['/plans', 'edit', id]);
// }
// getLinks(currentLinks: Link[]) {
// this.links = currentLinks;

View File

@ -13,7 +13,7 @@
</div>
</div>
<div *ngIf="!fieldSet?.multiplicity?.tableView" class="col-12">
<div class="row" *ngFor="let fieldSetItemPropertiesControl of propertiesFormGroup?.get('items')?.controls">
<div class="row" *ngFor="let fieldSetItemPropertiesControl of propertiesFormGroup?.get('items')?.controls; let i=index;">
<div class="col" *ngIf="visibilityRulesService && visibilityRulesService.isVisibleMap[fieldSet.id + '_' + fieldSetItemPropertiesControl.get('ordinal').value] ?? true">
<div class="row">
<div *ngFor="let field of fieldSet.fields; let i = index;" class="col-12">

View File

@ -107,7 +107,7 @@
</ng-container>
<ng-container *ngSwitchCase="descriptionTemplateFieldTypeEnum.UPLOAD">
<div class="col-12 d-flex justify-content-center">
<ngx-dropzone class="drop-file col-12" (change)="fileChangeEvent($event, true)" [multiple]="false" [accept]="typesToString()" [disabled]="propertiesFormGroup?.get(field.id).get('textValue').disabled">
<ngx-dropzone #drop class="drop-file col-12" (change)="fileChangeEvent($event, true)" [multiple]="false" [accept]="typesToString()" [disabled]="propertiesFormGroup?.get(field.id).get('textValue').disabled">
<ngx-dropzone-preview *ngIf="propertiesFormGroup?.get(field.id).get('textValue').value" class="file-preview" [removable]="true" (removed)="onRemove()">
<ngx-dropzone-label class="file-label">{{ fileNameDisplay }}</ngx-dropzone-label>
</ngx-dropzone-preview>

View File

@ -116,6 +116,7 @@ export class SidebarComponent implements OnInit {
if (this.authentication.hasPermission(AppPermission.ViewReferenceTypePage)) this.adminItems.routes.push({ path: '/reference-type', title: 'SIDE-BAR.REFERENCE-TYPES', icon: 'add_link' });
if (this.authentication.hasPermission(AppPermission.ViewPrefillingSourcePage)) this.adminItems.routes.push({ path: '/prefilling-sources', title: 'SIDE-BAR.PREFILLING-SOURCES', icon: 'quick_reference_all' });
if (this.authentication.hasPermission(AppPermission.ViewTenantPage)) this.adminItems.routes.push({ path: '/tenants', title: 'SIDE-BAR.TENANTS', icon: 'tenancy' });
if (this.authentication.hasPermission(AppPermission.ViewTenantConfigurationPage)) this.adminItems.routes.push({ path: '/tenant-configuration', title: 'SIDE-BAR.TENANT-CONFIGURATION', icon: 'settings' });
if (this.authentication.hasPermission(AppPermission.ViewUserPage)) this.adminItems.routes.push({ path: '/users', title: 'SIDE-BAR.USERS', icon: 'people' });
if (this.authentication.hasPermission(AppPermission.ViewLanguagePage)) this.adminItems.routes.push({ path: '/languages', title: 'SIDE-BAR.LANGUAGES', icon: 'language' });
if (this.authentication.hasPermission(AppPermission.ViewSupportiveMaterialPage)) this.adminItems.routes.push({ path: '/supportive-material', title: 'SIDE-BAR.SUPPORTIVE-MATERIAL', icon: 'help_center' });