feat: add admin creation entry switches
Some checks failed
CI / verify (pull_request) Has been cancelled
Some checks failed
CI / verify (pull_request) Has been cancelled
This commit is contained in:
@@ -1,4 +1,6 @@
|
|||||||
import type {
|
import type {
|
||||||
|
AdminUpsertCreationEntryTypeConfigRequest,
|
||||||
|
AdminCreationEntryConfigResponse,
|
||||||
AdminDebugHttpRequest,
|
AdminDebugHttpRequest,
|
||||||
AdminDebugHttpResponse,
|
AdminDebugHttpResponse,
|
||||||
AdminDisableProfileRedeemCodeRequest,
|
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) {
|
export function listProfileRedeemCodes(token: string) {
|
||||||
return request<ProfileRedeemCodeAdminListResponse>(
|
return request<ProfileRedeemCodeAdminListResponse>(
|
||||||
'/admin/api/profile/redeem-codes',
|
'/admin/api/profile/redeem-codes',
|
||||||
|
|||||||
@@ -141,6 +141,34 @@ export interface AdminTrackingEventListQuery {
|
|||||||
limit?: number;
|
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 {
|
export interface AdminUpsertProfileRedeemCodeRequest {
|
||||||
code: string;
|
code: string;
|
||||||
mode: ProfileRedeemCodeMode;
|
mode: ProfileRedeemCodeMode;
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
getStoredAdminToken,
|
getStoredAdminToken,
|
||||||
setStoredAdminToken,
|
setStoredAdminToken,
|
||||||
} from '../auth/adminAuthStore';
|
} from '../auth/adminAuthStore';
|
||||||
|
import {AdminCreationEntrySwitchPage} from '../pages/AdminCreationEntrySwitchPage';
|
||||||
import {AdminDebugHttpPage} from '../pages/AdminDebugHttpPage';
|
import {AdminDebugHttpPage} from '../pages/AdminDebugHttpPage';
|
||||||
import {AdminDatabaseTablesPage} from '../pages/AdminDatabaseTablesPage';
|
import {AdminDatabaseTablesPage} from '../pages/AdminDatabaseTablesPage';
|
||||||
import {AdminInviteCodePage} from '../pages/AdminInviteCodePage';
|
import {AdminInviteCodePage} from '../pages/AdminInviteCodePage';
|
||||||
@@ -192,6 +193,12 @@ export function AdminApp() {
|
|||||||
onResultChange={setInviteResult}
|
onResultChange={setInviteResult}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
{routeId === 'creation-entry' ? (
|
||||||
|
<AdminCreationEntrySwitchPage
|
||||||
|
token={token}
|
||||||
|
onUnauthorized={handleUnauthorized}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
{routeId === 'tasks' ? (
|
{routeId === 'tasks' ? (
|
||||||
<AdminTaskConfigPage
|
<AdminTaskConfigPage
|
||||||
result={taskConfigResult}
|
result={taskConfigResult}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
LogOut,
|
LogOut,
|
||||||
ShieldCheck,
|
ShieldCheck,
|
||||||
ListChecks,
|
ListChecks,
|
||||||
|
SlidersHorizontal,
|
||||||
Database,
|
Database,
|
||||||
Table2,
|
Table2,
|
||||||
TicketCheck,
|
TicketCheck,
|
||||||
@@ -31,6 +32,7 @@ const routeIcons = {
|
|||||||
redeem: TicketPercent,
|
redeem: TicketPercent,
|
||||||
invite: TicketCheck,
|
invite: TicketCheck,
|
||||||
tasks: ListChecks,
|
tasks: ListChecks,
|
||||||
|
'creation-entry': SlidersHorizontal,
|
||||||
} satisfies Record<AdminRouteId, typeof LayoutDashboard>;
|
} satisfies Record<AdminRouteId, typeof LayoutDashboard>;
|
||||||
|
|
||||||
export function AdminShell({
|
export function AdminShell({
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ export type AdminRouteId =
|
|||||||
| 'tracking'
|
| 'tracking'
|
||||||
| 'redeem'
|
| 'redeem'
|
||||||
| 'invite'
|
| 'invite'
|
||||||
| 'tasks';
|
| 'tasks'
|
||||||
|
| 'creation-entry';
|
||||||
|
|
||||||
export interface AdminRouteDefinition {
|
export interface AdminRouteDefinition {
|
||||||
id: AdminRouteId;
|
id: AdminRouteId;
|
||||||
@@ -21,6 +22,7 @@ export const adminRoutes: AdminRouteDefinition[] = [
|
|||||||
{id: 'redeem', label: '兑换码', hash: '#redeem'},
|
{id: 'redeem', label: '兑换码', hash: '#redeem'},
|
||||||
{id: 'invite', label: '邀请码', hash: '#invite'},
|
{id: 'invite', label: '邀请码', hash: '#invite'},
|
||||||
{id: 'tasks', label: '任务配置', hash: '#tasks'},
|
{id: 'tasks', label: '任务配置', hash: '#tasks'},
|
||||||
|
{id: 'creation-entry', label: '入口开关', hash: '#creation-entry'},
|
||||||
];
|
];
|
||||||
|
|
||||||
export function resolveAdminRoute(hash: string): AdminRouteId {
|
export function resolveAdminRoute(hash: string): AdminRouteId {
|
||||||
|
|||||||
260
apps/admin-web/src/pages/AdminCreationEntrySwitchPage.tsx
Normal file
260
apps/admin-web/src/pages/AdminCreationEntrySwitchPage.tsx
Normal 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;
|
||||||
|
}
|
||||||
@@ -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。
|
||||||
@@ -18,6 +18,8 @@ use reqwest::Client;
|
|||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_json::{Map, Value};
|
use serde_json::{Map, Value};
|
||||||
use shared_contracts::admin::{
|
use shared_contracts::admin::{
|
||||||
|
AdminCreationEntryConfigResponse, AdminCreationEntryTypeConfigPayload,
|
||||||
|
AdminUpsertCreationEntryTypeConfigRequest,
|
||||||
AdminDatabaseOverviewPayload, AdminDatabaseTableListResponse, AdminDatabaseTableRowPayload,
|
AdminDatabaseOverviewPayload, AdminDatabaseTableListResponse, AdminDatabaseTableRowPayload,
|
||||||
AdminDatabaseTableRowsQuery, AdminDatabaseTableRowsResponse, AdminDatabaseTableStatPayload,
|
AdminDatabaseTableRowsQuery, AdminDatabaseTableRowsResponse, AdminDatabaseTableStatPayload,
|
||||||
AdminDebugHeaderInput, AdminDebugHttpRequest, AdminDebugHttpResponse, AdminLoginRequest,
|
AdminDebugHeaderInput, AdminDebugHttpRequest, AdminDebugHttpResponse, AdminLoginRequest,
|
||||||
@@ -194,6 +196,94 @@ pub async fn admin_list_database_table_rows(
|
|||||||
Ok(json_success_body(Some(&request_context), response))
|
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(
|
pub async fn require_admin_auth(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
mut request: Request,
|
mut request: Request,
|
||||||
@@ -1237,7 +1327,9 @@ mod tests {
|
|||||||
};
|
};
|
||||||
use axum::{http::StatusCode, response::IntoResponse};
|
use axum::{http::StatusCode, response::IntoResponse};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use shared_contracts::admin::{AdminDatabaseTableRowsQuery, AdminTrackingEventListQuery};
|
use shared_contracts::admin::{
|
||||||
|
AdminCreationEntryConfigResponse, AdminCreationEntryTypeConfigPayload,
|
||||||
|
AdminUpsertCreationEntryTypeConfigRequest,AdminDatabaseTableRowsQuery, AdminTrackingEventListQuery};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn normalize_debug_path_rejects_absolute_url() {
|
fn normalize_debug_path_rejects_absolute_url() {
|
||||||
|
|||||||
@@ -15,8 +15,9 @@ use tracing::{Level, Span, error, info, info_span, warn};
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
admin::{
|
admin::{
|
||||||
admin_debug_http, admin_list_database_table_rows, admin_list_database_tables,
|
admin_debug_http, admin_get_creation_entry_config, admin_list_database_table_rows,
|
||||||
admin_list_tracking_events, admin_login, admin_me, admin_overview, require_admin_auth,
|
admin_list_database_tables, admin_list_tracking_events, admin_login, admin_me, admin_overview,
|
||||||
|
admin_upsert_creation_entry_config, require_admin_auth,
|
||||||
},
|
},
|
||||||
ai_tasks::{
|
ai_tasks::{
|
||||||
append_ai_text_chunk, attach_ai_result_reference, cancel_ai_task, complete_ai_stage,
|
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,
|
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(
|
.route(
|
||||||
"/admin/api/profile/redeem-codes",
|
"/admin/api/profile/redeem-codes",
|
||||||
get(admin_list_profile_redeem_codes)
|
get(admin_list_profile_redeem_codes)
|
||||||
|
|||||||
@@ -228,6 +228,27 @@ impl AppState {
|
|||||||
&self.refresh_cookie_config
|
&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(
|
pub async fn get_creation_entry_config(
|
||||||
&self,
|
&self,
|
||||||
) -> Result<CreationEntryConfigResponse, SpacetimeClientError> {
|
) -> Result<CreationEntryConfigResponse, SpacetimeClientError> {
|
||||||
|
|||||||
@@ -88,6 +88,19 @@ pub struct CreationEntryConfigSnapshot {
|
|||||||
pub updated_at_micros: i64,
|
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))]
|
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub struct CreationEntryConfigProcedureResult {
|
pub struct CreationEntryConfigProcedureResult {
|
||||||
|
|||||||
@@ -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)]
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct AdminLoginResponse {
|
pub struct AdminLoginResponse {
|
||||||
|
|||||||
@@ -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 {
|
impl From<module_runtime::RuntimeSettingGetInput> for RuntimeSettingGetInput {
|
||||||
fn from(input: module_runtime::RuntimeSettingGetInput) -> Self {
|
fn from(input: module_runtime::RuntimeSettingGetInput) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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_procedure_result_type;
|
||||||
pub mod creation_entry_config_snapshot_type;
|
pub mod creation_entry_config_snapshot_type;
|
||||||
pub mod creation_entry_start_card_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_config_snapshot_type;
|
||||||
pub mod creation_entry_type_modal_snapshot_type;
|
pub mod creation_entry_type_modal_snapshot_type;
|
||||||
pub mod database_migration_authorize_operator_input_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 update_visual_novel_work_procedure;
|
||||||
pub mod upsert_auth_store_snapshot_procedure;
|
pub mod upsert_auth_store_snapshot_procedure;
|
||||||
pub mod upsert_chapter_progression_and_return_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_chapter_progression_reducer;
|
||||||
pub mod upsert_custom_world_agent_operation_progress_procedure;
|
pub mod upsert_custom_world_agent_operation_progress_procedure;
|
||||||
pub mod upsert_custom_world_profile_and_return_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_procedure_result_type::CreationEntryConfigProcedureResult;
|
||||||
pub use creation_entry_config_snapshot_type::CreationEntryConfigSnapshot;
|
pub use creation_entry_config_snapshot_type::CreationEntryConfigSnapshot;
|
||||||
pub use creation_entry_start_card_snapshot_type::CreationEntryStartCardSnapshot;
|
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_config_snapshot_type::CreationEntryTypeSnapshot;
|
||||||
pub use creation_entry_type_modal_snapshot_type::CreationEntryTypeModalSnapshot;
|
pub use creation_entry_type_modal_snapshot_type::CreationEntryTypeModalSnapshot;
|
||||||
pub use database_migration_authorize_operator_input_type::DatabaseMigrationAuthorizeOperatorInput;
|
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 update_visual_novel_work_procedure::update_visual_novel_work;
|
||||||
pub use upsert_auth_store_snapshot_procedure::upsert_auth_store_snapshot;
|
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_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_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_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;
|
pub use upsert_custom_world_profile_and_return_procedure::upsert_custom_world_profile_and_return;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,25 @@ impl SpacetimeClient {
|
|||||||
.await
|
.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(
|
pub async fn get_runtime_settings(
|
||||||
&self,
|
&self,
|
||||||
user_id: String,
|
user_id: String,
|
||||||
|
|||||||
@@ -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(
|
fn get_or_seed_creation_entry_config_snapshot(
|
||||||
ctx: &ReducerContext,
|
ctx: &ReducerContext,
|
||||||
) -> Result<CreationEntryConfigSnapshot, String> {
|
) -> Result<CreationEntryConfigSnapshot, String> {
|
||||||
|
|||||||
Reference in New Issue
Block a user