@wordpress/core-data 7.43.2-next.v.202604091042.0 → 7.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1428,7 +1428,7 @@ describe( 'crdt-blocks', () => {
1428
1428
  expect( a1Content.toString() ).toBe( 'A1-edited' );
1429
1429
  } );
1430
1430
 
1431
- it( 'rebuilds Y.Array when row count changes (structural edit)', () => {
1431
+ it( 'preserves existing elements and appends new rows when row count increases', () => {
1432
1432
  const tableBlocks: Block[] = [
1433
1433
  {
1434
1434
  name: 'core/table',
@@ -1567,6 +1567,549 @@ describe( 'crdt-blocks', () => {
1567
1567
  doc2.destroy();
1568
1568
  } );
1569
1569
 
1570
+ it( 'concurrent cell edit is preserved when another user appends a row', () => {
1571
+ // Two users: A edits a cell, B appends a row. After sync,
1572
+ // A's edit should be preserved alongside the new row.
1573
+ const initialBlocks: Block[] = [
1574
+ {
1575
+ name: 'core/table',
1576
+ attributes: {
1577
+ body: [
1578
+ {
1579
+ cells: [
1580
+ { content: 'A1', tag: 'td' },
1581
+ { content: 'B1', tag: 'td' },
1582
+ ],
1583
+ },
1584
+ {
1585
+ cells: [
1586
+ { content: 'A2', tag: 'td' },
1587
+ { content: 'B2', tag: 'td' },
1588
+ ],
1589
+ },
1590
+ ],
1591
+ },
1592
+ innerBlocks: [],
1593
+ },
1594
+ ];
1595
+
1596
+ // Set up doc1 (User A).
1597
+ mergeCrdtBlocks( yblocks, initialBlocks, null );
1598
+
1599
+ // Set up doc2 (User B) by syncing initial state.
1600
+ const doc2 = new Y.Doc();
1601
+ const yblocks2 = doc2.getArray< YBlock >();
1602
+ Y.applyUpdate( doc2, Y.encodeStateAsUpdate( doc ) );
1603
+
1604
+ // User A edits cell A1.
1605
+ const userABlocks: Block[] = [
1606
+ {
1607
+ name: 'core/table',
1608
+ attributes: {
1609
+ body: [
1610
+ {
1611
+ cells: [
1612
+ { content: 'A1-userA', tag: 'td' },
1613
+ { content: 'B1', tag: 'td' },
1614
+ ],
1615
+ },
1616
+ {
1617
+ cells: [
1618
+ { content: 'A2', tag: 'td' },
1619
+ { content: 'B2', tag: 'td' },
1620
+ ],
1621
+ },
1622
+ ],
1623
+ },
1624
+ innerBlocks: [],
1625
+ },
1626
+ ];
1627
+ mergeCrdtBlocks( yblocks, userABlocks, null );
1628
+
1629
+ // User B appends a third row (concurrently, before syncing).
1630
+ const userBBlocks: Block[] = [
1631
+ {
1632
+ name: 'core/table',
1633
+ attributes: {
1634
+ body: [
1635
+ {
1636
+ cells: [
1637
+ { content: 'A1', tag: 'td' },
1638
+ { content: 'B1', tag: 'td' },
1639
+ ],
1640
+ },
1641
+ {
1642
+ cells: [
1643
+ { content: 'A2', tag: 'td' },
1644
+ { content: 'B2', tag: 'td' },
1645
+ ],
1646
+ },
1647
+ {
1648
+ cells: [
1649
+ { content: 'A3', tag: 'td' },
1650
+ { content: 'B3', tag: 'td' },
1651
+ ],
1652
+ },
1653
+ ],
1654
+ },
1655
+ innerBlocks: [],
1656
+ },
1657
+ ];
1658
+ mergeCrdtBlocks( yblocks2, userBBlocks, null );
1659
+
1660
+ // Sync: apply each other's changes.
1661
+ const updateA = Y.encodeStateAsUpdate( doc );
1662
+ const updateB = Y.encodeStateAsUpdate( doc2 );
1663
+ Y.applyUpdate( doc2, updateA );
1664
+ Y.applyUpdate( doc, updateB );
1665
+
1666
+ // Both docs should have A's cell edit and B's new row.
1667
+ for ( const checkBlocks of [ yblocks, yblocks2 ] ) {
1668
+ const attrs = checkBlocks
1669
+ .get( 0 )
1670
+ .get( 'attributes' ) as YBlockAttributes;
1671
+ const body = (
1672
+ attrs.get( 'body' ) as Y.Array< unknown >
1673
+ ).toJSON() as { cells: { content: string }[] }[];
1674
+
1675
+ expect( body.length ).toBe( 3 );
1676
+ expect( body[ 0 ].cells[ 0 ].content ).toBe( 'A1-userA' );
1677
+ expect( body[ 0 ].cells[ 1 ].content ).toBe( 'B1' );
1678
+ expect( body[ 1 ].cells[ 0 ].content ).toBe( 'A2' );
1679
+ expect( body[ 2 ].cells[ 0 ].content ).toBe( 'A3' );
1680
+ }
1681
+
1682
+ doc2.destroy();
1683
+ } );
1684
+
1685
+ it( 'concurrent cell edit is preserved when another user deletes a different row', () => {
1686
+ // Two users: A edits a cell in row 1, B deletes row 2.
1687
+ // After sync, A's edit should survive and row 2 should be gone.
1688
+ const initialBlocks: Block[] = [
1689
+ {
1690
+ name: 'core/table',
1691
+ attributes: {
1692
+ body: [
1693
+ {
1694
+ cells: [
1695
+ { content: 'A1', tag: 'td' },
1696
+ { content: 'B1', tag: 'td' },
1697
+ ],
1698
+ },
1699
+ {
1700
+ cells: [
1701
+ { content: 'A2', tag: 'td' },
1702
+ { content: 'B2', tag: 'td' },
1703
+ ],
1704
+ },
1705
+ {
1706
+ cells: [
1707
+ { content: 'A3', tag: 'td' },
1708
+ { content: 'B3', tag: 'td' },
1709
+ ],
1710
+ },
1711
+ ],
1712
+ },
1713
+ innerBlocks: [],
1714
+ },
1715
+ ];
1716
+
1717
+ // Set up doc1 (User A).
1718
+ mergeCrdtBlocks( yblocks, initialBlocks, null );
1719
+
1720
+ // Set up doc2 (User B) by syncing initial state.
1721
+ const doc2 = new Y.Doc();
1722
+ const yblocks2 = doc2.getArray< YBlock >();
1723
+ Y.applyUpdate( doc2, Y.encodeStateAsUpdate( doc ) );
1724
+
1725
+ // User A edits cell A1 in row 1.
1726
+ const userABlocks: Block[] = [
1727
+ {
1728
+ name: 'core/table',
1729
+ attributes: {
1730
+ body: [
1731
+ {
1732
+ cells: [
1733
+ { content: 'A1-userA', tag: 'td' },
1734
+ { content: 'B1', tag: 'td' },
1735
+ ],
1736
+ },
1737
+ {
1738
+ cells: [
1739
+ { content: 'A2', tag: 'td' },
1740
+ { content: 'B2', tag: 'td' },
1741
+ ],
1742
+ },
1743
+ {
1744
+ cells: [
1745
+ { content: 'A3', tag: 'td' },
1746
+ { content: 'B3', tag: 'td' },
1747
+ ],
1748
+ },
1749
+ ],
1750
+ },
1751
+ innerBlocks: [],
1752
+ },
1753
+ ];
1754
+ mergeCrdtBlocks( yblocks, userABlocks, null );
1755
+
1756
+ // User B deletes row 2 (the middle row).
1757
+ const userBBlocks: Block[] = [
1758
+ {
1759
+ name: 'core/table',
1760
+ attributes: {
1761
+ body: [
1762
+ {
1763
+ cells: [
1764
+ { content: 'A1', tag: 'td' },
1765
+ { content: 'B1', tag: 'td' },
1766
+ ],
1767
+ },
1768
+ {
1769
+ cells: [
1770
+ { content: 'A3', tag: 'td' },
1771
+ { content: 'B3', tag: 'td' },
1772
+ ],
1773
+ },
1774
+ ],
1775
+ },
1776
+ innerBlocks: [],
1777
+ },
1778
+ ];
1779
+ mergeCrdtBlocks( yblocks2, userBBlocks, null );
1780
+
1781
+ // Sync: apply each other's changes.
1782
+ const updateA = Y.encodeStateAsUpdate( doc );
1783
+ const updateB = Y.encodeStateAsUpdate( doc2 );
1784
+ Y.applyUpdate( doc2, updateA );
1785
+ Y.applyUpdate( doc, updateB );
1786
+
1787
+ // Both docs should have A's cell edit and B's row deletion.
1788
+ for ( const checkBlocks of [ yblocks, yblocks2 ] ) {
1789
+ const attrs = checkBlocks
1790
+ .get( 0 )
1791
+ .get( 'attributes' ) as YBlockAttributes;
1792
+ const body = (
1793
+ attrs.get( 'body' ) as Y.Array< unknown >
1794
+ ).toJSON() as { cells: { content: string }[] }[];
1795
+
1796
+ expect( body.length ).toBe( 2 );
1797
+ expect( body[ 0 ].cells[ 0 ].content ).toBe( 'A1-userA' );
1798
+ expect( body[ 1 ].cells[ 0 ].content ).toBe( 'A3' );
1799
+ }
1800
+
1801
+ doc2.destroy();
1802
+ } );
1803
+
1804
+ it( 'preserves Y.Map identity for untouched rows when a row is appended', () => {
1805
+ const initialBlocks: Block[] = [
1806
+ {
1807
+ name: 'core/table',
1808
+ attributes: {
1809
+ body: [
1810
+ {
1811
+ cells: [ { content: 'A1', tag: 'td' } ],
1812
+ },
1813
+ ],
1814
+ },
1815
+ innerBlocks: [],
1816
+ },
1817
+ ];
1818
+
1819
+ mergeCrdtBlocks( yblocks, initialBlocks, null );
1820
+
1821
+ // Capture a reference to the Y.Map for the first row.
1822
+ const attrs = yblocks
1823
+ .get( 0 )
1824
+ .get( 'attributes' ) as YBlockAttributes;
1825
+ const body = attrs.get( 'body' ) as Y.Array< unknown >;
1826
+ const row0Before = body.get( 0 ) as Y.Map< unknown >;
1827
+
1828
+ // Append a second row.
1829
+ const updatedBlocks: Block[] = [
1830
+ {
1831
+ name: 'core/table',
1832
+ attributes: {
1833
+ body: [
1834
+ {
1835
+ cells: [ { content: 'A1', tag: 'td' } ],
1836
+ },
1837
+ {
1838
+ cells: [ { content: 'A2', tag: 'td' } ],
1839
+ },
1840
+ ],
1841
+ },
1842
+ innerBlocks: [],
1843
+ },
1844
+ ];
1845
+
1846
+ mergeCrdtBlocks( yblocks, updatedBlocks, null );
1847
+
1848
+ // The first row's Y.Map should be the exact same object.
1849
+ const row0After = body.get( 0 ) as Y.Map< unknown >;
1850
+ expect( row0After ).toBe( row0Before );
1851
+
1852
+ // And the new row should exist.
1853
+ expect( body.length ).toBe( 2 );
1854
+ const row1 = body.get( 1 ) as Y.Map< unknown >;
1855
+ const cells = ( row1.get( 'cells' ) as Y.Array< unknown > ).get(
1856
+ 0
1857
+ ) as Y.Map< unknown >;
1858
+ const content = cells.get( 'content' ) as Y.Text;
1859
+ expect( content.toString() ).toBe( 'A2' );
1860
+ } );
1861
+
1862
+ it( 'preserves Y.Map identity when a row is prepended', () => {
1863
+ const initialBlocks: Block[] = [
1864
+ {
1865
+ name: 'core/table',
1866
+ attributes: {
1867
+ body: [
1868
+ {
1869
+ cells: [ { content: 'A1', tag: 'td' } ],
1870
+ },
1871
+ {
1872
+ cells: [ { content: 'A2', tag: 'td' } ],
1873
+ },
1874
+ ],
1875
+ },
1876
+ innerBlocks: [],
1877
+ },
1878
+ ];
1879
+
1880
+ mergeCrdtBlocks( yblocks, initialBlocks, null );
1881
+
1882
+ const attrs = yblocks
1883
+ .get( 0 )
1884
+ .get( 'attributes' ) as YBlockAttributes;
1885
+ const body = attrs.get( 'body' ) as Y.Array< unknown >;
1886
+ const row0Before = body.get( 0 ) as Y.Map< unknown >;
1887
+ const row1Before = body.get( 1 ) as Y.Map< unknown >;
1888
+
1889
+ // Prepend a new row.
1890
+ const updatedBlocks: Block[] = [
1891
+ {
1892
+ name: 'core/table',
1893
+ attributes: {
1894
+ body: [
1895
+ {
1896
+ cells: [ { content: 'NEW', tag: 'td' } ],
1897
+ },
1898
+ {
1899
+ cells: [ { content: 'A1', tag: 'td' } ],
1900
+ },
1901
+ {
1902
+ cells: [ { content: 'A2', tag: 'td' } ],
1903
+ },
1904
+ ],
1905
+ },
1906
+ innerBlocks: [],
1907
+ },
1908
+ ];
1909
+
1910
+ mergeCrdtBlocks( yblocks, updatedBlocks, null );
1911
+
1912
+ expect( body.length ).toBe( 3 );
1913
+
1914
+ const bodyJson = body.toJSON() as {
1915
+ cells: { content: string }[];
1916
+ }[];
1917
+ expect( bodyJson[ 0 ].cells[ 0 ].content ).toBe( 'NEW' );
1918
+ expect( bodyJson[ 1 ].cells[ 0 ].content ).toBe( 'A1' );
1919
+ expect( bodyJson[ 2 ].cells[ 0 ].content ).toBe( 'A2' );
1920
+
1921
+ // Original rows should be the same Y.Map objects (shifted).
1922
+ expect( body.get( 1 ) ).toBe( row0Before );
1923
+ expect( body.get( 2 ) ).toBe( row1Before );
1924
+ } );
1925
+
1926
+ it( 'preserves Y.Map identity when a row is inserted in the middle', () => {
1927
+ const initialBlocks: Block[] = [
1928
+ {
1929
+ name: 'core/table',
1930
+ attributes: {
1931
+ body: [
1932
+ {
1933
+ cells: [ { content: 'A1', tag: 'td' } ],
1934
+ },
1935
+ {
1936
+ cells: [ { content: 'A3', tag: 'td' } ],
1937
+ },
1938
+ ],
1939
+ },
1940
+ innerBlocks: [],
1941
+ },
1942
+ ];
1943
+
1944
+ mergeCrdtBlocks( yblocks, initialBlocks, null );
1945
+
1946
+ const attrs = yblocks
1947
+ .get( 0 )
1948
+ .get( 'attributes' ) as YBlockAttributes;
1949
+ const body = attrs.get( 'body' ) as Y.Array< unknown >;
1950
+ const row0Before = body.get( 0 ) as Y.Map< unknown >;
1951
+ const row1Before = body.get( 1 ) as Y.Map< unknown >;
1952
+
1953
+ // Insert a new row in the middle.
1954
+ const updatedBlocks: Block[] = [
1955
+ {
1956
+ name: 'core/table',
1957
+ attributes: {
1958
+ body: [
1959
+ {
1960
+ cells: [ { content: 'A1', tag: 'td' } ],
1961
+ },
1962
+ {
1963
+ cells: [ { content: 'A2', tag: 'td' } ],
1964
+ },
1965
+ {
1966
+ cells: [ { content: 'A3', tag: 'td' } ],
1967
+ },
1968
+ ],
1969
+ },
1970
+ innerBlocks: [],
1971
+ },
1972
+ ];
1973
+
1974
+ mergeCrdtBlocks( yblocks, updatedBlocks, null );
1975
+
1976
+ expect( body.length ).toBe( 3 );
1977
+
1978
+ const bodyJson = body.toJSON() as {
1979
+ cells: { content: string }[];
1980
+ }[];
1981
+ expect( bodyJson[ 0 ].cells[ 0 ].content ).toBe( 'A1' );
1982
+ expect( bodyJson[ 1 ].cells[ 0 ].content ).toBe( 'A2' );
1983
+ expect( bodyJson[ 2 ].cells[ 0 ].content ).toBe( 'A3' );
1984
+
1985
+ // Original rows should be preserved.
1986
+ expect( body.get( 0 ) ).toBe( row0Before );
1987
+ expect( body.get( 2 ) ).toBe( row1Before );
1988
+ } );
1989
+
1990
+ it( 'preserves Y.Map identity when a row is deleted from the end', () => {
1991
+ const initialBlocks: Block[] = [
1992
+ {
1993
+ name: 'core/table',
1994
+ attributes: {
1995
+ body: [
1996
+ {
1997
+ cells: [ { content: 'A1', tag: 'td' } ],
1998
+ },
1999
+ {
2000
+ cells: [ { content: 'A2', tag: 'td' } ],
2001
+ },
2002
+ {
2003
+ cells: [ { content: 'A3', tag: 'td' } ],
2004
+ },
2005
+ ],
2006
+ },
2007
+ innerBlocks: [],
2008
+ },
2009
+ ];
2010
+
2011
+ mergeCrdtBlocks( yblocks, initialBlocks, null );
2012
+
2013
+ const attrs = yblocks
2014
+ .get( 0 )
2015
+ .get( 'attributes' ) as YBlockAttributes;
2016
+ const body = attrs.get( 'body' ) as Y.Array< unknown >;
2017
+ const row0Before = body.get( 0 ) as Y.Map< unknown >;
2018
+ const row1Before = body.get( 1 ) as Y.Map< unknown >;
2019
+
2020
+ // Delete the last row.
2021
+ const updatedBlocks: Block[] = [
2022
+ {
2023
+ name: 'core/table',
2024
+ attributes: {
2025
+ body: [
2026
+ {
2027
+ cells: [ { content: 'A1', tag: 'td' } ],
2028
+ },
2029
+ {
2030
+ cells: [ { content: 'A2', tag: 'td' } ],
2031
+ },
2032
+ ],
2033
+ },
2034
+ innerBlocks: [],
2035
+ },
2036
+ ];
2037
+
2038
+ mergeCrdtBlocks( yblocks, updatedBlocks, null );
2039
+
2040
+ expect( body.length ).toBe( 2 );
2041
+
2042
+ const bodyJson = body.toJSON() as {
2043
+ cells: { content: string }[];
2044
+ }[];
2045
+ expect( bodyJson[ 0 ].cells[ 0 ].content ).toBe( 'A1' );
2046
+ expect( bodyJson[ 1 ].cells[ 0 ].content ).toBe( 'A2' );
2047
+
2048
+ // Remaining rows should be the same Y.Map objects.
2049
+ expect( body.get( 0 ) ).toBe( row0Before );
2050
+ expect( body.get( 1 ) ).toBe( row1Before );
2051
+ } );
2052
+
2053
+ it( 'updates all elements in-place when every row changes', () => {
2054
+ const initialBlocks: Block[] = [
2055
+ {
2056
+ name: 'core/table',
2057
+ attributes: {
2058
+ body: [
2059
+ {
2060
+ cells: [ { content: 'A1', tag: 'td' } ],
2061
+ },
2062
+ {
2063
+ cells: [ { content: 'A2', tag: 'td' } ],
2064
+ },
2065
+ ],
2066
+ },
2067
+ innerBlocks: [],
2068
+ },
2069
+ ];
2070
+
2071
+ mergeCrdtBlocks( yblocks, initialBlocks, null );
2072
+
2073
+ const attrs = yblocks
2074
+ .get( 0 )
2075
+ .get( 'attributes' ) as YBlockAttributes;
2076
+ const body = attrs.get( 'body' ) as Y.Array< unknown >;
2077
+ const row0Before = body.get( 0 ) as Y.Map< unknown >;
2078
+ const row1Before = body.get( 1 ) as Y.Map< unknown >;
2079
+
2080
+ // Replace all row contents.
2081
+ const updatedBlocks: Block[] = [
2082
+ {
2083
+ name: 'core/table',
2084
+ attributes: {
2085
+ body: [
2086
+ {
2087
+ cells: [ { content: 'X1', tag: 'td' } ],
2088
+ },
2089
+ {
2090
+ cells: [ { content: 'X2', tag: 'td' } ],
2091
+ },
2092
+ ],
2093
+ },
2094
+ innerBlocks: [],
2095
+ },
2096
+ ];
2097
+
2098
+ mergeCrdtBlocks( yblocks, updatedBlocks, null );
2099
+
2100
+ expect( body.length ).toBe( 2 );
2101
+
2102
+ const bodyJson = body.toJSON() as {
2103
+ cells: { content: string }[];
2104
+ }[];
2105
+ expect( bodyJson[ 0 ].cells[ 0 ].content ).toBe( 'X1' );
2106
+ expect( bodyJson[ 1 ].cells[ 0 ].content ).toBe( 'X2' );
2107
+
2108
+ // Y.Map objects should be updated in-place, not recreated.
2109
+ expect( body.get( 0 ) ).toBe( row0Before );
2110
+ expect( body.get( 1 ) ).toBe( row1Before );
2111
+ } );
2112
+
1570
2113
  it( 'migrates plain array to Y.Array on first update', () => {
1571
2114
  // Manually set up a block with a plain array body (old format).
1572
2115
  const block = new Y.Map() as unknown as YBlock;