ssjs to save multiple documents - xpages

With the onclick event of a button I would like to save multiple documents, but only the last one gets saved.
<xp:this.data>
<xp:dominoDocument var="document1" formName="tg"></xp:dominoDocument>
</xp:this.data>
and in the onclick event of the button :
...
while (re.next()) {
document1.replaceItemValue("TGARKD",tgarkd);
document1.replaceItemValue("TGKDOM",tgkdom);
document1.replaceItemValue("TGARGR",tgargr);
document1.replaceItemValue("TGDLGR",tgdlgr);
document1.save();
}

If you want to create multiple Documents in the NSF, you'll need to use backend classes. The DominoDocument datasource is tied to a single backend document. var doc = document1.getDocument(true) will get a handle on the (first) backend document, then in your loop use
var doc2 = database.createDocument();
doc.copyAllItems(doc2, true);
doc2.save(true, false);

Alternatively you could define your data source inside a repeat control and bind the fields to that one. Then outside the repeat you call save() which saves all data sources

Related

documentId resolving code doesn't get executed XPages

In my XPage I need to set a data source (Domino document)
I try to do it as follows:
<xp:this.data>
<xp:dominoDocument var="requestDocument" action="openDocument" databaseName="#{javascript: print('db ok'); return database.getFilePath();}"
documentId="#{javascript:
print('heloooo');
var conclusion = database.getDocumentByUNID(doc_source.getDocument().getParentDocumentUNID());
var oConclusion = new OsnovaUI_document(conclusion);
var requestDoc = oConclusion.getMainDocument();
print('docID: ' + requestDoc.getUniversalID());
return requestDoc.getUniversalID();
}">
</xp:dominoDocument>
</xp:this.data>
The thing I've noticed is that code section in documentId doesn't get executed. At all. That's why I've put heloooo in there. However, the databaseName works as expected. In console I always see
09.03.2020 00:52:11 HTTP JVM: db ok
But not heloooo :(
What am I doing wrong? Thanks in advance
The most likely cause is ignoreRequestParams is not set to true. Unless you set that, the data source is retrieving all specifics about which document to edit based on the URL query string parameters (the HTTP request parameters). As a result, the URL query string parameters take precedence, and in the case of nothing being set, that means "use a new document". If you've defined that the URL query string parameters should take precedence, running your code to merely ignore it afterwards is inefficient. As a result, action="openDocument" is also being ignored - if you have a docId in the query string it will open that, otherwise it will create a new document.
documentId can only get processed once, when the page first loads. Depending on whether the datasource is bound to a panel or an XPage / Custom Control, it will run before the beforePageLoad event as well. So runtime binding (#{javascript:...) has no effect. ${javascript:... will avoid confusion.
Error handling may help identify if there is an error. XPages OpenLog Logger is one of the most pervasive (disclaimer, I'm the author) https://openntf.org/main.nsf/project.xsp?r=project/XPages%20OpenLog%20Logger.
Change documentId to be computed on page load ($) and not dynamically (#):
<xp:this.data>
<xp:dominoDocument var="requestDocument" action="openDocument">
<xp:this.documentId><![CDATA[${javascript:
var conclusion = database.getDocumentByUNID(doc_source.getDocument().getParentDocumentUNID());
var oConclusion = new OsnovaUI_document(conclusion);
var requestDoc = oConclusion.getMainDocument();
print('docID: ' + requestDoc.getUniversalID());
return requestDoc.getUniversalID();
}]]></xp:this.documentId>
</xp:dominoDocument>
</xp:this.data>
The databaseName is not required if the database is itself.

How to get marked rows in the XPages Extension Library <xe:dataView> design element?

I have a XPage with design element. How can I get list of checked rows to post it to an agent?
<xe:dataView id="dataView1" columnTitles="true"
expandedDetail="true" var="dview1"
openDocAsReadonly="false" rows="15" showCheckbox="true"
showHeaderCheckbox="true">
Thank you!
For the client-side, you can use Dojo. The following CSJS script will return NoteIds for all selected rows:
dojo.query(".lotusFirstCell > input:checked").attr('value')
For the server side, you can grab the IDs of selected documents by:
var idList = getComponent("dataView1").getSelectedIds();
This will return a string array of NoteIDs. Then pass it to an in-memory document and call the agent.
var doc = database.createDocument();
doc.replaceItemValue("IDList", IDList);
var agent:NotesAgent=database.getAgent("SomeAgent");
agent.runWithDocumentContext(doc);
Perfect!
This is exactly what I need:
var IDs = dojo.query(".xspFirstCell > input:checked").attr('value');

xpages custom deleting doc. from view panel

I'm trying to delete selected doc from viewPanel1. The view is categorized ( can be > 1 category ) and is listing documents from 2 different datasources, let say: Cdoc and Pdoc. These docs. are linked by a common field.
my scenario: If the users select a Cdoc => the delete action will take place to the respective Cdoc but also for the all Pdoc being in the same category. If the user selects a Pdoc => delete just the Pdoc. Also, I would like to add some confirmation text with some information ( Value fields ) from the selected documents.
I tried the following
var viewPanel=getComponent("viewPanel1");
var docIDArray=viewPanel.getSelectedIds();
for(i=0;i < docIDArray.length;i++){
var docId=docIDArray[i];
var doc=database.getDocumentByID(docId);
var formName = (doc == null)? null : doc.getItemValueString("Form");
if( formName =="fmPersContact" ){
.....
} // in this case, it works OK.
else if ( formName =="fmCompanie" ){ // here if I selected > 1 Cdoc, it deletes just one Cdoc + the respective PDocs.
var doc:NotesDocument = null;
doc=database.getDocumentByID(docId);
var ky:java.util.Vector = new java.util.Vector();
ky.add(doc.getItemValueString("txt_NumeCompanie"));
... // delete method
}
Could you tell me what I did wrong and what am I missing in the above code? thanks for your time!
The first thing you want to do is confirm. Unlike Lotusscript, you cannot use a function in the middle of a script to open the confirm dialog and get the answer. To do this, I recommend using the confirm simple action before going into an execute script simple action.
<xp:button
value="delete"
id="button1"
>
<xp:eventHandler event="onclick" submit="true" refreshMode="complete">
<xp:this.action>
<xp:actionGroup>
<xp:confirm message="Are you certain?"></xp:confirm>
<xp:executeScript
script="#{javascript:doSomething();}"
>
</xp:executeScript>
</xp:actionGroup>
</xp:this.action></xp:eventHandler></xp:button>
EDIT
In the past, I have also used built my own dialogs with the extension library, filled the text in with SSJS and then called the doWhatever() or close() from the dialog itself. This is not the best solution as it requires an update from the server to get the string. The best solution would be, as Paul Withers says, to use CSJS to perform the confirmation. I have yet to do this though.
/EDIT
For your delete function, I recommend getting the document you want to delete, then tell whether it is a P- or C- doc, either by the form name or whatever mechanism you use, and then either delete the single document or by getting a documentcollection from the view by using getAllDocumentsBykey(), then iterating through them all, deleting them one by one.
var ky:java.util.Vector = new java.util.Vector();
ky.add("MainCat");
ky.add("subCat");
ky.add("subCat2");
var vw:NotesView = database.getView("vw_myView");
var docs:NotesDocumentCollection = vw.getAllDocumentsByKey(ky);
//... delete stuff...
//dont forget to recycle
Post Question Edit
I recommend the following to get the form name:
var getSelectedDoc = function(){
var vwPnl = getComponent("viewpanel");
var ids = vwPnl.getSelectedIds();
var id = null;
var doc:NotesDocument = null;
if(ids.length > 0){ //could use for loop var i = 0; i < ids.length;i++
id = ids[0]; //could pack all ids into java.util.ArrayList and return that list to work on further
//be warned that if the user selects a parent doc and those automatically deleted by it that you need a mechanism to check if the document was already deleted!
}
if(id != null){
doc = database.getDocumentByID(id);
}
return doc;
}
var doc = getSelectedDoc();
var formName = (doc == null)? null : doc.getItemValueString("form");
if(PDOC_FORM_NAME.equalsIgnoreCase(formName)){
deleteFunctionComplete(doc);
} else if (CDOC_FORM_NAME.equalsIgnoreCase(formName)){
deleteFunctionTwo(doc);
} else {
// uh-oh
}
this also allows you to have the document in case you want to delete it right away.
Edits for comments
If Cdoc should delete more than one document, then yes. You should be using the getAllDocumentsBykey keeping in mind that the view needs to be built for it. By that I mean if you have a view with one single category, there is no issue, just plug in the string and you are fine. If you have a view with three categories, you cannot feed a vector into the getalldocs function with only two values, it must be all three. So, you want to delete all for "mycomp" with the underlying pdocs "Greg" "Sally "Bob", just use alldocsbykey("mycomp"), if the view looks like:
mycomp
---Greg
---Sally
---Bob
but if the view looks like
Poland
---mycomp
------Greg
------Sally
------Bob
then the a vector with poland and mycomp must be used. "poland" does not get the correct documents. --just an fyi and pitfall that is sometimes had.
Edit after further question clarification
I prefer this loop style to remove docs
var doc_temp:NotesDocument = null;
var doc_toDelete:NotesDocument = null;
var coll_docs:NotesDocumentCollection = ...; //get document collection
var doc_nextDoc = coll_docs.getFirstDocument();
while(doc_nextDoc != null){
doc_temp = doc_nextDoc; //set document to delete
doc_nextDoc = coll_docs.getNextDocument(doc_nextDoc); // set next document before deletion
try{
doc_temp.remove(true);//lots of errors can happen here, such as ACL settings
} catch(e) {
//handle, or just break
} finally{
if(doc_temp != null) try{doc_temp.recycle()} catch(e){}// try to recycle, could also cause errors
doc_temp = null;// for the sense of completeness
}
}
Even further edit based on the question edits
of course you are only deleting one Pdoc, the way you have that set up, you are only ever returning one document. You could expand the getSelectedDoc() to put all selected documents into an java.util.ArrayList or something, and then use that arraylist to delete more than one at a time, but that could be dangerous depending on what you do because NotesDocuments are not serialisable. In that case, I recommend using the same code that you use for getSelected doc, use a for loop to get the document IDs, get the document, if the document is not null, then delete.
apropos getAllDocumentsByKey(with a vector)
The way this is currently set up, no Vector is necessary.
If you have a view with a category and sub category and you want to get all the documents in that sub category, then you must use a vector to get at it. If you include a simple string or a vector with only one value, then the documents in the sub category will not be returned. The vector can be thought of as "cat1", "subcat", "furtherSubCat"
Furthermore, there is no check here to see if the string returned from the document is empty. This should be done. There is also no check to see if the DocumentCollection is empty. This should also be done. My expectation is that there is an issue retrieving the collection based on above mentioned reasons.

Repeat control with checkbox?

I have a repeat control which displays a view. I now want to include a checkbox, so that I can perform actions on the selected documents. I have no problem adding the check box, but how can I check if the checkbox is checked then get to the associated document for that row?
Thoughts I had were:
Have the check box change event add or remove the document's UNID from a array scope variable. Then just perform actions on documents in that array.
Forget the checkbox and just popup a list box, to allow the user to select from that.
But is there an easier way?
For maximum flexibility, it's best to not bind our user interface components directly to data; rather, if we introduce a middle "data model" layer (typically, one that describes the real-world objects / people / processes the data is representative of, rather than thinking in "documents", which are ultimately just evidence that these real-world things exist), our UI code becomes very clean, easy to understand, and easy to maintain. It also makes it much easier to introduce features that are otherwise frustrating to implement when we continue to think in documents.
Suppose, for instance, that we use the Object Data Source from the Extension Library to create an arbitrary object (for example, let's call it pendingRequests) that we can later bind our repeat control to (instead of binding it directly to the view):
// Create an empty array to return at the end:
var results = [];
// Create a view navigator instance for iterating the view contents:
var pendingView = database.getView("pendingRequests");
var entryNavigator = pendingView.createViewNav();
var eachEntry = entryNavigator.getFirst();
while (eachEntry != null) {
// Add metadata about each entry to result array:
var metaData = eachEntry.getColumnValues();
results.push({
startDate: metaData.get(0).getDateOnly(),
endDate: metaData.get(1).getDateOnly(),
employeeName: metaData.get(2),
status: metaData.get(3),
unid: eachEntry.getUniversalID(),
selected: "0"
});
// In case any column values were Domino objects:
recycleAll(metaData);
// Cruise on to the next:
eachEntry = navigateToNext(entryNavigator, eachEntry);
}
// Final Domino handle cleanup:
recycleAll(entryNavigator, pendingView);
// Return our now populated array:
return results;
Before proceeding, I should point out that the above example includes two pieces of syntactical candy that aren't native to the platform: recycleAll() and navigateToNext(). Both of these are just utility functions for making the stupid recycle stuff easier to handle:
recycleAll
* More convenient recycling
*/
function recycleAll() {
for(var i = 0; i < arguments.length; i++) {
var eachObject = arguments[i];
// assume this is a collection
try {
var iterator = eachObject.iterator();
while (iterator.hasNext()) {
recycleAll(iterator.next());
}
} catch (collectionException) {
try {
eachObject.recycle();
} catch (recycleException) {
}
}
}
}
navigateToNext
/*
* Safe way to navigate view entries
*/
function navigateToNext(navigator, currentEntry) {
var nextEntry = null;
try {
nextEntry = navigator.getNext(currentEntry);
} catch (e) {
} finally {
recycleAll(currentEntry);
}
return nextEntry;
}
Okay, now back to the data model... specifically, this block:
var metaData = eachEntry.getColumnValues();
results.push({
startDate: metaData.get(0).getDateOnly(),
endDate: metaData.get(1).getDateOnly(),
employeeName: metaData.get(2),
status: metaData.get(3),
unid: eachEntry.getUniversalID(),
selected: "0"
});
So for each view entry, we create a very simple object that has all the pertinent info we want to allow the user to interact with, as well as two extra properties that are there for our own code's convenience: unid, which allows to get back to the document if we need to, and selected, which gives us a way to bind a checkbox to a property of this metadata object... which means the user can toggle its value via the checkbox.
So here's a basic example of how we might represent this data to the user:
<ul style="list-style-type: none;">
<xp:repeat var="vacationRequest" value="#{pendingRequests}">
<li style="margin-bottom:10px;">
<strong>
<xp:checkBox value="#{vacationRequest.selected}" text="#{vacationRequest.startDate} - #{vacationRequest.endDate}"
checkedValue="1" uncheckedValue="0" />
</strong>
<xp:text value="#{vacationRequest.employeeName} (#{vacationRequest.status})" tagName="div" />
</li>
</xp:repeat>
</ul>
Each checkbox in the repeat control is now bound directly to the selected property of the metadata object each "row" represents... which also has a unid property, so acting on the actual documents that correspond to this data model is simple:
for (var i = 0; i < pendingRequests.length; i++) {
var eachRequest = pendingRequests[i];
if (eachRequest.selected == "1") {
var requestDataSource = database.getDocumentByUNID(eachRequest.unid);
requestDataSource.replaceItemValue("status", "Approved");
if (requestDataSource.save()) {
// update in-memory metadata:
eachRequest.status = "Approved";
}
}
}
Since our data source is just an array of these metadata objects, we can just loop through each, ask whether the user has toggled the selected property of each, and, if so, get a handle on its corresponding document, modify one or more items, and save it. NOTE: because we're using a data source in this example, it won't reload the back end view data on every event. For performance reasons, this is a Very Good Thing (tm). But it does mean that we have to update the in-memory metadata object to match what we changed on the document (i.e. eachRequest.status = "Approved")... but it also means we can update only that, instead of having to scrap our entire data source and have it read everything back in from the view.
As a bonus, adding something like a "Select All" button is even simpler:
for (var i = 0; i < pendingRequests.length; i++) {
pendingRequests[i].selected = "1";
}
So, in summary, we have an in-memory data model where, in many cases, equivalent operations will execute more rapidly, but also allows us to write less code -- and more readable code -- to do fancier things.
If you want to play with this pattern live (and / or download all of the above source code in context), I've immortalized it here.
Bruce,
I did a Video on selecting documents from a repeat control. I did not use check boxes but used an Add and Remove button and then did some CSS to highlight the selected documents. I'm sure a checkbox could be used with basically the same code as in the add/remove buttons.
Basically I created a java.util.ArrayList in memory to hold the unids and then populated that array when a repeat row is clicked on. I computed the CSS for each row and if that UNID exists in the Array I change the background color to show it's "selected". I don't actually show any processing on the selected unids but since that array is in scoped memory you can pretty much do anything you want with it. Anyway the video demo is here:
http://notesin9.com/index.php/2011/04/01/notesin9-025-selecting-documents-from-a-repeat-control/
As always, Tim's answer is your best bet for long-term health, sanity, and code-maintainability.
There's also another route I've taken in the past, before I started using Java for all of my back-end logic. You can create a page-load-bound dataContext containing a HashMap and then bind each checkbox to that - it will fill in true or false for each key, so you can then loop through the map entries and find the ones that are true, which are the checked values.
I put together a quick example that pulls in the list from the names database to show what I mean:
<?xml version="1.0" encoding="UTF-8"?>
<xp:view xmlns:xp="http://www.ibm.com/xsp/core">
<xp:this.data>
<xp:dominoView var="names" databaseName="names.nsf" viewName="$NamesFieldLookup"/>
</xp:this.data>
<xp:this.dataContexts>
<xp:dataContext var="checkedNames" value="${javascript: new java.util.HashMap() }"/>
</xp:this.dataContexts>
<xp:div id="refresher">
<xp:repeat value="#{names}" var="name" rows="3">
<xp:this.facets>
<xp:pager xp:key="header" id="pager1" layout="Previous Group Next" />
</xp:this.facets>
<div>
<xp:checkBox value="#{checkedNames[name.$9]}">
<xp:eventHandler event="onclick" submit="true" refreshMode="partial" refreshId="refresher"/>
</xp:checkBox>
<xp:text value="#{name.$9}"/>
</div>
</xp:repeat>
<p><xp:text value="#{checkedNames}"/></p>
</xp:div>
</xp:view>

save() method on datasource does not fire querySave/postSave events

My save button uses SSJS with some logic. I want to save datasource, so I use
document1.save();
Script works, but querySave/postSave code is not executed.
Only workaround is to use simple action and divide button event to blocks for "execute script", "Save document (simple action)" and "execute script" (just to return "navigation" string).
Is it possible to save datasource in SSJS and fire qS/pS events?
please try this SSJS code:
var dsName = "document1.DATASOURCE";
var app = facesContext.getApplication();
var ds = app.getVariableResolver().resolveVariable(facesContext, dsName);
ds.save( facesContext, true );
The variable dsName contains the name of your datasource followed by ".DATASOURCE". To use it f.e. with current document, you have to change to "currentDocument.DATASOURCE".
Hope this helps
Sven
Sven what is the difference between your code and currentDocument.save() is something else happening than querysave and postsave?

Resources