diff --git a/config/alfresco/messages/webclient.properties b/config/alfresco/messages/webclient.properties index 4643ac42d8..5438f502c4 100644 --- a/config/alfresco/messages/webclient.properties +++ b/config/alfresco/messages/webclient.properties @@ -130,6 +130,7 @@ view_in_webdav=View in WebDAV view_in_cifs=View in CIFS download_content=Download Content details_page_bookmark=External Access URL +noderef_link=Alfresco Node Reference links=Links create_shortcut=Create Shortcut navigation=Navigation @@ -277,6 +278,13 @@ show_results_categories=Show me results in the categories include_sub_categories=Include sub-categories also_search_results=More search options additional_options=Additional options +save_search=Save Search +saved_searches=Saved Searches +title_save_search=Save Search Query +save_search_description=Save a search query for use again later +search_props=Saved Search Properties +select_saved_search=Select a Saved Search... +saved_search_warning=This operation will attempt to overwrite the existing saved search ''{0}'' # Forum messages forums=Forum Space @@ -894,6 +902,8 @@ error_import_no_file=Can not find an ACP file to import! error_import_empty_file=You can not import an empty ACP file! error_import_all=Please correct the import errors below then click OK. error_export_all=Please correct the export errors below then click OK. +error_save_search=Failed to save search due to error: {0} +error_restore_search=Failed to restore saved search due to error: {0} # Confirmations return_to_application=Return to application diff --git a/source/java/org/alfresco/web/app/Application.java b/source/java/org/alfresco/web/app/Application.java index fc89f49d50..36d1ae2d5a 100644 --- a/source/java/org/alfresco/web/app/Application.java +++ b/source/java/org/alfresco/web/app/Application.java @@ -68,6 +68,7 @@ public class Application private static String glossaryFolderName; private static String spaceTemplatesFolderName; private static String contentTemplatesFolderName; + private static String savedSearchesFolderName; /** * Private constructor to prevent instantiation of this class @@ -261,7 +262,7 @@ public class Application } /** - * @return Returns the root path for the application (retrieved from config service) + * @return Returns the root path for the application */ public static String getRootPath(ServletContext context) { @@ -269,7 +270,7 @@ public class Application } /** - * @return Returns the root path for the application (retrieved from config service) + * @return Returns the root path for the application */ public static String getRootPath(FacesContext context) { @@ -277,7 +278,7 @@ public class Application } /** - * @return Returns the glossary folder name (retrieved from config service) + * @return Returns the glossary folder name */ public static String getGlossaryFolderName(ServletContext context) { @@ -285,7 +286,7 @@ public class Application } /** - * @return Returns the glossary folder name (retrieved from config service) + * @return Returns the glossary folder name */ public static String getGlossaryFolderName(FacesContext context) { @@ -293,7 +294,7 @@ public class Application } /** - * @return Returns the Space templates folder name (retrieved from config service) + * @return Returns the Space templates folder name */ public static String getSpaceTemplatesFolderName(ServletContext context) { @@ -301,7 +302,7 @@ public class Application } /** - * @return Returns the Space templates folder name (retrieved from config service) + * @return Returns the Space templates folder name */ public static String getSpaceTemplatesFolderName(FacesContext context) { @@ -309,7 +310,7 @@ public class Application } /** - * @return Returns the Content templates folder name (retrieved from config service) + * @return Returns the Content templates folder name */ public static String getContentTemplatesFolderName(ServletContext context) { @@ -317,13 +318,29 @@ public class Application } /** - * @return Returns the Content templates folder name (retrieved from config service) + * @return Returns the Content templates folder name */ public static String getContentTemplatesFolderName(FacesContext context) { return getContentTemplatesFolderName(FacesContextUtils.getRequiredWebApplicationContext(context)); } + /** + * @return Return the Saved Searches folder name + */ + public static String getSavedSearchesFolderName(ServletContext context) + { + return getSavedSearchesFolderName(WebApplicationContextUtils.getRequiredWebApplicationContext(context)); + } + + /** + * @return Return the Saved Searches folder name + */ + public static String getSavedSearchesFolderName(FacesContext context) + { + return getSavedSearchesFolderName(FacesContextUtils.getRequiredWebApplicationContext(context)); + } + /** * Set the language locale for the current user context * @@ -530,7 +547,7 @@ public class Application } /** - * Returns the repository store URL (retrieved from config service) + * Returns the repository store URL * * @param context The spring context * @return The repository store URL to use @@ -547,7 +564,7 @@ public class Application } /** - * Returns the root path for the application (retrieved from config service) + * Returns the root path for the application * * @param context The spring context * @return The application root path @@ -565,7 +582,7 @@ public class Application } /** - * Returns the glossary folder name (retrieved from config service) + * Returns the glossary folder name * * @param context The spring context * @return The glossary folder name @@ -583,7 +600,7 @@ public class Application } /** - * Returns the Space Templates folder name (retrieved from config service) + * Returns the Space Templates folder name * * @param context The spring context * @return The templates folder name @@ -601,7 +618,7 @@ public class Application } /** - * Returns the Content Templates folder name (retrieved from config service) + * Returns the Content Templates folder name * * @param context The spring context * @return The templates folder name @@ -618,6 +635,24 @@ public class Application return contentTemplatesFolderName; } + /** + * Returns the Saved Searches folder name + * + * @param context The spring context + * @return The saved searches folder name + */ + private static String getSavedSearchesFolderName(WebApplicationContext context) + { + if (savedSearchesFolderName == null) + { + ImporterBootstrap bootstrap = (ImporterBootstrap)context.getBean(BEAN_IMPORTER_BOOTSTRAP); + Properties configuration = bootstrap.getConfiguration(); + savedSearchesFolderName = configuration.getProperty("spaces.savedsearches.childname"); + } + + return savedSearchesFolderName; + } + /** * Retrieves the configured error page for the application * diff --git a/source/java/org/alfresco/web/bean/AdvancedSearchBean.java b/source/java/org/alfresco/web/bean/AdvancedSearchBean.java index fb6b80286d..5d4a1fc490 100644 --- a/source/java/org/alfresco/web/bean/AdvancedSearchBean.java +++ b/source/java/org/alfresco/web/bean/AdvancedSearchBean.java @@ -16,6 +16,8 @@ */ package org.alfresco.web.bean; +import java.io.Serializable; +import java.text.MessageFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; @@ -26,25 +28,36 @@ import java.util.Map; import javax.faces.component.UISelectBoolean; import javax.faces.context.FacesContext; import javax.faces.event.ActionEvent; +import javax.faces.event.ValueChangeEvent; import javax.faces.model.DataModel; import javax.faces.model.ListDataModel; import javax.faces.model.SelectItem; +import javax.transaction.UserTransaction; import org.alfresco.config.ConfigService; import org.alfresco.model.ContentModel; +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.repo.security.permissions.AccessDeniedException; import org.alfresco.service.ServiceRegistry; import org.alfresco.service.cmr.dictionary.AspectDefinition; import org.alfresco.service.cmr.dictionary.DataTypeDefinition; import org.alfresco.service.cmr.dictionary.DictionaryService; import org.alfresco.service.cmr.dictionary.PropertyDefinition; import org.alfresco.service.cmr.dictionary.TypeDefinition; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.ContentWriter; import org.alfresco.service.cmr.repository.MimetypeService; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.search.SearchService; +import org.alfresco.service.cmr.security.PermissionService; import org.alfresco.service.namespace.NamespaceService; import org.alfresco.service.namespace.QName; import org.alfresco.util.CachingDateFormat; import org.alfresco.web.app.Application; +import org.alfresco.web.bean.SearchContext.RangeProperties; import org.alfresco.web.bean.repository.MapNode; import org.alfresco.web.bean.repository.Node; import org.alfresco.web.bean.repository.Repository; @@ -52,6 +65,7 @@ import org.alfresco.web.config.ClientConfigElement; import org.alfresco.web.config.ClientConfigElement.CustomProperty; import org.alfresco.web.data.IDataContainer; import org.alfresco.web.data.QuickSort; +import org.alfresco.web.ui.common.Utils; import org.alfresco.web.ui.common.component.UIPanel.ExpandedEvent; import org.alfresco.web.ui.repo.component.UICategorySelector; import org.alfresco.web.ui.repo.component.UISearchCustomProperties; @@ -73,9 +87,9 @@ public class AdvancedSearchBean public AdvancedSearchBean() { // initial state of progressive panels that don't use the default - panels.put("categories-panel", false); - panels.put("attrs-panel", false); - panels.put("custom-panel", false); + panels.put(PANEL_CATEGORIES, false); + panels.put(PANEL_ATTRS, false); + panels.put(PANEL_CUSTOM, false); } @@ -106,6 +120,14 @@ public class AdvancedSearchBean this.namespaceService = namespaceService; } + /** + * @param searchService the search service + */ + public void setSearchService(SearchService searchService) + { + this.searchService = searchService; + } + /** * @return Returns the progressive panels expanded state map. */ @@ -122,6 +144,70 @@ public class AdvancedSearchBean this.panels = panels; } + /** + * @return Returns the saved search Description. + */ + public String getSearchDescription() + { + return this.searchDescription; + } + + /** + * @param searchDescription The saved search Description to set. + */ + public void setSearchDescription(String searchDescription) + { + this.searchDescription = searchDescription; + } + + /** + * @return Returns the saved search Name. + */ + public String getSearchName() + { + return this.searchName; + } + + /** + * @param searchName The saved search Name to set. + */ + public void setSearchName(String searchName) + { + this.searchName = searchName; + } + + /** + * @return ID of the last saved search selected by the user + */ + public String getSavedSearch() + { + return this.savedSearch; + } + + /** + * @param savedSearch ID of the saved search selected by the user + */ + public void setSavedSearch(String savedSearch) + { + this.savedSearch = savedSearch; + } + + /** + * @return name of the saved search to edit + */ + public String getEditSearchName() + { + return this.editSearchName; + } + + /** + * @param editSearchName name of the saved search to edit + */ + public void setEditSearchName(String editSearchName) + { + this.editSearchName = editSearchName; + } + /** * @return Returns the folder to search, null for all. */ @@ -496,19 +582,28 @@ public class AdvancedSearchBean * Handler to clear the advanced search screen form details */ public void reset(ActionEvent event) + { + resetFields(); + this.savedSearch = null; + } + + private void resetFields() { this.text = ""; this.mode = MODE_ALL; this.lookin = LOOKIN_ALL; this.contentType = null; + this.contentFormat = null; this.location = null; - this.locationChildren = false; + this.locationChildren = true; this.categories = new ArrayList(2); this.title = null; this.description = null; this.author = null; this.createdDateFrom = null; + this.createdDateTo = null; this.modifiedDateFrom = null; + this.modifiedDateTo = null; this.createdDateChecked = false; this.modifiedDateChecked = false; this.customProperties.clear(); @@ -519,8 +614,6 @@ public class AdvancedSearchBean */ public String search() { - String outcome = null; - if (this.text != null && this.text.length() != 0) { // construct the Search Context and set on the navigation bean @@ -637,8 +730,7 @@ public class AdvancedSearchBean // location path search if (this.lookin.equals(LOOKIN_OTHER) && this.location != null) { - search.setLocation(SearchContext.getPathFromSpaceRef( - new NodeRef(Repository.getStoreRef(), this.location.getId()), this.locationChildren)); + search.setLocation(SearchContext.getPathFromSpaceRef(this.location, this.locationChildren)); } // category path search @@ -648,7 +740,7 @@ public class AdvancedSearchBean for (int i=0; i props = null; + + // handle Edit e.g. Overwrite of existing search + // detect if was previously selected saved search (e.g. NodeRef not null) + boolean edit = false; + if (this.savedSearch != null && NO_SELECTION.equals(this.savedSearch) == false) + { + NodeRef searchRef = new NodeRef(Repository.getStoreRef(), this.savedSearch); + edit = (this.nodeService.exists(searchRef)); + } + + ContentService contentService = Repository.getServiceRegistry(context).getContentService(); + ContentWriter writer; + if (edit) + { + // edit existing saved search + NodeRef searchRef = new NodeRef(Repository.getStoreRef(), this.savedSearch); + props = this.nodeService.getProperties(searchRef); + props.put(ContentModel.PROP_NAME, this.searchName); + props.put(ContentModel.PROP_DESCRIPTION, this.searchDescription); + this.nodeService.setProperties(searchRef, props); + + writer = contentService.getWriter(searchRef, ContentModel.PROP_CONTENT, true); + } + else + { + // create new content node as the saved search object + props = new HashMap(2, 1.0f); + props.put(ContentModel.PROP_NAME, this.searchName); + props.put(ContentModel.PROP_DESCRIPTION, this.searchDescription); + ChildAssociationRef childRef = this.nodeService.createNode( + searchesRef, + ContentModel.ASSOC_CONTAINS, + QName.createQName(NamespaceService.ALFRESCO_URI, QName.createValidLocalName(this.searchName)), + ContentModel.TYPE_CONTENT, + props); + + writer = contentService.getWriter(childRef.getChildRef(), ContentModel.PROP_CONTENT, true); + } + + // get a writer to our new node ready for XML content + writer.setMimetype(MimetypeMap.MIMETYPE_XML); + writer.setEncoding("UTF-8"); + + // output an XML serialized version of the SearchContext object + writer.putContent(search.toXML()); + + tx.commit(); + + this.savedSearch = null; + } + catch (Throwable e) + { + try { if (tx != null) {tx.rollback();} } catch (Exception ex) {} + Utils.addErrorMessage(MessageFormat.format(Application.getMessage( + FacesContext.getCurrentInstance(), MSG_ERROR_SAVE_SEARCH), e.getMessage()), e); + outcome = null; + } } return outcome; } + /** + * @return list of saved searches as SelectItem objects + */ + public List getSavedSearches() + { + // TODO: cache for 1 minute? - dirty cache when new search is saved! + FacesContext fc = FacesContext.getCurrentInstance(); + String xpath = ".//*"; + + ServiceRegistry services = Repository.getServiceRegistry(fc); + + List savedSearches = null; + NodeRef searchesRef = getSavedSearchesRef(); + if (searchesRef != null) + { + List results = searchService.selectNodes( + searchesRef, + xpath, + null, + namespaceService, + false); + savedSearches = new ArrayList(results.size() + 1); + if (results.size() != 0) + { + DictionaryService dd = services.getDictionaryService(); + for (NodeRef ref : results) + { + Node childNode = new Node(ref); + if (dd.isSubClass(childNode.getType(), ContentModel.TYPE_CONTENT)) + { + savedSearches.add(new SelectItem(childNode.getId(), childNode.getName())); + } + } + + // make sure the list is sorted by the label + QuickSort sorter = new QuickSort(savedSearches, "label", true, IDataContainer.SORT_CASEINSENSITIVE); + sorter.sort(); + } + } + else + { + // handle missing folder case + savedSearches = new ArrayList(1); + } + + // add an entry (at the start) to instruct the user to select a saved search + savedSearches.add(0, new SelectItem(NO_SELECTION, + Application.getMessage(FacesContext.getCurrentInstance(), MSG_SELECT_SAVED_SEARCH))); + + return savedSearches; + } + + + /** + * Action handler called when a saved search is selected by the user + */ + public void selectSearch(ActionEvent event) + { + if (NO_SELECTION.equals(savedSearch) == false) + { + // read an XML serialized version of the SearchContext object + NodeRef searchSearchRef = new NodeRef(Repository.getStoreRef(), savedSearch); + ServiceRegistry services = Repository.getServiceRegistry(FacesContext.getCurrentInstance()); + ContentService cs = services.getContentService(); + try + { + if (services.getNodeService().exists(searchSearchRef)) + { + ContentReader reader = cs.getReader(searchSearchRef, ContentModel.PROP_CONTENT); + SearchContext search = new SearchContext().fromXML(reader.getContentString()); + + // if we get here we read the serialized object successfully + // now setup the UI to match the new SearchContext object + initialiseFromContext(search); + } + } + catch (Throwable err) + { + Utils.addErrorMessage(MessageFormat.format(Application.getMessage( + FacesContext.getCurrentInstance(), MSG_ERROR_RESTORE_SEARCH), err.getMessage()), err); + } + } + } + + /** + * Initialise the Advanced Search UI screen from a SearchContext + * + * @param search the SearchContext to retrieve state from + */ + private void initialiseFromContext(SearchContext search) + { + resetFields(); + + this.text = search.getText(); + + switch (search.getMode()) + { + case SearchContext.SEARCH_ALL: + this.mode = MODE_ALL; + break; + case SearchContext.SEARCH_FILE_NAMES_CONTENTS: + this.mode = MODE_FILES_TEXT; + break; + case SearchContext.SEARCH_FILE_NAMES: + this.mode = MODE_FILES; + break; + case SearchContext.SEARCH_SPACE_NAMES: + this.mode = MODE_FOLDERS; + break; + } + this.panels.put(PANEL_RESTRICT, true); + + if (search.getLocation() != null) + { + this.locationChildren = search.getLocation().endsWith("//*"); + this.location = findNodeRefFromPath(search.getLocation()); + this.lookin = LOOKIN_OTHER; + this.panels.put(PANEL_LOCATION, true); + } + + String[] categories = search.getCategories(); + if (categories != null && categories.length != 0) + { + for (String category : categories) + { + NodeRef categoryRef = findNodeRefFromPath(category); + if (categoryRef != null) + { + Node categoryNode = new MapNode(categoryRef); + // add a value bound propery used to indicate if searching across children is selected + categoryNode.getProperties().put(INCLUDE_CHILDREN, category.endsWith("//*")); + this.categories.add(categoryNode); + } + } + this.panels.put(PANEL_CATEGORIES, true); + } + + this.contentType = search.getContentType(); + this.contentFormat = search.getMimeType(); + + this.description = search.getAttributeQuery(ContentModel.PROP_DESCRIPTION); + this.title = search.getAttributeQuery(ContentModel.PROP_TITLE); + this.author = search.getAttributeQuery(ContentModel.PROP_AUTHOR); + if (this.contentType != null || this.contentFormat != null || + this.description != null || this.title != null || this.author != null) + { + this.panels.put(PANEL_ATTRS, true); + } + + RangeProperties createdDate = search.getRangeProperty(ContentModel.PROP_CREATED); + if (createdDate != null) + { + this.createdDateFrom = Utils.parseXMLDateFormat(createdDate.lower); + this.createdDateTo = Utils.parseXMLDateFormat(createdDate.upper); + this.createdDateChecked = true; + this.panels.put(PANEL_ATTRS, true); + } + RangeProperties modifiedDate = search.getRangeProperty(ContentModel.PROP_MODIFIED); + if (modifiedDate != null) + { + this.modifiedDateFrom = Utils.parseXMLDateFormat(modifiedDate.lower); + this.modifiedDateTo = Utils.parseXMLDateFormat(modifiedDate.upper); + this.modifiedDateChecked = true; + this.panels.put(PANEL_ATTRS, true); + } + + // custom fields - calculate which are required to set through the custom properties lookup table + for (String qname : getCustomPropertyLookup().keySet()) + { + DataTypeDefinition typeDef = getCustomPropertyLookup().get(qname); + if (typeDef != null) + { + QName typeName = typeDef.getName(); + if (DataTypeDefinition.DATE.equals(typeName) || DataTypeDefinition.DATETIME.equals(typeName)) + { + RangeProperties dateProps = search.getRangeProperty(QName.createQName(qname)); + if (dateProps != null) + { + this.customProperties.put(UISearchCustomProperties.PREFIX_DATE_FROM + qname, + Utils.parseXMLDateFormat(dateProps.lower)); + this.customProperties.put(UISearchCustomProperties.PREFIX_DATE_TO + qname, + Utils.parseXMLDateFormat(dateProps.upper)); + this.customProperties.put(qname, true); + this.panels.put(PANEL_CUSTOM, true); + } + } + else if (DataTypeDefinition.BOOLEAN.equals(typeName)) + { + String strBool = search.getFixedValueQuery(QName.createQName(qname)); + if (strBool != null) + { + this.customProperties.put(qname, Boolean.parseBoolean(strBool)); + this.panels.put(PANEL_CUSTOM, true); + } + } + else if (DataTypeDefinition.NODE_REF.equals(typeName) || DataTypeDefinition.CATEGORY.equals(typeName)) + { + String strNodeRef = search.getFixedValueQuery(QName.createQName(qname)); + if (strNodeRef != null) + { + this.customProperties.put(qname, new NodeRef(strNodeRef)); + this.panels.put(PANEL_CUSTOM, true); + } + } + else if (DataTypeDefinition.INT.equals(typeName) || DataTypeDefinition.LONG.equals(typeName) || + DataTypeDefinition.FLOAT.equals(typeName) || DataTypeDefinition.DOUBLE.equals(typeName)) + { + // currently numbers are rendered as text in UISearchCustomProperties component + // this code will need updating if that changes! + this.customProperties.put(qname, search.getFixedValueQuery(QName.createQName(qname))); + this.panels.put(PANEL_CUSTOM, true); + } + else + { + this.customProperties.put(qname, search.getAttributeQuery(QName.createQName(qname))); + this.panels.put(PANEL_CUSTOM, true); + } + } + } + } + + /** + * Return NodeRef to the last Node referenced on the end of the specified xpath value + * + * @param xpath XPath - note that any /* or //* will be removed to find trailing node + * + * @return NodeRef if found null otherwise + */ + private NodeRef findNodeRefFromPath(String xpath) + { + if (xpath.endsWith("//*")) + { + xpath = xpath.substring(0, xpath.lastIndexOf("//*")); + } + else if (xpath.endsWith("/*")) + { + xpath = xpath.substring(0, xpath.lastIndexOf("/*")); + } + NodeRef rootRef = new NodeRef(Repository.getStoreRef(), Application.getCompanyRootId()); + List results = null; + try + { + results = searchService.selectNodes( + rootRef, + xpath, + null, + namespaceService, + false); + } + catch (AccessDeniedException err) + { + // ignore and return null + } + + return (results != null && results.size() == 1) ? results.get(0) : null; + } + + /** + * @return the cached reference to the shared Saved Searches folder + */ + private NodeRef getSavedSearchesRef() + { + if (savedSearchesRef == null) + { + FacesContext fc = FacesContext.getCurrentInstance(); + String xpath = Application.getRootPath(fc) + "/" + + Application.getGlossaryFolderName(fc) + "/" + + Application.getSavedSearchesFolderName(fc); + + NodeRef rootNodeRef = this.nodeService.getRootNode(Repository.getStoreRef()); + List results = null; + try + { + results = searchService.selectNodes( + rootNodeRef, + xpath, + null, + namespaceService, + false); + } + catch (AccessDeniedException err) + { + // ignore and return null + } + + if (results != null && results.size() == 1) + { + savedSearchesRef = results.get(0); + } + } + + return savedSearchesRef; + } + /** * Action handler called when the Add button is pressed to add the current Category selection */ @@ -683,7 +1187,7 @@ public class AdvancedSearchBean { Node categoryNode = new MapNode(categoryRef); // add a value bound propery used to indicate if searching across children is selected - categoryNode.getProperties().put("includeChildren", chkChildren.isSelected()); + categoryNode.getProperties().put(INCLUDE_CHILDREN, chkChildren.isSelected()); this.categories.add(categoryNode); } } @@ -768,6 +1272,17 @@ public class AdvancedSearchBean private static final String MSG_CONTENT = "content"; private static final String MSG_ALL_FORMATS = "all_formats"; + private static final String MSG_ERROR_SAVE_SEARCH = "error_save_search"; + private static final String MSG_ERROR_RESTORE_SEARCH = "error_restore_search"; + private static final String MSG_SELECT_SAVED_SEARCH = "select_saved_search"; + + private static final String PANEL_CUSTOM = "custom-panel"; + private static final String PANEL_ATTRS = "attrs-panel"; + private static final String PANEL_CATEGORIES = "categories-panel"; + private static final String PANEL_RESTRICT = "restrict-panel"; + private static final String PANEL_LOCATION = "location-panel"; + + private static final String INCLUDE_CHILDREN = "includeChildren"; private static final String MODE_ALL = "all"; private static final String MODE_FILES_TEXT = "files_text"; @@ -777,6 +1292,8 @@ public class AdvancedSearchBean private static final String LOOKIN_ALL = "all"; private static final String LOOKIN_OTHER = "other"; + private static final String NO_SELECTION = "none"; + /** The NodeService to be used by the bean */ private NodeService nodeService; @@ -786,12 +1303,19 @@ public class AdvancedSearchBean /** The NavigationBean reference */ private NavigationBean navigator; + /** SearchService bean reference */ + private SearchService searchService; + /** Client Config reference */ private ClientConfigElement clientConfigElement = null; /** Progressive panel UI state */ private Map panels = new HashMap(5, 1.0f); + /** Saved search properties */ + private String searchName; + private String searchDescription; + /** custom property names to values */ private Map customProperties = new HashMap(5, 1.0f); @@ -854,4 +1378,10 @@ public class AdvancedSearchBean private boolean modifiedDateChecked = false; private boolean createdDateChecked = false; + + private NodeRef savedSearchesRef = null; + + private String savedSearch = null; + + private String editSearchName = null; } diff --git a/source/java/org/alfresco/web/bean/BrowseBean.java b/source/java/org/alfresco/web/bean/BrowseBean.java index 16ef142b38..4278a15381 100644 --- a/source/java/org/alfresco/web/bean/BrowseBean.java +++ b/source/java/org/alfresco/web/bean/BrowseBean.java @@ -83,9 +83,6 @@ public class BrowseBean implements IContextListener // ------------------------------------------------------------------------------ // Construction - private static final String VIEWMODE_DASHBOARD = "dashboard"; - private static final String PAGE_NAME_BROWSE = "browse"; - /** * Default Constructor */ @@ -1443,6 +1440,9 @@ public class BrowseBean implements IContextListener public static final String BROWSE_VIEW_ID = "/jsp/browse/browse.jsp"; + private static final String VIEWMODE_DASHBOARD = "dashboard"; + private static final String PAGE_NAME_BROWSE = "browse"; + /** I18N messages */ private static final String MSG_ERROR_DELETE_FILE = "error_delete_file"; private static final String MSG_ERROR_DELETE_SPACE = "error_delete_space"; diff --git a/source/java/org/alfresco/web/bean/DocumentDetailsBean.java b/source/java/org/alfresco/web/bean/DocumentDetailsBean.java index 1db1ddfecf..847e7aa3b4 100644 --- a/source/java/org/alfresco/web/bean/DocumentDetailsBean.java +++ b/source/java/org/alfresco/web/bean/DocumentDetailsBean.java @@ -198,6 +198,16 @@ public class DocumentDetailsBean return Utils.generateURL(FacesContext.getCurrentInstance(), getDocument(), URLMode.CIFS); } + /** + * Return the Alfresco NodeRef URL for the current document + * + * @return the Alfresco NodeRef URL + */ + public String getNodeRefUrl() + { + return getDocument().getNodeRef().toString(); + } + /** * Determines whether the current document is versionable * diff --git a/source/java/org/alfresco/web/bean/SearchContext.java b/source/java/org/alfresco/web/bean/SearchContext.java index c9b1a9dea9..ab5b622893 100644 --- a/source/java/org/alfresco/web/bean/SearchContext.java +++ b/source/java/org/alfresco/web/bean/SearchContext.java @@ -17,13 +17,19 @@ package org.alfresco.web.bean; import java.io.Serializable; +import java.io.StringReader; +import java.io.StringWriter; +import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; +import java.util.Iterator; +import java.util.List; import java.util.Map; import java.util.StringTokenizer; import javax.faces.context.FacesContext; +import org.alfresco.error.AlfrescoRuntimeException; import org.alfresco.model.ContentModel; import org.alfresco.repo.search.impl.lucene.QueryParser; import org.alfresco.service.cmr.repository.ChildAssociationRef; @@ -35,6 +41,12 @@ import org.alfresco.util.ISO9075; import org.alfresco.web.bean.repository.Repository; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.dom4j.Document; +import org.dom4j.DocumentHelper; +import org.dom4j.Element; +import org.dom4j.io.OutputFormat; +import org.dom4j.io.SAXReader; +import org.dom4j.io.XMLWriter; /** * Holds the context required to build a search query and can return the populated query. @@ -43,6 +55,28 @@ import org.apache.commons.logging.LogFactory; */ public final class SearchContext implements Serializable { + private static final long serialVersionUID = 6730844584074229969L; + + /** XML serialization elements */ + private static final String ELEMENT_VALUE = "value"; + private static final String ELEMENT_FIXED_VALUES = "fixed-values"; + private static final String ELEMENT_INCLUSIVE = "inclusive"; + private static final String ELEMENT_UPPER = "upper"; + private static final String ELEMENT_LOWER = "lower"; + private static final String ELEMENT_RANGE = "range"; + private static final String ELEMENT_RANGES = "ranges"; + private static final String ELEMENT_NAME = "name"; + private static final String ELEMENT_ATTRIBUTE = "attribute"; + private static final String ELEMENT_ATTRIBUTES = "attributes"; + private static final String ELEMENT_MIMETYPE = "mimetype"; + private static final String ELEMENT_CONTENT_TYPE = "content-type"; + private static final String ELEMENT_CATEGORY = "category"; + private static final String ELEMENT_CATEGORIES = "categories"; + private static final String ELEMENT_LOCATION = "location"; + private static final String ELEMENT_MODE = "mode"; + private static final String ELEMENT_TEXT = "text"; + private static final String ELEMENT_SEARCH = "search"; + /** Search mode constants */ public final static int SEARCH_ALL = 0; public final static int SEARCH_FILE_NAMES_CONTENTS = 1; @@ -55,18 +89,12 @@ public final class SearchContext implements Serializable /** mode for the search */ private int mode = SearchContext.SEARCH_ALL; - /** folder node location for the search */ + /** folder XPath location for the search */ private String location = null; /** categories to add to the search */ private String[] categories = new String[0]; - /** true to search location children as well as location */ - private boolean locationChildren = true; - - /** true to search category children as well as category */ - private boolean categoryChildren = true; - /** content type to restrict search against */ private String contentType = null; @@ -96,7 +124,7 @@ public final class SearchContext implements Serializable String query; // the QName for the well known "name" attribute - String nameAttr = Repository.escapeQName(QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "name")); + String nameAttr = Repository.escapeQName(QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, ELEMENT_NAME)); // match against content text String text = this.text.trim(); @@ -375,7 +403,7 @@ public final class SearchContext implements Serializable } /** - * @return Returns the node to search from or null for all. + * @return Returns the node XPath to search in or null for all. */ public String getLocation() { @@ -383,7 +411,7 @@ public final class SearchContext implements Serializable } /** - * @param location The node to search from or null for all.. + * @param location The node XPATH to search from or null for all.. */ public void setLocation(String location) { @@ -452,38 +480,6 @@ public final class SearchContext implements Serializable { this.mimeType = mimeType; } - - /** - * @return Returns true to search location children, false for just the specified location. - */ - public boolean getLocationChildren() - { - return this.locationChildren; - } - - /** - * @param locationChildren True to search location children, false for just the specified location. - */ - public void setLocationChildren(boolean locationChildren) - { - this.locationChildren = locationChildren; - } - - /** - * @return Returns true to search category children, false for just the specified category. - */ - public boolean getCategoryChildren() - { - return this.categoryChildren; - } - - /** - * @param categoryChildren True to search category children, false for just the specified category. - */ - public void setCategoryChildren(boolean categoryChildren) - { - this.categoryChildren = categoryChildren; - } /** * Add an additional attribute to search against @@ -496,6 +492,11 @@ public final class SearchContext implements Serializable this.queryAttributes.put(qname, value); } + public String getAttributeQuery(QName qname) + { + return this.queryAttributes.get(qname); + } + /** * Add an additional range attribute to search against * @@ -509,6 +510,11 @@ public final class SearchContext implements Serializable this.rangeAttributes.put(qname, new RangeProperties(qname, lower, upper, inclusive)); } + public RangeProperties getRangeProperty(QName qname) + { + return this.rangeAttributes.get(qname); + } + /** * Add an additional fixed value attribute to search against * @@ -520,11 +526,216 @@ public final class SearchContext implements Serializable this.queryFixedValues.put(qname, value); } + public String getFixedValueQuery(QName qname) + { + return this.queryFixedValues.get(qname); + } + + /** + * @return this SearchContext as XML + * + * Example: + * + * + * + * CDATA + * int + * XPath + * + * XPath + * + * String + * String + * + * String + * + * + * + * String + * String + * boolean + * + * + * + * String + * + * + * + */ + public String toXML() + { + try + { + NamespaceService ns = Repository.getServiceRegistry(FacesContext.getCurrentInstance()).getNamespaceService(); + + Document doc = DocumentHelper.createDocument(); + + Element root = doc.addElement(ELEMENT_SEARCH); + + root.addElement(ELEMENT_TEXT).addCDATA(this.text); + root.addElement(ELEMENT_MODE).addText(Integer.toString(this.mode)); + if (this.location != null) + { + root.addElement(ELEMENT_LOCATION).addText(this.location); + } + + Element categories = root.addElement(ELEMENT_CATEGORIES); + for (String path : this.categories) + { + categories.addElement(ELEMENT_CATEGORY).addText(path); + } + + if (this.contentType != null) + { + root.addElement(ELEMENT_CONTENT_TYPE).addText(this.contentType); + } + if (this.mimeType != null) + { + root.addElement(ELEMENT_MIMETYPE).addText(this.mimeType); + } + + Element attributes = root.addElement(ELEMENT_ATTRIBUTES); + for (QName attrName : this.queryAttributes.keySet()) + { + attributes.addElement(ELEMENT_ATTRIBUTE) + .addAttribute(ELEMENT_NAME, attrName.toPrefixString(ns)) + .addCDATA(this.queryAttributes.get(attrName)); + } + + Element ranges = root.addElement(ELEMENT_RANGES); + for (QName rangeName : this.rangeAttributes.keySet()) + { + RangeProperties rangeProps = this.rangeAttributes.get(rangeName); + Element range = ranges.addElement(ELEMENT_RANGE); + range.addAttribute(ELEMENT_NAME, rangeName.toPrefixString(ns)); + range.addElement(ELEMENT_LOWER).addText(rangeProps.lower); + range.addElement(ELEMENT_UPPER).addText(rangeProps.upper); + range.addElement(ELEMENT_INCLUSIVE).addText(Boolean.toString(rangeProps.inclusive)); + } + + Element values = root.addElement(ELEMENT_FIXED_VALUES); + for (QName valueName : this.queryFixedValues.keySet()) + { + values.addElement(ELEMENT_VALUE) + .addAttribute(ELEMENT_NAME, valueName.toPrefixString(ns)) + .addCDATA(this.queryFixedValues.get(valueName)); + } + + StringWriter out = new StringWriter(1024); + XMLWriter writer = new XMLWriter(OutputFormat.createPrettyPrint()); + writer.setWriter(out); + writer.write(doc); + + return out.toString(); + } + catch (Throwable err) + { + throw new AlfrescoRuntimeException("Failed to export SearchContext to XML.", err); + } + } + + /** + * Restore a SearchContext from an XML definition + * + * @param xml XML format SearchContext @see #toXML() + */ + public SearchContext fromXML(String xml) + { + try + { + NamespaceService ns = Repository.getServiceRegistry(FacesContext.getCurrentInstance()).getNamespaceService(); + + // get the root element + SAXReader reader = new SAXReader(); + Document document = reader.read(new StringReader(xml)); + Element rootElement = document.getRootElement(); + Element textElement = rootElement.element(ELEMENT_TEXT); + if (textElement != null) + { + this.text = textElement.getText(); + } + Element modeElement = rootElement.element(ELEMENT_MODE); + if (modeElement != null) + { + this.mode = Integer.parseInt(modeElement.getText()); + } + Element locationElement = rootElement.element(ELEMENT_LOCATION); + if (locationElement != null) + { + this.location = locationElement.getText(); + } + Element categoriesElement = rootElement.element(ELEMENT_CATEGORIES); + if (categoriesElement != null) + { + List categories = new ArrayList(4); + for (Iterator i=categoriesElement.elementIterator(ELEMENT_CATEGORY); i.hasNext(); /**/) + { + Element categoryElement = (Element)i.next(); + categories.add(categoryElement.getText()); + } + this.categories = categories.toArray(this.categories); + } + Element contentTypeElement = rootElement.element(ELEMENT_CONTENT_TYPE); + if (contentTypeElement != null) + { + this.contentType = contentTypeElement.getText(); + } + Element mimetypeElement = rootElement.element(ELEMENT_MIMETYPE); + if (mimetypeElement != null) + { + this.mimeType = mimetypeElement.getText(); + } + Element attributesElement = rootElement.element(ELEMENT_ATTRIBUTES); + if (attributesElement != null) + { + for (Iterator i=attributesElement.elementIterator(ELEMENT_ATTRIBUTE); i.hasNext(); /**/) + { + Element attrElement = (Element)i.next(); + QName qname = QName.createQName(attrElement.attributeValue(ELEMENT_NAME), ns); + addAttributeQuery(qname, attrElement.getText()); + } + } + Element rangesElement = rootElement.element(ELEMENT_RANGES); + if (rangesElement != null) + { + for (Iterator i=rangesElement.elementIterator(ELEMENT_RANGE); i.hasNext(); /**/) + { + Element rangeElement = (Element)i.next(); + Element lowerElement = rangeElement.element(ELEMENT_LOWER); + Element upperElement = rangeElement.element(ELEMENT_UPPER); + Element incElement = rangeElement.element(ELEMENT_INCLUSIVE); + if (lowerElement != null && upperElement != null && incElement != null) + { + QName qname = QName.createQName(rangeElement.attributeValue(ELEMENT_NAME), ns); + addRangeQuery(qname, + lowerElement.getText(), upperElement.getText(), + Boolean.parseBoolean(incElement.getText())); + } + } + } + + Element valuesElement = rootElement.element(ELEMENT_FIXED_VALUES); + if (valuesElement != null) + { + for (Iterator i=valuesElement.elementIterator(ELEMENT_VALUE); i.hasNext(); /**/) + { + Element valueElement = (Element)i.next(); + QName qname = QName.createQName(valueElement.attributeValue(ELEMENT_NAME), ns); + addFixedValueQuery(qname, valueElement.getText()); + } + } + } + catch (Throwable err) + { + throw new AlfrescoRuntimeException("Failed to import SearchContext from XML.", err); + } + return this; + } /** * Simple wrapper class for range query attribute properties */ - private static class RangeProperties + static class RangeProperties { QName qname; String lower; diff --git a/source/java/org/alfresco/web/bean/SpaceDetailsBean.java b/source/java/org/alfresco/web/bean/SpaceDetailsBean.java index 4b960d46fe..a6de8163f2 100644 --- a/source/java/org/alfresco/web/bean/SpaceDetailsBean.java +++ b/source/java/org/alfresco/web/bean/SpaceDetailsBean.java @@ -117,7 +117,7 @@ public class SpaceDetailsBean } /** - * Returns the id of the current document + * Returns the id of the current space * * @return The id */ @@ -127,9 +127,9 @@ public class SpaceDetailsBean } /** - * Returns the name of the current document + * Returns the name of the current space * - * @return Name of the current document + * @return Name of the current space */ public String getName() { @@ -137,7 +137,7 @@ public class SpaceDetailsBean } /** - * Returns the WebDAV URL for the current document + * Returns the WebDAV URL for the current space * * @return The WebDAV url */ @@ -147,7 +147,7 @@ public class SpaceDetailsBean } /** - * Returns the URL to access the details page for the current document + * Returns the URL to access the details page for the current space * * @return The bookmark URL */ @@ -157,7 +157,7 @@ public class SpaceDetailsBean } /** - * Returns the CIFS path for the current document + * Returns the CIFS path for the current space * * @return The CIFS path */ @@ -165,6 +165,16 @@ public class SpaceDetailsBean { return Utils.generateURL(FacesContext.getCurrentInstance(), getSpace(), URLMode.CIFS); } + + /** + * Return the Alfresco NodeRef URL for the current space + * + * @return the Alfresco NodeRef URL + */ + public String getNodeRefUrl() + { + return getSpace().getNodeRef().toString(); + } /** * @return Returns the template Id. diff --git a/source/java/org/alfresco/web/bean/preview/BasePreviewBean.java b/source/java/org/alfresco/web/bean/preview/BasePreviewBean.java index b1d7ca09f6..9a53ed3297 100644 --- a/source/java/org/alfresco/web/bean/preview/BasePreviewBean.java +++ b/source/java/org/alfresco/web/bean/preview/BasePreviewBean.java @@ -133,7 +133,6 @@ public abstract class BasePreviewBean public SelectItem[] getTemplates() { // TODO: could cache this last for say 1 minute before requerying - // get the template from the special Content Templates folder FacesContext context = FacesContext.getCurrentInstance(); String xpath = Application.getRootPath(context) + "/" + @@ -143,7 +142,7 @@ public abstract class BasePreviewBean NamespaceService resolver = Repository.getServiceRegistry(context).getNamespaceService(); List results = this.searchService.selectNodes(rootNodeRef, xpath, null, resolver, false); - List templates = new ArrayList(results.size()); + List templates = new ArrayList(results.size() + 1); if (results.size() != 0) { DictionaryService dd = Repository.getServiceRegistry(context).getDictionaryService(); diff --git a/source/java/org/alfresco/web/ui/common/Utils.java b/source/java/org/alfresco/web/ui/common/Utils.java index 59c4027239..785a052ec9 100644 --- a/source/java/org/alfresco/web/ui/common/Utils.java +++ b/source/java/org/alfresco/web/ui/common/Utils.java @@ -22,6 +22,7 @@ import java.io.StringReader; import java.net.URLEncoder; import java.text.DateFormat; import java.text.SimpleDateFormat; +import java.util.Calendar; import java.util.Date; import java.util.HashMap; import java.util.Iterator; @@ -987,6 +988,79 @@ public final class Utils throw new AlfrescoRuntimeException("Invalid DateTime pattern", err); } } + + /** + * Parse XML format date YYYY-MM-DDTHH:MM:SS + * @param isoDate + * @return Date or null if failed to parse + */ + public static Date parseXMLDateFormat(String isoDate) + { + Date parsed = null; + + try + { + int offset = 0; + + // extract year + int year = Integer.parseInt(isoDate.substring(offset, offset += 4)); + if (isoDate.charAt(offset) != '-') + { + throw new IndexOutOfBoundsException("Expected - character but found " + isoDate.charAt(offset)); + } + + // extract month + int month = Integer.parseInt(isoDate.substring(offset += 1, offset += 2)); + if (isoDate.charAt(offset) != '-') + { + throw new IndexOutOfBoundsException("Expected - character but found " + isoDate.charAt(offset)); + } + + // extract day + int day = Integer.parseInt(isoDate.substring(offset += 1, offset += 2)); + if (isoDate.charAt(offset) != 'T') + { + throw new IndexOutOfBoundsException("Expected T character but found " + isoDate.charAt(offset)); + } + + // extract hours, minutes, seconds and milliseconds + int hour = Integer.parseInt(isoDate.substring(offset += 1, offset += 2)); + if (isoDate.charAt(offset) != ':') + { + throw new IndexOutOfBoundsException("Expected : character but found " + isoDate.charAt(offset)); + } + int minutes = Integer.parseInt(isoDate.substring(offset += 1, offset += 2)); + if (isoDate.charAt(offset) != ':') + { + throw new IndexOutOfBoundsException("Expected : character but found " + isoDate.charAt(offset)); + } + int seconds = Integer.parseInt(isoDate.substring(offset += 1 , offset += 2)); + + // initialize Calendar object + Calendar calendar = Calendar.getInstance(); + calendar.setLenient(false); + calendar.set(Calendar.YEAR, year); + calendar.set(Calendar.MONTH, month - 1); + calendar.set(Calendar.DAY_OF_MONTH, day); + calendar.set(Calendar.HOUR_OF_DAY, hour); + calendar.set(Calendar.MINUTE, minutes); + calendar.set(Calendar.SECOND, seconds); + + // extract the date + parsed = calendar.getTime(); + } + catch(IndexOutOfBoundsException e) + { + } + catch(NumberFormatException e) + { + } + catch(IllegalArgumentException e) + { + } + + return parsed; + } /** * Return the image path to the filetype icon for the specified file name string diff --git a/source/java/org/alfresco/web/ui/common/component/UIActionLink.java b/source/java/org/alfresco/web/ui/common/component/UIActionLink.java index 0bb6889ff2..bb0ac4a88f 100644 --- a/source/java/org/alfresco/web/ui/common/component/UIActionLink.java +++ b/source/java/org/alfresco/web/ui/common/component/UIActionLink.java @@ -303,6 +303,7 @@ public class UIActionLink extends UICommand this.onclick = onclick; } + // ------------------------------------------------------------------------------ // Private data diff --git a/source/java/org/alfresco/web/ui/common/renderer/ActionLinkRenderer.java b/source/java/org/alfresco/web/ui/common/renderer/ActionLinkRenderer.java index 53bd5c5a24..767225cbc4 100644 --- a/source/java/org/alfresco/web/ui/common/renderer/ActionLinkRenderer.java +++ b/source/java/org/alfresco/web/ui/common/renderer/ActionLinkRenderer.java @@ -158,10 +158,10 @@ public class ActionLinkRenderer extends BaseRenderer linkBuf.append(" class=") .append(attrs.get("styleClass")); } - if (attrs.get("tooltip") != null) + if (link.getTooltip() != null) { linkBuf.append(" title=\"") - .append(Utils.encode((String)attrs.get("tooltip"))) + .append(Utils.encode(link.getTooltip())) .append('"'); } linkBuf.append('>'); @@ -266,10 +266,8 @@ public class ActionLinkRenderer extends BaseRenderer } buf.append(">"); - Map attrs = link.getAttributes(); - // render text link cell for the menu - if (attrs.get("href") == null) + if (link.getHref() == null) { buf.append("showTopic /jsp/forums/topic.jsp + + saveSearch + /jsp/dialog/save-search.jsp + diff --git a/source/web/WEB-INF/faces-config.xml b/source/web/WEB-INF/faces-config.xml index 10a8b2668e..0cdaa67850 100644 --- a/source/web/WEB-INF/faces-config.xml +++ b/source/web/WEB-INF/faces-config.xml @@ -191,6 +191,10 @@ namespaceService #{NamespaceService} + + searchService + #{SearchService} + diff --git a/source/web/images/icons/save_search.gif b/source/web/images/icons/save_search.gif new file mode 100644 index 0000000000..e38a2b413e Binary files /dev/null and b/source/web/images/icons/save_search.gif differ diff --git a/source/web/images/icons/save_search_large.gif b/source/web/images/icons/save_search_large.gif new file mode 100644 index 0000000000..7e20f01644 Binary files /dev/null and b/source/web/images/icons/save_search_large.gif differ diff --git a/source/web/jsp/browse/browse.jsp b/source/web/jsp/browse/browse.jsp index 226e277a91..3d051d3c3e 100644 --- a/source/web/jsp/browse/browse.jsp +++ b/source/web/jsp/browse/browse.jsp @@ -163,6 +163,7 @@ <%-- Current object actions --%>
+ @@ -209,7 +210,7 @@ <%-- component to display if the list is empty --%> <%-- TODO: either build complete message in BrowseBean or have no icon... --%> - + @@ -362,7 +363,7 @@ <%-- component to display if the list is empty --%> <%-- TODO: either build complete message in BrowseBean or have no icon... --%> - + diff --git a/source/web/jsp/dialog/advanced-search.jsp b/source/web/jsp/dialog/advanced-search.jsp index 231f85d2e0..d621deca16 100644 --- a/source/web/jsp/dialog/advanced-search.jsp +++ b/source/web/jsp/dialog/advanced-search.jsp @@ -104,6 +104,21 @@
+ + + <%-- Available Saved Searches --%> + +
+ <%-- Saved Searches drop-down selector --%> + <%-- uses a nasty hack to execute an ActionListener for the drop-down + tried using a valueChangedListener+formsubmit but the valueChangedListener + is called too late in the lifecycle for the form controls to be managed --%> + + + +
+
+ diff --git a/source/web/jsp/dialog/document-details.jsp b/source/web/jsp/dialog/document-details.jsp index 5e5303c74b..611f746037 100644 --- a/source/web/jsp/dialog/document-details.jsp +++ b/source/web/jsp/dialog/document-details.jsp @@ -225,7 +225,10 @@ - +
+ + + diff --git a/source/web/jsp/dialog/save-search.jsp b/source/web/jsp/dialog/save-search.jsp new file mode 100644 index 0000000000..460534c508 --- /dev/null +++ b/source/web/jsp/dialog/save-search.jsp @@ -0,0 +1,203 @@ +<%-- + Copyright (C) 2005 Alfresco, Inc. + + Licensed under the Mozilla Public License version 1.1 + with a permitted attribution clause. You may obtain a + copy of the License at + + http://www.alfresco.org/legal/license.txt + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + either express or implied. See the License for the specific + language governing permissions and limitations under the + License. +--%> +<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %> +<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %> +<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> +<%@ taglib uri="/WEB-INF/alfresco.tld" prefix="a" %> +<%@ taglib uri="/WEB-INF/repo.tld" prefix="r" %> + +<%@ page buffer="32kb" contentType="text/html;charset=UTF-8" %> +<%@ page isELIgnored="false" %> +<%@ page import="org.alfresco.web.ui.common.PanelGenerator" %> + + + + + + + + <%-- load a bundle of properties with I18N strings --%> + + + + + <%-- Main outer table --%> + + + <%-- Title bar --%> + + + + + <%-- Main area --%> + + <%-- Shelf --%> + + + <%-- Work Area --%> + + +
+ <%@ include file="../parts/titlebar.jsp" %> +
+ <%@ include file="../parts/shelf.jsp" %> + + + <%-- Breadcrumb --%> + <%@ include file="../parts/breadcrumb.jsp" %> + + <%-- Status and Actions --%> + + + + + + + <%-- separator row with gradient shadow --%> + + + + + + + <%-- Details --%> + + + + + + + <%-- separator row with bottom panel graphics --%> + + + + + + +
+ + <%-- Status and Actions inner contents table --%> + <%-- Generally this consists of an icon, textual summary and actions for the current object --%> + + + + + +
+ +
+
+
+ +
+ + + + + + +
+ + + + <% PanelGenerator.generatePanelStart(out, request.getContextPath(), "white", "white"); %> + + + + + + + + + + + + + + + + + + +
+ <% PanelGenerator.generatePanelStart(out, request.getContextPath(), "yellowInner", "#ffffcc"); %> + + + + + +
+ + + +
+ <% PanelGenerator.generatePanelEnd(out, request.getContextPath(), "yellowInner"); %> +
: +  * +
: + +
+ <% PanelGenerator.generatePanelEnd(out, request.getContextPath(), "white"); %> +
+ <% PanelGenerator.generatePanelStart(out, request.getContextPath(), "blue", "#D3E6FE"); %> + + + + + + + +
+ +
+ +
+ <% PanelGenerator.generatePanelEnd(out, request.getContextPath(), "blue"); %> +
+
+
+ +
+ +
+ +
\ No newline at end of file diff --git a/source/web/jsp/dialog/space-details.jsp b/source/web/jsp/dialog/space-details.jsp index 320a865030..f200c88823 100644 --- a/source/web/jsp/dialog/space-details.jsp +++ b/source/web/jsp/dialog/space-details.jsp @@ -225,7 +225,10 @@ - + + + +