diff --git a/confirm.html b/confirm.html new file mode 100644 index 00000000..138c8fa2 --- /dev/null +++ b/confirm.html @@ -0,0 +1,25 @@ + \ No newline at end of file diff --git a/data/controller.xql b/data/controller.xql index 030fe722..49504299 100644 --- a/data/controller.xql +++ b/data/controller.xql @@ -1,11 +1,15 @@ -xquery version "3.0"; +xquery version "3.1"; declare namespace exist="http://exist.sourceforge.net/NS/exist"; declare namespace request="http://exist-db.org/xquery/request"; +declare namespace session="http://exist-db.org/xquery/session"; declare namespace response="http://exist-db.org/xquery/response"; declare namespace transform="http://exist-db.org/xquery/transform"; - +declare namespace output="http://www.w3.org/2010/xslt-xquery-serialization"; +declare namespace sm="http://exist-db.org/xquery/securitymanager"; import module namespace config="https://github.com/edirom/mermeid/config" at "../modules/config.xqm"; +import module namespace crud="https://github.com/edirom/mermeid/crud" at "../modules/crud.xqm"; +import module namespace common="https://github.com/edirom/mermeid/common" at "../modules/common.xqm"; import module namespace xmldb="http://exist-db.org/xquery/xmldb"; import module namespace console="http://exist-db.org/xquery/console"; @@ -16,6 +20,44 @@ declare variable $exist:controller external; declare variable $exist:prefix external; declare variable $exist:root external; +(:~ + : Wrapper function for outputting JSON + : + : @param $response-body the response body + : @param $response-code the response status code + :) +declare %private function local:stream-json($response-body, $response-code as xs:integer) as empty-sequence() { + response:set-status-code($response-code), + response:stream( + serialize($response-body, + + json + + ), + 'method=text media-type=application/json encoding=utf-8' + ) +}; + +(:~ + : Wrapper function for redirecting to the main page + :) +declare %private function local:redirect-to-main-page() as element(exist:dispatch) { + + + +}; + +(:~ + : Check whether the overwrite flag is set + : Considered positive (string) values include '1', 'yes', 'ja', 'y', 'on', 'true', 'true()' + : everything else will be `false()` + : @return boolean true() or false() + :) +declare %private function local:overwrite() as xs:boolean { + let $overwriteString := request:get-parameter('overwrite', 'false') + return $overwriteString = ('1', 'yes', 'ja', 'y', 'on', 'true', 'true()') (: some string values that are considered boolean "true()" :) +}; + (console:log('/data Controller'), if (ends-with($exist:resource, ".xml")) then (console:log('/data Controller: XML data session: '||session:exists()), @@ -63,6 +105,103 @@ if (ends-with($exist:resource, ".xml")) then } default return (response:set-status-code(405), <_/>) ) +(:~ + : copy files endpoint + : For POST requests with Accept header 'application/json' the response + : of the crud operation (a map object) is returned as JSON, for all other + : Accept headers the client is redirected to the main page (after the + : execution of the crud operation) + :) +else if($exist:path = '/copy' and request:get-method() eq 'POST') then + let $source := request:get-parameter('source', '') + let $target := request:get-parameter('target', util:uuid() || '.xml') (: generate a unique filename if none is provided :) + let $title := request:get-parameter('title', ()) (: empty titles will get passed on and filled in later :) + let $overwrite := local:overwrite() + let $backend-response := crud:copy($source, $target, $overwrite, $title) + return + if(request:get-header('Accept') eq 'application/json') + then local:stream-json(map:remove($backend-response, 'document-node'), $backend-response?code) + else local:redirect-to-main-page() +(:~ + : delete files endpoint + : For POST requests with Accept header 'application/json' the response + : of the crud operation (an array) is returned as JSON, for all other + : Accept headers the client is redirected to the main page (after the + : execution of the crud operation) + :) +else if($exist:path = '/delete' and request:get-method() eq 'POST') then + let $filename := request:get-parameter('filename', '') + let $backend-response := crud:delete($filename) + return + if(request:get-header('Accept') eq 'application/json') + then local:stream-json($backend-response, $backend-response(1)?code) + else local:redirect-to-main-page() +(:~ + : read files endpoint + : For GET requests with Accept header 'application/json' the response + : of the crud operation (a map object) is returned as JSON, for an + : "application/xml" the raw XML document is returned + :) +else if($exist:path = '/read' and request:get-method() eq 'GET') then + let $filename := request:get-parameter('filename', '') + let $backend-response := crud:read($filename) + return + if(request:get-header('Accept') eq 'application/json') + then local:stream-json(map:remove($backend-response, 'document-node'), $backend-response?code) + else if(contains(request:get-header('Accept'), 'application/xml')) + then $backend-response?document-node + else () +(:~ + : rename files endpoint + : this simply chains a "copy" and a "delete" (if the first operation was successfull) + : the returned object is a merge of the copy-response and the delete-response with a precedence for the former + :) +else if($exist:path = '/rename' and request:get-method() eq 'POST') then + let $source := request:get-parameter('source', '') + let $target := request:get-parameter('target', util:uuid() || '.xml') (: generate a unique filename if none is provided :) + let $title := request:get-parameter('title', ()) (: empty titles will get passed on and filled in later :) + let $overwrite := local:overwrite() + let $backend-response-copy := crud:copy($source, $target, $overwrite, $title) + let $update-references := + if($backend-response-copy instance of map(*) and $backend-response-copy?code = 200) + then common:update-targets(collection($config:data-root), $source, $target, false()) + else console:log($backend-response-copy) + let $backend-response-delete := + if($update-references instance of map(*) and $update-references?replacements ge 0) + then crud:delete($source) + else console:log($update-references) + return + if(request:get-header('Accept') eq 'application/json') + then if($backend-response-delete instance of array(*)) + then local:stream-json(map:remove(map:merge(($backend-response-delete(1), $backend-response-copy)), 'document-node'), $backend-response-copy?code) + else ( + local:stream-json($backend-response-copy, $backend-response-copy?code), + console:log($backend-response-delete) + ) + else local:redirect-to-main-page() +(:~ + : create files endpoint + : + :) +else if($exist:path = '/create' and request:get-method() eq 'POST') then + let $templatepath := request:get-parameter('template', $config:app-root || '/forms/model/new_file.xml') + let $title := request:get-parameter('title', '') + let $username := common:get-current-username() => string() + let $change-message := 'file created with MerMEId' + let $template := + if(doc-available($templatepath)) + then doc($templatepath) => common:set-mei-title-in-memory($title) => common:add-change-entry-to-revisionDesc-in-memory($username, $change-message) + else () + let $filename := request:get-parameter('filename', common:mermeid-id('file') || '.xml') + let $overwrite := local:overwrite() + let $backend-response := + if($template and $filename) + then crud:create($template, $filename, $overwrite) + else () + return + if(request:get-header('Accept') eq 'application/json' and $backend-response instance of map(*)) + then local:stream-json(map:remove($backend-response, 'document-node'), $backend-response?code) + else local:redirect-to-main-page() else (: everything else is passed through :) (console:log('/data Controller: passthrough'), diff --git a/modules/common.xqm b/modules/common.xqm new file mode 100644 index 00000000..8d28fbd9 --- /dev/null +++ b/modules/common.xqm @@ -0,0 +1,300 @@ +xquery version "3.1"; + +(:~ + : Common MerMEId XQuery functions + :) +module namespace common="https://github.com/edirom/mermeid/common"; + +declare namespace mei="http://www.music-encoding.org/ns/mei"; +declare namespace map="http://www.w3.org/2005/xpath-functions/map"; +declare namespace err="http://www.w3.org/2005/xqt-errors"; +declare namespace util="http://exist-db.org/xquery/util"; + +import module namespace config="https://github.com/edirom/mermeid/config" at "config.xqm"; +import module namespace functx="http://www.functx.com"; + +(:~ + : Function for outputting the "Year" information on the main list page + : + : @param $doc the MEI document to extract the information from + : @return the string representation of a period of time + :) +declare function common:display-date($doc as node()?) as xs:string { + if($doc//mei:workList/mei:work/mei:creation/mei:date/(@notbefore|@notafter|@startdate|@enddate)!='') then + concat(substring($doc//mei:workList/mei:work/mei:creation/mei:date/@notbefore,1,4), + substring($doc//mei:workList/mei:work/mei:creation/mei:date/@startdate,1,4), + '–', + substring($doc//mei:workList/mei:work/mei:creation/mei:date/@enddate,1,4), + substring($doc//mei:workList/mei:work/mei:creation/mei:date/@notafter,1,4)) + else if($doc//mei:workList/mei:work/mei:creation/mei:date/@isodate!='') then + substring($doc//mei:workList/mei:work/mei:creation/mei:date[1]/@isodate,1,4) + else if($doc//mei:workList/mei:work/mei:expressionList/mei:expression[mei:creation/mei:date][1]/mei:creation/mei:date/(@notbefore|@notafter|@startdate|@enddate)!='') then + concat(substring($doc//mei:workList/mei:work/mei:expressionList/mei:expression[mei:creation/mei:date][1]/mei:creation/mei:date/@notbefore,1,4), + substring($doc//mei:workList/mei:work/mei:expressionList/mei:expression[mei:creation/mei:date][1]/mei:creation/mei:date/@startdate,1,4), + '–', + substring($doc//mei:workList/mei:work/mei:expressionList/mei:expression[mei:creation/mei:date][1]/mei:creation/mei:date/@enddate,1,4), + substring($doc//mei:workList/mei:work/mei:expressionList/mei:expression[mei:creation/mei:date][1]/mei:creation/mei:date/@notafter,1,4)) + else + substring($doc//mei:workList/mei:work/mei:expressionList/mei:expression[mei:creation/mei:date][1]/mei:creation/mei:date[@isodate][1]/@isodate,1,4) +}; + +(:~ + : Function for outputting the "Collection" information on the main list page + : + : @param $doc the MEI document to extract the information from + : @return the string representation of a MerMEId collection + :) +declare function common:get-edition-and-number($doc as node()?) as xs:string { + let $c := ($doc//mei:fileDesc/mei:seriesStmt/mei:identifier[@type="file_collection"])[1] => normalize-space() + let $no := ($doc//mei:meiHead/mei:workList/mei:work/mei:identifier[normalize-space(@label)=$c])[1] => normalize-space() + (: shorten very long identifiers (i.e. lists of numbers) :) + let $part1 := substring($no, 1, 11) + let $part2 := substring($no, 12) + let $delimiter := substring(concat(translate($part2,'0123456789',''),' '),1,1) + let $n := + if (string-length($no)>11) then + concat($part1,substring-before($part2,$delimiter),'...') + else + $no + return concat($c, ' ', $n) +}; + +(:~ + : Get the composers of a work. + : This is used for outputting the "Composer" information on the main list page + : as well as for crud:read() + : + : @param $doc the MEI document to extract the information from + : @return a string-join of the composers, an empty sequence if none are given + :) +declare function common:get-composers($doc as node()?) as xs:string? { + $doc//mei:workList/mei:work/mei:contributor/mei:persName[@role='composer'] => string-join(', ') +}; + +(:~ + : Get the (main) title of a work. + : This is used for outputting the "Title" information on the main list page + : as well as for crud:read() + : + : @param $doc the MEI document to extract the information from + : @return the (main) title + :) +declare function common:get-title($doc as node()?) as xs:string { + ($doc//mei:workList/mei:work/mei:title[text()])[1] => normalize-space() +}; + +(:~ + : Propose a new filename based on an existing one + : This is simply done by adding "-copy" to the basenam of the file + : + : @param $filename the existing filename + : @return a proposed filename + :) +declare function common:propose-filename($filename as xs:string) as xs:string { + let $tokens := $filename => tokenize('\.') + let $suffix := + if(count($tokens) gt 1) + then $tokens[last()] + else 'xml' + return + if(count($tokens) eq 1) + then $tokens || '-copy.' || $suffix + else (subsequence($tokens, 1, count($tokens) -1) => string-join('.')) || '-copy.' || $suffix +}; + +(:~ + : Add a change entry to the revisionDesc of an existing persistent document stored in the eXist database. + : NB, this will modify the existing document in the database! + : For changing documents or fragments in memory, see `common:add-change-entry-to-revisionDesc-in-memory#3` + : + : @param $document the input MEI document to add the change entry to + : @param $user the user identified with this change entry + : @param $desc a description of the change + : @return a map object with properties "message" and "filename". + A successful operation will return "Success" as message, the error description otherwise + :) +declare function common:add-change-entry-to-revisionDesc-in-document($document as document-node(), + $user as xs:string, $desc as xs:string) as map(*) { + let $change := + + + {$user} + + +

{$desc}

+
+
+ return + try { + update insert $change into $document/mei:mei/mei:meiHead/mei:revisionDesc, + map { + 'message': 'Success', + 'filename': util:document-name($document) + } + } + catch * { + map { + 'message': $err:description, + 'filename': util:document-name($document) + } + } +}; + +(:~ + : Add a change entry to the revisionDesc in memory. + : NB, this will _not_ modify any existing document in the database! + : For changing existing documents in the database, see `common:add-change-entry-to-revisionDesc-in-document#3` + : + : @param $node the input MEI document as node() or document-node() + : @param $user the user identified with this change entry + : @param $desc a description of the change + : @return the modified input node + :) +declare function common:add-change-entry-to-revisionDesc-in-memory($node as node(), + $user as xs:string, $desc as xs:string) as node() { + typeswitch($node) + case $elem as element(mei:revisionDesc) return + element { node-name($elem) } { + $elem/@*, for $child in $elem/node() return common:add-change-entry-to-revisionDesc-in-memory($child, $user, $desc), + + + {$user} + + +

{$desc}

+
+
+ } + case $elem as element() return + element { node-name($elem) } { + $elem/@*, for $child in $elem/node() return common:add-change-entry-to-revisionDesc-in-memory($child, $user, $desc) + } + case document-node() return common:add-change-entry-to-revisionDesc-in-memory($node/node(), $user, $desc) + default return $node +}; + +(:~ + : Generate an ID by prefixing an unique ID with an optional prefix + : + : @param $prefix an optional prefix for the ID + : @return a unique ID + :) +declare function common:mermeid-id($prefix as xs:string?) as xs:string { + $prefix || '_' || substring(util:uuid(),1,13) +}; + +(:~ + : Update target attributes + : + : @param $collection the collection of XML documents to look for and update references + : @param $old-identifier the old identifier of the XML document + : @param $new-identifier the new identifier of the XML document + : @param $dry-run "true()" will perform a dry run without changing the references + : @return a map object with properties "old_identifier", "new_identifier", "dry_run", + "replacements", "changed_documents", and "message". "replacements" and "changed_documents" + are their respective numbers and are negative (-1) if an error occured. + :) +declare function common:update-targets($collection as node()*, $old-identifier as xs:string, + $new-identifier as xs:string, $dry-run as xs:boolean) as map(*) { + try { + let $targets := $collection//@target[contains(., $old-identifier)] + let $documents := $targets/root() ! document-uri(.) + return ( + if($dry-run) then () + else ( + for $target in $targets + let $replacement := replace($target, $old-identifier, $new-identifier) + return + update replace $target with $replacement + ), + map { + 'old_identifier': $old-identifier, + 'new_identifier': $new-identifier, + 'replacements': count($targets), + 'changed_documents': count($documents), + 'message': 'Success', + 'dry_run': $dry-run + } + )} + catch * { + map { + 'old_identifier': $old-identifier, + 'new_identifier': $new-identifier, + 'replacements': -1, + 'changed_documents': -1, + 'message': $err:description, + 'dry_run': $dry-run + } + } +}; + +(:~ + : Set the MEI title in an existing persistent document stored in the eXist database. + : NB, this will modify the existing document in the database! + : For changing documents or fragments in memory, see `common:set-mei-title-in-memory#2` + : + : @param $doc the MEI document to change the title + : @param $new_title the new title + : @return a map object with properties "message", "title", and "filename" + :) +declare function common:set-mei-title-in-document($doc as document-node(), $new_title as xs:string) as map(*) { + try {( + for $title in $doc//mei:workList/mei:work[1]/mei:title[text()][1] + return + update value $title with $new_title + ), + map { + 'message': 'Success', + 'title': $new_title, + 'filename': util:document-name($doc) + } + } + catch * { + map { + 'message': $err:description, + 'title': $new_title, + 'filename': util:document-name($doc) + } + } +}; + +(:~ + : Set the MEI title of a MEI document in memory. + : NB, this will _not_ modify any existing document in the database! + : For changing existing documents in the database, see `common:set-mei-title-in-document#2` + : + : @param $node the input MEI document as node() or document-node() + : @param $new_title the new title + : @return the modified input node + :) +declare function common:set-mei-title-in-memory($node as node(), $new_title as xs:string) as node() { + typeswitch($node) + case $elem as element(mei:title) return + if(not($elem/preceding-sibling::mei:title) and $elem/parent::mei:work[1] and $elem/ancestor::mei:workList) + then + element { node-name($elem) } { + $elem/@*, $new_title + } + else + element { node-name($elem) } { + $elem/@*, for $child in $elem/node() return common:set-mei-title-in-memory($child, $new_title) + } + case $elem as element() return + element { node-name($elem) } { + $elem/@*, for $child in $elem/node() return common:set-mei-title-in-memory($child, $new_title) + } + case document-node() return common:set-mei-title-in-memory($node/node(), $new_title) + default return $node +}; + +(:~ + : Get the current username + : Wrapper function around `sm:id#0` + : + : @return the username of the currently logged in user + :) +declare function common:get-current-username() as xs:string? { + sm:id()//sm:real/sm:username => data() +}; diff --git a/modules/copy-file.xq b/modules/copy-file.xq deleted file mode 100755 index 63f96640..00000000 --- a/modules/copy-file.xq +++ /dev/null @@ -1,59 +0,0 @@ -xquery version "3.0"; - -import module namespace login="http://kb.dk/this/login" at "./login.xqm"; -import module namespace config="https://github.com/edirom/mermeid/config" at "./config.xqm"; - -declare namespace functx = "http://www.functx.com"; -declare namespace m="http://www.music-encoding.org/ns/mei"; -declare namespace xmldb="http://exist-db.org/xquery/xmldb"; -declare namespace request="http://exist-db.org/xquery/request"; -declare namespace response="http://exist-db.org/xquery/response"; -declare namespace fn="http://www.w3.org/2005/xpath-functions"; -declare namespace util="http://exist-db.org/xquery/util"; - - -declare option exist:serialize "method=xml media-type=text/html"; - -declare variable $dcmroot := $config:app-root; - -declare function functx:copy-attributes - ( $copyTo as element() , - $copyFrom as element() ) as element() { - - element { node-name($copyTo)} - { $copyTo/@*[not(node-name(.) = $copyFrom/@*/node-name(.))], - $copyFrom/@*, - $copyTo/node() } - - } ; - - -let $return_to := config:link-to-app("modules/list_files.xq") - - -let $log-in := login:function() -let $res := response:redirect-to($return_to cast as xs:anyURI) -let $parameters := request:get-parameter-names() - -return - - { - for $parameter in $parameters - let $new_file := concat($dcmroot,util:uuid(),".xml") - let $old_file := concat($dcmroot,$parameter) - where request:get-parameter($parameter,"")="copy" and contains($parameter,"xml") - return - let $odoc := doc($old_file) - let $stored := xmldb:store($dcmroot,$new_file, $odoc ) - let $new_doc := doc($new_file) - for $title in $new_doc//m:workList/m:work[1]/m:title[string()][1] - let $new_title_text := concat($title//string()," (copy) ") - let $new_title := - {$new_title_text} - let $upd := update replace $title[string()] with - functx:copy-attributes($new_title,$title) - return - } -
{$title[string()][1]//string()}{$new_title_text}
- - diff --git a/modules/create-file.xq b/modules/create-file.xq deleted file mode 100755 index d7588ce0..00000000 --- a/modules/create-file.xq +++ /dev/null @@ -1,28 +0,0 @@ -xquery version "3.0"; - -import module namespace login="http://kb.dk/this/login" at "./login.xqm"; -import module namespace config="https://github.com/edirom/mermeid/config" at "./config.xqm"; - -declare namespace xs="http://www.w3.org/2001/XMLSchema"; -declare namespace util="http://exist-db.org/xquery/util"; -declare namespace xmldb="http://exist-db.org/xquery/xmldb"; -declare namespace request="http://exist-db.org/xquery/request"; -declare namespace response="http://exist-db.org/xquery/response"; -declare option exist:serialize "method=xml encoding=UTF-8 media-type=text/html"; - -let $log-in := login:function() -let $exist_path := request:get-parameter("path","") -let $new_doc := doc("../forms/model/new_file.xml") - -let $file := concat(util:uuid(),".xml") -let $file_arg := concat("doc=",$file) - -let $return_to := concat(config:link-to-app("/forms/edit-work-case.xml"), "?", $file_arg) -let $res := response:redirect-to($return_to cast as xs:anyURI) -let $result := xmldb:store($config:data-root, $file, $new_doc) - -return - - - -
file{$file}
redirect{$return_to}
diff --git a/modules/crud.xqm b/modules/crud.xqm new file mode 100644 index 00000000..82638bcb --- /dev/null +++ b/modules/crud.xqm @@ -0,0 +1,162 @@ +xquery version "3.1"; + +(:~ + : Basic CRUD (Create, Read, Update, Delete) functions for the MerMEId data store + : + : All operations assume data is kept at $config:data-root and the file hierarchy is flat, + : i.e. there are no subfolders + :) +module namespace crud="https://github.com/edirom/mermeid/crud"; + +declare namespace mei="http://www.music-encoding.org/ns/mei"; +declare namespace request="http://exist-db.org/xquery/request"; +declare namespace response="http://exist-db.org/xquery/response"; +declare namespace xmldb="http://exist-db.org/xquery/xmldb"; +declare namespace map="http://www.w3.org/2005/xpath-functions/map"; +declare namespace err="http://www.w3.org/2005/xqt-errors"; +declare namespace jb="http://exist.sourceforge.net/NS/exist/java-binding"; +declare namespace util="http://exist-db.org/xquery/util"; +declare namespace sm="http://exist-db.org/xquery/securitymanager"; + +import module namespace config="https://github.com/edirom/mermeid/config" at "config.xqm"; +import module namespace common="https://github.com/edirom/mermeid/common" at "common.xqm"; + +(:~ + : Delete files within the data directory + : + : @param $filenames the files to delete + : @return an array of map object with filename, message and code properties concerning the delete operation + :) +declare function crud:delete($filenames as xs:string*) as array(map(xs:string,xs:string)*) { + array { + for $filename in $filenames + return + try {( + xmldb:remove($config:data-root, $filename), + map { + 'filename': $filename, + 'message': 'deleted successfully', + 'code': 200 + } + )} + catch jb:org.xmldb.api.base.XMLDBException { + map { + 'filename': $filename, + 'message': 'failed to delete: ' || $err:description, + 'code': 401 + } + } + catch * { + map { + 'filename': $filename, + 'message': 'failed to delete: ' || string-join(($err:code, $err:description), '; '), + 'code': 500 + } + } + } +}; + +(:~ + : Create a file within the data directory + : + : @param $node the XML document to store + : @param $filename the filename for the new file + : @param $overwrite whether an existent file may be overwritten + : @return a map object with filename, message and code properties concerning the create operation + :) +declare function crud:create($node as node(), $filename as xs:string, $overwrite as xs:boolean) as map(*) { + try { + if(not(doc-available($config:data-root || '/' || $filename)) or $overwrite) + then if(xmldb:store($config:data-root, $filename, $node)) + then crud:read($filename) => map:put('message', 'created successfully') + else map { + 'filename': $filename, + 'message': 'failed to create file', + 'code': 500 + } + else map { + 'filename': $filename, + 'message': 'file already exists and no overwrite flag was set', + 'code': 401 + } + } + catch jb:org.xmldb.api.base.XMLDBException { + map { + 'filename': $filename, + 'message': 'failed to create file: ' || $err:description, + 'code': 401 + } + } + catch * { + map { + 'filename': $filename, + 'message': 'failed to create file: ' || string-join(($err:code, $err:description), '; '), + 'code': 500 + } + } +}; + +(:~ + : Copy a file within the data directory + : + : @param $source-filename the input filename to copy + : @param $target-filename the output filename to copy to + : @param $overwrite whether an existent target file may be overwritten + : @param $new_title an optional new title. If omitted, the string "(copy)" will be appended to the old title + : @return a map object with source, target, message and code properties concerning the copy operation + :) +declare function crud:copy($source-filename as xs:string, $target-filename as xs:string, + $overwrite as xs:boolean, $new_title as xs:string?) as map(*) { + let $source := + if(doc-available($config:data-root || '/' || $source-filename)) + then doc($config:data-root || '/' || $source-filename) + else () + let $title := + if($new_title) + then $new_title + else common:get-title($source) || ' (Copy)' + let $username := common:get-current-username() => string() + let $change-message := 'file copied from ' || $source-filename || ' to ' || $target-filename + let $create-target := + if($source) then crud:create($source => common:set-mei-title-in-memory($title) => common:add-change-entry-to-revisionDesc-in-memory($username, $change-message), $target-filename, $overwrite) + else () + return + if($create-target instance of map(*)) + then $create-target => map:put('title', $new_title) + else map { + 'source': $source-filename, + 'target': $target-filename, + 'message': 'source does not exist', + 'code': 404 + } +}; + +(:~ + : Read a file from the data directory + : + : @param $filename the filename (aka MerMEId identifier) to read + : @return a map object with some metadata (e.g. "title", "composer") and the complete MEI document as "document-node" + :) +declare function crud:read($filename as xs:string) as map(*) { + let $doc := + if(doc-available($config:data-root || '/' || $filename)) + then doc($config:data-root || '/' || $filename) + else () + return + if($doc) + then map { + 'filename': $filename, + 'document-node': $doc, + 'composer': common:get-composers($doc), + 'title': common:get-title($doc), + 'year': common:display-date($doc), + 'collection': common:get-edition-and-number($doc), + 'message': 'read successfully', + 'code': 200 + } + else map { + 'filename': $filename, + 'message': 'file not found or permissions missing', + 'code': 404 + } +}; diff --git a/modules/delete-file.xq b/modules/delete-file.xq deleted file mode 100755 index 7c41f9a1..00000000 --- a/modules/delete-file.xq +++ /dev/null @@ -1,45 +0,0 @@ -xquery version "3.1" encoding "UTF-8"; - -import module namespace login="http://kb.dk/this/login" at "./login.xqm"; -import module namespace config="https://github.com/edirom/mermeid/config" at "./config.xqm"; - -declare namespace functx = "http://www.functx.com"; -declare namespace m="http://www.music-encoding.org/ns/mei"; -declare namespace xmldb="http://exist-db.org/xquery/xmldb"; -declare namespace request="http://exist-db.org/xquery/request"; -declare namespace response="http://exist-db.org/xquery/response"; -declare namespace fn="http://www.w3.org/2005/xpath-functions"; - -declare option exist:serialize "method=xml media-type=text/html"; - -declare variable $dcmroot := $config:data-root; - -let $return_to := config:link-to-app("modules/list_files.xq") - - -let $log-in := login:function() -let $res := response:redirect-to($return_to cast as xs:anyURI) -let $parameters := request:get-parameter-names() - -return -

- { - for $resource in $parameters - where request:get-parameter($resource,"")="delete" and contains($resource,"xml") - return xmldb:remove(xs:anyURI($dcmroot), $resource) - } -

- -(: -xquery version "3.1" encoding "UTF-8"; - -declare namespace xmldb="http://exist-db.org/xquery/xmldb"; - -let $dcmroot := "/db/dcm/" -let $resource := "nielsen_cnw0126.xml" - -return xmldb:remove(xs:anyURI($dcmroot), $resource) -:) - - - diff --git a/modules/list_files.xq b/modules/list_files.xq index cad39517..b37f8495 100755 --- a/modules/list_files.xq +++ b/modules/list_files.xq @@ -3,6 +3,7 @@ xquery version "3.0" encoding "UTF-8"; import module namespace loop="http://kb.dk/this/getlist" at "./main_loop.xqm"; import module namespace app="http://kb.dk/this/listapp" at "./list_utils.xqm"; import module namespace config="https://github.com/edirom/mermeid/config" at "./config.xqm"; +import module namespace common="https://github.com/edirom/mermeid/common" at "./common.xqm"; declare namespace xl="http://www.w3.org/1999/xlink"; declare namespace request="http://exist-db.org/xquery/request"; @@ -64,44 +65,35 @@ declare function local:format-reference( else "even" - let $date_output := - if($doc//m:workList/m:work/m:creation/m:date/(@notbefore|@notafter|@startdate|@enddate)!='') then - concat(substring($doc//m:workList/m:work/m:creation/m:date/@notbefore,1,4), - substring($doc//m:workList/m:work/m:creation/m:date/@startdate,1,4), - '-', - substring($doc//m:workList/m:work/m:creation/m:date/@enddate,1,4), - substring($doc//m:workList/m:work/m:creation/m:date/@notafter,1,4)) - else if($doc//m:workList/m:work/m:creation/m:date/@isodate!='') then - substring($doc//m:workList/m:work/m:creation/m:date[1]/@isodate,1,4) - else if($doc//m:workList/m:work/m:expressionList/m:expression[m:creation/m:date][1]/m:creation/m:date/(@notbefore|@notafter|@startdate|@enddate)!='') then - concat(substring($doc//m:workList/m:work/m:expressionList/m:expression[m:creation/m:date][1]/m:creation/m:date/@notbefore,1,4), - substring($doc//m:workList/m:work/m:expressionList/m:expression[m:creation/m:date][1]/m:creation/m:date/@startdate,1,4), - '-', - substring($doc//m:workList/m:work/m:expressionList/m:expression[m:creation/m:date][1]/m:creation/m:date/@enddate,1,4), - substring($doc//m:workList/m:work/m:expressionList/m:expression[m:creation/m:date][1]/m:creation/m:date/@notafter,1,4)) - else - substring($doc//m:workList/m:work/m:expressionList/m:expression[m:creation/m:date][1]/m:creation/m:date[@isodate][1]/@isodate,1,4) - (: for some reason the sort-key function must be called outside the actual searching to have correct work number sorting when searching within all collections :) let $dummy := loop:sort-key("dummy_collection", $doc, "null") let $ref := - {$doc//m:workList/m:work/m:contributor/m:persName[@role='composer']} + {common:get-composers($doc)} - {app:view-document-reference($doc)} - {" ",$date_output} - {app:get-edition-and-number($doc)} - + + {common:get-title($doc)} + + + {common:display-date($doc)} + {common:get-edition-and-number($doc)} + +
- view source - + action="{config:link-to-app('data/read')}"> + + +
{app:edit-form-reference($doc)} {app:copy-document-reference($doc)} @@ -132,10 +124,6 @@ declare function local:format-reference( href="../resources/css/login.css" type="text/css"/> - - @@ -147,21 +135,34 @@ declare function local:format-reference(
-
- Login -  Help +
+
+ Login
+
+
+ + + + + + +
+
+
+ +
MerMEId Logo + alt="MerMEId Logo"/>
@@ -331,6 +332,7 @@ declare function local:format-reference(
} {doc('../login.html')/*} + {doc('../confirm.html')/*} {config:replace-properties(config:get-property('footer'))} diff --git a/modules/list_utils.xqm b/modules/list_utils.xqm index 67c7b438..e979340d 100755 --- a/modules/list_utils.xqm +++ b/modules/list_utils.xqm @@ -1,7 +1,8 @@ -xquery version "1.0" encoding "UTF-8"; +xquery version "3.1" encoding "UTF-8"; module namespace app="http://kb.dk/this/listapp"; import module namespace config="https://github.com/edirom/mermeid/config" at "./config.xqm"; +import module namespace common="https://github.com/edirom/mermeid/common" at "./common.xqm"; declare namespace file="http://exist-db.org/xquery/file"; declare namespace fn="http://www.w3.org/2005/xpath-functions"; @@ -13,6 +14,7 @@ declare namespace response="http://exist-db.org/xquery/response"; declare namespace util="http://exist-db.org/xquery/util"; declare namespace xl="http://www.w3.org/1999/xlink"; declare namespace xdb="http://exist-db.org/xquery/xmldb"; +declare namespace html="http://www.w3.org/1999/xhtml"; declare function app:options() as node()* @@ -28,161 +30,139 @@ let $options:= }; +declare function app:get-publication-reference($doc as node()) as element(html:form) { + let $doc-name:= util:document-name($doc) + let $color_style := + if(doc-available(concat($config:data-public-root,'/',$doc-name))) + then ( + let $dcmtime := xs:dateTime(xdb:last-modified($config:data-root, $doc-name)) + let $pubtime := xs:dateTime(xdb:last-modified($config:data-public-root, $doc-name)) + return + if($dcmtime lt $pubtime) + then "publishedIsGreen" + else "pendingIsYellow" + ) + else "unpublishedIsRed" + return +
+
+ + +
+
+}; - declare function app:get-publication-reference($doc as node() ) as node()* - { - let $doc-name:=util:document-name($doc) - let $color_style := - if(doc-available(concat($config:data-public-root,'/',$doc-name))) then - ( - let $dcmtime := xs:dateTime(xdb:last-modified($config:data-root, $doc-name)) - let $pubtime := xs:dateTime(xdb:last-modified($config:data-public-root, $doc-name)) - return - if($dcmtime lt $pubtime) then - "publishedIsGreen" - else - "pendingIsYellow" - ) - else - "unpublishedIsRed" - - let $form:= -
- -
- - - - - -
-
- return $form - }; - - declare function app:get-edition-and-number($doc as node() ) as xs:string* { - let $c := - $doc//m:fileDesc/m:seriesStmt/m:identifier[@type="file_collection"][1]/string() - let $no := $doc//m:meiHead/m:workList/m:work[1]/m:identifier[@label=$c][1]/string() - (: shorten very long identifiers (i.e. lists of numbers) :) - let $part1 := substring($no, 1, 11) - let $part2 := substring($no, 12) - let $delimiter := substring(concat(translate($part2,'0123456789',''),' '),1,1) - let $n := - if (string-length($no)>11) then - concat($part1,substring-before($part2,$delimiter),'...') - else - $no - return ($c, $n) - }; - - declare function app:view-document-reference($doc as node()) as node() { - (: it is assumed that we live in the same collection 'modules' :) - let $ref := - - {$doc//m:workList/m:work/m:title[1]/string()} - - return $ref - }; - declare function app:view-document-notes($doc as node()) as node() { - let $note := $doc//m:fileDesc/m:notesStmt/m:annot[@type='private_notes']/string() - let $n := +declare function app:view-document-notes($doc as node()) as element() { + let $note := $doc//m:fileDesc/m:notesStmt/m:annot[@type='private_notes']/string() + return if (string-length($note)>20) then - {concat(substring($note,1,20), substring-before(substring($note,21),' '))}...{$note} + {concat(substring($note,1,20), + substring-before(substring($note,21),' '))}...{$note} else - {$note} - return $n - }; - - - declare function app:edit-form-reference($doc as node()) as node() - { - (: - Beware: Partly hard coded reference here!!! - It still assumes that the document resides on the same host as this - xq script but on port 80 - - The old form is called edit_mei_form.xml the refactored one starts on - edit-work-case.xml - :) - - let $form-id := util:document-name($doc) - let $ref := - - return $ref + {$note} +}; - }; +declare function app:edit-form-reference($doc as node()) as element(html:form) { +
+ + +
+}; - declare function app:copy-document-reference($doc as node()) as node() - { - let $form-id := util:document-name($doc) - let $uri := concat($config:data-public-root, "/", util:document-name($doc)) - let $form := -
- - -
- return $form - }; +declare function app:copy-document-reference($doc as node()) as element(html:form) { + let $doc-name := util:document-name($doc) + let $proposed-name := common:propose-filename($doc-name) + let $title := common:get-title($doc) + let $proposed-title := $title || ' (Copy)' + let $uri := concat($config:data-public-root, "/", $doc-name) + return +
+ + + + + + + + +
+}; - declare function app:rename-document-reference($doc as node()) as node() - { - let $doc-name := util:document-name($doc) - let $form-id := concat("rename",$doc-name) - let $uri := concat($config:data-public-root,"/",$doc-name) - let $form := -
- - - Rename -
- return $form - }; +declare function app:rename-document-reference($doc as node()) as element(html:form) { + let $doc-name := util:document-name($doc) + let $proposed-name := common:propose-filename($doc-name) + let $title := common:get-title($doc) + let $proposed-title := $title || ' (Copy)' + let $uri := concat($config:data-public-root, "/", $doc-name) + return +
+ + + + + + + + +
+}; - declare function app:delete-document-reference($doc as node()) as node() - { - let $form-id := util:document-name($doc) - let $uri := concat($config:data-public-root,"/",util:document-name($doc)) - let $form := - if(doc-available($uri)) then - - Remove (disabled) +declare function app:delete-document-reference($doc as node()) as element() { + let $doc-name := util:document-name($doc) + let $uri := concat($config:data-public-root,"/",util:document-name($doc)) + return + if(doc-available($uri)) then + + Remove (disabled) else -
- - + + + + +
- return $form - }; +}; + declare function app:list-title() { diff --git a/modules/rename-file.xq b/modules/rename-file.xq deleted file mode 100755 index e54c956d..00000000 --- a/modules/rename-file.xq +++ /dev/null @@ -1,110 +0,0 @@ -xquery version "1.0" encoding "UTF-8"; - -import module namespace login="http://kb.dk/this/login" at "./login.xqm"; -import module namespace config="https://github.com/edirom/mermeid/config" at "./config.xqm"; - -declare namespace m="http://www.music-encoding.org/ns/mei"; -declare namespace xdb="http://exist-db.org/xquery/xmldb"; -declare namespace request="http://exist-db.org/xquery/request"; -declare namespace response="http://exist-db.org/xquery/response"; -declare namespace fn="http://www.w3.org/2005/xpath-functions"; -declare namespace util="http://exist-db.org/xquery/util"; - -declare option exist:serialize "method=xml media-type=text/html"; - -declare variable $dcmroot := $config:data-root; -declare variable $old_name := normalize-space(request:get-parameter("doc", "")); -declare variable $name := normalize-space(request:get-parameter("name", "")); -declare variable $doc_path := config:link-to-app("data/"); -declare variable $old_name_abs := concat($doc_path,$old_name); -declare variable $now := fn:current-dateTime() cast as xs:string; -declare variable $isodate := concat(substring($now,1,23),"Z"); - - -declare function local:id($element_name) { - (: generate an ID :) - let $id := concat($element_name,"_",substring(util:uuid(),1,13)) - return $id -}; - -declare function local:add_revision($document, $user, $desc) { - (: add a comment to the revision history :) - let $change := - - - {$user} - - -

{$desc}

-
-
- let $add := update insert $change into $document/m:meiHead/m:revisionDesc - return "" -}; - -declare function local:replace_target ($target as node(), $new_name) { - (: update @target value :) - let $old_name_abs := concat($doc_path,$old_name) - let $new_name_abs := concat($doc_path,$new_name) - let $replace := - if (starts-with($target,$old_name)) then - let $upd := update replace $target[string()] with concat($new_name,substring-after($target,$old_name)) - return $upd - else - if (starts-with($target,$old_name_abs)) then - let $upd := update replace $target[string()] with concat($doc_path,$new_name,substring-after($target,$old_name)) - return $upd - else - "" - return $replace -}; - -declare function local:change_targets ($document, $new_name) { - (: update references (@target) pointing to the renamed file :) - let $update := - for $target in $document/*//@target[contains(.,$old_name)] - return local:replace_target($target, $new_name) - - (: add a comment to the revision history :) - let $change := local:add_revision($document, "MerMEId", concat("file reference updated (a file was renamed from ",$old_name," to ",$new_name,")")) - return "" - -}; - - -let $log-in := login:function() - -let $new_name := - if (substring($name,string-length($name)-3,4)=".xml") then - $name - else - concat($name,".xml") - -let $result:= - if ($old_name!="" and $name!="") then - xdb:rename($dcmroot, $old_name, $new_name) - else - "" - -(: update all references to the renamed file :) -let $list := - if ($old_name!="" and $name!="") then - for $doc in collection($dcmroot)/m:mei[*//@target[starts-with(.,$old_name) or starts-with(.,$old_name_abs)]] - return local:change_targets($doc,$new_name) - else - "" - -(: add a comment to the revision history :) -let $change := - if ($old_name!="" and $name!="") then - local:add_revision(fn:doc(concat($dcmroot,$new_name))/m:mei, "MerMEId user", concat("file renamed from ",$old_name," to ",$new_name)) - else - "" - - -let $return_to := config:link-to-app("modules/list_files.xq") -let $res := response:redirect-to($return_to cast as xs:anyURI) - -return $change - - \ No newline at end of file diff --git a/resources/css/list_style.css b/resources/css/list_style.css index 65d57b85..9257a3e0 100755 --- a/resources/css/list_style.css +++ b/resources/css/list_style.css @@ -57,22 +57,6 @@ a { color:black; } -a.addLink { - text-decoration: none; -} - -a.addLink img { - padding: 2px; - border: none; - vertical-align:top; -} - -a.addLink:hover img { - padding: 1px; - border: 1px solid #999; -} - - table { width: 100%; } @@ -350,4 +334,53 @@ form.search a.help .help_label { padding-right: 8px; white-space: nowrap; font-family: courier, fixed, monospace; -} \ No newline at end of file +} + +.ajaxform .ajaxform_input, .ajaxform .ajaxform_label { + display: none; +} + +form.modal-content button { + display: block; + margin-top: 2ex; + padding: 0.5ex 2em; + cursor: pointer; +} + +#confirm_modal_statusmessage { + display:none; +} + +#confirm_modal_statusmessage.error { + display: block; + border: 1px solid red; + padding: 1ex; + color: red; + font-weight: bold; +} + +#confirm_modal_statusmessage.info { + display: block; + border: 1px solid green; + padding: 1ex; + color: green; + font-weight: bold; +} + +.result_table button, .header_right button { + border: none; + cursor: pointer; + background: transparent; +} +.header_right { + float: right; +} + +.header_right > div { + display: inline; + padding-left:.3em; +} +.header_right form { + display: inline; +} + diff --git a/resources/js/confirm.js b/resources/js/confirm.js index f6645242..dd204f3e 100755 --- a/resources/js/confirm.js +++ b/resources/js/confirm.js @@ -1,25 +1,120 @@ -function show_confirm(formid, text) -{ - var r=confirm("Do you really want to delete the file '" + text +"'?" ); - if (r==true) { - var form = document.getElementById(formid); - form.submit(); - } else { - } +/* + * main event handler for the forms on the main page + * it will simply show the modal and clone all inputs etc + * to the modal form + */ +function ajaxform(ev) { + ev.preventDefault(); + const curForm = ev.target, + orgButton = curForm.querySelector("button[type=submit]"); + + // remove class information from the status message to hide it initially + document.getElementById("confirm_modal_statusmessage").className = ""; + // set the title of the modal + document.getElementById("confirm_modal_heading").innerHTML = ev.target.getAttribute("title"); + // copy the action and method attribute to the modal form + document.querySelector("#confirm_modal form").setAttribute("action", curForm.getAttribute("action")); + document.querySelector("#confirm_modal form").setAttribute("method", curForm.getAttribute("method")); + // empty the modal body + document.getElementById("confirm_modal_body").innerHTML = ""; + // fill the modal body by (deep) cloning inputs and labels from the original form + curForm.querySelectorAll(".ajaxform_input, .ajaxform_label").forEach(function(p) { + document.getElementById("confirm_modal_body").appendChild(p.cloneNode(true)); + }) + // add the submit button by (shallow) cloning the original button + document.getElementById("confirm_modal_body").appendChild(orgButton.cloneNode(false)).innerHTML=orgButton.value; + // finally show the modal + document.getElementById("confirm_modal").style.display = "block"; } -function filename_prompt(formid, text, published) -{ - if (published) { - alert("Only unpublished documents can be renamed.\nPlease unpublish the document before renaming it."); - } else { - var name = prompt("Rename '" + text +"' to " ); - if (name!=null && name!="") { - var form = document.getElementById(formid); - form.name.value = name; - form.submit(); - } else { +/* + * event handler for the modal form + * it will call the backend crud functions via AJAX and + * update the main table if successfull + */ +function modal_submit_handler(ev) { + ev.preventDefault(); + const curForm = ev.target, + endpoint = new URL(curForm.getAttribute("action")), + method = curForm.getAttribute("method"); + params = new URLSearchParams(new FormData(curForm)); + + fetch(endpoint, { + method: method, + headers: { + "Content-type": "application/x-www-form-urlencoded", + "Accept": "application/json" + }, + body: params + }) + .then(response => { + if (!response.ok) { + throw response; + } + return response.json(); + }) + .then(data => { + update_statusmessage(data, "info"); + setTimeout(() => { + document.getElementById("confirm_modal").style.display = "none"; + if(endpoint.pathname === '/data/create') { + // for new documents, directly redirect to the edit page + window.location.href = '../forms/edit-work-case.xml?doc=' + params.get('filename'); + } + else { + // for all other actions, reload the list page + // (instead of simply reloading the page we might update the table dynamically) + location.reload(); + } + }, 1000); + + }) + .catch(error => { + if (typeof error.json === 'function') { + error.json().then( + obj => { + update_statusmessage(obj, "error"); + } + )} + else { + update_statusmessage("Some unknown error occured", "error"); } + }) +}; + +/* + * helper function for modal_submit_handler() + */ +function update_statusmessage(obj, classname) { + if(obj.constructor === Array) { + document.getElementById("confirm_modal_statusmessage").innerHTML = obj[0].message; + document.getElementById("confirm_modal_statusmessage").className = classname; + } + else { + document.getElementById("confirm_modal_statusmessage").innerHTML = obj.message; + document.getElementById("confirm_modal_statusmessage").className = classname; } } +/* + * Generic form submit event handler for the main page + * to capture clicks on e.g. "copy", or "rename" buttons + */ +document.querySelectorAll(".ajaxform").forEach(el => {el.addEventListener("submit", ajaxform)}); +document.querySelector("#confirm_modal form").addEventListener("submit", modal_submit_handler); + +/* + * close modal on click + */ +document.querySelectorAll(".close-modal").forEach(el => {el.addEventListener("click", function(ev) { + document.getElementById("confirm_modal").style.display = "none";}, false) +}); + +/* + * close modal on escape key press + */ +document.addEventListener('keydown', evt => { + if (evt.key === 'Escape') { + document.getElementById("confirm_modal").style.display = "none"; + } +}); diff --git a/resources/js/login.js b/resources/js/login.js index 542bf42d..c415ccbb 100644 --- a/resources/js/login.js +++ b/resources/js/login.js @@ -72,23 +72,11 @@ function updateLoginInfo(obj) { } /* - * Toggle css and events for edit buttons + * Toggle css and events for forms * param: hide|show */ function toggleButtons(status) { - // input elements toggle the "disabled" attribute - document.querySelectorAll(".loginRequired input").forEach(el => { - if(status === 'show') { - el.removeAttribute('disabled'); - el.parentElement.parentElement.classList.add(status); - } - else { - el.setAttribute('disabled', 'disabled'); - el.parentElement.parentElement.classList.remove('show'); - } - }); - // all other elements toggle the pointerEvents and toggle the class "show" - document.querySelectorAll(".loginRequired a, .loginRequired img, .loginRequired label").forEach(el => { + document.querySelectorAll(".loginRequired form").forEach(el => { if(status === 'show') { el.style.pointerEvents = "auto"; el.parentElement.classList.add(status); diff --git a/testing/src/test/java/mermeid/MermeidTest.java b/testing/src/test/java/mermeid/MermeidTest.java index 2a02f4c2..1226d0a5 100644 --- a/testing/src/test/java/mermeid/MermeidTest.java +++ b/testing/src/test/java/mermeid/MermeidTest.java @@ -89,8 +89,8 @@ public void OpenEditPage(){ assertTrue(buttonText.equals("Try MerMEId")); enterLogin(); - WebElement edit = driver.findElement(By.cssSelector("[href=\"../forms/edit-work-case.xml?doc=incipit_demo.xml\"]")); - edit.click(); + WebElement editButton = driver.findElement(By.xpath("//form[@action='http://localhost:8080/forms/edit-work-case.xml'][input/@value='incipit_demo.xml']/button")); + editButton.click(); try { new WebDriverWait(driver, Duration.ofSeconds(10)).until(ExpectedConditions.titleContains("MerMEId ")); @@ -154,8 +154,8 @@ public void checkWorkTabInputText(){ Actions builder = new Actions(driver); enterLogin(); - WebElement edit = driver.findElement(By.cssSelector("[href=\"../forms/edit-work-case.xml?doc=incipit_demo.xml\"]")); - edit.click(); + WebElement editButton = driver.findElement(By.xpath("//form[@action='http://localhost:8080/forms/edit-work-case.xml'][input/@value='incipit_demo.xml']/button")); + editButton.click(); // wait for page to have loaded wait.until(ExpectedConditions.elementToBeClickable(By.xpath("//span[@id='xf-293']/a"))); @@ -178,8 +178,8 @@ public void checkWorkTabInputText(){ saveChangesAndReturnToMainPage(); // Reopen edit pane - edit = driver.findElement(By.cssSelector("[href=\"../forms/edit-work-case.xml?doc=incipit_demo.xml\"]")); - edit.click(); + editButton = driver.findElement(By.xpath("//form[@action='http://localhost:8080/forms/edit-work-case.xml'][input/@value='incipit_demo.xml']/button")); + editButton.click(); // wait for page to have loaded wait.until(ExpectedConditions.elementToBeClickable(By.xpath("//span[@id='xf-293']/a"))); @@ -203,8 +203,8 @@ public void checkWorkTabPopupInputText(){ Actions builder = new Actions(driver); enterLogin(); - WebElement edit = driver.findElement(By.cssSelector("[href=\"../forms/edit-work-case.xml?doc=incipit_demo.xml\"]")); - edit.click(); + WebElement editButton = driver.findElement(By.xpath("//form[@action='http://localhost:8080/forms/edit-work-case.xml'][input/@value='incipit_demo.xml']/button")); + editButton.click(); // hover over "add more titles" WebElement addTitlesButton = @@ -243,9 +243,11 @@ public void checkWorkTabPopupInputText(){ // Save changes and return to main menu saveChangesAndReturnToMainPage(); + // Reopen edit pane - edit = driver.findElement(By.cssSelector("[href=\"../forms/edit-work-case.xml?doc=incipit_demo.xml\"]")); - edit.click(); + editButton = driver.findElement(By.xpath("//form[@action='http://localhost:8080/forms/edit-work-case.xml'][input/@value='incipit_demo.xml']/button")); + editButton.click(); + // check changes for (String id: changedIds) {