From 0a238c551c85223eff96ea89d289897dde57ca09 Mon Sep 17 00:00:00 2001 From: Syngnat Date: Sun, 9 Aug 2026 10:46:50 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(connection):=20=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E6=96=B0=E5=BB=BA=E8=BF=9E=E6=8E=A5=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E6=BA=90=E7=BD=AE=E9=A1=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增数据源类型置顶与取消置顶交互 - 按个人置顶顺序优先展示并持久化偏好 - 完善主题样式、无障碍标签与六语言文案 - 补充排序、持久化及图钉视觉回归测试 Refs #876 --- frontend/src/App.css | 70 ++++++++- .../components/ConnectionModal.i18n.test.tsx | 90 ++++++++++++ frontend/src/components/ConnectionModal.tsx | 133 ++++++++++++------ frontend/src/store.test.ts | 49 +++++++ frontend/src/store.ts | 47 +++++++ .../src/utils/connectionTypeCatalog.test.ts | 23 +++ frontend/src/utils/connectionTypeCatalog.ts | 27 ++++ shared/i18n/de-DE.json | 2 + shared/i18n/en-US.json | 2 + shared/i18n/ja-JP.json | 2 + shared/i18n/ru-RU.json | 2 + shared/i18n/zh-CN.json | 2 + shared/i18n/zh-TW.json | 2 + 13 files changed, 399 insertions(+), 52 deletions(-) diff --git a/frontend/src/App.css b/frontend/src/App.css index 9b348081..3bf1de0b 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -957,17 +957,14 @@ body[data-theme='dark'] .gn-connection-type-group:hover:not([aria-pressed='true' } .gn-conn-type-card { - appearance: none; - text-align: left; - cursor: pointer; - font: inherit; + position: relative; color: inherit; background: var(--gn-bg-panel-2, rgba(16, 24, 40, 0.03)); border: 1px solid var(--gn-br-2, rgba(16, 24, 40, 0.1)); border-radius: var(--radius, 8px); - padding: 12px; transition: border-color 0.15s, transform 0.15s, background 0.15s; min-width: 0; + overflow: hidden; } .gn-conn-type-card:hover { @@ -976,11 +973,72 @@ body[data-theme='dark'] .gn-connection-type-group:hover:not([aria-pressed='true' transform: translateY(-1px); } -.gn-conn-type-card:focus-visible { +.gn-conn-type-card[data-connection-type-pinned='true'] { + border-color: color-mix(in srgb, var(--gn-accent, #1677ff) 54%, var(--gn-br-2, transparent)); + background: color-mix(in srgb, var(--gn-accent, #1677ff) 7%, var(--gn-bg-panel-2, transparent)); +} + +.gn-conn-type-card-select { + appearance: none; + display: block; + width: 100%; + min-height: 100%; + padding: 12px 42px 12px 12px; + border: 0; + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} + +.gn-conn-type-card-select:focus-visible { + outline: 2px solid color-mix(in srgb, var(--gn-accent, #1677ff) 68%, transparent); + outline-offset: -3px; +} + +.gn-conn-type-card-pin { + appearance: none; + position: absolute; + top: 8px; + right: 8px; + z-index: 1; + width: 28px; + height: 28px; + display: grid; + place-items: center; + padding: 0; + border: 0; + border-radius: 7px; + background: transparent; + color: var(--gn-fg-5, rgba(0, 0, 0, 0.4)); + cursor: pointer; + transition: color 0.15s, background 0.15s; +} + +.gn-conn-type-card-pin .anticon { + transition: transform 0.15s; +} + +.gn-conn-type-card-pin:hover { + color: var(--gn-accent, #1677ff); + background: color-mix(in srgb, var(--gn-accent, #1677ff) 12%, transparent); +} + +.gn-conn-type-card-pin:focus-visible { outline: 2px solid color-mix(in srgb, var(--gn-accent, #1677ff) 68%, transparent); outline-offset: 1px; } +.gn-conn-type-card-pin[aria-pressed='true'] { + color: var(--gn-accent, #1677ff); + background: color-mix(in srgb, var(--gn-accent, #1677ff) 14%, transparent); +} + +.gn-conn-type-card-pin[aria-pressed='true'] .anticon { + transform: rotate(-18deg); +} + .gn-conn-type-card-top { display: flex; align-items: center; diff --git a/frontend/src/components/ConnectionModal.i18n.test.tsx b/frontend/src/components/ConnectionModal.i18n.test.tsx index 506513af..cdc464ed 100644 --- a/frontend/src/components/ConnectionModal.i18n.test.tsx +++ b/frontend/src/components/ConnectionModal.i18n.test.tsx @@ -10,6 +10,14 @@ import { getCustomConnectionDriverHelp } from "../utils/driverImportGuidance"; const storeState = { addConnection: vi.fn(), updateConnection: vi.fn(), + pinnedConnectionTypes: [] as string[], + setConnectionTypePinned: vi.fn((dbType: string, pinned: boolean) => { + const normalized = dbType.trim().toLowerCase(); + storeState.pinnedConnectionTypes = pinned + ? [normalized, ...storeState.pinnedConnectionTypes.filter((item) => item !== normalized)] + : storeState.pinnedConnectionTypes.filter((item) => item !== normalized); + notifyStoreSubscribers(); + }), theme: "light", languagePreference: "zh-CN", setLanguagePreference: vi.fn((languagePreference: "zh-CN" | "en-US") => { @@ -92,6 +100,16 @@ const findConnectionTypeGroup = ( node.props?.["data-connection-group-key"] === groupKey, )[0]; +const findConnectionTypePin = ( + renderer: ReactTestRenderer, + dbType: string, +) => + renderer.root.findAll( + (node) => + node.type === "button" && + node.props?.["data-connection-type-pin"] === dbType, + )[0]; + const findInputByPlaceholder = ( renderer: ReactTestRenderer, placeholder: string, @@ -120,6 +138,7 @@ const flushConnectionTestTick = async () => { }; const source = readFileSync(new URL("./ConnectionModal.tsx", import.meta.url), "utf8"); +const appCssSource = readFileSync(new URL("../App.css", import.meta.url), "utf8"); const step2Source = readFileSync(new URL("./connectionModal/ConnectionModalStep2.tsx", import.meta.url), "utf8"); const networkSecuritySource = readFileSync( new URL("./connectionModal/ConnectionModalNetworkSecuritySection.tsx", import.meta.url), @@ -210,6 +229,7 @@ vi.mock("@ant-design/icons", () => { DownOutlined: Icon, RightOutlined: Icon, SearchOutlined: Icon, + PushpinOutlined: Icon, }; }); @@ -406,6 +426,7 @@ describe("ConnectionModal i18n", () => { storeState.languagePreference = "zh-CN"; storeState.appearance.uiVersion = "legacy"; storeState.appearance.opacity = 1; + storeState.pinnedConnectionTypes = []; backendApp.GetDriverStatusList.mockResolvedValue({ success: true, data: { drivers: [] } }); backendApp.SaveConnection.mockReset(); backendApp.SaveConnection.mockImplementation(async (input) => ({ @@ -427,6 +448,7 @@ describe("ConnectionModal i18n", () => { modalConfirm.mockReset(); storeState.addConnection.mockReset(); storeState.updateConnection.mockReset(); + storeState.setConnectionTypePinned.mockClear(); storeState.setLanguagePreference.mockClear(); mockFormValues = {}; mockValidateFields = undefined; @@ -1573,6 +1595,74 @@ describe("ConnectionModal i18n", () => { expect(findClickableCard(renderer!, "MySQL").props.type).toBe("button"); }); + it("pins data source types without opening the form and restores their personal order", async () => { + setCurrentLanguage("en-US"); + const { default: ConnectionModal } = await import("./ConnectionModal"); + + let renderer: ReactTestRenderer; + await act(async () => { + renderer = create(); + }); + + expect( + findConnectionTypeButtons(renderer!).slice(0, 3).map( + (node) => node.props["data-connection-type-key"], + ), + ).toEqual(["mysql", "mariadb", "diros"]); + expect(findConnectionTypePin(renderer!, "postgres").props["aria-pressed"]).toBe(false); + expect(findConnectionTypePin(renderer!, "postgres").props["aria-label"]).toBe( + "Pin PostgreSQL", + ); + + await act(async () => { + findConnectionTypePin(renderer!, "postgres").props.onClick(); + }); + + expect(storeState.setConnectionTypePinned).toHaveBeenCalledWith("postgres", true); + expect(findConnectionTypeButtons(renderer!)[0].props["data-connection-type-key"]).toBe( + "postgres", + ); + expect(findConnectionTypePin(renderer!, "postgres").props["aria-pressed"]).toBe(true); + expect(findConnectionTypePin(renderer!, "postgres").props["aria-label"]).toBe( + "Unpin PostgreSQL", + ); + expect( + renderer!.root.findAll( + (node) => node.props?.["data-connection-step"] === "1", + ), + ).toHaveLength(1); + + await act(async () => { + findConnectionTypePin(renderer!, "redis").props.onClick(); + }); + expect( + findConnectionTypeButtons(renderer!).slice(0, 3).map( + (node) => node.props["data-connection-type-key"], + ), + ).toEqual(["redis", "postgres", "mysql"]); + + await act(async () => { + findConnectionTypePin(renderer!, "redis").props.onClick(); + }); + expect(findConnectionTypeButtons(renderer!)[0].props["data-connection-type-key"]).toBe( + "postgres", + ); + }); + + it("keeps the pinned button frame level while rotating only the pushpin icon", () => { + const pinnedButtonRule = appCssSource.match( + /\.gn-conn-type-card-pin\[aria-pressed='true'\]\s*\{([^}]*)\}/, + ); + const pinnedIconRule = appCssSource.match( + /\.gn-conn-type-card-pin\[aria-pressed='true'\]\s+\.anticon\s*\{([^}]*)\}/, + ); + + expect(pinnedButtonRule).not.toBeNull(); + expect(pinnedButtonRule?.[1]).not.toMatch(/transform\s*:/); + expect(pinnedIconRule).not.toBeNull(); + expect(pinnedIconRule?.[1]).toMatch(/transform:\s*rotate\(-18deg\)/); + }); + it("renders English custom driver DSN copy after the module was loaded in another language", async () => { storeState.appearance.uiVersion = "legacy"; setCurrentLanguage("zh-CN"); diff --git a/frontend/src/components/ConnectionModal.tsx b/frontend/src/components/ConnectionModal.tsx index 2a82bbd3..5f60d177 100644 --- a/frontend/src/components/ConnectionModal.tsx +++ b/frontend/src/components/ConnectionModal.tsx @@ -32,6 +32,7 @@ import { DownOutlined, RightOutlined, SearchOutlined, + PushpinOutlined, } from "@ant-design/icons"; import { getDbIcon, @@ -107,6 +108,7 @@ import { getAllConnectionTypeCatalogItems, getConnectionTypeDefaultPort as getDefaultPortByType, getConnectionTypeHint, + orderConnectionTypeCatalogItems, } from "../utils/connectionTypeCatalog"; import { isFileDatabaseType, @@ -397,6 +399,10 @@ const ConnectionModal: React.FC<{ const updateConnection = useStore((state) => state.updateConnection); const savedConnections = useStore((state) => state.connections) ?? []; const recentConnectionTargets = useStore((state) => state.recentConnectionTargets) ?? []; + const pinnedConnectionTypes = useStore((state) => state.pinnedConnectionTypes) ?? []; + const setConnectionTypePinned = useStore( + (state) => state.setConnectionTypePinned, + ); const theme = useStore((state) => state.theme); const appearance = useStore((state) => state.appearance); const languagePreference = useStore((state) => state.languagePreference); @@ -2487,13 +2493,18 @@ const ConnectionModal: React.FC<{ ]; const normalizedDbTypeQuery = dbTypeQuery.trim().toLowerCase(); - const visibleDbTypeItems = normalizedDbTypeQuery + const filteredDbTypeItems = normalizedDbTypeQuery ? (dbTypeGroups[0]?.items ?? []).filter( (item) => item.name.toLowerCase().includes(normalizedDbTypeQuery) || String(item.key).toLowerCase().includes(normalizedDbTypeQuery), ) : (dbTypeGroups[activeGroup]?.items ?? []); + const visibleDbTypeItems = orderConnectionTypeCatalogItems( + filteredDbTypeItems, + pinnedConnectionTypes, + ); + const pinnedConnectionTypeSet = new Set(pinnedConnectionTypes); const handleDbTypeQueryChange = (value: string) => { setDbTypeQuery(value); @@ -2671,6 +2682,7 @@ const ConnectionModal: React.FC<{
{visibleDbTypeItems.map((item) => { + const pinned = pinnedConnectionTypeSet.has(item.key); // 卡片所属的数据源分类展示标签。 const categoryLabel = localizedDbTypeGroups.find((group) => group.items.some((groupItem) => groupItem.key === item.key), @@ -2681,56 +2693,85 @@ const ConnectionModal: React.FC<{ supportsSSLForType(item.key) ? "SSL" : null, ].filter((tag): tag is string => Boolean(tag)); return ( -
- {studioTags.length > 0 ? ( - - ) : null} - + {studioTags.length > 0 ? ( + + ) : null} + + + ); })} diff --git a/frontend/src/store.test.ts b/frontend/src/store.test.ts index 0d2835ea..82f5f652 100644 --- a/frontend/src/store.test.ts +++ b/frontend/src/store.test.ts @@ -3804,3 +3804,52 @@ describe('sidebar database pin persistence', () => { expect(reloaded.useStore.getState().pinnedSidebarDatabases).toEqual([]); }); }); + +describe('connection type pin persistence', () => { + let storage: MemoryStorage; + + beforeEach(() => { + storage = new MemoryStorage(); + vi.stubGlobal('localStorage', storage); + vi.resetModules(); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + vi.resetModules(); + }); + + it('persists an ordered and sanitized list of pinned connection types', async () => { + const { updatePinnedConnectionTypeKeys, useStore } = await importStore(); + + expect(updatePinnedConnectionTypeKeys(['mysql'], ' Redis ', true)).toEqual([ + 'redis', + 'mysql', + ]); + expect(updatePinnedConnectionTypeKeys(['redis', 'mysql'], 'redis', true)).toEqual([ + 'redis', + 'mysql', + ]); + expect(updatePinnedConnectionTypeKeys(['redis', 'mysql'], '../bad', true)).toEqual([ + 'redis', + 'mysql', + ]); + + useStore.getState().setConnectionTypePinned('mysql', true); + useStore.getState().setConnectionTypePinned('redis', true); + expect(useStore.getState().pinnedConnectionTypes).toEqual(['redis', 'mysql']); + + const persisted = JSON.parse(storage.getItem('lite-db-storage') || '{}'); + expect(persisted.state.pinnedConnectionTypes).toEqual(['redis', 'mysql']); + + vi.resetModules(); + const reloaded = await importStore(); + expect(reloaded.useStore.getState().pinnedConnectionTypes).toEqual([ + 'redis', + 'mysql', + ]); + + reloaded.useStore.getState().setConnectionTypePinned('redis', false); + expect(reloaded.useStore.getState().pinnedConnectionTypes).toEqual(['mysql']); + }); +}); diff --git a/frontend/src/store.ts b/frontend/src/store.ts index 348c4860..af0c6c12 100644 --- a/frontend/src/store.ts +++ b/frontend/src/store.ts @@ -1798,6 +1798,7 @@ interface AppState { externalSQLDirectories: ExternalSQLDirectory[]; recentConnectionTargets: RecentConnectionTarget[]; recentSQLFiles: RecentSQLFile[]; + pinnedConnectionTypes: string[]; theme: ThemeMode; themePreference: ThemePreference; /** Built-in brand mascot icon id (01-10), used in title bar / about / favicon. */ @@ -2040,6 +2041,7 @@ interface AppState { dbName: string, pinned: boolean, ) => void; + setConnectionTypePinned: (dbType: string, pinned: boolean) => void; setTableColumnOrder: ( connectionId: string, dbName: string, @@ -2996,6 +2998,31 @@ const sanitizePinnedSidebarTables = (value: unknown): string[] => { ); }; +const sanitizePinnedConnectionTypes = (value: unknown): string[] => { + if (!Array.isArray(value)) return []; + return Array.from( + new Set( + value + .map((entry) => toTrimmedString(entry).toLowerCase()) + .filter((entry) => /^[a-z0-9][a-z0-9_-]{0,63}$/.test(entry)), + ), + ).slice(0, 64); +}; + +export const updatePinnedConnectionTypeKeys = ( + pinnedTypes: unknown, + dbType: string, + pinned: boolean, +): string[] => { + const current = sanitizePinnedConnectionTypes(pinnedTypes); + const normalizedType = toTrimmedString(dbType).toLowerCase(); + if (!/^[a-z0-9][a-z0-9_-]{0,63}$/.test(normalizedType)) { + return current; + } + const withoutCurrent = current.filter((entry) => entry !== normalizedType); + return pinned ? [normalizedType, ...withoutCurrent] : withoutCurrent; +}; + const isLegacyDefaultTabDisplaySettings = (value: unknown): boolean => { if (!value || typeof value !== "object") return false; const raw = value as Partial; @@ -3475,6 +3502,7 @@ const PERSISTED_STATE_DEPENDENCY_KEYS = [ "externalSQLDirectories", "recentConnectionTargets", "recentSQLFiles", + "pinnedConnectionTypes", "theme", "themePreference", "brandIconId", @@ -3531,6 +3559,9 @@ const buildPersistedStateProjection = ( state.recentConnectionTargets, ), recentSQLFiles: sanitizeRecentSQLFiles(state.recentSQLFiles), + pinnedConnectionTypes: sanitizePinnedConnectionTypes( + state.pinnedConnectionTypes, + ), theme: state.theme, themePreference: state.themePreference, brandIconId: sanitizeBrandIconIdLocal(state.brandIconId), @@ -3651,6 +3682,7 @@ export const useStore = create()( externalSQLDirectories: [], recentConnectionTargets: [], recentSQLFiles: [], + pinnedConnectionTypes: [], theme: "light", themePreference: "light", brandIconId: "02", @@ -5401,6 +5433,15 @@ export const useStore = create()( ), })), + setConnectionTypePinned: (dbType, pinned) => + set((state) => ({ + pinnedConnectionTypes: updatePinnedConnectionTypeKeys( + state.pinnedConnectionTypes, + dbType, + pinned, + ), + })), + setTableColumnOrder: (connectionId, dbName, tableName, order) => set((state) => { const key = `${connectionId}-${dbName}-${tableName}`; @@ -5937,6 +5978,9 @@ export const useStore = create()( state.recentConnectionTargets, ); nextState.recentSQLFiles = sanitizeRecentSQLFiles(state.recentSQLFiles); + nextState.pinnedConnectionTypes = sanitizePinnedConnectionTypes( + state.pinnedConnectionTypes, + ); nextState.theme = sanitizeTheme(state.theme); nextState.themePreference = sanitizeThemePreference( state.themePreference, @@ -6081,6 +6125,9 @@ export const useStore = create()( state.recentConnectionTargets, ), recentSQLFiles: sanitizeRecentSQLFiles(state.recentSQLFiles), + pinnedConnectionTypes: sanitizePinnedConnectionTypes( + state.pinnedConnectionTypes, + ), theme: sanitizeTheme(state.theme), themePreference: sanitizeThemePreference( state.themePreference, diff --git a/frontend/src/utils/connectionTypeCatalog.test.ts b/frontend/src/utils/connectionTypeCatalog.test.ts index 59e2a3cf..8bf8c1bf 100644 --- a/frontend/src/utils/connectionTypeCatalog.test.ts +++ b/frontend/src/utils/connectionTypeCatalog.test.ts @@ -6,6 +6,7 @@ import { getAllConnectionTypeCatalogItems, getConnectionTypeDefaultPort, getConnectionTypeHint, + orderConnectionTypeCatalogItems, } from './connectionTypeCatalog'; const translatedCopy: Record = { @@ -120,4 +121,26 @@ describe('connectionTypeCatalog', () => { expect(getConnectionTypeHint('duckdb', translate)).toBe('T:file'); expect(getConnectionTypeHint('mysql', translate)).toBe('T:standard'); }); + + it('moves pinned types first without changing the default order of other types', () => { + const items = [ + { key: 'mysql', name: 'MySQL' }, + { key: 'postgres', name: 'PostgreSQL' }, + { key: 'sqlite', name: 'SQLite' }, + { key: 'redis', name: 'Redis' }, + ]; + + expect(orderConnectionTypeCatalogItems(items, [])).toEqual(items); + expect( + orderConnectionTypeCatalogItems(items, ['redis', 'postgres', 'redis']).map( + (item) => item.key, + ), + ).toEqual(['redis', 'postgres', 'mysql', 'sqlite']); + expect(items.map((item) => item.key)).toEqual([ + 'mysql', + 'postgres', + 'sqlite', + 'redis', + ]); + }); }); diff --git a/frontend/src/utils/connectionTypeCatalog.ts b/frontend/src/utils/connectionTypeCatalog.ts index 46557476..ee6d5081 100644 --- a/frontend/src/utils/connectionTypeCatalog.ts +++ b/frontend/src/utils/connectionTypeCatalog.ts @@ -15,6 +15,33 @@ export type ConnectionTypeCatalogGroup = { items: ConnectionTypeCatalogItem[]; }; +export const orderConnectionTypeCatalogItems = ( + items: readonly ConnectionTypeCatalogItem[], + pinnedTypes: readonly string[], +): ConnectionTypeCatalogItem[] => { + const pinnedRank = new Map(); + pinnedTypes.forEach((type, index) => { + const normalized = String(type || "").trim().toLowerCase(); + if (normalized && !pinnedRank.has(normalized)) { + pinnedRank.set(normalized, index); + } + }); + + return items + .map((item, originalIndex) => ({ item, originalIndex })) + .sort((left, right) => { + const leftRank = pinnedRank.get(left.item.key.toLowerCase()); + const rightRank = pinnedRank.get(right.item.key.toLowerCase()); + if (leftRank !== undefined && rightRank !== undefined) { + return leftRank - rightRank; + } + if (leftRank !== undefined) return -1; + if (rightRank !== undefined) return 1; + return left.originalIndex - right.originalIndex; + }) + .map(({ item }) => item); +}; + const translateCatalogCopy = ( translate: ConnectionTypeCatalogTranslator | undefined, key: string, diff --git a/shared/i18n/de-DE.json b/shared/i18n/de-DE.json index 7f6789f3..98a53793 100644 --- a/shared/i18n/de-DE.json +++ b/shared/i18n/de-DE.json @@ -9065,6 +9065,8 @@ "connection_modal.step1.recent": "Zuletzt", "connection_modal.step1.categories": "Kategorien", "connection_modal.step1.clickToConfigure": "Zum Konfigurieren klicken", + "connection_modal.step1.pin": "{{name}} anheften", + "connection_modal.step1.unpin": "Anheften von {{name}} lösen", "connection_modal.step1.recent.empty": "Keine letzten Verbindungen", "connection_modal.studio.step.chooseType": "Typ wählen", "connection_modal.studio.step.configure": "Konfigurieren", diff --git a/shared/i18n/en-US.json b/shared/i18n/en-US.json index aeddba8a..1fbb2590 100644 --- a/shared/i18n/en-US.json +++ b/shared/i18n/en-US.json @@ -9065,6 +9065,8 @@ "connection_modal.step1.recent": "Recent", "connection_modal.step1.categories": "Categories", "connection_modal.step1.clickToConfigure": "Click to configure", + "connection_modal.step1.pin": "Pin {{name}}", + "connection_modal.step1.unpin": "Unpin {{name}}", "connection_modal.step1.recent.empty": "No recent connections", "connection_modal.studio.step.chooseType": "Choose type", "connection_modal.studio.step.configure": "Configure", diff --git a/shared/i18n/ja-JP.json b/shared/i18n/ja-JP.json index ea758ef6..6053315c 100644 --- a/shared/i18n/ja-JP.json +++ b/shared/i18n/ja-JP.json @@ -9065,6 +9065,8 @@ "connection_modal.step1.recent": "最近", "connection_modal.step1.categories": "カテゴリ", "connection_modal.step1.clickToConfigure": "クリックして設定", + "connection_modal.step1.pin": "{{name}}をピン留め", + "connection_modal.step1.unpin": "{{name}}のピン留めを解除", "connection_modal.step1.recent.empty": "最近の接続なし", "connection_modal.studio.step.chooseType": "種類を選択", "connection_modal.studio.step.configure": "パラメータ", diff --git a/shared/i18n/ru-RU.json b/shared/i18n/ru-RU.json index e6c5291e..33157c07 100644 --- a/shared/i18n/ru-RU.json +++ b/shared/i18n/ru-RU.json @@ -9065,6 +9065,8 @@ "connection_modal.step1.recent": "Недавние", "connection_modal.step1.categories": "Категории", "connection_modal.step1.clickToConfigure": "Нажмите для настройки", + "connection_modal.step1.pin": "Закрепить {{name}}", + "connection_modal.step1.unpin": "Открепить {{name}}", "connection_modal.step1.recent.empty": "Нет недавних подключений", "connection_modal.studio.step.chooseType": "Выбрать тип", "connection_modal.studio.step.configure": "Параметры", diff --git a/shared/i18n/zh-CN.json b/shared/i18n/zh-CN.json index ce2cac3e..c3919298 100644 --- a/shared/i18n/zh-CN.json +++ b/shared/i18n/zh-CN.json @@ -9065,6 +9065,8 @@ "connection_modal.step1.recent": "最近", "connection_modal.step1.categories": "分类", "connection_modal.step1.clickToConfigure": "单击进入配置表单", + "connection_modal.step1.pin": "置顶 {{name}}", + "connection_modal.step1.unpin": "取消置顶 {{name}}", "connection_modal.step1.recent.empty": "暂无连接记录", "connection_modal.studio.step.chooseType": "选类型", "connection_modal.studio.step.configure": "配参数", diff --git a/shared/i18n/zh-TW.json b/shared/i18n/zh-TW.json index 4d64c271..78a1f46a 100644 --- a/shared/i18n/zh-TW.json +++ b/shared/i18n/zh-TW.json @@ -9065,6 +9065,8 @@ "connection_modal.step1.recent": "最近", "connection_modal.step1.categories": "分類", "connection_modal.step1.clickToConfigure": "按一下進入設定表單", + "connection_modal.step1.pin": "置頂 {{name}}", + "connection_modal.step1.unpin": "取消置頂 {{name}}", "connection_modal.step1.recent.empty": "尚無連線紀錄", "connection_modal.studio.step.chooseType": "選類型", "connection_modal.studio.step.configure": "設參數",