diff --git a/backend/src/migrations/1723119327053-ApplicationLabel.ts b/backend/src/migrations/1723119327053-ApplicationLabel.ts
new file mode 100644
index 000000000..4f3430333
--- /dev/null
+++ b/backend/src/migrations/1723119327053-ApplicationLabel.ts
@@ -0,0 +1,33 @@
+import { MigrationInterface, QueryRunner } from 'typeorm';
+
+export class ApplicationLabel1723119327053 implements MigrationInterface {
+ name = 'ApplicationLabel1723119327053';
+
+ public async up(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(
+ `CREATE TABLE "_application-label" ("id" SERIAL NOT NULL, "deleted_on" TIMESTAMP WITH TIME ZONE, "created_on" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_on" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "name" text NOT NULL, CONSTRAINT "PK_c0aaf1127ad3beeaf0d3ad70096" PRIMARY KEY ("id"))`,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_e4a4e4b1582c4e665cff9be33e" ON "_application-label" ("created_on") `,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "application" ADD "application_label_id" integer`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "application" ADD CONSTRAINT "FK_318029631a770782ba1c66721fd" FOREIGN KEY ("application_label_id") REFERENCES "_application-label"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(
+ `ALTER TABLE "application" DROP CONSTRAINT "FK_318029631a770782ba1c66721fd"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "application" DROP COLUMN "application_label_id"`,
+ );
+ await queryRunner.query(
+ `DROP INDEX "public"."IDX_e4a4e4b1582c4e665cff9be33e"`,
+ );
+ await queryRunner.query(`DROP TABLE "_application-label"`);
+ }
+}
diff --git a/backend/src/modules/application/constants/application.constants.ts b/backend/src/modules/application/constants/application.constants.ts
index 27d353eec..ed5d0f177 100644
--- a/backend/src/modules/application/constants/application.constants.ts
+++ b/backend/src/modules/application/constants/application.constants.ts
@@ -10,6 +10,7 @@ export const ORGANIZATION_ALL_APPS_COLUMNS = [
'ongApp.status as "ongStatus"',
'ongApp.created_on as "createdOn"',
'application.type as type',
+ 'applicationLabel.name as "applicationLabel"',
];
export const APPLICATIONS_FILES_DIR = 'applications';
diff --git a/backend/src/modules/application/controllers/application.controller.ts b/backend/src/modules/application/controllers/application.controller.ts
index 28fae5c19..9dd3843b3 100644
--- a/backend/src/modules/application/controllers/application.controller.ts
+++ b/backend/src/modules/application/controllers/application.controller.ts
@@ -108,24 +108,6 @@ export class ApplicationController {
);
}
- @Roles(Role.SUPER_ADMIN)
- @ApiParam({ name: 'id', type: String })
- @Patch(':id/activate')
- activate(@Param('id') id: number) {
- return this.appService.update(id, {
- status: ApplicationStatus.ACTIVE,
- });
- }
-
- @Roles(Role.SUPER_ADMIN)
- @ApiParam({ name: 'id', type: String })
- @Patch(':id/deactivate')
- deactivate(@Param('id') id: number) {
- return this.appService.update(id, {
- status: ApplicationStatus.DISABLED,
- });
- }
-
@Roles(Role.SUPER_ADMIN)
@ApiParam({ name: 'id', type: String })
@ApiQuery({ type: () => ApplicationAccessFilterDto })
diff --git a/backend/src/modules/application/dto/create-application.dto.ts b/backend/src/modules/application/dto/create-application.dto.ts
index 1726aacbb..78a303fe9 100644
--- a/backend/src/modules/application/dto/create-application.dto.ts
+++ b/backend/src/modules/application/dto/create-application.dto.ts
@@ -10,6 +10,7 @@ import {
import { REGEX } from 'src/common/constants/patterns.constant';
import { ApplicationPullingType } from '../enums/application-pulling-type.enum';
import { ApplicationTypeEnum } from '../enums/ApplicationType.enum';
+import { ApplicationLabel } from 'src/shared/entities/application-labels.entity';
export class CreateApplicationDto {
@IsString()
@@ -56,4 +57,7 @@ export class CreateApplicationDto {
@IsOptional()
@Length(2, 100, { each: true })
steps?: string[];
+
+ @IsOptional()
+ applicationLabel: Partial;
}
diff --git a/backend/src/modules/application/entities/application.entity.ts b/backend/src/modules/application/entities/application.entity.ts
index b88beb87e..05ebbc077 100644
--- a/backend/src/modules/application/entities/application.entity.ts
+++ b/backend/src/modules/application/entities/application.entity.ts
@@ -1,9 +1,17 @@
import { BaseEntity } from 'src/common/base/base-entity.class';
-import { Column, Entity, OneToMany } from 'typeorm';
+import {
+ Column,
+ Entity,
+ JoinColumn,
+ JoinTable,
+ ManyToOne,
+ OneToMany,
+} from 'typeorm';
import { ApplicationPullingType } from '../enums/application-pulling-type.enum';
import { ApplicationStatus } from '../enums/application-status.enum';
import { ApplicationTypeEnum } from '../enums/ApplicationType.enum';
import { OngApplication } from './ong-application.entity';
+import { ApplicationLabel } from 'src/shared/entities/application-labels.entity';
@Entity()
export class Application extends BaseEntity {
@@ -60,4 +68,11 @@ export class Application extends BaseEntity {
@OneToMany(() => OngApplication, (ongApp) => ongApp.application)
ongApplications: OngApplication[];
+
+ @Column({ type: 'integer', name: 'application_label_id', nullable: true })
+ applicationLabelId: number;
+
+ @ManyToOne((type) => ApplicationLabel)
+ @JoinColumn({ name: 'application_label_id' })
+ applicationLabel: ApplicationLabel;
}
diff --git a/backend/src/modules/application/interfaces/ong-application.interface.ts b/backend/src/modules/application/interfaces/ong-application.interface.ts
index 80dcb4b4c..902e88e26 100644
--- a/backend/src/modules/application/interfaces/ong-application.interface.ts
+++ b/backend/src/modules/application/interfaces/ong-application.interface.ts
@@ -1,3 +1,4 @@
+import { ApplicationLabel } from 'src/shared/entities/application-labels.entity';
import { ApplicationPullingType } from '../enums/application-pulling-type.enum';
import { ApplicationStatus } from '../enums/application-status.enum';
import { ApplicationTypeEnum } from '../enums/ApplicationType.enum';
@@ -23,4 +24,5 @@ export interface IOngApplicationDetails extends IOngApplication {
videoLink: string;
userStatus: UserOngApplicationStatus;
pullingType: ApplicationPullingType;
+ applicationLabel: ApplicationLabel;
}
diff --git a/backend/src/modules/application/services/application.service.ts b/backend/src/modules/application/services/application.service.ts
index 901e8637f..90d5ef357 100644
--- a/backend/src/modules/application/services/application.service.ts
+++ b/backend/src/modules/application/services/application.service.ts
@@ -37,6 +37,8 @@ import { ApplicationTableViewRepository } from '../repositories/application-tabl
import { ApplicationRepository } from '../repositories/application.repository';
import { OngApplicationRepository } from '../repositories/ong-application.repository';
import { UserOngApplicationRepository } from '../repositories/user-ong-application.repository';
+import { ApplicationLabel } from 'src/shared/entities/application-labels.entity';
+import { NomenclaturesService } from 'src/shared/services';
@Injectable()
export class ApplicationService {
@@ -49,6 +51,7 @@ export class ApplicationService {
private readonly ongApplicationRepository: OngApplicationRepository,
private readonly userOngApplicationRepository: UserOngApplicationRepository,
private readonly applicationOngViewRepository: ApplicationOngViewRepository,
+ private readonly nomenclatureService: NomenclaturesService,
) {}
public async create(
@@ -250,6 +253,7 @@ export class ApplicationService {
'application.video_link as "videoLink"',
'application.pulling_type as "pullingType"',
'application.status as "applicationStatus"',
+ 'applicationLabel',
])
.leftJoin(
'ong_application',
@@ -262,6 +266,11 @@ export class ApplicationService {
'userOngApp',
'userOngApp.ong_application_id = ongApp.id',
)
+ .leftJoin(
+ '_application-label',
+ 'applicationLabel',
+ 'applicationLabel.id = application.application_label_id',
+ )
.where('application.id = :applicationId', { applicationId });
// for employee add further filtersin by user id
@@ -292,8 +301,19 @@ export class ApplicationService {
);
}
+ const applicationLabel = {
+ id: applicationWithDetails.applicationLabel_id,
+ name: applicationWithDetails.applicationLabel_name,
+ };
+
+ delete applicationWithDetails.applicationLabel_id;
+ delete applicationWithDetails.applicationLabel_name;
+ delete applicationWithDetails.applicationLabel_created_on;
+ delete applicationWithDetails.applicationLabel_updated_on;
+
return {
...applicationWithDetails,
+ applicationLabel,
logo,
};
}
@@ -346,7 +366,17 @@ export class ApplicationService {
};
}
- return this.applicationRepository.update({ id }, applicationPayload);
+ let applicationLabel = null;
+ if (applicationPayload.applicationLabel) {
+ applicationLabel = await this.saveAndGetApplicationLabel(
+ applicationPayload.applicationLabel,
+ );
+ }
+
+ return this.applicationRepository.update(
+ { id },
+ { ...applicationPayload, applicationLabel },
+ );
} catch (error) {
this.logger.error({
error: { error },
@@ -502,4 +532,17 @@ export class ApplicationService {
return applicationCount;
}
+
+ private async saveAndGetApplicationLabel(
+ label: Partial,
+ ): Promise {
+ if (label.id) {
+ return label as ApplicationLabel;
+ }
+
+ const newLabel =
+ await this.nomenclatureService.createApplicationLabel(label);
+
+ return newLabel;
+ }
}
diff --git a/backend/src/modules/application/services/ong-application.service.ts b/backend/src/modules/application/services/ong-application.service.ts
index 6dc627bc3..31c101c39 100644
--- a/backend/src/modules/application/services/ong-application.service.ts
+++ b/backend/src/modules/application/services/ong-application.service.ts
@@ -91,7 +91,7 @@ export class OngApplicationService {
: OngApplicationStatus.ACTIVE,
});
- if(application.type === ApplicationTypeEnum.STANDALONE) {
+ if (application.type === ApplicationTypeEnum.STANDALONE) {
// 8. trigger emails for admin and super-admin
this.eventEmitter.emit(
EVENTS.REQUEST_APPLICATION_ACCESS,
@@ -99,12 +99,11 @@ export class OngApplicationService {
);
}
-
return ongApp;
} catch (error) {
Sentry.captureException(error, {
- extra: {organizationId, applicationId},
- })
+ extra: { organizationId, applicationId },
+ });
this.logger.error({ error: { error }, ...ONG_APPLICATION_ERRORS.CREATE });
const err = error?.response;
throw new BadRequestException({
@@ -174,6 +173,11 @@ export class OngApplicationService {
const applicationsQuery = this.applicationRepository
.getQueryBuilder()
.select(ORGANIZATION_ALL_APPS_COLUMNS)
+ .leftJoin(
+ '_application-label',
+ 'applicationLabel',
+ 'applicationLabel.id = application.application_label_id',
+ )
.leftJoin(
'ong_application',
'ongApp',
diff --git a/backend/src/shared/controllers/nomenclatures.controller.ts b/backend/src/shared/controllers/nomenclatures.controller.ts
index 26060590d..9135f7927 100644
--- a/backend/src/shared/controllers/nomenclatures.controller.ts
+++ b/backend/src/shared/controllers/nomenclatures.controller.ts
@@ -13,69 +13,86 @@ import { NomenclaturesService } from '../services';
import { CacheInterceptor } from '@nestjs/cache-manager';
@Public()
-@UseInterceptors(ClassSerializerInterceptor, CacheInterceptor)
+@UseInterceptors(ClassSerializerInterceptor)
@Controller('nomenclatures')
export class NomenclaturesController {
constructor(private nomenclaturesService: NomenclaturesService) {}
@Get('cities')
+ @UseInterceptors(CacheInterceptor)
getCities(@Query() citySearchDto: CitySearchDto) {
return this.nomenclaturesService.getCitiesSearch(citySearchDto);
}
@Get('counties')
+ @UseInterceptors(CacheInterceptor)
getCounties() {
return this.nomenclaturesService.getCounties({});
}
@Get('domains')
+ @UseInterceptors(CacheInterceptor)
getDomains() {
return this.nomenclaturesService.getDomains({});
}
@Get('regions')
+ @UseInterceptors(CacheInterceptor)
getRegions() {
return this.nomenclaturesService.getRegions({});
}
@Get('federations')
+ @UseInterceptors(CacheInterceptor)
getFederations() {
return this.nomenclaturesService.getFederations({});
}
@Get('coalitions')
+ @UseInterceptors(CacheInterceptor)
getCoalitions() {
return this.nomenclaturesService.getCoalitions({});
}
@Get('faculties')
+ @UseInterceptors(CacheInterceptor)
getFaculties(@Query() { search }: FacultySearchDto) {
const options = search ? { where: { name: ILike(`%${search}%`) } } : {};
return this.nomenclaturesService.getFaculties(options);
}
@Get('skills')
+ @UseInterceptors(CacheInterceptor)
getSkills() {
return this.nomenclaturesService.getSkills({});
}
@Get('practice-domains')
+ @UseInterceptors(CacheInterceptor)
getPracticeDomains() {
return this.nomenclaturesService.getPracticeDomains({});
}
@Get('service-domains')
+ @UseInterceptors(CacheInterceptor)
getServiceDomains() {
return this.nomenclaturesService.getServiceDomains({});
}
@Get('beneficiaries')
+ @UseInterceptors(CacheInterceptor)
getBeneficiaries() {
return this.nomenclaturesService.getBeneficiaries({});
}
@Get('issuers')
+ @UseInterceptors(CacheInterceptor)
getIssuers() {
return this.nomenclaturesService.getIssuers({});
}
+
+ @Get('application-labels')
+ getApplicationLabels() {
+ return this.nomenclaturesService.getApplicationLabels({});
+ }
}
diff --git a/backend/src/shared/entities/application-labels.entity.ts b/backend/src/shared/entities/application-labels.entity.ts
new file mode 100644
index 000000000..74b687b83
--- /dev/null
+++ b/backend/src/shared/entities/application-labels.entity.ts
@@ -0,0 +1,8 @@
+import { BaseEntity } from 'src/common/base/base-entity.class';
+import { Column, Entity } from 'typeorm';
+
+@Entity({ name: '_application-label' })
+export class ApplicationLabel extends BaseEntity {
+ @Column({ type: 'text', name: 'name' })
+ name: string;
+}
diff --git a/backend/src/shared/services/nomenclatures.service.ts b/backend/src/shared/services/nomenclatures.service.ts
index af2d37465..790afe0c9 100644
--- a/backend/src/shared/services/nomenclatures.service.ts
+++ b/backend/src/shared/services/nomenclatures.service.ts
@@ -17,6 +17,7 @@ import { Federation } from '../entities/federation.entity';
import { PracticeDomain } from 'src/modules/practice-program/entities/practice_domain.entity';
import { ServiceDomain } from 'src/modules/civic-center-service/entities/service-domain.entity';
import { Beneficiary } from 'src/modules/civic-center-service/entities/beneficiary.entity';
+import { ApplicationLabel } from '../entities/application-labels.entity';
@Injectable()
export class NomenclaturesService {
@@ -45,6 +46,8 @@ export class NomenclaturesService {
private readonly beneficiaryRepository: Repository,
@InjectRepository(Issuer)
private readonly issuersRepository: Repository,
+ @InjectRepository(ApplicationLabel)
+ private readonly applicationLabelRepository: Repository,
) {}
public getCity(conditions: FindOneOptions) {
@@ -169,4 +172,14 @@ export class NomenclaturesService {
public getIssuers(conditions: FindManyOptions) {
return this.issuersRepository.find(conditions);
}
+
+ public getApplicationLabels(conditions: FindManyOptions) {
+ return this.applicationLabelRepository.find(conditions);
+ }
+
+ public createApplicationLabel(
+ applicationLabel: Partial,
+ ): Promise {
+ return this.applicationLabelRepository.save(applicationLabel);
+ }
}
diff --git a/backend/src/shared/shared.module.ts b/backend/src/shared/shared.module.ts
index dc13eab07..31086f825 100644
--- a/backend/src/shared/shared.module.ts
+++ b/backend/src/shared/shared.module.ts
@@ -20,6 +20,7 @@ import { FileManagerService } from './services/file-manager.service';
import { PracticeDomain } from 'src/modules/practice-program/entities/practice_domain.entity';
import { ServiceDomain } from 'src/modules/civic-center-service/entities/service-domain.entity';
import { Beneficiary } from 'src/modules/civic-center-service/entities/beneficiary.entity';
+import { ApplicationLabel } from './entities/application-labels.entity';
@Global()
@Module({
@@ -37,6 +38,7 @@ import { Beneficiary } from 'src/modules/civic-center-service/entities/beneficia
ServiceDomain,
Beneficiary,
Issuer,
+ ApplicationLabel,
]),
HttpModule,
],
diff --git a/frontend/src/assets/locales/ro/translation.json b/frontend/src/assets/locales/ro/translation.json
index 0e215641a..d25ead90c 100644
--- a/frontend/src/assets/locales/ro/translation.json
+++ b/frontend/src/assets/locales/ro/translation.json
@@ -27,6 +27,7 @@
"completed": "Completat",
"not_completed": "Necompletat"
},
+ "add_option": "Adaugă opțiunea",
"any": "Toate",
"restricted": "Restricționat",
"active": "Activ",
@@ -1062,6 +1063,15 @@
"label": "Descriere scurtă",
"helper": "Descrie aplicația în maxim 200 de caractere"
},
+ "status": {
+ "label": "Status aplicație",
+ "options": {
+ "active": "Activă (aplicația poate fi adăugată de organizații în profilul lor)",
+ "disabled": "Inactivă (aplicația nu poate fi adăugată de organizații în profilul lor)"
+ },
+ "section_title": "Status aplicație",
+ "section_subtitle": "Statusul aplicației influențează disponibilitatea acesteia pentru organizații"
+ },
"description": {
"required": "Descrierea extinsă este obligatorie",
"max": "Descrierea extinsă poate avea maxim 7000 de caractere",
@@ -1097,6 +1107,11 @@
"min": "Pasul trebuie să aibă minim 2 caractere",
"max": "Pasul trebuie să aibă maxim 100 de caractere",
"label": "Pas"
+ },
+ "application_label": {
+ "label": "Eticheta pentru aplicație (eticheta apare în meniul Toate aplicațiile)",
+ "helper": "Adaugă o etichetă deja existentă sau creează una nouă",
+ "maxLength": "Eticheta poate avea maxim 30 de caractere"
}
},
"request_modal": {
diff --git a/frontend/src/common/helpers/format.helper.ts b/frontend/src/common/helpers/format.helper.ts
index 818b93e60..d9f0e3027 100644
--- a/frontend/src/common/helpers/format.helper.ts
+++ b/frontend/src/common/helpers/format.helper.ts
@@ -70,6 +70,11 @@ export const mapSelectToSkill = (
): { id?: number; name: string } =>
item?.__isNew__ ? { name: item.label } : { id: item.value, name: item.label };
+export const mapSelectToApplicationLabel = (
+ item: ISelectData & { __isNew__?: boolean },
+): { id?: number; name: string } =>
+ item?.__isNew__ ? { name: item.label } : { id: item.value, name: item.label };
+
// Cities / Counties
export const mapCitiesToSelect = (item: any): ISelectData => ({
value: item?.id,
diff --git a/frontend/src/common/interfaces/application-label.interface.ts b/frontend/src/common/interfaces/application-label.interface.ts
new file mode 100644
index 000000000..d3b372483
--- /dev/null
+++ b/frontend/src/common/interfaces/application-label.interface.ts
@@ -0,0 +1,3 @@
+import { BaseNomenclatureEntity } from './base-nomenclature-entity.interface';
+
+export interface ApplicationLabel extends BaseNomenclatureEntity {}
diff --git a/frontend/src/components/content-wrapper/ContentWrapper.tsx b/frontend/src/components/content-wrapper/ContentWrapper.tsx
index 1ad0b486f..6eb629276 100644
--- a/frontend/src/components/content-wrapper/ContentWrapper.tsx
+++ b/frontend/src/components/content-wrapper/ContentWrapper.tsx
@@ -53,7 +53,7 @@ const ContentWrapper = ({
)}
- {error && (
-
- {error}
-
- )}
+
);
};
diff --git a/frontend/src/index.css b/frontend/src/index.css
index 1e18ca122..cd01e5a8d 100644
--- a/frontend/src/index.css
+++ b/frontend/src/index.css
@@ -176,13 +176,21 @@
transform: rotate(-45deg);
position: absolute;
background-color: #374159;
- padding: 0.5rem 4.5rem;
+ padding-top: 0.5rem;
+ padding-left: 3.5rem;
+ padding-bottom: 0.5rem;
+ padding-right: 4.5rem;
color: white;
overflow: hidden;
+ width: 18rem;
+ margin-left: -0.5rem;
}
.ribbon p {
margin: 0;
+ white-space: normal;
+ overflow-wrap: break-word;
+ text-align: center;
}
.richtext_html ul {
diff --git a/frontend/src/pages/application/ApplicationWithOngList.tsx b/frontend/src/pages/application/ApplicationWithOngList.tsx
index 7c3699a89..3ce05447a 100644
--- a/frontend/src/pages/application/ApplicationWithOngList.tsx
+++ b/frontend/src/pages/application/ApplicationWithOngList.tsx
@@ -5,8 +5,11 @@ import { IPageTab } from '../../common/interfaces/tabs.interface';
import ContentWrapper from '../../components/content-wrapper/ContentWrapper';
import { Loading } from '../../components/loading/Loading';
import Select from '../../components/Select/Select';
-import { useApplicationQuery } from '../../services/application/Application.queries';
+import { useApplicationQuery, useRemoveApplication } from '../../services/application/Application.queries';
import { APPLICATION_TABS } from './constants/ApplicationTabs';
+import ConfirmationModal from '../../components/confim-removal-modal/ConfirmationModal';
+import { useTranslation } from 'react-i18next';
+import { useErrorToast } from '../../common/hooks/useToast';
const ApplicationWithOngList = () => {
const navigate = useNavigate();
@@ -17,6 +20,9 @@ const ApplicationWithOngList = () => {
});
const params = useParams();
const locationLength = location.pathname.split('/').length - 1;
+ const { t } = useTranslation(['appstore', 'common']);
+
+ const [applicationToBeRemoved, setApplicationToBeRemoved] = useState(null);
const {
data: application,
@@ -24,6 +30,10 @@ const ApplicationWithOngList = () => {
refetch: refecthApplication,
} = useApplicationQuery(params.id ? params?.id : '');
+ const {
+ mutate: removeApplication,
+ } = useRemoveApplication();
+
useEffect(() => {
const found: IPageTab | undefined = APPLICATION_TABS.find(
(tab) => tab.href === location.pathname.split('/')[locationLength],
@@ -59,6 +69,31 @@ const ApplicationWithOngList = () => {
navigate('/applications');
};
+ const onCancelRemoveApplication = () => {
+ setApplicationToBeRemoved(null);
+ }
+
+ const onConfirmDeleteApplication = () => {
+ if (applicationToBeRemoved) {
+ removeApplication({ applicationId: applicationToBeRemoved },
+ {
+ onSuccess: () => {
+ setApplicationToBeRemoved(null);
+ naivgateBack();
+ },
+ onError: () => {
+ useErrorToast(t('list.remove_error'));
+ }
+ })
+ }
+ }
+
+ const onSetApplicationToBeRemoved = () => {
+ if (params.id) {
+ setApplicationToBeRemoved(+params.id);
+ }
+ }
+
return (
{
onBtnClick: onApplicationEdit,
visible: true,
}}
+ deleteButton={{
+ btnLabel: 'Sterge',
+ onBtnClick: onSetApplicationToBeRemoved,
+ visible: true,
+ }}
>
-
+ {applicationToBeRemoved && (
+
+ )}
+
+
);
};
diff --git a/frontend/src/pages/apps-store/components/AddApplicationConfig.ts b/frontend/src/pages/apps-store/components/AddApplicationConfig.ts
index 7bbc8681c..74bae085b 100644
--- a/frontend/src/pages/apps-store/components/AddApplicationConfig.ts
+++ b/frontend/src/pages/apps-store/components/AddApplicationConfig.ts
@@ -3,6 +3,7 @@ import InputFieldHttpAddon from '../../../components/InputField/components/Input
import { ApplicationTypeEnum, ApplicationTypeNaming } from '../constants/ApplicationType.enum';
import i18n from '../../../common/config/i18n';
import { ApplicationPullingType } from '../enums/application-pulling-type.enum';
+import { ApplicationStatus } from '../../../services/application/interfaces/Application.interface';
const translations = {
name: {
@@ -17,6 +18,9 @@ const translations = {
label: i18n.t('appstore:config.type.label'),
required: i18n.t('appstore:config.type.required'),
},
+ status: {
+ label: i18n.t('appstore:config.status.label'),
+ },
short: {
required: i18n.t('appstore:config.short.required'),
max: i18n.t('appstore:config.short.max'),
@@ -139,6 +143,44 @@ export const AddAppConfig: Record = {
},
],
},
+ status: {
+ key: 'status',
+ label: translations.status.label,
+ rules: {
+ required: {
+ value: true,
+ message: translations.type.required,
+ },
+ },
+ helperText: '',
+ radioConfigs: [
+ {
+ label: i18n.t('appstore:config.status.options.active'),
+ name: 'status',
+ value: ApplicationStatus.ACTIVE,
+ },
+ {
+ label: i18n.t('appstore:config.status.options.disabled'),
+ name: 'status',
+ value: ApplicationStatus.DISABLED,
+ },
+ ],
+ },
+ applicationLabel: {
+ key: 'applicationLabel',
+ rules: {
+ maxLength: {
+ value: 30,
+ message: translations.short.max,
+ },
+ },
+ config: {
+ type: 'text',
+ label: i18n.t('appstore:config.application_label.label'),
+ helperText: i18n.t('appstore:config.application_label.helper'),
+ placeholder: '',
+ },
+ },
shortDescription: {
key: 'shortDescription',
rules: {
diff --git a/frontend/src/pages/apps-store/components/ApplicationForm.tsx b/frontend/src/pages/apps-store/components/ApplicationForm.tsx
index f73eb0cd7..9fb0140fd 100644
--- a/frontend/src/pages/apps-store/components/ApplicationForm.tsx
+++ b/frontend/src/pages/apps-store/components/ApplicationForm.tsx
@@ -7,28 +7,33 @@ import {
FieldErrorsImpl,
DeepRequired,
UseFormWatch,
+ UseFormClearErrors,
} from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { FILE_TYPES_ACCEPT } from '../../../common/constants/file.constants';
-import { fileToURL } from '../../../common/helpers/format.helper';
+import { fileToURL, mapNameToSelect } from '../../../common/helpers/format.helper';
import InputField from '../../../components/InputField/InputField';
import RadioGroup from '../../../components/RadioGroup/RadioGroup';
import SectionHeader from '../../../components/section-header/SectionHeader';
import Select from '../../../components/Select/Select';
-import Textarea from '../../../components/Textarea/Textarea';
import { CreateApplicationDto } from '../../../services/application/interfaces/Application.dto';
import { ApplicationTypeEnum } from '../constants/ApplicationType.enum';
import { AddAppConfig } from './AddApplicationConfig';
import RichText from '../../../components/RichText/RichText';
+import { ApplicationStatus } from '../../../services/application/interfaces/Application.interface';
+import CreatableSelectComponent from '../../../components/creatable-multi-select/CreatableMultiSelect';
+import { useNomenclature } from '../../../store/selectors';
+import { useApplicationLabelsQuery } from '../../../services/nomenclature/Nomenclature.queries';
interface ApplicationFormProps {
control: Control;
- errors: FieldErrorsImpl>;
+ errors: FieldErrorsImpl & { status?: ApplicationStatus }>;
watch: UseFormWatch;
file: File | null;
setFile: (file: File) => void;
logo?: string | null;
readonly?: boolean;
+ clearErrors?: UseFormClearErrors;
}
const ApplicationForm = ({
@@ -39,6 +44,7 @@ const ApplicationForm = ({
logo,
file,
setFile,
+ clearErrors
}: ApplicationFormProps) => {
const { fields, append, remove } = useFieldArray({
control,
@@ -47,6 +53,10 @@ const ApplicationForm = ({
const { t } = useTranslation('appstore');
+ const { applicationLabels } = useNomenclature()
+
+ useApplicationLabelsQuery();
+
// watchers
const type = watch('type');
@@ -59,6 +69,18 @@ const ApplicationForm = ({
}
};
+ const ribbonValidation = (inputValue: string) => {
+ if (inputValue.length > 30 && !(errors as Record)[AddAppConfig.applicationLabel.key]) {
+ control.setError(AddAppConfig.applicationLabel.key, { type: 'maxLength', message: t('config.application_label.maxLength') });
+ return false;
+ } else if (inputValue.length > 30) {
+ return false;
+ } else if (inputValue.length < 30 && clearErrors && (errors as Record)[AddAppConfig.applicationLabel.key]) {
+ clearErrors(AddAppConfig.applicationLabel.key);
+ }
+ return true;
+ }
+
return (
@@ -264,6 +286,34 @@ const ApplicationForm = ({
)}
{/* End Logo */}
+ {readonly &&
+
+
+ {
+ return (
+ )[
+ AddAppConfig.applicationLabel.key
+ ]?.message?.toString()}
+ onChange={onChange}
+ options={[...applicationLabels.map(mapNameToSelect)]}
+ validation={ribbonValidation}
+ />
+ );
+ }}
+ />
+
}
{fields.map((item, index) => {
@@ -288,7 +338,7 @@ const ApplicationForm = ({
)?.steps[index]?.item?.message as any),
defaultValue: value,
onChange: onChange,
- id: 'application-form__step',
+ id: `application-form__step_${index}`,
}}
/>
);
@@ -299,6 +349,8 @@ const ApplicationForm = ({
})}
{fields.length > 0 && (
);
};
diff --git a/frontend/src/pages/apps-store/components/EditApplication.tsx b/frontend/src/pages/apps-store/components/EditApplication.tsx
index 91d990555..27cf1efd3 100644
--- a/frontend/src/pages/apps-store/components/EditApplication.tsx
+++ b/frontend/src/pages/apps-store/components/EditApplication.tsx
@@ -13,6 +13,7 @@ import { CreateApplicationDto } from '../../../services/application/interfaces/A
import ApplicationForm from './ApplicationForm';
import { useTranslation } from 'react-i18next';
import { PullingTypeOptions } from './AddApplicationConfig';
+import { mapNameToSelect } from '../../../common/helpers/format.helper';
const EditApplication = () => {
const navigate = useNavigate();
@@ -27,6 +28,7 @@ const EditApplication = () => {
formState: { errors },
reset,
watch,
+ clearErrors
} = useForm
({
mode: 'onChange',
reValidateMode: 'onChange',
@@ -51,6 +53,7 @@ const EditApplication = () => {
pullingType: application.pullingType
? (PullingTypeOptions.find((item) => item.value === application.pullingType) as any)
: undefined,
+ applicationLabel: mapNameToSelect(application.applicationLabel)
});
setLogo(application.logo);
}
@@ -65,7 +68,6 @@ const EditApplication = () => {
const onSubmit = async (data: Partial) => {
// don't set the logo path
const { logo, ...payload } = data;
-
await updateApplication(
{
applicationId: id as string,
@@ -117,6 +119,7 @@ const EditApplication = () => {
logo={logo}
file={file}
setFile={setFile}
+ clearErrors={clearErrors}
/>
diff --git a/frontend/src/pages/my-apps/components/ApplicationCard.tsx b/frontend/src/pages/my-apps/components/ApplicationCard.tsx
index b01b174bd..80ccfc651 100644
--- a/frontend/src/pages/my-apps/components/ApplicationCard.tsx
+++ b/frontend/src/pages/my-apps/components/ApplicationCard.tsx
@@ -31,8 +31,7 @@ const ApplicationCard = ({ application }: { application: ApplicationWithOngStatu
return (
- {(application.ongStatus === OngApplicationStatus.RESTRICTED ||
- application.status === ApplicationStatus.DISABLED) && (
+ {(application.ongStatus === OngApplicationStatus.RESTRICTED) && (
{t('unavailable', { ns: 'common' })}
@@ -45,6 +44,12 @@ const ApplicationCard = ({ application }: { application: ApplicationWithOngStatu
)}
+ {application.applicationLabel && application.ongStatus !== OngApplicationStatus.PENDING_REMOVAL && application.ongStatus !== OngApplicationStatus.RESTRICTED && (
+
+
+ {application.applicationLabel}
+
+
)}

{
);
};
-export const useActivateApplication = () => {
- return useMutation(({ applicationId }: { applicationId: string }) =>
- activateApplication(applicationId),
- );
-};
-
-export const useDectivateApplication = () => {
- return useMutation(({ applicationId }: { applicationId: string }) =>
- deactivateApplication(applicationId),
- );
-};
-
export const useRestrictApplicationMutation = () => {
return useMutation(
({ applicationId, organizationId }: { applicationId: number; organizationId: string }) =>
diff --git a/frontend/src/services/application/Application.service.ts b/frontend/src/services/application/Application.service.ts
index 522a5abf8..f4de9789a 100644
--- a/frontend/src/services/application/Application.service.ts
+++ b/frontend/src/services/application/Application.service.ts
@@ -1,6 +1,6 @@
import { AxiosResponse } from 'axios';
import { OrderDirection } from '../../common/enums/sort-direction.enum';
-import { cleanupPayload } from '../../common/helpers/format.helper';
+import { cleanupPayload, mapSelectToApplicationLabel } from '../../common/helpers/format.helper';
import { PaginatedEntity } from '../../common/interfaces/paginated-entity.interface';
import { ApplicationTypeEnum } from '../../pages/apps-store/constants/ApplicationType.enum';
import { OngApplicationStatus } from '../../pages/requests/interfaces/OngApplication.interface';
@@ -14,6 +14,7 @@ import {
ApplicationStatus,
ApplicationWithOngStatus,
} from './interfaces/Application.interface';
+import { mapEntityToFormData } from '../organization/OrganizationFormDataMapper.service';
export const createApplication = (
createApplicationDto: CreateApplicationDto,
@@ -137,9 +138,9 @@ const generateApplicationFormDataPayload = (
logo?: File,
): FormData => {
// we remove the logo and steps
- const { steps, pullingType, ...data } = applicationDto;
+ const { steps, pullingType, applicationLabel, ...data } = applicationDto;
// create form data payload
- const payload = new FormData();
+ let payload = new FormData();
for (const prop in cleanupPayload(data)) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
payload.append(prop, (data as any)[prop] as string);
@@ -161,5 +162,13 @@ const generateApplicationFormDataPayload = (
payload.append('logo', logo);
}
+ if (applicationLabel) {
+ payload = mapEntityToFormData(
+ payload,
+ 'applicationLabel',
+ mapSelectToApplicationLabel(applicationLabel),
+ );
+ }
+
return payload;
};
diff --git a/frontend/src/services/application/interfaces/Application.dto.ts b/frontend/src/services/application/interfaces/Application.dto.ts
index db046df80..3e2763d66 100644
--- a/frontend/src/services/application/interfaces/Application.dto.ts
+++ b/frontend/src/services/application/interfaces/Application.dto.ts
@@ -11,4 +11,5 @@ export interface CreateApplicationDto {
website: string;
logo: string;
steps: { item: string }[];
+ applicationLabel: { label: string; value: string };
}
diff --git a/frontend/src/services/application/interfaces/Application.interface.ts b/frontend/src/services/application/interfaces/Application.interface.ts
index 531a2ae4c..b81812267 100644
--- a/frontend/src/services/application/interfaces/Application.interface.ts
+++ b/frontend/src/services/application/interfaces/Application.interface.ts
@@ -1,3 +1,4 @@
+import { ApplicationLabel } from '../../../common/interfaces/application-label.interface';
import { ApplicationTypeEnum } from '../../../pages/apps-store/constants/ApplicationType.enum';
import { ApplicationPullingType } from '../../../pages/apps-store/enums/application-pulling-type.enum';
import {
@@ -38,6 +39,7 @@ export interface ApplicationWithOngStatus {
pullingType?: ApplicationPullingType;
website: string;
createdOn: Date;
+ applicationLabel: string;
}
export interface ApplicationAccess {
diff --git a/frontend/src/services/nomenclature/Nomenclature.queries.ts b/frontend/src/services/nomenclature/Nomenclature.queries.ts
index 3781ebee2..ce471168b 100644
--- a/frontend/src/services/nomenclature/Nomenclature.queries.ts
+++ b/frontend/src/services/nomenclature/Nomenclature.queries.ts
@@ -17,6 +17,7 @@ import {
getServiceDomains,
getBeneficiaries,
getIssuers,
+ getApplicationLabels,
} from './Nomenclatures.service';
import { Coalition } from '../../common/interfaces/coalitions.interface';
import { Federation } from '../../common/interfaces/federations.interface';
@@ -129,3 +130,12 @@ export const useFacultiesQuery = () => {
},
});
};
+
+export const useApplicationLabelsQuery = () => {
+ const { setApplicationLabels } = useStore();
+ return useQuery('application-labels', () => getApplicationLabels(), {
+ onSuccess: (data) => {
+ setApplicationLabels(data);
+ },
+ });
+};
diff --git a/frontend/src/services/nomenclature/Nomenclatures.service.ts b/frontend/src/services/nomenclature/Nomenclatures.service.ts
index 543fa81cd..5e80752cb 100644
--- a/frontend/src/services/nomenclature/Nomenclatures.service.ts
+++ b/frontend/src/services/nomenclature/Nomenclatures.service.ts
@@ -64,3 +64,7 @@ export const getFaculties = (): Promise
=> {
export const getIssuers = (): Promise<{ id: number; name: string }[]> => {
return API.get(`/nomenclatures/issuers`).then((res) => res.data);
};
+
+export const getApplicationLabels = (): Promise<{ id: number; name: string }[]> => {
+ return API.get(`/nomenclatures/application-labels`).then((res) => res.data);
+};
diff --git a/frontend/src/store/nomenclature/nomenclature.selectors.ts b/frontend/src/store/nomenclature/nomenclature.selectors.ts
index f11fe9be8..b12cdc1c2 100644
--- a/frontend/src/store/nomenclature/nomenclature.selectors.ts
+++ b/frontend/src/store/nomenclature/nomenclature.selectors.ts
@@ -10,6 +10,7 @@ export const useNomenclature = () => {
const skills = useStore((state) => state.skills);
const faculties = useStore((state) => state.faculties);
const issuers = useStore((state) => state.issuers);
+ const applicationLabels = useStore((state) => state.applicationLabels);
return {
counties,
cities,
@@ -20,5 +21,6 @@ export const useNomenclature = () => {
skills,
faculties,
issuers,
+ applicationLabels,
};
};
diff --git a/frontend/src/store/nomenclature/nomenclature.slice.ts b/frontend/src/store/nomenclature/nomenclature.slice.ts
index c148e2b79..5a1ad5a96 100644
--- a/frontend/src/store/nomenclature/nomenclature.slice.ts
+++ b/frontend/src/store/nomenclature/nomenclature.slice.ts
@@ -1,3 +1,4 @@
+import { ApplicationLabel } from '../../common/interfaces/application-label.interface';
import { City } from '../../common/interfaces/city.interface';
import { Coalition } from '../../common/interfaces/coalitions.interface';
import { County } from '../../common/interfaces/county.interface';
@@ -19,6 +20,7 @@ export const nomenclatureSlice = (set: any) => ({
skills: [],
faculties: [],
issuers: [],
+ applicationLabels: [],
setCounties: (counties: County[]) => {
set({ counties });
},
@@ -46,6 +48,9 @@ export const nomenclatureSlice = (set: any) => ({
setIssuers: (issuers: Issuer[]) => {
set({ issuers });
},
+ setApplicationLabels: (applicationLabels: ApplicationLabel[]) => {
+ set({ applicationLabels });
+ },
});
export default { nomenclatureSlice };
diff --git a/frontend/src/store/store.ts b/frontend/src/store/store.ts
index 5993583bd..06dada152 100644
--- a/frontend/src/store/store.ts
+++ b/frontend/src/store/store.ts
@@ -47,6 +47,7 @@ import { Faculty } from '../common/interfaces/faculty.interface';
import { IFeedback } from '../pages/civic-center-service/interfaces/Feedback.interface';
import { feedbacksSlice } from './civic-center-service/Feedback.slice';
import { Issuer } from '../common/interfaces/issuer.interface';
+import { ApplicationLabel } from '../common/interfaces/application-label.interface';
interface OrganizationState {
organizations: PaginatedEntity;
@@ -74,6 +75,7 @@ interface NomenclatureState {
skills: Skill[];
faculties: Faculty[];
issuers: Issuer[];
+ applicationLabels: ApplicationLabel[];
setCounties: (counties: County[]) => void;
setCities: (cities: City[]) => void;
setDomains: (domains: Domain[]) => void;
@@ -83,6 +85,7 @@ interface NomenclatureState {
setSkills: (skills: Skill[]) => void;
setFaculties: (faculties: Faculty[]) => void;
setIssuers: (issuers: Issuer[]) => void;
+ setApplicationLabels: (applicationLabels: ApplicationLabel[]) => void;
}
interface ProfileState {