From 23ebc67e6c457e3991b0ad28f78d015b163905da Mon Sep 17 00:00:00 2001 From: cowking <3200017240@qq.com> Date: Wed, 9 Sep 2026 14:57:03 +0800 Subject: [PATCH] feat(grid): support UUID v4 and v7 value generation --- apps/desktop/src/components/grid/DataGrid.vue | 10 +++- apps/desktop/src/i18n/locales/az.ts | 3 +- apps/desktop/src/i18n/locales/en.ts | 3 +- apps/desktop/src/i18n/locales/es.ts | 3 +- apps/desktop/src/i18n/locales/it.ts | 3 +- apps/desktop/src/i18n/locales/ja.ts | 3 +- apps/desktop/src/i18n/locales/ko.ts | 3 +- apps/desktop/src/i18n/locales/pt-BR.ts | 3 +- apps/desktop/src/i18n/locales/tr.ts | 3 +- apps/desktop/src/i18n/locales/zh-CN.ts | 3 +- apps/desktop/src/i18n/locales/zh-TW.ts | 3 +- .../dataGrid/cellValueGeneration.spec.ts | 58 ++++++++++++++++++- .../src/lib/dataGrid/cellValueGeneration.ts | 8 ++- 13 files changed, 92 insertions(+), 14 deletions(-) diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue index 124b5b8aeb..2685ddd8fc 100644 --- a/apps/desktop/src/components/grid/DataGrid.vue +++ b/apps/desktop/src/components/grid/DataGrid.vue @@ -7932,10 +7932,15 @@ function generateSelectionMenuItems(disabled: boolean): ContextMenuItem[] { disabled, }, { - label: t("grid.generateUuid"), + label: t("grid.generateUuidV4"), action: () => applyGeneratedSelectionValue("uuid"), disabled, }, + { + label: t("grid.generateUuidV7"), + action: () => applyGeneratedSelectionValue("uuid-v7"), + disabled, + }, { label: t("grid.generateSnowflakeId"), action: () => applyGeneratedSelectionValue("snowflake"), @@ -12817,7 +12822,8 @@ function openGridSnapshot() { {{ t("grid.generateNull") }} {{ t("grid.generateCurrentDatetime") }} {{ t("grid.generateCurrentDate") }} - {{ t("grid.generateUuid") }} + {{ t("grid.generateUuidV4") }} + {{ t("grid.generateUuidV7") }} {{ t("grid.generateSnowflakeId") }} {{ t("grid.generateIncrementId") }} diff --git a/apps/desktop/src/i18n/locales/az.ts b/apps/desktop/src/i18n/locales/az.ts index 6ae6fb62cf..49ec31d709 100644 --- a/apps/desktop/src/i18n/locales/az.ts +++ b/apps/desktop/src/i18n/locales/az.ts @@ -2027,7 +2027,8 @@ export default withEnglishFallback({ generateNull: "NULL", generateCurrentDatetime: "Cari tarix və vaxt", generateCurrentDate: "Cari tarix", - generateUuid: "UUID", + generateUuidV4: "UUID v4", + generateUuidV7: "UUID v7", generateIncrementId: "Artan ID", generateSnowflakeId: "Snowflake ID", generateSequenceDescription: "Seçilmiş {count} xana üçün ardıcıl dəyərlər yaradın. Başlanğıc dəyəri daxil edin.", diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index 654ec509cf..a58c09bccf 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -2028,7 +2028,8 @@ export default { generateNull: "NULL", generateCurrentDatetime: "Current Datetime", generateCurrentDate: "Current Date", - generateUuid: "UUID", + generateUuidV4: "UUID v4", + generateUuidV7: "UUID v7", generateIncrementId: "Increment ID", generateSnowflakeId: "Snowflake ID", generateSequenceDescription: "Generate consecutive values for {count} selected cell(s). Enter the start value.", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index 79d90ff3aa..7557868701 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -1882,7 +1882,8 @@ export default withEnglishFallback({ generateNull: "NULL", generateCurrentDatetime: "Fecha y hora actuales", generateCurrentDate: "Fecha actual", - generateUuid: "UUID", + generateUuidV4: "UUID v4", + generateUuidV7: "UUID v7", generateIncrementId: "ID incremental", generateSnowflakeId: "ID Snowflake", generateSequenceDescription: "Generar valores consecutivos para {count} celda(s) seleccionada(s). Introduzca el valor inicial.", diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts index 177259a3a7..f2aae64a4a 100644 --- a/apps/desktop/src/i18n/locales/it.ts +++ b/apps/desktop/src/i18n/locales/it.ts @@ -1880,7 +1880,8 @@ export default withEnglishFallback({ generateNull: "NULL", generateCurrentDatetime: "Data e ora correnti", generateCurrentDate: "Data corrente", - generateUuid: "UUID", + generateUuidV4: "UUID v4", + generateUuidV7: "UUID v7", generateIncrementId: "ID incrementale", generateSnowflakeId: "ID Snowflake", generateSequenceDescription: "Genera valori consecutivi per le {count} celle selezionate. Inserisci il valore iniziale.", diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts index cc3dbad6a8..87c0b7fdc8 100644 --- a/apps/desktop/src/i18n/locales/ja.ts +++ b/apps/desktop/src/i18n/locales/ja.ts @@ -1892,7 +1892,8 @@ export default withEnglishFallback({ generateNull: "NULL", generateCurrentDatetime: "現在の日時", generateCurrentDate: "現在の日付", - generateUuid: "UUID", + generateUuidV4: "UUID v4", + generateUuidV7: "UUID v7", generateIncrementId: "連番ID", generateSnowflakeId: "Snowflake ID", generateSequenceDescription: "選択した{count}セルに連続値を生成します。開始値を入力してください。", diff --git a/apps/desktop/src/i18n/locales/ko.ts b/apps/desktop/src/i18n/locales/ko.ts index f4e6ae4395..75f1e4720c 100644 --- a/apps/desktop/src/i18n/locales/ko.ts +++ b/apps/desktop/src/i18n/locales/ko.ts @@ -1864,7 +1864,8 @@ export default withEnglishFallback({ generateNull: "NULL", generateCurrentDatetime: "현재 날짜/시간", generateCurrentDate: "현재 날짜", - generateUuid: "UUID", + generateUuidV4: "UUID v4", + generateUuidV7: "UUID v7", generateIncrementId: "증가 ID", generateSnowflakeId: "Snowflake ID", generateSequenceDescription: "선택한 {count}개 셀에 연속 값을 생성합니다. 시작 값을 입력하세요.", diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts index 338f69b996..d76c7eaf95 100644 --- a/apps/desktop/src/i18n/locales/pt-BR.ts +++ b/apps/desktop/src/i18n/locales/pt-BR.ts @@ -1882,7 +1882,8 @@ export default withEnglishFallback({ generateNull: "NULL", generateCurrentDatetime: "Data e hora atuais", generateCurrentDate: "Data atual", - generateUuid: "UUID", + generateUuidV4: "UUID v4", + generateUuidV7: "UUID v7", generateIncrementId: "ID incremental", generateSnowflakeId: "ID Snowflake", generateSequenceDescription: "Gere valores consecutivos para {count} célula(s) selecionada(s). Informe o valor inicial.", diff --git a/apps/desktop/src/i18n/locales/tr.ts b/apps/desktop/src/i18n/locales/tr.ts index 4a65ed1cb8..4984ec4dc3 100644 --- a/apps/desktop/src/i18n/locales/tr.ts +++ b/apps/desktop/src/i18n/locales/tr.ts @@ -2022,7 +2022,8 @@ export default withEnglishFallback({ generateNull: "NULL", generateCurrentDatetime: "Geçerli Tarih-Saat", generateCurrentDate: "Geçerli Tarih", - generateUuid: "UUID", + generateUuidV4: "UUID v4", + generateUuidV7: "UUID v7", generateIncrementId: "Artan Kimlik", generateSnowflakeId: "Snowflake Kimliği", generateSequenceDescription: "Seçili {count} hücre için ardışık değerler üretin. Başlangıç değerini girin.", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index 9570b0842b..4577884bea 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -1952,7 +1952,8 @@ export default withEnglishFallback({ generateNull: "NULL", generateCurrentDatetime: "当前日期时间", generateCurrentDate: "当前日期", - generateUuid: "UUID", + generateUuidV4: "UUID v4", + generateUuidV7: "UUID v7", generateIncrementId: "递增 ID", generateSnowflakeId: "雪花 ID", generateSequenceDescription: "为已选 {count} 个单元格生成连续值,请输入起始值。", diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts index 51614bf5af..ca31674d29 100644 --- a/apps/desktop/src/i18n/locales/zh-TW.ts +++ b/apps/desktop/src/i18n/locales/zh-TW.ts @@ -1881,7 +1881,8 @@ export default withEnglishFallback({ generateNull: "NULL", generateCurrentDatetime: "目前日期時間", generateCurrentDate: "目前日期", - generateUuid: "UUID", + generateUuidV4: "UUID v4", + generateUuidV7: "UUID v7", generateIncrementId: "遞增 ID", generateSnowflakeId: "雪花 ID", generateSequenceDescription: "為已選 {count} 個儲存格產生連續值,請輸入起始值。", diff --git a/apps/desktop/src/lib/__tests__/dataGrid/cellValueGeneration.spec.ts b/apps/desktop/src/lib/__tests__/dataGrid/cellValueGeneration.spec.ts index 93ff7e53bf..7891898a70 100644 --- a/apps/desktop/src/lib/__tests__/dataGrid/cellValueGeneration.spec.ts +++ b/apps/desktop/src/lib/__tests__/dataGrid/cellValueGeneration.spec.ts @@ -1,7 +1,11 @@ -import { describe, expect, it } from "vitest"; +import { afterEach, describe, expect, it, vi } from "vitest"; import { createSnowflakeIdGenerator, generateCellValues } from "@/lib/dataGrid/cellValueGeneration"; describe("generateCellValues", () => { + afterEach(() => { + vi.unstubAllGlobals(); + }); + it("generates empty strings and nulls", () => { expect(generateCellValues("empty", 2)).toEqual(["", ""]); expect(generateCellValues("null", 2)).toEqual([null, null]); @@ -18,6 +22,58 @@ describe("generateCellValues", () => { expect(generateCellValues("uuid", 3, { uuidFactory: () => `uuid-${++index}` })).toEqual(["uuid-1", "uuid-2", "uuid-3"]); }); + it.each([ + ["uuid", /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/], + ["uuid-v7", /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/], + ] as const)("generates distinct %s values for cells in the same millisecond", (kind, pattern) => { + const values = generateCellValues(kind, 5000, { now: new Date(1_645_557_742_000) }); + + expect(values).toHaveLength(5000); + expect(values.every((value) => typeof value === "string" && pattern.test(value))).toBe(true); + expect(new Set(values).size).toBe(values.length); + }); + + it("encodes the UUID v7 timestamp and random fields from the RFC 9562 test vector", () => { + const values = generateCellValues("uuid-v7", 1, { + now: new Date(1_645_557_742_000), + uuidFactory: () => "11111111-2222-4cc3-98c4-dc0c0c07398f", + }); + + expect(values).toEqual(["017f22e2-79b0-7cc3-98c4-dc0c0c07398f"]); + }); + + it("pads early UUID v7 timestamps to 48 bits", () => { + expect( + generateCellValues("uuid-v7", 1, { + now: new Date(1), + uuidFactory: () => "11111111-2222-4cc3-98c4-dc0c0c07398f", + }), + ).toEqual(["00000000-0001-7cc3-98c4-dc0c0c07398f"]); + }); + + it("orders UUID v7 values by millisecond across a timestamp field boundary", () => { + const [earlier] = generateCellValues("uuid-v7", 1, { + now: new Date(0xffff_ffff), + uuidFactory: () => "ffffffff-ffff-4fff-bfff-ffffffffffff", + }); + const [later] = generateCellValues("uuid-v7", 1, { + now: new Date(0x1_0000_0000), + uuidFactory: () => "00000000-0000-4000-8000-000000000000", + }); + + expect(earlier).toBe("0000ffff-ffff-7fff-bfff-ffffffffffff"); + expect(later).toBe("00010000-0000-7000-8000-000000000000"); + expect(earlier! < later!).toBe(true); + }); + + it("generates UUID v7 values when crypto.randomUUID is unavailable", () => { + vi.stubGlobal("crypto", {}); + const values = generateCellValues("uuid-v7", 20, { now: new Date(1_645_557_742_000) }); + + expect(values.every((value) => /^017f22e2-79b0-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(value!))).toBe(true); + expect(new Set(values).size).toBe(20); + }); + it("increments values without losing bigint precision", () => { expect(generateCellValues("increment", 3, { startValue: 9_007_199_254_740_993n })).toEqual(["9007199254740993", "9007199254740994", "9007199254740995"]); }); diff --git a/apps/desktop/src/lib/dataGrid/cellValueGeneration.ts b/apps/desktop/src/lib/dataGrid/cellValueGeneration.ts index bcd76866fb..b833979f95 100644 --- a/apps/desktop/src/lib/dataGrid/cellValueGeneration.ts +++ b/apps/desktop/src/lib/dataGrid/cellValueGeneration.ts @@ -1,6 +1,6 @@ import { uuid } from "@/lib/common/utils"; -export type CellValueGenerationKind = "empty" | "null" | "datetime" | "date" | "uuid" | "increment" | "snowflake"; +export type CellValueGenerationKind = "empty" | "null" | "datetime" | "date" | "uuid" | "uuid-v7" | "increment" | "snowflake"; export interface SnowflakeIdGenerator { next(nowMs?: number): string; @@ -59,6 +59,12 @@ export function generateCellValues( if (kind === "datetime") return localDateTimeText(now); if (kind === "date") return localDateText(now); if (kind === "uuid") return uuidFactory(); + if (kind === "uuid-v7") { + // RFC 9562: 48-bit Unix milliseconds, version 7, then 74 random bits and the UUID variant. + // A v4 UUID already supplies the random bits and variant in the same positions. + const timestamp = now.getTime().toString(16).padStart(12, "0"); + return `${timestamp.slice(0, 8)}-${timestamp.slice(8)}-7${uuidFactory().slice(15)}`; + } if (kind === "increment") return String((options.startValue ?? 1n) + BigInt(index)); return snowflakeGenerator.next(now.getTime()); });