From 094568c1d21c7eb3313ea631c6baa093ffc95c12 Mon Sep 17 00:00:00 2001 From: sogla Date: Tue, 20 Jan 2026 21:27:52 +0100 Subject: [PATCH 01/17] feat(create): copy from existing entry --- apps/keira/src/app/routes.ts | 107 +++++- apps/keira/src/assets/i18n/en.json | 20 +- apps/keira/src/assets/i18n/es.json | 20 +- apps/keira/src/assets/i18n/ru.json | 20 +- apps/keira/src/assets/i18n/zh.json | 20 +- .../creature-copy.component.html | 10 + .../creature-copy/creature-copy.component.ts | 77 +++++ .../creature/src/creature-handler.service.ts | 12 +- libs/features/creature/src/index.ts | 1 + .../select-creature.component.html | 1 + .../game-tele-copy.component.html | 10 + .../game-tele-copy.component.ts | 34 ++ .../src/game-tele-handler.service.ts | 11 + libs/features/game-tele/src/index.ts | 1 + .../select-game-tele.component.html | 1 + .../gameobject-copy.component.html | 10 + .../gameobject-copy.component.ts | 47 +++ .../src/gameobject-handler.service.ts | 12 +- libs/features/gameobject/src/index.ts | 1 + .../select-gameobject.component.html | 1 + .../gossip-copy/gossip-copy.component.html | 10 + .../src/gossip-copy/gossip-copy.component.ts | 34 ++ .../gossip/src/gossip-handler.service.ts | 11 + libs/features/gossip/src/index.ts | 1 + .../select-gossip.component.html | 1 + libs/features/item/src/index.ts | 1 + .../src/item-copy/item-copy.component.html | 10 + .../item/src/item-copy/item-copy.component.ts | 50 +++ .../features/item/src/item-handler.service.ts | 11 + .../select-item/select-item.component.html | 1 + .../fishing-loot-copy.component.html | 10 + .../fishing-loot-copy.component.ts | 34 ++ .../fishing-loot-handler.service.ts | 10 + libs/features/other-loots/src/index.ts | 4 + .../mail-loot/mail-loot-copy.component.html | 10 + .../src/mail-loot/mail-loot-copy.component.ts | 34 ++ .../mail-loot/mail-loot-handler.service.ts | 10 + .../reference-loot-copy.component.html | 10 + .../reference-loot-copy.component.ts | 34 ++ .../reference-loot-handler.service.spec.ts | 11 + .../reference-loot-handler.service.ts | 10 + .../src/select-loot.component.html | 1 + .../spell-loot/spell-loot-copy.component.html | 10 + .../spell-loot/spell-loot-copy.component.ts | 34 ++ .../spell-loot-handler.service.spec.ts | 11 + .../spell-loot/spell-loot-handler.service.ts | 10 + libs/features/quest/src/index.ts | 1 + .../src/quest-copy/quest-copy.component.html | 10 + .../src/quest-copy/quest-copy.component.ts | 72 ++++ .../quest/src/quest-handler.service.ts | 11 + .../select-quest/select-quest.component.html | 1 + libs/features/spell/src/index.ts | 1 + .../select-spell/select-spell.component.html | 1 + .../src/spell-copy/spell-copy.component.html | 10 + .../src/spell-copy/spell-copy.component.ts | 34 ++ .../spell/src/spell-handler.service.ts | 11 + .../acore-string-copy.component.html | 10 + .../acore-string-copy.component.ts | 34 ++ .../acore-string-handler.service.ts | 10 + .../select-acore-string.component.html | 1 + libs/features/texts/src/index.ts | 3 + .../src/npc-text/npc-text-copy.component.html | 10 + .../src/npc-text/npc-text-copy.component.ts | 34 ++ .../src/npc-text/npc-text-handler.service.ts | 10 + .../npc-text/select-npc-text.component.html | 1 + .../page-text/page-text-copy.component.html | 10 + .../src/page-text/page-text-copy.component.ts | 34 ++ .../page-text/page-text-handler.service.ts | 10 + .../page-text/select-page-text.component.html | 1 + libs/features/trainer/src/index.ts | 1 + .../select-trainer.component.html | 1 + .../trainer-copy/trainer-copy.component.html | 10 + .../trainer-copy/trainer-copy.component.ts | 34 ++ .../trainer/src/trainer-handler.service.ts | 11 + .../src/service/handlers/handler.service.ts | 4 +- .../copy-output/copy-output.component.html | 124 +++++++ .../copy-output/copy-output.component.scss | 5 + .../src/copy-output/copy-output.component.ts | 312 ++++++++++++++++++ .../src/create/create.component.html | 130 ++++++-- .../src/create/create.component.spec.ts | 71 +++- .../src/create/create.component.ts | 78 +++++ .../base-editor-components/src/index.ts | 1 + .../db-layer/src/query/mysql-query.service.ts | 138 ++++++++ 83 files changed, 1981 insertions(+), 48 deletions(-) create mode 100644 libs/features/creature/src/creature-copy/creature-copy.component.html create mode 100644 libs/features/creature/src/creature-copy/creature-copy.component.ts create mode 100644 libs/features/game-tele/src/game-tele-copy/game-tele-copy.component.html create mode 100644 libs/features/game-tele/src/game-tele-copy/game-tele-copy.component.ts create mode 100644 libs/features/gameobject/src/gameobject-copy/gameobject-copy.component.html create mode 100644 libs/features/gameobject/src/gameobject-copy/gameobject-copy.component.ts create mode 100644 libs/features/gossip/src/gossip-copy/gossip-copy.component.html create mode 100644 libs/features/gossip/src/gossip-copy/gossip-copy.component.ts create mode 100644 libs/features/item/src/item-copy/item-copy.component.html create mode 100644 libs/features/item/src/item-copy/item-copy.component.ts create mode 100644 libs/features/other-loots/src/fishing-loot/fishing-loot-copy.component.html create mode 100644 libs/features/other-loots/src/fishing-loot/fishing-loot-copy.component.ts create mode 100644 libs/features/other-loots/src/mail-loot/mail-loot-copy.component.html create mode 100644 libs/features/other-loots/src/mail-loot/mail-loot-copy.component.ts create mode 100644 libs/features/other-loots/src/reference-loot/reference-loot-copy.component.html create mode 100644 libs/features/other-loots/src/reference-loot/reference-loot-copy.component.ts create mode 100644 libs/features/other-loots/src/spell-loot/spell-loot-copy.component.html create mode 100644 libs/features/other-loots/src/spell-loot/spell-loot-copy.component.ts create mode 100644 libs/features/quest/src/quest-copy/quest-copy.component.html create mode 100644 libs/features/quest/src/quest-copy/quest-copy.component.ts create mode 100644 libs/features/spell/src/spell-copy/spell-copy.component.html create mode 100644 libs/features/spell/src/spell-copy/spell-copy.component.ts create mode 100644 libs/features/texts/src/acore-string/acore-string-copy.component.html create mode 100644 libs/features/texts/src/acore-string/acore-string-copy.component.ts create mode 100644 libs/features/texts/src/npc-text/npc-text-copy.component.html create mode 100644 libs/features/texts/src/npc-text/npc-text-copy.component.ts create mode 100644 libs/features/texts/src/page-text/page-text-copy.component.html create mode 100644 libs/features/texts/src/page-text/page-text-copy.component.ts create mode 100644 libs/features/trainer/src/trainer-copy/trainer-copy.component.html create mode 100644 libs/features/trainer/src/trainer-copy/trainer-copy.component.ts create mode 100644 libs/shared/base-editor-components/src/copy-output/copy-output.component.html create mode 100644 libs/shared/base-editor-components/src/copy-output/copy-output.component.scss create mode 100644 libs/shared/base-editor-components/src/copy-output/copy-output.component.ts diff --git a/apps/keira/src/app/routes.ts b/apps/keira/src/app/routes.ts index 001fa54f7cd..f23d062c6d8 100644 --- a/apps/keira/src/app/routes.ts +++ b/apps/keira/src/app/routes.ts @@ -4,6 +4,7 @@ import { DashboardComponent } from '@keira/features/dashboard'; import { ConditionsComponent, ConditionsHandlerService, SelectConditionsComponent } from '@keira/features/conditions'; import { + CreatureCopyComponent, CreatureEquipTemplateComponent, CreatureFormationsComponent, CreatureHandlerService, @@ -28,6 +29,7 @@ import { } from '@keira/features/creature'; import { GameobjectHandlerService, + GameobjectCopyComponent, GameobjectLootTemplateComponent, GameobjectQuestitemComponent, GameobjectSpawnAddonComponent, @@ -37,11 +39,18 @@ import { SaiGameobjectComponent, SelectGameobjectComponent, } from '@keira/features/gameobject'; -import { GossipHandlerService, GossipMenuComponent, GossipMenuOptionComponent, SelectGossipComponent } from '@keira/features/gossip'; +import { + GossipHandlerService, + GossipMenuComponent, + GossipMenuOptionComponent, + SelectGossipComponent, + GossipCopyComponent, +} from '@keira/features/gossip'; import { DisenchantLootTemplateComponent, ItemEnchantmentTemplateComponent, ItemHandlerService, + ItemCopyComponent, ItemLootTemplateComponent, ItemTemplateComponent, MillingLootTemplateComponent, @@ -53,6 +62,10 @@ import { FishingLootTemplateComponent, MailLootHandlerService, MailLootTemplateComponent, + FishingLootCopyComponent, + SpellLootCopyComponent, + MailLootCopyComponent, + ReferenceLootCopyComponent, ReferenceLootHandlerService, ReferenceLootTemplateComponent, SelectFishingLootComponent, @@ -74,9 +87,10 @@ import { QuestTemplateLocaleComponent, QuestTemplateComponent, SelectQuestComponent, + QuestCopyComponent, } from '@keira/features/quest'; import { SaiFullEditorComponent, SaiSearchEntityComponent, SaiSearchExistingComponent } from '@keira/features/smart-scripts'; -import { SelectSpellComponent, SpellDbcComponent } from '@keira/features/spell'; +import { SelectSpellComponent, SpellDbcComponent, SpellCopyComponent, SpellHandlerService } from '@keira/features/spell'; import { SqlEditorComponent } from '@keira/features/sql-editor'; import { SaiHandlerService } from '@keira/shared/sai-editor'; import { @@ -86,15 +100,25 @@ import { NpcTextHandlerService, PageTextComponent, PageTextHandlerService, + PageTextCopyComponent, + NpcTextCopyComponent, SelectBroadcastTextComponent, SelectNpcTextComponent, SelectPageTextComponent, AcoreStringComponent, AcoreStringHandlerService, + AcoreStringCopyComponent, SelectAcoreStringComponent, } from 'texts'; -import { GameTeleComponent, GameTeleHandlerService, SelectGameTeleComponent } from '@keira/features/game-tele'; -import { SelectTrainerComponent, TrainerComponent, TrainerHandlerService, TrainerSpellComponent } from '@keira/features/trainer'; +import { + SelectTrainerComponent, + TrainerCopyComponent, + TrainerComponent, + TrainerHandlerService, + TrainerSpellComponent, +} from '@keira/features/trainer'; +import { GameTeleComponent, GameTeleHandlerService, SelectGameTeleComponent, GameTeleCopyComponent } from '@keira/features/game-tele'; + import { UnusedGuidSearchComponent } from '@keira/features/unused-guid-search'; export const KEIRA_ROUTES: Routes = [ @@ -113,6 +137,11 @@ export const KEIRA_ROUTES: Routes = [ path: 'select', component: SelectCreatureComponent, }, + { + path: 'copy', + component: CreatureCopyComponent, + canActivate: [CreatureHandlerService], + }, { path: 'creature-template', component: CreatureTemplateComponent, @@ -223,6 +252,11 @@ export const KEIRA_ROUTES: Routes = [ component: QuestTemplateComponent, canActivate: [QuestHandlerService], }, + { + path: 'copy', + component: QuestCopyComponent, + canActivate: [QuestHandlerService], + }, { path: 'quest-template-addon', component: QuestTemplateAddonComponent, @@ -277,6 +311,11 @@ export const KEIRA_ROUTES: Routes = [ component: GameobjectTemplateComponent, canActivate: [GameobjectHandlerService], }, + { + path: 'copy', + component: GameobjectCopyComponent, + canActivate: [GameobjectHandlerService], + }, { path: 'gameobject-template-addon', component: GameobjectTemplateAddonComponent, @@ -321,6 +360,11 @@ export const KEIRA_ROUTES: Routes = [ component: ItemTemplateComponent, canActivate: [ItemHandlerService], }, + { + path: 'copy', + component: ItemCopyComponent, + canActivate: [ItemHandlerService], + }, { path: 'item-enchantment-template', component: ItemEnchantmentTemplateComponent, @@ -360,6 +404,16 @@ export const KEIRA_ROUTES: Routes = [ component: ReferenceLootTemplateComponent, canActivate: [ReferenceLootHandlerService], }, + { + path: 'reference-copy', + component: ReferenceLootCopyComponent, + canActivate: [ReferenceLootHandlerService], + }, + { + path: 'spell-copy', + component: SpellLootCopyComponent, + canActivate: [SpellLootHandlerService], + }, { path: 'select-spell', component: SelectSpellLootComponent, @@ -378,6 +432,11 @@ export const KEIRA_ROUTES: Routes = [ component: FishingLootTemplateComponent, canActivate: [FishingLootHandlerService], }, + { + path: 'fishing-copy', + component: FishingLootCopyComponent, + canActivate: [FishingLootHandlerService], + }, { path: 'select-mail', component: SelectMailLootComponent, @@ -387,6 +446,11 @@ export const KEIRA_ROUTES: Routes = [ component: MailLootTemplateComponent, canActivate: [MailLootHandlerService], }, + { + path: 'mail-copy', + component: MailLootCopyComponent, + canActivate: [MailLootHandlerService], + }, ], }, { @@ -396,6 +460,11 @@ export const KEIRA_ROUTES: Routes = [ path: 'select-page-text', component: SelectPageTextComponent, }, + { + path: 'page-text-copy', + component: PageTextCopyComponent, + canActivate: [PageTextHandlerService], + }, { path: 'page-text', component: PageTextComponent, @@ -414,6 +483,11 @@ export const KEIRA_ROUTES: Routes = [ path: 'select-npc-text', component: SelectNpcTextComponent, }, + { + path: 'npc-text-copy', + component: NpcTextCopyComponent, + canActivate: [NpcTextHandlerService], + }, { path: 'npc-text', component: NpcTextComponent, @@ -424,6 +498,11 @@ export const KEIRA_ROUTES: Routes = [ component: AcoreStringComponent, canActivate: [AcoreStringHandlerService], }, + { + path: 'acore-string-copy', + component: AcoreStringCopyComponent, + canActivate: [AcoreStringHandlerService], + }, { path: 'select-acore-string', component: SelectAcoreStringComponent, @@ -437,6 +516,11 @@ export const KEIRA_ROUTES: Routes = [ path: 'select', component: SelectGossipComponent, }, + { + path: 'copy', + component: GossipCopyComponent, + canActivate: [GossipHandlerService], + }, { path: 'gossip-menu', component: GossipMenuComponent, @@ -492,6 +576,11 @@ export const KEIRA_ROUTES: Routes = [ path: 'spell-dbc', component: SpellDbcComponent, }, + { + path: 'copy', + component: SpellCopyComponent, + canActivate: [SpellHandlerService], + }, ], }, { @@ -501,6 +590,11 @@ export const KEIRA_ROUTES: Routes = [ path: 'select', component: SelectGameTeleComponent, }, + { + path: 'copy', + component: GameTeleCopyComponent, + canActivate: [GameTeleHandlerService], + }, { path: 'tele', component: GameTeleComponent, @@ -515,6 +609,11 @@ export const KEIRA_ROUTES: Routes = [ path: 'select', component: SelectTrainerComponent, }, + { + path: 'copy', + component: TrainerCopyComponent, + canActivate: [TrainerHandlerService], + }, { path: 'trainer', component: TrainerComponent, diff --git a/apps/keira/src/assets/i18n/en.json b/apps/keira/src/assets/i18n/en.json index f9858c443c8..0fe2c912149 100644 --- a/apps/keira/src/assets/i18n/en.json +++ b/apps/keira/src/assets/i18n/en.json @@ -14,7 +14,12 @@ "CREATE_A_NEW": "Create a new {{ ENTITY_TABLE }} by selecting an empty {{ ENTITY_ID_FIELD }}:", "FREE_ENTRY": "The {{ ENTITY_ID_FIELD }} is", "FREE": "free", - "ALREADY_USE": "already in use" + "ALREADY_USE": "already in use", + "SOURCE_ENTRY": "The {{ ENTITY_ID_FIELD }}", + "EXISTS": "exists", + "DOES_NOT_EXIST": "does not exist", + "START_FROM_BLANK": "Start from blank", + "COPY_FROM_EXISTING": "Copy from existing" }, "CONNECTED": "Connected", "SEARCH": "Search", @@ -205,6 +210,19 @@ "LEARN_SQL": "learn the SQL language", "AFFECTED_ROWS": "Affected rows:" }, + "COPY_OUTPUT": { + "TITLE": "Copy Entry SQL", + "DESCRIPTION": "Review and execute the SQL below to copy the entry. You can copy the query to execute it manually, or use the Execute button to run it directly.", + "COPY": "Copy", + "COPY_TOOLTIP": "Copy SQL query to clipboard", + "EXECUTE": "Execute", + "EXECUTE_TOOLTIP": "Execute the SQL query and create the new entry", + "EXECUTED": "Executed", + "CONTINUE": "Continue to Editor", + "CONTINUE_TOOLTIP": "Navigate to the editor for the new entry", + "SELECT_ALL": "Select All", + "SELECT_NONE": "Select None" + }, "CREATURE": { "TEMPLATE": { "BASE": "Base", diff --git a/apps/keira/src/assets/i18n/es.json b/apps/keira/src/assets/i18n/es.json index 986add99670..64783a447b5 100644 --- a/apps/keira/src/assets/i18n/es.json +++ b/apps/keira/src/assets/i18n/es.json @@ -14,7 +14,12 @@ "CREATE_A_NEW": "Crear nuevo {{ ENTITY_TABLE }} seleccionando un {{ ENTITY_ID_FIELD }}:", "FREE_ENTRY": "El o La {{ ENTITY_ID_FIELD }} es", "FREE": "Libre", - "ALREADY_USE": "Está en uso" + "ALREADY_USE": "Está en uso", + "SOURCE_ENTRY": "The {{ ENTITY_ID_FIELD }}", + "EXISTS": "exists", + "DOES_NOT_EXIST": "does not exist", + "START_FROM_BLANK": "Start from blank", + "COPY_FROM_EXISTING": "Copy from existing" }, "CONNECTED": "Conectado", "SEARCH": "Buscar", @@ -205,6 +210,19 @@ "LEARN_SQL": "Aprenda el Lenguaje SQL", "AFFECTED_ROWS": "Líneas afectadas:" }, + "COPY_OUTPUT": { + "TITLE": "Copy Entry SQL", + "DESCRIPTION": "Review and execute the SQL below to copy the entry. You can copy the query to execute it manually, or use the Execute button to run it directly.", + "COPY": "Copy", + "COPY_TOOLTIP": "Copy SQL query to clipboard", + "EXECUTE": "Execute", + "EXECUTE_TOOLTIP": "Execute the SQL query and create the new entry", + "EXECUTED": "Executed", + "CONTINUE": "Continue to Editor", + "CONTINUE_TOOLTIP": "Navigate to the editor for the new entry", + "SELECT_ALL": "Select All", + "SELECT_NONE": "Select None" + }, "CREATURE": { "TEMPLATE": { "BASE": "Base", diff --git a/apps/keira/src/assets/i18n/ru.json b/apps/keira/src/assets/i18n/ru.json index fc83cd02ba5..788b25d4d3f 100644 --- a/apps/keira/src/assets/i18n/ru.json +++ b/apps/keira/src/assets/i18n/ru.json @@ -14,7 +14,12 @@ "CREATE_A_NEW": "Создать новый {{ ENTITY_TABLE }} выбрав пустой {{ ENTITY_ID_FIELD }}:", "FREE_ENTRY": "Значение {{ ENTITY_ID_FIELD }}", "FREE": "свободно", - "ALREADY_USE": "уже используется" + "ALREADY_USE": "уже используется", + "SOURCE_ENTRY": "The {{ ENTITY_ID_FIELD }}", + "EXISTS": "exists", + "DOES_NOT_EXIST": "does not exist", + "START_FROM_BLANK": "Start from blank", + "COPY_FROM_EXISTING": "Copy from existing" }, "CONNECTED": "Connected", "SEARCH": "Поиск", @@ -205,6 +210,19 @@ "LEARN_SQL": "изучить язык SQL", "AFFECTED_ROWS": "Затронутые строки:" }, + "COPY_OUTPUT": { + "TITLE": "Copy Entry SQL", + "DESCRIPTION": "Review and execute the SQL below to copy the entry. You can copy the query to execute it manually, or use the Execute button to run it directly.", + "COPY": "Copy", + "COPY_TOOLTIP": "Copy SQL query to clipboard", + "EXECUTE": "Execute", + "EXECUTE_TOOLTIP": "Execute the SQL query and create the new entry", + "EXECUTED": "Executed", + "CONTINUE": "Continue to Editor", + "CONTINUE_TOOLTIP": "Navigate to the editor for the new entry", + "SELECT_ALL": "Select All", + "SELECT_NONE": "Select None" + }, "CREATURE": { "TEMPLATE": { "BASE": "Base", diff --git a/apps/keira/src/assets/i18n/zh.json b/apps/keira/src/assets/i18n/zh.json index 74bfc3cd1fc..5ce7fe2c94f 100644 --- a/apps/keira/src/assets/i18n/zh.json +++ b/apps/keira/src/assets/i18n/zh.json @@ -14,7 +14,12 @@ "CREATE_A_NEW": "新建一个 {{ ENTITY_TABLE }},选择一个未使用的 {{ ENTITY_ID_FIELD }}:", "FREE_ENTRY": "{{ ENTITY_ID_FIELD }}是", "FREE": "可用的", - "ALREADY_USE": "已在使用中" + "ALREADY_USE": "已在使用中", + "SOURCE_ENTRY": "The {{ ENTITY_ID_FIELD }}", + "EXISTS": "exists", + "DOES_NOT_EXIST": "does not exist", + "START_FROM_BLANK": "Start from blank", + "COPY_FROM_EXISTING": "Copy from existing" }, "CONNECTED": "连接", "SEARCH": "搜索", @@ -205,6 +210,19 @@ "LEARN_SQL": "学习SQL语言", "AFFECTED_ROWS": "受影响的行:" }, + "COPY_OUTPUT": { + "TITLE": "Copy Entry SQL", + "DESCRIPTION": "Review and execute the SQL below to copy the entry. You can copy the query to execute it manually, or use the Execute button to run it directly.", + "COPY": "Copy", + "COPY_TOOLTIP": "Copy SQL query to clipboard", + "EXECUTE": "Execute", + "EXECUTE_TOOLTIP": "Execute the SQL query and create the new entry", + "EXECUTED": "Executed", + "CONTINUE": "Continue to Editor", + "CONTINUE_TOOLTIP": "Navigate to the editor for the new entry", + "SELECT_ALL": "Select All", + "SELECT_NONE": "Select None" + }, "CREATURE": { "TEMPLATE": { "BASE": "基础", diff --git a/libs/features/creature/src/creature-copy/creature-copy.component.html b/libs/features/creature/src/creature-copy/creature-copy.component.html new file mode 100644 index 00000000000..eef15371ccb --- /dev/null +++ b/libs/features/creature/src/creature-copy/creature-copy.component.html @@ -0,0 +1,10 @@ +@if (sourceId && newId) { + +} diff --git a/libs/features/creature/src/creature-copy/creature-copy.component.ts b/libs/features/creature/src/creature-copy/creature-copy.component.ts new file mode 100644 index 00000000000..e9019fba60d --- /dev/null +++ b/libs/features/creature/src/creature-copy/creature-copy.component.ts @@ -0,0 +1,77 @@ +import { ChangeDetectionStrategy, Component, inject, OnInit } from '@angular/core'; +import { + CREATURE_TEMPLATE_ID, + CREATURE_TEMPLATE_TABLE, + CREATURE_TEMPLATE_ADDON_TABLE, + CREATURE_TEMPLATE_MODEL_TABLE, + CREATURE_TEMPLATE_RESISTANCE_TABLE, + CREATURE_TEMPLATE_SPELL_TABLE, + CREATURE_TEMPLATE_MOVEMENT_TABLE, + CREATURE_ONKLL_REPUTATION_TABLE, + CREATURE_EQUIP_TEMPLATE_TABLE, + NPC_VENDOR_TABLE, + CREATURE_DEFAULT_TRAINER_TABLE, + CREATURE_QUESTITEM_TABLE, + CREATURE_LOOT_TEMPLATE_TABLE, + PICKPOCKETING_LOOT_TEMPLATE_TABLE, + SKINNING_LOOT_TEMPLATE_TABLE, + CREATURE_TEMPLATE_MOVEMENT_ID, + CREATURE_TEMPLATE_SPELL_ID, + CREATURE_TEMPLATE_RESISTANCE_ID, + CREATURE_TEMPLATE_MODEL_ID, + CREATURE_TEMPLATE_ADDON_ID, + CREATURE_ONKLL_REPUTATION_ID, + CREATURE_EQUIP_TEMPLATE_ID, + NPC_VENDOR_ID, + CREATURE_DEFAULT_TRAINER_ID, + CREATURE_QUESTITEM_ID, +} from '@keira/shared/acore-world-model'; +import { CopyOutputComponent } from '@keira/shared/base-editor-components'; +import { CreatureHandlerService } from '../creature-handler.service'; +import { Router } from '@angular/router'; + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'keira-creature-copy', + templateUrl: './creature-copy.component.html', + standalone: true, + imports: [CopyOutputComponent], +}) +export class CreatureCopyComponent implements OnInit { + private readonly router = inject(Router); + protected readonly handlerService = inject(CreatureHandlerService); + + protected readonly tableName = CREATURE_TEMPLATE_TABLE; + protected readonly idField = CREATURE_TEMPLATE_ID; + protected sourceId!: string | number; + protected newId!: string | number; + + protected readonly relatedTables = [ + { tableName: CREATURE_TEMPLATE_ADDON_TABLE, idField: CREATURE_TEMPLATE_ADDON_ID }, + { tableName: CREATURE_TEMPLATE_MODEL_TABLE, idField: CREATURE_TEMPLATE_MODEL_ID }, + { tableName: CREATURE_TEMPLATE_RESISTANCE_TABLE, idField: CREATURE_TEMPLATE_RESISTANCE_ID }, + { tableName: CREATURE_TEMPLATE_SPELL_TABLE, idField: CREATURE_TEMPLATE_SPELL_ID }, + { tableName: CREATURE_TEMPLATE_MOVEMENT_TABLE, idField: CREATURE_TEMPLATE_MOVEMENT_ID }, + { tableName: CREATURE_ONKLL_REPUTATION_TABLE, idField: CREATURE_ONKLL_REPUTATION_ID }, + { tableName: CREATURE_EQUIP_TEMPLATE_TABLE, idField: CREATURE_EQUIP_TEMPLATE_ID }, + { tableName: NPC_VENDOR_TABLE, idField: NPC_VENDOR_ID }, + { tableName: CREATURE_DEFAULT_TRAINER_TABLE, idField: CREATURE_DEFAULT_TRAINER_ID }, + { tableName: CREATURE_QUESTITEM_TABLE, idField: CREATURE_QUESTITEM_ID }, + { tableName: CREATURE_LOOT_TEMPLATE_TABLE, idField: 'Entry' }, + { tableName: PICKPOCKETING_LOOT_TEMPLATE_TABLE, idField: 'Entry' }, + { tableName: SKINNING_LOOT_TEMPLATE_TABLE, idField: 'Entry' }, + /* Note: creature (spawns), creature_addon, smart_scripts, creature_text, + and creature_formations are intentionally excluded as they typically + should not be copied automatically with the template */ + ]; + + ngOnInit(): void { + if (!this.handlerService.sourceId || !this.handlerService.selected) { + this.router.navigate(['/creature/select']); + return; + } + + this.sourceId = this.handlerService.sourceId; + this.newId = this.handlerService.selected; + } +} diff --git a/libs/features/creature/src/creature-handler.service.ts b/libs/features/creature/src/creature-handler.service.ts index 69bb0332cfc..db23ee67105 100644 --- a/libs/features/creature/src/creature-handler.service.ts +++ b/libs/features/creature/src/creature-handler.service.ts @@ -30,6 +30,7 @@ import { SaiCreatureHandlerService } from './sai-creature-handler.service'; export class CreatureHandlerService extends HandlerService { protected saiCreatureHandler = inject(SaiCreatureHandlerService); protected readonly mainEditorRoutePath = 'creature/creature-template'; + protected readonly copyRoutePath = 'creature/copy'; get isCreatureTemplateUnsaved(): Signal { return this.statusMap[CREATURE_TEMPLATE_TABLE].asReadonly(); @@ -110,8 +111,15 @@ export class CreatureHandlerService extends HandlerService { [CREATURE_FORMATIONS_TABLE]: signal(false), }; - override select(isNew: boolean, id: string | number | Partial, name?: string) { + override select(isNew: boolean, id: string | number | Partial, name?: string, navigate = true, sourceId?: string) { this.saiCreatureHandler.select(isNew, { entryorguid: +id, source_type: 0 }, null, false); - super.select(isNew, id, name); + + // If we're creating a new entity from a copy, navigate to copy route + if (isNew && sourceId) { + super.select(isNew, id, name, false, sourceId); + this.router.navigate([this.copyRoutePath]); + } else { + super.select(isNew, id, name, navigate); + } } } diff --git a/libs/features/creature/src/index.ts b/libs/features/creature/src/index.ts index 3a80fe56ee4..db80b7f66e1 100644 --- a/libs/features/creature/src/index.ts +++ b/libs/features/creature/src/index.ts @@ -1,3 +1,4 @@ +export * from './creature-copy/creature-copy.component'; export * from './creature-equip-template/creature-equip-template.component'; export * from './creature-handler.service'; export * from './creature-loot-template/creature-loot-template.component'; diff --git a/libs/features/creature/src/select-creature/select-creature.component.html b/libs/features/creature/src/select-creature/select-creature.component.html index 1449b3b80bd..48ef4268053 100644 --- a/libs/features/creature/src/select-creature/select-creature.component.html +++ b/libs/features/creature/src/select-creature/select-creature.component.html @@ -8,6 +8,7 @@ [customStartingId]="customStartingId" [handlerService]="handlerService" [queryService]="queryService" + [allowCopy]="true" /> diff --git a/libs/features/game-tele/src/game-tele-copy/game-tele-copy.component.html b/libs/features/game-tele/src/game-tele-copy/game-tele-copy.component.html new file mode 100644 index 00000000000..eef15371ccb --- /dev/null +++ b/libs/features/game-tele/src/game-tele-copy/game-tele-copy.component.html @@ -0,0 +1,10 @@ +@if (sourceId && newId) { + +} diff --git a/libs/features/game-tele/src/game-tele-copy/game-tele-copy.component.ts b/libs/features/game-tele/src/game-tele-copy/game-tele-copy.component.ts new file mode 100644 index 00000000000..4dcba1eeb5a --- /dev/null +++ b/libs/features/game-tele/src/game-tele-copy/game-tele-copy.component.ts @@ -0,0 +1,34 @@ +import { ChangeDetectionStrategy, Component, inject, OnInit } from '@angular/core'; +import { CopyOutputComponent } from '@keira/shared/base-editor-components'; +import { GameTeleHandlerService } from '../game-tele-handler.service'; +import { Router } from '@angular/router'; +import { GAME_TELE_TABLE, GAME_TELE_ID } from '@keira/shared/acore-world-model'; + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'keira-game-tele-copy', + templateUrl: './game-tele-copy.component.html', + standalone: true, + imports: [CopyOutputComponent], +}) +export class GameTeleCopyComponent implements OnInit { + private readonly router = inject(Router); + protected readonly handlerService = inject(GameTeleHandlerService); + + protected readonly tableName = GAME_TELE_TABLE; + protected readonly idField = GAME_TELE_ID ?? 'id'; + protected sourceId!: string | number; + protected newId!: string | number; + + protected readonly relatedTables: Array<{ tableName: string; idField: string }> = []; + + ngOnInit(): void { + if (!this.handlerService.sourceId || !this.handlerService.selected) { + this.router.navigate(['/game-tele/select']); + return; + } + + this.sourceId = this.handlerService.sourceId; + this.newId = this.handlerService.selected; + } +} diff --git a/libs/features/game-tele/src/game-tele-handler.service.ts b/libs/features/game-tele/src/game-tele-handler.service.ts index b7e90183ee2..4ebc629b608 100644 --- a/libs/features/game-tele/src/game-tele-handler.service.ts +++ b/libs/features/game-tele/src/game-tele-handler.service.ts @@ -7,6 +7,7 @@ import { GAME_TELE_TABLE, GameTele } from '@keira/shared/acore-world-model'; }) export class GameTeleHandlerService extends HandlerService { protected readonly mainEditorRoutePath = 'game-tele/tele'; + protected readonly copyRoutePath = 'game-tele/copy'; get isGameTeleUnsaved(): Signal { return this.statusMap[GAME_TELE_TABLE].asReadonly(); @@ -15,4 +16,14 @@ export class GameTeleHandlerService extends HandlerService { protected _statusMap = { [GAME_TELE_TABLE]: signal(false), }; + + override select(isNew: boolean, id: string | number | Partial, name?: string, navigate = true, sourceId?: string) { + // If we're creating a new entity from a copy, navigate to copy route + if (isNew && sourceId) { + super.select(isNew, id, name, false, sourceId); + this.router.navigate([this.copyRoutePath]); + } else { + super.select(isNew, id, name, navigate); + } + } } diff --git a/libs/features/game-tele/src/index.ts b/libs/features/game-tele/src/index.ts index 0cc6f603f17..cf523959a1b 100644 --- a/libs/features/game-tele/src/index.ts +++ b/libs/features/game-tele/src/index.ts @@ -1,3 +1,4 @@ export { GameTeleComponent } from './edit-game-tele/game-tele.component'; export { SelectGameTeleComponent } from './select-game-tele/select-game-tele.component'; export { GameTeleHandlerService } from './game-tele-handler.service'; +export * from './game-tele-copy/game-tele-copy.component'; diff --git a/libs/features/game-tele/src/select-game-tele/select-game-tele.component.html b/libs/features/game-tele/src/select-game-tele/select-game-tele.component.html index edd0687d15a..47069fe5083 100644 --- a/libs/features/game-tele/src/select-game-tele/select-game-tele.component.html +++ b/libs/features/game-tele/src/select-game-tele/select-game-tele.component.html @@ -8,6 +8,7 @@ [customStartingId]="customStartingId" [handlerService]="handlerService" [queryService]="queryService" + [allowCopy]="true" /> diff --git a/libs/features/gameobject/src/gameobject-copy/gameobject-copy.component.html b/libs/features/gameobject/src/gameobject-copy/gameobject-copy.component.html new file mode 100644 index 00000000000..eef15371ccb --- /dev/null +++ b/libs/features/gameobject/src/gameobject-copy/gameobject-copy.component.html @@ -0,0 +1,10 @@ +@if (sourceId && newId) { + +} diff --git a/libs/features/gameobject/src/gameobject-copy/gameobject-copy.component.ts b/libs/features/gameobject/src/gameobject-copy/gameobject-copy.component.ts new file mode 100644 index 00000000000..3534a08ad15 --- /dev/null +++ b/libs/features/gameobject/src/gameobject-copy/gameobject-copy.component.ts @@ -0,0 +1,47 @@ +import { ChangeDetectionStrategy, Component, inject, OnInit } from '@angular/core'; +import { + GAMEOBJECT_TEMPLATE_TABLE, + GAMEOBJECT_TEMPLATE_ID, + GAMEOBJECT_TEMPLATE_ADDON_TABLE, + GAMEOBJECT_TEMPLATE_ADDON_ID, + GAMEOBJECT_QUESTITEM_TABLE, + GAMEOBJECT_QUESTITEM_ID, + GAMEOBJECT_LOOT_TEMPLATE_TABLE, + LOOT_TEMPLATE_ID, +} from '@keira/shared/acore-world-model'; +import { CopyOutputComponent } from '@keira/shared/base-editor-components'; +import { GameobjectHandlerService } from '../gameobject-handler.service'; +import { Router } from '@angular/router'; + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'keira-gameobject-copy', + templateUrl: './gameobject-copy.component.html', + standalone: true, + imports: [CopyOutputComponent], +}) +export class GameobjectCopyComponent implements OnInit { + private readonly router = inject(Router); + protected readonly handlerService = inject(GameobjectHandlerService); + + protected readonly tableName = GAMEOBJECT_TEMPLATE_TABLE; + protected readonly idField = GAMEOBJECT_TEMPLATE_ID; + protected sourceId!: string | number; + protected newId!: string | number; + + protected readonly relatedTables = [ + { tableName: GAMEOBJECT_TEMPLATE_ADDON_TABLE, idField: GAMEOBJECT_TEMPLATE_ADDON_ID }, + { tableName: GAMEOBJECT_QUESTITEM_TABLE, idField: GAMEOBJECT_QUESTITEM_ID }, + { tableName: GAMEOBJECT_LOOT_TEMPLATE_TABLE, idField: LOOT_TEMPLATE_ID }, + ]; + + ngOnInit(): void { + if (!this.handlerService.sourceId || !this.handlerService.selected) { + this.router.navigate(['/gameobject/select']); + return; + } + + this.sourceId = this.handlerService.sourceId; + this.newId = this.handlerService.selected; + } +} diff --git a/libs/features/gameobject/src/gameobject-handler.service.ts b/libs/features/gameobject/src/gameobject-handler.service.ts index 6fdd8d8f160..e83f60e50be 100644 --- a/libs/features/gameobject/src/gameobject-handler.service.ts +++ b/libs/features/gameobject/src/gameobject-handler.service.ts @@ -18,6 +18,7 @@ import { SaiGameobjectHandlerService } from './sai-gameobject-handler.service'; export class GameobjectHandlerService extends HandlerService { protected saiGameobjectHandler = inject(SaiGameobjectHandlerService); protected readonly mainEditorRoutePath = 'gameobject/gameobject-template'; + protected readonly copyRoutePath = 'gameobject/copy'; get isGameobjectTemplateUnsaved(): Signal { return this.statusMap[GAMEOBJECT_TEMPLATE_TABLE].asReadonly(); @@ -50,8 +51,15 @@ export class GameobjectHandlerService extends HandlerService [GAMEOBJECT_SPAWN_ADDON_TABLE]: signal(false), }; - override select(isNew: boolean, id: string | number | Partial, name?: string) { + override select(isNew: boolean, id: string | number | Partial, name?: string, navigate = true, sourceId?: string) { this.saiGameobjectHandler.select(isNew, { entryorguid: +id, source_type: 1 }, null, false); - super.select(isNew, id, name); + + // If we're creating a new entity from a copy, navigate to copy route + if (isNew && sourceId) { + super.select(isNew, id, name, false, sourceId); + this.router.navigate([this.copyRoutePath]); + } else { + super.select(isNew, id, name, navigate); + } } } diff --git a/libs/features/gameobject/src/index.ts b/libs/features/gameobject/src/index.ts index f8813f195d9..caaa4028b74 100644 --- a/libs/features/gameobject/src/index.ts +++ b/libs/features/gameobject/src/index.ts @@ -6,5 +6,6 @@ export * from './gameobject-template/gameobject-template.component'; export * from './gameobject-template-addon/gameobject-template-addon.component'; export * from './sai-gameobject/sai-gameobject.component'; export * from './select-gameobject/select-gameobject.component'; +export * from './gameobject-copy/gameobject-copy.component'; export * from './gameobject-handler.service'; export * from './sai-gameobject-handler.service'; diff --git a/libs/features/gameobject/src/select-gameobject/select-gameobject.component.html b/libs/features/gameobject/src/select-gameobject/select-gameobject.component.html index 13c4991a19d..f17e5f13d72 100644 --- a/libs/features/gameobject/src/select-gameobject/select-gameobject.component.html +++ b/libs/features/gameobject/src/select-gameobject/select-gameobject.component.html @@ -8,6 +8,7 @@ [customStartingId]="customStartingId" [handlerService]="handlerService" [queryService]="queryService" + [allowCopy]="true" /> diff --git a/libs/features/gossip/src/gossip-copy/gossip-copy.component.html b/libs/features/gossip/src/gossip-copy/gossip-copy.component.html new file mode 100644 index 00000000000..eef15371ccb --- /dev/null +++ b/libs/features/gossip/src/gossip-copy/gossip-copy.component.html @@ -0,0 +1,10 @@ +@if (sourceId && newId) { + +} diff --git a/libs/features/gossip/src/gossip-copy/gossip-copy.component.ts b/libs/features/gossip/src/gossip-copy/gossip-copy.component.ts new file mode 100644 index 00000000000..3f27f4e8a96 --- /dev/null +++ b/libs/features/gossip/src/gossip-copy/gossip-copy.component.ts @@ -0,0 +1,34 @@ +import { ChangeDetectionStrategy, Component, inject, OnInit } from '@angular/core'; +import { CopyOutputComponent } from '@keira/shared/base-editor-components'; +import { GossipHandlerService } from '../gossip-handler.service'; +import { Router } from '@angular/router'; +import { GOSSIP_MENU_TABLE, GOSSIP_MENU_ID, GOSSIP_MENU_OPTION_TABLE, GOSSIP_MENU_OPTION_ID } from '@keira/shared/acore-world-model'; + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'keira-gossip-copy', + templateUrl: './gossip-copy.component.html', + standalone: true, + imports: [CopyOutputComponent], +}) +export class GossipCopyComponent implements OnInit { + private readonly router = inject(Router); + protected readonly handlerService = inject(GossipHandlerService); + + protected readonly tableName = GOSSIP_MENU_TABLE; + protected readonly idField = GOSSIP_MENU_ID; + protected sourceId!: string | number; + protected newId!: string | number; + + protected readonly relatedTables = [{ tableName: GOSSIP_MENU_OPTION_TABLE, idField: GOSSIP_MENU_OPTION_ID }]; + + ngOnInit(): void { + if (!this.handlerService.sourceId || !this.handlerService.selected) { + this.router.navigate(['/gossip/select']); + return; + } + + this.sourceId = this.handlerService.sourceId; + this.newId = this.handlerService.selected; + } +} diff --git a/libs/features/gossip/src/gossip-handler.service.ts b/libs/features/gossip/src/gossip-handler.service.ts index c225bdb640b..b98c10a2d30 100644 --- a/libs/features/gossip/src/gossip-handler.service.ts +++ b/libs/features/gossip/src/gossip-handler.service.ts @@ -7,6 +7,7 @@ import { GOSSIP_MENU_OPTION_TABLE, GOSSIP_MENU_TABLE, GossipMenu } from '@keira/ }) export class GossipHandlerService extends HandlerService { protected readonly mainEditorRoutePath = 'gossip/gossip-menu'; + protected readonly copyRoutePath = 'gossip/copy'; get isGossipMenuTableUnsaved(): Signal { return this.statusMap[GOSSIP_MENU_TABLE].asReadonly(); @@ -19,4 +20,14 @@ export class GossipHandlerService extends HandlerService { [GOSSIP_MENU_TABLE]: signal(false), [GOSSIP_MENU_OPTION_TABLE]: signal(false), }; + + override select(isNew: boolean, id: string | number | Partial, name?: string, navigate = true, sourceId?: string) { + // If we're creating a new entity from a copy, navigate to copy route + if (isNew && sourceId) { + super.select(isNew, id, name, false, sourceId); + this.router.navigate([this.copyRoutePath]); + } else { + super.select(isNew, id, name, navigate); + } + } } diff --git a/libs/features/gossip/src/index.ts b/libs/features/gossip/src/index.ts index 7c25c6209d0..9c163c4d809 100644 --- a/libs/features/gossip/src/index.ts +++ b/libs/features/gossip/src/index.ts @@ -3,3 +3,4 @@ export * from './gossip-menu-option/gossip-menu-option.component'; export * from './gossip-menu-option-preview/gossip-menu-option-preview.component'; export * from './select-gossip/select-gossip.component'; export * from './gossip-handler.service'; +export * from './gossip-copy/gossip-copy.component'; diff --git a/libs/features/gossip/src/select-gossip/select-gossip.component.html b/libs/features/gossip/src/select-gossip/select-gossip.component.html index 06ee06fc784..e53438cdfe8 100644 --- a/libs/features/gossip/src/select-gossip/select-gossip.component.html +++ b/libs/features/gossip/src/select-gossip/select-gossip.component.html @@ -8,6 +8,7 @@ [customStartingId]="customStartingId" [handlerService]="handlerService" [queryService]="queryService" + [allowCopy]="true" /> diff --git a/libs/features/item/src/index.ts b/libs/features/item/src/index.ts index 46d0443cd58..fd2c78a49ac 100644 --- a/libs/features/item/src/index.ts +++ b/libs/features/item/src/index.ts @@ -5,4 +5,5 @@ export * from './item-template/item-template.component'; export * from './milling-loot-template/milling-loot-template.component'; export * from './prospecting-loot-template/prospecting-loot-template.component'; export * from './select-item/select-item.component'; +export * from './item-copy/item-copy.component'; export * from './item-handler.service'; diff --git a/libs/features/item/src/item-copy/item-copy.component.html b/libs/features/item/src/item-copy/item-copy.component.html new file mode 100644 index 00000000000..eef15371ccb --- /dev/null +++ b/libs/features/item/src/item-copy/item-copy.component.html @@ -0,0 +1,10 @@ +@if (sourceId && newId) { + +} diff --git a/libs/features/item/src/item-copy/item-copy.component.ts b/libs/features/item/src/item-copy/item-copy.component.ts new file mode 100644 index 00000000000..11822d99ebb --- /dev/null +++ b/libs/features/item/src/item-copy/item-copy.component.ts @@ -0,0 +1,50 @@ +import { ChangeDetectionStrategy, Component, inject, OnInit } from '@angular/core'; +import { CopyOutputComponent } from '@keira/shared/base-editor-components'; +import { ItemHandlerService } from '../item-handler.service'; +import { Router } from '@angular/router'; +import { + ITEM_TEMPLATE_TABLE, + ITEM_TEMPLATE_ID, + ITEM_ENCHANTMENT_TEMPLATE_TABLE, + ITEM_ENCHANTMENT_TEMPLATE_ID, + ITEM_LOOT_TEMPLATE_TABLE, + DISENCHANT_LOOT_TEMPLATE_TABLE, + PROSPECTING_LOOT_TEMPLATE_TABLE, + MILLING_LOOT_TEMPLATE_TABLE, + LOOT_TEMPLATE_ID, +} from '@keira/shared/acore-world-model'; + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'keira-item-copy', + templateUrl: './item-copy.component.html', + standalone: true, + imports: [CopyOutputComponent], +}) +export class ItemCopyComponent implements OnInit { + private readonly router = inject(Router); + protected readonly handlerService = inject(ItemHandlerService); + + protected readonly tableName = ITEM_TEMPLATE_TABLE; + protected readonly idField = ITEM_TEMPLATE_ID; + protected sourceId!: string | number; + protected newId!: string | number; + + protected readonly relatedTables = [ + { tableName: ITEM_ENCHANTMENT_TEMPLATE_TABLE, idField: ITEM_ENCHANTMENT_TEMPLATE_ID }, + { tableName: ITEM_LOOT_TEMPLATE_TABLE, idField: LOOT_TEMPLATE_ID }, + { tableName: DISENCHANT_LOOT_TEMPLATE_TABLE, idField: LOOT_TEMPLATE_ID }, + { tableName: PROSPECTING_LOOT_TEMPLATE_TABLE, idField: LOOT_TEMPLATE_ID }, + { tableName: MILLING_LOOT_TEMPLATE_TABLE, idField: LOOT_TEMPLATE_ID }, + ]; + + ngOnInit(): void { + if (!this.handlerService.sourceId || !this.handlerService.selected) { + this.router.navigate(['/item/select']); + return; + } + + this.sourceId = this.handlerService.sourceId; + this.newId = this.handlerService.selected; + } +} diff --git a/libs/features/item/src/item-handler.service.ts b/libs/features/item/src/item-handler.service.ts index 6feda73731c..eabb694e1bb 100644 --- a/libs/features/item/src/item-handler.service.ts +++ b/libs/features/item/src/item-handler.service.ts @@ -15,6 +15,7 @@ import { }) export class ItemHandlerService extends HandlerService { protected readonly mainEditorRoutePath = 'item/item-template'; + protected readonly copyRoutePath = 'item/copy'; get isItemTemplateUnsaved(): Signal { return this.statusMap[ITEM_TEMPLATE_TABLE].asReadonly(); @@ -48,4 +49,14 @@ export class ItemHandlerService extends HandlerService { [PROSPECTING_LOOT_TEMPLATE_TABLE]: signal(false), [MILLING_LOOT_TEMPLATE_TABLE]: signal(false), }; + + override select(isNew: boolean, id: string | number | Partial, name?: string, navigate = true, sourceId?: string) { + // If we're creating a new entity from a copy, navigate to copy route + if (isNew && sourceId) { + super.select(isNew, id, name, false, sourceId); + this.router.navigate([this.copyRoutePath]); + } else { + super.select(isNew, id, name, navigate); + } + } } diff --git a/libs/features/item/src/select-item/select-item.component.html b/libs/features/item/src/select-item/select-item.component.html index 893cd5c9e2d..3a6e69ba413 100644 --- a/libs/features/item/src/select-item/select-item.component.html +++ b/libs/features/item/src/select-item/select-item.component.html @@ -8,6 +8,7 @@ [customStartingId]="customStartingId" [handlerService]="handlerService" [queryService]="queryService" + [allowCopy]="true" /> diff --git a/libs/features/other-loots/src/fishing-loot/fishing-loot-copy.component.html b/libs/features/other-loots/src/fishing-loot/fishing-loot-copy.component.html new file mode 100644 index 00000000000..eef15371ccb --- /dev/null +++ b/libs/features/other-loots/src/fishing-loot/fishing-loot-copy.component.html @@ -0,0 +1,10 @@ +@if (sourceId && newId) { + +} diff --git a/libs/features/other-loots/src/fishing-loot/fishing-loot-copy.component.ts b/libs/features/other-loots/src/fishing-loot/fishing-loot-copy.component.ts new file mode 100644 index 00000000000..3271abe813e --- /dev/null +++ b/libs/features/other-loots/src/fishing-loot/fishing-loot-copy.component.ts @@ -0,0 +1,34 @@ +import { ChangeDetectionStrategy, Component, inject, OnInit } from '@angular/core'; +import { CopyOutputComponent } from '@keira/shared/base-editor-components'; +import { FishingLootHandlerService } from './fishing-loot-handler.service'; +import { Router } from '@angular/router'; +import { FISHING_LOOT_TEMPLATE_TABLE, LOOT_TEMPLATE_ID } from '@keira/shared/acore-world-model'; + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'keira-fishing-loot-copy', + templateUrl: './fishing-loot-copy.component.html', + standalone: true, + imports: [CopyOutputComponent], +}) +export class FishingLootCopyComponent implements OnInit { + private readonly router = inject(Router); + protected readonly handlerService = inject(FishingLootHandlerService); + + protected readonly tableName = FISHING_LOOT_TEMPLATE_TABLE; + protected readonly idField = LOOT_TEMPLATE_ID; + protected sourceId!: string | number; + protected newId!: string | number; + + protected readonly relatedTables: Array<{ tableName: string; idField: string }> = []; + + ngOnInit(): void { + if (!this.handlerService.sourceId || !this.handlerService.selected) { + this.router.navigate(['/other-loots/select-fishing']); + return; + } + + this.sourceId = this.handlerService.sourceId; + this.newId = this.handlerService.selected; + } +} diff --git a/libs/features/other-loots/src/fishing-loot/fishing-loot-handler.service.ts b/libs/features/other-loots/src/fishing-loot/fishing-loot-handler.service.ts index d63b31a5957..6b78783a9cd 100644 --- a/libs/features/other-loots/src/fishing-loot/fishing-loot-handler.service.ts +++ b/libs/features/other-loots/src/fishing-loot/fishing-loot-handler.service.ts @@ -7,6 +7,7 @@ import { FISHING_LOOT_TEMPLATE_TABLE, FishingLootTemplate } from '@keira/shared/ }) export class FishingLootHandlerService extends HandlerService { protected readonly mainEditorRoutePath = 'other-loots/fishing'; + protected readonly copyRoutePath = 'other-loots/fishing-copy'; get isUnsaved(): Signal { return this.statusMap[FISHING_LOOT_TEMPLATE_TABLE].asReadonly(); @@ -15,4 +16,13 @@ export class FishingLootHandlerService extends HandlerService, name?: string, navigate = true, sourceId?: string) { + if (isNew && sourceId) { + super.select(isNew, id, name, false, sourceId); + this.router.navigate([this.copyRoutePath]); + } else { + super.select(isNew, id, name, navigate); + } + } } diff --git a/libs/features/other-loots/src/index.ts b/libs/features/other-loots/src/index.ts index 3db633e5f8e..e3339bb94b6 100644 --- a/libs/features/other-loots/src/index.ts +++ b/libs/features/other-loots/src/index.ts @@ -4,9 +4,13 @@ export * from './fishing-loot/fishing-loot-handler.service'; export * from './mail-loot/mail-loot-template.component'; export * from './mail-loot/select-mail-loot.component'; export * from './mail-loot/mail-loot-handler.service'; +export * from './mail-loot/mail-loot-copy.component'; export * from './reference-loot/reference-loot-template.component'; export * from './reference-loot/select-reference-loot.component'; export * from './reference-loot/reference-loot-handler.service'; +export * from './reference-loot/reference-loot-copy.component'; export * from './spell-loot/spell-loot-template.component'; export * from './spell-loot/select-spell-loot.component'; export * from './spell-loot/spell-loot-handler.service'; +export * from './spell-loot/spell-loot-copy.component'; +export * from './fishing-loot/fishing-loot-copy.component'; diff --git a/libs/features/other-loots/src/mail-loot/mail-loot-copy.component.html b/libs/features/other-loots/src/mail-loot/mail-loot-copy.component.html new file mode 100644 index 00000000000..eef15371ccb --- /dev/null +++ b/libs/features/other-loots/src/mail-loot/mail-loot-copy.component.html @@ -0,0 +1,10 @@ +@if (sourceId && newId) { + +} diff --git a/libs/features/other-loots/src/mail-loot/mail-loot-copy.component.ts b/libs/features/other-loots/src/mail-loot/mail-loot-copy.component.ts new file mode 100644 index 00000000000..5776800b896 --- /dev/null +++ b/libs/features/other-loots/src/mail-loot/mail-loot-copy.component.ts @@ -0,0 +1,34 @@ +import { ChangeDetectionStrategy, Component, inject, OnInit } from '@angular/core'; +import { CopyOutputComponent } from '@keira/shared/base-editor-components'; +import { MailLootHandlerService } from './mail-loot-handler.service'; +import { Router } from '@angular/router'; +import { MAIL_LOOT_TEMPLATE_TABLE, LOOT_TEMPLATE_ID } from '@keira/shared/acore-world-model'; + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'keira-mail-loot-copy', + templateUrl: './mail-loot-copy.component.html', + standalone: true, + imports: [CopyOutputComponent], +}) +export class MailLootCopyComponent implements OnInit { + private readonly router = inject(Router); + protected readonly handlerService = inject(MailLootHandlerService); + + protected readonly tableName = MAIL_LOOT_TEMPLATE_TABLE; + protected readonly idField = LOOT_TEMPLATE_ID; + protected sourceId!: string | number; + protected newId!: string | number; + + protected readonly relatedTables: Array<{ tableName: string; idField: string }> = []; + + ngOnInit(): void { + if (!this.handlerService.sourceId || !this.handlerService.selected) { + this.router.navigate(['/other-loots/select-mail']); + return; + } + + this.sourceId = this.handlerService.sourceId; + this.newId = this.handlerService.selected; + } +} diff --git a/libs/features/other-loots/src/mail-loot/mail-loot-handler.service.ts b/libs/features/other-loots/src/mail-loot/mail-loot-handler.service.ts index 1d0203995af..c8d5382be67 100644 --- a/libs/features/other-loots/src/mail-loot/mail-loot-handler.service.ts +++ b/libs/features/other-loots/src/mail-loot/mail-loot-handler.service.ts @@ -7,6 +7,7 @@ import { MAIL_LOOT_TEMPLATE_TABLE, MailLootTemplate } from '@keira/shared/acore- }) export class MailLootHandlerService extends HandlerService { protected readonly mainEditorRoutePath = 'other-loots/mail'; + protected readonly copyRoutePath = 'other-loots/mail-copy'; get isUnsaved(): Signal { return this.statusMap[MAIL_LOOT_TEMPLATE_TABLE].asReadonly(); @@ -15,4 +16,13 @@ export class MailLootHandlerService extends HandlerService { protected _statusMap = { [MAIL_LOOT_TEMPLATE_TABLE]: signal(false), }; + + override select(isNew: boolean, id: string | number | Partial, name?: string, navigate = true, sourceId?: string) { + if (isNew && sourceId) { + super.select(isNew, id, name, false, sourceId); + this.router.navigate([this.copyRoutePath]); + } else { + super.select(isNew, id, name, navigate); + } + } } diff --git a/libs/features/other-loots/src/reference-loot/reference-loot-copy.component.html b/libs/features/other-loots/src/reference-loot/reference-loot-copy.component.html new file mode 100644 index 00000000000..eef15371ccb --- /dev/null +++ b/libs/features/other-loots/src/reference-loot/reference-loot-copy.component.html @@ -0,0 +1,10 @@ +@if (sourceId && newId) { + +} diff --git a/libs/features/other-loots/src/reference-loot/reference-loot-copy.component.ts b/libs/features/other-loots/src/reference-loot/reference-loot-copy.component.ts new file mode 100644 index 00000000000..6fb1a861bd6 --- /dev/null +++ b/libs/features/other-loots/src/reference-loot/reference-loot-copy.component.ts @@ -0,0 +1,34 @@ +import { ChangeDetectionStrategy, Component, inject, OnInit } from '@angular/core'; +import { CopyOutputComponent } from '@keira/shared/base-editor-components'; +import { ReferenceLootHandlerService } from './reference-loot-handler.service'; +import { Router } from '@angular/router'; +import { REFERENCE_LOOT_TEMPLATE_TABLE, LOOT_TEMPLATE_ID } from '@keira/shared/acore-world-model'; + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'keira-reference-loot-copy', + templateUrl: './reference-loot-copy.component.html', + standalone: true, + imports: [CopyOutputComponent], +}) +export class ReferenceLootCopyComponent implements OnInit { + private readonly router = inject(Router); + protected readonly handlerService = inject(ReferenceLootHandlerService); + + protected readonly tableName = REFERENCE_LOOT_TEMPLATE_TABLE; + protected readonly idField = LOOT_TEMPLATE_ID; + protected sourceId!: string | number; + protected newId!: string | number; + + protected readonly relatedTables: Array<{ tableName: string; idField: string }> = []; + + ngOnInit(): void { + if (!this.handlerService.sourceId || !this.handlerService.selected) { + this.router.navigate(['/other-loots/select-reference']); + return; + } + + this.sourceId = this.handlerService.sourceId; + this.newId = this.handlerService.selected; + } +} diff --git a/libs/features/other-loots/src/reference-loot/reference-loot-handler.service.spec.ts b/libs/features/other-loots/src/reference-loot/reference-loot-handler.service.spec.ts index a3c54251999..db05dc833b0 100644 --- a/libs/features/other-loots/src/reference-loot/reference-loot-handler.service.spec.ts +++ b/libs/features/other-loots/src/reference-loot/reference-loot-handler.service.spec.ts @@ -1,4 +1,5 @@ import { TestBed } from '@angular/core/testing'; +import { Router } from '@angular/router'; import { provideZonelessChangeDetection } from '@angular/core'; import { provideNoopAnimations } from '@angular/platform-browser/animations'; import { RouterTestingModule } from '@angular/router/testing'; @@ -15,4 +16,14 @@ describe('ReferenceLootHandlerService', () => { it('should be created', () => { expect(TestBed.inject(ReferenceLootHandlerService)).toBeTruthy(); }); + + it('select with sourceId and isNew should navigate to copy route', () => { + const service = TestBed.inject(ReferenceLootHandlerService); + const router = TestBed.inject(Router); + const navigateSpy = spyOn(router, 'navigate'); + + service.select(true, 123, undefined, true, '1'); + + expect(navigateSpy).toHaveBeenCalledWith(['other-loots/reference-copy']); + }); }); diff --git a/libs/features/other-loots/src/reference-loot/reference-loot-handler.service.ts b/libs/features/other-loots/src/reference-loot/reference-loot-handler.service.ts index 0bb50fbfb59..9ae62fce7b7 100644 --- a/libs/features/other-loots/src/reference-loot/reference-loot-handler.service.ts +++ b/libs/features/other-loots/src/reference-loot/reference-loot-handler.service.ts @@ -7,6 +7,7 @@ import { REFERENCE_LOOT_TEMPLATE_TABLE, ReferenceLootTemplate } from '@keira/sha }) export class ReferenceLootHandlerService extends HandlerService { protected readonly mainEditorRoutePath = 'other-loots/reference'; + protected readonly copyRoutePath = 'other-loots/reference-copy'; get isUnsaved(): Signal { return this.statusMap[REFERENCE_LOOT_TEMPLATE_TABLE].asReadonly(); @@ -15,4 +16,13 @@ export class ReferenceLootHandlerService extends HandlerService, name?: string, navigate = true, sourceId?: string) { + if (isNew && sourceId) { + super.select(isNew, id, name, false, sourceId); + this.router.navigate([this.copyRoutePath]); + } else { + super.select(isNew, id, name, navigate); + } + } } diff --git a/libs/features/other-loots/src/select-loot.component.html b/libs/features/other-loots/src/select-loot.component.html index 0bd0f07ab91..f7746993a6c 100644 --- a/libs/features/other-loots/src/select-loot.component.html +++ b/libs/features/other-loots/src/select-loot.component.html @@ -8,6 +8,7 @@ [customStartingId]="customStartingId" [handlerService]="handlerService" [queryService]="queryService" + [allowCopy]="true" /> diff --git a/libs/features/other-loots/src/spell-loot/spell-loot-copy.component.html b/libs/features/other-loots/src/spell-loot/spell-loot-copy.component.html new file mode 100644 index 00000000000..eef15371ccb --- /dev/null +++ b/libs/features/other-loots/src/spell-loot/spell-loot-copy.component.html @@ -0,0 +1,10 @@ +@if (sourceId && newId) { + +} diff --git a/libs/features/other-loots/src/spell-loot/spell-loot-copy.component.ts b/libs/features/other-loots/src/spell-loot/spell-loot-copy.component.ts new file mode 100644 index 00000000000..8e4767bd426 --- /dev/null +++ b/libs/features/other-loots/src/spell-loot/spell-loot-copy.component.ts @@ -0,0 +1,34 @@ +import { ChangeDetectionStrategy, Component, inject, OnInit } from '@angular/core'; +import { CopyOutputComponent } from '@keira/shared/base-editor-components'; +import { SpellLootHandlerService } from './spell-loot-handler.service'; +import { Router } from '@angular/router'; +import { SPELL_LOOT_TEMPLATE_TABLE, LOOT_TEMPLATE_ID } from '@keira/shared/acore-world-model'; + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'keira-spell-loot-copy', + templateUrl: './spell-loot-copy.component.html', + standalone: true, + imports: [CopyOutputComponent], +}) +export class SpellLootCopyComponent implements OnInit { + private readonly router = inject(Router); + protected readonly handlerService = inject(SpellLootHandlerService); + + protected readonly tableName = SPELL_LOOT_TEMPLATE_TABLE; + protected readonly idField = LOOT_TEMPLATE_ID; + protected sourceId!: string | number; + protected newId!: string | number; + + protected readonly relatedTables: Array<{ tableName: string; idField: string }> = []; + + ngOnInit(): void { + if (!this.handlerService.sourceId || !this.handlerService.selected) { + this.router.navigate(['/other-loots/select-spell']); + return; + } + + this.sourceId = this.handlerService.sourceId; + this.newId = this.handlerService.selected; + } +} diff --git a/libs/features/other-loots/src/spell-loot/spell-loot-handler.service.spec.ts b/libs/features/other-loots/src/spell-loot/spell-loot-handler.service.spec.ts index ff161255b02..14b182b4302 100644 --- a/libs/features/other-loots/src/spell-loot/spell-loot-handler.service.spec.ts +++ b/libs/features/other-loots/src/spell-loot/spell-loot-handler.service.spec.ts @@ -1,4 +1,5 @@ import { TestBed } from '@angular/core/testing'; +import { Router } from '@angular/router'; import { provideZonelessChangeDetection } from '@angular/core'; import { provideNoopAnimations } from '@angular/platform-browser/animations'; import { RouterTestingModule } from '@angular/router/testing'; @@ -15,4 +16,14 @@ describe('SpellLootHandlerService', () => { it('should be created', () => { expect(TestBed.inject(SpellLootHandlerService)).toBeTruthy(); }); + + it('select with sourceId and isNew should navigate to spell copy route', () => { + const service = TestBed.inject(SpellLootHandlerService); + const router = TestBed.inject(Router); + const navigateSpy = spyOn(router, 'navigate'); + + service.select(true, 123, undefined, true, '1'); + + expect(navigateSpy).toHaveBeenCalledWith(['other-loots/spell-copy']); + }); }); diff --git a/libs/features/other-loots/src/spell-loot/spell-loot-handler.service.ts b/libs/features/other-loots/src/spell-loot/spell-loot-handler.service.ts index 276629b1b13..b3b30df9cdd 100644 --- a/libs/features/other-loots/src/spell-loot/spell-loot-handler.service.ts +++ b/libs/features/other-loots/src/spell-loot/spell-loot-handler.service.ts @@ -7,6 +7,16 @@ import { SPELL_LOOT_TEMPLATE_TABLE, SpellLootTemplate } from '@keira/shared/acor }) export class SpellLootHandlerService extends HandlerService { protected readonly mainEditorRoutePath = 'other-loots/spell'; + protected readonly copyRoutePath = 'other-loots/spell-copy'; + + override select(isNew: boolean, id: string | number | Partial, name?: string, navigate = true, sourceId?: string) { + if (isNew && sourceId) { + super.select(isNew, id, name, false, sourceId); + this.router.navigate([this.copyRoutePath]); + } else { + super.select(isNew, id, name, navigate); + } + } get isUnsaved(): Signal { return this.statusMap[SPELL_LOOT_TEMPLATE_TABLE].asReadonly(); diff --git a/libs/features/quest/src/index.ts b/libs/features/quest/src/index.ts index 9a7c36713fb..12e823da68c 100644 --- a/libs/features/quest/src/index.ts +++ b/libs/features/quest/src/index.ts @@ -10,3 +10,4 @@ export * from './quest-template-addon/quest-template-addon.component'; export * from './select-quest/select-quest.component'; export * from './quest-handler.service'; export * from './quest-template-locale/quest-template-locale.component'; +export * from './quest-copy/quest-copy.component'; diff --git a/libs/features/quest/src/quest-copy/quest-copy.component.html b/libs/features/quest/src/quest-copy/quest-copy.component.html new file mode 100644 index 00000000000..eef15371ccb --- /dev/null +++ b/libs/features/quest/src/quest-copy/quest-copy.component.html @@ -0,0 +1,10 @@ +@if (sourceId && newId) { + +} diff --git a/libs/features/quest/src/quest-copy/quest-copy.component.ts b/libs/features/quest/src/quest-copy/quest-copy.component.ts new file mode 100644 index 00000000000..a20c1fcb2f4 --- /dev/null +++ b/libs/features/quest/src/quest-copy/quest-copy.component.ts @@ -0,0 +1,72 @@ +import { ChangeDetectionStrategy, Component, inject, OnInit } from '@angular/core'; +import { CopyOutputComponent } from '@keira/shared/base-editor-components'; +import { QuestHandlerService } from '../quest-handler.service'; +import { Router } from '@angular/router'; +import { + QUEST_TEMPLATE_TABLE, + QUEST_TEMPLATE_ID, + QUEST_TEMPLATE_ADDON_TABLE, + QUEST_TEMPLATE_ADDON_ID, + QUEST_TEMPLATE_LOCALE_TABLE, + QUEST_TEMPLATE_LOCALE_ID, + QUEST_OFFER_REWARD_TABLE, + QUEST_OFFER_REWARD_ID, + QUEST_REQUEST_ITEMS_TABLE, + QUEST_REQUEST_ITEMS_ID, + CREATURE_QUESTSTARTER_TABLE, + CREATURE_QUESTSTARTER_ID, + CREATURE_QUESTENDER_TABLE, + CREATURE_QUESTENDER_ID, + GAMEOBJECT_QUESTSTARTER_TABLE, + GAMEOBJECT_QUESTSTARTER_ID, + GAMEOBJECT_QUESTENDER_TABLE, + GAMEOBJECT_QUESTENDER_ID, +} from '@keira/shared/acore-world-model'; + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'keira-quest-copy', + templateUrl: './quest-copy.component.html', + standalone: true, + imports: [CopyOutputComponent], +}) +export class QuestCopyComponent implements OnInit { + private readonly router = inject(Router); + protected readonly handlerService = inject(QuestHandlerService); + + protected readonly tableName = QUEST_TEMPLATE_TABLE; + protected readonly idField = QUEST_TEMPLATE_ID; + protected sourceId!: string | number; + protected newId!: string | number; + + protected readonly relatedTables = [ + { tableName: QUEST_TEMPLATE_ADDON_TABLE, idField: QUEST_TEMPLATE_ADDON_ID }, + { tableName: QUEST_TEMPLATE_LOCALE_TABLE, idField: QUEST_TEMPLATE_LOCALE_ID }, + { tableName: QUEST_OFFER_REWARD_TABLE, idField: QUEST_OFFER_REWARD_ID }, + { tableName: QUEST_REQUEST_ITEMS_TABLE, idField: QUEST_REQUEST_ITEMS_ID }, + { tableName: CREATURE_QUESTSTARTER_TABLE, idField: CREATURE_QUESTSTARTER_ID, copyMode: 'RAW' as const, columns: ['id', 'quest'] }, + { tableName: CREATURE_QUESTENDER_TABLE, idField: CREATURE_QUESTENDER_ID, copyMode: 'RAW' as const, columns: ['id', 'quest'] }, + { + tableName: GAMEOBJECT_QUESTSTARTER_TABLE, + idField: GAMEOBJECT_QUESTSTARTER_ID, + copyMode: 'RAW' as const, + columns: ['id', 'quest'], + }, + { + tableName: GAMEOBJECT_QUESTENDER_TABLE, + idField: GAMEOBJECT_QUESTENDER_ID, + copyMode: 'RAW' as const, + columns: ['id', 'quest'], + }, + ]; + + ngOnInit(): void { + if (!this.handlerService.sourceId || !this.handlerService.selected) { + this.router.navigate(['/quest/select']); + return; + } + + this.sourceId = this.handlerService.sourceId; + this.newId = this.handlerService.selected; + } +} diff --git a/libs/features/quest/src/quest-handler.service.ts b/libs/features/quest/src/quest-handler.service.ts index 26bfb8e9591..7665a9c17bc 100644 --- a/libs/features/quest/src/quest-handler.service.ts +++ b/libs/features/quest/src/quest-handler.service.ts @@ -18,6 +18,7 @@ import { }) export class QuestHandlerService extends HandlerService { protected readonly mainEditorRoutePath = 'quest/quest-template'; + protected readonly copyRoutePath = 'quest/copy'; get isQuestTemplateUnsaved(): Signal { return this.statusMap[QUEST_TEMPLATE_TABLE].asReadonly(); @@ -58,4 +59,14 @@ export class QuestHandlerService extends HandlerService { [GAMEOBJECT_QUESTSTARTER_TABLE]: signal(false), [GAMEOBJECT_QUESTENDER_TABLE]: signal(false), }; + + override select(isNew: boolean, id: string | number | Partial, name?: string, navigate = true, sourceId?: string) { + // If we're creating a new entity from a copy, navigate to copy route + if (isNew && sourceId) { + super.select(isNew, id, name, false, sourceId); + this.router.navigate([this.copyRoutePath]); + } else { + super.select(isNew, id, name, navigate); + } + } } diff --git a/libs/features/quest/src/select-quest/select-quest.component.html b/libs/features/quest/src/select-quest/select-quest.component.html index 93c2bc65760..91a4196f704 100644 --- a/libs/features/quest/src/select-quest/select-quest.component.html +++ b/libs/features/quest/src/select-quest/select-quest.component.html @@ -8,6 +8,7 @@ [customStartingId]="customStartingId" [handlerService]="handlerService" [queryService]="queryService" + [allowCopy]="true" /> diff --git a/libs/features/spell/src/index.ts b/libs/features/spell/src/index.ts index c909726d4a0..884498f6e27 100644 --- a/libs/features/spell/src/index.ts +++ b/libs/features/spell/src/index.ts @@ -1,3 +1,4 @@ export * from './select-spell/select-spell.component'; export * from './spell-dbc/spell-dbc.component'; export * from './spell-handler.service'; +export * from './spell-copy/spell-copy.component'; diff --git a/libs/features/spell/src/select-spell/select-spell.component.html b/libs/features/spell/src/select-spell/select-spell.component.html index 80029670797..7e408120800 100644 --- a/libs/features/spell/src/select-spell/select-spell.component.html +++ b/libs/features/spell/src/select-spell/select-spell.component.html @@ -31,6 +31,7 @@ [handlerService]="handlerService" [queryService]="queryService" [maxEntryValue]="2147483647" + [allowCopy]="true" />
diff --git a/libs/features/spell/src/spell-copy/spell-copy.component.html b/libs/features/spell/src/spell-copy/spell-copy.component.html new file mode 100644 index 00000000000..eef15371ccb --- /dev/null +++ b/libs/features/spell/src/spell-copy/spell-copy.component.html @@ -0,0 +1,10 @@ +@if (sourceId && newId) { + +} diff --git a/libs/features/spell/src/spell-copy/spell-copy.component.ts b/libs/features/spell/src/spell-copy/spell-copy.component.ts new file mode 100644 index 00000000000..b1d34844351 --- /dev/null +++ b/libs/features/spell/src/spell-copy/spell-copy.component.ts @@ -0,0 +1,34 @@ +import { ChangeDetectionStrategy, Component, inject, OnInit } from '@angular/core'; +import { CopyOutputComponent } from '@keira/shared/base-editor-components'; +import { SpellHandlerService } from '../spell-handler.service'; +import { Router } from '@angular/router'; +import { SPELL_DBC_TABLE, SPELL_DBC_ID, SPELL_LOOT_TEMPLATE_TABLE, LOOT_TEMPLATE_ID } from '@keira/shared/acore-world-model'; + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'keira-spell-copy', + templateUrl: './spell-copy.component.html', + standalone: true, + imports: [CopyOutputComponent], +}) +export class SpellCopyComponent implements OnInit { + private readonly router = inject(Router); + protected readonly handlerService = inject(SpellHandlerService); + + protected readonly tableName = SPELL_DBC_TABLE; + protected readonly idField = SPELL_DBC_ID; + protected sourceId!: string | number; + protected newId!: string | number; + + protected readonly relatedTables = [{ tableName: SPELL_LOOT_TEMPLATE_TABLE, idField: LOOT_TEMPLATE_ID }]; + + ngOnInit(): void { + if (!this.handlerService.sourceId || !this.handlerService.selected) { + this.router.navigate(['/spell/select']); + return; + } + + this.sourceId = this.handlerService.sourceId; + this.newId = this.handlerService.selected; + } +} diff --git a/libs/features/spell/src/spell-handler.service.ts b/libs/features/spell/src/spell-handler.service.ts index 384e0064012..2478720acf3 100644 --- a/libs/features/spell/src/spell-handler.service.ts +++ b/libs/features/spell/src/spell-handler.service.ts @@ -7,6 +7,7 @@ import { SPELL_DBC_TABLE, SpellDbc } from '@keira/shared/acore-world-model'; }) export class SpellHandlerService extends HandlerService { protected readonly mainEditorRoutePath = 'spell/spell-dbc'; + protected readonly copyRoutePath = 'spell/copy'; get isSpellDbcUnsaved(): Signal { return this.statusMap[SPELL_DBC_TABLE].asReadonly(); @@ -15,4 +16,14 @@ export class SpellHandlerService extends HandlerService { protected _statusMap = { [SPELL_DBC_TABLE]: signal(false), }; + + override select(isNew: boolean, id: string | number | Partial, name?: string, navigate = true, sourceId?: string) { + // If we're creating a new entity from a copy, navigate to copy route + if (isNew && sourceId) { + super.select(isNew, id, name, false, sourceId); + this.router.navigate([this.copyRoutePath]); + } else { + super.select(isNew, id, name, navigate); + } + } } diff --git a/libs/features/texts/src/acore-string/acore-string-copy.component.html b/libs/features/texts/src/acore-string/acore-string-copy.component.html new file mode 100644 index 00000000000..eef15371ccb --- /dev/null +++ b/libs/features/texts/src/acore-string/acore-string-copy.component.html @@ -0,0 +1,10 @@ +@if (sourceId && newId) { + +} diff --git a/libs/features/texts/src/acore-string/acore-string-copy.component.ts b/libs/features/texts/src/acore-string/acore-string-copy.component.ts new file mode 100644 index 00000000000..4cee0d9b117 --- /dev/null +++ b/libs/features/texts/src/acore-string/acore-string-copy.component.ts @@ -0,0 +1,34 @@ +import { ChangeDetectionStrategy, Component, inject, OnInit } from '@angular/core'; +import { CopyOutputComponent } from '@keira/shared/base-editor-components'; +import { AcoreStringHandlerService } from './acore-string-handler.service'; +import { Router } from '@angular/router'; +import { ACORE_STRING_TABLE, ACORE_STRING_ENTRY } from '@keira/shared/acore-world-model'; + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'keira-acore-string-copy', + templateUrl: './acore-string-copy.component.html', + standalone: true, + imports: [CopyOutputComponent], +}) +export class AcoreStringCopyComponent implements OnInit { + private readonly router = inject(Router); + protected readonly handlerService = inject(AcoreStringHandlerService); + + protected readonly tableName = ACORE_STRING_TABLE; + protected readonly idField = ACORE_STRING_ENTRY; + protected sourceId!: string | number; + protected newId!: string | number; + + protected readonly relatedTables: Array<{ tableName: string; idField: string }> = []; + + ngOnInit(): void { + if (!this.handlerService.sourceId || !this.handlerService.selected) { + this.router.navigate(['/texts/select-acore-string']); + return; + } + + this.sourceId = this.handlerService.sourceId; + this.newId = this.handlerService.selected; + } +} diff --git a/libs/features/texts/src/acore-string/acore-string-handler.service.ts b/libs/features/texts/src/acore-string/acore-string-handler.service.ts index 6112bd75fc2..7cd8909d706 100644 --- a/libs/features/texts/src/acore-string/acore-string-handler.service.ts +++ b/libs/features/texts/src/acore-string/acore-string-handler.service.ts @@ -7,6 +7,7 @@ import { ACORE_STRING_TABLE, AcoreString } from '@keira/shared/acore-world-model }) export class AcoreStringHandlerService extends HandlerService { protected readonly mainEditorRoutePath = 'texts/acore-string'; + protected readonly copyRoutePath = 'texts/acore-string-copy'; get isUnsaved(): Signal { return this.statusMap[ACORE_STRING_TABLE].asReadonly(); @@ -15,4 +16,13 @@ export class AcoreStringHandlerService extends HandlerService { protected _statusMap = { [ACORE_STRING_TABLE]: signal(false), }; + + override select(isNew: boolean, id: string | number | Partial, name?: string, navigate = true, sourceId?: string) { + if (isNew && sourceId) { + super.select(isNew, id, name, false, sourceId); + this.router.navigate([this.copyRoutePath]); + } else { + super.select(isNew, id, name, navigate); + } + } } diff --git a/libs/features/texts/src/acore-string/select-acore-string.component.html b/libs/features/texts/src/acore-string/select-acore-string.component.html index d6bfd767bdd..a4ae495dee4 100644 --- a/libs/features/texts/src/acore-string/select-acore-string.component.html +++ b/libs/features/texts/src/acore-string/select-acore-string.component.html @@ -8,6 +8,7 @@ [customStartingId]="customStartingId" [handlerService]="handlerService" [queryService]="queryService" + [allowCopy]="true" />
diff --git a/libs/features/texts/src/index.ts b/libs/features/texts/src/index.ts index c4209b8c94b..23f9353f4d2 100644 --- a/libs/features/texts/src/index.ts +++ b/libs/features/texts/src/index.ts @@ -1,6 +1,7 @@ export { PageTextHandlerService } from './page-text/page-text-handler.service'; export { SelectPageTextComponent } from './page-text/select-page-text.component'; export { PageTextComponent } from './page-text/page-text.component'; +export * from './page-text/page-text-copy.component'; export { BroadcastTextHandlerService } from './broadcast-text/broadcast-text-handler.service'; export { SelectBroadcastTextComponent } from './broadcast-text/select-broadcast-text.component'; @@ -9,7 +10,9 @@ export { BroadcastTextComponent } from './broadcast-text/broadcast-text.componen export { NpcTextHandlerService } from './npc-text/npc-text-handler.service'; export { SelectNpcTextComponent } from './npc-text/select-npc-text.component'; export { NpcTextComponent } from './npc-text/npc-text.component'; +export * from './npc-text/npc-text-copy.component'; export { AcoreStringHandlerService } from './acore-string/acore-string-handler.service'; export { SelectAcoreStringComponent } from './acore-string/select-acore-string.component'; export { AcoreStringComponent } from './acore-string/acore-string.component'; +export { AcoreStringCopyComponent } from './acore-string/acore-string-copy.component'; diff --git a/libs/features/texts/src/npc-text/npc-text-copy.component.html b/libs/features/texts/src/npc-text/npc-text-copy.component.html new file mode 100644 index 00000000000..eef15371ccb --- /dev/null +++ b/libs/features/texts/src/npc-text/npc-text-copy.component.html @@ -0,0 +1,10 @@ +@if (sourceId && newId) { + +} diff --git a/libs/features/texts/src/npc-text/npc-text-copy.component.ts b/libs/features/texts/src/npc-text/npc-text-copy.component.ts new file mode 100644 index 00000000000..be53fd89867 --- /dev/null +++ b/libs/features/texts/src/npc-text/npc-text-copy.component.ts @@ -0,0 +1,34 @@ +import { ChangeDetectionStrategy, Component, inject, OnInit } from '@angular/core'; +import { CopyOutputComponent } from '@keira/shared/base-editor-components'; +import { NpcTextHandlerService } from './npc-text-handler.service'; +import { Router } from '@angular/router'; +import { NPC_TEXT_TABLE, NPC_TEXT_ID } from '@keira/shared/acore-world-model'; + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'keira-npc-text-copy', + templateUrl: './npc-text-copy.component.html', + standalone: true, + imports: [CopyOutputComponent], +}) +export class NpcTextCopyComponent implements OnInit { + private readonly router = inject(Router); + protected readonly handlerService = inject(NpcTextHandlerService); + + protected readonly tableName = NPC_TEXT_TABLE; + protected readonly idField = NPC_TEXT_ID; + protected sourceId!: string | number; + protected newId!: string | number; + + protected readonly relatedTables: Array<{ tableName: string; idField: string }> = []; + + ngOnInit(): void { + if (!this.handlerService.sourceId || !this.handlerService.selected) { + this.router.navigate(['/texts/select-npc-text']); + return; + } + + this.sourceId = this.handlerService.sourceId; + this.newId = this.handlerService.selected; + } +} diff --git a/libs/features/texts/src/npc-text/npc-text-handler.service.ts b/libs/features/texts/src/npc-text/npc-text-handler.service.ts index 313b96aff47..20cde79b8d8 100644 --- a/libs/features/texts/src/npc-text/npc-text-handler.service.ts +++ b/libs/features/texts/src/npc-text/npc-text-handler.service.ts @@ -7,6 +7,7 @@ import { NpcText, NPC_TEXT_TABLE } from '@keira/shared/acore-world-model'; }) export class NpcTextHandlerService extends HandlerService { protected readonly mainEditorRoutePath = 'texts/npc-text'; + protected readonly copyRoutePath = 'texts/npc-text-copy'; get isUnsaved(): Signal { return this.statusMap[NPC_TEXT_TABLE].asReadonly(); @@ -15,4 +16,13 @@ export class NpcTextHandlerService extends HandlerService { protected _statusMap = { [NPC_TEXT_TABLE]: signal(false), }; + + override select(isNew: boolean, id: string | number | Partial, name?: string, navigate = true, sourceId?: string) { + if (isNew && sourceId) { + super.select(isNew, id, name, false, sourceId); + this.router.navigate([this.copyRoutePath]); + } else { + super.select(isNew, id, name, navigate); + } + } } diff --git a/libs/features/texts/src/npc-text/select-npc-text.component.html b/libs/features/texts/src/npc-text/select-npc-text.component.html index 55d59422ce5..c4e3b5b98ea 100644 --- a/libs/features/texts/src/npc-text/select-npc-text.component.html +++ b/libs/features/texts/src/npc-text/select-npc-text.component.html @@ -8,6 +8,7 @@ [customStartingId]="customStartingId" [handlerService]="handlerService" [queryService]="queryService" + [allowCopy]="true" /> diff --git a/libs/features/texts/src/page-text/page-text-copy.component.html b/libs/features/texts/src/page-text/page-text-copy.component.html new file mode 100644 index 00000000000..eef15371ccb --- /dev/null +++ b/libs/features/texts/src/page-text/page-text-copy.component.html @@ -0,0 +1,10 @@ +@if (sourceId && newId) { + +} diff --git a/libs/features/texts/src/page-text/page-text-copy.component.ts b/libs/features/texts/src/page-text/page-text-copy.component.ts new file mode 100644 index 00000000000..41206042eea --- /dev/null +++ b/libs/features/texts/src/page-text/page-text-copy.component.ts @@ -0,0 +1,34 @@ +import { ChangeDetectionStrategy, Component, inject, OnInit } from '@angular/core'; +import { CopyOutputComponent } from '@keira/shared/base-editor-components'; +import { PageTextHandlerService } from './page-text-handler.service'; +import { Router } from '@angular/router'; +import { PAGE_TEXT_TABLE, PAGE_TEXT_ID } from '@keira/shared/acore-world-model'; + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'keira-page-text-copy', + templateUrl: './page-text-copy.component.html', + standalone: true, + imports: [CopyOutputComponent], +}) +export class PageTextCopyComponent implements OnInit { + private readonly router = inject(Router); + protected readonly handlerService = inject(PageTextHandlerService); + + protected readonly tableName = PAGE_TEXT_TABLE; + protected readonly idField = PAGE_TEXT_ID; + protected sourceId!: string | number; + protected newId!: string | number; + + protected readonly relatedTables: Array<{ tableName: string; idField: string }> = []; + + ngOnInit(): void { + if (!this.handlerService.sourceId || !this.handlerService.selected) { + this.router.navigate(['/texts/select-page-text']); + return; + } + + this.sourceId = this.handlerService.sourceId; + this.newId = this.handlerService.selected; + } +} diff --git a/libs/features/texts/src/page-text/page-text-handler.service.ts b/libs/features/texts/src/page-text/page-text-handler.service.ts index c96fc83b86c..aa492112e8a 100644 --- a/libs/features/texts/src/page-text/page-text-handler.service.ts +++ b/libs/features/texts/src/page-text/page-text-handler.service.ts @@ -7,6 +7,7 @@ import { PageText, PAGE_TEXT_TABLE } from '@keira/shared/acore-world-model'; }) export class PageTextHandlerService extends HandlerService { protected readonly mainEditorRoutePath = 'texts/page-text'; + protected readonly copyRoutePath = 'texts/page-text-copy'; get isUnsaved(): Signal { return this.statusMap[PAGE_TEXT_TABLE].asReadonly(); @@ -15,4 +16,13 @@ export class PageTextHandlerService extends HandlerService { protected _statusMap = { [PAGE_TEXT_TABLE]: signal(false), }; + + override select(isNew: boolean, id: string | number | Partial, name?: string, navigate = true, sourceId?: string) { + if (isNew && sourceId) { + super.select(isNew, id, name, false, sourceId); + this.router.navigate([this.copyRoutePath]); + } else { + super.select(isNew, id, name, navigate); + } + } } diff --git a/libs/features/texts/src/page-text/select-page-text.component.html b/libs/features/texts/src/page-text/select-page-text.component.html index d17e50dbde8..e5b854b80d1 100644 --- a/libs/features/texts/src/page-text/select-page-text.component.html +++ b/libs/features/texts/src/page-text/select-page-text.component.html @@ -8,6 +8,7 @@ [customStartingId]="customStartingId" [handlerService]="handlerService" [queryService]="queryService" + [allowCopy]="true" /> diff --git a/libs/features/trainer/src/index.ts b/libs/features/trainer/src/index.ts index 557168e9b8f..a66553ee9f7 100644 --- a/libs/features/trainer/src/index.ts +++ b/libs/features/trainer/src/index.ts @@ -5,3 +5,4 @@ export * from './edit-trainer/trainer.component'; export * from './edit-trainer/trainer.service'; export * from './trainer-spell/trainer-spell.component'; export * from './trainer-spell/trainer-spell.service'; +export * from './trainer-copy/trainer-copy.component'; diff --git a/libs/features/trainer/src/select-trainer/select-trainer.component.html b/libs/features/trainer/src/select-trainer/select-trainer.component.html index 71bef3c2538..1b976d1cdd2 100644 --- a/libs/features/trainer/src/select-trainer/select-trainer.component.html +++ b/libs/features/trainer/src/select-trainer/select-trainer.component.html @@ -8,6 +8,7 @@ [customStartingId]="customStartingId" [handlerService]="handlerService" [queryService]="queryService" + [allowCopy]="true" /> diff --git a/libs/features/trainer/src/trainer-copy/trainer-copy.component.html b/libs/features/trainer/src/trainer-copy/trainer-copy.component.html new file mode 100644 index 00000000000..eef15371ccb --- /dev/null +++ b/libs/features/trainer/src/trainer-copy/trainer-copy.component.html @@ -0,0 +1,10 @@ +@if (sourceId && newId) { + +} diff --git a/libs/features/trainer/src/trainer-copy/trainer-copy.component.ts b/libs/features/trainer/src/trainer-copy/trainer-copy.component.ts new file mode 100644 index 00000000000..54ccc77aff2 --- /dev/null +++ b/libs/features/trainer/src/trainer-copy/trainer-copy.component.ts @@ -0,0 +1,34 @@ +import { ChangeDetectionStrategy, Component, inject, OnInit } from '@angular/core'; +import { CopyOutputComponent } from '@keira/shared/base-editor-components'; +import { TrainerHandlerService } from '../trainer-handler.service'; +import { Router } from '@angular/router'; +import { TRAINER_ID, TRAINER_SPELL_ID, TRAINER_SPELL_TABLE, TRAINER_TABLE } from '@keira/shared/acore-world-model'; + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'keira-trainer-copy', + templateUrl: './trainer-copy.component.html', + standalone: true, + imports: [CopyOutputComponent], +}) +export class TrainerCopyComponent implements OnInit { + private readonly router = inject(Router); + protected readonly handlerService = inject(TrainerHandlerService); + + protected readonly tableName = TRAINER_TABLE; + protected readonly idField = TRAINER_ID ?? 'id'; + protected sourceId!: string | number; + protected newId!: string | number; + + protected readonly relatedTables = [{ tableName: TRAINER_SPELL_TABLE, idField: TRAINER_SPELL_ID }]; + + ngOnInit(): void { + if (!this.handlerService.sourceId || !this.handlerService.selected) { + this.router.navigate(['/trainer/select']); + return; + } + + this.sourceId = this.handlerService.sourceId; + this.newId = this.handlerService.selected; + } +} diff --git a/libs/features/trainer/src/trainer-handler.service.ts b/libs/features/trainer/src/trainer-handler.service.ts index 39e0f897c25..de657b51d6c 100644 --- a/libs/features/trainer/src/trainer-handler.service.ts +++ b/libs/features/trainer/src/trainer-handler.service.ts @@ -7,6 +7,7 @@ import { TRAINER_TABLE, TRAINER_SPELL_TABLE, Trainer } from '@keira/shared/acore }) export class TrainerHandlerService extends HandlerService { protected readonly mainEditorRoutePath = 'trainer/trainer'; + protected readonly copyRoutePath = 'trainer/copy'; get isTrainerUnsaved(): Signal { return this.statusMap[TRAINER_TABLE].asReadonly(); @@ -20,4 +21,14 @@ export class TrainerHandlerService extends HandlerService { [TRAINER_TABLE]: signal(false), [TRAINER_SPELL_TABLE]: signal(false), }; + + override select(isNew: boolean, id: string | number | Partial, name?: string, navigate = true, sourceId?: string) { + // If we're creating a new entity from a copy, navigate to copy route + if (isNew && sourceId) { + super.select(isNew, id, name, false, sourceId); + this.router.navigate([this.copyRoutePath]); + } else { + super.select(isNew, id, name, navigate); + } + } } diff --git a/libs/shared/base-abstract-classes/src/service/handlers/handler.service.ts b/libs/shared/base-abstract-classes/src/service/handlers/handler.service.ts index 20ed9998851..6c96d6c6684 100644 --- a/libs/shared/base-abstract-classes/src/service/handlers/handler.service.ts +++ b/libs/shared/base-abstract-classes/src/service/handlers/handler.service.ts @@ -12,6 +12,7 @@ export abstract class HandlerService extends SubscriptionHan selectedName!: string; isNew = false; forceReload = false; + sourceId?: string; protected abstract _statusMap: { [key: string]: WritableSignal }; protected abstract readonly mainEditorRoutePath: string; @@ -50,9 +51,10 @@ export abstract class HandlerService extends SubscriptionHan return this._customItemScssClass; } - select(isNew: boolean, id: string | number | Partial, name?: string, navigate = true) { + select(isNew: boolean, id: string | number | Partial, name?: string, navigate = true, sourceId?: string) { this.resetStatus(); this.isNew = isNew; + this.sourceId = sourceId; const currentSelected = this._selected; diff --git a/libs/shared/base-editor-components/src/copy-output/copy-output.component.html b/libs/shared/base-editor-components/src/copy-output/copy-output.component.html new file mode 100644 index 00000000000..bcd68c9fb0b --- /dev/null +++ b/libs/shared/base-editor-components/src/copy-output/copy-output.component.html @@ -0,0 +1,124 @@ +
+
+
+
+

{{ 'COPY_OUTPUT.TITLE' | translate }}

+

{{ 'COPY_OUTPUT.DESCRIPTION' | translate }}

+
+
+ + @if (error()) { + + } + + @if (relatedTableStates().length > 0) { +
+
+ +
+
+ } + +
+
+
+
Generated SQL
+
+ + +
+
+ +
+ +
+ +
+ + + + + +
+
+
+
+
diff --git a/libs/shared/base-editor-components/src/copy-output/copy-output.component.scss b/libs/shared/base-editor-components/src/copy-output/copy-output.component.scss new file mode 100644 index 00000000000..5a4406457f3 --- /dev/null +++ b/libs/shared/base-editor-components/src/copy-output/copy-output.component.scss @@ -0,0 +1,5 @@ +.sql-output-container { + background-color: #f8f9fa; + max-height: 500px; + overflow-y: auto; +} diff --git a/libs/shared/base-editor-components/src/copy-output/copy-output.component.ts b/libs/shared/base-editor-components/src/copy-output/copy-output.component.ts new file mode 100644 index 00000000000..58ac75d8f65 --- /dev/null +++ b/libs/shared/base-editor-components/src/copy-output/copy-output.component.ts @@ -0,0 +1,312 @@ +import { ChangeDetectionStrategy, Component, inject, Input, OnInit, signal } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TranslateModule } from '@ngx-translate/core'; +import { TooltipModule } from 'ngx-bootstrap/tooltip'; +import { ClipboardService } from 'ngx-clipboard'; +import { HandlerService } from '@keira/shared/base-abstract-classes'; +import { TableRow } from '@keira/shared/constants'; +import { MysqlQueryService } from '@keira/shared/db-layer'; +import { SubscriptionHandler } from '@keira/shared/utils'; +import { QueryError } from 'mysql2'; +import { QueryErrorComponent } from '../query-output/query-error/query-error.component'; +import { HighlightjsWrapperComponent } from '../highlightjs-wrapper/highlightjs-wrapper.component'; + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'keira-copy-output', + templateUrl: './copy-output.component.html', + styleUrls: ['./copy-output.component.scss'], + imports: [CommonModule, TranslateModule, TooltipModule, QueryErrorComponent, HighlightjsWrapperComponent], +}) +export class CopyOutputComponent extends SubscriptionHandler implements OnInit { + protected readonly clipboardService = inject(ClipboardService); + protected readonly queryService = inject(MysqlQueryService); + + @Input({ required: true }) handlerService!: HandlerService; + @Input({ required: true }) tableName!: string; + @Input({ required: true }) idField!: string; + @Input({ required: true }) sourceId!: string | number; + @Input({ required: true }) newId!: string | number; + @Input() relatedTables?: Array<{ + tableName: string; + idField: string; + copyMode?: 'RAW' | 'ALL'; // RAW = explicit columns, ALL = temp table + columns?: string[]; + }>; + + @Input() mainCopyMode?: 'RAW' | 'ALL'; + @Input() mainColumns?: string[]; + + protected mainCopyModeSignal = signal<'RAW' | 'ALL'>('RAW'); + protected mainColumnsSignal = signal(undefined); + + protected copyQuery = signal(''); + protected relatedTableStates = signal< + Array<{ tableName: string; idField: string; count: number; included: boolean; copyMode: 'RAW' | 'ALL'; columns?: string[] }> + >([]); + protected error = signal(undefined); + protected executing = signal(false); + protected executed = signal(false); + protected sqlExpanded = signal(false); + + ngOnInit(): void { + this.mainCopyModeSignal.set(this.mainCopyMode || 'RAW'); + this.mainColumnsSignal.set(this.mainColumns); + this.populateRelatedTables(); + } + + protected setCopyModeForTable(tableName: string, mode: string): void { + const normalized: 'RAW' | 'ALL' = mode === 'RAW' ? 'RAW' : 'ALL'; + + if (tableName === this.tableName) { + this.mainCopyModeSignal.set(normalized); + + const states = this.relatedTableStates().map((s) => ({ + tableName: s.tableName, + idField: s.idField, + count: s.count, + included: s.included, + copyMode: s.copyMode, + columns: s.columns, + })); + const idx = states.findIndex((s) => s.tableName === tableName); + if (idx !== -1) { + states[idx].copyMode = normalized; + this.relatedTableStates.set(states); + } + + this.generateCopyQuery(); + return; + } + + const states = this.relatedTableStates().map((s) => ({ + tableName: s.tableName, + idField: s.idField, + count: s.count, + included: s.included, + copyMode: s.copyMode, + columns: s.columns, + })); + + const idx = states.findIndex((s) => s.tableName === tableName); + if (idx !== -1) { + states[idx].copyMode = normalized; + this.relatedTableStates.set(states); + this.generateCopyQuery(); + } + } + + protected populateRelatedTables(): void { + const inputTables: Array<{ tableName: string; idField: string; copyMode?: 'RAW' | 'ALL'; columns?: string[] }> = []; + + // Include main table first (default to RAW) + inputTables.push({ tableName: this.tableName, idField: this.idField, copyMode: this.mainCopyMode || 'RAW', columns: this.mainColumns }); + + if (this.relatedTables && this.relatedTables.length > 0) { + for (const t of this.relatedTables) { + inputTables.push({ tableName: t.tableName, idField: t.idField, copyMode: t.copyMode || 'RAW', columns: t.columns }); + } + } + + const states: Array<{ + tableName: string; + idField: string; + count: number; + included: boolean; + copyMode: 'RAW' | 'ALL'; + columns?: string[]; + }> = []; + let remaining = inputTables.length; + + for (const table of inputTables) { + this.subscriptions.push( + this.queryService.getRowsCount(table.tableName, table.idField, this.sourceId).subscribe((count: number | null) => { + const num = Number(count || 0); + + const isMain = table.tableName === this.tableName; + + if (isMain || num > 0) { + states.push({ + tableName: table.tableName, + idField: table.idField, + count: num, + included: isMain ? true : true, + copyMode: table.copyMode || 'RAW', + columns: table.columns, + }); + } + + remaining--; + if (remaining === 0) { + this.relatedTableStates.set(states); + this.generateCopyQuery(); + } + }), + ); + } + } + + protected generateCopyQuery(): void { + const setVars = this.queryService.getCopyVarsSet(this.sourceId, this.newId); + + const selectedTables = this.relatedTableStates().filter((t) => t.included); + + // If no tables selected, just set main table + if (selectedTables.length === 0) { + const query = setVars + this.queryService.getCopyQuery(this.tableName, this.sourceId, this.newId, this.idField, true); + this.copyQuery.set(query); + return; + } + + // If nothing is RAW, we can quickly generate everything using getCopyQuery + const hasAnyRaw = selectedTables.some((t) => t.copyMode === 'RAW'); + + if (!hasAnyRaw) { + let query = setVars; + for (const table of selectedTables) { + query += '\n' + this.queryService.getCopyQuery(table.tableName, this.sourceId, this.newId, table.idField, true); + } + this.copyQuery.set(query); + return; + } + + // We have at least one RAW table. We'll fetch data for RAW tables and construct queries. + const rawTables = selectedTables.filter((t) => t.copyMode === 'RAW'); + + let remainingRaw = rawTables.length; + const tableQueries: Map = new Map(); + + for (const table of rawTables) { + this.subscriptions.push( + this.queryService.selectAll(table.tableName, table.idField, this.sourceId).subscribe((rows) => { + const cols = table.columns && table.columns.length > 0 ? table.columns : rows[0] ? Object.keys(rows[0]) : []; + const rawQuery = this.queryService.getCopyQueryRawWithValues(table.tableName, rows, this.newId, table.idField, cols, true); + tableQueries.set(table.tableName, rawQuery); + + remainingRaw--; + if (remainingRaw === 0) { + let finalQuery = setVars; + for (const t of selectedTables) { + if (t.copyMode === 'RAW') { + finalQuery += '\n' + tableQueries.get(t.tableName); + } else { + finalQuery += '\n' + this.queryService.getCopyQuery(t.tableName, this.sourceId, this.newId, t.idField, true); + } + } + this.copyQuery.set(finalQuery); + } + }), + ); + } + } + + protected copy(): void { + this.clipboardService.copyFromContent(this.copyQuery()); + } + + protected toggleRelatedTable(tableName: string): void { + const states = this.relatedTableStates().map((s) => ({ + tableName: s.tableName, + idField: s.idField, + count: s.count, + included: s.included, + copyMode: s.copyMode, + columns: s.columns, + })); + const idx = states.findIndex((s) => s.tableName === tableName); + if (idx !== -1) { + states[idx].included = !states[idx].included; + this.relatedTableStates.set(states); + this.generateCopyQuery(); + } + } + + protected toggleCopyMode(tableName: string): void { + const states = this.relatedTableStates().map((s) => ({ + tableName: s.tableName, + idField: s.idField, + count: s.count, + included: s.included, + copyMode: s.copyMode, + columns: s.columns, + })); + const idx = states.findIndex((s) => s.tableName === tableName); + if (idx !== -1) { + states[idx].copyMode = states[idx].copyMode === 'RAW' ? 'ALL' : 'RAW'; + this.relatedTableStates.set(states); + this.generateCopyQuery(); + } + } + + protected setAllIncluded(checked: boolean): void { + const states = this.relatedTableStates().map((s) => ({ + tableName: s.tableName, + idField: s.idField, + count: s.count, + included: checked, + copyMode: s.copyMode, + columns: s.columns, + })); + this.relatedTableStates.set(states); + this.generateCopyQuery(); + } + + protected allIncluded(): boolean { + const states = this.relatedTableStates(); + return states.length > 0 && states.every((s) => s.included); + } + + protected setAllCopyMode(mode: 'RAW' | 'ALL'): void { + const states = this.relatedTableStates().map((s) => ({ + tableName: s.tableName, + idField: s.idField, + count: s.count, + included: s.included, + copyMode: mode, + columns: s.columns, + })); + this.relatedTableStates.set(states); + + this.mainCopyModeSignal.set(mode); + + this.generateCopyQuery(); + } + + protected allCopyModeIsRaw(): boolean { + const states = this.relatedTableStates(); + return states.length > 0 && states.every((s) => s.copyMode === 'RAW'); + } + + protected toggleMainCopyMode(): void { + const newMode = this.mainCopyModeSignal() === 'RAW' ? 'ALL' : 'RAW'; + this.mainCopyModeSignal.set(newMode); + this.generateCopyQuery(); + } + + protected toggleSqlExpanded(): void { + this.sqlExpanded.set(!this.sqlExpanded()); + } + + protected execute(): void { + this.executing.set(true); + this.error.set(undefined); + + this.subscriptions.push( + this.queryService.query(this.copyQuery()).subscribe({ + next: () => { + this.executing.set(false); + this.executed.set(true); + }, + error: (error: QueryError) => { + this.executing.set(false); + this.error.set(error); + }, + }), + ); + } + + protected continue(): void { + // Navigate to the editor for the newly created entry + this.handlerService.select(false, this.newId); + } +} diff --git a/libs/shared/base-editor-components/src/create/create.component.html b/libs/shared/base-editor-components/src/create/create.component.html index e6879131d00..90bc2bc3d36 100644 --- a/libs/shared/base-editor-components/src/create/create.component.html +++ b/libs/shared/base-editor-components/src/create/create.component.html @@ -1,37 +1,103 @@

- -
-
-
- - + +@if (allowCopy) { +
+
+
+ + +
+ +
+ + +
-
- - - {{ 'CREATE.FREE_ENTRY' | translate: { ENTITY_ID_FIELD: entityIdField } }} - {{ isIdFree ? ('CREATE.FREE' | translate) : ('CREATE.ALREADY_USE' | translate) }} - +} + +
+
+
+
+ +
+ +
+
+ + + {{ 'CREATE.SOURCE_ENTRY' | translate: { ENTITY_ID_FIELD: entityIdField } }} + {{ isSourceIdValid ? ('CREATE.EXISTS' | translate) : ('CREATE.DOES_NOT_EXIST' | translate) }} + +
+
+ +
+ +
+ +
+
+ + + {{ 'CREATE.FREE_ENTRY' | translate: { ENTITY_ID_FIELD: entityIdField } }} + {{ isIdFree ? ('CREATE.FREE' | translate) : ('CREATE.ALREADY_USE' | translate) }} + +
+
+ +
+ +
+
diff --git a/libs/shared/base-editor-components/src/create/create.component.spec.ts b/libs/shared/base-editor-components/src/create/create.component.spec.ts index 27a3816e192..94a89b9ee01 100644 --- a/libs/shared/base-editor-components/src/create/create.component.spec.ts +++ b/libs/shared/base-editor-components/src/create/create.component.spec.ts @@ -1,16 +1,81 @@ -import { TestBed } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { provideZonelessChangeDetection } from '@angular/core'; import { provideNoopAnimations } from '@angular/platform-browser/animations'; import { FormsModule } from '@angular/forms'; -import { BrowserModule } from '@angular/platform-browser'; +import { BrowserModule, By } from '@angular/platform-browser'; +import { of, throwError } from 'rxjs'; +import { TranslateModule } from '@ngx-translate/core'; import { MockHandlerService } from '@keira/shared/base-abstract-classes'; import { TableRow } from '@keira/shared/constants'; import { MysqlQueryService } from '@keira/shared/db-layer'; import { PageObject, TranslateTestingModule } from '@keira/shared/test-utils'; -import { of, throwError } from 'rxjs'; import { anything, instance, mock, reset, when } from 'ts-mockito'; + import { CreateComponent } from './create.component'; +describe('CreateComponent', () => { + let component: CreateComponent; + let fixture: ComponentFixture>; + + const fakeQueryService: any = { + getMaxId: () => of([{ max: 5 }]), + selectAll: () => of([]), + }; + + const fakeHandlerService: any = { + select: jasmine.createSpy('select'), + }; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [FormsModule, TranslateModule.forRoot(), CreateComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(CreateComponent); + component = fixture.componentInstance; + // Provide required inputs + component.entityTable = 'test'; + component.entityIdField = 'id'; + component.customStartingId = 1; + component.queryService = fakeQueryService; + component.handlerService = fakeHandlerService; + fixture.detectChanges(); + }); + + it('does not render the copy option when allowCopy is false', () => { + // Default allowCopy is false + fixture.detectChanges(); + const copyRadio = fixture.debugElement.query(By.css('#method-copy')); + expect(copyRadio).toBeNull(); + expect(component.creationMethod).toBe('blank'); + }); + + it('renders the copy option when allowCopy is true', () => { + component.allowCopy = true; + fixture.detectChanges(); + const copyRadio = fixture.debugElement.query(By.css('#method-copy')); + expect(copyRadio).not.toBeNull(); + // when allowed the copy radio should not be disabled + const copyInput = copyRadio.nativeElement as HTMLInputElement; + expect(copyInput.disabled).toBeFalse(); + }); + + it('resets creationMethod to blank when allowCopy is set to false', () => { + component.allowCopy = true; + component.creationMethod = 'copy'; + expect(component.creationMethod).toBe('copy'); + component.sourceIdModel = 123; + component.isSourceIdValid = true; + + component.allowCopy = false; + // creationMethod should be reset and source-related state cleared + expect(component.creationMethod).toBe('blank'); + expect(component.sourceIdModel).toBeUndefined(); + expect(component.isSourceIdValid).toBeFalse(); + }); +}); +// (Additional tests below use the imports declared above) + class CreateComponentPage extends PageObject> { get idInput(): HTMLInputElement { return this.query('#id'); diff --git a/libs/shared/base-editor-components/src/create/create.component.ts b/libs/shared/base-editor-components/src/create/create.component.ts index d77e1e248d1..d364eeff578 100644 --- a/libs/shared/base-editor-components/src/create/create.component.ts +++ b/libs/shared/base-editor-components/src/create/create.component.ts @@ -21,13 +21,32 @@ export class CreateComponent extends SubscriptionHandler imp @Input({ required: true }) customStartingId!: number; @Input({ required: true }) handlerService!: HandlerService; @Input({ required: true }) queryService!: MysqlQueryService; + // Controls whether "copy from existing" is allowed for the current entity table. + // Default is false (disabled). Individual selectors can enable it by passing [allowCopy]="true". + private _allowCopy = false; + @Input() + set allowCopy(value: boolean) { + this._allowCopy = !!value; + if (!this._allowCopy) { + // Reset creation method if copy is disabled + this.creationMethod = 'blank'; + this.sourceIdModel = undefined; + this.isSourceIdValid = false; + } + } + get allowCopy(): boolean { + return this._allowCopy; + } @Input() maxEntryValue = MAX_INT_UNSIGNED_VALUE; private readonly changeDetectorRef = inject(ChangeDetectorRef); public idModel!: number; + public sourceIdModel?: number; + public creationMethod: 'blank' | 'copy' = 'blank'; private _loading = false; isIdFree = false; + isSourceIdValid = false; get loading(): boolean { return this._loading; @@ -37,6 +56,10 @@ export class CreateComponent extends SubscriptionHandler imp if (this.queryService) { this.getNextId(); } + + if (!this.allowCopy) { + this.creationMethod = 'blank'; + } } checkId() { @@ -79,9 +102,64 @@ export class CreateComponent extends SubscriptionHandler imp if (this.idModel > MAX_INT_UNSIGNED_VALUE) { this.idModel = MAX_INT_UNSIGNED_VALUE; } + if (this.sourceIdModel && this.sourceIdModel > MAX_INT_UNSIGNED_VALUE) { + this.sourceIdModel = MAX_INT_UNSIGNED_VALUE; + } } private calculateNextId(currentMax: number): number { return currentMax < this.customStartingId ? this.customStartingId : currentMax + 1; } + + onCreationMethodChange(): void { + if (this.creationMethod === 'blank') { + this.sourceIdModel = undefined; + this.isSourceIdValid = false; + } + this.changeDetectorRef.markForCheck(); + } + + checkSourceId(): void { + if (!this.sourceIdModel) { + this.isSourceIdValid = false; + this.changeDetectorRef.markForCheck(); + return; + } + + this._loading = true; + this.subscriptions.push( + this.queryService.selectAll(this.entityTable, this.entityIdField, this.sourceIdModel).subscribe({ + next: (data) => { + // Source ID should exist (opposite of new ID check) + this.isSourceIdValid = data.length > 0; + this._loading = false; + this.changeDetectorRef.markForCheck(); + }, + error: (error: QueryError) => { + console.error(error); + this.isSourceIdValid = false; + this._loading = false; + this.changeDetectorRef.markForCheck(); + }, + }), + ); + } + + isFormValid(): boolean { + const isNewIdValid = !!this.idModel && this.isIdFree; + + if (this.creationMethod === 'copy') { + return isNewIdValid && !!this.sourceIdModel && this.isSourceIdValid; + } + + return isNewIdValid; + } + + onCreate(): void { + if (this.creationMethod === 'copy') { + this.handlerService.select(true, this.idModel, undefined, true, this.sourceIdModel!.toString()); + } else { + this.handlerService.select(true, this.idModel); + } + } } diff --git a/libs/shared/base-editor-components/src/index.ts b/libs/shared/base-editor-components/src/index.ts index 7ef49a4ce6b..f76aae06f36 100644 --- a/libs/shared/base-editor-components/src/index.ts +++ b/libs/shared/base-editor-components/src/index.ts @@ -10,5 +10,6 @@ export * from './icon/icon.service'; export * from './icon/icon.component'; export * from './create/create.component'; +export * from './copy-output/copy-output.component'; export * from './query-output/query-output.component'; export * from './query-output/query-error/query-error.component'; diff --git a/libs/shared/db-layer/src/query/mysql-query.service.ts b/libs/shared/db-layer/src/query/mysql-query.service.ts index 80edae6901d..cc2032271cc 100644 --- a/libs/shared/db-layer/src/query/mysql-query.service.ts +++ b/libs/shared/db-layer/src/query/mysql-query.service.ts @@ -462,4 +462,142 @@ export class MysqlQueryService extends BaseQueryService { `SELECT displayId AS v FROM gameobject_template WHERE entry=${gameObjectId}`, ); } + + // Utility to format a value for SQL (quote strings, keep numbers unquoted) + private toSqlValue(value: string | number): string { + if (typeof value === 'number') return String(value); + // if string is an integer string, keep it without quotes + if (/^\d+$/.test(value)) return value as string; + // escape single quotes + return `'${(value as string).replace(/'/g, "\\'")}'`; + } + + getCopyVarsSet(sourceId: string | number, newId: string | number): string { + const source = this.toSqlValue(sourceId); + const entry = this.toSqlValue(newId); + return `SET @SOURCE = ${source};\nSET @ENTRY = ${entry};\n`; + } + + // Generates SQL to copy an entry from one ID to another using temporary table + // If useVars is true, the query uses @SOURCE and @ENTRY session variables instead of concrete values + getCopyQuery(tableName: string, sourceId: string | number, newId: string | number, idField: string, useVars: boolean = false): string { + const sourceRef = useVars ? '@SOURCE' : this.toSqlValue(sourceId); + const entryRef = useVars ? '@ENTRY' : this.toSqlValue(newId); + + const query = + `DELETE FROM \`${tableName}\` WHERE \`${idField}\` = ${entryRef};\n` + + `CREATE TEMPORARY TABLE temp_copy_table AS\n` + + ` SELECT * FROM \`${tableName}\` WHERE \`${idField}\` = ${sourceRef};\n` + + `UPDATE temp_copy_table SET \`${idField}\` = ${entryRef};\n` + + `INSERT INTO \`${tableName}\` SELECT * FROM temp_copy_table;\n` + + `DROP TEMPORARY TABLE temp_copy_table;\n`; + + return this.formatQuery(query); + } + + /** + * Generates SQL to copy entries with explicit column names (RAW mode) + * + * Use RAW mode when: + * - Table has multiple rows per ID (e.g., creature_queststarter, npc_vendor) + * - You want to preserve exact data including additional columns + * - The table has simple structure with few columns + * + * Use ALL mode (getCopyQuery) when: + * - Table has single row per ID (e.g., creature_template, quest_template) + * - Table has many columns (easier to use SELECT *) + * + * @param tableName - The table name + * @param sourceId - The source entry ID to copy from + * @param newId - The new entry ID to copy to + * @param idField - The primary ID field name + * @param columns - Array of all column names to include in the copy + * @param useVars - Whether to use @SOURCE/@ENTRY variables instead of concrete values + */ + getCopyQueryRaw( + tableName: string, + sourceId: string | number, + newId: string | number, + idField: string, + columns: string[], + useVars: boolean = false, + ): string { + const sourceRef = useVars ? '@SOURCE' : this.toSqlValue(sourceId); + const entryRef = useVars ? '@ENTRY' : this.toSqlValue(newId); + + // Build the column list for INSERT + const columnList = columns.map((col) => `\`${col}\``).join(', '); + + // Build the SELECT list - replace idField with new value + const selectList = columns.map((col) => (col === idField ? `${entryRef} AS \`${col}\`` : `\`${col}\``)).join(', '); + + const query = + `DELETE FROM \`${tableName}\` WHERE \`${idField}\` = ${entryRef};\n` + + `INSERT INTO \`${tableName}\` (${columnList})\n` + + ` SELECT ${selectList}\n` + + ` FROM \`${tableName}\`\n` + + ` WHERE \`${idField}\` = ${sourceRef};\n`; + + return this.formatQuery(query); + } + + // Generates SQL to copy entries with explicit column names and VALUES (RAW mode) + // This generates INSERT statements with actual row data, not SELECT statements + getCopyQueryRawWithValues( + tableName: string, + rows: any[], + newId: string | number, + idField: string, + columns: string[], + useVars: boolean = false, + ): string { + const entryRef = useVars ? '@ENTRY' : this.toSqlValue(newId); + + // Always include DELETE against the destination entry + let query = `DELETE FROM \`${tableName}\` WHERE \`${idField}\` = ${entryRef};\n`; + + if (!rows || rows.length === 0) { + // Nothing to insert, return only the delete statement + return this.formatQuery(query); + } + + // If no explicit columns were provided, derive from the first row + if (!columns || columns.length === 0) { + columns = Object.keys(rows[0]); + } + + // Build the column list for INSERT + const columnList = columns.map((col) => `\`${col}\``).join(', '); + + // Build VALUES list + const valuesList = rows + .map((row) => { + const values = columns.map((col) => { + // Replace the ID field with the new entry value + if (col === idField) { + return entryRef; + } + // Convert value to SQL format + const value = row[col]; + if (value === null || value === undefined) { + return 'NULL'; + } + if (typeof value === 'number') { + return value.toString(); + } + // Escape strings + return this.toSqlValue(value); + }); + return `(${values.join(', ')})`; + }) + .join(',\n'); + + query += `INSERT INTO \`${tableName}\` (${columnList}) VALUES\n${valuesList};\n`; + + return this.formatQuery(query); + } + + getRowsCount(tableName: string, idField: string, idValue: string | number) { + return this.queryValue(`SELECT COUNT(1) AS v FROM \`${tableName}\` WHERE \`${idField}\` = ${idValue};\n`); + } } From b2d724b6bab352bc0594d6f38cc86e283c162c47 Mon Sep 17 00:00:00 2001 From: sogla Date: Wed, 21 Jan 2026 02:59:58 +0100 Subject: [PATCH 02/17] update with suggestions remove default specify imports remove some imports move select handler logic to shared handler move text to translation file remove 'public' add 'protected' refactor @Input to signal, define interface remove 'standalone' --- apps/keira/src/assets/i18n/en.json | 3 +- .../creature-copy/creature-copy.component.ts | 1 - .../creature/src/creature-handler.service.ts | 2 +- libs/features/creature/src/index.ts | 2 +- .../game-tele-copy.component.ts | 1 - .../src/game-tele-handler.service.ts | 2 +- libs/features/game-tele/src/index.ts | 2 +- .../gameobject-copy.component.ts | 1 - .../src/gameobject-handler.service.ts | 2 +- libs/features/gameobject/src/index.ts | 2 +- .../src/gossip-copy/gossip-copy.component.ts | 1 - .../gossip/src/gossip-handler.service.ts | 2 +- libs/features/gossip/src/index.ts | 2 +- libs/features/item/src/index.ts | 2 +- .../item/src/item-copy/item-copy.component.ts | 1 - .../features/item/src/item-handler.service.ts | 2 +- .../fishing-loot-copy.component.ts | 1 - .../fishing-loot-handler.service.ts | 11 +- .../src/mail-loot/mail-loot-copy.component.ts | 1 - .../mail-loot/mail-loot-handler.service.ts | 11 +- .../reference-loot-copy.component.ts | 1 - .../reference-loot-handler.service.ts | 11 +- .../src/shared/copy.component.html | 10 ++ .../spell-loot/spell-loot-copy.component.ts | 1 - .../spell-loot/spell-loot-handler.service.ts | 11 +- libs/features/quest/src/index.ts | 2 +- .../src/quest-copy/quest-copy.component.ts | 2 +- .../quest/src/quest-handler.service.ts | 2 +- libs/features/spell/src/index.ts | 2 +- .../src/spell-copy/spell-copy.component.ts | 2 +- .../spell/src/spell-handler.service.ts | 2 +- .../acore-string-copy.component.ts | 2 +- .../acore-string-handler.service.ts | 2 +- libs/features/texts/src/index.ts | 4 +- .../src/npc-text/npc-text-copy.component.ts | 2 +- .../src/npc-text/npc-text-handler.service.ts | 2 +- .../src/page-text/page-text-copy.component.ts | 2 +- .../page-text/page-text-handler.service.ts | 2 +- libs/features/trainer/src/index.ts | 2 +- .../trainer-copy/trainer-copy.component.ts | 2 +- .../trainer/src/trainer-handler.service.ts | 2 +- .../src/service/handlers/handler.service.ts | 7 +- .../src/copy-output/copy-output.component.ts | 109 ++++++++++-------- .../src/create/create.component.html | 2 +- .../src/create/create.component.ts | 16 +-- .../db-layer/src/query/mysql-query.service.ts | 2 +- 46 files changed, 117 insertions(+), 139 deletions(-) create mode 100644 libs/features/other-loots/src/shared/copy.component.html diff --git a/apps/keira/src/assets/i18n/en.json b/apps/keira/src/assets/i18n/en.json index 0fe2c912149..da09dd90eee 100644 --- a/apps/keira/src/assets/i18n/en.json +++ b/apps/keira/src/assets/i18n/en.json @@ -19,7 +19,8 @@ "EXISTS": "exists", "DOES_NOT_EXIST": "does not exist", "START_FROM_BLANK": "Start from blank", - "COPY_FROM_EXISTING": "Copy from existing" + "COPY_FROM_EXISTING": "Copy from existing", + "COPY_DISABLED": "Copy from existing is not available for this entity" }, "CONNECTED": "Connected", "SEARCH": "Search", diff --git a/libs/features/creature/src/creature-copy/creature-copy.component.ts b/libs/features/creature/src/creature-copy/creature-copy.component.ts index e9019fba60d..03c7e1c9f12 100644 --- a/libs/features/creature/src/creature-copy/creature-copy.component.ts +++ b/libs/features/creature/src/creature-copy/creature-copy.component.ts @@ -34,7 +34,6 @@ import { Router } from '@angular/router'; changeDetection: ChangeDetectionStrategy.OnPush, selector: 'keira-creature-copy', templateUrl: './creature-copy.component.html', - standalone: true, imports: [CopyOutputComponent], }) export class CreatureCopyComponent implements OnInit { diff --git a/libs/features/creature/src/creature-handler.service.ts b/libs/features/creature/src/creature-handler.service.ts index db23ee67105..1a916be4660 100644 --- a/libs/features/creature/src/creature-handler.service.ts +++ b/libs/features/creature/src/creature-handler.service.ts @@ -30,7 +30,7 @@ import { SaiCreatureHandlerService } from './sai-creature-handler.service'; export class CreatureHandlerService extends HandlerService { protected saiCreatureHandler = inject(SaiCreatureHandlerService); protected readonly mainEditorRoutePath = 'creature/creature-template'; - protected readonly copyRoutePath = 'creature/copy'; + protected override readonly copyRoutePath = 'creature/copy'; get isCreatureTemplateUnsaved(): Signal { return this.statusMap[CREATURE_TEMPLATE_TABLE].asReadonly(); diff --git a/libs/features/creature/src/index.ts b/libs/features/creature/src/index.ts index db80b7f66e1..6dc92a87847 100644 --- a/libs/features/creature/src/index.ts +++ b/libs/features/creature/src/index.ts @@ -1,4 +1,4 @@ -export * from './creature-copy/creature-copy.component'; +export { CreatureCopyComponent } from './creature-copy/creature-copy.component'; export * from './creature-equip-template/creature-equip-template.component'; export * from './creature-handler.service'; export * from './creature-loot-template/creature-loot-template.component'; diff --git a/libs/features/game-tele/src/game-tele-copy/game-tele-copy.component.ts b/libs/features/game-tele/src/game-tele-copy/game-tele-copy.component.ts index 4dcba1eeb5a..5b85b29bbd5 100644 --- a/libs/features/game-tele/src/game-tele-copy/game-tele-copy.component.ts +++ b/libs/features/game-tele/src/game-tele-copy/game-tele-copy.component.ts @@ -8,7 +8,6 @@ import { GAME_TELE_TABLE, GAME_TELE_ID } from '@keira/shared/acore-world-model'; changeDetection: ChangeDetectionStrategy.OnPush, selector: 'keira-game-tele-copy', templateUrl: './game-tele-copy.component.html', - standalone: true, imports: [CopyOutputComponent], }) export class GameTeleCopyComponent implements OnInit { diff --git a/libs/features/game-tele/src/game-tele-handler.service.ts b/libs/features/game-tele/src/game-tele-handler.service.ts index 4ebc629b608..07a5a328ba8 100644 --- a/libs/features/game-tele/src/game-tele-handler.service.ts +++ b/libs/features/game-tele/src/game-tele-handler.service.ts @@ -7,7 +7,7 @@ import { GAME_TELE_TABLE, GameTele } from '@keira/shared/acore-world-model'; }) export class GameTeleHandlerService extends HandlerService { protected readonly mainEditorRoutePath = 'game-tele/tele'; - protected readonly copyRoutePath = 'game-tele/copy'; + protected override readonly copyRoutePath = 'game-tele/copy'; get isGameTeleUnsaved(): Signal { return this.statusMap[GAME_TELE_TABLE].asReadonly(); diff --git a/libs/features/game-tele/src/index.ts b/libs/features/game-tele/src/index.ts index cf523959a1b..9d918b137bb 100644 --- a/libs/features/game-tele/src/index.ts +++ b/libs/features/game-tele/src/index.ts @@ -1,4 +1,4 @@ export { GameTeleComponent } from './edit-game-tele/game-tele.component'; export { SelectGameTeleComponent } from './select-game-tele/select-game-tele.component'; export { GameTeleHandlerService } from './game-tele-handler.service'; -export * from './game-tele-copy/game-tele-copy.component'; +export { GameTeleCopyComponent } from './game-tele-copy/game-tele-copy.component'; diff --git a/libs/features/gameobject/src/gameobject-copy/gameobject-copy.component.ts b/libs/features/gameobject/src/gameobject-copy/gameobject-copy.component.ts index 3534a08ad15..5d76148bb1b 100644 --- a/libs/features/gameobject/src/gameobject-copy/gameobject-copy.component.ts +++ b/libs/features/gameobject/src/gameobject-copy/gameobject-copy.component.ts @@ -17,7 +17,6 @@ import { Router } from '@angular/router'; changeDetection: ChangeDetectionStrategy.OnPush, selector: 'keira-gameobject-copy', templateUrl: './gameobject-copy.component.html', - standalone: true, imports: [CopyOutputComponent], }) export class GameobjectCopyComponent implements OnInit { diff --git a/libs/features/gameobject/src/gameobject-handler.service.ts b/libs/features/gameobject/src/gameobject-handler.service.ts index e83f60e50be..68fbff0870b 100644 --- a/libs/features/gameobject/src/gameobject-handler.service.ts +++ b/libs/features/gameobject/src/gameobject-handler.service.ts @@ -18,7 +18,7 @@ import { SaiGameobjectHandlerService } from './sai-gameobject-handler.service'; export class GameobjectHandlerService extends HandlerService { protected saiGameobjectHandler = inject(SaiGameobjectHandlerService); protected readonly mainEditorRoutePath = 'gameobject/gameobject-template'; - protected readonly copyRoutePath = 'gameobject/copy'; + protected override readonly copyRoutePath = 'gameobject/copy'; get isGameobjectTemplateUnsaved(): Signal { return this.statusMap[GAMEOBJECT_TEMPLATE_TABLE].asReadonly(); diff --git a/libs/features/gameobject/src/index.ts b/libs/features/gameobject/src/index.ts index caaa4028b74..19a373e5845 100644 --- a/libs/features/gameobject/src/index.ts +++ b/libs/features/gameobject/src/index.ts @@ -6,6 +6,6 @@ export * from './gameobject-template/gameobject-template.component'; export * from './gameobject-template-addon/gameobject-template-addon.component'; export * from './sai-gameobject/sai-gameobject.component'; export * from './select-gameobject/select-gameobject.component'; -export * from './gameobject-copy/gameobject-copy.component'; +export { GameobjectCopyComponent } from './gameobject-copy/gameobject-copy.component'; export * from './gameobject-handler.service'; export * from './sai-gameobject-handler.service'; diff --git a/libs/features/gossip/src/gossip-copy/gossip-copy.component.ts b/libs/features/gossip/src/gossip-copy/gossip-copy.component.ts index 3f27f4e8a96..a579cb5e206 100644 --- a/libs/features/gossip/src/gossip-copy/gossip-copy.component.ts +++ b/libs/features/gossip/src/gossip-copy/gossip-copy.component.ts @@ -8,7 +8,6 @@ import { GOSSIP_MENU_TABLE, GOSSIP_MENU_ID, GOSSIP_MENU_OPTION_TABLE, GOSSIP_MEN changeDetection: ChangeDetectionStrategy.OnPush, selector: 'keira-gossip-copy', templateUrl: './gossip-copy.component.html', - standalone: true, imports: [CopyOutputComponent], }) export class GossipCopyComponent implements OnInit { diff --git a/libs/features/gossip/src/gossip-handler.service.ts b/libs/features/gossip/src/gossip-handler.service.ts index b98c10a2d30..3587bb61b65 100644 --- a/libs/features/gossip/src/gossip-handler.service.ts +++ b/libs/features/gossip/src/gossip-handler.service.ts @@ -7,7 +7,7 @@ import { GOSSIP_MENU_OPTION_TABLE, GOSSIP_MENU_TABLE, GossipMenu } from '@keira/ }) export class GossipHandlerService extends HandlerService { protected readonly mainEditorRoutePath = 'gossip/gossip-menu'; - protected readonly copyRoutePath = 'gossip/copy'; + protected override readonly copyRoutePath = 'gossip/copy'; get isGossipMenuTableUnsaved(): Signal { return this.statusMap[GOSSIP_MENU_TABLE].asReadonly(); diff --git a/libs/features/gossip/src/index.ts b/libs/features/gossip/src/index.ts index 9c163c4d809..6eedf776b37 100644 --- a/libs/features/gossip/src/index.ts +++ b/libs/features/gossip/src/index.ts @@ -3,4 +3,4 @@ export * from './gossip-menu-option/gossip-menu-option.component'; export * from './gossip-menu-option-preview/gossip-menu-option-preview.component'; export * from './select-gossip/select-gossip.component'; export * from './gossip-handler.service'; -export * from './gossip-copy/gossip-copy.component'; +export { GossipCopyComponent } from './gossip-copy/gossip-copy.component'; diff --git a/libs/features/item/src/index.ts b/libs/features/item/src/index.ts index fd2c78a49ac..c1fc9a4036e 100644 --- a/libs/features/item/src/index.ts +++ b/libs/features/item/src/index.ts @@ -5,5 +5,5 @@ export * from './item-template/item-template.component'; export * from './milling-loot-template/milling-loot-template.component'; export * from './prospecting-loot-template/prospecting-loot-template.component'; export * from './select-item/select-item.component'; -export * from './item-copy/item-copy.component'; +export { ItemCopyComponent } from './item-copy/item-copy.component'; export * from './item-handler.service'; diff --git a/libs/features/item/src/item-copy/item-copy.component.ts b/libs/features/item/src/item-copy/item-copy.component.ts index 11822d99ebb..ec31f2430d0 100644 --- a/libs/features/item/src/item-copy/item-copy.component.ts +++ b/libs/features/item/src/item-copy/item-copy.component.ts @@ -18,7 +18,6 @@ import { changeDetection: ChangeDetectionStrategy.OnPush, selector: 'keira-item-copy', templateUrl: './item-copy.component.html', - standalone: true, imports: [CopyOutputComponent], }) export class ItemCopyComponent implements OnInit { diff --git a/libs/features/item/src/item-handler.service.ts b/libs/features/item/src/item-handler.service.ts index eabb694e1bb..44250146cd6 100644 --- a/libs/features/item/src/item-handler.service.ts +++ b/libs/features/item/src/item-handler.service.ts @@ -15,7 +15,7 @@ import { }) export class ItemHandlerService extends HandlerService { protected readonly mainEditorRoutePath = 'item/item-template'; - protected readonly copyRoutePath = 'item/copy'; + protected override readonly copyRoutePath = 'item/copy'; get isItemTemplateUnsaved(): Signal { return this.statusMap[ITEM_TEMPLATE_TABLE].asReadonly(); diff --git a/libs/features/other-loots/src/fishing-loot/fishing-loot-copy.component.ts b/libs/features/other-loots/src/fishing-loot/fishing-loot-copy.component.ts index 3271abe813e..74515d8d181 100644 --- a/libs/features/other-loots/src/fishing-loot/fishing-loot-copy.component.ts +++ b/libs/features/other-loots/src/fishing-loot/fishing-loot-copy.component.ts @@ -8,7 +8,6 @@ import { FISHING_LOOT_TEMPLATE_TABLE, LOOT_TEMPLATE_ID } from '@keira/shared/aco changeDetection: ChangeDetectionStrategy.OnPush, selector: 'keira-fishing-loot-copy', templateUrl: './fishing-loot-copy.component.html', - standalone: true, imports: [CopyOutputComponent], }) export class FishingLootCopyComponent implements OnInit { diff --git a/libs/features/other-loots/src/fishing-loot/fishing-loot-handler.service.ts b/libs/features/other-loots/src/fishing-loot/fishing-loot-handler.service.ts index 6b78783a9cd..2d524ca9a17 100644 --- a/libs/features/other-loots/src/fishing-loot/fishing-loot-handler.service.ts +++ b/libs/features/other-loots/src/fishing-loot/fishing-loot-handler.service.ts @@ -7,7 +7,7 @@ import { FISHING_LOOT_TEMPLATE_TABLE, FishingLootTemplate } from '@keira/shared/ }) export class FishingLootHandlerService extends HandlerService { protected readonly mainEditorRoutePath = 'other-loots/fishing'; - protected readonly copyRoutePath = 'other-loots/fishing-copy'; + protected override readonly copyRoutePath = 'other-loots/fishing-copy'; get isUnsaved(): Signal { return this.statusMap[FISHING_LOOT_TEMPLATE_TABLE].asReadonly(); @@ -16,13 +16,4 @@ export class FishingLootHandlerService extends HandlerService, name?: string, navigate = true, sourceId?: string) { - if (isNew && sourceId) { - super.select(isNew, id, name, false, sourceId); - this.router.navigate([this.copyRoutePath]); - } else { - super.select(isNew, id, name, navigate); - } - } } diff --git a/libs/features/other-loots/src/mail-loot/mail-loot-copy.component.ts b/libs/features/other-loots/src/mail-loot/mail-loot-copy.component.ts index 5776800b896..d549fa2c718 100644 --- a/libs/features/other-loots/src/mail-loot/mail-loot-copy.component.ts +++ b/libs/features/other-loots/src/mail-loot/mail-loot-copy.component.ts @@ -8,7 +8,6 @@ import { MAIL_LOOT_TEMPLATE_TABLE, LOOT_TEMPLATE_ID } from '@keira/shared/acore- changeDetection: ChangeDetectionStrategy.OnPush, selector: 'keira-mail-loot-copy', templateUrl: './mail-loot-copy.component.html', - standalone: true, imports: [CopyOutputComponent], }) export class MailLootCopyComponent implements OnInit { diff --git a/libs/features/other-loots/src/mail-loot/mail-loot-handler.service.ts b/libs/features/other-loots/src/mail-loot/mail-loot-handler.service.ts index c8d5382be67..53d27f9ce70 100644 --- a/libs/features/other-loots/src/mail-loot/mail-loot-handler.service.ts +++ b/libs/features/other-loots/src/mail-loot/mail-loot-handler.service.ts @@ -7,7 +7,7 @@ import { MAIL_LOOT_TEMPLATE_TABLE, MailLootTemplate } from '@keira/shared/acore- }) export class MailLootHandlerService extends HandlerService { protected readonly mainEditorRoutePath = 'other-loots/mail'; - protected readonly copyRoutePath = 'other-loots/mail-copy'; + protected override readonly copyRoutePath = 'other-loots/mail-copy'; get isUnsaved(): Signal { return this.statusMap[MAIL_LOOT_TEMPLATE_TABLE].asReadonly(); @@ -16,13 +16,4 @@ export class MailLootHandlerService extends HandlerService { protected _statusMap = { [MAIL_LOOT_TEMPLATE_TABLE]: signal(false), }; - - override select(isNew: boolean, id: string | number | Partial, name?: string, navigate = true, sourceId?: string) { - if (isNew && sourceId) { - super.select(isNew, id, name, false, sourceId); - this.router.navigate([this.copyRoutePath]); - } else { - super.select(isNew, id, name, navigate); - } - } } diff --git a/libs/features/other-loots/src/reference-loot/reference-loot-copy.component.ts b/libs/features/other-loots/src/reference-loot/reference-loot-copy.component.ts index 6fb1a861bd6..0c7cf133409 100644 --- a/libs/features/other-loots/src/reference-loot/reference-loot-copy.component.ts +++ b/libs/features/other-loots/src/reference-loot/reference-loot-copy.component.ts @@ -8,7 +8,6 @@ import { REFERENCE_LOOT_TEMPLATE_TABLE, LOOT_TEMPLATE_ID } from '@keira/shared/a changeDetection: ChangeDetectionStrategy.OnPush, selector: 'keira-reference-loot-copy', templateUrl: './reference-loot-copy.component.html', - standalone: true, imports: [CopyOutputComponent], }) export class ReferenceLootCopyComponent implements OnInit { diff --git a/libs/features/other-loots/src/reference-loot/reference-loot-handler.service.ts b/libs/features/other-loots/src/reference-loot/reference-loot-handler.service.ts index 9ae62fce7b7..0b64d1f9de7 100644 --- a/libs/features/other-loots/src/reference-loot/reference-loot-handler.service.ts +++ b/libs/features/other-loots/src/reference-loot/reference-loot-handler.service.ts @@ -7,7 +7,7 @@ import { REFERENCE_LOOT_TEMPLATE_TABLE, ReferenceLootTemplate } from '@keira/sha }) export class ReferenceLootHandlerService extends HandlerService { protected readonly mainEditorRoutePath = 'other-loots/reference'; - protected readonly copyRoutePath = 'other-loots/reference-copy'; + protected override readonly copyRoutePath = 'other-loots/reference-copy'; get isUnsaved(): Signal { return this.statusMap[REFERENCE_LOOT_TEMPLATE_TABLE].asReadonly(); @@ -16,13 +16,4 @@ export class ReferenceLootHandlerService extends HandlerService, name?: string, navigate = true, sourceId?: string) { - if (isNew && sourceId) { - super.select(isNew, id, name, false, sourceId); - this.router.navigate([this.copyRoutePath]); - } else { - super.select(isNew, id, name, navigate); - } - } } diff --git a/libs/features/other-loots/src/shared/copy.component.html b/libs/features/other-loots/src/shared/copy.component.html new file mode 100644 index 00000000000..eef15371ccb --- /dev/null +++ b/libs/features/other-loots/src/shared/copy.component.html @@ -0,0 +1,10 @@ +@if (sourceId && newId) { + +} diff --git a/libs/features/other-loots/src/spell-loot/spell-loot-copy.component.ts b/libs/features/other-loots/src/spell-loot/spell-loot-copy.component.ts index 8e4767bd426..b22ca0c7322 100644 --- a/libs/features/other-loots/src/spell-loot/spell-loot-copy.component.ts +++ b/libs/features/other-loots/src/spell-loot/spell-loot-copy.component.ts @@ -8,7 +8,6 @@ import { SPELL_LOOT_TEMPLATE_TABLE, LOOT_TEMPLATE_ID } from '@keira/shared/acore changeDetection: ChangeDetectionStrategy.OnPush, selector: 'keira-spell-loot-copy', templateUrl: './spell-loot-copy.component.html', - standalone: true, imports: [CopyOutputComponent], }) export class SpellLootCopyComponent implements OnInit { diff --git a/libs/features/other-loots/src/spell-loot/spell-loot-handler.service.ts b/libs/features/other-loots/src/spell-loot/spell-loot-handler.service.ts index b3b30df9cdd..589be9d08e1 100644 --- a/libs/features/other-loots/src/spell-loot/spell-loot-handler.service.ts +++ b/libs/features/other-loots/src/spell-loot/spell-loot-handler.service.ts @@ -7,16 +7,7 @@ import { SPELL_LOOT_TEMPLATE_TABLE, SpellLootTemplate } from '@keira/shared/acor }) export class SpellLootHandlerService extends HandlerService { protected readonly mainEditorRoutePath = 'other-loots/spell'; - protected readonly copyRoutePath = 'other-loots/spell-copy'; - - override select(isNew: boolean, id: string | number | Partial, name?: string, navigate = true, sourceId?: string) { - if (isNew && sourceId) { - super.select(isNew, id, name, false, sourceId); - this.router.navigate([this.copyRoutePath]); - } else { - super.select(isNew, id, name, navigate); - } - } + protected override readonly copyRoutePath = 'other-loots/spell-copy'; get isUnsaved(): Signal { return this.statusMap[SPELL_LOOT_TEMPLATE_TABLE].asReadonly(); diff --git a/libs/features/quest/src/index.ts b/libs/features/quest/src/index.ts index 12e823da68c..103239ff974 100644 --- a/libs/features/quest/src/index.ts +++ b/libs/features/quest/src/index.ts @@ -10,4 +10,4 @@ export * from './quest-template-addon/quest-template-addon.component'; export * from './select-quest/select-quest.component'; export * from './quest-handler.service'; export * from './quest-template-locale/quest-template-locale.component'; -export * from './quest-copy/quest-copy.component'; +export { QuestCopyComponent } from './quest-copy/quest-copy.component'; diff --git a/libs/features/quest/src/quest-copy/quest-copy.component.ts b/libs/features/quest/src/quest-copy/quest-copy.component.ts index a20c1fcb2f4..1fa6df9d41f 100644 --- a/libs/features/quest/src/quest-copy/quest-copy.component.ts +++ b/libs/features/quest/src/quest-copy/quest-copy.component.ts @@ -27,7 +27,7 @@ import { changeDetection: ChangeDetectionStrategy.OnPush, selector: 'keira-quest-copy', templateUrl: './quest-copy.component.html', - standalone: true, + imports: [CopyOutputComponent], }) export class QuestCopyComponent implements OnInit { diff --git a/libs/features/quest/src/quest-handler.service.ts b/libs/features/quest/src/quest-handler.service.ts index 7665a9c17bc..cd67627083c 100644 --- a/libs/features/quest/src/quest-handler.service.ts +++ b/libs/features/quest/src/quest-handler.service.ts @@ -18,7 +18,7 @@ import { }) export class QuestHandlerService extends HandlerService { protected readonly mainEditorRoutePath = 'quest/quest-template'; - protected readonly copyRoutePath = 'quest/copy'; + protected override readonly copyRoutePath = 'quest/copy'; get isQuestTemplateUnsaved(): Signal { return this.statusMap[QUEST_TEMPLATE_TABLE].asReadonly(); diff --git a/libs/features/spell/src/index.ts b/libs/features/spell/src/index.ts index 884498f6e27..dd5eff9bdc2 100644 --- a/libs/features/spell/src/index.ts +++ b/libs/features/spell/src/index.ts @@ -1,4 +1,4 @@ export * from './select-spell/select-spell.component'; export * from './spell-dbc/spell-dbc.component'; export * from './spell-handler.service'; -export * from './spell-copy/spell-copy.component'; +export { SpellCopyComponent } from './spell-copy/spell-copy.component'; diff --git a/libs/features/spell/src/spell-copy/spell-copy.component.ts b/libs/features/spell/src/spell-copy/spell-copy.component.ts index b1d34844351..e9748732809 100644 --- a/libs/features/spell/src/spell-copy/spell-copy.component.ts +++ b/libs/features/spell/src/spell-copy/spell-copy.component.ts @@ -8,7 +8,7 @@ import { SPELL_DBC_TABLE, SPELL_DBC_ID, SPELL_LOOT_TEMPLATE_TABLE, LOOT_TEMPLATE changeDetection: ChangeDetectionStrategy.OnPush, selector: 'keira-spell-copy', templateUrl: './spell-copy.component.html', - standalone: true, + imports: [CopyOutputComponent], }) export class SpellCopyComponent implements OnInit { diff --git a/libs/features/spell/src/spell-handler.service.ts b/libs/features/spell/src/spell-handler.service.ts index 2478720acf3..a9128f3fe13 100644 --- a/libs/features/spell/src/spell-handler.service.ts +++ b/libs/features/spell/src/spell-handler.service.ts @@ -7,7 +7,7 @@ import { SPELL_DBC_TABLE, SpellDbc } from '@keira/shared/acore-world-model'; }) export class SpellHandlerService extends HandlerService { protected readonly mainEditorRoutePath = 'spell/spell-dbc'; - protected readonly copyRoutePath = 'spell/copy'; + protected override readonly copyRoutePath = 'spell/copy'; get isSpellDbcUnsaved(): Signal { return this.statusMap[SPELL_DBC_TABLE].asReadonly(); diff --git a/libs/features/texts/src/acore-string/acore-string-copy.component.ts b/libs/features/texts/src/acore-string/acore-string-copy.component.ts index 4cee0d9b117..629fe505068 100644 --- a/libs/features/texts/src/acore-string/acore-string-copy.component.ts +++ b/libs/features/texts/src/acore-string/acore-string-copy.component.ts @@ -8,7 +8,7 @@ import { ACORE_STRING_TABLE, ACORE_STRING_ENTRY } from '@keira/shared/acore-worl changeDetection: ChangeDetectionStrategy.OnPush, selector: 'keira-acore-string-copy', templateUrl: './acore-string-copy.component.html', - standalone: true, + imports: [CopyOutputComponent], }) export class AcoreStringCopyComponent implements OnInit { diff --git a/libs/features/texts/src/acore-string/acore-string-handler.service.ts b/libs/features/texts/src/acore-string/acore-string-handler.service.ts index 7cd8909d706..cd2ca010377 100644 --- a/libs/features/texts/src/acore-string/acore-string-handler.service.ts +++ b/libs/features/texts/src/acore-string/acore-string-handler.service.ts @@ -7,7 +7,7 @@ import { ACORE_STRING_TABLE, AcoreString } from '@keira/shared/acore-world-model }) export class AcoreStringHandlerService extends HandlerService { protected readonly mainEditorRoutePath = 'texts/acore-string'; - protected readonly copyRoutePath = 'texts/acore-string-copy'; + protected override readonly copyRoutePath = 'texts/acore-string-copy'; get isUnsaved(): Signal { return this.statusMap[ACORE_STRING_TABLE].asReadonly(); diff --git a/libs/features/texts/src/index.ts b/libs/features/texts/src/index.ts index 23f9353f4d2..e2d315f57f0 100644 --- a/libs/features/texts/src/index.ts +++ b/libs/features/texts/src/index.ts @@ -1,7 +1,7 @@ export { PageTextHandlerService } from './page-text/page-text-handler.service'; export { SelectPageTextComponent } from './page-text/select-page-text.component'; export { PageTextComponent } from './page-text/page-text.component'; -export * from './page-text/page-text-copy.component'; +export { PageTextCopyComponent } from './page-text/page-text-copy.component'; export { BroadcastTextHandlerService } from './broadcast-text/broadcast-text-handler.service'; export { SelectBroadcastTextComponent } from './broadcast-text/select-broadcast-text.component'; @@ -10,7 +10,7 @@ export { BroadcastTextComponent } from './broadcast-text/broadcast-text.componen export { NpcTextHandlerService } from './npc-text/npc-text-handler.service'; export { SelectNpcTextComponent } from './npc-text/select-npc-text.component'; export { NpcTextComponent } from './npc-text/npc-text.component'; -export * from './npc-text/npc-text-copy.component'; +export { NpcTextCopyComponent } from './npc-text/npc-text-copy.component'; export { AcoreStringHandlerService } from './acore-string/acore-string-handler.service'; export { SelectAcoreStringComponent } from './acore-string/select-acore-string.component'; diff --git a/libs/features/texts/src/npc-text/npc-text-copy.component.ts b/libs/features/texts/src/npc-text/npc-text-copy.component.ts index be53fd89867..25434e0169a 100644 --- a/libs/features/texts/src/npc-text/npc-text-copy.component.ts +++ b/libs/features/texts/src/npc-text/npc-text-copy.component.ts @@ -8,7 +8,7 @@ import { NPC_TEXT_TABLE, NPC_TEXT_ID } from '@keira/shared/acore-world-model'; changeDetection: ChangeDetectionStrategy.OnPush, selector: 'keira-npc-text-copy', templateUrl: './npc-text-copy.component.html', - standalone: true, + imports: [CopyOutputComponent], }) export class NpcTextCopyComponent implements OnInit { diff --git a/libs/features/texts/src/npc-text/npc-text-handler.service.ts b/libs/features/texts/src/npc-text/npc-text-handler.service.ts index 20cde79b8d8..7305565e94d 100644 --- a/libs/features/texts/src/npc-text/npc-text-handler.service.ts +++ b/libs/features/texts/src/npc-text/npc-text-handler.service.ts @@ -7,7 +7,7 @@ import { NpcText, NPC_TEXT_TABLE } from '@keira/shared/acore-world-model'; }) export class NpcTextHandlerService extends HandlerService { protected readonly mainEditorRoutePath = 'texts/npc-text'; - protected readonly copyRoutePath = 'texts/npc-text-copy'; + protected override readonly copyRoutePath = 'texts/npc-text-copy'; get isUnsaved(): Signal { return this.statusMap[NPC_TEXT_TABLE].asReadonly(); diff --git a/libs/features/texts/src/page-text/page-text-copy.component.ts b/libs/features/texts/src/page-text/page-text-copy.component.ts index 41206042eea..771b9d58fa7 100644 --- a/libs/features/texts/src/page-text/page-text-copy.component.ts +++ b/libs/features/texts/src/page-text/page-text-copy.component.ts @@ -8,7 +8,7 @@ import { PAGE_TEXT_TABLE, PAGE_TEXT_ID } from '@keira/shared/acore-world-model'; changeDetection: ChangeDetectionStrategy.OnPush, selector: 'keira-page-text-copy', templateUrl: './page-text-copy.component.html', - standalone: true, + imports: [CopyOutputComponent], }) export class PageTextCopyComponent implements OnInit { diff --git a/libs/features/texts/src/page-text/page-text-handler.service.ts b/libs/features/texts/src/page-text/page-text-handler.service.ts index aa492112e8a..8ba1c263885 100644 --- a/libs/features/texts/src/page-text/page-text-handler.service.ts +++ b/libs/features/texts/src/page-text/page-text-handler.service.ts @@ -7,7 +7,7 @@ import { PageText, PAGE_TEXT_TABLE } from '@keira/shared/acore-world-model'; }) export class PageTextHandlerService extends HandlerService { protected readonly mainEditorRoutePath = 'texts/page-text'; - protected readonly copyRoutePath = 'texts/page-text-copy'; + protected override readonly copyRoutePath = 'texts/page-text-copy'; get isUnsaved(): Signal { return this.statusMap[PAGE_TEXT_TABLE].asReadonly(); diff --git a/libs/features/trainer/src/index.ts b/libs/features/trainer/src/index.ts index a66553ee9f7..e41439e1f19 100644 --- a/libs/features/trainer/src/index.ts +++ b/libs/features/trainer/src/index.ts @@ -5,4 +5,4 @@ export * from './edit-trainer/trainer.component'; export * from './edit-trainer/trainer.service'; export * from './trainer-spell/trainer-spell.component'; export * from './trainer-spell/trainer-spell.service'; -export * from './trainer-copy/trainer-copy.component'; +export { TrainerCopyComponent } from './trainer-copy/trainer-copy.component'; diff --git a/libs/features/trainer/src/trainer-copy/trainer-copy.component.ts b/libs/features/trainer/src/trainer-copy/trainer-copy.component.ts index 54ccc77aff2..352b4b9bf31 100644 --- a/libs/features/trainer/src/trainer-copy/trainer-copy.component.ts +++ b/libs/features/trainer/src/trainer-copy/trainer-copy.component.ts @@ -8,7 +8,7 @@ import { TRAINER_ID, TRAINER_SPELL_ID, TRAINER_SPELL_TABLE, TRAINER_TABLE } from changeDetection: ChangeDetectionStrategy.OnPush, selector: 'keira-trainer-copy', templateUrl: './trainer-copy.component.html', - standalone: true, + imports: [CopyOutputComponent], }) export class TrainerCopyComponent implements OnInit { diff --git a/libs/features/trainer/src/trainer-handler.service.ts b/libs/features/trainer/src/trainer-handler.service.ts index de657b51d6c..01540879a2a 100644 --- a/libs/features/trainer/src/trainer-handler.service.ts +++ b/libs/features/trainer/src/trainer-handler.service.ts @@ -7,7 +7,7 @@ import { TRAINER_TABLE, TRAINER_SPELL_TABLE, Trainer } from '@keira/shared/acore }) export class TrainerHandlerService extends HandlerService { protected readonly mainEditorRoutePath = 'trainer/trainer'; - protected readonly copyRoutePath = 'trainer/copy'; + protected override readonly copyRoutePath = 'trainer/copy'; get isTrainerUnsaved(): Signal { return this.statusMap[TRAINER_TABLE].asReadonly(); diff --git a/libs/shared/base-abstract-classes/src/service/handlers/handler.service.ts b/libs/shared/base-abstract-classes/src/service/handlers/handler.service.ts index 6c96d6c6684..bd15e2b8592 100644 --- a/libs/shared/base-abstract-classes/src/service/handlers/handler.service.ts +++ b/libs/shared/base-abstract-classes/src/service/handlers/handler.service.ts @@ -16,6 +16,7 @@ export abstract class HandlerService extends SubscriptionHan protected abstract _statusMap: { [key: string]: WritableSignal }; protected abstract readonly mainEditorRoutePath: string; + protected readonly copyRoutePath?: string; /* istanbul ignore next */ // TODO: fix coverage get statusMap(): { [key: string]: WritableSignal } { @@ -72,7 +73,11 @@ export abstract class HandlerService extends SubscriptionHan this.selectedName = name as string; if (navigate) { - this.router.navigate([this.mainEditorRoutePath]); + if (this.copyRoutePath && isNew && sourceId) { + this.router.navigate([this.copyRoutePath]); + } else { + this.router.navigate([this.mainEditorRoutePath]); + } } } diff --git a/libs/shared/base-editor-components/src/copy-output/copy-output.component.ts b/libs/shared/base-editor-components/src/copy-output/copy-output.component.ts index 58ac75d8f65..f81cee1cbf0 100644 --- a/libs/shared/base-editor-components/src/copy-output/copy-output.component.ts +++ b/libs/shared/base-editor-components/src/copy-output/copy-output.component.ts @@ -1,7 +1,6 @@ -import { ChangeDetectionStrategy, Component, inject, Input, OnInit, signal } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { TranslateModule } from '@ngx-translate/core'; -import { TooltipModule } from 'ngx-bootstrap/tooltip'; +import { ChangeDetectionStrategy, Component, inject, OnInit, signal, input } from '@angular/core'; +import { TranslatePipe } from '@ngx-translate/core'; +import { TooltipDirective } from 'ngx-bootstrap/tooltip'; import { ClipboardService } from 'ngx-clipboard'; import { HandlerService } from '@keira/shared/base-abstract-classes'; import { TableRow } from '@keira/shared/constants'; @@ -11,54 +10,64 @@ import { QueryError } from 'mysql2'; import { QueryErrorComponent } from '../query-output/query-error/query-error.component'; import { HighlightjsWrapperComponent } from '../highlightjs-wrapper/highlightjs-wrapper.component'; +type CopyMode = 'RAW' | 'ALL'; + +interface RelatedTable { + tableName: string; + idField: string; + copyMode?: CopyMode; + columns?: string[]; +} + +interface RelatedTableState { + tableName: string; + idField: string; + count: number; + included: boolean; + copyMode: CopyMode; + columns?: string[]; +} + @Component({ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'keira-copy-output', templateUrl: './copy-output.component.html', styleUrls: ['./copy-output.component.scss'], - imports: [CommonModule, TranslateModule, TooltipModule, QueryErrorComponent, HighlightjsWrapperComponent], + imports: [TooltipDirective, QueryErrorComponent, HighlightjsWrapperComponent, TranslatePipe], }) export class CopyOutputComponent extends SubscriptionHandler implements OnInit { protected readonly clipboardService = inject(ClipboardService); protected readonly queryService = inject(MysqlQueryService); - @Input({ required: true }) handlerService!: HandlerService; - @Input({ required: true }) tableName!: string; - @Input({ required: true }) idField!: string; - @Input({ required: true }) sourceId!: string | number; - @Input({ required: true }) newId!: string | number; - @Input() relatedTables?: Array<{ - tableName: string; - idField: string; - copyMode?: 'RAW' | 'ALL'; // RAW = explicit columns, ALL = temp table - columns?: string[]; - }>; - - @Input() mainCopyMode?: 'RAW' | 'ALL'; - @Input() mainColumns?: string[]; - - protected mainCopyModeSignal = signal<'RAW' | 'ALL'>('RAW'); + readonly handlerService = input | undefined>(); + readonly tableName = input.required(); + readonly idField = input.required(); + readonly sourceId = input.required(); + readonly newId = input.required(); + readonly relatedTables = input(); + readonly mainCopyMode = input(); + readonly mainColumns = input(); + + protected mainCopyModeSignal = signal('RAW'); protected mainColumnsSignal = signal(undefined); protected copyQuery = signal(''); - protected relatedTableStates = signal< - Array<{ tableName: string; idField: string; count: number; included: boolean; copyMode: 'RAW' | 'ALL'; columns?: string[] }> - >([]); + protected relatedTableStates = signal([]); protected error = signal(undefined); protected executing = signal(false); protected executed = signal(false); protected sqlExpanded = signal(false); ngOnInit(): void { - this.mainCopyModeSignal.set(this.mainCopyMode || 'RAW'); - this.mainColumnsSignal.set(this.mainColumns); + this.mainCopyModeSignal.set(this.mainCopyMode() || 'RAW'); + this.mainColumnsSignal.set(this.mainColumns()); this.populateRelatedTables(); } - protected setCopyModeForTable(tableName: string, mode: string): void { - const normalized: 'RAW' | 'ALL' = mode === 'RAW' ? 'RAW' : 'ALL'; + protected setCopyModeForTable(tableName: string, mode: CopyMode): void { + const normalized = mode === 'RAW' ? 'RAW' : 'ALL'; - if (tableName === this.tableName) { + if (tableName === this.tableName()) { this.mainCopyModeSignal.set(normalized); const states = this.relatedTableStates().map((s) => ({ @@ -97,33 +106,31 @@ export class CopyOutputComponent extends SubscriptionHandler } protected populateRelatedTables(): void { - const inputTables: Array<{ tableName: string; idField: string; copyMode?: 'RAW' | 'ALL'; columns?: string[] }> = []; + const inputTables: RelatedTable[] = []; // Include main table first (default to RAW) - inputTables.push({ tableName: this.tableName, idField: this.idField, copyMode: this.mainCopyMode || 'RAW', columns: this.mainColumns }); - - if (this.relatedTables && this.relatedTables.length > 0) { - for (const t of this.relatedTables) { + inputTables.push({ + tableName: this.tableName(), + idField: this.idField(), + copyMode: this.mainCopyMode() || 'RAW', + columns: this.mainColumns(), + }); + + if (this.relatedTables() && this.relatedTables()!.length > 0) { + for (const t of this.relatedTables()!) { inputTables.push({ tableName: t.tableName, idField: t.idField, copyMode: t.copyMode || 'RAW', columns: t.columns }); } } - const states: Array<{ - tableName: string; - idField: string; - count: number; - included: boolean; - copyMode: 'RAW' | 'ALL'; - columns?: string[]; - }> = []; + const states: RelatedTableState[] = []; let remaining = inputTables.length; for (const table of inputTables) { this.subscriptions.push( - this.queryService.getRowsCount(table.tableName, table.idField, this.sourceId).subscribe((count: number | null) => { + this.queryService.getRowsCount(table.tableName, table.idField, this.sourceId()).subscribe((count: number | null) => { const num = Number(count || 0); - const isMain = table.tableName === this.tableName; + const isMain = table.tableName === this.tableName(); if (isMain || num > 0) { states.push({ @@ -147,13 +154,13 @@ export class CopyOutputComponent extends SubscriptionHandler } protected generateCopyQuery(): void { - const setVars = this.queryService.getCopyVarsSet(this.sourceId, this.newId); + const setVars = this.queryService.getCopyVarsSet(this.sourceId(), this.newId()); const selectedTables = this.relatedTableStates().filter((t) => t.included); // If no tables selected, just set main table if (selectedTables.length === 0) { - const query = setVars + this.queryService.getCopyQuery(this.tableName, this.sourceId, this.newId, this.idField, true); + const query = setVars + this.queryService.getCopyQuery(this.tableName(), this.sourceId(), this.newId(), this.idField(), true); this.copyQuery.set(query); return; } @@ -164,7 +171,7 @@ export class CopyOutputComponent extends SubscriptionHandler if (!hasAnyRaw) { let query = setVars; for (const table of selectedTables) { - query += '\n' + this.queryService.getCopyQuery(table.tableName, this.sourceId, this.newId, table.idField, true); + query += '\n' + this.queryService.getCopyQuery(table.tableName, this.sourceId(), this.newId(), table.idField, true); } this.copyQuery.set(query); return; @@ -178,9 +185,9 @@ export class CopyOutputComponent extends SubscriptionHandler for (const table of rawTables) { this.subscriptions.push( - this.queryService.selectAll(table.tableName, table.idField, this.sourceId).subscribe((rows) => { + this.queryService.selectAll(table.tableName, table.idField, this.sourceId()).subscribe((rows) => { const cols = table.columns && table.columns.length > 0 ? table.columns : rows[0] ? Object.keys(rows[0]) : []; - const rawQuery = this.queryService.getCopyQueryRawWithValues(table.tableName, rows, this.newId, table.idField, cols, true); + const rawQuery = this.queryService.getCopyQueryRawWithValues(table.tableName, rows, this.newId(), table.idField, cols, true); tableQueries.set(table.tableName, rawQuery); remainingRaw--; @@ -190,7 +197,7 @@ export class CopyOutputComponent extends SubscriptionHandler if (t.copyMode === 'RAW') { finalQuery += '\n' + tableQueries.get(t.tableName); } else { - finalQuery += '\n' + this.queryService.getCopyQuery(t.tableName, this.sourceId, this.newId, t.idField, true); + finalQuery += '\n' + this.queryService.getCopyQuery(t.tableName, this.sourceId(), this.newId(), t.idField, true); } } this.copyQuery.set(finalQuery); @@ -307,6 +314,6 @@ export class CopyOutputComponent extends SubscriptionHandler protected continue(): void { // Navigate to the editor for the newly created entry - this.handlerService.select(false, this.newId); + this.handlerService()!.select(false, this.newId()); } } diff --git a/libs/shared/base-editor-components/src/create/create.component.html b/libs/shared/base-editor-components/src/create/create.component.html index 90bc2bc3d36..0bfaf2e6a56 100644 --- a/libs/shared/base-editor-components/src/create/create.component.html +++ b/libs/shared/base-editor-components/src/create/create.component.html @@ -28,7 +28,7 @@ [(ngModel)]="creationMethod" (change)="onCreationMethodChange()" [disabled]="!allowCopy" - [title]="allowCopy ? '' : 'Copy from existing is not available for this entity'" + [title]="allowCopy ? '' : ('CREATE.COPY_DISABLED' | translate)" />