@@ -3134,8 +3134,10 @@ describe("ActiveTableView filtering", () => {
31343134 expect ( document . body . textContent ) . toContain ( "include column header" ) ;
31353135 expect ( document . body . textContent ) . toContain ( "copy markdown" ) ;
31363136 expect ( document . body . textContent ) . toContain ( "copy csv" ) ;
3137+ expect ( document . body . textContent ) . toContain ( "copy json" ) ;
31373138 expect ( document . body . textContent ) . toContain ( "save markdown" ) ;
31383139 expect ( document . body . textContent ) . toContain ( "save csv" ) ;
3140+ expect ( document . body . textContent ) . toContain ( "save json" ) ;
31393141
31403142 const checkedCheckbox = findMenuCheckboxByText ( "include column header" ) ;
31413143
@@ -3156,6 +3158,354 @@ describe("ActiveTableView filtering", () => {
31563158 view . cleanup ( ) ;
31573159 } ) ;
31583160
3161+ it ( "copies row selections as json using the current visible column order" , async ( ) => {
3162+ const writeText = vi . fn ( ) . mockResolvedValue ( undefined ) ;
3163+ const activeTable = {
3164+ columns : {
3165+ created_at : {
3166+ ...createColumn ( {
3167+ datatypeName : "timestamptz" ,
3168+ group : "datetime" ,
3169+ name : "created_at" ,
3170+ } ) ,
3171+ datatype : {
3172+ format : "YYYY-MM-DD HH:mm:ss.SSSZZ" ,
3173+ group : "datetime" ,
3174+ isArray : false ,
3175+ isNative : true ,
3176+ name : "timestamptz" ,
3177+ options : [ ] ,
3178+ schema : "pg_catalog" ,
3179+ } ,
3180+ } ,
3181+ email : createColumn ( {
3182+ datatypeName : "character varying(64)" ,
3183+ group : "string" ,
3184+ name : "email" ,
3185+ } ) ,
3186+ id : createColumn ( {
3187+ datatypeName : "uuid" ,
3188+ group : "string" ,
3189+ name : "id" ,
3190+ pkPosition : 1 ,
3191+ } ) ,
3192+ is_active : createColumn ( {
3193+ datatypeName : "boolean" ,
3194+ group : "string" ,
3195+ name : "is_active" ,
3196+ } ) ,
3197+ meta : createColumn ( {
3198+ datatypeName : "jsonb" ,
3199+ group : "string" ,
3200+ name : "meta" ,
3201+ } ) ,
3202+ score : createColumn ( {
3203+ datatypeName : "integer" ,
3204+ group : "numeric" ,
3205+ name : "score" ,
3206+ } ) ,
3207+ tags : createColumn ( {
3208+ datatypeName : "jsonb" ,
3209+ group : "string" ,
3210+ name : "tags" ,
3211+ } ) ,
3212+ } ,
3213+ name : "users" ,
3214+ schema : "public" ,
3215+ } ;
3216+
3217+ Object . defineProperty ( navigator , "clipboard" , {
3218+ configurable : true ,
3219+ value : {
3220+ writeText,
3221+ } ,
3222+ } ) ;
3223+ useNavigationMock . mockImplementation ( ( ) => ( {
3224+ createUrl : vi . fn ( ( ) => "#" ) ,
3225+ metadata : {
3226+ activeTable,
3227+ } ,
3228+ searchParam : navigationSearchParam ,
3229+ setPageIndexParam : setPageIndexParamMock ,
3230+ setSearchParam : setSearchParamMock ,
3231+ } ) ) ;
3232+ useIntrospectionMock . mockReturnValue ( {
3233+ data : {
3234+ filterOperators : [
3235+ "=" ,
3236+ "!=" ,
3237+ ">" ,
3238+ ">=" ,
3239+ "<" ,
3240+ "<=" ,
3241+ "is" ,
3242+ "is not" ,
3243+ "like" ,
3244+ "not like" ,
3245+ "ilike" ,
3246+ "not ilike" ,
3247+ ] ,
3248+ query : {
3249+ parameters : [ ] ,
3250+ sql : "" ,
3251+ } ,
3252+ schemas : {
3253+ public : {
3254+ name : "public" ,
3255+ tables : {
3256+ users : activeTable ,
3257+ } ,
3258+ } ,
3259+ } ,
3260+ timezone : "UTC" ,
3261+ } ,
3262+ refetch : vi . fn ( ) ,
3263+ } ) ;
3264+
3265+ useActiveTableQueryMock . mockReturnValue ( {
3266+ data : {
3267+ filteredRowCount : 2 ,
3268+ rows : [
3269+ {
3270+ __ps_rowid : "row-1" ,
3271+ created_at : "2026-03-11T00:00:00.000Z" ,
3272+ email : "alice@example.com" ,
3273+ id : "user_1" ,
3274+ is_active : true ,
3275+ meta : { owner : "alice" } ,
3276+ score : 42 ,
3277+ tags : [ "vip" , "beta" ] ,
3278+ } ,
3279+ {
3280+ __ps_rowid : "row-2" ,
3281+ created_at : "2026-03-12T00:00:00.000Z" ,
3282+ email : "bob@example.com" ,
3283+ id : "user_2" ,
3284+ is_active : false ,
3285+ meta : null ,
3286+ score : 0 ,
3287+ tags : [ ] ,
3288+ } ,
3289+ ] ,
3290+ } ,
3291+ isFetching : false ,
3292+ refetch : vi . fn ( ) ,
3293+ } ) ;
3294+ useSelectionMock . mockReturnValue ( {
3295+ deleteSelection : vi . fn ( ) ,
3296+ isSelecting : true ,
3297+ rowSelectionState : {
3298+ "row-2" : true ,
3299+ "row-1" : true ,
3300+ } ,
3301+ setRowSelectionState : vi . fn ( ) ,
3302+ } ) ;
3303+ gridColumnOrderState = [
3304+ "email" ,
3305+ "id" ,
3306+ "is_active" ,
3307+ "score" ,
3308+ "tags" ,
3309+ "meta" ,
3310+ "created_at" ,
3311+ ] ;
3312+
3313+ const view = renderView ( ) ;
3314+ const copyAsTrigger = findButtonByText ( "copy as" ) ;
3315+
3316+ expect ( copyAsTrigger ) . toBeDefined ( ) ;
3317+
3318+ dispatchPointerClick ( copyAsTrigger ) ;
3319+ await flush ( ) ;
3320+
3321+ const copyJsonButton = findMenuItemByText ( "copy json" ) ;
3322+
3323+ expect ( copyJsonButton ) . toBeDefined ( ) ;
3324+
3325+ dispatchPointerClick ( copyJsonButton ) ;
3326+
3327+ expect ( writeText ) . toHaveBeenCalledWith (
3328+ JSON . stringify (
3329+ [
3330+ {
3331+ email : "alice@example.com" ,
3332+ id : "user_1" ,
3333+ is_active : true ,
3334+ score : 42 ,
3335+ tags : [ "vip" , "beta" ] ,
3336+ meta : { owner : "alice" } ,
3337+ created_at : "2026-03-11T00:00:00.000Z" ,
3338+ } ,
3339+ {
3340+ email : "bob@example.com" ,
3341+ id : "user_2" ,
3342+ is_active : false ,
3343+ score : 0 ,
3344+ tags : [ ] ,
3345+ meta : null ,
3346+ created_at : "2026-03-12T00:00:00.000Z" ,
3347+ } ,
3348+ ] ,
3349+ null ,
3350+ 2 ,
3351+ ) ,
3352+ ) ;
3353+
3354+ view . cleanup ( ) ;
3355+ } ) ;
3356+
3357+ it ( "copies a single selected row as one json object" , async ( ) => {
3358+ const writeText = vi . fn ( ) . mockResolvedValue ( undefined ) ;
3359+ const activeTable = {
3360+ columns : {
3361+ created_at : {
3362+ ...createColumn ( {
3363+ datatypeName : "timestamptz" ,
3364+ group : "datetime" ,
3365+ name : "created_at" ,
3366+ } ) ,
3367+ datatype : {
3368+ format : "YYYY-MM-DD HH:mm:ss.SSSZZ" ,
3369+ group : "datetime" ,
3370+ isArray : false ,
3371+ isNative : true ,
3372+ name : "timestamptz" ,
3373+ options : [ ] ,
3374+ schema : "pg_catalog" ,
3375+ } ,
3376+ } ,
3377+ email : createColumn ( {
3378+ datatypeName : "character varying(64)" ,
3379+ group : "string" ,
3380+ name : "email" ,
3381+ } ) ,
3382+ id : createColumn ( {
3383+ datatypeName : "uuid" ,
3384+ group : "string" ,
3385+ name : "id" ,
3386+ pkPosition : 1 ,
3387+ } ) ,
3388+ is_active : createColumn ( {
3389+ datatypeName : "boolean" ,
3390+ group : "string" ,
3391+ name : "is_active" ,
3392+ } ) ,
3393+ } ,
3394+ name : "users" ,
3395+ schema : "public" ,
3396+ } ;
3397+
3398+ Object . defineProperty ( navigator , "clipboard" , {
3399+ configurable : true ,
3400+ value : {
3401+ writeText,
3402+ } ,
3403+ } ) ;
3404+ useNavigationMock . mockImplementation ( ( ) => ( {
3405+ createUrl : vi . fn ( ( ) => "#" ) ,
3406+ metadata : {
3407+ activeTable,
3408+ } ,
3409+ searchParam : navigationSearchParam ,
3410+ setPageIndexParam : setPageIndexParamMock ,
3411+ setSearchParam : setSearchParamMock ,
3412+ } ) ) ;
3413+ useIntrospectionMock . mockReturnValue ( {
3414+ data : {
3415+ filterOperators : [
3416+ "=" ,
3417+ "!=" ,
3418+ ">" ,
3419+ ">=" ,
3420+ "<" ,
3421+ "<=" ,
3422+ "is" ,
3423+ "is not" ,
3424+ "like" ,
3425+ "not like" ,
3426+ "ilike" ,
3427+ "not ilike" ,
3428+ ] ,
3429+ query : {
3430+ parameters : [ ] ,
3431+ sql : "" ,
3432+ } ,
3433+ schemas : {
3434+ public : {
3435+ name : "public" ,
3436+ tables : {
3437+ users : activeTable ,
3438+ } ,
3439+ } ,
3440+ } ,
3441+ timezone : "UTC" ,
3442+ } ,
3443+ refetch : vi . fn ( ) ,
3444+ } ) ;
3445+
3446+ useActiveTableQueryMock . mockReturnValue ( {
3447+ data : {
3448+ filteredRowCount : 2 ,
3449+ rows : [
3450+ {
3451+ __ps_rowid : "row-1" ,
3452+ created_at : "2026-03-11T00:00:00.000Z" ,
3453+ email : "alice@example.com" ,
3454+ id : "user_1" ,
3455+ is_active : true ,
3456+ } ,
3457+ {
3458+ __ps_rowid : "row-2" ,
3459+ created_at : "2026-03-12T00:00:00.000Z" ,
3460+ email : "bob@example.com" ,
3461+ id : "user_2" ,
3462+ is_active : false ,
3463+ } ,
3464+ ] ,
3465+ } ,
3466+ isFetching : false ,
3467+ refetch : vi . fn ( ) ,
3468+ } ) ;
3469+ useSelectionMock . mockReturnValue ( {
3470+ deleteSelection : vi . fn ( ) ,
3471+ isSelecting : true ,
3472+ rowSelectionState : {
3473+ "row-2" : true ,
3474+ } ,
3475+ setRowSelectionState : vi . fn ( ) ,
3476+ } ) ;
3477+ gridColumnOrderState = [ "email" , "id" , "is_active" , "created_at" ] ;
3478+
3479+ const view = renderView ( ) ;
3480+ const copyAsTrigger = findButtonByText ( "copy as" ) ;
3481+
3482+ expect ( copyAsTrigger ) . toBeDefined ( ) ;
3483+
3484+ dispatchPointerClick ( copyAsTrigger ) ;
3485+ await flush ( ) ;
3486+
3487+ const copyJsonButton = findMenuItemByText ( "copy json" ) ;
3488+
3489+ expect ( copyJsonButton ) . toBeDefined ( ) ;
3490+
3491+ dispatchPointerClick ( copyJsonButton ) ;
3492+
3493+ expect ( writeText ) . toHaveBeenCalledWith (
3494+ JSON . stringify (
3495+ {
3496+ email : "bob@example.com" ,
3497+ id : "user_2" ,
3498+ is_active : false ,
3499+ created_at : "2026-03-12T00:00:00.000Z" ,
3500+ } ,
3501+ null ,
3502+ 2 ,
3503+ ) ,
3504+ ) ;
3505+
3506+ view . cleanup ( ) ;
3507+ } ) ;
3508+
31593509 it ( "saves markdown for all selected rows when header inclusion is turned off" , async ( ) => {
31603510 const createObjectURL = vi
31613511 . spyOn ( URL , "createObjectURL" )
0 commit comments