Skip to content

Commit 2fdb08d

Browse files
committed
* Fixed that subtree-import would error if node being imported under was a claim.
* MS if a subtree is imported where the root-layer has no "text" field, that layer is interpreted as just a children-container layer rather than an actual node. (so its children are added directly under the right-clicked node, rather than put under a new, empty-text category node) * Cleaned up file-structure for the import-subtree dialog. * Deprecated the text-fields other than "text". (they are redundant information, since the collection field-name already has that info) * Added support for the "questions" field. (might as well, to make every claim-gen node type able to be exported as an array; also needed now due to change just above) * Added support for the "premises" field. (this not only adds children, it also forces the node's type to become "argument") * Added support for entries in the "atomic_claims", "counter_claims", and "examples" collections being objects rather than strings. (also deprecated support for strings)
1 parent 361ed56 commit 2fdb08d

20 files changed

Lines changed: 987 additions & 924 deletions

Packages/client/Source/Store/main/maps.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {MapState} from "./maps/mapStates/@MapState.js";
1111
import {GetMapView, GetNodeView} from "./maps/mapViews/$mapView.js";
1212
import {GetPlaybackInfo} from "./maps/mapStates/PlaybackAccessors/Basic.js";
1313
import {GetPathVisibilityInfoAfterEffects, GetPlaybackEffects} from "./maps/mapStates/PlaybackAccessors/ForEffects.js";
14-
import {SubtreeIncludeKeys, SubtreeOperation} from "../../UI/@Shared/Maps/Node/NodeUI_Menu/Dialogs/SubtreeOpsStructs.js";
14+
import {SubtreeIncludeKeys, SubtreeOperation} from "../../UI/@Shared/Maps/Node/NodeUI_Menu/Dialogs/SubtreeOps/SubtreeOpsStructs.js";
1515

1616
export enum RatingPreviewType {
1717
none = "none",
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
import {gql} from "@apollo/client";
2+
import {AsNodeL1Input, ChildGroup, CullNodePhrasingToBeEmbedded, GetMap, GetNode, GetNodeChildrenL2, GetNodeDisplayText, GetNodeL2, NodeL1, NodeL3, NodeLink, NodePhrasing, NodeRevision, NodeType, Polarity} from "dm_common";
3+
import {E, ModifyString} from "js-vextensions";
4+
import {CreateAccessor, GetAsync} from "mobx-graphlink";
5+
import React from "react";
6+
import {Button, CheckBox, Column, Row, Text} from "react-vcomponents";
7+
import {BaseComponent} from "react-vextensions";
8+
import {ShowMessageBox} from "react-vmessagebox";
9+
import {store} from "Store";
10+
import {GetOpenMapID} from "Store/main.js";
11+
import {ImportResource, IR_NodeAndRevision} from "Utils/DataFormats/DataExchangeFormat.js";
12+
import {RunCommand_AddChildNode} from "Utils/DB/Command.js";
13+
import {apolloClient} from "Utils/LibIntegrations/Apollo.js";
14+
import {AddNotificationMessage, ES, Observer, RunInAction_Set} from "web-vcore";
15+
import {CreateAncestorForResource, CreateResource, ResolveNodeIDsForInsertPath} from "./Utils.js";
16+
17+
@Observer
18+
export class ImportResourceUI extends BaseComponent<
19+
{
20+
importUnderNode: NodeL3, resource: ImportResource, index: number, resources: ImportResource[],
21+
autoSearchByTitle: boolean,
22+
searchQueryGen: number, onNodeCreated: ()=>any,
23+
},
24+
{search: boolean, existingNodesWithTitle: number|n}
25+
> {
26+
ComponentWillMountOrReceiveProps(props, forMount) {
27+
if (forMount || props.autoSearchByTitle != this.props.autoSearchByTitle) {
28+
this.SetState({search: props.autoSearchByTitle}, ()=>{
29+
this.ApplySearchSetting();
30+
});
31+
}
32+
// here, we only rerun the search (based on search-query-generation), if we haven't found a matching node yet
33+
else if (props.searchQueryGen != this.props.searchQueryGen && (this.state.existingNodesWithTitle ?? 0) == 0) {
34+
this.ApplySearchSetting();
35+
}
36+
}
37+
async ApplySearchSetting() {
38+
const res = this.props.resource;
39+
if (this.state.search && res instanceof IR_NodeAndRevision && res.CanSearchByTitle()) {
40+
// todo: update this to work against new rust backend! (atm this query fails, hence ui option for it disabled)
41+
const result = await apolloClient.query({
42+
query: gql`
43+
query SearchQueryForImport($title: String!) {
44+
nodeRevisions(filter: {phrasing: {contains: {text_base: $title}}}) {
45+
nodes { id }
46+
}
47+
}
48+
`,
49+
variables: {title: res.revision.phrasing.text_base},
50+
fetchPolicy: "network-only",
51+
});
52+
const foundNodeIDs = result.data.nodeRevisions.nodes.map(a=>a.id);
53+
this.SetState({existingNodesWithTitle: foundNodeIDs.length});
54+
} else {
55+
this.SetState({existingNodesWithTitle: null});
56+
}
57+
}
58+
59+
render() {
60+
const {importUnderNode, resource: res, index, resources, onNodeCreated} = this.props;
61+
const {search, existingNodesWithTitle} = this.state;
62+
const uiState = store.main.maps.importSubtreeDialog;
63+
const pathStr = res.pathInData.join("."); //+ (resource.path.length > 0 ? "." : "");
64+
65+
const map = GetMap(GetOpenMapID());
66+
67+
const insertPath = res instanceof IR_NodeAndRevision && res.insertPath_titles ? res.insertPath_titles : [];
68+
const insertPath_resolvedNodeIDs = ResolveNodeIDsForInsertPath(importUnderNode.id, insertPath);
69+
const parentNodeID = insertPath.length > 0 ? insertPath_resolvedNodeIDs.LastOrX() : importUnderNode.id;
70+
const ownNodeTextResolved = parentNodeID != null && res instanceof IR_NodeAndRevision && res.ownTitle != null
71+
// use CatchBail, so that after each node-add, it doesn't cause the rows to switch to "Loading..." (which causes loss of the scroll-position)
72+
? ResolveNodeIDsForInsertPath.CatchBail([null], parentNodeID, [res.ownTitle]).Last() != null
73+
: false;
74+
75+
return (
76+
<Column mt={index == 0 ? 0 : 5} pr={5} sel style={{border: "solid gray", borderWidth: index == 0 ? 0 : "1px 0 0 0"}}>
77+
<Row>
78+
{res instanceof IR_NodeAndRevision &&
79+
<>
80+
<Text style={{flexShrink: 0, fontWeight: "bold", padding: "0 3px", background: "rgba(128,128,128,.5)", marginBottom: -5}}>{pathStr}</Text>
81+
<Row ml={5} mr={5} style={{flex: 1, display: "block"}}>
82+
<Text mr={3} style={{display: "inline-block", flexShrink: 0, fontWeight: "bold"}}>
83+
{ModifyString(res.node.type, m=>[m.startLower_to_upper])}
84+
{res.node.type == NodeType.argument && res.link.polarity != null &&
85+
<Text style={{display: "inline-block", flexShrink: 0, fontWeight: "bold"}}> [{res.link.polarity == Polarity.supporting ? "pro" : "con"}]</Text>}
86+
{":"}
87+
</Text>
88+
<Column>
89+
<Row>{res.revision.phrasing.text_base}</Row>
90+
{(res.revision.phrasing.text_question ?? "").length > 0 && <Row>{`<question form> ${res.revision.phrasing.text_question}`}</Row>}
91+
{(res.revision.phrasing.text_narrative ?? "").length > 0 && <Row>{`<narrative form> ${res.revision.phrasing.text_narrative}`}</Row>}
92+
</Column>
93+
</Row>
94+
</>}
95+
<Column>
96+
{res instanceof IR_NodeAndRevision && res.CanSearchByTitle() &&
97+
<CheckBox ml={5} text={`Search: ${existingNodesWithTitle ?? "?"}`}
98+
style={ES(
99+
{flex: 1},
100+
existingNodesWithTitle == 0 && {background: "rgba(0,255,0,.5)"},
101+
existingNodesWithTitle != null && existingNodesWithTitle > 0 && {background: "rgba(255,0,0,.5)"},
102+
)}
103+
104+
// temp-disabled, till backend supports the search feature
105+
enabled={false}
106+
title="This feature is currently disabled, until the backend is updated to support title-based node[-revision] searching."
107+
108+
value={search} onChange={async val=>{
109+
this.SetState({search: val}, ()=>{
110+
this.ApplySearchSetting();
111+
});
112+
}}/>}
113+
<CheckBox ml={5} text="Selected"
114+
style={E(
115+
{flexShrink: 0},
116+
uiState.selectedImportResources.has(res) && {background: "rgba(255,0,255,.5)"},
117+
)}
118+
value={uiState.selectedImportResources.has(res)}
119+
onChange={(val, e)=>{
120+
const ev = e.nativeEvent as MouseEvent;
121+
RunInAction_Set(this, ()=>{
122+
const newSelected = val;
123+
let startI = index;
124+
let lastI = index;
125+
// select range, if holding down ctrl-key (on windows), command-key (on mac), or shift-key (on either -- though must click *exactly* on the checkbox, not the label)
126+
if (ev.ctrlKey || ev.metaKey || ev.shiftKey) {
127+
if (uiState.selectFromIndex != -1) {
128+
startI = Math.min(uiState.selectFromIndex, index);
129+
lastI = Math.max(uiState.selectFromIndex, index);
130+
}
131+
} else {
132+
uiState.selectFromIndex = index;
133+
}
134+
135+
for (let i = startI; i <= lastI; i++) {
136+
if (newSelected) {
137+
uiState.selectedImportResources.add(resources[i]);
138+
} else {
139+
uiState.selectedImportResources.delete(resources[i]);
140+
}
141+
}
142+
});
143+
}}/>
144+
</Column>
145+
</Row>
146+
{uiState.showAutoInsertTools &&
147+
<Row sel style={{background: "rgba(0,0,0,.3)", padding: 3}}>
148+
<Text>Path:</Text>
149+
{insertPath.map((segment, segmentIndex)=>{
150+
const prevResolvedNodeID = segmentIndex == 0 ? importUnderNode.id : insertPath_resolvedNodeIDs[segmentIndex - 1];
151+
const prevResolvedNode = GetNodeL2(prevResolvedNodeID);
152+
const resolvedNodeID = insertPath_resolvedNodeIDs[segmentIndex];
153+
return (
154+
<Row center key={segmentIndex}
155+
style={E(
156+
{marginLeft: 5, padding: "0 3px", borderRadius: 5, cursor: "pointer"},
157+
!resolvedNodeID && {background: "rgba(255,0,0,.5)"},
158+
resolvedNodeID && {background: "rgba(0,255,0,.5)"},
159+
)}
160+
onClick={()=>{
161+
if (prevResolvedNodeID && !resolvedNodeID && res instanceof IR_NodeAndRevision) {
162+
ShowMessageBox({
163+
cancelButton: true,
164+
title: "Create this category node?",
165+
message: `
166+
Parent:${prevResolvedNode ? GetNodeDisplayText(prevResolvedNode, null, map) : "n/a"} (id: ${prevResolvedNodeID})
167+
NewNode:${segment}
168+
`.AsMultiline(0),
169+
onOK: async()=>{
170+
const success = await CreateAncestorForResource(res, map?.id, prevResolvedNodeID, segment, res.node.accessPolicy);
171+
if (success) {
172+
//await command.RunOnServer();
173+
onNodeCreated();
174+
} else {
175+
AddNotificationMessage(`Could not create ancestor "${segment}".`);
176+
}
177+
},
178+
});
179+
}
180+
}}
181+
>
182+
{segment}
183+
</Row>
184+
);
185+
})}
186+
<Row ml="auto">
187+
{res instanceof IR_NodeAndRevision &&
188+
<>
189+
<Button text={ownNodeTextResolved ? "Create (again)" : "Create"} p="0 10px" enabled={insertPath_resolvedNodeIDs.length == 0 || insertPath_resolvedNodeIDs.LastOrX() != null}
190+
style={E(
191+
ownNodeTextResolved && {backgroundColor: "rgba(0,255,0,.5)"},
192+
)}
193+
onClick={async()=>{
194+
await CreateResource(res, map?.id, insertPath_resolvedNodeIDs.LastOrX() ?? importUnderNode.id);
195+
onNodeCreated();
196+
}}/>
197+
</>}
198+
</Row>
199+
</Row>}
200+
</Column>
201+
);
202+
}
203+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import {AsNodeL1Input, ChildGroup, CullNodePhrasingToBeEmbedded, GetNode, GetNodeChildrenL2, NodeL1, NodeLink, NodePhrasing, NodeRevision, NodeType} from "dm_common";
2+
import {E} from "js-vextensions";
3+
import {CreateAccessor, GetAsync} from "mobx-graphlink";
4+
import {Assert} from "react-vextensions/Dist/Internals/FromJSVE";
5+
import {GetOpenMapID} from "Store/main.js";
6+
import {ImportResource, IR_NodeAndRevision} from "Utils/DataFormats/DataExchangeFormat.js";
7+
import {RunCommand_AddChildNode} from "Utils/DB/Command.js";
8+
import {CommandEntry, RunCommandBatch} from "Utils/DB/RunCommandBatch.js";
9+
10+
export async function ImportResourcesOnServer(resources: ImportResource[], mapID: string, importUnderNodeID: string, onProgress: (resourcesImported: number)=>void) {
11+
const commandEntries = resources.map(res=>{
12+
const parentResource = res instanceof IR_NodeAndRevision ? resources.find(a=>a.localID == res.insertPath_parentResourceLocalID) : null;
13+
const parentResource_indexInBatch = parentResource ? resources.indexOf(parentResource) : -1;
14+
if (res instanceof IR_NodeAndRevision && res.insertPath_parentResourceLocalID != null) {
15+
Assert(parentResource != null && parentResource_indexInBatch != -1, "Parent-resource not found in batch.");
16+
}
17+
18+
const parentNodeIDOrPlaceholder = parentResource_indexInBatch != -1 ? "[placeholder; should be replaced by command-entry's field-override]" : importUnderNodeID;
19+
const [commandFunc, args] = GetCommandFuncAndArgsToCreateResource(res, GetOpenMapID(), parentNodeIDOrPlaceholder);
20+
21+
let commandName: string;
22+
if (commandFunc == RunCommand_AddChildNode) commandName = "addChildNode";
23+
else Assert(false, "Unrecognized command function for batch import.");
24+
return E(
25+
{[commandName]: args},
26+
parentResource_indexInBatch != -1 && {setParentNodeToResultOfCommandAtIndex: parentResource_indexInBatch},
27+
) as CommandEntry;
28+
});
29+
30+
return await RunCommandBatch(commandEntries, onProgress);
31+
}
32+
33+
export function GetCommandFuncAndArgsToCreateResource(res: ImportResource, mapID: string|n, parentID: string) {
34+
if (res instanceof IR_NodeAndRevision) {
35+
return [RunCommand_AddChildNode, {
36+
mapID, parentID,
37+
node: AsNodeL1Input(res.node),
38+
revision: res.revision.ExcludeKeys("creator", "createdAt"),
39+
link: res.link,
40+
}] as const;
41+
}
42+
Assert(false, `Cannot generate command to create resource of type "${res.constructor.name}".`);
43+
}
44+
export async function CreateResource(res: ImportResource, mapID: string|n, parentID: string) {
45+
const [commandFunc, args] = GetCommandFuncAndArgsToCreateResource(res, mapID, parentID);
46+
await commandFunc(args);
47+
}
48+
49+
export const ResolveNodeIDsForInsertPath = CreateAccessor((importUnderNodeID: string, insertPath: string[])=>{
50+
const resolvedNodeIDs = [] as (string|null)[];
51+
for (const segment of insertPath) {
52+
const prevNodeID = resolvedNodeIDs.length == 0 ? importUnderNodeID : resolvedNodeIDs.Last();
53+
const prevNodeChildren = prevNodeID != null ? GetNodeChildrenL2(prevNodeID) : [];
54+
const nodeForSegment = prevNodeChildren.find(a=>{
55+
return a.current.phrasing.text_base.trim() == segment.trim()
56+
// maybe temp; also match on text_question (needed atm for SL imports, but should maybe find a more elegant/generalizable way to handle this need)
57+
|| (a.current.phrasing.text_question ?? "").trim() == segment.trim()
58+
// maybe temp; also match on text_narrative (newer version of SL imports should use this instead of text_question)
59+
|| (a.current.phrasing.text_narrative ?? "").trim() == segment.trim();
60+
});
61+
resolvedNodeIDs.push(nodeForSegment?.id ?? null);
62+
}
63+
return resolvedNodeIDs;
64+
});
65+
66+
export async function CreateAncestorForResource(res: ImportResource, mapID: string|n, parentIDOfNewNode: string, newNodeTitle: string, newNodeAccessPolicy: string): Promise<boolean> {
67+
const parentOfNewNode = await GetAsync(()=>GetNode(parentIDOfNewNode));
68+
if (parentOfNewNode == null) return false;
69+
await RunCommand_AddChildNode({
70+
mapID, parentID: parentIDOfNewNode,
71+
link: new NodeLink({
72+
group: parentOfNewNode.type == NodeType.category ? ChildGroup.generic : ChildGroup.freeform,
73+
}),
74+
node: AsNodeL1Input(new NodeL1({
75+
type: NodeType.category,
76+
accessPolicy: newNodeAccessPolicy,
77+
//creator: systemUserID,
78+
})),
79+
revision: new NodeRevision({
80+
//creator: systemUserID,
81+
phrasing: CullNodePhrasingToBeEmbedded(new NodePhrasing({
82+
text_base: newNodeTitle,
83+
})),
84+
}),
85+
});
86+
return true;
87+
}

0 commit comments

Comments
 (0)