From fbdfb97c3e5181e9e9ef9149536264e2067b70b2 Mon Sep 17 00:00:00 2001 From: chan Date: Thu, 4 Jun 2026 11:12:47 +0900 Subject: [PATCH 1/5] feat(admin): improve tenant bulk import reporting with detailed results - Backend: Enhanced 'ImportTenantsCSV' to return row-by-row details including action, status, and modified fields. - Backend: Refactored 'upsertTenantCSVRecord' to detect and return specific modified fields (Name, Type, ParentID, Slug, Description, Config, Domains). - Frontend: Added 'TenantImportDetail' and updated 'TenantImportResult' types. - Frontend: Implemented a detailed results modal in 'TenantListPage' showing processing summary and row-level feedback for better transparency. --- .../tenants/routes/TenantListPage.tsx | 138 +++++++++++++-- adminfront/src/lib/adminApi.ts | 11 ++ backend/internal/handler/tenant_handler.go | 161 ++++++++++++++---- 3 files changed, 268 insertions(+), 42 deletions(-) diff --git a/adminfront/src/features/tenants/routes/TenantListPage.tsx b/adminfront/src/features/tenants/routes/TenantListPage.tsx index b535cc9b..9351e1aa 100644 --- a/adminfront/src/features/tenants/routes/TenantListPage.tsx +++ b/adminfront/src/features/tenants/routes/TenantListPage.tsx @@ -285,6 +285,9 @@ function TenantListPage() { Record >({}); const [previewOpen, setPreviewOpen] = React.useState(false); + const [importResult, setImportResult] = + React.useState(null); + const [importResultOpen, setImportResultOpen] = React.useState(false); const [selectedBulkStatus, setSelectedBulkStatus] = React.useState(""); const _tenantTableScrollRef = React.useRef(null); @@ -387,17 +390,8 @@ function TenantListPage() { const importMutation = useMutation({ mutationFn: (file: File) => importTenantsCSV(file), onSuccess: (result) => { - setImportMessage( - t( - "msg.admin.tenants.import_result", - "생성 {{created}}, 갱신 {{updated}}, 실패 {{failed}}", - { - created: result.created, - updated: result.updated, - failed: result.failed, - }, - ), - ); + setImportResult(result); + setImportResultOpen(true); setPreviewOpen(false); setPreviewRows([]); setSelectedMatches({}); @@ -1006,6 +1000,128 @@ function TenantListPage() { )} + + + + + {t( + "ui.admin.tenants.import_result.title", + "가져오기 결과 리포트", + )} + + + {importResult && + t( + "msg.admin.tenants.import_result.summary", + "총 {{total}}건 처리: 생성 {{created}}, 갱신 {{updated}}, 실패 {{failed}}", + { + total: importResult.details.length, + created: importResult.created, + updated: importResult.updated, + failed: importResult.failed, + }, + )} + + + +
+ + + + + {t("ui.common.row", "행")} + + + {t("ui.admin.tenants.table.name", "NAME")} + + + {t("ui.admin.tenants.table.slug", "SLUG")} + + + {t("ui.admin.tenants.import_result.action", "작업")} + + + {t("ui.admin.tenants.import_result.status", "상태")} + + + {t("ui.admin.tenants.import_result.message", "상세 내용")} + + + + + {importResult?.details.map((detail) => ( + + + {detail.row} + + {detail.name} + + {detail.slug} + + + + {detail.action.toUpperCase()} + + + + {detail.success ? ( + + SUCCESS + + ) : ( + + FAILED + + )} + + + {detail.message} + {detail.modifiedFields && + detail.modifiedFields.length > 0 && ( +
+ + {t( + "ui.admin.tenants.import_result.modified", + "수정됨:", + )} + + {detail.modifiedFields.map((field) => ( + + {field} + + ))} +
+ )} +
+
+ ))} +
+
+
+ + + + +
+
+ diff --git a/adminfront/src/lib/adminApi.ts b/adminfront/src/lib/adminApi.ts index f083b12a..f0720947 100644 --- a/adminfront/src/lib/adminApi.ts +++ b/adminfront/src/lib/adminApi.ts @@ -70,11 +70,22 @@ export type TenantUpdateRequest = { config?: Record; }; +export type TenantImportDetail = { + row: number; + slug: string; + name: string; + success: boolean; + action: "created" | "updated" | "failed" | "skipped"; + message: string; + modifiedFields?: string[]; +}; + export type TenantImportResult = { created: number; updated: number; failed: number; errors: string[]; + details: TenantImportDetail[]; }; export type ApiKeySummary = { diff --git a/backend/internal/handler/tenant_handler.go b/backend/internal/handler/tenant_handler.go index 3082ba5b..38a22dd7 100644 --- a/backend/internal/handler/tenant_handler.go +++ b/backend/internal/handler/tenant_handler.go @@ -102,11 +102,22 @@ func pageTenantsByCursor(tenants []domain.Tenant, limit int, cursorRaw string) ( }) } +type tenantImportDetail struct { + Row int `json:"row"` + Slug string `json:"slug"` + Name string `json:"name"` + Success bool `json:"success"` + Action string `json:"action"` // "created", "updated", "failed", "skipped" + Message string `json:"message"` // Detailed error or success message + ModifiedFields []string `json:"modifiedFields"` // List of fields changed during update +} + type tenantImportResult struct { - Created int `json:"created"` - Updated int `json:"updated"` - Failed int `json:"failed"` - Errors []string `json:"errors"` + Created int `json:"created"` + Updated int `json:"updated"` + Failed int `json:"failed"` + Errors []string `json:"errors"` + Details []tenantImportDetail `json:"details"` } type tenantDomainConflict struct { @@ -557,32 +568,64 @@ func (h *TenantHandler) ImportTenantsCSV(c *fiber.Ctx) error { } } - result := tenantImportResult{Errors: make([]string, 0)} + result := tenantImportResult{ + Errors: make([]string, 0), + Details: make([]tenantImportDetail, 0, len(records)), + } for i, record := range records { rowNumber := i + 2 + detail := tenantImportDetail{ + Row: rowNumber, + Slug: record.Slug, + Name: record.Name, + } + if record.ParentTenantID == nil && record.ParentTenantSlug != "" { parentID := tenantIDBySlug[strings.ToLower(record.ParentTenantSlug)] if parentID == "" { result.Failed++ - result.Errors = append(result.Errors, fmt.Sprintf("row %d: parent tenant slug not found: %s", rowNumber, record.ParentTenantSlug)) + msg := fmt.Sprintf("row %d: parent tenant slug not found: %s", rowNumber, record.ParentTenantSlug) + result.Errors = append(result.Errors, msg) + detail.Success = false + detail.Action = "failed" + detail.Message = msg + result.Details = append(result.Details, detail) continue } record.ParentTenantID = &parentID } if record.TenantID != "" || (h.DB != nil && record.Slug != "") { - tenant, updated, err := h.upsertTenantCSVRecord(c, record) + tenant, modifiedFields, err := h.upsertTenantCSVRecord(c, record) if err != nil { result.Failed++ - result.Errors = append(result.Errors, fmt.Sprintf("row %d: %s", rowNumber, err.Error())) + msg := fmt.Sprintf("row %d: %s", rowNumber, err.Error()) + result.Errors = append(result.Errors, msg) + detail.Success = false + detail.Action = "failed" + detail.Message = msg + result.Details = append(result.Details, detail) continue } - if updated { - tenantIDBySlug[strings.ToLower(record.Slug)] = tenant.ID - result.Updated++ - if h.Worksmobile != nil { - _ = h.Worksmobile.EnqueueTenantUpsertIfInScope(c.Context(), *tenant) + if tenant != nil { + if len(modifiedFields) > 0 { + tenantIDBySlug[strings.ToLower(record.Slug)] = tenant.ID + result.Updated++ + detail.Success = true + detail.Action = "updated" + detail.ModifiedFields = modifiedFields + if h.Worksmobile != nil { + _ = h.Worksmobile.EnqueueTenantUpsertIfInScope(c.Context(), *tenant) + } + result.Details = append(result.Details, detail) + continue + } else { + // No changes, skip + detail.Success = true + detail.Action = "skipped" + detail.Message = "no changes detected" + result.Details = append(result.Details, detail) + continue } - continue } } @@ -594,19 +637,32 @@ func (h *TenantHandler) ImportTenantsCSV(c *fiber.Ctx) error { tenant, err := h.createTenantCSVRecord(c, record, recordCreatorID) if err != nil { result.Failed++ - result.Errors = append(result.Errors, fmt.Sprintf("row %d: %s", rowNumber, err.Error())) + msg := fmt.Sprintf("row %d: %s", rowNumber, err.Error()) + result.Errors = append(result.Errors, msg) + detail.Success = false + detail.Action = "failed" + detail.Message = msg + result.Details = append(result.Details, detail) continue } if tenant == nil { result.Failed++ - result.Errors = append(result.Errors, fmt.Sprintf("row %d: tenant creation returned empty result", rowNumber)) + msg := fmt.Sprintf("row %d: tenant creation returned empty result", rowNumber) + result.Errors = append(result.Errors, msg) + detail.Success = false + detail.Action = "failed" + detail.Message = msg + result.Details = append(result.Details, detail) continue } tenantIDBySlug[strings.ToLower(record.Slug)] = tenant.ID result.Created++ + detail.Success = true + detail.Action = "created" if h.Worksmobile != nil { _ = h.Worksmobile.EnqueueTenantUpsertIfInScope(c.Context(), *tenant) } + result.Details = append(result.Details, detail) } return c.JSON(result) @@ -1385,12 +1441,12 @@ func (h *TenantHandler) replaceTenantDomains(ctx context.Context, tenantID strin return nil } -func (h *TenantHandler) upsertTenantCSVRecord(c *fiber.Ctx, record tenantCSVRecord) (*domain.Tenant, bool, error) { +func (h *TenantHandler) upsertTenantCSVRecord(c *fiber.Ctx, record tenantCSVRecord) (*domain.Tenant, []string, error) { if h.DB == nil { if record.TenantID != "" { - return nil, false, errors.New("database not available for tenant update") + return nil, nil, errors.New("database not available for tenant update") } - return nil, false, nil + return nil, nil, nil } var tenant domain.Tenant @@ -1403,42 +1459,85 @@ func (h *TenantHandler) upsertTenantCSVRecord(c *fiber.Ctx, record tenantCSVReco } if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, false, nil + return nil, nil, nil } if err != nil { - return nil, false, err + return nil, nil, err + } + + modifiedFields := []string{} + if tenant.Name != record.Name { + tenant.Name = record.Name + modifiedFields = append(modifiedFields, "Name") + } + if tenant.Type != record.Type { + tenant.Type = record.Type + modifiedFields = append(modifiedFields, "Type") + } + if record.ParentTenantID != nil { + oldParentID := "" + if tenant.ParentID != nil { + oldParentID = *tenant.ParentID + } + if oldParentID != *record.ParentTenantID { + tenant.ParentID = record.ParentTenantID + modifiedFields = append(modifiedFields, "ParentID") + } + } else if tenant.ParentID != nil { + tenant.ParentID = nil + modifiedFields = append(modifiedFields, "ParentID") + } + + if tenant.Slug != record.Slug { + tenant.Slug = record.Slug + modifiedFields = append(modifiedFields, "Slug") + } + if tenant.Description != record.Memo { + tenant.Description = record.Memo + modifiedFields = append(modifiedFields, "Description") } - tenant.Name = record.Name - tenant.Type = record.Type - tenant.ParentID = record.ParentTenantID - tenant.Slug = record.Slug - tenant.Description = record.Memo if tenant.Status == "" { tenant.Status = domain.TenantStatusActive } mergedConfig, changedConfig, err := mergeTenantCSVRecordConfig(tenant.Config, record) if err != nil { - return nil, false, err + return nil, nil, err } if changedConfig { tenant.Config = mergedConfig + modifiedFields = append(modifiedFields, "Config") + } + + existingDomains := make([]string, len(tenant.Domains)) + for i, d := range tenant.Domains { + existingDomains[i] = d.Domain + } + sort.Strings(existingDomains) + newDomains := append([]string(nil), record.Domains...) + sort.Strings(newDomains) + if !reflect.DeepEqual(existingDomains, newDomains) { + modifiedFields = append(modifiedFields, "Domains") + } + + if len(modifiedFields) == 0 { + return &tenant, nil, nil } if err := h.DB.Save(&tenant).Error; err != nil { - return nil, false, err + return nil, nil, err } if err := h.DB.Delete(&domain.TenantDomain{}, "tenant_id = ?", tenant.ID).Error; err != nil { - return nil, false, err + return nil, nil, err } repo := repository.NewTenantRepository(h.DB) for _, domainName := range record.Domains { if err := repo.AddDomain(c.Context(), tenant.ID, domainName, true); err != nil { - return nil, false, err + return nil, nil, err } } - return &tenant, true, nil + return &tenant, modifiedFields, nil } func (h *TenantHandler) createTenantCSVRecord(c *fiber.Ctx, record tenantCSVRecord, creatorID string) (*domain.Tenant, error) { From c6c79f7306c25a1b8ae6c6413dbc19aee23353cc Mon Sep 17 00:00:00 2001 From: chan Date: Thu, 4 Jun 2026 11:20:21 +0900 Subject: [PATCH 2/5] fix(admin): add missing Tabs import and refine import result UI type safety --- .../tenants/routes/TenantListPage.tsx | 203 +++++++++++------- 1 file changed, 126 insertions(+), 77 deletions(-) diff --git a/adminfront/src/features/tenants/routes/TenantListPage.tsx b/adminfront/src/features/tenants/routes/TenantListPage.tsx index 9351e1aa..ba62a6e5 100644 --- a/adminfront/src/features/tenants/routes/TenantListPage.tsx +++ b/adminfront/src/features/tenants/routes/TenantListPage.tsx @@ -76,6 +76,7 @@ import { TableHeader, TableRow, } from "../../../components/ui/table"; +import { Tabs, TabsList, TabsTrigger } from "../../../components/ui/tabs"; import { toast } from "../../../components/ui/use-toast"; import type { UserProfileResponse } from "../../../lib/adminApi"; import { @@ -288,6 +289,17 @@ function TenantListPage() { const [importResult, setImportResult] = React.useState(null); const [importResultOpen, setImportResultOpen] = React.useState(false); + const [importResultFilter, setImportResultFilter] = React.useState< + "all" | "created" | "updated" | "failed" | "skipped" + >("all"); + + const filteredImportDetails = React.useMemo(() => { + if (!importResult) return []; + if (importResultFilter === "all") return importResult.details; + if (importResultFilter === "failed") + return importResult.details.filter((d) => !d.success); + return importResult.details.filter((d) => d.action === importResultFilter); + }, [importResult, importResultFilter]); const [selectedBulkStatus, setSelectedBulkStatus] = React.useState(""); const _tenantTableScrollRef = React.useRef(null); @@ -1009,22 +1021,60 @@ function TenantListPage() { "가져오기 결과 리포트", )} - - {importResult && - t( - "msg.admin.tenants.import_result.summary", - "총 {{total}}건 처리: 생성 {{created}}, 갱신 {{updated}}, 실패 {{failed}}", - { - total: importResult.details.length, - created: importResult.created, - updated: importResult.updated, - failed: importResult.failed, - }, - )} - -
+ {importResult && ( +
+
+ + Total + + + {importResult.details.length} + +
+
+ Created + + {importResult.created} + +
+
+ Updated + + {importResult.updated} + +
+
+ + Failed + + + {importResult.failed} + +
+
+ )} + + + setImportResultFilter( + v as "all" | "created" | "updated" | "failed" | "skipped", + ) + } + className="w-full" + > + + ALL + CREATED + UPDATED + FAILED + SKIPPED + + + +
@@ -1037,10 +1087,7 @@ function TenantListPage() { {t("ui.admin.tenants.table.slug", "SLUG")} - - {t("ui.admin.tenants.import_result.action", "작업")} - - + {t("ui.admin.tenants.import_result.status", "상태")} @@ -1049,67 +1096,69 @@ function TenantListPage() { - {importResult?.details.map((detail) => ( - - - {detail.row} - - {detail.name} - - {detail.slug} - - - - {detail.action.toUpperCase()} - - - - {detail.success ? ( - - SUCCESS - - ) : ( - - FAILED - - )} - - - {detail.message} - {detail.modifiedFields && - detail.modifiedFields.length > 0 && ( -
- - {t( - "ui.admin.tenants.import_result.modified", - "수정됨:", - )} - - {detail.modifiedFields.map((field) => ( - - {field} - - ))} -
- )} + {filteredImportDetails.length === 0 ? ( + + + {t("ui.common.no_results", "표시할 결과가 없습니다.")} - ))} + ) : ( + filteredImportDetails.map((detail) => ( + + + {detail.row} + + + {detail.name} + + + {detail.slug} + + + + {detail.action.toUpperCase()} + + + + {detail.message} + {detail.modifiedFields && + detail.modifiedFields.length > 0 && ( +
+ + {t( + "ui.admin.tenants.import_result.modified", + "수정됨:", + )} + + {detail.modifiedFields.map((field) => ( + + {field} + + ))} +
+ )} +
+
+ )) + )}
From 5ba0d0fb866864ef4aa178b359d6dda646658136 Mon Sep 17 00:00:00 2001 From: chan Date: Thu, 4 Jun 2026 11:20:35 +0900 Subject: [PATCH 3/5] fix(backend): add missing reflect import --- backend/internal/handler/tenant_handler.go | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/internal/handler/tenant_handler.go b/backend/internal/handler/tenant_handler.go index 38a22dd7..89000cd8 100644 --- a/backend/internal/handler/tenant_handler.go +++ b/backend/internal/handler/tenant_handler.go @@ -14,6 +14,7 @@ import ( "fmt" "io" "maps" + "reflect" "sort" "strings" "time" From 499b5d65dac0364eaed11b0d0abd2aa3891e67c9 Mon Sep 17 00:00:00 2001 From: chan Date: Thu, 4 Jun 2026 11:26:24 +0900 Subject: [PATCH 4/5] style(admin): enhance UI/UX of tenant import result modal - Added visual summary cards with color-coded counts (Total, Created, Updated, Failed). - Implemented Tabs for status-based filtering (ALL, CREATED, UPDATED, FAILED, SKIPPED). - Improved tab visibility with bold text and status-specific active colors. - Refined table layout with consolidated status badges and small tags for modified fields. --- .../tenants/routes/TenantListPage.tsx | 61 ++++++++++++++----- 1 file changed, 45 insertions(+), 16 deletions(-) diff --git a/adminfront/src/features/tenants/routes/TenantListPage.tsx b/adminfront/src/features/tenants/routes/TenantListPage.tsx index ba62a6e5..06c2fa73 100644 --- a/adminfront/src/features/tenants/routes/TenantListPage.tsx +++ b/adminfront/src/features/tenants/routes/TenantListPage.tsx @@ -1025,28 +1025,32 @@ function TenantListPage() { {importResult && (
-
- +
+ Total {importResult.details.length}
-
- Created - +
+ + Created + + {importResult.created}
-
- Updated - +
+ + Updated + + {importResult.updated}
-
- +
+ Failed @@ -1065,12 +1069,37 @@ function TenantListPage() { } className="w-full" > - - ALL - CREATED - UPDATED - FAILED - SKIPPED + + + ALL + + + CREATED + + + UPDATED + + + FAILED + + + SKIPPED + From 8f2e35187520913f9228a778355150965803b309 Mon Sep 17 00:00:00 2001 From: chan Date: Thu, 4 Jun 2026 12:59:32 +0900 Subject: [PATCH 5/5] fix(admin): stabilize tenant import report UI and satisfy E2E tests - Added missing i18n keys for import results in both root and common locales. - Fixed TypeScript type errors and implicit 'any' types in TenantListPage. - Added 'destructive' variant to common Badge component. - Updated Playwright tests with refined locators and enhanced API mocks to match the new reporting structure. - Restored quick summary message in Tenant Registry for backward compatibility. --- .../tenants/routes/TenantListPage.tsx | 23 +++++++-- adminfront/tests/tenants.spec.ts | 51 +++++++++++++++++-- common/locales/en.toml | 1 + common/locales/ko.toml | 1 + common/locales/template.toml | 1 + common/ui/badge.ts | 2 + locales/en.toml | 12 +++++ locales/ko.toml | 12 +++++ locales/template.toml | 12 +++++ 9 files changed, 106 insertions(+), 9 deletions(-) diff --git a/adminfront/src/features/tenants/routes/TenantListPage.tsx b/adminfront/src/features/tenants/routes/TenantListPage.tsx index 06c2fa73..88151c11 100644 --- a/adminfront/src/features/tenants/routes/TenantListPage.tsx +++ b/adminfront/src/features/tenants/routes/TenantListPage.tsx @@ -85,6 +85,8 @@ import { fetchMe, fetchTenants, importTenantsCSV, + type TenantImportDetail, + type TenantImportResult, type TenantSummary, updateTenant, } from "../../../lib/adminApi"; @@ -297,8 +299,10 @@ function TenantListPage() { if (!importResult) return []; if (importResultFilter === "all") return importResult.details; if (importResultFilter === "failed") - return importResult.details.filter((d) => !d.success); - return importResult.details.filter((d) => d.action === importResultFilter); + return importResult.details.filter((d: TenantImportDetail) => !d.success); + return importResult.details.filter( + (d: TenantImportDetail) => d.action === importResultFilter, + ); }, [importResult, importResultFilter]); const [selectedBulkStatus, setSelectedBulkStatus] = React.useState(""); const _tenantTableScrollRef = React.useRef(null); @@ -404,6 +408,17 @@ function TenantListPage() { onSuccess: (result) => { setImportResult(result); setImportResultOpen(true); + setImportMessage( + t( + "msg.admin.tenants.import_result", + "생성 {{created}}, 갱신 {{updated}}, 실패 {{failed}}", + { + created: result.created, + updated: result.updated, + failed: result.failed, + }, + ), + ); setPreviewOpen(false); setPreviewRows([]); setSelectedMatches({}); @@ -1135,7 +1150,7 @@ function TenantListPage() { ) : ( - filteredImportDetails.map((detail) => ( + filteredImportDetails.map((detail: TenantImportDetail) => ( {detail.row} @@ -1173,7 +1188,7 @@ function TenantListPage() { "수정됨:", )} - {detail.modifiedFields.map((field) => ( + {detail.modifiedFields.map((field: string) => ( { importRequested = true; importBody = route.request().postData() ?? ""; return route.fulfill({ - json: { created: 0, updated: 1, failed: 0, errors: [] }, + json: { + created: 0, + updated: 1, + failed: 0, + errors: [], + details: [ + { + row: 2, + name: "Tenant Alpha", + slug: "tenant-alpha", + success: true, + action: "updated", + message: "updated successfully", + }, + ], + }, headers, }); } @@ -774,9 +789,12 @@ test.describe("Tenants Management", () => { ); await page.getByTestId("tenant-import-confirm-btn").click(); - await expect(page.getByTestId("tenant-import-result")).toContainText( - /갱신 1|Updated 1/i, - ); + const resultDialog = page.getByRole("dialog").filter({ + hasText: /가져오기 결과 리포트|Import Result Report/i, + }); + await expect(resultDialog).toBeVisible({ timeout: 15000 }); + await expect(resultDialog).toContainText(/Updated|갱신/i); + await expect(resultDialog).toContainText("1"); expect(importRequested).toBe(true); expect(importBody).toContain('filename="tenants.csv"'); if (browserName !== "webkit") { @@ -799,7 +817,30 @@ test.describe("Tenants Management", () => { if (url.includes("/import")) { importBody = route.request().postData() ?? ""; return route.fulfill({ - json: { created: 2, updated: 0, failed: 0, errors: [] }, + json: { + created: 2, + updated: 0, + failed: 0, + errors: [], + details: [ + { + row: 2, + name: "Child A", + slug: "child-a", + success: true, + action: "created", + message: "created successfully", + }, + { + row: 3, + name: "Child B", + slug: "child-b", + success: true, + action: "created", + message: "created successfully", + }, + ], + }, headers, }); } diff --git a/common/locales/en.toml b/common/locales/en.toml index a9a3a9d3..8b6d9b81 100644 --- a/common/locales/en.toml +++ b/common/locales/en.toml @@ -26,6 +26,7 @@ subtitle = "View administrator activity history." subtitle = "View developer activity history within the current app scope." [ui.common] +no_results = "No results to display." apply = "Apply" actions = "Actions" add = "Add" diff --git a/common/locales/ko.toml b/common/locales/ko.toml index 9efc4959..57ba4cdb 100644 --- a/common/locales/ko.toml +++ b/common/locales/ko.toml @@ -26,6 +26,7 @@ subtitle = "관리자 작업 이력을 조회합니다." subtitle = "현재 앱 범위의 개발자 작업 이력을 조회합니다." [ui.common] +no_results = "표시할 결과가 없습니다." apply = "적용" actions = "액션" add = "추가" diff --git a/common/locales/template.toml b/common/locales/template.toml index c07c1054..9e536ab3 100644 --- a/common/locales/template.toml +++ b/common/locales/template.toml @@ -26,6 +26,7 @@ subtitle = "" subtitle = "" [ui.common] +no_results = "" apply = "Apply" actions = "" add = "" diff --git a/common/ui/badge.ts b/common/ui/badge.ts index 8d2b7db5..36f069d8 100644 --- a/common/ui/badge.ts +++ b/common/ui/badge.ts @@ -12,6 +12,8 @@ export const commonBadgeVariantClasses = { "border-transparent bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-300", warning: "border-transparent bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-200", + destructive: + "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", info: "border-transparent bg-blue-500 text-white hover:bg-blue-500/90", } as const; diff --git a/locales/en.toml b/locales/en.toml index dde44f75..6c33882b 100644 --- a/locales/en.toml +++ b/locales/en.toml @@ -1179,6 +1179,12 @@ pick = "Select parent scope" description = "" title = "Domain conflict" +[ui.admin.tenants.import_result] +message = "Message" +modified = "Modified:" +status = "Status" +title = "Import Result Report" + [ui.admin.tenants.import_preview] candidates = "Candidates" confirm = "Run import" @@ -1323,6 +1329,12 @@ total = "Total" total_label = "Total" view_profile = "View Profile" +[ui.admin.tenants.import_result] +message = "Message" +modified = "Modified:" +status = "Status" +title = "Import Result Report" + [ui.admin.tenants.import_preview] candidates = "Candidates" confirm = "Confirm Import" diff --git a/locales/ko.toml b/locales/ko.toml index e99be6df..f2655ee2 100644 --- a/locales/ko.toml +++ b/locales/ko.toml @@ -375,6 +375,12 @@ view_org_chart = "전체 조직도 보기" description = "" title = "도메인 충돌" +[ui.admin.tenants.import_result] +message = "상세 내용" +modified = "수정됨:" +status = "상태" +title = "가져오기 결과 리포트" + [ui.admin.tenants.import_preview] candidates = "후보" confirm = "가져오기 실행" @@ -1786,6 +1792,12 @@ total = "전체" total_label = "전체" view_profile = "상세 정보" +[ui.admin.tenants.import_result] +message = "상세 내용" +modified = "수정됨:" +status = "상태" +title = "가져오기 결과 리포트" + [ui.admin.tenants.import_preview] candidates = "후보" confirm = "임포트 확정" diff --git a/locales/template.toml b/locales/template.toml index bd9cf3f9..eac89aca 100644 --- a/locales/template.toml +++ b/locales/template.toml @@ -233,6 +233,12 @@ view_org_chart = "" description = "" title = "" +[ui.admin.tenants.import_result] +message = "" +modified = "" +status = "" +title = "" + [ui.admin.tenants.import_preview] candidates = "" confirm = "" @@ -1747,6 +1753,12 @@ search_placeholder = "" title = "" tree_search_placeholder = "" +[ui.admin.tenants.import_result] +message = "" +modified = "" +status = "" +title = "" + [ui.admin.tenants.import_preview] candidates = "" confirm = ""