feat(grid): support UUID v4 and v7 value generation

This commit is contained in:
cowking authored and GitHub committed 2026-09-09 14:57:03 +08:00
1 parent a04ba3abd4
commit 23ebc67e6c
13 files changed
+92 -14

No files matched your search

@@ -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() {
<DropdownMenuItem @click="applyGeneratedDetailValue('null')">{{ t("grid.generateNull") }}</DropdownMenuItem>
<DropdownMenuItem @click="applyGeneratedDetailValue('datetime')">{{ t("grid.generateCurrentDatetime") }}</DropdownMenuItem>
<DropdownMenuItem @click="applyGeneratedDetailValue('date')">{{ t("grid.generateCurrentDate") }}</DropdownMenuItem>
<DropdownMenuItem @click="applyGeneratedDetailValue('uuid')">{{ t("grid.generateUuid") }}</DropdownMenuItem>
<DropdownMenuItem @click="applyGeneratedDetailValue('uuid')">{{ t("grid.generateUuidV4") }}</DropdownMenuItem>
<DropdownMenuItem @click="applyGeneratedDetailValue('uuid-v7')">{{ t("grid.generateUuidV7") }}</DropdownMenuItem>
<DropdownMenuItem @click="applyGeneratedDetailValue('snowflake')">{{ t("grid.generateSnowflakeId") }}</DropdownMenuItem>
<DropdownMenuItem @click="openGenerateIncrementDialog('detail')">{{ t("grid.generateIncrementId") }}</DropdownMenuItem>
</DropdownMenuContent>
+2 -1
View File
@@ -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.",
+2 -1
View File
@@ -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.",
+2 -1
View File
@@ -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.",
+2 -1
View File
@@ -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.",
+2 -1
View File
@@ -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}セルに連続値を生成します。開始値を入力してください。",
+2 -1
View File
@@ -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}개 셀에 연속 값을 생성합니다. 시작 값을 입력하세요.",
+2 -1
View File
@@ -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.",
+2 -1
View File
@@ -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.",
+2 -1
View File
@@ -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} 个单元格生成连续值,请输入起始值。",
+2 -1
View File
@@ -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} 個儲存格產生連續值,請輸入起始值。",
@@ -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"]);
});
@@ -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());
});