feat: add campaigns module with CRUD operations and UI integration
This commit is contained in:
27
src/api.ts
27
src/api.ts
@@ -76,15 +76,32 @@ export type MediaAsset = {
|
||||
updatedAt?: string;
|
||||
};
|
||||
|
||||
export type Campaign = {
|
||||
id: string;
|
||||
slug: string;
|
||||
title: string;
|
||||
description: string | null;
|
||||
coverImage: string | null;
|
||||
priceAmount: number | null;
|
||||
priceUnit: string | null;
|
||||
tags: string[];
|
||||
status: "draft" | "published";
|
||||
startsAt: string | null;
|
||||
endsAt: string | null;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
};
|
||||
|
||||
export type SiteConfig = {
|
||||
heroSlides: Array<{ id: string; title: string; kicker: string | null; image: string | null; isActive: boolean; sortOrder: number; createdAt?: string; updatedAt?: string }>;
|
||||
destinations: Destination[];
|
||||
map: Array<{ id: string; image: string | null; isActive: boolean; createdAt?: string; updatedAt?: string }>;
|
||||
themes: Array<{ id: string; label: string; image: string; targetType?: string | null; targetValue?: string | null; isActive: boolean }>;
|
||||
campaigns: Campaign[];
|
||||
ctaBanners: Array<{ id: string; alt: string; image: string; targetType: string; targetValue?: string | null; isActive: boolean }>;
|
||||
};
|
||||
|
||||
export type SiteModule = "heroSlides" | "destinations" | "map" | "themes" | "ctaBanners";
|
||||
export type SiteModule = "heroSlides" | "destinations" | "map" | "themes" | "campaigns" | "ctaBanners";
|
||||
export type SiteConfigItem = SiteConfig[SiteModule][number];
|
||||
type SiteConfigItemResponse = SiteConfigItem | { item: SiteConfigItem };
|
||||
|
||||
@@ -97,11 +114,19 @@ export type SiteItemPatch = {
|
||||
label?: string;
|
||||
alt?: string;
|
||||
image?: string | null;
|
||||
description?: string | null;
|
||||
coverImage?: string | null;
|
||||
priceAmount?: number | null;
|
||||
priceUnit?: string | null;
|
||||
tags?: string[];
|
||||
targetType?: string | null;
|
||||
targetValue?: string | null;
|
||||
isHot?: boolean;
|
||||
isActive?: boolean;
|
||||
sortOrder?: number;
|
||||
status?: "draft" | "published";
|
||||
startsAt?: string | null;
|
||||
endsAt?: string | null;
|
||||
};
|
||||
|
||||
export type ProductInput = {
|
||||
|
||||
@@ -131,9 +131,23 @@ export function siteItemPrimaryKey(moduleId: SiteModule): keyof SiteItemPatch {
|
||||
if (moduleId === "destinations") return "name";
|
||||
if (moduleId === "map") return "image";
|
||||
if (moduleId === "themes") return "label";
|
||||
if (moduleId === "campaigns") return "title";
|
||||
return "alt";
|
||||
}
|
||||
|
||||
function createCampaignSlug(title: string) {
|
||||
const slug = title
|
||||
.trim()
|
||||
.normalize("NFKD")
|
||||
.toLowerCase()
|
||||
.replace(/[\u0300-\u036f]/g, "")
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/^-+|-+$/g, "")
|
||||
.slice(0, 64);
|
||||
|
||||
return slug || `campaign-${Date.now().toString(36)}`;
|
||||
}
|
||||
|
||||
export function createEmptySiteItemDraft(moduleId: SiteModule, sortOrder: number): SiteItemPatch {
|
||||
if (moduleId === "heroSlides") {
|
||||
return { title: "", kicker: "", image: "", isActive: false, sortOrder };
|
||||
@@ -147,6 +161,20 @@ export function createEmptySiteItemDraft(moduleId: SiteModule, sortOrder: number
|
||||
if (moduleId === "themes") {
|
||||
return { label: "", image: "", isActive: false, sortOrder };
|
||||
}
|
||||
if (moduleId === "campaigns") {
|
||||
return {
|
||||
title: "",
|
||||
slug: "",
|
||||
description: "",
|
||||
coverImage: "",
|
||||
priceAmount: null,
|
||||
priceUnit: "起/人",
|
||||
tags: [],
|
||||
status: "draft",
|
||||
startsAt: "",
|
||||
endsAt: "",
|
||||
};
|
||||
}
|
||||
return { alt: "", image: "", isActive: false, sortOrder };
|
||||
}
|
||||
|
||||
@@ -174,6 +202,23 @@ export function compactSiteItemPayload(moduleId: SiteModule, draft: SiteItemPatc
|
||||
if (moduleId === "themes") {
|
||||
payload.label = draft.label?.trim();
|
||||
}
|
||||
if (moduleId === "campaigns") {
|
||||
payload.title = draft.title?.trim();
|
||||
payload.slug = draft.slug?.trim() || createCampaignSlug(payload.title || "");
|
||||
payload.description = draft.description?.trim() || null;
|
||||
payload.coverImage = draft.coverImage?.trim() || null;
|
||||
payload.priceAmount = draft.priceAmount === null || draft.priceAmount === undefined ? null : Number(draft.priceAmount);
|
||||
payload.priceUnit = draft.priceUnit?.trim() || "起/人";
|
||||
payload.tags = (draft.tags ?? []).map((tag) => tag.trim()).filter(Boolean).slice(0, 3);
|
||||
payload.status = draft.status === "published" ? "published" : "draft";
|
||||
payload.startsAt = undefined;
|
||||
payload.endsAt = undefined;
|
||||
payload.image = undefined;
|
||||
payload.isActive = undefined;
|
||||
payload.sortOrder = undefined;
|
||||
payload.targetType = undefined;
|
||||
payload.targetValue = undefined;
|
||||
}
|
||||
if (moduleId === "ctaBanners") {
|
||||
payload.alt = draft.alt?.trim();
|
||||
}
|
||||
@@ -204,17 +249,35 @@ export function itemName(item: EditableSiteItem) {
|
||||
return "地图图片";
|
||||
}
|
||||
|
||||
export function itemMeta(item: EditableSiteItem) {
|
||||
export function itemMeta(item: EditableSiteItem): string {
|
||||
if ("kicker" in item) return item.kicker || "轮播";
|
||||
if ("aliases" in item) return item.region || item.aliases?.map((alias) => alias.alias).join(" / ") || "目的地";
|
||||
if ("priceAmount" in item) {
|
||||
const priceUnit = "priceUnit" in item && typeof item.priceUnit === "string" ? item.priceUnit : "";
|
||||
const description = "description" in item && typeof item.description === "string" ? item.description : "";
|
||||
const status = "status" in item && typeof item.status === "string" ? item.status : "";
|
||||
return typeof item.priceAmount === "number" ? `¥${item.priceAmount}${priceUnit}` : description || status;
|
||||
}
|
||||
if ("description" in item) {
|
||||
const description = typeof item.description === "string" ? item.description : "";
|
||||
const status = "status" in item && typeof item.status === "string" ? item.status : "";
|
||||
return description || status;
|
||||
}
|
||||
if (!("title" in item) && !("name" in item) && !("label" in item) && !("alt" in item)) return "地图素材";
|
||||
return "";
|
||||
}
|
||||
|
||||
export function itemImage(item: EditableSiteItem) {
|
||||
if ("coverImage" in item) return item.coverImage || "";
|
||||
return item.image || "";
|
||||
}
|
||||
|
||||
export function itemTags(item: EditableSiteItem) {
|
||||
return "tags" in item && Array.isArray(item.tags)
|
||||
? item.tags.filter((tag): tag is string => typeof tag === "string").slice(0, 3)
|
||||
: [];
|
||||
}
|
||||
|
||||
export function itemSortOrder(item: EditableSiteItem) {
|
||||
return "sortOrder" in item ? item.sortOrder : undefined;
|
||||
}
|
||||
@@ -224,5 +287,5 @@ export function moduleItems(config: SiteConfig, moduleId: SiteModule): EditableS
|
||||
}
|
||||
|
||||
export function moduleEditable(moduleId: ModuleId): moduleId is SiteModule {
|
||||
return moduleId === "heroSlides" || moduleId === "destinations" || moduleId === "map" || moduleId === "themes" || moduleId === "ctaBanners";
|
||||
return moduleId === "heroSlides" || moduleId === "destinations" || moduleId === "map" || moduleId === "themes" || moduleId === "campaigns" || moduleId === "ctaBanners";
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import { SingleImageUploader } from "@/components/admin/SingleImageUploader";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import {
|
||||
compactSiteItemPayload,
|
||||
createEmptySiteItemDraft,
|
||||
@@ -36,6 +37,7 @@ import {
|
||||
itemMeta,
|
||||
itemName,
|
||||
itemSortOrder,
|
||||
itemTags,
|
||||
moduleEditable,
|
||||
moduleItems,
|
||||
siteItemPrimaryKey,
|
||||
@@ -93,6 +95,12 @@ const pageSpecs: {
|
||||
hint: "控制横向主题卡和搜索跳转。",
|
||||
frontPosition: "首页「主题甄选」",
|
||||
},
|
||||
{
|
||||
id: "campaigns",
|
||||
label: "特价优惠",
|
||||
hint: "维护活动标题、封面和发布状态,供首页特价优惠入口使用。",
|
||||
frontPosition: "首页「特价优惠」",
|
||||
},
|
||||
{
|
||||
id: "routeProducts",
|
||||
label: "精选线路",
|
||||
@@ -218,6 +226,9 @@ export function StructurePage({
|
||||
const editableItems =
|
||||
config && moduleEditable(moduleId) ? moduleItems(config, moduleId) : [];
|
||||
const isMapModule = moduleId === "map";
|
||||
const isCampaignModule = moduleId === "campaigns";
|
||||
const canReorderSiteItems =
|
||||
moduleEditable(moduleId) && !isMapModule && !isCampaignModule;
|
||||
const canCreateSiteItem =
|
||||
moduleEditable(moduleId) && (!isMapModule || editableItems.length === 0);
|
||||
const isCreatingSiteItem = selectedId === NEW_SITE_ITEM_ID;
|
||||
@@ -246,9 +257,17 @@ export function StructurePage({
|
||||
label: "label" in item ? item.label : undefined,
|
||||
alt: "alt" in item ? item.alt : undefined,
|
||||
image: "image" in item ? item.image || "" : "",
|
||||
description: "description" in item ? item.description || "" : undefined,
|
||||
coverImage: "coverImage" in item ? item.coverImage || "" : undefined,
|
||||
priceAmount: "priceAmount" in item ? item.priceAmount : undefined,
|
||||
priceUnit: "priceUnit" in item ? item.priceUnit || "起/人" : undefined,
|
||||
tags: "tags" in item ? item.tags.slice(0, 3) : undefined,
|
||||
isHot: "isHot" in item ? item.isHot : undefined,
|
||||
isActive: item.isActive,
|
||||
isActive: "isActive" in item ? item.isActive : undefined,
|
||||
sortOrder: itemSortOrder(item),
|
||||
status: "status" in item && (item.status === "draft" || item.status === "published") ? item.status : undefined,
|
||||
startsAt: "startsAt" in item ? item.startsAt || "" : undefined,
|
||||
endsAt: "endsAt" in item ? item.endsAt || "" : undefined,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -367,11 +386,12 @@ export function StructurePage({
|
||||
const messageText =
|
||||
moduleId === "map"
|
||||
? "请先上传贵州地图图片。"
|
||||
: moduleId === "campaigns"
|
||||
? "请先填写特价优惠标题。"
|
||||
: "请先填写当前模块的主标题/名称。";
|
||||
notify({ tone: "warning", title: "内容未保存", message: messageText });
|
||||
return;
|
||||
}
|
||||
|
||||
setSaving(true);
|
||||
setMessage("");
|
||||
|
||||
@@ -621,17 +641,24 @@ export function StructurePage({
|
||||
<b>{itemName(item)}</b>
|
||||
<small>
|
||||
{itemMeta(item)}
|
||||
{!isMapModule && typeof itemSortOrder(item) === "number"
|
||||
{canReorderSiteItems && typeof itemSortOrder(item) === "number"
|
||||
? ` · 排序 ${itemSortOrder(item)}`
|
||||
: ""}
|
||||
</small>
|
||||
{itemTags(item).length ? (
|
||||
<span className="mapped-list-tags">
|
||||
{itemTags(item).map((tag) => (
|
||||
<em key={tag}>{tag}</em>
|
||||
))}
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
</Button>
|
||||
<div
|
||||
className="mapped-list-actions"
|
||||
aria-label={`${itemName(item)} 操作`}
|
||||
>
|
||||
{!isMapModule ? (
|
||||
{canReorderSiteItems ? (
|
||||
<>
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -715,6 +742,67 @@ export function StructurePage({
|
||||
);
|
||||
}
|
||||
|
||||
function CampaignTagEditor({
|
||||
tags,
|
||||
onChange,
|
||||
}: {
|
||||
tags: string[];
|
||||
onChange: (tags: string[]) => void;
|
||||
}) {
|
||||
const normalizedTags = tags.slice(0, 3);
|
||||
const updateTag = (index: number, value: string) => {
|
||||
onChange(
|
||||
normalizedTags.map((tag, tagIndex) =>
|
||||
tagIndex === index ? value : tag,
|
||||
),
|
||||
);
|
||||
};
|
||||
const removeTag = (index: number) => {
|
||||
onChange(normalizedTags.filter((_, tagIndex) => tagIndex !== index));
|
||||
};
|
||||
const addTag = () => {
|
||||
if (normalizedTags.length >= 3) return;
|
||||
onChange([...normalizedTags, ""]);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="tag-editor compact-tag-editor">
|
||||
{normalizedTags.map((tag, index) => (
|
||||
<label className="tag-input-row" key={`${index}-${tag}`}>
|
||||
标签 {index + 1}
|
||||
<span>
|
||||
<Input
|
||||
value={tag}
|
||||
onChange={(event) => updateTag(index, event.target.value)}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="icon-action danger-icon"
|
||||
onClick={() => removeTag(index)}
|
||||
aria-label={`删除标签 ${index + 1}`}
|
||||
>
|
||||
<X size={16} />
|
||||
</Button>
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="inline-action"
|
||||
onClick={addTag}
|
||||
disabled={normalizedTags.length >= 3}
|
||||
>
|
||||
<Plus size={16} />
|
||||
添加标签
|
||||
</Button>
|
||||
<p className="field-help">最多维护 3 个标签,保存时会自动去掉空标签。</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ReferencePanel({
|
||||
moduleId,
|
||||
products,
|
||||
@@ -792,13 +880,20 @@ function SiteItemEditor({
|
||||
? "地图图片"
|
||||
: moduleId === "themes"
|
||||
? "主题名称"
|
||||
: moduleId === "campaigns"
|
||||
? "活动标题"
|
||||
: "入口文案";
|
||||
const editorTitle = `${isCreating ? "新增" : "编辑"}${moduleLabel}`;
|
||||
const isImageOnlyModule = moduleId === "map";
|
||||
const isCampaignModule = moduleId === "campaigns";
|
||||
const editorHint = isImageOnlyModule
|
||||
? isCreating
|
||||
? `上传后会作为「${moduleLabel}」唯一展示图片`
|
||||
: `保存后会替换「${moduleLabel}」前台展示图片`
|
||||
: isCampaignModule
|
||||
? isCreating
|
||||
? `创建后会进入「${moduleLabel}」的活动列表`
|
||||
: `保存后会影响「${moduleLabel}」活动入口`
|
||||
: isCreating
|
||||
? `创建后会进入「${moduleLabel}」的数据列表`
|
||||
: `保存后会影响「${moduleLabel}」前台模块`;
|
||||
@@ -822,68 +917,152 @@ function SiteItemEditor({
|
||||
<div className="admin-disclosure-stack">
|
||||
{!isImageOnlyModule ? (
|
||||
<AdminDisclosure title="展示内容">
|
||||
<label>
|
||||
{primaryLabel}
|
||||
<Input
|
||||
value={String(draft[primaryKey] ?? "")}
|
||||
onChange={(event) =>
|
||||
onDraft({ ...draft, [primaryKey]: event.target.value })
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
{moduleId === "heroSlides" ? (
|
||||
{isCampaignModule ? (
|
||||
<>
|
||||
<label>
|
||||
{primaryLabel}
|
||||
<Input
|
||||
value={draft.title ?? ""}
|
||||
onChange={(event) =>
|
||||
onDraft({ ...draft, title: event.target.value })
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
活动描述
|
||||
<Textarea
|
||||
value={draft.description ?? ""}
|
||||
onChange={(event) =>
|
||||
onDraft({ ...draft, description: event.target.value })
|
||||
}
|
||||
rows={3}
|
||||
/>
|
||||
</label>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<label>
|
||||
{primaryLabel}
|
||||
<Input
|
||||
value={String(draft[primaryKey] ?? "")}
|
||||
onChange={(event) =>
|
||||
onDraft({ ...draft, [primaryKey]: event.target.value })
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
{moduleId === "heroSlides" ? (
|
||||
<label>
|
||||
副标题
|
||||
<Input
|
||||
value={draft.kicker ?? ""}
|
||||
onChange={(event) =>
|
||||
onDraft({ ...draft, kicker: event.target.value })
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</AdminDisclosure>
|
||||
) : null}
|
||||
{isCampaignModule ? (
|
||||
<AdminDisclosure title="价格信息">
|
||||
<div className="form-grid compact-grid">
|
||||
<label>
|
||||
副标题
|
||||
价格
|
||||
<Input
|
||||
value={draft.kicker ?? ""}
|
||||
type="number"
|
||||
value={draft.priceAmount ?? ""}
|
||||
onChange={(event) =>
|
||||
onDraft({ ...draft, kicker: event.target.value })
|
||||
onDraft({
|
||||
...draft,
|
||||
priceAmount: event.target.value
|
||||
? Number(event.target.value)
|
||||
: null,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
) : null}
|
||||
<label>
|
||||
价格单位
|
||||
<Input
|
||||
value={draft.priceUnit ?? "起/人"}
|
||||
onChange={(event) =>
|
||||
onDraft({ ...draft, priceUnit: event.target.value })
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</AdminDisclosure>
|
||||
) : null}
|
||||
<AdminDisclosure title={isImageOnlyModule ? primaryLabel : "资源图片"}>
|
||||
{isCampaignModule ? (
|
||||
<AdminDisclosure title="活动标签">
|
||||
<CampaignTagEditor
|
||||
tags={draft.tags ?? []}
|
||||
onChange={(tags) => onDraft({ ...draft, tags })}
|
||||
/>
|
||||
</AdminDisclosure>
|
||||
) : null}
|
||||
<AdminDisclosure title={isImageOnlyModule ? primaryLabel : isCampaignModule ? "活动封面图" : "资源图片"}>
|
||||
<SingleImageUploader
|
||||
value={draft.image}
|
||||
onChange={(image) => onDraft({ ...draft, image })}
|
||||
value={isCampaignModule ? draft.coverImage : draft.image}
|
||||
onChange={(image) =>
|
||||
onDraft(
|
||||
isCampaignModule
|
||||
? { ...draft, coverImage: image }
|
||||
: { ...draft, image },
|
||||
)
|
||||
}
|
||||
group={moduleId}
|
||||
/>
|
||||
</AdminDisclosure>
|
||||
<AdminDisclosure title={isImageOnlyModule ? "显示状态" : "排序与状态"}>
|
||||
{!isImageOnlyModule ? (
|
||||
<label>
|
||||
排序值
|
||||
<Input
|
||||
type="number"
|
||||
value={draft.sortOrder ?? 0}
|
||||
onChange={(event) =>
|
||||
onDraft({ ...draft, sortOrder: Number(event.target.value) })
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
) : null}
|
||||
{moduleId === "destinations" ? (
|
||||
<AdminDisclosure title={isImageOnlyModule || isCampaignModule ? "显示状态" : "排序与状态"}>
|
||||
{isCampaignModule ? (
|
||||
<label className="switch-line">
|
||||
<Switch
|
||||
checked={Boolean(draft.isHot)}
|
||||
checked={draft.status === "published"}
|
||||
onCheckedChange={(checked) =>
|
||||
onDraft({ ...draft, isHot: checked })
|
||||
onDraft({ ...draft, status: checked ? "published" : "draft" })
|
||||
}
|
||||
/>
|
||||
设为热门
|
||||
前台启用
|
||||
</label>
|
||||
) : null}
|
||||
<label className="switch-line">
|
||||
<Switch
|
||||
checked={Boolean(draft.isActive)}
|
||||
onCheckedChange={(checked) =>
|
||||
onDraft({ ...draft, isActive: checked })
|
||||
}
|
||||
/>
|
||||
前台启用
|
||||
</label>
|
||||
) : (
|
||||
<>
|
||||
{!isImageOnlyModule ? (
|
||||
<label>
|
||||
排序值
|
||||
<Input
|
||||
type="number"
|
||||
value={draft.sortOrder ?? 0}
|
||||
onChange={(event) =>
|
||||
onDraft({ ...draft, sortOrder: Number(event.target.value) })
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
) : null}
|
||||
{moduleId === "destinations" ? (
|
||||
<label className="switch-line">
|
||||
<Switch
|
||||
checked={Boolean(draft.isHot)}
|
||||
onCheckedChange={(checked) =>
|
||||
onDraft({ ...draft, isHot: checked })
|
||||
}
|
||||
/>
|
||||
设为热门
|
||||
</label>
|
||||
) : null}
|
||||
<label className="switch-line">
|
||||
<Switch
|
||||
checked={Boolean(draft.isActive)}
|
||||
onCheckedChange={(checked) =>
|
||||
onDraft({ ...draft, isActive: checked })
|
||||
}
|
||||
/>
|
||||
前台启用
|
||||
</label>
|
||||
</>
|
||||
)}
|
||||
</AdminDisclosure>
|
||||
</div>
|
||||
<footer className="drawer-editor-footer">
|
||||
|
||||
@@ -794,6 +794,32 @@ textarea {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.mapped-list-main > span {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.mapped-list-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.mapped-list-tags em {
|
||||
max-width: 88px;
|
||||
overflow: hidden;
|
||||
padding: 1px 5px;
|
||||
border: 1px solid #84c9c4;
|
||||
border-radius: 3px;
|
||||
color: #247d79;
|
||||
font-size: 11px;
|
||||
font-style: normal;
|
||||
line-height: 1.25;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mapped-list-actions {
|
||||
display: inline-flex;
|
||||
gap: 4px;
|
||||
@@ -1418,6 +1444,10 @@ textarea {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.compact-tag-editor .inline-action {
|
||||
justify-self: start;
|
||||
}
|
||||
|
||||
.tag-input-row span {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 38px;
|
||||
|
||||
@@ -19,4 +19,5 @@ export type EditableSiteItem =
|
||||
| SiteConfig["destinations"][number]
|
||||
| SiteConfig["map"][number]
|
||||
| SiteConfig["themes"][number]
|
||||
| SiteConfig["campaigns"][number]
|
||||
| SiteConfig["ctaBanners"][number];
|
||||
|
||||
Reference in New Issue
Block a user