diff --git a/apps/keira/src/app/routes.ts b/apps/keira/src/app/routes.ts index 001fa54f7c..f23d062c6d 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 f9858c443c..da09dd90ee 100644 --- a/apps/keira/src/assets/i18n/en.json +++ b/apps/keira/src/assets/i18n/en.json @@ -14,7 +14,13 @@ "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", + "COPY_DISABLED": "Copy from existing is not available for this entity" }, "CONNECTED": "Connected", "SEARCH": "Search", @@ -205,6 +211,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 0324d31101..cc757c2d98 100644 --- a/apps/keira/src/assets/i18n/es.json +++ b/apps/keira/src/assets/i18n/es.json @@ -14,7 +14,13 @@ "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", + "COPY_DISABLED": "Copy from existing is not available for this entity" }, "CONNECTED": "Conectado", "SEARCH": "Buscar", @@ -205,6 +211,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 267c94b637..c6a54147a3 100644 --- a/apps/keira/src/assets/i18n/ru.json +++ b/apps/keira/src/assets/i18n/ru.json @@ -14,7 +14,13 @@ "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", + "COPY_DISABLED": "Copy from existing is not available for this entity" }, "CONNECTED": "Подключено", "SEARCH": "Поиск", @@ -205,6 +211,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/apps/keira/src/assets/i18n/zh.json b/apps/keira/src/assets/i18n/zh.json index 96a30281ce..3452832c92 100644 --- a/apps/keira/src/assets/i18n/zh.json +++ b/apps/keira/src/assets/i18n/zh.json @@ -14,7 +14,13 @@ "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", + "COPY_DISABLED": "Copy from existing is not available for this entity" }, "CONNECTED": "连接", "SEARCH": "搜索", @@ -205,6 +211,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 0000000000..eef15371cc --- /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 0000000000..03c7e1c9f1 --- /dev/null +++ b/libs/features/creature/src/creature-copy/creature-copy.component.ts @@ -0,0 +1,76 @@ +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', + 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 69bb0332cf..527ee7b87e 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 override readonly copyRoutePath = 'creature/copy'; get isCreatureTemplateUnsaved(): Signal { return this.statusMap[CREATURE_TEMPLATE_TABLE].asReadonly(); @@ -110,8 +111,9 @@ 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); + + super.select(isNew, id, name, navigate, sourceId); } } diff --git a/libs/features/creature/src/index.ts b/libs/features/creature/src/index.ts index 3a80fe56ee..6dc92a8784 100644 --- a/libs/features/creature/src/index.ts +++ b/libs/features/creature/src/index.ts @@ -1,3 +1,4 @@ +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/creature/src/select-creature/select-creature.component.html b/libs/features/creature/src/select-creature/select-creature.component.html index 1449b3b80b..48ef426805 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 0000000000..eef15371cc --- /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 0000000000..5b85b29bbd --- /dev/null +++ b/libs/features/game-tele/src/game-tele-copy/game-tele-copy.component.ts @@ -0,0 +1,33 @@ +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', + 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 b7e90183ee..2b82ccd934 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 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 0cc6f603f1..9d918b137b 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 { GameTeleCopyComponent } 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 edd0687d15..47069fe508 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 0000000000..eef15371cc --- /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 0000000000..5d76148bb1 --- /dev/null +++ b/libs/features/gameobject/src/gameobject-copy/gameobject-copy.component.ts @@ -0,0 +1,46 @@ +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', + 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 6fdd8d8f16..c50a6f693b 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 override readonly copyRoutePath = 'gameobject/copy'; get isGameobjectTemplateUnsaved(): Signal { return this.statusMap[GAMEOBJECT_TEMPLATE_TABLE].asReadonly(); @@ -50,8 +51,9 @@ 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); + + super.select(isNew, id, name, navigate, sourceId); } } diff --git a/libs/features/gameobject/src/index.ts b/libs/features/gameobject/src/index.ts index f8813f195d..19a373e584 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 { GameobjectCopyComponent } 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 13c4991a19..f17e5f13d7 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 0000000000..eef15371cc --- /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 0000000000..a579cb5e20 --- /dev/null +++ b/libs/features/gossip/src/gossip-copy/gossip-copy.component.ts @@ -0,0 +1,33 @@ +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', + 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 c225bdb640..11bf7075d5 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 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 7c25c6209d..6eedf776b3 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 { GossipCopyComponent } 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 06ee06fc78..e53438cdfe 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 46d0443cd5..c1fc9a4036 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 { ItemCopyComponent } 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 0000000000..eef15371cc --- /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 0000000000..ec31f2430d --- /dev/null +++ b/libs/features/item/src/item-copy/item-copy.component.ts @@ -0,0 +1,49 @@ +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', + 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 6feda73731..8d60c60ae1 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 override readonly copyRoutePath = 'item/copy'; get isItemTemplateUnsaved(): Signal { return this.statusMap[ITEM_TEMPLATE_TABLE].asReadonly(); 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 893cd5c9e2..3a6e69ba41 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 0000000000..eef15371cc --- /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 0000000000..74515d8d18 --- /dev/null +++ b/libs/features/other-loots/src/fishing-loot/fishing-loot-copy.component.ts @@ -0,0 +1,33 @@ +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', + 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 d63b31a595..2d524ca9a1 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 override readonly copyRoutePath = 'other-loots/fishing-copy'; get isUnsaved(): Signal { return this.statusMap[FISHING_LOOT_TEMPLATE_TABLE].asReadonly(); diff --git a/libs/features/other-loots/src/index.ts b/libs/features/other-loots/src/index.ts index 3db633e5f8..e3339bb94b 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 0000000000..eef15371cc --- /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 0000000000..d549fa2c71 --- /dev/null +++ b/libs/features/other-loots/src/mail-loot/mail-loot-copy.component.ts @@ -0,0 +1,33 @@ +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', + 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 1d0203995a..53d27f9ce7 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 override readonly copyRoutePath = 'other-loots/mail-copy'; get isUnsaved(): Signal { return this.statusMap[MAIL_LOOT_TEMPLATE_TABLE].asReadonly(); 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 0000000000..eef15371cc --- /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 0000000000..0c7cf13340 --- /dev/null +++ b/libs/features/other-loots/src/reference-loot/reference-loot-copy.component.ts @@ -0,0 +1,33 @@ +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', + 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 a3c5425199..db05dc833b 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 0bb50fbfb5..0b64d1f9de 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 override readonly copyRoutePath = 'other-loots/reference-copy'; get isUnsaved(): Signal { return this.statusMap[REFERENCE_LOOT_TEMPLATE_TABLE].asReadonly(); diff --git a/libs/features/other-loots/src/select-loot.component.html b/libs/features/other-loots/src/select-loot.component.html index 0bd0f07ab9..f7746993a6 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/shared/copy.component.html b/libs/features/other-loots/src/shared/copy.component.html new file mode 100644 index 0000000000..eef15371cc --- /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.html b/libs/features/other-loots/src/spell-loot/spell-loot-copy.component.html new file mode 100644 index 0000000000..eef15371cc --- /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 0000000000..b22ca0c732 --- /dev/null +++ b/libs/features/other-loots/src/spell-loot/spell-loot-copy.component.ts @@ -0,0 +1,33 @@ +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', + 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 ff161255b0..14b182b430 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 276629b1b1..589be9d08e 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,7 @@ import { SPELL_LOOT_TEMPLATE_TABLE, SpellLootTemplate } from '@keira/shared/acor }) export class SpellLootHandlerService extends HandlerService { protected readonly mainEditorRoutePath = 'other-loots/spell'; + 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 9a7c36713f..103239ff97 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 { QuestCopyComponent } 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 0000000000..eef15371cc --- /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 0000000000..1fa6df9d41 --- /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', + + 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 26bfb8e959..3ec9d86a46 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 override readonly copyRoutePath = 'quest/copy'; get isQuestTemplateUnsaved(): Signal { return this.statusMap[QUEST_TEMPLATE_TABLE].asReadonly(); 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 93c2bc6576..91a4196f70 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 c909726d4a..dd5eff9bdc 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 { SpellCopyComponent } 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 8002967079..7e40812080 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 0000000000..eef15371cc --- /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 0000000000..e974873280 --- /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', + + 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 384e006401..135ae1bb2e 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 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.html b/libs/features/texts/src/acore-string/acore-string-copy.component.html new file mode 100644 index 0000000000..eef15371cc --- /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 0000000000..629fe50506 --- /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', + + 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 6112bd75fc..f835352a85 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 override readonly copyRoutePath = 'texts/acore-string-copy'; get isUnsaved(): Signal { return this.statusMap[ACORE_STRING_TABLE].asReadonly(); 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 d6bfd767bd..a4ae495dee 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 c4209b8c94..e2d315f57f 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 { 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'; @@ -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 { 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'; 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 0000000000..eef15371cc --- /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 0000000000..25434e0169 --- /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', + + 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 313b96aff4..c703e82e96 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 override readonly copyRoutePath = 'texts/npc-text-copy'; get isUnsaved(): Signal { return this.statusMap[NPC_TEXT_TABLE].asReadonly(); 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 55d59422ce..c4e3b5b98e 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 0000000000..eef15371cc --- /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 0000000000..771b9d58fa --- /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', + + 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 c96fc83b86..f00889bb98 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 override readonly copyRoutePath = 'texts/page-text-copy'; get isUnsaved(): Signal { return this.statusMap[PAGE_TEXT_TABLE].asReadonly(); 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 d17e50dbde..e5b854b80d 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 557168e9b8..e41439e1f1 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 { TrainerCopyComponent } 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 71bef3c253..1b976d1cdd 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 0000000000..eef15371cc --- /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 0000000000..352b4b9bf3 --- /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', + + 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 39e0f897c2..955f284a5f 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 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.spec.ts b/libs/shared/base-abstract-classes/src/service/handlers/handler.service.spec.ts index 589396602c..205416f630 100644 --- a/libs/shared/base-abstract-classes/src/service/handlers/handler.service.spec.ts +++ b/libs/shared/base-abstract-classes/src/service/handlers/handler.service.spec.ts @@ -9,7 +9,7 @@ describe('HandlerService', () => { beforeEach(() => TestBed.configureTestingModule({ imports: [RouterTestingModule], - providers: [provideZonelessChangeDetection(), provideNoopAnimations(), MockHandlerService], + providers: [provideZonelessChangeDetection(), provideNoopAnimations(), { provide: MockHandlerService, useClass: TestHandler }], }), ); @@ -43,6 +43,17 @@ describe('HandlerService', () => { expect(navigateSpy).toHaveBeenCalledWith(['mock/route']); }); + it('should navigate to copy route when creating from a copy', () => { + const navigateSpy = spyOn(TestBed.inject(Router), 'navigate'); + const id = 'copyId'; + const name = 'copyName'; + const isNew = true; + + service.select(isNew, id, name, true, 'source-123'); + + expect(navigateSpy).toHaveBeenCalledWith(['mock/copy']); + }); + it('should not throw error when _statusMap is undefined in resetStatus()', () => { const { service } = setup(); (service as any)._statusMap = undefined; 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 20ed999885..bd15e2b859 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,9 +12,11 @@ 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; + protected readonly copyRoutePath?: string; /* istanbul ignore next */ // TODO: fix coverage get statusMap(): { [key: string]: WritableSignal } { @@ -50,9 +52,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; @@ -70,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.html b/libs/shared/base-editor-components/src/copy-output/copy-output.component.html new file mode 100644 index 0000000000..bcd68c9fb0 --- /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 0000000000..5a4406457f --- /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 0000000000..62f6ddf53d --- /dev/null +++ b/libs/shared/base-editor-components/src/copy-output/copy-output.component.ts @@ -0,0 +1,237 @@ +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'; +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'; +import { CopyMode, RelatedTable, RelatedTableState } from './copy-output.model'; +import { CopyOutputService } from './copy-output.service'; + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'keira-copy-output', + templateUrl: './copy-output.component.html', + styleUrls: ['./copy-output.component.scss'], + imports: [TooltipDirective, QueryErrorComponent, HighlightjsWrapperComponent, TranslatePipe], +}) +export class CopyOutputComponent extends SubscriptionHandler implements OnInit { + protected readonly clipboardService = inject(ClipboardService); + protected readonly queryService = inject(MysqlQueryService); + protected readonly copyOutputService = inject(CopyOutputService); + + 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([]); + 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: CopyMode): void { + const normalized = 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: 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: t.tableName, idField: t.idField, copyMode: t.copyMode || 'RAW', columns: t.columns }); + } + } + + this.subscriptions.push( + this.copyOutputService.computeRelatedTableStates(inputTables, this.sourceId(), this.tableName()).subscribe((states) => { + this.relatedTableStates.set(states); + this.generateCopyQuery(); + }), + ); + } + + protected generateCopyQuery(): void { + this.subscriptions.push( + this.copyOutputService + .generateCopyQueryForStates(this.relatedTableStates(), this.sourceId(), this.newId(), this.tableName(), this.idField()) + .subscribe((query) => { + this.copyQuery.set(query); + }), + ); + } + + 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/copy-output/copy-output.model.ts b/libs/shared/base-editor-components/src/copy-output/copy-output.model.ts new file mode 100644 index 0000000000..60459a6320 --- /dev/null +++ b/libs/shared/base-editor-components/src/copy-output/copy-output.model.ts @@ -0,0 +1,17 @@ +export type CopyMode = 'RAW' | 'ALL'; + +export interface RelatedTable { + tableName: string; + idField: string; + copyMode?: CopyMode; + columns?: string[]; +} + +export interface RelatedTableState { + tableName: string; + idField: string; + count: number; + included: boolean; + copyMode: CopyMode; + columns?: string[]; +} diff --git a/libs/shared/base-editor-components/src/copy-output/copy-output.service.ts b/libs/shared/base-editor-components/src/copy-output/copy-output.service.ts new file mode 100644 index 0000000000..6920184760 --- /dev/null +++ b/libs/shared/base-editor-components/src/copy-output/copy-output.service.ts @@ -0,0 +1,93 @@ +import { Injectable, inject } from '@angular/core'; +import { MysqlQueryService } from '@keira/shared/db-layer'; +import { RelatedTable, RelatedTableState } from './copy-output.model'; +import { forkJoin, of } from 'rxjs'; +import { map } from 'rxjs/operators'; + +@Injectable({ providedIn: 'root' }) +export class CopyOutputService { + private readonly queryService = inject(MysqlQueryService); + + computeRelatedTableStates(inputTables: RelatedTable[], sourceId: string | number, mainTableName: string) { + if (!inputTables || inputTables.length === 0) { + return of([]); + } + + const observables = inputTables.map((table) => + this.queryService.getRowsCount(table.tableName, table.idField, sourceId).pipe( + map((count) => { + const num = Number(count || 0); + const isMain = table.tableName === mainTableName; + + if (isMain || num > 0) { + return { + tableName: table.tableName, + idField: table.idField, + count: num, + included: true, + copyMode: table.copyMode || 'RAW', + columns: table.columns, + } as RelatedTableState; + } + + return null as any; + }), + ), + ); + + return forkJoin(observables).pipe(map((results) => results.filter((r) => !!r) as RelatedTableState[])); + } + + generateCopyQueryForStates( + states: RelatedTableState[], + sourceId: string | number, + newId: string | number, + mainTableName: string, + mainIdField: string, + ) { + const setVars = this.queryService.getCopyVarsSet(sourceId, newId); + + const selectedTables = (states || []).filter((t) => t.included); + + if (selectedTables.length === 0) { + const q = setVars + this.queryService.getCopyQuery(mainTableName, sourceId, newId, mainIdField, true); + return of(q); + } + + 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, sourceId, newId, table.idField, true); + } + return of(query); + } + + const rawTables = selectedTables.filter((t) => t.copyMode === 'RAW'); + const rawObservables = rawTables.map((table) => + this.queryService.selectAll(table.tableName, table.idField, sourceId).pipe( + map((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, newId, table.idField, cols, true); + return { tableName: table.tableName, rawQuery }; + }), + ), + ); + + return forkJoin(rawObservables).pipe( + map((raws) => { + const tableQueries = new Map(raws.map((r) => [r.tableName, r.rawQuery])); + 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, sourceId, newId, t.idField, true); + } + } + return finalQuery; + }), + ); + } +} 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 e6879131d0..c61ea68915 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 27a3816e19..d4295f3296 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 @@ -3,12 +3,14 @@ import { provideZonelessChangeDetection } from '@angular/core'; import { provideNoopAnimations } from '@angular/platform-browser/animations'; import { FormsModule } from '@angular/forms'; import { BrowserModule } 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'; class CreateComponentPage extends PageObject> { @@ -21,8 +23,85 @@ class CreateComponentPage extends PageObject> { get idFreeStatusBox(): HTMLDivElement { return this.query('#id-free-status'); } + get sourceInput(): HTMLInputElement { + return this.getInputById('source-id'); + } + get copyInput(): HTMLInputElement { + return this.getInputById('method-copy'); + } + get copyRadio(): HTMLInputElement | null { + return this.query('#method-copy', false); + } } +describe('CreateComponent', () => { + function setup() { + const fakeQueryService: any = { + getMaxId: () => of([{ max: 5 }]), + selectAll: () => of([]), + }; + + const fakeHandlerService: any = { + select: jasmine.createSpy('select'), + }; + + const fixture = TestBed.createComponent(CreateComponent); + const component = fixture.componentInstance; + // Provide required inputs + component.entityTable = 'test'; + component.entityIdField = 'id'; + component.customStartingId = 1; + component.queryService = fakeQueryService; + component.handlerService = fakeHandlerService; + fixture.detectChanges(); + const page = new CreateComponentPage(fixture); + + return { fixture, component, page, fakeQueryService, fakeHandlerService }; + } + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [FormsModule, TranslateModule.forRoot(), CreateComponent], + providers: [provideZonelessChangeDetection(), provideNoopAnimations()], + }).compileComponents(); + }); + + it('does not render the copy option when allowCopy is false', () => { + // Default allowCopy is false + const { fixture, component, page } = setup(); + fixture.detectChanges(); + const copyRadio = page.copyRadio; + expect(copyRadio).toBeNull(); + expect(component.creationMethod()).toBe('blank'); + }); + + it('renders the copy option when allowCopy is true', () => { + const { fixture, component, page } = setup(); + component.allowCopy = true; + fixture.detectChanges(); + const copyInput = page.copyInput; + expect(copyInput).toBeDefined(); + // when allowed the copy radio should not be disabled + expect(copyInput.disabled).toBeFalse(); + }); + + it('resets creationMethod to blank when allowCopy is set to false', () => { + const { component } = setup(); + component.allowCopy = true; + component.creationMethod.set('copy'); + expect(component.creationMethod()).toBe('copy'); + component.sourceIdModel.set(123); + component.isSourceIdValid.set(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) + describe('CreateComponent', () => { const mockTable = 'mock_table'; const mockId = 'mockId'; @@ -83,7 +162,7 @@ describe('CreateComponent', () => { when(MockedMysqlQueryService.getMaxId(mockTable, mockId)).thenReturn(throwError('error')); when(MockedMysqlQueryService.selectAll(mockTable, mockId, anything())).thenReturn(throwError('error')); - component.checkId(); + (component as any).checkId(); component['getNextId'](); expect(spyError).toHaveBeenCalledTimes(2); @@ -115,11 +194,11 @@ describe('CreateComponent', () => { it('does not allow a higher value than max value', () => { const { component } = setup(); const unallowedIdValue = MAX_INT_UNSIGNED_VALUE + 1; - component.idModel = unallowedIdValue; + component.idModel.set(unallowedIdValue); component['checkMaxValue'](); - expect(component.idModel).toEqual(MAX_INT_UNSIGNED_VALUE); + expect(component.idModel()).toEqual(MAX_INT_UNSIGNED_VALUE); }); it('the customStartId should be preferred when greater than the currentMax', () => { @@ -127,4 +206,178 @@ describe('CreateComponent', () => { component.customStartingId = 10; expect(component['calculateNextId'](5)).toEqual(10); }); + + it('validates source id and calls select with copy params on create', async () => { + const { fixture, page, component } = setup(); + + // Enable copying and switch to copy method directly (radio may not always be present in this env) + component.allowCopy = true; + component.creationMethod.set('copy'); + fixture.detectChanges(); + await fixture.whenStable(); + + const sourceRadio = page.copyRadio; + // Guard: the radio is optional in this test runner, but the source input must be present for copy flow + if (sourceRadio) { + (sourceRadio as HTMLInputElement).checked = true; + sourceRadio.dispatchEvent(new Event('change')); + fixture.detectChanges(); + } + + // Provide an existing source id and trigger input (setup returns an existing item for `takenId`) + const sourceInput = page.sourceInput; + page.setInputValue(sourceInput, takenId); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(component.isSourceIdValid()).toBeTrue(); + + // Prepare a new id and create + component.idModel.set(2000); + component.isIdFree.set(true); + fixture.detectChanges(); + + const selectSpy = spyOn(component.handlerService, 'select'); + + page.clickElement(page.selectBtn); + + expect(selectSpy).toHaveBeenCalledTimes(1); + expect(selectSpy).toHaveBeenCalledWith(true, 2000, undefined, true, `${takenId}`); + }); + + it('does not allow a higher source value than max value', () => { + const { component } = setup(); + component.sourceIdModel.set(MAX_INT_UNSIGNED_VALUE + 1); + + (component as any).checkMaxValue(); + + expect(component.sourceIdModel()).toEqual(MAX_INT_UNSIGNED_VALUE); + }); + + it('onCreationMethodChange clears source state when switched to blank', () => { + const { component } = setup(); + component.creationMethod.set('copy'); + component.sourceIdModel.set(123); + component.isSourceIdValid.set(true); + + component.creationMethod.set('blank'); + (component as any).onCreationMethodChange(); + expect(component.sourceIdModel()).toBeUndefined(); + expect(component.isSourceIdValid()).toBeFalse(); + }); + + it('checkSourceId handles errors and marks source invalid', () => { + const { component, MockedMysqlQueryService } = setup(); + reset(MockedMysqlQueryService); + when(MockedMysqlQueryService.selectAll(mockTable, mockId, anything())).thenReturn(throwError('error')); + + component.sourceIdModel.set(999); + (component as any).checkSourceId(); + + expect(component.isSourceIdValid()).toBeFalse(); + expect(component.loading).toBe(false); + }); + + it('checkSourceId early-return when no sourceIdModel', () => { + const { component } = setup(); + component.sourceIdModel.set(undefined); + + (component as any).checkSourceId(); + + expect(component.isSourceIdValid()).toBeFalse(); + }); + + it('isFormValid correctly evaluates copy vs blank methods', () => { + const { component } = setup(); + + // blank creation method + component.creationMethod.set('blank'); + component.idModel.set(1); + component.isIdFree.set(true); + expect((component as any).isFormValid()).toBeTrue(); + + // copy method without source data should be invalid + component.creationMethod.set('copy'); + component.sourceIdModel.set(undefined); + component.isSourceIdValid.set(false); + expect((component as any).isFormValid()).toBeFalse(); + + // copy method with valid source should be valid + component.sourceIdModel.set(123); + component.isSourceIdValid.set(true); + expect((component as any).isFormValid()).toBeTrue(); + }); + + it('checkId early-returns when idModel is undefined', () => { + const { component } = setup(); + component.idModel.set(undefined); + + (component as any).checkId(); + + expect(component.isIdFree()).toBeFalse(); + expect(component.loading).toBe(false); + }); + + it('onCreate with blank method calls select without copy params', () => { + const { component } = setup(); + const selectSpy = spyOn(component.handlerService, 'select'); + component.creationMethod.set('blank'); + component.idModel.set(500); + + (component as any).onCreate(); + + expect(selectSpy).toHaveBeenCalledTimes(1); + expect(selectSpy).toHaveBeenCalledWith(true, 500); + }); + + it('onCreate with copy method calls select with copy params', () => { + const { component } = setup(); + const selectSpy = spyOn(component.handlerService, 'select'); + component.creationMethod.set('copy'); + component.idModel.set(600); + component.sourceIdModel.set(700); + + (component as any).onCreate(); + + expect(selectSpy).toHaveBeenCalledTimes(1); + expect(selectSpy).toHaveBeenCalledWith(true, 600, undefined, true, '700'); + }); + + it('isFormValid returns false when idModel is undefined', () => { + const { component } = setup(); + component.idModel.set(undefined); + component.isIdFree.set(true); + + expect((component as any).isFormValid()).toBeFalse(); + }); + + it('isFormValid returns false when isIdFree is false', () => { + const { component } = setup(); + component.idModel.set(100); + component.isIdFree.set(false); + + expect((component as any).isFormValid()).toBeFalse(); + }); + + it('isFormValid with copy method returns false when sourceIdModel is undefined', () => { + const { component } = setup(); + component.creationMethod.set('copy'); + component.idModel.set(100); + component.isIdFree.set(true); + component.sourceIdModel.set(undefined); + component.isSourceIdValid.set(true); + + expect((component as any).isFormValid()).toBeFalse(); + }); + + it('isFormValid with copy method returns false when isSourceIdValid is false', () => { + const { component } = setup(); + component.creationMethod.set('copy'); + component.idModel.set(100); + component.isIdFree.set(true); + component.sourceIdModel.set(200); + component.isSourceIdValid.set(false); + + expect((component as any).isFormValid()).toBeFalse(); + }); }); 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 d77e1e248d..20feac042d 100644 --- a/libs/shared/base-editor-components/src/create/create.component.ts +++ b/libs/shared/base-editor-components/src/create/create.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, Input, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, Component, Input, OnInit, signal } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { HandlerService } from '@keira/shared/base-abstract-classes'; import { TableRow } from '@keira/shared/constants'; @@ -21,67 +21,147 @@ 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.set('blank'); + this.sourceIdModel.set(undefined); + this.isSourceIdValid.set(false); + } + } + get allowCopy(): boolean { + return this._allowCopy; + } @Input() maxEntryValue = MAX_INT_UNSIGNED_VALUE; - private readonly changeDetectorRef = inject(ChangeDetectorRef); - - public idModel!: number; - private _loading = false; - isIdFree = false; + idModel = signal(undefined); + sourceIdModel = signal(undefined); + creationMethod = signal<'blank' | 'copy'>('blank'); + private _loading = signal(false); + isIdFree = signal(false); + isSourceIdValid = signal(false); get loading(): boolean { - return this._loading; + return this._loading(); } ngOnInit() { if (this.queryService) { this.getNextId(); } + + if (!this.allowCopy) { + this.creationMethod.set('blank'); + } } - checkId() { - this._loading = true; + protected checkId() { + this._loading.set(true); + const id = this.idModel(); + if (id === undefined) { + this.isIdFree.set(false); + this._loading.set(false); + return; + } + this.subscriptions.push( - this.queryService.selectAll(this.entityTable, this.entityIdField, this.idModel).subscribe({ + this.queryService.selectAll(this.entityTable, this.entityIdField, id).subscribe({ next: (data) => { - this.isIdFree = data.length <= 0; - this._loading = false; - this.changeDetectorRef.markForCheck(); + this.isIdFree.set(data.length <= 0); + this._loading.set(false); }, error: (error: QueryError) => { console.error(error); - this._loading = false; + this._loading.set(false); }, }), ); } private getNextId(): void { - this._loading = true; + this._loading.set(true); this.subscriptions.push( this.queryService.getMaxId(this.entityTable, this.entityIdField).subscribe({ next: (data) => { const currentMax = data[0].max; - this.idModel = this.calculateNextId(currentMax); - this.isIdFree = true; - this._loading = false; - this.changeDetectorRef.markForCheck(); + this.idModel.set(this.calculateNextId(currentMax)); + this.isIdFree.set(true); + this._loading.set(false); }, error: (error: QueryError) => { console.error(error); - this._loading = false; + this._loading.set(false); }, }), ); } protected checkMaxValue(): void { - if (this.idModel > MAX_INT_UNSIGNED_VALUE) { - this.idModel = MAX_INT_UNSIGNED_VALUE; + const idVal = this.idModel(); + if (typeof idVal === 'number' && idVal > MAX_INT_UNSIGNED_VALUE) { + this.idModel.set(MAX_INT_UNSIGNED_VALUE); + } + const srcVal = this.sourceIdModel(); + if (typeof srcVal === 'number' && srcVal > MAX_INT_UNSIGNED_VALUE) { + this.sourceIdModel.set(MAX_INT_UNSIGNED_VALUE); } } private calculateNextId(currentMax: number): number { return currentMax < this.customStartingId ? this.customStartingId : currentMax + 1; } + + protected onCreationMethodChange(): void { + if (this.creationMethod() === 'blank') { + this.sourceIdModel.set(undefined); + this.isSourceIdValid.set(false); + } + } + + protected checkSourceId(): void { + const src = this.sourceIdModel(); + if (src === undefined) { + this.isSourceIdValid.set(false); + return; + } + + this._loading.set(true); + this.subscriptions.push( + this.queryService.selectAll(this.entityTable, this.entityIdField, src).subscribe({ + next: (data) => { + // Source ID should exist (opposite of new ID check) + this.isSourceIdValid.set(data.length > 0); + this._loading.set(false); + }, + error: (error: QueryError) => { + console.error(error); + this.isSourceIdValid.set(false); + this._loading.set(false); + }, + }), + ); + } + + protected isFormValid(): boolean { + const isNewIdValid = !!this.idModel() && this.isIdFree(); + + if (this.creationMethod() === 'copy') { + return isNewIdValid && !!this.sourceIdModel() && this.isSourceIdValid(); + } + + return isNewIdValid; + } + + protected 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 7ef49a4ce6..f76aae06f3 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.spec.ts b/libs/shared/db-layer/src/query/mysql-query.service.spec.ts index 0065c52419..4a5f178552 100644 --- a/libs/shared/db-layer/src/query/mysql-query.service.spec.ts +++ b/libs/shared/db-layer/src/query/mysql-query.service.spec.ts @@ -918,5 +918,105 @@ describe('MysqlQueryService', () => { expect(service.query).toHaveBeenCalledWith(`SELECT * FROM reputation_reward_rate WHERE faction = ${id}`); expect(service['cache'].size).toBe(1); }); + + describe('copy query helpers', () => { + it('toSqlValue should format numbers and strings correctly', () => { + expect((service as any).toSqlValue(123)).toEqual('123'); + expect((service as any).toSqlValue('456')).toEqual('456'); + expect((service as any).toSqlValue("Anub'Rekhan")).toEqual("'Anub\\'Rekhan'"); + }); + + it('getCopyVarsSet should return proper SET statements', () => { + expect(service.getCopyVarsSet(10, 20)).toEqual('SET @SOURCE = 10;\nSET @ENTRY = 20;\n'); + }); + + it('getCopyQuery should generate temporary table copy SQL', () => { + const q = service.getCopyQuery('t', 10, 20, 'id'); + expect(q).toEqual( + 'DELETE FROM `t` WHERE `id` = 20;\n' + + 'CREATE TEMPORARY TABLE temp_copy_table AS\n' + + ' SELECT * FROM `t` WHERE `id` = 10;\n' + + 'UPDATE temp_copy_table SET `id` = 20;\n' + + 'INSERT INTO `t` SELECT * FROM temp_copy_table;\n' + + 'DROP TEMPORARY TABLE temp_copy_table;\n', + ); + }); + + it('getCopyQueryRaw should generate INSERT ... SELECT SQL with explicit columns', () => { + const q = service.getCopyQueryRaw('t', 10, 20, 'id', ['a', 'b', 'id']); + expect(q).toEqual( + 'DELETE FROM `t` WHERE `id` = 20;\n' + + 'INSERT INTO `t` (`a`, `b`, `id`)\n' + + ' SELECT `a`, `b`, 20 AS `id`\n' + + ' FROM `t`\n' + + ' WHERE `id` = 10;\n', + ); + }); + + it('getCopyQueryRawWithValues should return only delete for empty rows and full insert for values', () => { + const empty = service.getCopyQueryRawWithValues('t', [], 20, 'id', ['id', 'a', 'b']); + expect(empty).toEqual('DELETE FROM `t` WHERE `id` = 20;\n'); + + const rows = [ + { id: 1, a: 'x', b: null }, + { id: 2, a: "Anub'Rekhan", b: 3 }, + ]; + + const full = service.getCopyQueryRawWithValues('t', rows, 20, 'id', ['id', 'a', 'b']); + expect(full).toEqual( + 'DELETE FROM `t` WHERE `id` = 20;\n' + + 'INSERT INTO `t` (`id`, `a`, `b`) VALUES\n' + + "(20, 'x', NULL),\n" + + "(20, 'Anub\\'Rekhan', 3);\n", + ); + }); + }); + + describe('copy query helpers using vars', () => { + it('getCopyQuery should use @SOURCE and @ENTRY when useVars is true', () => { + const q = service.getCopyQuery('t', 10, 20, 'id', true); + expect(q).toContain('WHERE `id` = @ENTRY'); + expect(q).toContain('WHERE `id` = @SOURCE'); + }); + + it('getCopyQueryRaw should use @ENTRY when useVars is true', () => { + const q = service.getCopyQueryRaw('t', 10, 20, 'id', ['a', 'id'], true); + expect(q).toContain('SELECT `a`, @ENTRY AS `id`'); + expect(q).toContain('WHERE `id` = @SOURCE'); + }); + + it('getCopyQueryRawWithValues should use @ENTRY when useVars is true', () => { + const rows = [{ id: 1, a: 'x' }]; + const q = service.getCopyQueryRawWithValues('t', rows, 20, 'id', ['id', 'a'], true); + expect(q).toContain('DELETE FROM `t` WHERE `id` = @ENTRY;'); + expect(q).toContain('INSERT INTO `t` (`id`, `a`)'); + expect(q).toContain('(@ENTRY,'); + }); + }); + + it('getRowsCount should call queryValue with COUNT SQL and return observable', () => { + // `queryValue` is already spied in the surrounding beforeEach; reuse and change its return + (service.queryValue as jasmine.Spy).and.returnValue(of(42)); + service.getRowsCount('t', 'id', 123).subscribe((res) => { + expect(res).toEqual(42); + }); + expect(service.queryValue as jasmine.Spy).toHaveBeenCalledWith('SELECT COUNT(1) AS v FROM `t` WHERE `id` = 123;\n'); + }); + + it('getCopyQueryRawWithValues should derive columns from first row when columns param is empty', () => { + const rows = [ + { id: 1, a: 'x', b: null, c: 7 }, + { id: 2, a: "Anub'Rekhan", b: 3, c: 8 }, + ]; + + // pass undefined columns so implementation derives them from rows[0] + const full = service.getCopyQueryRawWithValues('t', rows, 20, 'id', undefined as any); + + // columns order derived from Object.keys(rows[0]) can vary, but ensure key patterns exist + expect(full).toContain('DELETE FROM `t` WHERE `id` = 20;\n'); + expect(full).toContain('INSERT INTO `t`'); + expect(full).toContain("(20, 'x'"); + expect(full).toContain("(20, 'Anub\\'Rekhan'"); + }); }); }); 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 420c1934df..24d45676d4 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): Observable { + return this.queryValue(`SELECT COUNT(1) AS v FROM \`${tableName}\` WHERE \`${idField}\` = ${idValue};\n`); + } } diff --git a/libs/shared/sai-editor/src/constants/sai-targets.ts b/libs/shared/sai-editor/src/constants/sai-targets.ts index d95716a1da..8aa063f18c 100644 --- a/libs/shared/sai-editor/src/constants/sai-targets.ts +++ b/libs/shared/sai-editor/src/constants/sai-targets.ts @@ -315,7 +315,8 @@ SAI_TARGET_PARAM2_NAMES[SAI_TARGETS.INSTANCE_STORAGE] = 'Type'; SAI_TARGET_PARAM2_TOOLTIPS[SAI_TARGETS.INSTANCE_STORAGE] = 'creature (1), gameobject (2)'; // SMART_TARGET_FORMATION -SAI_TARGET_TOOLTIPS[SAI_TARGETS.FORMATION] = 'Targets members of the creature\'s formation group (creature_formations table). Dead members are excluded.'; +SAI_TARGET_TOOLTIPS[SAI_TARGETS.FORMATION] = + "Targets members of the creature's formation group (creature_formations table). Dead members are excluded."; SAI_TARGET_PARAM1_NAMES[SAI_TARGETS.FORMATION] = 'Type'; SAI_TARGET_PARAM2_NAMES[SAI_TARGETS.FORMATION] = 'CreatureEntry'; SAI_TARGET_PARAM3_NAMES[SAI_TARGETS.FORMATION] = 'ExcludeSelf';