fix(promotion): align review copy metadata and mobile tabs

This commit is contained in:
XiaoSeS 2026-10-09 17:34:32 +08:00
parent ad63f1acdc
commit 7fa3cb8889
8 changed files with 122 additions and 112 deletions

View file

@ -183,6 +183,7 @@ Web 端与 CLI 保持同一发布语义,只是在交互上可提供更明确
- 全局空间的新 skill 由其 owner 独立管理版本
- 原团队 skill 可继续独立迭代
- 两者版本不自动同步。来源 owner 或 namespace 管理员可在团队详情选择已发布版本,再次提交平台提升审核;审核通过后在原全局 skill 上新增版本,不覆盖原有版本
- 再次提升通过后,全局 skill 的展示名和摘要也更新为审核时来源 skill 的展示信息,使详情页与最新发布内容保持一致
- 申请和审批时均检查目标身份、来源版本状态及全局同号冲突。全局独立上传可以同时待审;不同号版本分别发布,后通过者成为 latest,同号后通过的申请失败
- 版本号不比较数值高低;例如全局当前 v1.3、团队提交尚未占用的 v1.1,确认提示后仍可提交,审批通过后 v1.1 成为 latest,v1.3 保留在历史

View file

@ -10,6 +10,7 @@ An authorized source owner or source namespace administrator SHALL be able to su
- **WHEN** an authorized user submits the version and a platform reviewer approves it
- **THEN** the same global Skill ID gets a new published version with the team's version string and source-version provenance
- **AND** the previous global versions remain unchanged
- **AND** the global skill's display name and summary reflect the source skill at approval time
#### Scenario: Team version is not published

View file

@ -202,6 +202,8 @@ public class PromotionService {
// Serialize approval with ordinary uploads that lock target Skill before publishing.
entityManager.lock(targetSkill, jakarta.persistence.LockModeType.PESSIMISTIC_WRITE);
assertTargetVersionAvailable(targetSkill.getId(), sourceVersion.getVersion());
targetSkill.setDisplayName(sourceSkill.getDisplayName());
targetSkill.setSummary(sourceSkill.getSummary());
} else {
if (promotionRequestRepository.findActiveInitialBySourceSkillId(sourceSkill.getId()).isPresent()) {
throw new DomainBadRequestException("promotion.already_promoted", sourceSkill.getId());

View file

@ -732,6 +732,8 @@ class PromotionServiceTest {
approved.setRequestKind(PromotionRequestKind.UPDATE);
approved.setTargetSkillId(NEW_SKILL_ID);
Skill target = linkedTarget();
target.setDisplayName("Independently updated global name");
target.setSummary("Independently updated global summary");
// The global skill may have advanced independently. Publication order, not
// numeric version order, determines the latest version after approval.
target.setLatestVersionId(999L);
@ -760,6 +762,8 @@ class PromotionServiceTest {
assertEquals(NEW_SKILL_ID, result.getTargetSkillId());
assertEquals(NEW_VERSION_ID, result.getTargetVersionId());
assertEquals(NEW_VERSION_ID, target.getLatestVersionId());
assertEquals("My Skill", target.getDisplayName());
assertEquals("A test skill", target.getSummary());
verify(promotionRequestRepository).updateStatusWithVersion(
PROMOTION_ID, ReviewTaskStatus.APPROVED, REVIEWER_ID, "approved",
NEW_SKILL_ID, pending.getVersion());

View file

@ -576,24 +576,6 @@
"promotionDuplicateDescription": "This version already has a pending promotion request.",
"promotionAlreadyPromotedTitle": "Skill already promoted",
"promotionAlreadyPromotedDescription": "This skill has already been promoted to the global namespace.",
"promotionVersionConflictTitle": "Global version already exists",
"promotionVersionConflictDescription": "The global skill already has this version number. Choose another published team version.",
"revocationDescription": "Revocation deletes the entire linked global skill, including independently uploaded versions. The team skill remains.",
"requestRevocation": "Request global revocation",
"directRevocation": "Revoke directly as admin",
"revocationConfirmTitle": "Confirm global revocation",
"revocationConfirmDescription": "This deletes @global/{{slug}} for “{{skill}}”, including all global versions, ratings, and statistics. The team skill remains. Installed local files cannot be recalled, and another skill may later use this address.",
"revocationReasonLabel": "Reason for revocation",
"revocationReasonPlaceholder": "Reason (optional)",
"revocationPending": "A revocation request is awaiting platform review.",
"revocationRequestSuccess": "Revocation request submitted",
"revocationDirectSuccess": "Global skill revoked",
"revocationError": "Revocation failed",
"revocationHistoryTitle": "Revocation history",
"revocationHistoryItem": "{{date}} · {{status}}",
"revocationHistoryLoading": "Loading revocation history…",
"revocationHistoryError": "Could not load revocation history. Please try again.",
"revocationStatus": { "PENDING": "Pending", "APPROVED": "Revoked", "REJECTED": "Rejected" },
"promotionErrorTitle": "Failed to submit promotion request",
"emptyTitle": "No skills yet",
"emptyDescription": "Start publishing your first skill",
@ -723,23 +705,6 @@
"typeSkill": "Skill Reviews",
"typeProfile": "Profile Reviews",
"tabPending": "Pending",
"tabRevocations": "Revocations to review",
"tabRevocationHistory": "Revocation history",
"revocationEmpty": "No revocation requests",
"revocationHistoryEmpty": "No reviewed revocation requests",
"revocationStatus": { "PENDING": "Pending", "APPROVED": "Revoked", "REJECTED": "Rejected" },
"revocationActors": "Requested by {{submitter}} · Reviewed by {{reviewer}}",
"revocationReviewComment": "Review comment: {{comment}}",
"revocationLoadError": "Could not load revocation requests. Please try again.",
"revocationIds": "Source skill #{{source}} · Global skill #{{target}}",
"revocationReason": "Reason: {{reason}}",
"approveRevocation": "Approve revocation",
"rejectRevocation": "Reject revocation",
"revocationConfirmTitle": "Confirm revocation approval",
"revocationConfirmDescription": "Approval deletes every global version, rating, and statistic for @global/{{slug}}. The address may be reused. The team skill remains, and installed files cannot be recalled.",
"revocationApproved": "Revocation approved",
"revocationRejected": "Revocation request rejected",
"revocationActionError": "Could not process revocation request",
"tabApproved": "Approved",
"tabRejected": "Rejected",
"empty": "No review tasks",
@ -810,6 +775,23 @@
"updateReviewHint": "Approval adds a version to the existing global skill. Check the source and current global versions.",
"subtitle": "Review team skill promotion requests to global namespace",
"tabPending": "Pending",
"tabRevocations": "Revocations to review",
"tabRevocationHistory": "Revocation history",
"revocationEmpty": "No revocation requests",
"revocationHistoryEmpty": "No reviewed revocation requests",
"revocationStatus": { "PENDING": "Pending", "APPROVED": "Revoked", "REJECTED": "Rejected" },
"revocationActors": "Requested by {{submitter}} · Reviewed by {{reviewer}}",
"revocationReviewComment": "Review comment: {{comment}}",
"revocationLoadError": "Could not load revocation requests. Please try again.",
"revocationIds": "Source skill #{{source}} · Global skill #{{target}}",
"revocationReason": "Reason: {{reason}}",
"approveRevocation": "Approve revocation",
"rejectRevocation": "Reject revocation",
"revocationConfirmTitle": "Confirm revocation approval",
"revocationConfirmDescription": "Approval deletes every global version, rating, and statistic for @global/{{slug}}. The address may be reused. The team skill remains, and installed files cannot be recalled.",
"revocationApproved": "Revocation approved",
"revocationRejected": "Revocation request rejected",
"revocationActionError": "Could not process revocation request",
"tabApproved": "Approved",
"tabRejected": "Rejected",
"commentPlaceholder": "Review comment (optional)",
@ -1269,6 +1251,24 @@
"submitGlobalUpdate": "Submit this version for global review",
"globalUpdateConfirmTitle": "Confirm global skill update",
"globalUpdateConfirmDescription": "Submit team v{{source}} of “{{skill}}” for global review. The current latest global version is v{{target}} and may change during review. If approved without a version conflict, this version becomes the latest published version even if its number is lower.",
"promotionVersionConflictTitle": "Global version already exists",
"promotionVersionConflictDescription": "The global skill already has this version number. Choose another published team version.",
"revocationDescription": "Revocation deletes the entire linked global skill, including independently uploaded versions. The team skill remains.",
"requestRevocation": "Request global revocation",
"directRevocation": "Revoke directly as admin",
"revocationConfirmTitle": "Confirm global revocation",
"revocationConfirmDescription": "This deletes @global/{{slug}} for “{{skill}}”, including all global versions, ratings, and statistics. The team skill remains. Installed local files cannot be recalled, and another skill may later use this address.",
"revocationReasonLabel": "Reason for revocation",
"revocationReasonPlaceholder": "Reason (optional)",
"revocationPending": "A revocation request is awaiting platform review.",
"revocationRequestSuccess": "Revocation request submitted",
"revocationDirectSuccess": "Global skill revoked",
"revocationError": "Revocation failed",
"revocationHistoryTitle": "Revocation history",
"revocationHistoryItem": "{{date}} · {{status}}",
"revocationHistoryLoading": "Loading revocation history…",
"revocationHistoryError": "Could not load revocation history. Please try again.",
"revocationStatus": { "PENDING": "Pending", "APPROVED": "Revoked", "REJECTED": "Rejected" },
"promotionConfirmTitle": "Submit promotion request",
"promotionConfirmDescription": "Submit v{{version}} of \"{{skill}}\" for promotion into the global namespace?",
"promotionSuccessTitle": "Promotion request submitted",

View file

@ -576,24 +576,6 @@
"promotionDuplicateDescription": "У этой версии уже есть ожидающий запрос на продвижение.",
"promotionAlreadyPromotedTitle": "Скилл уже продвинут",
"promotionAlreadyPromotedDescription": "Этот скилл уже продвинут в глобальное пространство имён.",
"promotionVersionConflictTitle": "Глобальная версия уже существует",
"promotionVersionConflictDescription": "У глобального скилла уже есть версия с таким номером. Выберите другую опубликованную командную версию.",
"revocationDescription": "Отзыв удалит весь связанный глобальный скилл, включая независимо загруженные версии. Командный скилл останется.",
"requestRevocation": "Запросить отзыв глобальной публикации",
"directRevocation": "Отозвать напрямую как администратор",
"revocationConfirmTitle": "Подтвердите отзыв глобальной публикации",
"revocationConfirmDescription": "Будет удалён @global/{{slug}} для «{{skill}}» со всеми глобальными версиями, оценками и статистикой. Командный скилл останется. Уже установленные файлы нельзя отозвать, а адрес позже может занять другой скилл.",
"revocationReasonLabel": "Причина отзыва",
"revocationReasonPlaceholder": "Причина (необязательно)",
"revocationPending": "Запрос на отзыв ожидает проверки платформой.",
"revocationRequestSuccess": "Запрос на отзыв отправлен",
"revocationDirectSuccess": "Глобальный скилл отозван",
"revocationError": "Не удалось отозвать публикацию",
"revocationHistoryTitle": "История отзывов",
"revocationHistoryItem": "{{date}} · {{status}}",
"revocationHistoryLoading": "Загрузка истории отзывов…",
"revocationHistoryError": "Не удалось загрузить историю отзывов. Повторите попытку.",
"revocationStatus": { "PENDING": "Ожидает", "APPROVED": "Отозван", "REJECTED": "Отклонён" },
"promotionErrorTitle": "Не удалось отправить запрос на продвижение",
"emptyTitle": "Скиллов пока нет",
"emptyDescription": "Опубликуйте свой первый скилл",
@ -723,23 +705,6 @@
"typeSkill": "Рецензии скиллов",
"typeProfile": "Рецензии профилей",
"tabPending": "Ожидают",
"tabRevocations": "Заявки на отзыв",
"tabRevocationHistory": "История отзывов",
"revocationEmpty": "Нет заявок на отзыв",
"revocationHistoryEmpty": "Нет рассмотренных заявок на отзыв",
"revocationStatus": { "PENDING": "Ожидает", "APPROVED": "Отозван", "REJECTED": "Отклонён" },
"revocationActors": "Отправитель: {{submitter}} · Рецензент: {{reviewer}}",
"revocationReviewComment": "Комментарий рецензии: {{comment}}",
"revocationLoadError": "Не удалось загрузить заявки на отзыв. Повторите попытку.",
"revocationIds": "Исходный скилл #{{source}} · Глобальный скилл #{{target}}",
"revocationReason": "Причина: {{reason}}",
"approveRevocation": "Одобрить отзыв",
"rejectRevocation": "Отклонить отзыв",
"revocationConfirmTitle": "Подтвердите одобрение отзыва",
"revocationConfirmDescription": "Будут удалены все глобальные версии, оценки и статистика @global/{{slug}}. Адрес может быть использован снова. Командный скилл останется, а установленные файлы нельзя отозвать.",
"revocationApproved": "Отзыв одобрен",
"revocationRejected": "Запрос на отзыв отклонён",
"revocationActionError": "Не удалось обработать запрос на отзыв",
"tabApproved": "Одобрены",
"tabRejected": "Отклонены",
"empty": "Нет задач рецензирования",
@ -810,6 +775,23 @@
"updateReviewHint": "После одобрения новая версия будет добавлена к существующему глобальному скиллу. Проверьте исходную и текущую версии.",
"subtitle": "Запросы на продвижение командных скиллов в глобальное пространство имён",
"tabPending": "Ожидают",
"tabRevocations": "Заявки на отзыв",
"tabRevocationHistory": "История отзывов",
"revocationEmpty": "Нет заявок на отзыв",
"revocationHistoryEmpty": "Нет рассмотренных заявок на отзыв",
"revocationStatus": { "PENDING": "Ожидает", "APPROVED": "Отозван", "REJECTED": "Отклонён" },
"revocationActors": "Отправитель: {{submitter}} · Рецензент: {{reviewer}}",
"revocationReviewComment": "Комментарий рецензии: {{comment}}",
"revocationLoadError": "Не удалось загрузить заявки на отзыв. Повторите попытку.",
"revocationIds": "Исходный скилл #{{source}} · Глобальный скилл #{{target}}",
"revocationReason": "Причина: {{reason}}",
"approveRevocation": "Одобрить отзыв",
"rejectRevocation": "Отклонить отзыв",
"revocationConfirmTitle": "Подтвердите одобрение отзыва",
"revocationConfirmDescription": "Будут удалены все глобальные версии, оценки и статистика @global/{{slug}}. Адрес может быть использован снова. Командный скилл останется, а установленные файлы нельзя отозвать.",
"revocationApproved": "Отзыв одобрен",
"revocationRejected": "Запрос на отзыв отклонён",
"revocationActionError": "Не удалось обработать запрос на отзыв",
"tabApproved": "Одобрены",
"tabRejected": "Отклонены",
"commentPlaceholder": "Комментарий рецензии (необязательно)",
@ -1334,6 +1316,24 @@
"submitGlobalUpdate": "Отправить эту версию на глобальное ревью",
"globalUpdateConfirmTitle": "Подтвердите обновление глобального скилла",
"globalUpdateConfirmDescription": "Отправить командную v{{source}} скилла «{{skill}}» на глобальное ревью. Сейчас последняя глобальная версия — v{{target}}; во время проверки она может измениться. Если версия не конфликтует, после одобрения она станет последней опубликованной, даже если её номер меньше.",
"promotionVersionConflictTitle": "Глобальная версия уже существует",
"promotionVersionConflictDescription": "У глобального скилла уже есть версия с таким номером. Выберите другую опубликованную командную версию.",
"revocationDescription": "Отзыв удалит весь связанный глобальный скилл, включая независимо загруженные версии. Командный скилл останется.",
"requestRevocation": "Запросить отзыв глобальной публикации",
"directRevocation": "Отозвать напрямую как администратор",
"revocationConfirmTitle": "Подтвердите отзыв глобальной публикации",
"revocationConfirmDescription": "Будет удалён @global/{{slug}} для «{{skill}}» со всеми глобальными версиями, оценками и статистикой. Командный скилл останется. Уже установленные файлы нельзя отозвать, а адрес позже может занять другой скилл.",
"revocationReasonLabel": "Причина отзыва",
"revocationReasonPlaceholder": "Причина (необязательно)",
"revocationPending": "Запрос на отзыв ожидает проверки платформой.",
"revocationRequestSuccess": "Запрос на отзыв отправлен",
"revocationDirectSuccess": "Глобальный скилл отозван",
"revocationError": "Не удалось отозвать публикацию",
"revocationHistoryTitle": "История отзывов",
"revocationHistoryItem": "{{date}} · {{status}}",
"revocationHistoryLoading": "Загрузка истории отзывов…",
"revocationHistoryError": "Не удалось загрузить историю отзывов. Повторите попытку.",
"revocationStatus": { "PENDING": "Ожидает", "APPROVED": "Отозван", "REJECTED": "Отклонён" },
"promotionConfirmTitle": "Отправить запрос на продвижение",
"promotionConfirmDescription": "Отправить v{{version}} скилла «{{skill}}» на продвижение в глобальное пространство имён?",
"promotionSuccessTitle": "Запрос на продвижение отправлен",

View file

@ -576,24 +576,6 @@
"promotionDuplicateDescription": "这个版本已经有待处理的提升申请,无需重复提交。",
"promotionAlreadyPromotedTitle": "技能已完成提升",
"promotionAlreadyPromotedDescription": "这个技能已经提升到全局命名空间,无需重复提交。",
"promotionVersionConflictTitle": "全局版本号已存在",
"promotionVersionConflictDescription": "全局技能已有同号版本。请选择另一个已发布的团队版本。",
"revocationDescription": "撤销会删除整条关联的全局技能,包括独立上传的版本。团队技能保留。",
"requestRevocation": "申请撤销全局发布",
"directRevocation": "管理员直接撤销",
"revocationConfirmTitle": "确认撤销全局发布",
"revocationConfirmDescription": "将删除“{{skill}}”对应的 @global/{{slug}} 及其所有全局版本、评价和统计;团队技能保留。已安装到客户端的文件无法收回,旧地址以后可能被其他技能使用。",
"revocationReasonLabel": "撤销原因",
"revocationReasonPlaceholder": "填写撤销原因(可选)",
"revocationPending": "撤销申请等待平台审核。",
"revocationRequestSuccess": "撤销申请已提交",
"revocationDirectSuccess": "全局技能已撤销",
"revocationError": "撤销操作失败",
"revocationHistoryTitle": "撤销记录",
"revocationHistoryItem": "{{date}} · {{status}}",
"revocationHistoryLoading": "正在读取撤销记录…",
"revocationHistoryError": "无法读取撤销记录,请稍后重试。",
"revocationStatus": { "PENDING": "待审核", "APPROVED": "已撤销", "REJECTED": "已拒绝" },
"promotionErrorTitle": "提交提升申请失败",
"emptyTitle": "还没有技能",
"emptyDescription": "开始发布你的第一个技能吧",
@ -723,23 +705,6 @@
"typeSkill": "技能审核",
"typeProfile": "资料审核",
"tabPending": "待审核",
"tabRevocations": "撤销待审核",
"tabRevocationHistory": "撤销历史",
"revocationEmpty": "暂无撤销申请",
"revocationHistoryEmpty": "暂无已处理的撤销记录",
"revocationStatus": { "PENDING": "待审核", "APPROVED": "已撤销", "REJECTED": "已拒绝" },
"revocationActors": "申请人 {{submitter}} · 审核人 {{reviewer}}",
"revocationReviewComment": "审核意见:{{comment}}",
"revocationLoadError": "无法读取撤销申请,请稍后重试。",
"revocationIds": "来源技能 #{{source}} · 全局技能 #{{target}}",
"revocationReason": "申请原因:{{reason}}",
"approveRevocation": "批准撤销",
"rejectRevocation": "拒绝撤销",
"revocationConfirmTitle": "确认批准撤销",
"revocationConfirmDescription": "批准后将删除 @global/{{slug}} 的全部全局版本、评价和统计,旧地址可能重新被占用;团队技能保留,已安装文件无法收回。",
"revocationApproved": "撤销已批准",
"revocationRejected": "撤销申请已拒绝",
"revocationActionError": "处理撤销申请失败",
"tabApproved": "已通过",
"tabRejected": "已拒绝",
"empty": "暂无审核任务",
@ -810,6 +775,23 @@
"updateReviewHint": "通过后会在现有全局技能新增版本;请核对来源和当前全局版本。",
"subtitle": "审核团队技能提升到全局空间的申请",
"tabPending": "待审核",
"tabRevocations": "撤销待审核",
"tabRevocationHistory": "撤销历史",
"revocationEmpty": "暂无撤销申请",
"revocationHistoryEmpty": "暂无已处理的撤销记录",
"revocationStatus": { "PENDING": "待审核", "APPROVED": "已撤销", "REJECTED": "已拒绝" },
"revocationActors": "申请人 {{submitter}} · 审核人 {{reviewer}}",
"revocationReviewComment": "审核意见:{{comment}}",
"revocationLoadError": "无法读取撤销申请,请稍后重试。",
"revocationIds": "来源技能 #{{source}} · 全局技能 #{{target}}",
"revocationReason": "申请原因:{{reason}}",
"approveRevocation": "批准撤销",
"rejectRevocation": "拒绝撤销",
"revocationConfirmTitle": "确认批准撤销",
"revocationConfirmDescription": "批准后将删除 @global/{{slug}} 的全部全局版本、评价和统计,旧地址可能重新被占用;团队技能保留,已安装文件无法收回。",
"revocationApproved": "撤销已批准",
"revocationRejected": "撤销申请已拒绝",
"revocationActionError": "处理撤销申请失败",
"tabApproved": "已通过",
"tabRejected": "已拒绝",
"commentPlaceholder": "审核意见(可选)",
@ -1269,6 +1251,24 @@
"submitGlobalUpdate": "提交此版本到全局审核",
"globalUpdateConfirmTitle": "确认更新全局技能",
"globalUpdateConfirmDescription": "将“{{skill}}”的团队 v{{source}} 提交全局审核。当前全局最新版本为 v{{target}};审核期间全局版仍可能独立更新。如审核通过且版本号未冲突,此版本将成为最新发布版,即使版本号较小。",
"promotionVersionConflictTitle": "全局版本号已存在",
"promotionVersionConflictDescription": "全局技能已有同号版本。请选择另一个已发布的团队版本。",
"revocationDescription": "撤销会删除整条关联的全局技能,包括独立上传的版本。团队技能保留。",
"requestRevocation": "申请撤销全局发布",
"directRevocation": "管理员直接撤销",
"revocationConfirmTitle": "确认撤销全局发布",
"revocationConfirmDescription": "将删除“{{skill}}”对应的 @global/{{slug}} 及其所有全局版本、评价和统计;团队技能保留。已安装到客户端的文件无法收回,旧地址以后可能被其他技能使用。",
"revocationReasonLabel": "撤销原因",
"revocationReasonPlaceholder": "填写撤销原因(可选)",
"revocationPending": "撤销申请等待平台审核。",
"revocationRequestSuccess": "撤销申请已提交",
"revocationDirectSuccess": "全局技能已撤销",
"revocationError": "撤销操作失败",
"revocationHistoryTitle": "撤销记录",
"revocationHistoryItem": "{{date}} · {{status}}",
"revocationHistoryLoading": "正在读取撤销记录…",
"revocationHistoryError": "无法读取撤销记录,请稍后重试。",
"revocationStatus": { "PENDING": "待审核", "APPROVED": "已撤销", "REJECTED": "已拒绝" },
"promotionConfirmTitle": "确认提交提升申请",
"promotionConfirmDescription": "确认将“{{skill}}”的 v{{version}} 提交为提升到全局空间的申请吗?",
"promotionSuccessTitle": "提升申请已提交",

View file

@ -432,13 +432,15 @@ export function PromotionsPage() {
<div className="space-y-8 animate-fade-up">
<DashboardPageHeader title={t('promotions.title')} subtitle={t('promotions.subtitle')} />
<Tabs defaultValue="PENDING">
<TabsList>
<TabsTrigger value="PENDING">{t('promotions.tabPending')}</TabsTrigger>
<TabsTrigger value="REVOCATIONS">{t('promotions.tabRevocations')}</TabsTrigger>
<TabsTrigger value="REVOCATION_HISTORY">{t('promotions.tabRevocationHistory')}</TabsTrigger>
<TabsTrigger value="APPROVED">{t('promotions.tabApproved')}</TabsTrigger>
<TabsTrigger value="REJECTED">{t('promotions.tabRejected')}</TabsTrigger>
</TabsList>
<div className="max-w-full overflow-x-auto">
<TabsList>
<TabsTrigger value="PENDING">{t('promotions.tabPending')}</TabsTrigger>
<TabsTrigger value="REVOCATIONS">{t('promotions.tabRevocations')}</TabsTrigger>
<TabsTrigger value="REVOCATION_HISTORY">{t('promotions.tabRevocationHistory')}</TabsTrigger>
<TabsTrigger value="APPROVED">{t('promotions.tabApproved')}</TabsTrigger>
<TabsTrigger value="REJECTED">{t('promotions.tabRejected')}</TabsTrigger>
</TabsList>
</div>
<TabsContent value="PENDING" className="mt-6">
<PendingPromotionList page={pages.PENDING} onPageChange={(page) => changePage('PENDING', page)} />
</TabsContent>