feat(admin): add module settings to toggle trading feature
Introduce module_settings table to store global feature flags Add admin UI to enable or disable the Trading module Hide trading-related UI and skip data fetching when disabled
This commit is contained in:
12
AGENTS.md
12
AGENTS.md
@@ -223,11 +223,19 @@ This project is developed from WSL, but runtime validation is done through Docke
|
|||||||
|
|
||||||
Agent workflow:
|
Agent workflow:
|
||||||
|
|
||||||
* Run when practical:
|
* Run once when practical:
|
||||||
|
|
||||||
* `pnpm lint`
|
* `pnpm lint`
|
||||||
* `pnpm typecheck`
|
* `pnpm typecheck`
|
||||||
|
|
||||||
|
* Do not repeatedly retry failed validation commands.
|
||||||
|
* If lint/typecheck fails because of missing dependencies, native optional bindings, WSL, network, registry, filesystem permission, or other environment/setup issues:
|
||||||
|
|
||||||
|
* Do not repeatedly run `pnpm install`, force reinstall dependencies, delete `node_modules`, or otherwise spend time repairing the local environment unless the user explicitly asks.
|
||||||
|
* Report the exact command attempted and the key error lines.
|
||||||
|
* Tell the user what command to run locally or in Docker.
|
||||||
|
* Wait for the user to paste `docker compose up --build`, lint, typecheck, or runtime output before fixing follow-up errors.
|
||||||
|
|
||||||
* Do NOT run tests in WSL.
|
* Do NOT run tests in WSL.
|
||||||
* Do NOT require local test execution before finishing a task.
|
* Do NOT require local test execution before finishing a task.
|
||||||
* The user will run `docker compose up --build`.
|
* The user will run `docker compose up --build`.
|
||||||
@@ -246,7 +254,7 @@ A task is complete ONLY IF:
|
|||||||
* Minimal diff, with no unrelated changes.
|
* Minimal diff, with no unrelated changes.
|
||||||
* No UI leaks of internal info.
|
* No UI leaks of internal info.
|
||||||
* Code is readable and concise.
|
* Code is readable and concise.
|
||||||
* Passes lint/typecheck when practical.
|
* Lint/typecheck has been run once when practical, or the environment blocker and user-side validation command have been reported.
|
||||||
* Docker runtime issues are handled from user-provided `docker compose up --build` output.
|
* Docker runtime issues are handled from user-provided `docker compose up --build` output.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
16
DESIGN.md
16
DESIGN.md
@@ -12,6 +12,7 @@
|
|||||||
- 全局顶部导航栏承载语言切换、通知、User Profile 和登录 / 退出等账号操作;除 User Profile 可展示用户名外,顶部操作以图标按钮呈现。
|
- 全局顶部导航栏承载语言切换、通知、User Profile 和登录 / 退出等账号操作;除 User Profile 可展示用户名外,顶部操作以图标按钮呈现。
|
||||||
- 全局顶部导航栏提供全站搜索。搜索结果按内容类型分组展示,覆盖 Pokemon、Habitats、Items、Ancient Artifacts、Recipes、Daily CheckList、公开可见的 Life Post 和公开用户 Profile;结果跳转到对应公开详情页、页面锚点或 `/profile/:id`。
|
- 全局顶部导航栏提供全站搜索。搜索结果按内容类型分组展示,覆盖 Pokemon、Habitats、Items、Ancient Artifacts、Recipes、Daily CheckList、公开可见的 Life Post 和公开用户 Profile;结果跳转到对应公开详情页、页面锚点或 `/profile/:id`。
|
||||||
- 管理入口用于维护全局配置、语言、系统文案、列表排序和每日 CheckList。
|
- 管理入口用于维护全局配置、语言、系统文案、列表排序和每日 CheckList。
|
||||||
|
- 管理员可在管理入口启用或禁用模块级功能开关;Trading 模块关闭时保留 `has_trading` 和 Trading 观察数据,但前台与编辑界面不展示 Trading 相关功能。
|
||||||
|
|
||||||
## 技术栈
|
## 技术栈
|
||||||
|
|
||||||
@@ -459,10 +460,25 @@
|
|||||||
- 名称
|
- 名称
|
||||||
- 是否有掉落物:`has_item_drop`
|
- 是否有掉落物:`has_item_drop`
|
||||||
- 是否支持 Trading:`has_trading`
|
- 是否支持 Trading:`has_trading`
|
||||||
|
- `has_trading` 是特长自身能力配置,不作为模块显示开关;Trading 模块关闭时该字段和已有 Trading 观察数据保留。
|
||||||
- 已移除 `subcategory` 字段。
|
- 已移除 `subcategory` 字段。
|
||||||
- 当特长允许掉落物时,Pokemon 编辑中可为该 Pokemon + 特长配置一个掉落物品。
|
- 当特长允许掉落物时,Pokemon 编辑中可为该 Pokemon + 特长配置一个掉落物品。
|
||||||
- 当 Pokemon 选择了至少一个支持 Trading 的特长时,Pokemon 详情页可直接维护该 Pokemon 对物品的 Trading 偏好观察。
|
- 当 Pokemon 选择了至少一个支持 Trading 的特长时,Pokemon 详情页可直接维护该 Pokemon 对物品的 Trading 偏好观察。
|
||||||
|
|
||||||
|
### 模块设置
|
||||||
|
|
||||||
|
- 模块设置存储在 `module_settings`,当前包含:
|
||||||
|
- `trading_enabled`:控制 Trading 相关界面和推断展示,默认启用。
|
||||||
|
- 管理端查看模块设置需要 `admin.config.read`,更新模块设置需要 `admin.config.update`。
|
||||||
|
- 关闭 Trading 模块时:
|
||||||
|
- 不删除、不清空 `skills.has_trading`。
|
||||||
|
- 不删除、不清空 `pokemon_trading_items`。
|
||||||
|
- Pokemon 详情页不展示 Trading 区块或管理 Trading 入口。
|
||||||
|
- Pokemon 创建 / 编辑流程不展示 Trading 相关编辑能力。
|
||||||
|
- Item 详情页不展示基于 Trading 观察推断的 Possible Tags 和证据区块。
|
||||||
|
- 管理端 Skill 配置不展示 `has_trading` 勾选项或列表标记。
|
||||||
|
- 重新启用 Trading 模块后,已有配置和观察数据恢复参与界面展示与推断。
|
||||||
|
|
||||||
### Pokemon Types
|
### Pokemon Types
|
||||||
|
|
||||||
- 名称
|
- 名称
|
||||||
|
|||||||
@@ -219,6 +219,18 @@ INSERT INTO rate_limit_settings (id)
|
|||||||
VALUES (true)
|
VALUES (true)
|
||||||
ON CONFLICT (id) DO NOTHING;
|
ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS module_settings (
|
||||||
|
id boolean PRIMARY KEY DEFAULT true CHECK (id = true),
|
||||||
|
trading_enabled boolean NOT NULL DEFAULT true,
|
||||||
|
updated_by_user_id integer REFERENCES users(id) ON DELETE SET NULL,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO module_settings (id)
|
||||||
|
VALUES (true)
|
||||||
|
ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
INSERT INTO permissions (key, name, description, category, system_permission)
|
INSERT INTO permissions (key, name, description, category, system_permission)
|
||||||
VALUES
|
VALUES
|
||||||
('admin.access', 'Access admin', 'Open the management area.', 'Admin', true),
|
('admin.access', 'Access admin', 'Open the management area.', 'Admin', true),
|
||||||
|
|||||||
@@ -569,6 +569,10 @@ type LanguagePayload = {
|
|||||||
sortOrder: number;
|
sortOrder: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ModuleSettings = {
|
||||||
|
tradingEnabled: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
type ValidationError = Error & { statusCode: number };
|
type ValidationError = Error & { statusCode: number };
|
||||||
type EditAction = 'create' | 'update' | 'delete';
|
type EditAction = 'create' | 'update' | 'delete';
|
||||||
type EditChange = {
|
type EditChange = {
|
||||||
@@ -1072,6 +1076,44 @@ function systemListJsonSql(expression: string, options: readonly SystemListOptio
|
|||||||
return `json_build_object('id', ${systemListIdSql(expression, options)}, 'key', ${expression}, 'name', ${systemListNameSql(expression, options, locale)})`;
|
return `json_build_object('id', ${systemListIdSql(expression, options)}, 'key', ${expression}, 'name', ${systemListNameSql(expression, options, locale)})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function publicModuleSettings(row: { tradingEnabled: boolean } | null): ModuleSettings {
|
||||||
|
return {
|
||||||
|
tradingEnabled: row?.tradingEnabled ?? true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function moduleSettingsForClient(client: DbClient): Promise<ModuleSettings> {
|
||||||
|
const result = await client.query<{ tradingEnabled: boolean }>(
|
||||||
|
'SELECT trading_enabled AS "tradingEnabled" FROM module_settings WHERE id = true'
|
||||||
|
);
|
||||||
|
return publicModuleSettings(result.rows[0] ?? null);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getModuleSettings(): Promise<ModuleSettings> {
|
||||||
|
const row = await queryOne<{ tradingEnabled: boolean }>(
|
||||||
|
'SELECT trading_enabled AS "tradingEnabled" FROM module_settings WHERE id = true'
|
||||||
|
);
|
||||||
|
return publicModuleSettings(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateModuleSettings(payload: Record<string, unknown>, userId: number): Promise<ModuleSettings> {
|
||||||
|
const current = await getModuleSettings();
|
||||||
|
const tradingEnabled = typeof payload.tradingEnabled === 'boolean' ? payload.tradingEnabled : current.tradingEnabled;
|
||||||
|
const row = await queryOne<{ tradingEnabled: boolean }>(
|
||||||
|
`
|
||||||
|
INSERT INTO module_settings (id, trading_enabled, updated_by_user_id, updated_at)
|
||||||
|
VALUES (true, $1, $2, now())
|
||||||
|
ON CONFLICT (id) DO UPDATE SET
|
||||||
|
trading_enabled = EXCLUDED.trading_enabled,
|
||||||
|
updated_by_user_id = EXCLUDED.updated_by_user_id,
|
||||||
|
updated_at = now()
|
||||||
|
RETURNING trading_enabled AS "tradingEnabled"
|
||||||
|
`,
|
||||||
|
[tradingEnabled, userId]
|
||||||
|
);
|
||||||
|
return publicModuleSettings(row);
|
||||||
|
}
|
||||||
|
|
||||||
function gameVersionOptions(locale: string): Promise<Array<{ id: number; name: string; changeLog: string }>> {
|
function gameVersionOptions(locale: string): Promise<Array<{ id: number; name: string; changeLog: string }>> {
|
||||||
const name = localizedName('game-versions', 'gv', locale);
|
const name = localizedName('game-versions', 'gv', locale);
|
||||||
return query(`SELECT gv.id, ${name} AS name, gv.change_log AS "changeLog" FROM game_versions gv ORDER BY ${orderByEntity('gv')}`);
|
return query(`SELECT gv.id, ${name} AS name, gv.change_log AS "changeLog" FROM game_versions gv ORDER BY ${orderByEntity('gv')}`);
|
||||||
@@ -2793,7 +2835,8 @@ export async function getOptions(locale = defaultLocale) {
|
|||||||
acquisitionMethods,
|
acquisitionMethods,
|
||||||
maps,
|
maps,
|
||||||
gameVersions,
|
gameVersions,
|
||||||
dishFlavors
|
dishFlavors,
|
||||||
|
moduleSettings
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
optionSelect('pokemon_types', 'pokemon-types', locale),
|
optionSelect('pokemon_types', 'pokemon-types', locale),
|
||||||
skillOptions(locale),
|
skillOptions(locale),
|
||||||
@@ -2802,7 +2845,8 @@ export async function getOptions(locale = defaultLocale) {
|
|||||||
optionSelect('acquisition_methods', 'acquisition-methods', locale),
|
optionSelect('acquisition_methods', 'acquisition-methods', locale),
|
||||||
optionSelect('maps', 'maps', locale),
|
optionSelect('maps', 'maps', locale),
|
||||||
gameVersionOptions(locale),
|
gameVersionOptions(locale),
|
||||||
optionSelect('dish_flavors', 'dish-flavors', locale)
|
optionSelect('dish_flavors', 'dish-flavors', locale),
|
||||||
|
getModuleSettings()
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -2817,7 +2861,8 @@ export async function getOptions(locale = defaultLocale) {
|
|||||||
itemTags: favoriteThings,
|
itemTags: favoriteThings,
|
||||||
maps,
|
maps,
|
||||||
gameVersions,
|
gameVersions,
|
||||||
dishFlavors
|
dishFlavors,
|
||||||
|
moduleSettings
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5569,10 +5614,14 @@ export async function updateConfig(
|
|||||||
const translations = cleanTranslations(payload.translations, ['name']);
|
const translations = cleanTranslations(payload.translations, ['name']);
|
||||||
const description = definition.hasDescription ? cleanOptionalText(payload.description) : '';
|
const description = definition.hasDescription ? cleanOptionalText(payload.description) : '';
|
||||||
const oppositeId = definition.oppositeColumn ? cleanOptionalPositiveInteger(payload.oppositeId) : null;
|
const oppositeId = definition.oppositeColumn ? cleanOptionalPositiveInteger(payload.oppositeId) : null;
|
||||||
const hasItemDrop = definition.hasItemDrop ? Boolean(payload.hasItemDrop) : false;
|
|
||||||
const hasTrading = definition.hasTrading ? Boolean(payload.hasTrading) : false;
|
|
||||||
const changeLog = definition.hasChangeLog ? cleanOptionalText(payload.changeLog) : '';
|
|
||||||
const before = await getConfigById(type, id, defaultLocale);
|
const before = await getConfigById(type, id, defaultLocale);
|
||||||
|
const hasItemDrop = definition.hasItemDrop ? Boolean(payload.hasItemDrop) : false;
|
||||||
|
const hasTrading = definition.hasTrading
|
||||||
|
? Object.hasOwn(payload, 'hasTrading')
|
||||||
|
? Boolean(payload.hasTrading)
|
||||||
|
: Boolean((before as { hasTrading?: boolean } | null)?.hasTrading)
|
||||||
|
: false;
|
||||||
|
const changeLog = definition.hasChangeLog ? cleanOptionalText(payload.changeLog) : '';
|
||||||
|
|
||||||
if (oppositeId === id) {
|
if (oppositeId === id) {
|
||||||
throw validationError('server.validation.invalidField');
|
throw validationError('server.validation.invalidField');
|
||||||
@@ -5775,6 +5824,7 @@ export async function getPokemon(id: number, locale = defaultLocale) {
|
|||||||
const relatedSkillName = localizedName('skills', 'related_skill', locale);
|
const relatedSkillName = localizedName('skills', 'related_skill', locale);
|
||||||
const relatedFavoriteThingName = localizedName('favorite-things', 'related_favorite_thing', locale);
|
const relatedFavoriteThingName = localizedName('favorite-things', 'related_favorite_thing', locale);
|
||||||
const tradingItemName = localizedName('items', 'trading_item', locale);
|
const tradingItemName = localizedName('items', 'trading_item', locale);
|
||||||
|
const moduleSettings = await getModuleSettings();
|
||||||
|
|
||||||
const [habitats, itemDrops, favoriteThingItems, tradingItems, relatedPokemon, editHistory, imageHistory] = await Promise.all([
|
const [habitats, itemDrops, favoriteThingItems, tradingItems, relatedPokemon, editHistory, imageHistory] = await Promise.all([
|
||||||
query(
|
query(
|
||||||
@@ -5825,7 +5875,8 @@ export async function getPokemon(id: number, locale = defaultLocale) {
|
|||||||
`,
|
`,
|
||||||
[id]
|
[id]
|
||||||
),
|
),
|
||||||
query(
|
moduleSettings.tradingEnabled
|
||||||
|
? query(
|
||||||
`
|
`
|
||||||
SELECT
|
SELECT
|
||||||
ti.item_id AS "itemId",
|
ti.item_id AS "itemId",
|
||||||
@@ -5846,7 +5897,8 @@ export async function getPokemon(id: number, locale = defaultLocale) {
|
|||||||
ORDER BY ti.preference DESC, ${orderByEntity('trading_item')}
|
ORDER BY ti.preference DESC, ${orderByEntity('trading_item')}
|
||||||
`,
|
`,
|
||||||
[id]
|
[id]
|
||||||
),
|
)
|
||||||
|
: Promise.resolve([]),
|
||||||
query(
|
query(
|
||||||
`
|
`
|
||||||
WITH current_pokemon AS (
|
WITH current_pokemon AS (
|
||||||
@@ -6088,12 +6140,14 @@ async function normalizePokemonDataIdentity(payload: PokemonPayload): Promise<vo
|
|||||||
payload.dataIdentifier = csvText(pokemonRow, 'identifier');
|
payload.dataIdentifier = csvText(pokemonRow, 'identifier');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function replacePokemonRelations(client: DbClient, pokemonId: number, payload: PokemonPayload): Promise<void> {
|
async function replacePokemonRelations(client: DbClient, pokemonId: number, payload: PokemonPayload, replaceTrading: boolean): Promise<void> {
|
||||||
await client.query('DELETE FROM pokemon_skill_item_drops WHERE pokemon_id = $1', [pokemonId]);
|
await client.query('DELETE FROM pokemon_skill_item_drops WHERE pokemon_id = $1', [pokemonId]);
|
||||||
await client.query('DELETE FROM pokemon_pokemon_types WHERE pokemon_id = $1', [pokemonId]);
|
await client.query('DELETE FROM pokemon_pokemon_types WHERE pokemon_id = $1', [pokemonId]);
|
||||||
await client.query('DELETE FROM pokemon_skills WHERE pokemon_id = $1', [pokemonId]);
|
await client.query('DELETE FROM pokemon_skills WHERE pokemon_id = $1', [pokemonId]);
|
||||||
await client.query('DELETE FROM pokemon_favorite_things WHERE pokemon_id = $1', [pokemonId]);
|
await client.query('DELETE FROM pokemon_favorite_things WHERE pokemon_id = $1', [pokemonId]);
|
||||||
|
if (replaceTrading) {
|
||||||
await client.query('DELETE FROM pokemon_trading_items WHERE pokemon_id = $1', [pokemonId]);
|
await client.query('DELETE FROM pokemon_trading_items WHERE pokemon_id = $1', [pokemonId]);
|
||||||
|
}
|
||||||
|
|
||||||
for (const [index, typeId] of payload.typeIds.entries()) {
|
for (const [index, typeId] of payload.typeIds.entries()) {
|
||||||
await client.query('INSERT INTO pokemon_pokemon_types (pokemon_id, type_id, slot_order) VALUES ($1, $2, $3)', [
|
await client.query('INSERT INTO pokemon_pokemon_types (pokemon_id, type_id, slot_order) VALUES ($1, $2, $3)', [
|
||||||
@@ -6114,10 +6168,10 @@ async function replacePokemonRelations(client: DbClient, pokemonId: number, payl
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const tradingSkillResult = payload.skillIds.length
|
const tradingSkillResult = replaceTrading && payload.skillIds.length
|
||||||
? await client.query<{ id: number }>('SELECT id FROM skills WHERE id = ANY($1::integer[]) AND has_trading = true', [payload.skillIds])
|
? await client.query<{ id: number }>('SELECT id FROM skills WHERE id = ANY($1::integer[]) AND has_trading = true', [payload.skillIds])
|
||||||
: { rows: [] };
|
: { rows: [] };
|
||||||
const hasTradingSkill = tradingSkillResult.rows.length > 0;
|
const hasTradingSkill = replaceTrading && tradingSkillResult.rows.length > 0;
|
||||||
|
|
||||||
if (hasTradingSkill) {
|
if (hasTradingSkill) {
|
||||||
for (const tradingItem of payload.tradingItems) {
|
for (const tradingItem of payload.tradingItems) {
|
||||||
@@ -6215,7 +6269,8 @@ export async function createPokemon(payload: Record<string, unknown>, userId: nu
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
await linkEntityImageUpload(client, 'pokemon', pokemonId, cleanPayload.image?.path, cleanPayload.name);
|
await linkEntityImageUpload(client, 'pokemon', pokemonId, cleanPayload.image?.path, cleanPayload.name);
|
||||||
await replacePokemonRelations(client, pokemonId, cleanPayload);
|
const moduleSettings = await moduleSettingsForClient(client);
|
||||||
|
await replacePokemonRelations(client, pokemonId, cleanPayload, moduleSettings.tradingEnabled);
|
||||||
await replaceEntityTranslations(client, 'pokemon', pokemonId, cleanPayload.translations, ['name', 'details', 'genus']);
|
await replaceEntityTranslations(client, 'pokemon', pokemonId, cleanPayload.translations, ['name', 'details', 'genus']);
|
||||||
await recordEditLog(client, 'pokemon', pokemonId, 'create', userId);
|
await recordEditLog(client, 'pokemon', pokemonId, 'create', userId);
|
||||||
return pokemonId;
|
return pokemonId;
|
||||||
@@ -6291,7 +6346,8 @@ export async function updatePokemon(id: number, payload: Record<string, unknown>
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
await linkEntityImageUpload(client, 'pokemon', id, cleanPayload.image?.path, cleanPayload.name);
|
await linkEntityImageUpload(client, 'pokemon', id, cleanPayload.image?.path, cleanPayload.name);
|
||||||
await replacePokemonRelations(client, id, cleanPayload);
|
const moduleSettings = await moduleSettingsForClient(client);
|
||||||
|
await replacePokemonRelations(client, id, cleanPayload, moduleSettings.tradingEnabled);
|
||||||
await replaceEntityTranslations(client, 'pokemon', id, cleanPayload.translations, ['name', 'details', 'genus']);
|
await replaceEntityTranslations(client, 'pokemon', id, cleanPayload.translations, ['name', 'details', 'genus']);
|
||||||
const changes = before ? await pokemonEditChanges(client, before as unknown as PokemonChangeSource, cleanPayload) : [];
|
const changes = before ? await pokemonEditChanges(client, before as unknown as PokemonChangeSource, cleanPayload) : [];
|
||||||
await recordEditLog(client, 'pokemon', id, 'update', userId, changes);
|
await recordEditLog(client, 'pokemon', id, 'update', userId, changes);
|
||||||
@@ -6701,6 +6757,7 @@ export async function getItem(id: number, locale = defaultLocale) {
|
|||||||
const skillName = localizedName('skills', 's', locale);
|
const skillName = localizedName('skills', 's', locale);
|
||||||
const possibleTagName = localizedName('favorite-things', 'possible_tag', locale);
|
const possibleTagName = localizedName('favorite-things', 'possible_tag', locale);
|
||||||
const evidenceTagName = localizedName('favorite-things', 'evidence_tag', locale);
|
const evidenceTagName = localizedName('favorite-things', 'evidence_tag', locale);
|
||||||
|
const moduleSettings = await getModuleSettings();
|
||||||
|
|
||||||
const [
|
const [
|
||||||
acquisitionMethods,
|
acquisitionMethods,
|
||||||
@@ -6841,14 +6898,17 @@ export async function getItem(id: number, locale = defaultLocale) {
|
|||||||
`,
|
`,
|
||||||
[id]
|
[id]
|
||||||
),
|
),
|
||||||
query<ItemPossibleTagEntity>(
|
moduleSettings.tradingEnabled
|
||||||
|
? query<ItemPossibleTagEntity>(
|
||||||
`
|
`
|
||||||
SELECT possible_tag.id, ${possibleTagName} AS name
|
SELECT possible_tag.id, ${possibleTagName} AS name
|
||||||
FROM favorite_things possible_tag
|
FROM favorite_things possible_tag
|
||||||
ORDER BY ${orderByEntity('possible_tag')}
|
ORDER BY ${orderByEntity('possible_tag')}
|
||||||
`
|
`
|
||||||
),
|
)
|
||||||
query<ItemPossibleTagObservation>(
|
: Promise.resolve([]),
|
||||||
|
moduleSettings.tradingEnabled
|
||||||
|
? query<ItemPossibleTagObservation>(
|
||||||
`
|
`
|
||||||
SELECT
|
SELECT
|
||||||
json_build_object(
|
json_build_object(
|
||||||
@@ -6878,7 +6938,8 @@ export async function getItem(id: number, locale = defaultLocale) {
|
|||||||
ORDER BY pti.preference DESC, p.display_id, p.id
|
ORDER BY pti.preference DESC, p.display_id, p.id
|
||||||
`,
|
`,
|
||||||
[id]
|
[id]
|
||||||
),
|
)
|
||||||
|
: Promise.resolve([]),
|
||||||
getEditHistory('items', id),
|
getEditHistory('items', id),
|
||||||
listEntityImageUploads('items', id)
|
listEntityImageUploads('items', id)
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ import {
|
|||||||
getItem,
|
getItem,
|
||||||
listDish,
|
listDish,
|
||||||
getLifePost,
|
getLifePost,
|
||||||
|
getModuleSettings,
|
||||||
getOptions,
|
getOptions,
|
||||||
getPokemon,
|
getPokemon,
|
||||||
getPublicUserProfile,
|
getPublicUserProfile,
|
||||||
@@ -149,6 +150,7 @@ import {
|
|||||||
updateItem,
|
updateItem,
|
||||||
updateLanguage,
|
updateLanguage,
|
||||||
updateLifePost,
|
updateLifePost,
|
||||||
|
updateModuleSettings,
|
||||||
updatePokemon,
|
updatePokemon,
|
||||||
updateRecipe,
|
updateRecipe,
|
||||||
updateAdminThreadChannel,
|
updateAdminThreadChannel,
|
||||||
@@ -1309,6 +1311,8 @@ app.get('/api/languages', async () => listLanguages());
|
|||||||
|
|
||||||
app.get('/api/system-wordings', async (request) => getSystemWordings(requestLocale(request)));
|
app.get('/api/system-wordings', async (request) => getSystemWordings(requestLocale(request)));
|
||||||
|
|
||||||
|
app.get('/api/module-settings', async () => getModuleSettings());
|
||||||
|
|
||||||
app.get('/api/options', async (request) => getOptions(requestLocale(request)));
|
app.get('/api/options', async (request) => getOptions(requestLocale(request)));
|
||||||
|
|
||||||
app.get('/api/project-updates', async (request) =>
|
app.get('/api/project-updates', async (request) =>
|
||||||
@@ -2385,6 +2389,19 @@ app.put('/api/admin/ai-moderation', async (request, reply) => {
|
|||||||
return updateAiModerationSettings(request.body as Record<string, unknown>, user.id);
|
return updateAiModerationSettings(request.body as Record<string, unknown>, user.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.get('/api/admin/module-settings', async (request, reply) => {
|
||||||
|
const user = await requirePermission(request, reply, 'admin.config.read');
|
||||||
|
return user ? getModuleSettings() : undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
app.put('/api/admin/module-settings', async (request, reply) => {
|
||||||
|
const user = await requirePermissionWithRateLimits(request, reply, 'admin.config.update', 'adminWrite');
|
||||||
|
if (!user) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return updateModuleSettings(request.body as Record<string, unknown>, user.id);
|
||||||
|
});
|
||||||
|
|
||||||
app.get('/api/admin/rate-limits', async (request, reply) => {
|
app.get('/api/admin/rate-limits', async (request, reply) => {
|
||||||
const user = await requirePermission(request, reply, 'admin.rate-limits.read');
|
const user = await requirePermission(request, reply, 'admin.rate-limits.read');
|
||||||
return user ? getRateLimitSettings() : undefined;
|
return user ? getRateLimitSettings() : undefined;
|
||||||
|
|||||||
@@ -67,6 +67,10 @@ export interface SystemWording {
|
|||||||
updatedBy: UserSummary | null;
|
updatedBy: UserSummary | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ModuleSettings {
|
||||||
|
tradingEnabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface NamedEntity {
|
export interface NamedEntity {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -768,6 +772,7 @@ export interface Options {
|
|||||||
maps: NamedEntity[];
|
maps: NamedEntity[];
|
||||||
gameVersions: GameVersion[];
|
gameVersions: GameVersion[];
|
||||||
dishFlavors: NamedEntity[];
|
dishFlavors: NamedEntity[];
|
||||||
|
moduleSettings: ModuleSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AuthUser {
|
export interface AuthUser {
|
||||||
@@ -1325,6 +1330,7 @@ export const api = {
|
|||||||
globalSearch: (query: string, signal?: AbortSignal) =>
|
globalSearch: (query: string, signal?: AbortSignal) =>
|
||||||
getJson<GlobalSearchResults>(`/api/search${buildQuery({ query: query.trim() })}`, signal),
|
getJson<GlobalSearchResults>(`/api/search${buildQuery({ query: query.trim() })}`, signal),
|
||||||
languages: () => getJson<Language[]>('/api/languages'),
|
languages: () => getJson<Language[]>('/api/languages'),
|
||||||
|
moduleSettings: () => getJson<ModuleSettings>('/api/module-settings'),
|
||||||
projectUpdates: (params: ProjectUpdatesParams = {}) =>
|
projectUpdates: (params: ProjectUpdatesParams = {}) =>
|
||||||
getJson<ProjectUpdates>(
|
getJson<ProjectUpdates>(
|
||||||
`/api/project-updates${buildQuery({
|
`/api/project-updates${buildQuery({
|
||||||
@@ -1346,6 +1352,9 @@ export const api = {
|
|||||||
aiModerationSettings: () => getJson<AiModerationSettings>('/api/admin/ai-moderation'),
|
aiModerationSettings: () => getJson<AiModerationSettings>('/api/admin/ai-moderation'),
|
||||||
updateAiModerationSettings: (payload: AiModerationSettingsPayload) =>
|
updateAiModerationSettings: (payload: AiModerationSettingsPayload) =>
|
||||||
sendJson<AiModerationSettings>('/api/admin/ai-moderation', 'PUT', payload),
|
sendJson<AiModerationSettings>('/api/admin/ai-moderation', 'PUT', payload),
|
||||||
|
adminModuleSettings: () => getJson<ModuleSettings>('/api/admin/module-settings'),
|
||||||
|
updateAdminModuleSettings: (payload: ModuleSettings) =>
|
||||||
|
sendJson<ModuleSettings>('/api/admin/module-settings', 'PUT', payload),
|
||||||
rateLimitSettings: () => getJson<RateLimitSettings>('/api/admin/rate-limits'),
|
rateLimitSettings: () => getJson<RateLimitSettings>('/api/admin/rate-limits'),
|
||||||
updateRateLimitSettings: (payload: RateLimitSettingsPayload) =>
|
updateRateLimitSettings: (payload: RateLimitSettingsPayload) =>
|
||||||
sendJson<RateLimitSettings>('/api/admin/rate-limits', 'PUT', payload),
|
sendJson<RateLimitSettings>('/api/admin/rate-limits', 'PUT', payload),
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ import {
|
|||||||
type Habitat,
|
type Habitat,
|
||||||
type Item,
|
type Item,
|
||||||
type Language,
|
type Language,
|
||||||
|
type ModuleSettings,
|
||||||
type NamedEntity,
|
type NamedEntity,
|
||||||
type Permission,
|
type Permission,
|
||||||
type PermissionPayload,
|
type PermissionPayload,
|
||||||
@@ -245,6 +246,7 @@ const wordingRows = ref<SystemWording[]>([]);
|
|||||||
const aiModerationSettings = ref<AiModerationSettings | null>(null);
|
const aiModerationSettings = ref<AiModerationSettings | null>(null);
|
||||||
const rateLimitSettings = ref<RateLimitSettings | null>(null);
|
const rateLimitSettings = ref<RateLimitSettings | null>(null);
|
||||||
const dataToolsSummary = ref<DataToolsSummary | null>(null);
|
const dataToolsSummary = ref<DataToolsSummary | null>(null);
|
||||||
|
const moduleSettings = ref<ModuleSettings>({ tradingEnabled: true });
|
||||||
const currentUser = ref<AuthUser | null>(null);
|
const currentUser = ref<AuthUser | null>(null);
|
||||||
const busy = ref(false);
|
const busy = ref(false);
|
||||||
const contentLoading = ref(false);
|
const contentLoading = ref(false);
|
||||||
@@ -375,6 +377,7 @@ const canEdit = computed(() => can('admin.access'));
|
|||||||
const canUseViewAs = computed(() => currentUser.value?.roles.some((role) => role.key === 'owner') === true && !currentUser.value?.viewAs);
|
const canUseViewAs = computed(() => currentUser.value?.roles.some((role) => role.key === 'owner') === true && !currentUser.value?.viewAs);
|
||||||
const showAdminSkeleton = computed(() => busy.value && !message.value && (!currentUser.value || contentLoading.value));
|
const showAdminSkeleton = computed(() => busy.value && !message.value && (!currentUser.value || contentLoading.value));
|
||||||
const canSetLanguageDefault = computed(() => languageForm.value.code === 'en');
|
const canSetLanguageDefault = computed(() => languageForm.value.code === 'en');
|
||||||
|
const tradingModuleEnabled = computed(() => moduleSettings.value.tradingEnabled);
|
||||||
const configModalTitle = computed(() =>
|
const configModalTitle = computed(() =>
|
||||||
configForm.value.id ? t('pages.admin.editConfig', { name: selectedConfig.value.label }) : t('pages.admin.newConfig', { name: selectedConfig.value.label })
|
configForm.value.id ? t('pages.admin.editConfig', { name: selectedConfig.value.label }) : t('pages.admin.newConfig', { name: selectedConfig.value.label })
|
||||||
);
|
);
|
||||||
@@ -616,7 +619,7 @@ async function run(action: () => Promise<void>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadConfig() {
|
async function loadConfig() {
|
||||||
await loadLanguages();
|
await Promise.all([loadLanguages(), loadModuleSettings()]);
|
||||||
configRows.value = (await api.config(activeConfigType.value)) as EditableConfig[];
|
configRows.value = (await api.config(activeConfigType.value)) as EditableConfig[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -624,6 +627,10 @@ async function loadLanguages() {
|
|||||||
languageRows.value = await api.adminLanguages();
|
languageRows.value = await api.adminLanguages();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadModuleSettings() {
|
||||||
|
moduleSettings.value = await api.adminModuleSettings();
|
||||||
|
}
|
||||||
|
|
||||||
function resetConfigForm() {
|
function resetConfigForm() {
|
||||||
configForm.value = { id: 0, name: '', description: '', oppositeId: '', translations: {}, hasItemDrop: false, hasTrading: false, changeLog: '' };
|
configForm.value = { id: 0, name: '', description: '', oppositeId: '', translations: {}, hasItemDrop: false, hasTrading: false, changeLog: '' };
|
||||||
}
|
}
|
||||||
@@ -1139,7 +1146,7 @@ async function saveConfig() {
|
|||||||
description: selectedConfig.value.supportsDescription ? configForm.value.description : undefined,
|
description: selectedConfig.value.supportsDescription ? configForm.value.description : undefined,
|
||||||
oppositeId: selectedConfig.value.supportsOpposite && configForm.value.oppositeId ? Number(configForm.value.oppositeId) : null,
|
oppositeId: selectedConfig.value.supportsOpposite && configForm.value.oppositeId ? Number(configForm.value.oppositeId) : null,
|
||||||
hasItemDrop: selectedConfig.value.supportsItemDrop ? configForm.value.hasItemDrop : undefined,
|
hasItemDrop: selectedConfig.value.supportsItemDrop ? configForm.value.hasItemDrop : undefined,
|
||||||
hasTrading: selectedConfig.value.supportsTrading ? configForm.value.hasTrading : undefined,
|
hasTrading: selectedConfig.value.supportsTrading && tradingModuleEnabled.value ? configForm.value.hasTrading : undefined,
|
||||||
changeLog: selectedConfig.value.supportsChangeLog ? configForm.value.changeLog : undefined
|
changeLog: selectedConfig.value.supportsChangeLog ? configForm.value.changeLog : undefined
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1154,6 +1161,14 @@ async function saveConfig() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function saveModuleSettings() {
|
||||||
|
await run(async () => {
|
||||||
|
moduleSettings.value = await api.updateAdminModuleSettings({
|
||||||
|
tradingEnabled: moduleSettings.value.tradingEnabled
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function loadChecklist() {
|
async function loadChecklist() {
|
||||||
await loadLanguages();
|
await loadLanguages();
|
||||||
checklistRows.value = await api.dailyChecklist();
|
checklistRows.value = await api.dailyChecklist();
|
||||||
@@ -2183,6 +2198,21 @@ onMounted(() => {
|
|||||||
{{ t('common.new') }}
|
{{ t('common.new') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<form class="admin-inline-form" @submit.prevent="saveModuleSettings">
|
||||||
|
<div class="detail-section__header">
|
||||||
|
<h3 class="section-subtitle">{{ t('pages.admin.moduleSettings') }}</h3>
|
||||||
|
<button v-if="can('admin.config.update')" type="submit" class="ui-button ui-button--primary ui-button--small" :disabled="busy">
|
||||||
|
<Icon :icon="iconSave" class="ui-icon" aria-hidden="true" />
|
||||||
|
{{ t('common.save') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="check-row">
|
||||||
|
<label>
|
||||||
|
<input v-model="moduleSettings.tradingEnabled" type="checkbox" :disabled="busy || !can('admin.config.update')" />
|
||||||
|
{{ t('pages.admin.tradingModule') }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
<Tabs id="admin-config-type" v-model="activeConfigTab" :tabs="configTabs" :label="t('pages.admin.configType')" />
|
<Tabs id="admin-config-type" v-model="activeConfigTab" :tabs="configTabs" :label="t('pages.admin.configType')" />
|
||||||
<h3 class="section-subtitle">{{ selectedConfig.label }}</h3>
|
<h3 class="section-subtitle">{{ selectedConfig.label }}</h3>
|
||||||
<ReorderableList
|
<ReorderableList
|
||||||
@@ -2203,7 +2233,7 @@ onMounted(() => {
|
|||||||
{{ item.name }}
|
{{ item.name }}
|
||||||
<span v-if="item.opposite" class="config-flag">{{ t('pages.admin.opposite') }}: {{ item.opposite.name }}</span>
|
<span v-if="item.opposite" class="config-flag">{{ t('pages.admin.opposite') }}: {{ item.opposite.name }}</span>
|
||||||
<span v-if="item.hasItemDrop" class="config-flag">{{ t('pages.admin.hasItemDrop') }}</span>
|
<span v-if="item.hasItemDrop" class="config-flag">{{ t('pages.admin.hasItemDrop') }}</span>
|
||||||
<span v-if="item.hasTrading" class="config-flag">{{ t('pages.admin.hasTrading') }}</span>
|
<span v-if="tradingModuleEnabled && item.hasTrading" class="config-flag">{{ t('pages.admin.hasTrading') }}</span>
|
||||||
</span>
|
</span>
|
||||||
<span v-if="item.description" class="meta-line">{{ item.description }}</span>
|
<span v-if="item.description" class="meta-line">{{ item.description }}</span>
|
||||||
<span class="row-actions">
|
<span class="row-actions">
|
||||||
@@ -3054,7 +3084,7 @@ onMounted(() => {
|
|||||||
{{ t('pages.admin.hasItemDrop') }}
|
{{ t('pages.admin.hasItemDrop') }}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="selectedConfig.supportsTrading" class="check-row">
|
<div v-if="selectedConfig.supportsTrading && tradingModuleEnabled" class="check-row">
|
||||||
<label>
|
<label>
|
||||||
<input v-model="configForm.hasTrading" type="checkbox" />
|
<input v-model="configForm.hasTrading" type="checkbox" />
|
||||||
{{ t('pages.admin.hasTrading') }}
|
{{ t('pages.admin.hasTrading') }}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import Skeleton from '../components/Skeleton.vue';
|
|||||||
import Tabs, { type TabOption } from '../components/Tabs.vue';
|
import Tabs, { type TabOption } from '../components/Tabs.vue';
|
||||||
import { iconAdd, iconBack, iconEdit, iconHabitat, iconItem } from '../icons';
|
import { iconAdd, iconBack, iconEdit, iconHabitat, iconItem } from '../icons';
|
||||||
import { applySeo, resolvedSeoHead, resolveSeo } from '../seo';
|
import { applySeo, resolvedSeoHead, resolveSeo } from '../seo';
|
||||||
import { api, type AuthUser, type ItemDetail } from '../services/api';
|
import { api, type AuthUser, type ItemDetail, type ModuleSettings } from '../services/api';
|
||||||
import ItemEdit from './ItemEdit.vue';
|
import ItemEdit from './ItemEdit.vue';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
@@ -73,6 +73,20 @@ const possibleTagEvidenceSections = computed(() => [
|
|||||||
{ key: 'neutral', title: t('pages.pokemon.tradingNeutral'), rows: item.value?.possibleTags?.evidence.neutral ?? [] }
|
{ key: 'neutral', title: t('pages.pokemon.tradingNeutral'), rows: item.value?.possibleTags?.evidence.neutral ?? [] }
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const { data: moduleSettings } = useAsyncData<ModuleSettings>(
|
||||||
|
'module-settings',
|
||||||
|
async () => {
|
||||||
|
try {
|
||||||
|
return await api.moduleSettings();
|
||||||
|
} catch {
|
||||||
|
return { tradingEnabled: true };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ default: () => ({ tradingEnabled: true }) }
|
||||||
|
);
|
||||||
|
|
||||||
|
const tradingModuleEnabled = computed(() => moduleSettings.value?.tradingEnabled ?? true);
|
||||||
|
|
||||||
const { data: initialItem } = useAsyncData<ItemDetail | null>(
|
const { data: initialItem } = useAsyncData<ItemDetail | null>(
|
||||||
`item-detail:${String(route.name)}:${activeItemRouteId() ?? 'none'}:${locale.value}`,
|
`item-detail:${String(route.name)}:${activeItemRouteId() ?? 'none'}:${locale.value}`,
|
||||||
async () => {
|
async () => {
|
||||||
@@ -347,7 +361,7 @@ watch(initialItem, applyInitialItem, { immediate: true });
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DetailSection :title="t('pages.items.possibleTags')">
|
<DetailSection v-if="tradingModuleEnabled" :title="t('pages.items.possibleTags')">
|
||||||
<div class="possible-tags-grid">
|
<div class="possible-tags-grid">
|
||||||
<div v-for="section in possibleTagSections" :key="section.key" class="possible-tags-group">
|
<div v-for="section in possibleTagSections" :key="section.key" class="possible-tags-group">
|
||||||
<h3 class="section-subtitle">{{ section.title }}</h3>
|
<h3 class="section-subtitle">{{ section.title }}</h3>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import StatusMessage from '../components/StatusMessage.vue';
|
|||||||
import Tabs, { type TabOption } from '../components/Tabs.vue';
|
import Tabs, { type TabOption } from '../components/Tabs.vue';
|
||||||
import { iconAdd, iconBack, iconCancel, iconCheck, iconEdit, iconHabitat, iconItem } from '../icons';
|
import { iconAdd, iconBack, iconCancel, iconCheck, iconEdit, iconHabitat, iconItem } from '../icons';
|
||||||
import { applySeo, resolvedSeoHead, resolveSeo } from '../seo';
|
import { applySeo, resolvedSeoHead, resolveSeo } from '../seo';
|
||||||
import { api, type AuthUser, type Item, type PokemonDetail, type PokemonPayload, type TradingPreference } from '../services/api';
|
import { api, type AuthUser, type Item, type ModuleSettings, type PokemonDetail, type PokemonPayload, type TradingPreference } from '../services/api';
|
||||||
import PokemonEdit from './PokemonEdit.vue';
|
import PokemonEdit from './PokemonEdit.vue';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
@@ -59,6 +59,18 @@ const { data: initialPokemon } = useAsyncData<PokemonDetail | null>(
|
|||||||
{ default: () => null }
|
{ default: () => null }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { data: moduleSettings } = useAsyncData<ModuleSettings>(
|
||||||
|
'module-settings',
|
||||||
|
async () => {
|
||||||
|
try {
|
||||||
|
return await api.moduleSettings();
|
||||||
|
} catch {
|
||||||
|
return { tradingEnabled: true };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ default: () => ({ tradingEnabled: true }) }
|
||||||
|
);
|
||||||
|
|
||||||
const initialPokemonLoaded = ref(false);
|
const initialPokemonLoaded = ref(false);
|
||||||
const pokemonSeo = computed(() =>
|
const pokemonSeo = computed(() =>
|
||||||
pokemon.value && route.meta.editorModal !== true
|
pokemon.value && route.meta.editorModal !== true
|
||||||
@@ -180,7 +192,8 @@ const habitatRows = computed<HabitatRow[]>(() => {
|
|||||||
});
|
});
|
||||||
const skillDropRows = computed(() => pokemon.value?.skills.filter((skill) => skill.hasItemDrop) ?? []);
|
const skillDropRows = computed(() => pokemon.value?.skills.filter((skill) => skill.hasItemDrop) ?? []);
|
||||||
const hasItemDropSkill = computed(() => skillDropRows.value.length > 0);
|
const hasItemDropSkill = computed(() => skillDropRows.value.length > 0);
|
||||||
const hasTradingSkill = computed(() => pokemon.value?.skills.some((skill) => skill.hasTrading) === true);
|
const tradingModuleEnabled = computed(() => moduleSettings.value?.tradingEnabled ?? true);
|
||||||
|
const hasTradingSkill = computed(() => tradingModuleEnabled.value && pokemon.value?.skills.some((skill) => skill.hasTrading) === true);
|
||||||
const tradingGroups = computed(() => ({
|
const tradingGroups = computed(() => ({
|
||||||
likes: pokemon.value?.tradingItems.filter((item) => item.preference === 'like') ?? [],
|
likes: pokemon.value?.tradingItems.filter((item) => item.preference === 'like') ?? [],
|
||||||
neutral: pokemon.value?.tradingItems.filter((item) => item.preference === 'neutral') ?? []
|
neutral: pokemon.value?.tradingItems.filter((item) => item.preference === 'neutral') ?? []
|
||||||
@@ -583,7 +596,7 @@ watch([filteredTradingItems, tradingDraftPreferenceByItemId], () => {
|
|||||||
watch(tradingActiveItemIndex, scrollActiveTradingItemIntoView);
|
watch(tradingActiveItemIndex, scrollActiveTradingItemIntoView);
|
||||||
|
|
||||||
async function openTradingModal() {
|
async function openTradingModal() {
|
||||||
if (!pokemon.value) {
|
if (!pokemon.value || !hasTradingSkill.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -145,7 +145,9 @@ const selectedUploadImage = computed(() => (selectedPokemonImage.value?.source =
|
|||||||
const canCreateConfig = computed(() => currentUser.value?.permissions.includes('admin.config.create') === true);
|
const canCreateConfig = computed(() => currentUser.value?.permissions.includes('admin.config.create') === true);
|
||||||
const canFetchPokemon = computed(() => currentUser.value?.permissions.includes('pokemon.fetch') === true);
|
const canFetchPokemon = computed(() => currentUser.value?.permissions.includes('pokemon.fetch') === true);
|
||||||
const canUploadImage = computed(() => currentUser.value?.permissions.includes('pokemon.upload') === true);
|
const canUploadImage = computed(() => currentUser.value?.permissions.includes('pokemon.upload') === true);
|
||||||
const hasTradingSkill = computed(() => pokemonForm.value.skillIds.some((skillId) => skillSupportsTrading(skillId)));
|
const tradingModuleEnabled = computed(() => options.value?.moduleSettings.tradingEnabled ?? true);
|
||||||
|
const selectedTradingSkill = computed(() => pokemonForm.value.skillIds.some((skillId) => skillSupportsTrading(skillId)));
|
||||||
|
const hasTradingSkill = computed(() => tradingModuleEnabled.value && selectedTradingSkill.value);
|
||||||
|
|
||||||
function toIds(values: string[]): number[] {
|
function toIds(values: string[]): number[] {
|
||||||
return values.map(Number).filter((item) => Number.isInteger(item) && item > 0);
|
return values.map(Number).filter((item) => Number.isInteger(item) && item > 0);
|
||||||
@@ -227,7 +229,7 @@ function skillSupportsTrading(skillId: string) {
|
|||||||
|
|
||||||
function syncSkillFeatures() {
|
function syncSkillFeatures() {
|
||||||
syncSkillItemDrops();
|
syncSkillItemDrops();
|
||||||
if (!hasTradingSkill.value) {
|
if (tradingModuleEnabled.value && !hasTradingSkill.value) {
|
||||||
pokemonForm.value.tradingItems = [];
|
pokemonForm.value.tradingItems = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1142,6 +1142,8 @@ export const systemWordingMessages = {
|
|||||||
languages: 'Languages',
|
languages: 'Languages',
|
||||||
newConfig: 'New {name}',
|
newConfig: 'New {name}',
|
||||||
editConfig: 'Edit {name}',
|
editConfig: 'Edit {name}',
|
||||||
|
moduleSettings: 'Module settings',
|
||||||
|
tradingModule: 'Trading',
|
||||||
hasItemDrop: 'Has item drop',
|
hasItemDrop: 'Has item drop',
|
||||||
hasTrading: 'Has trading',
|
hasTrading: 'Has trading',
|
||||||
opposite: 'Opposite',
|
opposite: 'Opposite',
|
||||||
@@ -2572,6 +2574,8 @@ export const systemWordingMessages = {
|
|||||||
languages: '语言',
|
languages: '语言',
|
||||||
newConfig: '新增{name}',
|
newConfig: '新增{name}',
|
||||||
editConfig: '编辑{name}',
|
editConfig: '编辑{name}',
|
||||||
|
moduleSettings: '模块设置',
|
||||||
|
tradingModule: 'Trading',
|
||||||
hasItemDrop: '有掉落物',
|
hasItemDrop: '有掉落物',
|
||||||
hasTrading: '有 Trading',
|
hasTrading: '有 Trading',
|
||||||
opposite: '反义词',
|
opposite: '反义词',
|
||||||
|
|||||||
Reference in New Issue
Block a user