Mindoo Blog - Cutting edge technologies - About Java, Lotus Notes and iPhone

    • Chad Schelfhout’s toolbar code to change document properties

      Karsten Lehmann  April 26 2022 11:10:22 AM
      I just noticed that Chad Schelfhout's website is no longer available, that contained formula code for a toolbar icon to read/edit document properties.
      So here it is to not get lost - the latest version I have.

      Please note that the Notes Client 12 contains Panagenda's Advanced Properties Box plugin to read and compare document properties.

      And Daniel Nashed describes how to get an extended version of the plugin that supports editing as well:
      https://blog.nashcom.de/nashcomblog.nsf/dx/advanced-document-properties-including-edit.htm

      So the code below is probably only relevant anymore for older Notes Client installs or the Basic Client.

      REM {Edit Document Fields 6.0.0 by Chad Schelfhout.};
      REM {Visit http://www.chadsmiley.com/EditDocumentFields for the latest updates};

      REM {Changable constants};
      cEnableConfirmation := @True;
      cFieldHistoryValues := 10;
      cStandardSeparators := ":" : ";" : " ";
      cPromptTitle :@DbTitle + " - " + @ViewTitle;

      REM {Unchangable constants};
      cProfileName :"ChadSmiley Tools";
      cEditLastField := "edfLastField";
      cEditLastFieldDataType := "edfLastFieldDataType";
      cEditLastFieldDataTypeValue := "edfLastFieldDataTypeValue";
      cEditLastSeparator := "%~%";
      cValueListSeparator := "^";
      cFromRawValueSeparator := "%@%";
      cSemicolonReplace := "#SC#";
      cMaxSearchForSelectedDocs := 5520;
      cMaxUpdatedDocuments := 1000;
      cArraySeparator := ";";
      cNoteEntryLength := 11;
      cPromptNewLineOne := @Char(13);
      cPromptNewLineTwo := cPromptNewLineOne + cPromptNewLineOne;
      cPromptTab :@Char(9);
      cCategoryNoteID := "NT00000000";
      cTextExtractList := "Text Left":"Text Left Back":"Text Right":"Text Right Back";
      cNoPromptList := cTextExtractList:"Remove Field":"Unique":"Sort Ascending":"Sort Descending":"Implode":"Explode":"Proper Case Text":"Proper Case Text Multi Value":"Lower Case Text":"Lower Case Text Multi Value":"Upper Case Text":"Upper Case Text Multi Value":"Password Convert":"Trim":"Trim then Unique";
      cErrorCheckCode := "@Implode( @Unique( @Explode( NoteIDList : ErrorNoteID ; cArraySeparator ; @False ) ) )";
      cErrorInformation := "\"Error documents: \" + @Implode( @Unique( @Explode( ErrorNoteIDList ; cArraySeparator ; @False ) ) ; \", \" ) + cPromptNewLineOne + \"Not updated documents: \" + @Implode( @Unique( @Explode( ErrorNoteIDList ; cArraySeparator ; @False ) ) ; \", \" )";


      REM {Data types|@Function execution};
      DataTypesCombo := @Explode(
      "Integer|@TextToNumber( RawValue )$"+
      "Integer Multi Value|@TextToNumber( @Explode( RawValue ; Separator; @True) )$"+
      "Date|@ToTime( RawValue )$"+
      "Date Multi Value|@ToTime( @Explode( RawValue ; Separator; @True) )$"+
      "Text|@Text( RawValue )$"+
      "Text Multi Value|@Text( @Explode( RawValue ; Separator; @True) )$"+
      "Text Left|@Left( @Text( @GetField( EditField[ef] ) ) ; ExtractValue )$"+
      "Text Left Back|@LeftBack( @Text( @GetField( EditField[ef] ) ) ; ExtractValue )$"+
      "Text Right|@Right( @Text( @GetField( EditField[ef] ) ) ; ExtractValue )$"+
      "Text Right Back|@RightBack( @Text( @GetField( EditField[ef] ) ) ; ExtractValue )$"+
      "Trim|@Trim( @Text( @GetField( EditField[ef] ) ) )$"+
      "Trim then Unique|@Unique(@Trim( @Text( @GetField( EditField[ef] ) ) ) )$"+
      "Name|RawValue$"+
      "Name Multi Value|RawValue$"+
      "Common Name|@Name( [CN]; RawValue )$"+
      "Common Name Multi Value|@Name( [CN]; @Explode( RawValue ; \":\"; @True ) )$"+
      "Upper Case Text|@UpperCase( @Implode( @Text( @GetField( EditField[ef] ) ) ) )$"+
      "Lower Case Text|@LowerCase( @Implode( @Text( @GetField( EditField[ef] ) ) ) )$"+
      "Proper Case Text|@ProperCase( @Implode( @Text( @GetField( EditField[ef] ) ) ) )$"+
      "Upper Case Text Multi Value|@UpperCase( @Explode( @Text( @GetField( EditField[ef] ) ) ; Separator; @True ))$"+
      "Lower Case Text Multi Value|@LowerCase( @Explode( @Text( @GetField( EditField[ef] ) ) ; Separator; @True) )$"+
      "Proper Case Text Multi Value|@ProperCase( @Explode( @Text( @GetField( EditField[ef] ) ) ; Separator; @True) )$"+
      "Replace Substring|@ReplaceSubstring( @GetField( EditField[ef] ); FromRawValue ; RawValue )$"+
      "Replace|@Explode( @Replace( @GetField( EditField[ef] ) ; FromRawValue ; RawValue ) ; Separator ; @True )$"+
      "Implode|@Implode( @Text( @GetField( EditField[ef] ) ) ; Separator )$"+
      "Explode|@Explode( @Text( @GetField( EditField[ef] ) ) ; Separator; @True )$"+
      "Formula|@Eval( RawValue )$"+
      "Abbreviate Name|@Name([Abbreviate]; RawValue )$"+
      "Abbreviate Name Multi Value|@Name( [Abbreviate]; @Explode( RawValue ; Separator; @True ) )$"+
      "Password Set|@Password( RawValue )$"+
      "Password Convert|@Password( @GetField( EditField[ef] ) )$"+
      "Remove Field|@DeleteField$"+
      "Unique|@Unique(@GetField( EditField[ef]))$"+
      "+ Append Values|@If(" +
      "     @GetField(EditField[ef]) = \"\"; RawValue;" +
      "     @Contains(DefaultDataType; \"Date\");" +
      "      @If( @IsError( @ToTime( RawValue ) ) ;" +
      "       \"\" ;" +
      "       @SetField( EditField[ef] ; @GetField(EditField[ef]) : @TextToTime( @Explode( RawValue ; Separator ) ) ) ) ;" +
      "     @Contains(DefaultDataType; \"Integer\" );" +
      "      @If( @IsError( @TextToNumber( @Explode( RawValue ; Separator ) ) ) ;" +
      "       \"\" ;" +
      "       @SetField( EditField[ef] ; @GetField(EditField[ef]) : @TextToNumber( @Explode( RawValue ; Separator ) ) ) ) ;" +
      "     @SetField( EditField[ef] ; @GetField(EditField[ef]) : @Explode( RawValue ; Separator ) ) )$"+
      "Sort Ascending|@Sort(@GetField(EditField[ef]) ; [Ascending] )$"+
      "Sort Descending|@Sort(@GetField(EditField[ef]); [Descending])" ; "$" );

      DataTypes := @Word( DataTypesCombo ; "|" ; 1 );
      DataTypesAction := @Word( DataTypesCombo ; "|" ; 2 );

      REM {Get a listing of all the fields on the current document};
      List := @Sort( @DocFields );

      REM {Look for last field modified in Profile Doc};
      FieldList := @Explode( @GetProfileField( cProfileName ; cEditLastField ; @UserName ) ; cArraySeparator ; @True ) ;

      REM {Get the list of forms and field that was updated using Edit Document Fields};
      FieldListForms := @Word( FieldList ; cEditLastSeparator ; 1 );
      FieldListField := @Word( FieldList ; cEditLastSeparator ; 2 );
      FieldListLastIndex := @Member( Form; FieldListForms );
      REM {If the FieldListLastIndex is greater than zero then set the last field to the what was in the profile document};
      @If( FieldListLastIndex > 0;
       @Do( LastField := FieldListField[ FieldListLastIndex ];
        FieldList := @ReplaceSubstring( FieldList ; FieldList[ FieldListLastIndex ] ; "" ) );
       LastField :="" );

      REM {Prompt for which field needs to be updated. Loop until a field is selected or 'Cancel' is selected};
      @DoWhile(
       EditField := @Prompt( [OkCancelEditCombo] ; cPromptTitle ; "Select the field you wish to alter or enter a new field to add:" ; LastField ; @Trim( @Unique( List : LastField ) ) );
       EditField = "" );
      EditFieldPromptTitle := "Change '" + EditField + "' in " + cPromptTitle;

      REM {This will allow the retrieval of the data type of the field that was last selected. Data is stored like Form+Field%~%DataType.};
      FormFieldList := @Explode( @GetProfileField( cProfileName ; cEditLastFieldDataType ; @UserName ) ; cArraySeparator ; @True ) ;
      FormFieldListFormField := @Word( FormFieldList ; cEditLastSeparator ; 1 );
      FormFieldListDataType := @Word( FormFieldList ; cEditLastSeparator ; 2 );
      FormFieldListFormulaCode := @ReplaceSubstring( @Word( FormFieldList ; cEditLastSeparator ; 3 ) ; cSemicolonReplace ; ";" );
      FormFieldListIndex := @Member( Form + EditField; FormFieldListFormField );
      @If( FormFieldListIndex > 0;
       @Do( DefaultDataType := FormFieldListDataType[ FormFieldListIndex ];
        FormFieldListFormulaCode := FormFieldListFormulaCode[ FormFieldListIndex ];
        FormFieldList := @ReplaceSubstring( FormFieldList ; FormFieldList[ FormFieldListIndex ] ; "" ) );
       DefaultDataType :="" );

      REM {If there was no data type used for the field on the form the try to determine the data type};
      DefaultDataType :=
       @If( DefaultDataType != "" ;
        DefaultDataType ;
        @If(
         @IsNumber( @GetField( EditField ) ) ;
          @If( @Count( @GetField( EditField ) ) > 1 ;
           "Integer Multi Value" ;
           "Integer" ) ;
         @IsTime( @GetField( EditField ) ) ;
          @If( @Count( @GetField( EditField ) ) > 1 ;
           "Date Mult iValue" ;
           "Date" ) ;
         @If( @Count( @GetField( EditField ) ) > 1 ;
           "Text Multi Value" ;
           "Text" )
         )
       );

      REM {If the data type is a type of error then select the data type of text};
      DefaultDataType := @IfError( DefaultDataType ; "Text" );

      REM {Prompt for which data type you would like the data to be. This needs to be done before value prompt to determine if the Picklist or any prompting needs to be used.};
      DataType := @Prompt( [OkCancelList] ; EditFieldPromptTitle; "Please select the correct data type or action for field: " + EditField + "."; DefaultDataType ; DataTypes );

      REM {The DataTypeAction will contain the formula that will be executed to retrieve the new value};
      DataTypeAction := DataTypesAction[ @Member( DataType ; DataTypes ) ];

      REM {If formula was used on this field then use that instead of the fields value. Format the original value as text because the @Prompt command requires text.};
      OriginalValue := @If( DataType = "Formula" & DefaultDataType = "Formula" & FormFieldListFormulaCode != "" ;
       FormFieldListFormulaCode ;
       @If( @Contains( DefaultDataType ; MultiValue ) ;
        @Implode( @Text( @GetField( EditField ) ) ; cArraySeparator );
        @Text( @GetField( EditField ) ) )
       );

      REM {This will allow the retrieval of history of values of the field. Data is stored like Form+Field+DataType%~%ValueList.};
      FormFieldListDataTypeValues := @Explode( @GetProfileField( cProfileName ; cEditLastFieldDataTypeValue ; @UserName ) ; cArraySeparator ; @True ) ;
      FormFieldListFormFieldDataType := @Word( FormFieldListDataTypeValues ; cEditLastSeparator ; 1 ) ;
      FormFieldListValuesLists := @Word( FormFieldListDataTypeValues ; cEditLastSeparator ; 2 ) ;
      FormFieldListDTIndex := @Member( Form + EditField + DataType; FormFieldListFormFieldDataType );
      @If( FormFieldListDTIndex > 0;
       @Do( FormFieldListValuesList := FormFieldListDataTypeValues[ FormFieldListDTIndex ];
        FormFieldListValuesList := @ReplaceSubstring( @Trim( @Explode( FormFieldListValuesLists[ FormFieldListDTIndex ] ; cValueListSeparator ; @False ) ) ; cSemicolonReplace; ";" );
        FormFieldListDataTypeValues := @ReplaceSubstring( FormFieldListDataTypeValues ; FormFieldListDataTypeValues[ FormFieldListDTIndex ] ; "" ) );
       FormFieldListValuesList :="" );

      REM {Prompt for additional fields and determine the string that they are searching for.};
      @If( DataType = ("Replace Substring":"Replace" ) ;
       @Do( EditField := @Unique( EditField : @Prompt( [OkCancelListMult] ; cPromptTitle ; "Select any addtional fields you wish to alter:" ; EditField ; List ) );
        FromRawValue := @Prompt( [OkCancelEditCombo] ; EditFieldPromptTitle ; "Enter or select the text (case sensitive) to search for in: " + @Implode( EditField ; ", " ) + "." ; "" ; @Unique( @Word( FormFieldListValuesList ; cFromRawValueSeparator ; 2 ) ) ) );
       @Do( EditField := EditField;
        FromRawValue := "" )
       );

      REM { With the Edit combo there will be a list of standard seperators to choose from.};
      Separator := @If( DataType = ("Implode":"Explode" ) ;
       @Prompt( [OkCancelEditCombo] ; cPromptTitle ; "Enter or select the " + @If( DataType = "Implode" ; "separator" ; "separators" ) + ":"  ; "" ; @Unique( @If( FormFieldListValuesList = "" ; cStandardSeparators  ;  FormFieldListValuesList : cStandardSeparators ) ) );
       cArraySeparator );

      REM {Determine the string to search for};
      ExtractValue :@If( DataType = cTextExtractList ;
       @Prompt( [OkCancelEditCombo] ; cPromptTitle ; "Enter or select the search string or string length:" ; @Subset( FormFieldListValuesList ; 1 ) ; @Unique( FormFieldListValuesList ) );
       "" );

      REM {Based on what type of data is being entered different prompts will happen if any at all.};
      RawValue := @If(
       @Contains( DataType ; "Name Multi Value" ) ; @PickList( [Name] );
       @Contains( DataType ; "Name" ) ; @PickList( [Name] : [Single] );
       DataType = ( cNoPromptList ) ; "" ;
       @Contains( DataType ; "Multi Value" ) ; @Prompt( [OkCancelEditCombo] ; EditFieldPromptTitle; "Enter or select the new desired value for: " + @Implode( EditField ; ", " ) + "." + cPromptNewLineTwo + "Seperated with ; for each value." ; OriginalValue ; @Unique( OriginalValue : FormFieldListValuesList ) ) ;
       @Contains( DataType ; "+ Append Values" ) ; @Prompt( [OkCancelEditCombo] ; EditFieldPromptTitle; "Enter or select values to append: " + @Implode( EditField ; ", " ) + "." + cPromptNewLineTwo + "Seperated with ; for each value." ; "" ; @Unique( FormFieldListValuesList ) ) ;
       DataType = ("Replace Substring":"Replace" ) ; @Prompt( [OkCancelEditCombo] ; EditFieldPromptTitle ; "Enter or select the text to repalce with in: " + EditField + "." ; "" ; @Unique( @Word( FormFieldListValuesList ; cFromRawValueSeparator ; 1 ) ) ) ;
       DataType = "Formula" ; @Do( @DoWhile(
        OriginalValue := @Prompt( [OkCancelEditCombo] ; EditFieldPromptTitle ; "Enter or select the new desired formula for: " + EditField + "." ; OriginalValue ; @Unique( OriginalValue : FormFieldListValuesList ) );
        tempReturnCheck := @CheckFormulaSyntax( OriginalValue );
        @If( tempReturnCheck != "1"; @Prompt( [Ok] ; "Invalid Formula - " + EditFieldPromptTitle ;
         "Invalid Formula entered: " +
         cPromptNewLineTwo + cPromptTab + "Error: " + cPromptTab + cPromptTab + @Text( tempReturnCheck ) +
         cPromptNewLineOne + cPromptTab + "Formula: " + cPromptTab +  cPromptTab + OriginalValue ) ; "" );
       tempReturnCheck != "1" );
       OriginalValue );
       @Prompt( [OkCancelEditCombo] ; EditFieldPromptTitle ; "Enter or select the new desired value for: " + EditField + "." ; OriginalValue  ; @Unique( OriginalValue : FormFieldListValuesList ) )
       );

      REM {Store Field in Profile doc};
      @SetProfileField( cProfileName ; cEditLastField ; @Unique( @Trim( FieldList : ( Form + cEditLastSeparator + EditField[1] ) ) ); @UserName );

      REM {Store Data Type of Field in Profile doc};
      @SetProfileField( cProfileName ; cEditLastFieldDataType ;
       @Unique( @Trim( FormFieldList : ( Form + EditField[1] + cEditLastSeparator + DataType + cEditLastSeparator +
        @ReplaceSubstring(
         @If( DataType = "Formula" ; RawValue ; FormFieldListFormulaCode ) ;
         ";" ; cSemicolonReplace ) ) ) ) ;
       @UserName );

      REM {Store Data Value of Field in Profile doc};
      @SetProfileField( cProfileName ; cEditLastFieldDataTypeValue ;
       @Unique( @Trim( FormFieldListDataTypeValues : ( Form + EditField[1] + DataType + cEditLastSeparator +
        @Implode(
         @Subset(
          @Unique(
          @ReplaceSubstring(
           @If( DataType = ("Implode":"Explode" ) ; Separator ;
            DataType = cTextExtractList ; ExtractValue ;
            DataType = ( "Replace Substring":"Replace" ) ; RawValue + cFromRawValueSeparator + FromRawValue ;
            RawValue ) : FormFieldListValuesList  ;
           ";" ; cSemicolonReplace ) ) ;
          cFieldHistoryValues );
         cValueListSeparator ) ) ) ) ;
       @UserName );REM {If multi docs selected, only process those checked ... an unchecked doc cannot be NavNextSelected};
      @Command([NavNextSelected]);
      @UpdateFormulaContext;

      REM {Store all Note IDs before manipulation in case field modifications cause categorized views or sorted columns to reorganize};
      NoteIDList :@Text( @NoteID );
      ErrorNoteIDList := "";
      @Command([NavNextSelected]);
      @UpdateFormulaContext;

      REM {Start Looping Selected documents to gather all the documents that need to be updated.};
      @While( ( @Left( NoteIDList ; cNoteEntryLength ) != ( @Text( @NoteID + cArraySeparator ) ) ) & ( @Length( NoteIDList ) < cMaxSearchForSelectedDocs ) ;
       NoteIDList := NoteIDList + cArraySeparator + @Text( @NoteID );
       NoteIDList := @ReplaceSubstring( NoteIDList ; cCategoryNoteID + cArraySeparator ; "" );
       @Command([NavNextSelected]);
       @UpdateFormulaContext
      );
      REM {Remove all category Note IDs};
      NoteIDList :@ReplaceSubstring( NoteIDList ; cCategoryNoteID ; "" );
      REM {Remove all duplicate Note IDs};
      NoteIDList :@Unique( @Explode( NoteIDList ; cArraySeparator ; @False ) );
      @StatusBar( "Found " + @Text( @Elements( NoteIDList ) ) + " documents." );
      NotNoteIDList := "";

      REM {Determine if the document should be updated.};
      tmpPrompt := @Implode( "The following information will be used to update the " + @Text( @Elements( NoteIDList ) ) + " document" + @If( @Elements( NoteIDList ) > 1 ; "s" ; "" ) + "." + cPromptTab + cPromptTab +
       cPromptNewLineTwo + cPromptTab + "Field:" + cPromptTab + cPromptTab + EditField +
       cPromptNewLineOne + cPromptTab + "Data type/action: " + cPromptTab + DataType +
       cPromptNewLineOne + cPromptTab +
        @If(  DataType = ("Implode":"Explode" ) ; "Separator:  " + cPromptTab + Separator ;
          DataType = ("Text Left":"Text Left Back":"Text Right":"Text Right Back" ) ; "Search string:  " + cPromptTab + ExtractValue ;
          DataType = ("Replace Substring":"Replace" ) ; "Search string:  " + cPromptTab + FromRawValue + cPromptNewLineOne + cPromptTab + "Replace string:  " + cPromptTab + RawValue ;
          DataType = cNoPromptList;
          "" ;
          "Value:  " + cPromptTab + cPromptTab + @Text( RawValue ) ) +
       cPromptNewLineTwo + "Would you like to continue?" );
      @If( cEnableConfirmation ;
       @Do(
         @StatusBar( @Explode( tmpPrompt ; cPromptNewLineOne ; @True ) );
        @If( @Prompt( [YesNo]; EditFieldPromptTitle ;
         tmpPrompt );
         "" ; @Return ( "" ) ) );
       "" );

      REM {Loop through selected docs taking each NoteIDList out of the list as it is processed};
      DocUpdateCount := 0;
      DocNavigationCount := 0;
      @While( DocUpdateCount < @Elements( NoteIDList ) ;

       @If( @TextToNumber( @Text( @DocumentUniqueID ) ) != 0 ;
        @Do(
         NoteIDList := @Replace( NoteIDList ; @NoteID ; "" ) ;
         NotNoteIDList := NotNoteIDList : @NoteID;
         @For( ef := 1; ef <= @Elements( EditField ); ef := ef + 1;
          formulaResult := @Eval( DataTypeAction );
      REMark := " **REM** The values entered above will be applied to all selected doc. If data conversion doesn't work then don't set field.";
          @If( @IsError( formulaResult );
           @Do(
            tmpPrompt := "Error with NoteID of " + @NoteID + ". Continue?" + cPromptTab +
            cPromptNewLineOne + cPromptTab + "Error: " + cPromptTab + cPromptTab + @Text( FormulaResult ) +
            cPromptNewLineOne + cPromptTab + "Formula: " + cPromptTab +  cPromptTab + DataTypeAction +
            cPromptNewLineOne + cPromptTab + @If( @Contains( DataTypeAction ; "EditField[ef]" ) ; "EditField[ef]:" ; "Field:" + cPromptTab ) + cPromptTab + EditField[ef] +
            cPromptNewLineOne + cPromptTab + "Data type/action: " + cPromptTab + DataType +
            cPromptNewLineOne + cPromptTab +
            @If(  DataType = ("Replace Substring":"Replace" ) ; "FromRawValue:  " + cPromptTab + @Text( FromRawValue )+ cPromptNewLineOne + cPromptTab + "RawValue:  " + cPromptTab + @Text( RawValue ) ;
              DataType = cNoPromptList;
              "" ;
              "RawValue:  " + cPromptTab + @Text( RawValue ) );
           @StatusBar( @Explode( tmpPrompt ; cPromptNewLineOne ; @True ) );
           @If( @Prompt( [YesNo] ;"Error - " + EditFieldPromptTitle ; tmpPrompt ) ;
             ErrorNoteIDList := ErrorNoteIDList+ cArraySeparator + @Text( @NoteID );
            @Return( @If( @Eval( cErrorCheckCode ) != "" ;
             @StatusBar( @Explode( @Eval( cErrorInformation ) ; cPromptNewLineOne ; @True ) ):
             @Prompt( [Ok] ; "Unable to Update - " + EditFieldPromptTitle ; @Eval( cErrorInformation ) );
            "" ) ) ) );
          @SetField( EditField[ef] ; formulaResult )
          )
         );
         @If( DocNavigationCount > cMaxUpdatedDocuments ;
           NoteIDList := "";
          @Do(
           DocUpdateCount := DocUpdateCount + 1;
           @Command([NavNextSelected]);
           @UpdateFormulaContext;
      REMark := " **REM** If we haven't processed all docs yet but the current doc is not in the NoteIDList list, keep looping ... if cnt exceeds MaxUpdatedDocuments assume infinite loop and stop ";
           @If( DocUpdateCount < @Elements( NoteIDList ) & ( !@Member( @NoteID ; NoteIDList ) ) & ( !@Member( @NoteID ; NotNoteIDList) );
            @While( (! @Member( @NoteID ; NoteIDList ) & DocNavigationCount < cMaxUpdatedDocuments );
             @Command([NavNextSelected]);
             @UpdateFormulaContext;
             DocNavigationCount := DocNavigationCount + 1);
           "")
          )
         )
        );
       @Do( @Command([NavNextSelected]);
        @UpdateFormulaContext )
       )
      );

      @If( @Implode( @Unique( @Explode( NoteIDList : ErrorNoteIDList ; cArraySeparator ; @False ) ) ) != "" ;
       @StatusBar( @Explode( @Eval( cErrorInformation ) ; cPromptNewLineOne ; @True ) ):
       @Prompt( [Ok] ; "Unable to Update - " + EditFieldPromptTitle ; @Eval( cErrorInformation ) );
       "" );

      @StatusBar( "Navigated through " + @Text( DocUpdateCount + DocNavigationCount ) + " documents." );
      @StatusBar( "Performed '" + DataType + "' for '" + @Implode( EditField ; ", " ) + "' field" + @If( @Elements( EditField ) > 1; "s " ; " " ) + "on " + @Text( DocUpdateCount ) + " document" + @If( DocUpdateCount > 1 ; "s" ; "" ) + "." )


        Ralf Petter’s blog about Notes, Eclipse and Expeditor back online

        Karsten Lehmann  December 20 2021 02:06:16 PM
        Yesterday I noticed that Ralf Petter's blog "Everything about IT" is not online anymore. Ralf died years ago from cancer and his blog contains invaluable tips around Notes, Eclipse and Expeditor development.

        In June 2017 when I heard about his death, I downloaded all blog articles and kept them on my local disk.

        Now that the official blog is gone, I uploaded them to our server, added them to the Google index and made them available here:

        https://ralfpetter-blog-mirror.mindoo.de

        Unfortunately I did not create copies of his free tools and they are not on OpenNTF or Github.

        I could find the ZIP archive with the latest DocumentSpy plugin in the Wayback machine and fixed the download link in this article:

        https://ralfpetter-blog-mirror.mindoo.de/www.everythingaboutit.eu/p/install-document-spy.html

        If anybody has a copy of these three tools, please send them to me:

        https://ralfpetter-blog-mirror.mindoo.de/www.everythingaboutit.eu/p/install-extension-spy.html

        https://ralfpetter-blog-mirror.mindoo.de/www.everythingaboutit.eu/p/install-todoactivator.html

        https://ralfpetter-blog-mirror.mindoo.de/www.everythingaboutit.eu/p/install-shortcutbuttonbar-enabler.html

          Mindoo CMS on Domino: Some examples

          Karsten Lehmann  July 27 2021 07:42:43 AM
          Mindoo CMS is a Domino powered web content management system that we developed as part of a large project for a local print shop to build a web authoring system for the articles of their local newspapers with approval workflow, custom spellchecker with dictionary stored in Domino (based on http://hunspell.github.io/) and various data exports (e.g. responsive UI and EPUB for subscribers, QuarkXPress and Adobe InDesign for printing machines).

          The CMS does not just display the final articles, it's driving the whole web application to compose and approve articles as well.

          Here is a list of the main features:
          • responsive user interface for desktop and mobile browsers
          • live editing of websites: as logged in user, just shift-click on any test in the live website and edit it or add content
          • full support of the Domino access model, e.g. to built corporate intranets with different layout/content for target groups
          • very flexible templating system based on custom Handlebars tags
          • no developer skills needed for authors
          • compose web site pages out of building blocks predefined by the template editor with unrestricted complexity
            (e.g. add base page layout, sections for text/images, gallery block and gallery images with meta data; data model is 100% defined within the CMS)
          • automatic history of all CMS pages, files and templates: compare current state with any previous versions and revert back changed as needed
          • implemented/deployed as OSGi plugins on Domino
          • extensible to add custom CMS tags via OSGi extension points

          Here are some references of web sites that we developed and that are powered by Mindoo CMS:


            News Update from Mindoo Land: Domino JNA / JNX

            Karsten Lehmann  July 26 2021 01:15:43 PM
            In a pandemic (in particular with two young kids of 5 and 7 years), times flies by even faster than in regular times, so it's no surprise that this blog has not seen many updates for a very long time.

            Just wanted to let you know that we are still very busy, leveraging and improving the Domino AppDev platform.

            The biggest project I have been working on in 2020 and so far in 2021 has to do with our Domino JNA codebase. For the project itself, we released 11 new versions since the beginning of 2020.

            We added functionality that we needed for our own development projects and the project got additional traction because it was used by the HCL Innovation Labs initially for Project Keep, now called HCL Domino REST API and in public beta for a few week.

            After the first few internal Keep versions, the HCL Innovation Labs team decided that having a modern Java API for Domino would not just be relevant for their own projects but for every developer out there and that this should (hopefully) become the new standard for Java development on Domino.

            That's why we teamed up with Jesse Gallagher to build something even better than Domino JNA - called Domino JNX.

            Let's call it Domino JNA on steroids. We took the codebase of Domino JNA and built a powerful API that is both easy to use and has even more features than Domino JNA.

            Some examples:
            • automatic garbage collection for Domino C handles (based on Java's PhantomReference)
            • leverages modern Java language features like Generics, lambda expressions, the Stream API and Optional's
            • more APIs, e.g. to read the database design and current efforts to provide write access as well
            • JSON serialization support
            • extensible, e.g. to store your own objects in Domino documents

            JNX is the foundation for the HCL Domino REST API which itself is the foundation for other cool projects like the OpenClient initiative that will provide a native EWS/JMAP API for Domino and therefore native access for Microsoft Outlook, the Apple Mail client and others.

            There are ongoing efforts to give you, the Domino developer community, access to the JNX API (not just as part of the existing public beta release of the Domino REST API).

            So stay stuned for upcoming announcements! :-)

              OpenNTF May 2021 Webinar: recent Mindoo ToDoManager changes for Notes 12

              Karsten Lehmann  May 20 2021 09:10:19 PM
              Today I took part in OpenNTF's May webinar on recent project updates. I presented the results of my efforts to leverage new functionality of the upcoming Notes 12 client to make our Mindoo ToDoManager application more responsive and adaptable to the available screen real estate.

              The application now autodetects the size of the Notes/Nomad client window on startup and picks the best application layout: you either see all four quadrants

              Image:OpenNTF May 2021 Webinar: recent Mindoo ToDoManager changes for Notes 12

              or the mobile optimized narrow layout with a single quadrant at a time:

              Image:OpenNTF May 2021 Webinar: recent Mindoo ToDoManager changes for Notes 12


              Notes 12 provides two new @formulas @ResolutionWidth / @ResolutionHeight to read the dimension of the current UI form or UI view.
              And you get a new form/view event "onsize" that gets triggered on window resize. Enter the formula @Command([RelayoutWindow]) here and your layout gets recomputed when the window is resized by the user, e.g. hide/when or subform formulas that depend on the form dimension might produce a different output.

              To be honest I was fighting a bit to get this new functionality to work reliable in the latest R12 beta 3 client. The TodoManager proves that responsive layouts for Notes applications are indeed possible.

              But I reported a bunch of issues in the beta program that hopefully get fixed until the final R12 release to make this easier to use.
              E.g. my Designer installation does not save the code that I enter in the "onsize" event. And the resolution formulas often seem to be computed too early, e.g. you still get UI view dimensions back after you have opened a doc in a view.

              While I was updating the OpenNTF project download with the latest version, I also added some documentation and references on the project summary page. I highly recommend watching the two videos that are linked in the text if you haven't done already. They were recorded in 2007, but are still very inspiring.

              Anyway, I hope you like the new version. Here is the updated project description:


              Four Quadrant Theory

              The TodoManager is based on Covey's Four Quadrant theory that is described in this article:
              Time management strategies for busy people using the 4-quadrant method
              It provides a graphical concept of todo management where you place your todos in a 2x2matrix of four quadrants. By doing that you decide whether your topic is important/ not important and due soon/ not due soon.
              Following the order of the quadrants is key to organizing your life:
              Start with quadrant 1 (important/ due soon), continue with 2 (important/ not due soon), 3 (not important/ due soon) and end with todos in quadrant 4 (not important/ not due soon). In each quadrant, pick the ugliest task first.
              I got to know Covey's theory when I was watching Randy Pausch's Last Lecture and his Lecture about Time Management. Both recordings are highly recommended to put your daily struggles and project deadlines into perspective:
              Carnegie Mellon Professor Randy Pausch (Oct. 23, 1960 - July 25, 2008) gave his last lecture at the university Sept. 18, 2007, before a packed McConomy Auditorium, months before he died from pancreatic cancer and reflects on how much he achieved his childhood dreams.

              Usage

              Use the “+” icon to create new todos and decide in which quadrant (=Notes folder) it should be moved on first save.
              Use drag and drop or context menu actions to move your tasks between quadrants (numbers “1”, “2”, “3”, “4” are drop targets) or into the recycle bin.

              Responsive UI

              The TodoManager application has a responsive user interface that works on desktop Notes Client, HCL Nomad clients on tablets/phones and even the new Nomad web client. By providing two different application layouts it adapts to the available screen real estate: one layout that displays all 4 quadrants at once and a second one where you only see a single quadrant and have hotspots to change quadrants in the top navigation.
              On Notes 12 we leverage new @formulas to query the current window dimension while still being backward compatible with older Notes Client versions. That means that even in the desktop Notes Client you get a different user experience when the client window width is not very wide. The window size is detected on application startup.

              Installation

              Just copy the provided application template to your server, sign it and create a new database instance. Then give yourself editor access in the ACL and all others no access.
              There are currently no background agents to enable.

              Mindoo TodoManager on OpenNTF

              Karsten Lehmann  December 14 2019 09:30:14 AM
              I created a small sample application for the HCL Nomad Client on the iPad and iPhone. It's a TodoManager that uses the 4 Quadrant time management method to structure the Todos.

              The application is now available for free on OpenNTF: Mindoo TodoManager

              In the desktop Notes Client, to dos can be moved between the quadrants and to the recycle bin via drag and drop ("1" - "4" and the recycle bin icon are drop targets).
              In HCL Nomad, just do a long click on a document to see the available options.

              The UI is responsive and should fit on all devices.

              Image:Mindoo TodoManager on OpenNTF

              Image:Mindoo TodoManager on OpenNTF

              Image:Mindoo TodoManager on OpenNTF

              Image:Mindoo TodoManager on OpenNTF

              Image:Mindoo TodoManager on OpenNTF


                Documentation for @GetMachineInfo

                Karsten Lehmann  December 4 2019 02:47:07 PM
                Since there currently is no place to find this information on HCL websites in Google, I am adding it here:


                @GetMachineInfo new in 8.5.3

                New @Function to support Machine Specific Policy Settings

                Syntax:

                @GetMachineInfo( [Keyword]; "Needed for some Keywords string" )

                Keywords:

                IsLaptop - boolean return - True if machine is a laptop, otherwise false

                IsDesktop - boolean return - True if machine is NOT a laptop, otherwise false

                IsSingleLogOn - boolean return - True if machine has Notes client installed with "single sign on", otherwise false

                IsMultiUser - boolean return - True if machine has Notes client installed as Multi-User, otherwise false

                HasDesigner - boolean return - True if machine has Designer client installed, otherwise false

                HasAdmin - boolean return - True if machine has Admin client installed, otherwise false

                IsStandard - boolean return - True if machine is running Standard Notes client, otherwise false

                MachineName - string return - Name of the machine
                boolean return        True if MachineName string after keyword matches this machine's MachineName, otherwise false

                Memory - number return - Total amount of memory (RAM)

                DiskSpace - number return - Amount of free disk space
                Note: With this keyword, you can add a second parameter for the drive to scan for free space. If this parameter is not passed
                to the function, free space for the first (logical) drive - for example, drive C on Windows system - is displayed.
                Example: @GetMachineInfo([DiskSpace];"d:")

                EnvVariable - string return - Requires string of the variable name in Notes.ini to read, and returns the value of that ini variable or "" (null string) if not found

                SysEnvVariable - string return - Requires string of the variable name in system environment to read, and returns the value of that variable or "" (null string) if not
                found

                IP - string/list return - String representation of the IP address(es) in the form XXX.XXX.XXX.XXX , otherwise "" (null string) if not available
                boolean return        True if pattern IP string after keyword matches this machine's IP address, otherwise false

                MAC - string/list return - String representation of the MAC address(es) in the form XX:XX:XX:XX:XX:XX , otherwise "" (null string) if not available
                boolean return        True if MAC string after keyword matches this machine's MAC address, otherwise false

                Configure Eclipse 4.6.x with HCL Notes 10

                Karsten Lehmann  November 4 2019 04:06:07 PM
                For years, whenever I needed to configure an Eclipse IDE for IBM Notes plugin development, I used Mikkel Heisterberg's instructions on his blog.

                Since I currently have a customer requirement to update an existing plugin that we built for them years ago to a newer Java version (1.8), I tried to set up Eclipse 2019-09 with HCL Notes 10.0.1FP2, but failed.
                The launching Client did not show any file/edit/view menus, probably caused by a ClassCastException I could see in the launch console.

                I guess the reason is that 9.0.1 FP10 brought a major update for the underlying Eclipse and OSGi platform.

                So I contacted HCL development and asked for the updated parameters to make this work again and quickly got a response that I would like to share with you.

                The document links to Eclipse Neon 4.6.3, but it's still working for me in Eclipse 2019-09 (4.13.0).


                1. Download Eclipse Neon (4.6.3) or later from link below:
                http://www.eclipse.org/downloads/download.php?file=/technology/epp/downloads/release/neon/3/eclipse-committers-neon-3-win32-x86_64.zip

                2. Get the latest V10 build

                3. Go to Windows => Preferences => Java => Installed JREs
                • Add => Standard VM =>
                • JRE home: [Notes Install path]\jvm, e.g. C:\Program Files (x86)\IBM\Notes\jvm
                • JRE name: “Notes JRE”
                • Select the JRE to point to this and Apply

                4. Go to Windows => Preferences => Plug-in Development => Target Platform
                • Add => default => Target Content
                • Name: “Notes Target”

                4a. In Locations Tab:
                • Add => Directory => Location => [Notes Install path]\framework\rcp\eclipse\plugins
                • Add => Directory => Location => [Notes Install path]\framework\shared\eclipse\plugins
                • Finish
                • Select the Target platform to point to “Notes Target” and Apply

                4 b. In Environment Tab:
                Please select following settings:
                • Operating System: win32
                • Windowing System: win32
                • Architecture: x86
                • Locale: en_US - English (United States)

                5. Go to Windows => Preferences => Run/Debug => String Substitution
                New => Add 2 strings (replace "xxxx" with the right plugin version!)

                5a. Name: rcp_home
                Value: [Notes Install path]\framework

                5b. Name: rcp_base
                Value: [Notes Install path]\framework\rcp\eclipse\plugins\com.ibm.rcp.base_10.0.0.xxxx

                OK
                Close this Preferences Window

                6. Put the following file inside the below plugin:
                [Notes Install path]\framework\rcp\eclipse\plugins\com.ibm.rcp.base_10.0.0.xxxx

                rcp.security.properties


                7. Open Debug Configurations in the ‘Debug Perspective’ => Eclipse Configuration => New
                7a. In the Main tab:

                Name: NotesDebug
                Program to Run =>  Run a product => com.ibm.notes.branding.notes
                Java Runtime Environment => Runtime JRE => Select “Notes JRE” i.e. the one we added in step 3

                7b. In the Arguments tab:

                Program Arguments:
                -clean -console -debug -log -personality com.ibm.rcp.platform.personality -config notes

                VM Arguments:
                -Xquickstart
                -Xss384k
                -Xshareclasses
                -Drcp_home="${rcp_home}"
                -Drcp.install.config=user
                -Dosgi.install.area="${rcp_home}\eclipse"
                -Disa.ignoreESR=true
                -Dcom.ibm.pvc.osgiagent.core.logfileloc="${rcp_home}\rcp"
                -Dcom.ibm.pvc.webcontainer.port=0
                -Declipse.pluginCustomization="${rcp_home}\rcp\plugin_customization.ini"
                -Djava.security.properties="${rcp_base}\rcp.security.properties"
                -Declipse.registry.nulltoken=true
                -Djava.protocol.handler.pkgs=com.ibm.net.ssl.www.protocol
                -Djava.util.logging.config.class=com.ibm.rcp.core.internal.logger.boot.LoggerConfig
                -Dosgi.hook.configurators.exclude=org.eclipse.core.runtime.internal.adaptor.EclipseLogHook
                -Dosgi.framework.extensions=com.ibm.rcp.core.logger.frameworkhook,com.ibm.rds,com.ibm.cds
                -Xbootclasspath/a:${rcp_base}\rcpbootcp.jar;


                8. Apply => Debug – This will launch your Notes in Debug mode.
                Ensure that whenever you launch the Notes from Eclipse, there should be no other instance of Notes already running. If so, please close Notes and then launch from Eclipse to debug.

                New Domino JNA version available with LOTS of new features

                Karsten Lehmann  September 19 2019 05:23:43 PM
                It's been a long time since the last release of our Domino JNA project. Version 0.9.21 is now available for download as XPages Extensibility Plugin and on its way to Maven Central (takes a few hours to appear).

                Here are a some highlights of the new version:

                • New method to read item definition table of a database
                • Added function to get the template db path by the template name
                • Support for profile note reading and writing
                • Added API to export mails as EML files on client and server
                • Added API to read client and server statistics
                • Tested in Notes/Domino 11 beta 1
                • Added method to harvest DB design for DQL speedup
                • DQL query builder updated for new Domino 11 "contains" terms (FT search)
                • New method to send server console commands
                • Improved richtext to HTML conversion sample to extract files as well
                • Added convenience functions NotesDatabase.toUnid / toNoteId
                • Added NotesDatabase.getAllCollections() to read infos about all views
                • NotesTimeDate.toString method with date/time/timezone formatting options
                • Added NotesIntlFormat argument to NotesTimeDate.fromString
                • Added setter to change timezone of NotesTimeDate
                • New methods to read ACL entries and all ACL roles
                • New methods in NotesCollection to read view design properties, e.g. .isConflict(), .isCollapsed(), isGotoTopOnOpen() etc.
                • Added method to check if $file item belongs to a TYPE_MIME_PART item
                • Added method NotesItem.copyToNote() with parameter to rename the item
                • Added check method to large summary buffer support on DB
                • Added checks for ERR 1028 when openings docs to return null if not found
                • Improved reading of data below view categories
                • Added method to convert RFC822 items to native Domino format
                • NotesDatabase.openMailDatabase(), hasFullAccess() and runWithDbLock()
                • Changed flag to find view in design, did not find all available views
                • Flags to update note without triggering replication (used by replicator)
                • Added experimental code to set sequence number and time
                • Added more HTML conversion options
                • More FTSearch options for NotesDatabase searches (e.g. get result sorted by created/modified/score)
                • Additional methods to write Notes.ini variables (e.g. OSGI_HTTP_DYNAMIC_BUNDLES)
                • Added getter to read TIMEDATE value from Notes.ini with testcase
                • NotesCollection column title/name no longer converted to lowercase
                • Removed ICU4J dependency, now using undocumented method OSTranslate32 to translate between LMBCS and Java strings with support for long texts (length > WORD size that OSTranslate supports)
                • Performance optimizations, fixed memory handle leaks

                Advanced view lookup strategies with Domino JNA for small view index sizes and dynamic filtering and sorting

                Karsten Lehmann  March 13 2019 12:09:50 AM
                This might be interesting for some of you, a pattern how I am using Domino JNA in a recent customer project to speed up view lookups and reduce overall view index size.

                I am using at least three views to produce the content for a data table in the web application:
                • one or more key lookup views
                • one view for the sorting
                • and the final one to read all required view columns.

                1. Key lookup views
                The key lookup views has the minimum required columns for the lookup, e.g. just the sorted columns containing the lookup key(s) and I use NotesCollection.getAllIdsByKey(EnumSet findFlags, Object... keys) to collect the note ids of all documents matching my lookup criteria(s) and the com.mindoo.domino.jna.utils.SetUtil class to AND/OR multiple note id sets.

                The first view might even contain less lookup columns (e.g. just the default index position column) if I am just interested in all documents that match the view selection formula. In that case I am using the view as a stored database search result.

                You can read the note ids of all documents in the view with NotesCollection.getAllIds(Navigate navigator, boolean filterTable, NotesIDTable idTable), which is EXTREMELY fast with parameters navigator=Navigate.NEXT and filterTable=false, as long as the flag "show response hierarchy" is NOT set in the view design. Then NIF just copies an internal index into the IDTable and is done (=>no b-tree traversion and reader list check for the current user).

                Unfortunately, AFAIK, "show response hierarchy" is set by default when you create new views. This lets NIF use a secondary index to search for responses for all view rows, which is slow.

                I first thought that this response hierarchy flag would be required to find conflict documents in the view (as they are response documents), but this is not the case. My tests have shown that this information is still there, although there was a "bug" in Domino JNA which declared each row as conflict because I was reading the conflict flag as it is documented in the C API toolkit.
                I fixed that recently with a special case for "show response hierarchy"==false (details: https://github.com/klehmann/domino-jna/commit/cdfbc6f8e3087eed1eb8328341451f4f0ffbc7dd).

                Additional note id sets could be retrieved from fulltext searches (NotesDatabase.ftSearch(String query, short limit, NotesIDTable filterIDTable)).

                With Domino 10, a DQL search could be used to collect the relevant note ids as well, in Domino JNA: NotesDatabase.query(DQLTerm query, EnumSet flags, int maxDocsScanned, int maxEntriesScanned, int maxMsecs).

                2. Sort view
                The sort view may be identical to one of the key lookup views. I am using this view to find all note ids of my key lookups that are visible in the requested page in the web datatable (=> offset / count received from the browser) and get them returned in view sorting.

                I call NotesCollection.select(Collection noteIds, boolean clearPrevSelection) with clearPrevSelection=true to select all relevant note ids in the view and then call
                NotesCollection.getAllEntries(final String startPosStr, int skipCount, EnumSet returnNav,int preloadEntryCount, EnumSet returnMask, ViewLookupCallback callback) with the following parameters:
                • startPosStr "0" => start at the beginning of the view
                • skipCount = offset+1 =>skip rows based on paging parameters received from browser, "+1" to go from row "0" (which is one row above the first row) to the first
                • returnNav = EnumSet.of(Navigate.NEXT_SELECTED)        => only return previously selected rows
                • preloadEntryCount        => count parameter from browser
                • returnMask = EnumSet.of(ReadMask.NOTEID)        => just read the note ids (in view sorting)
                • callback = a ViewLookupCallback implementation similar to the one I am using for the getAllIds methods internally (https://github.com/klehmann/domino-jna/blob/master/domino-jna/src/main/java/com/mindoo/domino/jna/NotesCollection.java#L996) which returns a LinkedHashSet with the note ids in view sorting, but stops after "count" entries.

                3. Data lookup view
                This "masterdata" view contains all the view columns required to fill the web datatable columns. It only has one fixed sorting (here: by creation date) and no resortable columns.
                As discussed before, "show response hierarchy" is not set here as well.

                Once again I call NotesCollection.select(Collection noteIds, boolean clearPrevSelection) with clearPrevSelection=true to set the view selection, but in this case I select just the note ids read from the sort view in step 2.
                So I read the data columns for all rows in the visible web datatable page, but as they are returned in the wrong sorting (creation date), I need to hash them by note id and reorder them based on the sort view sorting (not a problem, because my page size of 400 entries is quite small).

                To read the view rows, I use NotesCollection.getAllEntries(final String startPosStr, int skipCount, EnumSet returnNav,int preloadEntryCount, EnumSet returnMask, ViewLookupCallback callback) as in step 2, but this time I want to read the UNID and column values for the rows as well:
                • returnMask = EnumSet.of(ReadMask.NOTEID, ReadMask.SUMMARYVALUES, ReadMask.NOTEUNID)
                and get a List returned by the callback:
                • callback = new NotesCollection.EntriesAsListCallback(count)

                Benefits
                Sounds difficult? Yes, I agree, this still needs some convenience methods to make the code more readable.

                The benefit is that I only have one stable view with all the data, resulting in a large view index size and slow initial index time (here 800 MB for 300.000 documents).

                The key lookup and sort views are very small and fast to build (e.g. 60 MB each). So adding another lookup criteria or result sorting later is cheap, which would not be the case if I had to touch my data lookup view and add another resortable view column.

                And even though I collect data from three views, the lookup performance is very fast, almost instant.