oip-common 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -411,6 +411,7 @@ class LayoutService {
411
411
  return ngDateFormat;
412
412
  }, ...(ngDevMode ? [{ debugName: "monthFormat" }] : []));
413
413
  this.timeZone = computed(() => this.layoutConfig().timeZone, ...(ngDevMode ? [{ debugName: "timeZone" }] : []));
414
+ this.firstDayOfWeek = computed(() => this.layoutConfig().firstDayOfWeek ?? 1, ...(ngDevMode ? [{ debugName: "firstDayOfWeek" }] : []));
414
415
  this.adminMode = computed(() => this.layoutConfig().adminMode ?? false, ...(ngDevMode ? [{ debugName: "adminMode" }] : []));
415
416
  this.transitionComplete = signal(false, ...(ngDevMode ? [{ debugName: "transitionComplete" }] : []));
416
417
  /**
@@ -443,6 +444,7 @@ class LayoutService {
443
444
  if (appConfigUiString != null) {
444
445
  const config = JSON.parse(appConfigUiString);
445
446
  config.timeZone ??= Intl.DateTimeFormat().resolvedOptions().timeZone;
447
+ config.firstDayOfWeek ??= 1;
446
448
  return config;
447
449
  }
448
450
  return {
@@ -456,6 +458,7 @@ class LayoutService {
456
458
  timeFormat: 'HH:mm:ss',
457
459
  dateTimeFormat: 'yyyy-MM-dd HH:mm:ss',
458
460
  timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
461
+ firstDayOfWeek: 1,
459
462
  adminMode: false
460
463
  };
461
464
  }
@@ -810,6 +813,8 @@ class L10nService {
810
813
  // Global dictionaries are available before the first `use()` call, so TranslateHttpLoader is never asked for them.
811
814
  L10nService.registerTranslations(globalTranslations);
812
815
  Object.entries(globalTranslations).forEach(([lang, translations]) => this.mergeTranslation(lang, translations));
816
+ // The first day of the week is a user setting, it overrides the value of the language dictionary.
817
+ effect(() => this.primeNg.setTranslation({ firstDayOfWeek: this.layoutService.firstDayOfWeek() }));
813
818
  // Static translations are merged once per language, so they have to be re-merged on every language change.
814
819
  this.translateService.onLangChange.subscribe((event) => {
815
820
  L10nService.staticTranslations.forEach((byLang) => {
@@ -904,7 +909,7 @@ class L10nService {
904
909
  if (key) {
905
910
  this.get(key);
906
911
  }
907
- this.translateService.use(selectedLanguage);
912
+ this.translateService.use(selectedLanguage).subscribe(() => this.applyPrimeNgTranslation());
908
913
  }
909
914
  init(languages) {
910
915
  this.availableLanguages = languages;
@@ -913,9 +918,17 @@ class L10nService {
913
918
  this.translateService.setDefaultLang(lang);
914
919
  this.translateService.use(lang).subscribe(() => {
915
920
  this.loadComponentTranslations('app-info');
916
- this.translateService.get('primeng').subscribe((res) => this.primeNg.setTranslation(res));
921
+ this.applyPrimeNgTranslation();
917
922
  });
918
923
  }
924
+ /**
925
+ * Passes the PrimeNG dictionary of the current language to PrimeNG, keeping the user's first day of the week
926
+ */
927
+ applyPrimeNgTranslation() {
928
+ this.translateService
929
+ .get('primeng')
930
+ .subscribe((res) => this.primeNg.setTranslation({ ...res, firstDayOfWeek: this.layoutService.firstDayOfWeek() }));
931
+ }
919
932
  instant(key, interpolateParams) {
920
933
  return this.translateService.instant(key, interpolateParams);
921
934
  }
@@ -4037,6 +4050,13 @@ class MenuApi extends HttpClient {
4037
4050
  secure: true,
4038
4051
  ...params,
4039
4052
  });
4053
+ this.copyModuleInstance = ({ id, ...query }, params = {}) => this.request({
4054
+ path: `/api/menu/copy-module-instance/${id}`,
4055
+ method: "POST",
4056
+ secure: true,
4057
+ format: "json",
4058
+ ...params,
4059
+ });
4040
4060
  this.changeOrder = (query, params = {}) => this.request({
4041
4061
  path: `/api/menu/change-order`,
4042
4062
  method: "POST",
@@ -4191,9 +4211,9 @@ class StartPageService {
4191
4211
  if (item.separator) {
4192
4212
  continue;
4193
4213
  }
4194
- if (item.items?.length) {
4214
+ if (item.isFolder || item.items?.length) {
4195
4215
  // A folder is never a landing page itself, its children are.
4196
- result.push(...this.collectNavigableLeaves(item.items));
4216
+ result.push(...this.collectNavigableLeaves(item.items ?? []));
4197
4217
  continue;
4198
4218
  }
4199
4219
  // A module without a real route would resolve back to the empty path and loop.
@@ -4318,13 +4338,22 @@ class MenuItemComponent {
4318
4338
  {
4319
4339
  label: this.localization.new,
4320
4340
  icon: PrimeIcons.PLUS,
4321
- command: (event) => this.newClick(event)
4341
+ command: (event) => this.newClick(event),
4342
+ // Only a folder can have child items.
4343
+ visible: !!item.isFolder
4322
4344
  },
4323
4345
  {
4324
4346
  label: this.localization.edit,
4325
4347
  icon: PrimeIcons.FILE_EDIT,
4326
4348
  command: (event) => this.editClick(event)
4327
4349
  },
4350
+ {
4351
+ label: this.localization.copy,
4352
+ icon: PrimeIcons.COPY,
4353
+ command: () => this.copyItem(item),
4354
+ // Only a leaf instance can be copied, folders with children are not supported.
4355
+ visible: !item.items?.length
4356
+ },
4328
4357
  { separator: true },
4329
4358
  {
4330
4359
  label: this.localization.delete,
@@ -4356,10 +4385,10 @@ class MenuItemComponent {
4356
4385
  this.contextMenu.show($event);
4357
4386
  }
4358
4387
  /**
4359
- * Builds the start page entries of the context menu. Only a navigable leaf can be a start page.
4388
+ * Builds the start page entries of the context menu. Only a navigable non-folder item can be a start page.
4360
4389
  */
4361
4390
  getStartModuleItems(item) {
4362
- if (!item?.routerLink || item.items?.length) {
4391
+ if (!item?.routerLink || item.isFolder || item.items?.length) {
4363
4392
  return [];
4364
4393
  }
4365
4394
  return item.isStart
@@ -4388,6 +4417,11 @@ class MenuItemComponent {
4388
4417
  this.msgService.success(this.localization.unsetStartModuleSuccessMessage);
4389
4418
  await this.menuService.loadMenu();
4390
4419
  }
4420
+ async copyItem(item) {
4421
+ await this.menuDataService.copyModuleInstance({ id: item.moduleInstanceId });
4422
+ this.msgService.success(this.localization.copyItemSuccessMessage);
4423
+ await this.menuService.loadMenu();
4424
+ }
4391
4425
  deleteItem(event) {
4392
4426
  this.confirmationService.confirm({
4393
4427
  header: this.localization.deleteItemConfirmHeader,
@@ -4677,6 +4711,7 @@ class MenuItemCreateDialogComponent {
4677
4711
  this.msgService = inject(MsgService);
4678
4712
  this.visibleChange = new EventEmitter();
4679
4713
  this.modules = [];
4714
+ this.moduleOptions = [];
4680
4715
  this.iconOptions = Object.values(PrimeIcons)
4681
4716
  .filter((icon) => typeof icon === 'string')
4682
4717
  .map((icon) => ({
@@ -4684,14 +4719,35 @@ class MenuItemCreateDialogComponent {
4684
4719
  value: icon
4685
4720
  }))
4686
4721
  .sort((left, right) => left.label.localeCompare(right.label));
4687
- this.selectIcon = 'pi pi-box';
4722
+ this.selectIcon = MenuItemCreateDialogComponent.defaultIcon;
4688
4723
  this.saving = false;
4689
4724
  }
4725
+ static { this.defaultIcon = 'pi pi-box'; }
4690
4726
  get canSave() {
4691
4727
  return !this.saving && !!this.selectModule;
4692
4728
  }
4693
4729
  async ngOnInit() {
4694
4730
  this.modules = await this.menu.getModules();
4731
+ this.updateModuleOptions();
4732
+ }
4733
+ /**
4734
+ * Restricts the module list to the folder when the item is created at the menu root,
4735
+ * since only a folder can be placed there.
4736
+ */
4737
+ updateModuleOptions() {
4738
+ const isRoot = !this.menuService.contextMenuItem;
4739
+ this.moduleOptions = isRoot ? this.modules.filter((module) => module.isFolder) : this.modules;
4740
+ if (isRoot && this.moduleOptions.length === 1) {
4741
+ this.selectModule = this.moduleOptions[0].key;
4742
+ this.onModuleChange(this.selectModule);
4743
+ }
4744
+ else if (!this.moduleOptions.some((module) => module.key === this.selectModule)) {
4745
+ this.selectModule = undefined;
4746
+ }
4747
+ }
4748
+ onModuleChange(moduleId) {
4749
+ const module = this.modules.find((item) => item.key === moduleId);
4750
+ this.selectIcon = module?.icon || MenuItemCreateDialogComponent.defaultIcon;
4695
4751
  }
4696
4752
  changeVisible() {
4697
4753
  this.visible = !this.visible;
@@ -4728,6 +4784,7 @@ class MenuItemCreateDialogComponent {
4728
4784
  this.visibleChange.emit(this.visible);
4729
4785
  }
4730
4786
  showDialog() {
4787
+ this.updateModuleOptions();
4731
4788
  this.visible = true;
4732
4789
  this.visibleChange.emit(this.visible);
4733
4790
  }
@@ -4774,8 +4831,9 @@ class MenuItemCreateDialogComponent {
4774
4831
  placeholder="{{ 'menuItemCreateDialogComponent.selectModule' | translate }}"
4775
4832
  scrollHeight="18rem"
4776
4833
  [filter]="true"
4777
- [options]="modules"
4778
- [(ngModel)]="selectModule" />
4834
+ [options]="moduleOptions"
4835
+ [(ngModel)]="selectModule"
4836
+ (ngModelChange)="onModuleChange($event)" />
4779
4837
  </div>
4780
4838
  <div class="flex items-center gap-4 mb-4">
4781
4839
  <label class="font-semibold w-1/3" for="oip-menu-item-create-dialog-icon">
@@ -4871,8 +4929,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
4871
4929
  placeholder="{{ 'menuItemCreateDialogComponent.selectModule' | translate }}"
4872
4930
  scrollHeight="18rem"
4873
4931
  [filter]="true"
4874
- [options]="modules"
4875
- [(ngModel)]="selectModule" />
4932
+ [options]="moduleOptions"
4933
+ [(ngModel)]="selectModule"
4934
+ (ngModelChange)="onModuleChange($event)" />
4876
4935
  </div>
4877
4936
  <div class="flex items-center gap-4 mb-4">
4878
4937
  <label class="font-semibold w-1/3" for="oip-menu-item-create-dialog-icon">
@@ -5197,6 +5256,8 @@ var menuComponent$1 = {
5197
5256
  var menuItemComponent$1 = {
5198
5257
  "new": "Add",
5199
5258
  edit: "Edit",
5259
+ copy: "Copy",
5260
+ copyItemSuccessMessage: "Copied successfully",
5200
5261
  "delete": "Delete",
5201
5262
  deleteItemConfirmHeader: "Delete",
5202
5263
  deleteItemConfirmMessage: "Are you sure you want to delete the item?",
@@ -5246,6 +5307,8 @@ var menuComponent = {
5246
5307
  var menuItemComponent = {
5247
5308
  "new": "Добавить",
5248
5309
  edit: "Редактировать",
5310
+ copy: "Копировать",
5311
+ copyItemSuccessMessage: "Успешно скопировано",
5249
5312
  "delete": "Удалить",
5250
5313
  deleteItemConfirmHeader: "Удаление",
5251
5314
  deleteItemConfirmMessage: "Вы уверены что хотите удалить элемент?",
@@ -6544,6 +6607,7 @@ var config$1 = {
6544
6607
  applicationManagement: "Application management",
6545
6608
  dateFormat: "Date format",
6546
6609
  dateTimeFormat: "Date and time format:",
6610
+ firstDayOfWeek: "First day of the week",
6547
6611
  goTo: "Go to",
6548
6612
  localization: "Localization",
6549
6613
  menu: "Menu",
@@ -6565,6 +6629,7 @@ var config = {
6565
6629
  applicationManagement: "Управление приложениями",
6566
6630
  dateFormat: "Формат даты",
6567
6631
  dateTimeFormat: "Формат даты и времени:",
6632
+ firstDayOfWeek: "Первый день недели",
6568
6633
  goTo: "Перейти",
6569
6634
  localization: "Локализация",
6570
6635
  menu: "Меню",
@@ -6593,10 +6658,20 @@ class ConfigComponent {
6593
6658
  this.timeFormats = ['HH:mm:ss', 'HH:mm'];
6594
6659
  // @ts-ignore
6595
6660
  this.allTimeZones = Intl.supportedValuesOf('timeZone');
6661
+ /** Days of the week starting from Monday, named in the current language. */
6662
+ this.weekDays = computed(() => {
6663
+ const format = new Intl.DateTimeFormat(this.layoutService.language() ?? 'en', { weekday: 'long' });
6664
+ // 1 January 2024 is a Monday, so day N of that week falls on N January (Sunday - on 7 January).
6665
+ return [1, 2, 3, 4, 5, 6, 0].map((day) => {
6666
+ const name = format.format(new Date(2024, 0, day || 7));
6667
+ return { value: day, label: name.charAt(0).toUpperCase() + name.slice(1) };
6668
+ });
6669
+ }, ...(ngDevMode ? [{ debugName: "weekDays" }] : []));
6596
6670
  this.selectedLanguage = this.layoutService.language();
6597
6671
  this.selectedDateFormat = this.layoutService.dateFormat();
6598
6672
  this.selectedTimeFormat = this.layoutService.timeFormat();
6599
6673
  this.selectedTimeZone = this.layoutService.timeZone();
6674
+ this.selectedFirstDayOfWeek = this.layoutService.firstDayOfWeek();
6600
6675
  }
6601
6676
  changeLanguage() {
6602
6677
  this.layoutService.layoutConfig.update((config) => ({
@@ -6611,6 +6686,9 @@ class ConfigComponent {
6611
6686
  changeTimeFormat() {
6612
6687
  this.layoutService.layoutConfig.update((config) => ({ ...config, timeFormat: this.selectedTimeFormat }));
6613
6688
  }
6689
+ changeFirstDayOfWeek() {
6690
+ this.layoutService.layoutConfig.update((config) => ({ ...config, firstDayOfWeek: this.selectedFirstDayOfWeek }));
6691
+ }
6614
6692
  changeTimeZone() {
6615
6693
  this.layoutService.layoutConfig.update((config) => ({ ...config, timeZone: this.selectedTimeZone }));
6616
6694
  }
@@ -6681,6 +6759,16 @@ class ConfigComponent {
6681
6759
  [placeholder]="'config.timeFormat' | translate"
6682
6760
  [(ngModel)]="selectedTimeFormat"
6683
6761
  (onChange)="changeTimeFormat()" />
6762
+ <div class="mt-5">{{ 'config.firstDayOfWeek' | translate }}</div>
6763
+ <p-select
6764
+ class="w-full md:w-56"
6765
+ optionLabel="label"
6766
+ optionValue="value"
6767
+ qa-id="oip-app-config-first-day-of-week-select"
6768
+ [options]="weekDays()"
6769
+ [placeholder]="'config.firstDayOfWeek' | translate"
6770
+ [(ngModel)]="selectedFirstDayOfWeek"
6771
+ (onChange)="changeFirstDayOfWeek()" />
6684
6772
  <div class="mt-5">{{ 'config.timeZone' | translate }}</div>
6685
6773
  <p-select
6686
6774
  class="w-full md:w-56"
@@ -6799,6 +6887,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
6799
6887
  [placeholder]="'config.timeFormat' | translate"
6800
6888
  [(ngModel)]="selectedTimeFormat"
6801
6889
  (onChange)="changeTimeFormat()" />
6890
+ <div class="mt-5">{{ 'config.firstDayOfWeek' | translate }}</div>
6891
+ <p-select
6892
+ class="w-full md:w-56"
6893
+ optionLabel="label"
6894
+ optionValue="value"
6895
+ qa-id="oip-app-config-first-day-of-week-select"
6896
+ [options]="weekDays()"
6897
+ [placeholder]="'config.firstDayOfWeek' | translate"
6898
+ [(ngModel)]="selectedFirstDayOfWeek"
6899
+ (onChange)="changeFirstDayOfWeek()" />
6802
6900
  <div class="mt-5">{{ 'config.timeZone' | translate }}</div>
6803
6901
  <p-select
6804
6902
  class="w-full md:w-56"