fix(i18n): prevent base name overwrites when editing in localized UI
Include base names in API responses to correctly populate edit forms. Show base values as placeholders in translation fields for better UX. Use default locale when fetching previous state for history diffs.
This commit is contained in:
@@ -861,6 +861,7 @@ function pokemonProjection(locale: string): string {
|
|||||||
SELECT
|
SELECT
|
||||||
p.id,
|
p.id,
|
||||||
${pokemonName} AS name,
|
${pokemonName} AS name,
|
||||||
|
p.name AS "baseName",
|
||||||
${translationsSelect('pokemon', 'p.id')} AS translations,
|
${translationsSelect('pokemon', 'p.id')} AS translations,
|
||||||
${auditSelect('p', 'pokemon_created_user', 'pokemon_updated_user')},
|
${auditSelect('p', 'pokemon_created_user', 'pokemon_updated_user')},
|
||||||
json_build_object('id', e.id, 'name', ${environmentName}) AS environment,
|
json_build_object('id', e.id, 'name', ${environmentName}) AS environment,
|
||||||
@@ -924,7 +925,7 @@ export async function listDailyChecklistItems(locale = defaultLocale) {
|
|||||||
const title = localizedField('daily-checklist-items', 'c.id', 'c.title', 'title', locale);
|
const title = localizedField('daily-checklist-items', 'c.id', 'c.title', 'title', locale);
|
||||||
return query(
|
return query(
|
||||||
`
|
`
|
||||||
SELECT c.id, ${title} AS title, ${translationsSelect('daily-checklist-items', 'c.id')} AS translations
|
SELECT c.id, ${title} AS title, c.title AS "baseTitle", ${translationsSelect('daily-checklist-items', 'c.id')} AS translations
|
||||||
FROM daily_checklist_items c
|
FROM daily_checklist_items c
|
||||||
ORDER BY c.sort_order, c.id
|
ORDER BY c.sort_order, c.id
|
||||||
`
|
`
|
||||||
@@ -935,7 +936,7 @@ async function getDailyChecklistItemById(id: number, locale = defaultLocale) {
|
|||||||
const title = localizedField('daily-checklist-items', 'c.id', 'c.title', 'title', locale);
|
const title = localizedField('daily-checklist-items', 'c.id', 'c.title', 'title', locale);
|
||||||
return queryOne(
|
return queryOne(
|
||||||
`
|
`
|
||||||
SELECT c.id, ${title} AS title, ${translationsSelect('daily-checklist-items', 'c.id')} AS translations
|
SELECT c.id, ${title} AS title, c.title AS "baseTitle", ${translationsSelect('daily-checklist-items', 'c.id')} AS translations
|
||||||
FROM daily_checklist_items c
|
FROM daily_checklist_items c
|
||||||
WHERE c.id = $1
|
WHERE c.id = $1
|
||||||
`,
|
`,
|
||||||
@@ -1442,7 +1443,7 @@ export async function createPokemon(payload: Record<string, unknown>, userId: nu
|
|||||||
|
|
||||||
export async function updatePokemon(id: number, payload: Record<string, unknown>, userId: number, locale = defaultLocale) {
|
export async function updatePokemon(id: number, payload: Record<string, unknown>, userId: number, locale = defaultLocale) {
|
||||||
const cleanPayload = cleanPokemonPayload({ ...payload, id });
|
const cleanPayload = cleanPokemonPayload({ ...payload, id });
|
||||||
const before = await getPokemon(id, locale);
|
const before = await getPokemon(id, defaultLocale);
|
||||||
|
|
||||||
const updated = await withTransaction(async (client) => {
|
const updated = await withTransaction(async (client) => {
|
||||||
const result = await client.query(
|
const result = await client.query(
|
||||||
@@ -1487,6 +1488,7 @@ export async function listHabitats(locale = defaultLocale) {
|
|||||||
SELECT
|
SELECT
|
||||||
h.id,
|
h.id,
|
||||||
${habitatName} AS name,
|
${habitatName} AS name,
|
||||||
|
h.name AS "baseName",
|
||||||
${translationsSelect('habitats', 'h.id')} AS translations,
|
${translationsSelect('habitats', 'h.id')} AS translations,
|
||||||
${auditSelect('h', 'habitat_created_user', 'habitat_updated_user')},
|
${auditSelect('h', 'habitat_created_user', 'habitat_updated_user')},
|
||||||
COALESCE((
|
COALESCE((
|
||||||
@@ -1521,6 +1523,7 @@ export async function getHabitat(id: number, locale = defaultLocale) {
|
|||||||
SELECT
|
SELECT
|
||||||
h.id,
|
h.id,
|
||||||
${habitatName} AS name,
|
${habitatName} AS name,
|
||||||
|
h.name AS "baseName",
|
||||||
${translationsSelect('habitats', 'h.id')} AS translations,
|
${translationsSelect('habitats', 'h.id')} AS translations,
|
||||||
${auditSelect('h', 'habitat_created_user', 'habitat_updated_user')},
|
${auditSelect('h', 'habitat_created_user', 'habitat_updated_user')},
|
||||||
COALESCE((
|
COALESCE((
|
||||||
@@ -1650,7 +1653,7 @@ export async function createHabitat(payload: Record<string, unknown>, userId: nu
|
|||||||
|
|
||||||
export async function updateHabitat(id: number, payload: Record<string, unknown>, userId: number, locale = defaultLocale) {
|
export async function updateHabitat(id: number, payload: Record<string, unknown>, userId: number, locale = defaultLocale) {
|
||||||
const cleanPayload = cleanHabitatPayload(payload);
|
const cleanPayload = cleanHabitatPayload(payload);
|
||||||
const before = await getHabitat(id, locale);
|
const before = await getHabitat(id, defaultLocale);
|
||||||
|
|
||||||
const updated = await withTransaction(async (client) => {
|
const updated = await withTransaction(async (client) => {
|
||||||
const result = await client.query(
|
const result = await client.query(
|
||||||
@@ -1692,6 +1695,7 @@ function itemProjection(locale: string): string {
|
|||||||
SELECT
|
SELECT
|
||||||
i.id,
|
i.id,
|
||||||
${itemName} AS name,
|
${itemName} AS name,
|
||||||
|
i.name AS "baseName",
|
||||||
${translationsSelect('items', 'i.id')} AS translations,
|
${translationsSelect('items', 'i.id')} AS translations,
|
||||||
${auditSelect('i', 'item_created_user', 'item_updated_user')},
|
${auditSelect('i', 'item_created_user', 'item_updated_user')},
|
||||||
json_build_object('id', c.id, 'name', ${categoryName}) AS category,
|
json_build_object('id', c.id, 'name', ${categoryName}) AS category,
|
||||||
@@ -1980,7 +1984,7 @@ export async function createItem(payload: Record<string, unknown>, userId: numbe
|
|||||||
|
|
||||||
export async function updateItem(id: number, payload: Record<string, unknown>, userId: number, locale = defaultLocale) {
|
export async function updateItem(id: number, payload: Record<string, unknown>, userId: number, locale = defaultLocale) {
|
||||||
const cleanPayload = cleanItemPayload(payload);
|
const cleanPayload = cleanItemPayload(payload);
|
||||||
const before = await getItem(id, locale);
|
const before = await getItem(id, defaultLocale);
|
||||||
|
|
||||||
const updated = await withTransaction(async (client) => {
|
const updated = await withTransaction(async (client) => {
|
||||||
await ensureItemCanDisableRecipe(client, id, cleanPayload.noRecipe);
|
await ensureItemCanDisableRecipe(client, id, cleanPayload.noRecipe);
|
||||||
@@ -2170,7 +2174,7 @@ export async function createRecipe(payload: Record<string, unknown>, userId: num
|
|||||||
|
|
||||||
export async function updateRecipe(id: number, payload: Record<string, unknown>, userId: number, locale = defaultLocale) {
|
export async function updateRecipe(id: number, payload: Record<string, unknown>, userId: number, locale = defaultLocale) {
|
||||||
const cleanPayload = cleanRecipePayload(payload);
|
const cleanPayload = cleanRecipePayload(payload);
|
||||||
const before = await getRecipe(id, locale);
|
const before = await getRecipe(id, defaultLocale);
|
||||||
|
|
||||||
const updated = await withTransaction(async (client) => {
|
const updated = await withTransaction(async (client) => {
|
||||||
await ensureItemCanHaveRecipe(client, cleanPayload.itemId);
|
await ensureItemCanHaveRecipe(client, cleanPayload.itemId);
|
||||||
|
|||||||
@@ -30,6 +30,10 @@ function fieldValue(language: Language): string {
|
|||||||
return props.translations[language.code]?.[props.field] ?? '';
|
return props.translations[language.code]?.[props.field] ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fieldPlaceholder(language: Language): string {
|
||||||
|
return language.code === defaultLanguage.value?.code ? '' : props.baseValue;
|
||||||
|
}
|
||||||
|
|
||||||
function updateField(language: Language, value: string) {
|
function updateField(language: Language, value: string) {
|
||||||
if (language.code === defaultLanguage.value?.code) {
|
if (language.code === defaultLanguage.value?.code) {
|
||||||
emit('update:baseValue', value);
|
emit('update:baseValue', value);
|
||||||
@@ -68,6 +72,7 @@ function inputValue(event: Event): string {
|
|||||||
<input
|
<input
|
||||||
:id="`${idPrefix}-${language.code}`"
|
:id="`${idPrefix}-${language.code}`"
|
||||||
:value="fieldValue(language)"
|
:value="fieldValue(language)"
|
||||||
|
:placeholder="fieldPlaceholder(language)"
|
||||||
:required="required && language.code === defaultLanguage?.code"
|
:required="required && language.code === defaultLanguage?.code"
|
||||||
@input="updateField(language, inputValue($event))"
|
@input="updateField(language, inputValue($event))"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ export interface EditHistoryEntry {
|
|||||||
export interface Pokemon extends EditInfo {
|
export interface Pokemon extends EditInfo {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
|
baseName?: string;
|
||||||
translations?: TranslationMap;
|
translations?: TranslationMap;
|
||||||
environment: NamedEntity;
|
environment: NamedEntity;
|
||||||
skills: Skill[];
|
skills: Skill[];
|
||||||
@@ -79,6 +80,7 @@ export interface PokemonDetail extends Pokemon {
|
|||||||
export interface Habitat extends EditInfo {
|
export interface Habitat extends EditInfo {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
|
baseName?: string;
|
||||||
translations?: TranslationMap;
|
translations?: TranslationMap;
|
||||||
recipe: Array<NamedEntity & { quantity: number }>;
|
recipe: Array<NamedEntity & { quantity: number }>;
|
||||||
pokemon?: NamedEntity[];
|
pokemon?: NamedEntity[];
|
||||||
@@ -113,6 +115,7 @@ export interface HabitatUsage {
|
|||||||
export interface Item extends EditInfo {
|
export interface Item extends EditInfo {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
|
baseName?: string;
|
||||||
translations?: TranslationMap;
|
translations?: TranslationMap;
|
||||||
category: NamedEntity;
|
category: NamedEntity;
|
||||||
usage: NamedEntity | null;
|
usage: NamedEntity | null;
|
||||||
@@ -147,6 +150,7 @@ export interface Recipe extends EditInfo {
|
|||||||
export interface DailyChecklistItem {
|
export interface DailyChecklistItem {
|
||||||
id: number;
|
id: number;
|
||||||
title: string;
|
title: string;
|
||||||
|
baseTitle?: string;
|
||||||
translations?: TranslationMap;
|
translations?: TranslationMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ const configNameInput = computed({
|
|||||||
return configForm.value.name;
|
return configForm.value.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
return configForm.value.translations[currentConfigLocale.value]?.name ?? configForm.value.name;
|
return configForm.value.translations[currentConfigLocale.value]?.name ?? '';
|
||||||
},
|
},
|
||||||
set: (value: string) => {
|
set: (value: string) => {
|
||||||
if (isConfigDefaultLocale.value) {
|
if (isConfigDefaultLocale.value) {
|
||||||
@@ -92,6 +92,7 @@ const configNameInput = computed({
|
|||||||
updateConfigTranslation(currentConfigLocale.value, value);
|
updateConfigTranslation(currentConfigLocale.value, value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
const configNamePlaceholder = computed(() => (isConfigDefaultLocale.value ? '' : configForm.value.name));
|
||||||
const activeConfigTab = computed({
|
const activeConfigTab = computed({
|
||||||
get: () => activeConfigType.value,
|
get: () => activeConfigType.value,
|
||||||
set: (value: string) => {
|
set: (value: string) => {
|
||||||
@@ -194,7 +195,7 @@ function closeChecklistModal() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function editChecklistItem(item: DailyChecklistItem) {
|
function editChecklistItem(item: DailyChecklistItem) {
|
||||||
checklistForm.value = { id: item.id, title: item.title, translations: item.translations ?? {} };
|
checklistForm.value = { id: item.id, title: item.baseTitle ?? item.title, translations: item.translations ?? {} };
|
||||||
checklistModalOpen.value = true;
|
checklistModalOpen.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -240,7 +241,7 @@ function updateConfigTranslation(localeCode: string, value: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function configBaseNameForSave() {
|
function configBaseNameForSave() {
|
||||||
if (configForm.value.name.trim() !== '' || isConfigDefaultLocale.value) {
|
if (configForm.value.name.trim() !== '') {
|
||||||
return configForm.value.name;
|
return configForm.value.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -805,7 +806,7 @@ onMounted(() => {
|
|||||||
<form id="admin-config-form" class="modal-edit-form" @submit.prevent="saveConfig">
|
<form id="admin-config-form" class="modal-edit-form" @submit.prevent="saveConfig">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="config-name">{{ t('common.name') }}</label>
|
<label for="config-name">{{ t('common.name') }}</label>
|
||||||
<input id="config-name" v-model="configNameInput" :required="configNameRequired" />
|
<input id="config-name" v-model="configNameInput" :placeholder="configNamePlaceholder" :required="configNameRequired" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="selectedConfig.supportsItemDrop" class="check-row">
|
<div v-if="selectedConfig.supportsItemDrop" class="check-row">
|
||||||
<label>
|
<label>
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ async function loadEditor() {
|
|||||||
if (isEditing.value) {
|
if (isEditing.value) {
|
||||||
const habitat = await api.habitatDetail(routeId.value);
|
const habitat = await api.habitatDetail(routeId.value);
|
||||||
habitatForm.value = {
|
habitatForm.value = {
|
||||||
name: habitat.name,
|
name: habitat.baseName ?? habitat.name,
|
||||||
translations: habitat.translations ?? {},
|
translations: habitat.translations ?? {},
|
||||||
recipeItems: habitat.recipe.map((recipeItem) => ({ itemId: String(recipeItem.id), quantity: recipeItem.quantity })),
|
recipeItems: habitat.recipe.map((recipeItem) => ({ itemId: String(recipeItem.id), quantity: recipeItem.quantity })),
|
||||||
pokemonAppearances: groupPokemonAppearances(habitat)
|
pokemonAppearances: groupPokemonAppearances(habitat)
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ async function loadEditor() {
|
|||||||
if (isEditing.value) {
|
if (isEditing.value) {
|
||||||
const item = await api.itemDetail(routeId.value);
|
const item = await api.itemDetail(routeId.value);
|
||||||
itemForm.value = {
|
itemForm.value = {
|
||||||
name: item.name,
|
name: item.baseName ?? item.name,
|
||||||
translations: item.translations ?? {},
|
translations: item.translations ?? {},
|
||||||
categoryId: String(item.category.id),
|
categoryId: String(item.category.id),
|
||||||
usageId: item.usage ? String(item.usage.id) : '',
|
usageId: item.usage ? String(item.usage.id) : '',
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ async function loadEditor() {
|
|||||||
const pokemon = await api.pokemonDetail(routeId.value);
|
const pokemon = await api.pokemonDetail(routeId.value);
|
||||||
pokemonForm.value = {
|
pokemonForm.value = {
|
||||||
id: String(pokemon.id),
|
id: String(pokemon.id),
|
||||||
name: pokemon.name,
|
name: pokemon.baseName ?? pokemon.name,
|
||||||
translations: pokemon.translations ?? {},
|
translations: pokemon.translations ?? {},
|
||||||
environmentId: String(pokemon.environment.id),
|
environmentId: String(pokemon.environment.id),
|
||||||
skillIds: pokemon.skills.map((skill) => String(skill.id)),
|
skillIds: pokemon.skills.map((skill) => String(skill.id)),
|
||||||
|
|||||||
Reference in New Issue
Block a user