jazz-tools 0.19.2 → 0.19.4
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.
- package/.svelte-kit/__package__/jazz.class.svelte.d.ts +2 -2
- package/.svelte-kit/__package__/jazz.class.svelte.d.ts.map +1 -1
- package/.svelte-kit/__package__/jazz.class.svelte.js +15 -17
- package/.turbo/turbo-build.log +54 -54
- package/CHANGELOG.md +24 -0
- package/dist/{chunk-NCNM6UDZ.js → chunk-PT7FCV26.js} +148 -78
- package/dist/chunk-PT7FCV26.js.map +1 -0
- package/dist/index.js +14 -7
- package/dist/index.js.map +1 -1
- package/dist/inspector/{custom-element-ABVPHX53.js → custom-element-P76EIWEV.js} +322 -146
- package/dist/inspector/custom-element-P76EIWEV.js.map +1 -0
- package/dist/inspector/index.js +302 -126
- package/dist/inspector/index.js.map +1 -1
- package/dist/inspector/register-custom-element.js +1 -1
- package/dist/inspector/tests/viewer/co-plain-text-view.test.d.ts +2 -0
- package/dist/inspector/tests/viewer/co-plain-text-view.test.d.ts.map +1 -0
- package/dist/inspector/utils/history.d.ts +5 -1
- package/dist/inspector/utils/history.d.ts.map +1 -1
- package/dist/inspector/utils/permissions.d.ts +3 -0
- package/dist/inspector/utils/permissions.d.ts.map +1 -0
- package/dist/inspector/viewer/co-map-view.d.ts.map +1 -1
- package/dist/inspector/viewer/co-plain-text-view.d.ts +4 -2
- package/dist/inspector/viewer/co-plain-text-view.d.ts.map +1 -1
- package/dist/inspector/viewer/grid-view.d.ts.map +1 -1
- package/dist/inspector/viewer/page.d.ts.map +1 -1
- package/dist/inspector/viewer/use-resolve-covalue.d.ts +0 -1
- package/dist/inspector/viewer/use-resolve-covalue.d.ts.map +1 -1
- package/dist/react-core/hooks.d.ts.map +1 -1
- package/dist/react-core/index.js +4 -17
- package/dist/react-core/index.js.map +1 -1
- package/dist/svelte/jazz.class.svelte.d.ts +2 -2
- package/dist/svelte/jazz.class.svelte.d.ts.map +1 -1
- package/dist/svelte/jazz.class.svelte.js +15 -17
- package/dist/testing.js +1 -1
- package/dist/tools/coValues/coFeed.d.ts.map +1 -1
- package/dist/tools/coValues/group.d.ts.map +1 -1
- package/dist/tools/coValues/interfaces.d.ts +7 -6
- package/dist/tools/coValues/interfaces.d.ts.map +1 -1
- package/dist/tools/coValues/promise.d.ts +9 -0
- package/dist/tools/coValues/promise.d.ts.map +1 -0
- package/dist/tools/coValues/request.d.ts.map +1 -1
- package/dist/tools/exports.d.ts +1 -1
- package/dist/tools/exports.d.ts.map +1 -1
- package/dist/tools/implementation/refs.d.ts +1 -1
- package/dist/tools/implementation/refs.d.ts.map +1 -1
- package/dist/tools/implementation/zodSchema/runtimeConverters/schemaFieldToCoFieldDef.d.ts +3 -1
- package/dist/tools/implementation/zodSchema/runtimeConverters/schemaFieldToCoFieldDef.d.ts.map +1 -1
- package/dist/tools/implementation/zodSchema/unionUtils.d.ts.map +1 -1
- package/dist/tools/subscribe/SubscriptionScope.d.ts +5 -2
- package/dist/tools/subscribe/SubscriptionScope.d.ts.map +1 -1
- package/dist/tools/subscribe/index.d.ts +1 -1
- package/dist/tools/subscribe/index.d.ts.map +1 -1
- package/dist/tools/subscribe/types.d.ts +2 -1
- package/dist/tools/subscribe/types.d.ts.map +1 -1
- package/dist/tools/tests/SubscriptionScope.test.d.ts +2 -0
- package/dist/tools/tests/SubscriptionScope.test.d.ts.map +1 -0
- package/package.json +4 -4
- package/src/inspector/tests/utils/history.test.ts +233 -2
- package/src/inspector/tests/viewer/co-plain-text-view.test.tsx +125 -0
- package/src/inspector/tests/viewer/comap-view.test.tsx +309 -1
- package/src/inspector/tests/viewer/history-view.test.tsx +134 -2
- package/src/inspector/utils/history.ts +168 -1
- package/src/inspector/utils/permissions.ts +10 -0
- package/src/inspector/viewer/co-map-view.tsx +27 -15
- package/src/inspector/viewer/co-plain-text-view.tsx +102 -3
- package/src/inspector/viewer/grid-view.tsx +2 -1
- package/src/inspector/viewer/history-view.tsx +5 -23
- package/src/inspector/viewer/page.tsx +8 -1
- package/src/inspector/viewer/use-resolve-covalue.ts +2 -6
- package/src/react-core/hooks.ts +5 -29
- package/src/svelte/jazz.class.svelte.ts +16 -34
- package/src/tools/coValues/coFeed.ts +10 -7
- package/src/tools/coValues/coMap.ts +10 -7
- package/src/tools/coValues/group.ts +6 -2
- package/src/tools/coValues/interfaces.ts +48 -28
- package/src/tools/coValues/promise.ts +34 -0
- package/src/tools/coValues/request.ts +12 -8
- package/src/tools/exports.ts +1 -0
- package/src/tools/implementation/refs.ts +9 -17
- package/src/tools/implementation/zodSchema/runtimeConverters/schemaFieldToCoFieldDef.ts +62 -30
- package/src/tools/implementation/zodSchema/unionUtils.ts +3 -4
- package/src/tools/subscribe/SubscriptionScope.ts +45 -2
- package/src/tools/subscribe/index.ts +28 -13
- package/src/tools/subscribe/types.ts +5 -2
- package/src/tools/tests/SubscriptionScope.test.ts +397 -0
- package/src/tools/tests/deepLoading.test.ts +22 -0
- package/src/tools/tests/subscribe.test.ts +69 -0
- package/dist/chunk-NCNM6UDZ.js.map +0 -1
- package/dist/inspector/custom-element-ABVPHX53.js.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @vitest-environment happy-dom
|
|
2
|
-
import { afterEach, beforeAll, describe, expect, it } from "vitest";
|
|
2
|
+
import { afterEach, assert, beforeAll, describe, expect, it } from "vitest";
|
|
3
3
|
import { createJazzTestAccount, setupJazzTestSync } from "jazz-tools/testing";
|
|
4
4
|
import { co, z } from "jazz-tools";
|
|
5
5
|
import {
|
|
@@ -578,4 +578,312 @@ describe("CoMapView", async () => {
|
|
|
578
578
|
});
|
|
579
579
|
});
|
|
580
580
|
});
|
|
581
|
+
|
|
582
|
+
describe("Permissions", () => {
|
|
583
|
+
it("should disable Add Property button for reader account", async () => {
|
|
584
|
+
const reader = await createJazzTestAccount();
|
|
585
|
+
const group = co.group().create({ owner: account });
|
|
586
|
+
group.addMember(reader, "reader");
|
|
587
|
+
|
|
588
|
+
const schema = co.map({
|
|
589
|
+
pet: z.string(),
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
const value = schema.create({ pet: "dog" }, group);
|
|
593
|
+
|
|
594
|
+
const valueOnReader = await schema.load(value.$jazz.id, {
|
|
595
|
+
loadAs: reader,
|
|
596
|
+
});
|
|
597
|
+
assert(valueOnReader.$isLoaded);
|
|
598
|
+
const data = valueOnReader.$jazz.raw.toJSON() as JsonObject;
|
|
599
|
+
|
|
600
|
+
render(
|
|
601
|
+
<CoMapView
|
|
602
|
+
coValue={valueOnReader.$jazz.raw}
|
|
603
|
+
data={data}
|
|
604
|
+
node={reader.$jazz.localNode}
|
|
605
|
+
onNavigate={() => {}}
|
|
606
|
+
/>,
|
|
607
|
+
);
|
|
608
|
+
|
|
609
|
+
const addButton = screen.getByTitle("Add Property");
|
|
610
|
+
expect(addButton).toBeDefined();
|
|
611
|
+
expect((addButton as HTMLButtonElement).disabled).toBe(true);
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
it("should enable Add Property button for writer account", async () => {
|
|
615
|
+
const writer = await createJazzTestAccount();
|
|
616
|
+
const group = co.group().create({ owner: account });
|
|
617
|
+
group.addMember(writer, "writer");
|
|
618
|
+
|
|
619
|
+
const schema = co.map({
|
|
620
|
+
pet: z.string(),
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
const value = schema.create({ pet: "dog" }, group);
|
|
624
|
+
|
|
625
|
+
const valueOnWriter = await schema.load(value.$jazz.id, {
|
|
626
|
+
loadAs: writer,
|
|
627
|
+
});
|
|
628
|
+
assert(valueOnWriter.$isLoaded);
|
|
629
|
+
const data = valueOnWriter.$jazz.raw.toJSON() as JsonObject;
|
|
630
|
+
|
|
631
|
+
render(
|
|
632
|
+
<CoMapView
|
|
633
|
+
coValue={valueOnWriter.$jazz.raw}
|
|
634
|
+
data={data}
|
|
635
|
+
node={writer.$jazz.localNode}
|
|
636
|
+
onNavigate={() => {}}
|
|
637
|
+
/>,
|
|
638
|
+
);
|
|
639
|
+
|
|
640
|
+
const addButton = screen.getByTitle("Add Property");
|
|
641
|
+
expect(addButton).toBeDefined();
|
|
642
|
+
expect((addButton as HTMLButtonElement).disabled).toBe(false);
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
it("should hide restore buttons for reader account when multiple timestamps exist", async () => {
|
|
646
|
+
const reader = await createJazzTestAccount();
|
|
647
|
+
const group = co.group().create({ owner: account });
|
|
648
|
+
group.addMember(reader, "reader");
|
|
649
|
+
|
|
650
|
+
const schema = co.map({
|
|
651
|
+
pet: z.string(),
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
const value = schema.create({ pet: "dog" }, group);
|
|
655
|
+
await sleep(2);
|
|
656
|
+
value.$jazz.set("pet", "cat");
|
|
657
|
+
|
|
658
|
+
const valueOnReader = await schema.load(value.$jazz.id, {
|
|
659
|
+
loadAs: reader,
|
|
660
|
+
});
|
|
661
|
+
assert(valueOnReader.$isLoaded);
|
|
662
|
+
const data = valueOnReader.$jazz.raw.toJSON() as JsonObject;
|
|
663
|
+
|
|
664
|
+
render(
|
|
665
|
+
<CoMapView
|
|
666
|
+
coValue={valueOnReader.$jazz.raw}
|
|
667
|
+
data={data}
|
|
668
|
+
node={reader.$jazz.localNode}
|
|
669
|
+
onNavigate={() => {}}
|
|
670
|
+
/>,
|
|
671
|
+
);
|
|
672
|
+
|
|
673
|
+
const restoreButton = screen.getByTitle("Timeline");
|
|
674
|
+
fireEvent.click(restoreButton);
|
|
675
|
+
|
|
676
|
+
await waitFor(() => {
|
|
677
|
+
expect(screen.getByText("Select Timestamp")).toBeDefined();
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
expect(screen.queryByText("Restore")).toBeNull();
|
|
681
|
+
expect(screen.queryByRole("checkbox")).toBeNull();
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
it("should show restore buttons for writer account when multiple timestamps exist", async () => {
|
|
685
|
+
const writer = await createJazzTestAccount();
|
|
686
|
+
const group = co.group().create({ owner: account });
|
|
687
|
+
group.addMember(writer, "writer");
|
|
688
|
+
|
|
689
|
+
const schema = co.map({
|
|
690
|
+
pet: z.string(),
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
const value = schema.create({ pet: "dog" }, group);
|
|
694
|
+
await sleep(2);
|
|
695
|
+
value.$jazz.set("pet", "cat");
|
|
696
|
+
|
|
697
|
+
const valueOnWriter = await schema.load(value.$jazz.id, {
|
|
698
|
+
loadAs: writer,
|
|
699
|
+
});
|
|
700
|
+
assert(valueOnWriter.$isLoaded);
|
|
701
|
+
const data = valueOnWriter.$jazz.raw.toJSON() as JsonObject;
|
|
702
|
+
|
|
703
|
+
render(
|
|
704
|
+
<CoMapView
|
|
705
|
+
coValue={valueOnWriter.$jazz.raw}
|
|
706
|
+
data={data}
|
|
707
|
+
node={writer.$jazz.localNode}
|
|
708
|
+
onNavigate={() => {}}
|
|
709
|
+
/>,
|
|
710
|
+
);
|
|
711
|
+
|
|
712
|
+
const restoreButton = screen.getByTitle("Timeline");
|
|
713
|
+
fireEvent.click(restoreButton);
|
|
714
|
+
|
|
715
|
+
await waitFor(() => {
|
|
716
|
+
expect(screen.getByText("Restore")).toBeDefined();
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
expect(screen.getByRole("checkbox")).toBeDefined();
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
it("should hide edit buttons in GridView for reader account", async () => {
|
|
723
|
+
const reader = await createJazzTestAccount();
|
|
724
|
+
const group = co.group().create({ owner: account });
|
|
725
|
+
group.addMember(reader, "reader");
|
|
726
|
+
|
|
727
|
+
const schema = co.map({
|
|
728
|
+
pet: z.string(),
|
|
729
|
+
age: z.number(),
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
const value = schema.create({ pet: "dog", age: 10 }, group);
|
|
733
|
+
|
|
734
|
+
const valueOnReader = await schema.load(value.$jazz.id, {
|
|
735
|
+
loadAs: reader,
|
|
736
|
+
});
|
|
737
|
+
assert(valueOnReader.$isLoaded);
|
|
738
|
+
const data = valueOnReader.$jazz.raw.toJSON() as JsonObject;
|
|
739
|
+
|
|
740
|
+
render(
|
|
741
|
+
<CoMapView
|
|
742
|
+
coValue={valueOnReader.$jazz.raw}
|
|
743
|
+
data={data}
|
|
744
|
+
node={reader.$jazz.localNode}
|
|
745
|
+
onNavigate={() => {}}
|
|
746
|
+
/>,
|
|
747
|
+
);
|
|
748
|
+
|
|
749
|
+
expect(screen.getByText("pet")).toBeDefined();
|
|
750
|
+
expect(screen.getByText("age")).toBeDefined();
|
|
751
|
+
|
|
752
|
+
const editButtons = screen.queryAllByLabelText("Edit");
|
|
753
|
+
const deleteButtons = screen.queryAllByLabelText("Delete");
|
|
754
|
+
|
|
755
|
+
expect(editButtons).toHaveLength(0);
|
|
756
|
+
expect(deleteButtons).toHaveLength(0);
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
it("should show edit buttons in GridView for writer account", async () => {
|
|
760
|
+
const writer = await createJazzTestAccount();
|
|
761
|
+
const group = co.group().create({ owner: account });
|
|
762
|
+
group.addMember(writer, "writer");
|
|
763
|
+
|
|
764
|
+
const schema = co.map({
|
|
765
|
+
pet: z.string(),
|
|
766
|
+
age: z.number(),
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
const value = schema.create({ pet: "dog", age: 10 }, group);
|
|
770
|
+
|
|
771
|
+
const valueOnWriter = await schema.load(value.$jazz.id, {
|
|
772
|
+
loadAs: writer,
|
|
773
|
+
});
|
|
774
|
+
assert(valueOnWriter.$isLoaded);
|
|
775
|
+
const data = valueOnWriter.$jazz.raw.toJSON() as JsonObject;
|
|
776
|
+
|
|
777
|
+
render(
|
|
778
|
+
<CoMapView
|
|
779
|
+
coValue={valueOnWriter.$jazz.raw}
|
|
780
|
+
data={data}
|
|
781
|
+
node={writer.$jazz.localNode}
|
|
782
|
+
onNavigate={() => {}}
|
|
783
|
+
/>,
|
|
784
|
+
);
|
|
785
|
+
|
|
786
|
+
expect(screen.getByText("pet")).toBeDefined();
|
|
787
|
+
expect(screen.getByText("age")).toBeDefined();
|
|
788
|
+
|
|
789
|
+
const editButtons = screen.queryAllByLabelText("Edit");
|
|
790
|
+
const deleteButtons = screen.queryAllByLabelText("Delete");
|
|
791
|
+
|
|
792
|
+
expect(editButtons.length).toBeGreaterThan(0);
|
|
793
|
+
expect(deleteButtons.length).toBeGreaterThan(0);
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
it("should enable Add Property button for admin account", async () => {
|
|
797
|
+
const admin = await createJazzTestAccount();
|
|
798
|
+
const group = co.group().create({ owner: account });
|
|
799
|
+
group.addMember(admin, "admin");
|
|
800
|
+
|
|
801
|
+
const schema = co.map({
|
|
802
|
+
pet: z.string(),
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
const value = schema.create({ pet: "dog" }, group);
|
|
806
|
+
|
|
807
|
+
const valueOnAdmin = await schema.load(value.$jazz.id, {
|
|
808
|
+
loadAs: admin,
|
|
809
|
+
});
|
|
810
|
+
assert(valueOnAdmin.$isLoaded);
|
|
811
|
+
const data = valueOnAdmin.$jazz.raw.toJSON() as JsonObject;
|
|
812
|
+
|
|
813
|
+
render(
|
|
814
|
+
<CoMapView
|
|
815
|
+
coValue={valueOnAdmin.$jazz.raw}
|
|
816
|
+
data={data}
|
|
817
|
+
node={admin.$jazz.localNode}
|
|
818
|
+
onNavigate={() => {}}
|
|
819
|
+
/>,
|
|
820
|
+
);
|
|
821
|
+
|
|
822
|
+
const addButton = screen.getByTitle("Add Property");
|
|
823
|
+
expect(addButton).toBeDefined();
|
|
824
|
+
expect((addButton as HTMLButtonElement).disabled).toBe(false);
|
|
825
|
+
});
|
|
826
|
+
|
|
827
|
+
it("should enable Add Property button for manager account", async () => {
|
|
828
|
+
const manager = await createJazzTestAccount();
|
|
829
|
+
const group = co.group().create({ owner: account });
|
|
830
|
+
group.addMember(manager, "manager");
|
|
831
|
+
|
|
832
|
+
const schema = co.map({
|
|
833
|
+
pet: z.string(),
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
const value = schema.create({ pet: "dog" }, group);
|
|
837
|
+
|
|
838
|
+
const valueOnManager = await schema.load(value.$jazz.id, {
|
|
839
|
+
loadAs: manager,
|
|
840
|
+
});
|
|
841
|
+
assert(valueOnManager.$isLoaded);
|
|
842
|
+
const data = valueOnManager.$jazz.raw.toJSON() as JsonObject;
|
|
843
|
+
|
|
844
|
+
render(
|
|
845
|
+
<CoMapView
|
|
846
|
+
coValue={valueOnManager.$jazz.raw}
|
|
847
|
+
data={data}
|
|
848
|
+
node={manager.$jazz.localNode}
|
|
849
|
+
onNavigate={() => {}}
|
|
850
|
+
/>,
|
|
851
|
+
);
|
|
852
|
+
|
|
853
|
+
const addButton = screen.getByTitle("Add Property");
|
|
854
|
+
expect(addButton).toBeDefined();
|
|
855
|
+
expect((addButton as HTMLButtonElement).disabled).toBe(false);
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
it("should enable Add Property button for writeOnly account", async () => {
|
|
859
|
+
const writeOnly = await createJazzTestAccount();
|
|
860
|
+
const group = co.group().create({ owner: account });
|
|
861
|
+
group.addMember(writeOnly, "writeOnly");
|
|
862
|
+
|
|
863
|
+
const schema = co.map({
|
|
864
|
+
pet: z.string(),
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
const value = schema.create({ pet: "dog" }, group);
|
|
868
|
+
|
|
869
|
+
const valueOnWriteOnly = await schema.load(value.$jazz.id, {
|
|
870
|
+
loadAs: writeOnly,
|
|
871
|
+
});
|
|
872
|
+
assert(valueOnWriteOnly.$isLoaded);
|
|
873
|
+
const data = valueOnWriteOnly.$jazz.raw.toJSON() as JsonObject;
|
|
874
|
+
|
|
875
|
+
render(
|
|
876
|
+
<CoMapView
|
|
877
|
+
coValue={valueOnWriteOnly.$jazz.raw}
|
|
878
|
+
data={data}
|
|
879
|
+
node={writeOnly.$jazz.localNode}
|
|
880
|
+
onNavigate={() => {}}
|
|
881
|
+
/>,
|
|
882
|
+
);
|
|
883
|
+
|
|
884
|
+
const addButton = screen.getByTitle("Add Property");
|
|
885
|
+
expect(addButton).toBeDefined();
|
|
886
|
+
expect((addButton as HTMLButtonElement).disabled).toBe(false);
|
|
887
|
+
});
|
|
888
|
+
});
|
|
581
889
|
});
|
|
@@ -17,6 +17,8 @@ import { HistoryView } from "../../viewer/history-view";
|
|
|
17
17
|
import { setup } from "goober";
|
|
18
18
|
import React from "react";
|
|
19
19
|
|
|
20
|
+
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
21
|
+
|
|
20
22
|
function extractAction(row: HTMLElement | null | undefined) {
|
|
21
23
|
if (!row) return "";
|
|
22
24
|
// index 0: author, index 1: action, index 2: timestamp
|
|
@@ -30,7 +32,6 @@ function extractActions(): string[] {
|
|
|
30
32
|
|
|
31
33
|
describe("HistoryView", async () => {
|
|
32
34
|
const account = await setupJazzTestSync();
|
|
33
|
-
const account2 = await createJazzTestAccount();
|
|
34
35
|
|
|
35
36
|
beforeAll(() => {
|
|
36
37
|
// setup goober
|
|
@@ -270,7 +271,138 @@ describe("HistoryView", async () => {
|
|
|
270
271
|
});
|
|
271
272
|
});
|
|
272
273
|
|
|
273
|
-
describe("co.
|
|
274
|
+
describe("co.plaintext", () => {
|
|
275
|
+
it("should render co.plaintext initial append in a single row", async () => {
|
|
276
|
+
const value = co.plainText().create("hello", account);
|
|
277
|
+
render(
|
|
278
|
+
<HistoryView coValue={value.$jazz.raw} node={value.$jazz.localNode} />,
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
expect(extractActions()).toEqual(['"hello" has been appended']);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it("should render co.plaintext appends in a single row", async () => {
|
|
285
|
+
const value = co.plainText().create("hello", account);
|
|
286
|
+
value.$jazz.applyDiff("hello world");
|
|
287
|
+
value.$jazz.applyDiff("hello world!");
|
|
288
|
+
|
|
289
|
+
expect(value.$jazz.raw.toString()).toEqual("hello world!");
|
|
290
|
+
|
|
291
|
+
render(
|
|
292
|
+
<HistoryView coValue={value.$jazz.raw} node={value.$jazz.localNode} />,
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
const history = [
|
|
296
|
+
'"hello" has been appended',
|
|
297
|
+
'" world" has been inserted after "o"',
|
|
298
|
+
'"!" has been inserted after " "', // it is after " " because previous action is reversed
|
|
299
|
+
].toReversed(); // Default sort is descending
|
|
300
|
+
|
|
301
|
+
expect(extractActions()).toEqual(history);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it("should render co.plaintext delete in tail", async () => {
|
|
305
|
+
const value = co.plainText().create("hello", account);
|
|
306
|
+
value.$jazz.applyDiff("hell");
|
|
307
|
+
|
|
308
|
+
expect(value.$jazz.raw.toString()).toEqual("hell");
|
|
309
|
+
|
|
310
|
+
render(
|
|
311
|
+
<HistoryView coValue={value.$jazz.raw} node={value.$jazz.localNode} />,
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
const history = [
|
|
315
|
+
'"hello" has been appended',
|
|
316
|
+
'"o" has been deleted',
|
|
317
|
+
].toReversed(); // Default sort is descending
|
|
318
|
+
|
|
319
|
+
expect(extractActions()).toEqual(history);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it("should render co.plaintext delete in head", async () => {
|
|
323
|
+
const value = co.plainText().create("hello", account);
|
|
324
|
+
value.$jazz.applyDiff("ello");
|
|
325
|
+
|
|
326
|
+
expect(value.$jazz.raw.toString()).toEqual("ello");
|
|
327
|
+
|
|
328
|
+
render(
|
|
329
|
+
<HistoryView coValue={value.$jazz.raw} node={value.$jazz.localNode} />,
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
const history = [
|
|
333
|
+
'"hello" has been appended',
|
|
334
|
+
'"h" has been deleted',
|
|
335
|
+
].toReversed(); // Default sort is descending
|
|
336
|
+
|
|
337
|
+
expect(extractActions()).toEqual(history);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it("should render co.plaintext delete history of multiple old insertions in a single row", async () => {
|
|
341
|
+
const value = co.plainText().create("hello", account);
|
|
342
|
+
await sleep(2);
|
|
343
|
+
value.$jazz.applyDiff("hello world");
|
|
344
|
+
await sleep(2);
|
|
345
|
+
value.$jazz.applyDiff("hed");
|
|
346
|
+
|
|
347
|
+
expect(value.$jazz.raw.toString()).toEqual("hed");
|
|
348
|
+
|
|
349
|
+
render(
|
|
350
|
+
<HistoryView coValue={value.$jazz.raw} node={value.$jazz.localNode} />,
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
const history = [
|
|
354
|
+
'"hello" has been appended',
|
|
355
|
+
'" world" has been inserted after "o"',
|
|
356
|
+
'"lod" has been deleted',
|
|
357
|
+
'" worl" has been deleted',
|
|
358
|
+
].toReversed(); // Default sort is descending
|
|
359
|
+
|
|
360
|
+
expect(extractActions()).toEqual(history);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it("should render co.plaintext insertBefore in history", async () => {
|
|
364
|
+
const value = co.plainText().create("world", account);
|
|
365
|
+
await sleep(2);
|
|
366
|
+
value.insertBefore(0, "Hello, ");
|
|
367
|
+
|
|
368
|
+
expect(value.$jazz.raw.toString()).toEqual("Hello, world");
|
|
369
|
+
|
|
370
|
+
render(
|
|
371
|
+
<HistoryView coValue={value.$jazz.raw} node={value.$jazz.localNode} />,
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
const history = [
|
|
375
|
+
'"world" has been appended',
|
|
376
|
+
'"H" has been inserted before "w"',
|
|
377
|
+
'"ello, " has been inserted after "H"',
|
|
378
|
+
].toReversed(); // Default sort is descending
|
|
379
|
+
|
|
380
|
+
expect(extractActions()).toEqual(history);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it("should render co.plaintext insertAfter in history", async () => {
|
|
384
|
+
const value = co.plainText().create("world", account);
|
|
385
|
+
await sleep(2);
|
|
386
|
+
value.insertAfter(0, "Hello, ");
|
|
387
|
+
|
|
388
|
+
expect(value.$jazz.raw.toString()).toEqual("wHello, orld");
|
|
389
|
+
|
|
390
|
+
render(
|
|
391
|
+
<HistoryView coValue={value.$jazz.raw} node={value.$jazz.localNode} />,
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
const history = [
|
|
395
|
+
'"world" has been appended',
|
|
396
|
+
'"Hello, " has been inserted after "w"',
|
|
397
|
+
].toReversed(); // Default sort is descending
|
|
398
|
+
|
|
399
|
+
expect(extractActions()).toEqual(history);
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
describe("co.group", async () => {
|
|
404
|
+
const account2 = await createJazzTestAccount();
|
|
405
|
+
|
|
274
406
|
it("should render co.group changes", async () => {
|
|
275
407
|
const group = co.group().create(account);
|
|
276
408
|
|
|
@@ -1,5 +1,172 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
JsonObject,
|
|
3
|
+
JsonValue,
|
|
4
|
+
OpID,
|
|
5
|
+
RawCoMap,
|
|
6
|
+
RawCoPlainText,
|
|
7
|
+
RawCoValue,
|
|
8
|
+
Role,
|
|
9
|
+
} from "cojson";
|
|
10
|
+
import { stringifyOpID } from "cojson";
|
|
11
|
+
import type { VerifiedTransaction } from "cojson/dist/coValueCore/coValueCore.js";
|
|
2
12
|
import type { MapOpPayload } from "cojson/dist/coValues/coMap.js";
|
|
13
|
+
import * as TransactionChanges from "./transactions-changes";
|
|
14
|
+
import type {
|
|
15
|
+
DeletionOpPayload,
|
|
16
|
+
InsertionOpPayload,
|
|
17
|
+
} from "cojson/dist/coValues/coList.js";
|
|
18
|
+
|
|
19
|
+
export function areSameOpIds(
|
|
20
|
+
opId1: OpID | string,
|
|
21
|
+
opId2: OpID | string,
|
|
22
|
+
): boolean {
|
|
23
|
+
if (typeof opId1 === "string" || typeof opId2 === "string") {
|
|
24
|
+
return opId1 === opId2;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
opId1.sessionID === opId2.sessionID &&
|
|
29
|
+
opId1.txIndex === opId2.txIndex &&
|
|
30
|
+
opId1.changeIdx === opId2.changeIdx
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function isCoPlainText(coValue: RawCoValue): coValue is RawCoPlainText {
|
|
35
|
+
return coValue.type === "coplaintext";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function getTransactionChanges(
|
|
39
|
+
tx: VerifiedTransaction,
|
|
40
|
+
coValue: RawCoValue,
|
|
41
|
+
): JsonValue[] {
|
|
42
|
+
if (tx.isValid === false && tx.tx.privacy === "private") {
|
|
43
|
+
const readKey = coValue.core.getReadKey(tx.tx.keyUsed);
|
|
44
|
+
if (!readKey) {
|
|
45
|
+
return [
|
|
46
|
+
`Unable to decrypt transaction: read key ${tx.tx.keyUsed} not found.`,
|
|
47
|
+
];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
coValue.core.verified.decryptTransaction(
|
|
52
|
+
tx.txID.sessionID,
|
|
53
|
+
tx.txID.txIndex,
|
|
54
|
+
readKey,
|
|
55
|
+
) ?? []
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Trying to collapse multiple changes into a single action in the history
|
|
60
|
+
if (isCoPlainText(coValue)) {
|
|
61
|
+
if (tx.changes === undefined || tx.changes.length === 0) return [];
|
|
62
|
+
const firstChange = tx.changes[0]!;
|
|
63
|
+
|
|
64
|
+
if (
|
|
65
|
+
TransactionChanges.isItemAppend(firstChange) &&
|
|
66
|
+
tx.changes.every(
|
|
67
|
+
(c) =>
|
|
68
|
+
TransactionChanges.isItemAppend(c) &&
|
|
69
|
+
areSameOpIds(c.after, firstChange.after),
|
|
70
|
+
)
|
|
71
|
+
) {
|
|
72
|
+
const changes = tx.changes as InsertionOpPayload<string>[];
|
|
73
|
+
if (firstChange.after !== "start") {
|
|
74
|
+
changes.reverse();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return [
|
|
78
|
+
{
|
|
79
|
+
op: "app",
|
|
80
|
+
value: changes.map((c) => c.value).join(""),
|
|
81
|
+
after: firstChange.after,
|
|
82
|
+
},
|
|
83
|
+
];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (
|
|
87
|
+
TransactionChanges.isItemPrepend(firstChange) &&
|
|
88
|
+
tx.changes.every(
|
|
89
|
+
(c) =>
|
|
90
|
+
TransactionChanges.isItemPrepend(c) &&
|
|
91
|
+
areSameOpIds(c.before, firstChange.before),
|
|
92
|
+
)
|
|
93
|
+
) {
|
|
94
|
+
const changes = tx.changes as InsertionOpPayload<string>[];
|
|
95
|
+
if (firstChange.before !== "end") {
|
|
96
|
+
changes.reverse();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return [
|
|
100
|
+
{
|
|
101
|
+
op: "pre",
|
|
102
|
+
value: changes.map((c) => c.value).join(""),
|
|
103
|
+
before: firstChange.before,
|
|
104
|
+
},
|
|
105
|
+
];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (
|
|
109
|
+
TransactionChanges.isItemDeletion(firstChange) &&
|
|
110
|
+
tx.changes.every((c) => TransactionChanges.isItemDeletion(c))
|
|
111
|
+
) {
|
|
112
|
+
const coValueBeforeDeletions = coValue.atTime(tx.madeAt - 1);
|
|
113
|
+
|
|
114
|
+
// Verify if the deleted chars are consecutive
|
|
115
|
+
function changesAreConsecutive(changes: DeletionOpPayload[]): boolean {
|
|
116
|
+
if (changes.length < 2) return false;
|
|
117
|
+
const mapping = coValueBeforeDeletions.mapping.idxAfterOpID;
|
|
118
|
+
|
|
119
|
+
for (let i = 1; i < changes.length; ++i) {
|
|
120
|
+
const prevIdx = mapping[stringifyOpID(changes[i - 1]!.insertion)];
|
|
121
|
+
const currIdx = mapping[stringifyOpID(changes[i]!.insertion)];
|
|
122
|
+
if (currIdx !== prevIdx && currIdx !== (prevIdx ?? -2) + 1) {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (changesAreConsecutive(tx.changes)) {
|
|
130
|
+
// Group the deletions by insertion.sessionID-txIndex
|
|
131
|
+
// This is to help the readability of deletions that act on different previous transactions
|
|
132
|
+
const groupedBySession: Map<string, DeletionOpPayload[]> = new Map();
|
|
133
|
+
for (const change of tx.changes) {
|
|
134
|
+
const group = `${change.insertion.sessionID}-${change.insertion.txIndex}`;
|
|
135
|
+
if (!groupedBySession.has(group)) groupedBySession.set(group, []);
|
|
136
|
+
groupedBySession.get(group)!.push(change);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return Array.from(groupedBySession.values()).map((changes) => {
|
|
140
|
+
const stringDeleted = changes
|
|
141
|
+
// order by txIndex and changeIdx
|
|
142
|
+
.toSorted((a, b) => {
|
|
143
|
+
if (a.insertion.txIndex === b.insertion.txIndex) {
|
|
144
|
+
return a.insertion.changeIdx - b.insertion.changeIdx;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return a.insertion.txIndex - b.insertion.txIndex;
|
|
148
|
+
})
|
|
149
|
+
// extract the single char from the insertions
|
|
150
|
+
.map((c) =>
|
|
151
|
+
coValueBeforeDeletions.get(
|
|
152
|
+
coValueBeforeDeletions.mapping.idxAfterOpID[
|
|
153
|
+
stringifyOpID(c.insertion)
|
|
154
|
+
]!,
|
|
155
|
+
),
|
|
156
|
+
)
|
|
157
|
+
.join("");
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
op: "custom",
|
|
161
|
+
action: `"${stringDeleted}" has been deleted`,
|
|
162
|
+
};
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return tx.changes ?? (tx.tx as any).changes ?? [];
|
|
169
|
+
}
|
|
3
170
|
|
|
4
171
|
export function restoreCoMapToTimestamp(
|
|
5
172
|
coValue: RawCoMap,
|