master #14

Merged
kdletters merged 226 commits from master into release 2026-05-13 13:23:09 +08:00
18 changed files with 745 additions and 4 deletions
Showing only changes of commit 0c9254502c - Show all commits

View File

@@ -1,4 +1,6 @@
import type {
AdminUpsertCreationEntryTypeConfigRequest,
AdminCreationEntryConfigResponse,
AdminDebugHttpRequest,
AdminDebugHttpResponse,
AdminDisableProfileRedeemCodeRequest,
@@ -167,6 +169,28 @@ export function listAdminTrackingEvents(
);
}
export function getAdminCreationEntryConfig(token: string) {
return request<AdminCreationEntryConfigResponse>(
'/admin/api/creation-entry/config',
{token},
);
}
export function upsertAdminCreationEntryConfig(
token: string,
payload: AdminUpsertCreationEntryTypeConfigRequest,
) {
return request<AdminCreationEntryConfigResponse>(
'/admin/api/creation-entry/config',
{
method: 'POST',
token,
body: payload,
},
);
}
export function listProfileRedeemCodes(token: string) {
return request<ProfileRedeemCodeAdminListResponse>(
'/admin/api/profile/redeem-codes',

View File

@@ -141,6 +141,34 @@ export interface AdminTrackingEventListQuery {
limit?: number;
}
export interface AdminCreationEntryConfigResponse {
entries: AdminCreationEntryTypeConfigPayload[];
}
export interface AdminCreationEntryTypeConfigPayload {
id: string;
title: string;
subtitle: string;
badge: string;
imageSrc: string;
visible: boolean;
open: boolean;
sortOrder: number;
updatedAtMicros: number;
}
export interface AdminUpsertCreationEntryTypeConfigRequest {
id: string;
title: string;
subtitle: string;
badge: string;
imageSrc: string;
visible: boolean;
open: boolean;
sortOrder: number;
}
export interface AdminUpsertProfileRedeemCodeRequest {
code: string;
mode: ProfileRedeemCodeMode;

View File

@@ -17,6 +17,7 @@ import {
getStoredAdminToken,
setStoredAdminToken,
} from '../auth/adminAuthStore';
import {AdminCreationEntrySwitchPage} from '../pages/AdminCreationEntrySwitchPage';
import {AdminDebugHttpPage} from '../pages/AdminDebugHttpPage';
import {AdminDatabaseTablesPage} from '../pages/AdminDatabaseTablesPage';
import {AdminInviteCodePage} from '../pages/AdminInviteCodePage';
@@ -192,6 +193,12 @@ export function AdminApp() {
onResultChange={setInviteResult}
/>
) : null}
{routeId === 'creation-entry' ? (
<AdminCreationEntrySwitchPage
token={token}
onUnauthorized={handleUnauthorized}
/>
) : null}
{routeId === 'tasks' ? (
<AdminTaskConfigPage
result={taskConfigResult}

View File

@@ -4,6 +4,7 @@ import {
LogOut,
ShieldCheck,
ListChecks,
SlidersHorizontal,
Database,
Table2,
TicketCheck,
@@ -31,6 +32,7 @@ const routeIcons = {
redeem: TicketPercent,
invite: TicketCheck,
tasks: ListChecks,
'creation-entry': SlidersHorizontal,
} satisfies Record<AdminRouteId, typeof LayoutDashboard>;
export function AdminShell({

View File

@@ -5,7 +5,8 @@ export type AdminRouteId =
| 'tracking'
| 'redeem'
| 'invite'
| 'tasks';
| 'tasks'
| 'creation-entry';
export interface AdminRouteDefinition {
id: AdminRouteId;
@@ -21,6 +22,7 @@ export const adminRoutes: AdminRouteDefinition[] = [
{id: 'redeem', label: '兑换码', hash: '#redeem'},
{id: 'invite', label: '邀请码', hash: '#invite'},
{id: 'tasks', label: '任务配置', hash: '#tasks'},
{id: 'creation-entry', label: '入口开关', hash: '#creation-entry'},
];
export function resolveAdminRoute(hash: string): AdminRouteId {

View File

@@ -0,0 +1,260 @@
import {RefreshCcw, Save} from 'lucide-react';
import {FormEvent, useEffect, useState} from 'react';
import {
getAdminCreationEntryConfig,
upsertAdminCreationEntryConfig,
} from '../api/adminApiClient';
import type {AdminCreationEntryTypeConfigPayload} from '../api/adminApiTypes';
import {useAdminWriteConfirm} from '../components/useAdminWriteConfirm';
import {handlePageError} from './pageUtils';
interface AdminCreationEntrySwitchPageProps {
token: string;
onUnauthorized: (message?: string) => void;
}
export function AdminCreationEntrySwitchPage({
token,
onUnauthorized,
}: AdminCreationEntrySwitchPageProps) {
const [entries, setEntries] = useState<AdminCreationEntryTypeConfigPayload[]>([]);
const [selectedId, setSelectedId] = useState('puzzle');
const [title, setTitle] = useState('');
const [subtitle, setSubtitle] = useState('');
const [badge, setBadge] = useState('');
const [imageSrc, setImageSrc] = useState('');
const [visible, setVisible] = useState(true);
const [open, setOpen] = useState(true);
const [sortOrder, setSortOrder] = useState('30');
const [isLoading, setIsLoading] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const [listErrorMessage, setListErrorMessage] = useState('');
const [errorMessage, setErrorMessage] = useState('');
const {confirmWrite, confirmDialog} = useAdminWriteConfirm();
useEffect(() => {
void refreshEntries();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [token]);
async function refreshEntries() {
setIsLoading(true);
setListErrorMessage('');
try {
const response = await getAdminCreationEntryConfig(token);
const nextEntries = sortEntries(response.entries);
setEntries(nextEntries);
fillForm(
nextEntries.find((entry) => entry.id === selectedId) ?? nextEntries[0] ?? null,
);
} catch (error: unknown) {
handlePageError(error, onUnauthorized, setListErrorMessage);
} finally {
setIsLoading(false);
}
}
async function handleSave(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
if (isSaving) {
return;
}
const targetId = selectedId.trim();
setErrorMessage('');
const confirmed = await confirmWrite({
action: '保存创作入口开关',
target: targetId,
});
if (!confirmed) {
return;
}
setIsSaving(true);
try {
const response = await upsertAdminCreationEntryConfig(token, {
id: targetId,
title: title.trim(),
subtitle: subtitle.trim(),
badge: badge.trim(),
imageSrc: imageSrc.trim(),
visible,
open,
sortOrder: parseInteger(sortOrder),
});
const nextEntries = sortEntries(response.entries);
setEntries(nextEntries);
fillForm(nextEntries.find((entry) => entry.id === targetId) ?? null);
} catch (error: unknown) {
handlePageError(error, onUnauthorized, setErrorMessage);
} finally {
setIsSaving(false);
}
}
function fillForm(entry: AdminCreationEntryTypeConfigPayload | null) {
if (!entry) {
return;
}
setSelectedId(entry.id);
setTitle(entry.title);
setSubtitle(entry.subtitle);
setBadge(entry.badge);
setImageSrc(entry.imageSrc);
setVisible(entry.visible);
setOpen(entry.open);
setSortOrder(String(entry.sortOrder));
}
return (
<section className="admin-page">
<div className="admin-page-heading">
<div>
<h2></h2>
<p></p>
</div>
<button
className="admin-secondary-button"
disabled={isLoading}
type="button"
onClick={refreshEntries}
>
<RefreshCcw size={17} aria-hidden="true" />
<span>{isLoading ? '刷新中' : '刷新'}</span>
</button>
</div>
{listErrorMessage ? (
<div className="admin-alert" role="status">
{listErrorMessage}
</div>
) : null}
<div className="admin-two-column admin-two-column-wide">
<form className="admin-panel admin-form" onSubmit={handleSave}>
<div className="admin-form-row">
<label className="admin-field admin-field-fill">
<span> ID</span>
<input
value={selectedId}
onChange={(event) => setSelectedId(event.target.value)}
/>
</label>
<label className="admin-switch-field">
<input
checked={visible}
type="checkbox"
onChange={(event) => setVisible(event.target.checked)}
/>
<span></span>
</label>
<label className="admin-switch-field">
<input
checked={open}
type="checkbox"
onChange={(event) => setOpen(event.target.checked)}
/>
<span></span>
</label>
</div>
<div className="admin-form-row">
<label className="admin-field">
<span></span>
<input value={title} onChange={(event) => setTitle(event.target.value)} />
</label>
<label className="admin-field">
<span></span>
<input value={badge} onChange={(event) => setBadge(event.target.value)} />
</label>
</div>
<label className="admin-field">
<span></span>
<input value={subtitle} onChange={(event) => setSubtitle(event.target.value)} />
</label>
<label className="admin-field">
<span></span>
<input value={imageSrc} onChange={(event) => setImageSrc(event.target.value)} />
</label>
<label className="admin-field">
<span></span>
<input
inputMode="numeric"
value={sortOrder}
onChange={(event) => setSortOrder(event.target.value)}
/>
</label>
{errorMessage ? (
<div className="admin-alert" role="status">
{errorMessage}
</div>
) : null}
<div className="admin-form-actions">
<button className="admin-primary-button" disabled={isSaving} type="submit">
<Save size={17} aria-hidden="true" />
<span>{isSaving ? '保存中' : '保存入库'}</span>
</button>
</div>
</form>
<section className="admin-panel">
<div className="admin-table-wrap">
<table className="admin-table admin-table-compact">
<thead>
<tr>
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
{entries.map((entry) => (
<tr key={entry.id}>
<td>
<button
className="admin-link-button"
type="button"
onClick={() => fillForm(entry)}
>
{entry.title || entry.id}
</button>
</td>
<td>{entry.visible ? '是' : '否'}</td>
<td>{entry.open ? '是' : '否'}</td>
<td>{entry.sortOrder}</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
</div>
{confirmDialog}
</section>
);
}
function sortEntries(entries: AdminCreationEntryTypeConfigPayload[]) {
return [...entries].sort((left, right) => {
if (left.sortOrder !== right.sortOrder) {
return left.sortOrder - right.sortOrder;
}
return left.id.localeCompare(right.id);
});
}
function parseInteger(value: string) {
const parsed = Number.parseInt(value, 10);
if (!Number.isFinite(parsed)) {
return 0;
}
return parsed;
}

View File

@@ -0,0 +1,74 @@
# 后台创作入口开关操作入口
日期2026-05-11
## 背景
创作中心入口配置已经迁移到 SpacetimeDB前端创作中心与 api-server 路由熔断共用同一份入口配置。为了让运营/管理员可以调整这张开关表,需要在后台提供显式操作入口,而不是只通过数据库表查询或手工 reducer 操作。
## 后台入口
后台新增导航:
- 名称:入口开关
- hash`#creation-entry`
- 页面:`apps/admin-web/src/pages/AdminCreationEntrySwitchPage.tsx`
页面能力:
- 读取当前入口开关表。
- 编辑单个入口:
- `id`
- `title`
- `subtitle`
- `badge`
- `imageSrc`
- `visible`
- `open`
- `sortOrder`
- 保存前复用后台写操作确认弹窗。
- 保存后重新用后端返回的配置刷新列表。
## API
后台新增受管理员鉴权保护的接口:
```text
GET /admin/api/creation-entry/config
POST /admin/api/creation-entry/config
```
POST body
```json
{
"id": "puzzle",
"title": "拼图",
"subtitle": "拼图关卡创作",
"badge": "可创建",
"imageSrc": "/creation-type-references/puzzle.webp",
"visible": true,
"open": true,
"sortOrder": 30
}
```
响应统一返回当前全量入口列表,方便后台页面直接刷新本地状态。
## 入库链路
```text
Admin Web
-> api-server /admin/api/creation-entry/config
-> AppState::upsert_creation_entry_type_config
-> spacetime-client procedure upsert_creation_entry_type_config
-> spacetime-module creation_entry_type_config 表
```
`visible=false` 会让创作中心不展示对应入口;`open=false` 会让前端展示锁定态api-server 的运行态熔断继续以 `visible && open` 判断路由是否可用。
## 注意
- 前端后台页面只做管理表单,不成为配置事实源。
- `src/config/newWorkEntryConfig.ts` 不应恢复。
- SpacetimeDB client bindings 当前新增了对应临时 binding 文件;后续执行标准 bindings regenerate 时应覆盖并保持同名 procedure/type。

View File

@@ -18,6 +18,8 @@ use reqwest::Client;
use serde::Deserialize;
use serde_json::{Map, Value};
use shared_contracts::admin::{
AdminCreationEntryConfigResponse, AdminCreationEntryTypeConfigPayload,
AdminUpsertCreationEntryTypeConfigRequest,
AdminDatabaseOverviewPayload, AdminDatabaseTableListResponse, AdminDatabaseTableRowPayload,
AdminDatabaseTableRowsQuery, AdminDatabaseTableRowsResponse, AdminDatabaseTableStatPayload,
AdminDebugHeaderInput, AdminDebugHttpRequest, AdminDebugHttpResponse, AdminLoginRequest,
@@ -194,6 +196,94 @@ pub async fn admin_list_database_table_rows(
Ok(json_success_body(Some(&request_context), response))
}
pub async fn admin_get_creation_entry_config(
State(state): State<AppState>,
Extension(request_context): Extension<RequestContext>,
Extension(_admin): Extension<AuthenticatedAdmin>,
) -> Result<Json<Value>, AppError> {
let config = state.get_creation_entry_config().await.map_err(map_admin_spacetime_error)?;
Ok(json_success_body(
Some(&request_context),
AdminCreationEntryConfigResponse {
entries: config
.creation_types
.into_iter()
.map(map_admin_creation_entry_type_config)
.collect(),
},
))
}
pub async fn admin_upsert_creation_entry_config(
State(state): State<AppState>,
Extension(request_context): Extension<RequestContext>,
Extension(_admin): Extension<AuthenticatedAdmin>,
Json(payload): Json<AdminUpsertCreationEntryTypeConfigRequest>,
) -> Result<Json<Value>, AppError> {
let entry = validate_admin_creation_entry_config(payload)?;
let config = state
.upsert_creation_entry_type_config(entry)
.await
.map_err(map_admin_spacetime_error)?;
Ok(json_success_body(
Some(&request_context),
AdminCreationEntryConfigResponse {
entries: config
.creation_types
.into_iter()
.map(map_admin_creation_entry_type_config)
.collect(),
},
))
}
fn map_admin_creation_entry_type_config(
entry: shared_contracts::creation_entry_config::CreationEntryTypeResponse,
) -> AdminCreationEntryTypeConfigPayload {
AdminCreationEntryTypeConfigPayload {
id: entry.id,
title: entry.title,
subtitle: entry.subtitle,
badge: entry.badge,
image_src: entry.image_src,
visible: entry.visible,
open: entry.open,
sort_order: entry.sort_order,
updated_at_micros: entry.updated_at_micros,
}
}
fn validate_admin_creation_entry_config(
payload: AdminUpsertCreationEntryTypeConfigRequest,
) -> Result<module_runtime::CreationEntryTypeAdminUpsertInput, AppError> {
let id = payload.id.trim().to_string();
if id.is_empty() {
return Err(AppError::from_status(StatusCode::BAD_REQUEST).with_message("入口 ID 不能为空"));
}
let title = payload.title.trim().to_string();
if title.is_empty() {
return Err(AppError::from_status(StatusCode::BAD_REQUEST).with_message("入口标题不能为空"));
}
Ok(module_runtime::CreationEntryTypeAdminUpsertInput {
id,
title,
subtitle: payload.subtitle.trim().to_string(),
badge: payload.badge.trim().to_string(),
image_src: payload.image_src.trim().to_string(),
visible: payload.visible,
open: payload.open,
sort_order: payload.sort_order,
})
}
fn map_admin_spacetime_error(error: spacetime_client::SpacetimeClientError) -> AppError {
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(serde_json::json!({
"provider": "spacetimedb",
"message": error.to_string(),
}))
}
pub async fn require_admin_auth(
State(state): State<AppState>,
mut request: Request,
@@ -1237,7 +1327,9 @@ mod tests {
};
use axum::{http::StatusCode, response::IntoResponse};
use serde_json::json;
use shared_contracts::admin::{AdminDatabaseTableRowsQuery, AdminTrackingEventListQuery};
use shared_contracts::admin::{
AdminCreationEntryConfigResponse, AdminCreationEntryTypeConfigPayload,
AdminUpsertCreationEntryTypeConfigRequest,AdminDatabaseTableRowsQuery, AdminTrackingEventListQuery};
#[test]
fn normalize_debug_path_rejects_absolute_url() {

View File

@@ -15,8 +15,9 @@ use tracing::{Level, Span, error, info, info_span, warn};
use crate::{
admin::{
admin_debug_http, admin_list_database_table_rows, admin_list_database_tables,
admin_list_tracking_events, admin_login, admin_me, admin_overview, require_admin_auth,
admin_debug_http, admin_get_creation_entry_config, admin_list_database_table_rows,
admin_list_database_tables, admin_list_tracking_events, admin_login, admin_me, admin_overview,
admin_upsert_creation_entry_config, require_admin_auth,
},
ai_tasks::{
append_ai_text_chunk, attach_ai_result_reference, cancel_ai_task, complete_ai_stage,
@@ -225,6 +226,15 @@ pub fn build_router(state: AppState) -> Router {
require_admin_auth,
)),
)
.route(
"/admin/api/creation-entry/config",
get(admin_get_creation_entry_config)
.post(admin_upsert_creation_entry_config)
.route_layer(middleware::from_fn_with_state(
state.clone(),
require_admin_auth,
)),
)
.route(
"/admin/api/profile/redeem-codes",
get(admin_list_profile_redeem_codes)

View File

@@ -228,6 +228,27 @@ impl AppState {
&self.refresh_cookie_config
}
pub async fn upsert_creation_entry_type_config(
&self,
input: module_runtime::CreationEntryTypeAdminUpsertInput,
) -> Result<CreationEntryConfigResponse, SpacetimeClientError> {
match self
.spacetime_client
.upsert_creation_entry_type_config(input)
.await
{
Ok(config) => {
#[cfg(test)]
self.cache_test_creation_entry_config(config.clone());
Ok(config)
}
#[cfg(test)]
Err(_) => Ok(self.read_test_creation_entry_config()),
#[cfg(not(test))]
Err(error) => Err(error),
}
}
pub async fn get_creation_entry_config(
&self,
) -> Result<CreationEntryConfigResponse, SpacetimeClientError> {

View File

@@ -88,6 +88,19 @@ pub struct CreationEntryConfigSnapshot {
pub updated_at_micros: i64,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CreationEntryTypeAdminUpsertInput {
pub id: String,
pub title: String,
pub subtitle: String,
pub badge: String,
pub image_src: String,
pub visible: bool,
pub open: bool,
pub sort_order: i32,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CreationEntryConfigProcedureResult {

View File

@@ -10,6 +10,44 @@ pub struct AdminLoginRequest {
}
// 登录成功后返回管理员访问令牌与基础会话信息。
/// 后台创作入口开关列表响应。
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AdminCreationEntryConfigResponse {
pub entries: Vec<AdminCreationEntryTypeConfigPayload>,
}
/// 后台单个创作入口开关配置。
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AdminCreationEntryTypeConfigPayload {
pub id: String,
pub title: String,
pub subtitle: String,
pub badge: String,
pub image_src: String,
pub visible: bool,
pub open: bool,
pub sort_order: i32,
pub updated_at_micros: i64,
}
/// 后台保存创作入口开关配置请求。
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AdminUpsertCreationEntryTypeConfigRequest {
pub id: String,
pub title: String,
pub subtitle: String,
pub badge: String,
pub image_src: String,
pub visible: bool,
pub open: bool,
pub sort_order: i32,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AdminLoginResponse {

View File

@@ -46,6 +46,23 @@ impl From<module_assets::AssetHistoryListInput> for AssetHistoryListInput {
}
}
impl From<module_runtime::CreationEntryTypeAdminUpsertInput>
for CreationEntryTypeAdminUpsertInput
{
fn from(input: module_runtime::CreationEntryTypeAdminUpsertInput) -> Self {
Self {
id: input.id,
title: input.title,
subtitle: input.subtitle,
badge: input.badge,
image_src: input.image_src,
visible: input.visible,
open: input.open,
sort_order: input.sort_order,
}
}
}
impl From<module_runtime::RuntimeSettingGetInput> for RuntimeSettingGetInput {
fn from(input: module_runtime::RuntimeSettingGetInput) -> Self {
Self {

View File

@@ -0,0 +1,22 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
pub struct CreationEntryTypeAdminUpsertInput {
pub id: String,
pub title: String,
pub subtitle: String,
pub badge: String,
pub image_src: String,
pub visible: bool,
pub open: bool,
pub sort_order: i32,
}
impl __sdk::InModule for CreationEntryTypeAdminUpsertInput {
type Module = super::RemoteModule;
}

View File

@@ -252,6 +252,7 @@ pub mod custom_world_works_list_result_type;
pub mod creation_entry_config_procedure_result_type;
pub mod creation_entry_config_snapshot_type;
pub mod creation_entry_start_card_snapshot_type;
pub mod creation_entry_type_admin_upsert_input_type;
pub mod creation_entry_type_config_snapshot_type;
pub mod creation_entry_type_modal_snapshot_type;
pub mod database_migration_authorize_operator_input_type;
@@ -758,6 +759,7 @@ pub mod update_square_hole_work_procedure;
pub mod update_visual_novel_work_procedure;
pub mod upsert_auth_store_snapshot_procedure;
pub mod upsert_chapter_progression_and_return_procedure;
pub mod upsert_creation_entry_type_config_procedure;
pub mod upsert_chapter_progression_reducer;
pub mod upsert_custom_world_agent_operation_progress_procedure;
pub mod upsert_custom_world_profile_and_return_procedure;
@@ -1054,6 +1056,7 @@ pub use custom_world_works_list_result_type::CustomWorldWorksListResult;
pub use creation_entry_config_procedure_result_type::CreationEntryConfigProcedureResult;
pub use creation_entry_config_snapshot_type::CreationEntryConfigSnapshot;
pub use creation_entry_start_card_snapshot_type::CreationEntryStartCardSnapshot;
pub use creation_entry_type_admin_upsert_input_type::CreationEntryTypeAdminUpsertInput;
pub use creation_entry_type_config_snapshot_type::CreationEntryTypeSnapshot;
pub use creation_entry_type_modal_snapshot_type::CreationEntryTypeModalSnapshot;
pub use database_migration_authorize_operator_input_type::DatabaseMigrationAuthorizeOperatorInput;
@@ -1560,6 +1563,7 @@ pub use update_square_hole_work_procedure::update_square_hole_work;
pub use update_visual_novel_work_procedure::update_visual_novel_work;
pub use upsert_auth_store_snapshot_procedure::upsert_auth_store_snapshot;
pub use upsert_chapter_progression_and_return_procedure::upsert_chapter_progression_and_return;
pub use upsert_creation_entry_type_config_procedure::upsert_creation_entry_type_config;
pub use upsert_chapter_progression_reducer::upsert_chapter_progression;
pub use upsert_custom_world_agent_operation_progress_procedure::upsert_custom_world_agent_operation_progress;
pub use upsert_custom_world_profile_and_return_procedure::upsert_custom_world_profile_and_return;

View File

@@ -0,0 +1,57 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
use super::creation_entry_config_procedure_result_type::CreationEntryConfigProcedureResult;
use super::creation_entry_type_admin_upsert_input_type::CreationEntryTypeAdminUpsertInput;
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
struct UpsertCreationEntryTypeConfigArgs {
pub input: CreationEntryTypeAdminUpsertInput,
}
impl __sdk::InModule for UpsertCreationEntryTypeConfigArgs {
type Module = super::RemoteModule;
}
#[allow(non_camel_case_types)]
/// Extension trait for access to the procedure `upsert_creation_entry_type_config`.
///
/// Implemented for [`super::RemoteProcedures`].
pub trait upsert_creation_entry_type_config {
fn upsert_creation_entry_type_config(&self, input: CreationEntryTypeAdminUpsertInput) {
self.upsert_creation_entry_type_config_then(input, |_, _| {});
}
fn upsert_creation_entry_type_config_then(
&self,
input: CreationEntryTypeAdminUpsertInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<CreationEntryConfigProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
impl upsert_creation_entry_type_config for super::RemoteProcedures {
fn upsert_creation_entry_type_config_then(
&self,
input: CreationEntryTypeAdminUpsertInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<CreationEntryConfigProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, CreationEntryConfigProcedureResult>(
"upsert_creation_entry_type_config",
UpsertCreationEntryTypeConfigArgs { input },
__callback,
);
}
}

View File

@@ -17,6 +17,25 @@ impl SpacetimeClient {
.await
}
pub async fn upsert_creation_entry_type_config(
&self,
input: module_runtime::CreationEntryTypeAdminUpsertInput,
) -> Result<CreationEntryConfigRecord, SpacetimeClientError> {
let procedure_input: CreationEntryTypeAdminUpsertInput = input.into();
self.call_after_connect(move |connection, sender| {
connection.procedures().upsert_creation_entry_type_config_then(
procedure_input,
move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_creation_entry_config_procedure_result);
send_once(&sender, mapped);
},
);
})
.await
}
pub async fn get_runtime_settings(
&self,
user_id: String,

View File

@@ -48,6 +48,57 @@ pub fn get_creation_entry_config(
}
}
#[spacetimedb::procedure]
pub fn upsert_creation_entry_type_config(
ctx: &mut ProcedureContext,
input: CreationEntryTypeAdminUpsertInput,
) -> CreationEntryConfigProcedureResult {
match ctx.try_with_tx(|tx| upsert_creation_entry_type_config_in_tx(tx, input.clone())) {
Ok(record) => CreationEntryConfigProcedureResult {
ok: true,
record: Some(record),
error_message: None,
},
Err(message) => CreationEntryConfigProcedureResult {
ok: false,
record: None,
error_message: Some(message),
},
}
}
fn upsert_creation_entry_type_config_in_tx(
ctx: &ReducerContext,
input: CreationEntryTypeAdminUpsertInput,
) -> Result<CreationEntryConfigSnapshot, String> {
seed_creation_entry_config_if_missing(ctx);
let now = ctx.timestamp;
let id = input.id.trim().to_string();
if id.is_empty() {
return Err("入口 ID 不能为空".to_string());
}
if input.title.trim().is_empty() {
return Err("入口标题不能为空".to_string());
}
let row = CreationEntryTypeConfig {
id: id.clone(),
title: input.title.trim().to_string(),
subtitle: input.subtitle.trim().to_string(),
badge: input.badge.trim().to_string(),
image_src: input.image_src.trim().to_string(),
visible: input.visible,
open: input.open,
sort_order: input.sort_order,
updated_at: now,
};
if ctx.db.creation_entry_type_config().id().find(&id).is_some() {
ctx.db.creation_entry_type_config().id().update(row);
} else {
ctx.db.creation_entry_type_config().insert(row);
}
get_or_seed_creation_entry_config_snapshot(ctx)
}
fn get_or_seed_creation_entry_config_snapshot(
ctx: &ReducerContext,
) -> Result<CreationEntryConfigSnapshot, String> {