From 7f6fabb0b6410737d0e538c3f4c2964c26dadf91 Mon Sep 17 00:00:00 2001 From: Kevin Roast Date: Tue, 9 May 2006 20:05:27 +0000 Subject: [PATCH] . Soft Delete UI checkpoint - List of deleted items for the Admin user - Search for deleted items by Name and full-text - First pass at Recover single item - First pass at Delete single item - Icons for Soft Delete UI from Linton git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@2801 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261 --- config/alfresco/messages/webclient.properties | 7 + config/alfresco/web-client-config-actions.xml | 1 + .../org/alfresco/web/bean/TrashcanBean.java | 457 +++++++++++++++++- .../repo/renderer/NodePathLinkRenderer.java | 2 +- source/web/WEB-INF/faces-config-beans.xml | 8 + source/web/images/icons/recover.gif | Bin 553 -> 1078 bytes source/web/images/icons/recover_all.gif | Bin 553 -> 1046 bytes source/web/images/icons/recover_all_large.gif | Bin 1304 -> 1341 bytes source/web/images/icons/recover_large.gif | Bin 1304 -> 1473 bytes source/web/images/icons/trashcan.gif | Bin 970 -> 608 bytes source/web/images/icons/trashcan_large.gif | Bin 778 -> 916 bytes source/web/jsp/trashcan/trash-list.jsp | 15 +- 12 files changed, 470 insertions(+), 20 deletions(-) diff --git a/config/alfresco/messages/webclient.properties b/config/alfresco/messages/webclient.properties index 3dc181ee0f..da271c7044 100644 --- a/config/alfresco/messages/webclient.properties +++ b/config/alfresco/messages/webclient.properties @@ -778,6 +778,7 @@ original_location=Original Location deleted_date=Date Deleted deleted_user=Deleted by User recover=Recover +clear_search_results=Clear Search Results search_deleted_items=Search Deleted Items delete_item=Delete Item delete_item_info=Permanently delete an item from the deleted file store @@ -797,6 +798,12 @@ delete_listed_items_confirm=Are you sure you want to permanently delete the list recover_listed_items=Recover Listed Items recover_listed_items_info=Recover the listed files and spaces from the deleted file store recover_listed_items_confirm=Are you sure you want to recover the listed deleted files and spaces from the deleted file store? +recovered_item_success=The item \"{0}\" has been successfully recovered. +recovered_item_parent=Failed to recover the item \"{0}\" as the original parent folder is missing, please select a new folder destination. +recovered_item_permission=Failed to recover the item \"{0}\" as you do not have the appropriate permissions to restore the item to the original parent folder, please select a new folder destination. +recovered_item_integrity=Failed to recover the item \"{0}\" as there is now an item in the original parent folder with the same name, please select a new folder destination. +recovered_item_failure=Failed to recover the item \"{0}\" due to error: {1} +delete_item_success=The item \"{0}\" has been permanently deleted. # Admin Console messages title_admin_console=Administration Console diff --git a/config/alfresco/web-client-config-actions.xml b/config/alfresco/web-client-config-actions.xml index cb3ac79ca3..4817bbb163 100644 --- a/config/alfresco/web-client-config-actions.xml +++ b/config/alfresco/web-client-config-actions.xml @@ -378,6 +378,7 @@ manage_deleted_items /images/icons/trashcan.gif + #{TrashcanBean.setupTrashcan} dialog:manageDeletedItems diff --git a/source/java/org/alfresco/web/bean/TrashcanBean.java b/source/java/org/alfresco/web/bean/TrashcanBean.java index 3e4882b9e2..cd212494f4 100644 --- a/source/java/org/alfresco/web/bean/TrashcanBean.java +++ b/source/java/org/alfresco/web/bean/TrashcanBean.java @@ -16,13 +16,41 @@ */ package org.alfresco.web.bean; +import java.text.MessageFormat; +import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Map; +import javax.faces.application.FacesMessage; +import javax.faces.context.FacesContext; +import javax.faces.event.ActionEvent; +import javax.transaction.UserTransaction; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.node.archive.NodeArchiveService; +import org.alfresco.repo.node.archive.RestoreNodeReport; +import org.alfresco.repo.node.archive.RestoreNodeReport.RestoreStatus; +import org.alfresco.repo.search.impl.lucene.QueryParser; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.repository.InvalidNodeRefException; +import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.Path; +import org.alfresco.service.cmr.search.ResultSet; +import org.alfresco.service.cmr.search.ResultSetRow; +import org.alfresco.service.cmr.search.SearchParameters; import org.alfresco.service.cmr.search.SearchService; +import org.alfresco.service.namespace.QName; +import org.alfresco.web.app.Application; import org.alfresco.web.app.context.IContextListener; +import org.alfresco.web.bean.repository.MapNode; import org.alfresco.web.bean.repository.Node; +import org.alfresco.web.bean.repository.NodePropertyResolver; +import org.alfresco.web.bean.repository.QNameNodeMap; +import org.alfresco.web.bean.repository.Repository; +import org.alfresco.web.ui.common.Utils; +import org.alfresco.web.ui.common.component.UIActionLink; import org.alfresco.web.ui.common.component.data.UIRichList; /** @@ -30,17 +58,52 @@ import org.alfresco.web.ui.common.component.data.UIRichList; */ public class TrashcanBean implements IContextListener { + private static final String MSG_RECOVERED_ITEM_INTEGRITY = "recovered_item_integrity"; + private static final String MSG_RECOVERED_ITEM_PERMISSION = "recovered_item_permission"; + private static final String MSG_RECOVERED_ITEM_PARENT = "recovered_item_parent"; + private static final String MSG_RECOVERED_ITEM_FAILURE = "recovered_item_failure"; + private static final String MSG_RECOVERED_ITEM_SUCCESS = "recovered_item_success"; + + private static final String OUTCOME_DIALOGCLOSE = "dialog:close"; + + private static final String RICHLIST_ID = "trashcan-list"; + private static final String RICHLIST_MSG_ID = "trashcan" + ':' + RICHLIST_ID; + + private final static String NAME_ATTR = Repository.escapeQName(ContentModel.PROP_NAME); + + private final static String SEARCH_ALL = "PARENT:\"%s\" AND ASPECT:\"%s\""; + private final static String SEARCH_NAME = "PARENT:\"%s\" AND ASPECT:\"%s\" AND (@" + NAME_ATTR + ":*%s* TEXT:%s)"; + private final static String SEARCH_NAME_QUOTED = "PARENT:\"%s\" AND ASPECT:\"%s\" AND (@" + NAME_ATTR + ":\"%s\" TEXT:\"%s\")"; + /** NodeService bean reference */ protected NodeService nodeService; + + /** NodeArchiveService bean reference */ + protected NodeArchiveService nodeArchiveService; /** SearchService bean reference */ protected SearchService searchService; + /** The DictionaryService bean reference */ + protected DictionaryService dictionaryService; + /** Component reference for Deleted Items RichList control */ protected UIRichList itemsRichList; /** Search text */ - private String searchText; + private String searchText = null; + + /** We show an empty list until a Search or Show All is executed */ + private boolean showItems = false; + + /** Currently listed items */ + private List listedItems = Collections.emptyList(); + + /** Current action context Node */ + private Node actionNode; + + /** Root node to the spaces store archive store*/ + private NodeRef archiveRootRef = null; // ------------------------------------------------------------------------------ @@ -55,13 +118,29 @@ public class TrashcanBean implements IContextListener } /** - * @param searchService the search service + * @param searchService The search service */ public void setSearchService(SearchService searchService) { this.searchService = searchService; } + /** + * @param nodeArchiveService The nodeArchiveService to set. + */ + public void setNodeArchiveService(NodeArchiveService nodeArchiveService) + { + this.nodeArchiveService = nodeArchiveService; + } + + /** + * @param dictionaryService The DictionaryService to set. + */ + public void setDictionaryService(DictionaryService dictionaryService) + { + this.dictionaryService = dictionaryService; + } + /** * @return Returns the itemsRichList. */ @@ -94,31 +173,382 @@ public class TrashcanBean implements IContextListener this.searchText = searchText; } + /** + * @return Returns the listed items. + */ + public List getListedItems() + { + return this.listedItems; + } + + /** + * @param listedItems The listed items to set. + */ + public void setListedItems(List listedItems) + { + this.listedItems = listedItems; + } + + /** + * @param node The item context for the current action + */ + public void setItem(Node node) + { + this.actionNode = node; + } + + /** + * @return the item context for the current action + */ + public Node getItem() + { + return this.actionNode; + } + /** * @return the list of deleted items to display */ public List getItems() { - // TODO: need the following MapNode properties: - // deletedDate, locationPath, displayPath, deletedUsername [only for admin user] - // TODO: get deleted items from deleted items store - // use a search - also use filters by name/username - return Collections.emptyList(); + // to get deleted items from deleted items store + // use a search to find the items - also filters by name/username + List itemNodes = null; + + UserTransaction tx = null; + ResultSet results = null; + try + { + tx = Repository.getUserTransaction(FacesContext.getCurrentInstance(), true); + tx.begin(); + + // get the root node to the deleted items store + if (getArchiveRootRef() != null && this.showItems == true) + { + String query = getSearchQuery(); + SearchParameters sp = new SearchParameters(); + sp.setLanguage(SearchService.LANGUAGE_LUCENE); + sp.setQuery(query); + sp.addStore(getArchiveRootRef().getStoreRef()); // the Archived Node store + + results = this.searchService.query(sp); + itemNodes = new ArrayList(results.length()); + } + + if (results != null && results.length() != 0) + { + for (ResultSetRow row : results) + { + NodeRef nodeRef = row.getNodeRef(); + + if (this.nodeService.exists(nodeRef)) + { + QName type = this.nodeService.getType(nodeRef); + + if (this.dictionaryService.isSubClass(type, ContentModel.TYPE_FOLDER) == true && + this.dictionaryService.isSubClass(type, ContentModel.TYPE_SYSTEM_FOLDER) == false) + { + MapNode node = new MapNode(nodeRef, this.nodeService, false); + node.addPropertyResolver("locationPath", resolverLocationPath); + node.addPropertyResolver("displayPath", resolverDisplayPath); + node.addPropertyResolver("deletedDate", resolverDeletedDate); + node.addPropertyResolver("deletedBy", resolverDeletedBy); + node.addPropertyResolver("typeIcon", this.resolverSmallIcon); + itemNodes.add(node); + } + else + { + MapNode node = new MapNode(nodeRef, this.nodeService, false); + node.addPropertyResolver("locationPath", resolverLocationPath); + node.addPropertyResolver("displayPath", resolverDisplayPath); + node.addPropertyResolver("deletedDate", resolverDeletedDate); + node.addPropertyResolver("deletedBy", resolverDeletedBy); + node.addPropertyResolver("typeIcon", this.resolverFileType16); + itemNodes.add(node); + } + } + } + } + + tx.commit(); + } + catch (Throwable err) + { + Utils.addErrorMessage(MessageFormat.format(Application.getMessage( + FacesContext.getCurrentInstance(), Repository.ERROR_GENERIC), new Object[] {err.getMessage()}), err ); + try { if (tx != null) {tx.rollback();} } catch (Exception tex) {} + } + finally + { + if (results != null) + { + results.close(); + } + } + + this.listedItems = (itemNodes != null ? itemNodes : Collections.emptyList()); + + return this.listedItems; } + private NodePropertyResolver resolverLocationPath = new NodePropertyResolver() { + public Object get(Node node) { + //ChildAssociationRef childRef = (ChildAssociationRef)node.getProperties().get(ContentModel.PROP_ARCHIVED_ORIGINAL_PARENT_ASSOC); + //return nodeService.getPath(childRef.getChildRef()); + return (Path)node.getProperties().get(ContentModel.PROP_ARCHIVED_ORIGINAL_PATH); + } + }; + + private NodePropertyResolver resolverDisplayPath = new NodePropertyResolver() { + public Object get(Node node) { + //ChildAssociationRef childRef = (ChildAssociationRef)node.getProperties().get(ContentModel.PROP_ARCHIVED_ORIGINAL_PARENT_ASSOC); + return Repository.getDisplayPath((Path)node.getProperties().get(ContentModel.PROP_ARCHIVED_ORIGINAL_PATH)); + } + }; + + private NodePropertyResolver resolverFileType16 = new NodePropertyResolver() { + public Object get(Node node) { + return Utils.getFileTypeImage(node.getName(), true); + } + }; + + private NodePropertyResolver resolverSmallIcon = new NodePropertyResolver() { + public Object get(Node node) { + QNameNodeMap props = (QNameNodeMap)node.getProperties(); + String icon = (String)props.getRaw("app:icon"); + return "/images/icons/" + (icon != null ? icon + "-16.gif" : BrowseBean.SPACE_SMALL_DEFAULT + ".gif"); + } + }; + + private NodePropertyResolver resolverDeletedDate = new NodePropertyResolver() { + public Object get(Node node) { + return node.getProperties().get(ContentModel.PROP_ARCHIVED_DATE); + } + }; + + private NodePropertyResolver resolverDeletedBy = new NodePropertyResolver() { + public Object get(Node node) { + return node.getProperties().get(ContentModel.PROP_ARCHIVED_BY); + } + }; + // ------------------------------------------------------------------------------ // Action handlers // TODO: - // need the following navigation outcomes - // DONE deleteItem, recoverItem, recoverAllItems, deleteAllItems, recoverListedItems, deleteListedItems // need the following Action Handlers: // deleteItemOK, recoverItemOK, deleteAllItemsOK, recoverAllItemsOK, recoverListedItemsOK, deleteListedItemsOK - // and following Action Event Handlers: - // setupItemAction, search - // and following getters: - // listedItems, item (setup by setupItemAction!) + + /** + * Search the deleted item store by name + */ + public void search(ActionEvent event) + { + // simply clear the current list and refresh the screen + // the search query text will be found and processed by the getItems() method + contextUpdated(); + this.showItems = true; + } + + /** + * Action handler to clear the current search results and show all items + */ + public void clearSearch(ActionEvent event) + { + contextUpdated(); + this.searchText = null; + this.showItems = true; + } + + /** + * Action handler called to prepare the selected item for an action + */ + public void setupItemAction(ActionEvent event) + { + UIActionLink link = (UIActionLink)event.getComponent(); + Map params = link.getParameterMap(); + String id = params.get("id"); + if (id != null && id.length() != 0) + { + try + { + // create the node ref, then our node representation + NodeRef ref = new NodeRef(getArchiveRootRef().getStoreRef(), id); + Node node = new Node(ref); + + // resolve icon in-case one has not been set + //node.addPropertyResolver("icon", this.resolverSpaceIcon); + + // prepare a node for the action context + setItem(node); + } + catch (InvalidNodeRefException refErr) + { + Utils.addErrorMessage(MessageFormat.format(Application.getMessage( + FacesContext.getCurrentInstance(), Repository.ERROR_NODEREF), new Object[] {id}) ); + } + } + else + { + setItem(null); + } + + // clear the UI state in preparation for finishing the next action + contextUpdated(); + } + + public String deleteItemOK() + { + Node item = getItem(); + if (item != null) + { + try + { + this.nodeArchiveService.purgeArchivedNode(item.getNodeRef()); + + FacesContext fc = FacesContext.getCurrentInstance(); + String msg = MessageFormat.format( + Application.getMessage(fc, "delete_item_success"), item.getName()); + FacesMessage facesMsg = new FacesMessage(FacesMessage.SEVERITY_INFO, msg, msg); + fc.addMessage(RICHLIST_MSG_ID, facesMsg); + } + catch (Throwable err) + { + Utils.addErrorMessage(MessageFormat.format(Application.getMessage( + FacesContext.getCurrentInstance(), Repository.ERROR_GENERIC), err.getMessage()), err); + } + } + return OUTCOME_DIALOGCLOSE; + } + + public String recoverItemOK() + { + String outcome = null; + + Node item = getItem(); + if (item != null) + { + FacesContext fc = FacesContext.getCurrentInstance(); + try + { + String msg; + FacesMessage errorfacesMsg = null; + + RestoreNodeReport report = this.nodeArchiveService.restoreArchivedNode(item.getNodeRef()); + switch (report.getStatus()) + { + case SUCCESS: + msg = MessageFormat.format( + Application.getMessage(fc, MSG_RECOVERED_ITEM_SUCCESS), item.getName()); + FacesMessage facesMsg = new FacesMessage(FacesMessage.SEVERITY_INFO, msg, msg); + fc.addMessage(RICHLIST_MSG_ID, facesMsg); + outcome = OUTCOME_DIALOGCLOSE; + break; + + case FAILURE_INVALID_PARENT: + msg = MessageFormat.format( + Application.getMessage(fc, MSG_RECOVERED_ITEM_PARENT), item.getName()); + errorfacesMsg = new FacesMessage(FacesMessage.SEVERITY_ERROR, msg, msg); + break; + + case FAILURE_PERMISSION: + msg = MessageFormat.format( + Application.getMessage(fc, MSG_RECOVERED_ITEM_PERMISSION), item.getName()); + errorfacesMsg = new FacesMessage(FacesMessage.SEVERITY_ERROR, msg, msg); + break; + + case FAILURE_INTEGRITY: + msg = MessageFormat.format( + Application.getMessage(fc, MSG_RECOVERED_ITEM_INTEGRITY), item.getName()); + errorfacesMsg = new FacesMessage(FacesMessage.SEVERITY_ERROR, msg, msg); + break; + + default: + String reason = report.getCause().getMessage(); + msg = MessageFormat.format( + Application.getMessage(fc, MSG_RECOVERED_ITEM_FAILURE), item.getName(), reason); + errorfacesMsg = new FacesMessage(FacesMessage.SEVERITY_ERROR, msg, msg); + break; + } + + // report the failure if one occured we stay on the current screen + if (errorfacesMsg != null) + { + fc.addMessage(null, errorfacesMsg); + } + } + catch (Throwable err) + { + // most exceptions will be caught and returned as RestoreNodeReport objects by the service + String reason = err.getMessage(); + String msg = MessageFormat.format( + Application.getMessage(fc, MSG_RECOVERED_ITEM_FAILURE), item.getName(), reason); + FacesMessage facesMsg = new FacesMessage(FacesMessage.SEVERITY_ERROR, msg, msg); + fc.addMessage(null, facesMsg); + } + } + + return outcome; + } + + /** + * Action handler to reset all filters and search + */ + public void resetAll(ActionEvent event) + { + // TODO: reset all filter and search + } + + /** + * Action handler to initially setup the trashcan screen + */ + public void setupTrashcan(ActionEvent event) + { + contextUpdated(); + } + + + // ------------------------------------------------------------------------------ + // Private helpers + + /** + * @return the archive store root node ref + */ + private NodeRef getArchiveRootRef() + { + if (this.archiveRootRef == null) + { + this.archiveRootRef = this.nodeArchiveService.getStoreArchiveNode(Repository.getStoreRef()); + } + return this.archiveRootRef; + } + + /** + * @return the search query to use when displaying the list of deleted items + */ + private String getSearchQuery() + { + String query; + if (this.searchText == null || this.searchText.length() == 0) + { + // search for ALL items in the archive store + query = String.format(SEARCH_ALL, archiveRootRef, ContentModel.ASPECT_ARCHIVED); + } + else + { + // search by name in the archive store + String safeText = QueryParser.escape(this.searchText); + if (safeText.indexOf(' ') == -1) + { + query = String.format(SEARCH_NAME, archiveRootRef, ContentModel.ASPECT_ARCHIVED, safeText, safeText); + } + else + { + query = String.format(SEARCH_NAME_QUOTED, archiveRootRef, ContentModel.ASPECT_ARCHIVED, safeText, safeText); + } + } + return query; + } // ------------------------------------------------------------------------------ @@ -133,5 +563,6 @@ public class TrashcanBean implements IContextListener { this.itemsRichList.setValue(null); } + this.showItems = false; } } diff --git a/source/java/org/alfresco/web/ui/repo/renderer/NodePathLinkRenderer.java b/source/java/org/alfresco/web/ui/repo/renderer/NodePathLinkRenderer.java index 4736f9bd22..75c5d131a6 100644 --- a/source/java/org/alfresco/web/ui/repo/renderer/NodePathLinkRenderer.java +++ b/source/java/org/alfresco/web/ui/repo/renderer/NodePathLinkRenderer.java @@ -244,7 +244,7 @@ public class NodePathLinkRenderer extends BaseRenderer } } - if (disabled == false) + if (disabled == false && lastElementRef != null) { return renderPathElement(context, component, lastElementRef, buf.toString()); } diff --git a/source/web/WEB-INF/faces-config-beans.xml b/source/web/WEB-INF/faces-config-beans.xml index 3812693aba..fd1d2f52a9 100644 --- a/source/web/WEB-INF/faces-config-beans.xml +++ b/source/web/WEB-INF/faces-config-beans.xml @@ -1483,10 +1483,18 @@ nodeService #{NodeService} + + nodeArchiveService + #{nodeArchiveService} + searchService #{SearchService} + + dictionaryService + #{DictionaryService} + diff --git a/source/web/images/icons/recover.gif b/source/web/images/icons/recover.gif index c49ece5db07707b47c0c9eef3f993dd407d46a60..2cc19c2f109f7ef0d116460609a4b55f4b5af6a3 100644 GIT binary patch delta 913 zcmV;C18)4O1hxo&M@dFFIbje05CHeU0QUCw@$m3Jq+0Io?(6I8#N*8M_V(f8;o{=r z?dGbsVF(MzaoMW$fq z=jQ0>=qi~^OQ&P7u&}bSvOS?#Td#b*+{M}0*rcMOqtUv5<>lpwg@xqhFQ^*j-a2P zfq#Cy)55LR!oJ?eo}8V}&d&Ar_lSsyiin6FlS5{+dO)OH{r&t|u78EXr0(wREt*R4 z^7H2A=8DCrxD=r(){s>)_yj;3k(v`S|$q^78Za^qiZU*Vos+ zzP+lcsp;wJ!{O58<>kl5#)-$7udc60q+dFYP-C%p<>lm}p`mMNXXfVRFPc~2-{3){ zU1YL(Ub1@M-{0rw=RKlWB9=o-sc7oz>hkgNrKF^|+{oeK;P3A4d%273?Cs#--_z33 zsMNkxrlqEAwvvpBiZ!28XSRRz^zZ9d;q8d5-HF_ z0e2)=cu|A`uO9#)R3M|{127IyJ8JUK;REBe*q6iz`5eX)}{asR1kPD=IIh(HUe}2fp*IP0-6+f z0Y*eXoeOU8tVpvp4;KX*8Uf3dB$XsJLXHHPH0Z&GI4DQ}yVOZnFC1Y=m2kDl(j!KN zm;gJJsa6yTx8$^;g~i022nRSloP`NP2s)_`qBYjXXgIPd~W>A)MV4#2%b3qq)MIU)dAbU$JfmAbZ)MFe!h)tt9ogsc6+&qfxeA}!I6Nth>*vYlE#*Sw1bz*nx4*`inxfQ(4dmO zj-}C|rqH0O)1#`=qO8=Vu-B)v*r>JHskhpyx7ew=+pN6Yt-jr^xYnk#(W1fMuf^fA z!{4yS;#w$SFc)91L?>ATwNyx#4;;qJiY@WSfy#Q*>Q zlTiX=eYF06QVs&LaQ- diff --git a/source/web/images/icons/recover_all.gif b/source/web/images/icons/recover_all.gif index c49ece5db07707b47c0c9eef3f993dd407d46a60..29c35e8a24dc7dd588e8f7990e399d73e2643696 100644 GIT binary patch delta 744 zcmVw9 zsHn2p#_{p-TwGjzeSKcCdbQffHJ?*(aBxPZXr!d1;qm5wEO|hW$E+cfK~M|9E1F3~remYgx~SB?j>xFP;nGE>U~g}4*VorJo?K^!Z%v|PB$Yx(sA96R zvK*2>!^6X~%DJzvuZ)b0Y`K6QlS7r2m1nb#v$L~tadBT?UqPf@goK2Pi;Ieiia?}X zx3{-cR8+Ws+{mn~tXZyqJ)&5Jg@tg4cZb8K*4Eao*212io{Gh&KBHU3#l?+{ja;#F zhK7c{-prM>|;1Nz!CXm^~@@`o%g03K%yVu*87zVou+< zSSw&CR3^a*Eo0K?x%?NBiaHq%jff*fM;|jbj+90yp@O1CE!H@=)22ojtXm-+K=DH= ag#dgE0N_`qBYjXXgIPd~W>A)MV4#2%b3qq)MIU)dAbU$JfmAbZ)MFe!h)tt9ogsc6+&qfxeA}!I6Nth>*vYlE#*Sw1bz*nx4*`inxfQ(4dmO zj-}C|rqH0O)1#`=qO8=Vu-B)v*r>JHskhpyx7ew=+pN6Yt-jr^xYnk#(W1fMuf^fA z!{4yS;#w$SFc)91L?>ATwNyx#4;;qJiY@WSfy#Q*>Q zlfeOFeYF06Q&7&T;?% diff --git a/source/web/images/icons/recover_all_large.gif b/source/web/images/icons/recover_all_large.gif index 035d0764208c35b46ecada9a2cd8eaefa42eb8a8..aef3ea4484649178aa0a2067ccb4e1e2362ce220 100644 GIT binary patch delta 1217 zcmV;y1U~zi3cU({M@dFFIbk3GAOQEu0NB{rySuxuudlJOvFPaN$H&Lw;^K{sjeLB3 zq@<*ai;EMFG}+nNva+(MsHl{bl(@LKyu7^U=jS4pLA12Am6esx&(Eu?tDBpfwY9aN zprEX*tk>7q_xJa`y}iG`zihdH@bK`txw*Q!y0EaYrq#25uCA^>q*~$O;e>>QmzS5| z;NYpLsV0|6x3{FI}uhbx*(r>Cb*t7q!!>htsS#N*6{hK9t%#NOWC(b3VT)4rpl zqn@6gfq{XJj*c;%O|{&={r&xScXzY1v((horKP2S5%rCzXt&*$4bqE|YhR?Fsp*FU6Oot>SI$E$RVd+hA&qtUxx zu!!*Z@|w-Gp`oE_wUC8{g@C}I%*@PmlYDf%j9jjUx7*5zppT-_xlo*qgkcAs~nO+7mzyL-Q7i}U@MtWhr^~mqg#fm>+3|OV7uMTeZi7rw0lmfYL?5dip8iK6#D@zp+$i#NQBs!1QHDi3RGMyh$Mjt3J?gf zh%mxMh6W@|U_juZb%)KB1q^^9C_up2g8?Xx2`C020Oo_IUJ2W= zD;FowqDdP&Fu;In)~*o(VRFb!p+J*xCmUUwx9`<|tzR2#@kP|8GPp1)XTH1l@8}3G zt^b)3M45sF4?-{3?fiT2g9|_mJfA*ybMMgK8=OG!d?NMSwU?iC1uf8D0t@J3Uw6*| zM<78HoG@PkRp3#=diOO5-a$}wQOOuK_|b?aa)44ofcQCxP#(1C(TXd3bfU-^ZbX6# zdns;zXwV5xP{9f#Pc-qy4uHTCgCrTc_uxQ5{Gx;-vkcJPizQmBn2-%1%&@=$EiCfL1h(L)p?(&f7ljl+Xo2FLOCEGy7H9~9A%jYe zWL^STl(L|JYFgI#LB9h(HN6P@&}ThcN))z{@deTrkQJKPYNJ8&(J! fFoFm#nQ(y&324B_92LN!Ku(GVE9|fq1q1*)vPN=_ delta 1194 zcmV;b1XcUJ3YZFiM@dFFIbk3G9{~3N00000|Ns9xoohXwY(b!HL!oakm|ioQVn(BI zNu+Wsms(7wb5EyrQmA$$lu#y>Q&g#URjPPct9c@nO*o!kTdjLtu6$mueIS!bNu_CF zuzq8)e`K+MX0m~4w1XvI<`$4R8cfEaq!G@K|nP|3beZPQ~%9)tUnwQFdns&W-mdcl#%$%Cbn}WiFkH?Rk z&77ako}SH}h{c7X(4e8upODFnqR*d>$cUxUp`_8Cr_!RP(V>*ei=xn*snesZ)upS{ zq?*lsIu3mv)HJ$*{QbKs<+yzw%Mt$*q*uDtGe5) zyWFgQzuvCC-LAddt+m>qx!j|{-mk;pvBco9!r!pH-lfOlv&rMM$l|ld;j+cyt;*xH z%jC7q<+jb_wa?|Y(dM_)=eX49xz*^p*y_91>AKqMyxHo!-R-{I?7iUbzu)b@;qJiX z@WJEn!R7G6=JCVn^2F%z!|U_K?DWO&^~dfJ^v3b_$n*Ef_4vy4_sReN|C4b7fqx$@72^>gJAd!Ly6DlOw4|`y72(cnVJFX~Ntk}bj zw2T`$dW@DzW5|RZr$Kv$%H&Cga-cb5wk*_+A2@LmDRLxa4>``7DMMCl)}Be3Dp@+j zCX5mpK#V}_5$Bn*WXFaH1LjK?u76swSgA5K>QS2{J9?swL(^EXV86QNx&=#Ct5T;v zb=pKp(ij>+5KhH7raNDvY>tWbgn9c&PSkh6&~NEt#7X#)uy zXkbAGJ18`p7i2_9P!Tm;P=N#x$gt3CO?XMr4~3^>gwwC_4V%V?&jv^;o;%*^z`TF=l1sY-rnB&`uY=(H2nPh_xJbT z-{1K7`1$$yt*x!2qodv3-PP6A`}_N$p`rc#{msqIqN1XI%*@Q#*x1|K+sVnvsHmu{ ztgO`3)ZE&Gq*@@AKZ(bg zRjPnNqG!W@;>*9^&%WQwhQpU)x@69x3k&DF`Z4u#>Sn?sXC!nfWV-`+Q~7UPovSh6_7WY z&9tYdrx%bqGn`$~(b1a7rbDA_uh+yOl|>eiI9#rOhO5=VPO56P+`qZl!kD+A%;wl$ zu!5YoIZ=GUgwvbo&LdAyt;ltiM@y0Y25Jfc@Npi`WjoL{hro64s}r(n0+${>?S;NalW z($eee>;M1%laT^^lb`}Ae>LyKoA)WIu2}M0Yk`=7%ZU7 z;DBX;6d+Qvb?0Dw*!G1w|s=-^X zE|H3LVJp|B1puh%pu)qiA2AC8X(UAqCtMdc2u|76CjbK#NRTkmg$D|b1Do){Bt(#+ zLl-h=rqC7cVFd^d93$a@#Q}n6P_4;S1(=fyoIA7pYCyq*(8q{)Fm{aKkWY5L;K?KB zf`gtCX2-34TXX~6f4m8TA;ySTjsV!RV~_i}`LPBJdhGx1J@5h$fhGVHSkE57Vdl8M zGoUk`;EaJu3>fg^{(WuR@qw3sF(pJ--asiRpuh!b{HNe;0Wy%l1WFW0o8i~iqe<$II3$P%9ipZ#OiY=go zfyFG4&>~A4b>#S+fD=NnzyKipcp)-Nm=c8}!7}bObV3oZ z3M7M?Z_rSRnrf~`uCxOk(ZRm}udo6PIWX+7!y_uh;e!uH@ZiBJARvGc p0L6e{%*7aItTDzQ6azp)0L16y$Ry`@00IDTK-9`Cx4d#d06T`N!ma=S delta 1194 zcmV;b1XcUN3z!OjM@dFFIbk3G9{~3N00000|Ns9xoohXwY(b!HL!oakm|ioQVn(BI zNu+Wsms(7wb5EyrQmA$$lu#y>Q&g#URjPPct9c@nO*o!kTdjLtu6$mueIS!bNu_CF zuzq8)e`K+MX0m~4w1XvI<`$4R8cfEaq!G@K|nP|3beZPQ~%9)tUnwQFdns&W-mdcl#%$%Cbn}WiFkH?Rk z&77ako}SH}h{c7X(4e8upODFnqR*d>$cUxUp`_8Cr_!RP(V>*ei=xn*snesZ)upS{ zq?*lsIu3mv)HJ$*{QbKs<+yzw%Mt$*q*uDtGe5) zyWFgQzuvCC-LAddt+m>qx!j|{-mk;pvBco9!r!pH-lfOlv&rMM$l|ld;j+cyt;*xH z%jC7q<+jb_wa?|Y(dM_)=eX49xz*^p*y_91>AKqMyxHo!-R-{I?7iUbzu)b@;qJiX z@WJEn!R7G6=JCVn^2F%z!|U_K?DWO&^~dfJ^v3b_$n*Ef_4vy4_sReN|C4b7fqx$@72^>gJAd!Ly6DlOw4|`y72(cnVJFX~Ntk}bj zw2T`$dW@DzW5|RZr$Kv$%H&Cga-cb5wk*_+A2@LmDRLxa4>``7DMMCl)}Be3Dp@+j zCX5mpK#V}_5$Bn*WXFaH1LjK?u76swSgA5K>QS2{J9?swL(^EXV86QNx&=#Ct5T;v zb=pKp(ij>+5KhH7raNDvY>tWbgn9c&PSkh6&~NEt#7X#)uy zXkbAGJ18`p7i2_9P!Tm;P=N#x$gt3CO?XMr4+yDRnhK7c|zP_=sv51I>qobp#si}W|f6L3uwzjt3-rmN>#`X2}k&%(& z;^M)FMcz)YR1a`up+m@w>ab?Cb2L zq@-C{S?=!c_V)Gp`1#k@*NcmbW@cuDgoKQYjLpr>va+(o#KfTVD-s}L88i?xV2WZu85SB73q&3qVzwMS z4n0C)GE)o77Z`$K9V1CNUS9xV2M0L@4$le#2nhw&*as&YP2S*C;^azHQZBV)V&CE8 z*DpRnS@QJj_hB#;sdA-X2O}Fu41wV!jFlEDO7y#MB!vVPCbm2wz>z?|8((x$2+$=0 XlO2j`G|*unf`=soVqk%iC=dWU5r`FZ literal 970 zcmZ?wbhEHbQCnMEUtizY*x1z6)ZEBU#HmxKPMTOUwr%_N?K^ht*u8uAzJ2=+9z1yH(4oVJ4<9*l^|W diff --git a/source/web/images/icons/trashcan_large.gif b/source/web/images/icons/trashcan_large.gif index cfe75d348b54f6a400630de26c5e0223e61611ee..2ddf47312e5e5b061f738b2d26913497a54fe945 100644 GIT binary patch delta 887 zcmV--1Bm>J29yVXM@dFFIbk3GAOPlY0F;!J+1c5Ae0+_Kji;xl$;rvFv9aal<>%+; zrKP3f;^OP;>-6;W?(XjQ_xG~0vcJE-@bK{C8`G>&(F{5 z>gx9P_O!IL#Kgq>{QUX(`M9{afq{Xyx3{UOsjaQ8sHmuahK7dT-rkXskU2PZ*O;EcX!Il%KG~H*4EaAg@u-umZ71cpP!%7($d@8+nJe}!^6Yf-QAs?ouZIw~qRMJP%&iI##R zJrSERE*c1GMVEz6Y#1CMRwFn+A{Gxl7&~qy1PKW^mnIx32o7(+OcWP9Fd1nOAP+hv zFS@+Gz;D9D#>mRd&X!IgZZ{+#6(R;iV?sMsC~kjbNFiuOqfli(6Tl7&Vo3!+MkQB6 zZjeA31xF2s8z+E4D4<~t5DPH&37OHu4M8<*00aOgAWfS!01QkhU;_n<3KmCU)X34} zngIq{q{tAZ#R?S=RA$`xrsKx}1ZH^fAjJWL8WxURTFKMrO9BNxd|0x90*Me$mhNo% zlR$rm4K93WXmdaU4XrGDx&*p{fd-j0@onNv#FXpP3NjD~C2AC@(nEFCb`{YeNYtrS zuVw|jRq)dX3nqMZuz^97PALrqb_x_jn-gsrP-qgN!Gg?%Hm@Zx;y@O>G9tui;Nt>n zLaMv6kr04zg9a`ZTtIMpQ0}KD+|Yo$V1a*z;RlKfIu4Wtgan@3v_Y_@tn`A7IU7(= zAVKQ`2Z97qFyTA(@#S|*P{5wY3HRaG!cUOgjsO2=F_xBi_{rmg-`uh6#`1tnr_VxAk z^YioV?d|F5>E`C<<>lq$f$jHaX$HvCS#l^*c!otG8zrVe`y}Z1%^r>Cc;rlzH( zrKF^!qobpuqN1Upp`V|ho}Qkaot>PVoSU1QnwpxKnVFcFn3tEAmX?;4m6eo~l#`Q_ zk&%&)kB^N(jg5?qjEjqlii(Peh=_-WhlYlRg@uKLgoJ~GgMxyBfq{X3etvy@eSCa; zdwY9&dU|(vcXV`ga&mHQZEgSm|C3<>ER%2n9g}bY1%Dy=1OWg5WdJMy0000W03ZMW z2mgR&f`f#GhKGoTAtWk4M@LLhRaiBNmV!Y$44VxO5foccmxU=tJvTE+K`${hHaR;z zPFg}kMMW={UN<8Y3I+lL2@4V#A0;R)GdVv(UAnx!z{14F$jZ#lmnmUdOg=L(G&(>; zN={K#T7O(%VPZI=WLGg52n7ld8X+exI6_NOw16Qqs?3%fOg0cO(Bedd3NCDX;HSjI z4lM<@7@5)mfq^k7CRCWB1H%a(RXo7JI7%Z&k03>6(Q)Bq%9R;6cKjH!!pxc|RkFO~ za%W7RG;Ipa`I4v1q(O1E)CsfaPo+|cQgwUHVVrbn+v1)E4LSG8Yf znHseUHSXEDWU;bE     +
<%-- Filter controls --%> @@ -172,7 +173,7 @@ - + @@ -195,12 +196,12 @@ - <%-- Username column --%> + <%-- Deleted by user column --%> - + - + <%-- Actions column --%> @@ -208,10 +209,10 @@ - + - + @@ -219,6 +220,8 @@ + +